操作系统关键词——fork、exec和wait

当中断或系统调用把控制交给操作系统时,通常使用与被中断进程运行栈不同的内核栈,为什么?

(1)如果内核也使用用户栈,那么内核的信息容易被泄露,可能使得某些居心叵测的用户利用,从而使得计算机的安全受到威胁。
(2)如果用户也使用内核栈,那么用户或应用程序开发者难以了解到自己程序的执行细节,用户体验感差或者程序调试不方便。
(3)用户栈的大小有限。

SHELLFORK()EXEC()WAIT() 之间的关联是什么?

(1)shell是一个用户程序,而fork()exec()wait()都是系统调用。①用户往往可以向shell输入一个命令,这个命令包括某个可执行文件的名称和相关的参数;②随后shell在文件系统找到该可执行程序;③接着shell使用系统调用 fork() 创建一个新的进程;④并调用系统调用 exec() 来执行这个可执行程序;⑤shell自己使用系统调用 wait()来等待命令(实际上就是子进程)的完成;⑥ shellwait() 函数返回,继续接受用户的输入。

  • 编写一个打开文件的程序(使用 open() 系统调用),然后调用 fork() 创建一个新进程。子进程和父进程都可以访问 open() 返回的文件描述符吗?当它们并发(即同谁)写入文件时,会发生什么?
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <fcntl.h> // O_CREAT|O_WRONLY|O_TRUNC
# include <string.h>

