深入理解linux內核


3.進程管理

3.1 進程:處於執行期程序和相關資源的總稱

線程:私有:進程棧,程序計數器,進程寄存器
進程資源:打開的文件,掛起的信號,內核內部數據,處理器狀態,內存地址空間,數據段
調用fork()來創建子進程
exee()創建地址空間
exit()退出進程
父進程調用wait4()查看子進程是否終結
陷入等待時,進程退出執行后被設置為僵死,直到父進程調用wait(),waitpid()

進程描述符及任務結構

進程描述符task_struct結構體里的成員變量被用來描述進程信息,slab分配器用來分配task_struct,thread_info結構體內部有一個指向進程描述符的指針,它被存放在進程內核棧的底部,PID號唯一的標識了某個進程,PID最大值實際上是內核中允許同時存在進程的總數,最大值越小,轉一圈就越快,且數值大的進程遲運行。
current宏負責查找當前進程的描述符

進程狀態

1.TASK_RUNNING 運行:進程是可執行的
2.TASK_INTERRUOTIBLE可中斷的:等待狀態
3.TASK_UNINTERRUOTIBLE不可中斷:等待狀態,並不會被信號喚醒
4.TASK_TRACED被其他進程跟蹤的進程
5.TASK_STOPPED停止執行

3.2.6 進程家族樹

所有進程都是一個PID為1的init進程的后代,這個進程在內核啟動最后被調用,用於初始化並啟動相關進程,此進程的描述符init_task是靜態分配的
遍歷可執行雙向鏈表可以遍歷系統中所有進程

寫時拷貝

在創建子進程時,如果子進程不需要寫入,就不必拷貝一份父進程資源
fork():1.創建內核棧,描述符和描述符指針
2.檢查子進程數量是否超過限制
3.把拷貝來的描述符里的某些值初始化
4.設置為不可中斷態
5.設置進程PID
6.根據傳遞給clone()的參數(資源權限描述),進行各種資源分配
vfork():不拷貝父進程頁表項,只能讀

3.4 進程在linux中的實現

linux內核把線程當作進程對待,線程沒有什么私有資源的特別描述,只有共有資源的描述
clone()可加參數來設定創建子進程的需共享資源

內核線程

在內核后台運行,跟用戶空間沒有交集,也沒有被分配進程空間

3.5 進程終結

終結時釋放所有資源,並把其告訴父進程:記賬信息,文件描述符,文件系統的引用計數,切換到新進程。。。
如果子進程正在執行時父進程退出了,它就會僵死在那里永遠不會被釋放,變成孤兒;為了防止,要給它和它的兄弟們找養父進程,如果它所在的線程組沒有其他進程,就把它交給init進程;找到它的兄弟,只需遍歷子進程鏈表;
init會調用wait()清除所有的僵死進程;

4.進程調度

4.1多任務

多任務操作系統:能並發執行多個進程
搶占式:可以強制性掛起一個進程
非搶占式:不可掛起

I/O消耗性和處理器消耗性的進程

I/O消耗進程多數時間用來等待文件輸入,運行時間很少,處理器消耗反之

4.2 linux 調度算法

linux調度器Schedule()選擇最優的擁有進程的調度類,調度類schedule()再選擇最優的進程運行,進程運行的時間取決於它被分配到的時間片的多少
進程優先級:nice值來設置優先級(-20~19)默認是0,值越大優先級越低
時間片:進程被強占前可運行時間
CFS(公平調度):大多數普通進程都用CFS策略調度,CFS沒有時間片的概念,而是根據目標延遲值/進程的數量計算出處理器分配的份額,nice會成為處理器分配的權重,每個進程分配到的份額都相等,但處理器使用比也是可變的,當進程使用cpu的時間遠小於被分配的份額時,另一些進程會搶占此進程,重新計算這個使用比;
這里有一個問題就是當進程無限多時,處理器使用比會趨於0,從而內核忙於調度,因此設置一個時間片底線(最小粒度)為1ms

