在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便直接回收了进程。
linux中exec操作对线程组的影响
最新推荐文章于 2022-08-05 17:27:44 发布