从Crtl+C开始:
之前一直有一个问题:在shell下运行一个程序,每次想中途停止这个程序时,下意识的就会按下Ctrl+C就可以终止当前阻塞在终端的进程,Ctrl+C这个组合键按下到底都发生了什么?
其实这个操作就是向前台进程发送SIGINT信号。
以下是linux支持的信号列表:
使用kill -n pid或在代码中使用int kill(pid_t pid, int sig);可以向一个进程发送信号。
如果代码中没有显式的用signal去注册信号对应的句柄,那就会采用默认的处理方式,例如接收到SIGINT默认会将进程停止。
新的问题来了,当使用了kill、signal这些API之后,内核发生了什么?这些信号怎么能够传到指定的进程?又是怎么被处理的?
signal:这是一个signal_struct结构,里面有一个比较关键的成员wait_chldexit,指向一个等待队列,当进程调用wait或者waitpid的时候,该进程就会在这个队列上等,知道内核发现对应的子进程退出则将其唤醒
sighand:这是个大小为64的sigaction的指针数组对应上文64种信号的handler,sigaction中有一个sa_handler成员默认是0,代表采用系统默认操作处理该信号。
pending:一个sigpending结构,底下串着sigqueue,这些sigqueue就是待处理的信号了,它包含user_struct用于记录信号发起者的信息、siginfo用于记录关于信号的全部信息。
这些信息都是有默认值的,在fork的copy_process过程中,会调用copy_sighand和copy_signal来拷贝父进程的信号处理方法,如果父进程对某个信号设置了句柄,那么子进程会继承过来。如果一直父进程一直往上都没有为任何信号设置句柄,那就继承init_task设置好的值,sighand->action[n].sa_handler应该为0(SIG_DFL)。init_task初始化时信号相关成员的初始化如下:
拿子进程退出举例,在编程中一般会调用wait来处理子进程退出,一旦子进程调用了exit,父进程就会从wait返回。
了解上面task结构体中的这些和信号相关的成员,大概也可以猜到内核是怎么实现信号处理的了,实际上子进程在退出时向父进程发送SIGCHLD信号,并去唤醒父进程处理。
所以就可以猜测sys_exit系统调用肯定会构造一个sigqueue的结构,然后挂到父进程的pending里面并尝试去父进程的wait_chldexit中唤醒父进程处理。实际就是这样的,exit系统调用的通知父进程的流程大致如下:
值得一提的是SIG_IGN和SA_NOCLDWAIT可能会导致不向进程发送信号,但是子进程退出时一定会唤醒父进程。
kill系统调流程也差不多,最终也是调用__send_signal把sigqueue挂到进程的pending上。
至于信号的处理的时机,即从内核态返回用户态(从系统调用或者中断返回)的时候都在work_pending检查处理。
TIF_SIGPENDING是在__send_signal中调用complete_signal设置的。