linux kernel 进程管理,Linux内核 | 进程管理
1. 進程和線程
1.1 定義
進程是處于運行狀態的程序和相關資源的總稱,是資源分配的最小單位。
線程是進程的內部的一個執行序列,是CPU調度的最小單位。有一段可執行程序代碼。
有一段進程專用的系統堆棧空間和系統空間堆棧。
有進程描述符,用于描述進程的相關信息。
有獨立的存儲空間,也就是專有的用戶空間,相應的又會有用戶空間堆棧。
Linux系統對于線程實現非常特殊,他并不區分線程和進程,線程只是一種特殊的進程罷了。從上面四點要素來看,擁有前三點而缺第四點要素的就是線程,如果完全沒有第四點的用戶空間,那就是系統線程,如果是共享用戶空間,那就是用戶線程。
1.2 主要區別
進程作為分配資源的基本單位,而把線程作為獨立運行和獨立調度的基本單位,由于線程比進程更小,基本上不擁有系統資源,故對它的調度所付出的開銷就會小得多,能更高效的提高系統多個程序間并發執行的程度。
進程和線程的主要差別在于它們是不同的操作系統資源管理方式。進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等于整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些要求同時進行并且又要共享某些變量的并發操作,只能用線程,不能用進程。
總結:linux中,進程和線程唯一區別是有沒有獨立的地址空間。
2. 進程描述符及任務結構
32位機器上,大約有1.7KB,進程描述符完整描述一個正在執行的進程的所有信息。
任務隊列(雙向循環鏈表)
struct task_struct {
volatile long state; // -1為不可運行, 0為可運行, >0為已中斷
int lock_depth; // 鎖的深度
unsigned int policy; // 調度策略:一般有FIFO,RR,CFS
pid_t pid; // 進程標識符,用來代表一個進程
struct task_struct *parent; // 父進程
struct list_head children; // 子進程
struct list_head sibling; // 兄弟進程
}
2.1 分配進程描述符
2.1.1 slab分配器
linux采用slab分配器分配task_struct結構
目的:對象復用和緩存著色。
slab分配器動態生成task_struct,只需在棧底(相對于向下增長的棧)或棧頂(相對于向上增長的棧)創建一個新結構struct thread_info。
2.1.2 進程描述符存放
PID最大值默認為32768(short int 短整形的最大值)可通過修改/proc/sys/kernel/pid_max提高上限。
current宏查找當前正在運行進程的進程描述符。
x86系統中,current把棧指針后13個有效位屏蔽掉,用來計算出thread_info的偏移。
current_thread_info函數movl $-8192,%eax
andl %esp,%eax
2.1.3 進程狀態TASK_RUNNING:1. 正在執行 2. 在運行隊列中等待執行
TASK_INTERRUPTIBLE:阻塞(可中斷)
TASK_UNINTERRUPTIBLE:阻塞(不可中斷)
\_\_TASK_TRACED:被其他進程跟蹤的進程
\_\_TASK_STOPPED:進程停止
陷入內核執行系統調用
異常處理程序
2.1.4 進程家族樹
init進程所有進程都是PID為1的init進程的后代
內核在系統啟動的最后階段啟動init進程。
init進程目的:讀取系統的初始化腳本,并執行其他的相關程序,最終完成系統啟動的整個過程。
task_struct中記錄父子進程parent指針(指向父進程)
children子進程鏈表
3. 進程創建
其他操作系統提供產生(spawn)進程機制,首先在新地址空間里創建進程,讀入可執行文件,最后開始執行。
UNIX將上述機制流程分成兩步fork()和exec()fork()拷貝當前進程創建一個子進程
exec()負責讀取可執行文件,并將其入地址空間
3.1 寫時拷貝(copy-on-write)
使地址空間上的頁的拷貝推遲到實際發生寫入的時候才進行。
原理:如果有進程試圖修改一個頁,就會產生一個缺頁中斷。內核處理缺頁中斷的方式就是對該頁進行一次透明復制。這時會清除頁面的COW屬性,表示著它不再被共享。
3.2 fork()函數
fork()的實際開銷就是復制父進程的頁表以及給子進程創建唯一的進程描述符。
在現在linux內核中,fork()實際上是由clone()系統調用實現的
3.2.1 copy_process()函數dup_task_struct()為新進程創建一個內核棧,thread_info結構和task_struct與當前進程相同。父子進程描述符是完全相同的。(分配空間)
檢查并確保新創建這個進程后,當前用戶所擁有的進程數目沒有超出給它分配的資源的限制。(檢查邊界)
子進程與父進程區別開。進程描述符的許多成員都要被清0或設初始值,那些不是繼承來的進程描述符的成員,主要是統計信息。task_struct中的大多數數據都依然未被修改。(子進程初始化)
子進程的狀態被設置為TASK_UNINTERRUPTIBLE(不可中斷,阻塞狀態),以保證它不會投入運行。(設置子進程狀態)
copy_process()調用copy_flags()以更新task_struct的flags成員。(設置標志位)表明進程是否擁有超級用戶權限的PF_SUPERPRIV標志被清0
表明進程還沒有調用exec()函數的PF_FORKNOEXEC標志被設置
調用alloc_pid()為新進程分配一個有效的PID。(為子進程分配pid)
根據傳遞給clone()的參數,copy_process()拷貝或共享打開的文件、文件系統信息、信號處理函數、進程地址空間和命名空間等。一般情況下,這些資源會被給定的進程的所有線程共享;否則,這些資源對每個進程是不同的,因此被拷貝到這里。(將資源參數標志賦值給結構體)
copy_process()做掃尾工作并返回一個指向子進程的指針,再回到do_fork()函數,如果copy_process()函數成功返回,新創建的子進程被喚醒并讓其投入運行。(返回子進程指針,并喚醒子進程執行)
注:內核有意讓子進程先執行,并非總能如此,因為一般子進程都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷。因為父進程先執行,可能往地址空間寫入。
3.3 vfork函數
vfork()和fork()區別:vfork()不拷貝父進程的頁表項。
vfork():子進程作為父進程的一個單獨線程在它的地址空間里運行,父進程被阻塞,直到子進程退出或執行exec(),子進程不能向地址空間寫入。
4. 線程創建
線程創建和進程創建基本一致,通過調用clone()函數傳遞的參數標志,指明需要共享的資源。
創建線程clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
// CLONE_VM : 地址空間
// CLONE_FS : 文件系統
// CLONE_FILES : 文件描述符
// CLONE_SIGHAND : 信號處理程序及被阻斷的信號
創建進程(等同fork()函數)clone(SIGCHLD,0);
創建進程(等同vfork()函數)clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
4.1 內核線程
內核線程只在內核空間執行,從不切換到用戶空間。
內核線程和普通進程的區別:內核線程沒有獨立的地址空間。(task_struct的mm指針被設置為NULL)
內核線程只能由其他內核線程創建,通過kthreadd內核線程衍生出所有新的內核線程。(kthreadd是所有內核線程的祖宗)
4.1.1 kthreadd內核線程
kthreadd內核線程是在內核初始化時被創建,循環執行kthreadd函數,它的作用是管理調度其它的內核線程。
kthreadd函數的作用是運行kthread_create_list全局鏈表中維護的kthread。可以調用kthread_create函數創建一個kthread,它會被加入到kthread_create_list鏈表中,同時kthread_create函數會喚醒kthreadd_task。kthreadd在執行kthread會調用老的接口,kthreadd內核線程在運行kthread時,會調用老接口kernel_thread,它會運行一個名為“kthread”的內核線程,去運行創建kthread,被執行的kthread會從kthread_create_list鏈表中刪除,并且kthreadd會不斷地調用scheduler讓出CPU,這個線程不能關閉。
創建內核線程,不運行
kthread_create函數(源代碼 | linux/kthread.h | v5.4)是通過clone()系統調用,創建一個內核線程,但新創建的線程處于不可運行狀態。kthread_create(threadfn, data, namefmt, arg...)
創建內核線程,并運行
kthread_run函數(源代碼 | linux/kthread.h | v5.4),通過調用kthread_create函數創建內核線程,然后調用wake_up_process()進行喚醒。#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
內核線程停止int kthread_stop(struct task_struct *k);
5. 進程終結
釋放所占用的資源,并告知父進程。
一般來說,進程的析構是自身引起的,它發生在進程調用exit()系統調用的時候。
既可以顯式地調用exit()這個系統調用,也可以隱性地從某個程序的主函數返回。(C語言編輯器會在main()函數的返回點后面放置調用exit代碼)
終結的任務大部分都靠do_exit()()
5.1 do_exit()函數將task_struct中標志成員設置成PF_EXITING
調用del_timer_sync()刪除任一內核定時器。確保沒有定時器在排隊,也沒有定時器處理程序在運行。
如果BSD的記賬功能是開啟的,do_exit()調用acct_update_integrals()來輸出記賬信息。
調用exit_mm()函數釋放進程占用的mm_struct,如果沒有別的進程同時使用它們(也就是說,這個地址空間沒有被共享),就徹底釋放它們。
調用sem__exit()函數,如果進程排隊等待IPC信號,它則離開隊列。
調用exit_files()和exit_fs()分別遞減文件描述符,文件系統數據引用計數,如果其中某個引用計數的數值降為零,那就不用代表沒有進程在使用相應的資源,此時可以釋放。
把存放在task_struct的exit_code()成員中的任務退出代碼置為由exit()提供的退出代碼,或者去完成任何其他有內核機制規定的退出動作。退出代碼存放在這里供父進程隨時檢索。
調用exit_notify向父進程發送信號,給子進程重新找養父(其他線程或init進程),并將存放在task_struct結構中的exit_state設置為EXIT_ZOMBIE。
do_exit調用schedule()切換到新的進程,因為處于EXIT_ZOMBIE狀態的進程不會被調度,所以這是進程所執行的最后一段代碼,do_exit()永不返回。
5.2 wait族函數
wait族函數都是通過唯一但很復雜的一個系統調用wait4()來實現的,掛起調用它的進程,直到其中的一個子進程退出,此時函數會返回子進程的PID。此外,調用此函數時提供的指針會包含子函數的退出代碼。
總結
以上是生活随笔為你收集整理的linux kernel 进程管理,Linux内核 | 进程管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux系统中agent服务器,Zab
- 下一篇: linux 其他常用命令