You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C语言:为阻塞系统调用设置超时时间

如何给阻塞I/O调用设置超时(避免非阻塞I/O的误统计问题)

嘿,你的需求抓得很准——既要用阻塞I/O来精准统计真实的I/O失败次数,又要让那些卡太久的调用在500ms后报错终止,还因为涉及磁盘文件描述符没法用select/poll这类多路复用工具对吧?我给你分享两个实用的方案:

方案一:用SIGALRM信号实现超时打断

这是最经典的系统级方案,利用Linux的信号机制打断阻塞的系统调用:

  1. 注册信号处理函数:用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);
    }
    
  2. 设置毫秒级超时闹钟:因为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);
    }
    
  3. 执行带超时的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做其他事情,或者担心信号处理的副作用,可以用线程来实现独立的超时监控:

  1. 实现辅助监控线程:创建一个线程,在主线程执行阻塞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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 09:13:03