天天看點

線程的那些事兒zz

http://edsionte.com/techblog/archives/3223

通過作業系統原理課,我們知道程序是系統資源配置設定的基本機關,線程是程式獨立運作的基本機關。線程有時候也被稱作小型程序,首先,這是因為多個線程之間是可以共享資源的;其次,多個線程之間的切換所花費的代價遠遠比程序低。

在使用者态下,使用最廣泛的線程操作接口即為POSIX線程接口,即pthread。通過這組接口可以進行線程的建立以及多線程之間的并發控制等。

如果核心要對線程進行排程,那麼線程必須像程序那樣在核心中對應一個資料結構。程序在核心中有相應的程序描述符,即task_struct結構。事實上,從Linux核心的角度而言,并不存線上程這個概念。核心對線程并沒有設立特别的資料結構,而是與程序一樣使用task_struct結構進行描述。也就是說線程在核心中也是以一個程序而存在的,隻不過它比較特殊,它和同類的程序共享某些資源,比如程序位址空間,程序的信号,打開的檔案等。我們将這類特殊的程序稱之為輕量級程序(Light Weight Process)。

按照這種線程機制的了解,每個使用者态的線程都和核心中的一個輕量級程序相對應。多個輕量級程序之間共享資源,進而展現了多線程之間資源共享的特性。同時這些輕量級程序跟普通程序一樣由核心進行獨立排程,進而實作了多個程序之間的并發執行。

使用者線程和核心中輕量級程序的關聯通常實在符合POSIX線程标準的線程庫中完成的。支援輕量級程序的線程庫有三個:LinuxThreads、NGPT(Next-Generation POSIX Threads)和NPTL(Native POSIX Thread Library)。由于LinuxThreads并不能完全相容POSIX标準以及NGPT的放棄,目前Linux中所采用的線程庫即為NPTL。

POSIX标準規定在一個多線程的應用程式中,所有線程都必須具有相同的PID。從線程在核心中的實作可得知,每個線程其實都有自己的pid。為此,Linux引入了線程組的概念。在一個多線程的程式中,所有線程形成一個線程組。每一個線程通常是由主線程建立的,主線程即為調用pthread_create()的線程。是以該線程組中所有線程的pid即為主線程的pid。

對于線程組中的線程來說,其task_struct結構中的tpid字段儲存該線程組中主線程的pid,而pid字段則儲存每個輕量級程序的本身的pid。對于普通的程序而言,tgid和pid是相同的。事實上,getpid()系統調用中傳回的是程序的tgid而不是pid。

上面所描述的都是使用者态下的線程,而在核心中還有一種特殊的線程,稱之為核心線程(Kernel Thread)。由于在核心中程序和線程不做區分,是以也可以将其稱為核心程序。毫無疑問,核心線程在核心中也是通過task_struct結構來表示的。

核心線程和普通程序一樣也是核心排程的實體,隻不過他們有以下不同:

1).核心線程永遠都運作在核心态,而不同程序既可以運作在使用者态也可以運作在核心态。從另一個角度講,核心線程隻能之用大于PAGE_OFFSET(即3GB)的位址空間,而普通程序則可以使用整個4GB的位址空間。

2).核心線程隻能調用核心函數,而普通程序必須通過系統調用才能使用核心函數。

程序、線程以及核心線程都有對應的建立函數,不過這三者所對應的建立函數最終在核心都是由do_fork()進行建立的,具體的調用關系圖如下:

線程的那些事兒zz

從圖中可以看出,核心中建立程序的核心函數即為看do_fork(),該函數的原型如下:

  

該函數的參數個數是固定的,每個參數的功能如下:

clone_flags:代表程序各種特性的标志。低位元組指定子程序結束時發送給父程序的信号代碼,一般為SIGCHLD信号,剩餘三個位元組是若幹個标志或運算的結果。

stack_start:子程序使用者态堆棧的指針,該參數會被指派給子程序的esp寄存器。

regs:指向通用寄存器值的指針,當程序從使用者态切換到核心态時通用寄存器中的值會被儲存到核心态堆棧中。

