C语言:为阻塞系统调用设置超时时间
如何给阻塞I/O调用设置超时(避免非阻塞I/O的误统计问题)
嘿,你的需求抓得很准——既要用阻塞I/O来精准统计真实的I/O失败次数,又要让那些卡太久的调用在500ms后报错终止,还因为涉及磁盘文件描述符没法用select/poll这类多路复用工具对吧?我给你分享两个实用的方案:
方案一:用SIGALRM信号实现超时打断
这是最经典的系统级方案,利用Linux的信号机制打断阻塞的系统调用:
注册信号处理函数:用
sigaction()注册SIGALRM的处理函数,这里不需要复杂逻辑,只要让信号能触发并打断系统调用就行(默认SIGALRM会终止进程,所以我们要自定义一个空处理函数)。#include <signal.h> #include <unistd.h> #include <errno.h> static void alarm_handler(int sig) { // 什么都不用做,触发信号就会打断阻塞的系统调用 } // 初始化信号处理 void setup_alarm_handler() { struct sigaction sa; sa.sa_handler = alarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; // 绝对不能设SA_RESTART,否则系统调用会自动重启,达不到超时终止效果 sigaction(SIGALRM, &sa, NULL); }设置毫秒级超时闹钟:因为
alarm()只支持整数秒,我们用setitimer()实现500ms的精确超时:#include <sys/time.h> // 设置500ms超时 void set_timeout() { struct itimerval timer; timer.it_value.tv_sec = 0; timer.it_value.tv_usec = 500000; // 500毫秒 timer.it_interval.tv_sec = 0; timer.it_interval.tv_usec = 0; // 只触发一次超时 setitimer(ITIMER_REAL, &timer, NULL); } // 取消超时,避免影响后续操作 void cancel_timeout() { struct itimerval timer = {0}; setitimer(ITIMER_REAL, &timer, NULL); }执行带超时的I/O调用:
ssize_t timed_read(int fd, void *buf, size_t count) { set_timeout(); ssize_t ret = read(fd, buf, count); cancel_timeout(); // 无论成功失败,都立刻取消超时 if (ret == -1 && errno == EINTR) { // 被SIGALRM打断,说明超时了,我们把错误码改成更直观的ETIMEDOUT errno = ETIMEDOUT; } return ret; }
这样,当read()阻塞超过500ms时,SIGALRM会直接打断它,返回-1且errno为ETIMEDOUT,你可以把这种情况计入超时失败;而真正的I/O错误(比如EIO磁盘读写失败)会正常返回,不会被误统计。
方案二:用线程做超时监控(更灵活,避免信号冲突)
如果你的程序已经在使用SIGALRM做其他事情,或者担心信号处理的副作用,可以用线程来实现独立的超时监控:
- 实现辅助监控线程:创建一个线程,在主线程执行阻塞I/O前启动,等待500ms后如果主线程还在阻塞,就发自定义信号打断它。
#include <pthread.h> #include <signal.h> #include <unistd.h> #include <errno.h> typedef struct { pthread_t main_thread; int timed_out; } MonitorData; static void *timeout_monitor(void *arg) { MonitorData *data = (MonitorData *)arg; usleep(500000); // 等待500ms if (!data->timed_out) { // 主线程还在阻塞,发SIGUSR1信号打断 pthread_kill(data->main_thread, SIGUSR1); } return NULL; } // 注册SIGUSR1的空处理函数 static void usr1_handler(int sig) {} void setup_usr1_handler() { struct sigaction sa; sa.sa_handler = usr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); } ssize_t threaded_timed_read(int fd, void *buf, size_t count) { MonitorData data = { .main_thread = pthread_self(), .timed_out = 0 }; pthread_t monitor_thread; pthread_create(&monitor_thread, NULL, timeout_monitor, &data); ssize_t ret = read(fd, buf, count); data.timed_out = 1; // 标记I/O已完成,避免监控线程发信号 pthread_join(monitor_thread, NULL); if (ret == -1 && errno == EINTR) { errno = ETIMEDOUT; } return ret; }
这个方案的优势是完全独立于其他信号逻辑,不会和SIGALRM之类的信号冲突,还能给不同的I/O操作设置不同的超时时间,灵活性更高。
关键注意事项
- 禁用SA_RESTART:如果在
sigaction里设置了SA_RESTART,被信号打断的系统调用会自动重启,直接失效超时逻辑,一定要去掉这个标志。 - 及时清理超时状态:不管I/O成功还是失败,都要取消闹钟或标记操作完成,避免影响后续的I/O调用。
- 区分超时与真实错误:只有当
errno是EINTR且是我们的超时信号导致的,才计入超时失败;其他错误(比如EIO、EBADF)都是真实的I/O故障,正常统计即可。
这样就能完美满足你的需求:用阻塞I/O保证错误统计的准确性,同时给卡壳的调用设置超时,不会像非阻塞I/O那样频繁返回EAGAIN导致误判。
内容的提问来源于stack exchange,提问作者Anirban
相关产品推荐
相关产品推荐

