一文详解Linux内核-信号的产生过程

电子说

1.3w人已加入

描述

1 `specific_send_sig_info()`函数

2 send_signal()函数

3 group_send_sig_info()函数

许多内核函数产生信号:它们完成信号处理的第一阶段,也就是更新一个或多个进程描述符。她们不会直接执行第二阶段的信号处理,也就是传递信号;但是,依赖信号类型和目标进程的状态,可能会唤醒某些进程并强制它们接收信号。

信号的发送,可以是内核,也可以是其它进程,内核使用下表中的函数产生信号。

表11-9 为进程产生信号的内核函数

函数名称 描述
send_sig() 给单个进程发送信号
send_sig_info() 与send_sig()类似,在siginfo_t中带有扩展信息
force_sig() 发送不能被进程显式忽略或阻塞的信号
force_sig_info() 类似force_sig(),在siginfo_t中带有扩展信息
force_sig_specific() 类似force_sig(),但是针对SIGSTOP和SIGKILL信号进行了优化
sys_tkill() tkill()系统调用处理程序
sys_tgkill() tgkill()系统调用处理程序

表格11-9中所有函数最后都会调用specific_send_sig_info()函数,后面会介绍。

发送到整个线程组的信号,可以来自内核或其它进程,产生信号的函数如下表所示。

表11-10 为线程组产生信号的内核函数

函数名称 描述
send_group_sig_info() 发送信号给线程组,由线程组中的某个进程描述符标识
kill_pg() 发送信号给进程组中所有线程组(参加第1章的进程管理一节)
kill_pg_info() 类似kill_pg(),只是siginfo_t带有扩展信息
kill_proc() 发送信号给线程组,由线程组中的某个进程PID标识
kill_proc_info() 类似kill_proc(),只是siginfo_t带有扩展信息
sys_kill() kill()系统调用的处理程序(参见后面与信号处理有关的系统调用)
sys_rt_sigqueueinfo() rt_sigqueueinfo()系统调用的处理程序

上表中的函数最终调用group_send_sig_info()更新进程描述符,函数会在group_send_sig_info()函数一节中介绍。

1 specific_send_sig_info()函数

specific_send_sig_info()函数可以发送信号到具体的进程。作用于3个参数:

sig

信号编号。

info

既可以是siginfo_t表的地址,也可以是3个特殊值:0意味着用户进程发送的信号;1意味着内核发送的信号;2意味着内核发送的信号,且信号是SIGSTOP或SIGKILL。

t

目标进程描述符的指针。

specific_send_sig_info()必须在本地中断禁止且申请了t->sighand->siglock自旋锁的情况下调用。执行步骤如下:

检查进程是否忽略信号;如果是,则返回0(不用产生信号)。满足下面3个条件,信号即会被忽略:

进程没有被跟踪(t->ptrace中PT_PTRACED标志清除)

信号没被阻塞sigismember(&t->blocked, sig) returns 0

信号显式忽略(t->sighand->action[sig-1]中的sa_handler字段等于SIG_IGN)或隐式忽略(sa_handler字段等于SIG_DFL并且信号是SIGCONT、SIGCHLD、SIGWINCH或SIGURG)

检查信号是否是非实时(sig<32),相同信号是否已经在进程的私有挂起信号队列(sigismember(&t->pending.signal,sig) returns 1):如果确定,什么也不做,然后返回0。

调用send_signal(sig, info, t, &t->pending)将信号添加到进程的挂起信号集中;详细描述如下所示:

如果send_signal()成功终止,且信号也没有被阻塞(sigismember(&t->blocked,sig) returns 0),然后调用signal_wake_up()函数通知进程新的挂起信号。因此,函数执行如下步骤:

在t->thread_info->flags中设置TIF_SIGPENDING标志。

调用try_to_wake_up()唤醒进程(这些进程处于TASK_INTERRUPTIBLE或TASK_STOPPED状态,且信号是SIGKILL),具体可以参考第7章的try_to_wake_up()函数一节。

