Linux进程间通信

Posted 小倪同学 -_-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程间通信相关的知识,希望对你有一定的参考价值。

文章目录

进程间通信介绍

进程间通信的概念

进程通信是指在进程间传输数据(交换信息)

进程间通信目的

  • 数据传输: 一个进程需要将它的数据发送给另一个进程
  • 资源共享: 多个进程之间共享同样的资源
  • 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

进程间通信分类

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

管道

什么是管道

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“

例:统计云服务器上登录的用户个数

who和wc运行起来后变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据。


who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。

匿名管道

pipe函数

作用: 创建匿名管道
函数原型:

int pipe(int pipefd[2]);

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
pipefd[0]表示管道读端的文件描述符
pipefd[1]表示管道写端的文件描述符

pipe函数调用成功时返回0,调用失败时返回-1。

匿名管道使用步骤

  1. 父进程调用pipe函数创建管道。

2、父进程创建子进程。


3、父进程关闭写端,子进程关闭读端。


注意:

  1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。

站在文件描述符角度-深度理解管道

  1. 父进程调用pipe函数创建管道。

  1. 父进程创建子进程。

  1. 父进程关闭写端,子进程关闭读端。

代码演示

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>

int main()

        int pipe_fd[2]=0;

        if(pipe(pipe_fd)<0)//使用pipe创建匿名管道
               perror("pipe");
               return 1;
        

	      pid_t id=fork();
         if(id<0)
              perror("fork");
              return 2;
	     
         else if(id==0) // write24                 // child
 	          close(pipe_fd[0]); // 子进程关闭读端 
 	          
	          const char *msg="hello parent,I am child\\n";
            int count=5;
            while(count)
            
            			//子进程向管道写入数据
                  write(pipe_fd[1],msg,strlen(msg));
                  sleep(1);
                  count--;
	        
	         
           close(pipe_fd[1]);//子进程写入完毕,关闭文件
           exit(0);
     	
       else  // read41                
    	 // parent
       close(pipe_fd[1]); //父进程关闭写端
       char buffer[64];
       //父进程从管道读取数据
       while(1)
            buffer[0]=0;
            ssize_t size=read(pipe_fd[0],buffer,sizeof(buffer)-1);
             if(size > 0)
                    buffer[size]=0;
                    printf("parent get message from child# %s",buffer);
              
              else if(size==0)
                  printf("pipe file close , child quit!\\n");
                  break;
	         	
             else
			      break;
          		
      	 

          int status=0;
          if(waitpid(id,&status,0)>0)
                  printf("child quit,wait success!\\n");
         
          close(pipe_fd[0]);
  	 
     return 0;

运行结果:

管道读写规则

  1. 当没有数据可读时

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

  1. 当管道满的时候

O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

  1. 如果所有管道写端对应的文件描述符被关闭,则read返回0
  2. 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  3. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  4. 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点

  1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  2. 管道提供流式服务

对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务: 数据有明确的分割,拿数据按报文段拿。

  1. 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  2. 一般而言,内核会对管道操作进行同步与互斥

同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

  1. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

单工通信(Simplex Communication): 单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
半双工通信(Half Duplex): 半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
全双工通信(Full Duplex): 全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道的四种特殊情况

  1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。

我们可以通过以下代码看看第4种情况中写段收到了何种信号

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()

	int fd[2] =  0 ;
	if (pipe(fd) < 0) //创建匿名管道
		perror("pipe");
		return 1;
	
	pid_t id = fork(); 
	if (id == 0)
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--)
			write(fd[1], msg, strlen(msg));
			sleep(1);
		
		close(fd[1]); //关闭写端
		exit(0);
	
	//father
	close(fd[1]); //父进程关闭写端
	close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
	int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d\\n", status & 0x7F); //打印子进程收到的信号
	return 0;

写端收到的是13信号

我们可以通过 kill -l 命令查看信号含义

管道的大小

管道的容量时有限的,那么管道究竟能同时存储多少数据呢?

方法一:

使用ulimit -a命令,查看当前资源限制的设定。


根据显示,管道的最大容量是 512 × 8 = 4096 字节。

方法二:

可以向管道中不断写数据,看最多能写多少数据

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()

	int fd[2] =  0 ;
	if (pipe(fd) < 0) //创建匿名管道
		perror("pipe");
		return 1;
	
	pid_t id = fork();
	if (id == 0)
		//child 
		close(fd[0]); //子进程关闭读端
		char c = 'a';
		int count = 0;
		//子进程一直进行写入,一次写入一个字节
		while (1)
			write(fd[1], &c, 1);
			count++;
			printf("%d\\n", count); //打印当前写入的字节数
		
		close(fd[1]);
		exit(0);
	
	//father
	close(fd[1]); //父进程关闭写端

	waitpid(id, NULL, 0);
	close(fd[0]);
	return 0;

运行结果:

不同操作系统的管道大小不同,一切以实际为准。

命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

注意:

  1. 普通文件很难做到通信,这里的命名管道是一种特殊类型的文件。
  2. 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

创建命名管道

我们可以使用 mkfifo 命令创建一个命名管道。


使用这个命名管道文件,就能实现两个进程之间的通信了。

我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。


当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉。我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。

在进程中创建命名管道

