<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Linux 內(nèi)核線程及普通進(jìn)程總結(jié)

          共 6214字,需瀏覽 13分鐘

           ·

          2022-04-18 18:44

          Linux 中的進(jìn)程與線程

          對于 Linux 來講,所有的線程都當(dāng)作進(jìn)程來實(shí)現(xiàn),因?yàn)闆]有單獨(dú)為線程定義特定的調(diào)度算法,也沒有單獨(dú)為線程定義特定的數(shù)據(jù)結(jié)構(gòu)(所有的線程或進(jìn)程的核心數(shù)據(jù)結(jié)構(gòu)都是?task_struct)。

          對于一個進(jìn)程,相當(dāng)于是它含有一個線程,就是它自身。對于多線程來說,原本的進(jìn)程稱為主線程,它們在一起組成一個線程組。

          進(jìn)程擁有自己的地址空間,所以每個進(jìn)程都有自己的頁表。而線程卻沒有,只能和其它線程共享某一個地址空間和同一份頁表。

          這個區(qū)別的根本原因是,在 進(jìn)程/線程 創(chuàng)建時,因是否拷貝當(dāng)前進(jìn)程的地址空間還是共享當(dāng)前進(jìn)程的地址空間,而使得指定的參數(shù)不同而導(dǎo)致的。

          具體地說,進(jìn)程和線程的創(chuàng)建都是執(zhí)行?clone?系統(tǒng)調(diào)用進(jìn)行的。而?clone?系統(tǒng)調(diào)用會執(zhí)行?do_fork?內(nèi)核函數(shù),而它則又會調(diào)用?copy_process?內(nèi)核函數(shù)來完成。主要包括如下操作:

          在調(diào)用?copy_process?的過程中,會創(chuàng)建并拷貝當(dāng)前進(jìn)程的?task_stuct,同時還會創(chuàng)建屬于子進(jìn)程的?thread_info?結(jié)構(gòu)以及內(nèi)核棧。此后,會為創(chuàng)建好的?task_stuct?指定一個新的?pid(在?task_struct?結(jié)構(gòu)體中)。然后根據(jù)傳遞給?clone?的參數(shù)標(biāo)志,來選擇拷貝還是共享打開的文件,文件系統(tǒng)信息,信號處理函數(shù),進(jìn)程地址空間等。這就是進(jìn)程和線程不一樣地方的本質(zhì)所在。

          三個數(shù)據(jù)結(jié)構(gòu)

          每個進(jìn)程或線程都有三個數(shù)據(jù)結(jié)構(gòu),分別是:?

          • struct thread_info

          • struct task_struct?

          • 內(nèi)核棧

          注意,雖然線程與主線程共享地址空間,但是線程也是有自己獨(dú)立的內(nèi)核棧的。

          thread_info?對象中存放的進(jìn)程/線程的基本信息,它和這個 進(jìn)程/線程 的內(nèi)核棧存放在內(nèi)核空間里的一段 2 倍頁長的空間中。其中?thread_info?結(jié)構(gòu)存放在低地址段的末尾,其余空間用作內(nèi)核棧。內(nèi)核使用 伙伴系統(tǒng) 為每個進(jìn)程/線程分配這塊空間。

          thread_info?結(jié)構(gòu)體中有一個?struct task_struct *tasktask?指向的就是這個進(jìn)程或線程相關(guān)的?task_struct?對象(也在內(nèi)核空間中),這個對象叫做進(jìn)程描述符(叫做任務(wù)描述符更為貼切,因?yàn)槊總€線程也都有自己的?task_struct)。內(nèi)核使用?slab?分配器為每個進(jìn)程/線程分配這塊空間。

          如下圖所示:

          task_struct 結(jié)構(gòu)體

          每個進(jìn)程或線程都有只屬于自己的 task_struct 對象,是它們各自最為核心的數(shù)據(jù)結(jié)構(gòu)。

          task_struct 結(jié)構(gòu)體中的主要元素

          struct?thread_info?*thread_infothread_info?指向該進(jìn)程/線程的基本信息。
          struct?mm_struct?*mmmm_struct?對象用來管理該進(jìn)程/線程的頁表以及虛擬內(nèi)存區(qū)。
          struct?mm_struct?*active_mm。主要用于內(nèi)核線程訪問主內(nèi)核頁全局目錄。
          struct?fs_struct?*fsfs_struct?是關(guān)于文件系統(tǒng)的對象。
          struct?files_struct?*filesfiles_struct?是關(guān)于打開的文件的對象。
          struct?signal_struct?*signalsignal_struct?是關(guān)于信號的對象。

          task_struct 結(jié)構(gòu)體中的三個 ID 與一個指針

          • pid:每個 task_struct 都會有一個不同的 ID,就是這個 PID。

          • tid:線程 ID,用來標(biāo)識每個線程的。

          • tgid:線程組領(lǐng)頭線程的 PID,事實(shí)上就是主線程的 PID。當(dāng)創(chuàng)建一個子進(jìn)程時,它的 tgid 與 pid 相等;當(dāng)創(chuàng)建一個線程時,它的 tgid 等于主線程的 pid。getpid() 函數(shù)事實(shí)上返回的是當(dāng)前進(jìn)程或線程的 tgid。

          • pgid:進(jìn)程組領(lǐng)頭進(jìn)程的 PID。

          • sid:會話領(lǐng)頭進(jìn)程的 PID。

          • group_leader:是一個 task_struct 類型的指針,指向的是進(jìn)程組的組長對應(yīng)的 task_struct 對象。

          虛擬內(nèi)存地址空間

          內(nèi)存管理

          內(nèi)存是由內(nèi)核來管理的。

          內(nèi)存被分為 n 個頁框,然后進(jìn)一步組織為多個區(qū)。而裝入頁框中的內(nèi)容稱為頁。

          當(dāng)內(nèi)核函數(shù)申請內(nèi)存時,內(nèi)核總是立即滿足(因?yàn)閮?nèi)核完全信任它們,所以優(yōu)先級最高)。在分配適當(dāng)內(nèi)存空間后,將其映射到內(nèi)核地址空間中(3-4GB 中的某部分空間),然后將地址映射寫入頁表。

          申請內(nèi)存空間的內(nèi)核函數(shù)有?vmalloc,?kmalloc,?alloc_pages,?__get_free_pages?等。

          內(nèi)核常駐內(nèi)存

          就是說,內(nèi)核地址空間(3-4GB)中的頁面所映射的頁框始終在物理內(nèi)存中存在,不會被換出。即使是?vmalloc?動態(tài)申請的頁面也會一直在物理內(nèi)存中,直至通過相關(guān)內(nèi)核函數(shù)釋放掉。

          其原因在于,一方面內(nèi)核文件不是太大,完全可以一次性裝入物理內(nèi)存;另一方面在于即使是動態(tài)申請內(nèi)存空間,也能立即得到滿足。

          因此,處于內(nèi)核態(tài)的普通進(jìn)程或內(nèi)核線程(后面會提到)不會因?yàn)轫撁鏇]有在內(nèi)存中而產(chǎn)生缺頁異常(不過處于內(nèi)核態(tài)的普通進(jìn)程會因?yàn)轫摫眄?xiàng)沒有同步的原因而產(chǎn)生缺頁異常)。

          為什么要有虛擬地址空間

          普通進(jìn)程在申請內(nèi)存空間時會被內(nèi)核認(rèn)為是不緊要的,優(yōu)先級較低。因而總是延遲處理,在之后的某個時候才會真正為其分配物理內(nèi)存空間。

          比如,普通進(jìn)程中的?malloc?函數(shù)在申請物理內(nèi)存空間時,內(nèi)核不會直接為其分配頁框。

          另一方面,普通進(jìn)程對應(yīng)的可執(zhí)行程序文件較大,不能夠立即裝入內(nèi)存,而是采取運(yùn)行時按需裝入。

          要實(shí)現(xiàn)這種延遲分配策略,就需要引入一種新的地址空間,即 虛擬地址空間。可執(zhí)行文件在裝入時或者進(jìn)程在執(zhí)行?malloc?時,內(nèi)核只會為其分配適當(dāng)大小的虛擬地址空間。

          虛擬地址空間并不單純地指線性地址空間。準(zhǔn)確地說,指的是頁面不能因?yàn)榱⒓囱b入物理內(nèi)存而采取折衷處理后擁有的線性地址空間。因此,雖然普通進(jìn)程的虛擬地址空間為 4GB,但是從內(nèi)核的角度來說,內(nèi)核地址空間(也是線性空間)不能稱為虛擬地址空間,內(nèi)核線程不擁有也不需要虛擬地址空間。因此,虛擬地址空間只針對普通進(jìn)程。

          當(dāng)然,這樣的話就會產(chǎn)生所要訪問的頁面不在物理內(nèi)存中而發(fā)生缺頁異常。

          虛擬地址空間的劃分

          每一個普通進(jìn)程都擁有 4GB 的虛擬地址空間(對于 32 位的 CPU 來說,即 232 B)。

          主要分為兩部分,一部分是用戶空間(0-3GB),一部分是內(nèi)核空間(3-4GB)。每個普通進(jìn)程都有自己的用戶空間,但是內(nèi)核空間被所有普通進(jìn)程所共享。

          如下圖所示:

          虛擬地址空間并不單純地指線性地址空間。準(zhǔn)確地說,指的是頁面不能因?yàn)榱⒓囱b入物理內(nèi)存而采取折衷處理后擁有的線性地址空間。因此,雖然普通進(jìn)程的虛擬地址空間為 4GB,但是從內(nèi)核的角度來說,內(nèi)核地址空間(也是線性空間)不能稱為虛擬地址空間,內(nèi)核線程不擁有也不需要虛擬地址空間。因此,虛擬地址空間只針對普通進(jìn)程。

          另外,

          用戶態(tài)下的普通進(jìn)程只能訪問 0-3GB 的用戶空間;內(nèi)核態(tài)下的普通進(jìn)程既能訪問 0-3GB 的用戶空間,也能訪問 3-4GB 的內(nèi)核空間(內(nèi)核態(tài)下的普通進(jìn)程有時也會需要訪問用戶空間)。

          普通線程的用戶堆棧與寄存器

          對于多線程環(huán)境,雖然所有線程都共享同一片虛擬地址空間,但是每個線程都有自己的用戶棧空間和寄存器,而用戶堆仍然是所有線程共享的。

          棧空間的使用是有明確限制的,棧中相鄰的任意兩條數(shù)據(jù)在地址上都是連續(xù)的。試想,假設(shè)多個普通線程函數(shù)都在執(zhí)行遞歸操作。如果多個線程共有用戶棧空間,由于線程是異步執(zhí)行的,那么某個線程從棧中取出數(shù)據(jù)時,這條數(shù)據(jù)就很有可能是其它線程之前壓入的,這就導(dǎo)致了沖突。所以,每個線程都應(yīng)該有自己的用戶棧空間。

          寄存器也是如此,如果共用寄存器,很可能出現(xiàn)使用混亂的現(xiàn)象。

          而堆空間的使用則并沒有這樣明確的限制,某個線程在申請堆空間時,內(nèi)核只要從堆空間中分配一塊大小合適的空間給線程就行了。所以,多個線程同時執(zhí)行時不會出現(xiàn)棧那樣產(chǎn)生沖突的情況,因而線程組中的所有線程共享用戶堆。

          那么在創(chuàng)建線程時,內(nèi)核是怎樣為每個線程分配棧空間的呢?

          由之前所講解可知,進(jìn)程/線程的創(chuàng)建主要是由?clone?系統(tǒng)調(diào)用完成的。而?clone?系統(tǒng)調(diào)用的參數(shù)中有一個?void *child_stack,它就是用來指向所創(chuàng)建的進(jìn)程/線程的堆棧指針。

          而在該進(jìn)程/線程在用戶態(tài)下是通過調(diào)用?pthread_create?庫函數(shù)而陷入內(nèi)核的。對于?pthread_create?函數(shù),它則會調(diào)用一個名為?pthread_allocate_stack?的函數(shù),專門用來為所創(chuàng)建的線程分配的棧空間(通過 mmap 系統(tǒng)調(diào)用)。然后再將這個棧空間的地址傳遞給?clone?系統(tǒng)調(diào)用。這也是為什么線程組中的每個線程都有自己的棧空間。

          普通進(jìn)程的頁表

          有兩種頁表,一種是內(nèi)核頁表(會在后面說明),另一種是進(jìn)程頁表。

          普通進(jìn)程使用的則是進(jìn)程頁表,而且每個普通進(jìn)程都有自己的進(jìn)程頁表。如果是多線程,則這些線程共享的是主線程的進(jìn)程頁表。

          四級頁表

          現(xiàn)在的 Linux 內(nèi)核中采用四級頁表,分別為:

          • 頁全局目錄 (Page Global Directory, pgd);
          • 頁上級目錄 (Page Upper Directory, pud);
          • 頁中間目錄 (Page Middle Directory, pmd);
          • 頁表 (Page Table, pt)。

          task_struct?中的?mm_struct?對象用于管理該進(jìn)程(或者線程共享的)頁表。準(zhǔn)確地說,mm_struct?中的?pgd?指針指向著該進(jìn)程的頁全局目錄。

          普通進(jìn)程的頁全局目錄

          普通進(jìn)程的頁全局目錄中,第一部分表項(xiàng)映射的線性地址為 0-3GB 部分,剩余部分存放的是主內(nèi)核頁全局目錄(后面會提到)中的所有表項(xiàng)。

          內(nèi)核線程

          內(nèi)核線程是一種只運(yùn)行在內(nèi)核地址空間的線程。所有的內(nèi)核線程共享內(nèi)核地址空間(對于 32 位系統(tǒng)來說,就是 3-4GB 的虛擬地址空間),所以也共享同一份內(nèi)核頁表。這也是為什么叫內(nèi)核線程,而不叫內(nèi)核進(jìn)程的原因。

          由于內(nèi)核線程只運(yùn)行在內(nèi)核地址空間中,只會訪問 3-4GB 的內(nèi)核地址空間,不存在虛擬地址空間,因此每個內(nèi)核線程的?task_struct?對象中的?mm?為 NULL。

          普通線程雖然也是同主線程共享地址空間,但是它的?task_struct?對象中的?mm?不為空,指向的是主線程的?mm_struct?對象。

          普通進(jìn)程與內(nèi)核線程有如下區(qū)別:

          內(nèi)核線程只運(yùn)行在內(nèi)核態(tài),而普通進(jìn)程既可以運(yùn)行在內(nèi)核態(tài),也可以運(yùn)行在用戶態(tài);內(nèi)核線程只使用 3-4GB (假設(shè)為 32 位系統(tǒng)) 的內(nèi)核地址空間(共享的),但普通進(jìn)程由于既可以運(yùn)行在用戶態(tài),又可以運(yùn)行在內(nèi)核態(tài),因此可以使用 4GB 的虛擬地址空間。

          系統(tǒng)在正式啟動內(nèi)核時,會執(zhí)行?start_kernel?函數(shù)。在這個函數(shù)中,會自動創(chuàng)建一個進(jìn)程,名為?init_task。其 PID 為 0,運(yùn)行在內(nèi)核態(tài)中。然后開始執(zhí)行一系列初始化。

          init 內(nèi)核線程

          init_task?在執(zhí)行?rest_init?函數(shù)時,會執(zhí)行?kernel_thread?創(chuàng)建?init?內(nèi)核線程。它的 PID 為 1,用來完成內(nèi)核空間初始化。

          在內(nèi)核空間完成初始化后,會調(diào)用?exceve?執(zhí)行?init?可執(zhí)行程序 (/sbin/init)。之后,init 內(nèi)核線程變成了一個普通的進(jìn)程,運(yùn)行在用戶空間中。

          init 內(nèi)核線程沒有地址空間,且它的?task_struct?對象中的?mm?為 NULL。因此,執(zhí)行?exceve?會使這個?mm?指向一個?mm_struct,而不會影響到?init_task?進(jìn)程的地址空間。也正因?yàn)榇耍?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;box-sizing: border-box;font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;overflow-wrap: break-word;border-radius: 4px;color: rgb(239, 112, 96);background-color: rgba(27, 31, 35, 0.05);word-break: break-all;">init?在轉(zhuǎn)變?yōu)檫M(jìn)程后,其 PID 沒變,仍為 1。

          創(chuàng)建完?init?內(nèi)核線程后,init_task?進(jìn)程演變?yōu)?idle 進(jìn)程(PID 仍為 0)。

          之后,init?進(jìn)程再根據(jù)再啟動其它系統(tǒng)進(jìn)程 (/etc/init.d?目錄下的各個可執(zhí)行文件)。

          kthreadd 內(nèi)核線程

          init_task?進(jìn)程演變?yōu)?idle?進(jìn)程后,idle?進(jìn)程會執(zhí)行?kernel_thread?來創(chuàng)建?kthreadd?內(nèi)核線程(仍然在?rest_init?函數(shù)中)。它的 PID 為 2,用來創(chuàng)建并管理其它內(nèi)核線程(用?kthread_create,?kthread_run,?kthread_stop?等內(nèi)核函數(shù))。

          系統(tǒng)中有很多內(nèi)核守護(hù)進(jìn)程 (線程),可以通過:

          $?ps?-efj

          進(jìn)行查看,其中帶有?[]?號的就屬于內(nèi)核守護(hù)進(jìn)程。它們的祖先都是這個?kthreadd?內(nèi)核線程。

          主內(nèi)核頁全局目錄

          內(nèi)核維持著一組自己使用的頁表,也即主內(nèi)核頁全局目錄。當(dāng)內(nèi)核在初始化完成后,其存放在?swapper_pg_dir?中,而且所有的普通進(jìn)程和內(nèi)核線程就不再使用它了。

          內(nèi)核線程如何訪問頁表

          active_mm

          對于內(nèi)核線程,雖然它的?task_struct?中的?mm?為 NULL,但是它仍然需要訪問內(nèi)核空間,因此需要知道關(guān)于內(nèi)核空間映射到物理內(nèi)存的頁表。然而不再使用?swapper_pg_dir,因此只能另外想法解決。

          由于所有的普通進(jìn)程的頁全局目錄中的后面部分為主內(nèi)核頁全局目錄,因此內(nèi)核線程只需要使用某個普通進(jìn)程的頁全局目錄就可以了。

          在 Linux 中,task_struct?中還有一個很重要的元素為?active_mm,它主要就是用于內(nèi)核線程訪問主內(nèi)核頁全局目錄。

          對于普通進(jìn)程來說,task_struct?中的?mm?和?active_mm?指向的是同一片區(qū)域;然而對內(nèi)核線程來說,task_struct?中的?mm?為 NULL,active_mm 指向的是前一個普通進(jìn)程的?mm_struct?對象。

          mm_users 和 mm_count

          但是這樣還是不行,因?yàn)槿绻驗(yàn)榍耙粋€普通進(jìn)程退出了而導(dǎo)致它的?mm_struct?對象也被釋放了,則內(nèi)核線程就訪問不到了。

          為此,mm_struct?對象維護(hù)了一個計數(shù)器?mm_count,專門用來對引用這個?mm_struct?對象的自身及內(nèi)核線程進(jìn)行計數(shù)。初始時為 1,表示普通進(jìn)程本身引用了它自己的?mm_struct?對象。只有當(dāng)這個引用計數(shù)為 0 時,才會真正釋放這個?mm_struct?對象。

          另外,mm_struct?中還定義了一個?mm_users?計數(shù)器,它主要是用來對共享地址空間的線程計數(shù)。事實(shí)上,就是這個主線程所在線程組中線程的總個數(shù)。初始時為 1。

          注意,兩者在實(shí)質(zhì)上都是針對引用?mm_struct?對象而設(shè)置的計數(shù)器。?不同的是,mm_count?是專門針對自身及內(nèi)核線程或引用?mm_struct?而進(jìn)行計數(shù);而?mm_users?是專門針對該普通線程所在線程組的所有普通線程而進(jìn)行計數(shù)。?另外,只有當(dāng)?mm_count?為 0 時,才會釋放?mm_struct?對象,并不會因?yàn)?mm_users?為 0 就進(jìn)行釋放。

          原文地址:http://abcdxyzk.github.io/blog/2018/01/10/kernel-task-thread/


          瀏覽 64
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  婷婷精品国产亚洲AV老牛 | 亚洲日韩欧美综合热 | 美韩av| 国产999久久久 | www亚洲无 码A片 |