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

来源:互联网 发布:年度总结数据分析报告 编辑:程序博客网 时间:2024/05/22 21:46

在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
#include
#include
#include
#include
#include
pid_t gettid()  //自己实现一个gettid,就是得到线程号。
{
    return syscall(SYS_gettid);
}
int clone_func1(void * data)
{
    int a = 3
    printf("sub2:%d,%d/n",getpid(),gettid());
    //scanf("%d",&a);           //运行中调试专用,相当于在此处下了一个断点,然后观察程序断点前后的行为
    execve("./mm",NULL,NULL);   //调用了exec,以检测是否主线程和别的线程会退出
}
int clone_func2(void * data)
{   
        printf("sub1:%d,%d/n",getpid(),gettid());
    while(1){}    //此线程永不退出
}
int main(int argc, char* argv[])
{   
    printf("main:%d,%d/n",getpid(),gettid());
    void * stack1 = malloc(10240);   //分配线程堆栈
    void * stack2 = malloc(10240);   //分配线程堆栈
    clone(&clone_func2, stack2+10240, CLONE_VM|CLONE_THREAD|CLONE_SIGHAND, NULL); //没有考虑文件相关的东西
    clone(&clone_func1, stack1+10240, CLONE_VM|CLONE_THREAD|CLONE_SIGHAND, NULL);
    while(1)  //主线程永不退出
    {
        sleep(1);
        printf("main/n");
    }
    return 0; 
}
执行以后发现除了“./mm”在运行之外,主线程和clone_func2线程都退出了,而且clone_func1还在exec后用了主线程的pid,我的mm.c如下:
#include
#include
pid_t gettid()
{
        return syscall(SYS_gettid);
}
int main()
{
        while(1)
        {sleep(1);
        printf("mm:%d,%d/n",getpid(),gettid());
        }
}
在 程序执行到scanf("%d",&a)的时候,实际上有3个线程,main,sub1,sub2,比如getpid得到1036,那么我们可以 从/proc/1036/status中看到线程的数量,还可以从/proc/1036/task目录中得到具体信息,其实该task目录和/proc /1036/目录的内容几乎一样,而性质却不一样,后者是进程信息,前者是各个线程的信息,从/proc文件系统这么安排目录结构的方式也可以看出 linux用相同的方式处理了性质完全不同的进程和线程。当我在终端输入一个数字并回车后,
scanf("%d",&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的引用 计数:
if (clone_flags & CLONE_VM) {
    atomic_inc(&oldmm->mm_users);
    mm = oldmm;
一 旦有进程exec了,就会递减mm的引用计数,这时计数当然不会为0,mm_struct不会被释放,用来用去就那一个mm_struct,只是其引用计 数在不断变化而已;如果在CLONE_VM的基础上加上了CLONE_SIGHAND,那么因为没有CLONE_THREAD,所以这个新clone的进 程不会和主进程在一个线程组,也就是说它们之间不是线程关系,这样的话,内核在exec的时候会处理以使得主进程不受影响,下面的内核代码中我会给出注 释。
根据线程的意义,只要一个执行exec单飞,那么整个进程就要随着飞,这在语义上是很合理的,exec本身就有蒸发当前地址空间的语义,因此posix就 作出了上面论述的约定。那么面对这些约定,linux内核是怎么实现的呢?现在又到了看内核的时间,这个实现将再次展示linux是怎样将进程和线程这两 种本质不同的东西纳入一个机制去管理的。在sys_execve中层层调用最终要调用flush_old_exec,我们从flush_old_exec 开始看:
int flush_old_exec(struct linux_binprm * bprm)
{
         char * name;
         int i, ch, retval;
         struct files_struct *files;
         char tcomm[sizeof(current->comm)];  //本程序的进程名,注意不是全路径,想得到全路径请看上一篇文章。
         retval = de_thread(current);  //和线程们分道扬镳,另外还杀死了这些线程们.
         if (retval)
                 goto out;
         files = current->files;        
         retval = unshare_files();
         if (retval)
                 goto out;
         retval = exec_mmap(bprm->mm); //替换进程地址空间
...//共享打开文件的处理
         name = bprm->filename;
         for (i=0; (ch = *(name++)) != '/0';) {
                 if (ch == '/')
                         i = 0;
                 else
                         if (i < (sizeof(tcomm) - 1))
                                 tcomm[i++] = ch;
         }
         tcomm[i] = '/0';
         set_task_comm(current, tcomm); //至此拷贝完毕进程名字
...
}
static inline int de_thread(struct task_struct *tsk)
{
         struct signal_struct *newsig, *oldsig = tsk->signal;
         struct sighand_struct *newsighand, *oldsighand = tsk->sighand;
         spinlock_t *lock = &oldsighand->siglock;
         int count;
         if (atomic_read(&oldsighand->count) <= 1)
                 return 0;
...//分配newsighand
         atomic_set(&newsighand->count, 1);
         memcpy(newsighand->action, oldsighand->action, sizeof(newsighand->action));
         newsig = NULL;
         if (atomic_read(&oldsig->count) > 1) {
...//分配以及初始化newsig
         }
         if (thread_group_empty(current))  //如果在clone中没有CLONE_THREAD参数,那么就不在一个线程组,那么就不用退出别的线程。
                 goto no_thread_group;
...
         if (oldsig->group_exit) {
...//别的线程已经在退出了,这里就不必再进行处理了,直接返回,我们的目的就是促使别的线程退出
         }
         oldsig->group_exit = 1;  //预示着别的线程全部要退出但是不包括这个线程,因为马上就要用新的newsighand了
         zap_other_threads(current);  //杀死别的全部线程但是不包含主线程。
...
    while (atomic_read(&oldsig->count) > count) {  //等待所有别的线程退出。
                 oldsig->group_exit_task = current;
                 oldsig->notify_count = count;
                 __set_current_state(TASK_UNINTERRUPTIBLE);
                 spin_unlock_irq(lock);
                 schedule();
                 spin_lock_irq(lock);
         }
...
         if (current->pid != current->tgid) {  //至此除了主线程之外的别的线程应该退出了,我们要等待主线程不可用从而可以用它的pid
                 struct task_struct *leader = current->group_leader, *parent;
                 struct dentry *proc_dentry1, *proc_dentry2;
                 unsigned long state, ptrace;
                 while (leader->state != TASK_ZOMBIE)  //等待主线程退化成TASK_ZOMBIE
                         yield();
...
                 switch_exec_pids(leader, current); //此调用exec的线程和主线程交换pid
...
                 if (state != TASK_ZOMBIE)
                         BUG();
                 release_task(leader);
         }
no_thread_group:
...//最终自立门户
         return 0;
}
void zap_other_threads(struct task_struct *p)
{
         struct task_struct *t;
         p->signal->group_stop_count = 0;
...
         for (t = next_thread(p); t != p; t = next_thread(t)) {  //除我之外,全部该死
                 if (t->state & (TASK_ZOMBIE|TASK_DEAD)) //既然已死,由他去吧!
                         continue;
                 if (t != p->group_leader)  //有个线程调用exec想单飞,其余的线程(可能包括主线程)必须退出,这种退出是内部争斗造成的,子线程的退出没有必要通知主线程。也就是说主线程不 用为子线程收尸,why?因为这个单飞的线程呆会儿要替代主线程,在大局上,主线程仅仅换了个执行者,并没有死亡。对于别的线程,因为主线程马上就要换成 单飞线程了,此单飞者没有义务为别的将死的线程收尸,因此就将其exit_signal设置为-1,由内核来收尸吧。结果就是主线程会变成僵尸,因此后面 的代码将会等待主线程成为僵尸。
                         t->exit_signal = -1;
                 sigaddset(&t->pending.signal, SIGKILL);
                 rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending);
                 signal_wake_up(t, 1);
         }
}
最终线程们在被唤醒后会执行do_exit,在do_exit中发现其exit_signal为-1便直接回收了进程。

原创粉丝点击