• unix系统创建新进程的方式是一对系统调用fork()exec(),系统调用wait()用于等待创建的子进程执行完

fork()系统调用

  • 例子:使用fork
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#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; 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)
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}
  • 运行输出:
1
2
3
4
5
$ ./p1
# 输出:
# hello world (pid:29146)
# hello, I am parent of 29147 (pid:29146)
# hello, I am child (pid:29147)
  • 进程调用fork创建新进程,新进程几乎与原进程完全一样,对OS来说有两个完全一样的程序在运行,并都从fork返回
  • fork创建的子进程不会从main开始执行,而是直接从fork中返回,就好像是它自己调用了fork
  • 子进程并非完全拷贝父进程。它虽然拥有自己的地址空间、寄存器等,但从fork返回的值与父进程不一样:父进程从fork返回的值是新创建子进程的PID,子进程从fork中返回0,这使得子进程和父进程接下来可以做不同的事
  • fork时子进程和父进程谁先执行,由cpu调度程序决定

wait()系统调用

  • 例子:使用wait
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#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 wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}
  • 运行输出:
1
2
3
4
5
$ ./p2 
# 输出:
# hello world (pid:29266) 
# hello, I am child (pid:29267)
# hello, I am parent of 29267 (wc:29267) (pid:29266)
  • 在fork结束后,父进程调用wait可等待子进程状态发生改变(如terminated、stopped、resumed),父进程才从wait中返回,继续执行

最后是exec()系统调用

  • 例子:使用exec
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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 wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}
  • 运行输出:
1
2
3
4
5
6
$ ./p3 
# 输出:
# hello world (pid:29383) 
# hello, I am child (pid:29384) 
#     29  107 1030 p3.c
# hello, I am parent of 29384 (wc:29384) (pid:29383)
  • exec系统调用可让子进程执行与父进程不同的程序
  • 对exec给定可执行程序的名称和运行参数,exec从可执行程序中加载代码和静态数据,用其覆盖自己的代码段和静态数据段,堆栈和其他内存空间被重新初始化,OS按照运行时参数执行该程序。
  • exec并未创建新进程,而是将当前运行的程序替换为不同的程序。子进程执行exec后就像原来的进程从未运行过一样
  • 对exec的成功调用永远不会返回

为什么这样设计API

  • 将fork和exec分离的做法在构建shell时非常有用,这可使得shell在fork之后exec之前运行代码,这些代码可在exec运行新程序之前改变环境
  • shell是一个用户程序,它首先输出提示符,用户输入命令后,shell在文件系统中找到该可执行程序,调用fork创建子进程,调用exec执行该程序,调用wait等待该命令完成。子进程结束后shell从wait返回并输出提示符等待下一个命令
  • fork和exec分离实现重定向:shell用fork完成子进程创建后,调用exec之前关闭标准输出,打开重定向目标文件,再用exec运行程序。
  • 例子:使用fork和exec重定向输出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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) {           // fork failed; exit
        fprintf(stderr, "fork failed\n"); 
        exit(1);
    } else if (rc == 0) {   // child: redirect standard output to a file 
        close(STDOUT_FILENO);
        open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
        // now exec "wc"... 
        char *myargs[3];
        myargs[0] = strdup("wc");   // program: "wc" (word count)
        myargs[1] = strdup("p4.c"); // argument: file to count
        myargs[2] = NULL;           // marks end of array
        execvp(myargs[0], myargs);  // runs word count
    } else {                // parent goes down this path (main)
        int wc = wait(NULL); 
    } 
    return 0; 
}
  • 运行输出:
1
2
3
4
5
$ ./p4
# 无输出
$ cat p4.output
# 输出:
# 32 109 846 p4.c
  • 重定向的工作原理,是基于对OS管理文件描述符方式的假设。unix系统从0开始寻找可使用的文件描述符。上例中STDOUT_FILENO是第一个可用的文件描述符,将其close并open另一个文件时,使用新的文件描述符,故对标准输出文件的写入都会被写到新的文件描述符
  • unix的管道也和重定向的实现类似,但用的是pipe系统调用。此时一个进程的输出被连接到内核管道(队列)上,另一个进程的输入也被连接到该管道。前一个进程的输出作为后一个进程的输入,可用这种方式串联多个命令共同完成任务

