如果你在程序中調用了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