Linux 系统编程|进程与信号
Linux 系统编程
第五章 进程
5.1 进程的基本概念
一、程序与进程
程序是存放在磁盘文件中的可执行文件,本质上是代码集合。
程序的执行实例被称为进程
- 进程有独立的权限与职责,如果系统中的某个进程崩溃,不会影响到其余的进程。
- 每个进程运行在各自的虚拟地址空间中,进程之间可以通过由内核控制的机制相互通讯
每个 Linux 进程都有一个唯一的数字标识符(非负整数),称为进程 ID(process ID)。
查看进程 ID
1
ps -ef | more
二、进程在内核中的结构
进程表项 / 进程控制块task_struct
三、进程的启动与终止
启动例程
- 在进程的
main
函数执行之前内核会启动特殊的启动例程 - 编译器在编译时会将启动例程编译进可执行文件
- 启动例程的作用
- 搜集命令行参数传递给
main
函数的argc
和argv
- 搜集环境信息构建环境表并传递给
main
函数 - 登记进程的终止函数
- 搜集命令行参数传递给
进程终止
- 正常终止
- 从
main
函数返回 - 调用
exit
函数(标准 C 库<stdlib.h>
) - 调用
_exit
或_Exit
函数(系统调用) - 最后一个线程从其启动例程返回
- 最后一个线程调用
pthread_exit
- 从
- 异常终止
- 调用
abort
- 接收到一个信号并终止
- 最后一个线程对取消请求做处理响应
- 调用
进程返回
- 通常,程序运行成功返回 0,否则返回非 0。
- 在 Shell 中可以查看进程返回值
echo $?
1 | // 登记进程的终止函数 |
测试:进程终止函数的调用顺序及不同终止方式的异同
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
void fun1() { printf("term fun1\n"); }
void fun2() { printf("term fun2\n"); }
void fun3() { printf("term fun3\n"); }
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "-usage: %s [return|exit|_exit]\n", argv[0]);
exit(1);
}
atexit(fun1); atexit(fun2); atexit(fun3);
FILE *fp = fopen("./hello.txt", "w");
fprintf(fp, "hello");
if (!strcmp(argv[1], "return")) return 0;
else if (!strcmp(argv[1], "exit")) exit(0);
else if (!strcmp(argv[1], "_exit")) _exit(0);
else {
fprintf(stderr, "usage: %s file return | exit | _exit\n", argv[0]);
exit(1);
}
}
- 调用
return
或exit
时,按照fun3
、fun2
、fun1
顺序执行进程终止函数,并且成功创建文件并写入内容。- 调用
_exit
时,用户登记的终止函数将不被执行,同时由于标准 I/O 采用全缓存,而_exit
不会主动清缓存,所以文件仅被创建,而未被写入内容。
四、进程的属性和状态
通过ps -aux
或ps -ef
等命令查看进程的详细信息。
常见的信息有
缩写 | 信息 |
---|---|
USER / UID | 进程的属主(ID) |
PID | 进程的编号 |
PPID | 父进程的编号 |
%CPU | 进程占用的 CPU 百分比 |
%MEM | 进程占用的内存百分比 |
VSZ | 进程使用虚拟内存的大小 |
TTY | 终端的 ID |
START | 启动进程的时间 |
TIME | 进程消耗 CPU 的时间 |
COMMAND | 命令的名称和参数 |
其中 STAT 为进程的状态
5.2 进程编程
一、进程属性的获取
1 |
|
关于实际用户和有效用户的说明
使用账号密码登陆 Linux 系统运行程序,此即实际用户。对于一个普通的可执行程序
a.out
,使用命令sudo chown root.root a.out
将其拥有者改为超级用户root
,并增加黏着位sudo u+s a.out
,此时运行./a.out
,实际用户不变,而有效用户变为root
,黏着位的作用即是赋予普通用户更高的权限。
二、创建进程
1 |
|
fork
创建的新进程被称为子进程,该函数被调用一次,但返回两次。在子进程中返回值为 0,在父进程中返回的是子进程的 ID。
示例:fork 出子进程,并在父子进程中各自打印信息
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
int main()
{
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid > 0) {
//----- 这是父进程将执行的代码 -----
printf("I'm parent. pid = %d, ppid = %d, fork() return: %d\n",
getpid(), getppid(), pid);
//-------------------------------
} else {
//----- 这是子进程将执行的代码 -----
printf("I'm child. pid = %d, ppid = %d, fork() return: %d\n"
getpid(), getppid(), pid););
//-------------------------------
}
//----- 这是父子进程都将执行的代码 -----
printf("pid = %d\n", getpid());
sleep(1);
return 0;
//----------------------------------
}注意:
fork
后父子进程谁先运行不确定,根据系统调度决定。
当fork
成功的时候,子进程被创建,并将复制父进程的内存空间,父子进程都将从变量pid
接收到fork
的返回值那一步开始,继续往后运行。具体的复制情况如下
子进程继承来的属性
用户信息和权限、目录信息、信号信息、环境、共享存储段、资源限制、堆、栈、全局数据段、共享代码段
子进程特有的属性
进程 ID、锁信息、运行时间、未决信号
操作文件时的内核结构变化
- 子进程只继承父进程的文件描述符表,不继承但共享文件表项和 i 节点。
- 父进程创建子进程后,文件表项中的引用计数器 +1,当父进程关闭文件后,计数器 -1,但是子进程还可以操作文件。只有当计数器为 0 后才会释放文件表项。
示例:考察子进程对父进程的继承
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
int g_v = 0;
int main()
{
int l_v = 0;
static int s_v = 0;
int *h_v = (int *)malloc(5 * sizeof(int));
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork error"); exit(1);
} else if (pid > 0) {
printf("[parent] g_v@%p, l_v@%p, s_v@%p, h_v@%p\n", &g_v, &l_v, &s_v, &h_v);
g_v = 1; l_v = 2; s_v = 3;
for (int i = 0; i < 5; i++) h_v[i] = i;
} else {
printf("[child] g_v@%p, l_v@%p, s_v@%p, h_v@%p\n", &g_v, &l_v, &s_v, &h_v);
g_v = 10; l_v = 20; s_v = 30;
for (int i = 0; i < 5; i++) h_v[i] = i * 10;
}
printf("g_v = %d, l_v = %d, s_v = %d, array h_v: ", g_v, l_v, s_v);
for (int i = 0; i < 5; i++) printf("%d ", h_v[i]);
printf("\n");
sleep(1);
return 0;
}
fork 出子进程后,子进程也会拥有父进程的文件描述符和文件指针,可以对该文件进行操作。父子进程结束前,应各自关闭资源。标准 I/O 是有缓存的,该缓冲区位于堆上,如果在 fork 之前父进程用标准 I/O 写了一些内容而没有 flush,则子进程会把堆上缓冲区里的内容复制过来,从而一共输出两次。
演示:两类 I/O 方式的区别
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
int main()
{
FILE *fp = fopen("libIO.txt", "w");
int fd = open("sysIO.txt", O_WRONLY | O_CREAT | O_TRUNC, 0660);
char *s = "This is a test."; // 父进程实现写一些内容
fprintf(fp, "%s", s); // 用标准 I/O 实现,实际存进了堆上的缓冲区里
ssize_t size_s = strlen(s);
write(fd, s, size_s); // 用系统调用实现,直接写入文件
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork error"); exit(1);
} else if (pid > 0) {
char *f = "\nparent\n";
fprintf(fp, "%s", f);
ssize_t size_f = strlen(f);
write(fd, f, size_f);
fclose(fp); // 父进程关闭资源,This is a test.[/n]parent[/n] 将被写入文件
close(fd);
} else {
char *c = "\nchild\n";
fprintf(fp, "%s", c);
ssize_t size_c = strlen(c);
write(fd, c, size_c);
fclose(fp); // 子进程关闭资源,This is a test.[/n]child[/n] 将被写入文件
close(fd);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11 # libIO.txt
This is a test.
parent
This is a test.
child
# sysIO.txt
This is a test.
parent
child
fork 子进程时父子进程共享全部代码,则连续 fork 会导致进程数量指数级别增加。一般通过带有条件控制的循环构造进程链或进程扇。
构造有 $n$ 个进程的进程链
1
2
3
4
5
6
7
8
9
10 pid_t pid;
for (int i = 1; i < n; i++) { // 最开始已经有一个进程了
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid > 0) {
break; // 父进程跳出循环,子进程继续循环 fork
}
}构造有 $n$ 个进程的进程扇
1
2
3
4
5
6
7
8
9
10 pid_t pid;
for (int i = 1; i < n; i++) { // 最开始已经有一个进程了
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
break; // 子进程跳出循环,父进程继续 fork
}
}
三、三类特殊的进程
守护进程(daemon)
- 守护进程是生存周期很长的一种进程,通常在系统引导装入时启动,在系统关闭时终止。
- 所有守护进程都以超级用户优先权运行。
- 守护进程不控制终端。
- 守护进程的父进程都是 init 进程。
孤儿进程
父进程结束,子进程就称为孤儿进程,会被 1 号进程 init 进程领养。
示例:孤儿进程的领养者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main()
{
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork error"); exit(1);
} else if (pid > 0) {
printf("%d died.\n", getpid());
exit(0);
} else {
sleep(4); // 睡眠 4 秒,保证在此期间父进程已经结束
printf("pid: %d, ppid: %d", getpid(), getppid());
}
return 0;
}
僵尸进程
子进程结束但是没有释放内存中的进程表项,则称为僵尸进程。
僵尸进程不会占用很多系统资源,但会一直占有一个进程表项,而系统可供使用的进程表项数量是有限的。
避免僵尸进程的方法
- 直接杀死该进程
- 结束父进程,让僵尸进程变成孤儿进程,由 1 号进程回收释放
- 通过
wait
或waitpid
系统调用,让父进程轮询子进程是否结束,结束则通知内核回收 - 采用信号
SIGCHLD
通知父进程,并在信号处理函数中调用wait
制造一个僵尸进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
printf("pid: %d, ppid: %d\n", getpid(), getppid());
exit(0); // 子进程结束,本该由父进程通知内核回收
}
while (1) sleep(1); // 父进程一直睡眠,子进程成为僵尸进程
return 0;
}
四、wait
系统调用
1 |
|
示例:
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
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
void out_status(int status) // 解析状态码
{
printf ("status: %d\n", status); // 输出原始状态码
if (WIFEXITED(status)) { // 正常退出
printf("normal exit. return value: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { // 异常终止
printf("abnormal terminate. sig: %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) { // 暂停
printf("stopped. stop sig: %d\n", WSTOPSIG(status));
} else {
printf("unknown type\n");
}
}
int main()
{
pid_t pid;
int status;
// 正常退出
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
exit(5);
}
wait(&status);
out_status(status);
// 访问空指针,异常终止
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
int *p = NULL;
*p = 10;
}
wait(&status);
out_status(status);
// 强行 abort,异常终止
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
abort();
}
wait(&status);
out_status(status);
// 子进程暂停
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
printf("pid: %d\n", getpid()); // 输出 pid,从而发信号 kill -SIGSTOP {pid}
// 两类暂停方式皆可
// pause();
while (1) {
sleep(1);
}
}
int ret = 0; // 接收 waitpid 返回值
do {
ret = waitpid(pid, &status, WNOHANG | WUNTRACED); // 非阻塞且查看是否暂停
if (ret == 0) sleep(1); // 若返回值为 0,表示子进程尚未终止
} while (ret == 0);
out_status(status);
return 0;
}
五、exec
和system
系统调用
exec
是一族函数
当进程调用了一种
exec
函数后,另一个新程序将替换当前进程的正文、数据和堆栈。通过
exec
可以执行另一个程序,但一般通过 fork 出的子进程调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int execl(const char *pathname,
const char *arg0, .../* (char *)0 */);
int execv(const char *pathname,
char *const argv[]);
int execle(const char *pathname,
const char *arg0, .../* (char *)0, char *const envp[] */);
int execve(const char *pathname,
char *const argv[],
char *const envp[]);
int execlp(const char *pathname,
const char *arg0, .../* (char *)0 */);
int execvp(const char *pathname,
char *const argv[]);
// 出错返回 -1,成功不返回execve
函数为系统调用,其余均为库函数。六个函数的区别
- 名字中含有 l 的:形参列表从第二个参数开始是要传递给新程序的参数,第一个参数必须是程序名,最后一个参数必须是
NULL
。 - 名字中含有 p 的:第一个形参可以是绝对路径,也可以是相对路径。如果是相对路径,必须包含于程序环境表中
PATH
指定的路径中。 - 名字中含有 v 的:形参列表的第二个参数是一个字符串数组,第一个元素必须是程序名,最后一个元素必须是
NULL
。 - 名字中含有 e 的:用户可以传入自定义的环境表。
- 名字中含有 l 的:形参列表从第二个参数开始是要传递给新程序的参数,第一个参数必须是程序名,最后一个参数必须是
system
是简化版的命令执行函数
1 |
|
system
函数内部 fork 出一个子进程,由子进程调用exec
函数。- 在终端上(如 bash)执行命令
cmd
,等价于执行bash -c cmd
。
示例:编写自己的
system
函数
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
const char *cmd1 = "date > date.txt";
const char *cmd2 = "ls -al > contents.txt";
void my_system(const char *cmd)
{
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
if (execlp("/bin/bash", "bash", "-c", cmd, NULL) < 0) {
perror("execlp error");
exit(1);
}
}
wait(NULL);
}
int main()
{
system("clear");
my_system(cmd1);
my_system(cmd2);
return 0;
}
5.3 进程信号
一、信号的基本概念
信号(signal)机制是 Linux 最古老的进程间通信机制,用于解决进程在正常运行时被中断的问题,使得进程的处理流程发生变化。
信号是软件中断。
信号是异步事件
- 具有不可预见性
- 信号有自己的名称和编号
- 可自定义信号的异常处理机制
信号的来源
- 硬件来源:键盘的按键或其他硬件故障,由硬件驱动程序产生。
- 软件来源:常见的发送信号的函数有
kill
raise
alarm
setitimer
等,此外软件来源还包括非法操作(如除 0)和软件设置条件(如 gdb 调试)等。信号由内核产生。
信号无优先级。通过命令
kill -l
可查看所有信号。其中,编号 1~31 为非实时信号,发送的信号可能会丢失,且不支持信号排队;32~64 为实时信号,支持排队,发送的多个信号都一定会被接收。
一些常见的信号
编号 | 名称 | 功能 | 编号 | 名称 | 功能 |
---|---|---|---|---|---|
1 | SIGHUP | 让进程挂起的信号 | 2 | SIGINT | 让进程中断的信号 |
3 | SIGQUIT | 让进程退出的信号 | 6 | SIGABRT | 让进程强行终止的信号 |
7 | SIGBUS | 总线错误信号 | 8 | SIGFPE | 浮点运算错误信号 |
9 | SIGKILL | 杀死进程的信号 | 10 | SIGUSR1 | 用户可自定义信号 |
11 | SIGSEGV | 段错误信号 | 12 | SIGUSR2 | 用户可自定义信号 |
13 | SIGPIPE | 管道信号 | 14 | SIGALRM | 定时器发出的信号 |
17 | SIGCHLD | 子进程状态变化的信号 | 18 | SIGCONT | 让进程继续运行的信号 |
19 | SIGSTOP | 让进程暂停运行的信号 | 20 | SIGTSTP | 通过键盘暂停程序的信号 |
- control + Z:SIGTSTP
- control + C:SIGINT
- control + \:SIGQUIT
二、信号的处理方式
- 忽略信号
- SIGKILL 和 SIGSTOP 不能被忽略
- 忽略硬件异常
- SIGUSR1 和 SIGUSR2 默认情况下被忽略
- 执行默认操作
- 每个信号都有默认处理方法,大部分情况下是终止进程
- 捕获信号
- 告诉内核出现信号时调用的处理函数
- SIGKILL 和 SIGSTOP 不能被捕获
三、signal系统调用
1 |
|
示例:
signal
的使用
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
int cnt = 0;
void sig_handler(int sig)
{
printf("sig [%d] occurred\n", sig);
cnt++;
}
int main()
{
if (signal(SIGINT, sig_handler) == SIG_ERR) {
perror("signal SIGINT error");
}
if (signal(SIGTSTP, sig_handler) == SIG_ERR) {
perror("signal SIGTSTP error");
}
if (signal(SIGKILL, sig_handler) == SIG_ERR) { // 登记失败
perror("signal SIGKILL error");
}
while (cnt < 5) sleep(2);
return 0;
}
利用 SIGCHLD 信号回收子进程,避免僵尸进程
- 子进程状态发生变化(暂停 / 继续运行 / 结束)时产生该信号,父进程收到信号调用信号处理函数并通知内核回收。
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
void sig_handler(int sig)
{
printf("child exit. signal: %d\n", sig);
wait(NULL);
}
void out(int n, const char *s)
{
for (int i = 1; i <= n; i++) {
printf("%s: %d\n", s, i);
sleep(1);
}
}
int main()
{
pid_t pid;
if (signal(SIGCHLD, sig_handler) == SIG_ERR) {
perror("signal SIGCHLD error");
exit(1);
}
if ((pid = fork()) < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
out(10, "child");
} else {
out(20, "parent");
}
return 0;
}
四、信号发送
- 除了内核和超级用户,并非所有进程都可以向其他进程发信号。
- 一般的进程只能向具有相同 uid 和 gid 的进程发信号,或者向相同进程组的其他进程发信号。
1 |
|
0 为空信号,通常用来检测特定的进程是否存在。
五、alarm
系统调用
1 |
|
示例:设置周期性定时器
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
void sig_handler(int sig)
{
if (sig == SIGALRM) {
printf("time out\n");
alarm(2); // 在信号处理后再次设置定时器,实现周期性计时
}
}
void out()
{
for (int i = 1; i <= 20; i++) {
printf("[pid]%d: %d\n", getpid(), i);
sleep(1);
}
}
int main()
{
if (signal(SIGALRM, sig_handler) == SIG_ERR) {
perror("signal SIGALRM error"); exit(1);
}
printf("--- clock start ---\n");
alarm(2); // 首次设置定时器
out();
printf("--- clock stop ---\n");
return 0;
}
六、信号集
信号在内核中的表示
三个概念
- 信号递达(delivery):执行信号处理的动作
- 信号未决(pending):信号从产生到递达之间的状态
- 信号阻塞(block):被阻塞的信号产生时将处于未决状态,直至进程解除对此信号的阻塞
注意
只要信号被阻塞就不会抵达,除非解除阻塞。而忽略是在信号递达之后的一种可选的处理动作。
信号在进程表项中由三张表存储
- SIGHUP 信号不会阻塞,也未产生过。如果产生且成功递达,将执行默认操作。
- SIGINT 信号会阻塞。由其 pending 状态为 1 可知已经产生该信号,并处于阻塞状态。虽然其处理动作是忽略,但在阻塞期间,进程也可能改变处理方式。
- SIGQUIT 信号会阻塞。由其 pending 状态未 0 可知暂未产生该信号。如果产生,并且解除阻塞、成功递达,将由用户自定义的
sig_handler
处理。
信号集合类型 sigset_t
基本操作
1
2
3
4
5
6
7
8
9
int sigemptyset(sigset_t *set); // 将信号集 set 清空
int sigfillset(sigset_t *set); // 将信号集 set 填满
int sigaddset(sigset_t *set, int signo); // 添加信号
int sigdelset(sigset_t *set, int signo); // 删除信号
/* 成功返回 0,出错返回 -1 */
int sigismember(const sigset_t *set, int signo); // 判断某个新城是否在信号集中
/* 存在返回 1,不存在返回 0 */对进程中信号表的修改与读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int sigprocmask(int how, const sig_set *set, sigset_t *oset);
/*
返回值:成功返回 0,出错返回 -1
功能:读取或修改进程的 block 表
参数
- set 输入型参数,若非空,将更改进程的信号屏蔽字(mask),由参数 how 指定更改方式
- oset 是输出型参数
若 oset 非空而 set 为空,读取进程当前的信号屏蔽字由 oset 传出
若 oset 和 set 均非空,则将原来的信号屏蔽字备份到 oset 中,然后修改
- how
SIG_BLOCK 表示添加 set 中的信号屏蔽字,相当于 mask |= set
SIG_UNBLOCK 表示删除 set 中的信号,相当于 mask |= ~set
SIG_SETBLOCK 表示将信号屏蔽字设置为 set,相当于 mask = set
【注意】
信号集 sigset_t 本质上就是长整型变量。其二进制的 0/1 串对应进程的信号表
*/
int sigpending(sigset_t *sig) // 获取进程的 pending 表
/* 成功返回 0,出错返回 -1 */示例:在短时间内将把 SIGINT 信号设置为阻塞方式,连续按下 control + C 观察进程 pending 表的变化。随后解除阻塞,观察先前发送的 SIGINT 是否能被进程接收。
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
void out_sigset(sigset_t *set) // 打印 pending 表
{
for (int i = 1; i <= 31; i++) {
if (sigismember(set, i)) putchar('1');
// 通过提供的接口判断表中是否有对应信号
else putchar('0');
}
putchar('\n');
}
int main()
{
sigset_t set1, set2;
sigemptyset(&set1); // 信号集必须要初始化
sigemptyset(&set2);
sigaddset(&set1, SIGINT);
sigprocmask(SIG_BLOCK, &set1, NULL); // 将 SIGINT 设置为阻塞方式
int i = 1;
while (i < 5) {
sigpending(&set2); // 每次循环获取 pending 表
out_sigset(&set2); // 打印 pending 表
sleep(2);
i++;
}
sigprocmask(SIG_UNBLOCK, &set1, NULL); // 解除对 SIGINT 的阻塞,观察
return 0;
}
利用信号集处理信号
内核的信号捕捉机制
信号处理类型
struct sigaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct sigaction {
void (*sa_handler)(int);
// 信号处理函数指针 / SIG_IGN / SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *);
// 另一种信号处理函数,一般不用
sigset_t sa_mask;
// 信号处理期间屏蔽的信号(不包括注册的信号本身,注册的信号本身默认屏蔽)
int sa_flags;
/*
- SA_RESTART 使被信号打断的系统调用自动重新发起
- SA_NOCLDSTOP 使父进程在其子进程暂停或继续运行时不会收到 SIGCHLD 信号
- SA_NOCLDWAIT 使父进程在其子进程退出时不会收到 SIGCHLD 信号,
保证不会产生僵尸进程
- SA_NODEFER 使对注册的信号的屏蔽无效(默认情况下都是有效的)
- SA_RESETHAND 信号处理之后重新设置为默认处理方式
- SA_SIGINFO 使用 sa_sigaction 作为信号处理函数
某些系统中 sa_handler 和 sa_sigaction 会被放进联合中,不应同时设置
*/
void (*sa_restorer)(void);
// 已弃用
};signal
函数不属于 POSIX 标准,各平台上的实现不尽相同。标准的注册信号的函数为sigaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sigaction(int signum,
const struct sigaction *act,
struct sigaction *oldact);
/*
返回值:成功注册返回 0,出错返回 -1
参数
- 要注册的信号由 signum 指定
- 信号处理函数由 act->sa_handler 指定
- 信号处理期间要屏蔽的信号由 act->sa_mask 指定
- 是否在信号处理后重新发起系统调用 / 是否在一次信号处理后将处理方式改为默认
等特殊设置由 act->flags 指定,一般默认为 0
- 对该信号的原有的处理方式通过参数 oldact 传出
*/示例:使用
read
读取标准输入并输出,若输入一半受到 SIGINT 信号干扰,允许继续输入。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
void sig_handler(int sig)
{
printf("\nRead is interupted.\nContinue:\n"); // 输出受干扰信息
}
int main()
{
struct sigaction act;
act.sa_handler = sig_handler; // 设置信号处理函数
sigemptyset(&act.sa_mask); // 初始化屏蔽信号集
act.sa_flags = SA_RESTART; // 设置重新发起系统调用的方式
sigaction(SIGINT, &act, NULL); // 注册信号
char buf[512];
int n;
n = read(STDIN_FILENO, buf, 512);
if (n >= 0) {
buf[n] = '\0';
printf("input success. read %d bytes: %s\n", n, buf);
}
return 0;
}测试:设置在处理 SIGTSTP(control + Z)信号期间屏蔽 SIGQUIT(contro + \)信号。当调用信号处理函数时分别发送 SIGQUIT 和 SIGINT 信号,观察现象。
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
void sig_handler(int sig)
{
printf("interupted by sig %d\n", sig);
int i = 0;
while (i++ < 3) {
printf("handling...\n");
sleep(2);
}
printf("handle over\n");
}
int main()
{
struct sigaction act;
act.sa_handler = sig_handler;
act.sa_flags = 0; // 一般情况置零即可
sigemptyset(&act.sa_mask); // 初始化信号集
sigaddset(&act.sa_mask, SIGQUIT); // 加入信号
sigaction(SIGTSTP, &act, NULL); // 注册信号
while (1) sleep(1);
return 0;
}
/*
在信号处理函数调用期间发送信号 SIGQUIT,发现进程未退出;处理结束后,进程退出。
在信号处理函数调用期间发送信号 SIGINT,进程直接退出。
【信号屏蔽字 sa_mask 仅在信号处理期间暂时阻塞信号,处理结束后仍将递达该信号】
*/
竞态条件与sigsuspend
考察通过
alarm
实现的mysleep
函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void sig_alrm_handler(int sig)
{
// do nothing
}
void mysleep(unsigned int seconds)
{
struct sigaction act, oact;
act.sa_handler = sig_alrm_handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, &oact); // 注册信号,并保存原有的操作
alarm(seconds); // 设置定时器
pause(); // 等待
sigaction(SIGALRM, &oact, NULL); // 恢复原有的操作
return;
}- 默认情况下,SIGALRM 信号会终止进程,所以如欲响应该信号,必须注册,哪怕信号处理函数什么也不做。
- 对于
pause
系统调用,如果信号处理动作是终止进程,则进程终止,pause
也没有机会返回;如果信号处理动作是忽略,则pause
继续使进程挂起;如果信号处理动作是捕获,则在信号处理函数调用完毕后,pause
返回 -1,且errno
被设置为EINTR
。 - 这个函数的问题在于,假如在
alarm
设置定时器之后,系统转而运行优先级更高的进程,在此期间 SIGALRM 发送,则将处于未决状态。当优先级更高的进程运行完毕,SIGLARM 信号递达,直接调用信号处理函数后经内核返回用户态,则此时才执行pause
,将产生错误。
异步事件任何时候都有可能发生,这类由时序问题导致的错误称为竞态条件(race condition)。
针对上述问题,必须在设置定时器之前屏蔽 SIGALRM 信号,再挂起等待之前解除屏蔽。但是,解除屏蔽和
pause
之间,仍存在竞态条件。因此,必须将解除信号屏蔽和挂起等待合并为一个原子操作(通过一条语句实现)。sigsuspend
系统调用1
2
3
4
5
6
int sigsuspend(const sigset_t *sigmask);
/*
返回情况同 pause
参数 sigmask 指定了等待期间阻塞的信号
*/针对上例正确的做法
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
27void sig_alrm_handler(int sig) { }
void mysleep(unsigned int seconds)
{
struct sigaction act, oact;
sigset_t newmask, oldmask, susmask;
act.sa_handler = sig_alrm_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, &oact); // 注册信号同上
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM); // 设置仅含 SIGALRM 的信号集
sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 使进程屏蔽 SIGLARM
alarm(seconds);
susmask = oldmask;
sigdelset(&susmask, SIGALRM); // 从系统原有的屏蔽信号集中删去 SIGALRM
sigsuspend(&susmask);
// 保证在屏蔽原有信号的屏蔽、同时能够响应 SIGALRM 的情况下等待
sigprocmask(SIG_SETMASK, &oldmask, NULL); // 恢复进程原来的信号屏蔽字
sigaction(SIGALRM, &oact, NULL);
// 恢复进程原来对 SIGALRM 的处理方式
return;
}