练习 sigaction

题目:使用 fork() 编写一个程序。子进程应打印 “hello”,父进程应打印 “goodbye”。你应该尝试确保子进程始终先打印。你能否不在父进程调用 wait() 而做到这一点呢?

方法一:

策略:可以使用 sleep 函数让父进程休眠,等待子进程先打印,父进程再打印即可。

缺点:sleep 多久合适?如果要求父进程要在子进程打印完后,随即让父进程打印呢?

方法二:

策略:使用信号,子进程打印后发出信号,父进程接收到信号后,执行打印操作。

方法二实现:

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
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

void parent_handler(int signal) {
if (signal == SIGCHLD) {
printf("byebye\n");
exit(EXIT_SUCCESS);
}
}

void set_handler() {
struct sigaction sa;
sigemptyset(&sa.sa_mask); /* clear the signal set */
sa.sa_flags = 0; /* enables setting sa_handler, not sa_action */
sa.sa_handler = parent_handler; /* specify a handler */

/* register the handler */
if (-1 == sigaction(SIGCHLD, &sa, NULL)) {
exit(EXIT_FAILURE);
}
}

int main() {
pid_t cpid = fork();

if (cpid < 0) {
exit(EXIT_FAILURE);
}

if (cpid == 0) {
printf("hello\n");
_exit(EXIT_SUCCESS);
} else {
set_handler();
while (1) { sleep(1); }
}

return 0;
}

子进程中的 _exit(0) 的作用?

当父进程完成等待时,父进程将通过调用常规退出函数来终止。相比之下,子进程通过调用 _exit 变体来终止,这会快速跟踪终止通知。实际上,子进程会告诉系统:「尽快」通知父进程,自己已经终止。

练习 exec

编写一个调用 fork() 的程序,然后调用 execl() 来运行程序 /bin/ls

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
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
pid_t cpid = fork();

if (cpid < 0) {
exit(EXIT_FAILURE);
}

if (cpid == 0) {
execl("/bin/ls", "ls", "-l", NULL); // 目录, 程序, 参数, 结束标记
/* 如果 execl 执行成功,以下代码不会被执行 */
printf("execl error\n");
_exit(EXIT_FAILURE);
} else {
wait(NULL);

exit(EXIT_SUCCESS);
}

return 0;
}

子进程调用 execl() 成功执行指定程序后,execl() 后面的代码 不会再被执行

练习 wait

现在编写一个程序,在父进程中使用 wait(),等待子进程完成。wait() 返回什么?如果在子进程中使用 wait() 会发生什么?

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
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
pid_t cpid = fork();

if (cpid < 0) {
exit(EXIT_FAILURE);
}

if (cpid == 0) {
printf("child process (PID: %d)\n", getpid());
_exit(EXIT_SUCCESS);
} else {
/* test: move to child process, start */
int wstatus;
pid_t wc = wait(&wstatus); // wait code
printf("parent process (PID: %d), wc: %d, wstatus: %d\n", getpid(), wc, wstatus);

/* 通过系统定义的宏,来检查子进程的退出状态是什么 */
if (WIFEXITED(wstatus)) {
printf("exited, status=%d\n", WEXITSTATUS(wstatus));
} else if (WIFSIGNALED(wstatus)) {
printf("killed by signal %d\n", WTERMSIG(wstatus));
} else if (WIFSTOPPED(wstatus)) {
printf("stopped by signal %d\n", WSTOPSIG(wstatus));
} else if (WIFCONTINUED(wstatus)) {
printf("continued\n");
}
/* test: move to child process, end */
exit(EXIT_SUCCESS);
}
return 0;
}

可以看出,整形指针 wstatus 用于存储子进程的返回状态——至于这些状态到底是哪个,可以通过系统定义的一些宏来检查。父进程中调用的 wait() 的返回值,正常情况下为子进程的 PID。

1
2
3
4
5
% ./a.out
child process (PID: 669052)
parent process (PID: 669051), wc: 669052, wstatus: 256
exited, status=1
%

如果将父进程中的代码块移动到子进程中,从打印可以看出,子进程异常退出,调用 wait() 失败(返回值为 -1)。

1
2
3
4
5
% ./a.out
child process (PID: 669073)
parent process (PID: 669073), wc: -1, wstatus: 0 # parent should be child
exited, status=0
%

关闭标准输出

题目:编写一个程序,创建一个子进程,然后在子进程中关闭标准输出(STDOUT_FILENO)。如果子进程在关闭描述符后调用 printf() 来打印一些输出,会发生什么?

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 <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
pid_t cpid = fork();

if (cpid < 0) {
exit(EXIT_FAILURE);
}

if (cpid == 0) {
printf("child process (PID: %d) is running.\n", getpid());
close(STDOUT_FILENO);
printf("some messages after close standard output.\n");
_exit(EXIT_SUCCESS);
} else {
wait(NULL);
exit(EXIT_SUCCESS);
}
return 0;
}

在关闭标准输出之前,子进程会正常打印内容到屏幕。当关闭了标准输出后,printf() 函数会尝试将消息写入已关闭的文件描述符,但这样的操作会失败,因为标准输出已经不可用了。因此,printf() 函数 不会在屏幕上打印任何消息

标准输出连接到标准输入

