1. 進程概念
1.1 進程的本質
核心定義
用戶視角:程序的動態執行實例(如同時運行多個Chrome窗口即多個進程)。
內核視角:資源分配的最小實體單位,獨享CPU時間片、內存空間和文件資源。
現代定義:
進程 = 內核數據結構(task_struct) + 程序代碼和數據段
進程不僅是“程序的執行實例”或“正在執行的程序”,從內核角度看,它是分配系統資源(CPU時間、內存)的實體。更精確地說,進程 = 內核數據結構(task_struct) + 程序代碼和數據。當程序被加載到內存時,操作系統為其創建一個task_struct實例,該結構體封裝了進程的所有屬性和狀態信息。生動示例:想象一個C程序(如hello.c
)被編譯執行時,操作系統會動態分配內存和CPU時間片,并將程序指令映射到虛擬地址空間,同時初始化task_struct來跟蹤其狀態,就像給每個運行的程序貼上一個“身份證”和“健康記錄卡”。
示例:啟動兩個
vim
編輯不同文件時,系統創建兩個獨立進程,各自擁有獨立的代碼執行流和內存空間,互不干擾。?我們先創建一個myprocess.c文件,然后死循環,每隔一秒打印
這里直接運行這個可執行程序,當我們把這個程序運行起來,其實就是一個進程
1.2?進程控制塊 (PCB)
task_struct:Linux的PCB實現
存儲位置:常駐內存(RAM),由內核動態管理。
關鍵字段分類(擴展版):
字段類別 具體內容 標識符 PID(進程ID)、PPID(父進程ID)、PGID(進程組ID) 狀態 運行態(TASK_RUNNING)、睡眠態(TASK_INTERRUPTIBLE)、僵尸態(EXIT_ZOMBIE)等 內存指針 代碼段( mm_struct->code_start
)、數據段(mm_struct->data_start
)指針上下文數據 保存暫停時的CPU寄存器值(eip, eax等),用于恢復執行 文件描述符表 記錄打開的文件( files_struct
結構體)資源限制 最大文件打開數、CPU時間配額( struct rlimit
)
進程組織方式
// 內核源碼示例(簡化版)
struct task_struct {volatile long state; // 進程狀態struct mm_struct *mm; // 內存管理結構體pid_t pid; // 進程IDstruct files_struct *files; // 打開文件表struct list_head tasks; // 雙向鏈表指針// ... 其他字段
};
全局進程鏈表:內核通過
struct list_head tasks
將所有進程組成雙向鏈表,頭節點為init_task
(PID=1的init進程)。
1.3?查看進程的實戰方法
1. /proc文件系統
動態虛擬文件系統:以目錄形式暴露內核進程信息。
# 查看PID為1的進程信息 $ ls /proc/1 exe -> /usr/lib/systemd/systemd # 可執行文件鏈接 cwd # 當前工作目錄 fd/ # 打開的文件描述符 status # 進程狀態摘要
2. 命令行工具
# 顯示進程樹(含父子關系)
$ pstree -p
systemd(1)─┬─sshd(1234)───bash(5678)───vim(9012)└─crond(2345)# 動態監控進程
$ top -p 9012 # 監控PID 9012(vim進程)的資源占用
3. ps指令
一、ps
?命令核心功能
作用:捕捉系統當前進程快照(非實時),用于:
查看進程狀態(運行/睡眠/僵尸)
分析資源占用(CPU/內存)
定位問題進程
查看進程間關系
二、參數詳解與使用場景
1. 基礎查看
命令 | 作用 | 示例輸出片段 |
---|---|---|
ps aux | 查看所有用戶的所有進程 | USER PID %CPU %MEM VSZ RSS TTY... |
ps ajx | 顯示進程樹關系(含PPID/PGID) | PPID PID PGID SID TTY COMMAND... |
輸出字段解析:
VSZ:虛擬內存大小 (KB)
RSS:實際物理內存 (KB)
TTY:關聯終端(
?
?表示無終端)STAT:進程狀態(后文詳解)
2. 進程狀態(STAT)解碼
狀態碼 | 含義 | 說明 |
---|---|---|
R | 運行中 (Running) | 正在執行或就緒狀態 |
S | 可中斷睡眠 (Sleeping) | 等待事件完成(如 I/O 操作) |
D | 不可中斷睡眠 (Disk sleep) | 通常發生在磁盤 I/O,不可被信號中斷 |
T | 暫停狀態 (Stopped) | 被信號暫停(如?SIGSTOP ) |
Z | 僵尸進程 (Zombie) | 進程已終止,但父進程未回收 |
< | 高優先級進程 | 優先級高于默認值 |
N | 低優先級進程 | 優先級低于默認值 |
s | 會話領導者 (Session leader) | 控制終端的進程 |
+ | 前臺進程組 (Foreground group) | 與終端交互的進程 |
示例:
Ss+
?= 會話領導者 + 可中斷睡眠 + 前臺進程
3. 高級過濾與顯示
參數組合 | 作用 | 示例應用場景 |
---|---|---|
ps -e | grep ssh | 查找特定進程 | 檢查 SSH 服務是否運行 |
ps -fC nginx | 顯示進程完整命令行 (-f ) + 按名稱過濾 | 查看 Nginx 配置參數 |
ps -p 1234 -o pid,ppid,cmd | 自定義輸出字段 | 查看指定進程的父子關系 |
ps --forest | 樹形顯示進程層級 | 分析進程派生關系 |
ps -eo pid,ppid,cmd --sort=-%mem | 按內存排序 | 找出內存消耗最大的進程 |
1.4 獲取進程標識符(代碼解析)
我們可以通過man手冊來查看一下getpid
基本定義
進程ID(PID)
- 定義:進程ID(Process ID)是操作系統分配給每個進程的唯一標識符。它用于區分系統中的不同進程。
- 作用:
資源分配:PID是分配系統資源(如內存、CPU時間等)的重要依據。
進程控制:操作系統可以使用PID來對進程進行操作,例如啟動、停止、暫停或終止進程。
唯一標識:每個進程在系統中都有一個唯一的PID,操作系統通過PID來管理進程。
示例:
cat
進程的PID為3538。
父進程ID(PPID)
定義:父進程ID(Parent Process ID)是指創建當前進程的進程的ID。每個進程都有一個父進程(除了初始進程)。
作用:
進程關系:PPID用于表示進程之間的父子關系。通過PPID,可以追蹤進程的創建過程。
資源繼承:子進程通常會繼承父進程的資源(如文件描述符、環境變量等)。
進程管理:操作系統可以通過PPID來管理進程樹結構,例如在父進程終止時,清理其子進程。
示例:
bash
進程(PPID=2686)創建了cat
進程(PID=3538),因此cat
的PPID為2686。
核心特性
(1) 父子進程關系
創建機制:父進程通過系統調用(如
fork()
)創建子進程。子進程繼承父進程環境,但操作系統為其分配新PID,同時將其PPID設為父進程的PID。
代碼示例(C++):pid_t t = fork(); // 創建子進程 if (t == 0) {// 子進程:getpid()返回自身PID,getppid()返回父進程PIDcout << "子進程 PID:" << getpid() << " PPID:" << getppid() << endl; } else {// 父進程:getpid()返回自身PIDcout << "父進程 PID:" << getpid() << endl; }
關系規則:
- 一個父進程可創建多個子進程,所有子進程共享同一PPID(即父進程PID)。
- 子進程退出后,父進程需回收其資源,否則可能產生僵尸進程。(至于什么是僵尸進程后面章節會講)
(2) 特殊進程
示例:進程表中PID=1的進程PPID為-1。init
進程(PID=1):
系統啟動的第一個進程(Linux中通常為systemd
),是所有用戶進程的最終祖先,其PPID為0或-1(表示無父進程)。內核進程(PID=0):
管理內存交換等核心任務,無PPID。
注意:
數據類型本質:
pid_t
?是一個帶符號整數類型(signed int
),在 Linux 系統中被明確定義為?int
?的別名。
設計目的:
提供進程 ID 的抽象表示,屏蔽不同操作系統(如 Linux/Windows)或硬件架構(32/64 位)的底層差異,增強代碼可移植性。例如:- Linux 使用?
pid_t
?表示 PID,而 Windows 使用?HANDLE
。 - 直接使用?
int
?可能導致平臺兼容性問題。
- Linux 使用?
進程id示例:
我們修改一下之前的代碼
運行結果:
ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288
我是一個進程!我的pid:453288...
運行后我們可以發現可執行程序myprocess的pid是453288,我們還可以來驗證一下
使用ps指令來查看:
ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep
ps ajx
:ps
?是進程查看命令,用于顯示當前系統中的進程快照(非動態更新)。- 選項?
ajx
?指定輸出格式:a
?顯示所有用戶進程,j
?以作業控制格式顯示,x
?包括無終端的進程(如后臺進程)。 - 示例輸出:包含 PID(進程ID)、PPID(父進程ID)、COMMAND(命令名稱)等列。
| head -1
:|
?是管道符,將前一個命令的輸出作為后一個命令的輸入。head -1
?只保留輸出的第一行,即進程信息的表頭(如?PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
)。- 作用:確保后續進程信息有清晰的列名,便于閱讀。
&&
:- 邏輯與運算符,表示只有前一個命令(
ps ajx | head -1
)成功執行(返回狀態碼 0)時,才執行后續命令。 - 這里用于分隔兩個獨立操作:先顯示表頭,再顯示進程詳情。
- 邏輯與運算符,表示只有前一個命令(
ps ajx | grep myprocess
:- 再次運行?
ps ajx
?獲取所有進程信息。 grep myprocess
?過濾出包含關鍵字 "myprocess" 的行(通常是目標進程的命令名稱)。- 問題:
grep
?命令自身在運行時也會被列為進程,且其命令中包含 "myprocess",因此會被錯誤地包含在結果中(例如輸出?grep --color=auto myprocess
)。
- 再次運行?
| grep -v grep
:grep -v
?表示反向過濾,排除包含指定關鍵字 "grep" 的行。- 作用:移除?
grep myprocess
?自身產生的進程條目,避免干擾。例如,如果未加此部分,輸出會多出一行?grep --color=auto myprocess
。
如果我們想殺掉這個進程可以使用快捷鍵CTRL + c(左邊的Shell),或者在右邊的Shell中使用
kill -9 [想要殺掉的pid]
至于kill這個指令的是如何殺掉這個進程的,我們在后面的信號章節也會講到
父進程id示例:
再次修改一下代碼
運行:
ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
我是一個進程!我的pid:451964,我的父進程id:450425
...
運行結果可看到進程pid為451964,父進程ppid為450425
同樣也可以在驗證一下
這里我們可以看到怎么這次的父進程ppid和上次的父進程ppid一樣,都是450425.
我們可以多次運行看一下
父進程id一直不變,這是什么情況呢?
我們也可以用ps來查一下
我們可以看到原來我們的父進程是bash
因為 你每次都是在同一個交互式 Bash 會話里手動啟動程序,所以:
Bash 是 Linux 默認的命令行解釋器(Shell),用戶通過終端輸入命令時,Bash 會創建子進程來執行這些命令
父進程就是當前這個 Bash 進程;
Bash 進程的 PID 在你退出或關閉終端之前不會改變;
于是你看到的 PPID 始終就是 那個 Bash 的 PID(
-bash
或bash
)。
換句話說,只要你不關掉這個終端(或顯式 exit
掉這個 Bash),它就是所有你手動啟動命令的父進程,PPID 自然看起來“不變”。
1.5?fork() 機制深度解析
同樣我們可以使用man手冊來查一下
關鍵特性
一次調用,兩次返回:
父進程返回子進程PID(>0)
子進程返回0
失敗返回-1(如進程數超限)
寫時拷貝(Copy-On-Write):
初始狀態:父子進程共享同一物理內存。
修改觸發:當任一進程嘗試寫入數據時,內核為該進程復制新內存頁。
我們先來修改一下代碼,淺嘗一下fork
運行結果:
fork之后的代碼被執行了兩次,why?
fork: 如何呢?又能怎?
核心原理:一次調用,兩次返回
當程序執行到?fork()
?系統調用時,操作系統會創建一個與原進程(父進程)幾乎完全相同的副本(子進程)。這個副本包括:
代碼段的復制(共享只讀)
數據段和堆棧的復制(寫時拷貝)
程序計數器(PC)位置 -?指向?
fork()
?之后的下一條指令
#include <stdio.h>
#include <unistd.h>int main() {printf("父進程開始運行,pid:%d\n", getpid()); // 步驟1:父進程執行fork(); // 步驟2:分水嶺!// ↓ 步驟3:此處開始有兩個獨立的執行流printf("進程開始運行,pid:%d\n", getpid()); // 步驟4:父子進程各執行一次return 0;
}
執行流程:
關鍵機制解析:
寫時拷貝 (Copy-On-Write):
子進程創建時不立即復制物理內存
父子共享相同物理內存頁(標記為只讀)
當任一進程嘗試寫入內存時,觸發缺頁異常
內核再為該進程復制新的內存頁
程序計數器繼承:
子進程創建時復制父進程的CPU寄存器狀態
包括指向?
fork()
?后下一條指令的EIP寄存器因此子進程從?
fork()
?返回處開始執行
返回值差異:
返回值 含義 執行進程 >0 子進程PID 父進程 0 成功創建標志 子進程 -1 創建失敗 父進程
要是我們想讓父子進程執行不同的代碼邏輯,應該怎么做呢
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("父進程開始運行,pid:%d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){// childwhile(1){printf("我是一個子進程!我的pid:%d,我的父進程id:%d\n", getpid(), getppid());sleep(1); }}else{// fatherwhile(1){printf("我是一個父進程!我的pid:%d,我的父進程id:%d\n", getpid(), getppid());sleep(1); }}//printf("進程開始運行,pid:%d\n", getpid());//while(1)//{// printf("我是一個進程!我的pid:%d\n", getpid());// printf("我是一個進程!我的pid:%d,我的父進程id:%d\n", getpid(), getppid());// sleep(1);//}return 0;
}
修改完代碼我們來運行一下
ltx@hcss-ecs-d90d:~/lesson2$ make
gcc -o myprocess myprocess.c
ltx@hcss-ecs-d90d:~/lesson2$ ./myprocess
父進程開始運行,pid:457987
我是一個父進程!我的pid:457987,我的父進程id:450425
我是一個子進程!我的pid:457988,我的父進程id:457987
我是一個父進程!我的pid:457987,我的父進程id:450425
我是一個子進程!我的pid:457988,我的父進程id:457987
我是一個父進程!我的pid:457987,我的父進程id:450425
我是一個子進程!我的pid:457988,我的父進程id:457987
我是一個父進程!我的pid:457987,我的父進程id:450425
我是一個子進程!我的pid:457988,我的父進程id:457987
我是一個父進程!我的pid:457987,我的父進程id:450425
我是一個子進程!我的pid:457988,我的父進程id:457987
我是一個父進程!我的pid:457987,我的父進程id:450425
我是一個子進程!我的pid:457988,我的父進程id:457987
我是一個父進程!我的pid:457987,我的父進程id:450425...
運行可以看到父進程myprocess可執行程序id為457987,父進程的父進程bash的id為450425,父進程的子進程id為457988。
1.?為什么fork要給父子進程返回不同的值?
這是為了在代碼中區分父子進程的執行路徑,讓程序員能編寫不同的邏輯分支:
設計考量:
父進程需要知道子進程ID:用于后續管理(等待、發送信號等)
子進程需要明確自身身份:避免遞歸創建進程
錯誤處理統一:只有父進程能處理fork失敗
類比:就像雙胞胎出生時獲得不同的名字,雖然基因相同但身份不同
2.?為什么fork會"返回兩次"?
實際上不是函數返回兩次,而是創建了兩個獨立的執行流:
關鍵機制:
調用fork時,內核復制父進程的:
寄存器狀態(包括程序計數器PC)
頁表(通過寫時拷貝)
文件描述符表
在返回用戶空間前,內核修改:
父進程的EAX寄存器 = 子進程PID
子進程的EAX寄存器 = 0
兩個進程從相同的代碼位置繼續執行:
父進程:從fork()調用后繼續
子進程:"誕生"后的第一條指令就是fork()之后的代碼
3.?為什么同一個變量既等于0又大于0?
核心原理:兩個進程擁有獨立的地址空間
int ret = fork(); // 這行代碼在兩個進程中都有!// 內存布局示意:
// 父進程內存空間:ret_addr = 0x1000, 值=457988(子進程PID)
// 子進程內存空間:ret_addr = 0x1000, 值=0
執行過程:
時間線 父進程(PID=457987) 子進程(PID=457988)
---------------------------------------------------------------T1 執行 fork() 系統調用T2 | 內核創建子進程T3 | 設置父進程返回值=457988T4 | 設置子進程返回值=0T5 從fork返回 ↓T6 ret = 457988 (>0) 從"誕生點"開始執行 ↓T7 執行 else 分支 ret = 0 (==0)T8 printf("Parent...") 執行 else if 分支T9 printf("Child...")
注意:
ret
?不是同一個物理內存位置!父子進程有各自獨立的變量副本。
技術本質:寫時拷貝(COW)的作用
int global = 100; // 全局變量pid_t pid = fork();if (pid == 0) {global = 200; // 子進程修改
} else {sleep(1);printf("%d", global); // 父進程仍輸出100
}
內存變化:
fork時:父子共享同一物理內存頁(標記為只讀)
子進程寫global:觸發頁錯誤
內核復制該內存頁給子進程
子進程在新頁上修改值
父進程仍訪問原內存頁
總結要點
進程是資源容器:通過
task_struct
實現資源隔離與調度。fork()是進程分身術:通過寫時拷貝高效復制,雙返回值區分父子。