linux进程间通信(IPC) -- 管道(pipe)源码分析

目录

什么是管道 ?

管道的应用

内核角度-管道本质

管道的源码分析

pipefs 初始化

pipe 的创建

pipe_read

pipe_write

总结


什么是管道 ?

所谓管道,是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称 pipe 文件。

向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接收管道输出的接收进程(即读进程),可从管道中接收数据。由于发送进程和接收进程是利用管道进行通信的,故又称管道通信。

为了协调双方的通信,管道通信机制必须提供以下3 方面的协调能力。

  • 互斥。当一个进程正在对 pipe 进行读/写操作时,另一个进程必须等待。
  • 同步。当写(输入)进程把一定数量(如4KB)数据写入 pipe 后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空 pipe 时,也应睡眠等待,直至写进程将数据写入管道后,才将它唤醒。
  • 对方是否存在。只有确定对方已存在时,才能进行通信。

管道的应用

管道是利用pipe()系统调用而不是利用 open()系统调用建立的。pipe()调用的原型是:

int pipe(int fd[2])

我们看到,有两个文件描述符与管道结合在一起,一个文件描述符用于管道的read() 端,一个文件描述符用于管道的 write() 端。

由于一个函数调用不能返回两个值,pipe() 的参数是指向两个元素的整型数组的指针,它将由调用两个所要求的文件描述符填入。

fd[0]元素将含有管道read()端的文件描述符,而fd[1]含有管道write()端的文件描述符。系统可根据fd[0]和fd[1]分别找到对应的file结构。

注意,在pipe的参数中,没有路径名,这表明,创建管道并不像创建文件一样,要为它创建一个目录连接。这样做的好处是,其他现存的进程无法得到该管道的文件描述符,从而不能访问它。

那么,两个进程如何使用一个管道来通信呢?

我们知道,fork()和exec()系统调用可以保证文件描述符的复制品既可供双亲进程使用,也可供它的子女进程使用。也就是说,一个进程用pipe()系统调用创建管道,然后用fork()调用创建一个或多个进程,那么,管道的文件描述符将可供所有这些进程使用。

这里更明确的含义是:一个普通的管道仅可供具有共同祖先的两个进程之间共享,并且这个祖先必须已经建立了供它们使用的管道

注意,在管道中的数据始终以和写数据相同的次序来进行读,这表示lseek()系统调用对管道不起作用。下面给出在两个进程之间设置和使用管道的简单程序:

 
  1. #include <stdio.h>

  2. #include <unistd.h>

  3. #include <sys/types.h>

  4. int main(void)

  5. {

  6. int fd[2], nbytes;

  7. pid_t childpid;

  8. char string[] = "Hello, world!\n";

  9. char readbuffer[80];

  10. pipe(fd);

  11. if((childpid = fork()) == -1)

  12. {

  13. printf("Error:fork");

  14. exit(1);

  15. }

  16. if(childpid == 0) /* 子进程是管道的读进程 */

  17. {

  18. close(fd[1]); /*关闭管道的写端 */

  19. nbytes = read(fd[0], readbuffer, sizeof(readbuffer));

  20. printf("Received string: %s", readbuffer);

  21. exit(0);

  22. }

  23. else /* 父进程是管道的写进程 */

  24. {

  25. close(fd[0]); /*关闭管道的读端 */

  26. write(fd[1], string, strlen(string));

  27. }

  28. return(0);

  29. }

注意,在这个例子中,为什么这两个进程都关闭它所不需的管道端呢?

这是因为写进程完全关闭管道端时,文件结束的条件被正确地传递给读进程。而读进程完全关闭管道端时,写进程无需等待继续写数据。

阻塞读和写分别成为对空和满管道的默认操作,这些默认操作也可以改变,这就需要调用 fcntl() 系统调用,对管道文件描述符设置 O_NONBLOCK 标志可以忽略默认操作:

 
  1. #include <fcntl.h>

  2. fcntl(fd,F_SETFL,O_NONBlOCK);

