Linux進程概念
1. 進程概念
1.1 理解馮諾依曼體系解構
馮諾依曼體系解構五大核心:
-
運算器:負責算數運算(加減乘除)和邏輯運算(與或非)。
-
控制器:從內存中讀取指令,并協調其他部件執行。
-
存儲器:本質是內存,存儲程序的指令和數據。
-
輸入設備:將外部信息轉換為計算機可處理的數據,如鍵盤、鼠標、網卡、磁盤等。
-
輸出設備:將計算結果反饋給用戶,如顯示器、磁盤、網卡等。
運算器和控制器又統一稱為中央處理器。
CPU獲取、寫入數據,只能從內存中來進行。如果沒有內存,那么CPU只能和輸入設備和輸出設備去交互,而CPU的處理速度和輸入輸出設備的速度不在一個量級,這就會導致 木桶效應(一只木桶能裝多少水,不取決于最長的那塊木板,而取決于最短的那塊。),計算機的效率就會完全取決于輸入輸出設備,所以內存是計算機中不可缺失的一部分,也是當代計算機性價比的產物。
從上述來看,引入內存好像還是沒有解決問題。這里又會引出一個局部性原理的概念。
局部性原理分為兩類:
-
時間局部性:如果一個數據或指令被訪問,那么在不久的將來很有可能再次訪問(循環中的變量、頻繁調用的函數)。
- 場景:緩存熱門數據,緩存最近訪問被訪問的數據。
-
空間局部性:如果一個數據或指令被訪問,那么其附近的數據很可能在不久的將來也會被訪問。(數組、順序執行的指令)。
- 場景:CPU預加載相鄰的數據到緩存。
木桶效應和局部性原理的結合,是計算機系統中解決CPU與I/O設備速度不匹配的問題的關鍵思想之一。
而現代計算中存在一個金字塔,依次是:寄存器、高速緩存L1、高速緩存L2、高速緩存L3、內存、外存(磁盤)、網盤等。從左到右效率和價格都是遞減的。
我們可以在硬件上簡單理解數據流動,當兩臺計算機在微信中相互發送消息,本質上是兩臺馮諾依曼體系解構在交互,微信是一個可執行程序,點開登錄微信的操作,其實是將微信的數據加載到內存中,用戶A從鍵盤中輸入消息,微信在內存中獲取鍵盤中的數據,CPU處理數據后,再寫回內存當中,再由微信把數據交給輸出設備網卡上,由網卡經過網絡轉交給用戶B,當用戶B收到數據后進行逆過程。
所以程序在運行之前是在磁盤中,軟件運行,必須先加載到內存,這是因為體系結構規定的。而加載到內存的本質其實是把數據從一個設備"拷貝"到另一個設備當中。
1.2 理解操作系統(Operator System)
1.2.1 操作系統是什么?
操作系統:是一款進行軟硬件管理的軟件。
狹義上的OS:指的是操作系統內核,其中包括進程/線程管理、文件系統、內存管理、驅動管理等。
廣義上的OS:指的是操作系統內核 + 外殼程序(Shell)、一些第三方庫、預裝的系統軟件。
1.2.2 軟硬件體系層狀結構
操作系統的目的:對上,為用戶程序(應用程序)提供一個良好的執行環境;對下,與硬件交互,管理所有的軟硬件資源。
訪問操作系統,必須使用系統調用,而一個程序如果想要訪問硬件,必須貫穿軟硬件體系結構!在用戶層語言為我們提供的標準庫,底層都是封裝了系統調用接口,系統調用是因為操作系統不相信任何人操作內核,但還要向上為用戶提供服務,那么用戶和操作系統之間,就可以采用系統調用來進行數據的交互,本質是操作系統的封裝。
操作系統的管理:管理是一個可以很抽象的詞語,操作系統的管理本質上是對數據做管理,第一步需要先描述被管理的對象(struct),然后把被管理的對象在組織起來(數據結構),那么對軟硬件的管理就轉換成了對數據的管理。而數據是通過驅動程序的接口填充的。
2. 進程
2.1 怎么理解進程?
-
基礎概念:內存中的可執行程序等。
-
內核觀點:擔當系統資源分配的實體。
-
進程 = 內核數據結構對象(task_struct) + 自己的代碼和數據。
2.2 PCB(Process control block)
進程控制塊(PCB)是內核用于管理進程的核心數據結構,是一個寬泛性的詞語,適用于任意一款操作系統。而在Liunx中,PCB是task_struct。
task_struct主要包括:
-
標識符:描述本進程的唯一標識符,用來區別其他進程
-
狀態:運行狀態、睡眠狀態、僵尸狀態等
-
優先級:相對于其他進程的優先級
-
程序計數器:程序中即將被執行的下一條指令的地址
-
內存指針:指向程序代碼和進程相關數據的指針
-
上下文數據:進程執行時處理器的寄存器中的數據
-
uid:(User ID),是OS為每個用戶分配的唯一標識符,在權限管理機制中,區分不同用戶的身份。
-
其他信息
內核中是以雙向鏈表的方式來組織task_struct結構體的。
2.3 查看進程信息
2.3.1 ps
ps axj # 查看所有進程
ps axj | head -1 && ps axj | grep process-name | grep -v grep # 查看指定進程名的進程
-
a:顯示所有用戶的進程。默認情況下,ps只會顯示當前終端的進程,a選項會顯示所有用戶的進程(包括其他終端和后臺進程)
-
x:顯示沒有控制終端的進程,例如后臺運行的守護進程
-
j:添加進程工作控制信息,包括進程組ID、會話ID、父進程ID,以及相關信息
-
u:以用戶友好的格式顯示進程信息,包括進程所有者、CPU和內存的使用情況等
grep - v grep:grep也是一個進程,使用grep命令查詢進程也會把grep自己查詢出來,因為grep查詢進程中的關鍵字是你所查詢進程的名字。
2.3.2 top
top
2.3.3 ls /proc
通過文件的方式查看內核中PCB的信息。
ls /proc # 里面中目錄的名字都是以進程的PID命名
2.4 通過系統調用修改進程的工作目錄
cwd(current work director)代表進程當前的工作目錄。
頭文件 | 系統調用 | 返回值 |
---|---|---|
#include <unistd.h> | int chdir(const char * path); | 成功0被返回,失敗-1被返回 |
2.5 通過系統調用獲取進程標識符
頭文件 | 系統調用 | 返回值 |
---|---|---|
#include <sys/types.h> #include <unistd.h> | pid_t getpid(void); | 返回當前進程的進程標識符 |
#include <sys/types.h> #include <unistd.h> | pid_t getppid(void); | 返回當前進程的父進程標識符 |
2.6 通過系統調用創建子進程
頭文件 | 系統調用 | 返回值 |
---|---|---|
#include <sys/types.h> #include <unistd.h> | pid_t fork(void); | 1. 給父進程返回子進程的pid 2.給子進程返回0 3.函數調用失敗返回-1 |
fork有兩個返回值。
父子進程代碼共享,數據各自開辟空間,采用寫時拷貝的方式。
fork之后通常要用if進行分流。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main () {printf("父進程開始運行,pid = %d\n" , getpid());pid_t id = fork();if (id < 0) {perror("fork");return -1;} else if (id == 0) {// child processwhile (1) {sleep(1);printf("我是一個子進程,我的id = %d,我的父進程id = %d\n" , getpid() , getppid());}} else {// parent processwhile (1) {sleep(1);printf("我是一個子進程,我的id = %d,我的父進程id = %d\n" , getpid() , getppid());}}return 0;
}
2.6.1 為什么fork給父子返回不同的返回值?
因為父子的關系是 1:n 的,父進程可能有多個子進程,所以父進程需要子進程的 pid 來管理;子進程只有一個唯一的父進程。
2.6.2 為什么一個函數會返回兩次?
fork本質是一個函數,在內部的工作:
-
創建新的PCB 。
-
拷貝父進程的PCB給子進程(修改一些單獨的數據)。
-
將子進程的PCB放入進程鏈表中甚至放入調度隊列中。
所以當fork函數return時,函數中主體邏輯已經執行完成,代碼已經開始分流,所以return語句也會執行兩次。
2.6.3 為什么一個變量即等于0又可以大于0,導致if else同時成立?
每個進程訪問的內存實際上是一個虛擬地址空間 。操作系統會為每個運行的進程分配一個獨立的虛擬地址空間(也稱為進程地址空間)。fork
之后,return
本質也是對一個變量做修改,雖然父子進程數據和代碼是共享的,但是父子任意一方修改共享數據,操作系統會為其修改數據的一方分配新的物理內存,復制原數據到該物理內存中,再讓修改者在新的物理內存中寫入。這種機制叫做 寫時拷貝。
進程具有獨立性。
2.7 task_struct的緩存鏈表
task_struct是C語言中的結構體,操作系統每次創建進程都需要在內存中開辟空間,大量的申請釋放內存會存在效率的問題,而現代操作系統幾乎不會做浪費時間和空間的事情。
在內核中OS也會維持一個task_struct的緩存鏈表,當一個進程終止后,OS不會釋放task_struct,而是鏈入緩存鏈表中,每次創建PCB時只需要從緩存鏈表中取,覆蓋其對應的字段數據即可。
3. 進程狀態
進程狀態本質就是 tast_struct 數據結構中的一個整數。
Linux內核源碼中的定義:
static const char *const task_state_array[] = {"R (running)", /*0 */ "S (sleeping)", /*1 */"D (disk sleep)", /*2 */"T (stopped)", /*4 */"t (tracing stop)", /*8 */"X (dead)", /*16 */"Z (zombie)", /*32 */
};
3.1 內核數據結構設計
PCB為什么既在進程鏈表中又在運行隊列中?一個結構體節點可以在不同的數據結構中?
3.1.1 常規的雙鏈表設計
struct list {int val;struct list* prev;struct list* next;
};
常規的雙鏈表設計,在某些應用場景是滿足不了需求的。比如需要一個節點既是鏈表節點又是隊列節點,像這樣的定義方式無法滿足。
3.1.2 內核中嵌入子結構
在Linux系統內核中,task_struct
結構使用嵌入子結構的方式,來實現管理進程相關的多種數據結構(一個節點被多種數據結構管理),通過將鏈表節點和其它結構的指針嵌入到 task_struct
中,實現高效的多數據結構交互。
Linux內核定義了一個通用的雙向鏈表節點結構 list_head
,僅包含前驅和后繼指針。
struct list_head {struct list_head * next , *prev;
};
嵌入到 task_struct
,task_struct
中通過list_head
嵌入多個鏈表節點,比如:管理全局進程鏈表、管理子進程鏈表、運行隊列等。
// 模擬Linux PCB數據結構設計
struct task_struct {int pid;int ppid;int status;// ...struct list_head process_links; // 管理進程的雙鏈表// ...int x;int y;// ...struct list_head run_queue; // CPU運行隊列// ...
};
int main () {// 初始化3個進程服務struct task_struct proc1 = {1 , 1 , 1 , {NULL , NULL} , 1 , 1 , {NULL , NULL}};struct task_struct proc2 = {2 , 2 , 2 , {NULL , NULL} , 2 , 2 , {NULL , NULL}};struct task_struct proc3 = {3 , 3 , 3 , {NULL , NULL} , 3 , 3 , {NULL , NULL}};// 連接proc1.process_links.next = &proc2.process_links;proc2.process_links.prev = &proc1.process_links;proc2.process_links.next = &proc3.process_links;proc3.process_links.prev = &proc2.process_links;proc1.process_links.prev = &proc3.process_links;proc3.process_links.next = &proc1.process_links;// 定義進程雙向鏈表的頭節點struct list_head* process_list_head; process_list_head = &proc1.process_links;// 定義運行隊列雙向鏈表的頭節點struct list_head* run_queue_head;run_queue_head = &proc1.run_queue;return 0;
}
下圖所示中,三個節點既在雙向鏈表plh頭指針中,又在運行隊列rqh頭指針中,在操作系統中,情況會更加復雜,所有節點都會在雙向鏈表頭指針中管理,其中運行的節點又會放在運行隊列頭指針管理連接起來,那么這只是進程鏈表和運行隊列,如果還需要被其他數據結構管理呢?操作系統數據結構中的指針是網狀的。Linux內核的設計通過混合數據結構和層級的劃分實現高效的、可維護的系統。
常規的雙向鏈表中的next指向的是下一個節點的起始地址,嵌入子結構中的next指向的是下一個節點中list_head成員地址,并不是頭節點,所以無法直接訪問當前節點內部成員。訪問C語言中的結構體,要從結構體地址的起始處訪問成員。
3.1.3 訪問嵌入式子結構的成員
訪問proc2的pid成員:
// 1.獲得process_links成員相較于結構體對象起始地址的偏移量
size_t offset = (size_t)(&((struct task_struct*)0)->process_links);
// 2.使用proc2的process_links的地址 - 相較于起始地址的偏移量 = 起始地址
// process_list_head->next 是一個 struct list_head* 指針,而 offset 是 size_t 類型(代表偏移量字節數)
// 指針減法是按指針類型的大小為單位計算的,不是按字節,
// 例如,struct list_head* p; p - 1 會減去 sizeof(struct list_head) 字節,而不是 1 字節。所以需要轉換為char* 再減去偏移量
int proc2_pid = ((struct task_struct*)((char*)(process_list_head->next) - offset))->pid;
注意:指針減法是按指針類型的大小為單位計算的,所以需要先將
process_list_head->next
轉換為char*
(按字節計算),再減去偏移量
((struct task_struct*)((char*)(process_list_head->next) - (size_t)(&((struct task_struct*)0)->process_links)))->pid
3.2 進程狀態
3.2.1 R(Running)
進程正在CPU上執行或在運行隊列中等待CPU調度。
3.2.2 S(Sleeping)
進程正在等待某個事件完成(如I/O完成、用戶輸入等),這里的睡眠也可以叫做可中斷睡眠。
3.2.3 D(Disk Sleep)
進程在等待不可中斷的I/O,不能被信號終止。
3.2.4 T(Stopped)
進程被暫停,可以通過發送信號SIGSTOP暫停,也可以發送SIGCONT信號讓進程繼續運行。
3.2.5 X(Dead)
進程完全終止,瞬時狀態。
3.2.6 Z(Zombie)
僵尸狀態,當前進程已經運行結束,但是父進程并沒有讀取子進程的退出結果(正常退出/異常退出、結果正確/錯誤),導致子進程PCB需要一直在內存中維持。大量的僵尸進程會導致內存泄漏。
3.2.7 孤兒進程
父子進程關系中,如果父進程先退出,子進程被稱為孤兒進程。孤兒進程會被1號systemd進程領養并回收。
3.2.8 掛起
-
進程 = 內核數據結構 + 自己的代碼和數據
-
掛起:當系統內存資源嚴重不足時,OS可能會將處于等待狀態且位于資源隊列尾部的進程中的代碼和數據喚出到磁盤swap分區中。當等待的資源就緒、進程被調度、內存壓力緩解時,會被操作系統喚入。
3.3 進程優先級
進程優先級是得到CPU資源的先后順序。優先級高的進程有優先執行的權力。而操作系統分為分時操作系統和實時操作系統。
-
分時操作系統:基于時間片的調度,追求公平,優先級可能會變化,但是變化幅度不能太大。
- 場景:Linux、Windows等
-
實時操作系統:強調優先級搶占式調度,確保優先級高的先響應。
- 場景:工業領域、自動駕駛等
3.3.1 查看進程優先級
ps -al | grep process-name
3.3.2 PRI與NI
-
PRI:進程優先級,值越小越早執行(默認80)。取值范圍
[60 , 99]
,依賴nice值。 -
NI:nice值,進程優先級的修正值。取值范圍
[-20 , 19]
,修改nice值,本質就是調整進程優先級。
真實優先級:PRI(默認80) + nice值。
每次修改nice值,都會以PRI默認值80為基準計算。
修改nice值: top -> r -> pid -> nice值;nice、renice命令、系統調用等。
3.4 進程補充概念
-
競爭性:系統進程數量眾多,而CPU只有少量,甚至1個,所以進程之間是具有競爭性的。
-
獨立性:進程具有獨立性,一個進程不會影響其他進程的運行。
-
并行:多個進程在多個CPU下同時運行,被稱為并行。
-
并發:多個進程在一個CPU下采取 進程切換 的方式,使多個進程得以推進,稱為并發。
3.5 進程切換
進程切換 指的是 操作系統內核 將CPU的 執行權 從一個進程轉移到另一個進程的過程。涉及到保存當前進程的上下文數據,并在下次切換時恢復進程的上下文數據。
進程切換的觸發條件:分時操作系統中時間片耗盡、高優先級進程搶占等。
進程切換的核心是上下文切換。核心步驟:
- 將CPU寄存器(如pc、ebp、esp、eax…)中的數據保存當前進程的PCB中(TTS,任務狀態段)。
- 從運行隊列中選擇下一個要運行的進程。
- 恢復當前進程的上下文數據,CPU從目標進程的PC(程序計數器)處繼續執行。
PC指針:程序計數器,通常存儲的是下一條待執行指令的地址。
可以使用一個標記位來標識當前進程是第一次運行的進程,還是已經調度過的進程。
3.6 Linux內核O(1)調度隊列
3.6.1 偽代碼
struct RunQueue{// ...*active;*expired;// 活躍隊列nr_active;bitmap[5];queue[140];// 過期隊列nr_active;bitmap[5];queue[140];// ...
};
3.6.2 說明
-
一個CPU擁有一個runqueue
- 如果有多個CPU就需要考慮負載均衡問題。
-
優先級(和queue數組下標對應)
-
普通優先級:100 ~ 139(和nice取值對應)。
-
實時優先級:0 ~ 99(不考慮)。
-
計算優先級:PRI(80)+NI(nice值)?PRImin(60)+100PRI(80) + NI(nice值) - PRI_{min}(60) + 100PRI(80)+NI(nice值)?PRImin?(60)+100。
-
-
Linux內核中,存在一個
struct task_struct* current
變量,用于指向當前正在運行的進程。 -
活躍隊列
-
正在參與調度或即將被調度的進程放在該隊列。
-
nr_active:總共有多少個運行狀態的進程。
-
queue[140]:數組的每個元素對應一個優先級進程隊列,相同優先級按照FIFO規則進行排隊,數組下標本質就是優先級。
-
bitmap[5]:位圖,每一位與queue數組元素一一對應,提升非空隊列的查找效率。
-
位圖還是需要遍歷140個bit,效率提高了嗎?
-
由于絕大多數進程采用默認優先級,大規模 修改優先級的場景較少,因此該數組大部分元素通常為
NULL
。而bitmap
每一位對應一個優先級隊列是否為空,通過按字節批量檢查,若bitmap[i] == 0
(即8
個連續優先級隊列均為空),僅當bitmap[i] != 0
,才需進一步檢查。
-
-
-
過期隊列
-
過期隊列的結構和活躍隊列一摸一樣。
-
存放時間片已經耗盡的進程,成為新的活躍隊列后重新分配時間片。
-
目的:實現公平的調度,避免高優先級的進程長期壟斷CPU。
- 若采用 單隊列數組
queue[140]
結構,且進程執行完畢后仍重新插入原優先級的尾部,則可能導致低優先級進程長期無法獲得CPU調度。
- 若采用 單隊列數組
-
-
active指針和expired指針
-
active指針永遠指向活躍隊列。
-
expired指針永遠指向過期隊列。
-
隨著CPU的調度,活躍隊列中的進程會越來越少,過期隊列中的進程越來越多。
-
在合適時(活躍隊列全為空時),調度器會交換active和expired指針(
swap(&active,&expired)
)。這樣就會產生一個新的活躍隊列。 -
struct rqs {nr_active;bitmap[5];queue[140]; }; struct rqs priority_array[2]; struct rqs* active = &priority_array[0]; struct rqs* expired = &priority_array[1]; swap(&active , &expired);
-
3.6.3 圖示
4. 命令行參數和環境變量
在操作系統中,命令行參數和環境變量 是兩個重要的進程執行上下文信息,他們由操作系統或bash傳遞給程序,影響程序的行為。
4.1 命令行參數
命令行參數是用戶在啟動程序時通過命令行的方式傳遞給程序的參數。
#include <stdio.h>int main(int argc, char *argv[]) {printf("程序名: %s\n", argv[0]); // argv[0] 是程序名printf("參數個數: %d\n", argc - 1);for (int i = 1; i < argc; i++) {printf("%s\n", argv[i]);}return 0;
}
運行方式:
./code arg1 arg2 arg3
輸出:
程序名: ./code
參數個數: 3
arg1
arg2
arg3
main的命令行參數是實現程序不同子功能的方法。
Linux下的部分指令是通過C語言的命令行參數實現的。
4.2 環境變量
4.2.1 概念
環境變量一般是指在操作系統中用來指定操作系統運行環境的一些參數。環境變量是系統或用戶定義的鍵值對(key=value)。環境變量通常具有某些特殊的用途。
常見的環境變量:
-
PATH:指定程序或命令的搜索路徑。
-
HOME:指定用戶的家目錄。
-
USER:記錄當前的用戶。
-
其他環境變量。
Bash中,變量分為普通變量(本地變量)和環境變量(全局變量):
-
環境變量:具有全局屬性,由bash或父進程傳遞,會被子進程繼承(環境變量最初是通過系統配置文件導入的)。
-
本地變量:不會被子進程繼承,只會在bash內部使用。
- 聲明方式:
name=value
- 聲明方式:
Bash中,命令的執行方式分為 外部命令(通過創建子進程執行) 和 內建命令(由bash親自執行):
-
外部命令:通過創建子進程和程序替換實現的(
fork
+exec
)。 -
常見的外部命令:
ls , grep , cat , python , gcc
-
內建命令:通過bash自己內部實現的方法調用系統調用接口實現的。
-
常見的內建命令:
cd , export , echo
4.2.2 命令行操作環境變量
1?? 顯示所有環境變量:
env
2?? 顯示本地變量和環境變量:
set
3?? 顯示某個環境變量值:
echo $NAME # NAME 環境變量的名稱
4?? 設置和清除環境變量:
export key=value # 設置
unset key # 清除
4.2.3 程序中操作環境變量
1?? main函數的第三個參數:
#include <stdio.h>int main(int argc, char *argv[], char *env[]) {for(int i = 0; env[i]; i++){printf("%s\n", env[i]);}return 0;
}
2?? 通過全局變量 environ 獲取:
#include <stdio.h>extern char** environ;
int main(int argc, char *argv[]) {for(int i = 0; environ[i]; i++){printf("%s\n", environ[i]);}return 0;
}
3?? 通過系統調用獲取:
#include <stdio.h>
#include <stdlib.h>int main() {printf("%s\n", getenv("PATH"));return 0;
}
4.2.4 配置PATH環境變量
在Linux中,PATH
環境變量決定了系統在哪些目錄中查找可執行程序。通過配置 PATH
,使其包含自定義的路徑,以便直接運行程序而無需輸入完整路徑。
1?? 修改 ~/.profile
Shell配置文件。
// ubuntu
vim ~/.profile
2?? 在文件末尾添加:
export PAHT=$PATH:/your/path
3?? 然后重新加載或重啟終端。
source ~/.profile
5. 進程地址空間(虛擬地址空間)第一講
進程地址空間是操作系統為每個運行中的進程分配的虛擬內存視圖,定義進程可以訪問的內存范圍。
-
32位系統和64位系統的區別之一是虛擬內存的地址編號是 000 ~ 2322^{32}232 或 $000 ~ 2642^{64}264 。
-
進程地址空間在內核中其實就是一個數據結構
mm_struct
。每個進程 只有? 個mm_struct
結構,在每個進程task_struct
結構中,有?個指向該進程的mm_struct
的結構體指針。-
每個進程的虛擬地址空間(
mm_struct
)又由多個vm_area_struct
節點 組成,它們通過 紅黑樹(rb_tree) 和 鏈表 組織,用于高效管理不同內存區域。-
記錄虛擬內存的起始和結束
-
記錄控制內存的的訪問權限
-
其它
-
-
mm_struct
:記錄整個虛擬地址空間的全局信息,以及記錄關鍵段的起始和結束地址(如代碼段、數據段、堆、棧等)。 -
vm_area_struct
:描述進程地址空間中一段連續的虛擬內存區域。
-
-
虛擬地址是通過 頁表 的映射轉換為物理地址的。
-
重新理解什么是進程?
-
享受系統分配資源的實體。
-
進程 = 內核數據結構(task_struct + mm_struct + 頁表)+ 自己的代碼和數據。
-
驗證虛擬地址的存在:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int g_val = 0;
int main() {pid_t id = fork();if(id < 0){perror("fork");return 0;} else if(id == 0) { //child,?進程肯定先跑完,也就是?進程先修改,完成之后,?進程再讀取g_val=100;printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else { //parentsleep(3);printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);} sleep(1);return 0;
}
以上代碼輸出結果顯示,地址是相同的,但是值卻不同。
-
變量的內容不一樣,所以父子進程的輸出的變量絕對不是同一個變量。
-
地址值一樣,說明該地址絕對不是物理地址。
解析:當父子進程修改共享數據時,會觸發寫時拷貝。因為進程具有獨立性。
為什么需要虛擬地址空間?
物理內存的局限性:
-
程序直接使用物理地址時,內存會被分割成不連續的塊,導致無法分配大塊連續內存。
-
程序可以隨意訪問任何物理地址,可能破壞其他進程或內核數據。
虛擬內存的優勢:
-
內存隔離和安全性
-
每個進程擁有獨立的虛擬地址空間,從
0x000...
開始,仿佛獨占整個內存。 -
進程無法直接訪問其他進程或內核的物理內存(進程獨立性)。
-
-
無序變有序
-
虛擬地址對程序呈現為連續的地址空間,實際物理內存可以是分散的。
-
解決物理內存碎片化問題。
-
-
內存擴展
-
虛擬地址空間可以大于物理內存,通過磁盤交換(Swap)實現無限內存假象。
-
按需加載:一個游戲程序可能申請 16GB 虛擬內存,但物理內存僅 8GB,未活躍數據會被換出到磁盤。
-
6. 補充
6.1 缺頁中斷(Page Fault)的本質與作用
缺頁中斷是 CPU 訪問虛擬內存時,因目標頁面無法直接訪問而觸發的一種硬件異常。它是操作系統實現虛擬內存管理的核心機制,使得程序可以按需使用物理內存,而非一次性加載全部數據。其核心邏輯是:當程序訪問的虛擬內存頁尚未映射到物理內存時,CPU 暫停當前指令,由操作系統介入處理,完成后恢復程序執行。
觸發缺頁中斷的三大原因:
類型 | 觸發條件 | 典型場景 |
---|---|---|
硬缺頁(Major Fault) | 目標頁不在物理內存中,需從磁盤(如文件或Swap分區)加載數據 | 程序啟動時加載代碼段、讀取大文件 |
軟缺頁(Minor Fault) | 目標頁已在物理內存中,但未與當前進程的頁表關聯(如共享庫被其他進程提前加載) | 共享庫映射 |
寫時拷貝(COW Fault) | 進程嘗試寫入共享的只讀頁(如 fork() 后的內存修改) | 父子進程間修改共享數據 |
為什么需要缺頁中斷?
-
內存超售(Overcommit):程序可申請比物理內存更大的虛擬空間,實際使用時再分配物理頁。
-
延遲加載:避免啟動時加載全部代碼/數據(如僅加載程序當前執行的函數)。
-
共享內存:多個進程共享同一物理頁(如動態庫
libc
)。 -
寫時拷貝:
fork()
后父子進程共享內存,僅在修改時復制,減少開銷。