计算机系统篇之异常控制流(10):Chapter 8 Exceptional Control Flow 章节习题与解答
Author: stormQ
Created: Saturday, 15. August 2020 01:38PM
Last Modified: Tuesday, 22. September 2020 07:12PM
判断两个进程是否并发地运行的依据为:两者的执行时间是否存在重叠。因此,该题的解答如下:
进程对 | 是否并发地运行 |
---|---|
AB | 否 |
AC | 是 |
AD | 是 |
BC | 是 |
BD | 是 |
CD | 是 |
函数 | 返回行为 |
---|---|
setjmp | 调用一次,返回一次或多次 |
longjmp | 调用一次,从不返回 |
execve | 调用一次,从不返回 |
fork | 调用一次,返回两次 |
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。
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。
fork() 函数被调用的次数为 1,共生成了 2 个进程。
在父进程中,fork() 函数的返回值为子进程的 PID。因此,条件表达式fork() != 0
的求值结果在父进程中为 true。也就是说,父进程执行的语句依次为:std::printf("x=%d\n", ++x);
、std::printf("x=%d\n", --x);
。即父进程的输出结果依次为:x=4
、x=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
。
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。
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。
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.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 的进程图为:
"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 种,即02
、12
、0
、1
全排列的结果。因此,可能的输出结果为 A、C、E。
源码,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 次幂。
实现方式 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 的进程图为:
"a"
-------->*------------------->*
| printf exit
| |
| |
| "b" V "c"
*-------->*-------->*-------->*-------->*-------->*-------->*-------->*
main fork printf fflush waitpid printf fflush exit
所有可能的输出有以下 2 种。
输出 1:
abc
输出 2:
bac
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
子进程在发送第一个SIGUSR2
信号后,父进程接收该信号并执行信号处理程序handler
。在信号处理程序第一次运行期间,子进程发送了 4 个SIGUSR2
信号,但父进程中SIGUSR2
待处理信号的数量只有 1 个而不是相应地 4 个。这是因为,同一种类型的待处理信号至多有一个。在信号处理程序第一次运行完成后,内核发现还有一个SIGUSR2
待处理信号,于是让父进程接收该信号并执行信号处理程序。所以,虽然子进程发送了 5 个SIGUSR2
信号,但是父进程实际接收的信号数量为 2,即父进程中的信号处理程序一共调用了两次。因此,counter 的值是 2。
源码,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
tfgets() 函数的实现:
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);
}
完整的程序实现,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, 0, sizeof(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
关于在前台和后台运行程序的功能实现,见 计算机系统篇之异常控制流(7):利用 fork 和 execve 实现一个简易的 shell 程序,其余功能暂未实现。