• Linux 进程间通信(一)(经典IPC:管道、FIFO)


    管道

    管道是Unix系统IPC的最古老方式,有两种局限性:

    (1)   历史上它们是半双工的(即数据只能在一个方向上流动),虽然现在某些系统提供了全双工管道,但是为了可移植性,不要抱有绝对的全双工假设。

    (2)   管道只能在具有公共祖先的两个进程之间使用(一般都是用于父子进程之间)。

    管道是通过调用pipe函数创建的:

    #include <unistd.h>

    int pipe(int fd[2]);

    返回值:成功,返回0;失败,返回-1

    说明:

    fd返回两个文件描述符:fd[0]用于读,fd[1]用于写,fd[1]的输出刚好是fd[0]的输入。

    即shell为每一条命令单独创建一个进程,然后管道将前一条命令的标准输出与后一条命令的标准输入相连接。

    注:

    POSIX.1允许实现支持全双工管道,对于这些实现,fd[0]和fd[1]以读/写方式打开。

    如下给出了两种描绘半双工管道的方法,左图中管道的两端在一个进程中相互连接,右图中则强调数据需要通过内核在管道中流动:

     

    管道通常在单个进程中没有太大用处,下图显示了父子进程之间的管道:进程先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC管道

     

    fork之后具体要做什么取决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭管道的写端(fd[1]):

     

    当管道的一端被关闭后,下列两条规则起作用:

    (1)   当read一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。

    (2)   当write一个读端已被关闭的管道时,则产生信号SIGPIPE,如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE。

     

    如下为管道程序实例:

    实例一:创建一个从父进程到子进程的管道,并且父进程经由该管道向子进程传送数据:

     1 [root@benxintuzi ipc]# cat pipe1.c
     2 #include <unistd.h>
     3 #include <stdio.h>
     4 
     5 #define MAXLINE 1024
     6 
     7 int main(void)
     8 {
     9         int             n;
    10         int             fd[2];
    11         pid_t   pid;
    12         char    line[MAXLINE];
    13 
    14         if (pipe(fd) < 0)
    15                 printf("pipe error
    ");
    16         if ((pid = fork()) < 0) {
    17                 printf("fork error
    ");
    18         } else if (pid > 0) {           /* parent */
    19                 close(fd[0]);
    20                 write(fd[1], "hello world
    ", 12);              /* write data to fd[1] */
    21         } else {                                        /* child */
    22                 close(fd[1]);
    23                 n = read(fd[0], line, MAXLINE);                 /* read data from fd[0] */
    24                 write(STDOUT_FILENO, line, n);                  /* write data to standard output */
    25         }
    26 
    27         return (0);
    28 }
    29 
    30 [root@benxintuzi ipc]# ./pipe1
    31 [root@benxintuzi ipc]# hello world
    32 
    33 [root@benxintuzi ipc]#
    View Code

     

    实例二:编写一个程序,其功能是每次一页地显示已产生的输出。为了避免先将所有数据写到一个临时文件中,然后再调用系统中有关程序显示该文件,我们希望通过管道将输出直接送到分页程序。为此,先创建一个管道,fork一个子进程,使子进程的标准输入成为管道的读端,然后调用exec,执行分页程序:(说明点:1. 在调用fork之前,先创建一个管道。调用fork之后,父进程关闭读端,子进程关闭写端,然后子进程调用dup2,使其标准输入成为管道的读端。当执行分页程序时,其标准输入就是管道的读端了; 2. 我们使用环境变量PAGER来获得用户分页程序名,如果没有成功,则使用系统默认值,这是环境变量的常见用法。)

      1 [root@benxintuzi ipc]# cat pipe2.c
      2 #include <unistd.h>
      3 #include <sys/wait.h>
      4 #include <stdio.h>
      5 
      6 #define DEF_PAGER       "/bin/more"             /* default pager program */
      7 #define MAXLINE 1024
      8 
      9 int main(int argc, char *argv[])
     10 {
     11         int             n;
     12         int             fd[2];
     13         pid_t   pid;
     14         char    *pager, *argv0;
     15         char    line[MAXLINE];
     16         FILE    *fp;
     17 
     18         if (argc != 2)
     19         {
     20                 printf("usage: a.out <pathname>
    ");
     21                 return (-1);
     22         }
     23 
     24         if ((fp = fopen(argv[1], "r")) == NULL)
     25                 printf("can't open %s
    ", argv[1]);
     26         if (pipe(fd) < 0)
     27                 printf("pipe error
    ");
     28 
     29         if ((pid = fork()) < 0) {
     30                 printf("fork error
    ");
     31         } else if (pid > 0) {                                             /* parent */
     32                 close(fd[0]);           /* close read end */
     33 
     34                 /* parent copies argv[1] to pipe */
     35                 while (fgets(line, MAXLINE, fp) != NULL) {
     36                         n = strlen(line);
     37                         if (write(fd[1], line, n) != n)
     38                                 printf("write error to pipe
    ");
     39                 }
     40                 if (ferror(fp))
     41                         printf("fgets error
    ");
     42 
     43                 close(fd[1]);   /* close write end of pipe for reader */
     44 
     45                 if (waitpid(pid, NULL, 0) < 0)
     46                         printf("waitpid error
    ");
     47                 return (0);
     48         } else {                                                          /* child */
     49                 close(fd[1]);   /* close write end */
     50                 if (fd[0] != STDIN_FILENO) {
     51                         if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)
     52                                 printf("dup2 error to stdin
    ");
     53                         close(fd[0]);   /* don't need this after dup2 */
     54                 }
     55 
     56                 /* get arguments for execl() */
     57                 if ((pager = getenv("PAGER")) == NULL)
     58                         pager = DEF_PAGER;
     59                 if ((argv0 = strrchr(pager, '/')) != NULL)
     60                         argv0++;                /* step past rightmost slash */
     61                 else
     62                         argv0 = pager;  /* no slash in pager */
     63 
     64                 if (execl(pager, argv0, (char *)0) < 0)
     65                         printf("execl error for %s
    ", pager);
     66         }
     67         return (0);
     68 }
     69 [root@benxintuzi ipc]# ./pipe2 pipe2.c
     70 #include <unistd.h>
     71 #include <sys/wait.h>
     72 #include <stdio.h>
     73 
     74 #define DEF_PAGER       "/bin/more"             /* default pager program */
     75 #define MAXLINE 1024
     76 
     77 int main(int argc, char *argv[])
     78 {
     79         int             n;
     80         int             fd[2];
     81         pid_t   pid;
     82         char    *pager, *argv0;
     83         char    line[MAXLINE];
     84         FILE    *fp;
     85 
     86         if (argc != 2)
     87         {
     88                 printf("usage: a.out <pathname>
    ");
     89                 return (-1);
     90         }
     91 
     92         if ((fp = fopen(argv[1], "r")) == NULL)
     93                 printf("can't open %s
    ", argv[1]);
     94         if (pipe(fd) < 0)
     95                 printf("pipe error
    ");
     96 
     97         if ((pid = fork()) < 0) {
     98                 printf("fork error
    ");
     99         } else if (pid > 0) {                                             /* parent */
    100                 close(fd[0]);           /* close read end */
    101 
    102                 /* parent copies argv[1] to pipe */
    103                 while (fgets(line, MAXLINE, fp) != NULL) {
    104 --More--
    View Code

     

    实例三:父子进程同步函数的管道实现:TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT、WAIT_CHILD:(说明点:父进程在调用TELL_CHILD时,经由上一个管道写一个字符“p”,子进程在调用TELL_PARENT时,经由下一个管道写一个字符“c”,相应的WAIT_XXX函数调用read读一个字符,没有读到字符时则阻塞)

      

     1 static int    pfd1[2], pfd2[2];
     2 
     3 void TELL_WAIT(void)
     4 {
     5     if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
     6         printf("pipe error
    ");
     7 }
     8 
     9 void TELL_PARENT(pid_t pid)
    10 {
    11     if (write(pfd2[1], "c", 1) != 1)
    12         printf("write error
    ");
    13 }
    14 
    15 void WAIT_PARENT(void)
    16 {
    17     char    c;
    18 
    19     if (read(pfd1[0], &c, 1) != 1)
    20         printf("read error
    ");
    21 
    22     if (c != 'p')
    23     {
    24         printf("WAIT_PARENT: incorrect data
    ");
    25         return ;
    26     }
    27 
    28 }
    29 
    30 void TELL_CHILD(pid_t pid)
    31 {
    32     if (write(pfd1[1], "p", 1) != 1)
    33         printf("write error
    ");
    34 }
    35 
    36 void WAIT_CHILD(void)
    37 {
    38     char    c;
    39 
    40     if (read(pfd2[0], &c, 1) != 1)
    41         printf("read error
    ");
    42 
    43     if (c != 'c')
    44     {
    45         printf("WAIT_CHILD: incorrect data
    ");
    46         return ;
    47     }
    48 }
    View Code

    常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准I/O库提供了两个函数popen和pclose。这两个函数的功能是:创建一个管道,fork一个子进程,关闭未使用的管道端,然后执行一个shell运行命令,等待命令终止(使用popen可以减少代码编写量)。

    #include <stdio.h>

    FILE* popen(const char* cmdstring, const char* type);

    返回值:成功,返回文件指针;失败,返回NULL

    int pclose(FILE* fp);

    返回值:成功,返回cmdstring的终止状态;失败,返回-1

    说明:

    函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。如果type是r,则文件指针连接到cmdstring的标准输出;如果type是w,则文件指针连接到cmdstring的标准输入,见下图:

     

    pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。

    cmdstring由Bourne shell以下列方式执行:sh –c cmdstring

     shell命令${PAGER:-more}的意思是:如果shell变量PAGER已经定义,且其值非空,则使用其值,否则使用字符串more。利用popen函数重写实例二

     1 [root@benxintuzi ipc]# cat pipe3.c
     2 #include <stdio.h>
     3 #include <sys/wait.h>
     4 
     5 #define MAXLINE 1024
     6 #define PAGER   "${PAGER:-more}" /* environment variable, or default */
     7 
     8 int main(int argc, char *argv[])
     9 {
    10         char    line[MAXLINE];
    11         FILE    *fpin, *fpout;
    12 
    13         if (argc != 2)
    14         {
    15                 printf("usage: a.out <pathname>
    ");
    16                 return (-1);
    17         }
    18 
    19         if ((fpin = fopen(argv[1], "r")) == NULL)
    20                 printf("can't open %s
    ", argv[1]);
    21 
    22         if ((fpout = popen(PAGER, "w")) == NULL)
    23                 printf("popen error
    ");
    24 
    25         /* copy argv[1] to pager */
    26         while (fgets(line, MAXLINE, fpin) != NULL) {
    27                 if (fputs(line, fpout) == EOF)
    28                         printf("fputs error to pipe
    ");
    29         }
    30         if (ferror(fpin))
    31                 printf("fgets error
    ");
    32         if (pclose(fpout) == -1)
    33                 printf("pclose error
    ");
    34 
    35         return (0);
    36 }
    37 [root@benxintuzi ipc]# ./pipe3 pipe2.c
    38 #include <unistd.h>
    39 #include <sys/wait.h>
    40 #include <stdio.h>
    41 
    42 #define DEF_PAGER       "/bin/more"             /* default pager program */
    43 #define MAXLINE 1024
    44 
    45 int main(int argc, char *argv[])
    46 {
    47         int             n;
    48         int             fd[2];
    49         pid_t   pid;
    50         char    *pager, *argv0;
    51         char    line[MAXLINE];
    52         FILE    *fp;
    53 
    54         if (argc != 2)
    55         {
    56                 printf("usage: a.out <pathname>
    ");
    57                 return (-1);
    58         }
    59 
    60         if ((fp = fopen(argv[1], "r")) == NULL)
    61                 printf("can't open %s
    ", argv[1]);
    62         if (pipe(fd) < 0)
    63                 printf("pipe error
    ");
    64 
    65         if ((pid = fork()) < 0) {
    66                 printf("fork error
    ");
    67         } else if (pid > 0) {                                             /* parent */
    68                 close(fd[0]);           /* close read end */
    69 
    70                 /* parent copies argv[1] to pipe */
    71                 while (fgets(line, MAXLINE, fp) != NULL) {
    72 --More--
    View Code

     

    协同进程:

    当一个进程既要产生某个程序的输入,又读取该程序的输出时,它就变成了协同进程(coprocess)。协同进程通常在shell后台运行,其标准输入和标准输出通过管道连接到另一个程序。popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道,而协同进程则有连接到另一个进程的两个单向管道:一个连接到其标准输入,另一个则来自其标准输出。

    实例:从标准输入读取两个数,计算它们的和,然后将和写至其标准输出。

     1 [root@benxintuzi ipc]# cat coprocess.c
     2 #include <unistd.h>
     3 #include <stdio.h>
     4 
     5 #define MAXLINE 1024
     6 
     7 int main(void)
     8 {
     9         int             n, int1, int2;
    10         char    line[MAXLINE];
    11 
    12         while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
    13                 line[n] = 0;            /* null terminate */
    14                 if (sscanf(line, "%d%d", &int1, &int2) == 2) {
    15                         sprintf(line, "%d
    ", int1 + int2);
    16                         n = strlen(line);
    17                         if (write(STDOUT_FILENO, line, n) != n)
    18                                 printf("write error
    ");
    19                 } else {
    20                         if (write(STDOUT_FILENO, "invalid args
    ", 13) != 13)
    21                                 printf("write error
    ");
    22                 }
    23         }
    24 
    25         return (0);
    26 }
    27 
    28 [root@benxintuzi ipc]# ./coprocess
    29 1 2
    30 3
    31 10 20
    32 30
    33 100 200
    34 300
    35 ^C
    36 [root@benxintuzi ipc]#
    View Code

     

    实例:将上述程序编译成为add2协同进程,然后下列程序创建了两个管道,父进程、子进程各自关闭了它们不需要的管道端,必须使用两个管道:一个用作协同进程的标准输入,另一个用作它的标准输出。然后子进程调用dup2使管道描述符移至其标准输入和标准输出,最后调用了excel执行add2:

     1 [root@benxintuzi ipc]# gcc coprocess.c -o add2
     2 [root@benxintuzi ipc]# cat coprocess2.c
     3 #include <unistd.h>
     4 #include <signal.h>
     5 #include <stdio.h>
     6 
     7 #define MAXLINE 1024
     8 
     9 static void     sig_pipe(int);          /* our signal handler */
    10 
    11 int main(void)
    12 {
    13         int             n, fd1[2], fd2[2];
    14         pid_t   pid;
    15         char    line[MAXLINE];
    16 
    17         if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
    18                 printf("signal error
    ");
    19 
    20         if (pipe(fd1) < 0 || pipe(fd2) < 0)
    21                 printf("pipe error
    ");
    22 
    23         if ((pid = fork()) < 0) {
    24                 printf("fork error
    ");
    25         } else if (pid > 0) {                                             /* parent */
    26                 close(fd1[0]);
    27                 close(fd2[1]);
    28 
    29                 while (fgets(line, MAXLINE, stdin) != NULL) {
    30                         n = strlen(line);
    31                         if (write(fd1[1], line, n) != n)
    32                                 printf("write error to pipe
    ");
    33                         if ((n = read(fd2[0], line, MAXLINE)) < 0)
    34                                 printf("read error from pipe
    ");
    35                         if (n == 0) {
    36                                 printf("child closed pipe
    ");
    37                                 break;
    38                         }
    39                         line[n] = 0;    /* null terminate */
    40                         if (fputs(line, stdout) == EOF)
    41                                 printf("fputs error
    ");
    42                 }
    43 
    44                 if (ferror(stdin))
    45                         printf("fgets error on stdin
    ");
    46                 exit(0);
    47         } else {                                                          /* child */
    48                 close(fd1[1]);
    49                 close(fd2[0]);
    50                 if (fd1[0] != STDIN_FILENO) {
    51                         if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
    52                                 printf("dup2 error to stdin
    ");
    53                         close(fd1[0]);
    54                 }
    55 
    56                 if (fd2[1] != STDOUT_FILENO) {
    57                         if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
    58                                 printf("dup2 error to stdout
    ");
    59                         close(fd2[1]);
    60                 }
    61                 if (execl("./add2", "add2", (char *)0) < 0)
    62                         printf("execl error
    ");
    63         }
    64         exit(0);
    65 }
    66 
    67 static void sig_pipe(int signo)
    68 {
    69         printf("SIGPIPE caught
    ");
    70         exit(1);
    71 }
    72 
    73 [root@benxintuzi ipc]# ./coprocess2
    74 100 50
    75 150
    76 100 500
    77 600
    View Code

     

    FIFO

    FIFO有时被称为命名管道,未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的祖先进程。但是,通过FIFO,不相关的进程之间也能交换数据。

    使用如下函数创建FIFO:

    #include <sys/stat.h>

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

    int mkfifoat(int fd, const char* path, mode_t mode);

    返回值:成功,返回0;失败,返回-1

    说明:

    mkfifoat与mkfifo相似,像之前其他*at系列函数一样,有3种情形:

    (1)   如果path参数指定了绝对路径名,则fd被忽略,此时mkfifoat和mkfifo一样。

    (2)   如果path参数指定了相对路径名,则fd参数是一个打开目录的有效文件描述符,路径名和目录有关。

    (3)   如果path参数指定了相对路径名,并且fd参数指定了AT_FDCWD,则路径名以当前目录开始,mkfifoat和mkfifo类似。

    当我们使用mkfifo或者mkfifoat函数创建FIFO时,要用open打开,确是,正常的I/O函数(如close、read、write、unlink)都需要FIFO。当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生如下影响:

    (1)   没有指定O_NONBLOCK时,只读open要阻塞到某个其他进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其他进程为读而打开这个FIFO为止。

    (2)   如果指定了P_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开这个FIFO,那么只写open将返回-1,并将errno设置为ENXIO。

    一个给定的FIFO有多个写进程是很常见的,这就意味着,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作。和管道一样,常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量。

    FIFO主要有以下两种用途:

    (1)   shell命令使用FIFO将数据从一条管道传送到另一条管道,无需创建中间临时文件。

    实例:考虑这样一个过程,他需要对一个输入文件进行两次处理,示意图如下:

     

    我们可以使用FIFO和tee命令如下处理:

    mkfifo fifo1

    prog3 < fifo1 &

    prog1 < (输入文件) | tee fifo1 | prog2

    执行流程如下:

     

    (2)   客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。

    实例:有一个服务器进程,它与很多客户进程相关,每个客户进程都可将请求写到一个该服务器进程创建的FIFO中。由于该FIFO有多个写进程,因此客户进程每次发送给服务器的数据长度要小于PIPE_BUF字节,这样就能避免客户进程之间的写交叉。

     

    但是这种类型的FIFO设计有问题,服务器如何回应各个客户进程呢?一种解决方法是,每个客户进程都在其请求中包含它的进程ID,然后服务器进程为每个客户进程创建一个FIFO,所使用的路径名是以客户进程的进程ID为基础的。例如,服务进程可以用名字/tmp/serv1.XXXXX创建FIFO,其中XXXXX被替换成客户进程的进程ID,如下图所示:

     

  • 相关阅读:
    Bellman-Ford 单源最短路径算法
    Prim 最小生成树算法
    Kruskal 最小生成树算法
    Kosaraju 算法检测有向图的强连通性
    Kosaraju 算法查找强连通分支
    不相交集合森林的启发式策略
    Union-Find 检测无向图有无环路算法
    redis的持久化方式RDB和AOF的区别
    Docker -v 对挂载的目录没有权限 Permission denied
    postgresql如何让主键自增
  • 原文地址:https://www.cnblogs.com/benxintuzi/p/4781779.html
Copyright © 2020-2023  润新知