《操作系统导论》第五章“Process API”课后习题

实验环境:ubuntu20.04
IDE:VScode

只有纸质版的书,所以懒得打字了,使用的是作者提供的电子版PDF上的题目,应该也不难理解。

the first question

1. Write a program that calls fork(). Before calling fork(), have the main process access a variable (e.g., x) and set its value to something (e.g., 100). What value is the variable in the child process? What happens to the variable when both the child and parent change the value of x?

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"

int main(int argc, char **argv){
    int x = 100;
    printf("main process(pid: %d)\n", (int)getpid());
    printf("(main process) x: %d, address: %p\n", x, &x);
    x = 10;
    int rc = fork();
    if(rc < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(rc == 0){
        printf("child process(pid: %d)\n", (int)getpid());
        printf("(child process) x: %d, address: %p\n", x, &x);
        x = 50;
        printf("(child process)changed x: %d\n", x);
    }else{
        //wait subprocess to finish
        wait(NULL);
        printf("(main process)changed x: %d\n", x);
    }
    return 0;
}

output

main process(pid: 21641)
(main process) x: 100, address: 0x7fffffffde60
child process(pid: 21658)
(child process) x: 10, address: 0x7fffffffde60
(child process)changed x: 50
(main process)changed x: 10

理解:

从上述结果中可以看出,子进程和主进程中x的地址是相同的,但是在子进程中修改x的值并不会影响主进程中x的值。

程序在分配内存中有两种方式:动态分配提前分配

显然这里x的内存已经提前申请了,所以在子进程和主进程中的x的地址必然是一样的,区别在于该地址是虚拟地址,子进程和主进程通过映射,将虚拟内存映射到不同的物理内存上,所以在子进程中修改主进程变量的值并不会作用的主进程变量的地址上。
通过fork创建子进程时,子进程会复制一块主进程的内存,但是拥有自己独立的内存空间。

the second question

2. Write a program that opens a file (with the open() system call) and then calls fork() to create a new process. Can both the child and parent access the file descriptor returned by open()? What happens when they are writing to the file concurrently, i.e., at the same time?

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"
#include "fcntl.h"

int main(int argc, char **argv){
    int i = 50;
    close(STDOUT_FILENO);
    open("./info.output", O_CREAT | O_WRONLY | O_TRUNC);
    int rc = fork();
    if(rc < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(rc == 0){
        while(i > 0){
            printf("child process\n");
            i--;
        }
    }else{
        while(i > 0){
            printf("main process\n");
            i--;
        }
    }
    return 0;
}

主程序中关闭了标准输出,打开了一个文件输出流,程序开始执行时,会自动打开三个标准输入输出流:STDIO/STDOUT/STDERR,如果关闭了标准输出流则操作系统会扫描第一个可用的文件标识符(上面通过open函数,成功会返回一个文件描述符)作为输出流。
在上面的程序中,分别在主进程和子进程中打印消息50次。
output

main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
main process
child process
child process
child process
child process
child process
child process
child process
······

我运行了很多次发现,不管在主进程中打印一次延时一秒还是上面的程序这样的执行逻辑,子进程和主进程的输出总是在一起的,查阅了一些资料,大致了解了一下,后面学习可能还会学到这部分内容,每一个进程打开文件的时候都会有一个描述文件的文件表项(这里的说法可能不太严谨,只是个人理解),而每个进程向文件中写入内容是通过记录偏移量,所以最终的文件内容是主进程的内容在前或者子进程的在前(这个就不一定了)。

the third question

3. Write another program using fork(). The child process should print “hello”; the parent process should print “goodbye”. You should try to ensure that the child process always prints first; can you do this without calling wait() in the parent?

这个我直接使用在主进程中睡眠来实现的,或许还有其他的方法。

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"

int main(int argc, char **argv){

    int rc = fork();
    if(rc < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(rc == 0){
        printf("Hello ");
    }else{
        sleep(1);
        printf("goodbye\n");
    }
    return 0;
}

the forth question

4. Write a program that calls fork() and then calls some form of exec() to run the program /bin/ls. See if you can try all of the variants of exec(), including (on Linux) execl(), execle(), execlp(), execv(), execvp(), and execvpe(). Why do you think there are so many variants of the same basic call?

我使用的ubuntu20.04提供了其中的几个系统接口,用法都大同小异。
用法这里举例两个,其他的用法,我看库函数说明也比较容易理解和使用,所以就没写。

  • execvp函数原型:int execvp(const char *__file, char *const *__argv)
    这里的file是可执行文件的名称,在系统环境变量里寻找,我试了一下,如果环境变量没加,添加相对路径也是可以的。第二个参数是执行该可执行文件添加的参数
  • execl函数原型:int execl(const char *__path, const char *__arg, ...)
    这里的path是可执行文件的路径,该函数在调用时,会将所有的参数添加到可执行文件后面,直到遇到空指针,所以传参的时候,最后要传一个NULL进去。
  • 其他的函数我也没用,太累了

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "string.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){
        //record:find command from env
        char *myargs[2];
        myargs[0] = strdup("ls");

        if(myargs[0] == NULL){
            fprintf(stderr, "malloc failed");
        }
        myargs[1] = NULL;
        execvp(myargs[0], myargs);

        //record:need add command path
        // char *myargs[2];
        // myargs[0] = strdup("/bin/ls");

        // if(myargs[0] == NULL){
        //     fprintf(stderr, "malloc failed");
        // }
        // myargs[1] = NULL;
        // execl(myargs[0], myargs[1], NULL);

        // execle(path, args);
        // execlp(file, args);
        // execv(path, param);
        // execve(path, argv, envp);
    }else{
        //wait subprocess to finish, return pid of subprocess
        wait(NULL);
        printf("over");
    }
    return 0;
}

output:
这是我当前文件夹下的内容

info.output  output  problem1.c  problem2.c  problem3.c  problem4.c  problem5.c  problem6.c  problem7.c  problem8.c

书的前面也提到过,调用上面的函数是不会有返回的,意思是调用该函数时,子进程相当于把当前要执行的内容进行了替换,所以子进程中该函数后面的语句是不会执行的。

exec()有这么多变种,我的理解就是为了适应不同的场景,总不能把所有的可执行文件全放环境变量里吧。

the fifth question

5. Now write a program that uses wait() to wait for the child process to finish in the parent. What does wait() return? What happens if you use wait() in the child?

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"


int main(int argc, char **argv){

    int rc = fork();
    if(rc < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(rc == 0){
        int wc = wait(NULL);
        printf("(child process)wc: %d\n", wc);
    }else{
        //wait subprocess to finish, return pid of child process
        int wc = wait(NULL);
        printf("(parent process)wc: %d\n", wc);
    }
    return 0;
}

output:

(child process)wc: -1
(parent process)wc: 24428

wait函数返回值是结束的子进程的PID(进程标识符), 如果发生错误,返回-1
所以上面的程序中,主进程返回子进程的pid,而子进程中没有创建子进程,所以返回了-1.

这里再解释一下wait的参数是一个int类型的指针 int *__stat_loc,或者理解为一个数组。当子进程结束后,将子进程的状态记录到该指针所指向的内存,传入NULL,表示我并不想要子进程结束后的状态。

the sixth question

6. Write a slight modification of the previous program, this time using waitpid() instead of wait(). When would waitpid() be useful?

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"


int main(int argc, char **argv){

    int rc = fork();
    if(rc < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(rc == 0){
        int wc = wait(NULL);
        printf("(child process)wc: %d\n", wc);
    }else{
        //wait subprocess to finish, return pid of child process
        int wc = waitpid(rc, NULL, 0);
        printf("(parent process)wc: %d\n", wc);
    }
    return 0;
}

output:

(child process)wc: -1
(parent process)wc: 24603

先看一下waitpid的函数原型:

pid_t waitpid(pid_t __pid, int *__stat_loc, int __options)
库函数解释:
Wait for a child matching PID to die. 
If PID is greater than 0, match any process whose process ID is PID. 
If PID is (pid_t) -1, match any process.
If PID is (pid_t) 0, match any process with the same process group as the current process. 
If PID is less than -1, match any process whose process group is the absolute value of PID. 
If the WNOHANG bit is set in OPTIONS, and that child is not already dead, return (pid_t) 0.
If successful, return PID and store the dead child's status in STAT_LOC. Return (pid_t) -1 for errors. 
If the WUNTRACED bit is set in OPTIONS, return status for stopped children; otherwise don't. 
This function is a cancellation point and therefore not marked with __THROW.

这个函数可以指定等待的子进程的pid:
当pid为0时,匹配位于同一进程组的任意一个进程,那就和wait一样了。
当pid小于-1时,匹配绝对值为pid的进程组的任意进程(不太理解)。
如果pid大于0,就匹配指定的的子进程。
如果pid为-1,匹配任意的进程。

最后一个参数option,当WNOHANG被设置的时候(该参数是一个宏定义,如果设置,直接传进去就行了),并且子进程还没有死亡就返回0,可以用于循环检查子进程是否结束。

如果成功调用,返回子进程的pid并将子进程的状态存到STAT_LOC,发生错误返回-1.

该函数确实比wait更强大一些,当我们在主进程中创建多个子进程时,可以使用该函数在主进程中进行检查子进程的运行状态。

the seventh question

7. Write a program that creates a child process, and then in the child closes standard output (STDOUT FILENO). What happens if the child calls printf() to print some output after closing the descriptor?

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"


int main(int argc, char **argv){

    int rc = fork();
    if(rc < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(rc == 0){
        close(STDOUT_FILENO);
        printf("hello world\n");
    }else{
        //wait subprocess to finish, return pid of child process
        int wc = wait(NULL);
        printf("(parent process)wc: %d\n", wc);
    }
    return 0;
}

I don’t know what happened, seem nothing
There is no output in subprocess.

the eighth question

8. Write a program that creates two children, and connects the standard output of one to the standard input of the other, using the pipe() system call.

program:

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"


int main(int argc, char **argv){
    int f[2];
    int flag = pipe(f);
    if(flag < 0){
        fprintf(stderr, "pipe failed\n");
        exit(1);
    }

    int pid1 = fork();
    if(pid1 < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(pid1 == 0){
        char *msg = "message from process";
        write(f[1], msg, 21);
        close(f[1]);
    }

    int pid2 = fork();
    if(pid2 < 0){
        fprintf(stderr, "fork failed");
        exit(1);
    }else if(pid2 == 0){
        char rev[128];
        read(f[0], rev, 21);
        close(f[0]);
        printf("%s\n", rev);
    }
    return 0;
}

output:

message from process

pipe参数是一个大小为2的数组,目标是建立一个单向的管道可以用于进程间信息交换,参数的第一个是读,第二个是写。比较容易使用,如果要建立双向通道,需要造两个管道。

write函数原型:

ssize_t write(int __fd, const void *__buf, size_t __n)

功能是将__buf缓冲区中的n个字节写入文件描述符指向的输入流
read函数原型:

ssize_t read(int __fd, void *__buf, size_t __nbytes)

功能,将输出流中的数据写入缓冲区n个字节。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值