简单的进程间通讯: 管道
管道是 UNIX 最传统, 最简单, 也是最有效的进程间通讯方法. NetBSD 处理管道的代码在 kern/sys_pipe.c, 它的读写函数作为 file 结构的 fileops 挂载, 并在 read(2), write(2) 时被调用.
管道创建
pipe(2) 的响应函数实 sys_pipe(). 它首先两次调用 pipe_create(), 第一次申请读端口将调用 pipespace() 申请一个用作缓冲区的内核地址空间 (回忆 BsdSrcUvm, submap); 第二次申请写端口则不申请空间. 这样, 我们就得到了两个 pipe 结构, 对应管道的两个端口. 把缓冲区放到 rpipe 是有道理的, 因为这样即使写端口关闭了我们还可以把剩余数据读出, 而如果读端口 (消费者) 关闭了, 写端口 (生产者) 写数据也没有意义了.
接下来 sys_pipe() 的工作就是让两个端口相连和使它们与文件接口相连. 我们分别申请读写两段对应的 file 结构, 填写返回值; 然后设置 pipe_peer 将 pipe 的两个端口相连. 所有的工作就完成了.
管道读
管道读的响应函数为 pipe_read(). 我们进入了一个常见的循环, 如果请求读的字节还没完成, 我们就继续在缓冲区取数据 (XXX. 忽略 NODIRECT 的情况, 我们得先保证我们的叙述能构建一个 可运行 的系统); 否则, 假如我们没收到文件结束讯号, 又在阻塞读状态, 我们就得等待, 等待前我们还得先唤醒控制得 select/poll 函数或者生产者 writer, 否则我们的等待是永远不会结束的.
管道写
我们再来看管道写的 pipe_write() 函数. 我们首先注意到一个比较郁闷的 rpipe 和 wpipe 的指称. 这时候的 wpipe 所指向的, 其实是 pipe(2) 创建的 rpipe, 因为我们要把数据 写到读端口让它来读. 我们还可以根据要写入的数据大小判断当前 pipe 的 buffer 是否够大, 可以随时扩充之.
我们又进入了一个与 pipe_read() 相似的循环. (XXX. NODIRECT) 接下来的处理过程也是相似的, 注意 space 的计算, 我们倾向于一次性把数据写入, 如果不行, 我们宁愿先等待.
管道关闭
pipe_close() 只需应付一下文件接口, 将 f_data 清空, 真正的 pipe 关闭工作由 pipeclose() 完成. 在这里, 我们主要要修改对端状态, 告诉它我们要死了, pipe 结束了, 把对端指向自己的 pipe_peer 域清空. 然后对因读或写等待的对端, 唤醒之, 让它处理这个事件. 接着, 我们返还所有资源: 缓冲区(如果是读端口), pipe 结构, 这个端口的生命就结束了.
BSD 进程间通讯: socket
请参考 BsdSrcSocket
SystemV IPC
SystemV IPC 是由商业发行 SystemV UNIX 带来的通信机制. 它包括消息, 共享存储区, 信号量, 实现了良好的本地进程通信接口. 在 NetBSD 中实现的代码在 kern/sysv_*.
对象标识
作为进程间通讯单位并允许多个进程 (而非 pipe 的两个进程点对点) 通讯, 其通讯单位显然是系统的且全局的. 三种通信机制的对象都有相同的标识方法: 每种机制都包含一个表, 其中的表项描述该机制的所有实例; 每个表项都有一个用户选定的 key, 各个进程可以通过 key 获取通信对象实例, 从而互相通信. 每个通信对象实例又对应一个全局唯一的标识符, 以进行真正的通信.
考虑这个全局标识符就是对象实例再表中的位置, 我们将面临一种情况, 某个程序使用一个 id 做通讯, 可是这个 id 所对应的通信实例可能已被撤消, 这个位置又换上了另一个通信实体, 而这个程序对此一无所知, 继续操作, 这会造成灾难性后果. 我们的解决方法是加上一个 _seq 域, 每次申请一个实例, 就将其 _seq 域加 1, 我们返回的标识符为 _seq * 65536 + 实例在表中的位置, 这样就不会产生这种混乱了.
每个 IPC 结构都有一个权限表项, 其权限设置与文件的权限设置相似, 通过它我们可以限定某些进程相互通信的权限.
以上标识方法的描述在 struct ipc_perm, 每个 IPC 对象结构都带有一个 ipc_perm.
消息队列 (message queue)
消息的对象结构描述在 struct msqid_ds, 全局的控制结构在 struct msginfo.