深入理解Linux內核 讀書筆記
一、概論
操作系統基本概念
- 多用戶系統
- 允許多個用戶登錄系統,不同用戶之間的有私有的空間
- 用戶和組
- 每個用於屬於一個組,組的權限和其他人的權限,和擁有者的權限不一樣。對應的是Linux的文件權限系統
- 進程
- 和程序的區別。幾個進程能並發執行同一個程序,一個進程能順序執行幾個程序
- 程序更像是代碼片段,進程是執行代碼的容器
- linux是搶占式操作系統,也就是一個進程只能占用CPU一段時間。非搶占式系統中,進程如果不釋放CPU,可以一直占用
- 內核體系結構
- Linux是單塊內核,同時提供模塊(module)功能
- 模塊是指:例如一個程序,引用了一個系統模塊,這個系統模塊不會是這個進程單獨擁有,當其他程序也需要這個模塊時,內核會把這個模塊鏈接到其他程序。這樣可以節省內存,也就是這個模塊只會在內存中存在一份。模塊就是一組函數,或者一段代碼。
文件系統
- 文件
- 文件是以字節序列組成的信息載體(container)
- 文件目錄是樹結構
- 每個進程都有一個工作目錄,通過pwdx 進程ID 命令可以查看
- 硬鏈接和軟連接
- 鏈接類似window的快捷方式,創建一個文件,指向另一個文件
- ln p1 p2 就是創建一個文件p2,指向p1
- 硬鏈接只能指向文件,不能指向目錄,因為會導致循環指向
- 硬鏈接只能指向同一個文件系統的文件(文件系統是物理划分,例如不同硬盤)
- 軟鏈接沒有硬鏈接這些限制,創建方法是加-s參數
- 文件類型
- 普通文件
- 目錄
- 符號鏈接
- 面向塊的設備文件
- 面向字符的設備文件
- 管道和命名管道(pipe named pipe)
- 套接字(socket)
- 文件描述符與索引節點
- 每個文件都有一個索引節點(inode)的數據結構,用來存儲文件的描述信息,和文件的內容是區分開的。
- inode有(通過ll命令看到的):
- 文件類型
- 硬鏈接個數
- 文件長度
- 文件擁有者的uid
- 用戶組的id
- 修改時間等
- 訪問權限
- inode有(通過ll命令看到的):
- 每個文件都有一個索引節點(inode)的數據結構,用來存儲文件的描述信息,和文件的內容是區分開的。
- 訪問權限和文件模式
- 擁有者,組,其他人,各有讀寫執行3種權限
- 文件操作
- 打開文件
- 讀
- 寫
- 移動光標
- 關閉
Unix內核概述
-
進程/內核模式
- 進程有用戶態和內核態
- 用戶態不能訪問內核的數據結構和內核程序
- 兩種態會經常切換,例如在時刻A,進程在用戶態,在時刻B,進程在內核態
- 從用戶態切換到內核態的情況:
- 調用系統調用
- 執行進程的CPU發送異常
- 外圍設備向CPU發出中斷
- 內核線程被執行
-
進程
- 每個進程有一個進程ID,pid
- 內核切換執行的進程時,會保存舊進程的信息,包括:
- 程序計數器和棧指針寄存器
- 通用寄存器
- 浮點寄存器
- CPU狀態
- 內存管理寄存器
-
可重入內核
- unix內核都是可重入的
- 可重入是指,可以被重復進入,也就是可以同時有多個進程處於內核態
-
進程地址空間
- 每個進程有自己私有的地址空間
-
同步和臨界區
- 類似鎖
- linux是搶占式內核,所以需要同步
- 信號量
- 每個資源都有一個信號量,類似int類型,初始值是1
- 每個進程訪問資源,調用down方法,信號量減1,如果減1后,信號量小於0,進程被加入到訪問隊列中。如果大於等於0,進程可以訪問資源
- 每個進程訪問完資源,調用up方法,信號量加1,如果信號量大於等於0,激活訪問隊列的第一個進程
- 進程鎖,線程鎖的機制,應該都是這樣的
- 這里要保證down和up的操作都是原子性的,不能並發
- 要防止死鎖
- 鎖里面的區域就是臨界區,也就是acquire和release之間的代碼
-
信號和進程間通信
- 信號和信號量是不一樣的
- linux有20多種不同的信號,例如kill -9 中的 9就是一種信號
- 進程收到信號后,可以
- 忽略
- 異步執行指定程序(新開一個線程?),這種需要事先定義信號處理函數。
- 內核收到信號后,可以
- 終止進程(例如kill - 9)
- 忽略信號
- 掛起進程
- 恢復進程
- 進程間通信(IPC)
- 信號
- 消息(msgget(),msgsnd())兩個系統調用,發信息和收信息,Python里面的進程間Queue應該就是用這個實現的
- 共享內存(shmget shmdt)兩個系統調用
-
進程管理
- fork來啟動一個子進程,一般在啟動的時候復制父進程的數據和代碼,但是這樣效率較低,所以會使用寫時復制,也就是一開始父子進程共享內存,當其中一個進程需要修改數據時,才執行復制操作
- exec用於啟動子進程
- exit用於結束子進程
- wait4用於父進程等待子進程結束
-
內存管理
- 虛擬內存,在物理內存(MMU)和程序之間的抽象,相當於訪問內存的代理。
- 內核內存分配器,KMA,用於管理內存
- 高速緩存 由於內存比硬盤快很多,所以從硬盤讀取得數據會緩存在內存,使下次可以快速訪問
二、內存尋址
- 內存地址
- 內存地址有3種
- 邏輯地址,由一個段(segment)和偏移量(offset)組成,用來指明一個操作數,或者一條指令的地址
- 線性地址。是一個32位無符號整數(在32位系統中是這樣),從0x00000000到0xffffffff。內存相當於一個超大的列表,下標(地址)是一個32位整數,值就是內存的內容,值得大小是1字節
- 物理地址。內存芯片級的地址
- 邏輯地址,經過分段單元,轉換為線性地址,線性地址,經過分頁單元,轉換為物理地址
- 內存地址有3種
- 分段單元(用於把邏輯地址,轉換為線性地址)
- 概念
- 段選擇符,也叫段標識符,也就是上面說的段,程序傳入給分段單元。有字段:
- index,表示段描述符在GDT或者LDT中下標
- TI,表示段描述符在GDT中還是LDT中
- RPL,特權級
- 段描述符,8字節,存放在GDT或者LDT中,有字段
- Base表示段在內存中首字節的線性地址
- S,0表示系統段,1表示普通段
- DPL,特權級,0表示只有內核態才能訪問,3表示內核態和用戶態都能訪問。(cs寄存器中,有一個兩位的字段,指明CPU的當前特權級,0表示內核級,3表示用戶級。所以通過這個機制,可以限制用戶態的進程不能訪問內核態的內存數據)
- D或者B,表示這是代碼段,還是數據段
- GDT,是全局段列表,item是段描述符
- LDT,是局部段列表,item是段描述符
- 段選擇符,也叫段標識符,也就是上面說的段,程序傳入給分段單元。有字段:
- 轉換流程
- 傳入邏輯地址給分段單元,邏輯地址包含段選擇符和偏移量
- 查看段選擇符的TI字段,決定是從GDT中還是LDT中獲取段描述符,假如是GDT
- 查看段選擇符的index字段,假如是2,從gdtr寄存器中獲取GDT列表的首字節地址,假如是0x00002000,計算段描述符的位置=0x00002000+2*8,=0x00002016 (每個段描述符8字節),所以段描述符在內存的0x00002016-0x00002024位置
- 查看段描述符的Base字段,假如是0x00003000,加上偏移量,假如是100,得到線性地址是0x00003100
- 概念
三、進程
進程,輕量級進程(LWP)和線程
- 進程是程序執行時的一個實例
- 線程 是進程里面的一個執行流,線程的切換時在用戶態進行的。但是這樣就不能做到並發了
- 輕量級進程,類似線程,但是切換時在內核態進行
所以Linux的做法是(TODO 這一塊還不是很明白)
- 把線程和輕量級進程關聯起來,所以線程和輕量級進程是等價的
- 對內核來說,進程和LWP是一樣的,使用同樣的調度方法
- LWP之間可以共享部分數據
進程描述符
-
進程描述符是一個數據結構(c的struct,類似Python的字典)
-
進程描述符有字段:
- state 狀態
- 可運行狀態(TASK_RUNNING),要么在運行,要么准備運行
- 可中斷的等待狀態(TASK_INTERRUPTIBLE)進程被掛起(睡眠),表示它在等待一個事件的發生,例如等待某個系統資源。當這個系統資源可用,內核會產生一個硬件中斷,來喚醒進程
- 不可中斷的等待狀態(TASK_UNINTERRUPTIBLE),和可中斷的等待狀態類似,這個狀態較少用到
- 暫停狀態(TASK_TOPPED)進程被暫停執行,當進程收到信號SIGSTOP,SIGSTP,SIGTTIN SIGTTOU信號后,會進入暫停狀態
- 跟蹤狀態(TASK_TRACED)當進程被另一個進程跟蹤,例如執行ptrace命令,
- 僵死狀態(EXIT_ZOMBIE)進程的執行被終止,但是父進程還沒有發布wait4或者waitpid命令來獲取進程信息。這時內核不會自動丟棄進程的信息,因為父進程可能還需要這些信息
10.僵死撤銷狀態
- thread_info 進程的基本信息
- fs_struct 當前目錄
- signal_struct 收到的信號
- pid 進程的ID。順序遞增,最大是32767,超過后,從1開始獲取閑置的PID值。進程里面的線程,也擁有自己的pid,同時每個線程有一個tgid(thread group id),表示線程組ID,這個ID等於進程中第一個線程的pid。
- 一個進程里面至少有一個線程
- state 狀態
進程鏈表
- 一個進程描述符表示一個進程
- Linux把所有進程放在一個雙向鏈表里面,每個item是一個進程描述符
- TASK_RUNNING狀態的進程鏈表
- 由於CPU在進行進程切換時,需要快速知道下一個執行的進程是什么,所以Linux把所有可以執行的進程都放在一個單獨的鏈表。
- 由於不同進程有不同的優先級,所以linux的做法是
- 由於有140種優先級(優先級用prio表示,0-139),所以用140個鏈表來保存
- 用一個140長度的位圖(bitmap)來表示140個鏈接中,哪些有數據
- 所以獲取下一個優先級最高的進程的做法是:
- 查看位圖,看第一個=1的位的下標是多少,例如是15
- 訪問第15個鏈表,queue[15],獲取第一個元素
進程間的關系
進程描述符里面有特定的字段,記錄每個進程的父進程,兄弟進程和子進程
- real_parent 父進程的描述符指針,如果父進程不存在,指向進程1
- parent 當前父進程,通常和real_parent一致,指引當進程被追蹤時不一致
- children 鏈表,記錄所有子進程
- sibling 有prev和next兩個元素,表示上一個兄弟進程,和下一個兄弟進程
pidhash
有時候內核需要根據pid來獲取進程描述符
所以內核會保存一個pidhash數據結構,是個hash表(c里面的hash表的實現和redis的hash表實現類似),key是pid,value是進程描述符
進程切換
進程切換,任務切換,上下文切換是一樣的
每個進程都有自己的地址空間(在內存),但是進程之間是共享寄存器的,所以進程的切換需要(硬件上下文是寄存器的數據):
- 保存prev進程的硬件上下文
- 用next硬件上下文替換prev
上面的操作使用一個switch_to宏來實現,傳入參數prev,next,prev。傳入兩次prev是怕切換上下文后,把第一個prev丟了。
創建進程
Linux進程的特性:
- 寫時復制
- 輕量級進程允許父子進程共享很多數據結構
創建進程的系統調用:
-
close()
- fn 子進程創建后執行的函數,函數結束,子進程終止
- arg 傳給函數的數據
- 其他還有很多參數
-
fork close函數的封裝
-
vfork close函數的封裝
內核進程
內核進程是一直運行在內核態的
進程0
進程0是linux啟動后的第一個進程,由它創建進程1
進程1
進程1也叫init進程,進程1會一直運行知道linux關閉
撤銷進程
進程執行完指定的代碼后,就會終止,這時必須通知內核回收進程的資源。
一般是exit系統調用,c編譯程序會自己動把exit函數插入到main函數最后
內核可以強迫整個線程組死掉(例如收到kill -9)
進程刪除
當進程終止后,進程會進入僵死狀態,直到父進程調用wait4來獲取進程的狀態數據,然后進程就會被刪除。
如果父進程已經不存在,進程會交給init進程托管,init進程會定期執行wait4命令來查看進程的狀態,如果進程已經終止,就會刪除這個進程