0%

信号相关

大部分都是参考manual

1. 整体介绍

1.1. 信号默认处理动作

  • Term: 中止进程
  • Ign: 忽略信号
  • Core: 中止进程并转储文件
  • Stop: 暂停进程
  • Cont: 继续当前暂停的进程

除此之外还可以自定义处理函数,处理函数一般是在当前进程栈,也可以用sigaltstack(2)设置信号处理函数使用另一个栈。

1.2. 发送信号

  • rasie(3): 向当前线程
  • kill(2): 向指定进程、指定进程组或系统上的所有进程
  • killpg(2): 向指定进程组的所有成员
  • pthread_kill(3): 向当前进程的指定POSIX线程
  • tgkill(2): 向指定进程的指定线程(实现pthread_kill(3)的系统调用)
  • sigqueue(3): 向指定进程发送一个带附加数据的实时信号

1.3. 等待信号

  • pause(2): 暂停执行,直到捕获任何信号
  • sigsuspend(2): 暂时改变信号掩码,暂停执行,直到未屏蔽的信号被捕获

1.4. 同步接收信号

除了自定义信号处理函数来异步接收信号,还可以同步的接收信号

  • sigwaitinfo(2), sigtimedwait(2), sigwait(2)会暂停执行,直到捕获一个信号
  • signalfd(2)会返回一个文件句柄,告诉调用者当前接收到的信号,使用read(2)可以读出期望收到的信号,信号用结构体描述。

1.5. 信号掩码和挂起

可以阻塞(屏蔽)信号,也就是说,信号发生时先不分发,直到阻塞解除。在阻塞和解除之间的这段时间,称为信号挂起(pending)。

进程中的每个线程都有一个独立的信号掩码,表示当前线程正在阻塞哪些信号。线程用pthread_sigmask(3)来调整信号掩码。在单线程中,可以用sigprocmask(2)

fork(2)的子进程会继承父进程的信号掩码;execve(2)后信号掩码也会保留。

可以向一个进程或一个线程发送信号,向进程发送信号时,系统会发给进程中没有屏蔽该信号的线程,如果有多余1个线程都没屏蔽,系统会随机选一个线程发送。

线程可以使用sigpending(2)获取当前正被挂起的信号,返回的集合中包含向进程整体发送的信号和向当前调用线程发送的信号。

fork(2)的子进程刚开始没有正被挂起的信号,execve(2)后正被挂起的信号也会保留。

1.6. 标准信号和实时信号的区别

  • 同一类型的多个实时信号会排队,同一类型的多个标准信号只会通知1次,不会通知多次;
  • 实时信号发送时可以外带用户数据,标准信号就不行;
  • 实时信号发送时的顺序是确定的,多个同一类型的实时信号按发送时的顺序通知,不同类型的多个实时信号从编号小的(优先级高)信号开始通知;多个标准信号被阻塞时,通知顺序不确定。

1.7. 异步信号安全函数

如果信号中断了一个不安全函数的执行,并且handler调用了不安全函数,则程序行为是未定义的。POSIX有个“安全函数”的概念,规定可以在handler中安全调用的函数有:

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
_Exit()
_exit()
abort()
accept()
access()
aio_error()
aio_return()
aio_suspend()
alarm()
bind()
cfgetispeed()
cfgetospeed()
cfsetispeed()
cfsetospeed()
chdir()
chmod()
chown()
clock_gettime()
close()
connect()
creat()
dup()
dup2()
execle()
execve()
fchmod()
fchown()
fcntl()
fdatasync()
fork()
fstat()
fsync()
ftruncate()
getegid()
geteuid()
getgid()
getgroups()
getpeername()
getpgrp()
getpid()
getppid()
getsockname()
getsockopt()
getuid()
kill()
link()
listen()
lseek()
lstat()
mkdir()
mkfifo()
open()
pause()
pipe()
poll()
posix_trace_event()
pselect()
raise()
read()
readlink()
recv()
recvfrom()
recvmsg()
rename()
rmdir()
select()
sem_post()
send()
sendmsg()
sendto()
setgid()
setpgid()
setsid()
setsockopt()
setuid()
shutdown()
sigaction()
sigaddset()
sigdelset()
sigemptyset()
sigfillset()
sigismember()
signal()
sigpause()
sigpending()
sigprocmask()
sigqueue()
sigset()
sigsuspend()
sleep()
sockatmark()
socket()
socketpair()
stat()
symlink()
tcdrain()
tcflow()
tcflush()
tcgetattr()
tcgetpgrp()
tcsendbreak()
tcsetattr()
tcsetpgrp()
time()
timer_getoverrun()
timer_gettime()
timer_settime()
times()
umask()
uname()
unlink()
utime()
wait()
waitpid()
write()
execl()
execv()
faccessat()
fchmodat()
fchownat()
fexecve()
fstatat()
futimens()
linkat()
mkdirat()
mkfifoat()
mknod()
mknodat()
openat()
readlinkat()
renameat()
symlinkat()
unlinkat()
utimensat()
utimes()

1.8. 系统接口和库函数被信号处理函数中断

如果在系统接口和库函数在被阻塞调用的时候,触发了信号处理,可能会有如下行为:

  • 信号处理返回时,调用自动恢复;
  • 调用立即返回EINTR错误;

