信号
学习《Linux高性能服务器编程》第十章信号,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中
Linux信号概述、信号集、信号函数和一些疑惑。
Linux信号概述
发送信号
Linux 下,一个进程给其他进程发送信号的API是kill
函数。其定义如下:
1 2 3 4
| #include <sys/types.h> #include <signal.h>
int kill(pid_t pid, int sig);
|
这个函数把信号sig
发送给目标进程;目标进程pid
参数指定,其可能的取值以及含义如表所展示:
Linux
当中信号都大于0,如果sig
取值为0,则kill
函数不发送任何信号。这种方法可以用来检测目标进程或进程组是否存在,但是这种方法是不可靠的(这种方法不是原子操作)。
该函数成功时返回0,失败则返回-1并设置errno
。几种可能的errno
如表所示。
信号处理方式
在目标进程收到信息时,需要定义一个接收函数来处理。信号处理函数的原则如下:
1 2 3
| #include <signal.h>
typedef void (*__sighandler_t) (int);
|
信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。
除了用户自己定义信号处理函数之外,Linux当中还定义了信息号的两种其他处理方式:
1 2
| #define SIG_DFL ((__sighandler_t) 0) #define SIG_IGN ((__sighandler_t) 1)
|
SIG_IGN
表示忽略目标信号,SIG_DFL
表示使用信号默认处理方式。信号默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop),以及继续进程(Cont)。
Linux信号
在linux上,可以使用kill -l
命令看到所有的信号,但是我们并不关心这些所有的信号,只用重点关心SIGHUP
、SIGPIPE
、SIGURG
、SIGALRM
、SIGCHLD
等几个信号即可。
信号 |
起源 |
默认行为 |
含义 |
SIGHUP |
POSIX |
Term |
控制终端挂起 |
SIGPIPE |
POSIX |
Term |
往读端被关闭的管道或者socket连接中些数据 |
SIGURG |
4.2BSD |
Ign |
socket连接上接收到紧急数据 |
SIGALRM |
POSIX |
Term |
由alarm 或setitimer设置的实时闹钟超时引起 |
SIGCHLD |
POSIX |
Ign |
子进程状态发生变化(退出或者暂停) |
信号集
信号集函数
信号集sigset_t
的定义如下
1 2 3 4 5 6 7
| #define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } __sigset_t;
#endif
|
由该定义可见,sigset_t
实际上是一个长整型数组,数组的每个元素的每个位表示一个信号(虽然不知道为啥定义多个元素)。这种定义方式和文件描述符集fd_set
类似。Linux
提供了如下一组函数来设置、修改、删除和查询信号集:
1 2 3 4 5 6 7 8 9 10 11
| #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);
|
进程掩码
内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递。如果将遭阻塞的信号发给某进程,那么对该信号的传递将延后,直至从进程信号掩码中移除该信号,从而解除阻塞为止。(信号掩码实际属于线程属性,在多线程进程中,每个线程都可使用 pthread_sigmask() 函数来独立检查和修改其信号掩码。)
下面的函数可以用于设置或查看进程的信号掩码:
1 2 3 4
| #include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
|
set
参数指定新的信号掩码
oldset
参数输出原来的信号掩码(不过不为NULL)
如果set
参数不为NULL,则how
参数指定设置进程信号掩码的方式,其可选值如表所示。
如果set
为 NULL,则进程信号掩码不变,此时我们仍然可以利用oldset
参数来获得进程当前的信号掩码。
sigprocmask
成功时返回0,失败则返回-1并设置errno
。
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:
1 2 3
| #include <signal.h>
int sigpending(sigset_t *set);
|
set
用于保存被挂起的信号集。
sigpending
成功时返回0,失败则返回-1并设置errno
。
进程多次接收到同一个被挂起的信号,sigpending
函数也只能反映一次。并且,当我们再次使用sigprocmask
使能该挂起的信号时,该信号的处理函数也只被触发一次。
信号集这几个函数举例:
我们以SIGINT
和SIGQUIT
为例,就是键盘上按下(Ctrl+C)和(Ctrl+\)为例。
通过sigprocmask
设置这两个信号被挂起,然后分别按下(Ctrl+C)和(Ctrl+\),再通过sigpending
查看那些进程被挂起(这个进程可以通过kill
杀死)。
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
| #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> void printset(sigset_t *ped) { int i; for (i = 1; i < 32; i++) {
if ((sigismember(ped, i) == 1)) { putchar('1'); } else { putchar('0'); } } printf("\n"); }
int main(int argc, char const *argv[]) { sigset_t set, oldset, ped; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGQUIT); sigprocmask(SIG_BLOCK, &set, &oldset); printf("进程信号掩码:"); printset(&set); while (1) { sigpending(&ped); printf("被挂起的信号掩码:"); printset(&ped); sleep(1); } return 0; }
|
信号函数
处理或者说捕捉信号的函数有signal
和sigaction
signal系统调用
1 2 3 4 5
| #include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
|
signum
参数指出要捕获的信号类型。
handler
参数是sighandler_t
类型的函数指针,用于指定信号signum
的处理函数。
signal
函数成功时返回一个函数指针,该函数指针的类型也是sighandler_t
。这个返回值是前一次调用signal
函数时传入的函数指针,或者是信号signum
对应的默认处理函数指针SIG_DEF
(如果是第一次调用signal
的话)。
signal
系统调用出错时返回SIG_ERR
,并设置errno
。
1
| #define SIG_ERR ((__sighandler_t) -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
| #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <signal.h>
void do_sig(int a) { printf("Hi, SIGINT, how do you do !\n"); }
int main(int argc, char const *argv[]) { if (signal(SIGINT, do_sig) == SIG_ERR) { perror("signal"); exit(1); }
while (1) { printf("---------------------\n"); sleep(1); }
return 0; }
|
可以看的按下ctrl+c
之后是杀不死这个进程的,但是可以通过“ctrl+\”或者关闭shell或者通过kill命令进行杀死。
sigaction系统函数
设置信号处理函数更为健壮的方法如下
1 2 3
| #include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
|
signum
参数指出要捕获的信号类型
act
参数指定新的信号处理方式
oldact
参数输出信号之前处理的方式(如果不为NULL的话)。
act
和oldact
都是sigaction
结构体类型的指针,sigaction
结构体定义如下:
1 2 3 4 5 6 7
| struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
|
该结构体中的sa_handler
成员指定信号处理函数。
sa_mask
成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask
是信号集sigset_t
(_sigset_t
的同义词)类型,该类型指定一组信号。
sa_flags
成员用于设置程序收到信号时的行为,其可选值如表所示。
sa_restorer
成员已经过时,最好不要使用。
sigaction
成功时返回0,失败则返回-1并设置errno
。
sigaction
中有信号集,所以最好配合信号集函数一起使用。
简单是使用例子
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
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h>
void sig_int(int signo) { printf("catch signal SIGINT\n"); }
int main(int argc, char const *argv[]) { struct sigaction act;
act.sa_handler = sig_int; act.sa_flags = 0; sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, NULL);
while (1) { printf("---------------------\n"); sleep(1); }; return 0; }
|
一些疑惑
第一个疑惑是进程在处理一个信号的过程中,能接收另一个信号吗?
答案是可以的,下面的代码接收了两个SIGINT
和SIGQUIT
两个信号,在按下(Ctrl+C)后立刻按下(Ctrl+\),都能进行输出,说明进程在处理一个信号的过程中,能接收另一个信号
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 <stdio.h> #include <signal.h> #include <unistd.h>
void sig_int(int signo) { printf("catch signal SIGINT\n"); sleep(10); printf("end of SIGINT handler\n"); }
void sig_quit(int signo) { printf("catch signal SIGQUIT\n"); sleep(10); printf("end of SIGQUIT handler\n"); }
int main(int argc, char const *argv[]) { struct sigaction act1, act2;
act1.sa_handler = sig_int; sigemptyset(&act1.sa_mask); act1.sa_flags = 0;
act2.sa_handler = sig_quit; sigemptyset(&act2.sa_mask); act2.sa_flags = 0;
sigaction(SIGINT, &act1, NULL); sigaction(SIGQUIT, &act2, NULL);
while (1) { printf("---------------------\n"); sleep(1); };
return 0; }
|
第二个疑惑是信号在处理一个信号的过程中,会阻塞(挂起)这个信号吗?
个人感觉是阻塞了这个信号的。也就是第1个信号在处理的过程中,收到再多这个信号也是不会进行处理的,知道第2个信号处理完毕,后面的第2到n个信号当作一次信号进行处理(这里的信号指相同一种信息)。
我们以SIGINT
为例,当我们按下(Ctrl+C)后,SIGINT
信号的回调函数在进行处理,处理的过程中我们疯狂的按(Ctrl+C),最终后续的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
| #include <stdio.h> #include <signal.h> #include <unistd.h>
void sig_int(int signo) { printf("catch signal SIGINT\n"); sleep(5); printf("end of SIGINT handler\n"); }
int main(int argc, char const *argv[]) { struct sigaction act; act.sa_handler = sig_int; sigemptyset(&act.sa_mask); act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
while (1) { printf("---------------------\n"); sleep(1); };
return 0; }
|
第三个疑惑是信号被屏蔽之后,我们多次发送该信号,信号被挂起了,再“解挂”或者叫取消屏蔽情况会如何?
实际情况是,取消屏蔽后只会执行一次信号处理,后续的信号处理和普通信号处理相同。
我们以SIGINT
为例,先屏蔽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
| #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
void sig_int(int signo) { printf("catch signal SIGINT\n"); }
int main(int argc, char const *argv[]) { struct sigaction act; act.sa_handler = sig_int; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, NULL);
sigset_t set, oldset, ped; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_BLOCK, &set, &oldset); printf("-----begin sleep 10s--\n"); sleep(10); printf("-----end sleep 10s--\n"); sigprocmask(SIG_UNBLOCK, &set, &oldset); while (1) { printf("---------------------\n"); sleep(1); }
return 0; }
|