linux調度的實現

4.5.1時間記賬

每一個時鍾節拍會使得時間片減少一個節拍周期,當節拍周期減到0時,它會被其他進程搶占
CFS里使用調度器實體結構來記賬,它的實體結構被放在進程描述符里
viruntine變量:虛擬運行時間,記錄一個程序運行了多長時間,相關函數計算

4.5.2進程選擇

挑選viruntine最小的進程作為最大優先級,CFS使用紅黑樹管理進程,最左葉子節點是最大優先級的進程,進程堵塞或終止時從紅黑樹刪除,陷入等待時被標記成休眠狀態,加入等待隊列並設置成不可執行狀態,再從紅黑樹中刪除,事件發生時再改為可運行狀態,再把它加入運行隊列,再從等待隊列刪掉
need_reached描述符表示是否需要重新調度

4.6.1 用戶搶占

系統調用/中斷處理程序要返回用戶空間的時,會檢查need_reached,如果設置了,就會重新調度

4.6.2 內核搶占

1.中斷處理程序正在執行,且返回內核空間之前
2.內核代碼再一次擁有可搶占性
3.內核任務顯示(阻塞也會導致)調用schedule()

實時調度策略

就是比某進程優先級高的進程才可以搶占它
SCHED_FIFO沒有時間片,而SCHED_RR帶有時間片
軟實時:進程時間片用完前可能被搶占
硬實時:不會被搶占

4.8與調度相關的系統調用

4.8.2處理器綁定

可以通過設置位掩碼(保存在進程task_struct標志中)指定某進程只能在哪些cpu上運行,每一位標志着一個處理器,子進程繼承了父進程的位掩碼

4.8.3 放棄處理器時間

sched_yield()系統調用可把某進程調為最小優先級,並放入過期隊列,確保其一段時間不會被執行

5.系統調用

graph LR A[應用程序]--調用-->B[庫函數] B--調用-->C[系統調用] C--指揮-->D[操作系統]

POSIX是一套系統調用API規范,應用於LINUX,UNIX,macOS

5.3系統調用

系統調用會通過一個long類型返回值來表示成功或者錯誤,0為成功,負值是錯誤,錯誤碼被保存在errno全局變量中。系統調用限定詞為asmlinkage,為了兼容32位和64位,用戶空間返回值為int,內核空間為long,另外系統調用和內核命名規則也不一樣

5.3.1 系統調用號

每個系統調用都有一個系統調用號,此號不能輕易改變,刪除一個號之后,用‘未實現系統調用函數’:sys_ni_syscall()填補空缺,該函數返回-ENOSYS,這些號被存在系統調用表里

系統調用處理程序

用戶空間函數不可直接訪問內核空間,但可以間接通知內核,使內核陷入內核態,使用軟中斷:system_call()實現內核陷入。
系統調用把系統調用號參數分別通過eax和其他五個寄存器傳給內核

graph TB A[用戶空間函數]-->B[系統調用] B--觸發軟中斷-->C[system_call函數檢查系統調用號是否小於NR_syscalls值] C-->D[函數陷入內核空間檢查參數權限合法性] D-->E[系統調用號和參數通過寄存器傳入內核]

參數驗證

系統調用必須檢查參數的合法性,不能傳入訪問不到的地址,比如:其他進程空間的,內核空間的,內存的只讀只寫。。。訪問限制
權限的合法性通過suser()檢查是否為超級用戶,capable()檢查對資源操作的權能

系統調用上下文

綁定系統調用

在entry.s文件里的調用表里添加一個表項,在<asm/unisted>設置系統調用號,系統調用要被編譯進內核映像kernel/

用戶訪問系統調用

用戶可通過一組linux宏訪問系統調用,它會設置好寄存器並調用陷入指令,
宏的形式為:

_syscall3(long, open, const char*,filename,int,flags,int,mode)
第一二個參數對應着返回類型和系統調用名字

7.中斷和中斷處理