上述例子如下图:

内核角度-管道本质

管道的源码分析

pipefs 初始化

pipefs是一种简单的、虚拟的文件系统类型,因为它没有对应的物理设备,因此其安装时不需要块设备。大部分文件系统是以模块的形式来实现的。该文件系统相关的代码在fs/pipe.c中:

 
  1. static struct file_system_type pipe_fs_type = {

  2. .name = "pipefs",

  3. .get_sb = pipefs_get_sb,

  4. .kill_sb = kill_anon_super,

  5. };

  6. static int __init init_pipe_fs(void)

  7. {

  8. /*

  9. pipe_fs_type 链接到file_systems 链表可以通过读/proc/filesystems 找到

  10. “pipefs”入口点,在那里,“nodev”标志表示没有设置

  11. FS_REQUIRES_DEV 标志,即该文件系统没有对应的物理设备。

  12. */

  13. int err = register_filesystem(&pipe_fs_type);

  14. if (!err) {

  15. //安装pipefs 文件系统

  16. pipe_mnt = kern_mount(&pipe_fs_type);

  17. if (IS_ERR(pipe_mnt)) {

  18. err = PTR_ERR(pipe_mnt);

  19. unregister_filesystem(&pipe_fs_type);

  20. }

  21. }

  22. return err;

  23. }

  24. //pipefs 文件系统是作为一个模块来安装的

  25. fs_initcall(init_pipe_fs);

  26. module_exit(exit_pipe_fs); //模块卸载函数

上述就是初始化时注册 pipefs文件系统的过程,操作如下:

  • 把 pipe_fs_type 链接到 file_systems 链表中。
  • 创建一个struct vfsmount 结构,然后把该结构赋值给全局变量pipe_mnt。该 vfsmount 结构通过 super_block 与 pipefs 文件系统进行了关联。

pipe 的创建

pipefs 文件系统的入口点就是pipe()系统调用,其内核实现函数为sys_pipe(),而真正的工作是调用 do_pipe() 函数来完成的,其代码在 fs/pipe.c 中:

 
  1. int do_pipe(int *fd)

  2. {

  3. struct file *fw, *fr;

  4. int fdw, fdr;

  5. //创建管道写端的file结构

  6. fw = create_write_pipe();

  7. //在写端的file结构基础上构建读端

  8. fr = create_read_pipe(fw);

  9. //创建读端fd

  10. fdr = get_unused_fd();

  11. //创建写端fd

  12. fdw = get_unused_fd();

  13. //fd 和 file进行关联

  14. fd_install(fdr, fr);

  15. fd_install(fdw, fw);

  16. //返回读写端fd

  17. fd[0] = fdr;

  18. fd[1] = fdw;

  19. ...

  20. return 0;

  21. }

  22. struct file *create_write_pipe(void)

  23. {

  24. ...

  25. struct qstr name = { .name = "" };

  26. //创建 file 结构

  27. f = get_empty_filp();

  28. //创建一个pipe相关的 inode

  29. inode = get_pipe_inode();

  30. //创建一个dentry结构

  31. dentry = d_alloc(pipe_mnt->mnt_sb->s_root, &name);

  32. //inode 和 dentry 相关联

  33. d_instantiate(dentry, inode);

  34. // pipe 和 pipe_mnt 关联

  35. f->f_path.mnt = mntget(pipe_mnt);

  36. //file 与 dentry 相关联

  37. f->f_path.dentry = dentry;

  38. //该file是只写的

  39. f->f_flags = O_WRONLY;

  40. //该pipe的可操作方法

  41. f->f_op = &write_pipe_fops;

  42. ...

  43. return f;

  44. }

  45. struct file *create_read_pipe(struct file *wrf)

  46. {

  47. //创建一个file结构用于读

  48. struct file *f = get_empty_filp();

  49. //file 与 已有的dentry、inode、struct vfsmount 相关联

  50. f->f_path.mnt = mntget(wrf->f_path.mnt);

  51. f->f_path.dentry = dget(wrf->f_path.dentry);

  52. f->f_mapping = wrf->f_path.dentry->d_inode->i_mapping;

  53. //该file是只读的

  54. f->f_flags = O_RDONLY;

  55. f->f_op = &read_pipe_fops;

  56. f->f_mode = FMODE_READ;

  57. f->f_version = 0;

  58. return f;

  59. }

