摘要:进程间通信的目的数据传输一个进程需要将它的数据发送给另一个进程。进程间通信的本质进程间通信的本质就是,让不同的进程看到同一份资源。匿名管道匿名管道的原理匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
进程间通信的本质就是,让不同的进程看到同一份资源。
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
管道
System V IPC
POSIX IPC
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
例如,统计我们当前使用云服务器上的登录用户个数。
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
注明: who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
注意:
pipe函数用于创建匿名管道,pip函数的函数原型如下:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
数组元素 | 含义 |
---|---|
pipefd[0] | 管道读端的文件描述符 |
pipefd[1] | 管道写端的文件描述符 |
pipe函数调用成功时返回0,调用失败时返回-1。
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
1、父进程调用pipe函数创建管道。
2、父进程创建子进程。
3、父进程关闭写端,子进程关闭读端。
注意:
我们可以站在文件描述符的角度再来看看这三个步骤:
1、父进程调用pipe函数创建管道。
2、父进程创建子进程。
3、父进程关闭写端,子进程关闭读端。
例如,在以下代码当中,子进程向匿名管道当中写入10行数据,父进程从匿名管道当中将数据读出。
//child->write, father->read #include #include #include #include #include #include int main(){ int fd[2] = { 0 }; if (pipe(fd) < 0){ //使用pipe创建匿名管道 perror("pipe"); return 1; } pid_t id = fork(); //使用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]); //父进程关闭写端 //父进程从管道读取数据 char buff[64]; while (1){ ssize_t s = read(fd[0], buff, sizeof(buff)); if (s > 0){ buff[s] = "/0"; printf("child send to father:%s/n", buff); } else if (s == 0){ printf("read file end/n"); break; } else{ printf("read error/n"); break; } } close(fd[0]); //父进程读取完毕,关闭文件 waitpid(id, NULL, 0); return 0;}
运行结果如下:
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
2、当管道满的时候:
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
1、管道内部自带同步与互斥机制。
我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥:
实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。
也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
2、管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
3、管道提供的是流式服务。
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
4、管道是半双工通信的。
在数据通信中,数据在线路上的传送方式可以分为以下三种:
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。
在使用管道时,可能出现以下四种特殊情况:
其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
我们可以通过以下代码看看情况四中,子进程退出时究竟是收到了什么信号。
#include #include #include #include #include #include int main(){ int fd[2] = { 0 }; if (pipe(fd) < 0){ //使用pipe创建匿名管道 perror("pipe"); return 1; } pid_t id = fork(); //使用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
命令可以查看13对应的具体信号。
[cl@VM-0-15-centos nonamepipe]$ kill -l
由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE
信号将子进程终止的。
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?
方法一:使用man手册
根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。
然后我们可以使用uname -r命令,查看自己使用的Linux版本。
根据man手册,我使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节。
方法二:使用ulimit命令
其次,我们还可以使用ulimit -a
命令,查看当前资源限制的设定。
根据显示,管道的最大容量是 512 × 8 = 4096 512/times8=4096 512×8=4096 字节。
方法三:自行测试
这里发现,根据man手册得到的管道容量与使用ulimit命令得到的管道容量不同,那么此时我们可以自行进行测试。
前面说到,若是读端进程一直不读取管道当中的数据,写端进程一直向管道写入数据,当管道被写满后,写端进程就会被挂起。据此,我们可以写出以下代码来测试管道的最大容量。
#include #include #include #include int main(){ int fd[2] = { 0 }; if (pipe(fd) < 0){ //使用pipe创建匿名管道 perror("pipe"); return 1; } pid_t id = fork(); //使用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;}
可以看到,在读端进程不进行读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节。
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
注意:
我们可以使用mkfifo
命令创建一个命名管道。
[cl@VM-0-15-centos fifo]$ mkfifo fifo
可以看到,创建出来的文件的类型是p
,代表该文件是命名管道文件。
使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
例如,将mode设置为0666,则命名管道文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask
函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
mkfifo函数的返回值。
创建命名管道示例:
使用以下代码即可在当前路径下,创建出一个名为myfifo的命名管道。
#include #include #include #define FILE_NAME "myfifo"int main(){ umask(0); //将文件默认掩码设置为0 if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件 perror("mkfifo"); return 1; } //create success... return 0;}
运行代码后,命名管道myfifo就在当前路径下被创建了。
1、如果当前打开操作是为读而打开FIFO时。
2、如果当前打开操作是为写而打开FIFO时。
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
服务端的代码如下:
//server.c#include "comm.h"int main(){ umask(0); //将文件默认掩码设置为0 if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件 perror("mkfifo"); return 1; } int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件 if (fd < 0){ perror("open"); return 2; } char msg[128]; while (1){ msg[0] = "/0"; //每次读之前将msg清空 //从命名管道当中读取信息 ssize_t s = read(fd, msg, sizeof(msg)-1); if (s > 0){ msg[s] = "/0"; //手动设置"/0",便于输出 printf("client# %s/n", msg); //输出客户端发来的信息 } else if (s == 0){ printf("client quit!/n"); break; } else{ printf("read error!/n"); break; } } close(fd);
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/123582.html
摘要:进程间通信方式虽然不同进程在用户空间不能直接进行通讯,但它们却是共享一份内核空间。系统为什么需要再实现一个进程间通信协议呢在移动设备上,的传输效率和可操作性很好。 完整原文:http://tryenough.com/android-... Android开发的进程间通讯,整个Android的应用都依赖于binder做底层通信机制。而Linux中提供的进程间通讯方式并没有binder机...
摘要:进程间通信方式虽然不同进程在用户空间不能直接进行通讯,但它们却是共享一份内核空间。系统为什么需要再实现一个进程间通信协议呢在移动设备上,的传输效率和可操作性很好。 完整原文:http://tryenough.com/android-... Android开发的进程间通讯,整个Android的应用都依赖于binder做底层通信机制。而Linux中提供的进程间通讯方式并没有binder机...
摘要:线程与进程的区别及其通信方式强烈推荐读者阅读文章最后的参考文章,本文只是概括和总结,更详细的内容参见引用列表。线程在执行过程中与进程还是有区别的。信号量主要作为进程间以及同一进程不同线程之间的同步手段。 线程与进程的区别及其通信方式 强烈推荐读者阅读文章最后的参考文章,本文只是概括和总结,更详细的内容参见引用列表。你也可以在我的 GitHub 里获得所有文章:https://githu...
摘要:进程与线程声明文章均为本人技术笔记,转载请注明出处进程线程基本概念进程程序的执行实体,操作系统分配资源的最小单位线程被称为轻量级进程,是调度分配的最小单位。一个标准的线程由线程,程序计数器,寄存器集合和堆栈组成进程间线程间通信方式进程间通信 进程与线程 声明 文章均为本人技术笔记,转载请注明出处https://segmentfault.com/u/yzwall 进程&线程基本概念 进...
阅读 3772·2021-11-24 09:38
阅读 1726·2021-11-17 09:33
阅读 1056·2021-10-19 11:42
阅读 1687·2021-10-14 09:42
阅读 2055·2019-08-30 15:44
阅读 436·2019-08-30 14:04
阅读 2784·2019-08-30 13:13
阅读 1779·2019-08-30 12:51