引入——为什么需要进程间通信
再前面的进程章节中,我们除了可以让子进程在退出时返回一个状态码,没有别的办法获取子进程在运行过程中的状态和数据,反过来子进程也无法得知父进程的状态。
在实际的编程实践中,这些信息实际又是非常重要的,缺少了我们要想控制子进程的运行就很困难。
在Linux环境下,进程通信主要有这么几种类型:管道(无名与命名)、消息队列、共享内存、信号及信号量、Socket、Stream。其中,管道、FIFO、消息队列、共享队列、信号常用于单机进程通信,多机进程间通信常用的是Socket和Stream
无名管道
特点
- 半双工工作方式,即数据只能单向流动,具有固定的写端和读端
- 只能用于具有亲缘关系的进程间通信(父子进程间、兄弟进程间)
- 可以看成一种特殊的文件,可以使用read、write等进行读写,但其不属于任何文件系统,只存在于内存中
原型:
1 2
| #include<unistd.h> int pipe(int fd[2])
|
建立一个管道时,传入两个文件描述符fd,分别用于读(fd[0])和写(fd[1])。对管道内容进行读之前,先关闭其写端,写之前先关闭读端
pipe编程实践
如下的代码,使用fork创建一个子进程并利用pipe进行父子进程之间的通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main() { int fd[2]; int pid; char buf[128]; if(pipe(fd) == -1){ printf("creat pipe failed\n"); } pid = fork(); if(pid<0){ printf("creat child failed\n"); } else if(pid > 0){ sleep(3); printf("this is father\n");
close(fd[0]); write(fd[1],"hello from father",strlen("hello form father")); wait(); }else{ printf("this is child\n"); close(fd[1]); read(fd[0],buf,128); printf("read from father: %s\n",buf); exit(0); } return 0; }
|
命名管道
与只适用于父子进程间通信的无名管道不同,命名管道可以在无关的进程之间传递数据,另外它以一种特殊的设备文件形式存在于文件系统中并可以用一般的文件I/O函数操作,这是需要注意的两个点,
原型及基本创建方法
1 2 3
| #include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
|
创建成功返回0,失败设置errono并返回-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <sys/types.h> #include <sys/stat.h> #include <stdio.h>
int main() { int ret = mkfifo("./file",0600); if(ret == 0){ printf("mkfifo suscceess\n"); } if(ret == -1){ printf("mkfifo failuer\n"); perror("why"); } return 0; }
|
首先尝试创建一个管道,如果返回值为0,提示创建成功,否则输出失败原因
进一步分析处理逻辑,如果因为同名管道已经存在而导致失败,那其实也不影响后续操作,直接使用即可,只有因其他原因造成创建失败的情况才需要报错,相关逻辑修改为:
1 2 3 4
| if( mkfifo("./file",0600) == -1 && errno!=EEXIST){ printf("mkfifo failuer\n"); perror("why"); }
|
FIFO 数据通信编程实现
首先创建好管道,创建两个进程使用open打开管道。一个使用write往管道里写,一个利用read从管道里读。
需要注意的是当使用open函数打开管道,如果没有设置非阻塞标志(O_NONBLOCK),则默认是会阻塞的,即:读进程会阻塞直到有进程为了写打开管道,写进程会阻塞知道有进程为了读打开管道
写进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h>
int main() { int cnt = 0; char *str = "message from fifo"; int fd = open("./file",O_WRONLY); printf("write open success\n"); while(1){ write(fd, str, strlen(str)); sleep(1); if(cnt == 5){ break; } } close(fd); return 0; }
|
读进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include<errno.h> #include <fcntl.h>
int main() { char buf[30] = {0}; int nread = 0;
if( (mkfifo("./file",0600) == -1) && errno!=EEXIST){ printf("mkfifo failuer\n"); perror("why"); }
int fd = open("./file",O_RDONLY); printf("open success\n"); while(1){ nread = read(fd,buf,30); printf("read %d byte from fifo,context:%s\n",nread,buf); } close(fd); return 0; }
|
一般情况下进程通信通常使用非阻塞方式打开管道,以保证消息的时效性,这里使用阻塞方式更方便演示。
消息队列
基本原理
前面的两种管道都是半双工的,一个只能读,一个只能写。消息队列为我们实现进程通信提供了不同的方式。消息队列有如下特点:
- 消息队列是消息的链接表,存放在内核中。一个消息队列唯一标识于队列ID。每条消息以结构体形式存储于消息队列的一个节点。
- 消息队列面向记录,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程,由Linux内核管理。进程终止时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
原型及相关api
下面内容参考自 《进程间通信的五种方式》,来源CSDN
1 2 3 4 5 6 7 8 9
| #include <sys/msg.h>
int msgget(key_t key, int flag);
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
|
在下面情况下,msgget将直接创建一个消息队列:
- 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
- key参数为IPC_PRIVATE。
而函数msgrcv在读取消息队列时,type参数有下面几种情况:
- type == 0,返回队列中的第一个消息;
- type > 0,返回队列中消息类型为 type 的第一个消息;
- type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
在对消息队列进行读写之前,读写进程要对消息的类型进行约定,以便正确读取到想要的信息
消息队列编程收发数据
使用消息队列收发数据,主要用到msgget、msgsend和msgrcv三个函数,其原型上面已介绍,
编程过程中主要注意以下几点:
- msgget创建新消息队列的情况
- 结构体和消息队列ID,消息发送端和接收端需统一
- 消息类型的问题,如果想要发端在数据发送后等待收端的反馈消息,那么发送数据的类型和后面反馈消息的类型需要设置为不同值
消息读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <string.h> typedef struct msgbuf{ long mtype; char mtext[256]; }msgbuf;
int main() { msgbuf readBuf;
int msgId = msgget(0x1235, IPC_CREAT|0777); if(msgId == -1 ){ printf("get message queue failuer\n"); } memset(&readBuf,0,sizeof(msgbuf));
msgrcv(msgId, &readBuf,sizeof(readBuf.mtext),888,0); printf("read from que:%s\n",readBuf.mtext);
msgbuf sendBuf = {988,"thank you for reach"}; msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0); return 0; }
|
消息发送:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <string.h>
typedef struct msgbuf { long mtype; char mtext[256]; }msgbuf;
int main() { msgbuf sendBuf = {888,"this is message from quen"}; msgbuf readBuf;
memset(&readBuf,0,sizeof(msgbuf));
int msgId = msgget(0x1235, IPC_CREAT|0777);
if(msgId == -1 ){ printf("get message queue failuer\n"); }
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0); printf("send over\n");
msgrcv(msgId, &readBuf,sizeof(readBuf.mtext),988,0); printf("reaturn from get:%s\n",readBuf.mtext);
return 0; }
|
键值生成及消息队列移除
直接使用固定的键进行msgget,这种方式比较麻烦,不易记录。而且上一小节的代码中,消息发送完直接就退出了,对消息队列没有移除,这不是一个良好的习惯。
对于消息队列键的生成,可以使用ftok()函数,该函数的作用是根据指定的路径和项目标识符(id,可以是),生成一个系统IPC值。在Linux环境下一切数据都是以文件形式管理,包括目录,使用ls -i
命令就可查看。
因此,上面msgget函数中的key,通过ftok函数生成就可以这样写
1 2 3 4
| key_t key; key = ftok(".",'m'); printf("key=%x\n",key); int msgId = msgget(key, IPC_CREAT|0777);
|
最后,关于清除消息队列:
1
| msgctl(msgId,IPC_RMID,NULL);
|
共享内存原理及编程实践
消息队列虽然是全双工工作方式,两端都可读可写,但毕竟要先放入消息,才能读消息,且消息的“所有权”掌握在Linux内核中。
共享内存,顾名思义就是独立于读、写运行进程之间的一块公有内存,它异于前面所介绍几种通信方式,进程可以对其中的内容进行随意操作。
使用共享内存进行IPC,主要有如下步骤:
- 创建/打开共享内存
- 实现共享内存与进程内存的映射
- 进行数据交换操作
- 释放共享内存
与共享内存相关的api有(头文件#include <sys/shm.h>
):
- int shmget(key_t key, size_t size, int flag);// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
- void *shmat(int shm_id, const void *addr, int flag);// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
- int shmdt(void *addr);// 断开与共享内存的连接:成功返回0,失败返回-1
- int shmctl(int shm_id, int cmd, struct shmid_ds *buf);// 控制共享内存的相关信息:成功返回0,失败返回-1
先看共享内存的写:共享内存的数据可以看作是字符串,因此先定义一个字符串常量,直接使用strcpy就能把消息写到里面去。
要注意的是shmget第二个参数size是共享内存的大小,一般以1MB(1024)为单位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> #include <string.h>
int main() { int shmid; char *shmaddr;
key_t key; key = ftok(".",1); shmid = shmget(key,1024*4,IPC_CREAT|0666); if(shmid == -1){ printf("shmget noOk\n"); exit(-1); } shmaddr = shmat(shmid,0,0); printf("shmat ok\n"); strcpy(shmaddr,"Linux Programming —— IPC");
sleep(5); shmdt(shmaddr); shmctl(shmid, IPC_RMID, 0);
printf("quit\n"); return 0; }
|
对于内容的读则更简单了,获取到共享内存地址后,使用普通的字符串读取方法比如printf("%s",shmaddr)
就能将内容打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int main() { int shmid; char *shmaddr;
key_t key; key = ftok(".",1); shmid = shmget(key,1024*4,0); if(shmid == -1){ printf("shmget noOk\n"); exit(-1); } shmaddr = shmat(shmid,0,0);
printf("shmat ok\n"); printf("data: %s\n:",shmaddr);
shmdt(shmaddr); printf("quit\n"); return 0; }
|
使用完共享内存,释放掉空间,是一个良好的习惯
这种方式使用方便,双方也没有什么使用的先后限制,但是有一个缺陷就是如果双方同时对共享内存进行写,会出问题。这就要用到接下来讲到的信号来解决了。
信号概述
信号,对于当前正在运行的进程来说,其本质就是影响当前工作的一些外部因素。在处理这些信号的时候,必是停下手头的工作来处理其他事务,尽管中断时间可能不会很长,但终究是中断。
在Linux中,信号本质就是一种软中断,许多重要的程序都需要处理信号。信号为Linux处理异步事件提供了方法,比如当终端用户输入Ctrl+C,便会终止当前进程。
信号的标识
信号的标识包括信号的名字和编号,在Linux中这些名字都以“SIG”开头,比如“SIGO”、“SIGCHLD”等。关于信号的定义,存放在signal.h
头文件中,信号名都定义为正整数,从1开始,不存在0号。如果想要查看所有的信号名,可以使用kill -l
查看信号的名字及序号。
信号的处理
对待信号,操作系统一般有三种策略:忽略、捕捉和默认动作。
- 忽略,可以用于大多数信号的处理。有两类信号不可被忽略:
SIGKILL
与SIGSTOP
,这两类命令项内核和超级用户提供了进程终止和停止的可靠方法,如果被忽略,那就“没人能管得了了”
- 捕捉,是通知内核,用户希望如何处理某一种信号,实质是设计一个处理函数,然后将函数注册到内核,当信号发生时,由内核调用用户自定义的函数,依次实现信号的处理。
- 默认动作,对于每种信号来说都应当有的默认处理动作。当发生该信号,系统会自动执行。对系统来说,大部分的处理方式比较粗暴,比如直接杀死进程。具体的默认动作可以使用
man 7 signal
来查看系统的具体定义。
信号的应用(主要指产生信号)
其实,kill -9 pid
就是一种简单的信号产生方式,该行为会产生SIGKILL信号,通知内核执行9号信号的默认动作——杀死进程
信号处理编程实战
前面提到过,信号捕捉实质是注册一个处理函数,那么处理函数的注册主要通过signal函数进行,函数原型如下:
1 2 3 4
| #include<signal.h> typedef void (*sighandler\_t)(int);
sighandler\_t signal(int signum,sighandler\_t handler)
|
可以看到,注册函数两个输入参数分别是信号编号、处理函数,处理函数的类型是void,
- 按下Ctrl+C也无法停止
思路就是捕捉Ctrl的信号,编写一个不会退出的处理函数,让main函数不断循环下去。而通过man手册查询到的结果是,从键盘得到的interrupt信号(也就是Ctrl+C),信号名就叫SIGINT
,我们只需要使用signal(SIGINT,handler)注册我们的信号处理函数即可。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> #include <unistd.h>
void handler(int num){ printf("receive signum=%d, ",num); printf("never quit!\n"); }
int main(){ signal(SIGINT,handler); while(1){ usleep(100); } return 0; }
|
运行结果:
进一步,尝试把我们的处理函数注册到其他信号的处理上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void handler(int signum) { printf("get signum=%d\n",signum); switch(signum){ case 2: printf("SIGINT\n"); break; case 9: printf("SIGKILL\n"); break; case 10: printf("SIGUSR1\n"); break; } printf("never quit\n"); }
int main() { signal(SIGINT,handler); signal(SIGKILL,handler); signal(SIGUSR1,handler); while(1); return 0; }
|
经过尝试发现,kill -9
的信号看似修改了但还是控制不了,依然会终止进程。
调用kill命令,根据传入的信号编号和进程id杀死指定进程
1 2 3 4 5 6 7 8 9 10 11 12 13
| int main(int argc ,char **argv) { int signum; int pid; signum = atoi(argv[1]); pid = atoi(argv[2]);
printf("num=%d,pid=%d\n",signum,pid); kill(pid, signum); printf("send signal ok") return 0; }
|
或:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int main(int argc ,char **argv) { int signum; int pid;
char cmd[128]={0}; signum = atoi(argv[1]); pid = atoi(argv[2]);
printf("num=%d,pid=%d\n",signum,pid); sprintf(cmd,"kill -%d %d",signum,pid); system(cmd); printf("send signal ok"); return 0; }
|
信号的忽略
通过man命令可以找到,SIG_IGN是忽略信号,因此只要将捕获到的信号与之绑定即可实现对目标信号的忽略:signal(SIGINT,SIG_IGN)
信号发送携带消息方法与实战
前面的信号处理,均是对信号进行捕捉,然后绑定一个自定义的处理函数,但我们在产生信号的时候,除了信号本身并不能携带其他的消息,这无论是对于原理的理解,还是对于实际应用都造成了一定的局限性。
想要让信号携带消息,需要从信号的发送端和接收端两方面考虑问题。对于发送端而言,我们想知道怎样把消息放入信号,以及用什么函数发送信号;对于消息接收端而言,我们需要知道如何绑定信号处理函数,以及如何把信号中的消息读取出来。
对于消息的接收端,要想绑定信号和处理函数,主要使用sigaction
函数,该函数原型如下:
1 2
| #include<signal.h> int sigaction(int signum, const struct sigaction \*act,struct sigaction \*oldact);
|
其中,第一个参数signum是信号名(信号编号),后面两个参数是需自定义的结构体sigaction
,其中第二个定义了信号处理方式,第三个参数则是信号的“备份”,即对信号本身的保存,这个可以设置为NULL
关于结构体sigaction,原型如下:
1 2 3 4 5 6 7
| struct sigaction { void(*sa\_handler)(int); void(*sa\_sigaction)(int, siginfo_t *, void *); sigset\_t sa\_mask; int sa\_flags; };
|
接收信号并把信号消息取出的关键,就在于sigaction函数中,siginfo_t
类型的第二参数,该参数也是一个结构体,有如下内容:
在接收到信号后,我们可以使用info->si_int
或info->si_value.sival_int
将内容取出来
了解了这些之后,一个简易版的信号消息打印函数就可以写出来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <signal.h> #include <stdio.h> #include <unistd.h>
void handler(int signum, siginfo_t *info, void *context) { printf("get signum %d\n",signum); if(context != NULL){ printf("from:%d\n",info->si_pid);
printf("get data=%d\n",*(int *)(info->si_value.sival_ptr)); } }
int main() { struct sigaction act; printf("pid = %d\n",getpid()); act.sa_sigaction = handler; act.sa_flags = SA_SIGINFO; sigaction(SIGUSR1,&act,NULL); while(1){ usleep(100); }; return 0; }
|
而对于消息的发送端,消息的绑定以及信号的发送主要使用sigqueue
函数,函数原型:
1 2 3 4 5 6
| #include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval value); union sigval { int sival_int; void *sival_ptr; };
|