在进程中创建命名管道需要用到mkfifo函数

int mkfifo(const char *pathname, mode_t mode);

参数:

pathname

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。

mode

创建命名管道文件的默认权限。

返回值:

创建成功,返回0,创建失败,返回-1。

用命名管道实现不同文件的通信

我们先写一个服务端(server)和客户端(client)程序

服务端代码

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

#define FIFO "./fifo"
int main()

        int ret=mkfifo(FIFO,0644); // 创建命名管道文件
        if(ret<0)
                perror("mkfilo");
                return 1;
        

        int fd=open(FIFO,O_RDONLY);//以读的方式打开命名管道文件
        if(fd<0)
                perror("open");
                return 2;
        

        char buffer[128];
        while(1)
                buffer[0]=0;
                //从命名管道当中读取信息到buffer中
                ssize_t s=read(fd,buffer,sizeof(buffer)-1);
                if(s>0)
                        buffer[s]=0;
                        printf("client# %s\\n",buffer);//输出客户端发来的信息
                
                else if(s==0)
                        printf("client quit...\\n");
                        break;
                
                else
                        break;
                

        
        close(fd);//关闭命名管道文件
        return 0;

客户端代码

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

#define FIFO "./fifo"9 
int main()

        int fd=open(FIFO,O_WRONLY);
        if(fd<0)
                perror("open");
                return 2;
        

        char buffer[128];

        while(1)
                printf("Please Enter# ");
                fflush(stdout);
                buffer[0]=0;
                ssize_t s=read(0,buffer,sizeof(buffer)-1);
                if(s>0)
                        buffer[s]=0;
                        write(fd,buffer,strlen(buffer));
                
                else if(s==0)
                        printf("client quit...\\n");
                        break;
                
                else
                        break;
                

        
        return 0;
 

当服务端和客户端代码运行起来后,我们再客户端输入信息再服务端能够接收到。

system V共享内存

system V IPC提供的通信方式有以下三种:

  1. system V共享内存
  2. system V消息队列
  3. system V信号量

system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥。

共享内存的基本原理

共享内存为了让不同进程看到同一份资源,在物理内存中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射。在进程虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应映射关系,这样不同进程就看到了同一份物理内存,即共享内存。


共享内存数据结构

在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。

struct shmid_ds 
	struct ipc_perm     shm_perm;   /* operation perms */
	int         shm_segsz;  /* size of segment (bytes) */
	__kernel_time_t     shm_atime;  /* last attach time */
	__kernel_time_t     shm_dtime;  /* last detach time */
	__kernel_time_t     shm_ctime;  /* last change time */
	__kernel_ipc_pid_t  shm_cpid;   /* pid of creator */
	__kernel_ipc_pid_t  shm_lpid;   /* pid of last operator */
	unsigned short      shm_nattch; /* no. of current attaches */
	unsigned short      shm_unused; /* compatibility */
	void            *shm_unused2;   /* ditto - used by DIPC */
	void            *shm_unused3;   /* unused */
;

共享内存函数

shmget函数

功能: 创建共享内存
函数原型:

int shmget(key_t key, size_t size, int shmflg);

返回值:

  • 调用成功返回该共享内存段的标识码
  • 调用失败返回-1

参数说明:

  • 第一个参数key,表示要创建共享内存在系统当中的唯一标识(类似哈希表)。
  • 第二个参数size,表示要创建共享内存的大小。(建议4KB整数倍)
  • 第三个参数shmflg,表示创建共享内存的方式。

注意: 系统分配共享内存是按4KB整数倍分配的,如果创建共享内存的大小不为4KB整数倍会造成空间浪费。

注意: 传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数原型

key_t ftok(const char *pathname, int proj_id);

ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。

shmflg

传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

  1. IPC_CREAT : 如果内核中不存在键值与key相等的共享内存,则新建一个,如果存在直接返回该共享内存。
  2. IPC_CREAT | IPC_EXCL : 如果内核中不存在键值与key相等的共享内存,则新建一个,如果存在直接报错。

shmget函数的使用

我们用shmget函数创建共享内存,用ipcs命令查看相关信息。

ipcs指令可以查看进程间通信的有关信息

我们可以在ipcs后加上选项来查看指定通信设施的信息

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。


我们可以用 ipcs -m 来查看共享内存相关信息。

ipcs命令输出的每列信息的含义如下:

标题含义
key系统区别各个共享内存的唯一标识
shmid共享内存的d
owner共享内存的拥有者
perms共享内存的权限
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

shmget函数创建共享内存

comm.h文件

#pragma once

#include<stdio.h>

#define PATH_NAME "/home/nzb/lesson20"
#define PROJ_ID 0x6666

#define SIZE 4097

server.c文件

#include"comm.h"
#include<sys/ipc.h>
#include<sys/shm.h>

int main()

        key_t k=ftok(PATH_NAME,PROJ_ID);
        if(k<0)
                perror("ftok");
                return 1;
        
        printf("key:%x\\n",k);

        int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL);// 共享内存不存在创建,>    存在报错
        if(shmid<0进程间通信的方法

思考总结Linux系统框架进程间通信

Linux 进程间通信方式都有哪些

简述Linux进程间通信的几种方式

Linux进程间通信简介

深刻理解Linux进程间通信(IPC)