作者
楊沙洲
內容
·基礎知識
線程和進程
·Linux
內核中的輕量進程實現
·LinuxThread的線程機制
·其他的線程實現機制
·參考資料
·關於作者
自從多線程編程的概念出現在 Linux 中以來
Linux 多線應用的發展總是與兩個問題脫不開干系
兼容性
效率
本文從線程模型入手
通過分析目前 Linux 平台上最流行的 LinuxThreads 線程庫的實現及其不足
描述了 Linux 社區是如何看待和解決兼容性和效率這兩個問題的
一
基礎知識
線程和進程
按照教科書上的定義
進程是資源管理的最小單位
線程是程序執行的最小單位
在操作系統設計上
從進程演化出線程
最主要的目的就是更好的支持SMP以及減小(進程/線程)上下文切換開銷
無論按照怎樣的分法
一個進程至少需要一個線程作為它的指令執行體
進程管理著資源(比如cpu
內存
文件等等)
而將線程分配到某個cpu上執行
一個進程當然可以擁有多個線程
此時
如果進程運行在SMP機器上
它就可以同時使用多個cpu來執行各個線程
達到最大程度的並行
以提高效率
同時
即使是在單cpu的機器上
采用多線程模型來設計程序
正如當年采用多進程模型代替單進程模型一樣
使設計更簡潔
功能更完備
程序的執行效率也更高
例如采用多個線程響應多個輸入
而此時多線程模型所實現的功能實際上也可以用多進程模型來實現
而與後者相比
線程的上下文切換開銷就比進程要小多了
從語義上來說
同時響應多個輸入這樣的功能
實際上就是共享了除cpu以外的所有資源的
針對線程模型的兩大意義
分別開發出了核心級線程和用戶級線程兩種線程模型
分類的標准主要是線程的調度者在核內還是在核外
前者更利於並發使用多處理器的資源
而後者則更多考慮的是上下文切換開銷
在目前的商用系統中
通常都將兩者結合起來使用
既提供核心線程以滿足smp系統的需要
也支持用線程庫的方式在用戶態實現另一套線程機制
此時一個核心線程同時成為多個用戶態線程的調度者
正如很多技術一樣
混合
通常都能帶來更高的效率
但同時也帶來更大的實現難度
出於
簡單
的設計思路
Linux從一開始就沒有實現混合模型的計劃
但它在實現上采用了另一種思路的
混合
在線程機制的具體實現上
可以在操作系統內核上實現線程
也可以在核外實現
後者顯然要求核內至少實現了進程
而前者則一般要求在核內同時也支持進程
核心級線程模型顯然要求前者的支持
而用戶級線程模型則不一定基於後者實現
這種差異
正如前所述
是兩種分類方式的標准不同帶來的
當核內既支持進程也支持線程時
就可以實現線程
進程的
多對多
模型
即一個進程的某個線程由核內調度
而同時它也可以作為用戶級線程池的調度者
選擇合適的用戶級線程在其空間中運行
這就是前面提到的
混合
線程模型
既可滿足多處理機系統的需要
也可以最大限度的減小調度開銷
絕大多數商業操作系統(如Digital Unix
Solaris
Irix)都采用的這種能夠完全實現POSIX
c標准的線程模型
在核外實現的線程又可以分為
一對一
多對一
兩種模型
前者用一個核心進程(也許是輕量進程)對應一個線程
將線程調度等同於進程調度
交給核心完成
而後者則完全在核外實現多線程
調度也在用戶態完成
後者就是前面提到的單純的用戶級線程模型的實現方式
顯然
這種核外的線程調度器實際上只需要完成線程運行棧的切換
調度開銷非常小
但同時因為核心信號(無論是同步的還是異步的)都是以進程為單位的
因而無法定位到線程
所以這種實現方式不能用於多處理器系統
而這個需求正變得越來越大
因此
在現實中
純用戶級線程的實現
除算法研究目的以外
幾乎已經消失了
Linux內核只提供了輕量進程的支持
限制了更高效的線程模型的實現
但Linux著重優化了進程的調度開銷
一定程度上也彌補了這一缺陷
目前最流行的線程機制LinuxThreads所采用的就是線程
進程
一對一
模型
調度交給核心
而在用戶級實現一個包括信號處理在內的線程管理機制
Linux
LinuxThreads的運行機制正是本文的描述重點
二
Linux
內核中的輕量進程實現
最初的進程定義都包含程序
資源及其執行三部分
其中程序通常指代碼
資源在操作系統層面上通常包括內存資源
IO資源
信號處理等部分
而程序的執行通常理解為執行上下文
包括對cpu的占用
後來發展為線程
在線程概念出現以前
為了減小進程切換的開銷
操作系統設計者逐漸修正進程的概念
逐漸允許將進程所占有的資源從其主體剝離出來
允許某些進程共享一部分資源
例如文件
信號
數據內存
甚至代碼
這就發展出輕量進程的概念
Linux內核在
x版本就已經實現了輕量進程
應用程序可以通過一個統一的clone()系統調用接口
用不同的參數指定創建輕量進程還是普通進程
在內核中
clone()調用經過參數傳遞和解釋後會調用do_fork()
這個核內函數同時也是fork()
vfork()系統調用的最終實現
int do_fork(unsigned long clone_flags unsigned long stack_start
struct pt_regs *regs unsigned long stack_size)
其中的clone_flags取自以下宏的或值
#define CSIGNAL xff
/* signal mask to be sent at exit */
#define CLONE_VM x
/* set if VM shared between processes */
#define CLONE_FS x
/* set if fs info shared between processes */
#define CLONE_FILES x
/* set if open files shared between processes */
#define CLONE_SIGHAND x
/* set if signal handlers and blocked signals shared */
#define CLONE_PID x
/* set if pid shared */
#define CLONE_PTRACE x
/* set if we want to let tracing continue on the child too */
#define CLONE_VFORK x
/* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT x
/* set if we want to have the same parent as the cloner */
#define CLONE_THREAD x
/* Same thread group? */
#define CLONE_NEWNS x
/* New namespace group? */
#define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
在do_fork()中不同的clone_flags將導致不同的行為對於LinuxThreads它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)參數來調用clone()創建線程表示共享內存共享文件系統訪問計數共享文件描述符表以及共享信號處理方式本節就針對這幾個參數看看Linux內核是如何實現這些資源的共享的
CLONE_VM
do_fork()需要調用copy_mm()來設置task_struct中的mm和active_mm項這兩個mm_struct數據與進程所關聯的內存空間相對應如果do_fork()時指定了CLONE_VM開關copy_mm()將把新的task_struct中的mm和active_mm設置成與current的相同同時提高該mm_struct的使用者數目(mm_struct::mm_users)也就是說輕量級進程與父進程共享內存地址空間由下圖示意可以看出mm_struct在進程中的地位
CLONE_FS
task_struct中利用fs(struct fs_struct *)記錄了進程所在文件系統的根目錄和當前目錄信息do_fork()時調用copy_fs()復制了這個結構而對於輕量級進程則僅增加fs>count計數與父進程共享相同的fs_struct也就是說輕量級進程沒有獨立的文件系統相關的信息進程中任何一個線程改變當前目錄根目錄等信息都將直接影響到其他線程
CLONE_FILES
一個進程可能打開了一些文件在進程結構task_struct中利用files(struct files_struct *)來保存進程打開的文件結構(struct file)信息do_fork()中調用了copy_files()來處理這個進程屬性輕量級進程與父進程是共享該結構的copy_files()時僅增加files>count計數這一共享使得任何線程都能訪問進程所維護的打開文件對它們的操作會直接反映到進程中的其他線程
CLONE_SIGHAND
每一個Linux進程都可以自行定義對信號的處理方式在task_struct中的sig(struct signal_struct)中使用一個struct k_sigaction結構的數組來保存這個配置信息do_fork()中的copy_sighand()負責復制該信息輕量級進程不進行復制而僅僅增加signal_struct::count計數與父進程共享該結構也就是說子進程與父進程的信號處理方式完全相同而且可以相互更改
do_fork()中所做的工作很多在此不詳細描述對於SMP系統所有的進程fork出來後都被分配到與父進程相同的cpu上一直到該進程被調度時才會進行cpu選擇
盡管Linux支持輕量級進程但並不能說它就支持核心級線程因為Linux的線程和進程實際上處於一個調度層次共享一個進程標識符空間這種限制使得不可能在Linux上實現完全意義上的POSIX線程機制因此眾多的Linux線程庫實現嘗
From:http://tw.wingwit.com/Article/program/Oracle/201311/17455.html