linux中exec操作对线程组的影响

在linux的多线程程序中,如果一个线程调用了exec会怎样?是影响整个进程还是仅仅影响单个线程?实际上是影响整个进程,因为exec就是替换进程 地址空间的,而线程是共享进程地址空间的,从本质上讲,线程是没有地址空间这个概念的,只不过在linux中,其独特的线程实现方式使得进程和线程的概念 更加模糊了,在linux中只要一个执行绪就有一个task_struct结构体与之对应,但是实际上按照现代的操作系统观点,执行绪只有线程,进程已经 仅仅成了一个资源容器,然而linux并没有区分这一点。
在阐述一切机制之前,我们必须首先明白linux中线程是如何建立的,这里我不谈pthread_create建立的线程,而是更加本质地说明linux 中不用任何库,原生的建立线程的过程。其实任何库包装的线程都是clone系统调用建立的,于是我们看一下这个clone,它的原形是:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
参 数不用说也能猜出来啥意思,所以就真的不说了。唯一要说的参数就是flags,它有以下几种选 择:CLONE_PARENT,CLONE_FS,CLONE_FILES,CLONE_NEWNS,CLONE_SIGHAND,CLONE_PTRACE,CLONE_UNTRACED,CLONE_STOPPED,CLONE_VFORK,CLONE_VM,CLONE_PID,CLONE_THREAD,...。 这么多的可能不能一一说明,可是这里面有几个最重要的:CLONE_THREAD,CLONE_VM,CLONE_SIGHAND,我们姑且不讨论文件相 关的,一个线程和同一进程的别的线程必须共享地址空间,但是这是唯一的要求吗?当然不是。要知道,线程实际上是和同一进程的别的线程共享资源的,而地址空 间仅仅是资源的一种而已,按照posix的约定,线程们必须共享信号,因此,CLONE_SIGHAND也是必须的,而且既然是线程那么当然所有的同一进 程的线程要在一个线程组里面了,因此CLONE_THREAD也是必须的,从man手册可以看出,CLONE_THREAD的设置要求 CLONE_SIGHAND被设置,而CLONE_SIGHAND的设置要求CLONE_VM被设置,在内核的copy_process函数里面有:
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
以 上代码提供了机制的约束。事实上,既然是线程了,按照线程的原始概念,它们也必须共享信号,而信号是一种资源,信号处理函数在进程的地址空间中,既然都共 享sighand了,那么就必然地要共享地址空间,于是就有了上述的约束。也即是说,只要你都共享了SIGHAND了,那十有八九你就是在玩线程了。在库 实现的pthread中,其实质也是用了clone系统调用,那样用的很方便但是理解起来不是很直观。于是我用clone实现了一个简单版的线程例子:
#include <stdio.h><br>#include <unistd.h><br>#include <sched.h><br>#include <errno.h><br>#include <sys><br>#include <linux><br>pid_t gettid() //自己实现一个gettid,就是得到线程号。 <br>{ <br> return syscall(SYS_gettid); <br>} <br>int clone_func1(void * data) <br>{ <br> int a = 3 <br> printf("sub2:%d,%d/n",getpid(),gettid()); <br> //scanf("%d",&amp;a); //运行中调试专用,相当于在此处下了一个断点,然后观察程序断点前后的行为 <br> execve("./mm",NULL,NULL); //调用了exec,以检测是否主线程和别的线程会退出 <br>} <br>int clone_func2(void * data) <br>{ <br> printf("sub1:%d,%d/n",getpid(),gettid()); <br> while(1){} //此线程永不退出 <br>} <br>int main(int argc, char* argv[]) <br>{ <br> printf("main:%d,%d/n",getpid(),gettid()); <br> void * stack1 = malloc(10240); //分配线程堆栈 <br> void * stack2 = malloc(10240); //分配线程堆栈 <br> clone(&amp;clone_func2, stack2+10240, CLONE_VM|CLONE_THREAD|CLONE_SIGHAND, NULL); //没有考虑文件相关的东西 <br> clone(&amp;clone_func1, stack1+10240, CLONE_VM|CLONE_THREAD|CLONE_SIGHAND, NULL); <br> while(1) //主线程永不退出 <br> { <br> sleep(1); <br> printf("main/n"); <br> } <br> return 0; <br>} <br>执行以后发现除了“./mm”在运行之外,主线程和clone_func2线程都退出了,而且clone_func1还在exec后用了主线程的pid,我的mm.c如下: <br>#include <sys><br>#include <linux><br>pid_t gettid() <br>{ <br> return syscall(SYS_gettid); <br>} <br>int main() <br>{ <br> while(1) <br> {sleep(1); <br> printf("mm:%d,%d/n",getpid(),gettid()); <br> } <br>} <br>在 程序执行到scanf("%d",&amp;a)的时候,实际上有3个线程,main,sub1,sub2,比如getpid得到1036,那么我们可以 从/proc/1036/status中看到线程的数量,还可以从/proc/1036/task目录中得到具体信息,其实该task目录和/proc /1036/目录的内容几乎一样,而性质却不一样,后者是进程信息,前者是各个线程的信息,从/proc文件系统这么安排目录结构的方式也可以看出 linux用相同的方式处理了性质完全不同的进程和线程。当我在终端输入一个数字并回车后, <br>scanf("%d",&amp;a)继续往下走,执 行完exec后再看这个/proc/1036目录,status中显示线程数量为1,task目录下也只有一个1036,其他的线程呢?其它的都退出了, 并且在主线程退出前将自己的pid等一切给了想单飞的exec线程。但是我们看一下clone的flags参数 CLONE_VM|CLONE_THREAD|CLONE_SIGHAND,如果我们省去CLONE_THREAD|CLONE_SIGHAND或者省去 CLONE_THREAD会怎样呢(注意约束条件不允许仅省去CLONE_SIGHAND)?如果省了别的线程就不会退出了,如果仅仅有 CLONE_VM,那么就是仅仅共享地址空间,当一个共享地址空间的进程(注意这里的叫法,不是线程)执行exec的时候,原始的主进程并不会受到什么影 响,在clone的时候会copy_process,后者会copy_mm,在copy_mm中如果发现有CLONE_VM标志则直接增加原始mm的引用 计数: <br>if (clone_flags &amp; CLONE_VM) { <br> atomic_inc(&amp;oldmm-&gt;mm_users); <br> mm = oldmm; <br>一 旦有进程exec了,就会递减mm的引用计数,这时计数当然不会为0,mm_struct不会被释放,用来用去就那一个mm_struct,只是其引用计 数在不断变化而已;如果在CLONE_VM的基础上加上了CLONE_SIGHAND,那么因为没有CLONE_THREAD,所以这个新clone的进 程不会和主进程在一个线程组,也就是说它们之间不是线程关系,这样的话,内核在exec的时候会处理以使得主进程不受影响,下面的内核代码中我会给出注 释。 <br>根据线程的意义,只要一个执行exec单飞,那么整个进程就要随着飞,这在语义上是很合理的,exec本身就有蒸发当前地址空间的语义,因此posix就 作出了上面论述的约定。那么面对这些约定,linux内核是怎么实现的呢?现在又到了看内核的时间,这个实现将再次展示linux是怎样将进程和线程这两 种本质不同的东西纳入一个机制去管理的。在sys_execve中层层调用最终要调用flush_old_exec,我们从flush_old_exec 开始看: <br>int flush_old_exec(struct linux_binprm * bprm) <br>{ <br> char * name; <br> int i, ch, retval; <br> struct files_struct *files; <br> char tcomm[sizeof(current-&gt;comm)]; //本程序的进程名,注意不是全路径,想得到全路径请看上一篇文章。 <br> retval = de_thread(current); //和线程们分道扬镳,另外还杀死了这些线程们. <br> if (retval) <br> goto out; <br> files = current-&gt;files; <br> retval = unshare_files(); <br> if (retval) <br> goto out; <br> retval = exec_mmap(bprm-&gt;mm); //替换进程地址空间 <br>...//共享打开文件的处理 <br> name = bprm-&gt;filename; <br> for (i=0; (ch = *(name++)) != '/0';) { <br> if (ch == '/') <br> i = 0; <br> else <br> if (i tcomm[i++] = ch; <br> } <br> tcomm[i] = '/0'; <br> set_task_comm(current, tcomm); //至此拷贝完毕进程名字 <br>... <br>} <br>static inline int de_thread(struct task_struct *tsk) <br>{ <br> struct signal_struct *newsig, *oldsig = tsk-&gt;signal; <br> struct sighand_struct *newsighand, *oldsighand = tsk-&gt;sighand; <br> spinlock_t *lock = &amp;oldsighand-&gt;siglock; <br> int count; <br> if (atomic_read(&amp;oldsighand-&gt;count) return 0; <br>...//分配newsighand <br> atomic_set(&amp;newsighand-&gt;count, 1); <br> memcpy(newsighand-&gt;action, oldsighand-&gt;action, sizeof(newsighand-&gt;action)); <br> newsig = NULL; <br> if (atomic_read(&amp;oldsig-&gt;count) &gt; 1) { <br>...//分配以及初始化newsig <br> } <br> if (thread_group_empty(current)) //如果在clone中没有CLONE_THREAD参数,那么就不在一个线程组,那么就不用退出别的线程。 <br> goto no_thread_group; <br>... <br> if (oldsig-&gt;group_exit) { <br>...//别的线程已经在退出了,这里就不必再进行处理了,直接返回,我们的目的就是促使别的线程退出 <br> } <br> oldsig-&gt;group_exit = 1; //预示着别的线程全部要退出但是不包括这个线程,因为马上就要用新的newsighand了 <br> zap_other_threads(current); //杀死别的全部线程但是不包含主线程。 <br>... <br> while (atomic_read(&amp;oldsig-&gt;count) &gt; count) { //等待所有别的线程退出。 <br> oldsig-&gt;group_exit_task = current; <br> oldsig-&gt;notify_count = count; <br> __set_current_state(TASK_UNINTERRUPTIBLE); <br> spin_unlock_irq(lock); <br> schedule(); <br> spin_lock_irq(lock); <br> } <br>... <br> if (current-&gt;pid != current-&gt;tgid) { //至此除了主线程之外的别的线程应该退出了,我们要等待主线程不可用从而可以用它的pid <br> struct task_struct *leader = current-&gt;group_leader, *parent; <br> struct dentry *proc_dentry1, *proc_dentry2; <br> unsigned long state, ptrace; <br> while (leader-&gt;state != TASK_ZOMBIE) //等待主线程退化成TASK_ZOMBIE <br> yield(); <br>... <br> switch_exec_pids(leader, current); //此调用exec的线程和主线程交换pid <br>... <br> if (state != TASK_ZOMBIE) <br> BUG(); <br> release_task(leader); <br> } <br> no_thread_group: <br>...//最终自立门户 <br> return 0; <br>} <br>void zap_other_threads(struct task_struct *p) <br>{ <br> struct task_struct *t; <br> p-&gt;signal-&gt;group_stop_count = 0; <br>... <br> for (t = next_thread(p); t != p; t = next_thread(t)) { //除我之外,全部该死 <br> if (t-&gt;state &amp; (TASK_ZOMBIE|TASK_DEAD)) //既然已死,由他去吧! <br> continue; <br> if (t != p-&gt;group_leader) //有个线程调用exec想单飞,其余的线程(可能包括主线程)必须退出,这种退出是内部争斗造成的,子线程的退出没有必要通知主线程。也就是说主线程不 用为子线程收尸,why?因为这个单飞的线程呆会儿要替代主线程,在大局上,主线程仅仅换了个执行者,并没有死亡。对于别的线程,因为主线程马上就要换成 单飞线程了,此单飞者没有义务为别的将死的线程收尸,因此就将其exit_signal设置为-1,由内核来收尸吧。结果就是主线程会变成僵尸,因此后面 的代码将会等待主线程成为僵尸。 <br> t-&gt;exit_signal = -1; <br> sigaddset(&amp;t-&gt;pending.signal, SIGKILL); <br> rm_from_queue(SIG_KERNEL_STOP_MASK, &amp;t-&gt;pending); <br> signal_wake_up(t, 1); <br> } <br>} <br>最终线程们在被唤醒后会执行do_exit,在do_exit中发现其exit_signal为-1便直接回收了进程。</linux></sys></linux></sys></errno.h></sched.h></unistd.h></stdio.h>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值