具体发生哪种,需要看是哪个接口和使用sigaction(2)时是否设置了SA_RESTART标记,不同平台上的细节也不同,linux平台的细节如下,

阻塞在如下接口时,如果设置了SA_RESTART,处理完信号后会自动恢复;如果没设置,会返回EINTR errno:

  • 在”慢”设备上调用read(2), readv(2), write(2), writev(2), and ioctl(2)时,”慢”设备是那些可能一直阻塞的设备,比如终端,pipe,socket等(磁盘设备不是慢设备)。
  • open(2)
  • wait(2), wait3(2), wait4(2), waitid(2), and waitpid(2)
  • 未设置超时的socket接口:accept(2), connect(2), recv(2), recvfrom(2), recvmsg(2), send(2), sendto(2), and sendmsg(2)
  • 文件锁接口: flock(2) and fcntl(2) F_SETLKW
  • POSIX消息队列: mq_receive(3), mq_timedreceive(3), mq_send(3), and mq_timedsend(3)
  • futex(2) FUTEX_WAIT
  • POSIX信号量:sem_wait(3) and sem_timedwait(3)

阻塞在如下接口时,总是返回EINTR,忽略SA_RESTART标记:

  • socket接口:使用setsockopt(2)accept(2), recv(2), recvfrom(2), and recvmsg(2)设置接收超时时间时(SO_RCVTIMEO), 给connect(2), send(2), sendto(2), and sendmsg(2)设置发送超时时间时(SO_SNDTIMEO)
  • 等待信号的接口: pause(2), sigsuspend(2), sigtimedwait(2), and sigwaitinfo(2).
  • 文件描述符多路复用: epoll_wait(2), epoll_pwait(2), poll(2), ppoll(2), select(2), and pselect(2).
  • systemV IPC接口: msgrcv(2), msgsnd(2), semop(2), and semtimedop(2)
  • sleep接口: clock_nanosleep(2), nanosleep(2), and usleep(3)
  • inotify(7)文件描述符上read(2)
  • io_getevents(2)

1.9. 系统接口和库函数被停止信号中断

linux上有些调用,即使没有信号处理函数,也会因为停止信号而返回失败,并设置EINTR,然后通过SIGCONT恢复运行:

  • socket接口:使用setsockopt(2)accept(2), recv(2), recvfrom(2), and recvmsg(2)设置接收超时时间时(SO_RCVTIMEO), 给connect(2), send(2), sendto(2), and sendmsg(2)设置发送超时时间时(SO_SNDTIMEO)
  • epoll_wait(2), epoll_pwait(2)
  • semop(2), semtimedop(2)
  • sigtimedwait(2), sigwaitinfo(2)
  • inotify(7)文件描述符上read(2)

2. 基本使用

2.1. 设置信号处理动作

有两个:signal(2)sigaction(2),前者不标准,可移植性差,后者是POSIX标准的接口,可移植性好。后者使用上更灵活。

1
2
3
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

不同UNIX版本之间该函数的行为不太一致,linux不同历史版本之间也不移植,可移植性低,要避免使用。

设置某一信号signum的处理函数为handlerhandler可以是SIG_IGN, SIG_DEL或者用户自定义的处理函数地址。

如果在用户自定义的处理函数中阻塞了信号,那么该信号在处理函数返回前还是未阻塞状态,直到处理函数返回才阻塞。也就是说,在处理函数返回前,系统仍然会收到该信号,并且会触发处理函数。

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
#include <signal.h>

siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
}

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

有些体系上 struct sigaction使用 union 实现,所以不要同时设置sa_handlesa_sigaction
sa_restorer被废弃不再使用。
如果在sa_flags上设置了SA_SIGINFO,会使用sa_sigaction,而不是sa_handler来处理信号,触发的信号值在第一个参数返回,第三个参数是ucontext_t结构体,但一般不用。

sa_mask指定了要被阻塞的信号集。
sa_flags可以修改信号的行为,可以用OR来一起使用:

  • SA_NOCLDSTOP: 如果signumSIGCHLD, 不会收到子进程停止的信号。
  • SA_NOCLDWAIT: 如果signumSIGCHLD,不会在子进程中止的时候转换成僵尸进程
  • SA_NODEFER: 设置了,可以继续接收处理该信号,不会被阻塞。SA_NOMASK被废弃
  • SA_ONSTACK: 在sigaltstack(2)提供的栈上调用信号处理函数,而不是当前用户栈
  • SA_RESETHAND: 进入信号处理函数后,恢复默认处理逻辑。SA_ONESHOT被废弃
  • SA_RESTART: 提供跟BSD兼容的信号中断恢复语义(如何继续系统调用)
  • SA_SIGINFO: 使用3个参数的sa_sigaction回调,而不是1个参数的sa_handler回调。

2.2. 阻塞信号

有两个接口: sigprocmask(2)pthread_sigmask(3),前者只适用于单线程,多线程使用后者。

1
2
3
4
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); // Compile and link with -pthread.
  • how: SIG_BLOCK(新增), SIG_UNBLOCK(删除), SIG_SETMASK(重置),
  • oldset: 不为空,表示返回之前旧的信号屏蔽集合

2.3. 对信号集的操作

1
2
3
4
5
6
7
#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

需要注意的是,set必须使用sigemptyset()sigfillset()初始化。