1. 進程的基本概念
1.1 進程的定義
進程就是運行中的程序。程序本身是沒有生命周期的,它只是存在磁盤上面的一些指令(也可能是一些靜態數據)。是操作系統讓這些字節運行起來,讓程序發揮作用。
1.2 CPU的時分共享
操作系統通過讓一個進程只運行一個時間片,然后切換到其他進程,提供了存在多個虛擬CPU的假象。這就是時分共享(time sharing)CPU技術,允許用戶如愿運行多個并發進程。潛在的開銷就是性能損失,因為如果CPU必須共享,每個進程的運行就會慢一點。
1.3 進程的機器狀態
進程的機器狀態包括:
- 內存:進程可以訪問的內存(稱為地址空間,address space)
- 指令存在內存中
- 正在運行的程序讀取和寫入的數據也在內存中
- 寄存器:
- 程序計數器(Program Counter,PC):告訴我們程序當前正在執行哪個指令
- 棧指針(stack pointer)和幀指針(frame pointer):用于管理函數參數棧、局部變量和返回地址
2. 程序與進程的區別
- 程序(Program):一個靜態實體,通常是以可執行文件形式存儲在磁盤上的指令和數據的集合
- 進程(Process):程序的動態執行實例,運行時加載到內存中,擁有獨立的地址空間、執行狀態和系統資源
簡單來說,程序是靜態的"藍圖",而進程是程序被激活后的"活體"。
3. 操作系統啟動并運行程序的過程
3.1 加載程序到內存
- 定位可執行文件:根據用戶命令或系統調用找到磁盤上的可執行文件
- 讀取文件頭部:
- 入口點(Entry Point):程序開始執行的地址
- 段信息:代碼段、數據段等的地址和大小
- 從磁盤讀取字節:
- 代碼段:包含程序的指令,通常是只讀的
- 數據段:包含初始化的全局變量和靜態變量
- BSS段:包含未初始化的全局變量
- 分配虛擬地址空間:為進程分配獨立的虛擬地址空間
- 按需加載(可選):延遲加載優化性能
3.2 分配堆棧和堆
- 堆棧(Stack):存儲函數調用信息、局部變量等
- 堆(Heap):用于動態內存分配
3.3 設置執行環境
- 寄存器初始化
- 命令行參數和環境變量
- 文件描述符
3.4 創建進程控制塊(PCB)
記錄進程元數據,包括:
- 進程ID(PID)
- 進程狀態
- 內存管理信息
- 打開的文件描述符
3.5 調度執行
進程被加入就緒隊列,等待CPU調度
4. 進程狀態
進程的三種基本狀態:
- 運行(running):在處理器上運行,執行指令
- 就緒(ready):準備好運行,但操作系統選擇不在此時運行
- 阻塞(blocked):執行了某種操作,直到發生其他事件時才會準備運行
從就緒到運行意味著該進程已經被調度(scheduled)。從運行轉移到就緒意味著該進程已經取消調度(descheduled)。一旦進程被阻塞(例如,通過發起 I/O 操作),OS 將保持進程的這種狀態,直到發生某種事件(例如,I/O 完成)。此時,進程再次轉入就緒狀態(也可能立即再次運行,如果操作系統這樣決定)。
寄存器上下文將保存其寄存器的內容。當一個進程停止時,它的寄存器將被保存到這個內存位置。==通過恢復這些寄存器(將它們的值放回實際的物理寄存器中)?,==操作系統可以恢復運行該進程。我們將在后面的章節中更多地了解這種技術,它被稱為上下文切換(contextswitch)?。
5. 進程管理系統調用
5.1 fork系統調用
- 創建一個新進程,作為調用進程的副本
- 子進程復制父進程的地址空間、PCB等
- 父進程返回子進程PID,子進程返回0
#include <stdio.h> // 包含標準輸入輸出庫,提供 printf() 等函數
#include <stdlib.h> // 包含標準庫,提供 exit() 函數等
#include <unistd.h> // 包含 UNIX 標準函數聲明,提供 fork()、getpid() 等int main(int argc, char *argv[]) // 程序入口,argc/argv 用于獲取命令行參數{// 在創建子進程之前,先打印當前進程的 PID(進程標識符)printf("hello world (pid:%d)\n", (int)getpid());// 調用 fork(),創建一個新進程(子進程)int rc = fork();if (rc < 0) { // fork 返回值小于 0,表示創建子進程失敗fprintf(stderr, "fork failed\n"); // 向標準錯誤輸出錯誤信息exit(1); // 退出程序,并返回非零狀態碼表示異常} else if (rc == 0) { // fork 返回值等于 0,表示當前是子進程// 子進程執行的代碼路徑printf("hello, I am child (pid:%d)\n", (int)getpid());} else { // fork 返回值大于 0,表示當前是父進程// rc 存儲的是子進程的 PIDprintf("hello, I am parent of %d (pid:%d)\n",rc, (int)getpid());}return 0; // 程序正常退出,返回值 0}
子進程并不是完全拷貝了父進程。具體來說,雖然它擁有自己的地址空間(即擁有自己的私有內存)?、寄存器、程序計數器等,但是它從fork()返回的值是不同的。父進程獲得的返回值是新創建子進程的PID,而子進程獲得的返回值是0。
系統顯示父進程先執行,但是這是隨機的,CPU調度程序(scheduler)決定了某個時刻哪個進程被執行
5.2 wait系統調用
- 父進程阻塞直到子進程結束
- 返回已結束子進程的PID
#include <stdio.h> // 標準輸入輸出,提供 printf()
#include <stdlib.h> // 標準庫,提供 exit()
#include <unistd.h> // POSIX API,提供 fork()、getpid()
#include <sys/wait.h> // 等待子進程,提供 wait()int main(int argc, char *argv[])
{// 在 fork 之前,先打印當前進程(父進程)的 PIDprintf("hello world (pid:%d)\n", (int)getpid());// 創建一個新進程;父進程中 rc > 0,子進程中 rc == 0,失敗時 rc < 0int rc = fork();if (rc < 0) {// fork 調用失敗fprintf(stderr, "fork failed\n");exit(1);} else if (rc == 0) {// 子進程執行這里的代碼printf("hello, I am child (pid:%d)\n", (int)getpid());} else {// 父進程執行這里的代碼// wait(NULL) 阻塞直到任意子進程結束,返回值是已結束子進程的 PIDint wc = wait(NULL);printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",rc, wc, (int)getpid());}return 0; // 正常退出
}
5.3 exec系統調用
- 在當前進程中加載并執行新程序
- 替換當前地址空間
- 重置堆棧、堆和寄存器
#include <stdio.h> // 標準輸入輸出,提供 printf()
#include <stdlib.h> // 標準庫,提供 exit()
#include <unistd.h> // POSIX API,提供 fork()、execvp()、getpid()
#include <string.h> // 字符串操作,提供 strdup()
#include <sys/wait.h> // 等待子進程,提供 wait()int main(int argc, char *argv[])
{// 程序啟動時打印當前進程(父進程)的 PIDprintf("hello world (pid:%d)\n", (int)getpid());// 創建子進程:父進程 rc>0,子進程 rc==0,失敗時 rc<0int rc = fork();if (rc < 0) {// fork 失敗,打印錯誤并退出fprintf(stderr, "fork failed\n");exit(1);}else if (rc == 0) {// 子進程執行此路徑printf("hello, I am child (pid:%d)\n", (int)getpid());// 準備 execvp 的參數數組// myargs[0] 指定要運行的程序名 "wc"// myargs[1] 指定要處理的文件 "p3.c"// myargs[2] 置 NULL,標記參數數組結束char *myargs[3];myargs[0] = strdup("wc"); myargs[1] = strdup("p3.c"); myargs[2] = NULL;// 用 execvp 替換當前子進程映像,執行 word count 程序execvp(myargs[0], myargs);// 如果 execvp 返回,說明執行失敗,才會走到這里perror("execvp failed");exit(1);}else {// 父進程執行此路徑// wait(NULL) 阻塞直到任意子進程結束,返回已結束子進程的 PIDint wc = wait(NULL);// 打印父進程信息,rc 是子進程的 PID,wc 是 wait 返回的 PIDprintf("hello, I am parent of %d (wc:%d) (pid:%d)\n",rc, wc, (int)getpid());}return 0; // 正常退出
}
要點說明
strdup()
:復制字符串并返回指向新內存的指針,用于給execvp
準備參數。execvp()
:用指定程序替換當前進程映像,不返回成功;如果失敗,會返回 -1,此時應打印錯誤并退出。wait(NULL)
:父進程阻塞直到子進程結束,避免子進程成為僵尸。
6. 安全機制:地址空間布局隨機化(ASLR)
6.1 ASLR的定義
ASLR是一種安全技術,通過隨機化進程的內存地址布局,防止攻擊者利用已知的內存地址執行惡意代碼。
6.2 工作原理
隨機化內存布局的關鍵區域:
- 堆棧(Stack)
- 堆(Heap)
- 可執行代碼(Text Segment)
- 動態鏈接庫(Shared Libraries)
6.3 優缺點
優點:
- 提升安全性
- 兼容性強
缺點:
- 非絕對防御
- 輕微性能開銷
7. 內存分配:堆和棧
7.1 棧(Stack)
保存內容:
- 局部變量
- 函數參數
- 返回地址
- 棧幀
特點:
- 先進后出(LIFO)
- 速度快
- 大小有限
7.2 堆(Heap)
保存內容:
- 動態分配的對象
- 全局數據(部分情況)
特點:
- 手動管理
- 靈活性高
- 速度較慢
- 可能產生碎片
7.3 堆和棧的區別對比
特性 | 棧(Stack) | 堆(Heap) |
---|---|---|
分配方式 | 自動分配和釋放 | 手動分配和釋放 |
存儲內容 | 局部變量、函數參數 | 動態分配的數據 |
生命周期 | 隨函數調用結束而銷毀 | 在手動釋放前一直存在 |
大小限制 | 容量較小 | 容量較大 |
速度 | 操作更快 | 操作較慢 |