从函数原型和man手册中都可以看到,使用sigqueue要找到接收信号进程的pid、明确发送什么信号(sig)、以及发送什么消息(value),至于具体发送什么消息,将其绑定到sigval的sival_ptr即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <stdio.h> #include <signal.h> #include <string.h> int main(int argc, char **argv) { int signum; int pid; signum = atoi(argv[1]); pid = atoi(argv[2]); char *data = "message from sender"; union sigval value; value.sival_ptr=&data; sigqueue(pid,signum,value); printf("%d,done\n",getpid()); return 0; }
|
信号量概述及编程实践
虽然名字看起来比较像,但信号量和信号是完全两码事。信号量不是用来传递数据的,而是用来实现资源的互斥共享。所谓互斥共享,指的是同一资源,同一时刻只能由一个进程进行操作。比如前面讲到的共享内存,是一种临界资源,如果没有访问的限制,一个资源可以同时被多个进程操作,那么内容的完整性和一致性就无法保证,可能写入一些脏数据,甚至损坏资源本身
与信号量相关的概念还有信号量集,指系统中所有信号量的集合。每一个信号量都对应着两种操作:P和V,P操作是给资源“上锁”,防止自己操作的过程中被其他进程修改,V操作则是“解锁”,使用完资源后将使用权还给大家。
对于编程实践,Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作
与信号量相关的api主要有semget、semop、semctl,分别用于信号量的获取/创建、操作和控制,原型如下:
(参考文章《五种进程间的通信方式》,作者代码的搬运工)
1 2 3 4 5 6 7 8 9 10 11
| #include <sys/sem.h> int semget(key_t key, int num_sems, int sem_flags); int semop(int semid, struct sembuf semoparray[], size_t numops); int semctl(int semid, int sem_num, int cmd, ...);
struct sembuf { short sem_num; short sem_op; short sem_flg; }
|
具体实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>
union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; };
void pGetKey(int id) { struct sembuf set;
set.sem_num = 0; set.sem_op = -1; set.sem_flg=SEM_UNDO;
semop(id, &set ,1); printf("getkey\n"); }
void vPutBackKey(int id) { struct sembuf set;
set.sem_num = 0; set.sem_op = 1; set.sem_flg=SEM_UNDO;
semop(id, &set ,1); printf("put back the key\n"); }
int main(int argc, char const *argv[]) { key_t key; int semid;
key = ftok(".",2); semid = semget(key, 1, IPC_CREAT|0666);
union semun initsem; initsem.val = 0; semctl(semid, 0, SETVAL, initsem); int pid = fork(); if(pid > 0){ pGetKey(semid); printf("this is father\n"); vPutBackKey(semid); semctl(semid,0,IPC_RMID); } else if(pid == 0){ printf("this is child\n"); vPutBackKey(semid); }else{ printf("fork error\n"); } return 0; }
|
在实践中,还可以将信号量与共享内存配合使用