一文让你搞懂进程间常用通信方式(管道通信、消息队列、共享内存、信号量)

427 阅读12分钟

一文搞懂进程间常用通信方式(管道通信、消息队列、共享内存、信号量)

进程间的通信应用是很广泛的,比如后台进程和 GUI 界面数据传递,发送信号关机,Ctrl+C 终止正在运行的程序等。

Linux 进程间通信机制分三类数据交互,同步,信号。理解了这些机制才能灵活运用操作系统提供的 IPC 工具。

本章以常用的管道(包括有名管道和无名管道),System V IPC消息队列,共享内存,信号灯)为例来说明 Linux 进程通信常用的方法。

1.Linux内核提供的进程间通信方式

  1. 管道通信:无名管道和有名管道
  2. IPC通信:消息队列、共享内存、信号量
  3. 信号通信:比较复杂的通信方式,一般用于通知接收进程某个事件已经发生。
  4. socket域套接字
  5. 文件锁:与信号量类似,用于进程间对同一文件的互斥访问。

2.管道通信

2.1无名管道

(1)原理:内核维护的一块内存,有读端和写端。
(2)方法:父进程创建管道后fork子进程,子进程继承父进程的管道fd。
(3)限制:
1.只能在有关联的进程间通信,在目录中看不到管道文件节点,读写文件描述符存在于一个int型数组中。
2.半双工,只能单向传输数据,一个进程读,另一个进程只能写,读出来的字节顺序与写入的顺序一样。
(4)函数:pipe、write、read、close。

函数原型:int pipe(int pipefd[2]);
功能:创建无名管道
头文件:#include <unistd.h>
参数 pipefd[2]:一个 int 型数组,表示管道的文件描述符,pipefd[0]为读,pipefd[1]为写
返回值:成功返回 0,失败返回-1

(5)实验代码:创建无名管道,实现父进程与子进程间的通信;父进程将数据写入管道,子进程从管道读取数据并打印出来。

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

int main(void)
{
	char buf[32] = {0};
	pid_t pid;
	int fd[2];//定义一个变量来保存文件描述符,因为一个读端,一个写端,所以数量为 2 个
	
	pipe(fd);//创建无名管道
	print("fd[0] is %d\n", fd[0]);
	print("fd[1] is %d\n", fd[1]);
	
	pid = fork();//创建子进程
	if (pid < 0)
	{
		printf("fork error\n");
	}
	if (pid > 0)
	{
		int status;
		close(fd[0]);//在父进程中关闭读端
		write(fd[1], "hello", 5);//在父进程中通过写端向管道写数据
		close(fd[1]);//写完数据关闭写端
		wait(&status);//等待子进程退出
		exit(0);
	}
	if (pid == 0)
	{
		close(fd[1]);//关闭写端
		read(fd[0], buf, 32);//从读端读信息
		printf("buf is %s\n", buf);
		close(fd[0]);//关闭读端
		exit(0);
	}
	return 0;
}

运行结果: image.png

2.2 有名管道

(1)原理:实质也是内核维护的一块内存,表现为一个有名字的文件。
(2)方法:固定一个文件名,两个进程分别使用mkfifo创建fifo文件,然后分别open打开获取fd,然后一个读一个写。
(3)特点:1.可以使无关联的进程通过fifo文件描述符进行数据传递。2.半双工方式,有一个写入端,一个读出端。
(4)函数:mkfifo、open、write、read、close。

函数原型:int mkfifo(const char *pathname, mode_t mode);
头文件:#include <sys/types.h>
	#include <sys/stat.h>
参数 pathname: 有名管道的路径
参数 mode: 权限
返回值 成功返回 0,失败返回-1

(5)实验代码:使用mkfifo创建文件描述符,打开管道文件描述符,通过读写文件描述符进行单向数据传输。

/ifo_write.c/

/*ifo_write.c*/

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