stack_size:未被使用,預設值為0。

parent_tidptr:該子程序的父程序使用者态變量的位址,僅當CLONE_PARENT_SETTID被設定時有效。

child_tidptr:該子程序使用者态變量的位址,僅當CLONE_CHILD_SETTID被設定時有效。

既然程序、線程和核心線程在核心中都是通過do_fork()完成建立的,那麼do_fork()是如何展現其功能的多樣性?其實,clone_flags參數在這裡起到了關鍵作用,通過選取不同的标志,進而保證了do_fork()函數實作多角色——建立程序、線程和核心線程——功能的實作。clone_flags參數可取的标志很多,下面隻介紹幾個與本文相關的标志。

CLONE_VIM:子程序共享父程序記憶體描述符和所有的頁表。

CLONE_FS:子程序共享父程序所在檔案系統的根目錄和目前工作目錄。

CLONE_FILES:子程序共享父程序打開的檔案。

CLONE_SIGHAND:子程序共享父程序的信号處理程式、阻塞信号和挂起的信号。使用該标志必須同時設定CLONE_VM标志。

如果建立子程序時設定了上述标志,那麼子程序會共享這些标志所代表的父程序資源。

在使用者态程式中,可以通過fork()、vfork()和clone()三個接口函數建立程序,這三個函數在庫中分别對應同名的系統調用。系統調用函數通過128号軟中斷進入核心後,會調用相應的系統調用服務例程。這三個函數對應的服務曆程分别是sys_fork()、sys_vfork()和sys_clone()。

通過上述系統調用服務例程的源碼可以發現,三個服務曆程内部都調用了do_fork(),隻不過差别在于第一個參數所傳的值不同。這也正好導緻由這三個程序建立函數所建立的程序有不同的特性。下面對每種程序作以簡單說明。

fork():由于do_fork()中clone_flags參數除了子程序結束時傳回給父程序的SIGCHLD信号外并無其他特性标志,是以由fork()建立的程序不會共享父程序的任何資源。子程序會完全複制父程序的資源,也就是說父子程序相對獨立。不過由于寫時複制技術(Copy On Write,COW)的引入,子程序可以隻讀父程序的實體頁,隻有當兩者之一去寫某個實體頁時,核心此時才會将這個頁的内容拷貝到一個新的實體頁,并把這個新的實體頁配置設定給正在寫的程序。

vfork():do_fork()中的clone_flags使用了CLONE_VFORK和CLONE_VM兩個标志。CLONE_VFORK标志使得子程序先于父程序執行,父程序會阻塞到子程序結束或執行新的程式。CLONE_VM标志使得子程序共享父程序的記憶體位址空間(父程序的頁表項除外)。在COW技術引入之前,vfork()适用子程序形成後立馬執行execv()的情形。是以,vfork()現如今已經沒有特别的使用之處,因為寫實複制技術完全可以取代它建立程序時所帶來的高效性。

clone():clone通常用于建立輕量級程序。通過傳遞不同的标志可以對父子程序之間資料的共享和複制作精确的控制,一般flags的取值為CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND。由上述标志可以看到,輕量級程序通常共享父程序的記憶體位址空間、父程序所在檔案系統的根目錄以及工作目錄資訊、父程序目前打開的檔案以及父程序所擁有的信号處理函數。

每個線程在核心中對應一個輕量級程序,兩者的關聯是通過線程庫完成的。是以通過pthread_create()建立的線程最終在核心中是通過clone()完成建立的,而clone()最終調用do_fork()。

一個新核心線程的建立是通過在現有的核心線程中使用kernel_thread()而建立的,其本質也是向do_fork()提供特定的flags标志而建立的。

從上面的組合的flag可以看出,新的核心線程至少會共享父核心線程的記憶體位址空間。這樣做其實是為了避免指派調用線程的頁表,因為核心線程無論如何都不會通路使用者位址空間。CLONE_UNTRACED标志保證核心線程不會被任何程序所跟蹤,

繼續閱讀