进程间通信---管道

港控/mmm° 2022-05-29 01:52 370阅读 0赞

进程间通信

一般而言,不同的进程看到的那一份公共资源,是由操作系统提供的,任何一个进程的全局变量在另一个进程中都看不到,所以,进程之间要交换数据就需要通过内核,在内核中开辟一块缓冲区,进程一把数据从用户空间拷贝到缓冲区,进程二再从缓冲区把数据读走,内核提供的这种机制就叫做进程间通信 (IPC)。
![这里写图片描述][Image 1]

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程间通信的分类:

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 读写锁
  • 条件变量

今天我们主要来谈谈管道,管道是从一个进程连接到另一个进程的一个数据流

匿名管道

  1. #include <unistd.h>
  2. int pipe(int pipefd[2]);

调用pipe函数时在内核中开辟一块缓冲区,称为管道,创建成功就返回0,创建失败则返回-1。
管道有一个读端一个写端,通过pipefd参数传出给用户程序两个文件描述符,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端

看几张图来理解一下管道的通信过程:

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

![这里写图片描述][Image 1]
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一个管道。

![这里写图片描述][Image 1]
3. 父进程关闭管道写端,子进程关闭管道读端。子进程可以往管道里写,父进程可以从管道里读。管道是用环形队列实现的,数据从写端流入,从读端流出,这样就实现了进程间通信。

![这里写图片描述][Image 1]

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. int main()
  6. {
  7. int fd[2] = {0};
  8. if(pipe(fd)==-1)
  9. {
  10. perror("pipe");
  11. return 1;
  12. }
  13. pid_t pid = fork();
  14. if(pid==-1)
  15. {
  16. perror("fork");
  17. return 2;
  18. }
  19. else if(pid==0)
  20. {
  21. //child write
  22. close(fd[0]);
  23. char *msg="father,I am child";
  24. int i = 5;
  25. while(i--)
  26. {
  27. write(fd[1],msg,strlen(msg));
  28. sleep(1);
  29. }
  30. }
  31. else
  32. {
  33. //father read
  34. close(fd[1]);
  35. char buf[1024];
  36. while(1)
  37. {
  38. ssize_t s = read(fd[0],buf,sizeof(buf));
  39. if(s>0)
  40. {
  41. buf[s]=0;
  42. printf("father: %s\n",buf);
  43. }
  44. else if(s==0)
  45. {
  46. printf("child quit!!!\n");
  47. break;
  48. }
  49. else
  50. {
  51. perror("read");
  52. return 3;
  53. }
  54. }
  55. }
  56. return 0;
  57. }

![这里写图片描述][Image 1]

匿名管道的特点

  • 只能用于具有亲缘关系的进程之间进行通信(常为父子)
  • 管道的生命周期随进程
  • 管道是半双工的,只能进行单向通信
  • 管道自带同步机制
  • 管道是面向字节流的

管道的读写规则

  • 写端的文件描述符关闭,读端一直读
    —–读到文件结尾返回0
  • 写端的文件描述符没关闭,但写端也不写数据,而读端一直读
    —–读端一直等待
  • 写端一直写数据,读端不读,也不关闭它的文件描述符
    —–写端写满后一直等待
  • 写端写数据,而读端的文件描述符关闭
    —–系统直接结束掉写进程(用13信号)

命名管道

匿名管道的一个不足之处是只能用于具有亲缘关系的进程间通信,而命名管道(FIFO)克服了这个缺点。
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。

命名管道的创建

  1. 通过命令创建

    $ mkfifo filename

![这里写图片描述][Image 1]

  1. 通过函数创建

    include

    include

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

创建成功返回0,创建失败返回-1
参数pathname代表的是你创建出的管道名称,mode是这个管道具有的权限

来看例子:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. int main()
  6. {
  7. if(mkfifo("fifo",0644)==-1)
  8. {
  9. perror("mkfifo");
  10. return 1;
  11. }
  12. return 0;
  13. }

![这里写图片描述][Image 1]
命名管道和匿名管道除了打开和创建方式不同,其他语义基本相同


用命名管道实现client/server通信

(实现用户发送消息,服务器接受消息)

直接上代码
server.c

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. #include <string.h>
  8. int main()
  9. {
  10. if(mkfifo("mypipe",0644)==-1)//创建命名管道
  11. {
  12. perror("mkfifo");
  13. return 1;
  14. }
  15. int rfd = 0;
  16. if((rfd = open("mypipe",O_RDONLY))==-1)//以只读方式打开管道
  17. {
  18. perror("open");
  19. return 2;
  20. }
  21. char buf[1024];
  22. while(1)
  23. {
  24. ssize_t s = read(rfd,buf,sizeof(buf)-1);//从管道中读数据
  25. if(s>0)
  26. {
  27. buf[s-1]=0;
  28. printf("client say:%s\n",buf);
  29. }
  30. else if(s==0)
  31. {
  32. printf("client quit!!!\n");
  33. break;
  34. }
  35. else
  36. {
  37. perror("read");
  38. return 3;
  39. }
  40. }
  41. return 0;
  42. }

