do_signal_stop()函数注解
首先讲一下对do_siganl_stop()函数的理解。
在内核执行do_signal()来处理信号的时候,在get_signal_to_deliver()中执行sig_kernel_stop()来判断信号的默认动作是否会让进程暂停,即是否属于暂停信号,如果是就以信号为入参执行do_signal_stop()来暂停该进程或线程组。
1 if (!sig->group_stop_count) { 2 struct task_struct *t; 3 4 if (!likely(sig->flags & SIGNAL_STOP_DEQUEUED) || 5 unlikely(signal_group_exit(sig))) 6 return 0; 7 /* 8 * There is no group stop already in progress. 9 * We must initiate one now. 10 */ 11 sig->group_exit_code = signr; 12 13 sig->group_stop_count = 1; 14 for (t = next_thread(current); t != current; t = next_thread(t)) 15 /* 16 * Setting state to TASK_STOPPED for a group 17 * stop is always done with the siglock held, 18 * so this check has no races. 19 */ 20 if (!(t->flags & PF_EXITING) && 21 !task_is_stopped_or_traced(t)) { 22 sig->group_stop_count++; 23 signal_wake_up(t, 0); 24 } 25 }
在do_signal_stop()中先判断current->signal->group_stop_count是否为0,如果group_stop_count为0说明对进程(或线程组)的暂停刚开始,当前进程是第一个要被暂停的,设置group_stop_count为1,如果当前进程是个处于线程组内的轻量级进程,就用一个for循环从它的下一个轻量级进程开始遍历线程组内的所有轻量级进程(进程描述符中有个thread_group节点,线程组中的轻量级进程会把其自身的thread_group节点添加到领头进程的thread_group节点构成的一个双向链表上),调用signal_wake_up()将其唤醒,并且增加group_stop_count的计数,在遍历的时候会做一个判断,如果该轻量级进程已经处于终止状态,就没有必要再使其暂停。
从这可以知道,group_stop_count的作用就是表征当前线程组中需要被暂停的轻量级进程的个数。
假设用户层有个测试程序,进程A创建了线程B和线程C,那么从内核的角度来讲,进程A是个普通进程,线程B和C是轻量级进程,ABC属于同一个线程组,有相同的tgid,对用户态来说,ABC属于同一个进程,对内核来说,ABC有各自的pid,假设应用程序在A中调用pthread_kill()向线程B发送一个SIGSTOP信号,该函数会调用到内核的tgkill()系统调用。那么B收到这个信号,在某个时刻(比如从中断或者异常返回)执行do_signal()来处理该信号,最终会调用到do_signal_stop()函数,B是该线程组中第一个被暂停的线程,它遍历到C和A,将它们唤醒,并且此时group_stop_count增加到3(当然,有个前提是,此时线程B和C还没执行完毕退出)。由于B和C是轻量级进程,那么调用do_fork()创建它俩的时候clone_flags是有CLONE_THREAD标志的,在copy_process()中调用copy_signal()可以知道是不会重新分配signal_struct成员的,它们和父进程A共用同一个signal成员,这意味着ABC共用信号,共用同一个group_stop_count变量。
好,再回到do_signal_stop()函数:
1 /* 2 * If there are no other threads in the group, or if there is 3 * a group stop in progress and we are the last to stop, report 4 * to the parent. When ptraced, every thread reports itself. 5 */ 6 notify = sig->group_stop_count == 1 ? CLD_STOPPED : 0; 7 notify = tracehook_notify_jctl(notify, CLD_STOPPED); 8 9 10 if (notify) { 11 read_lock(&tasklist_lock); 12 do_notify_parent_cldstop(current, notify); 13 read_unlock(&tasklist_lock); 14 }
如果当前进程只是一个普通进程,那么group_stop_count就为1;或者如果当前要暂停掉的轻量级进程是线程组中最后一个轻量级进程,那么group_stop_count也是1;都表示当前进程(或线程组)将被完全暂停,那么notify为CLD_STOPPED,通知调试器相应的事件,如果线程组内的进程还未完全暂停,notify为0,就不会去通知。
1 if (sig->group_stop_count) { 2 if (!--sig->group_stop_count) 3 sig->flags = SIGNAL_STOP_STOPPED; 4 current->exit_code = sig->group_exit_code; 5 __set_current_state(TASK_STOPPED); 6 }
这段代码先判断group_stop_count当前是否为1,如果是就置位SIGNAL_STOP_STOPPED,因为线程组即将被完全暂停;进程退出码exit_code是使进程暂停的暂停信号,设置当前进程状态为TASK_STOPPED。
1 /* Now we don't run again until woken by SIGCONT or SIGKILL */ 2 do { 3 schedule(); 4 } while (try_to_freeze());
最后调用schedule()放弃CPU,将当前进程调度出去,实现了对进程的暂停。
按上面的叙述,在do_signal_stop()中第一个被暂停的B唤醒的A和C也会相继执行do_signal_stop(),但是不会再进入第一个if判定,因为此时的group_stop_count不为0,ABC是共享同一个信号成员的,A和C各自将同一个group_stop_count减1,将自身调度出去,所以会发现一个现象:主进程A调用pthread_kill()发送SIGSTOP信号给线程B,本意应该是想暂停B,但是却把整一个线程组ABC都给暂停了,这似乎不太合理,还没想通,但是从代码实际运行现象来看是这样的。并且调用pthread_kill()发送信号给线程B是通过系统调用tgkill实现的,信号是发送给轻量级进程B的task_struct->pending成员,是B的私有挂起信号队列,而不是B的task_struct->signal->shared_pending,ABC不共享信号挂起成员task_struct->pending,因此A和C不会从自己的信号挂起队列中发现SIGSTOP信号,由于在调用do_signal_stop()去暂停B的时候会调用signal_wake_up()唤醒其他的轻量级进程,而signal_wake_up会设置进程的TIF_SIGPENDING标志,导致A和C在执行到do_notify_resume()时会进入do_signal(),发现group_stop_count不为0就会执行到do_signal_stop(),就把自身也停止了。A和C自身并没有挂起的信号。