進程與線程基礎及 Linux 進程間通信(IPC)詳解
一、程序與進程
1. 程序(靜態文件)
程序是存儲在磁盤上的可執行文件,是靜態實體,不占用 CPU、內存等運行時資源,僅占用磁盤空間。不同操作系統的可執行文件格式不同:
- Windows:
.exe
- Linux:
ELF
(Executable and Linkable Format) - Android:
.apk
(本質是包含 ELF 可執行文件的壓縮包)
2. 進程(動態執行)
進程是程序的動態執行過程,是操作系統進行資源分配和調度的基本單位,擁有獨立的生命周期和運行時資源(CPU、內存、文件描述符等)。
(1)ELF 文件解析工具
Linux 下通過以下工具查看和分析 ELF 文件:
-
file
命令:查看文件類型基本信息
示例:file /bin/ls
輸出:/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...
-
readelf
命令:解析 ELF 文件詳細結構(需掌握核心選項)選項 功能描述 示例 -h
查看 ELF 文件頭信息(位數、字節序等) readelf -h /bin/ls
-S
查看 ELF 節頭信息(代碼段、數據段等) readelf -S /bin/ls
(2)ELF 文件頭關鍵信息
通過 readelf -h
可獲取以下核心信息:
- 文件位數:第 5 個字節(Magic 字段)標識,
01
表示 32 位,02
表示 64 位。 - 字節序:第 6 個字節標識,
01
表示小端序,02
表示大端序。- 小端序(Little-Endian):低位字節存低地址,高位字節存高地址。例如
0x12345678
存儲為78 56 34 12
。 - 大端序(Big-Endian):高位字節存低地址,低位字節存高地址。例如
0x12345678
存儲為12 34 56 78
。
- 小端序(Little-Endian):低位字節存低地址,高位字節存高地址。例如
(3)ELF 文件類型
ELF 格式包含 4 類核心文件,對應不同使用場景:
類型 | 描述 | 示例 |
---|---|---|
可執行文件 | 可直接運行,包含完整的代碼和數據,加載后可執行 | /bin/ls 、自己編譯的 ./test |
可重定位文件(.o) | 編譯器生成的中間文件,需鏈接后才能執行(單個源文件編譯產物) | gcc -c test.c 生成的 test.o |
共享目標文件(.so) | 動態共享庫,可被多個程序動態鏈接復用,節省內存 | /lib/x86_64-linux-gnu/libc.so.6 |
核心轉儲文件(core) | 程序崩潰時生成的內存快照,用于調試(默認關閉,需 ulimit -c unlimited 開啟) | 程序崩潰后生成的 core.12345 |
3. 進程控制塊(PCB)—— task_struct
當 ELF 程序被執行時,Linux 內核會創建一個 task_struct
結構體 來描述該進程,即進程控制塊(PCB)。它記錄了進程的所有運行時信息,包括:
- 進程 ID(PID)、父進程 ID(PPID)
- 內存資源(虛擬地址空間、頁表)
- CPU 調度信息(優先級、狀態)
- 文件描述符表、信號處理方式
- 鎖資源、信號量等
查看 task_struct
定義
task_struct
定義在 Linux 內核頭文件中,路徑如下:
/usr/src/linux-headers-<版本號>/include/linux/sched.h
查看命令:
cd /usr/src/linux-headers-$(uname -r)/include/linux
vim sched.h
4. 進程查看命令
命令 | 功能描述 | 示例 |
---|---|---|
pstree | 以樹狀圖展示進程間的父子關系 | pstree (查看所有進程樹) |
ps -ef | 查看系統中所有進程的詳細信息(PID、PPID 等) | `ps -ef |
二、進程狀態
Linux 進程有 7 種核心狀態,可歸納為 5 大類,狀態轉換是進程調度的核心邏輯。
1. 進程的“誕生”—— fork()
系統調用
- 觸發條件:父進程調用
fork()
系統調用。 - 核心邏輯:內核復制父進程的上下文(PCB、內存空間等),創建一個幾乎完全相同的子進程(子進程 PID 唯一,PPID 為父進程 PID)。
- 初始狀態:子進程創建后進入 就緒態(TASK_RUNNING),等待 CPU 調度。
2. 核心狀態解析
狀態分類 | 內核標識 | 含義與典型場景 |
---|---|---|
就緒態 | TASK_RUNNING | 進程已準備好運行,等待 CPU 時間片(放在就緒隊列中)。 |
執行態 | TASK_RUNNING | 進程正在 CPU 上執行代碼(內核復用 TASK_RUNNING 標識,通過是否在 CPU 上區分就緒/執行)。 |
睡眠態(掛起態) | TASK_INTERRUPTIBLE | 可中斷睡眠:等待非關鍵事件(如 sleep(10) 、鍵盤輸入),可被信號(如 SIGINT)喚醒。 |
TASK_UNINTERRUPTIBLE | 不可中斷睡眠:等待關鍵硬件操作(如磁盤修復),僅事件完成后喚醒,ps 顯示為 D 狀態。 | |
暫停態 | TASK_STOPPED | 進程被暫停信號(如 SIGSTOP、Ctrl+Z)暫停,可通過 SIGCONT 恢復。 |
TASK_TRACED | 進程被調試器(如 gdb)跟蹤,處于暫停調試狀態。 | |
退出相關狀態 | EXIT_ZOMBIE(僵尸態) | 進程已終止,但父進程未讀取其退出狀態,保留 PCB(ps 顯示為 Z 狀態)。 |
EXIT_DEAD(死亡態) | 父進程調用 wait() /waitpid() 讀取退出狀態后,內核釋放所有資源(進程徹底消失)。 |
三、進程控制核心函數
1. fork()
——創建子進程
函數原型
#include <unistd.h>
pid_t fork(void);
返回值規則
- 父進程:返回子進程的 PID(正數)。
- 子進程:返回 0。
- 失敗:返回 -1(如內存不足)。
示例代碼(父子進程區分)
#include <stdio.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork error"); // 錯誤處理return -1;} else if (pid == 0) {// 子進程邏輯printf("我是子進程,PID:%d,PPID:%d\n", getpid(), getppid());} else {// 父進程邏輯printf("我是父進程,PID:%d,子進程PID:%d\n", getpid(), pid);pause(); // 暫停父進程,避免子進程先退出}return 0;
}
關鍵特性:寫時復制(Copy-On-Write)
父子進程初始共享同一份物理內存,但當任一進程修改數據(棧、堆、全局變量等)時,內核才為修改的頁分配新物理內存,避免不必要的復制開銷。示例如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int g_num = 123; // 全局變量int main() {int stack_num = 10; // 棧變量int *heap_num = malloc(4); // 堆變量*heap_num = 100;pid_t pid = fork();if (pid == 0) {// 子進程修改數據(觸發寫時復制)g_num++;stack_num++;(*heap_num)++;printf("子進程:g_num=%d, stack_num=%d, heap_num=%d\n", g_num, stack_num, *heap_num);free(heap_num);} else {sleep(1); // 等待子進程修改完成// 父進程數據未被修改printf("父進程:g_num=%d, stack_num=%d, heap_num=%d\n", g_num, stack_num, *heap_num);free(heap_num);}return 0;
}
輸出結果:
子進程:g_num=124, stack_num=11, heap_num=101
父進程:g_num=123, stack_num=10, heap_num=100
2. exit()
/_exit()
——進程退出
函數區別
函數 | 功能描述 | 緩沖區處理 |
---|---|---|
exit(int status) | 終止進程,執行退出清理(調用 atexit() 注冊的函數),刷新標準 I/O 緩沖區。 | 刷新緩沖區 |
_exit(int status) | 直接終止進程,不執行清理,不刷新緩沖區(內核級退出)。 | 不刷新緩沖區 |
退出碼規則
exit(0)
/exit(EXIT_SUCCESS)
:正常退出。exit(1)
/exit(EXIT_FAILURE)
:異常退出(非 0 即可,通常用 1)。- 退出碼范圍:0~255,超出則取模 256。
示例代碼(atexit()
注冊退出函數)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>// 注冊的退出函數(棧式調用,反向執行)
void clean1() { printf("clean1: 退出清理1\n"); }
void clean2() { printf("clean2: 退出清理2\n"); }int main() {atexit(clean1); // 先注冊atexit(clean2); // 后注冊printf("程序執行中(未刷新緩沖區)"); // 無換行符,緩沖區未刷新
#ifdef USE__EXIT_exit(0); // 直接退出,不刷新緩沖區,不執行 clean1/clean2
#elseexit(0); // 刷新緩沖區,執行 clean2 → clean1(反向)
#endifreturn 0;
}
編譯與運行:
- 正常退出(
exit(0)
):
gcc test.c -o test && ./test
輸出:程序執行中(未刷新緩沖區)clean2: 退出清理2 clean1: 退出清理1
- 直接退出(
_exit(0)
):
gcc test.c -o test -DUSE__EXIT && ./test
輸出:程序執行中(未刷新緩沖區)
(無清理函數執行)
3. wait()
/waitpid()
——回收子進程
父進程通過這兩個函數回收子進程的退出狀態,避免子進程成為僵尸進程。
函數原型
#include <sys/wait.h>
#include <sys/types.h>// 等待任意子進程退出,獲取退出狀態
pid_t wait(int *status);// 等待指定 PID 的子進程退出,可設置非阻塞
pid_t waitpid(pid_t pid, int *status, int options);
核心參數說明(waitpid()
)
pid
:指定等待的子進程 PID(-1
表示等待任意子進程)。status
:存儲子進程退出狀態的指針(需通過宏解析)。options
:選項(0
表示阻塞,WNOHANG
表示非阻塞)。
退出狀態解析宏
通過 status
指針獲取子進程退出詳情,核心宏如下:
宏 | 功能描述 | 適用場景 |
---|---|---|
WIFEXITED(status) | 判斷子進程是否正常退出(exit /_exit ) | 正常退出 |
WEXITSTATUS(status) | 提取正常退出的退出碼(需先通過 WIFEXITED 判斷) | 正常退出 |
WIFSIGNALED(status) | 判斷子進程是否被信號終止 | 信號終止(如 SIGKILL、SIGSEGV) |
WTERMSIG(status) | 提取終止子進程的信號編號(需先通過 WIFSIGNALED 判斷) | 信號終止 |
WIFSTOPPED(status) | 判斷子進程是否被暫停 | 暫停狀態(如 SIGSTOP) |
示例代碼(回收正常退出的子進程)
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int main() {pid_t pid = fork();if (pid == 0) {// 子進程:正常退出,退出碼 42printf("子進程 PID:%d,即將退出\n", getpid());exit(42);} else {// 父進程:等待子進程退出int status;pid_t ret = waitpid(pid, &status, 0); // 阻塞等待if (ret == -1) {perror("waitpid error");return -1;}// 解析退出狀態if (WIFEXITED(status)) {printf("子進程 %d 正常退出,退出碼:%d\n", ret, WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("子進程 %d 被信號 %d 終止\n", ret, WTERMSIG(status));}}return 0;
}
輸出結果:
子進程 PID:1234,即將退出
子進程 1234 正常退出,退出碼:42
示例代碼(回收被信號終止的子進程)
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>int main() {pid_t pid = fork();if (pid == 0) {// 子進程:觸發段錯誤(SIGSEGV,信號 11)int *null_ptr = NULL;*null_ptr = 10; // 非法內存訪問exit(0); // 不會執行} else {// 父進程:等待子進程退出int status;pid_t ret = waitpid(pid, &status, 0);if (WIFSIGNALED(status)) {printf("子進程 %d 被信號 %d 終止(段錯誤)\n", ret, WTERMSIG(status));}}return 0;
}
輸出結果:
子進程 1235 被信號 11 終止(段錯誤)
4. exec
系列函數——替換進程映像
exec
系列函數將當前進程的代碼段和數據段替換為新的可執行文件(如 /bin/ls
),實現“進程復用”(PID 不變,僅映像替換)。
核心函數(常用 execl
)
#include <unistd.h>// 格式:路徑 + argv[0] + 參數列表 + NULL
int execl(const char *path, const char *arg0, ..., (char *)NULL);
示例代碼(子進程執行 /bin/ls
)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子進程:替換為 ls 命令(顯示當前目錄下的文件)printf("子進程即將執行 ls 命令\n");execl("/bin/ls", "ls", "-l", NULL); // 路徑:/bin/ls,參數:-l// execl 成功則不返回,失敗才執行以下代碼perror("execl error");exit(1);} else {wait(NULL); // 等待子進程執行完成printf("父進程:子進程執行完畢\n");}return 0;
}
輸出結果:
子進程即將執行 ls 命令
total 8
-rwxr-xr-x 1 user user 4568 Aug 10 15:30 test
父進程:子進程執行完畢
四、進程組、會話與終端
1. 進程組(Process Group)
- 定義:由一個或多個進程組成的集合,用于統一管理(如向組內所有進程發送信號)。
- 核心屬性:
- 進程組 ID(PGID):組內所有進程的 PGID 相同。
- 組長進程:PGID 等于其 PID 的進程(組長可終止,但組內有進程則組存在)。
2. 會話(Session)
- 定義:進程組的集合,由會話首進程(創建會話的進程)初始化。
- 核心屬性:
- 會話分為 前臺進程組 和 后臺進程組。
- 前臺進程組可接收終端輸入(如 Ctrl+C 發送 SIGINT),后臺進程組不可。
- 會話首進程終止時,會話依然存在。
3. 終端(Terminal)
- 定義:用戶與系統交互的接口(如 SSH 終端、本地終端)。
- 關聯關系:一個控制終端對應一個會話,終端產生的信號(如 Ctrl+Z)發送給前臺進程組。
五、守護進程(Daemon)
1. 定義與特點
守護進程是 Linux 中 脫離終端、后臺長期運行 的進程,用于執行周期性任務(如日志收集、服務監聽)。核心特點:
- 脫離終端:不依賴任何交互窗口,終端關閉不影響其運行。
- 父進程為
init
(或systemd
):啟動后斷開與原父進程的聯系,由系統初始化進程托管。 - 后臺運行:
ps
顯示為daemon
或無終端關聯(TTY 為?
)。
2. 守護進程編寫流程(簡化版)
fork()
創建子進程,父進程退出(脫離原進程組)。- 子進程調用
setsid()
創建新會話(脫離原終端)。 fork()
創建孫子進程,子進程退出(避免成為會話首進程,無法再次脫離終端)。- 切換工作目錄(如
/
),關閉不需要的文件描述符,重定向標準 I/O 到/dev/null
。 - 執行核心業務邏輯(如循環監聽端口)。
六、Linux 進程間通信(IPC)
Linux 提供多種 IPC 機制,適用于不同場景,以下是核心方式:
1. 管道(Pipe)—— 匿名管道
- 定義:內核維護的字節流緩沖區,用于父子進程或兄弟進程間的單向通信。
- 核心特點:
- 半雙工:數據只能單向流動(需兩個管道實現雙向通信)。
- 無名稱:僅能在有親緣關系的進程間使用。
- 基于文件描述符:
pipe(fd)
創建兩個描述符,fd[0]
讀,fd[1]
寫。
示例代碼(父子進程管道通信)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>int main() {int fd[2];if (pipe(fd) == -1) { // 創建管道perror("pipe error");return -1;}pid_t pid = fork();if (pid == 0) {// 子進程:寫數據(關閉讀端)close(fd[0]);char msg[] = "你好,父進程!";write(fd[1], msg, strlen(msg));close(fd[1]);exit(0);} else {// 父進程:讀數據(關閉寫端)close(fd[1]);char buf[100] = {0};ssize_t n = read(fd[0], buf, sizeof(buf));if (n > 0) {printf("父進程收到:%s\n", buf);}close(fd[0]);wait(NULL);}return 0;
}
輸出結果:
父進程收到:你好,父進程!
2. 有名管道(FIFO)
- 定義:有文件系統路徑的管道(如
/tmp/myfifo
),可在無親緣關系的進程間通信。 - 核心特點:
- 有名稱:通過文件路徑標識,任意進程可通過路徑訪問。
- 阻塞特性:
open
時若管道未被另一端打開,會阻塞直到另一端連接。
示例代碼(兩個獨立進程 FIFO 通信)
發送端(Jack)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>#define FIFO_PATH "/tmp/jack_rose_fifo"int main() {// 若 FIFO 不存在則創建if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return -1;}// 以只寫方式打開 FIFO(阻塞直到讀端打開)int fd = open(FIFO_PATH, O_WRONLY);if (fd == -1) {perror("open error");return -1;}// 循環發送消息char buf[100] = {0};while (1) {printf("Jack: ");fgets(buf, sizeof(buf), stdin);write(fd, buf, strlen(buf));}close(fd);return 0;
}
接收端(Rose)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>#define FIFO_PATH "/tmp/jack_rose_fifo"int main() {// 若 FIFO 不存在則創建if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return -1;}// 以只讀方式打開 FIFO(阻塞直到寫端打開)int fd = open(FIFO_PATH, O_RDONLY);if (fd == -1) {perror("open error");return -1;}// 循環接收消息char buf[100] = {0};while (1) {memset(buf, 0, sizeof(buf));ssize_t n = read(fd, buf, sizeof(buf) - 1);if (n > 0) {printf("Rose 收到: %s", buf);}}close(fd);return 0;
}
運行方式:
- 先啟動接收端:
gcc rose.c -o rose && ./rose
- 再啟動發送端:
gcc jack.c -o jack && ./jack
- 發送端輸入消息,接收端實時顯示。
3. 共享內存(Shared Memory)
- 定義:內核創建的一塊內存區域,多個進程可將其映射到自身虛擬地址空間,實現高效數據共享(無需拷貝,直接訪問內存)。
- 核心 API:
shm_open()
:創建或打開共享內存對象(類似文件操作)。ftruncate()
:設置共享內存大小。mmap()
:將共享內存映射到進程虛擬地址空間。munmap()
:解除映射。shm_unlink()
:刪除共享內存對象(所有進程解除映射后釋放)。
示例代碼(父子進程共享內存通信)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/stat.h>#define SHM_NAME "/my_shared_mem"
#define SHM_SIZE 1024int main() {// 1. 創建/打開共享內存對象int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);if (shm_fd == -1) {perror("shm_open error");return -1;}// 2. 設置共享內存大小if (ftruncate(shm_fd, SHM_SIZE) == -1) {perror("ftruncate error");return -1;}// 3. 映射共享內存到虛擬地址空間char *shm_ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);if (shm_ptr == MAP_FAILED) {perror("mmap error");return -1;}// 關閉文件描述符(映射后不再需要)close(shm_fd);pid_t pid = fork();if (pid == 0) {// 子進程:寫入數據strcpy(shm_ptr, "共享內存通信成功!");printf("子進程寫入:%s\n", shm_ptr);exit(0);} else {// 父進程:讀取數據wait(NULL);printf("父進程讀取:%s\n", shm_ptr);// 4. 解除映射munmap(shm_ptr, SHM_SIZE);// 5. 刪除共享內存對象(所有進程解除映射后釋放)shm_unlink(SHM_NAME);}return 0;
}
編譯與運行:
gcc shm_test.c -o shm_test -lrt && ./shm_test
(-lrt
鏈接實時庫)
輸出結果:
子進程寫入:共享內存通信成功!
父進程讀取:共享內存通信成功!
4. 消息隊列(Message Queue)
- 定義:內核維護的消息鏈表,進程可按優先級發送/接收消息(類似“郵箱”)。
- 核心特點:
- 消息有序:按優先級或發送順序排隊。
- 非阻塞選項:支持超時機制(
mq_timedsend
/mq_timedreceive
)。 - 基于描述符:通過
mq_open()
獲取消息隊列描述符。
核心 API 與結構體
mq_attr
結構體:描述消息隊列屬性(最大消息數、單條消息最大大小等)。struct mq_attr {long mq_flags; // 標志(忽略)long mq_maxmsg; // 最大消息數long mq_msgsize; // 單條消息最大字節數long mq_curmsgs; // 當前消息數(忽略) };
mq_open()
:創建或打開消息隊列。mq_timedsend()
:發送消息(支持超時)。mq_timedreceive()
:接收消息(支持超時)。mq_unlink()
:刪除消息隊列。
示例代碼(父子進程消息隊列通信)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <time.h>
#include <sys/wait.h>
#include <stdlib.h>#define MQ_NAME "/father_son_mq"
#define MAX_MSG_NUM 10 // 最大消息數
#define MAX_MSG_SIZE 100 // 單條消息最大大小int main() {// 1. 初始化消息隊列屬性struct mq_attr attr;attr.mq_maxmsg = MAX_MSG_NUM;attr.mq_msgsize = MAX_MSG_SIZE;attr.mq_flags = 0;attr.mq_curmsgs = 0;// 2. 創建/打開消息隊列mqd_t mq_fd = mq_open(MQ_NAME, O_CREAT | O_RDWR, 0666, &attr);if (mq_fd == (mqd_t)-1) {perror("mq_open error");return -1;}pid_t pid = fork();if (pid == 0) {// 子進程:接收消息char buf[MAX_MSG_SIZE] = {0};struct timespec timeout;for (int i = 0; i < 5; i++) {// 設置超時時間(當前時間 + 15 秒)clock_gettime(CLOCK_REALTIME, &timeout);timeout.tv_sec += 15;// 接收消息(超時返回錯誤)ssize_t n = mq_timedreceive(mq_fd, buf, MAX_MSG_SIZE, NULL, &timeout);if (n == -1) {perror("mq_timedreceive error");break;}printf("子進程收到:%s\n", buf);}exit(0);} else {// 父進程:發送消息char buf[MAX_MSG_SIZE] = {0};struct timespec timeout;for (int i = 0; i < 5; i++) {// 構造消息sprintf(buf, "父進程的第 %d 條消息", i + 1);// 設置超時時間(當前時間 + 5 秒)clock_gettime(CLOCK_REALTIME, &timeout);timeout.tv_sec += 5;// 發送消息if (mq_timedsend(mq_fd, buf, strlen(buf), 0, &timeout) == -1) {perror("mq_timedsend error");break;}printf("父進程發送:%s\n", buf);sleep(1); // 間隔 1 秒發送}// 等待子進程結束wait(NULL);// 3. 關閉消息隊列描述符mq_close(mq_fd);// 4. 刪除消息隊列mq_unlink(MQ_NAME);}return 0;
}
編譯與運行:
gcc mq_test.c -o mq_test -lrt && ./mq_test
輸出結果:
父進程發送:父進程的第 1 條消息
子進程收到:父進程的第 1 條消息
父進程發送:父進程的第 2 條消息
子進程收到:父進程的第 2 條消息
…
5. 信號(Signal)
信號是 Linux 中輕量級的進程間通信機制,用于通知進程發生了某種事件(如中斷、錯誤)。
(1)常用信號與編號
信號名稱 | 編號 | 含義與默認行為 |
---|---|---|
SIGINT | 2 | 中斷(Ctrl+C),默認終止進程 |
SIGKILL | 9 | 強制終止,不可被捕獲/阻塞 |
SIGSTOP | 19 | 暫停(Ctrl+Z),不可被捕獲/阻塞 |
SIGSEGV | 11 | 段錯誤(非法內存訪問),默認終止并生成 core 文件 |
SIGCONT | 18 | 恢復暫停的進程 |
(2)信號發送函數—— kill()
/sigqueue()
-
kill()
:發送信號給指定進程。
原型:int kill(pid_t pid, int sig);
示例:kill(1234, SIGINT);
(給 PID 1234 的進程發送中斷信號)。 -
sigqueue()
:發送信號并攜帶數據(僅支持實時信號)。
原型:int sigqueue(pid_t pid, int sig, const union sigval value);
示例:union sigval val; val.sival_int = 1001; // 攜帶整數數據 sigqueue(1234, SIGUSR1, val); // 發送自定義信號 SIGUSR1
(3)信號捕獲函數—— signal()
/sigaction()
-
signal()
:簡單信號捕獲(不推薦,兼容性差)。
原型:void (*signal(int sig, void (*handler)(int)))(int);
示例:#include <stdio.h> #include <signal.h>void sigint_handler(int sig) {printf("捕獲到 SIGINT 信號(編號:%d),不終止進程!\n", sig); }int main() {signal(SIGINT, sigint_handler); // 捕獲 SIGINTwhile (1) { sleep(1); } // 循環等待信號return 0; }
-
sigaction()
:功能強大的信號捕獲(推薦,支持攜帶數據、信號掩碼等)。
示例(捕獲信號并接收數據):#include <stdio.h> #include <signal.h> #include <unistd.h>// 信號處理函數(支持接收數據) void sigusr1_handler(int sig, siginfo_t *info, void *arg) {printf("捕獲到信號:%d\n", sig);printf("攜帶的數據:%d\n", info->si_int); // 讀取 sigqueue 發送的數據 }int main() {struct sigaction act;act.sa_sigaction = sigusr1_handler; // 設置處理函數act.sa_flags = SA_SIGINFO; // 啟用數據接收sigemptyset(&act.sa_mask); // 清空信號掩碼// 注冊 SIGUSR1 信號的處理函數sigaction(SIGUSR1, &act, NULL);printf("進程 PID:%d,等待 SIGUSR1 信號...\n", getpid());while (1) { sleep(1); }return 0; }
(4)信號阻塞—— sigprocmask()
通過信號掩碼(sigset_t
)阻塞指定信號,阻塞期間信號會被掛起,解除阻塞后再處理。
示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void sigint_handler(int sig) {printf("捕獲到 SIGINT 信號\n");
}int main() {// 1. 注冊信號處理函數signal(SIGINT, sigint_handler);// 2. 初始化信號集,添加 SIGINTsigset_t set;sigemptyset(&set);sigaddset(&set, SIGINT);// 3. 阻塞 SIGINT(期間按 Ctrl+C 不會觸發處理函數)printf("開始阻塞 SIGINT,持續 5 秒...\n");sigprocmask(SIG_BLOCK, &set, NULL);sleep(5);// 4. 解除阻塞(掛起的 SIGINT 會立即觸發處理函數)printf("解除阻塞 SIGINT\n");sigprocmask(SIG_UNBLOCK, &set, NULL);while (1) { sleep(1); }return 0;
}
七、線程(Thread)
1. 進程與線程的核心區別
對比維度 | 進程(Process) | 線程(Thread) |
---|---|---|
資源分配單位 | 系統資源分配的基本單位(獨立內存、文件描述符等) | CPU 調度的基本單位(共享進程資源) |
資源占用 | 占用資源多,創建/銷毀開銷大 | 僅需少量棧空間,創建/切換開銷小 |
獨立性 | 進程間獨立,一個崩潰不影響其他進程 | 線程依賴進程,一個線程崩潰可能導致整個進程崩潰 |
通信方式 | 依賴 IPC 機制(管道、共享內存等) | 直接共享進程資源(全局變量、堆內存等) |
2. 線程的共享與非共享資源
(1)共享資源(進程級資源)
- 文件描述符表
- 信號處理方式
- 當前工作目錄
- 用戶 ID 和組 ID
- 內存地址空間(代碼段
.text
、數據段.data
、堆.heap
、共享庫)
(2)非共享資源(線程級資源)
- 線程 ID(TID)
- 處理器現場(寄存器、程序計數器)
- 獨立的用戶棧和內核棧
errno
變量(每個線程獨立)- 信號屏蔽字
- 調度優先級
3. 線程核心 API(POSIX 線程庫 pthread
)
編譯時需鏈接線程庫:gcc test.c -o test -lpthread
。
(1)pthread_self()
——獲取當前線程 ID
#include <stdio.h>
#include <pthread.h>int main() {// 主線程 ID(%lu 對應 unsigned long)printf("主線程 ID:%lu\n", pthread_self());return 0;
}
(2)pthread_create()
——創建線程
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 線程函數(返回值和參數均為 void*,需強制類型轉換)
void *thread_func(void *arg) {char *msg = (char *)arg;printf("子線程 ID:%lu,收到參數:%s\n", pthread_self(), msg);sleep(3); // 模擬業務邏輯pthread_exit("子線程退出!"); // 線程退出并返回數據
}int main() {pthread_t tid; // 線程 IDchar *msg = "Hello, Thread!";// 創建線程(attr 為 NULL 表示默認屬性)int ret = pthread_create(&tid, NULL, thread_func, (void *)msg);if (ret != 0) {perror("pthread_create error");return -1;}printf("主線程 ID:%lu,子線程 ID:%lu\n", pthread_self(), tid);// 等待子線程退出并回收資源(類似 wait())void *exit_msg;pthread_join(tid, &exit_msg);printf("子線程返回:%s\n", (char *)exit_msg);return 0;
}
輸出結果:
主線程 ID:140709376947008,子線程 ID:140709368554240
子線程 ID:140709368554240,收到參數:Hello, Thread!
子線程返回:子線程退出!
(3)pthread_join()
——回收線程資源
- 功能:阻塞等待指定線程退出,回收其資源,獲取退出狀態(類似進程的
waitpid()
)。 - 注意:僅適用于 可接合屬性 的線程(默認屬性),若線程為 分離屬性,
pthread_join()
會直接返回失敗。
(4)線程屬性——分離屬性(Detached)
分離屬性的線程退出時會自動釋放資源,無需 pthread_join()
回收(避免僵尸線程)。
示例代碼:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>void *thread_func(void *arg) {printf("分離線程 ID:%lu\n", pthread_self());sleep(2);return NULL;
}int main() {pthread_t tid;pthread_attr_t attr;// 1. 初始化線程屬性pthread_attr_init(&attr);// 2. 設置分離屬性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// 3. 創建分離線程pthread_create(&tid, &attr, thread_func, NULL);// 4. 銷毀線程屬性(不再需要)pthread_attr_destroy(&attr);// 嘗試回收分離線程(會失敗)void *exit_msg;int ret = pthread_join(tid, &exit_msg);if (ret != 0) {printf("pthread_join 失敗:%s\n", strerror(ret));}sleep(3); // 等待分離線程執行完成return 0;
}
輸出結果:
分離線程 ID:140709368554240
pthread_join 失敗:Invalid argument
4. 示例代碼(創建兩個線程并發執行)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 線程 1 函數
void *thread1_func(void *arg) {while (1) {printf("線程 1(%lu):%s\n", pthread_self(), (char *)arg);sleep(1);}
}// 線程 2 函數
void *thread2_func(void *arg) {while (1) {printf("線程 2(%lu):%s\n", pthread_self(), (char *)arg);sleep(1);}
}int main() {pthread_t tid1, tid2;// 創建兩個線程pthread_create(&tid1, NULL, thread1_func, "Hello World!");pthread_create(&tid2, NULL, thread2_func, "Hello Thread!");// 等待線程(避免主線程先退出)pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
輸出結果(兩個線程交替執行):
線程 1(140709368554240):Hello World!
線程 2(140709360161536):Hello Thread!
線程 1(140709368554240):Hello World!
線程 2(140709360161536):Hello Thread!
…