深入理解進程:從底層原理到嵌入式實戰(3-4 萬字詳解)
前言:為什么硬件開發者必須吃透進程?
作為嵌入式開發者,你可能會說:“我平時用的 RTOS 里只有任務(Task),沒有進程啊!” 但如果你想在珠三角拿到 12k + 的嵌入式開發 offer,尤其是進入智能硬件或汽車電子領域,進程管理是繞不開的硬骨頭 ——
-
智能硬件常需要 Linux 系統跑應用程序,多進程協作是基礎
-
汽車電子的 ECU(電子控制單元)里,RTOS 的任務管理本質是簡化的進程管理
-
面試時,進程相關知識點(如 IPC、調度算法)是大廠必考題
本文將從 “是什么 - 為什么 - 怎么做” 三個維度,用 3-4 萬字的篇幅徹底講透進程。包含 15 + 代碼示例、8 張思維導圖、10 + 實戰案例,保證刷過牛客 100 題的嵌入式開發者都能看懂。
一、進程的本質:從 “死代碼” 到 “活程序” 的蛻變
1.1 程序與進程的核心區別(附實例對比)
很多人搞不清 “程序” 和 “進程” 的區別,我們用一個嵌入式場景舉例:
程序(Program):你寫的led_blink.c
編譯后生成的led_blink.elf
文件,存儲在開發板的 Flash 里,這是靜態的—— 就像一本菜譜,躺在書架上不會自己做菜。
進程(Process):當你在 Linux 開發板上執行./led_blink
,操作系統會把led_blink.elf
加載到內存,分配 CPU 時間、GPIO 資源,讓代碼跑起來 —— 這是動態的,就像廚師按照菜譜實際做菜的過程。
用表格對比關鍵區別:
對比項 | 程序(Program) | 進程(Process) | 嵌入式場景舉例 |
---|---|---|---|
存在形式 | 靜態文件(.elf/.bin) | 動態執行過程 | Flash 里的固件 vs 運行中的固件 |
資源占用 | 不占用 CPU / 內存(僅占磁盤) | 占用 CPU、內存、I/O 資源 | 未運行的 APP vs 后臺運行的 WiFi 服務 |
生命周期 | 永久存在(除非刪除文件) | 有創建、運行、終止的過程 | 下載固件 vs 啟動 / 關閉傳感器服務 |
獨立性 | 無(多個程序可共享文件) | 獨立地址空間、獨立資源 | 多個任務共享 UART vs 進程獨占 SPI |
實戰驗證:在 Linux 開發板上執行ls -l /bin/ls
(查看程序)和ps -ef | grep ls
(查看進程),前者顯示文件屬性,后者顯示運行狀態。
1.2 進程的 “三要素”:程序、數據、PCB
一個進程能跑起來,必須具備三個核心要素:
-
程序段(Code Segment):存放指令,比如
while(1){toggle_led();delay(1000);}
-
數據段(Data Segment):存放變量,比如
int led_state = 0;
(全局變量)、棧上的局部變量 -
進程控制塊(PCB):操作系統管理進程的 “身份證”,記錄進程狀態、資源等信息
用思維導圖展示三者關系:
嵌入式視角:在 STM32 的 FreeRTOS 中,任務控制塊(TCB)就是簡化的 PCB,包含任務棧指針、優先級、狀態等信息,對應的數據結構類似:
// FreeRTOS任務控制塊(簡化版)typedef struct tskTaskControlBlock {  StackType\_t \*pxTopOfStack; // 棧頂指針(對應PCB的CPU上下文)  xListItem xStateListItem; // 狀態鏈表項(對應PCB的狀態)  UBaseType\_t uxPriority; // 優先級(對應PCB的調度信息)  // ... 其他資源信息} TCB\_t;
1.3 進程的 5 個核心特征(附反例說明)
進程有 5 個特征,缺一個都不能叫 “進程”:
-
動態性:能被創建、調度、終止(反例:ROM 里的固化程序,無法動態調度)
舉例:在 Linux 中用
./app &
啟動進程,kill
終止進程,體現動態性。 -
并發性:多個進程可同時存在(反例:單任務單片機程序,一次只能跑一個功能)
舉例:開發板上同時運行
溫度采集進程
和WiFi上傳進程
。 -
獨立性:擁有獨立地址空間(反例:線程,共享進程地址空間)
舉例:一個進程崩潰(如段錯誤),不會影響其他進程。
-
異步性:進程按不可預知的速度推進(反例:實時任務,需嚴格按時間執行)
舉例:兩個進程打印日志,輸出順序可能每次不同。
-
結構性:由程序段、數據段、PCB 組成(反例:裸機程序,沒有 PCB 管理)
舉例:Linux 的
/proc/[pid]/
目錄下的文件,就是進程結構的體現。
面試陷阱:面試官可能問 “線程是否具備這些特征?”—— 線程沒有獨立性(共享地址空間),所以不是進程。
二、進程狀態:從 “就緒” 到 “運行” 的生死輪回
2.1 進程的 5 種基本狀態(附 Linux 實際驗證)
進程在生命周期中會經歷 5 種狀態,我們結合ps
命令的實際輸出理解:
狀態名稱 | 英文標識 | 含義(大白話) | Linux 中查看方式(ps aux) |
---|---|---|---|
創建態 | NEW | 剛被創建,還沒加入就緒隊列 | 一般看不到(持續時間極短) |
就緒態 | READY | 萬事俱備,就等 CPU 時間片 | R(Running 的縮寫,包含就緒) |
運行態 | RUNNING | 正在 CPU 上執行 | R |
阻塞態 | BLOCKED | 等資源(如 I/O),主動放棄 CPU | S(Sleeping)或 D(深度睡眠) |
終止態 | TERMINATED | 已結束,等待回收 PCB | Z(Zombie,僵尸進程) |
實戰操作:在 Linux 開發板上執行:
\# 啟動一個會阻塞的進程(如ping一個不存在的IP)ping 192.168.1.254 &\# 查看狀態(會顯示S,阻塞在網絡I/O)ps aux | grep ping
你會看到ping
進程狀態為S
,表示它因等待網絡響應而阻塞。
2.2 狀態轉換的 6 種場景(附代碼觸發示例)
進程狀態不會憑空變化,每種轉換都有明確的觸發條件。我們用 “嵌入式傳感器采集” 場景舉例:
-
創建態 → 就緒態
觸發:進程創建完成,資源分配完畢。
代碼示例:
\#include \<stdio.h>\#include \<unistd.h>int main() {  pid\_t pid = fork(); // 創建子進程(進入創建態)  if (pid == 0) { // 子進程創建完成,進入就緒態  printf("子進程就緒\n");  }  return 0;}
-
就緒態 → 運行態
觸發:調度器選中該進程,分配 CPU。
場景:就緒隊列中只有你的傳感器進程,調度器會立即讓它運行。
-
運行態 → 就緒態
觸發:時間片用完,或被高優先級進程搶占。
Linux 驗證:
\# 啟動一個占用CPU的進程while true; do :; done &\# 再啟動一個高優先級進程(nice值更小)nice -n -5 ./high\_prio\_app &\# 查看第一個進程會變成就緒態(R,但實際未運行)ps -l
-
運行態 → 阻塞態
觸發:進程請求 I/O(如讀取傳感器數據)。
代碼示例:
// 讀取I2C傳感器(會阻塞等待數據)int fd = open("/dev/i2c-1", O\_RDWR);char data\[10];read(fd, data, 10); // 執行到此處,進程進入阻塞態
-
阻塞態 → 就緒態
觸發:等待的資源到了(如傳感器數據讀取完成)。
原理:I/O 完成后,硬件會產生中斷,內核處理中斷時將進程從阻塞隊列移到就緒隊列。
-
運行態 → 終止態
觸發:進程執行完畢,或被 kill。
代碼示例:
// 正常終止int main() {  printf("任務完成\n");  return 0; // 執行到此處,進程進入終止態}
狀態轉換思維導圖:
2.3 嵌入式 RTOS 中的狀態變種(以 FreeRTOS 為例)
RTOS 的任務狀態是進程狀態的簡化版,但更貼近硬件實際:
FreeRTOS 任務狀態 | 對應進程狀態 | 嵌入式場景舉例 |
---|---|---|
就緒態(Ready) | 就緒態 | 等待調度器分配 CPU 的傳感器任務 |
運行態(Running) | 運行態 | 正在采集溫濕度的任務 |
阻塞態(Blocked) | 阻塞態 | 調用 vTaskDelay () 的延時任務 |
掛起態(Suspended) | 無對應 | 被 vTaskSuspend () 暫停的調試任務 |
關鍵區別:RTOS 沒有 “僵尸態”,任務刪除后資源立即回收(因為嵌入式系統資源有限,不允許浪費)。
代碼對比:
// FreeRTOS任務狀態轉換示例void vSensorTask(void \*pvParameters) {  while(1) {  // 讀取傳感器(可能進入阻塞態)  read\_sensor();     // 延時100ms(主動進入阻塞態)  vTaskDelay(pdMS\_TO\_TICKS(100)); // 對應進程的阻塞態  }}
三、進程控制塊(PCB):進程的 “身份證 + 檔案袋”
3.1 PCB 的作用:操作系統如何 “記住” 進程?
想象一個場景:你正在用開發板調試程序,突然被打斷去接電話,回來后能接著調試 —— 因為你 “記住” 了之前的狀態(斷點位置、變量值)。
操作系統管理進程也是同理,PCB 就是用來 “記住” 進程狀態的結構。沒有 PCB,操作系統就無法管理進程。
具體來說,PCB 的作用有三個:
-
唯一標識:通過 PID 區分不同進程(就像身份證號)。
-
狀態記錄:記錄進程當前狀態(就緒 / 阻塞等),供調度器參考。
-
資源索引:保存進程占用的內存、文件、設備等資源的指針。
類比理解:PCB 就像醫院的病歷卡 —— 每個病人(進程)一張,記錄病情(狀態)、檢查結果(資源),醫生(操作系統)通過病歷卡了解病人情況。
3.2 Linux 內核中的 PCB:task_struct 結構體詳解
Linux 中的 PCB 是task_struct
結構體(定義在linux/sched.h
),包含 300 + 字段,我們挑嵌入式開發者必懂的 10 個字段詳解:
struct task\_struct {  // 1. 進程標識  pid\_t pid; // 進程ID(唯一標識)  pid\_t tgid; // 線程組ID(多線程時用)     // 2. 狀態信息  volatile long state; // 進程狀態(TASK\_RUNNING等)  unsigned int flags; // 進程標志(如PF\_KTHREAD表示內核線程)     // 3. 調度信息  int prio; // 動態優先級  int static\_prio; // 靜態優先級  struct sched\_entity se; // 調度實體(用于CFS調度器)     // 4. 內存信息  struct mm\_struct \*mm; // 內存描述符(用戶空間內存)  struct mm\_struct \*active\_mm;// 活躍內存描述符(內核線程用)     // 5. 上下文信息(CPU寄存器)  struct thread\_struct thread;// 存放寄存器值(切換時保存/恢復)     // 6. 父子關系  struct task\_struct \*parent; // 父進程指針  struct list\_head children; // 子進程鏈表     // 7. 文件信息  struct files\_struct \*files; // 打開的文件列表     // 8. 信號處理  struct signal\_struct \*signal; // 信號描述符  struct sighand\_struct \*sighand; // 信號處理函數     // 9. 時間信息  cputime\_t utime; // 用戶態CPU時間  cputime\_t stime; // 內核態CPU時間     // 10. 其他  struct task\_struct \*real\_parent; // 實際父進程(被領養前)};
關鍵字段解析:
- pid 與 tgid:
-
單進程:pid = tgid
-
多線程:主線程 pid = tgid,子線程 pid 不同但 tgid 相同
-
查看方式:
ps -L -p <pid>
可看到線程的 LWP(輕量級進程 ID,即 pid)
- state:
-
TASK_RUNNING:運行 / 就緒態
-
TASK_INTERRUPTIBLE:可中斷阻塞(如等待鍵盤輸入)
-
TASK_UNINTERRUPTIBLE:不可中斷阻塞(如等待磁盤 I/O,
ps
顯示 D) -
注意:
ps
命令中 R = 運行 / 就緒,S = 可中斷阻塞,D = 不可中斷阻塞
- mm 與 active_mm:
-
用戶進程:mm 指向自己的內存空間
-
內核線程:mm=NULL,active_mm 指向借用的用戶內存
-
嵌入式意義:內核線程不占用用戶內存,適合資源緊張的嵌入式系統
- thread_struct:
-
存放 CPU 寄存器值(如 ARM 的 sp、pc、lr 等)
-
進程切換時,內核會保存當前 thread_struct,加載下一個進程的 thread_struct
-
舉例:當進程因中斷切換時,pc(程序計數器)的值會被保存,恢復時從該地址繼續執行
3.3 PCB 的組織方式:進程鏈表與哈希表
操作系統需要快速找到某個進程的 PCB,Linux 用兩種數據結構組織:
- 雙向循環鏈表:
-
所有 PCB 通過
task_struct
的tasks
字段鏈接成鏈表 -
遍歷所有進程時使用(如
ps aux
命令) -
定義:
struct list_head tasks;
- 哈希表:
-
通過 PID 快速查找 PCB(
pid_hash
數組) -
時間復雜度 O (1),比遍歷鏈表快
-
嵌入式優化:嵌入式 Linux 可能精簡哈希表大小,減少內存占用
圖示:
graph LRsubgraph 進程鏈表A[PCB1(pid=1)] <--> B[PCB2(pid=2)]B <--> C[PCB3(pid=3)]C <--> Aendsubgraph 哈希表(pid_hash)D[哈希桶0] --> AE[哈希桶1] --> BF[哈希桶2] --> Cend
實戰查看:在 Linux 內核源碼中,init_task
是第一個進程(swapper)的 PCB,所有進程都從它衍生:
// 內核啟動時創建的第一個進程struct task\_struct init\_task = INIT\_TASK(init\_task);
四、進程創建:從 fork () 到 exec () 的完整流程
4.1 進程創建的 4 個步驟(附內核源碼分析)
創建進程就像開分店:總店(父進程)復制一套經營模式(代碼),準備新店面(資源),招聘員工(分配 PID),最后開業(加入就緒隊列)。
具體步驟:
- 分配 PID:
-
從
pidmap
位圖中找一個未使用的 PID -
代碼邏輯(簡化):
static int alloc\_pid(struct pid\_namespace \*ns) {  // 遍歷pidmap,找第一個0位  for (i = 0; i < PIDMAP\_ENTRIES; i++) {  if (pidmap\[i].page) {  // 找到空閑PID  return pid;  }  }}
- 復制 PCB:
-
調用
dup_task_struct()
復制父進程的task_struct
-
關鍵操作:分配新的內核棧(
alloc_thread_info
) -
注意:默認不復制用戶內存(用寫時復制技術)
- 初始化新 PCB:
-
修改 PID、狀態等信息(設為 TASK_RUNNING)
-
清空父進程特有的信息(如信號處理、計時器)
-
代碼片段:
p->pid = alloc\_pid(p->nsproxy->pid\_ns);p->state = TASK\_RUNNING;p->parent = current; // current是當前進程(父進程)
- 加入進程隊列:
-
將新 PCB 加入進程鏈表(
list_add(&p->tasks, &init_task.tasks)
) -
加入對應優先級的就緒隊列
-
通知調度器有新進程就緒
4.2 fork () 系統調用:從 “一分為二” 到 “寫時復制”
fork()
是創建進程的 “瑞士軍刀”,我們從用法、原理、優化三個層面解析。
4.2.1 fork () 的基本用法(附嵌入式場景示例)
函數原型:
\#include \<unistd.h>pid\_t fork(void); // 返回值:父進程得到子進程PID,子進程得到0,失敗返回-1
嵌入式場景示例:開發板上同時采集溫濕度和光照數據:
\#include \<stdio.h>\#include \<unistd.h>\#include \<sys/wait.h>// 采集溫度(子進程)void collect\_temperature() {  while(1) {  printf("溫度: 25℃\n");  sleep(2); // 模擬2秒采集一次  }}// 采集光照(父進程)void collect\_light() {  while(1) {  printf("光照: 500lux\n");  sleep(3); // 模擬3秒采集一次  }}int main() {  pid\_t pid = fork();  if (pid < 0) {  perror("fork failed");  return 1;  } else if (pid == 0) {  // 子進程:采集溫度  collect\_temperature();  } else {  // 父進程:采集光照  collect\_light();  // 等待子進程(實際中不會在循環里等)  wait(NULL);  }  return 0;}
運行結果:溫度和光照數據交替打印,實現并行采集。
4.2.2 fork () 的 “寫時復制”(COW)優化
早期的fork()
會完整復制父進程的內存,效率極低(比如父進程有 1GB 內存,復制就要 1GB 空間)。現代操作系統用 “寫時復制” 優化:
-
原理:父子進程共享同一塊物理內存,只有當任一進程修改內存時,才復制被修改的部分(頁)
-
好處:創建進程快(不用復制內存),節省內存(未修改的頁共享)
圖示:
graph TDA[父進程內存] -->|fork()| B[共享物理頁]B --> C[父進程修改頁1]C --> D[復制頁1,父進程用新頁1]B --> E[子進程未修改]E --> F[子進程仍用共享頁]
驗證 COW:在 Linux 上用fork()
創建子進程后,立即查看內存使用(top
命令),會發現父子進程共享大部分內存。
4.2.3 vfork () 與 fork () 的區別(嵌入式必知)
嵌入式系統資源有限,vfork()
比fork()
更輕量,區別如下:
對比項 | fork() | vfork() |
---|---|---|
內存共享 | 寫時復制 | 完全共享(包括棧) |
執行順序 | 父子進程執行順序不確定 | 子進程先執行,父進程阻塞到子進程 exit () |
用途 | 通用進程創建 | 子進程立即調用 exec () 的場景 |
風險 | 低 | 高(子進程修改內存會影響父進程) |
嵌入式使用場景:在內存只有 64MB 的嵌入式設備上,用vfork()
+execve()
啟動新程序,比fork()
節省內存。
代碼示例:
\#include \<stdio.h>\#include \<unistd.h>\#include \<sys/stat.h>\#include \<sys/wait.h>int main() {  pid\_t pid = vfork();  if (pid == 0) {  // 子進程必須調用exec系列函數或exit  execl("/bin/ls", "ls", "-l", NULL);  \_exit(0); // 如果exec失敗,必須exit  } else {  // 父進程在子進程exit或exec后才執行  printf("子進程已執行\n");  wait(NULL);  }  return 0;}
4.3 exec 系列函數:進程 “改頭換面”
fork()
創建的子進程與父進程執行相同代碼,exec
系列函數能讓子進程執行新程序(“換代碼”)。
常用 exec 函數:
函數名 | 功能 | 示例 |
---|---|---|
execl() | 命令行參數列表傳參 | execl(“/bin/ls”, “ls”, “-l”, NULL) |
execv() | 命令行參數數組傳參 | char *argv[] = {“ls”, “-l”, NULL}; execv(“/bin/ls”, argv) |
execlp() | 從 PATH 找程序,不用寫全路徑 | execlp(“ls”, “ls”, “-l”, NULL) |
execvp() | 結合 execv () 和 execlp () 的特點 | char *argv[] = {“ls”, “-l”, NULL}; execvp(“ls”, argv) |
嵌入式場景:父進程監控傳感器,子進程執行不同的處理程序:
\#include \<stdio.h>\#include \<unistd.h>\#include \<sys/wait.h>int main() {  pid\_t pid = fork();  if (pid == 0) {  // 子進程:執行溫度處理程序  execl("./temperature\_handler", "temperature\_handler", "25", NULL);  // 如果exec失敗才會執行下面的代碼  perror("exec failed");  \_exit(1);  } else {  // 父進程:繼續監控  printf("監控中...\n");  wait(NULL);  }  return 0;}
注意:exec
成功后,子進程的代碼、數據會被新程序替換,但 PID 不變(還是原來的子進程)。
五、進程終止與資源回收:避免 “僵尸” 橫行
5.1 進程終止的 3 種方式(附代碼)
進程終止就像 “死亡”,有自然死亡、意外死亡、被殺死三種方式:
- 正常終止(自然死亡):
\#include \<stdio.h>\#include \<stdlib.h> // exit()\#include \<unistd.h> // \_exit()int main() {  printf("正常終止"); // 沒有換行符  exit(0); // 會刷新緩沖區,輸出"正常終止"  // \_exit(0); // 不刷新緩沖區,可能不輸出}
-
從
main()
返回(return 0;
) -
調用
exit()
(會刷新緩沖區) -
調用
_exit()
(不刷新緩沖區,嵌入式常用)
- 異常終止(意外死亡):
int main() {  int a = 1 / 0; // 會產生SIGFPE信號,異常終止  return 0;}
-
除以零、非法內存訪問(段錯誤)
-
收到致命信號(如 SIGSEGV、SIGFPE)
- 被其他進程殺死(他殺):
\#include \<signal.h>\#include \<stdio.h>int main() {  pid\_t pid = fork();  if (pid == 0) {  while(1) sleep(1); // 子進程死循環  } else {  sleep(2);  kill(pid, SIGKILL); // 父進程殺死子進程  }  return 0;}
-
其他進程調用
kill()
發送信號 -
用
kill
命令(如kill -9 <pid>
)
5.2 僵尸進程:是什么、為什么、怎么辦
5.2.1 僵尸進程的產生(附復現代碼)
定義:子進程終止后,PCB 未被回收,變成僵尸進程(Zombie)。
產生原因:父進程未調用wait()
或waitpid()
回收子進程資源。
復現代碼:
\#include \<stdio.h>\#include \<unistd.h>int main() {  pid\_t pid = fork();  if (pid == 0) {  // 子進程:立即終止  printf("子進程終止\n");  \_exit(0);  } else {  // 父進程:不調用wait(),進入死循環  while(1) sleep(1);  }  return 0;}
查看僵尸進程:
\# 編譯運行上述程序后ps aux | grep defunct # defunct表示僵尸進程
會看到子進程狀態為Z+
(Zombie)。
5.2.2 僵尸進程的危害與解決方法
危害:
-
占用 PID(系統 PID 有限,如 32768 個),僵尸太多會導致無法創建新進程
-
占用 PCB 內存(每個 PCB 約 1KB,10 萬個僵尸就占 100MB)
解決方法:
- 父進程主動回收:調用
wait()
或waitpid()
\#include \<stdio.h>\#include \<unistd.h>\#include \<sys/wait.h>int main() {  pid\_t pid = fork();  if (pid == 0) {  \_exit(0);  } else {  int status;  waitpid(pid, \&status, 0); // 等待子進程終止  // 可以通過status獲取子進程退出狀態  if (WIFEXITED(status)) {  printf("子進程正常退出,返回值:%d\n", WEXITSTATUS(status));  }  }  return 0;}
- 父進程忽略 SIGCHLD 信號:
\#include \<signal.h>signal(SIGCHLD, SIG\_IGN); // 告訴內核:子進程終止后自動回收
- 雙重 fork ():讓 init 進程領養孫子進程:
// 父進程 -> 子進程A -> 子進程B// 子進程A創建B后立即退出,B成為孤兒進程被init領養,init會回收B
5.3 孤兒進程:被 “福利院”(init)領養
定義:父進程先于子進程終止,子進程被 init 進程(PID=1)領養。
特點:
-
無害(init 會負責回收)
-
狀態為
S
(就緒 / 阻塞),不是僵尸
復現代碼:
\#include \<stdio.h>\#include \<unistd.h>int main() {  pid\_t pid = fork();  if (pid == 0) {  // 子進程:等待父進程死亡  sleep(2);  // 父進程已死,打印新的父進程PID(應為1)  printf("子進程的新父進程PID:%d\n", getppid());  } else {  // 父進程:立即退出  \_exit(0);  }  return 0;}
運行結果:子進程的新父進程 PID 為 1(init 進程)。
六、進程間通信(IPC):讓進程 “說話”
進程是獨立的,但需要協作(如傳感器進程將數據傳給上傳進程),這就需要 IPC。
6.1 管道(Pipe):最簡單的 “傳話筒”
管道是最古老的 IPC 方式,像一根 “管子”,數據從一端進,另一端出。
6.1.1 匿名管道(父子進程專用)
特點:
-
半雙工(數據單向流動)
-
只能用于有親緣關系的進程(父子、兄弟)
-
基于文件描述符(讀端 fd [0],寫端 fd [1])
代碼示例:父進程給子進程發送傳感器數據
\#include \<stdio.h>\#include \<unistd.h>\#include \<string.h>int main() {  int fd\[2];  // 創建管道  if (pipe(fd) == -1) {  perror("pipe failed");  return 1;  }  pid\_t pid = fork();  if (pid == 0) {  // 子進程:讀數據  close(fd\[1]); // 關閉寫端(只需要讀)  char buf\[100];  read(fd\[0], buf, sizeof(buf));  printf("子進程收到:%s\n", buf);  close(fd\[0]);  } else {  // 父進程:寫數據  close(fd\[0]); // 關閉讀端(只需要寫)  char \*data = "溫度:25℃";  write(fd\[1], data, strlen(data));  close(fd\[1]);  }  return 0;}
注意:
-
管道有緩沖(默認 64KB),滿了會阻塞寫操作
-
讀端關閉后寫操作會產生 SIGPIPE 信號(默認終止進程)
6.1.2 命名管道(FIFO):任意進程通信
特點:
-
有文件名(在文件系統中可見,如
/tmp/myfifo
) -
可用于任意進程(無親緣關系)
-
用法類似匿名管道,但需要先創建
創建 FIFO:
mkfifo /tmp/sensor\_fifo # 命令行創建
或代碼創建:
\#include \<sys/stat.h>mkfifo("/tmp/sensor\_fifo", 0666); // 0666是權限
通信示例:
寫進程(傳感器采集):
\#include \<stdio.h>\#include \<unistd.h>\#include \<fcntl.h>\#include \<string.h>int main() {  int fd = open("/tmp/sensor\_fifo", O\_WRONLY);  char \*data = "光照:500lux";  write(fd, data, strlen(data));  close(fd);  return 0;}
讀進程(數據處理):
\#include \<stdio.h>\#include \<unistd.h>\#include \<fcntl.h>int main() {  int fd = open("/tmp/sensor\_fifo", O\_RDONLY);  char buf\[100];  read(fd, buf, sizeof(buf));  printf("收到:%s\n", buf);  close(fd);  return 0;}
嵌入式應用:在嵌入式 Linux 中,多個進程(如采集、處理、顯示)可通過 FIFO 傳遞數據,無需考慮進程關系。
6.2 信號(Signal):進程間的 “緊急電報”
信號是異步通知機制,像 “發電報” 一樣簡單粗暴,適合傳遞簡單指令(如終止、暫停)。
6.2.1 常見信號及默認行為
信號編號 | 名稱 | 含義 | 默認行為 | 嵌入式場景舉例 |
---|---|---|---|---|
2 | SIGINT | 中斷(Ctrl+C) | 終止進程 | 手動停止調試中的程序 |
9 | SIGKILL | 殺死進程 | 終止進程 | 強制結束無響應的進程 |
11 | SIGSEGV | 段錯誤(非法內存訪問) | 終止 + CoreDump | 程序 bug 導致內存越界 |
17 | SIGCHLD | 子進程終止 | 忽略 | 父進程回收子進程 |
19 | SIGSTOP | 暫停進程 | 暫停進程 | 調試時暫停程序執行 |
查看所有信號:kill -l
6.2.2 發送信號:kill () 函數與 kill 命令
用 kill 命令發送信號:
kill -9 1234 # 給PID=1234的進程發SIGKILLkill -SIGSTOP 1234 # 暫停進程
用 kill () 函數發送信號:
\#include \<signal.h>\#include \<stdio.h>\#include \<unistd.h>int main() {  pid\_t pid = fork();  if (pid == 0) {  while(1) {  printf("運行中...\n");  sleep(1);  }  } else {  sleep(2);  kill(pid, SIGSTOP); // 暫停子進程  sleep(2);  kill(pid, SIGCONT); // 繼續子進程  sleep(2);  kill(pid, SIGKILL); // 殺死子進程  }  return 0;}
6.2.3 捕獲信號:自定義信號處理函數
進程可以自定義信號的處理方式(除 SIGKILL 和 SIGSTOP,這兩個信號不能被捕獲)。
代碼示例:捕獲 SIGINT,實現優雅退出
\#include \<stdio.h>\#include \<signal.h>\#include \<unistd.h>// 信號處理函數void sigint\_handler(int signo) {  if (signo == SIGINT) {  printf("\n收到中斷信號,正在保存數據...\n");  // 保存傳感器數據等清理工作  sleep(1);  printf("數據保存完成,退出\n");  \_exit(0);  }}int main() {  // 注冊信號處理函數  if (signal(SIGINT, sigint\_handler) == SIG\_ERR) {  perror("signal failed");  return 1;  }     // 模擬傳感器采集  while(1) {  printf("采集數據中...\n");  sleep(1);  }  return 0;}
運行:按 Ctrl+C 時,進程會先保存數據再退出,而不是立即終止。
6.3 共享內存:最快的 IPC(嵌入式首選)
共享內存是效率最高的 IPC 方式 —— 數據直接在內存中共享,無需拷貝。
6.3.1 共享內存的使用步驟
-
創建 / 打開共享內存:
shmget()
-
映射到進程地址空間:
shmat()
-
讀寫共享內存:直接操作指針
-
解除映射:
shmdt()
-
刪除共享內存:
shmctl()
代碼示例:
寫進程(傳感器):
\#include \<stdio.h>\#include \<sys/ipc.h>\#include \<sys/shm.h>\#include \<string.h>\#define SHM\_SIZE 1024 // 共享內存大小\#define SHM\_KEY 0x1234 // 共享內存鍵值(唯一標識)int main() {  // 1. 創建共享內存  int shmid = shmget(SHM\_KEY, SHM\_SIZE, IPC\_CREAT | 0666);  if (shmid == -1) {  perror("shmget failed");  return 1;  }  // 2. 映射到地址空間  char \*shmaddr = shmat(shmid, NULL, 0);  if (shmaddr == (void\*)-1) {  perror("shmat failed");  return 1;  }  // 3. 寫數據  strcpy(shmaddr, "溫度:25℃ 濕度:60%");  printf("寫入共享內存: %s\n", shmaddr);  // 等待讀進程讀取  sleep(5);  // 4. 解除映射  shmdt(shmaddr);  // 5. 刪除共享內存(通常由一個進程負責)  shmctl(shmid, IPC\_RMID, NULL);  return 0;}
讀進程(數據處理):
\#include \<stdio.h>\#include \<sys/ipc.h>\#include \<sys/shm.h>\#define SHM\_SIZE 1024\#define SHM\_KEY 0x1234int main() {  // 1. 獲取共享內存(已由寫進程創建)  int shmid = shmget(SHM\_KEY, SHM\_SIZE, 0666);  if (shmid == -1) {  perror("shmget failed");  return 1;  }  // 2. 映射到地址空間  char \*shmaddr = shmat(shmid, NULL, 0);  if (shmaddr == (void\*)-1) {  perror("shmat failed");  return 1;  }  // 3. 讀數據  printf("從共享內存讀取: %s\n", shmaddr);  // 4. 解除映射  shmdt(shmaddr);  return 0;}
6.3.2 共享內存的同步問題(必知)
共享內存不提供同步機制,多進程同時讀寫會導致數據錯亂(如兩個進程同時寫同一位置)。
解決方法:用信號量(Semaphore)同步。
示例:用信號量保護共享內存讀寫:
// 初始化信號量(確保先于共享內存操作)sem\_t \*sem = sem\_open("/sensor\_sem", O\_CREAT, 0666, 1);// 寫共享內存前加鎖sem\_wait(sem);// 寫操作...sem\_post(sem);// 讀共享內存前加鎖sem\_wait(sem);// 讀操作...sem\_post(sem);
嵌入式注意:嵌入式 Linux 可能需要開啟CONFIG_SYSVIPC
配置才能使用共享內存。
6.4 信號量(Semaphore):進程同步的 “紅綠燈”
信號量像 “紅綠燈”,控制進程何時可以訪問共享資源(如共享內存、硬件設備)。
6.4.1 信號量的基本概念
-
計數信號量:值可以是任意非負數,用于控制資源數量(如 3 個串口設備)
-
二元信號量(互斥鎖):值只能是 0 或 1,用于互斥訪問(如同一時間只能一個進程用 SPI 總線)
P 操作(等待):sem_wait()
—— 信號量減 1,若值 < 0 則阻塞
V 操作(釋放):sem_post()
—— 信號量加 1,喚醒阻塞進程
6.4.2 System V 信號量與 POSIX 信號量
Linux 有兩種信號量接口,嵌入式常用 POSIX 信號量(更簡單):
POSIX 信號量示例(互斥訪問 SPI):
\#include \<semaphore.h>\#include \<stdio.h>\#include \<unistd.h>\#include \<pthread.h>sem\_t sem; // 全局信號量// 模擬SPI操作void spi\_operation(int id) {  sem\_wait(\&sem); // P操作:獲取鎖  printf("進程%d開始使用SPI\n", id);  sleep(2); // 模擬SPI操作  printf("進程%d結束使用SPI\n", id);  sem\_post(\&sem); // V操作:釋放鎖}int main() {  // 初始化信號量(1表示互斥鎖)  sem\_init(\&sem, 0, 1); // 第二個參數0表示線程間共享  pid\_t pid = fork();  if (pid == 0) {  spi\_operation(2); // 子進程  } else {  spi\_operation(1); // 父進程  }  // 銷毀信號量  sem\_destroy(\&sem);  return 0;}
運行結果:兩個進程不會同時使用 SPI,體現互斥效果。
6.4.3 信號量解決生產者 - 消費者問題
場景:傳感器(生產者)采集數據到緩沖區,處理程序(消費者)從緩沖區取數據。
代碼示例:
\#include \<semaphore.h>\#include \<stdio.h>\#include \<unistd.h>\#include \<pthread.h>\#define BUFFER\_SIZE 5int buffer\[BUFFER\_SIZE];int in = 0, out = 0;sem\_t empty; // 空緩沖區數量sem\_t full; // 滿緩沖區數量sem\_t mutex; // 互斥鎖// 生產者(傳感器)void \*producer(void \*arg) {  for (int i = 0; i < 10; i++) {  int data = i; // 模擬傳感器數據  sem\_wait(\&empty); // 等空緩沖區  sem\_wait(\&mutex);  buffer\[in] = data;  printf("生產: %d, 位置: %d\n", data, in);  in = (in + 1) % BUFFER\_SIZE;  sem\_post(\&mutex);  sem\_post(\&full); // 滿緩沖區+1  sleep(1); // 模擬采集間隔  }  return NULL;}// 消費者(數據處理)void \*consumer(void \*arg) {  for (int i = 0; i < 10; i++) {  sem\_wait(\&full); // 等滿緩沖區  sem\_wait(\&mutex);  int data = buffer\[out];  printf("消費: %d, 位置: %d\n", data, out);  out = (out + 1) % BUFFER\_SIZE;  sem\_post(\&mutex);  sem\_post(\&empty); // 空緩沖區+1  sleep(2); // 模擬處理時間  }  return NULL;}int main() {  // 初始化信號量  sem\_init(\&empty, 0, BUFFER\_SIZE); // 初始有5個空緩沖區  sem\_init(\&full, 0, 0); // 初始0個滿緩沖區  sem\_init(\&mutex, 0, 1); // 互斥鎖  pthread\_t prod\_tid, cons\_tid;  pthread\_create(\&prod\_tid, NULL, producer, NULL);  pthread\_create(\&cons\_tid, NULL, consumer, NULL);  pthread\_join(prod\_tid, NULL);  pthread\_join(cons\_tid, NULL);  // 清理  sem\_destroy(\&empty);  sem\_destroy(\&full);  sem\_destroy(\&mutex);  return 0;}
運行結果:生產者和消費者交替操作緩沖區,不會出現數據混亂。
七、進程調度:誰先 “上車” 誰說了算
7.1 進程調度的基本概念(嵌入式視角)
進程調度就是 “決定哪個進程先使用 CPU”,像公交車調度 —— 誰先上車、誰后上車,需要規則。
為什么需要調度:
-
CPU 是稀缺資源(通常只有 1-4 核)
-
多個進程需要 “公平” 使用 CPU
-
不同進程有不同需求(如實時進程需要立即響應)
嵌入式調度 vs 通用 OS 調度:
-
嵌入式:強調實時性(如傳感器數據必須 10ms 內處理)
-
通用 OS:強調公平性和交互性(如桌面系統)
7.2 Linux 的 CFS 調度器(完全公平調度)
Linux 采用 CFS(Completely Fair Scheduler)調度器,核心思想是 “讓每個進程獲得公平的 CPU 時間”。
7.2.1 CFS 的基本原理
-
虛擬運行時間:進程實際運行時間按優先級加權后的時間
-
紅黑樹:所有就緒進程按虛擬運行時間排序,每次選虛擬運行時間最小的進程
舉例:
-
高優先級進程的虛擬時間流逝慢(如實際運行 1ms,虛擬時間 + 0.5ms)
-
低優先級進程的虛擬時間流逝快(如實際運行 1ms,虛擬時間 + 2ms)
-
這樣高優先級進程能獲得更多實際 CPU 時間
7.2.2 進程優先級與 nice 值
Linux 用 nice 值表示進程優先級:
-
范圍:-20(最高優先級)~ 19(最低優先級)
-
默認值:0
-
調整優先級:
nice -n <值> 命令
或renice <值> -p <pid>
查看進程 nice 值:ps -l
(NI 列)
嵌入式應用:在嵌入式系統中,可將實時任務的 nice 值設為 - 20,確保優先執行。
7.3 實時調度策略(嵌入式必備)
嵌入式系統常需要實時調度(如汽車的剎車控制必須立即響應),Linux 提供兩種實時調度策略:
- SCHED_FIFO:
-
先進先出,一旦獲得 CPU 就一直運行,直到主動放棄或被更高優先級進程搶占
-
適合短時間運行的實時任務(如傳感器數據處理)
- SCHED_RR:
-
時間片輪轉,相同優先級的進程輪流執行
-
適合需要定期執行的任務(如 10ms 一次的電機控制)
設置實時調度策略:
\#include \<stdio.h>\#include \<sched.h>int main() {  struct sched\_param param;  param.sched\_priority = 50; // 優先級(1-99,值越大優先級越高)  // 設置SCHED\_FIFO調度策略  if (sched\_setscheduler(0, SCHED\_FIFO, \¶m) == -1) {  perror("sched\_setscheduler failed");  return 1;  }  // 實時任務...  return 0;}
注意:需要 root 權限才能設置實時優先級,嵌入式系統中通常會開啟相關配置。
7.4 嵌入式 RTOS 的調度器(以 FreeRTOS 為例)
FreeRTOS 的調度器比 Linux 簡單,適合資源有限的嵌入式系統:
-
搶占式調度:高優先級任務可立即搶占低優先級任務
-
時間片調度:相同優先級任務輪流執行(可配置)
代碼示例:
// 高優先級任務(傳感器數據處理)void vHighPriorityTask(void \*pvParameters) {  while(1) {  // 處理數據(必須快速完成)  vTaskDelay(pdMS\_TO\_TICKS(10));  }}// 低優先級任務(日志打印)void vLowPriorityTask(void \*pvParameters) {  while(1) {  // 打印日志(可延遲)  vTaskDelay(pdMS\_TO\_TICKS(100));  }}int main() {  // 創建任務,高優先級任務優先執行  xTaskCreate(vHighPriorityTask, "HighTask", 128, NULL, 2, NULL);  xTaskCreate(vLowPriorityTask, "LowTask", 128, NULL, 1, NULL);  vTaskStartScheduler(); // 啟動調度器  return 0;}
關鍵區別:FreeRTOS 的任務切換開銷小(約幾微秒),適合微控制器(如 STM32),而 Linux 調度切換開銷大(約幾十微秒)。
八、實戰:用進程知識解決嵌入式實際問題
8.1 案例 1:嵌入式設備的多進程架構設計
以 “智能溫濕度傳感器” 為例,設計多進程架構:
進程名稱 | 功能 | 優先級 | IPC 方式 |
---|---|---|---|
采集進程 | 讀取溫濕度傳感器 | 高 | 共享內存 |
處理進程 | 數據校準、轉換 | 中 | 共享內存 + 信號量 |
上傳進程 | WiFi 上傳數據 | 低 | 管道 |
日志進程 | 記錄系統日志 | 最低 | 命名管道 |
優勢:
-
模塊化(一個進程出問題不影響其他進程)
-
可獨立升級(如只更新上傳進程支持新協議)
-
方便調試(可單獨重啟某個進程)
8.2 案例 2:解決傳感器數據丟失問題
問題:傳感器數據采集快(10ms 一次),但上傳慢(100ms 一次),導致數據丟失。
分析:采集進程和上傳進程速度不匹配,沒有緩沖機制。
解決方案:用共享內存 + 信號量實現環形緩沖區:
-
采集進程:將數據寫入環形緩沖區,信號量計數 + 1
-
上傳進程:從緩沖區讀數據,信號量計數 - 1
-
緩沖區滿時,采集進程可選擇覆蓋舊數據或等待
核心代碼:參考 6.4.3 的生產者 - 消費者模型,將緩沖區改為環形。
8.3 案例 3:調試進程相關問題的工具
工具 | 用途 | 嵌入式場景示例 |
---|---|---|
ps | 查看進程狀態 | 檢查是否有僵尸進程(Z 狀態) |
top/htop | 實時查看進程 CPU / 內存使用 | 發現 CPU 占用 100% 的異常進程 |
pstree | 查看進程樹(父子關系) | 找到某個進程的父進程 |
strace | 跟蹤進程系統調用 | 調試進程為何無法打開設備文件 |
gdb attach | 調試運行中的進程 | 在不重啟的情況下調試上傳進程 |
調試示例:用strace
查看進程為何無法讀取傳感器:
strace -f ./sensor\_collect # -f跟蹤子進程
會輸出所有系統調用,若看到open("/dev/i2c-1", O_RDWR) = -1 ENOENT
,說明設備文件不存在。
九、總結:進程知識體系與面試重點
9.1 進程知識體系思維導圖
9.2 面試高頻問題與答案要點
- 進程與線程的區別?
-
進程:資源分配單位,有獨立地址空間
-
線程:調度單位,共享進程資源
-
開銷:進程創建 / 切換開銷大,線程小
- 僵尸進程產生原因及解決方法?
-
原因:子進程終止后父進程未回收 PCB
-
解決:wait ()/waitpid ()、忽略 SIGCHLD、雙重 fork ()
- 什么是寫時復制?為什么用它?
-
原理:fork () 后父子進程共享內存,修改時才復制
-
好處:加快進程創建速度,節省內存
- 進程間通信方式及優缺點?
-
管道:簡單,僅限親緣進程
-
共享內存:最快,需同步
-
信號量:用于同步,不傳遞數據
-
信號:異步,適合簡單通知
- 實時調度與普通調度的區別?
-
實時:優先保證響應時間(如 SCHED_FIFO)
-
普通:優先保證公平性(如 CFS)
9.3 下一步學習建議
-
動手實踐:用本文代碼在開發板上實際運行,觀察進程行為
-
閱讀源碼:看 FreeRTOS 的任務調度器源碼(理解簡化版進程管理)
-
項目實戰:實現一個多進程的嵌入式應用(如智能家居網關)
-
深入內核:學習 Linux 內核進程調度和 IPC 的實現細節
掌握進程知識,不僅能通過面試,更能設計出穩定、高效的嵌入式系統。記住:最好的學習方法是 “用起來”—— 在實際項目中遇到問題、解決問題,才能真正理解進程的精髓。
(注:文檔部分內容可能由 AI 生成)