7.1 中斷

中斷是一個由硬件產生的電信號,先通過中斷線傳給中斷處理器,中斷控制器收到此硬件的中斷信號之后會通過地址總線存入一個該設備的編號,表示這次中斷需要關注的設備,在由中斷處理器傳信號給cpu,然后cpu從地址總線取出設備編號,通過編號找到中斷向量所包含的中斷服務的入口地址,壓入 PC 寄存器,然后內核陷入一個中斷處理程序ISR)來處理這個中斷
不同的設備對應着不同的中斷,不同的中斷由叫中斷請求線(IRQ)的值來唯一標識
異常和中斷的區別:當執行代碼時出現特殊情況:如錯誤指令,時,cpu會產生一個異常,通知內核處理,而中斷來自處理器外部,而異常是執行某指令的結果

7.2 中斷處理程序

程序通過特定代碼去響應一個中斷;
設備驅動程序:一個設備的中斷處理程序,是對設備管理的內核代碼;
linux中斷處理程序就是C代碼,運行於中斷上下文中(原子上下文),中斷處理程序有時間限制,還要通知硬件是否收到信號

7.3上半部和下半部

上半部指很迫切需要執行的代碼,需要立即執行且有時間限制
下半部指允許稍后完成執行的代碼
網絡硬件的例子:網卡傳中斷信號給cpu,中斷上半部需要立即復制網絡數據包到系統內存,然后把控制權交給中斷之前的代碼,然后下半部再挑時機對數據包處理后再交給協議棧或應用程序

7.4 注冊中斷處理程序

request_irq()函數注冊一個中斷處理程序:
1.irq參數表示要分配的中斷號
2.hander是一個指向處理函數的指針
3.flags參數可以為0或以下標志的位掩碼:
·IRQF_DISABLED此參數表示不能同時運行兩個同cpu的中斷處理程序
·IRQF_SAMPLE_RANDOM:來自設備中斷的間隔時間會作為熵值填充到內核熵池
·IRQF_SHARED:在多個處理程序之間共享中斷線
4.name與中斷設備關聯的文本表示
5.dev參數用於區分共享中短線上的諸多處理程序
這個函數可能會睡眠,不能在不允許阻塞的函數中調用它
free_irq()刪除指定的中斷處理程序,如它所在的中斷線上只有一個程序,則禁用此中斷線
內核接受到中斷時,檢查此中斷線上的每個程序

7.6 中斷上下文

進程上下文:進程已執行過的字段/數據(存放在堆棧中),進程執行活動全過程的靜態描述
中斷上下文盡量節約時間和內存棧

7.9 中斷控制

可禁止/激活整個處理器上所有的中斷函數,或只禁止某一條中斷線

8.下半部和推后執行的工作

下半部機制:內核中所有將工作推遲的機制都叫下半部機制
1.中斷處理程序以異步方式執行,並可能打斷其他代碼
2.不能消耗太長時間
3.中斷處理不在進程上下文中運行,所以不能阻塞
所以一般將時間敏感的操作硬件的不可被中斷的程序放在(中斷處理程序)上半部中

下半部的重要性

為了縮短中斷處理程序的響應時間及其他程序被屏蔽的時間,如果系統不太繁忙,一般上半部返回時就立即執行下半部

8.1.2 下半部的環境

BH:它提供了一個靜態創建,由32個bottem harves組成的鏈表,上半部提供一個數來確定執行哪個,此機制在所有cpu上只能運行一個中斷
任務隊列:內核定義了一組隊列來挨個執行函數組成的鏈表,此機制用來代替BH
軟中斷和tacklst:軟中斷是靜態定義的32個下半部接口,可以在多個cpu上同時運行相同/不同的中斷;不同類型的tacklst可在不同處理器上同時運行,相同類型就不可以

8.2 軟中斷

