文章目录
一、什么是管道
进程间通信主要目的为以下几点
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程之间通信,就一定会有一个进程把数据交给另一个进程处理的情况。但是,进程是具有独立性的(一个进程时看不到另一个进程的资源的),进行信息交互时成本一定很高。所以,为了解决这个问题,操作系统就会提供一块公共的资源供不同的进程使用。
所以,进程间通信的本质就是操作系统参与,提供一份所有通信进程能看到的公共资源。而这份公共资源的提供方式可能是文件和各种结构体等,而资源提供方式的多样性也是通信方式多样性的根本原因。
如果看过我之前的文章的读者一定知道,每一个进程都有自己的进程控制块(PCB),在这个控制块里存放着一个指针,它指向一个结构体(struct files_struct),这个结构体中包含一个存放文件描述符的数组,每一个文件描述符都指向一个确定的文件结构体(struct file),其中存放文件的属性和文件操作指针集合。
当需要对特定的文件进行操作时,进程就会经过每一层结构体找到对应的文件,进而调用对应的文件接口进行操作。
而当一个进程创建了自己的子进程时,毫无疑问,父子进程是两个独立的进程。
那么需要为子进程创建自己的struct files_struct吗?
答:当然需要。因为struct files_struct是属于进程的,而不是属于文件的,它是为进程和文件之间建立联系的。为了进程的独立性,是一定要为子进程创建自己的结构体的。
那么需要为子进程创建自己的struct file吗?
答:不需要。因为进程跟文件之间是关联关系,而不是拥有关系。所以创建新的进程不需要将原先进程的文件复制一份。但是,子进程和父进程的文件描述符指向的文件是相同的。
当我们调用你某一个文件进行写操作时,进程会通过文件描述符找到对应的struct file,调用其中的write函数,而这个函数又会进行两个操作----将数据从用户拷贝到内核以及触发底层的写入函数将其写入磁盘。
这里注意,数据并不是直接写入磁盘的。而是先存入操作系统提供的一个缓冲区中,再经过刷新才到磁盘中的。
而如果不把已经存放到缓冲区中的数据刷新到磁盘中,子进程就可以通过和父进程相同的文件描述符找到对应的struct file,进而找到存放数据的缓冲区。这样也就实现了两个进程共享一份资源。这种通过文件的方式实现进程间通信的就是管道(从一个进程连接到另一个进程的一个数据流)。
二、匿名管道
创建一个管道分为三部分。
- 首先,父进程创建管道
- 然后,创建子进程
前面说过,创建子进程时,会为子进程创建一个进程控制块,也会为它创建一个struct files_struct,但是这时父子进程中的文件描述符表是相同的,指向的文件也是相同的。所以此时分别有两个进程的两个文件描述符指向一个管道。但是同一个进程是不能同时对一个文件即进行读操作又进行写操作的,所以接下来就进入最后一步
- 父子进程都关闭一个文件描述符,使得两个进程一个进行读操作,另一个进行写操作
在Linux中,用来创建管道的函数叫做pipe。
代码如下:
对应的Makefile内容如下:
结果如下:
[sny@VM-8-12-centos practice]$ ls
Makefile test.c
[sny@VM-8-12-centos practice]$ make
gcc -otest test.c -std=c99
[sny@VM-8-12-centos practice]$ ls
Makefile test test.c
[sny@VM-8-12-centos practice]$ ./test
pipefd[0]:3 pipifd[1]:4
因为前三个文件描述符被占用,父子进程打开的文件描述符不相同(一个进行读,一个进行写),所以结果毫无疑问是3和4。
下面创建子进程,并进行父子进程中的一个文件描述符的关闭。
补充:pipefd[0]和pipefd[1]分别表示读和写
接下来,给父子进程增加一些代码,让它们相互通信:
执行结果如下:
[sny@VM-8-12-centos practice]$ make clean;makerm-ftest
gcc -otest test.c -std=c99
[sny@VM-8-12-centos practice]$ ./test
pipefd[0]:3 pipifd[1]:4
child to father:hello world!
child to father:hello world!
child to father:hello world!
child to father:hello world!
child to father:hello world!
上面的代码中,子进程每写入一次就sleep一秒,但父进程却一直在读取。接下来,让子进程一直写入,让父进程每读取一次就sleep一秒看一下结果(代码雷同,就不粘贴了):
[sny@VM-8-12-centos practice]$ make clean;makerm-ftest
gcc -otest test.c -std=c99
[sny@VM-8-12-centos practice]$ ./test
pipefd[0]:3 pipifd[1]:4
child to father:hello world!hello world!hello world!hello world!hello world!hel
child to father:lo world!hello world!hello world!hello world!hello world!hello
child to father:world!hello world!hello world!hello world!hello world!hello wor
child to father:ld!hello world!hello world!hello world!hello world!hello world!
child to father:hello world!hello world!hello world!hello world!hello world!hel
child to father:lo world!hello world!hello world!hello world!hello world!hello
可以看到,每一次打印出来的内容都是没有规律的。这是因为缓冲区中的内容不会在每一次读取后被刷新,而是要等每一次缓冲区存满之后才刷新,这是管道面向字节流的特性。
下面再对代码稍作修改:
让子进程每次向缓冲区中写入一个字节的内容,并输出已经写入的字节的个数,而父进程什么也不做,结果如下:
可见,缓冲区能存储的最大容量为65536字节,也就是64KB。
由于这时,缓冲区已经存满,但是另一个进程并没有进行任何的读操作,所以为了不将没有被读取的信息覆盖,管道的写端会停止写入,直到读端进行读取信息才会继续写入。
所以,管道是有大小的。
而如果,当缓冲区满的时候,我们从缓冲区中读取一小段数据,它还是不会立刻就像其中写入信息。只有当读取的字节数比较大时,才会继续写入,这叫做管道的同步机制。(具体可以使缓冲区继续写入的读取字节数是多少,各位读者可以自己动手试一试,一般64字节以下是不足以让缓冲区继续写入的)
接下来看一下管道的大小:
[sny@VM-8-12-centos practice]$ ulimit-a
core file size (blocks, -c)0
data seg size (kbytes, -d) unlimited
scheduling priority (-e)0file size (blocks, -f) unlimited
pending signals (-i)7908
max locked memory (kbytes, -l) unlimited
max memory size (kbytes, -m) unlimited
open files (-n)100001
pipe size (512 bytes, -p)8
POSIX message queues (bytes, -q)819200
real-time priority (-r)0
stack size (kbytes, -s)8192
cpu time(seconds, -t) unlimited
max user processes (-u)7908
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
其中的pipe size=512*8bytes,也就是4KB。
所以,向管道中写书数据的大小是有限制的,也就是不能超过4KB,一旦超过,将不能保证管道的原子性。
而如果管道的写端关闭,那么读端将会读完管道中的内容,然后读端对应的进程将会退出。
而如果管道的读端关闭,这时如果写端继续写入将毫无意义,操作系统不会允许这种浪费时间和资源的事情出现,就会向写端对应的进程发送信号,进而杀死进程。
如果一个进程打开了一个文件,然后文件相关进程退出了,文件会怎么处理呢?
答:与文件相关的引用计数会自动进行–,然后操作系统会将该文件关闭。
总结一下管道的特点:
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
三、命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件。
创建命名管道可以用命令行mkfifo filename来实现:
下面用一种更通俗易懂的方式介绍一下命名管道:
我们通常标识一个文件时采用路径+文件名的方式,这样的标识使文件具有唯一性。
再思考一个问题:一个文件可以同时被两个进程以读/写的方式打开吗?
答案无疑是可以的,比如stdout和stderr,它们对应的都是显示器,也就是同一个文件。
所以,当我们以某种方式打开文件时,该文件就会被加载到内存中产生一个临时文件,这个文件可以被多个进程读取。而进程找到该文件的方式是通过磁盘中存储的此文件的路径+文件名。
而上述在内存中产生的临时文件就是命名管道!
命名管道的打开规则:
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
接下来用代码制造一个命名管道:
先创建两个源程序文件:
test1.c如下:
test2.c中只有一个空的main函数,对应的Makefile内容如下:
执行结果如下:
[sny@VM-8-12-centos practice]$ make clean;makerm-f test1 test2
gcc -o test1 test1.c
gcc -o test2 test2.c
[sny@VM-8-12-centos practice]$ ls
Makefile test1 test1.c test2 test2.c
[sny@VM-8-12-centos practice]$ ./test1
[sny@VM-8-12-centos practice]$ ll
total 36
prw-rw-r-- 1 sny sny 0 Jan 1115:16 fifo
-rw-rw-r-- 1 sny sny 122 Jan 1115:14 Makefile
-rwxrwxr-x 1 sny sny 8408 Jan 1115:16 test1
-rw-rw-r-- 1 sny sny 190 Jan 1115:16 test1.c
-rwxrwxr-x 1 sny sny 8304 Jan 1115:16 test2
-rw-rw-r-- 1 sny sny 47 Jan 1114:59 test2.c
可以看到,成功生成了fifo文件。但是代码中设置的初始权限为0666,结果却和代码不同?
只是因为创建管道文件时,会受到umask的影响,如果不想让规定的权限被改变,可以在创建之前将umask置0,如下:
再将Makefile中的clean中增加fifo,就可以生成新的管道文件了:
[sny@VM-8-12-centos practice]$ ./test1
[sny@VM-8-12-centos practice]$ ll
total 36
prw-rw-rw- 1 sny sny 0 Jan 1115:23 fifo
-rw-rw-r-- 1 sny sny 127 Jan 1115:23 Makefile
-rwxrwxr-x 1 sny sny 8464 Jan 1115:23 test1
-rw-rw-r-- 1 sny sny 202 Jan 1115:20 test1.c
-rwxrwxr-x 1 sny sny 8304 Jan 1115:23 test2
-rw-rw-r-- 1 sny sny 47 Jan 1114:59 test2.c
权限设置成功!
管道创建成功,如何通信呢?
我们先创建两个源文件。
sever.c如下:
#include"comm.h"23intmain()4{5umask(0);6if(mkfifo(MY_FIFO,0666)<0)7{8perror("mkfifo");9return1;10}11//进行文件操作12int fd=open(MY_FIFO,O_RDONLY);13if(fd<0)14{15perror("open");16return2;17}18while(1)19{20char buf[64]={0};21ssize_t s=read(fd,buf,sizeof(buf)-1);22if(s>0)23{24 buf[s]=0;25printf("client:%s\n",buf);26}27elseif(s==0)28{29printf("client finish\n");30break;31}32else33{34perror("read");if(s>0)23{24 buf[s]=0;25printf("client:%s\n",buf);26}27elseif(s==0)28{29printf("client finish\n");30break;31}32else33{34perror("read");35break;36}37}38return0;39}
client.c如下:
#include"comm.h"2 #include <string.h>34intmain()5{6int fd=open(MY_FIFO,O_WRONLY);7if(fd<0)8{9perror("open");10return1;11}12while(1)13{14char buf[64]={0};15//先把数据从标准输入拿到进程内部 16ssize_t s=read(0,buf,sizeof(buf)-1);17if(s>0)18{19 buf[s-1]=0;20printf("%s\n",buf);21write(fd,buf,strlen(buf));22}23}24close(fd);25return0;26}
它们包含的头文件comm.h如下:
#pragmaonce 2 #include <stdio.h>3 #include <unistd.h>4 #include <sys/types.h>5 #include <sys/stat.h>6 #include <fcntl.h>78 #define MY_FIFO "./fifo"
对应的Makefile如下:
执行结果如下:
可见,两个进程成功实现通信!
补充:命名管道和匿名管道一样,也是面向字节流的,所以需要通信双方“制定协议”,这里不具体详谈。
另外,命名管道中的内容不会被刷新到磁盘中,这样做是为了保证效率。
命名管道与匿名管道的区别:
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open。
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
本篇结束!坚持!努力!
版权归原作者 Undefined__yu 所有, 如有侵权,请联系我们删除。