int main(int argc, char *argv[])
{
	int ret;
	char buf[32] = {0};
	int fd;
	if (argc < 2)
	{
		printf("Usage:%s <fifo name>\n", argv[0]);
		return -1;
	}
	
	if (access(argv[1], F_OK) == 1)
	{
		ret = mkfifo(argv[1], 0666);
		if (ret == -1)
		{
			printf("mkfifo error\n");
			return -2;
		}
		printf("mkfifo success\n");
	}
	fd = open(argv[1], O_WRONLY);
	while(1)
	{
		sleep(1);
		write(fd, "hello", 5);
	}
	close(fd);
	return 0;
}

/fifo_read.c/



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

int main(int argc, char *argv[])
{
	char buf[32] = {0};
	int fd;
	if (argc < 2)
	{
		printf("Usage:%s <fifo name> \n", argv[0]);
		return -1;
	}
	fd = open(argv[1], O_RDONLY);
	while (1)
	{
		sleep(1);
		read(fd, buf, 32);
		printf("buf is %s\n", buf);
		memset(buf, 0, sizeof(buf));//memset:在一段内存块中填充某个给定的值
	}
	close(fd);
	return 0;
}

运行结果:

image.png

3.IPC通信

这些 IPC 对象存在于内核空间,应用层使用 IPC 通信的步骤为:

image.png

  1. 获取 key 值,内核会将 key 值映射成 IPC 标识符,获取 key 值常用方法: (1)在 get 调用中将 IPC_PRIVATE 常量作为 key 值。 (2)使用 ftok()生成 key
  2. 执行 IPC get 调用,通过 key 获取整数 IPC 标识符 id,每个 id 表示一个 IPC 对象

image.png

  1. 通过 id 访问 IPC 对象

image.png

  1. 通过 id 控制 IPC 对象

image.png

创建这三种 IPC 对象都要先获取 key 值,然后根据 key 获取 id,用到的函数如下:

函数原型:key_t ftok(const char *pathname, int proj_id)
功能:建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。
	 通常情况下,该 id 值通过 ftok 函数得到
参数 const char *pathnam:文件路径以及文件名
参数 int proj_id:同一个文件根据此值生成多个 key 值,int 型或字符型,
				 多个若想访问同一 IPC 对象,此值必须相同。
返回值:成功返回 key 值,失败返回-1
复制代码

3.1共享内存

(1)共享内存和其他进程间数据交互方式相比,有以下几个突出特点:

  1. 速度快,因为共享内存不需要内核控制,所以没有系统调用。而且没有向内核拷贝数据的过程, 所以效率和前面几个相比是最快的,可以用来进行批量数据的传输。
  2. 没有同步机制,需要借助 Linux 提供其他工具来进行同步,通常使用信号灯。

(2)使用共享内存的步骤:
1.调用 shmget()创建共享内存段 id。 2.调用 shmat()将 id 标识的共享内存段加到进程的虚拟地址空间。3.访问加入到进程的那部分映射后地址空间,可用 IO 操作读写。

常用函数:

函数原型:int shmget(key_t key, size_t size, int shmflg)
功能:创建共享内存
头文件:#include <sys/ipc.h>
       #include <sys/shm.h>
参数 key:由 ftok 生成的 key 标识,标识系统的唯一 IPC 资源
参数 size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是 4k 字节,
		  为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
参数 shmflg:如果要创建新的共享内存,需要使用 IPC_CREATIPC_EXCL,
			如果是已经存在的,可以使用 IPC_CREAT 或直接传 
返回值:成功时返回一个新建或已经存在的的共享内存标识符,取决于 shmflg 的参数。
	   失败返回-1 并设置错误码
函数原型:key_t ftok(const char *pathname, int proj_id)
功能:建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。
	 通常情况下,该 id 值通过 ftok 函数得到
参数 const char *pathnam:文件路径以及文件名
参数 int proj_id:同一个文件根据此值生成多个 key 值,int 型或字符型,
				 多个若想访问同一 IPC 对象,此值必须相同。
