【关于Linux中----进程间通信方式之管道】
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
一、什么是管道
进程间通信主要目的为以下几点
数据传输一个进程需要将它的数据发送给另一个进程
资源共享多个进程之间共享同样的资源。
通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。
进程控制有些进程希望完全控制另一个进程的执行如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 -o test 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;make
rm -f test
gcc -o test 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;make
rm -f test
gcc -o test 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) 0
file 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 disableread调用阻塞即进程暂停执行一直等到有数据来到为止。
O_NONBLOCK enableread调用返回-1errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable write调用阻塞直到有进程读走数据
O_NONBLOCK enable调用返回-1errno值为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;make
rm -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 11 15:16 fifo
-rw-rw-r-- 1 sny sny 122 Jan 11 15:14 Makefile
-rwxrwxr-x 1 sny sny 8408 Jan 11 15:16 test1
-rw-rw-r-- 1 sny sny 190 Jan 11 15:16 test1.c
-rwxrwxr-x 1 sny sny 8304 Jan 11 15:16 test2
-rw-rw-r-- 1 sny sny 47 Jan 11 14: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 11 15:23 fifo
-rw-rw-r-- 1 sny sny 127 Jan 11 15:23 Makefile
-rwxrwxr-x 1 sny sny 8464 Jan 11 15:23 test1
-rw-rw-r-- 1 sny sny 202 Jan 11 15:20 test1.c
-rwxrwxr-x 1 sny sny 8304 Jan 11 15:23 test2
-rw-rw-r-- 1 sny sny 47 Jan 11 14:59 test2.c
权限设置成功
管道创建成功如何通信呢
我们先创建两个源文件。
sever.c如下
#include "comm.h"
2
3 int main()
4 {
5 umask(0);
6 if(mkfifo(MY_FIFO,0666)<0)
7 {
8 perror("mkfifo");
9 return 1;
10 }
11 //进行文件操作
12 int fd=open(MY_FIFO,O_RDONLY);
13 if(fd<0)
14 {
15 perror("open");
16 return 2;
17 }
18 while(1)
19 {
20 char buf[64]={0};
21 ssize_t s=read(fd,buf,sizeof(buf)-1);
22 if(s>0)
23 {
24 buf[s]=0;
25 printf("client:%s\n",buf);
26 }
27 else if(s==0)
28 {
29 printf("client finish\n");
30 break;
31 }
32 else
33 {
34 perror("read");
if(s>0)
23 {
24 buf[s]=0;
25 printf("client:%s\n",buf);
26 }
27 else if(s==0)
28 {
29 printf("client finish\n");
30 break;
31 }
32 else
33 {
34 perror("read");
35 break;
36 }
37 }
38 return 0;
39 }
client.c如下
#include "comm.h"
2 #include <string.h>
3
4 int main()
5 {
6 int fd=open(MY_FIFO,O_WRONLY);
7 if(fd<0)
8 {
9 perror("open");
10 return 1;
11 }
12 while(1)
13 {
14 char buf[64]={0};
15 //先把数据从标准输入拿到进程内部
16 ssize_t s=read(0,buf,sizeof(buf)-1);
17 if(s>0)
18 {
19 buf[s-1]=0;
20 printf("%s\n",buf);
21 write(fd,buf,strlen(buf));
22 }
23 }
24 close(fd);
25 return 0;
26 }
它们包含的头文件comm.h如下
#pragma once
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 #define MY_FIFO "./fifo"
对应的Makefile如下
执行结果如下
可见两个进程成功实现通信
补充命名管道和匿名管道一样也是面向字节流的所以需要通信双方“制定协议”这里不具体详谈。
另外命名管道中的内容不会被刷新到磁盘中这样做是为了保证效率。
命名管道与匿名管道的区别
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建打开用open。
FIFO命名管道与pipe匿名管道之间唯一的区别在它们创建与打开的方式不同一但这些工作完成之后它们具有相同的语义。
本篇结束坚持努力