双线程下阻塞式CAN套接字偶发延迟问题排查求助
问题:CAN通信随机延迟排查与解决
我正在为采用CAN通信协议的CubeMars电机开发库,该库使用两个线程:主线程处理大部分逻辑,监听线程负责监听CAN总线(接收电机的响应与确认包)并保存信息,供主线程后续读取。通常主线程向CAN总线发送指令后,电机发送的确认包会被监听线程捕获保存,主线程等待确认后再继续执行,整个过程耗时约500us。但约1%的随机场景下,耗时会骤升至10ms,打乱控制循环,且问题并非出自电机本身。
使用环境
- CubeMars电机:AK60-6 v.1.1(MIT模式)
- Raspberry 4B(搭载Arch Linux ARM)
- MCP2515 CAN板
- CAN通信,波特率1M
- C++编写代码
相关代码
创建套接字代码
int MotorHandler::openSocket(const char* can_bus) { // Create a CAN socket int s = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (s < 0) { cout << "Error creating the socket! Exiting" << endl; exit(1); } else cout << "Socket created successfully" << endl; // Set socket timeout struct timeval tv; tv.tv_sec = 0; tv.tv_usec = SOCKET_TIMEOUT_US; int success = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); if (success < 0) { cout << "Error setting the timeout to the CAN socket" << endl; exit(1); } // Bind the socket to the CAN bus struct sockaddr_can addr; struct ifreq ifr; strcpy(ifr.ifr_name, can_bus); ioctl(s, SIOCGIFINDEX, &ifr); memset(&addr, 0, sizeof(addr)); addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; int result = bind(s, (struct sockaddr *)&addr, sizeof(addr)); if (result < 0) { cout << "Binding error! Exiting" << endl; exit(1); } else cout << "Binding successful" << endl; return s; }
监听线程函数
int Listener::listenerLoop(int s) { bool stopThread = 0; cout << "Starting CAN bus monitoring" << endl; while(!stopThread) { struct can_frame frame; int nbytes = read(s, &frame, sizeof(can_frame)); if (nbytes > 0) parseFrame(frame); // Thread sleep for scheduling std::this_thread::sleep_for(chrono::microseconds(10)); { scoped_lock lock(m_mutex); stopThread = m_stopThread; } } return 0; }
已尝试操作
- 最初使用非阻塞套接字,单线程控制单个电机时仍有随机延迟;切换为阻塞模式后,问题消失。
- 在双线程库中设置套接字为阻塞模式,随机延迟问题仍存在。
推测问题可能出在缓冲或调度上,尝试过不同线程休眠时长但无效,请求协助排查原因与解决方法。
排查与解决建议
核心问题分析
- 线程调度延迟:Raspberry Pi 4B的Linux内核默认CFS调度器的最小调度粒度受HZ参数影响(Arch Linux ARM默认HZ=100,即10ms),监听线程主动调用
sleep_for(10us)会强制放弃CPU,唤醒时间无法保证微秒级精度,这是10ms延迟的主要来源。 - 阻塞套接字的线程竞争:双线程模式下,监听线程在
read()返回后主动休眠,可能错过电机响应的处理窗口,同时主线程的等待逻辑若存在轮询,也会增加额外开销。 - MCP2515硬件/内核缓冲:若CAN驱动层缓冲配置过小,可能导致帧堆积,后续一次性处理时出现延迟。
具体解决步骤
1. 移除监听线程的主动休眠
阻塞模式下,read()本身会在无数据时挂起线程,无需主动休眠,强制休眠会直接导致调度延迟。修改后的监听线程代码:
int Listener::listenerLoop(int s) { bool stopThread = false; cout << "Starting CAN bus monitoring" << endl; while(!stopThread) { struct can_frame frame; int nbytes = read(s, &frame, sizeof(can_frame)); if (nbytes > 0) { parseFrame(frame); } // 仅在检查停止标志时加锁,减少锁开销 { scoped_lock lock(m_mutex); stopThread = m_stopThread; } } return 0; }
2. 设置监听线程为实时调度优先级
避免监听线程被低优先级线程抢占,保证响应及时性:
#include <pthread.h> // 在监听线程启动后调用此函数 void setRealtimePriority(pthread_t thread) { struct sched_param param; param.sched_priority = sched_get_priority_max(SCHED_FIFO); pthread_setschedparam(thread, SCHED_FIFO, ¶m); }
注意:需以root权限运行程序,或给程序添加
CAP_SYS_NICE权限才能设置实时优先级。
3. 调整CAN套接字与内核缓冲
- 临时调整CAN总线发送队列长度:
ip link set can0 txqueuelen 1000
- 永久生效:在
/etc/network/interfaces.d/can0中添加post-up ip link set can0 txqueuelen 1000 - 在
openSocket函数中设置套接字接收缓冲:
// bind操作完成后添加 int rcv_buf_size = 1024 * 1024; setsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, sizeof(rcv_buf_size));
4. 优化主线程与监听线程的同步机制
将轮询等待改为条件变量通知,减少无效轮询开销:
// 监听线程解析到确认帧时 void parseFrame(struct can_frame frame) { // 帧处理逻辑... if (isAckFrame(frame)) { scoped_lock lock(m_mutex); m_ackReceived = true; m_cv.notify_one(); } } // 主线程等待确认 void waitForAck() { unique_lock lock(m_mutex); m_cv.wait(lock, []{ return m_ackReceived; }); m_ackReceived = false; // 重置标志 }
5. 优化MCP2515 SPI参数
提升SPI总线速度,避免SPI传输瓶颈:
# 临时生效 echo 8000000 > /sys/bus/spi/devices/spi0.0/max_speed_hz
验证方法
使用perf工具分析线程调度延迟:
perf record -g -p <监听线程PID> perf report
查看是否存在大量sched_switch事件导致线程被抢占,或hrtimer相关的延迟。
内容的提问来源于stack exchange,提问作者Katarina
相关产品推荐
相关产品推荐