返回值:成功返回 key 值,失败返回-1
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:挂接共享内存
头文件:#include <sys/types.h>
		#include <sys/shm.h>
参数 int shmid:共享内存的标识符,也就是 shmget 函数的返回值
参数 const void *shmaddr:映射到的地址,一般写 NULLNULL 为系统自动帮我们完成映射
参数 int shmflg:通常为 0,表示共享内存可读可写,或者为 SHM_RDONLY,表示共享内存可读可写
返回值:成功返回共享内存映射到进程中的地址,失败返回-1
函数原型:int shmdt(const void *shmaddr);
功能:去关联共享内存
头文件:#include <sys/types.h>
		#include <sys/shm.h>
参数 const void *shmaddr:共享内存映射后的地址
返回值:成功返回 0,失败返回-1
注意:shmdt 函数是将进程中的地址映射删除,
	 也就是说当一个进程不需要共享内存的时候,就可以使用这个函数将他从进程地址空间中脱离,并不会删除内核里面的共享内存对象。
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:销毁共享内存
头文件:#include <sys/ipc.h>
	   #include <sys/shm.h>
参数 int shmid:要删除的共享内存的标识符
参数 cmd:IPC_STAT (获取对象属性) IPC_SET (设置对象属性) IPC_RMID(删除对象)
参数 struct shmid_ds *buf:指定 IPC_STAT (获取对象属性) IPC_SET (设置对象属性) 时用来保存或者设置的属性

(3)实验代码

/*在程序中,父子进程通过共享内存通信。*/

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
	int shmid;
	key_t key;
	pid_t pid;
	char *s_addr, *p_addr;
	key = ftok("./a.c", 'a');
	shmid = shmget(key, 1024, 0777 | IPC_CREAT);
	if (shmid < 0)
	{
		printf("shmget is error\n");
		return -1;
	}
	printf("shmget is ok and shmid is %d\n", shmid);
	pid = fork();
	if (pid > 0)//父进程
	{
		p_addr = shmat(shmid, NULL, 0);
		strncpy(p_addr, "hello-morris", 12);
		wait(NULL);
		exit(0);
	}
	if (pid == 0)//子进程
	{
		sleep(2);
		s_addr = shmat(shmid, NULL, 0);
		printf("s_addr is %s\n", s_addr);
		exit(0);
	}
	return 0;
}

运行结果:

image.png

查看共享内存命令:ipcs -m
删除共享内存的命令:ipcrm -m [id]

image.png

3.2 消息队列

(1)本质上是一个队列,队列可以理解为内核维护的一个FIFO
(2)工作时A和B两个进程进行通信,A向队列中放入消息,B从队列中读出消息。 (3)消息队列使用步骤:1.创建key;2.msgget()通过key创建或打开消息队列对象id;3.使用msgsnd()/msgrcv()进行收发4.通过msgctl()删除ipc对象

函数原型:int msgget(key_t key, int msgflg)
功能:获取 IPC 对象唯一标识 i
头文件:#include <sys/types.h>
	#include <sys/ipc.h>
	#include <sys/msg.h>
参数 key_t key:和消息队列相关的 key 
参数 int msgflg:访问权限
返回值:成功返回消息队列的 ID,失败返回-1
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:发送数据
头文件:#include <sys/types.h>
	   #include <sys/ipc.h>
	   #include <sys/msg.h>
参数 int msqid:消息队列 ID
参数 const void *msgp:指向消息类型的指针
参数 size_t msgsz:发送的消息的字节数。
参数 int msgflg:如果为 0,直到发送完成函数才返回,即阻塞发送
				IPC_NOWAIT:消息没有发送完成, 函数也会返回,即非阻塞发
