并发控制1:进程通信之管道
注:关于进程间通信机制也可以参考https://www.jianshu.com/p/206a95ed784f。总结很全面,本文更侧重理解和细节问题。
多个进程之间通信,实际上是内核提供一定缓冲区,进程通过该缓冲区交换数据。内核提供的这种机制即进程通信机制(Interprocess Communication, IPC)。IPC的实现方法有很多:管道,共享内存,消息队列和信号。本小节主要记录管道通信机制。
管道主要分为两种,匿名管道和命名管道。前者只能在有血缘关系的进程间通信,缓存在内存中;而后者可以在任意进程之间通信,并且是以实际文件形式存在。
1. 匿名管道
在LINUX系统中,创建匿名管道的方式非常简单,如下:
#include <unistd.h> int pipe(int fieldes[2]); //成功返回0,失败返回-1 fieldes是用来保存使用该管道的文件描述符 fieldes[0]:管道出口,即读出文件内容 fieldes[1]:管道入口,即写入文件内容
创建了管道,就可以像所有其他通信方式一样,从该文件描述符read和write。下面看示例:
#include <stdio.h> #include <unistd.h> int main(int argc,char* argv[]) { char message[]="How are you"; char buf[30]; int fd1[2]; pipe(fd1); pid_t pid=fork(); if(pid==0) //子进程 { write(fd1[1],message,sizeof(message)); } else{ read(fd1[0],buf,30); printf("Parent process read:%s\n",buf); } return 0; }
运行该程序,结果终端打印: Parent process read:How are you
修改该程序:
在子进程write函数之后添加 read(fd1[0],buf,30); printf("Child process read:%s\n",buf); 父进程read函数之前添加 sleep(3)
运行结果:Child process read:How are you 。并且程序不会终止
再次修改程序:
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(int argc,char* argv[]) 5 { 6 char message[]="How are you"; 7 char message1[]="I'm fine!"; 8 char buf[30]; 9 char buf1[30]; 10 int fd1[2]; 11 pipe(fd1); 12 pid_t pid=fork(); 13 14 if(pid==0) //子进程 15 { 16 write(fd1[1],message,sizeof(message)); 17 read(fd1[0],buf,30); 18 write(fd1[1],message1,sizeof(message1)); 19 printf("Child process read:%s\n",buf); 20 } 21 else{ 22 sleep(3); 23 read(fd1[0],buf1,30); 24 printf("Parent process read:%s\n",buf1); 25 } 26 return 0; 27 }
此时运行结果:
先打印:Child process read:How are you,隔3s再打印:Parent process read:I'm fine!
说明:
(1)管道其实是无流向的,就相当于一个文件夹,谁先读谁获取管道内的信息。所以为了实现进程双向通信,如果用一个管道实现,此时时序很复杂,基本不可能实现(因此也说匿名管道只能单向通信)。因此一般采用两个管道,一个用于父进程向子进程传递数据,一个用于子进程向父进程传递。
(2)如果read,write函数是阻塞型的,会出现:
写端关闭,读端读到管道无数据时会自动返回;写端未关闭,读端读到管道无数据时会阻塞;
读端关闭,写端写道到缓存满时,收到信号SIGPIPE,关闭;读端未关闭,写端写道到缓存满时,阻塞。
PS:最近在《UNIX环境高级编程》中看到了管道一个比较有趣的程序,记录在此。该程序创建一个管道,父进程读取文件传给子进程(写入管道),子进程将管道读取端与标准输入结合,执行分页程序显示文件内容。
#include <sys/wait.h> #include <string.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #define DEF_PAGER "/bin/more" int main() { int fd[2]; pid_t pid; FILE* filefd; char line[100]; char *pager,*argv0; if(pipe(fd)<0){ //创建管道 printf("pipe error\n"); exit(1); } if((filefd=fopen("pipe.txt","r"))==NULL){ //打开文件 printf("open file failed\n"); exit(1); } if((pid=fork())<0) { printf("fork error\n"); exit(1); } else if(pid>0) //parent,将文件内容通过管道发送给子进程 { close(fd[0]); //关闭读 while(fgets(line,100,filefd)!=NULL){ int n=strlen(line); write(fd[1],line,n); } close(fd[1]); int status; wait(&status); } else { close(fd[1]); //关闭写 if(fd[0]!=STDIN_FILENO) { dup2(fd[0],STDIN_FILENO); //将管道读端与标准输入结合 close(fd[0]); } //获取分页程序 if((pager=getenv("PAGER"))==NULL) pager=DEF_PAGER; if((argv0=strrchr(pager,'/'))!=NULL) argv0++; else argv0=pager; execl(pager,argv0,(char*)0); //执行分页程序,显示标准输入的内容 } exit(0); }
该程序执行结果就是将pipe.txt的内容打印到终端。
2. 命名管道
命名管道相比于匿名管道,就是指定了管道的存储位置,并建立实体文件。系统就能通过该文件进行读写,因此能实现不同进程之间数据交互。创建命名管道的函数有两个:
#include <sys/types.h> #include <sys/stat.h> int mknod(const char* path, mode_t mode, dev_t deev); //较老版本,一般不使用 int mkfifo(const char* path, mode_t mode); path: 文件路径 mode: 打开模式,一般为0777
创建了命名管道(即FIFO文件),在调用open函数打开即可使用
int open(const char* path, mode); //返回文件描述符
path: 打开文件的名称
mode:打开模式,常用模式四种 O_RDONLY, ORDONLY|O_NONBLOCK, O_WRONLY, O_WRONLY|O_NONBLOCK 含义从名称即可看出
下面演示使用命名管道进行通信,为了能显示不具有血缘关系的进程间通信,创建两个程序,一读一写,同时在终端运行。
1 //写进程 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <fcntl.h> 7 #include <string.h> 8 #define BUF_SIZE 30 9 10 int main(int argc,char* argv[]) 11 { 12 const char *path="/tmp/mypipe"; 13 char buf[BUF_SIZE]; 14 if(access(path,F_OK)==-1){ 15 if(mkfifo(path,0777)==-1){ 16 printf("Make fifo failed\n"); 17 return -1; 18 } 19 } 20 21 int fd=open(path,O_WRONLY); 22 while(1) 23 { 24 fputs("Input your message(Q to quit)\n",stdout); 25 fgets(buf,BUF_SIZE,stdin); 26 if(!strcmp(buf,"Q\n")) break; 27 else write(fd,buf,strlen(buf)); 28 } 29 close(fd); 30 return 0; 31 32 } 33 34 //读进程,头文件与写进程相同 35 #define BUF_SIZE 30 36 37 int main(int argc,char* argv[]) 38 { 39 const char path[]="/tmp/mypipe"; 40 char buf[BUF_SIZE]; 41 int fd=open(path,O_RDONLY); 42 while(1) 43 { 44 memset(buf,0,sizeof(buf)); 45 int str_len=read(fd,buf,BUF_SIZE); 46 printf("%s\n",buf); 47 if(str_len==0) break; 48 } 49 close(fd); 50 return 0; 51 }
注:
(1)在Windows和Linux的共享文件夹下mnt/hgfs创建fifo会失败,所以Linux虚拟机的情况不要把fifi放到共享文件夹下;
(2)在阻塞模式下的fifo,只有当读和写都打开时(调用open函数),两个进程才能返回接着运行,否则会阻塞到open函数处(这个也很好理解,fifo本来就是用于进程间共享数据的,单方面打开也没有意义);在非阻塞模式下,单方面调用open会显示错误。
(3)管道内的内容并不是写入了文件,而是驻留在内存中。只有当输入输出都打开,才能开始传输。