Linux2.6内核实现的是NPTL线程模型,依然是用进程来模拟线程,但新引入了线程组(进程组)的概念,使得实现效率更好。
在2.4内核中,不存在线程组的概念,当运行一个多线程得程序时,使用ps命令,可以看到有许多个进程,在ps命令看来,线程基本上是等同于进程,在信号处理中,情况也是如此,只有指定进程号的线程,可以接收到信号。在2.6内核中引入了线程组的概念,在2.6内核中,如果使用ps命令看,一个多线程的进程,只会显示一个进程,在给线程组中的任何一个线程发送信号的时候,整个线程组中的进程都能收到信号。
在内核task_struct中相关字段如下(位于include/linux/sched.h):
代码: 全选
937 struct task_struct {
...
993 pid_t pid;
994 pid_t tgid;
...
1013 struct task_struct *group_leader; /* threadgroup leader */
...
1017 struct list_head thread_group;
...
1198 };
pid,从字面上是process id,但其实是thread id。
tgid,从字面上,应该是thread group id,也就是真正的process id。
这一点,可以从系统调用getpid和gettid中看出来(位于kernel/timer.c)。
代码: 全选
954 asmlinkage long sys_getpid(void)
955 {
956 return current->tgid;
957 }
1100 asmlinkage long sys_gettid(void)
1101 {
1102 return current->pid;
1103 }
group_leader字段,指向线程组中的第一个线程,创建第一个线程的时候,group_leader指向自己,创建其后的线程时,指向第一个线程的task_struct结构;
thread_group,当前进程所有线程的队列,对于group_leader,这是个队列头,对于其后的进程而言,通过这个字段,挂入队列中,可以通过此队列,遍历所有线程。
线程组中各个线程的关系,是在do_fork中设定的,具体的代码在copy_process中(位于kernel/fork.c):代码: 全选
959 copy_process()
960 {
...
1112 p->tgid = p->pid;
1113 if (clone_flags & CLONE_THREAD)
1114 p->tgid = current->tgid;
...
1181 p->group_leader = p;
1182 INIT_LIST_HEAD(&p->thread_group);
...
1234 if (clone_flags & CLONE_THREAD) {
1235 p->group_leader = current->group_leader;
1236 list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
...
1252 }
1254 if (likely(p->pid)) {
...
1259 if (thread_group_leader(p)) {
...
1266 list_add_tail_rcu(&p->tasks, &init_task.tasks);
...
1268 }
...
1271 }
...
1320 }
1113-1114行说明在创建线程时,从父进程获取tgid,表明他们在同一个线程组中;1181-1182则对group_leader和thread_group初始化,对于第一个线程,则group_leader就是它自己;1234-1236行,将新创建的线程的group_leader设置成为父进程得group_leader,无论父进程是不是线程组中的第一个线程,它的group_leader都是指向第一个线程的task_struct,同时通过thread_group字段,挂入到第一个线程的thread_group队列中;1266行表明只有线程组中的第一个线程,才会通过tasks字段,挂入到init_task队列中。
在引入线程组概念后,退出部分也引入了一个新的系统调用exit_group(位于kernel/exit.c)
1055 NORET_TYPE void
1056 do_group_exit(int exit_code)
1057 {
1058 BUG_ON(exit_code & 0x80); /* core dumps don't get here */
1059
1060 if (current->signal->flags & SIGNAL_GROUP_EXIT)
1061 exit_code = current->signal->group_exit_code;
1062 else if (!thread_group_empty(current)) {
1063 struct signal_struct *const sig = current->signal;
1064 struct sighand_struct *const sighand = current->sighand;
1065 spin_lock_irq(&sighand->siglock);
1066 if (sig->flags & SIGNAL_GROUP_EXIT)
1067 /* Another thread got here before we took the lock. */
1068 exit_code = sig->group_exit_code;
1069 else {
1070 sig->group_exit_code = exit_code;
1071 zap_other_threads(current);
1072 }
1073 spin_unlock_irq(&sighand->siglock);
1074 }
1075
1076 do_exit(exit_code);
1077 /* NOTREACHED */
1078 }
在1060行中,current->signal其实是线程组中所有线程共享的,对于调用exit_group的那个线程,如果是一个多线程的进程,就会进入1062-1074这部分代码,如果是单线程,则直接进入do_exit退出进程。这部分代码的主要操作在zap_other_threads中(位于kernel/signal.c)
void zap_other_threads(struct task_struct *p)
982 {
983 struct task_struct *t;
984
985 p->signal->flags = SIGNAL_GROUP_EXIT;
986 p->signal->group_stop_count = 0;
987
988 if (thread_group_empty(p))
989 return;
990
991 for (t = next_thread(p); t != p; t = next_thread(t)) {
992 /*
993 * Don't bother with already dead threads
994 */
995 if (t->exit_state)
996 continue;
997
998 /* SIGKILL will be handled before any pending SIGSTOP */
999 sigaddset(&t->pending.signal, SIGKILL);
1000 signal_wake_up(t, 1);
1001 }
1002 }
next_thread定义在include/linux/sched.h中,如下
651 static inline struct task_struct *next_thread(const struct task_struct *p)
1652 {
1653 return list_entry(rcu_dereference(p->thread_group.next),
1654 struct task_struct, thread_group);
1655 }
其实就是通过task_struct中的thread_group队列来遍历线程组中的所有线程。
在其中,会在signal->flags中设置SIGNAL_GROUP_EXIT,同时,搜索线程组中所有进程,在每个线程中挂上一个SIGKILL信号,这样,当那些线程调度到运行的时候,就会处理SIGKILL信号,对于SIGKILL信号的处理,会调用do_group_exit,不过,当这次调用到do_group_exit的时候,将运行到1061行,然后就到了1076行的do_exit。这样,当线程组中的每个线程都运行过一遍后,整个线程组就退出了。