do_pipe()的操作也很简单,操作如下:

  • 创建管道的读写端关联的 file、inode、dentry 结构。
  • 把 fd 和 file 进行联系,并返回给用户关于管道的读写描述符。
  • 创建一个 pipe_inode_info 对象,该结构指向具体的数据页内存缓冲区。后续所有向管道的读写均操作于该数据页内存缓冲区中。

上述pipe的初始化中,创建的 pipe_inode_info 对象记录了 pipe 读写过程中所有的得数据。pipe_inode_info 对象的结构如下

 
  1. struct pipe_inode_info {

  2. wait_queue_head_t wait;//存储等待读写进程的等待队列

  3. /* nrbufs: 写入但还未被读取的数据占用缓冲区的页数

  4. curbuf:当前正在读取环形缓冲区中的页节点

  5. */

  6. unsigned int nrbufs, curbuf;

  7. struct page *tmp_page; //临时缓冲区页面

  8. unsigned int readers; //正在读取pipe的读进程数目

  9. unsigned int writers; //正在写pipe的写进程数目

  10. unsigned int waiting_writers; //等待管道可以写的进程数目

  11. ...

  12. struct inode *inode; //pipe 对应的inode结构

  13. struct pipe_buffer bufs[PIPE_BUFFERS]; //环形缓冲区,每个元素对应一个内存页

  14. };

经过上述的一系列初始化,整个管道的内存结构如下图所示

pipe_read

 
  1. static ssize_t pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t pos)

  2. {

  3. ...

  4. //要读的数据长度

  5. total_len = iov_length(iov, nr_segs);

  6. do_wakeup = 0;

  7. ret = 0;

  8. //读之前先加锁

  9. mutex_lock(&inode->i_mutex);

  10. //循环读数据

  11. for (;;) {

  12. int bufs = pipe->nrbufs;

  13. if (bufs) {

  14. int curbuf = pipe->curbuf;

  15. struct pipe_buffer *buf = pipe->bufs + curbuf;

  16. size_t chars = buf->len;

  17. //待读的数据比要读的数据多,则设置要读的长度

  18. if (chars > total_len)

  19. chars = total_len;

  20. error = ops->confirm(pipe, buf);

  21. if (error) {

  22. if (!ret)

  23. error = ret;

  24. break;

  25. }

  26. atomic = !iov_fault_in_pages_write(iov, chars);

  27. redo:

  28. //把数据拷贝到用户缓冲区

  29. addr = ops->map(pipe, buf, atomic);

  30. error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);

  31. ops->unmap(pipe, buf, addr);

  32. if (unlikely(error)) {

  33. ...

  34. }

  35. ret += chars;

  36. buf->offset += chars;

  37. buf->len -= chars;

  38. //该页的数据全部读完,释放该page

  39. if (!buf->len) {

  40. buf->ops = NULL;

  41. ops->release(pipe, buf);

  42. curbuf = (curbuf + 1) & (PIPE_BUFFERS-1);

  43. pipe->curbuf = curbuf;

  44. pipe->nrbufs = --bufs;

  45. do_wakeup = 1;

  46. }

  47. total_len -= chars;

  48. if (!total_len)

  49. break; /* common path: read succeeded */

  50. } //if (bufs)

  51. //若该页没读完,继续循环读,若该page读完,则读下一个page

  52. if (bufs) /* More to do? */

  53. continue;

  54. if (!pipe->writers)

  55. break;

  56. if (!pipe->waiting_writers) {

  57. if (ret)

  58. break;

  59. //非阻塞跳出循环,不进行休眠

  60. if (filp->f_flags & O_NONBLOCK) {

  61. ret = -EAGAIN;

  62. break;

  63. }

  64. }

  65. if (signal_pending(current)) {

  66. if (!ret)

  67. ret = -ERESTARTSYS;

  68. break;

  69. }

  70. //唤醒写进程

  71. if (do_wakeup) {

  72. wake_up_interruptible_sync(&pipe->wait);

  73. kill_fasync(&pipe->fasync_writers, SIGIO,POLL_OUT);

  74. }

  75. //让出cpu,进行休眠,等待条件唤醒

  76. pipe_wait(pipe);

  77. } // for

  78. mutex_unlock(&inode->i_mutex);

  79. if (do_wakeup) {

  80. wake_up_interruptible_sync(&pipe->wait);

  81. kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);

  82. }

  83. if (ret > 0)

  84. file_accessed(filp);

  85. return ret;

  86. }