其他API

  • kill系统调用可向进程发送信号,包括睡眠、终止等指令。整个信号子系统提供了很多向进程传递外部事件的途径
  • 工具ps可查看当前运行的进程
  • 工具top可查看系统中进程消耗cpu等资源的情况

作业(编码)

  1. 调用fork前让主进程定义变量,子进程和父进程都改变它的值时发生什么?
  • 答:如下,各自维护一个副本,互不影响
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
    int x=10;
    int rc=fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }
    else if(rc==0){
        x=1;
        printf("child,x=%d\n",x);
    }
    else{
        x=100;
        printf("parent,x=%d\n",x);
    }
    return 0;
}
  1. 使用open打开文件,然后fork创建子进程,子进程和父进程是否都可访问open返回的文件描述符?他们并发写入时会发生什么?
  • 答:可以写,按照子进程和父进程调度的顺序串行写入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/wait.h>
#define BUF_LEN 4096
int main(){
    int out=open("./test.out",O_CREAT|O_WRONLY|O_TRUNC,S_IRWXU);
    char buf_child[BUF_LEN];
    char buf_parent[BUF_LEN];
    for(int i=0;i<BUF_LEN;++i){
        buf_child[i]='c';
        buf_parent[i]='p';
    }
    int rc=fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }
    else if(rc==0){
        write(out,buf_child,BUF_LEN);
    }
    else{
        write(out,buf_parent,BUF_LEN);
    }
    return 0;
}
  1. 了解exec的变体:execl()、execle()、execlp()、execv()、execvp()和 execvP()
  • 答:
    • 名字带l的,参数以可变参数列表给出
    • 名字带v的,参数以argv数组给出
    • 名字带p的,指定程序时不需给完整路径,而是在PATH中查找
    • 名字带e的,可在调用时传入指定的环境变量
  • 函数原型:
1
2
3
4
5
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
  1. 父进程中使用wait返回什么?子进程中使用wait发生什么?
  • 答:
    • 父进程中的wait:子进程执行成功返回PID,失败返回-1
    • 子进程中的wait:返回-1,不影响父进程
  1. 什么时候需要用waitpid?
  • 答:waitpid可通过PID指定等待哪一个子进程,且可增加option参数
  1. 子进程中关闭标准输出后调用printf打印到标准输出会怎样?
  • 答:不会打印到标准输出。若在close标准输出后open一个文件,printf将打印到该文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
int main(){
    int rc=fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }
    else if(rc==0){
        close(STDOUT_FILENO);
        //open("./test.out",O_CREAT|O_WRONLY|O_TRUNC,S_IRWXU);
        printf("test\n");
    }
    else{
    }
}
  1. 创建两个子进程,使用pipe系统调用将一个子进程的标准输出连接到另一个子进程的标准输入
  • 答:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#define BUF_LEN 1024
int main(){
    pid_t pid_1,pid_2;
    int fd[2];
    // construct pipe
    if(pipe(fd)<0){
        fprintf(stderr,"pipe failed\n");
        exit(1);
    }
    // fork 1
    pid_1=fork();
    if(pid_1<0){
        fprintf(stderr,"fork1 failed\n");
        exit(1);
    }
    else if(pid_1==0){
        char write_buf[BUF_LEN]="pipe demo program\n";
        close(fd[0]);
        write(fd[1],write_buf,strlen(write_buf));
        exit(0);
    }
    // fork 2
    pid_2=fork();
    if(pid_2<0){
        fprintf(stderr,"fork2 failed\n");
        exit(1);
    }
    else if(pid_2==0){
        char read_buf[BUF_LEN]="";
        close(fd[1]);
        read(fd[0],read_buf,BUF_LEN);
        printf("%s",read_buf);
        exit(0);
    }
    // parent
    waitpid(pid_1,NULL,0);
    waitpid(pid_2,NULL,0);
    return 0;
}

小结