操作系统实验二-管道通信
管道
创建匿名管道
实际管道的创建调用的是系统调用pipe()
,该函数建了一个管道 pipe
,返回了两个文件描述符,这表示管道的两端,一个是管道的读取端描述符 fd[0]
,另一个是管道的写入端描述符 fd[1]
,fd[2]
为管道标号。
1 | int pipe(int fd[2]) |
如果对于 fd[1]
写入,调用的是 write()
,向 pipe_buffer
里面写入数据;如果对于 fd[0]
的读入,调用的是 read()
,也就是从 pipe_buffer
里面读取数据。至此,我们在一个进程内创建了管道,但是尚未实现进程间通信。
要注意,在管道中没有数据的情况下,对管道的读操作会阻塞,直到管道内有数据为止。这就是为什么示例实验中的父子进程之间的执行是交替的。
当然写操作也不会再所有情况下都不阻塞。这里我们要先来了解一下管道的内核实现。管道实际上就是内核控制的一个内存缓冲区,既然是缓冲区,就有容量上限。我们把管道一次最多可以缓存的数据量大小叫做PIPESIZE。内核在处理管道数据的时候,底层也要调用类似read和write这样的方法进行数据拷贝,这种内核操作每次可以操作的数据量也是有限的,一般的操作长度为一个page,即默认为4k字节。我们把每次可以操作的数据量长度叫做PIPEBUF。POSIX标准中,对PIPEBUF有长度限制,要求其最小长度不得低于512字节。PIPEBUF的作用是,内核在处理管道的时候,如果每次读写操作的数据长度不大于PIPEBUF时,保证其操作是原子的。而PIPESIZE的影响是,大于其长度的写操作会被阻塞,直到当前管道中的数据被读取为止。
匿名管道通信
匿名管道实现A|B
进程间的通信的原理如下:
- 利用
fork
创建子进程,复制file_struct
会同样复制fd
输入输出数组,但是fd
指向的文件仅有一份,即两个进程间可以通过fd
数组实现对同一个管道文件的跨进程读写操作 - 禁用父进程的读,禁用子进程的写,即从父进程写入从子进程读出,从而实现了单向管道,避免了混乱
- 对于
A|B
来说,shell
首先创建子进程A
,接着创建子进程B
,由于二者均从shell
创建,因此共用fd
数组。shell
关闭读写,A开写B开读,从而实现了A
和B
之间的通信。
接着我们需要调用dup2()
实现输入输出和管道两端的关联,该函数会将fd
赋值给fd2
1 | /* Duplicate FD to FD2, closing the old FD2 and making FD2 be |
在 files_struct
里面,有这样一个表,下标是 fd
,内容指向一个打开的文件 struct file
。在这个表里面,前三项是定下来的,其中第零项 STDIN_FILENO
表示标准输入,第一项 STDOUT_FILENO
表示标准输出,第三项 STDERR_FILENO
表示错误输出。
1 | struct files_struct { |
- 在 A 进程写入端通过
dup2(fd[1],STDOUT_FILENO)
将STDOUT_FILENO
(也即第一项)不再指向标准输出,而是指向创建的管道文件,那么以后往标准输出写入的任何东西,都会写入管道文件。 - 在 B 进程中读取端通过
dup2(fd[0],STDIN_FILENO)
将STDIN_FILENO
也即第零项不再指向标准输入,而是指向创建的管道文件,那么以后从标准输入读取的任何东西,都来自于管道文件。
至此,我们将 A|B
的功能完成。
示例实验与上面的过程略有不同(没有上面那么麻烦),因为不会涉及shell进程,所以是直接在父子进程中进行消息传递,而不是创建两个子进程。
独立实验
与示例实验思路相近,使用了两个管道。不同的是,两个管道都是由子进程向父进程发送数据。所以子进程直接关闭读,父进程直接关闭写。
出现的一些问题:不能在父进程中并列地进行进程创建,或者说,不能在父进程中同时声明子进程的创建,这会导致第二个子进程会被第一个子进程再创建一遍。所以第二个子进程的创建应该在第一个子进程内进行。
1 |
|