ALSA:如何可靠启动USB原始MIDI的接收(Rx)功能?
问题背景
我需要从Linux电脑向带USB MIDI接口的设备发送MIDI SysEx消息,流程是发送消息后接收响应,示例代码如下:
handle = open("/dev/midiXxx", O_RDWR); write(f, "\xF0...\xF7", LENGTH_OF_MESSAGE_1); read(f, response1, sizeof(response1)); write(f, "\xF0...\xF7", LENGTH_OF_MESSAGE_1); read(f, response2, sizeof(response2));
但首次调用read()有时会挂起,且发现MIDI驱动会丢弃首次read()调用前收到的所有字节——如果设备在程序调用read()前就发送了响应(比如首次write()和read()之间发生任务切换),响应会直接丢失。
该问题在以下场景均会出现:
- 使用
/dev/midiXxx设备文件 - 直接打开
/dev/snd/midiCXxxDxxx并调用read()/write() - 使用ALSA的
snd_rawmidi_Xxx()API替代原生open()/read()/write()(内部仍访问/dev/snd/midiCXxxDxxx)
核心问题:确保MIDI数据被正确读取的可靠方法是什么?
我试过两种可行方法:
- 先以
O_NONBLOCK模式打开设备,调用read()(返回EWOULDBLOCK)后,再用fcntl(F_SETFL)取消非阻塞模式(程序其余部分不需要该模式) - 在
write()前调用带POLLIN参数的poll()
但不确定这些方法在Linux版本更新后是否仍能可靠工作。
可靠解决方案及分析
推荐方案:全程非阻塞模式+事件监听
你尝试的两种方法均有合理性,但更可靠的做法是全程使用非阻塞模式配合poll()/select(),而非临时切换模式,可避免模式切换带来的竞态问题:
- 打开设备时直接指定
O_RDWR | O_NONBLOCK - 发送SysEx消息后,用
poll()监听设备的POLLIN事件,直到有数据可读或超时 - 有数据时调用
read()读取,循环读取直到获取完整响应(注意SysEx消息以0xF7结尾,需判断消息完整性)
示例代码片段:
#include <poll.h> #include <fcntl.h> // 打开设备 int handle = open("/dev/snd/midiC0D0", O_RDWR | O_NONBLOCK); if (handle < 0) { // 错误处理逻辑 } // 发送SysEx消息 write(handle, "\xF0...\xF7", LENGTH_OF_MESSAGE); // 等待响应 struct pollfd pfd = { .fd = handle, .events = POLLIN }; // 超时时间设为1秒,可根据设备响应速度调整 int ret = poll(&pfd, 1, 1000); if (ret > 0 && (pfd.revents & POLLIN)) { unsigned char buf[256]; ssize_t bytes_read; // 循环读取直到获取完整SysEx响应 while ((bytes_read = read(handle, buf, sizeof(buf))) > 0) { // 处理读取到的数据,检查是否以0xF7结尾 // ... } } else if (ret == 0) { // 超时处理逻辑 } else { // poll调用错误处理逻辑 }
临时切换非阻塞模式的隐患
临时用fcntl切换模式的做法存在竞态窗口:在取消O_NONBLOCK后,如果设备刚好此时发送数据,而程序还未调用read(),数据仍可能被丢弃(取决于驱动缓冲机制)。全程非阻塞+事件监听可避免该问题。
write()前调用poll()的注意事项
在write()前调用poll(POLLIN)的目的是清空驱动缓冲区中的残留旧数据,逻辑合理,但需注意:
- 调用
poll()后若有数据,必须将所有残留数据读完,否则旧数据会干扰后续响应读取 - 该步骤仅需在第一次发送消息前执行一次,无需每次发送前重复操作
驱动层面的本质原因
Linux ALSA的rawmidi驱动默认不会缓冲read()调用前收到的数据,只有当程序调用read()(或通过poll()注册监听)后,驱动才会开始缓存设备发来的数据。这就是提前到达的响应会被丢弃的核心原因——此时程序尚未通知驱动要接收数据。
因此核心逻辑是:必须在发送请求前,让驱动知晓你要接收数据,要么通过提前注册poll()监听,要么通过打开非阻塞模式并持续尝试读取(poll()的效率更高)。
内容的提问来源于stack exchange,提问作者Martin Rosenau