int main(){
    int fd = open("./test.txt", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
    /*
    O_CREAT     创建并打开一个新文件
    O_WRONLY    以只写的方式打开
    O_TRUNC     打开一个文件并将其长度截断为0(要求写权限)
    S_IRWXU     可读可写可执行
    */
    int rc = fork();
    if (rc < 0){
        fprintf(stderr, "fork failed\n");
        exit(1);
    }
    else if (rc == 0){ // 子进程
        printf("Hello! I am child. My pid is %d, and the file descriptor is %d\n", 
               (int)getpid(), fd);
        write(fd, "I am child.\n", 12);
    }
    else { // 父进程
        printf("Hello! I am parent. My pid is %d, and the file descriptor is %d\n", 
               (int)getpid(), fd);
        write(fd, "I am parent.\n", 13);
    }
    return 0;
}

输出:
image.png
可以看到,父子进程都可以访问到 open() 系统调用返回的文件描述符。如果它们并发写入文件,那么并不会发生覆盖,它们各自的内容都会正常写入文件。不过写入顺序取决于父子进程的执行顺序。比如如果编写如下程序,文件中的内容就有可能是有多种情况:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <fcntl.h> // O_CREAT|O_WRONLY|O_TRUNC
# include <string.h>

int main(){
    int fd = open("./test2_1.txt", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
    int rc = fork();
    if (rc < 0){
        fprintf(stderr, "fork failed\n");
        exit(1);
    }
    else if (rc == 0){ // 子进程
        printf("Hello! I am child. My pid is %d, and the file descriptor is %d\n", 
               (int)getpid(), fd);
        write(fd, "I am child1.\n", 13);
        write(fd, "I am child2.\n", 13);
        write(fd, "I am child3.\n", 13);
        write(fd, "I am child4.\n", 13);
    }
    else { // 父进程
        printf("Hello! I am parent. My pid is %d, and the file descriptor is %d\n", 
               (int)getpid(), fd);
        write(fd, "I am parent1.\n", 14);
        write(fd, "I am parent2.\n", 14);
        write(fd, "I am parent3.\n", 14);
        write(fd, "I am parent4.\n", 14);
    }
    return 0;
}

image.png

  • 编写一个调用 fork()的程序,然后调用某种形式的 exec()来运行程序/bin/ls。看看是否可以尝试 exec()的所有变体,包括 execl()、execle()、execlp()、execv()、execvp()和 execvpe()。
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <sys/wait.h>

int main(){
    int rc = fork();
    if (rc < 0){
        fprintf(stderr, "fork failed\n");
        exit(1);
    }
    else if (rc == 0){ // 子进程
        printf("Hello! I am child. My pid is %d\n", (int)getpid());

        // int execl(const char *path, const char *arg, ...);
        // 用于执行指定路径的可执行文件,并用数组方式传入参数。
        printf("Executing execl()\n");
        execl("/bin/ls", "ls", "-l", NULL);

        printf("If exec() fails, you will see me.\n"); // 这条语句不会执行
    }
    else { // 父进程
        wait(NULL);
        printf("Child is over.\nI am its father %d.\n", (int)getpid());
    }
    return 0;
}
/*
path:  可执行文件的路径名字
arg:   可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:  如果参数file中包含/,则就将其视为路径名,否则就按PATH环境变量在它所指定的各目录中搜寻可执行文件。

返回值:exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

p:    使用文件名,并从PATH环境进行寻找可执行文件
v:    应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:    多了envp[]数组,使用新的环境变量代替调用进程的环境变量
*/

值得注意的一点是如果某个exec()函数成功执行,那么后续的代码将被替换,即不再被执行,我们可以看到如下结果:
image.png
对于其他函数,下面给出相关代码:

// int execle(const char *path, const char *arg, ..., char *const envp[]);
// 用于执行指定路径或单独定义的环境变量 PATH 变量下的可执行文件,并用数组方式传入参数和环境变量。
char *envp[] = {NULL};
printf("Executing execle()\n");        
execle("/bin/ls", "ls", "-l", NULL, envp);
// int execlp(const char *file, const char *arg, ...);
// 用于执行当前目录或环境变量 PATH 变量下可执行文件,并用数组方式传入参数。
printf("Executing execlp()\n");
execlp("ls", "ls", "-l", NULL);
// int execv(const char *path, char *const argv[]);
// 用于执行指定路径的可执行文件,并用字符指针数组方式传入参数。
char *argv[] = {"ls", "-l", NULL};
printf("Executing execv()\n");
execv("/bin/ls", argv);
// int execvp(const char *file, char *const argv[]);
// 用于执行当前目录或环境变量 PATH 变量下可执行文件,并用字符指针数组方式传入参数。
char *argv[] = {"ls", "-l", NULL};
printf("Executing execvp()\n");
execvp("ls", argv);
// !!!注意要在文件最前面定义 # define _GNU_SOURCE
// int execvpe(const char *file, char *const argv[], char *const envp[]);
// 用于执行当前目录或单独定义的环境变量 PATH 变量下可执行文件,并用字符指针数组方式传入参数。
char *argv[] = {"ls", "-l", NULL};
char *envp[] = {NULL};
printf("Executing execvpe()\n");
execvp("ls", argv, envp);

虽然传入参数的形式不一样,但是这些系统调用实现的功能是一致的,实际上都是某个基本调用(execve)的包装。这是因为不同的基本调用的变种,对应着不同的需求情景,比如不同情况下shell的设计。

  • 现在编写一个程序,在父进程中使用 wait(),等待子进程完成。wait()返回什么?如果你在子进程中使用 wait() 会发生什么?对前一个程序稍作修改,这次使用 waitpid() 而不是 wait()。什么时候 waitpid() 会有用?
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <sys/wait.h>

int main(){
    int rc = fork();
    if (rc < 0){
        fprintf(stderr, "fork failed\n");
        exit(1);
    }
    else if (rc == 0){ // 子进程
        printf("Hello! I am child. My pid is %d", (int)getpid());
    }
    else { // 父进程
        int wt = wait(NULL);
        printf("Hello! I am parent. My pid is %d, and the ret of `wait()` is %d\n", (int)getpid(), wt);
    }  
    return 0;
}

输出:
image.png
可以看到,返回的是子进程的pid
如果我们在子进程中使用wait(),那么返回值会是-1

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

int main(){
    int rc = fork();
    if (rc < 0){
        fprintf(stderr, "fork failed\n");
        exit(1);
    }
    else if (rc == 0){ // 子进程
        int wt = wait(NULL);
        printf("Hello! I am child. My pid is %d. Wait: %d\n", (int)getpid(), wt);
    }
    else { // 父进程
        // int wt = wait(NULL);
        printf("Hello! I am parent. My pid is %d\n", (int)getpid());
    }
    return 0;
}

输出:
image.png
返回值为-1实际上表示出现了错误,因为wait()函数实际上只针对父进程等待子进程的情况。
waitpid()函数的基本定义如下:

pid_t waitpid(pid_t pid,int *status,int options);
pid<-1等待进程组号为pid绝对值的任何子进程。
pid=-1等待任何子进程,此时的waitpid()函数退化为wait()函数。
pid=0等待进程组号与目前进程相同的任何子进程,即任何和调用waitpid()函数的进程在同一个进程组的进程。
pid>0等待进程号为pid的子进程。

如果要等价替换,那么将wait(NULL)替换为waitpid(-1, NULL, 0)即可。
值得注意的一点是,就算是**waitpid()**,也是针对父进程对子进程的,而各子进程之间(兄弟进程之间)是无效的
比如,如果我们编写如下代码:

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

int main() {
    int rc1 = fork();
    if (rc1 < 0) {
        fprintf(stderr, "fork1 failed\n");
        exit(1);
    } else if (rc1 == 0) { // 子进程1
        printf("Hello! I am Child1. My pid is %d\n", (int)getpid());
        long int i;
        for (i = 0; i < 100000000l; i ++)
                ; // 延时
        printf("*Child1 wake up and exit\n");
    } else {
        int rc2 = fork();
        if (rc2 < 0) {
            fprintf(stderr, "fork1 failed\n");
            exit(1);
        } else if (rc2 == 0) { // 子进程2
            printf("Hello! I am Child2. My pid is %d\n", (int)getpid());
            printf("Child2 is waiting for Child1\n");
            int wt = waitpid(rc1, NULL, 0);
            printf("Child1 has exited. Child2 is exiting now. %d\n", wt);
        } else{
                printf("Hello! I am parent. My pid is %d\n", (int)getpid());
                printf("Parent process is exiting.\n");
        }
    }
    return 0;
}

那么结果就会是(注:其中文本并不表示真实情况):
image.png
waitpid()返回值为-1,说明结果失效。
如果使用下述代码:

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

int main() {
    int rc1 = fork();
    if (rc1 < 0) {
        fprintf(stderr, "fork1 failed\n");
        exit(1);
    } else if (rc1 == 0) { // 子进程1
        printf("Hello! I am Child1. My pid is %d\n", (int)getpid());
        long int i;
        for (i = 0; i < 100000000l; i ++)
            ; // 延时
        printf("*Child1 wake up and exit\n");
    } else {
        int rc2 = fork();
        if (rc2 < 0) {
            fprintf(stderr, "fork1 failed\n");
            exit(1);
        } else if (rc2 == 0) { // 子进程2
            printf("Hello! I am Child2. My pid is %d\n", (int)getpid());
            long int i;
            for (i = 0; i < 1000000000l; i ++)
                ; // 延时
            printf("*Child2 wake up and exit\n");
        } else {
            printf("Hello! I am parent. My pid is %d\n", (int)getpid());
            printf("Parent is waiting for Child1\n");
            int wt = waitpid(rc1, NULL, 0);
            printf("Child1(%d) has exited. Parent is exiting now.\n", wt);
        }
    }
    return 0;
}

image.png
那么函数正常返回,可见父进程等待第一个子进程结束后再执行而并没有等待第二个子进程结束,本质还是父进程在等待子进程。

  • 编写一个程序,创建两个子进程,并使用 pipe()系统调用,将一个子进程的标准输出连接到另一个子进程的标准输入。
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <sys/wait.h>

int main(){
    // pipe() 系统调用常常在 fork() 之前使用来构建管道
    int fd[2] = {0, 0};
    int pp = pipe(fd);
    if (pp != 0){
        fprintf(stderr, "creating pipe failed\n");
        exit(1);
    }

    int rc1 = fork();
    if (rc1 < 0){
        fprintf(stderr, "fork1 failed\n");
        exit(1);
    }
    else if (rc1 == 0){ // 子进程1
        printf("Hello! I am child1. My pid is %d\n", (int)getpid());
        close(fd[0]);
        write(fd[1], "I am Child1", 11);
        close(fd[1]);
        printf("Child1 has finished its sending\n");
    }
    else {
        printf("Waiting for Child1\n");
        int wt = waitpid(rc1, NULL, 0); // 等待子进程1结束
        printf("Child1(%d) has finished\n", wt);
        int rc2 = fork();
        if (rc2 < 0){
            fprintf(stderr, "fork2 failed\n");
            exit(1);
        }
        else if (rc2 == 0){ // 子进程
            printf("Hello! I am child2. My pid is %d\n", (int)getpid());
            close(fd[1]);
            char info[100];
            int l = read(fd[0], info, 99);
            info[l] = '\00';
            printf("I am Child2 and the length of '%s' is %d\n", info, (int)strlen(info));
        }
        else {
            waitpid(rc1, NULL, 0);
            waitpid(rc2, NULL, 0);
            printf("Hello! I am parent. My pid is %d\n", (int)getpid());
        }
    }
    return 0;
}

输出:
image.png
实际上我们前面不太用专门等待子进程1,子进程2会一直等到子进程1的通信:

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

int main(){
    // pipe() 系统调用常常在 fork() 之前使用来构建管道
    int fd[2] = {0, 0};
    int pp = pipe(fd);
    if (pp != 0){
        fprintf(stderr, "creating pipe failed\n");
        exit(1);
    }
    int rc1 = fork();
    if (rc1 < 0){
        fprintf(stderr, "fork1 failed\n");
        exit(1);
    }
    else if (rc1 == 0){ // 子进程1
        printf("Hello! I am child1. My pid is %d\n", (int)getpid());
        sleep(1);
        close(fd[0]);
        write(fd[1], "I am Child1", 11);
        close(fd[1]);
        printf("Child1 has finished its sending\n");
    }
    else {
        //int wt = waitpid(rc1, NULL, 0); // 等待子进程1结束
        //printf("Waiting for Child1 %d\n", wt);
        int rc2 = fork();
        if (rc2 < 0){
            fprintf(stderr, "fork2 failed\n");
            exit(1);
        }
        else if (rc2 == 0){ // 子进程2
            printf("Hello! I am child2. My pid is %d\n", (int)getpid());
            close(fd[1]);
            char info[100];
            int l = read(fd[0], info, 99);
            info[l] = '\00';
            printf("I am Child2 and the length of '%s' is %d\n", info, (int)strlen(info));
        }
        else {
            waitpid(rc1, NULL, 0);
            waitpid(rc2, NULL, 0);
            printf("Hello! I am parent. My pid is %d\n", (int)getpid());
        }
    }
    return 0;
}

输出:
image.png

  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值