linux的2.6内核更好的实现了内核级别的线程,使得线程的语义更加符合posix的约定,总的来说,线程会在两种地方退出,第一个是正常退出,第二种是异常退出,正常退出的情况下,比如在一个进程的一个线程调用exec的时候,那么所有的别的线程都会退出,另外在一个线程调用exit库函数的时候或者调用group_exit系统调用的时候,整个线程都会退出,异常的情况下,接收到内核的严重错误信号的时候也会退出所有的线程。前面一篇文章中大致说了一下exec对linux线程的影响,但是那篇文章说的不详细,于是本文详细说一下linux线程的一些细节。
首先我们熟悉一些机制,这些机制在内核源代码中均有体现,这些机制完全按照posix线程的约定来定义,总体看来就是在线程开始的时候设置一些字段变量,然后在线程退出的时候检查这些字段变量,然后采取一些不同的动作,在copy_process中,有下面的逻辑:
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
这个逻辑是怎么回事呢?我们看看CSIGNAL和CLONE_XX的定义:
#define CSIGNAL 0x000000ff //信号掩码
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
从上述的定义可以看出如果没有设置CLONE_THREAD标志,那么clone_flags和CSIGNAL相与之后就会成为一个信号的值附给新建task_struct的exit_signal字段,如果有CLONE_THREAD标志的话,那么新创建的task_struct的exit_signal就会成为-1,不管成为什么,这个exit_signal标志在task_struct退出的时候都会被检测,在do_exit中调用的exit_notify中有以下逻辑:
if (tsk->exit_signal != SIGCHLD && tsk->exit_signal != -1 &&( tsk->parent_exec_id != t->self_exec_id ||tsk->self_exec_id != tsk-
>parent_exec_id)
...
tsk->exit_signal = SIGCHLD;
if (tsk->exit_signal != -1 && thread_group_empty(tsk)) {
int signal = tsk->parent == tsk->real_parent ? tsk->exit_signal : SIGCHLD;
do_notify_parent(tsk, signal);
}
现在看看这个逻辑决定了什么,如果一个进程的一个单独调用exit系统调用,也就是1号系统调用,注意不是调用exit函数,因为后者调用的是group_exit系统调用,为了符合posix线程的语义,在linux中,我们知道task_struct是一个容器而不是容器的内容,作为容器内容的是mm_struct,一个进程的所有线程共享一个mm_struct,因此,按照unix/linux的传统,fork系统调用创建一个task_struct,而exit系统调用销毁一个系统调用,这里并没有线程和进程的区别,为了支持线程,clone系统调用允许传入一些标志以影响内核对容器内容共享的策略和程度,比如传入一个CLONE_THREAD标志就代表共享进程空间的一个线程将被创建,但是无论如何一个task_struct都会被创建,相应的在退出的时候,内核提供了group_exit系统调用,这个系统调用允许一次退出整个进程的所有线程,这个group_exit和前面的clone相对应,否则,内核中对task_struct的管理将失控。在linux中一个task_struct到底是一个进程还是一个线程呢?其实linux并没有区分进程和线程,而是只要一次do_fork调用就会创建一个task_struct,至于是线程还是进程和这个task_struct无关,而是和task_struct指向的数据有关,如果它们共享一个mm和sighand,那么就是创建了一个线程,反之就是一个新的进程,这种方式使得内核对执行绪的管理更加有效,就是简单树状结构,并且这个机制不允许创建远程线程,只能创建和当前进程共享上下文的一个线程,更好的实现了资源隔离,这是do_fork决定的。
理解了上面的以后,我们看看上面的那个逻辑有什么用。如果一个正在退出的task_struct的exit_signal为-1,那么可以肯定的就是exit_notify中肯定不会向父进程发送信号,到了exit_notify的后半部分,有以下逻辑:
if (tsk->exit_signal == -1 && ...
state = EXIT_DEAD;
这个EXIT_DEAD的state导致马上就会调用release_task,后者会将task_struct的引用计数降低为1,然后在schedule后会被彻底释放,如果调用exit系统调用的是thread-leader呢?leader在exit_notify的if (tsk->exit_signal != -1 && thread_group_empty(tsk))判断中可能仍然不能通过,因为虽然它的exit_signal不为-1,但是同一个线程组中可能还有别的线程活动,也就是thread_group_empty会返回0,那么也不会向父进程发送信号,可是试验发现即使leader单独调用exit系统调用,等到该进程的最后一个线程调用exit系统调用的时候,那么同样会向父进程发送信号,虽然不是在exit_notify中发送的,但是正如前面说的,如果这个线程不是leader,那么会马上调用release_task函数,而后者中有以下逻辑:
zap_leader = 0;
leader = p->group_leader;
if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
do_notify_parent(leader, leader->exit_signal);
这个逻辑就是说,如果这个task_struct是同一个进程的最后一个线程并且不是leader,那么就会向父进程发送信号,这里要说的是,一个进程的所有的线程的parent是同一个线程,这在copy_process中被指定:
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
p->real_parent = current->real_parent;
else
p->real_parent = current;
p->parent = p->real_parent;
到此为止,我们知道以下结论:
1.非leader的线程在单独exit系统调用时,不会向父进程发送信号;
2.一个进程的最后一个线程单独exit调用时,无论如何都会向父进程发送信号,如果是leader,那么就会在exit_notify中发送,因为thread_group_empty为真,如果是非leader线程,那么会在release_task中向父进程发送信号;
3.如果一个进程的线程的leader没有最后一个退出,那么只要这个leader退出,不管别的还有没有线程存在,整个进程都会成为僵尸状态,这个leader将一直保留,知道它的父进程或者父进程退出后过继的父亲将其回收。
前面我们谈了线程单独调用exit系统调用的情况,其实很少出现这种情况,只要一个线程调用了exit库函数,那么就是调用group_exit函数,进而调用do_group_exit,而后者的逻辑是:
zap_other_threads(current);
这个zap其实很重要,就是退出其它的所有的线程,zap的具体逻辑是:
void zap_other_threads(struct task_struct *p)
{
...
for (t = next_thread(p); t != p; t = next_thread(t)) {
if (t->exit_state)
continue;
if (t != p->group_leader) //leader的exit_signal不会被设置为-1,因为按照exit的逻辑leader负责被它的parent感知,因此它只能被处于僵尸状态而不能将其exit_signal设置为-1从而在exit_notify中被回收(注意虽然是回收也是在schedule之后才真正回收)。
t->exit_signal = -1;
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
}
}
这下就明白了,其实一个进程的线程的leader的exit_signal是不会被设置为-1的,主要原因是为了让其父进程收尸和收集状态,如果leader被回收了,那么即使它向父进程发送了信号,父进程也不能有效的收集其状态,这不符合unix/linux进程的语义,于是保留了进程的线程的leader以善后。
下面就再看看,当其中一个线程调用exec的时候会怎样,代码表明在flush_old_exec完成了这一切,在flush_old_exec中的主力是de_thread:
static int de_thread(struct task_struct *tsk)
{
struct signal_struct *sig = tsk->signal;
struct sighand_struct *newsighand, *oldsighand = tsk->sighand;
spinlock_t *lock = &oldsighand->siglock;
struct task_struct *leader = NULL;
int count;
if (atomic_read(&oldsighand->count) <= 1) {
exit_itimers(sig);
return 0;
}
...
if (thread_group_empty(current))
goto no_thread_group;
...
zap_other_threads(current); //前面说过,这个函数就是促使其它的线程退出,因为既然能到这里,就说明这个进程不止一个线程,如果我们是leader,那么我们在zap中显然没有退出我们自己,因此不往父进程发送信号,如果我们不是leader,那么起码还有我们这个线程没有退出,因此还是不给父进程发送信号(最后一个发送信号)
read_unlock(&tasklist_lock);
count = 1;
... //如果我们不是leader,那么count将被设置为2,以下的循环等待所有的其它的线程退出,why?只有在release_task中调用了__exit_signal函数,后者递减了sig的count字段
while (atomic_read(&sig->count) > count) {//为何我们不是leader的话,count就是2呢,因为只有在release_task中才会递减sig的count字段,leader退出的时候由于其exit_signal不是-1,那么不会调用release_tas而立即释放其task而期待父进程回收它,因此leader退出时不会递减sig的count,加上我们的count,一共是2个。
sig->group_exit_task = current; //希望被调用__exit_signal的线程唤醒
sig->notify_count = count;
__set_current_state(TASK_UNINTERRUPTIBLE);
spin_unlock_irq(lock);
schedule();
spin_lock_irq(lock);
}
sig->group_exit_task = NULL;
sig->notify_count = 0;
spin_unlock_irq(lock);
if (!thread_group_leader(current)) { //只在我们不是leader的情况下进行以下逻辑
leader = current->group_leader;
while (leader->exit_state != EXIT_ZOMBIE) //因为leader的exit_signal不为-1并且父进程不会回收leader,因此leader在显示调用release_task之前一定是僵尸状态,因此这里等待其变成僵尸
yield();
...//这里主要进行pid的交换,因为一个线程调用了exec,按照线程的语义,不管哪里线程exec了,都应该继承整个进程的pid,因此这里相当于重新开始了一个线程组,也就是当前的线程将转用leader的一些id,比如pid
}
sig->flags = 0;
...
no_thread_group:
exit_itimers(sig);
if (leader) //不能指望父进程回收,或者根本就不应该通知父进程,因为我们只是调用了exec而不是退出
release_task(leader);
}
为何只有在release_task中才可以释放signal呢?是因为最后一个线程退出时如果不是leader,那么它还要使用已经处于僵尸状态的leader的sig。
附:一个变量两个精彩
这个怎么理解呢?具体就是do_fork时的参数中可以有信号,这个信号就是在子进程退出的时候发给父进程的信号,这就是这一个变量,两个精彩在何处呢?一个就是linux内核巧妙的设置CLONE_XX和SIGXXX的值,让它们不相交,巧妙就在于CLONE_XX的低8位为0,这低8位正好为SIGXXX腾出了地方,这是一个精彩,另一个精彩就是在task_struct退出的时候,这个在do_fork的时候设置的exit_signal会直接被用到, 内核以此为参考向父进程发送信号,只不过在古老的fork系统调用中我们不能设置这个字段,它只能是一个SIGCHLD,但是在clone中却可以随意设置了,只要不和CLONE_THREAD冲突。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273395