文章目錄
- 一、Linux進程信號核心概念
- 1.1 信號本質
- 1.2 關鍵術語
- 1.3 Linux 信號機制的核心流程:
- 二、信號產生機制全景
- 2.1 通過終端按鍵產生信號
- 2.1.1 基本操作
- 2.2 調用系統命令向進程發信號
- 2.2.1 kill 命令:向指定進程發送信號
- 2.2.2 killall 命令:按進程名發送信號
- 2.2.3 pkill 命令:按進程名或屬性發送信號
- 2.2.4 發送信號的實際場景
- 2.2.5 查看信號列表
- 2.3 使用函數產生信號
- 2.3.1 kill
- 2.3.2 raise() 函數:向自身發送信號
- 2.3.3 sigqueue() 函數:發送帶數據的信號(實時信號)
- 2.3.4 信號發送的錯誤處理與注意事項
- 2.3.5 總結
- 2.4 由軟件條件產生信號
- 2.4.1 alarm
- 2.4.2 如何簡單快速理解系統鬧鐘
- 2.5 硬件異常產生信號
- 2.5.1 常見的硬件異常信號
- 2.5.2 硬件異常的處理流程
- 2.5.3 調試硬件異常信號
- 三、保存信號
- 3.1 信號其他相關常見概念
- 3.2 在內核中的表示
- 3.3 sigset_t(信號集)
- 3.4 信號集操作函數
- 3.4.1 sigprocmask
- 總結
- 3.4.2 sigpending
- 四、捕獲信號
- 4.1 信號捕捉的流程
- 4.2 sigaction
- 4.3 操作系統是怎么運行的
- 4.3.1 硬件中斷
- 4.3.2 時鐘中斷
- 4.3.3 死循環
- 4.3.4 軟中斷
- 4.3.5 缺頁中斷?內存碎片處理?除零野指針錯誤?
- 4.4 如何理解內核態和用戶態
- 五、可重入函數
- 5.1 什么是可重入函數?
- 5.2 不可重入的典型場景與風險
- 5.3 可重入函數的設計原則
- 5.4 可重入函數的實現示例
- 5.5 五、信號處理中的可重入性
- 六、volatile
- 6.1 volatile 的本質與作用
- 6.2 信號處理函數中的全局變量
- 七、SIGCHLD 信號:Linux 進程管理的 “子進程通知機制”
- 7.1 SIGCHLD 信號的本質與作用
- 7.2 SIGCHLD 的默認行為與問題
- 7.3 處理 SIGCHLD 的三種方式
- 7.4 調試與監控
- 7.5 常見誤區與注意事項
- 7.6 總結
一、Linux進程信號核心概念
1.1 信號本質
* 異步通信機制:事件驅動的進程間通知
* 信號類型:預定義整數(1-31為常規信號,34+為實時信號)
* 生命周期:產生 → 保存 → 處理
1.2 關鍵術語
術語 | 描述 | 內核表示 |
---|---|---|
遞達(Delivery) | 信號實際處理過程 | task_struct->ksigaction |
未決(Pending) | 信號產生到遞達間的狀態 | task_struct->signal->pending |
阻塞(Block) | 進程主動屏蔽的信號 | task_struct->blocked 位圖 |
1.3 Linux 信號機制的核心流程:
信號產生 — 信號保存 — 信號處理
二、信號產生機制全景
2.1 通過終端按鍵產生信號
2.1.1 基本操作
Ctrl + C
(SIGINT
)
向當前正在運行的前臺進程發送中斷信號,使進程立即終止運行。不過,若進程對SIGINT
信號進行了特殊處理,如捕獲并忽略該信號,那么按下Ctrl + C
可能無法終止進程。同時,Ctrl + C
僅對前臺進程有效,后臺進程不會受其影響Ctrl + \
(SIGOUT
)
不僅會終止進程,還會讓進程生成 核心轉儲文件(core dump),用于調試程序崩潰問題Ctrl + Z
(SIGSTP
)
將當前前臺進程暫停(掛起) 并放入后臺,使其暫時停止運行但不終止。
2.2 調用系統命令向進程發信號
2.2.1 kill 命令:向指定進程發送信號
基本語法:
kill [-信號名稱/編號] <進程ID>
常用信號選項:
-9
或-SIGKILL
:強制終止進程(不可被捕獲或忽略)。
-15
或-SIGTERM
:正常終止進程(默認選項,可被捕獲并執行清理)。
-1
或-SIGHUP
:重新加載配置(常用于守護進程,如nginx
)。
-2
或-SIGINT
:中斷進程(等價于Ctrl + C
)。
-3
或-SIGQUIT
:終止進程并生成core
文件(等價于Ctrl + \
)。
-19
或-SIGSTOP
:暫停進程(等價于Ctrl + Z
,不可被忽略)。
-18
或-SIGCONT
:恢復被暫停的進程。
示例:
# 正常終止進程(先嘗試清理)
kill 1234# 強制終止進程(不執行清理)
kill -9 1234# 向多個進程發送信號
kill -15 1234 5678 9012# 發送自定義信號(如 SIGUSR1,編號 10)
kill -10 1234
2.2.2 killall 命令:按進程名發送信號
基本語法:
killall [-信號名稱/編號] <進程名>
示例:
# 終止所有名為 "nginx" 的進程
killall nginx# 強制終止所有名為 "cpp" 的進程
killall -9 cpp# 重新加載所有名為 "httpd" 的進程的配置
killall -HUP httpd
2.2.3 pkill 命令:按進程名或屬性發送信號
基本語法:
pkill [-信號名稱/編號] [-選項] <匹配模式>
常用選項:
-u <用戶>
:按用戶名篩選進程。-t <終端>
:按終端會話篩選進程。-f
:匹配進程全名(包括命令行參數)。
示例:
# 終止用戶 "test" 運行的所有 "bash" 進程
pkill -u test bash# 暫停當前終端的所有 "vim" 進程
pkill -STOP -t pts/0 vim# 終止包含 "python script.py" 的進程
pkill -f "python script.py"
2.2.4 發送信號的實際場景
優雅重啟服務
# 重新加載 Nginx 配置(不中斷現有連接)
kill -HUP $(cat /run/nginx.pid)
批量管理進程
# 暫停所有用戶 "alice" 的進程
pkill -STOP -u alice# 恢復所有被暫停的進程
pkill -CONT -u alice
終止頑固進程
# 先嘗試正常終止(給進程清理資源的機會)
kill 1234# 若 5 秒后仍未終止,強制殺死
sleep 5 && kill -9 1234
2.2.5 查看信號列表
通過 kill -l
命令可查看系統支持的所有信號:
編號34以上的是實時信號,本章只討論編號34以下的信號,不討論實時信號。這些信號各自在什么條件下產生,默認的處理動作是什么,在signal(7)
中都有詳細說明:man 7 signal
2.3 使用函數產生信號
2.3.1 kill
函數原型:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
參數說明:
pid
:目標進程 ID(pid > 0
),或特殊值:pid = 0
:向當前進程組的所有進程發送信號。pid = -1
:向所有有權限發送的進程發送信號。sig
:要發送的信號(如SIGINT
、SIGKILL
,或自定義信號如SIGUSR1
)。
返回值:
- 成功返回
0
,失敗返回-1
(錯誤原因可通過errno
獲取)。
代碼示例:向指定進程發送 SIGTERM 信號
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {if (argc != 3) {printf("用法: %s <進程ID> <信號編號>\n", argv[0]);return 1;}pid_t target_pid = atoi(argv[1]);int signal_num = atoi(argv[2]);if (kill(target_pid, signal_num) == -1) {perror("kill 失敗");printf("錯誤碼: %d, 錯誤信息: %s\n", errno, strerror(errno));return 1;}printf("已向進程 %d 發送信號 %d\n", target_pid, signal_num);return 0;
}
編譯與使用:
gcc -o kill_demo kill_demo.c
# 向進程1234發送SIGTERM(信號15)
./kill_demo 1234 15
2.3.2 raise() 函數:向自身發送信號
函數原型:
#include <signal.h>
int raise(int sig);
參數說明:
sig
:要發送的信號(等價于kill(getpid(), sig)
)。
返回值:
- 成功返回
0
,失敗返回非零值。
代碼示例:程序自中斷(等價于 Ctrl + C)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void sigint_handler(int sig) {printf("捕獲到SIGINT信號,程序即將退出\n");exit(0); // 調用 exit 終止進程
}int main() {// 注冊SIGINT信號處理函數signal(SIGINT, sigint_handler);printf("程序運行中,3秒后自發送SIGINT信號...\n");sleep(3);// 向自身發送SIGINT信號raise(SIGINT);printf("該語句不會執行,因為進程已處理信號并退出\n");return 0;
}
2.3.3 sigqueue() 函數:發送帶數據的信號(實時信號)
函數原型:
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
參數說明:
pid
:目標進程 ID。sig
:要發送的信號(推薦使用實時信號,如SIGRTMIN + n
)。value
:包含整數或指針數據的聯合體,可隨信號傳遞給目標進程。
返回值:
- 成功返回
0
,失敗返回-1
。
代碼示例:發送帶數據的實時信號
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>// 目標進程(接收信號)
void target_process() {// 注冊信號處理函數struct sigaction sa;memset(&sa, 0, sizeof(sa));sa.sa_flags = SA_SIGINFO; // 支持接收信號附帶的數據sa.sa_sigaction = [](int sig, siginfo_t *info, void *context) {if (sig == SIGRTMIN) {printf("接收實時信號SIGRTMIN,附帶數據:%d\n", info->si_int);}};sigaction(SIGRTMIN, &sa, NULL);printf("目標進程運行中,等待信號...\n");while (1) sleep(1);
}// 發送信號的進程
void sender_process(pid_t target_pid) {union sigval value;value.sival_int = 100; // 附帶整數數據if (sigqueue(target_pid, SIGRTMIN, value) == -1) {perror("sigqueue 失敗");exit(1);}printf("已向進程 %d 發送帶數據的SIGRTMIN信號\n", target_pid);
}int main(int argc, char *argv[]) {if (argc != 2) {printf("用法: %s <0(目標)/1(發送者)>\n", argv[0]);return 1;}int mode = atoi(argv[1]);if (mode == 0) {target_process();} else if (mode == 1) {pid_t target_pid = 1234; // 替換為實際目標進程IDsender_process(target_pid);} else {printf("模式錯誤,需輸入0或1\n");}return 0;
}
2.3.4 信號發送的錯誤處理與注意事項
常見錯誤:
- **權限不足:**普通用戶只能向自己的進程發送信號,向其他用戶進程發送信號需 root 權限。
- **進程不存在:**目標進程已終止或 PID 錯誤時,
kill
會返回ESRCH
錯誤。 - **信號被阻塞:**目標進程若阻塞了該信號,信號會暫存直至阻塞解除。
推薦方式:
- 先檢查進程是否存在:使用
kill(pid, 0)
可在不發送信號的情況下檢查進程是否存在(sig=0
為 “空信號”)。 - 區分信號類型:
- 非實時信號(如
SIGINT
):若多次發送且未處理,僅保留最后一次。 - 實時信號(如
SIGRTMIN
):會排隊等待處理,適合需要可靠傳遞的場景。
- 非實時信號(如
- **避免濫用
SIGKILL
:**優先使用SIGTERM
讓進程優雅退出,僅在必要時使用SIGKILL
。
2.3.5 總結
函數 | 用途 | 核心參數 | 適用場景 |
---|---|---|---|
kill() | 向任意進程發送信號 | pid (進程 ID)、sig (信號) | 進程控制、常規信號發送 |
raise() | 向自身發送信號 | sig (信號) | 程序自中斷、自定義退出 |
sigqueue() | 發送帶數據的實時信號 | pid 、sig 、value (數據) | 進程間通信、需傳遞數據場景 |
2.4 由軟件條件產生信號
常見的軟件信號
- SIGALRM(鬧鐘信號)
- 觸發條件:通過
alarm()
或setitimer()
函數設置的定時器到期。 - 應用場景:實現超時控制、周期性任務(如心跳檢測)。
- 觸發條件:通過
- SIGUSR1/SIGUSR2(用戶自定義信號)
- 觸發條件:通過
kill()
、raise()
或sigqueue()
函數手動發送。 - 應用場景:進程間自定義通信(如通知配置更新、優雅重啟)。
- 觸發條件:通過
- SIGPIPE(管道破裂信號)
- 觸發條件:向已關閉的管道或套接字寫入數據。
- 應用場景:網絡編程中檢測連接狀態。
- SIGALRM/SIGVTALRM(虛擬定時器信號)
觸發條件:通過setitimer()設置的用戶態或內核態 CPU 時間到期。
應用場景:性能分析、CPU 時間統計。
SIGPIPE
是一種由軟件條件產生的信號,在“管道”中已經介紹過了。本節主要介紹alarm
函數和SIGALRM
信號。
2.4.1 alarm
- 基本功能與原型
函數原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
參數說明:
seconds
:設置的定時器秒數。seconds = 0
:取消之前設置的鬧鐘。seconds > 0
:在seconds
秒后觸發SIGALRM
信號。
返回值:
- 返回之前設置的鬧鐘剩余秒數(若之前未設置,則返回
0
)。
- 默認行為與信號處理
- 默認行為: 當定時器到期時,進程會收到
SIGALRM
信號,默認行為是終止進程。 - 自定義處理: 可通過
signal()
或sigaction()
注冊信號處理函數,避免進程被終止。
- 應用場景
- 超時控制: 例如等待用戶輸入或網絡請求時設置超時。
- 周期性任務: 結合信號處理實現簡單的定時器。
- 資源監控: 定時檢查系統資源使用情況。
- 代碼示例
示例 1:基本超時控制
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>void timeout_handler(int sig) {printf("超時!程序已運行超過5秒\n");exit(1);
}int main() {// 注冊SIGALRM信號處理函數signal(SIGALRM, timeout_handler);// 設置5秒后觸發SIGALRM信號alarm(5);printf("程序開始運行,等待5秒...\n");sleep(10); // 嘗試休眠10秒,但會在5秒后被中斷printf("該語句不會執行,因為進程已被信號中斷\n");return 0;
}
示例 2:非阻塞超時讀取用戶輸入
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>volatile int input_received = 0;void alarm_handler(int sig) {printf("\n超時!請加快輸入\n");input_received = 1;
}int main() {char buffer[100];// 注冊信號處理函數signal(SIGALRM, alarm_handler);// 設置3秒超時alarm(3);printf("請在3秒內輸入內容:");fgets(buffer, sizeof(buffer), stdin);// 取消鬧鐘(如果用戶在超時前輸入)alarm(0);if (!input_received) {printf("你輸入了:%s", buffer);}return 0;
}
示例 3:實現周期性任務
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void periodic_task(int sig) {printf("執行周期性任務:每秒打印一次\n");alarm(1); // 重新設置1秒后觸發
}int main() {// 注冊信號處理函數signal(SIGALRM, periodic_task);// 啟動第一個鬧鐘alarm(1);printf("程序運行中,按Ctrl+C終止...\n");while (1) {// 主循環保持程序運行pause(); // 等待信號}return 0;
}
- 注意事項
- 每個進程只能有一個鬧鐘:多次調用
alarm()
會覆蓋之前的設置。 - 時間精度有限:
alarm()
基于秒級計時,不適合毫秒級精度場景。 - 信號處理函數應簡潔:避免在信號處理函數中執行復雜操作,可能導致重入問題。
- 與 sleep() 沖突:
alarm()
會中斷sleep()
、pause()
等系統調用。
- 每個進程只能有一個鬧鐘:多次調用
2.4.2 如何簡單快速理解系統鬧鐘
系統鬧鐘,其實本質是OS必須自身具有定時功能,并能讓用戶設置這種定時功能,才可能實現鬧鐘這樣的技術。
現代Linux是提供了定時功能的,定時器也要被管理:先描述,在組織。內核中的定時器數據結構是:
#include <linux/timer.h>struct timer_list {struct list_head entry; // 內核鏈表結構unsigned long expires; // 到期時間(jiffies)struct tvec_base *base; // 內部使用的定時器基數void (*function)(unsigned long); // 回調函數unsigned long data; // 傳遞給回調函數的參數int slack; // 定時器執行的松弛時間// ...其他字段(內核版本不同可能有差異)
};
操作系統管理定時器,采用的是時間輪的做法。
核心原理:將時間劃分為固定槽位,每個槽存儲到期時間相同的定時器。指針隨時間移動,到期時觸發對應槽的定時器。
這里就簡單提一下,感興趣的自己去了解一下。如果難以理解,你就將它理解為一個時間軸,誰的過期時間更近那么就先調度誰。
2.5 硬件異常產生信號
2.5.1 常見的硬件異常信號
SIGSEGV(段錯誤,信號 11)
- 觸發原因: 進程訪問未分配給它的內存(如空指針解引用、數組越界)。
- 硬件機制: MMU(內存管理單元)檢測到非法地址,觸發頁錯誤(Page Fault)。
- 示例場景:
int *ptr = NULL;
*ptr = 10; // 觸發SIGSEGV
SIGFPE(浮點異常,信號 8)
- 觸發原因: 數學運算錯誤(如除零、溢出)。
- 硬件機制: CPU 的浮點運算單元(FPU)檢測到錯誤。
- 示例場景:
int a = 1 / 0; // 觸發SIGFPE(整數除零)
double b = 1.0 / 0.0; // 可能觸發(取決于編譯器和硬件)
SIGILL(非法指令,信號 4)
- 觸發原因: CPU 執行了無效指令(如未實現的指令、錯誤的操作碼)。
- 硬件機制: 指令解碼器檢測到非法指令。
- 示例場景:
// 手動構造非法指令(示例僅示意,實際不可執行)
unsigned char code[] = {0xFF, 0xFF, 0xFF}; // 無效操作碼
((void (*)())code)(); // 觸發SIGILL
SIGBUS(總線錯誤,信號 7)
- 觸發原因: 硬件訪問錯誤(如未對齊內存訪問、物理內存損壞)。
- 硬件機制: 內存總線檢測到錯誤。
- 示例場景:
// 在某些架構上,訪問未對齊的內存可能觸發SIGBUS
struct {int a;char b;
} __attribute__((packed)) s;
int *p = (int*)&s.b; // 未對齊的指針
*p = 10; // 可能觸發SIGBUS
2.5.2 硬件異常的處理流程
- 異常發生:CPU 執行指令時檢測到錯誤(如除零、無效內存訪問)。
- 硬件中斷:CPU 切換到內核模式,執行對應的中斷處理程序。
- 信號生成:內核識別異常類型,構造對應的信號(如
SIGSEGV
)。 - 信號傳遞:內核將信號添加到目標進程的未決信號隊列。
- 進程響應:
- 默認行為:終止進程,生成核心轉儲文件(
core dump
)。 - 自定義處理:若進程通過
signal()
或sigaction()
注冊了處理函數,則執行該函數。
- 默認行為:終止進程,生成核心轉儲文件(
2.5.3 調試硬件異常信號
子進程退出 core dump
核心轉儲文件(core dump)
- 作用:保存進程崩潰時的內存狀態,用于事后分析。
- 啟用方法:
ulimit -c unlimited # 允許生成core文件
- 分析工具:
gdb ./program core # 用GDB加載程序和core文件
GDB 調試技巧
# 設置信號處理方式(捕獲但不終止)
(gdb) handle SIGSEGV nostop print# 運行程序直到崩潰
(gdb) run# 查看堆棧跟蹤
(gdb) backtrace# 查看變量值
(gdb) print variable
三、保存信號
3.1 信號其他相關常見概念
- 實際執行信號的處理動作稱為信號遞達(Delivery)
- 信號從產生到遞達之間的狀態,稱為信號未決(Pending)。
- 進程可以選擇阻塞(Block)某個信號。
- 被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。
- 注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
3.2 在內核中的表示
示意圖:
task_struct 中的信號字段
每個進程的描述符(task_struct
)包含以下信號相關字段:
struct task_struct {// 信號掩碼(當前阻塞的信號)sigset_t blocked;// 未決信號(pending)struct signal_struct *signal;// 信號處理函數表struct k_sigaction ksigaction[_NSIG];// 其他字段...
};
signal_struct 結構
struct signal_struct {atomic_t count; // 引用計數struct sigpending pending; // 未決信號隊列spinlock_t siglock; // 保護鎖struct sigaction action[_NSIG]; // 用戶空間信號處理函數// 其他字段...
};
sigpending 結構
struct sigpending {struct list_head list; // 未決信號鏈表sigset_t signal; // 未決信號位圖
};
信號處理流程
- 信號產生:內核 / 其他進程通過系統調用(如
kill()
)發送信號,標記pending
對應位為1
。 - 檢查阻塞:進程調度或從內核態返回用戶態時,檢查
block
,若信號被阻塞(block=1
),則跳過處理,維持pending=1
;若未阻塞(block=0
),進入下一步。 - 執行處理動作:根據 handler 配置,執行默認動作(
SIG_DFL
)、忽略(SIG_IGN
)或自定義函數(sighandler
),處理后清零pending
對應位。
3.3 sigset_t(信號集)
sigset_t
本質上是一個 位圖(Bitmap),每個位對應一個信號編號:
- 位數:通常為 64 位(對應 64 個信號)。
- 實現:內核中定義為
unsigned long
數組:
typedef struct {unsigned long sig[_NSIG_WORDS]; // _NSIG_WORDS 通常為 2(64位系統)
} sigset_t;
信號表示
- 若信號集中包含信號
sig
,則對應位被置為1
。 - 例如:信號集包含
SIGINT(2)
,則第 2 位為1
。
3.4 信號集操作函數
初始化與修改
#include <signal.h>// 清空信號集(所有位設為 0)
int sigemptyset(sigset_t *set);// 填充信號集(所有位設為 1)
int sigfillset(sigset_t *set);// 添加信號到集合
int sigaddset(sigset_t *set, int signum);// 從集合中移除信號
int sigdelset(sigset_t *set, int signum);// 檢查信號是否在集合中
int sigismember(const sigset_t *set, int signum);
信號掩碼操作
// 設置當前進程的信號掩碼(阻塞/解除阻塞信號)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
參數:SIG_BLOCK
:添加set
中的信號到當前掩碼(阻塞這些信號)。SIG_UNBLOCK
:從當前掩碼中移除set
中的信號(解除阻塞)。SIG_SETMASK
:用set
替換當前掩碼。
3.4.1 sigprocmask
函數原型
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
參數說明
how
:操作類型,可選值:SIG_BLOCK
:將set
中的信號添加到當前掩碼(阻塞這些信號)。SIG_UNBLOCK
:從當前掩碼中移除set
中的信號(解除阻塞)。SIG_SETMASK
:用set
完全替換當前掩碼。
set
:信號集指針,指定要操作的信號。若為NULL
,則不修改掩碼,僅獲取當前掩碼到oldset
。oldset
:用于保存修改前的信號掩碼(若不為NULL
),可用于后續恢復。
返回值
- 成功:返回
0
。 - 失敗:返回
-1
,并設置errno
(如EFAULT
、EINVAL
)。
信號掩碼與未決信號
信號掩碼(Signal Mask)
- 本質是一個位圖,每位對應一個信號(如
SIGINT
、SIGTERM
)。 - 被掩碼標記的信號不會被進程接收,而是進入 未決狀態(
Pending
)。
未決信號(Pending Signals)
- 已產生但被阻塞的信號會暫存到進程的未決隊列。
- 掩碼解除后,未決信號會被立即處理(非實時信號可能合并,實時信號支持排隊)。
總結
pending信號集記錄已生成但未處理的信號,阻塞信號集決定哪些信號會被延遲處理。當信號產生時:
- 若阻塞信號集對應位為
0
(未阻塞),無論pending集
狀態如何,信號都會被立即遞送- 若阻塞信號集對應位為
1
(阻塞),信號會被加入pending集
(pending位設為1),保持未決狀態當進程通過
sigprocmask()
解除信號阻塞(將阻塞位設為0)后:
- 內核檢查
pending集
- 若對應信號位為
1
(存在未決信號)- 在下次進程從內核態返回用戶態的執行上下文中
- 該信號會被遞送處理
關鍵點說明
- 遞送時機:信號處理發生在進程從內核態返回用戶態時,這是Linux信號設計的核心機制
- 系統調用返回時
- 硬件中斷處理完成后
- 進程上下文切換時
- 特殊情形:
- 連續多次阻塞同一信號:只有第一次會進入pending(標準信號)
SIGKILL
和SIGSTOP
不能被阻塞- 實時信號(RT信號)會排隊,不丟失(FIFO)
3.4.2 sigpending
函數原型
#include <signal.h>int sigpending(sigset_t *set);
參數
set
:指向sigset_t
類型的指針,用于存儲當前未決信號集合。
返回值:
- 成功:返回
0
,并將未決信號集復制到set
中。 - 失敗:返回
-1
,并設置 errno(通常為EFAULT
,表示set
指針無效)。
四、捕獲信號
4.1 信號捕捉的流程
信號捕捉時,進程執行主控制流遇中斷、異常或系統調用進入內核態;內核處理完相關事務準備回用戶態前,經 do_signal()
檢查當前進程可遞送信號,若為自定義處理函數的信號,內核保存進程主控制流上下文,讓 CPU 跳轉到用戶態執行信號處理函數;處理函數返回時借 sigreturn
再次陷入內核,內核通過 sys_sigreturn
恢復進程之前保存的主控制流上下文,最終進程回到用戶態,從主控制流上次被中斷處繼續執行 ,實現異步信號的 “中斷 - 處理 - 恢復” 流程,保障主邏輯被打斷后可無縫續行。
這條水平線就是用戶層代碼和內核底層邏輯的 “分界線”,程序在不同權限、功能區域執行時,會以此為界完成切換,是理解信號處理中用戶態與內核態交互的基礎標識。
4.2 sigaction
函數原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
參數:
signum
:要操作的信號編號(如SIGINT
、SIGTERM
、SIGCHLD
等 ,SIGKILL
、SIGSTOP
這類內核強制處理的信號無法通過它修改行為 )。act
:指向struct sigaction
結構體的指針,**設置新的信號處理行為 **。若為NULL
,則不修改信號行為,僅用于查詢。oldact
:指向struct sigaction
結構體的指針,用于保存信號原來的處理行為 。若為NULL
,則不保存。
返回值:
- 成功返回
0
,失敗返回-1
并設置errno
(如信號編號無效、指針參數非法等)。
功能:
允許進程設置、查詢特定信號的處理邏輯,定義進程收到信號時應執行的操作,比如:
- 捕獲信號并執行自定義處理函數(如程序崩潰時記錄日志)。
- 恢復信號的默認行為(如讓
SIGINT
恢復 “終止進程” 的默認動作 )。 - 忽略特定信號(如忽略
SIGCHLD
避免子進程變成僵尸進程的場景優化 )。
4.3 操作系統是怎么運行的
4.3.1 硬件中斷
- 中斷向量表就是操作系統的一部分,啟動就加載到內存中了
- 通過外部硬件中斷,操作系統就不需要對外設進行任何周期性的檢測或者輪詢
- 由外部設備觸發的,中斷系統運行流程,叫做硬件中斷
4.3.2 時鐘中斷
問題:
- 進程可以在操作系統的指揮下,被調度,被執行,那么操作系統自己被誰指揮,被誰推動執行呢?
- 外部設備可以觸發硬件中斷,但是這個是需要用戶或者設備自己觸發,有沒有自己可以定期觸發的設備?
答:
- 操作系統的執行依賴于硬件引導流程與中斷驅動機制,本質是 “被動響應事件” 的系統,而非被外部實體 “指揮”。
- 定時器設備通過周期性中斷為操作系統提供時間基準,是實現進程調度、時間管理的核心硬件基礎,其作用如同系統的 “心跳”。理解這兩點,有助于深入掌握計算機系統的底層運行邏輯。
這樣操作系統就能在硬件的推動下,自動調度了。
4.3.3 死循環
如果是這樣,操作系統不就可以躺平了嗎?對,操作系統自己不做任何事情,需要什么功能,就向中斷向量表里面添加方法即可。操作系統的本質:就是一個死循環!
- 這樣,操作系統,就可以在硬件時鐘的推動下,自動調度了。
- 所以,什么是時間片?CPU為什么會有主頻?為什么主頻越快,CPU越快?
答:
- 時間片是多任務系統分配 CPU 時間的基本單位,決定了進程切換的粒度。
- 主頻是 CPU 每秒的時鐘周期數,直接影響指令執行的理論上限(每秒指令數 = 主頻 / CPI(指令周期))。
- 主頻越快 CPU 越快的前提是 CPI 不變,但實際性能還受架構、緩存、指令集等因素影響。
4.3.4 軟中斷
- 上述外部硬件中斷,需要硬件設備觸發。
- 有沒有可能,因為軟件原因,也觸發上面的邏輯?有!
- 為了讓操作系統支持進行系統調用,CPU也設計了對應的匯編指令(
int
或者syscall
),可以讓CPU內部觸發中斷邏輯。
所以:
問題:
- 用戶層怎么把系統調用號給操作系統?-寄存器(比如EAX)
- 操作系統怎么把返回值給用戶?-寄存器或者用戶傳入的緩沖區地址
- 系統調用的過程,其實就是先
intOx80
、syscall
陷入內核,本質就是觸發軟中斷,CPU就會自動執行系統調用的處理方法,而這個方法會根據系統調用號,自動查表,執行對應的方法 - 系統調用號的本質:數組下標!
- 可是為什么我們用的系統調用,從來沒有見過什么
intOx80
或者syscall
呢?都是直接調用上層的函數的啊? - 那是因為Linux的 gnu C 標準庫,給我們把幾乎所有的系統調用全部封裝了。
4.3.5 缺頁中斷?內存碎片處理?除零野指針錯誤?
- 缺頁中斷?內存碎片處理?除零野指針錯誤?這些問題,全部都會被轉換成為CPU內部的軟中斷,然后走中斷處理例程,完成所有處理。有的是進行申請內存,填充頁表,進行映射的。有的是用來處理內存碎片的,有的是用來給目標進行發送信號,殺掉進程等等。
所以:
- 操作系統就是躺在中斷處理例程上的代碼塊!
- CPU內部的軟中斷,比如
int 0x80
或者syscall
,我們叫做陷阱 - CPU內部的軟中斷,比如除零/野指針等,我們叫做異常。(所以,能理解“缺頁異常”為什么這么叫了嗎?)
4.4 如何理解內核態和用戶態
結論:
- 操作系統無論怎么切換進程,都能找到同一個操作系統!換句話說系統調用的內核代碼在共享的內核地址空間執行,但會訪問當前進程的用戶地址空間資源,并使用該進程的內核棧!
- 關于特權級別,涉及到段,段描述符,段選擇子,DPL,CPL,RPL等概念,而現在芯片為了保證兼容性,已經非常復雜了,進而導致OS也必須得照顧它的復雜性,這塊我們不做深究了。
- 用戶態就是執行用戶[0,3]GB時所處的狀態
- 內核態就是執行內核[3,4]GB時所處的狀態
- 區分就是按照CPU內的CPL決定,CPL的全稱是Current PrivilegeLevel,即當前特權級別。
- 一般執行
int0x80
或者syscall
軟中斷,CPL會在校驗之后自動變更
五、可重入函數
5.1 什么是可重入函數?
可重入函數是指在多個執行流同時調用時不會產生副作用的函數。其核心特點是:
- 線程安全:在多線程環境下被并發調用時,不會因共享資源(如全局變量)導致數據競爭。
- 信號安全:在信號處理函數中被調用時,不會破壞程序狀態(如正在執行的操作被中斷)。
5.2 不可重入的典型場景與風險
不可重入函數在多線程或信號處理中可能引發以下問題:
- 全局變量或靜態變量污染
// 不可重入函數示例:依賴全局變量
int total = 0;
int add_to_total(int value) {total += value; // 多線程訪問時可能導致數據競爭return total;
}
風險:若兩個線程同時調用add_to_total
,可能因線程切換導致計算錯誤(如兩個線程各加 1,但結果只加了 1)。
- 標準庫函數的不可重入性
許多標準庫函數依賴靜態緩沖區或狀態,如:
strtok()
:使用靜態指針保存分割位置。gmtime()/localtime()
:返回靜態緩沖區的指針。
示例:
// 不可重入函數示例:使用靜態緩沖區
char* format_time(void) {time_t now = time(NULL);return ctime(&now); // ctime()返回靜態緩沖區,多線程調用會覆蓋結果
}
- 信號處理中的不可重入風險
若信號處理函數調用不可重入函數,可能導致:
- 主程序正在執行的操作被中斷,數據結構被破壞。
- 信號處理函數與主程序同時修改共享資源,引發競態條件。
5.3 可重入函數的設計原則
- 避免共享資源
- 不使用全局變量或靜態變量。
- 若必須使用,通過互斥鎖(如
pthread_mutex_t
)保護。
- 使用局部變量和棧
所有數據存儲在棧上(如函數參數、局部變量),每個調用獨立擁有副本。 - 避免調用不可重入函數
例如:- 用
strtok_r()
替代strtok()
(帶_r
后綴的通常是可重入版本)。 - 用
gmtime_r()
替代gmtime()
。
- 用
5.4 可重入函數的實現示例
// 可重入版本:使用線程局部存儲(TLS)
#include <pthread.h>// 線程局部變量,每個線程獨立擁有副本
__thread int thread_total = 0;int add_to_total(int value) {thread_total += value; // 線程安全:每個線程使用自己的副本return thread_total;
}// 可重入版本:使用互斥鎖保護全局變量
#include <pthread.h>int global_total = 0;
pthread_mutex_t total_mutex = PTHREAD_MUTEX_INITIALIZER;int add_to_total_safe(int value) {pthread_mutex_lock(&total_mutex); // 加鎖global_total += value;pthread_mutex_unlock(&total_mutex); // 解鎖return global_total;
}
5.5 五、信號處理中的可重入性
在信號處理函數中,僅能調用可重入函數(如write()
、_exit()
),避免調用:
- 標準 IO 函數(如
printf()
、fprintf()
)。 - 內存分配函數(如
malloc()
、free()
)。 - 浮點運算函數(如
sin()
、cos()
)。
六、volatile
6.1 volatile 的本質與作用
volatile
是 C/C++ 中的一個類型修飾符,用于告訴編譯器:
- 不要對變量進行優化(如緩存到寄存器或重排序)。
- 每次訪問變量時都直接從內存讀取,寫入時立即刷新到內存。
其核心作用是確保變量的訪問與物理內存直接交互,而非編譯器的臨時緩存。
6.2 信號處理函數中的全局變量
- 場景:在信號處理函數中修改主程序使用的全局變量。
- 原因:信號可能在任意時刻觸發,編譯器不能假設變量不變。
- 示例:
volatile sig_atomic_t signal_received = 0;void signal_handler(int signo) {signal_received = 1; // 原子操作,確保可見性
}int main() {signal(SIGINT, signal_handler);while (!signal_received) { // 每次檢查都從內存讀取// 主程序工作...}return 0;
}
七、SIGCHLD 信號:Linux 進程管理的 “子進程通知機制”
7.1 SIGCHLD 信號的本質與作用
SIGCHLD
(信號編號 17)是 Linux 系統中由內核自動發送給父進程的信號,用于通知以下事件:
- 子進程終止(正常退出或被信號終止)。
- 子進程暫停(如收到
SIGSTOP
信號)。 - 子進程恢復(如收到
SIGCONT
信號)。
其核心作用是讓父進程能夠異步處理子進程狀態變化,避免父進程持續輪詢(如通過wait()
阻塞等待)。
7.2 SIGCHLD 的默認行為與問題
- 默認行為:忽略(進程收到信號后無動作)。
- 潛在問題:若父進程未處理
SIGCHLD
,子進程終止后會變成僵尸進程(Zombie Process),占用系統資源(如進程表項)。
7.3 處理 SIGCHLD 的三種方式
- 忽略信號(最簡單但有風險)
// 忽略SIGCHLD信號,子進程終止后直接釋放資源
signal(SIGCHLD, SIG_IGN); // 或使用sigaction// 子進程代碼
if (fork() == 0) {// 子進程執行...exit(0); // 退出后不會變成僵尸進程
}
注意:Linux 中忽略 SIGCHLD
會讓內核自動回收子進程資源,但某些 UNIX 系統可能不支持,建議使用方式 2 或 3。
- 捕獲信號并調用 wait ()/waitpid ()
#include <signal.h>
#include <sys/wait.h>void sigchld_handler(int signo) {int status;// 非阻塞等待所有子進程,避免wait()阻塞while (waitpid(-1, &status, WNOHANG) > 0) {// 處理子進程退出狀態if (WIFEXITED(status)) {printf("子進程正常退出,狀態碼: %d\n", WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("子進程被信號終止,信號: %d\n", WTERMSIG(status));}}
}int main() {// 注冊信號處理函數struct sigaction sa;sa.sa_handler = sigchld_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // SA_NOCLDSTOP忽略暫停/恢復信號sigaction(SIGCHLD, &sa, NULL);// 創建子進程pid_t pid = fork();if (pid == 0) {// 子進程執行...sleep(2);exit(42);}// 父進程繼續執行...return 0;
}
關鍵點:
- 使用
waitpid(-1, &status, WNOHANG)
非阻塞回收多個子進程。 SA_RESTART
標志避免系統調用被信號中斷。SA_NOCLDSTOP
忽略子進程暫停 / 恢復事件,僅關注終止。
- 使用 sigaction 的 SA_NOCLDWAIT 標志(現代方式)
struct sigaction sa;
sa.sa_handler = SIG_IGN; // 或自定義處理函數
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_NOCLDWAIT; // 內核自動回收子進程,不產生僵尸
sigaction(SIGCHLD, &sa, NULL);
優勢:內核自動釋放子進程資源,無需手動調用wait()
。
7.4 調試與監控
查看僵尸進程:
ps aux | grep Z # 顯示狀態為Z的僵尸進程
跟蹤信號處理:
strace -e signal your_program # 跟蹤信號處理系統調用
7.5 常見誤區與注意事項
- 競態條件
若父進程未捕獲SIGCHLD
,可能導致:- 子進程已終止,但父進程未及時回收,變成僵尸。
- 父進程調用
wait()
時,子進程尚未終止,導致阻塞。
- 信號丟失
非實時信號(如SIGCHLD
)不排隊,若多個子進程同時終止,可能只收到一個信號。需在處理函數中循環調用waitpid()
回收所有子進程。 - 與 fork ()/exec () 的關系
fork()
創建的子進程繼承父進程的SIGCHLD
處理方式。exec()
后,子進程保留SIGCHLD
的處理方式(除非設置了SA_RESETHAND
)。
7.6 總結
- SIGCHLD 的核心價值:
提供異步機制讓父進程感知子進程狀態變化,避免輪詢或阻塞等待。 - 最佳實踐:
- 優先使用
SA_NOCLDWAIT
自動回收子進程。 - 若需獲取子進程狀態,在信號處理函數中循環調用
waitpid()
。
- 優先使用
- 應用場景:
- 守護進程(如
init
進程管理所有子進程)。 - 多進程服務器(如 Web 服務器
fork
子進程處理請求)。
- 守護進程(如