Linux进程通信方式(1)--管道

  Linux进程间通信的方式有很多,在这里我们将着重讲述常用的方式,全部如下:

  1)管道:管道主要分为匿名管道和有名管道,匿名管道用于父子进程,有名管道可以用于任意进程。

  2)信号:唯一的一种异步处理的进程通信方式,所以是一种比较复杂的进程通信方式。

  3)消息队列:消息队列是消息的链接表,包括posix消息队列和system v消息队列。

  4)共享内存:多个进程访问同一块内存空间,是速度最快的进程间通信手段。

  5)信号量:主要作为进程间或者同一进程不同线程间的同步手段。

  6)套接字:套接字分为本地套接字和网络套接字,本地套接字用于本机不同进程间的通信;网络套接字用于不同主机的进程通信,是Linux网络功能的重点之一,必须熟练掌握。

背景知识

  既然我们讲到Linux进程间通信,我们有必要先介绍一下依据传输数据的方向分出来的三种三种传输模式:单工,半双工和全双工。

  单工:数据传输的方向是单向的,只有一个发送端和一个接收端,例如计算机和打印机之前的通信,只存在计算机向打印机传输数据,不存在打印机往回发送数据给计算机。单工传输如图所示:

                                        

 

  半双工:数据的传输方向是双向的,通信双方既可以是数据的发送端也可以是数据的接收端,虽然数据传输允许数据在两个方向上传输,但是,在任何时刻只能由其中的一方发送数据,另一方接收数据,即无法同时进行收发。

它实际上是一种切换方向的单工通信,就和对讲机(步话机)一样。半双工通信中每端需有一个收发切换电子开关,通过切换来决定数据向哪个方向传输。

                

  全双工:数据的传输方向是双向的,在任何时刻通信双方都可以同时发送数据和接收数据,例如打电话。

            

1.管道

  管道分为匿名管道和有名管道,需要注意的是管道属于半双工通信。

1.1 匿名管道

       匿名管道是父子进程间通信的常用手段之一,系统调用如下:

  #include <unistd.h>

    int pipe(int pipefd[2]);

  参数 pipefd[2]: 元素个数为2的整型数组,pipefd[0]用于存放pipe调用成功返回的管道读端文件描述符,pipefd[1]则用于存放写端文件描述符.

  返回值:成功返回0,失败返回-1,并设置errno。

  管道在父子进程传递数据,利用的是fork调用之后两个管道文件描述符都保持打开,一对管道文件描述符只能保证父子进程间一个方向的数据传输,管道两端分别用描述符fd[0]和fd[1]来描述,需要注意的是,两端是固定了任务的,fd[0]只能用于读,fd[1]只能用于写,如果试图从fd[0]中写数据或者从fd[1]中读数据,都将导致错误发生,所以父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。比如,我们要使用管道实现从父进程向子进程写数据,可按照图1-1所以来操作。

                              

 

 

我们来编写一个上图父进程通过匿名管道向子进程发送数据的例子。补充一下,一般的文件IO操作函数都可以用于管道,例如close,read,send,write等。

 //pipe.c
1
#include <sys/types.h> 2 #include <unistd.h> 3 #include <string.h> 4 #include <errno.h> 5 #include <stdio.h> 6 #include <sys/wait.h> 7 #include <stdlib.h> 8 9 10 int main(void) 11 { 12 13 pid_t pid; 14 int pipefd[2]; 15 if(pipe(pipefd) == -1) 16 { 17 printf("pipe failed, errmsg: %s\n", strerror(errno)); 18 return -1; 19 } 20 21 pid = fork(); 22 if(pid < 0) 23 { 24 printf("fork failed, errmsg: %s\n", strerror(errno)); 25 return -1; 26 } 27 else if(pid > 0) 28 { 29 printf("I am parent process, pid %d, I will send data to pipe\n", getpid()); 30 close(pipefd[0]); //关闭读端 31 char buf[24] = "I am your father!"; 32 write(pipefd[1], buf, strlen(buf)); 33 wait((int *)0);//阻塞等待 接收子进程退出状态,但是我们没有保存,只是为了预防子进程进入僵尸态 34 } 35 else 36 { 37 printf("I am child process, pid %d, I will get data from pipe\n", getpid()); 38 close(pipefd[1]); //关闭写端 39 char recv_buf[24] = {0}; 40 read(pipefd[0], recv_buf, 24); 41 printf("recv from parent %d, buf: %s\n", getppid(), recv_buf); 42 exit(1); 43 } 44 45 }

编译并执行

ydq@ubuntu:pipe$ ./pipe
I am parent process, pid 38930, I will send data to pipe
I am child process, pid 38931, I will get data from pipe
recv from parent 38930, buf: I am your father!

 1.2 有名管道

  有名管道提供一个路径名与之关联,以FIFO的路径名(带路径的文件名)形式存在于文件系统中,这样,即使不存在亲缘关系的进程,只要具有访问该路径名的权限,就能够彼此通过FIFO文件进行相互通信。FIFO严格遵循先进先出(first in first out),对管道FIFO的读总是从开始处返回数据,对它们的写则把数据写在末尾。不支持lseek()等文件定位操作。

