接下来一节就是进程间通信
我们要谈到几个机制
一个是管道 一个是进程信号量
一个是共享内存 一个是映射内存
还有消息队列、套接字
有几个
这一讲里面我们会讨论
前面五种——管道、进程信号量、
共享内存、映射内存、消息队列
套接字我们会在网络那一讲里
再详细讨论
我们首先来看管道
管道是什么东西
管道是允许单向通讯的自动同步设备
它是一种半双工的模式
一般情况下我们是以半双工的模式
去使用它的
数据在写入端写入 在读取端读取
就这个意思
管道是一个典型的串型设备
数据的读取和写入
顺序是完全相同的
写入是什么顺序
读取出来 它也是什么顺序
这个就是管道
管道只能应用于有亲缘关系的进程
常常是父进程和子进程之间的通讯
我们使用管道
在使用管道的时候 特别注意
管道的数据容量是有限的
在大部分情况下
它是一个内存页面的大小
如果写入的速度超过了它读取的速度
写入的进程就会被阻塞
一直到管道中有空闲的空间
可以朝里写
那么如果读取的速度
超过了写入速度呢
你会把那个管道里的数据都读完的
读完了以后呢
读进程呢 它也会被阻塞
一直到那个管道里
有新的数据你可以去读取
创建管道那个函数叫pipe()
函数倒很简单
带一个只包含两个元素的整数数组
那是管道的文件描述符
因为一个管道有一个写入端
有一个读取端
每一端都有一个文件描述符
所以它是双文件描述符
那么这个数组保存的就是这个管道的
双文件描述符
0号元是读取端 一号元是写入端
注意这个
这个函数调用的时候
成功的时候会返回0
不成功的时候就会返回-1
设errno的值
调用的方式其实也简单
定义这一个包含两个元素的整数数组
然后调用pipe()
这个pipe()一调用
它就会给你构造一个管道
构造一个管道之后呢
它的文件描述符
就会给你写到这个数组里
pipe_fds这个数组里
它帮你写进去的
然后接下来你就可以用了
所以这个pipe_fds数组的0号元
就是管道读取端的文件描述符
1号元就是写入端的文件描述符
你就可以直接用它
操纵这个管道
我们看管道通讯的一个例子
我实现一个函数Write()
向这个特定的一个流里输出一串信息
这串信息我们会重复输出好几遍
所以这个信息是用message传进来的
流是一个文件指针
FILE * stream传进来的
这个消息写多少遍呢 写count遍
我就一个for循环朝这里面写数据
就完了
读呢 我们就从这个流里边去读这个数据
读这个数据的话我们就蛮贪心地读取
就是当这个流没有结束的时候
当读取这个信息没有出错的时候
当这个流里还有数据可以读的时候
我们就把这个数据全都读出来
然后读出来以后就向标准输出流里
输出这个数据就完了 很典型
这个数据从哪来呢 管道里来
这个数据朝哪里写呢 管道里写
你看我们父子进程
怎么通过这个管道传这个数据的
在主函数里调用pipe()构造一个管道
我们会创建这样的一个管道
然后fork() 创建一个子进程
用pid保存子进程的PID
if( pid== 0 )
OK 子进程close( fds[1] )
关闭管道的写入端
表示在我们的子进程里
我们只从管道里读数据
我们不向管道里写数据
刚才不说了嘛
管道 你创建出来
虽然有一个写入端 有一个读取端
但是对于两个进程
使用管道进行通讯的时候
一个进程只使用写入端
一个进程只使用读取端
所以它实际上是一个半双工的模式
你也可以倒过来
A进程写入 B进程读取
可以倒过来
A进程读取 B进程写入
但是正常情况下
一个管道只能一端读取 一端写入
两个进程一个读取、一个写入
这叫半双工的模式
双工模式就是双向读写嘛
半双工呢 就是确实它都可读可写
但是每次都只能这边读、那边写
或者这边写、那边读
你如果真让两个进程
同时要互相传递数据
都要读写的话
那就只能创建两个管道
就是这个意思
我们这里面只创建一个管道
创建一个管道 对于子进程来讲呢
我们只负责读取 从管道中读数据
所以写入端对我们没有用
我们直接关闭它的文件描述符
close( fds[1] ) 上来就关掉 不要
然后我们调用一个系统调用fdopen()
传文件描述符 以读模式打开它
得到它的文件指针 FILE *
这样的话就可以和C的函数
完全地吻合起来
因为C的很多文件操作
实际上使用的是文件指针
C标准库使用的是这个东西
我们可以使用FILE * stream
来得到这个管道的读取端的文件指针
fdopen()函数干的就是这个事
然后我们从这个流里读数据
读完了关闭这个管道的读取端
这是子进程
接下来是父进程
父进程呢 我们会构造一个缓冲区
要朝里面写数据
数据在哪呢 数据存在这个缓冲区里 对吧
我们会构造一系列的数据
这个数据我们怎么构造呢
就是定义一个buffer
这是一个缓冲区 buf_size
我定义成4096个字节 一个页面
然后我就朝里面写
我写多少呢 我写4094个字节
都是“abcdefg” 一直到“z”
然后再来一遍“abcdefg”
就26个字符、26个字符不停地往下写
我写了4094个字节
最后两个字节呢 我们封装成“\0”
表示这个串结尾了 就这个意思
我们就生成这些信息
然后关闭管道的读取端
我们只负责写入 我不负责读取
这是父进程
所以要做到这个事
然后我们fdopen( fds[1] )
以写模式打开它的管道写入端
得到FILE * 文件指针
得到文件指针之后
我们就像这个stream里写数据
写这个buffer里面的数据 写三遍
一次一个页面 一次一个页面
我们写三遍
写完了我们close( fds[1] )
把管道的写入端也给它关闭掉
这个就是管道通讯的示例
我们前边用到了一个很特殊的函数 dup()
现在我们就谈它怎么进行
文件描述符的复制
管道可以进行重定向
在这里使用到的一个非常重要的概念
叫等位文件描述符
所谓等位文件描述符
就是共享相同的文件位置
和状态标志设置
它们代表的是同一个文件和设备
这个就叫等位文件描述符
dup()函数就将两个文件描述符等位对待
认为它两者是一样的
这个函数有两个版本
一个叫dup()带单参数的版本oldfd
一个是 dup2() 单双参数的版本
一个是oldfd 一个是newfd
都将创建
oldfd这个老的文件描述符的一份副本
构造它的一个拷贝
第一个就是选择最小的文件描述符
来作为新的文件描述符
第二个呢 就是你传的那个参数
作为新的文件描述符
一旦构造出来这样一个副本之后
在你的文件描述符那个表格里面
那么就会有两个文件描述符
它后面跟着的那个文件
或设备的那个指针
就会变成同样的值
你比如说0号文件描述符
它后面跟着指向的
不是那个标准输入流那地方吗
stdin嘛 它就会指向那个地方
你把它dup( 0 )
就按我刚才那个例子
如果你把三个标准流全关掉了
那么你把哑终端
又构造到了0号文件描述符上
一旦你dup( 0 )
就把0号文件描述符的
那个指向文件或设备的那个指针
也写到了1号描述符的那个地方
它优先占用最小的文件描述符
0号被占用 1号还空闲
所以占用1号
那就意味着1号文件描述符
将会指向那个哑终端
也指向哑终端
再来一个dup( 0 )
它就把0号文件描述符
又写到2号文件描述符后边的那个区域
就意味着2号文件描述符后面那个
文件或设备的指针也指向哑终端
这就意味着
我们的标准输入流、标准输出流、
标准错误流都指向了哑终端嘛
dup()干的就是这个事
dup2()就是把那个新的文件描述符
设定给它 设定给新的文件描述符
你看dup2( fd, STDIN_FILENO ) 什么意思
就是把fd这个文件描述符
所对应的那个文件或设备的
那个指针 那个值传
给STDIN_FILENO 这个文件描述符
让它也指向那个地方
这就意味着标准输入流会被我们关闭
然后会被作为fd的一个副本重新打开
我们的输入就不再从键盘来
而是从fd所表示那个文件设备里边来
dup2()干的就是这个事
我们看这个例子
构造一个管道pipe( fds )
当然还是两个整数的一个数组
fork() 创建一个子进程
if( pid == 0 ) 说明它是子进程里
关闭0号元 我们只写入 不读取
然后把1号元和标准输出流
挂接在一起
这个管道不是写入嘛 对吧
然后就会挂接到我们的标准输出流
所以你一写就向标准输出流里输数据了
就这个意思
然后我们一执行
execvp() 就执行这个程序“ls”
所以这个数据就全写到了管道里
“ls”一运行
向标准输出流里写数据
标准输出流被我挂到哪去了
挂到管道的写入端了呀
所以这个数据就哗啦哗啦
就全写到管道里了
然后父进程就去读这个管道
我们关闭这个管道的写入端
我们就去读
打开这个管道的读取端
得到它的文件指针
然后我们就从这里边
就把这个写入过来的数据就全读出来了
所以你看我们这个程序
子进程就会“ls” 就列目录
把这个信息列出来以后
全扔到管道里
而我们父进程
就从管道里把那个信息就拿出来了
你写了什么
我们这里边就读出什么来
就这个模式
最后当父进程读取完全部的数据之后
关闭我们管道的读取端
最后waitpid()
等待我们的子进程结束
这就把我们前面讲到的几个知识点
都串在一起了