如果try_to_wake_up()返回0,进程唤醒并可运行:如果是,检查该进程是否在其它CPU上正在运行,这种情况下,发送一个核间中断给那个CPU,强制重新调度当前进程。(参考第4章的核间中断处理一节)因为当从schedule()函数返回时,每个进程都会检查挂起信号,核间中断确保目标进程快速注意到新的挂起信号。

信号产生成功,则返回1。

2 send_signal()函数

send_signal()负责插入挂起信号队列中。接收参数:信号sig,数据结构siginfo_t中info的地址(或具体编码值,参考前面的specific_send_sig_info()描述,目标进程描述符地址t,挂起信号队列signals的地址。

函数执行如下内容:

如果info等于2,信号是SIGKILL或SIGSTOP且是内核通过force_sig_specific()函数产生的:这种情况直接跳转到第9步。与这些信号相对应的动作由内核立即强制执行,因此该函数可能会跳过将信号添加到挂起信号队列中。(如果是特殊信号,比如杀死、停止进程,则直接执行,不再走信号处理的通用流程)

如果进程拥有者的挂起信号的数量(t->user->sigpending)小于当前进程资源限制(t->signal->rlim[RLIMIT_SIGPENDING].rlim_cur),函数就会为新信号分配sigqueue数据结构:

 

q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);

 

如果挂起信号的数量太多或前一步内存分配失败,则跳转第9步。

挂起信号数量(t->user->sigpending)和每个用户数据结构的引用计数器(t->user)增加。

添加sigqueue数据结构到挂起信号队列(signals)中:

 

list_add_tail(&q->list, &signals->list);

 

完善sigqueue数据结构中的siginfo_t表:

 

if ((unsigned long)info == 0) {
    q->info.si_signo = sig;
    q->info.si_errno = 0;
    q->info.si_code = SI_USER;
    q->info._sifields._kill._pid = current->pid;
    q->info._sifields._kill._uid = current->uid;
} else if ((unsigned long)info == 1) {
    q->info.si_signo = sig;
    q->info.si_errno = 0;
    q->info.si_code = SI_KERNEL;
    q->info._sifields._kill._pid = 0;
    q->info._sifields._kill._uid = 0;
} else
    copy_siginfo(&q->info, info);

 

copy_siginfo()函数将调用者传递的siginfo_t表进行拷贝。

设置队列位掩码中信号对应的位:

 

sigaddset(&signals->signal, sig);

 

信号成功添加到挂起信号队列中,返回0。

该步骤主要是处理信号无法添加到信号挂起队列中的情况,比如,已经有太多挂起信号,或没有内存分配sigqueue,或者信号由内核立即强制执行。如果信号是实时的,且是有内核函数发送并明确要求添加到队列中时,该函数返回错误码-EAGAIN:

 

if (sig>=32 && info && (unsigned long) info != 1 &&
    info->si_code != SI_USER)
    return -EAGAIN;

 

设置队列位掩码中信号对应的位:

 

sigaddset(&signals->signal, sig);

 

返回0:即使信号没有被添加到队列中,对应的位也已经在挂起信号队列中位掩码中设置相应位。

即使挂起的信号队列中没有空间容纳相应的项,仍然让目标进程接收信号是很重要的。例如,假设一个进程正在消耗过多的内存。内核必须确保kill()成功,即使没有可用内存;否则,系统管理员没有任何机会通过终止违规进程来恢复系统。

3 group_send_sig_info()函数

group_send_sig_info()函数发送信号给整个线程组。它有三个参数:信号sig,siginfo_t表地址(或者具体值0,1或2),和进程描述符的地址p。

该函数执行的大概步骤如下:

检查sig是否正确:

 

if (sig < 0 || sig > 64)
    return -EINVAL;

 

如果信号是由用户进程发送的,则检查该操作是否被允许。只有满足以下条件之一,信号才会被发送:

如果用户进程不被允许发送信号,则返回-EPERM。

发送进程的所有者具有适当的权限(通常,这仅仅意味着信号是由系统管理员发出的,参见第20章)。

信号是SIGCONT,目标进程与发送进程处于相同的登录会话中。

两个进程属于同一个用户。

如果sig等于0,则立即返回,不会产生任何信号:

 