軟中斷在編譯期間是靜態分配https://www.cnblogs.com/zhxshseu/p/5293979.html 的
軟中斷不會搶占軟中斷,它只能被中斷處理程序搶占,最多有32個軟中斷
觸發軟中斷指軟中斷被標記后才能執行
在每個處理器上單獨的地址空間運行中斷,避免資源加鎖
https://www.sohu.com/a/216557079_236714

8.3 tacklst實現

tacklst是用tasklst結構體實現,結構體被放在一個鏈表中,每個結構體都是一個tasklst,tacklst是動態分配,tacklst是用軟中斷實現的
4.ksoftirqd:當內核出現大量軟中斷時,cpu將被中斷處理程序占滿,ksoftirqd是用來處理這些大量軟中斷的。
如果內核忙於處理中斷程序,用戶就會陷入飢餓,反之內核陷入飢餓,ksoftirqd做一個折中:每個cpu上會有一個內核線程,在最低的優先級上運行,通過死循環保證只要有空閑的處理器就會處理軟中斷
內核飢餓:把自行觸發的軟中斷放到下一次中斷返回之后去處理,確保及時響應用戶
用戶飢餓:軟中斷可能會在執行時重新觸發自己使自己再次得到執行,立即處理會造成cpu負載過重

8.4 工作隊列

可以把工作推后,交給一個內核線程執行,這個下半部總會在進程上下文中執行,所以可以睡眠,所以需要睡眠的任務,就給工作隊列,不需要睡眠的就給軟中斷/tacklst
工作隊列子系統可以創建一個內核線程接口,用來執行排隊的線程,這些線程叫做工作者線程,一般由缺省的工作者線程來處理,處理器密集型任務會有自己的工作者線程,確保性能

工作隊列的結構

所有工作者線程在一個workqueue_struct結構體中,結構體內有一個數組對應着很多處理器和很多工作者線程,每個工作者線程用cpu_workqueue_struct()表示,同時對應一個鏈表,每個鏈表節點有一個work_struct死循環實現,鏈表都處理完時就休眠

8.5 下半部機制的選擇

1.軟中斷:需要采取一些步驟保證共享數據安全,如果完全使用單處理器變量,那么軟中斷就很好
2.tasklet:本質上就是用軟中斷實現的,實現起來更簡單,不能並發運行,保證了資源安全,性能比軟中斷差一點
3.工作隊列:如果需要睡眠,就用工作隊列,此機制實現簡單,開銷也最大,因為涉及到上下文切換和內核線程

8.7 禁止下半部

為了保護內存資源可能要先得到一個鎖然后禁止下半部執行

9 內核同步介紹

臨界區和競爭條件

臨界區就是共享數據的代碼段,訪問臨界區時代碼不可被打斷。

造成並發執行的原因

由於內核的調度程序,用戶進程會在運行時被其他進程搶占,造成偽並發;
內核中造成並發執行的原因:

  • 中斷:中斷可在任何時刻打斷當前代碼
  • 軟中斷和tackest
  • 內核搶占
  • 睡眠和與用戶空間的同步:內核進程可能會睡眠導致調度一個新的用戶程序運行
  • 對稱多處理:進程分別運行在有多個處理器的系統會造成真並發。
    SMP安全代碼:對稱多處理機器中能避免並發
    搶占安全代碼:內核搶占時能避免並發

9.4 征用和擴展性

鎖的征用:鎖正在被占用時,其他程序試圖獲得它
擴展性:系統可擴展性的量度,只要是可被計數的設備都有擴展性,但性能和個數不一定成正比。
提高擴展性可以在大型SMP系統,處理能力強大的機器上獲得良好效果,比如用一個鎖控制一個鏈表,在很多進程訪問它的時候就會遇到擴展性瓶頸,如果用很多鎖控制此鏈表的每個節點就會好很多;但是在訪問進程少的情況下,鎖的粒度太細就會造成性能損耗。

10 內核同步方法

原子整數操作

SPARC體系結構是啥??
原子操作就是不可被分割的操作,原子函數只接受一個特殊的數據類型,叫做atomic_c,這種類型可以屏蔽不同體系結構上數據的差異,也可以確保編譯器不對相應的值進行訪問優化。
內聯函數是啥?

