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

如何终止GNU/Linux下SocketCAN+C++中的阻塞read调用

解决SocketCAN阻塞read的优雅终止问题

我之前在做SocketCAN项目时也碰到过一模一样的问题——阻塞的read()调用卡在那儿,没法在满足自定义条件时干净利落地退出程序,总不能一直靠Ctrl+C吧!下面几个方案都是我实际用过的,亲测有效:

方案1:给套接字设置接收超时(SO_RCVTIMEO)

这是最简单的办法,适合需要“记录X秒就退出”这类时间驱动的场景。给CAN套接字设置超时后,read()会在指定时间内没收到数据时返回-1,同时errno设为EAGAIN或EWOULDBLOCK。我们可以利用这个间隙检查终止条件,比如标记位是否被置位。

示例代码:

#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <errno.h>
#include <chrono>
#include <atomic>

std::atomic<bool> should_exit(false); // 线程安全的终止标记

int main() {
    // 初始化CAN套接字(省略绑定等常规步骤,假设已成功创建s)
    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    struct ifreq ifr;
    strcpy(ifr.ifr_name, "can0");
    ioctl(s, SIOCGIFINDEX, &ifr);
    struct sockaddr_can addr;
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    bind(s, (struct sockaddr*)&addr, sizeof(addr));

    // 设置接收超时:每次read最多等待100ms
    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 100000;
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

    struct can_frame frame;
    auto start_time = std::chrono::steady_clock::now();

    while (!should_exit) {
        // 检查是否已记录满5秒
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
            std::chrono::steady_clock::now() - start_time
        ).count();
        if (elapsed >= 5) {
            should_exit = true;
            break;
        }

        int nbytes = read(s, &frame, sizeof(struct can_frame));
        if (nbytes > 0) {
            // 处理收到的CAN帧
            printf("Received frame: ID=0x%X, DLC=%d\n", frame.can_id, frame.can_dlc);
        } else if (nbytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
            // 非超时类错误,退出
            perror("read error");
            break;
        }
        // 超时则直接进入下一轮循环,检查终止标记
    }

    close(s);
    return 0;
}

这个方案的好处是代码改动小,不需要额外线程或复杂逻辑。如果是事件触发的终止,只需要在其他地方把should_exit设为true即可。

方案2:用select/poll实现多路监听

如果你的终止条件更复杂(比如同时监听CAN总线和其他事件,如用户输入、外部触发信号),select()或poll()是更好的选择。我们可以把CAN套接字和一个“终止通知”的文件描述符(比如管道的读端)一起加入监听集合,当需要退出时,往管道写端发一个字节,select()就会返回,进而退出循环。

示例代码:

#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <errno.h>
#include <thread>

int main() {
    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    // 绑定CAN套接字(省略)

    // 创建管道用于终止通知
    int pipe_fd[2];
    pipe(pipe_fd);

    fd_set read_fds;
    int max_fd = std::max(s, pipe_fd[0]);

    struct can_frame frame;
    bool running = true;

    // 模拟终止条件:5秒后触发终止
    std::thread([&]() {
        sleep(5);
        write(pipe_fd[1], "x", 1);
    }).detach();

    while (running) {
        FD_ZERO(&read_fds);
        FD_SET(s, &read_fds);
        FD_SET(pipe_fd[0], &read_fds);

        int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if (ret == -1) {
            perror("select error");
            break;
        }

        if (FD_ISSET(s, &read_fds)) {
            int nbytes = read(s, &frame, sizeof(struct can_frame));
            if (nbytes > 0) {
                // 处理CAN帧
            }
        }

        if (FD_ISSET(pipe_fd[0], &read_fds)) {
            // 收到终止信号,退出循环
            char buf;
            read(pipe_fd[0], &buf, 1);
            running = false;
        }
    }

    close(pipe_fd[0]);
    close(pipe_fd[1]);
    close(s);
    return 0;
}

这个方案灵活性极高,不管是时间触发、外部事件还是标记位,都可以通过往管道写数据来触发终止。如果是高并发场景,用epoll性能会更好,但select已经足够应对大多数SocketCAN的使用场景。

方案3:信号处理(谨慎使用)

如果你想模拟Ctrl+C的行为,自己发送一个信号打断read()也是可行的。比如用SIGUSR1自定义信号,在终止条件满足时调用kill(getpid(), SIGUSR1),然后在信号处理函数里设置终止标记。

注意:信号处理函数里只能调用异步信号安全的函数(比如write()、_exit(),不能调用printf()、malloc()这类),所以最好只在信号处理函数里设置一个volatile标记位,主循环里检查这个标记。

示例代码:

#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <signal.h>
#include <errno.h>
#include <thread>

volatile sig_atomic_t should_exit = 0;

void sig_handler(int sig) {
    should_exit = 1;
}

int main() {
    // 注册自定义信号处理函数
    signal(SIGUSR1, sig_handler);

    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    // 绑定CAN套接字(省略)

    struct can_frame frame;

    // 模拟终止条件:5秒后发送SIGUSR1
    std::thread([&]() {
        sleep(5);
        kill(getpid(), SIGUSR1);
    }).detach();

    while (!should_exit) {
        int nbytes = read(s, &frame, sizeof(struct can_frame));
        if (nbytes == -1) {
            if (errno == EINTR) {
                // read被信号打断,退出循环
                break;
            } else {
                perror("read error");
                break;
            }
        }
        // 处理CAN帧
    }

    close(s);
    return 0;
}

这个方案要特别注意信号的异步安全性,而且如果read()在读取数据中途被打断,可能会出现部分读取的情况,需要额外处理。除非你特别需要用信号,否则前两个方案更稳妥。


总结一下:

  • 如果只是时间驱动或简单标记位,用SO_RCVTIMEO最省心;
  • 如果需要同时监听多个事件,选select/poll;
  • 信号处理适合需要模拟终端信号的场景,但要注意安全。

内容的提问来源于stack exchange,提问作者dvilela

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 10:02:37