第5章 插叙:进程API 读书笔记

UNIX系统通过一对系统调用fork()exec()来创建新进程,通过wait()等待其创建的子进程执行完成。

5.1 fork()系统调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork failed
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else { // parent goes down this path (main)
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}

对于操作系统来说,看起来有两个一样的程序在运行,并且都从fork()调用中返回。子进程不会从main()函数开始,而是直接从fork()调用中返回,就好像自己调用了fork()

子进程(child)并不是完全拷贝了父进程(parent)。子进程拥有自己的地址空间、寄存器、程序计数器等,但从fork()的返回值不同。父进程获得的返回值是子进程的PID,子进程获得的是0。

输出不是确定的(deterministic)。单CPU的系统上,两者都有先运行的可能。

CPU调度程序(scheduler)决定了某个时刻哪个进程被执行。

5.2 wait()系统调用

父进程等待子进程执行完毕。也可使用waitpid()

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else { // parent goes down this path (main)
        int rc_wait = wait(NULL);
        printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
    }
    return 0;
}

增加了wait()调用后,输出结果变得确定了。即使父进程先运行,会马上调用wait(),停下来等待子进程执行结束后才返回。附注中提到,有些情况下会在子进程退出前返回,细节见man

5.3 最后是exec()系统调用

有多个变体,个人看起来好像只是传参方式不一样而已。让子进程执行和父进程不一样的程序。而fork()只能运行相同程序。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) { // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
        char *myargs[3];
        myargs[0] = strdup("wc"); // program: "wc" (word count)
        myargs[1] = strdup("p3.c"); // argument: file to count
        myargs[2] = NULL; // marks end of array
        execvp(myargs[0], myargs); // runs word count
        printf("this shouldn’t print out");
    } else { // parent goes down this path (main)
        int rc_wait = wait(NULL);
        printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
    }
    return 0;
}

exec()会从可执行程序中加载代码和静态数据,覆盖原本的代码段和静态数据,重新初始化堆和栈等内存空间。没有创建新进程,而是直接替换为不同的程序。成功调用不会返回。

5.4 为什么这么设计API

事实证明,分离fork()exec()的做法在构建UNIX shell的时候非常有用,这给了shell在fork之后exec之前运行代码的机会。

做对事(Get it right)。抽象和简化都不能替代做对事。

Lampson – 《Hints for Computer Systems Design》

有许多方式来设计创建进程的API,但fork()exec()的组合既简单又极其强大。

shell找到可执行程序,调用fork()创建新进程,调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令完成。子进程执行结束后,shell从wait()中返回并再次输出一个提示符,等待用户输入下一条命令。

重定向则在调用exec()前先关闭标准输出(standard output),然后打开文件。其实也可以用dup2()之类的,似乎比较方便。

close(STDOUT_FILENO);
open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

UNIX管道也是用类似方法实现的,但用的是pipe()系统调用。一个进程的输出被链接到一个内核管道(pipe)上(队列),另一个进程的输入也被链接到了同一个管道上。前一个进程的输出无缝地作为后一个进程的输入。

5.5 其他API

UNIX中还有其他许多和进程交互的方式,比如通过kill()系统调用向进程发送信号(signal),包括要求睡眠、终止等。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据