• Linux操作系统~匿名管道和命名管道的使用及其原理分析


    目录

    1.匿名管道

    (1).匿名管道的原理

    (2).pipe接口的使用

    如果只写不读(求管道的大小)

    (3).匿名管道五个特点

    (4).匿名管道的四种情况

    3.命名管道

    (1).命名管道的原理/实质

    (2).在命令行中创建使用匿名管道

    (3).代码中创建使用命名管道

    1).同时编译多个文件,makefile

    2).mkfifo()函数

    3).e.g.

    (4).命名管道注意事项

    4.匿名管道和命名管道的区别与联系


    1.匿名管道

    (1).匿名管道的原理

            1.创建子进程的时候,PCB也是要自己有一份的,同样里面的files_struct也是要自己独立有一份的(这些都属于进程的数据结构,进程是具有独立性的)

            2.而通过文件描述符找到的struct_file则是不需要给子进程拷贝一份的,因为这个是属于文件的,和创建进程没有关系。

            3.调用write方法的时候,是系统调用,会先将数据放在文件的内核缓冲区(不是C语言层面的缓冲区),底层定期的将缓冲区中的内容写到磁盘中。

            可以看出,父进程和子进程可以看到的公共区域是struct_file,这里也就可以进行进程间通信了。struct_file里面有文件缓冲区,一个进程往缓冲区里面写数据,一个进程往缓冲区里面读数据,这就是匿名管道的原理,一种基于文件的通信方式


    (2).pipe接口的使用

    pipefd[2]:是一个输出性参数!我们想通过这个参数读取到打开的两个fd/

    int pipe(int pipefd[ 2]);

    pipe[0]表示读取端,pipe[1]表示写入端

    e.g.:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. int pipe_fd[2] = {0};
    9. if (pipe(pipe_fd) < 0)
    10. {
    11. perror("pipe");
    12. return 1;
    13. }
    14. printf("%d, %d\n", pipe_fd[0], pipe_fd[1]); // 输出3,4
    15. pid_t id = fork();
    16. if (id < 0)
    17. {
    18. perror("fork");
    19. return 2;
    20. }
    21. else if (id == 0)
    22. {
    23. // 让父进程读取,子进程写入
    24. close(pipe_fd[0]); // 关掉读取端
    25. char c = 'x';
    26. int count = 0;
    27. while (1)
    28. {
    29. write(pipe_fd[1], &c, 1);
    30. // sleep(1);
    31. count++;
    32. printf("write: %d\n", count);
    33. }
    34. close(pipe_fd[1]);
    35. exit(0);
    36. }
    37. else
    38. {
    39. // 父进程读
    40. close(pipe_fd[1]); // 关掉写入端
    41. char buffer[64];
    42. while (1)
    43. {
    44. // sleep(100);让父进程不读,用于模拟计算管道的大小
    45. sleep(1);
    46. size_t size = read(pipe_fd[0], buffer, sizeof(buffer) - 1); // 默认都是0,最后留一个位置作为\0
    47. // 如果read返回值为0,意味着子进程关闭文件描述符了,相当于读到文件结尾了
    48. if (size > 0)
    49. {
    50. buffer[size] = 0;
    51. printf("parent get messge from child# %s\n", buffer);
    52. }
    53. else if (size == 0)
    54. {
    55. printf("pipe file close, child quit!\n");
    56. break;
    57. }
    58. else
    59. {
    60. break;
    61. }
    62. }
    63. int status = 0;
    64. if (waitpid(id, &status, 0) > 0)
    65. {
    66. printf("child quit, wait success!, sig: %d\n", status & 0x7F);
    67. }
    68. close(pipe_fd[0]);
    69. }
    70. return 0;
    71. }

     这里是子进程写入,父进程读取,子进程每1s写入一个x,父进程每1s读取一个x。

    如果只写不读(求管道的大小)

    所以如果我们需要知道管道的大小,写一个程序算一下就行。

    这里管道的大小是64KB

            方法:将之前代码中的父进程(读取端)sleep(100),让自进程一直写,最后我们会发现它停止在65536,因为管道满了,写端被阻塞,需要等读端来读

    (3).匿名管道五个特点

            1.管道是一个只能单向通信的通信信道,这里父进程有一个读端和一个写端,这样子进程继承下去才可以读写,如果只有一个读端,那么子进程也就只能读了。因为是单向通信,所以父进程一次只能开一个写端或者一个读端,子进程也是。(要么父写子读,要么子写父读)

            2.管道是面向字节流的(也就是读取数据的时候只有字节的概念,你让我读多少个字节就读多少个,具体哪部分有用等读进来到用户层面处理)

            3.管道自带同步机制(写端写满了管道,就不写了,等对方读;读端读完了管道内容,就不读了,等对方写)  

    原子性写入/读取(如果写满之后,只读一点点数据,是不能唤醒对方来写的,要读一定的数据量以后,才能唤醒对方来写,这个数据量就是PIPE_BUF。同理只写一点点数据,也无法唤醒对方来读)

    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
    • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

            4.仅限于父子间通信(具有血缘关系的进程进行进程间通信)

          5.管道的生命周期是随进程的(进程结束后,管道作为文件会被OS自动关闭,即使没有close)


    (4).匿名管道的四种情况

    管道的四种情况:

    • 当没有数据可读时

    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止

    //O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。(非阻塞式)

    • 当管道满的时候

    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据

    //O_NONBLOCK enable:调用返回-1,errno值为EAGAIN(非阻塞式)

    • 如果所有管道写端对应的文件描述符被关闭,则read返回0,表明读到文件结尾
    • 如果所有管道读端对应的文件描述符被关闭,则write操作会让OS给目标进程发送信号SIGPIPE,终止写入进程

    ----(我们可以验证,父进程读,子进程写入;父进程关闭读端,此时操作系统会向进行写操作的进程发出SIGPIPE信号,从而使子进程异常终止。此时父进程通过waitpid获取子进程的status,从status中获取退出码和退出信号)


    3.命名管道

    为了解决匿名管道只能父子通信,引入了命名管道。

    命名管道让两个毫不相关的进程进行通信。

    (1).命名管道的原理/实质

    进程是具有独立性的

    ->进程通信的成本其实比较高

    ->必须先解决一个问题

    ->让不同的进程先要看到同一份资源(内存文件,内存,队列)[一定需要OS来提供]

    ->pipe本质:是通过子进程继承父进程资源的特性,达到一个让不同的进程看到同一份资源

            如果是两个不相关的进程,我们要通信可以创建一个文件来做到两个进程间的通信(我们标识一个磁盘文件的时候,可以用路径/文件名来标识,因为操作系统的文件结构是树形结构,每个叶子结点向上追溯到根节点的路径是唯一的)

            A进程把内容写入文件temp.txt,B进程从temp.txt中读取,这就实现了进程间通信。但是这种方法很慢,我们可以将文件放在内存中,不要放在磁盘中,这样就会更加快速,A和B同时打开这个放在内存中的文件

    所以我们需要一个文件,

    1.它被打开的时候,不会把数据刷新到磁盘里,而是保存在内存中

    2.在内存中也有一个文件名,方便两个进程通过路径+文件名看到这个文件。

            这样的文件就是命名管道(命名的意思是必须有名字,为了让两个进程可以通过同一个路径+文件名找到同一个文件)


    (2).在命令行中创建使用匿名管道

    p表示管道文件

    使用mkfifo创建一个命名管道,会出现一个类型为p的文件,也就是我们创建的命名管道

     

     一个进程往命名管道中写入内容,另一个进程可以从管道文件中读取数据,这里写了一个shell脚本

    while:; do echo "zebra的数据"; sleep 1; done > namedPipe


    (3).代码中创建使用命名管道

    1).同时编译多个文件,makefile

    1. all:client server
    2. client:client.c
    3. gcc -o $@ $^
    4. server:server.c
    5. gcc -o $@ $^
    6. .PHONY:clean
    7. clean:
    8. rm -f client server fifo

     

    2).mkfifo()函数

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

    filename表示管道的文件名(可以加上路径),mode表示创建的管道文件的权限

            1.在server.c里面调用mkfifo创建命名管道文件,权限设置为0666(注意这里需要设置一下umask,否则会受到系统默认umask的影响,默认是002)

    如果创建失败(比如同名管道文件已经存在),就打印出错误信息。

    1. #define MY_FIFO "./fifo"
    2. umask(0);
    3. if(mkfifo(MY_FIFO, 0666) < 0){
    4. perror("mkfifo");
    5. return 1;
    6. }
    7. //只需要文件操作即可,打开文件读
    8. int fd = open(MY_FIFO, O_RDONLY);
    9. if(fd < 0){
    10. perror("open");
    11. return 2;
    12. }

            2.一旦我们有了命名管道。此时,我们只需要让通信双方按照文件操作即可(推荐使用系统调用接口,没有用户层缓冲区的问题)

            3.在client.c里面不需要再创建管道文件,直接打开文件开写就行。

            4.因为命名管道也是基于字节流的,所以实际上,信息传递的时候,是需要通信双方定制“协议的”,这里暂时不考虑协议相关问题。

    3).e.g.

    实现一个让client从键盘读入数据,并向管道中写入内容,让server从命名管道中读取文件内容。

             这里在server里面加了一些处理逻辑,如果server读到的内容是show,就创建子进程执行ls -l命令,如果读到的内容是run,就创建子进程执行sl命令;如果读到的是其他内容,就直接将内容打印出来。

    server.c:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #define MY_FIFO "./fifo"
    11. int main()
    12. {
    13. umask(0);
    14. if(mkfifo(MY_FIFO, 0666) < 0){
    15. perror("mkfifo");
    16. return 1;
    17. }
    18. //只需要文件操作即可
    19. int fd = open(MY_FIFO, O_RDONLY);
    20. if(fd < 0){
    21. perror("open");
    22. return 2;
    23. }
    24. //业务逻辑,可以进行对应的读写了
    25. while(1){
    26. char buffer[64] = {0};
    27. sleep(1);
    28. ssize_t s = read(fd, buffer, sizeof(buffer)-1); //键盘输入的时候,\n也是输入字符的一部分
    29. if(s > 0){
    30. //success
    31. buffer[s] = 0;
    32. if(strcmp(buffer, "show") == 0){ //如果输入show,创建子进程执行ls -l命令
    33. if(fork() == 0){
    34. execl("/usr/bin/ls", "ls", "-l", NULL);
    35. exit(1);
    36. }
    37. waitpid(-1, NULL, 0);
    38. }
    39. else if(strcmp(buffer, "run") == 0){ //如果输入run,创建子进程执行sl命令
    40. if(fork() == 0){
    41. execl("/usr/bin/sl", "sl", NULL);
    42. }
    43. waitpid(-1, NULL, 0);
    44. }
    45. else{
    46. printf("client# %s\n", buffer);
    47. }
    48. }
    49. else if(s == 0){
    50. //peer close
    51. printf("client quit ...\n");
    52. break;
    53. }
    54. else{
    55. //error
    56. perror("read");
    57. break;
    58. }
    59. }
    60. close(fd);
    61. return 0;
    62. }

    client.c:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define MY_FIFO "./fifo"
    8. int main()
    9. {
    10. //用不用在创建fifo?? 我只要获取即可
    11. int fd = open(MY_FIFO, O_WRONLY); //不需要O_CREAT
    12. if(fd < 0){
    13. perror("open");
    14. return 1;
    15. }
    16. //业务逻辑
    17. while(1){
    18. printf("请输入# ");
    19. fflush(stdout);
    20. char buffer[64] = {0};
    21. //先把数据从标准输入拿到我们的client进程内部
    22. ssize_t s = read(0, buffer, sizeof(buffer)-1);
    23. if(s > 0){
    24. // //获取到数据以后,需要把最后一个字符置为\0(‘\0’就是0,这里去掉最后一个字符,并设置为\0)
    25. buffer[s-1] = 0;
    26. printf("%s\n", buffer);
    27. //拿到了数据
    28. write(fd, buffer, strlen(buffer)); //这里不需要-1,因为是写入数据,操作系统不需要最后/0作为标识符,C语言中的字符串才需要
    29. }
    30. }
    31. close(fd);
    32. return 0;
    33. }

    运行结果:

     


    (4).命名管道注意事项

    1.我们ls -l可以看到,我们让server进程sleep,然后client进程不断输入内容,但是此时管道文件的大小并没有发生变化,一直是0,因为命名管道的数据不会刷新到磁盘,在内存中放着。(ls指令查看的是磁盘中的文件信息)        

    2.命名管道必须要有名字,因为它需要保证两个进程可以通过路径+文件名共同看到同一个文件。

            匿名管道pipe不需要名字,打开两个fd,指向一块内核文件缓冲区用于通信,匿名管道没有文件实体,有名管道有文件实体

            因为匿名管道是通过父子继承的方式,子进程的file_struct里面也会有两个fd,分别指向读端和写端,此时我们只需要关闭一个用一个就行。

    3.为什么叫fifo?mkfifo?

    因为管道文件是遵循先进先出的原则的,先写入的数据,会被先读取。


    4.匿名管道和命名管道的区别与联系

    联系:

    1.都是基于文件的通信方式

    2.两者只能用于数据的单向传输,如果要用命名管道实现两个进程间数据的双向传输,建议使用两个单向的命名管道。(todo??)

    3.两者虽然都是基于文件的,但是管道中的内容都不会像普通文件一样保存在磁盘中,读取过的数据就会失效,不能重复读取

    区别:

    1.匿名管道没有文件实体,命名管道有文件实体(保存在内存中,不会写入磁盘)

    2.匿名管道pipe不需要名字,打开两个fd,指向一块内核文件缓冲区用于通信。因为匿名管道是通过父子继承的方式,子进程的file_struct里面也会有两个fd,分别指向读端和写端,此时我们只需要关闭一个用一个就行

    3.匿名管道只能用于有血缘关系,也就是有公共祖先的进程通信。命名管道可以用于任意进程间的通信。


  • 相关阅读:
    洋酒销售系统的设计与实现(附源码+资料+论文+截图+数据库)
    centos8同步时间安装时间校准服务
    【数据科学赛】2023年亚太眼科学会大数据竞赛 #$15000 #阿里天池 #分类
    皮革行业经销商在线系统:简化采购审批流程,轻松管控代理商
    基于 Stacking 的网络恶意加密流量识别方法
    寻找单身狗
    【微信公众号】一、获取 access_token
    Java编码规范--命名风格
    RPC实现简单解析
    SpringBoot - @RequestBody、@ResponseBody的使用场景
  • 原文地址:https://blog.csdn.net/qq_24016309/article/details/128123689