并发控制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 }
View Code

此时运行结果:

先打印: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 }
View Code

注:

(1)在Windows和Linux的共享文件夹下mnt/hgfs创建fifo会失败,所以Linux虚拟机的情况不要把fifi放到共享文件夹下

(2)在阻塞模式下的fifo,只有当读和写都打开时(调用open函数),两个进程才能返回接着运行,否则会阻塞到open函数处(这个也很好理解,fifo本来就是用于进程间共享数据的,单方面打开也没有意义);在非阻塞模式下,单方面调用open会显示错误

(3)管道内的内容并不是写入了文件,而是驻留在内存中。只有当输入输出都打开,才能开始传输。

 

posted @ 2020-03-05 14:12  晨枫1  阅读(317)  评论(0编辑  收藏  举报