if (!sig || !p->sighand)
    return 0;

 

因为0不是有效的信号数字,所以它用于允许发送进程检查它是否具有向目标线程组发送信号所需的特权。如果目标进程被杀死(通过检查其信号处理程序是否被释放进行判断),该函数也会返回。

申请p->sighand->siglock自旋锁并禁止本地中断。

调用handle_stop_signal()函数,检查某些类型的信号,这些信号可能使目标线程组中的其它挂起信号失效。

该函数执行如下步骤:

a. 如果线程组被杀死(信号描述符中flags字段的SIGNAL_GROUP_EXIT标志被设置),立即返回。

b. 如果sig是SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号,该函数会调用rm_from_queue()函数从共享挂起信号队列p->signal->shared_pending和线程组中所有成员的私有队列中移除SIGCONT信号。

c. 如果sig是SIGCONT,该函数会调用rm_from_queue()函数从共享挂起信号队列p->signal->shared_pending中移除;然后,将相同的信号从线程组进程的私有挂起信号队列中移除,并唤醒他们:

 

rm_from_queue(0x003c0000, &p->signal->shared_pending);
t = p;
do {
    rm_from_queue(0x003c0000, &t->pending);
    try_to_wake_up(t, TASK_STOPPED, 0);
    t = next_thread(t);
} while (t != p);

 

掩码0x003c0000选择了四个停止信号。每次迭代,next_thread宏返回线程组中一个不同的轻量级进程的描述符地址(参考第3章的进程之间的关系)。

实际的代码要远比上面的代码片段复杂,因为handle_stop_signal()还处理捕获SIGCONT信号的异常情况,以及在线程组中的所有进程都停止时由于SIGCONT信号发生而导致的竞态条件。

检查线程组是否忽略该信号,如果忽略,则返回成功(0)。满足忽略信号的三个条件即可,可以参考specific_send_sig_info()函数的介绍。

检查信号是否为非实时信号,且同一个信号是否已经在线程组的共享挂起信号队列中挂起:如果挂起,什么也不用做,返回成功即可(0):

 

if (sig<32 && sigismember(&p->signal->shared_pending.signal,sig))
    return 0;

 

通过以上检查,则调用send_signal()将信号添加到共享挂起信号队列中。如果send_signal()返回非零错误码,则将该错误码返回并终止执行。

调用__group_complete_signal()函数唤醒线程组中的一个轻量级进程。

释放p->sighand->siglock自旋锁,且使能本地中断。

返回成功(0)。

__group_complete_signal()函数会扫描线程组中的进程,查找可以接受新信号的进程。前提是满足一下条件:

该进程不会阻塞信号

该进程没有处于EXIT_ZOMBIE、EXIT_DEAD、TASK_TRACED或TASK_STOPPED(特例是,如果该信号是SIGKILL,则进程可以处于TASK_TRACED和TASK_STOPPED状态中)

进程没有被杀死,也就是没有设置PF_EXITING标志

进程当前正在某个CPU核上执行,或者它的TIF_SIGPENDING标志尚未设置。(事实上,唤醒一个有挂起信号的进程没有意义的:一般来说,这个操作已经由设置了TIF_SIGPENDING标志的内核控制路径执行了。另一方面,如果进程当前处于执行中,它应该收到新的挂起信号的通知。

线程组可能包含许多满足条件的进程。该函数选择其中一个:

如果进程(由函数group_send_sig_info()传递的进程描述符参数p标识)满足前述所有条件并可以接收信号,则函数选择它。

否则,该函数从接收到最后一个线程组信号的进程开始(p->signal->curr_target),通过扫描线程组的成员选择一个合适的进程。

如果__group_complete_signal()成功找到一个合适的进程,它将设置信号传递到的进程。首先,该函数会检查信号是否致命:这种情况下,向线程组中的每个轻量级进程发送SIGKILL信号来杀死整个线程组。如果信号不是致命的:则该函数调用signal_wake_up()来通知所选进程它有一个新的挂起信号(参见前面章节的specific_send_sig_info()函数中的第4步)。







审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分