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

#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便直接回收了进程。

原文链接: https://blog.csdn.net/dog250/article/details/5303654

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/409998

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午11:45
下一篇 2023年4月26日 上午11:45

相关推荐