在Linux开发(尤其是嵌入式Linux)中,进程是程序运行的载体,信号是进程间通信的核心手段。无论是调试“杀不死的进程”,还是实现程序的“优雅退出”,理解进程状态和信号机制都是必备技能。本文将从基础概念到代码实战,全面解析Linux进程与信号的核心知识点。
一、Linux进程基础
1.1 进程是什么?
进程是程序的一次运行实例,是操作系统资源分配的基本单位。在嵌入式Linux中,一个Modbus主站程序、一个串口通信服务、一个GPIO控制脚本,运行后都会成为一个独立的进程,拥有唯一的PID(进程ID)。
1.2 查看进程:ps指令的两个核心用法
ps是查看进程的最常用指令,嵌入式开发中最常使用ps -aux和ps -ef,两者核心功能一致,但输出格式和来源风格不同:
| 指令格式 | 风格来源 | 核心区别 | 输出字段(关键) | 嵌入式使用场景 |
|---|---|---|---|---|
ps -aux |
BSD风格(Unix) | 按CPU/内存使用率排序,含状态码、CPU占比 | USER, PID, %CPU, %MEM, STAT, COMMAND | 快速定位资源占用高的进程(如异常的Modbus进程) |
ps -ef |
SysV风格(System V) | 按PID排序,含父进程PPID、启动时间 | UID, PID, PPID, C, STIME, TTY, CMD | 排查进程父子关系(如僵尸进程溯源) |
示例输出对比:
ps -aux输出(重点看STAT列):root 2284 0.5 3.9 52336 17828 pts/0 Tl 08:25 0:00 ./001_Modbus_Masterps -ef输出(重点看PPID列):root 2284 1896 0 08:25 pts/0 00:00:00 ./001_Modbus_Master
1.3 进程状态码全解析(嵌入式重点)
ps输出中的STAT列(BSD)/S列(SysV)是进程状态的核心标识,单个字母对应进程的运行状态,嵌入式开发中常见状态如下:
| 状态码 | 风格 | 核心含义 | 嵌入式常见场景 | 处理建议 |
|---|---|---|---|---|
| R | 通用 | 运行中/可运行 | 业务进程正常执行(如Modbus数据收发) | 无需处理,正常状态 |
| S | 通用 | 可中断睡眠 | 等待IO(串口/网络/文件)、定时器 | 收到SIGTERM(kill PID)可响应退出 |
| D | 通用 | 不可中断睡眠 | 等待硬件IO(SPI/I2C/SD卡读写) | kill -9也无效,仅重启或等待IO完成 |
| T | 通用 | 暂停/停止 | Ctrl+Z暂停、调试器附着、SIGSTOP信号 | 需先SIGCONT恢复,或kill -9强制终止 |
| Z | 通用 | 僵尸进程 | 子进程退出但父进程未回收资源 | 重启父进程或用wait()回收,避免内存泄漏 |
| l | BSD | 多线程进程 | 多线程Modbus/网络程序 | kill PID会终止所有线程 |
| + | BSD | 前台进程 | 终端启动的业务程序 | 可通过Ctrl+C发送SIGINT |
二、Linux信号
信号是Linux内核向进程发送的异步通知,用于控制进程行为(终止、暂停、继续等)。kill指令本质是向进程发送信号,如kill PID默认发送SIGTERM,kill -9 PID发送SIGKILL。
2.1 常用信号全解析(嵌入式开发重点)
| 信号编号 | 信号名称 | kill参数 | 核心含义 | 嵌入式使用场景 | 是否可捕获/忽略 |
|---|---|---|---|---|---|
| 1 | SIGHUP | -1/-HUP | 终端挂起 | 串口/SSH断开时通知进程重连 | 是(可捕获并重连) |
| 2 | SIGINT | -2/-INT | 终端中断 | Ctrl+C终止前台进程 | 是(可捕获并优雅退出) |
| 9 | SIGKILL | -9/-KILL | 强制终止 | 杀死顽固进程(T/Z状态) | 否(内核直接终止,无法拦截) |
| 15 | SIGTERM | -15/-TERM | 优雅终止 | 正常停止业务进程(默认kill) | 是(推荐捕获并释放资源) |
| 18 | SIGCONT | -18/-CONT | 继续运行 | 恢复暂停(T状态)的进程 | 是(无忽略意义) |
| 19 | SIGSTOP | -19/-STOP | 强制暂停 | 临时暂停进程排查问题 | 否(无法捕获/忽略) |
| 20 | SIGTSTP | -20/-TSTP | 终端暂停 | Ctrl+Z暂停前台进程 | 是(可捕获但无需处理) |
| 11 | SIGSEGV | -11/-SEGV | 段错误 | 程序访问非法内存(空指针/越界) | 是(仅用于调试,无修复意义) |
2.2 核心规则
SIGKILL(9)和SIGSTOP(19)是“终极信号”,无法被捕获/忽略,内核直接处理;- 暂停状态(T)的进程无法响应
SIGTERM(15),必须用SIGKILL(9)强制终止; - 信号优先级:
SIGKILL > SIGTERM > 其他信号。
三、C/C++信号示例
嵌入式开发中,我们常需要通过代码主动发送信号、捕获信号实现优雅退出、屏蔽信号避免关键操作被中断。以下是完整实战代码。
3.1 发送信号:进程间/线程间控制
3.1.1 向指定进程发送信号(kill())
适用于父进程控制子进程、外部程序管理业务进程:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t child_pid = fork(); // 创建子进程(嵌入式多进程常用)if (child_pid == 0) {// 子进程:模拟嵌入式Modbus业务进程printf("子进程(PID:%d):Modbus主程序运行中...\n", getpid());while (1) {sleep(1); // 模拟业务循环}} else if (child_pid > 0) {// 父进程:3秒后发送SIGTERM优雅终止子进程printf("父进程:3秒后终止子进程(PID:%d)\n", child_pid);sleep(3);// 发送SIGTERM(等价于 kill -15 PID)int ret = kill(child_pid, SIGTERM);if (ret == -1) {perror("kill失败");exit(1);}wait(NULL); // 回收子进程资源(避免僵尸进程)printf("子进程已退出\n");} else {perror("fork创建子进程失败");exit(1);}return 0;
}
3.1.2 向自身发送信号(raise())
适用于进程自检失败时主动终止:
#include <stdio.h>
#include <signal.h>int main() {printf("进程自检中...\n");// 模拟自检失败int check_fail = 1;if (check_fail) {printf("自检失败,主动终止进程\n");raise(SIGTERM); // 等价于 kill(getpid(), SIGTERM)}return 0;
}
3.1.3 多线程场景:向指定线程发信号(pthread_kill())
#include <stdio.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>volatile sig_atomic_t thread_exit = 0;// 线程信号处理函数
void thread_sig_handler(int sig) {thread_exit = 1;
}void* worker_thread(void* arg) {// 线程内注册SIGUSR1信号处理struct sigaction sa;sa.sa_handler = thread_sig_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sigaction(SIGUSR1, &sa, NULL);printf("工作线程(ID:%lu)运行中\n", (unsigned long)pthread_self());while (!thread_exit) {sleep(1);printf("处理Modbus请求...\n");}printf("工作线程退出\n");pthread_exit(NULL);
}int main() {pthread_t tid;pthread_create(&tid, NULL, worker_thread, NULL);// 5秒后向工作线程发送SIGUSR1sleep(5);printf("主线程:向工作线程发送终止信号\n");pthread_kill(tid, SIGUSR1);pthread_join(tid, NULL);printf("主线程退出\n");return 0;
}
3.2 捕获信号:实现优雅退出
嵌入式程序退出时需释放串口、GPIO、网络等资源,直接kill -9会导致资源泄漏,因此需捕获SIGINT/SIGTERM实现优雅退出:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>// 原子标志位(信号/多线程安全)
volatile sig_atomic_t g_exit_flag = 0;// 信号处理函数(嵌入式要求:简洁,仅设标志位)
void sig_handler(int sig) {switch (sig) {case SIGINT: // Ctrl+C触发case SIGTERM: // kill PID触发printf("\n捕获信号%d,准备优雅退出...\n", sig);g_exit_flag = 1;break;default:break;}
}// 嵌入式资源释放函数
void release_resources() {printf("释放Modbus串口资源...\n");printf("保存配置到Flash...\n");printf("关闭GPIO外设...\n");
}int main() {struct sigaction sa;sa.sa_handler = sig_handler;sigemptyset(&sa.sa_mask); // 处理信号时不屏蔽其他信号sa.sa_flags = 0;// 注册信号处理函数if (sigaction(SIGINT, &sa, NULL) == -1 || sigaction(SIGTERM, &sa, NULL) == -1) {perror("注册信号失败");exit(1);}printf("Modbus主程序运行中(PID:%d)\n", getpid());printf("按Ctrl+C或执行kill %d触发优雅退出\n", getpid());// 嵌入式主循环while (!g_exit_flag) {printf("处理Modbus请求...\n");sleep(1);}// 释放资源(核心步骤)release_resources();printf("程序优雅退出\n");return 0;
}
关键注意事项:
- 信号处理函数禁止调用
malloc/free/fopen等“不可重入函数”,仅做“设置标志位”; - 标志位必须用
volatile sig_atomic_t,保证信号上下文与主循环的数据一致性; SIGKILL(9)无法捕获,因此优先用kill PID(SIGTERM)触发优雅退出。
3.3 控制信号:忽略、屏蔽、恢复默认
3.3.1 忽略信号(如屏蔽Ctrl+C)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main() {struct sigaction sa;sa.sa_handler = SIG_IGN; // SIG_IGN = 忽略信号sigemptyset(&sa.sa_mask);sa.sa_flags = 0;// 忽略SIGINT(Ctrl+C无效)sigaction(SIGINT, &sa, NULL);printf("SIGINT已忽略,按Ctrl+C无效(10秒后退出)\n");sleep(10);return 0;
}
3.3.2 恢复信号默认处理
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void sig_handler(int sig) {printf("捕获SIGINT,5秒后恢复默认处理\n");sleep(5);// 恢复默认行为(Ctrl+C直接终止)struct sigaction sa;sa.sa_handler = SIG_DFL; // SIG_DFL = 默认处理sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sigaction(SIGINT, &sa, NULL);printf("再次按Ctrl+C将终止程序\n");
}int main() {struct sigaction sa;sa.sa_handler = sig_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sigaction(SIGINT, &sa, NULL);printf("程序运行中,按Ctrl+C测试...\n");while (1) { sleep(1); }return 0;
}
3.3.3 临时屏蔽信号(嵌入式关键操作场景)
执行Flash写入、关键Modbus指令发送等操作时,需屏蔽信号避免中断:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t mask;sigemptyset(&mask);// 添加要屏蔽的信号sigaddset(&mask, SIGINT);sigaddset(&mask, SIGTERM);printf("执行Flash数据写入(屏蔽信号)...\n");// 屏蔽信号sigprocmask(SIG_BLOCK, &mask, NULL);// 模拟关键操作(5秒)sleep(5);printf("关键操作完成,恢复信号响应\n");// 解除屏蔽sigprocmask(SIG_UNBLOCK, &mask, NULL);printf("程序继续运行,按Ctrl+C可终止\n");while (1) { sleep(1); }return 0;
}
四、嵌入式Linux开发注意事项
- 防僵尸进程:子进程退出后,父进程必须调用
wait()/waitpid()回收资源,否则会变成僵尸进程(Z状态); - 信号处理轻量化:嵌入式CPU/内存资源有限,信号处理函数禁止耗时操作;
- 不可中断睡眠(D状态):多因硬件驱动/IO阻塞导致,需检查外设连接(如SD卡、传感器),或重启开发板;
- 多线程信号:主线程注册的信号处理函数对所有线程生效,线程级屏蔽用
pthread_sigmask()而非sigprocmask()。