Linux 中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以不能携带很大的信息量,但是信号在系统中的优先级是非常高的。
在 Linux 中的很多常规操作中都会有相关的信号产生,先从熟悉的场景说起: (1)通过键盘操作产生了信号 用户按下 Ctrl-C
,这个键盘输入产生一个硬件中断,使用这个快捷键会产生信号,这个信号会杀死对应的某个进程 (2)通过 shell
命令产生了信号 通过 kill
命令终止某一个进程,kill -9 进程PID
(3)通过函数调用产生了信号 如果 CPU 当前正在执行这个进程的代码调用(如:函数 sleep()
进程收到相关的信号,被迫挂起) (4)通过对硬件进行非法访问产生了信号 正在运行的程序访问了非法内存,发生段错误,进程退出。
信号也可以实现进程间通信,但是信号能传递的数据量很少,不能满足大部分需求。另外信号的优先级很高,并且它对应的处理动作是回调完成的,它会打乱程序原有的处理流程,影响到最终的处理结果。因此,不建议使用信号进行进程间通信。
信号 信号编号 通过 kill -l
命令查看系统定义的信号列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
信号产生的时机和对应的默认处理动作,参考表格如下:
查看信号信息 通过 Linux 提供的 man 文档可以查询所有信号的详细信息:
在信号描述中介绍了对产生的信号的五种默认处理动作,分别如下: (1)Term 信号将进程终止 (2)Ign 信号产生之后默认被忽略 (3)Core 信号将进程终止,并且生成一个 core 文件(一般用于 gdb 调试) (4)Stop 信号会暂停进程的运行 (5)Cont 信号会让暂停的进程继续运行
信号的状态 Linux 中的信号有三种状态,分别为:产生、未决、递达。 产生:键盘输入,函数调用,执行 shell 命令,对硬件进行非法访问都会产生信号 未决:信号产生了,但是这个信号还没有被处理掉,这个期间信号的状态称之为未决状态 递达:信号被处理了(被某个进程处理掉)
信号相关函数 Linux 中能够产生信号的函数有很多,介绍几个常用函数:
kill/raise/abort 这三个函数的功能比较类似,可以发送相关的信号给到对应的进程。
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> int kill (pid_t pid, int sig) ;#include <signal.h> int raise (int sig) ;#include <stdlib.h> void abort (void ) ;
定时器 alarm alarm()
函数只能进行单次定时,定时完成发射出一个信号。
1 2 3 4 5 6 7 8 9 10 #include <unistd.h> unsigned int alarm (unsigned int seconds) ;
使用这个定时器函数alarm()
,检测一下当前计算机 1s 钟之内能数多少个数,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { alarm (1 ); int i = 0 ; while (1 ) { printf ("%d\n" , i++); } return 0 ; }
执行示例代码的时候,计算一下时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ time ./a.out real 0m1.013s user 0m0.060s sys 0m0.324s $ time ./a.out > a.txt Alarm clock real 0m1.002s user 0m0.740s sys 0m0.236s real = user + sys + 消耗的时间(频率的从用户区到内核区进程切换)
文件 IO 操作需要进行用户区到内核区的切换,处理方式不同,二者之间切换的频率也不同。
setitimer setitimer()
函数可以进行周期性定时,每触发一次定时器就会发射出一个信号。
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 <sys/time.h> struct itimerval { struct timeval it_interval; struct timeval it_value; };struct timeval { time_t tv_sec; suseconds_t tv_usec; };int setitimer (int which, const struct itimerval *new_value, struct itimerval *old_value) ;
信号集 阻塞/未决信号集 在 PCB
中有两个非常重要的信号集。一个称为 “阻塞信号集”,另一个称为 “未决信号集”。这两个信号集体现在内核中就是两张表。但是操作系统不允许我们直接对这两个信号集进行任何操作,而是需要自定义另外一个集合,借助信号集操作函数来对 PCB
中的这两个信号集进行修改。 (1)信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。 (2)信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。一般情况下信号的阻塞只是暂时的,只是为了防止信号打断某些敏感的操作。
阻塞信号集和未决信号集在内核中的结构是相同的,它们都是一个整形数组(被封装过的,一共 128 字节,int [32] == 1024 bit)。 1024 个标志位,其中前 31 个标志位,每一个都对应一个 Linux 中的标准信号,通过标志位的值来标记当前信号在信号集中的状态。
1 2 3 4 5 6 7 8 信号 标志位(低地址位 -> 高地址位) 1 -> 0 2 1 3 2 4 3 31 30
(1)在阻塞信号集中,描述这个信号是否被阻塞 默认情况下没有信号是被阻塞的,这个信号对应的标志位的值为 0。 如果某个信号被设置为了阻塞状态,这个信号对应的标志位被设置为 1。 (2)在未决信号集中,描述信号是否处于未决状态 如果这个信号被阻塞,不能处理,这个信号对应的标志位被设置为 1。 如果这个信号的阻塞被解除,未决信号集中的这个信号马上就被处理了,这个信号对应的标志位值变为 0。 如果这个信号没有阻塞,信号产生之后直接被处理,因此不会在未决信号集中做任何记录。
信号集函数 因为用户是不能直接操作内核中的阻塞信号集和未决信号集的,需要调用系统函数。阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作。
读/写阻塞信号集,函数原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <signal.h> int sigprocmask (int how, const sigset_t *set, sigset_t *oldset) ;
sigprocmask()
函数有一个 sigset_t
类型的参数,对这种类型的数据进行初始化需要调用一些相关的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #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) ;
未决信号集不需要程序员修改。如果设置了某个信号阻塞,当这个信号产生之后,内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞,未决信号集中的信号随之被处理,内核再次修改未决信号集将该信号的状态修改为递达状态(标志位置 0)。因此,写未决信号集的动作都是内核做的。
这是一个读未决信号集的操作函数:
1 2 3 4 #include <signal.h> int sigpending (sigset_t *set) ;
信号集操作函数的使用,示例代码如下:
1 2 3 4 5 需求:在阻塞信号集中设置某些信号阻塞,通过一些操作产生这些信号,然后读未决信号集,最后再解除这些信号的阻塞 假设阻塞这些信号: 2号信号 SIGINT: ctrl+c 3号信号 SIGQUIT: ctrl+\ 9号信号 SIGKILL: shell 命令给进程发送这个信号 kill -9 PID
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <signal.h> int main () { sigset_t myset; sigemptyset (&myset); sigaddset (&myset, SIGINT); sigaddset (&myset, SIGQUIT); sigaddset (&myset, SIGKILL); sigset_t old; sigprocmask (SIG_BLOCK, &myset, &old); int i = 0 ; while (1 ) { sigset_t curset; sigpending (&curset); for (int i = 1 ; i < 32 ; ++i) { int ret = sigismember (&curset, i); printf ("%d" , ret); } printf ("\n" ); sleep (1 ); i++; if (i == 10 ) { sigprocmask (SIG_SETMASK, &old, NULL ); } } return 0 ; }
通过测试得到结论:程序中对 9 号信号的阻塞是无效的,因为它无法被阻塞。 一张图总结这些信号集操作函数之间的关系:
信号捕捉 Linux 中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略这个信号或者修改某些信号的默认行为需要在程序中捕捉该信号。程序中进行信号捕捉是一个注册的动作,提前告诉应用程序信号产生之后做什么样的处理,当进程中对应的信号产生了,这个处理动作也就被调用了。
signal 使用 signal()
函数可以捕捉进程中产生的信号,并且修改捕捉到的函数的行为。这个信号的自定义处理动作是一个回调函数,内核通过 signal()
得到这个回调函数的地址,在信号产生之后该函数会被内核调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <signal.h> sighandler_t signal (int signum, sighandler_t handler) ;typedef void (*sighandler_t ) (int ) ;
使用 signal()
函数来捕捉定时器产生的信号 SIGALRM
,示例代码如下:
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 <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/time.h> #include <signal.h> void doing (int arg) { printf ("当前捕捉到的信号是:%d\n" , arg); }int main () { signal (SIGALRM, doing); struct itimerval newact; newact.it_value.tv_sec = 3 ; newact.it_value.tv_usec = 0 ; newact.it_interval.tv_sec = 1 ; newact.it_interval.tv_usec = 0 ; setitimer (ITIMER_REAL, &newact, NULL ); while (1 ) { sleep (1000000 ); } return 0 ; }
sigaction sigaction()
函数和 signal()
函数的功能是一样的,用于捕捉进程中产生的信号,并将用户自定义的信号行为函数(回调函数)注册给内核,内核在信号产生之后调用这个处理动作。sigaction()
可以看做是 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 30 31 32 33 34 35 36 37 38 39 #include <signal.h> int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact) ;struct sigaction { void (*sa_handler)(int ); void (*sa_sigaction)(int , siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void ); };
通过 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 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <signal.h> void callback (int num) { printf ("当前捕捉的信号:%d\n" , num); }int main () { sigset_t myset; sigemptyset (&myset); sigaddset (&myset, SIGINT); sigaddset (&myset, SIGQUIT); sigaddset (&myset, SIGKILL); struct sigaction act; act.sa_handler = callback; act.sa_flags = 0 ; sigemptyset (&act.sa_mask); sigaction (SIGINT, &act, NULL ); sigaction (SIGQUIT, &act, NULL ); sigaction (SIGKILL, &act, NULL ); sigset_t old; sigprocmask (SIG_BLOCK, &myset, &old); int i = 0 ; while (1 ) { sigset_t curset; sigpending (&curset); for (int i = 1 ; i < 32 ; ++i) { int ret = sigismember (&curset, i); printf ("%d" , ret); } printf ("\n" ); sleep (1 ); i++; if (i == 10 ) { sigprocmask (SIG_SETMASK, &old, NULL ); } } return 0 ; }
通过测试得到结论:程序中对 9 号信号的捕捉是无效的,因为它无法被捕捉。
SIGCHLD 信号 当子进程退出、暂停、从暂停恢复运行的时候,在子进程中会产生一个 SIGCHLD
信号,并将其发送给父进程,但是父进程收到这个信号之后默认忽略了。可以在父进程中对这个信号加以利用,基于这个信号来回收子进程的资源,因此需要在父进程中捕捉子进程发送的这个信号。
基于信号回收子进程资源,示例代码如下:
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #include <signal.h> void recycle (int num) { printf ("捕捉到的信号是:%d\n" , num); while (1 ) { pid_t pid = waitpid (-1 , NULL , WNOHANG); if (pid > 0 ) { printf ("child died, pid = %d\n" , pid); } else if (pid == 0 ) { break ; } else if (pid == -1 ) { printf ("所有子进程都回收完毕...\n" ); break ; } } }int main () { sigset_t myset; sigemptyset (&myset); sigaddset (&myset, SIGCHLD); sigprocmask (SIG_BLOCK, &myset, NULL ); pid_t pid; for (int i = 0 ; i < 20 ; ++i) { pid = fork(); if (pid == 0 ) { break ; } } if (pid == 0 ) { printf ("child process, pid = %d\n" , getpid ()); } else if (pid > 0 ) { printf ("parent process, pid = %d\n" , getpid ()); struct sigaction act; act.sa_flags =0 ; act.sa_handler = recycle; sigemptyset (&act.sa_mask); sigaction (SIGCHLD, &act, NULL ); sigprocmask (SIG_UNBLOCK, &myset, NULL ); while (1 ) { sleep (100 ); } } return 0 ; }
参考资料 https://subingwen.cn/linux/signal