進程類似於人生:它們被產生,有或多或少有效生命,可以產生一個或多個子進程,最終都要死亡。一個微小的差異是進程之間沒有性別差異——每個進程都只有一個父親。那麼,作業系統有一個重要的概念——線程,在Linux上是怎麼實現的呢?可以明確的告訴你,Linux並沒有線程這個概念。呵呵,是不是Linux很落後呢,不是,恰恰相反,Linux提供了另一個概念——輕進程,其更具有擴充性,更偉大。
Linux是支援多線程的功能的,只不過是通過一個概念——輕量級進程來實現的。
從核心觀點看,進程的目的就是擔當分配系統資源(CPU時間、記憶體等)的實體。當一個進程建立時,它幾乎與父進程相同。它接受父進程地址空間的一個(邏輯)拷貝,並從進程建立系統調用(fork)的下一條指令開始執行與父進程相同的代碼。儘管父子進程可以共用含有程式碼(本文)的頁,但是它們各自有獨立的資料拷貝(包括堆和棧,寫時再拷貝),因此子進程對一個記憶體單元的修改對父進程是不可見的。
如今的UNIX核心早已擺脫了這種簡單的進程建立模式,大多數UNIX系統支援多線程應用程式:擁有很多相對獨立執行流的使用者程式共用應用程式的大部分資料結構。在這樣的系統中,一個進程由其他幾個使用者線程組成,每個線程都代表一個執行流。
而Linux核心的早期版本沒有提供多線程應用的支援。從核心觀點看,多線程應用程式僅僅是一個普通進程。多線程應用程式多個執行流的建立、處理、調度整個都是在使用者態進行的。使用者是通過使用C語言中,POSIX 1C提供的標準線程庫來實現使用者級線程,其中包括線程的建立、刪除、互斥和條件變數的同步操作以及調度和管理線程標準函數,無需核心的支援。
但是,這種多線程應用程式的實現方式不那麼令人滿意。例如,ULK-3上一個著名的例子,假設一個人機大戰象棋程式使用兩個線程:其中一個控製圖形化棋盤,等待人類選手的移動並顯示電腦的移動,而另一個思考棋的下一步移動。儘管第一個線程等待選手移動時,第二個線程應當繼續運行,以利用選手的考慮時間。但是,如果象棋程式僅是一個單獨的進程,第一個線程就不能簡單地發出等待使用者行為的阻塞系統調用;否則,第二個線程也被阻塞。因此,第一個線程必須使用複雜的非阻塞技術來確保進程仍然是可啟動並執行。
現在的Linux使用輕量級進程對多線程應用程式提供了更好的支援。兩個輕量級進程基本上可以共用一些資源,諸如地址空間、開啟檔案等等。只要其中一個修改共用資源,另一個就立即查看這種修改。當然,當兩個線程訪問共用資源時就必須同步它們自己。
那麼,實現多線程應用程式的一個簡單方式就是把輕量級進程與每個應用程式線程關聯起來。這樣,線程之間就可以通過簡單地共用同一記憶體位址空間、同一開啟檔案集等來訪問相同的應用程式資料結構集;同時,每個線程都可以由核心獨立調度,以便一個睡眠的同時另一個仍然可以運行。POSIX相容的多線程應用程式是由支援“線程組”的核心來處理的。在Linux中,一個線程組基本上就是實現了多線程應用的一組輕量級進程,對於像getpid(),kill(),和_exit()這樣的一些系統調用,它像一個組織,起整體的作用。
在Linux中,當使用系統調用clone()系統調用時,它建立的新進程與被調用者共用同一個使用者地址空間,從原理上來講,這個新建立的進程是調用者進程的一個線程。但是Linux並不承認,因為核心並沒有專門定義線程使用的資料結構,所以它的線程和進程在結構上並沒有任何區別。這也是為啥Linux強大的原因,因為Linux進程體系的結構已經夠合理了,不需要額外的資料結構就可以實現線程的功能,其實現線程的功能也是可以擴充的,因為你可以根據需要選擇共用的資源。呵呵,是不是很偉大啊?
1 clone()系統調用
在linux中,輕量級進程是由名為clone()的系統調用建立的:
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;
clone_flags = regs.ebx;
newsp = regs.ecx;
parent_tidptr = (int __user *)regs.edx;
child_tidptr = (int __user *)regs.edi;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
}
實際上,clone()是C語言庫中定義的一個封裝函數,它負責建立新輕量級進程的堆棧並且啟動clone系統調用。clone()所建立的進程可以製造出線程的效果,因為它會按照你給定的參數決定該共用哪些資訊。其使用以下參數:
fn:指定一個由新進程執行的函數。當這個函數返回時,子進程終止。函數返回一個整數,表示子進程的結束代碼。
arg:指向傳遞給fn()函數的資料。
flags:低位元組指定子進程結束時發送到父進程的訊號代碼,通常選擇SIGCHLD訊號。剩餘3個位元組給一clone標誌組用於編碼。標誌組如下表所示:
Flag name |
Description |
CLONE_VM |
Shares the memory descriptor and all Page Tables . |
CLONE_FS |
Shares the table that identifies the root directory and the current working directory, as well as the value of the bitmask used to mask the initial file permissions of a new file (the so-called file umask ). |
CLONE_FILES |
Shares the table that identifies the open files . |
CLONE_SIGHAND |
Shares the tables that identify the signal handlers and the blocked and pending signals . If this flag is true, the CLONE_VM flag must also be set. |
CLONE_PTRACE |
If traced, the parent wants the child to be traced too. Furthermore, the debugger may want to trace the child on its own; in this case, the kernel forces the flag to 1. |
CLONE_VFORK |
Set when the system call issued is a vfork( ) . |
CLONE_PARENT |
Sets the parent of the child (parent and real_parent fields in the process descriptor) to the parent of the calling process. |
CLONE_THREAD |
Inserts the child into the same thread group of the parent, and forces the child to share the signal descriptor of the parent. The child's tgid and group_leader fields are set accordingly. If this flag is true, the CLONE_SIGHAND flag must also be set. |
CLONE_NEWNS |
Set if the clone needs its own namespace, that is, its own view of the mounted filesystems ; it is not possible to specify both CLONE_NEWNS and CLONE_FS . |
CLONE_SYSVSEM |
Shares the System V IPC undoable semaphore operations . |
CLONE_SETTLS |
Creates a new Thread Local Storage (TLS) segment for the lightweight process; the segment is described in the structure pointed to by the tls parameter. |
CLONE_PARENT_SETTID |
Writes the PID of the child into the User Mode variable of the parent pointed to by the ptid parameter. |
CLONE_CHILD_CLEARTID |
When set, the kernel sets up a mechanism to be triggered when the child process will exit or when it will start executing a new program. In these cases, the kernel will clear the User Mode variable pointed to by the ctid parameter and will awaken any process waiting for this event. |
CLONE_DETACHED |
A legacy flag ignored by the kernel. |
CLONE_UNTRACED |
Set by the kernel to override the value of the CLONE_PTRACE flag (used for disabling tracing of kernel threads ). |
CLONE_CHILD_SETTID |
Writes the PID of the child into the User Mode variable of the child pointed to by the ctid parameter. |
CLONE_STOPPED |
Forces the child to start in the TASK_STOPPED state. |
child_stack:表示把使用者態堆棧指標賦給子進程的esp寄存器。調用進程(父進程)應該總是為子進程分配新的堆棧。
tls:表示局部儲存段TLS資料結構的地址,該結構是為新輕量級進程定義的。只有在CLONE_SETTLS標誌被設定時才有意義。
ptid:表示父進程的使用者態變數地址,該父進程具有與新輕量級進程相同的PID。只有在CLONE_PARENT_SETTID標誌被設定時才有意義。
ctid:表示新輕量級進程的使用者態變數地址,該進程具有這一類進程的PID。只有在CLONE_CHILD_SETTID被設定時才有意義。
不過,實現clone()系統調用的sys_clone()常式好像並沒有fn和arg等參數,只是一堆寄存器。別著急,因為封裝函數將fn指標存放在子進程堆棧的某個位置處,該位置就是該封裝函數本身返回地址存放的位置。arg指標正好存放在子進程堆棧中的fn下面。當封裝函數結束時,CPU從堆棧中取回返回地址,然後執行fn(arg)函數。
傳統的fork()系統調用在linux中是用clone()實現的,其中clone()的flags參數指定為SIGCHLD訊號以及所有清0的clone標誌,而它的child_stack參數是父進程當前的堆棧指標。因此,父進程和子進程暫時共用一個使用者態堆棧。但是,只要父子進程中有一個試圖去改變棧,則立即各自得到使用者態堆棧的一份拷貝。
vfork系統調用在Linux中也是用 clone( )實現的,其中clone( )的參數flags指定為SIGCHLD訊號和CLONE_VM以及CLONE_VFORK標誌,clone()的參數child_stack等於父進程當前的棧指標。