1.2.1 有名管道的创建

       #include <sys/types.h>
       #include <sys/stat.h>

       int mkfifo(const char *pathname, mode_t mode);

  参数:

  pathname  -- 普通的路径名,也就是创建后FIFO的名字;

  mode  --  与打开普通文件的open()函数中的mode参数相同。

  如果mkfifo的第一个参数是一个已经存在的路径名时,则会返回EEXIST错误,所以一般的调用代码会先判断是否返回该错误,如果确实返回了该错误,我们只需调用打开FIFO的函数就可以了。一般的文件IO函数都可以用于FIFO,如close,read,write等。

1.2.2有名管道的打开规则

  有名管道比匿名管道多了一个打开操作:open。

  FIFO的打开规则

  读打开

  1)如果当前的打开操作是为了读而打开FIFO,若已经有相应的进程为写而打开该FIFO,则当前打开操作将成功返回;

  2)如果当前的打开操作是为了读而打开FIFO,若没有相应的进程为写而打开该FIFO,则可能阻塞直到有相应进程为了写而打开该FIFO(当前打开操作设置了阻塞标志);或者,打开成功并返回fifo的文件描述符(当前打开操作设置了非阻塞标志O_NONBLOCK)。

  写打开

  1)如果当前的打开操作是为了写而打开FIFO,若已经有相应的进程为读而打开该FIFO,则当前打开操作将成功返回;

  2)如果当前的打开操作是为了写而打开FIFO,若没有相应的进程为读而打开该FIFO,则可能阻塞直到有相应进程为了读而打开该FIFO(当前打开操作设置了阻塞标志);或者,打开失败返回-1并设置错误码errno为6(当前打开操作设置了非阻塞标志O_NONBLOCK)。

    接下来我们来验证一下上述的打开规则。

    验证读的阻塞和非阻塞

 //fifo_read.c
1
#include <sys/types.h> 2 #include <sys/stat.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <sys/types.h> 8 #include <fcntl.h> 9 #include <errno.h> 10 11 int main(void) 12 { 13 const char *fifo_name = "./my_fifo"; 14 15 int res = 0; 16 if(access(fifo_name, F_OK) == -1) 17 { 18 printf("create the fifo file\n"); 19 res = mkfifo(fifo_name, 0777 | O_CREAT); 20 if(res == -1) 21 { 22 printf("mkfifo failed\n"); 23 return -1; 24 } 25 } 26 27 mode_t mode = O_RDONLY; 28 //非阻塞打开fifo管道文件 29 //mode |= O_NONBLOCK; 30 int fifo_fd = open(fifo_name, mode); 31 if(fifo_fd == -1) 32 { 33 printf("open failed, errno: %d\n", errno); 34 return -1; 35 } 36 37 printf("line %d, fifo_fd %d\n", __LINE__, fifo_fd); 38 39 close(fifo_fd); 40 return 0; 41 }

  现在,上述代码是阻塞状态下创建fifo文件并打开它,我们可以编译并运行来验证阻塞读打开的情况。

ydq@docsis4 fifo $ g++ fifo_read.c -o fifo_read
ydq@docsis4 fifo $ ./fifo_read

^C

  编译运行后,我们发现程序被阻塞了,而且行数为37的打印语句没有打印出来,这可以表明在没有相应的进程写打开FIFO情况下,以阻塞读调用open这会阻塞,这时候我们可以编写一个写打开FIFO继续验证。

 //fifo_write.c
1
#include <sys/types.h> 2 #include <sys/stat.h> 3 #include <unistd.h> 4 #include <string.h> 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <sys/types.h> 8 #include <fcntl.h> 9 #include <errno.h> 10 11 int main(void) 12 { 13 const char *fifo_name = "./my_fifo"; 14 15 int res = 0; 16 if(access(fifo_name, F_OK) == -1) 17 { 18 printf("create the fifo file\n"); 19 res = mkfifo(fifo_name, 0777 | O_CREAT); 20 if(res == -1) 21 { 22 printf("mkfifo failed\n"); 23 return -1; 24 } 25 } 26 27 mode_t mode = O_WRONLY; 28 //非阻塞打开fifo管道文件 29 //mode |= O_NONBLOCK; 30 int fifo_fd = open(fifo_name, mode); 31 if(fifo_fd == -1) 32 { 33 printf("open failed, errno: %d\n", errno); 34 return -1; 35 } 36 37 printf("line %d\n", __LINE__); 38 39 close(fifo_fd); 40 return 0; 41 }

  编译并执行fifo_write和再打开一个终端执行 ./fifo_read

