目錄
1.介紹
2.理解
3.Linux早期的內核調度隊列
1.介紹
這是32位的程序空間地址圖:
為了更好地理解這段圖,我們來寫一段代碼編譯運行:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int g_val=100;int main()
{pid_t id = fork();int cnt=3;if(id == 0){while(1){printf("I am child, pid=%d, ppid=%d,g_val=%d &g_val=%p\n", getpid(), getppid(),g_val,&g_val);sleep(2);cnt--;if(cnt==0){g_val=500;printf("I am child,change pid=%d->%d\n", 100,500);}}}else {while(1){printf("I am father, pid=%d, ppid=%d,g_val=%d &g_val=%p\n", getpid(), getppid(),g_val,&g_val);sleep(2);}}return 0;
}
我們可以看見子進程修改 g_val 的地址后,父進程的地址和子進程的地址是一模一樣的,一個地址為什么會有兩個不同的值?
答案是這是個虛擬地址,不是物理內存地址,我們在用C/C++語言所看到的地址,全部都是虛擬地址!物理地址,用戶一概看不到,由OS統一管理,下面我們從操作系統來理解地址空間。
地址空間的本質是內核中的結構體對象。
未寫時拷貝(初始共享)
父進程 fork 創建子進程,虛擬地址空間、頁表 “邏輯復制” :父子進程虛擬地址(如 g_val 地址 0x56ab5ceac010 )一致,頁表均映射到物理內存同一塊數據頁(g_val = 100 )。此時代碼、數據物理頁共享,不實際拷貝內存,快速創建進程,節省空間。
寫時拷貝(觸發拷貝)
當子 / 父進程嘗試修改共享數據(如子進程改 g_val值?),操作系統檢測到寫操作:
為寫操作進程(如子進程)新分配物理頁;
把原共享物理頁數據(100 )拷貝到新頁;
更新寫操作進程頁表,使其指向新物理頁(此時子進程 g_val 改 500 )。父進程頁表不變,仍訪問原物理頁(g_val 保持 100 ),實現 “寫時才真正拷貝內存”,避免冗余開銷。
核心邏輯:讀共享,寫拷貝,平衡進程創建效率與數據獨立性 。
2.理解
1.地址空間的本質是 struct 里面的一個結構體,內部很多屬性都是表示 start 和 end 的范圍。?
2.虛擬地址將無序變為有序,讓進程從統一的角度看待物理內存以及自己運行的各個區域。
3.進程管理模塊和內存管理模塊相互解耦。
在計算機系統(尤其是操作系統、分布式框架)中,進程管理模塊(負責進程的生命周期管理、調度、狀態維護、權限控制等)與內存管理模塊(負責內存分配、回收、地址映射、虛擬內存管理等)是核心功能模塊。
二者的 “相互解耦” 是指通過設計隔離模塊間的直接依賴,使它們能獨立完成各自功能,僅通過標準化接口協作,從而提升系統的可維護性、擴展性和容錯性。
虛擬地址頁表是實現兩者解耦的核心機制之一。
4.攔截非法請求。
虛擬地址頁表通過地址合法性驗證、權限檢查和進程地址空間隔離,構建了一層硬件級別的保護機制。它能有效攔截非法的內存訪問請求(如越界、權限違規、訪問未分配內存等),防止物理內存被錯誤或惡意操作破壞,是操作系統保障內存安全的核心手段之一。
之前我們介紹 Linux 進程的時候講過一段代碼
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed");return 1;} else if (pid == 0) {while(1){printf("Child process: ID=%d PID = %d, PPID = %d\n", pid , getpid(), getppid());sleep(2);}} else {while(1){printf("Parent process: ID=%d PID = %d, Child PID = %d\n",pid , getpid(), pid);sleep(2);}}return 0;
}
fork() 對子進程進行了寫時拷貝,所以才返回了兩個不同的值。
3.Linux早期的內核調度隊列
一個 CPU 擁有一個 runqueue
- 如果有多個 CPU 就要考慮進程個數的負載均衡問題
運行隊列優先級
queue[140]
之前我們介紹進程優先級的時候,我們介紹過進程默認優先級是 80,nice的范圍為 [20,-19]。
進程隊列的優先級為:
- 普通優先級:100~139
- 實時優先級:0~99
我們的進程值+40就能建立和進程隊列的映射
位圖:
long bitmap[ 5 ]
我們的隊列優先級有140個,要是一個個逐一檢測,會增加時間。
bitmap就是為了節約時間的 long bitmap[5]?有 32*5=160 足夠包含這么多的優先級,我們只要看位圖的數字就能找到哪個優先級還存在進程。
這就是大 O (1) 調度算法,大 O (1) 調度算法指的是無論輸入規模(比如進程數量、任務數量等)如何變化,算法執行所需的時間保持恒定,不隨輸入規模的增大而增加。
活動隊列(Active Queue)(只出不進)
- 作用:用于存放時間片尚未耗盡的進程,這些進程會依據優先級進行組織,系統優先調度優先級高的進程,以此保障系統能夠高效響應任務需求。
- 調度邏輯:利用 bitmap 快速查找出優先級最高的非空隊列,然后選取該隊列的隊首進程執行。不管系統中進程的總數是多少,查找和調度進程所花費的時間始終固定,其時間復雜度為?O(1)?,確保了調度過程高效、穩定。
過期隊列(Expired Queue)(只進不出)
- 作用:用來存放時間片已經耗盡的進程,它的結構和活動隊列完全一樣,可看作是進程時間片管理的 “過渡區域”。
- 特點:當活動隊列中的進程把自身時間片用完后,就會被轉移到過期隊列中。而當活動隊列為空(意味著所有進程的時間片都已耗盡 )時,系統會交換 active 和? expired?指針,此時過期隊列就轉變為新的活動隊列,同時重新計算該隊列中進程的時間片,讓這些進程能夠再次參與到系統調度中,以此實現 “批次輪換” 的調度機制,保障進程獲取調度的公平性。
active 指針和 expired 指針
- active 指針:始終指向當前可供調度使用的?活動隊列,系統會從該隊列里選取進程來執行任務。
- expired 指針:始終指向?過期隊列,用于暫時存放那些時間片已經耗盡的進程。
- 核心機制:在系統運行過程中,活動隊列里的進程會因為時間片不斷消耗而逐漸減少,與之相對,過期隊列里的進程數量會相應增多。當活動隊列為空時,交換這兩個指針,過期隊列就 “變身” 為新的活動隊列,原本過期隊列中的進程會重新獲得時間片,繼續參與系統調度。這種方式無需實際去搬運進程,就能瞬間重置調度資源池,保障調度持續高效地進行。