`
xitong
  • 浏览: 6204063 次
文章分类
社区版块
存档分类
最新评论

linux线程的退出--附:一个变量,两个精彩

 
阅读更多

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)

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冲突。

分享到:
评论

相关推荐

    linux系统编程之线程.zip

    两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。 实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数...

    线程池Linux C语言简单版本

    本线程池采用C语言实现。包括以下内容 > - thread_pool_create:创建...主要的核心点集中在thread_pool_post和thread_worker两个函数中,这两个函数也构成了生产者-消费者模型。本文采用队列+互斥锁+条件变量实现。

    Linuxc 信号的使用

    ② 创建两个线程; ③ 如果线程正常结束,得到线程的结束状态值,并打印; 线程一完成以下操作: ① 设置全局变量key的值为字符串“hello world”; ② 打印3次字符串“当前线程ID:key值”; ③ 接收到线程二发送的...

    cmd操作命令和linux命令大全收集

    3. Nslookup-------IP地址侦测器 ,是一个 监测网络中 DNS 服务器是否能正确实现域名解析的命令行工具。它在 Windows NT/2000/XP 中均可使用,但在 Windows 98 中却没有集成这一个工具。 4. explorer-------打开...

    易语言程序免安装版下载

     静态编译后的易语言EXE/DLL之间不能再共享譬如窗口、窗口组件等类似资源,对于已经静态连接到一个EXE/DLL中的支持库,该支持库中的数据或资源将不能再被其它EXE/DLL中所使用的同名支持库访问。这是因为代码被分别...

    KODExplorer 芒果云-资源管理器

    - 你可以把他当做管理linux的一个操作系统界面 - 可以用来作为私有云存储系统,存储你的文件... - 当然你也可以用来分享文件 - Web IDE / browser code editor awesomeness - 更多场景等你来挖掘!…… #### 3....

    ARM_Linux启动分析.pdf

    start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现 init线程,由start_kernel()创建,当前处于用户态,加载了init...

    gsoap 2.8 (SOAP/XML 关于C/C++ 语言的自动化实现工具内附 CSharp webservice例子,及GSOAP client和server例子)

    而在1.x版本中,由于静态分配环境变量,多线程技术是不被允许的(只有一个线程可以用这个环  境变量调用远程方法或处理请求信息)。  4 准备工作  要开始用gSOAP创建一个web服务应用, 你需要:  一个C/C++编译器....

    基于Linux的网络编程——网络聊天程序

    通过设置用户名变量,实现多用户同时通信,在实现多用户功能的同时,用户过多输入会使数据溢出,因此设置用户上限以解决此问题,某一客户端退出并不影响其他客户的使用,所有用户全部退出,服务器关闭端口,结束进程...

    java 面试题 总结

    然而可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。Abstract 类的子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类为。取而代之,在子类中...

    超级有影响力霸气的Java面试题大全文档

    然而可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。Abstract 类的子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类为。取而代之,在子类中...

    深入理解Android:卷I--详细书签版

    5.3.1 一个变量引发的思考 109 5.3.2 常用同步类 114 5.4 Looper和Handler类分析 121 5.4.1 Looper类分析 122 5.4.2 Handler分析 124 5.4.3 Looper和Handler的同步关系 127 5.4.4 HandlerThread介绍 129 5.5...

    python入门到高级全栈工程师培训 第3期 附课件代码

    06 django的一个简单应用 07 django静态文件之static 08 django的url控制系统 09 django的urlConf补充 第50章 01 django之视图函数的介绍 02 django视图之redirec 03 django模板之变量 04 django模板之过滤器 05 ...

    orcale常用命令

     dictionary 全部数据字典表的名称和解释,它有一个同义词dict dict_column 全部数据字典表里字段名称和解释 如果我们想查询跟索引有关的数据字典时,可以用下面这条SQL语句: SQL>select * from dictionary ...

    UNIX操作系统教程 张红光

    第1章绪论.1 1.1操作系统概述1 1.1.1建立操作系统的目标1 1.1.2操作系统是用户与计算机的接口1 1.1.3操作系统是资源管理器2 1.2UNIX系统的主要...UNIX基本概念及入门技术10 2.1UNIX系统基本常识10 2.1.1两种前端机10 />...

    sesvc.exe 阿萨德

    Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出: key 就是写入时的键。 value 自然就是值。 开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。 hash 存放的是当前...

Global site tag (gtag.js) - Google Analytics