39 | 管道:项目组A完成了,如何交接给项目组B?
下载APP
关闭
渠道合作
推荐作者
39 | 管道:项目组A完成了,如何交接给项目组B?
2019-06-26 刘超 来自北京
《趣谈Linux操作系统》
课程介绍
讲述:刘超
时长12:43大小11.66M
在这一章的第一节里,我们大致讲了管道的使用方式以及相应的命令行。这一节,我们就具体来看一下管道是如何实现的。
我们先来看,我们常用的匿名管道(Anonymous Pipes),也即将多个命令串起来的竖线,背后的原理到底是什么。
上次我们说,它是基于管道的,那管道如何创建呢?管道的创建,需要通过下面这个系统调用。
在这里,我们创建了一个管道 pipe,返回了两个文件描述符,这表示管道的两端,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。
我们来看在内核里面是如何实现的。
在内核中,主要的逻辑在 pipe2 系统调用中。这里面要创建一个数组 files,用来存放管道的两端的打开文件,另一个数组 fd 存放管道的两端的文件描述符。如果调用 __do_pipe_flags 没有错误,那就调用 fd_install,将两个 fd 和两个 struct file 关联起来。这一点和打开一个文件的过程很像了。
我们来看 __do_pipe_flags。这里面调用了 create_pipe_files,然后生成了两个 fd。从这里可以看出,fd[0]是用于读的,fd[1]是用于写的。
创建一个管道,大部分的逻辑其实都是在 create_pipe_files 函数里面实现的。这一章第一节的时候,我们说过,命名管道是创建在文件系统上的。从这里我们可以看出,匿名管道,也是创建在文件系统上的,只不过是一种特殊的文件系统,创建一个特殊的文件,对应一个特殊的 inode,就是这里面的 get_pipe_inode。
从 get_pipe_inode 的实现,我们可以看出,匿名管道来自一个特殊的文件系统 pipefs。这个文件系统被挂载后,我们就得到了 struct vfsmount *pipe_mnt。然后挂载的文件系统的 superblock 就变成了:pipe_mnt->mnt_sb。如果你对文件系统的操作还不熟悉,要返回去复习一下文件系统那一章啊。
我们从 new_inode_pseudo 函数创建一个 inode。这里面开始填写 Inode 的成员,这里和文件系统的很像。这里值得注意的是 struct pipe_inode_info,这个结构里面有个成员是 struct pipe_buffer *bufs。我们可以知道,所谓的匿名管道,其实就是内核里面的一串缓存。
另外一个需要注意的是 pipefifo_fops,将来我们对于文件描述符的操作,在内核里面都是对应这里面的操作。
我们回到 create_pipe_files 函数,创建完了 inode,还需创建一个 dentry 和他对应。dentry 和 inode 对应好了,我们就要开始创建 struct file 对象了。先创建用于写入的,对应的操作为 pipefifo_fops;再创建读取的,对应的操作也为 pipefifo_fops。然后把 private_data 设置为 pipe_inode_info。这样从 struct file 这个层级上,就能直接操作底层的读写操作。
至此,一个匿名管道就创建成功了。如果对于 fd[1]写入,调用的是 pipe_write,向 pipe_buffer 里面写入数据;如果对于 fd[0]的读入,调用的是 pipe_read,也就是从 pipe_buffer 里面读取数据。
但是这个时候,两个文件描述符都是在一个进程里面的,并没有起到进程间通信的作用,怎么样才能使得管道是跨两个进程的呢?还记得创建进程调用的 fork 吗?在这里面,创建的子进程会复制父进程的 struct files_struct,在这里面 fd 的数组会复制一份,但是 fd 指向的 struct file 对于同一个文件还是只有一份,这样就做到了,两个进程各有两个 fd 指向同一个 struct file 的模式,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。
由于管道只能一端写入,另一端读出,所以上面的这种模式会造成混乱,因为父进程和子进程都可以写入,也都可以读出,通常的方法是父进程关闭读取的 fd,只保留写入的 fd,而子进程关闭写入的 fd,只保留读取的 fd,如果需要双向通行,则应该创建两个管道。
一个典型的使用管道在父子进程之间的通信代码如下:
到这里,我们仅仅解析了使用管道进行父子进程之间的通信,但是我们在 shell 里面的不是这样的。在 shell 里面运行 A|B 的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系。
不过,有了上面父子进程之间的管道这个基础,实现 A 和 B 之间的管道就方便多了。
我们首先从 shell 创建子进程 A,然后在 shell 和 A 之间建立一个管道,其中 shell 保留读取端,A 进程保留写入端,然后 shell 再创建子进程 B。这又是一次 fork,所以,shell 里面保留的读取端的 fd 也被复制到了子进程 B 里面。这个时候,相当于 shell 和 B 都保留读取端,只要 shell 主动关闭读取端,就变成了一管道,写入端在 A 进程,读取端在 B 进程。
接下来我们要做的事情就是,将这个管道的两端和输入输出关联起来。这就要用到 dup2 系统调用了。
这个系统调用,将老的文件描述符赋值给新的文件描述符,让 newfd 的值和 oldfd 一样。
我们还是回忆一下,在 files_struct 里面,有这样一个表,下标是 fd,内容指向一个打开的文件 struct file。
在这个表里面,前三项是定下来的,其中第零项 STDIN_FILENO 表示标准输入,第一项 STDOUT_FILENO 表示标准输出,第三项 STDERR_FILENO 表示错误输出。
在 A 进程中,写入端可以做这样的操作:dup2(fd[1],STDOUT_FILENO),将 STDOUT_FILENO(也即第一项)不再指向标准输出,而是指向创建的管道文件,那么以后往标准输出写入的任何东西,都会写入管道文件。
在 B 进程中,读取端可以做这样的操作,dup2(fd[0],STDIN_FILENO),将 STDIN_FILENO 也即第零项不再指向标准输入,而是指向创建的管道文件,那么以后从标准输入读取的任何东西,都来自于管道文件。
至此,我们才将 A|B 的功能完成。
为了模拟 A|B 的情况,我们可以将前面的那一段代码,进一步修改成下面这样:
接下来,我们来看命名管道。我们在讲命令的时候讲过,命名管道需要事先通过命令 mkfifo,进行创建。如果是通过代码创建命名管道,也有一个函数,但是这不是一个系统调用,而是 Glibc 提供的函数。它的定义如下:
Glibc 的 mkfifo 函数会调用 mknodat 系统调用,还记得咱们学字符设备的时候,创建一个字符设备的时候,也是调用的 mknod。这里命名管道也是一个设备,因而我们也用 mknod。
对于 mknod 的解析,我们在字符设备那一节已经解析过了,先是通过 user_path_create 对于这个管道文件创建一个 dentry,然后因为是 S_IFIFO,所以调用 vfs_mknod。由于这个管道文件是创建在一个普通文件系统上的,假设是在 ext4 文件上,于是 vfs_mknod 会调用 ext4_dir_inode_operations 的 mknod,也即会调用 ext4_mknod。
在 ext4_mknod 中,ext4_new_inode_start_handle 会调用 __ext4_new_inode,在 ext4 文件系统上真的创建一个文件,但是会调用 init_special_inode,创建一个内存中特殊的 inode,这个函数我们在字符设备文件中也遇到过,只不过当时 inode 的 i_fop 指向的是 def_chr_fops,这次换成管道文件了,inode 的 i_fop 变成指向 pipefifo_fops,这一点和匿名管道是一样的。
这样,管道文件就创建完毕了。
接下来,要打开这个管道文件,我们还是会调用文件系统的 open 函数。还是沿着文件系统的调用方式,一路调用到 pipefifo_fops 的 open 函数,也就是 fifo_open。
在 fifo_open 里面,创建 pipe_inode_info,这一点和匿名管道也是一样的。这个结构里面有个成员是 struct pipe_buffer *bufs。我们可以知道,所谓的命名管道,其实是也是内核里面的一串缓存。
接下来,对于命名管道的写入,我们还是会调用 pipefifo_fops 的 pipe_write 函数,向 pipe_buffer 里面写入数据。对于命名管道的读入,我们还是会调用 pipefifo_fops 的 pipe_read,也就是从 pipe_buffer 里面读取数据。
总结时刻
无论是匿名管道,还是命名管道,在内核都是一个文件。只要是文件就要有一个 inode。这里我们又用到了特殊 inode、字符设备、块设备,其实都是这种特殊的 inode。
在这种特殊的 inode 里面,file_operations 指向管道特殊的 pipefifo_fops,这个 inode 对应内存里面的缓存。
当我们用文件的 open 函数打开这个管道设备文件的时候,会调用 pipefifo_fops 里面的方法创建 struct file 结构,他的 inode 指向特殊的 inode,也对应内存里面的缓存,file_operations 也指向管道特殊的 pipefifo_fops。
写入一个 pipe 就是从 struct file 结构找到缓存写入,读取一个 pipe 就是从 struct file 结构找到缓存读出。
课堂练习
上面创建匿名管道的程序,你一定要运行一下,然后试着通过 strace 查看自己写的程序的系统调用,以及直接在命令行使用匿名管道的系统调用,做一个比较。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 8
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
38 | 信号(下):项目组A完成了,如何及时通知项目组B?
下一篇
40 | IPC(上):不同项目组之间抢资源,如何协调?
精选留言(15)
- Sharry2019-06-27- 匿名管道: 只能在管道创建进程及其后代之间通信 - 通过 pipe 系统调用创建 - **inode 由特殊的文件系统 pipefs 创建** - **inode 关联的 fos 为 pipefifo_fops** - 命名管道: 通过管道文件名, 可以在任意进程之间通信 - 通过 mkfifo Glibc 库函数创建 - 内部调用 mknodat 系统调用 - **inode 由普通文件系统创建, 真实存在于磁盘中** - **inode 关联的 fos 与匿名管道一致, 为 pipefifo_fops** 老师, 在阅读的过程中产生了一个疑问, 匿名管道创建 inode 使用到的文件系统 pipefs, 也是属于内存文件系统吗? 这个 inode 是否会写到磁盘上呢?展开
作者回复: 不会到磁盘上,假的inode
6 - 海军上校2019-07-29管道代码是不是写错啦?pid=0应该是子进程 close fd1吧 我理解的
作者回复: 不是的,留着fd1
共 3 条评论3 - 八台上2020-06-11请问 管道是特殊的inode 这个inode也会占用磁盘吗 不然机器重启的时候不就没了吗?
作者回复: 管道重启就没了
2 - shangyu2019-12-22请问下老师pipe的缓存大小是多少呢 如果进程a的输出太大会有什么影响呢共 1 条评论2
- 静✨2021-05-21看了三遍 居然看懂了。 这个创建管道的实现真是牛皮1
- xavier2020-12-19好奇想问,在举例中的那个shell创建A和B进程的时候,在fork生成 B进程的时候,是保留了B进程的管道输出的部分,而将shell父进程的管道输出关闭掉了,那如果shell紧接着再去fork一个C进程,岂不是管道的输入和输出都没有了?共 3 条评论2
- 奔跑的码仔2019-09-29strace -f -o file ./npipe 可以看到咱们父、子进程的整个执行过程。 strace -f -o file1 ps -ef | grep systemd只可以看到ps -ef命令的execv,看不到grep命令的。1
- 石维康2019-06-26在ext4_mknod函数里调用init_special_inode时传入的是上一步ext4_new_inode_start_handle返回的inode。为什么文中还会说"但是会调用 init_special_inode,创建一个内存中特殊的 inode"? 在init_special_inode中也没有看到创建虚拟inode的地方?
作者回复: 参数不是struct inode *inode
1 - 小鳄鱼2022-06-01二刷:匿名管道通过pipefs,而命名管道被当成设备需要通过ext4在磁盘上创建文件。显然,后者不会因为重启而消失,管道依然存在。但管道中的内容会丢失,因为实际上是内核的缓存。而前者重启后,管道都找不着了。是这样吗?
- linker2021-12-19bash创建的匿名管道居然没有用dup2函数
- John1172020-11-09老师,对于有名管道来说,现在a和b两个进程需要通信。如果我在进程a中创建了一个管道,现在有个进程b,有什么优雅的方法将管道的path告诉进程b?共 1 条评论
- 蹦哒2020-06-21请问老师: fd只是一个int,如果一个进程不停循环的用各个整数来尝试读取写入,恰巧有一个fd另外一个进程打开准备读取或者写入。这样岂不有可能破坏了进程的隔离性?共 2 条评论
- 奔跑的码仔2019-09-29“当我们用文件的 open 函数打开这个管道设备文件的时候,会调用 pipefifo_fops 里面的方法创建 struc file”。应该vfs创建的struct file,并将文件inode指向的pipefifo_fops赋值给struct file的的f_ops的吧?
- cuikt2019-07-08老师你好,我在shell中执行echo 'aaa' | > a.txt ,为什么a.txt文件被创建了,但是a.txt是空的呢?
作者回复: 命令写的有问题,echo 'aaa'的输出,是后面的输入,而>的意思是输出导向a.txt,和输入没有关系呀
共 2 条评论 - 有铭2019-06-26管道更像是流处理,还是批处理?
作者回复: 都不像吧