UNIX 进程API

0x01 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) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else {
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}
# ./p1
hello world (pid:17)
hello, I am parent of 18 (pid:17)
hello, I am child (pid:18)

从上述代码的执行结果来看:

  • 父进程(pid=17)调用了fork()后,返回的是子进程的pid,所以rc=18
  • 只打印了一条hello world,说明子进程并不会从main函数开始执行,而是从rc=fork();开始,并且rc=0

小结:调用fork()后,父进程通过fork()函数的返回值可以拿到子进程的pid,子进程直接从fork()系统调用返回,就好像是它自己调用了fork(),并且返回值为0。上述的打印是不固定的,因为父进程和子进程谁先执行完是不确定的。

0x02 wait

#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) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else {
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}
# ./p2
hello world (pid:77)
hello, I am child (pid:78)
hello, I am parent of 78 (wc:78) (pid:77)

父进程调用wait(),延迟自己的执行,直到子进程执行完毕,当子进程结束时,wait()才返回父进程

0x03 exec

#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) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        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 {
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}
# ./p3    
hello world (pid:94)
hello, I am child (pid:95)
 26 107 828 p3.c
hello, I am parent of 95 (wc:95) (pid:94)

子进程调用execvp()来运行字符计数程序wc,它会针对源代码文件p3.c运行wc,输出文件有多少行、多少单词、多少字节

exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段,堆、栈和其他内存空间也会被重新初始化。因此它并没有创建新的进程,而是直接将当前运行的程序(以前的p3)替换为不同的运行程序(wc)。子进程执行exec()之后,几乎就像p3.c从为运行过一样,对exec()的成功调用永远不会返回。

0x04 为什么这样设计API

看了上面3个系统调用,或许我们只能理解wait()的用途,fork()、exec()这两个用于创建进程的API是不是显得比较奇怪?

我们看一下这两个API结合起来可以实现什么:

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

int main(int argc, char *argv[]) {
    int rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        close(STDOUT_FILENO);
        open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

        char *myargs[3];
        myargs[0] = strdup("wc");
        myargs[1] = strdup("p4.c");
        myargs[2] = NULL;
        execvp(myargs[0], myargs);
    } else {
        int wc = wait(NULL);
    }
    return 0;
}
# ./p4
# cat p4.output
 26  66 595 p4.c

这段代码实现的功能,就是通过wc计算出p4.c这个文件有多少行、多少单词、多少字节,并且将这些内容写到新的文件p4.output中。

具体步骤如下:

  • 父进程fork()出子进程
  • 子进程关闭标准输出
  • 通过调用execvp()执行wc,计算文件行数、单词数、字节数
  • 计算结果输出到文件p4.output中

本质上这段代码实现的功能就等价于 wc p4.c > p4.output

fork()和exec()是非常强大的API,在构建UNIX shell时非常有用。我们甚至在了解他们时觉得很奇怪,其实这些都是经过无数实验得出来的成果。一篇著名论文中也说过:Get it right!抽象和简化都不能替代 Get it right,只要这些API能帮助我们做正确的事,那就有它们存在的理由。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容