一、Linux 信号处理(Signal)
1. 什么是信号
信号是 Linux 进程间的一种异步通知机制。操作系统或其他进程可以向目标进程发送信号,通知它发生了某种事件。
常见信号:
- SIGINT:中断信号,通常由 Ctrl+C 产生,默认终止进程。
- SIGTERM:终止请求,默认终止进程,允许程序捕获并清理资源。
- SIGKILL:强制终止进程,不能捕获、忽略或自定义处理。
- SIGSTOP:暂停进程,不能捕获、忽略或自定义处理。
- SIGSEGV:非法内存访问,默认终止进程,可能产生 Core。
- SIGPIPE:向已关闭的管道或 socket 写入时可能产生,默认动作是终止进程。
- SIGALRM:定时器超时信号。
- SIGCHLD:子进程状态发生变化时通知父进程。
注意:信号编号可能因架构而异。代码中使用 SIGINT 等宏,不要依赖硬编码数字。
2. 信号的三种处理方式
- 执行默认动作。
- 忽略信号。
- 注册自定义信号处理函数。
SIGKILL 和 SIGSTOP 不能被捕获、忽略或自定义处理。
3. signal() 注册处理函数
示例:
#include <signal.h> #include <unistd.h> void handler(int signo) { /* 信号处理逻辑 */ } int main(void) { signal(SIGINT, handler); while (1) pause(); return 0; }signal() 可以注册信号处理函数,但工程开发通常更推荐 sigaction(),因为它的行为和配置更加明确。
4. sigaction() 注册处理函数
示例:
#include <signal.h> #include <string.h> #include <unistd.h> #include <stdio.h> void handler(int signo) { const char msg[] = "收到 SIGINT\n"; write(STDOUT_FILENO, msg, sizeof(msg) - 1); } int main(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); return 1; } while (1) pause(); return 0; }重要成员:
- sa_handler:指定信号处理函数。
- sa_mask:处理当前信号期间,额外屏蔽哪些信号。
- sa_flags:控制处理行为,例如 SA_RESTART。如果设置了 SA_RESTART,就可以让部分被该信号中断的系统调用在信号处理函数结束后自动重启,减少程序手动处理 EINTR(错误码:表示系统调用被信号中断)的情况。
5. 信号处理函数的限制:异步信号安全
信号处理函数可能在程序执行的任意时刻被异步调用。如果在处理函数中调用非异步信号安全函数,可能出现重入问题、死锁或数据损坏。
通常不要在信号处理函数中调用:
- printf()、fprintf()
- malloc()、free()
- new、delete
- std::cout
- pthread_mutex_lock()
常见异步信号安全操作(可以在信号处理函数中调用)包括:
- write()
- _exit()
- kill()
- sigaction()
具体以 POSIX 异步信号安全函数列表为准。
推荐做法:处理函数只设置一个退出标志,资源清理放在主流程中执行。
示例:
#include <signal.h> #include <stdio.h> #include <unistd.h> volatile sig_atomic_t running = 1; void handler(int signo) { running = 0; } int main(void) { struct sigaction sa = {0}; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); while (running) { /* 正常业务逻辑 */ sleep(1); } /* 在正常执行上下文中清理资源 */ /* close(fd)、停止线程、释放设备资源等 */ return 0; }说明:
- volatile sig_atomic_t 适合简单的信号标志位。
- 它不是通用的线程同步机制,不能替代 mutex、condition variable 或原子操作。
- sleep() 等阻塞调用可能被信号中断,程序需要正确处理其返回值。
6. 多线程程序中的信号
- 进程级信号可能递达某个未屏蔽该信号的线程。
- 工程中常见做法是:在创建工作线程前屏蔽指定信号,新线程继承创建者的信号屏蔽字,因此也屏蔽这两个信号,再由专门线程通过 sigwait() 接收。
- 也可以使用 signalfd(),把信号作为文件描述符事件交给 epoll 统一管理。
- 不要假设信号一定由主线程处理。
7. 嵌入式项目中的用途
例如 RK3568 多路 RTSP 程序收到 SIGTERM:
- 设置退出状态或通知专门的信号处理线程。
- 停止接收新的任务。
- 通知拉流、解码、图像处理线程退出。
- 等待线程结束。
- 停止 RTSP 会话并释放 FFmpeg、MPP、RGA 等资源。
- 关闭 socket、文件描述符。
- 正常退出。
核心:信号用于通知程序发生事件;资源清理应在安全的正常执行上下文中完成。
二、错误码与 errno
1. errno 是什么
很多 Linux 系统调用或库函数失败时,会通过返回值表示失败,并设置 errno,说明失败原因。
示例:
#include <stdio.h> #include <fcntl.h> #include <errno.h> #include <string.h> int main(void) { int fd = open("not_exist.txt", O_RDONLY); if (fd == -1) { printf("errno = %d\n", errno); printf("错误原因:%s\n", strerror(errno)); return 1; } close(fd); return 0; }也可以使用 perror("open"); 打印自定义前缀和当前 errno 对应的错误描述。
2. 常见错误码
- EINTR:系统调用被信号中断。
- EAGAIN:当前暂时无法完成操作,常见于非阻塞 I/O。
- EWOULDBLOCK:操作会阻塞;在 Linux 上通常与 EAGAIN 数值相同,但可移植代码不应假设所有系统都相同。
- EBADF:文件描述符无效。
- EINVAL:参数无效。
- ENOMEM:内存不足。
- EACCES:权限不足。
- ENOENT:文件或路径不存在。
- EPIPE:管道没有读端,或对端已关闭时写入。
- ETIMEDOUT:操作超时。
3. errno 的使用规则
最重要:先根据函数的返回值判断操作是否失败,确认失败后再读取 errno。
例如:
int fd = open("test.txt", O_RDONLY); if (fd == -1) { int err = errno; /* 先保存错误码 */ fprintf(stderr, "open failed: %s\n", strerror(err)); }注意:
- errno 不会因为一次成功调用而自动清零。
- 不要在没有确认失败时读取 errno 并据此判断本次调用结果。
- 如果失败后还要调用其他函数,建议先保存 errno,避免后续调用改变它。
- errno 在现代 Linux/POSIX 线程环境中是线程局部的,不同线程有各自的 errno。
4. EINTR:系统调用被信号中断
例如 read() 正在阻塞等待数据时,进程收到信号,read() 可能返回 -1,并设置 errno = EINTR。
常见重试写法:
ssize_t ret; do { ret = read(fd, buf, sizeof(buf)); } while (ret == -1 && errno == EINTR);注意:
- EINTR 表示被信号中断,不一定表示设备或网络故障。
- 某些系统调用在 SA_RESTART 设置下可能自动重启,但不是所有系统调用都如此。
- 重试前应确认该操作适合重试,并正确处理退出标志。
5. EAGAIN / EWOULDBLOCK:非阻塞 I/O 暂时无数据
将 socket 设置为非阻塞后,如果当前没有数据可读,read()/recv() 可能返回 -1,并设置 EAGAIN 或 EWOULDBLOCK。
含义:当前无法立即完成操作,不代表连接一定故障。通常应等待下一次 I/O 就绪通知。
常见处理思路:
- 使用 epoll_wait() 等待文件描述符就绪。
- 收到可读事件后循环 recv(),直到读完当前可用数据。
- 当 recv() 返回 -1 且 errno 为 EAGAIN/EWOULDBLOCK,结束本轮读取,等待下一次事件。
- 如果是其他错误,再按错误类型处理。
epoll_wait() 示例:
while (1) { int n = epoll_wait(epfd, events, MAX_EVENTS, -1); if (n == -1) { if (errno == EINTR) continue; perror("epoll_wait"); break; } else { //处理数据 } }三、系统调用返回值
不同系统调用的返回值规则不同。必须查阅具体函数的接口说明,不能用同一套判断方式套用所有函数。
1. open()
原型:int open(const char *pathname, int flags, ...);
- 成功:非负文件描述符。
- 失败:-1,并设置 errno。
2. read()
原型:ssize_t read(int fd, void *buf, size_t count);
- 大于 0:本次实际读取的字节数。
- 等于 0:到达 EOF;对流式 socket 通常表示对端有序关闭连接。
- 等于 -1:发生错误,查看 errno。
重点:read() 不保证一次读满 count 个字节。例如请求读取 1024 字节,返回 300,表示本次成功读取了 300 字节。TCP 是字节流,没有应用层消息边界,不能假设一次 recv()/read() 就能读到完整消息。
3. write()
原型:ssize_t write(int fd, const void *buf, size_t count);
- 大于 0:本次实际写入的字节数。
- 等于 -1:发生错误,查看 errno。
- 小于 count 但大于 0:部分写入,剩余数据需要继续处理。
重点:write() 不保证一次写完全部数据。网络发送需要考虑部分写入、EINTR、非阻塞情况下的 EAGAIN,以及连接关闭等情况。
4. close()
原型:int close(int fd);
- 成功:0。
- 失败:-1,并设置 errno。
注意:Linux 上 close() 报错时,文件描述符通常已经被释放。不要盲目重试 close(),否则 fd 可能已被其他代码复用,重试会误关其他资源。
5. ioctl()
原型:int ioctl(int fd, unsigned long request, ...);
- 成功:通常为 0,某些命令可能返回其他非负值。
- 失败:-1,并设置 errno。
嵌入式场景:V4L2 摄像头常用 ioctl() 执行 VIDIOC_QUERYCAP、VIDIOC_S_FMT、VIDIOC_REQBUFS 等操作。
6. mmap()
原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);- 成功:映射区域起始地址。
- 失败:MAP_FAILED,通常定义为 (void *)-1。
正确判断:
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (ptr == MAP_FAILED) { perror("mmap"); }注意:不能用 ptr == NULL 判断 mmap() 是否失败。
四、常见系统调用返回值速查
| 函数 | 成功返回值 | 失败返回值 |
|---|---|---|
| open | 非负文件描述符 | -1 |
| read | 实际读取字节数(可为 0) | -1 |
| write | 实际写入字节数 | -1 |
| close | 0 | -1 |
| socket | 非负文件描述符 | -1 |
| connect | 0 | -1 |
| accept | 新的非负文件描述符 | -1 |
| recv | 实际接收字节数(可为 0) | -1 |
| send | 实际发送字节数 | -1 |
| ioctl | 通常为 0 或其他非负值 | -1 |
| mmap | 映射地址 | MAP_FAILED |
| waitpid | 返回被等待的子进程 PID | -1;无选项时也可能因 WNOHANG 返回 0 |
提示:这是常见规则速查,不替代具体 API 文档。不同函数的成功返回值和特殊情况可能不同。
五、综合示例:TCP 接收、信号退出与错误处理
下面是简化示例,重点展示返回值和 errno 的判断方式:
#include <stdio.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <string.h> #include <sys/socket.h> volatile sig_atomic_t running = 1; void signal_handler(int signo) { running = 0; } int main(void) { struct sigaction sa = {0}; sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); return 1; } /* 此处省略 sockaddr_in 初始化和 connect() */ char buf[1024]; while (running) { ssize_t ret = recv(sockfd, buf, sizeof(buf), 0); if (ret > 0) { printf("收到 %zd 字节\n", ret); } else if (ret == 0) { printf("对端有序关闭连接\n"); break; } else { if (errno == EINTR) { if (!running) break; continue; } perror("recv"); break; } } close(sockfd); printf("程序退出\n"); return 0; }说明:
- socket() 返回 -1 时,使用 perror() 查看原因。
- recv() 大于 0:收到数据,返回的是实际字节数。
- recv() 等于 0:对端有序关闭连接。
- recv() 等于 -1 且 errno == EINTR:被信号中断,可结合退出标志决定重试还是退出。
- 其他错误:根据 errno 进一步处理。
- 生产代码还要补充地址初始化、connect() 错误处理、连接超时、非阻塞 I/O 和完整资源管理。
六、面试高频问题
1. Linux 信号是什么?
答:信号是 Linux 进程间的一种异步通知机制,用来通知进程发生了某种事件。
2. SIGINT 和 SIGTERM 有什么区别?
答:SIGINT 通常由终端 Ctrl+C 产生;SIGTERM 是通用终止请求,常用于要求程序正常退出并清理资源。两者都可以被捕获。
3. SIGKILL 能被捕获吗?
答:不能。SIGKILL 和 SIGSTOP 不能被捕获、忽略或自定义处理。
4. signal() 和 sigaction() 有什么区别?
答:sigaction() 提供更明确的信号处理语义,可以配置屏蔽信号集合和处理标志,工程中通常优先使用。
5. 为什么信号处理函数中不能随便调用 printf()?
答:printf() 不是异步信号安全函数,可能与主流程中的 stdio 内部状态发生冲突,导致重入问题或死锁。
6. errno 是什么?
答:errno 是线程局部错误码。许多系统调用失败时返回约定的失败值,并设置 errno 来说明失败原因。
7. errno 什么时候读取?
答:先根据函数返回值确认失败,再立即读取 errno。不要仅凭 errno 是否为非零判断本次调用失败。
8. EINTR 是什么?
答:表示系统调用被信号中断。对于适合重试的调用,通常可以判断 EINTR 后重试,同时考虑退出标志和 SA_RESTART。
9. EAGAIN 是什么?
答:表示当前操作暂时无法完成,常见于非阻塞 I/O。通常等待下一次 I/O 就绪事件,而不是当作致命错误。
10. read() 返回 0 是什么意思?
答:普通文件通常表示到达 EOF;流式 socket 通常表示对端有序关闭连接。
11. read() 一次能读满请求长度吗?
答:不能保证。read() 返回本次实际读取的字节数,可能小于请求长度。
12. write() 一次能写完所有数据吗?
答:不能保证。可能发生部分写入,需要循环处理剩余数据,并处理 EINTR、EAGAIN 等情况。
13. mmap() 如何判断失败?
答:判断返回值是否等于 MAP_FAILED,而不是判断是否为 NULL。
14. ioctl() 失败如何处理?
答:通常判断返回值是否为 -1,然后读取 errno,结合具体 ioctl 命令分析原因。
七、建议动手练习
练习 1:信号处理
- 使用 sigaction() 注册 SIGINT 和 SIGTERM。
- 按 Ctrl+C 后不在处理函数中直接清理资源。
- 设置退出标志,在主流程中关闭文件描述符并退出。
- 验证 SIGKILL 无法被捕获。
练习 2:错误码
- open() 一个不存在的文件,观察 ENOENT。
- 使用无效 fd 调用 read(),观察 EBADF。
- 输出 errno、strerror(errno) 和 perror() 的结果。
- 理解只有返回值表明失败后才读取 errno。
练习 3:非阻塞 socket
- 创建 TCP socket 并设置 O_NONBLOCK。
- 没有数据时观察 EAGAIN/EWOULDBLOCK。
- 使用 epoll_wait() 等待就绪事件。
- 读取数据直到 EAGAIN。
- 处理 EINTR 和连接关闭。
练习 4:部分读写
- 编写 read_exact()/send_all()。
- 正确处理部分读写、EINTR、EAGAIN。
- TCP 消息使用长度字段或分隔符设计应用层协议,不能依赖一次 recv() 对应一条消息。
八、学习时最需要记住的五组关系
1. sigaction + SIGTERM + 退出标志
让程序能够响应外部退出请求,并在正常执行上下文中清理资源。
2. errno + EINTR
处理被信号中断的系统调用。
3. errno + EAGAIN + epoll
处理非阻塞网络 I/O。
4. read()/write() + 部分读写
理解系统调用只承诺本次实际处理的字节数,不能假设一次完成全部数据。
5. ioctl() + errno
处理 V4L2 摄像头等 Linux 设备接口的控制操作。
对嵌入式 Linux C/C++ 应用开发来说,这些知识会频繁出现在 socket 网络编程、V4L2 采集、线程退出、设备控制和 RK 多媒体程序中。