计算机系统篇之异常控制流(10):Chapter 8 Exceptional Control Flow 章节习题与解答

Author: stormQ

Created: Saturday, 15. August 2020 01:38PM

Last Modified: Tuesday, 22. September 2020 07:12PM



习题


8.9 解答


8.10 解答


8.11 解答


8.12 解答



8.13 解答


8.14 解答


8.15 解答



8.16 解答



8.17 解答


8.18 解答


8.19 解答


8.20 解答


8.21 解答


8.22 解答



8.23 解答


8.24 解答


8.25 解答



8.26 解答


解答

8.9 解答

判断两个进程是否并发地运行的依据为:两者的执行时间是否存在重叠。因此,该题的解答如下:

进程对 是否并发地运行
AB
AC
AD
BC
BD
CD

返回题目


8.10 解答

函数 返回行为
setjmp 调用一次,返回一次或多次
longjmp 调用一次,从不返回
execve 调用一次,从不返回
fork 调用一次,返回两次

返回题目


8.11 解答

fork() 函数被调用的次数为 2,共生成了 4 个进程,每个进程都会打印一次“hello”。因此,“hello”输出行共 4 个。

源码,main8_11.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

int main()
{
    int i;
    for (i = 0; i < 2; i++)
    {
        fork();
    }
    std::printf("hello\n");
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_11 main8_11.cpp -g
$ ./main8_11
hello
hello
hello
hello

通过实际运行程序可以发现,“hello”输出行的个数确实为 4。

返回题目


8.12 解答

fork() 函数被调用的次数为 2,共生成了 4 个进程,每个进程都会打印两次“hello”。因此,“hello”输出行共 8 个。

源码,main8_12.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

void DoIt()
{
    fork();
    fork();
    std::printf("hello\n");
    return;
}

int main()
{
    DoIt();
    std::printf("hello\n");
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_12 main8_12.cpp -g
$ ./main8_12
hello
hello
hello
hello
hello
hello
hello
hello

通过实际运行程序可以发现,“hello”输出行的个数确实为 8。

返回题目


8.13 解答

fork() 函数被调用的次数为 1,共生成了 2 个进程。

在父进程中,fork() 函数的返回值为子进程的 PID。因此,条件表达式fork() != 0的求值结果在父进程中为 true。也就是说,父进程执行的语句依次为:std::printf("x=%d\n", ++x);std::printf("x=%d\n", --x);。即父进程的输出结果依次为:x=4x=3

在子进程中,fork() 函数的返回值为 0。因此,条件表达式fork() != 0的求值结果在子进程中为 false。也就是说,子进程执行的语句只有std::printf("x=%d\n", --x);。即子进程的输出结果为:x=2

由于子进程继承了父进程打开的文件,并且父子进程的运行是独立互不影响的。因此,所有可能的输出结果有 3 个。

输出结果 1:

x=2
x=4
x=3

输出结果 2:

x=4
x=2
x=3

输出结果 3:

x=4
x=3
x=2

源码,main8_13.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

int main()
{
    int x = 3;
    if (fork() != 0)
    {
        std::printf("x=%d\n", ++x);
    }
    std::printf("x=%d\n", --x);
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_13 main8_13.cpp -g
$ ./main8_13 
x=4
x=3
x=2

通过实际运行程序可以发现,程序的一种可能输出为输出结果 3

返回题目


8.14 解答

fork() 函数被调用的次数为 2,最多可生成 4 个进程。但是,第二次执行 fork() 函数的前提是条件表达式fork() != 0的求值结果为 ture。也就是说,父进程在调用第一个 fork() 函数后还会调用第二个 fork() 函数,而子进程不会调用第二个 fork() 函数。因此,实际的进程共 3 个。

父进程在执行完DoIt()函数中的:std::printf("hello\n");语句后就退出了。

第一个子进程在执行完main()函数中的:std::printf("hello\n");语句后就退出了。

第二个子进程在执行完DoIt()函数中的:std::printf("hello\n");语句后就退出了。

因此,“hello”输出行共 3 个。

源码,main8_14.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

void DoIt()
{
    if (fork() != 0)
    {
        fork();
        std::printf("hello\n");
        return;
    }
    return;
}

int main()
{
    DoIt();
    std::printf("hello\n");
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_14 main8_14.cpp -g
$ ./main8_14 
hello
hello
hello

通过实际运行程序可以发现,“hello”输出行的个数确实为 3。

返回题目


8.15 解答

fork() 函数被调用的次数为 2,最多可生成 4 个进程。但是,第二次执行 fork() 函数的前提是条件表达式fork() != 0的求值结果为 ture。也就是说,父进程在调用第一个 fork() 函数后还会调用第二个 fork() 函数,而子进程不会调用第二个 fork() 函数。因此,实际的进程共 3 个。

父进程在执行完DoIt()函数中的:std::printf("hello\n");语句后,继续执行main()函数中的:std::printf("hello\n");语句后退出。

第一个子进程在执行完main()函数中的:std::printf("hello\n");语句后就退出了。

第二个子进程在执行完DoIt()函数中的:std::printf("hello\n");语句后,继续执行main()函数中的:std::printf("hello\n");语句后退出。

因此,“hello”输出行共 5 个。

源码,main8_15.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

void DoIt()
{
    if (fork() != 0)
    {
        fork();
        std::printf("hello\n");
        std::exit(0);
    }
    return;
}

int main()
{
    DoIt();
    std::printf("hello\n");
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_15 main8_15.cpp -g
$ ./main8_15 
hello
hello
hello
hello
hello

通过实际运行程序可以发现,“hello”输出行的个数确实为 5。

返回题目


8.16 解答

fork() 函数被调用的次数为 1,共生成了 2 个进程。

在父进程中,fork() 函数的返回值为子进程的 PID。因此,条件表达式fork() == 0的求值结果在父进程中为 false。也就是说,父进程执行的语句依次为:wait(NULL);std::printf("counter=%d\n", ++counter);std::exit(0);

在子进程中,fork() 函数的返回值为 0。因此,条件表达式fork() == 0的求值结果在子进程中为 true。也就是说,子进程执行的语句依次为:counter--;std::exit(0);

由于父子进程的运行是独立互不影响的。也就是说,子进程对全局变量counter的修改对父进程来说是不可见的,因此,父进程中全局变量counter的最终值为 2。即程序的输出结果为counter=2

源码,main8_16.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstdlib>

int counter = 1;

int main()
{
    if (fork() == 0)
    {
        counter--;
        std::exit(0);
    }
    else
    {
        wait(NULL);
        std::printf("counter=%d\n", ++counter);
    }
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_16 main8_16.cpp -g
$ ./main8_16 
counter=2

通过实际运行程序可以发现,输出结果中全局变量counter的值确实为 2。

返回题目


8.17 解答

练习题 8.4 的进程图为:

                             "1\n"    "Bye\n"    2
                      -------->*-------->*------>*
                      |     printf    printf    exit
                      |                          |
                      |                          |
        "Hello\n"     |       "0\n"              v       "2\n"    "Bye\n"   2
  *-------->*-------->*-------->*--------------->*-------->*-------->*------>*
main    printf      fork     printf          waitpid   printf    printf    exit

所有可能的输出有以下 3 种。

输出 1:

Hello
0
1
Bye
2
Bye

输出 2:

Hello
1
0
Bye
2
Bye

输出 3:

Hello
1
Bye
0
2
Bye

返回题目


8.18 解答

习题 8.18 的进程图为:

                                          "0"                             "2"
                               ------>*------>*-------->*------>*----->*---->*
                               |   printf  fflush    exit     end  printf fflush
                               |
                               |      "1"                          "2"
            -------->*-------->*------>*-------->*-------->*-------->*----->*
            |     atexit     fork    printf    exit       end     printf  fflush
            |
            |                 "0"
            |         -------->*-------->*-------->*
            |         |     printf    fflush     exit
            |         |
            |         |        "1"
  *-------->*-------->*-------->*-------->*
main      fork      fork     printf      exit

注意: 进程图中最上面的进程也会打印 2 的原因在于其父进程中设置了exit 函数被调用时会调用的函数列表,它作为子进程拥有与父进程相同的函数列表。

所有可能的输出有 24 种,即021201全排列的结果。因此,可能的输出结果为 A、C、E。

返回题目


8.19 解答

源码,main8_19.cpp:

#include <unistd.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>

void foo(int n)
{
    int i;
    for (i = 0; i < n; i++)
    {
        fork();
    }
    std::printf("hello\n");
    std::exit(0);
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::printf("Usage: <fork-called-count>\n");
        std::exit(0);
    }
    foo(std::atoi(argv[1]));
    std::exit(0);
}

生成并运行可执行目标文件 :

$ g++ -o main8_19 main8_19.cpp -g
$ ./main8_19 1
hello
hello
$ ./main8_19 2
hello
hello
hello
hello
$ ./main8_19 3
hello
hello
hello
hello
hello
hello
hello
hello

通过实际运行程序可以发现,n 分别为 1、2、3、10 时,输出个数分别为 2、4、8、1024。可以归纳出一个结论:如果 fork() 函数被调用的次数为 n,那么生成的进程数(包括所有的进程)为 2 的 n 次幂。

返回题目


8.20 解答

实现方式 1:

#include <unistd.h>

int main(int argc, char *argv[], char *envp[])
{
    execve("/bin/ls", argv, envp);
    return 0;
}

实现方式 2:

#include <unistd.h>

int main(int argc, char *argv[])
{
    execve("/bin/ls", argv, environ);
    return 0;
}

返回题目


8.21 解答

习题 8.21 的进程图为:

                   "a"
           -------->*------------------->*
           |     printf                exit
           |                             |
           |                             |
           |        "b"                  V        "c"
 *-------->*-------->*-------->*-------->*-------->*-------->*-------->*
main     fork      printf   fflush    waitpid   printf    fflush     exit

所有可能的输出有以下 2 种。

输出 1:

abc

输出 2:

bac

返回题目


8.22 解答

mysystem 函数的实现:

int mysystem(char *command)
{
    char *myargv[4];
    myargv[0] = (char *)"/bin/bash";
    myargv[1] = (char *)"-c";
    myargv[2] = command;
    myargv[3] = NULL;

    if (fork() == 0)
    {
        execve(myargv[0], myargv, environ);
        std::printf("error:%s\n", command);
        exit(0);
    }

    int status = 0;
    Wait(&status);
    if (WIFEXITED(status))
    {
        return WEXITSTATUS(status);
    }
    if (WIFSIGNALED(status))
    {
        return WTERMSIG(status);
    }
    return -1;
}

执行一次mysystem函数,会导致两个进程被创建。一个是/bin/bash进程(父进程是执行mysystem函数的调用进程),一个是command进程(父进程是/bin/bash进程)。

注意: mysystem函数中是通过/bin/bash执行 command 的。这是因为,通过/bin/sh执行 command 时,如果 command 程序自杀或通过信号杀死command进程时(都属于 command 异常退出的情况),父进程中WIFEXITED(status)的求值结果为 true,而通过/bin/bash执行时,其求值结果为 false。另外,如果通过/bin/sh执行 command,通过信号杀死/bin/sh进程时,父进程中WIFEXITED(status)的求值结果才会为 false。

完整的程序实现,main8_22.cpp:

#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <sstream>

int Wait(int *status)
{
  int pid = 0;
  do
  {
    pid = wait(status);
  } while (-1 == pid && EINTR == errno);
  return pid;
}

int mysystem(char *command)
{
    char *myargv[4];
    myargv[0] = (char *)"/bin/bash";
    myargv[1] = (char *)"-c";
    myargv[2] = command;
    myargv[3] = NULL;

    if (fork() == 0)
    {
        execve(myargv[0], myargv, environ);
        std::printf("error:%s\n", command);
        exit(0);
    }

    int status = 0;
    Wait(&status);
    if (WIFEXITED(status))
    {
        return WEXITSTATUS(status);
    }
    if (WIFSIGNALED(status))
    {
        return WTERMSIG(status);
    }
    return -1;
}

int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        std::printf("Usage: ./main8_22 \"<command>\"\n");
        return 1;
    }
    const auto result = mysystem(argv[1]);
    std::printf("result of mysystem:%d\n", result);
    return 0;
}

测试程序,main8_26.cpp:

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        std::printf("Usage: ./main8_26 <exit_status>\n");
        std::printf("If exit_status < 0, kill itself. Otherwise, returns exit_status\n");
        return 1;
    }
    if (std::atoi(argv[1]) < 0)
    {
        kill(getpid(), SIGKILL);
    }
    return std::atoi(argv[1]);
}

编译:

$ g++ -o main8_22 main8_22.cpp -g
$ g++ -o main8_26 main8_26.cpp -g

运行,command 正常退出时:

$ ./main8_22 "./main8_26 1"
result of mysystem:1

运行,command 异常退出时:

$ ./main8_22 "./main8_26 -1"
result of mysystem:9

返回题目


8.23 解答

子进程在发送第一个SIGUSR2信号后,父进程接收该信号并执行信号处理程序handler。在信号处理程序第一次运行期间,子进程发送了 4 个SIGUSR2信号,但父进程中SIGUSR2待处理信号的数量只有 1 个而不是相应地 4 个。这是因为,同一种类型的待处理信号至多有一个。在信号处理程序第一次运行完成后,内核发现还有一个SIGUSR2待处理信号,于是让父进程接收该信号并执行信号处理程序。所以,虽然子进程发送了 5 个SIGUSR2信号,但是父进程实际接收的信号数量为 2,即父进程中的信号处理程序一共调用了两次。因此,counter 的值是 2。

返回题目


8.24 解答

源码,main8_24.cpp:

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sstream>

void foo(int exit_status)
{
    if (0 == fork())
    {
        const char *str = "abcdef";
        char *p = (char *)&str[0];
        *p = 'A';
        std::exit(exit_status);
    }
}

int main()
{
    for (int i = 0; i < 5; i++)
    {
        foo(i);
    }

    int child_pid = 0, child_status = 0;
    while ((child_pid = wait(&child_status)) > 0)
    {
        if (WIFSIGNALED(child_status))
        {
            const auto sig = WTERMSIG(child_status);
            std::stringstream str;
            str << "child " << child_pid << " terminated by signal " << sig;
            psignal(sig, str.str().c_str());
        }
    }

    return 0;
}

编译:

g++ -o main8_24 main8_24.cpp -g

运行:

$ ./main8_24 
child 13156 terminated by signal 11: Segmentation fault
child 13154 terminated by signal 11: Segmentation fault
child 13153 terminated by signal 11: Segmentation fault
child 13161 terminated by signal 11: Segmentation fault
child 13158 terminated by signal 11: Segmentation fault

返回题目


8.25 解答

tfgets() 函数的实现:

chartfgets (char *buf, int size, FILE *stream)
{
    SetSignalHanlder(SIGALRM, SigalrmHandler);
    alarm(5);
    if (1 == setjmp(env))
    {
        return NULL;
    }
    return std::fgets(buf, size, stream);
}

完整的程序实现,main8_25.cpp:

#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <setjmp.h>

#define SIZE 128

jmp_buf env;

bool SetSignalHanlder(int sig, void (*hanlder)(int))
{
  struct sigaction sa;
  std::memset(&sa, 0sizeof(sa));
  sa.sa_handler = hanlder;
  sa.sa_flags = SA_RESTART; // restart syscalls if possible
  return 0 == sigaction(sig, &sa, NULL);
}

void SigalrmHandler(int sig)
{
    if (SIGALRM != sig)
    {
        return;
    }
    longjmp(env, 1);
}

char* tfgets (char *buf, int size, FILE *stream)
{
    SetSignalHanlder(SIGALRM, SigalrmHandler);
    alarm(5);
    if (1 == setjmp(env))
    {
        return NULL;
    }
    return std::fgets(buf, size, stream);
}

int main()
{
    char buf[SIZE];
    char *input = tfgets(buf, SIZE, stdin);
    if (input)
    {
        std::printf("%s", input);
    }
    else
    {
        std::printf("tfgets returns NULL\n");
    }
    return 0;
}

编译:

$ g++ -o main8_25 main8_25.cpp -g

运行(用户在 5 秒内完成输入):

$ ./main8_25 
1234
1234

运行(用户在 5 秒内没有完成输入):

$ ./main8_25 
1234tfgets returns NULL

返回题目


8.26 解答

关于在前台和后台运行程序的功能实现,见 计算机系统篇之异常控制流(7):利用 fork 和 execve 实现一个简易的 shell 程序,其余功能暂未实现。

返回题目


下一篇:计算机系统篇之虚拟内存(3):如何使用内存映射文件在进程之间实现数据共享

上一篇:计算机系统篇之异常控制流(9):异常控制流 FAQ

首页