10 进程间通信
    进程是资源分配的单元,不同进程(指用户进程 不是内核进程)之间的资源是独立的没有关联的,不能在一个进程中直接访问另一个进程的资源。
    但是进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信IPC
    进程间通信的目的:
    数据传输:一个进程将它的数据发给另一个进程
    通知时间:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时需要通知父进程)。
    资源共享:多个进程之间共享同样的资源,为了做到这一点,需要内核提供互斥和同步机制
    进程控制:有些进程希望完全控制另一个进程的执行(如DeBug进程 GDB进程 控制被GDB调试的那个进程),此时控制进程能个拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
    Linux 进程间通信的方式

    2 Linux多进程开发3 父子进程间匿名管道通信 - 图1


    11匿名管道概述
    匿名管道,它是UNIX系统IPC进程间通信的最古老形式,所有的UNIX系统都支持这种通信机制
    比如 统计一个目录中文件的数目命令 ls | wc -l 为了执行该命令,shell创建了两个进程来分别执行ls和wc

    2 Linux多进程开发3 父子进程间匿名管道通信 - 图2

    ls获取当前目录的文件列表 |叫管道符 wc文件统计个数
    ls进程将文件列表通过管道发给wc进程
    ls默认是将文件列表显示到终端(即 将信息写到fd=1的文件中 对应的是stdout标准输出 输出到终端屏幕),管道有写入端和读取端,当加上|管道符后ls写的文件 fd=1 stdout指向的不再终端屏幕而是管道的写入端,wc同理wc读的文件 fd=0 stdin指向的不再终端屏幕而是管道的读入端。
    fd=0 stdin默认指向终端屏幕 fd=1stdout默认指向的也是终端屏幕

    管道的特点
    管道其实是一个在内核内存中维护的缓冲区,这个缓冲区的大小是有限的,不同操作系统的管道大小不一定相同。
    管道拥有文件的特质:读、写 管道的两端对应了两个文件描述符(写入端文件描述符,读取端文件描述符),匿名管道是没有文件实体的但是有名管道是有文件实体的(但这个文件是不存储数据的),可以按照造作文件的方式对管道进行操作。

    匿名管道只能用于有公共祖先的进程之间比如父进程和子进程,爷爷进程和孙子进程,兄弟进程之间。
    有名管道用于无关系的进程之间。

    一个管道是一个字节流,使用管道时不存在消息或消息边界的概念(就是没有一个数据帧的头尾内容这些概念 就是单纯的字节),从管道读取数据的进程可以读取任意大小的数据块,而不管写进程写入的数据块的大小是多大(读写分离 互不影响)。
    通过管道传递的数据是顺序的,管道实际为环形队列实现 先入先出 读取出来的字节的顺序和它们被写入管道的顺序完全一样。

    在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工(遥控器单向发送 单工 电话通信 双工同时传递消息 可以同时读也可以同时写 也可以一个读一个写 对讲机 半双工 同一时间只能 读或写 不能同时读或同时写 在同一时刻内数据的流动是单向的,但是可以在一个时刻选择数据的流动方向)。
    从管道读数据是一次性操作,数据一旦被读走它就从管道中被抛弃(pop),释放空间以便写更多的数据,在管道中无法使用lseek()来随机访问数据。

    2 Linux多进程开发3 父子进程间匿名管道通信 - 图3


    为什么可以使用管道进行通信
    为什么只有拥有公共祖先的进程可以通过匿名管道通信
    fork后的与祖先进程相关的进程的文件描述符表(在PCB中)是相同的。父进程的3号fd是指向A文件的,4号fd是指向B文件的(在fork前就已经将3,4号fd的指向明确了),fork后的子进程的3号fd也是指向A文件的,4号fd也是指向B文件的。父子进程都可以通过fd操作同一文件,这样其实就可以完成进程间通信了,一个读一个写。匿名管道的原理和操作同一文件实现通信的原理是一样的,比如父进程的5号fd对应管道的写端,子进程的5号fd对应管道的写端,父进程的6号fd对应管道的读端,子进程的6号fd对应管道的读端(fork前的父进程已经将fd5,6的指向明确了),则fork后父子进程可以通过匿名管道通信。

    2 Linux多进程开发3 父子进程间匿名管道通信 - 图4


    (fork后的父子进程文件描述符表是独立的(fork的时候 相当于为子进程初始化一份一摸一样的文件描述符表,但fork后想怎么改都随便),不需要完全相同,根据需要来让父子fd指向需要的文件)
    1、内核缓冲区2、非文件3、不占用磁盘空间4、两部分:读端和写端5、默认是阻塞的6、操作管道进程结束后,自动释放7、半双工通信8、默认缓冲区是 4k
    1、父进程和子进程可以共享打开的文件描述符。
    2、父子进程共享文件描述符的条件:在fork之前打开文件。(两个进程的文件描述符表其实还是独立的,只不过fork子进程的时候给子进程的文件描述符表初始化为与父进程一摸一样)
    3、对于两个完全不相关的进程,文件描述符不能共享。
    4、父子进程文件描述符是共享的,但是关闭的时候可以分别关闭,也可以同时在公有代码中关闭。
    文件表,也就是vnode数据结构,也可以说是内核对象,它里面有个计数器变量,表示有几个进程打开此文件,所以子进程关闭,父进程仍然可以读数据

    2 Linux多进程开发3 父子进程间匿名管道通信 - 图5

    进程A的fd1和fd20这种就是dup类系统调用的结果
    进程A的fd2和进程B的fd2就是fork的结果(fork之后指向相同的文件)
    进程A的fd0和进程B的fd3最终指向同一个inode,这是这两个进程都调用过open的结果,此时两个文件不共享文件内的偏移
    文件偏移是存放在第二个表中的,所以不管是dup还是fork,都是共享同一组偏移,
    左边的是进程级别的,中间的是系统级别的,记录整个系统中打开的文件,inode可以理解成存在硬盘上的文件

    匿名管道的底层实现为环形队列

    2 Linux多进程开发3 父子进程间匿名管道通信 - 图6

    写指针可以写空白处(比如E-U)和读指针已经读过的部分(比如ABCD)(读完了以后就会被写指针的写数据覆盖 所以读管道相当于pop读完以后就没有了)

    创建匿名管道
    #include
    int pipe(int pipefd[2]);
    查看管道缓冲大小
    ulimit - a
    查看管道缓冲大小函数
    #include
    long fpathconf(int fd,int name);

    12父子进程通过匿名管道通信

    1. /*
    2. #include <unistd.h>
    3. int pipe(int pipefd[2]);
    4. 创建一个匿名管道(半双工 同一时间内一段只能写或读)
    5. 用于有共同祖先线程的线程间通信
    6. pipefd 传出参数 返回的管道两端 对应的文件描述符号
    7. [0]为读端 [1]为写端
    8. 写数据到写端 会先存到内核中的缓冲区 直到读端读数据将这个数据取出(取出了就不在管道中了)
    9. 返回 0 成功 -1 失败
    10. 匿名管道默认阻塞 如果管道中没有数据 read阻塞(直到管道中有要求被读的那么多个字节 才不会阻塞 管道内数据被读出)
    11. 管道满了 write阻塞(直到管道被读出要写的那么多个字节 腾出了那么大的空间才能继续写)
    12. */
    13. #include <unistd.h>
    14. #include <sys/types.h>
    15. #include <stdio.h>
    16. #include <stdlib.h>
    17. #include <string.h> //strlen
    18. int main()
    19. {
    20. //父子进程通信
    21. //在fork之前创建匿名管道 创建出来的fd都存在父进程的文件描述符表中
    22. //fork之后 子进程的文件描述符表和父进程一摸一样 包括读写端的fd操作符
    23. //这样对于父子进程来说管道的读写fd指向是一样的 也就可以通过管道进行通信
    24. int pipefd[2];
    25. int ret = pipe(pipefd);
    26. if (ret == -1)
    27. {
    28. perror("pipe:");
    29. exit(-1);
    30. }
    31. //创建子进程
    32. pid_t pid = fork();
    33. //发送一次
    34. // if (pid > 0) //父进程
    35. // {
    36. // // 父进程从读取端读取数据
    37. // char buf[1024] = {0};
    38. // int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据
    39. // //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来
    40. // printf("parent recv :%s,pid : %d \n", buf, getpid());
    41. // }
    42. // else if (pid == 0) //子进程
    43. // {
    44. // //子进程向管道发数据
    45. // char *str = "im child";
    46. // write(pipefd[1], str, strlen(str));
    47. // //如果管道满了 无法再写write是阻塞的 直到read读出的字节数大于要写的字节数 write才能继续写
    48. // // 成功返回str中被写入fd文件的字节数,返回0表示没有数据被写入fd文件中, 出现错误返回 - 1并设置errorno
    49. // }
    50. //循环发送和读 一端只发 一端只收
    51. // if (pid > 0) //父进程
    52. // {
    53. // // 父进程从读取端读取数据
    54. // char buf[1024] = {0};
    55. // while (1)
    56. // {
    57. // int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据
    58. // //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来
    59. // printf("parent recv :%s,pid : %d \n", buf, getpid());
    60. // }
    61. // }
    62. // else if (pid == 0) //子进程
    63. // {
    64. // //子进程向管道发数据
    65. // while (1)
    66. // {
    67. // //每1秒向管道写入一次
    68. // char *str = "im child";
    69. // write(pipefd[1], str, strlen(str));
    70. // sleep(1);
    71. // }
    72. // //如果管道满了 无法再写write是阻塞的 直到read读出的字节数大于要写的字节数 write才能继续写
    73. // // 成功返回str中被写入fd文件的字节数,返回0表示没有数据被写入fd文件中, 出现错误返回 - 1并设置errorno
    74. // }
    75. //一端又发又收 但同一时间内只收或只发
    76. if (pid > 0) //父进程
    77. {
    78. // 父进程从读取端读取数据
    79. char buf[1024] = {0};
    80. while (1)
    81. {
    82. int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据
    83. //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来
    84. printf("parent recv :%s,pid : %d \n", buf, getpid());
    85. memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题
    86. //每1秒向管道写入一次
    87. char *str = "im parent";
    88. write(pipefd[1], str, strlen(str));
    89. sleep(1);
    90. }
    91. }
    92. else if (pid == 0) //子进程
    93. {
    94. //子进程向管道发数据
    95. char buf[1024] = {0};
    96. while (1)
    97. {
    98. //每1秒向管道写入一次
    99. char *str = "im child";
    100. write(pipefd[1], str, strlen(str));
    101. sleep(1);
    102. int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据
    103. //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来
    104. printf("child recv :%s,pid : %d \n", buf, getpid());
    105. memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题
    106. }
    107. }
    108. //如果父子进程都是既要读又要写 就需要把读写的顺序错开 一个读一个写
    109. //不能出现同时读的情况 这样程序就阻塞了跑不起来
    110. //如果将两个线程中的sleep去掉 会出现parent recv:im parent 和 child recv:im child
    111. //出现这种情况的原因是
    112. //父进程写的 被父进程读了(父进程比子进程先读) 子进程写的 被子进程读了(子进程比父进程先读)
    113. //在实际开发中不可能通过sleep来阻止这种现象
    114. //在实际开发中匿名管道的数据流向是单向的(一个线程只读 一个线程只写 关闭另一个fd close(pipefd[0]/[1]))!!!!!!!!
    115. return 0;
    116. }
    117. //ulimit -a 中有pipe isze 512bytes 8 表示共8块一块512字节 总共4k字节 管道最大4k字节
    118. //ulimit -p 修改管道大小
    119. // 查看管道缓冲大小函数
    120. // #include <unistd.h>
    121. // long fpathconf(int fd, int name);
    122. // fd 传入管道一端对应的文件描述符 name:_PC_PIPE_BUF
    123. // 返回fd对应的大小




    13匿名管道通信案例

    1. /*
    2. 实现ps aux | grep root 只显示root进程相关的信息
    3. | 管道符 利用父子进程通信实现
    4. 子进程执行ps aux(execlp 默认将内容输出到标准输出中) 因为子进程的标准输出初始为屏幕
    5. 所以要将 标准输出重定向到父进程(即管道写端),这样父进程才能得到execlp的返回值
    6. 父进程 获取到数据 将数据打印
    7. */
    8. #include <unistd.h>
    9. #include <sys/types.h>
    10. #include <stdio.h>
    11. #include <stdlib.h>
    12. #include <string.h> //strlen
    13. int main()
    14. {
    15. //创建一个匿名管道
    16. int fd[2];
    17. int ret = pipe(fd);
    18. if (ret == -1)
    19. {
    20. perror("pipe:");
    21. exit(-1);
    22. }
    23. //一定要在fork子进程前 创建管道 这样才能使父子进程拥有相同的管道描述符
    24. pid_t pid = fork();
    25. if (pid > 0)
    26. {
    27. //父进程 从管道中读取数据 再直接输出
    28. //父进程只读 关闭管道写端
    29. close(fd[1]);
    30. char buf[1024] = {0};
    31. int len = read(fd[0], buf, sizeof(buf) - 1); //buf中只读sizeof(buf) - 1个字节 因为最后一个字节必须是字符串结束符\0(十进制就是0)
    32. while (len > 0)
    33. {
    34. printf("%s", buf);
    35. memset(buf, 0, sizeof(buf));
    36. len = read(fd[0], buf, sizeof(buf) - 1); //返回值len为读到的字符数
    37. }
    38. wait(NULL); //回收子进程资源
    39. }
    40. else if (pid == 0)
    41. {
    42. //子进程
    43. // 子进程执行ps aux(execlp 默认将内容输出到标准输出中) 因为子进程的标准输出初始为屏幕
    44. // 所以要将 标准输出重定向到父进程(即管道写端) ,这样父进程才能得到execlp的返回值
    45. //子进程只写 关闭管道读端
    46. close(fd[0]);
    47. //首先重定向子进程的标准输出到管道的写端 stdout_fileno->fd[1]
    48. //int dup2(int oldfd, int newfd); //功能和dup类似
    49. //用于重定向文件描述符
    50. //oldfd指向a.txt newfd指向b.txt 在调用函数成功后将newfd->b.txt关闭并且再让newfd指向a.txt
    51. dup2(fd[1], STDOUT_FILENO); //newfd指向a.txt 这样STDOUT_FILENO就指向之前管道写端对应的那个虚拟文件
    52. // int execlp(const char *file, const char *arg, ... / (char *) NULL /);
    53. // 会到环境变量中的路径中查找指定的同名文件 找到了就执行 找不到返回 - 1
    54. // 参数:
    55. // file 可执行文件的文件名 a.out ps 可以不用写绝对路径 直接写文件名
    56. // arg(同execl)
    57. // 这个arg其实就是exec可执行文件的main的传入参数列表
    58. // 第一个arg一般没有作用 一般写的是执行的可执行文件的名称
    59. // 从第二个参数往后才是可执行文件main的真正参数列表
    60. // 这个参数列表一定要以NULL为结尾,这样才知道参数列表读入完毕
    61. // 返回值
    62. execlp("ps", "ps", "aux", NULL);
    63. //exec函数族的函数执行成功后不会返回,因为调用进程的实体,
    64. // 包括代码段数据段堆栈等(所有用户区内容)都被新的可执行文件的内容取代
    65. //但是文件描述符表在内核中并不会被替换
    66. }
    67. else
    68. {
    69. perror("fork:");
    70. exit(-1);
    71. }
    72. // 管道默认大小 4k字节
    73. // 子进程写管道,写满了就会被阻塞(这里的写是指调用write),然后轮到父进程读管道,
    74. // 所以子进程不需要while循环也可以把所有的超过4k的数据写完
    75. // execlp("ps", "ps", NULL)不是write ,它不可能管道写满了还阻塞着
    76. // 超过4096的数据是会分包,还是可以发送的。TLPI书上也说了,”写入不超过4096字节” 的操作保证是原子的
    77. // Linux 2.6.11后,管道的容量是65536,不同的操作系统实现可能不同。所以最后我试了一下,一次write() 确实最多只能写65536字节。记得将管道设置为非阻塞,才能看到无一次法写入大于65536字节的数据。
    78. // 不然,write是阻塞的,发一部分,阻塞一下,发一部分。。。多大都会发送完。
    79. return 0;
    80. }
    81. // #include <stdio.h>
    82. // #include <sys/types.h>
    83. // #include <sys/stat.h>
    84. // #include <fcntl.h>
    85. // #include <unistd.h>
    86. // #include <cstring>
    87. // #include <string>
    88. // #include <stdlib.h>
    89. // #include <limits.h>
    90. // #define BUF_SIZE 65544
    91. // int main()
    92. // {
    93. // int pipeFd[2];
    94. // if (pipe(pipeFd) == -1)
    95. // {
    96. // perror("pipe");
    97. // exit(-1);
    98. // }
    99. // // 设置管道为非阻塞
    100. // for (int i = 0; i < 2; ++i)
    101. // {
    102. // int flags = fcntl(pipeFd[i], F_GETFL);
    103. // if (fcntl(pipeFd[i], F_SETFL, flags | O_NONBLOCK) == -1)
    104. // perror("fcntl");
    105. // }
    106. // int pid = fork();
    107. // if (pid > 0)
    108. // {
    109. // sleep(1); // 确保子进程先写。管道才有数据能读
    110. // // 父进程读
    111. // close(pipeFd[1]);
    112. // char buf[BUF_SIZE + 1] = {0};
    113. // printf("父进程读到了:%ld字节\n", read(pipeFd[0], buf, BUF_SIZE));
    114. // }
    115. // else if (pid == 0)
    116. // {
    117. // // 子进程
    118. // close(pipeFd[0]);
    119. // // in.txt有65544字节的数据
    120. // int fd = open("in.txt", O_RDONLY);
    121. // char buf[BUF_SIZE + 1] = {0};
    122. // printf("从文件中读到: %ld字节\n", read(fd, buf, BUF_SIZE));
    123. // printf("子进程写入了:%ld字节\n", write(pipeFd[1], buf, BUF_SIZE));
    124. // }
    125. // else
    126. // {
    127. // perror("fork");
    128. // exit(-1);
    129. // }
    130. // return 0;
    131. // }



    管道的读写特点以及将管道设置为非阻塞

    使用管道时需要注意以下几种特殊情况(管道为阻塞 在执行阻塞IO的操作时),
    1 所有的指向管道写端的文件描述符 都关闭了(管道写端的引用计数为0)
    父进程创建管道,其内核区中的文件描述符表中新增两个fd一个指向管道读端一个指向管道写端,此时管道写端的引用计数为1。父进程fork出子进程,fork拷贝文件描述符表给子进程,所以子进程的文件描述符表中也有两个fd一个指向读端一个指向写端,此时管道写端的引用计数为2。
    当指向管道写端的fd都close了,此时有进程从管道读端读数据,若此时管道中有剩余数据则读取剩余数据,直到剩余数据被读完后,再次read返回0(读到文件末尾read是返回0的 读出现异常是返回-1的)。

    2 有指向管道写端的文件描述符没有关闭(管道写端的引用计数大于0)并且没有进程向管道写端写数据,此时将管道内数据读空后再继续读是出现read阻塞,直到有进程写了一些数据,read读到返回读到的字节数。

    3 所有的指向管道读端的文件描述符 都关闭了(管道读端的引用计数为0),此时有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常这个信号会导致进程异常终止。

    4 有指向管道读端的文件描述符没有关闭(管道读端的引用计数大于0)并且没有进程从管道读端读数据,此时有进程向管道中写数据,在管道写满后,再次调用写write会导致写进程阻塞,直到管道中有空位write才不会阻塞并返回写入的字节数。


    设置管道非阻塞(设置文件描述符非阻塞)
    设置管道读端非阻塞,在管道内无数据再读 read返回值为-1

    1. /*
    2. 设置管道非阻塞(设置文件描述符fd 非阻塞)
    3. flags = fcntl(fd[],F_GETFL) 获取原来的flag
    4. flags |= O_NONBLOCK;
    5. fcntl(fd[],F_SETFL,flags);//设置新的文件描述符属性
    6. */
    7. #include <unistd.h>
    8. #include <fcntl.h>
    9. #include <sys/types.h>
    10. #include <stdio.h>
    11. #include <stdlib.h>
    12. #include <string.h> //strlen
    13. int main()
    14. {
    15. //父子进程通信
    16. //在fork之前创建匿名管道 创建出来的fd都存在父进程的文件描述符表中
    17. //fork之后 子进程的文件描述符表和父进程一摸一样 包括读写端的fd操作符
    18. //这样对于父子进程来说管道的读写fd指向是一样的 也就可以通过管道进行通信
    19. int pipefd[2];
    20. int ret = pipe(pipefd);
    21. if (ret == -1)
    22. {
    23. perror("pipe:");
    24. exit(-1);
    25. }
    26. //创建子进程
    27. pid_t pid = fork();
    28. //一端又发又收 但同一时间内只收或只发
    29. if (pid > 0) //父进程
    30. {
    31. // 父进程从读取端读取数据
    32. char buf[1024] = {0};
    33. //父进程只读 关闭管道写端
    34. close(pipefd[1]);
    35. // 设置管道非阻塞(设置文件描述符fd 非阻塞)
    36. // 读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定
    37. //设置非阻塞读 当管道中没有要求的那么多的数据时 不会阻塞在read而是继续向下执行
    38. int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
    39. flags |= O_NONBLOCK; //设置文件(管道读端)为非阻塞
    40. fcntl(pipefd[0], F_SETFL, flags); //设置新的文件描述符属性
    41. while (1)
    42. {
    43. int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据
    44. //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来
    45. printf("parent recv :%s,pid : %d \n", buf, getpid());
    46. memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题
    47. // //每1秒向管道写入一次
    48. // char *str = "im parent";
    49. // write(pipefd[1], str, strlen(str));
    50. // sleep(1);
    51. }
    52. }
    53. else if (pid == 0) //子进程
    54. {
    55. //子进程向管道发数据
    56. char buf[1024] = {0};
    57. //子进程只写 关闭管道读端
    58. close(pipefd[0]);
    59. while (1)
    60. {
    61. //每1秒向管道写入一次
    62. char *str = "im child";
    63. write(pipefd[1], str, strlen(str));
    64. sleep(1);
    65. // int recvlen = read(pipefd[0], buf, sizeof(buf)); //从管道读端读数据
    66. // //read是阻塞的 假如管道中没有数据会一直阻塞在read这句 直到管道中有数据被read读出来
    67. // printf("child recv :%s,pid : %d \n", buf, getpid());
    68. // memset(buf, 0, sizeof(buf)); //读后记得清除读的buf 否则会有上次的数据残留导致下次读的数据有问题
    69. }
    70. }
    71. //如果父子进程都是既要读又要写 就需要把读写的顺序错开 一个读一个写
    72. //不能出现同时读的情况 这样程序就阻塞了跑不起来
    73. //如果将两个线程中的sleep去掉 会出现parent recv:im parent 和 child recv:im child
    74. //出现这种情况的原因是
    75. //父进程写的 被父进程读了(父进程比子进程先读) 子进程写的 被子进程读了(子进程比父进程先读)
    76. //在实际开发中不可能通过sleep来阻止这种现象
    77. //在实际开发中匿名管道的数据流向是单向的(一个线程只读 一个线程只写 关闭另一个fd close(pipefd[0]/[1]))!!!!!!!!
    78. return 0;
    79. }