如何终止GNU/Linux下SocketCAN+C++中的阻塞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

