• Linux下进程通信之管道


      每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。如下图所示。

      进程间通信

      目前进程通信的方式有:

    • 管道
    • FIFO
    • 消息队列
    • 信号量
    • 共享内存
    • 套接字

    管道

    管道概念

      管道是Linux支持的最初Unix IPC形式之一,具有以下特点:

    • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
    • 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
    • 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
    • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

    管道的创建

    #include <unistd.h>
    int pipe(int filedes[2]);
    // 返回值:成功返回0,出错返回-1

      该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。

    管道的读写规则

      从管道中读取数据:

    • 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;
    • 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同)。

      向管道中写入数据:

    • 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。 
      注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。

    简单例子

      下边是APUE提供的一个例子:

      管道

    1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。

    2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

    3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读。数据从写端流入从读端流出,这样就实现了进程间通信。

      具体程序如下:

     1 #include <stdlib.h>
     2 #include <unistd.h>
     3 #define MAXLINE 1024
     4 
     5 int main(void)
     6 {
     7     int n;
     8     int fd[2];
     9     pid_t pid;
    10     char line[MAXLINE];
    11 
    12     if (pipe(fd) < 0) 
    13     {
    14         perror("pipe");
    15         exit(1);
    16     }
    17 
    18     pid = fork();
    19     if (pid < 0) 
    20     {
    21         perror("fork");
    22         exit(1);
    23     }
    24     else if (pid > 0)   /* parent */
    25     { 
    26         close(fd[0]);   // 关闭读端
    27         write(fd[1], "hello world
    ", 12);
    28     } 
    29     else                /* child */
    30     {       
    31         close(fd[1]);   // 关闭写端
    32         n = read(fd[0], line, MAXLINE);
    33         write(STDOUT_FILENO, line, n);
    34     }
    35 
    36     return 0;
    37 }

    管道的局限性

      管道的主要局限性正体现在它的特点上:

    • 只支持单向数据流(现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不能预先假定系统提供此特性。);
    • 只能用于具有亲缘关系的进程之间;
    • 没有名字;
    • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
    • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

    FIFO

    FIFO概念

      管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

    FIFO的创建

      创建FIFO原型函数如下:

    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    // 返回值:若成功返回0,出错则返回-1

    该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。 如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。

      一个简单的程序实例如下:

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <sys/types.h>
     4 #include <sys/stat.h>
     5 
     6 int main()
     7 {
     8     int res = mkfifo("/tmp/my_fifo", 0777);
     9     if(res == 0)
    10         printf("FIFO created!
    ");
    11     else
    12     {
    13         printf("Create FIFO fails!
    ");
    14         return 1;
    15     }
    16 
    17     return 0;
    18 }

      编译并执行该程序后,我们可以查看得到所创建的管道:

    $ ls -lF /tmp/my_fifo
    prwxr-xr-x 1 root root 0 Aug 25 14:19 /tmp/my_fifo|

      注意:ls命令的输出结果中的第一个字符为p,表示这是一个管道。最后的|符号是由ls命令的-F选项添加的,它也表示是这是一个管道。

      下边的程序包括了对FIFO的删除(也可直接用命令rm):

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <unistd.h>
     4 #include <sys/types.h>
     5 #include <sys/stat.h>
     6 
     7 int main()
     8 {
     9     int res = mkfifo("/tmp/my_fifo", 0777);
    10     if(res == 0)
    11         printf("FIFO created!
    ");
    12     else
    13     {
    14         printf("Create FIFO fails!
    ");
    15         return 1;
    16     }
    17 
    18     res = unlink("/tmp/my_fifo");
    19     if(res == 0)
    20         printf("FIFO deleted!
    ");
    21     else
    22     {
    23         printf("Delete FIFO fails!
    ");
    24         return 1;
    25     }
    26 
    27     return 0;
    28 }
    View Code

    FIFO的打开规则

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

      FIFO的打开规则:

      如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。

      如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

    FIFO的读写规则

      从FIFO中读取数据:

      约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。

    • 如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
    • 对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
    • 读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
    • 如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。

      注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

      向FIFO中写入数据:

      约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。

      对于设置了阻塞标志的写操作:

    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
    • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

      对于没有设置阻塞标志的写操作:

    • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

      关于FIFO读写规则验证程序实例请参照博文Linux环境进程间通信(一)之管道

    与管道异同 

      管道常用于两个方面:(1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。

      FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。

      管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输"协议",采用传播具有特定意义的消息。

      要灵活应用管道及FIFO,理解它们的读写规则是关键。

    参考资料

      APUE

      Linux环境进程间通信(一)之管道

      进程间通信

      深刻理解Linux进程间通信(IPC)

      Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

  • 相关阅读:
    linux进程管理类
    linux关机重启指令
    linux分区及磁盘挂载
    linux的运行级别
    property
    访问限制机制
    类的组合与封装
    继承与派生
    logging模块
    re模块
  • 原文地址:https://www.cnblogs.com/xiehongfeng100/p/4756961.html
Copyright © 2020-2023  润新知