Linux 通信——管道详解
Linux 进程间通信(IPC)由以下几部分发展而来:
早期UNIX进程间通信、基于System V进程间通信、基于Socket进程间通信和POSIX进程间通信。
UNIX进程间通信方式包括:管道、FIFO、信号。
System V进程间通信方式包括:System V消息队列、SystemV信号灯、System V共享内存、
POSIX进程间通信包括:posix消息队列、posix信号灯、posix共享内存。
现在linux使用的进程间通信方式:
(1)管道(pipe)和有名管道(FIFO) 半双工 相当于两个进程通过一个文件交流
(2)信号(signal) 通过发送信号控制/通知另一个进程
(3)消息队列 一个进程把消息放进链表,其他进程去读取
(4)共享内存 开辟一块内存大家在上面交流
(5)信号量 排队去访问共享的一个资源
(6)套接字(socket)
Linux进程通信已基本学完,现在我着重复习管道通信的相关知识点。
管道
管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。管道包括无名管道和命名管道两种,前者用于父进程和子进程间的通信,后者可用于运行于同一系统中的任意两个进程间的通信。
无名管道
无名管道由pipe( )函数创建:
#include <unistd.h>
intpipe(int filedis[2]);
pipe()函数的参数是一个由两个整数类型的文件描述符组成的数组指针。该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno以表明失败原因——
EMFILE进程已用完文件描述词最大量。
ENFILE 系统已无文件描述词可用。
EFAULT 参数 filedes 数组地址不合法。
当一个管道被创建时,它会创建两个文件描述符:filedis[0]用于读管道,filedis[1]用于写管道。数据基于先进先出的原则(FIFO)进行处理。 特别注意:这里使用的是文件描述符,而不是文件流,我们必须用底层的read和write调用来访问数据,因为管道不是正规的文件,不能使用fread和fwrite。
write:ssize_twrite(int fd, const void *buf, size_t count);
数据按到达顺序依次写入管道。通常(清除O_NONBLOCK),如果管道满了,write将阻塞,直到read移除了足够的旧数据;没有局部写。如果设置了O_NONBLOCK,而且将被写入的数量是PIPE_BUF或者更少,则write要么立即写入数据,要么返回-1且把errno设置为EAGAIN;没有局部写。但如果数量超过PIPE_BUF,局部写是有可能的。
写管道
O_NONBLOCK | 写的总数 | 无立即可写的 | 部分立即可写的 | 所有立即可写的 |
清除 | <=PIPE_BUF | 阻塞;完全写;原子的; | 阻塞;完全写;原子的; | 不阻塞;完全写;原子的 |
清除 | >PIPE_BUF | 阻塞;完全写;非原子的 | 阻塞;完全写;非原子的; | 可能阻塞;完全写;非原子 |
设置 | <=PIPE_BUF | EAGAIN | EAGAIN | 不阻塞;完全写;原子的 |
设置 | >PIPE_BUF | EAGAIN | 不阻塞;部分或EAGAIN;原子的 | 不阻塞;完全、部分或EAGAIN;非原子的 |
read:ssize_tread(int fd, void *buf, size_t count);
和写入时一样,按到达顺序依次读取管道数据。通常(清除O_NONBLOCK),如果管道是空的,read将阻塞,直到至少有一字节的数据可用,除非关闭所有的写入文件描述符,这种情况下,read返回0(通常是文件结束指示)。但read的第三个参数的字数节不必满足——只要和那个时刻读取的字节相同,并且返回一个合适的计数就行了。当然,永远也不会超越该字节计数;下一个读操作可以读没有被读的字节。如果设置了O_NONBLOCK,那么空管道上的读操作将返回-1,并且设置errno为EAFAIN
读取管道
O_NONBLOCK | 无立即可读的 | 部分或所有立即可读的 |
清除 | 除非没有写操作,否则阻塞(返回0) | 不阻塞;可能部分读 |
设置 | 除非没有写操作,否则为EAGAIN(返回0) | 不阻塞;可能部分读 |
①用管道进行单向通信:
常用方法:
1、创建管道。
2、派生创建读子进程。
3、在子进程中,关闭管道的写结尾,并做好所需准备。
4、在子进程中,执行该子进程的程序。
5、在父进程中,关闭管道的读结尾。
6、如果第二个子进程需要写管道,那就创建它,做好必要准备,并执行其程序。如果父进程准备写,那么直接写就可以了。
示例:
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 char buff[BUFSIZ];
7 int fds[2];
8 pid_t pid;
9 int len;
10 int err;
11 if(err=pipe(fds)){ //创建管道
12 perror("pipe");
13 return 1;
14 }
15 pid=fork(); //调用fork创建一个读子进程
16 if(pid==0){
17 close(fds[1]); //在子进程中,关闭管道写结尾
18 while(1){
19 if(len=read(fds[0],buff,BUFSIZ)>0)
20 printf("%s\n",buff);
21 }
22 close(fds[0]);
23 }
24 if(pid>0){
25 close(fds[0]); //在父进程中,关闭管道读结尾
26 while(1){
27 len=write(fds[1],"hello",6);
28 printf("write return len is %d\n",len);
29 sleep(1);
30 }
31 close(fds[1]);
32 }
33 return 0;
34 }
运行结果:
②用管道进行双向通行:
方法与单向通信类似:
1、分别创建管道1和2。
2、派生创建一个子进程。
3、在子进程中,关闭管道1的写入端和管道2的读出端,并做好所需准备。
4、在子进程中,执行该子进程的程序。
5、在父进程中,父进程关闭管道1的读出端和管道2的写入端;
6、父子进程建立双向通信。
虽然全双工管道两端都可读可写,但是两端同时写的时候数据是否会相互覆盖?下面用程序验证这个问题。
1#include<unistd.h>
2#include<stdio.h>
3#include<sys/types.h>
4#include<sys/socket.h>
5#include<string.h>
6#include<stdlib.h>
7#include<sys/wait.h>
8
9 int main(){
10 int fds1[2];
11 int fds2[2];
12 int pipefd[2]; //全双工管道
13 char buff[BUFSIZ];
14 memset(buff,0,sizeof(buff));
15 int ret=pipe(fds1);
16 if(ret){
17 perror("pipe");
18 return 1;
19 }
20 ret=pipe(fds2);
21 if(ret){
22 perror("pipe");
23 return 1;
24 }
25 ret=socketpair(AF_UNIX,SOCK_STREAM,0,pipefd);
26 if(ret){
27 perror("pipe");
28 return 1;
29 }
30
31 int pid=fork();
32 if(pid<0){
33 printf("fork error");
34 return 1;
35 }
36 else if(pid==0){
37 close(pipefd[0]);
38 close(fds1[1]);
39 close(fds2[0]);
40 char* data="child writesocketpair";
41 write(pipefd[1],data,strlen(data)); //向全双工管道1端写数据
42 write(fds2[1],"y",1); //子进程通过管道fds2写端写入y通知父进程:子进程已经写了socketpair的1端
43 read(fds1[0],buff,BUFSIZ); //读取fds1管道查看是否父进程写了socketpair的0端
44 if(buff[0]=='y'){
45 memset(buff,0,sizeof(buff));
46
47 read(pipefd[1],buff,sizeof(buff));//获取socketpair的1端数据(该数据由父进程发送)
48 printf("child get socketpair%s\n ",buff);
49 exit(0);
50 }
51 else{
52 printf("child not getsockerpair");
53 exit(1);
54 }
55 }
56 else{
57 close(fds1[0]);
58 close(fds2[1]);
59 close(pipefd[1]);
60 char* data="parent writesocketpair";
61 write(pipefd[0],data,strlen(data)); //父进程写socketpair的0端
62 write(fds1[1],"y",1); //父进程通过管道fds1写端写入y通知子进程:父进程已经写了socketpair的0端
63 read(fds2[0],buff,BUFSIZ); //读取fds2管道查看子进程是否写了socketpair的1端
64 if(buff[0]=='y'){
65 memset(buff,0,sizeof(buff));
66 read(pipefd[0],buff,sizeof(buff)); //读取socketpair的0端(该数据由子进程发送)
67 printf("parent get socketpair%s\n",buff);
68
69 }
70 else{
71 printf("child not getsocketpair");
72 return 1;
73 }
74 }
75 return 0;
76 }
运行结果:
示例解析:父进程和子进程同时写进一个全双工管道socketpair并且都等待对方写完以后才读取数据。这里用了三个管道,一个全双工管道socketpair和两个半双工管道fds1,fds2,全双工管道用于父子进程同时读同时写,半双工管道用于父子进程通知对方自己已经写入了全双工管道。结果显示父子进程同时写的数据不会相互覆盖,可见全双工管道底层实现逻辑类似用两个半双工管道模拟的。