题目:编写一个程序,创建两个子进程,并使用 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
int pipefd[2]; // 0-read, 1-write
pid_t cpid1, cpid2; // 假设子进程 1 用于写,子进程 2 用于读

// 创建一个匿名管道
if (-1 == pipe(pipefd)) {
perror("pipe");
exit(EXIT_FAILURE);
}

cpid1 = fork();
if (cpid1 < 0) {
perror("fork");
exit(EXIT_FAILURE);
}

if (cpid1 == 0) { // 子进程 1
// 子进程 1 关闭未使用的读端
close(pipefd[0]);

// 将标准输出重定向到管道的写端
if (-1 == dup2(pipefd[1], STDOUT_FILENO)) {
perror("dup2");
exit(EXIT_FAILURE);
}

// 关闭多余的文件描述符
close(pipefd[1]);

execlp("ls", "ls", "-l", NULL);
perror("execlp");
_exit(EXIT_FAILURE);
}

// 父进程
cpid2 = fork();
if (cpid2 < 0) {
perror("fork");
exit(EXIT_FAILURE);
}

if (cpid2 == 0) { // 子进程 2
// 子进程 2 关闭未使用的写端
close(pipefd[1]);

// 将标准输入重定向到管道的读端
if (-1 == dup2(pipefd[0], STDIN_FILENO)) {
perror("dup2");
exit(EXIT_FAILURE);
}

// 关闭多余的文件描述符
close(pipefd[0]);

// 从标准输入(重定向的管道读端)读取数据,直到 EOF
char buf;
ssize_t bytes_read;
while ((bytes_read = read(STDIN_FILENO, &buf, sizeof(buf))) > 0) {
if (-1 == write(STDOUT_FILENO, &buf, bytes_read)) {
perror("write");
exit(EXIT_FAILURE);
}
}

_exit(EXIT_SUCCESS);
} else {
// 关闭父进程中创建的管道的两个端口
close(pipefd[0]);
close(pipefd[1]);

// 等待两个子进程完成
waitpid(cpid1, NULL, 0);
waitpid(cpid2, NULL, 0);
exit(EXIT_SUCCESS);
}

return 0;
}

本程序首先 fork 一个子进程 1,然后在父进程中再 fork 一个子进程 2。

管道重定向数据流向

子进程 1:

  1. 首先,关闭不使用的读端,将标准输出重定向到管道的写端
  2. 然后,因为后面将调用新程序,管道的写端不会再被使用,这里应提前关闭多余的写端;
  3. 最后,调用 execlp 执行 ls 命令(进程的地址空间将会被替换为新程序的地址空间)。

子进程 2:

  1. 首先,关闭不使用的写端,将标准输入重定向到管道的读端
  2. 然后,因为重定向的原因,可以提前关闭多余的读端,从标准输入读取数据(也可以先不关闭,从管道读端读取数据,读完后再关闭,但不符合本题目的要求);
  3. 最后,从标准输入读取数据流,写到标准输出,直到字节流结束。

父进程:

  1. 直接关闭不使用的管道读端和写端;
  2. 等待两个子进程的退出。

输出:

1
2
3
4
5
6
% ./a.out
总计 40
-rwxr-xr-x 1 root root 16360 4 月 26 10:24 a.out
-rw-r--r-- 1 root root 500 4 月 24 19:57 q1.c
-rw-r--r-- 1 root root 961 4 月 25 10:23 q2.c
%

重定向到文件

题目:编写一个程序,创建一个子进程,并使用 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
int pipefd[2]; // 0-read, 1-write
pid_t cpid;

/* 创建一个匿名管道 */
if (-1 == pipe(pipefd)) {
perror("pipe");
exit(EXIT_FAILURE);
}

cpid = fork();
if (cpid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}

/* 子进程往管道写入数据 */
if (cpid == 0) {
close(pipefd[0]);

char* msg[] = {
"child process: write some bytes...\n",
"child process: write some bytes, again...\n"
};
int msg_len = sizeof(msg) / sizeof(msg[0]);

// 往管道写入一些内容
for (int i = 0; i < msg_len; i++) {
if (-1 == write(pipefd[1], msg[i], strlen(msg[i]))) {
perror("write");
exit(EXIT_FAILURE);
}
}

close(pipefd[1]);
_exit(EXIT_SUCCESS);
}

/* 父进程读取管道的数据,并重定向到文件 */
close(pipefd[1]);

// 打开一个文件,作为读取管道数据的重定向文件
int redirect_fd = open("example.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (-1 == redirect_fd) {
perror("open");
exit(EXIT_FAILURE);
}

char buf;
ssize_t bytes_read;

// 从管道读取数据,并写入到重定向的文件
while ((bytes_read = read(pipefd[0], &buf, sizeof(buf))) > 0) {
if (-1 == write(redirect_fd, &buf, bytes_read)) {
perror("write");
exit(EXIT_FAILURE);
}
}

close(redirect_fd);
close(pipefd[0]);
wait(NULL);
exit(EXIT_SUCCESS);
return 0;
}

该程序,创建一个匿名管道,并 fork 出一个子进程。在子进程中,负责往管道内写入一些数据;在父进程中,负责读取管道中的数据,并将数据写入指定的文件(重定向到文件)。

输出:

1
2
3
4
5
% ./a.out
% cat example.txt
child process: write some bytes...
child process: write some bytes, again...
%