返回值:成功返回 0,失败返回-1
函数原型: ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
功能:接收消息
参数 msqid:IPC 对象对应的 id
参数 msgp:消息指针,消息包含类型和字段
参数 msgsz:消息里的字段大小
参数 msgtyp:消息里的类型
参数 msgflg:位掩码,不止一个
返回值:成功返回接收到的字段大小,错误返回-1
函数原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf)
功能:控制操作,删除消息队列对象等
头文件:#include <sys/types.h>
	   #include <sys/ipc.h>
	   #include <sys/msg.h>
参数 int msqid:消息队列的 ID
参数 int cmd:IPC_STAT:读取消息队列的属性,然后把它保存在 buf 指向的缓冲区。
			 IPC_SET:设置消息队列的属性,这个值取自 buf 参数
			 IPC_RMID:
参数 struct msqid_ds *buf:消息队列的缓冲区
返回值:成功返回 0,失败返回-1

实验代码:

/*a.c 向消息队列里面写*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf
{
	long mtype;
	char mtext[128];
};

int main(void)
{
	int msgid;
	key_t key;
	struct msgbuf msg;
	//获取 key 值
	key = ftok("./a.c", 'a');
	//获取到 id 后即可使用消息队列访问 IPC 对象
	msgid = msgget(key, 0666 | IPC_CREAT);
	if (msgid < 0)
	{
		printf("msgget is error\n");
		return -1;
	}
	printf("msgget is ok and msgid is %d \n", msgid);
	msg.mtype = 1;
	strncpy(msg.mtext, "hello", 5);
	//发送数据
	msgsnd(msgid, &msg, strlen(msg.mtext), 0);
	return 0;
}
/*b.c 从消息队列里面读*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf
{
	long mtype;
	char mtext[128];
};

int main(void)
{
	int msgid;
	key_t key;
	struct msgbuf msg;
	key = ftok("./a.c", 'a');
	//获取到 id 后即可使用消息队列访问 IPC 对象
	msgid = msgget(key, 0666 | IPC_CREAT);
	if (msgid < 0)
	{
		printf("msgget is error\n");
		return -1;
	}
	printf("msgget is ok and msgid is %d \n", msgid);
	//接收数据
	msgrcv(msgid, (void *)&msg, 128, 0, 0);
	printf("msg.mtype is %ld \n", msg.mtype);
	printf("msg.mtext is %s \n", msg.mtext);
	return 0;
}

查看共享队列命令:ipcs -q

3.3信号量(信号灯)

(1)实质就是个计数器(可理解为int a);通过计数值来提供互斥与同步。
注意请不要把信号量与之前所说的信号混淆起来,信号与信号量是不同的两种事物。让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的

信号量只能进行两种操作等待和发送信号,即 P(sv)和 V(sv),他们的行为是这样的。举个例子,就是两个进程共享信号量 sv,一旦其中一个进程执行了 P(sv)操作,它将得到信号量,并可 以进入临界区,使 sv 减 1。而第二个进程将被阻止进入临界区,因为当它试图执行 P(sv)时,sv 为 0,它会被挂起以等待第一个进程离开临界区域并执行 V(sv)释放信号量,这时第二个进程就可以恢复执行。

信号灯也叫信号量,它能够用来同步进程的动作,不能传输数据。它的应用场景就像红绿灯,控制各进程使用共享资源的顺序

(2)使用 System V 信号灯的步骤如下: 1.使用 semget()创建或打开一个信号灯集。 2.使用 semctl()初始化信号灯集。 3.使用 semop()操作信号灯值,即进行 P/V 操作。 P 操作:申请资源,申请完后信号灯值-1; V 操作:释放资源,释放资源后信号灯值+1

函数原型: int semget(key_t key, int nsems, int semflg)
功能 创建一个新信号量或取得一个已有信号量
头文件:#include <sys/types.h>
		#include <sys/ipc.h>
		#include <sys/sem.h>
参数 key_t key:信号量的键值
参数 int nsems:信号量的数量
参数 int semflg:标识
返回值:成功返回信号量的 ID,失败返回-1
函数原型: int semctl(int semid, int semnum, int cmd, union semun arg)
功能 :初始化信号灯集合
头文件 #include <sys/types.h>
		#include <sys/ipc.h>
		#include <sys/sem.h>
参数 int semid:信号量 ID
参数 int semnum:信号量编号
参数 cmd:IPC_STAT(获取信号量的属性) 
		 IPC_SET(设置信号量的属性)
		 IPC_RMID (删除信号量)
		 SETVAL(设置信号量的值)
参数 arg:union semun 
		 {
		 	int val;
			struct semid_ds *buf;
			unsigned short *array;
			struct seminfo *__buf;
		 }
函数原型: int semop(int semid, struct sembuf *sops, size_t nsops)
功能:在信号量上执行一个或多个操作。
头文件:#include <sys/types.h>
		#include <sys/ipc.h>
		#include <sys/sem.h>
参数 int semid:信号量 ID
参数 struct sembuf *sops:信号量结构体数组
参数 size_t nsops:要操作信号量的
struct sembuf{
        unsigned short sem_num; //要操作的信号量的编号
        short sem_op; //P/V 操作,1 为 V 操作,释放资源。-1 为 P操作,分配资源。0 为等待,直到信号量的值变成 0
        short sem_flg; //0 表示阻塞,IPC_NOWAIT 表示非阻塞
}

实验代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

union semun
{
	int val;
};

int main(void)
{
	int semid;
	int key;
	pid_t pid;
	struct sembuf sem;
	union semun semun_union;
	key = ftok("./a.c", 0666);
	semid = semget(key, 1, 0666 | IPC_CREAT);
	semun_union.val = 0;
	semctl(semid, 0, SETVAL, semun_union);
	pid = fork();
	if (pid > 0)
	{
		sem.sem_num = 0;
		sem.sem_op = -1;
		sem.sem_flg = 0;
		semop(semid, &sem, 1);
		printf("This is parents\n");
		sem.sem_num = 0;
		sem.sem_op = 1;
		sem.sem_flg = 0;
		semop(semid, &sem, 1);
	}
	if (pid == 0)
	{
		sleep(2);
		sem.sem_num = 0;
		sem.sem_op = 1;
		sem.sem_flg = 0;
		semop(semid, &sem, 1);
		printf("This is son\n");
	}
	return 0;
}

4.信号通信

(1)信号的产生有三种方式:

  1. 硬件产生,如从键盘输入 Ctrl+C 可以终止当前进程
  2. 其他进程发送,如可在 shell 进程下,使用命令 kill -信号标号 PID,向指定进程发送信号。
  3. 异常,进程异常时会发送信号

在 Ubuntu 终端输入 kill -l,查看所有信号

(2)几个常用的函数

函数原型:int kill(pid_t pid, int sig);
头文件:用于向任何进程组或进程发送信号
参数 pid:大于 0,时为向 PID 为 pid 的进程发送信号
		 等于 0,向同一个进程组的进程发送信号;
		 等于-1,除发送进程自身外,向所有进程 ID 大于 1 的进程发送信号。
		 小于-1,向组 ID 等于该 pid 绝对值的进程组内所有进程发送信号。
参数 sig:设置发送的信号;
		 等于 0 时为空信号,无信号发送。常用来进行错误检查
返回值:执行成功时,返回值为 0;错误时,返回-1,并设置相应的错误代码 errno

函数原型:int raise(int sig);
功能:向进程自身发送信号,相当于 kill(getpid(),sig)
头文件:#include <signal.h>
参数 sig:信号
函数原型:unsigned int alarm(unsigned int seconds);
功能:设定的时间超过后产生 SIGALARM 信号,默认动作是终止进程
头文件:#include <unistd.h>
参数:设定的时间
注意:每个进程只能有一个 alarm()函数,时间到后要想再次使用要重新注册。

参考引用:juejin.cn/post/707782…