进程通讯与 fork¶
fork的修改¶
对fork的文件支持本来应该在chapter6引入,但是为了更好的理解管道的继承机制,我们把它放在了这个章节。 fork 为什么是毒瘤呢?因为你总是要在新增加一个东西以后考虑要不要为新功能增加 fork 支持。这一章的文件就是第一个例子,那么在 fork 语境下,子进程也需要继承父进程的文件资源,也就是PCB之中的指针文件数组。我们应该如何处理呢?我们来看看 fork 在这一个 chapter 的实现:
int fork() {
// ...
+ for(int i = 3; i < FD_MAX; ++i)
+ if(p->files[i] != 0 && p->files[i]->type != FD_NONE) {
+ p->files[i]->ref++;
+ np->files[i] = p->files[i];
+ }
// ...
}
可以看到创建子进程时会遍历父进程,继承其所有打开的文件,并且给指定文件的ref + 1。因为我们记录的本身就只是一个指针,只需用ref来记录一个文件还有没有进程使用。
此外,进程结束需要清理的资源除了内存之外增加了文件:
void freeproc(struct proc *p)
{
// ...
+ for (int i = 3; i < FD_BUFFER_SIZE; i++) {
+ if (p->files[i] != NULL) {
+ fileclose(p->files[i]);
+ }
+ }
// ...
}
你会发现 exec 的实现竟然没有修改,注意 exec 仅仅重新加载进程执行的测例文件镜像,不会改变其他属性,比如文件。也就是说,fork 出的子进程打开了与父进程相同的文件,但是 exec 并不会把打开的文件刷掉,基于这一点,我们可以利用 pipe 进行进程间通信。
// user/src/ch6b_pipetest
char STR[] = "hello pipe!";
int main() {
uint64 pipe_fd[2];
int ret = pipe(&pipe_fd);
if (fork() == 0) {
// 子进程,从 pipe 读,和 STR 比较。
char buffer[32 + 1];
read(pipe_fd[0], buffer, 32);
assert(strncmp(buffer, STR, strlen(STR) == 0);
exit(0);
} else {
// 父进程,写 pipe
write(pipe_fd[1], STR, strlen(STR));
int exit_code = 0;
wait(&exit_code);
assert(exit_code == 0);
}
return 0;
}
由于 fork 会拷贝所有文件而 exec 不会改变文件,所以父子进程的fd列表一致,可以直接使用创建好的pipe进行通信。