系列文章目录
【1】linux内核信号的处理过程
前言
从今天开始在手撸一遍以前没写过的代码同时,认真的对其中的信号知识点做一次总结,主要为纵向和横向比较
- 如何使用信号编程以及陷阱在哪里?
- 信号从产生到死亡经历了什么?
21.1 设计信号处理器函数
说到信号处理函数的设计就不得不提:线程安全 ,可重入函数 ,异步信号安全 。这是不同的三个概念。 线程安全针对竞争而言,数据需要共享就加锁,不需要共享就每个线程私有。 异步信号安全是指在线程编程时要处理异步的信号处理函数,在信号处理函数中以及线程中不管怎样调用你的函数都不会涉及可能变化的全局变量的访问,不发生死锁,就说明这个函数时异步安全的。 可重入函数一般说的可重入函数指的就是异步安全函数。事实上可重入函数又分弱可重入(线程安全),强可重入(异步信号安全)。那么一个函数时可重入的,那一定是线程安全的(反之不一定)。
笔者写了一小段测试程序来测试 sigwait 在信号处理时的一些规则。
清单 1. sigwait_test.c
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void sig_handler(int signum)
{
printf("Receive signal. %d\n", signum);
}
void* sigmgr_thread()
{
sigset_t waitset, oset;
int sig;
int rc;
pthread_t ppid = pthread_self();
pthread_detach(ppid);
sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGRTMIN+2);
sigaddset(&waitset, SIGRTMAX);
sigaddset(&waitset, SIGUSR1);
sigaddset(&waitset, SIGUSR2);
while (1) {
rc = sigwait(&waitset, &sig);
if (rc != -1) {
sig_handler(sig);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}
int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGRTMIN+2);
sigaddset(&bset, SIGRTMAX);
sigaddset(&bset, SIGUSR1);
sigaddset(&bset, SIGUSR2);
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");
kill(pid, SIGRTMAX);
kill(pid, SIGRTMAX);
kill(pid, SIGRTMIN+2);
kill(pid, SIGRTMIN);
kill(pid, SIGRTMIN+2);
kill(pid, SIGRTMIN);
kill(pid, SIGUSR2);
kill(pid, SIGUSR2);
kill(pid, SIGUSR1);
kill(pid, SIGUSR1);
pthread_create(&ppid, NULL, sigmgr_thread, NULL);
sleep(10);
exit (0);
}
程序编译运行在 RHEL4 的结果如下:
图 1. sigwait 测试程序执行结果sigwait 测试程序执行结果 从以上测试程序发现以下规则:
对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。 如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。 sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。
- Linux 多线程应用中的信号处理模型
在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用 sigwait()使用同步模型进行处理。其程序流程如下:
- 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
- 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
- 主线程创建工作线程。
图 2. 在指定的线程中以同步方式处理异步信号的模型在指定的线程中以同步方式处理异步信号的模型 代码示例 以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。
主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()。
程序编译:gcc -o signal_sync signal_sync.c -lpthread
程序执行:./signal_sync
从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。
清单 2. signal_sync.c
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void sig_handler(int signum)
{
static int j = 0;
static int k = 0;
pthread_t sig_ppid = pthread_self();
if (signum == SIGUSR1) {
printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
j++;
} else if (signum == SIGRTMIN) {
printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
k++;
}
}
void* worker_thread()
{
pthread_t ppid = pthread_self();
pthread_detach(ppid);
while (1) {
printf("I'm thread %d, I'm alive\n", ppid);
sleep(10);
}
}
void* sigmgr_thread()
{
sigset_t waitset, oset;
siginfo_t info;
int rc;
pthread_t ppid = pthread_self();
pthread_detach(ppid);
sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGUSR1);
while (1) {
rc = sigwaitinfo(&waitset, &info);
if (rc != -1) {
printf("sigwaitinfo() fetch the signal - %d\n", rc);
sig_handler(info.si_signo);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}
int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGUSR1);
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");
pthread_create(&ppid, NULL, sigmgr_thread, NULL);
for (i = 0; i < 5; i++) {
pthread_create(&ppid, NULL, worker_thread, NULL);
}
for (i = 0; i < 50; i++) {
kill(pid, SIGUSR1);
printf("main thread, send SIGUSR1 No. %d\n", i);
kill(pid, SIGRTMIN);
printf("main thread, send SIGRTMIN No. %d\n", i);
sleep(10);
}
exit (0);
}
-
注意事项 在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()、 sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:
- 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
- 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
- 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
- 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
- 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。
-
小结 在开发 Linux 多线程应用中, 如果因为程序逻辑需要引入信号, 在信号处理后程序仍将继续正常运行。在这种背景下,如果以异步方式处理信号,在编写信号处理函数一定要考虑异步信号处理函数的安全; 同时, 程序中一些库函数可能会被信号中断,错误返回,这时需要考虑对 EINTR 的处理。另一方面,也可考虑使用上文介绍的同步模型处理信号,简化信号处理函数的编写,避免因为信号处理函数执行上下文的不确定性而带来的风险。
22.3 可中断和不可中断的进程睡眠状态
由内核可知当使用阻塞式IO的时候,进程可分为可中断唤醒和不可中断唤醒,都是指的信号中断。
5. 信号从产生到死亡经历了什么?
22.5 信号的同步生成和异步生成
信号无论是从其他进程发送过来,还是由内核发送过来,都是无法预测的,都属于异步产生。然而有时候信号的产生者是自身信号并发送给自己。
- 执行特定的机器语言指令,可导致硬件异常
- 进程可使用kill,raise,killpg向自身发送信号
以上这两种信号就是同步产生。对于同步产生的信号,其行为不仅可以预测还可以重现。
22.6 信号传递的时机与顺序
- 进程再调度超时后,再度获得调度时(即一个时间片开始时)
- 系统调用完成时
22.6.1 进程如何发现和接受信号
这里主要讨论异步信号的传递。一个进程的不可能等待信号的到来也不知道信号什么时候到来。**信号的传递不是由进程传递的,而是由内核代理,**例如,进程p2向进程p1发送信号,内核将信号放在p1的信号队列中,等p1再次被调度时会首先执行,会调用do_notify_resume()来处理信号队列中的信号。信号处理主要就是调用sighand_struct结构中对应的信号处理函数。do_notify_resume()(arch/arm/kernel/signal.c)函数的定义如下:linux内核中的信号机制–信号处理
asmlinkage void
do_notify_resume(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
if (thread_flags & _TIF_SIGPENDING)
do_signal(¤t->blocked, regs, syscall);
}
22.6.2 信号检测和响应时机
[root@localhost stack_dump]# cat /proc/20254/task/20255/stack
[<ffffffff81432637>] __skb_recv_datagram+0x237/0x290
[<ffffffff8149861b>] udp_recvmsg+0x8b/0x310
[<ffffffff8149f85a>] inet_recvmsg+0x5a/0x90
[<ffffffff81426693>] sock_recvmsg+0x133/0x160
[<ffffffff81426d5e>] sys_recvfrom+0xee/0x180
[<ffffffff8100b0d2>] system_call_fastpath+0x16/0x1b
[<ffffffffffffffff>] 0xffffffffffffffff
[root@localhost stack_dump]# kill -11 20255
[root@localhost stack_dump]# Stack trace (non-dedicated):
./funstack() [0x400c16]
/lib64/libpthread.so.0() [0x31cf40f7e0]
/lib64/libpthread.so.0(recvfrom+0x33) [0x31cf40eca3]
./funstack(func1+0xd8) [0x400de5]
./funstack(test_func+0xe) [0x400e11]
./funstack(task_entry+0x16) [0x400e29]
/lib64/libpthread.so.0() [0x31cf407aa1]
/lib64/libc.so.6(clone+0x6d) [0x31ce8e893d]
End of stack trace
recvfrom over
经过实验可知,信号检测并非再系统调用返回前夕 ,而是在本进程再次获得调度机会时 。
22.6.3 进入信号处理函数
由以上可知,信号处理函数在用户态,但是发现信号处理函数在内核态,如何进程状态变换呢? 有以下图可知,系统先从内核态转到用户态,再回到内核态,再跳到用户态。 如图中所见,处理信号的整个过程是这样的:进程由于 系统调用或者中断 进入内核,完成相应任务返回用户空间的前夕,检查信号队列,如果有信号,则根据信号向量表找到信号处理函数,设置好“frame ”(栈帧)后,跳到用户态执行信号处理函数。信号处理函数执行完毕后,返回内核态,设置“frame”,再返回到用户态继续执行程序。
22.6.4 为什么要设置frme,为什么执行完信号函数之后还要回到内核态?
什么叫Frame?
在调用一个子程序时,堆栈要往下(逻辑意义上是往上)伸展,这是因为需要在堆栈中保存子程序的返回地址,还因为子程序往往有局部变量,也要占用堆栈中的空间。此外,调用子程序时的参数也是在堆栈中。子程序调用嵌套越深,则堆栈伸展的层次也越多。在堆栈中的每一个这样的层次,就称为一个”框架”,即frame。
一般来说,当子程序和调用它的程序在同一空间中时,堆栈的伸展,也就是堆栈中框架的建立,过程主要如下:
call指令将返回地址压入堆栈(自动) 用push指令压入调用参数 调整堆栈指针来分配局部变量 为什么以及怎么设置frame?
我们知道,当进程陷入内核态的时候,会在堆栈中保存中断现场。因为用户态和内核态是两个运行级别,所以要使用两个不同的栈。当用户进程通过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:(图来自《Linux内核完全注释》) 在处理完系统调用以后,就要调用do_signal()函数进行设置frame等工作。这时内核堆栈的状态应该跟下图左半部分类似(系统调用将一些信息压入栈了): 在找到了信号处理函数之后,do_signal 函数首先把内核堆栈中存放返回执行点的 eip 保存为old_eip,然后将 eip 替换为信号处理函数的地址,然后将内核中保存的“原ESP”(即用户态栈地址)减去一定的值,目的是扩大用户态的栈,然后将内核栈上的内容保存到用户栈上,这个过程就是设置frame.值得注意的是下面两点:
之所以把EIP的值设置成信号处理函数的地址,是因为一旦进程返回用户态,就要去执行信号处理程序,所以EIP要指向信号处理程序而不是原来应该执行的地址。
之所以要把 frame 从内核栈拷贝到用户栈,是因为进程从内核态返回用户态会清理这次调用所用到的内核栈(类似函数调用),内核栈又太小,不能单纯的在栈上保存另一个frame(想象一下嵌套信号处理),而我们需要EAX(系统调用返回值)、EIP这些信息以便执行完信号处理函数后能继续执行程序,所以把它们拷贝到用户态栈以保存起来。
以上这些搞清楚之后,下面的事情就顺利多了。这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?
22.6.5 信号处理函数执行完后怎么办?
信号处理程序执行完毕之后,进程会主动调用 sigreturn() 系统调用再次回到内核(可以通过strace来进行验证),查看有没有其他信号需要处理,如果没有,这时内核就会做一些善后工作,将之前保存的frame恢复到内核栈,恢复eip的值为old_eip,然后返回用户空间,程序就能够继续执行。至此,内核遍完成了一次(或几次)信号处理工作。
总结
|