client.c

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. #include <string.h>
  8. int main()
  9. {
  10. int wfd = 0;
  11. if((wfd = open("mypipe",O_WRONLY))==-1)//以只写方式打开管道
  12. {
  13. perror("open");
  14. return 1;
  15. }
  16. char buf[1024];
  17. while(1)
  18. {
  19. printf("Please Enter:");
  20. //从标准输入读取数据
  21. fflush(stdout);
  22. ssize_t s = read(0,buf,sizeof(buf)-1);
  23. if(s>0)
  24. {
  25. buf[s]=0;
  26. write(wfd,buf,strlen(buf));//将读取到的数据写进管道
  27. }
  28. else if(s==-1)
  29. {
  30. perror("write");
  31. return 2;
  32. }
  33. }
  34. return 0;
  35. }

因为这里有两个文件需要编译,我在这里编写了一个Makefile来管理这两个文件

Makefile:
![这里写图片描述][Image 1]

测试结果:
1.编译通过后,我们先执行server创建出管道,如图:
(再开启一个终端来观察)
![这里写图片描述][Image 1]

2.再执行client开始传输数据
![这里写图片描述][Image 1]

这样我们就通过命名管道实现了两个毫不相关进程间的通信

Linux下管道的容量及管道的数据结构

1.查看管道的容量

![这里写图片描述][Image 1]
2.6标准版本的linux内核,pipe缓冲区是64KB,尽管命令ulimit -a看到管道大小是8块,这个大小是内核设定的管道缓冲区的大小。因为内核动态分配最大是16缓冲条目乘64K。

那么如何查看自己电脑上面管道的大小呢?

  • 通过ulimit -a查看pipe size一次原子性写入8*512bytes=4096bytes
    ![这里写图片描述][Image 1]
  • 查看缓冲条目个数:cat /usr/src/kernels/2.6.32-431.el6.i686/include/linux/pipe_fs_i.h 文件
    ![这里写图片描述][Image 1]

接下来就可以计算自己电脑上管道容量的大小了:16*4096bytes=65536bytes
也就验证了 man 7 pipe中容量的大小:
![这里写图片描述][Image 1]

2.管道的数据结构

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和 VFS 的索引节点inode。通过将两个file结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

  1. //inode结点信息结构
  2. struct inode {
  3. ...
  4. struct pipe_inode_info *i_pipe;
  5. ...
  6. };
  7. //管道缓冲区个数
  8. #define PIPE_BUFFERS (16)
  9. //管道缓存区对象结构
  10. struct pipe_buffer {
  11. struct page *page; //管道缓冲区页框的描述符地址
  12. unsigned int offset, len; //页框内有效数据的当前位置,和有效数据的长度
  13. struct pipe_buf_operations *ops; //管道缓存区方法表的地址
  14. };
  15. //管道信息结构
  16. struct pipe_inode_info {
  17. wait_queue_head_t wait; //管道等待队列
  18. unsigned int nrbufs, curbuf;
  19. //包含待读数据的缓冲区数和包含待读数据的第一个缓冲区的索引
  20. struct pipe_buffer bufs[PIPE_BUFFERS]; //管道缓冲区描述符数组
  21. struct page *tmp_page; //高速缓存区页框指针
  22. unsigned int start; //当前管道缓存区读的位置
  23. unsigned int readers; //读进程的标志,或编号
  24. unsigned int writers; //写进程的标志,或编号
  25. unsigned int waiting_writers; //在等待队列中睡眠的写进程的个数
  26. unsigned int r_counter; //与readers类似,但当等待写入FIFO的进程是使用
  27. unsigned int w_counter; //与writers类似,但当等待写入FIFO的进程时使用
  28. struct fasync_struct *fasync_readers; //用于通过信号进行的异步I/O通知
  29. struct fasync_struct *fasync_writers; //用于通过信号的异步I/O通知
  30. };

[Image 1]:

发表评论

表情:
评论列表 (有 0 条评论,370人围观)

还没有评论,来说两句吧...

相关阅读

    相关 进程通信-管道

          管道是进程间通信方式之一,进程间可以利用管道来进行通信,好比两个地方,相隔了一条河,管道就是这条河上的一座桥,两个地方通过这座桥才得以进行相互访问。 优点:1.管

    相关 进程通信---管道

    进程间通信 一般而言,不同的进程看到的那一份公共资源,是由操作系统提供的,任何一个进程的全局变量在另一个进程中都看不到,所以,进程之间要交换数据就需要通过内核,在内核中开

    相关 管道实现进程通信

    一、无名管道 1、什么是管道? 管道用于相关进程间的通信,相当于一个传递工具; 1、特点 (1)无名管道是半双工的,在管道的一端只能进行读或者是写,二者不可同