ydq@docsis4 fifo $ g++ fifo_write.c -o fifo_write
ydq@docsis4 fifo $ ./fifo_write
line 37, fifo_fd 3

    另一个终端.

ydq@docsis4 fifo $ ./fifo_read
line 37, fifo_fd 3

   执行结果可以验证了阻塞读打开和阻塞写打开  

  我们把29行的注释符去掉,验证非阻塞读打开FIFO和非阻塞写打开FIFO。

ydq@docsis4 fifo $ g++ fifo_read.c -o fifo_read
ydq@docsis4 fifo $
ydq@docsis4 fifo $ ./fifo_read
line 37, fifo_fd 3

  我们发现,在没有相应的进程写打开FIFO情况下,非阻塞读打开可以成功返回。

ydq@docsis4 fifo $ g++ fifo_write.c -o fifo_write
ydq@docsis4 fifo $ ./fifo_write
open failed, errno: 6

       而在没有相应的进程读打开FIFO情况下,非阻塞写打开返回失败并设置errno为6,以下是errno 6 -- ENXIO的解释。

    O_NONBLOCK | O_WRONLY is set, the named file is a FIFO and no process has the file open for reading.  Or, the file is a device special  file  and  no corresponding device exists.

  综上,我们已经验证完FIFO的读写规则,接下来我们在编写两个进程利用FIFO通信的例子。

 1 //process_fifo_read.c
 2 #include <sys/types.h>
 3 #include <sys/stat.h>
 4 #include <unistd.h>
 5 #include <string.h>
 6 #include <stdio.h>
 7 #include <stdlib.h>
 8 #include <sys/types.h>
 9 #include <fcntl.h>
10 #include <errno.h>
11 
12 int main(void)
13 {
14     const char *fifo_name = "./my_fifo";
15 
16     int res = 0;
17     if(access(fifo_name, F_OK) == -1)
18     {
19         printf("create the fifo file\n");
20         res = mkfifo(fifo_name, 0777 | O_CREAT);
21         if(res == -1)
22         {
23             printf("mkfifo failed\n");
24             return -1;
25         }
26     }
27 
28     mode_t mode = O_RDONLY;
29     //非阻塞打开fifo管道文件
30     //mode |= O_NONBLOCK;
31     int fifo_fd = open(fifo_name, mode);
32     if(fifo_fd == -1)
33     {
34         printf("open failed, errno: %d\n", errno);
35         return -1;
36     }
37 
38     char buf[16] = {0};
39     int ret = read(fifo_fd, buf, sizeof(buf));
40     if(ret == -1)
41     {
42         printf("write failed, errno %d\n", errno);
43         return -1;
44     }
45 
46     printf("read from FIFO: %s\n", buf);
47 
48     close(fifo_fd);
49     return 0;
50 }
 1 //process_fifo_write.c
 2 #include <sys/types.h>
 3 #include <sys/stat.h>
 4 #include <unistd.h>
 5 #include <string.h>
 6 #include <stdio.h>
 7 #include <stdlib.h>
 8 #include <sys/types.h>
 9 #include <fcntl.h>
10 #include <errno.h>
11 
12 int main(void)
13 {
14     const char *fifo_name = "./my_fifo";
15 
16     int res = 0;
17     if(access(fifo_name, F_OK) == -1)
18     {
19         printf("create the fifo file\n");
20         res = mkfifo(fifo_name, 0777 | O_CREAT);
21         if(res == -1)
22         {
23             printf("mkfifo failed\n");
24             return -1;
25         }
26     }
27 
28     mode_t mode = O_WRONLY;
29     //非阻塞打开fifo管道文件
30     //mode |= O_NONBLOCK;
31     int fifo_fd = open(fifo_name, mode);
32     if(fifo_fd == -1)
33     {
34         printf("open failed, errno: %d\n", errno);
35         return -1;
36     }
37 
38     char buf[16] = "Hello world!";
39     int ret = write(fifo_fd, buf, strlen(buf));
40     if(ret == -1)
41     {
42         printf("write failed, errno %d\n", errno);
43         return -1;
44     }
45 
46     close(fifo_fd);
47     return 0;
48 }

  分别在两个终端上编译和运行。

ydq@docsis4 fifo $ g++ process_fifo_write.c -o process_fifo_write
ydq@docsis4 fifo $ ./process_fifo_write

ydq@docsis4 fifo $ g++ process_fifo_read.c -o process_fifo_read
ydq@docsis4 fifo $ ./process_fifo_read
read from FIFO: Hello world!

2.注意事项

  对于管道通信,有两个注意事项,分别为:

  1)传输模式

  无论是匿名管道还是有名管道,它们都是半双工管道,所以同一时刻只支持一个方向上的数据传输。

     2)原子性

  当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性;当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。On  Linux, PIPE_BUF is 4096 bytes。

 

posted @ 2020-09-05 13:25  ydqun  阅读(363)  评论(0编辑  收藏  举报