读的操作很简单,操作如下:

  • 读之前先锁定内存。
  • 从缓冲区中获取数据页,把页中数据拷贝到用户缓冲区中。
  • 数据全部被读完,则发送信号唤醒写进程,同时读进程让出 CPU 进行休眠。

读取数据的过程如下

pipe_write

 
  1. static ssize_t pipe_write(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t ppos)

  2. {

  3. ...

  4. //需要写入数据的长度

  5. total_len = iov_length(iov, nr_segs);

  6. do_wakeup = 0;

  7. mutex_lock(&inode->i_mutex);

  8. //管道读端已关闭,返回SIGPIPE信号

  9. if (!pipe->readers) {

  10. send_sig(SIGPIPE, current, 0);

  11. ret = -EPIPE;

  12. goto out;

  13. }

  14. /* We try to merge small writes

  15. curbuf:当前的pipe缓冲节点

  16. nrbufs:非空的pipe缓冲节点数目

  17. buffers:buf缓冲区总数目

  18. buf->offset:页内可用数据的偏移量

  19. buf->len :可用数据的长度

  20. buf->offset + buf->len :页内可以往有效数据后追加数据的下标

  21. */

  22. //获取完整页之外的数量

  23. chars = total_len & (PAGE_SIZE-1); /* size of the last buffer */

  24. if (pipe->nrbufs && chars != 0) {

  25. //获取下一个可用的缓冲区,比如缓冲区是0~5, 有效数据起始buff是3,有效数据是4,那么存储buff数据依次为3,4,5,0,下一个可用的buff为1 ((3+4-1)/5)

  26. int lastbuf = (pipe->curbuf + pipe->nrbufs - 1) & (PIPE_BUFFERS-1);

  27. //获取下一个可用的buff,lastbuf为获取到的索引

  28. struct pipe_buffer *buf = pipe->bufs + lastbuf;

  29. const struct pipe_buf_operations *ops = buf->ops;

  30. /*offset: page中数据的偏移量。len:page中数据的长度

  31. 目前数据在page中的有效起始地址 + 有效数据长度 = 下一个可存放数据的地址。

  32. 管道是从前往后读的,并没规定读写大小,有可能只读取了page的前一部分,中间部分尚未读取,但是写的时候必须从中间有效数据后继续写

  33. */

  34. int offset = buf->offset + buf->len;

  35. //当前需要写入的数据 + 已有的数据若没有超过PAGE_SIZE大小,则拷贝到page中

  36. if (ops->can_merge && offset + chars <= PAGE_SIZE) {

  37. ...

  38. redo1:

  39. addr = ops->map(pipe, buf, atomic);

  40. //将用户数据拷贝到page中

  41. error = pipe_iov_copy_from_user(offset + addr, iov, chars, atomic);

  42. ops->unmap(pipe, buf, addr);

  43. ret = error;

  44. do_wakeup = 1;

  45. if (error) {

  46. ...

  47. }

  48. //更新有效数据

  49. buf->len += chars;

  50. total_len -= chars;

  51. ret = chars;

  52. //全拷贝完则跳出

  53. if (!total_len)

  54. goto out;

  55. }

  56. }

  57. for (;;) {

  58. int bufs;

  59. if (!pipe->readers) {

  60. ...

  61. }

  62. /*获取管道还有多少有效的buffer缓冲区*/

  63. bufs = pipe->nrbufs;

  64. if (bufs < PIPE_BUFFERS) { //有效的bufs小于缓冲区总数

  65. int newbuf = (pipe->curbuf + bufs) & (PIPE_BUFFERS-1); //获取下一个可用的buf

  66. struct pipe_buffer *buf = pipe->bufs + newbuf;

  67. struct page *page = pipe->tmp_page;

  68. if (!page) {

  69. page = alloc_page(GFP_HIGHUSER);

  70. if (unlikely(!page)) {

  71. ...

  72. }

  73. pipe->tmp_page = page;

  74. }

  75. do_wakeup = 1;

  76. chars = PAGE_SIZE;

  77. if (chars > total_len)

  78. chars = total_len;

  79. iov_fault_in_pages_read(iov, chars);

  80. redo2:

  81. error = pipe_iov_copy_from_user(src, iov, chars, atomic);

  82. ret += chars;

  83. //更新有效数据

  84. buf->page = page;

  85. buf->ops = &anon_pipe_buf_ops;

  86. buf->offset = 0;

  87. buf->len = chars;

  88. pipe->nrbufs = ++bufs;

  89. pipe->tmp_page = NULL;

  90. total_len -= chars;

  91. //数据写完,跳出循环结束

  92. if (!total_len)

  93. break;

  94. }

  95. //还有可用的缓冲区,继续写

  96. if (bufs < PIPE_BUFFERS)

  97. continue;

  98. //缓冲区全部写完了,则判断是否需要阻塞休眠

  99. if (filp->f_flags & O_NONBLOCK) {

  100. if (!ret)

  101. ret = -EAGAIN;

  102. break;

  103. }

  104. if (signal_pending(current)) {

  105. if (!ret)

  106. ret = -ERESTARTSYS;

  107. break;

  108. }

  109. //唤醒读进程

  110. if (do_wakeup) {

  111. wake_up_interruptible_sync(&pipe->wait);

  112. kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);

  113. do_wakeup = 0;

  114. }

  115. //写进程休眠

  116. pipe->waiting_writers++;

  117. pipe_wait(pipe);//将当前任务加入到等待列表,释放锁,让出CPU

  118. pipe->waiting_writers--;

  119. }

  120. out:

  121. ...

  122. return ret;

  123. }

管道写操作流程如下:

  • 写之前先锁定内存。
  • 获取可写的缓冲区,若可写则循环写。
  • 若不可写,则唤醒读进程,同时写进程进行休眠,让出 CPU。

往管道中写数据的过程如下

管道中最重要的2个方法就是管道的读写。从上述的分析来看,读写进程共同操作内核中的数据缓冲区,若有缓冲区可写,则进程往缓冲区中写,若条件不允许写,则进程休眠让出 CPU。读操作同理。

从上述管道读写操作可知,父子进程之所以能够通过 pipe 进行通信,是因为在内核中共同指向了同一个pipe_inode_info 对象,共同操作同一个内存页。

总结

  1. 管道也称无名管道,是 UNIX 系统中进程间通信(IPC)中的一种。
  2. 管道由于是无名管道,因此只能在有亲缘关系的进程间使用。
  3. 管道不是普通的文件,它是基于内存的。
  4. 管道属于半双工,数据只能从一方流向另一方,也即数据只能从一端写,从另一端读。
  5. 管道中读数据是一次性的操作,数据读取后就会释放空间,让出空间供更多的数据写。
  6. 管道写数据遵循先入先出的原则。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值