進程#
- new: 進程剛被創建但還沒有完成初始化
- ready: 進程可以被調度執行
- running: 進程正在 CPU 上執行
- blocked: 被阻塞,需要等待外部事件完成
- terminated: 執行完成,不會再被調度
進程地址空間 |
---|
內核代碼及數據 |
內核棧 |
用戶棧 |
⬇️ |
共享庫,只讀 (如 libc) |
⬆️ |
用戶堆 |
數據段 (全局變量) |
代碼段 |
Linux 使用 fork
從當前進程中創建一個新的子進程:對於父進程 fork 返回子進程的 PID; 對於子進程 fork 返回 0 執行 fork 時子進程會複製父進程 PCB(Process Control Block)
中的數據,比如打開的文件描述符表,但是兩進程 FD 表中指向的文件數據結構是相同的 (相當於只複製了指針) 導致兩進程共享一份打開文件的指針,在讀取文件時受到共享偏移量的影響,讀取的數據並不一致。
通過 fork 可以得到一個和父進程幾乎相同的子進程在 Linux 下還可以通過 exec
系列接口在當前進程中執行新的程序。使用 execve
後操作系統會執行以下操作:
- 根據指定的可執行文件路徑將數據段和代碼段載入地址空間
- 重新初始化堆和棧並進行
地址空間隨機化
改變堆棧起始地址 - 將 PC 寄存器設置到可執行文件代碼段的入口
Linux 的第一個進程是 init
進程,所有進程都是通過 fork 創建的,每個進程會記錄自己的父子進程,由此所有進程形成了一棵進程樹。父進程可以通過 wait
監控子進程的退出並檢查其狀態,比如是否異常退出。如果父進程沒有執行或者還沒來得及執行 wait 調用子進程就已經退出,此時子進程的資源並不會被完全回收 Linux 下稱這種進程為 zombie
進程。
Linux 內核定義了 進程組 process group
的概念,即由多個進程組成的集合,應用進程可以用過 killpg
向一個進程組發送信號,此信號會發送到組內的所有進程。多個進程組的集合構成 會話 session
會話將進程組分為前台進程組和後台進程組,前台進程組一般用於接受 控制終端 controlling terminal
進程的輸入 (比如標準輸入或者信號等)。會話和進程組主要用於 Shell 環境的進程管理。
线程#
线程是进程内可以独立执行的单元,同一进程内的所有线程共享同一份地址空间,但是拥有不同的运行上下文:比如寄存器状态和栈。多线程应用的地址空间分布如下:
有多个线程的进程地址空间 |
---|
内核代码及数据 |
内核栈 1 |
内核栈 2 |
内核栈 2 |
... |
用户栈 1 |
用户栈 2 |
用户栈 3 |
... |
共享库,只读 (如 libc) |
⬆️ |
用户堆 |
数据段 (全局变量) |
代码段 |
操作系统内线程分为用户态线程和内核线程,用户态线程如果要进行系统调用需要进入到内核线程中去执行,所以需要为每个用户态线程分配对应的内核线程。分配方式有多种:
- 多对一:多个用户线程对应一个内核线程,缺点是同时只能有一个用户线程进入内核
- 一对一:每个用户线程对应一个内核线程,缺点是内核线程的数量会随着用户进程的增多而增多
- 多对多:将 N 个用户线程对应 M 个内核线程 (N> M) 需要内核对映射关系做管理和调度
每个线程有一个 Thread Control Block TCB
数据结构用于保存线程的上下文信息。
用户态线程可以创建特殊的 Thread Local Storage TLS
线程局部存储变量,多个线程可以使用同一个 TLS 变量,但是每个线程读写的都只是本线程内的数据拷贝。如下伪代码:
__thread int count; // 声明一个 TLS 变量
thread1.set(&count, 1); // 线程1写 count
thread2.set(&count, 2); // 线程2写 count
thread1.get(&count) == 1; // 线程1读 count 的结果是 1
thread1.get(&count) == 2; // 线程2读 count 的结果是 2
在 x86-64
架构中,当一个线程被调度时 libpthread 会将该线程的 TLS 起始地址写入到 FS
段寄存器内,线程访问 TLS 变量时会使用 FS 寄存器中的地址加上变量偏移量进行访问,即实际上多线程读写各自的 TLS 变量时使用的地址是不同的。
在 Linux 下线程的创建,退出等操作都是通过调用 libpthread
接口实现:
- pthread_create: 在当前进程内创建一个线程,运行指定的函数,底层通过 clone 调用实现
- pthread_exit: 结束当前线程,函数执行结束时会隐式调用,可以通过主动调用设置返回值
- pthread_yield: 主动暂停当前线程,让出资源,等待下一次调度,但是线程仍处于 ready 状态
- pthread_join: 等待指定的线程结束并获取其返回值
- pthread_cond_wait: 等待一个指定的条件变量,用于多线程同步
調度概念#
調度的指標#
- 吞吐量:單位時間內處理的任務數量
- 周轉時間:任務從開始執行到結束執行的耗時
- 響應時間:交互式任務的請求響應耗時
- 實時性:實時任務的完成度 (某些任務需要在指定 deadline 前完成)
- 能耗
- 資源利用率
- 公平性
- 調度器本身的開銷
長期,中期與短期調度#
長期調度#
長期調度主要控制系統中可以被調度的任務總量,比如短時間內大量的進程被創建,長期調度不會立即將所有進程都轉移到 ready 狀態 (對於實時性要求較高的交互式任務例外), 否則會造成任務數量劇增調度開銷增大。另外長期調度會根據當前系統的 CPU 和 I/O 使用情況適當平衡可以被調度的進程,避免同時有大量的 I/O 任務或 CPU 任務造成劇烈的資源競爭和利用率不足。總的來說長期調度通過決定是否將一個任務納入調度來控制總量,降低短期調度的壓力。
短期調度#
短期調度執行具體的調度決策,將任務的狀態在 blocked/ready/running
之間轉換。比如:
- 處於 running 狀態的進程如果執行時間超過預估的時間片會被調度搶占,轉換為 ready 狀態等待下一次調度
- 任務執行或等待 I/O 時會被轉換為 blocked 狀態等到 I/O 事件就緒時轉換為 ready 狀態
- 軟 / 硬件 (比如時鐘) 中斷,系統調用等會中斷任務的執行
中期調度#
中期調度是換頁機制的一部分,主要是控制系統中可運行和正在運行任務的內存使用,避免使用量超出內存資源總量。實際運行過程中,如果系統中的任務已經占用了大量的內存,中期調度會選取某些任務 (比如頻繁觸發缺頁異常的任務) 將其掛起 suspend
而後換頁機制會傾向於將被掛起的進程內存換出到磁碟以緩解系統的內存壓力。被掛起的任務也無法被短期調度使用,進一步降低短期調度的壓力。
總體調度示意圖:
調度機制#
優先級調度#
時間片輪轉:每個任務執行指定時間片的時間,然後切換到下一個任務。這種調度策略會導致平均的周轉時間變長 (比如系統中一共就兩個任務,時間片輪轉會耗費大量的時間在兩個任務的切換上)。而且需要平衡時間片的大小:如果太小,調度和狀態轉換的開銷會很大;如果太大會造成某些任務延遲過高。
考慮到交互式任務的實時性需求,可以為每個進程指定一個優先級,高優先級的任務 (比如交互式任務或者實時任務) 會先被調度器執行。
實際運行時一般會有多個具有高優先級的任務,因此可以使用 多級隊列
的方式將所有任務分配到不同的優先級,依舊是高優先級任務先執行,但是同一優先級內的多個任務會放在一個隊列中通過時間片輪轉或者其他策略進行調度。此種策略可能會造成低優先級的任務長時間無法執行。
在多級隊列的基礎上,還可以根據任務的運行情況,動態調整優先級,稱為 多級反饋隊列 Multi-Level Feedback Queue
此種方式傾向於將將短任務 (比如 I/O 密集型和交互式這種在 CPU 上運行時間較短的任務) 設置更高的優先級,以獲取更好的平均周轉時間。
動態調整優先級策略:在任務運行之初 MLFQ 會將新任務設置較高的優先級 (便於交互和實時任務), 在每個優先級上 MLFQ 會設置一個最長運行時間 (任務多次運行的總時間) 如果任務在此隊列上的運行時間超過此時間則 MLFQ 會認為此任務是一個較長的任務,將其調整至下一级隊列,多次運行後該任務會根據執行時間情況被調整到一個合適的優先級。
由於低優先級的任務一般是長任務,所以低優先級隊列的時間片也會比較長,高優先級隊列同理時間片會比較短。
公平共享調度#
除了提升調度指標以外,某些情況下還需要實現對資源的公平調度。比如用戶 A 和用戶 B 花費同樣的價格共享一台機器但是用戶 A 運行了三個進程而用戶 B 只 運行了一個進程,此時系統需要保證用戶 A 和用戶 B 各自都能占用 50% 的資源,即 CPU 的時間片。
彩票調度#
彩票調度為每個任務分配一定份額的彩票,每次調度時抽取一個中獎號碼,根據此中獎號碼選取需要執行的任務,類似於帶權重的隨機,伪代码如下:
loop:
R = random(0, total_tickets)
for sum = 0; task in task_list; {
sum += task->tickets
if sum > R {
schedule(task)
goto loop
}
}
通過此種方式為每個用戶分配相同的彩票額即可保證 CPU 時間的公平分配。但實際的分配情況依賴隨機,在調度次數較少的情況下由於隨機數據不平均可能會導致短期的分配不平均。
步幅調度#
步幅調度通過設置步幅確定性的為任務分配公平的時間,消除彩票調度的隨機性影響。
假設兩個任務 A 和 B 應分配的資源比例是 6:5 則步幅調度使用其反比作為其步幅,即 A 和 B 的步幅為 5 和 6 步幅調度會設置一個固定的時間片,每次選取當前已走距離最小的任務執行一次,並將其走過的距離加上對應的步幅。由於步幅不同,兩任務最終執行的時間片數量比會是 6:5 即達到穩定公平分配的目的。伪碼如下:
loop:
task = queue.pop_min()
schedule(task)
task.pass += task.stride
queue.push(task)
goto loop
多核調度#
在多核場景下由於系統可以同時運行多個任務,調度器需要決定同一時間有哪些任務可以進行,這些任務分別在哪個核心上運行。還需要考慮在同一核心上,如果調度不同任務,頻繁切換造成的性能損耗 (頁表切換,寄存器存取等)
協同調度#
對於某些任務間有關聯的場景,比如有請求 - 響應關係的兩個任務,如果不能同時運行即 A 發送消息時 B 並沒有在運行那這個消息要等到下次 B 被調度時才能收到,且 B 在被調度運行時 A 可能又會被調度出去返回的消息沒辦法立即接收,這樣每次發送請求和返回響應都會由於調度產生至少一個時間片的延遲,降低程序的運行效率。對於此類場景,需要使用協同調度:每次調度一組任務並行執行,這樣可以將具有关聯關係 (需要同時運行) 的任務可以分配到同一組內並行執行,而具有依賴關係的任務 (某個任務必須等待另外一個任務執行完) 需要分配到不同的組,避免空等造成 CPU 資源浪費。
協同調度一般用於並行計算,即將一個大的任務切分成多個子任務在多核心上並行執行加快計算。但是在更為普遍的場景下,使用協同調度可能會由於組內任務的相互等待 (注意:協同調度是以組為單位而非任務) 造成 CPU 資源的浪費。
協同調度的一個經典策略是群組調度,基本思想是將所有任務分成不同的組,將關聯任務分配至同一組,並且以組為單位,調度任務在多個核心上運行。比如現在有 4 個 CPU 核心和三組任務 A/B/C 每個組內分別有 4/3/2 個任務,則調度情況如下:
Core 0 | Core 1 | Core 2 | Core 3 |
---|---|---|---|
A0 | A1 | A2 | A3 |
B0 | B1 | B2 | C1 |
C2 | A0 | A1 | A2 |
兩級調度#
多核心場景下,考慮任務在不同核心間的切換開銷,可以將某個任務固定到一個核心上進行調度和執行。即新任務首先通過一個全局調度器分配到某個核心上,每個核心再通過單核調度策略執行分配給自己的任務,在系統運行過程中,全局調度器也會根據每個核心的負載情況,平衡任務在不同核心間的分配。這種調度方式稱為兩級調度。
處理器親和性#
某些操作系統在自己運行的調度器之上,為用戶進程提供了處理器親和性相關的調用接口,可以讓應用進程自己控制 CPU 核心使用情況,比如 Linux 提供了 sched_setaffinity
接口可以讓用戶進程控制自己只能在某些核心上執行。