1 问题描述
这是在一个 生产者-消费者 场景下的问题。消费者A不断从生产者B那里读取数据,在收到的数据乱序时,A会重启B,即A向B先后发送 stop 和 start 指令。
这里的逻辑是,A处理数据发现数据乱序则发送 stop 指令,置标志位,在下一次处理数据时检测标志位和ACK,并发送 start。
预期两条指令相差1s 左右,但压测中发现,时差达到了 10s,严重影响性能。
2 问题排查
怀疑:在问题排查过程中,首先对外部因素进行了初步排查,例如怀疑生产者未及时对 stop 指令返回 ACK 响应。经核实,该可能性被排除。
验证:随后转向对接收机制的验证,确认本方系统实际已收到 ACK,但通过日志记录发现其接收时间显著延迟。
定位:进一步定位至底层串口读写操作,即基于 POSIX 标准的 read() 和 write() 系统调用。问题根源在于 read() 调用处于阻塞状态没能及时返回。猜测是由于 ACK 报文长度较短,未达到读取阈值,导致函数无法及时返回。在此期间,消费者 A 逐渐累积收到若干心跳信号(同样为短帧),直至总数据量满足最小读取要求,read() 才得以返回。
验证:为验证该判断,将 read(int fd, void *buf, size_t count) 的 count 参数调整成1后,问题得到暂时解决。但该调整会导致 read() 调用频繁返回,增加系统处理负荷,对整体性能产生显著压力,不具备可行性。进一步尝试中发现,即便逐步降低 count,等待时间仍稳定在 10 秒左右并未减少,该现象与预期不符。后来发现,串口 read() 的 conut 参数仅代表希望读取的最大字节数,即使数据量比 count 小,理论上也能正常返回。
最终:在源文件的某个角落藏着两个配置参数 VMIN 和 VTIME,它俩控制这 read() 的超时机制,分别为 最小读取字节数 和 超时时间。根本原因找到,将超时时间改成一个合理的小的值,问题解决。
3 系统调用 read()
3.1 read() 函数
read() 是一个用于从文件描述符(fd)中读取数据的系统调用。它不仅可以用于普通文件,也常用于设备(如串口)、管道、socket等。
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);参数:
fd:文件描述符,标识要读取的对象(如文件、串口)
buf:指向存储读取数据的缓冲区指针
count:期望读取的最大字节数
返回值:
成功时返回实际读取到的字节数;
如果到达文件末尾,返回i0;
失败时返回 -1,并设置 errno 以指示错误类型
代码示例:
int fd = open("/dev/ttyS0", O_RDONLY);
char buffer[100];
ssize_t bytesRead = read(fd, buffer, sizeod(buffer));
if (byteRead > 0) {
// ...
}3.2 串口控制参数 VMIN、VTIME
在串口编程中(如termios接口),VIM和VTIME是用来控制非范式模式和(即原始模式)下 read() 行为的两个重要参数。
termios 结构体:
struct termios {
...
cc_t c_cc[NCCS]; // 控制字符数组
...
}其中,c_cc[VMIN] 和 c_cc[VTIME] 分别表示最小读取字节数和超时时间。
VMIN:指定了 read() 调用时,必须读取到的最小字节数才返回。
VTIME:指定了等待数据的超时时间,以 0.1秒 为单位。
组合行为:
代码示例:
#include <termios.h>
struct termios options;
tcgetattr(fd, &options);
options.c_cc[VMIN] = 5; // 最少读取5字节
options.c_cc[VTIME] = 2; // 超时 2*0.1 秒
tcsetattr(fd, TCSANOW, &options);
评论区