10.2 自旋鎖

鎖的出現是因為原子操作並不能滿足在多個復雜函數共享數據的情況下安全。
linux內核主要是自旋鎖,它最多只能被一個可執行線程持有,並且征用時,線程不能睡眠,必須進行忙等待。由於從睡眠中喚醒線程會導致開銷,所以往往是短期占有鎖/禁止睡眠時才需要自旋鎖
要在獲取鎖之前禁止本地中斷,因為中斷處理程序會試圖打斷內核進程而陷入忙等待,同時鎖持有者又掛起,就陷入了死鎖
鎖的是數據而不是代碼

自旋鎖和下半部

同類的tasklet不可能同時運行,同一個處理器上不可能相互搶占;而軟中斷會在不同處理器上運行,因此需要自旋鎖,同一個處理器上軟中斷不可能被另外的軟中斷搶占,不需要自旋鎖。
大量讀者會使寫者處於飢餓狀態

10.3 讀—寫自旋鎖

又稱(共享/排斥鎖;並發/排斥鎖),鎖可被多個讀任務持有,一個寫任務持有,讀任務可以並發

10.4 信號量

信號量:征用信號量的進程會被加入一個睡眠隊列。
它適用於鎖被長時間使用的情況,短時間持有的鎖,中斷上下文中都不可用。

計數信號量和二值信號量

計數信號量:可以記錄持有者數量,允許任意數量的鎖持有者
二值信號量:鎖持有者只能是0,1
讀—寫信號量downgrade_write()方法動態把寫鎖轉換為讀鎖

10.6 互斥體

相比信號量,優先用mutex
mutex指可睡眠的強制互斥鎖,類似於信號量,只是接口更簡單。
只能在同一上下文中對它上鎖和解鎖,不可遞歸的上鎖和解鎖,不可被拷貝,手動或重復初始化。

10.9 順序鎖

seq鎖 :主要依靠一個序列計數器,寫入操作會導致值被增加,讀前和讀后查看此鎖的數據有沒有發生變化就能知道讀之前和之后有沒有被寫入過。
當數據存在很多讀者,很少寫者,希望寫者優先於讀者,數據很簡單時使用。

11定時器和時間管理

內核中的時間概念

系統定時器被用來計算流逝的時間,系統定時器以某種頻率自行觸發(擊中/射中)時鍾中斷,該頻率可以編程修改,叫做節拍率
節拍是兩次中斷間隔的時間,為1/節拍率秒,牆上時間(實際時間)
時間中斷周期中執行的操作

  • 更新系統運行時間
  • 在SMP系統上,均衡調度處理程序各處理器上的運行隊列負載
  • 檢查當前進程是否用盡了自己的時間片
  • 更新資源消耗和處理器時間的統計值

理想的HZ值

不同體系結構擁有默認節拍率,提高節拍率時:

  • 時鍾中斷更加頻繁
  • 提高時鍾中斷解析度
  • 時間驅動事件的精度提高
  • 提高系統負擔,頻繁陷入時鍾中斷
    無節拍的OS:如果一段時間內無事可做,就延長時鍾中斷頻率,這樣可以減少系統損失的能耗

jiffies

這個全局變量用來記錄系統啟動以來產生的節拍的總數,unsigned long類型,64位的jiffies變量永遠不可能溢出,因此一般只取變量的后32位,jiffies回繞是指變量溢出后會自動回到0值

實時時鍾

實時時鍾(RTC)在系統關閉后可以通過主板上的微型電池提供的電力確保系統的計時,內核讀取RTC來初始化牆上時間,存放在xtime變量中。

系統定時器

可編程中斷時鍾(PIT)在內核定時機制中很重要,采用周期性觸發中斷機制,有些體系結構利用衰減測量器或電子晶振分頻實現

11.5 時鍾中斷處理程序


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM