引言
在 《UNIX 网络编程》一书 135 页的末尾提到关于 select 与 stdio 相关函数混用的问题。这里我把它单独拿出来,以一个简单的例子说明一下。避免之后的使用中出现类似的问题。
问题根源
两者的缓冲区:
- 系统 I/O 在内核空间中存在缓冲,而在用户空间没有;
- stdio 系列函数除了在内核空间中有缓存,在用户空间也有缓冲;
缓冲区类型:
- 全缓冲(大部分缓冲都是这类型)
- 行缓冲(例如:stdio、stdout)
- 无缓冲(例如:stderr)
而具体的问题则是出现在 select 只会检测内核空间中的缓冲区,无法感知用户空间中的缓冲区。当数据从内核空间复制到用户空间的时候,即使该描述符对应的缓存空间有数据,select 也不会再给通知。如图:
select%E4%B8%8Estdio%E6%B7%B7%E7%94%A8%E6%89%80%E5%B8%A6%E6%9D%A5%E7%9A%84%E9%97%AE%E9%A2%98/info.png)
示例
1 |
|
📚 Tips
我们分配 3 字节大小的缓冲区,然后再每次读取玩缓冲中的数据之后,将缓冲中的数据清空,避免影响输出。当我们输入:123456 并按回车换行时(实际:123456\n),内容依次输出了。最后的 1 字节内容就是最后的换行符。
我们分析一下从我们输出完并按下回车到显示时,都发生了什么:
- 输入回车之后,数据从用户缓冲复制到了内核缓冲(行缓冲);
- select 检测到 stdin 对应的内核缓冲有数据可读的时候,解除阻塞;
- read 函数取 2 个字节的数据到 buffer 中;
- printf 将 buffer 中的数据显示出来,并进行下次循环,阻塞到 select;
- 由于内核中还有数据未读完,select 再次解除阻塞,直至数据取完为止;
1 |
|
我们发现输出已经出现问题了,我们继续分析一下该问题是怎么造成的:
- 当我们输入 123456 之后,数据由用户空间缓冲复制到了内核缓冲;
- select 检测到有数据可读,解除阻塞;
- getc 函数从用户缓冲中取 1 字节数据,发现缓冲中无数据可读,于是将内核中的数据复制到用户缓冲,并取 1 字节作为输出;
- 此时由于数据已经全部复制到了用户缓冲,所以 select 进入阻塞状态(即使用户空间的缓冲中有数据可读);
- 当输出 9 并回车时,该数据又被复制到了内核空间(行缓冲),select 解除阻塞;
- getc 函数从用户缓冲中取出 1 字节数据输出(由于用户缓冲中有数据,所以 getc 便不会再从内核中复制数据);
- 由于内核中有数据,所以 select 便再解除阻塞,getc 再取 1 字节直到 9 被复制到用户缓冲并输出为止;
📚 Tips
仔细看最后的输出,你会发现 9 之后的换行符还留在用户空间缓冲中,该数据只能等下次再有数据输出到内核空间中才会得到输出。