linux取消线程的原理,浅析 Linux 进程与线程
簡介
進程與線程是所有的程序員都熟知的概念,簡單來說進程是一個執行中的程序,而線程是進程中的一條執行路徑。進程是操作系統中基本的抽象概念,本文介紹 Linux 中進程和線程的用法以及原理,包括創建、消亡等。
進程
創建與執行
Linux 中進程的創建與執行分為兩個函數,分別是 fork 和 exec,如下代碼所示:
int main() {
pid_t pid;
if ((pid = fork() < 0) {
printf("fork error\n");
} else if (pid == 0) {
// child
if (execle("/home/work/bin/test1", "test1", NULL) < 0) {
printf("exec error\n");
}
}
// parent
if (waitpid(pid, NULL) < 0) {
printf("wait error\n");
}
}
fork 從當前進程創建一個子進程,此函數返回兩次,對于父進程而言,返回的是子進程的進程號,對于子進程而言返回 0。子進程是父進程的副本,擁有與父進程一樣的數據空間、堆和棧的副本,并且共享代碼段。
由于子進程通常是為了調用 exec 裝載其它程序執行,所以 Linux 采用了寫時拷貝技術,即數據段、堆和棧的副本并不會在 fork 之后就真的拷貝,只是將這些內存區域的訪問權限變為只讀,如果父子進程中有任一個要修改這些區域,才會修改對應的內存頁生成新的副本,這樣子是為了提高性能。
fork 之后父進程先執行還是子進程先執行是不確定的,所以如果要求父子進程進行同步,往往需要使用進程間通信。fork 之后子進程會繼承父進程的很多東西,如:
打開的文件
實際用戶 ID、組用戶 ID 等
進程組
當前工作目錄
信號屏蔽和安排
...
父子進程的區別在于:
進程 ID 不同
子進程不繼承父進程的文件鎖
子進程的未處理信號集為空
...
fork 之后,子進程可以執行不同的代碼段,也可以使用 exec 函數執行其它的程序。
進程描述符
進程在運行的時候,除了加載程序,還會打開文件、占用一些資源,并且會進入睡眠等其它狀態。操作系統為了支持進程的運行,必然有一個數據結構保存著這些東西。在 Linux 中,一個名為 task_struct 的結構保存了進程運行時的所有信息,稱為進程描述符:
struct task_struct {
unsigned long state;
int prio;
pid_t pid;
...
}
進程描述符完整描述了一個進程:打開的文件、進程的地址空間、掛起的信號以及進程的信號等。系統將所有的進程描述符放在一個雙端循環列表中:
進程描述符具體存放在內存的哪里呢?在內核棧的末尾。眾所周知,進程中占用的內存一部分是棧,主要用于函數調用,不過這里說的棧一般指的是用戶空間的棧,其實進程還有內核棧。當進程調用系統調用的時候,進程陷入內核,此時內核代表進程執行某個操作,此時使用的是內核空間的棧。
進程狀態
進程描述符中的 state 描述了進程當前的狀態,有如下 5 種:
TASK_RUNNING:進程是可執行的,此時進程要么是正在執行,要么是在運行隊列中等待被調度
TASK_INTERRUPTIBLE:進程正在睡眠(阻塞),等待條件達成。如果條件達成或者收到信號,進程會被喚醒并且進入可運行狀態
TASK_UNINTERRUPTIBLE:進程處于不可中斷狀態,就算信號也無法喚醒,這種狀態用的比較少
_TASK_TRACED:進程正在被其它進程追蹤,通常是為了調試
_TASK_STOPPED:進程停止運行,通常是接收到 SIGINT、SIGTSTP 信號的時候。
fork 與 vfork
在使用了寫時拷貝后,fork 的實際開銷就是復制父進程的頁表以及給子進程創建唯一的進程描述符。fork 為了創建一個進程到底做了什么呢?fork 其實調用了 clone,這是一個系統調用,通過給 clone 傳遞參數,表明父子進程需要共享的資源,clone 內部會調用 do_fork,而 do_fork 的主要邏輯在 copy_process 中,大致有以下幾步:
為新進程創建一個內核棧以及 task_struct,此時它們的值與父進程相同
將 task_struct 中某些變量,如統計信息,設置為 0
將子進程狀態設置為 TASK_UNINTERRUPTIBLE,保證它不會被投入運行
分配 pid
根據傳遞給 clone 的參數,拷貝或者共享打開的文件、文件系統信息、信號處理函數以及進程的地址空間等。
返回指向子進程的指針
除了 fork 之外,Linux 還有一個類似的函數 vfork。它的功能與 vfork 相同,子進程在父進程的地址空間運行。不過,父進程會阻塞,直到子進程退出或者執行 exec。需要注意的是,子進程不能向地址空間寫入數據。如果子進程修改數據、進行函數調用或者沒有調用 exec 那么會帶來未知的結果。vfork 在 fork 沒有寫時拷貝的技術時是有著性能優勢,現在已經沒有太大的意義。
退出
進程的運行終有退出的時候,有 8 種方式使進程終止,其中 5 中為正常終止:
從 main 返回
調用 exit
調用 _exit 或 _Exit
最后一個線程從其啟動例程返回
從最后一個線程調用 pthread_exit
異常終止方式有 3 種:
調用 abort
接收到一個信號
最后一個線程對取消請求作出響應
exit 函數會執行標準 I/O 庫的清理關閉操作:對所有打開的流調用 fclose 函數,所有緩沖中的數據會被沖洗,而 _exit 會直接陷入內核。看下面的代碼:
#include
#include
#include
int main()
{
printf("line 1\n");
printf("line 2"); // 沒有換行符
// exit(0)
_exit(0);
}
其中第二行輸出沒有 \n,如果末尾調用的是 _exit,則只會輸出 line 1,如果替換為 exit,則第二行 line 2 也會輸出。
進程退出最終會執行到系統的 do_exit 函數,主要有以下步驟:
刪除進程定時器
釋放進程占用的頁表
遞減文件描述符的引用計數,如果某個引用計數為 0,則關閉文件
向父進程發信號,給子進程重新找養父,并且把進程狀態設置為 EXIT_ZOMBIE
調度其它進程
此時,進程的大部分資源都被釋放了,并且不會進入運行狀態。不過還有些資源保持著,主要是 task_struct 結構。之所以要留著是給父進程提供信息,讓父進程知道子進程的一些信息,如退出碼等。
需要注意的是,如果父進程不進行任何操作,那么這些信息會一直保留在內存中,成為僵尸進程,占用系統資源,如下面的代碼:
int main() {
pid_t pid = fork();
if (pid == 0) {
exit(0);
} else {
sleep(10);
}
}
父進程 fork 出子進程后,子進程立刻退出,而父進程則進入睡眠。運行程序,觀察進程狀態:
可以看到,第一行進程為父進程,狀態為 S,表示其正在睡眠,而第二為子進程,狀態為 Z,表示僵尸狀態(zombie),因為此時子進程已經退出,然而 task_struct 還保存著,等待父進程來處理。
父進程如何處理?調用 wait 函數,正如本文第一段代碼中所示。當父進程調用 wait 后,子進程的 task_struct 才被釋放。
如果父進程先結束了呢?在父進程結束的時候,會為其子進程找新的父進程,一直往上找,最終成為 init 進程的子進程。init 子進程會負責調用 wait 釋放子進程的遺留信息。
線程
上面介紹了 Linux 中的進程,那么線程又是怎么的?網上一些說法是,Linux 中并沒有真正的內核線程,線程是以進程的方式實現的,只不過它們之間會共享內存。這種說法有一定道理,但并不完全準確。
Linux 中剛開始是不支持線程的,后來出現了線程庫 LinuxThreads,不過它有很多問題,主要是與 POXIS 標準不兼容。自 Linux 2.6 以來,Linux 中使用的就是新的線程庫,NPTL(Native POSIX Thread Library)。
NPTL 中線程的創建也是通過 clone 實現的,并且通過以下的參數表明了線程的特征:
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS |
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
部分參數的含義如下:
CLONE_VM:所有線程都共享同一個進程地址空間
CLONE_FILES:所有線程都共享進程的文件描述符列表
CLONE_THREAD:所有線程都共享同一個進程 ID 以及 父進程 ID
NPTL 所實現的線程庫是 1:1 的從用戶線程映射到內核線程,并且內核為了實現 POSIX 的線程標準也做了一些改動,比如對于信號的處理等。所以說 Linux 內核完全不區分進程和線程,甚至不知道線程的存在這種說法現在是不準確的。
線程間共享代碼段、堆以及打開的文件等,線程私有的部分有以下內容:
線程 ID
寄存器
錯誤碼(errno)
棧
信號屏蔽
...
總結
Linux 中進程與線程的使用是程序員必備的技能,而如果能了解一些實現的原理,則可以使用的更加得心應手。本文介紹了 Linux 中進程的創建、執行以及消亡等,對于線程的實現及其與進程的關系也進行了簡單的說明。進程和線程還有更多的內容可以研究,如進程調度、進程以及線程間的通信等。
參考
《UNIX 環境高級編程》
《Linux 內核設計與實現》
總結
以上是生活随笔為你收集整理的linux取消线程的原理,浅析 Linux 进程与线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lisp 设计盘形齿轮铣刀_机械设计基础
- 下一篇: linux 其他常用命令