如果你在程序中调用了exit,那么很显然你的程序会退出,可是至于为何会退出那就是库的事情了,我为什么说只是库的事情而不关linux内核的事情呢?那是因为linux内核根本不管用户空间的行为策略。库的策略是什么?很简单的退出当前进程吗?如果是多线程的程序呢?多线程的程序它的行为又是什么呢?在我们探究库的行为以及探究库为何会有这样的行为之前首先谈谈内核对exit的实现sys_exit,在sys_exit中,我丝毫找不到关于退出线程的代码,按照常规的思考,如果sys_exit负责将本进程的所有线程都退出的话,那么合理的办法就是在本进程的signal结构体上append上一个SIGKILL信号,然后唤醒所有的线程的task_struct,因为一个进程的所有线程共享信号处理结构,因此每个被唤醒的线程的task_struct都会在信号处理时自己退出,这样看起来很不错,也很合理,如果是windows想到了这个主意,那么它肯定会采用的,可是这是linux。
linux中没有线程的概念,我这么说不是在说linux很落后吗?现代操作系统中都会有线程的实现。其实不然,在linux中,线程并不是一个操作系统的内秉性概念,在linux中只有进程,然后可以在进程的基础上轻松实现线程,这个意义上,线程仅仅是一个策略,一个由很多的实体概念组合而成的概念。其实在linux上,完全不能用传统的操作系统的标准来说明,比如线程,进程,然后又分了什么X程,这样岂不是越来越乱,每引入一个概念,操作系统的体系就要大动一番,在linux中就有一个执行绪的概念,该执行绪就是task_struct,你把它理解成进程也好,理解成线程也罢,其实它可以表达成任何可以执行的东西,在task_struct的基础上,我们可以垒砌很多外延的东西,比如进程,线程等等,在linux中,只有task_struct一个概念,进程,线程只是它的外延而已,task_struct和不同的特性组合就可以表示程不同的外延,比如,它和gid,uid等组合就是进程,它和线程组或者TGID组合就是线程。
理解了一行原则,exit的内核行为就十分简单了,既然内核原始的只有task_struct这一个执行绪概念,那么它就是应该有它的创建和销毁的内核api,并且在哲学意义上这两个内核api是对称的,我们很多人都了解linux的线程创建,其实也是用的fork,cone在内核不也是用fork实现的吗?创建一个现代操作系统的线程在linux中就是创建一个执行绪,也就是创建一个task_struct,那么do_fork就是干这个的,相反的,对于退出并没有多少人去关注,既然fork创建了一个task_struct,那么exit就是销毁一个task_struct,别的并不做什么,fork没有线程的概念,它只负责按照用户提供的参数创建一个task_struct,这里线程这个外延是通过参数体现的,既然参数可以赋予一个task_struct以线程的含义,那么fork中也就根据此含义对task_struct的字段进行了设置,以表示这是一个线程,在linux内核中并没有线程的概念,而仅有线程的外延,既然创建行为fork如此,那么销毁行为exit也是如此,如此一来就可以理解exit中根本就不可能有什么向本进程的所有线程发送退出信号一说,它只管销毁这个调用exit的执行绪的task_struct(其实是递减这个task_stuct的引用计数),而不管什么线程的概念,那么谁会去管线程的概念呢?当然是谁定义谁管了,比如Posix或者用户的其它库,内核将线程这个task_struct的外延导出给用户,那么用户就可以用这个外延的一系列特性以及行为准则来操作这个线程外延,故而用户库可以用内核提供的最小化的正交组合接口配上线程这个外延来组合成一个可以退出所有线程的接口,其实就是对于每一个线程调用其exit。
内核实现毕竟是内核实现,linux还是遵循posix的,因此它提供了一个系统调用sys_exit_group,之所以如此是因为这样的话,用户库就不必再费劲心机切入每个线程并且在每个线程调用exit了,当然这也不是linux内核所希望的。sys_exit_group中会调用zap_other_threads(current)来退出每个线程,不管怎样,exit_group的提供仅仅是为了遵循posix,而exit才是linux的设计中原汁原味的执行绪操作系统调用,其实在用户库里面完全可以用向所有的线程发送SIGKILL信号来实现exit。
举个例子来说明一切:
#include.h>
#include
#include.h>
#include
void direct_exit() //这个函数直接用系统调用实现了exit,即到了内核直接调用sys_exit
{
int a = 1,b = 0;
asm("movl %0,%%eax/n/t" /
"movl %1,%%ebx/n/t" /
"int $0x80/n/t" /
::"r" (a),"r" (b));
}
int handler( void *p)
{
while(1)
{
sleep(1);
printf("Sub thread is running/n");
}
}
int main(int argc, char* argv[])
{
int i = 0;
clone(handler, &i-1024, CLONE_SIGHAND|CLONE_VM|CLONE_THREAD, NULL);
while(1)
{
sleep(1);
printf("Main thread is running/n");
if(i++>15)
{
direct_exit();//直接退出,和线程没有关系,子线程handler继续运行,不受影响
//exit(); //调用库里面的exit,当然要遵循posix的线程语义,所有线程退出
}
}
return 0;
}
作为最后,为了使得本文的副标题不是空设,稍微谈一下linux的进程uid等特征,前面的文章说过,linux靠uid,gid实现了多用户,这个多用户是进程意义上的,如果按照本文前面说的概念和外延的观点来看的话,多用户只是为了实现多用户而必须的一个执行绪的参数而已,其实每个执行绪即task_struct都有一个uid和gid等信息而并不一定仅仅指进程,如下的例子可以证明:
int handler( void *p)
{
open("/root/b",O_CREATE);
perror("open b");
setuid(500);
seteuid(500);
open("/root/c",O_CREATE);
perror("open c");
}
int main(int argc, char* argv[])
{
int i = 0;
clone(handler, &i-1024, CLONE_SIGHAND|CLONE_VM|CLONE_THREAD, NULL);
if(getchar()=="w")
{
open("/root/a",O_CREATE);
perror("open a");
}
return 0;
}
在以上的例子中,以root用户运行这个代码,c的打开将失败,而a,b将成功,这里可以说明在不同的线程里面可以有不同的uid和gid,其实这里的执行绪已经不再是线程了,这个例子再次说明,在linux内核中没有线程的明确定义,再抽象一点其实也没有进程,而仅仅有执行绪而已,这个执行绪到底是什么,就看用户提供什么策略使他成为什么外延了。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273411