C 迷你系列(六)select 与 stdio 混用所带来的问题
2021-08-09 本文已影响0人
Tubetrue01
引言
在 《UNIX 网络编程》一书 135 页的末尾提到关于 select 与 stdio 相关函数混用的问题。这里我把它单独拿出来,以一个简单的例子说明一下。避免之后的使用中出现类似的问题。
问题根源
两者的缓冲区:
- 系统 I/O 在内核空间中存在缓冲,而在用户空间没有;
- stdio 系列函数除了在内核空间中有缓存,在用户空间也有缓冲;
缓冲区类型:
- 全缓冲(大部分缓冲都是这类型)
- 行缓冲(例如:stdio、stdout)
- 无缓冲(例如:stderr)
而具体的问题则是出现在 select 只会检测内核空间中的缓冲区,无法感知用户空间中的缓冲区。当数据从内核空间复制到用户空间的时候,即使该描述符对应的缓存空间有数据,select 也不会再给通知。如图:
image.png
示例
-
正常输出
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
#include <string.h>
#define BUFFER 3
#define BUFFER_LEN (BUFFER - 1)
int main()
{
int n;
fd_set rset;
char buffer[BUFFER];
FD_ZERO(&rset);
for (;;)
{
FD_SET(fileno(stdin), &rset);
select(fileno(stdin) + 1, &rset, NULL, NULL, NULL);
n = read(fileno(stdin), buffer, BUFFER_LEN);
printf("读取到:[%d] 字节,内容为:[%s]\n", n, buffer);
memset(buffer, 0, sizeof(buffer));
}
}
--- input
123456
--- output
读取到:[2] 字节,内容为:[12]
读取到:[2] 字节,内容为:[34]
读取到:[2] 字节,内容为:[56]
读取到:[1] 字节,内容为:[
]
📚 Tips
我们分配 3 字节大小的缓冲区,然后再每次读取玩缓冲中的数据之后,将缓冲中的数据清空,避免影响输出。当我们输入:123456 并按回车换行时(实际:123456\n),内容依次输出了。最后的 1 字节内容就是最后的换行符。
我们分析一下从我们输出完并按下回车到显示时,都发生了什么:
- 输入回车之后,数据从用户缓冲复制到了内核缓冲(行缓冲);
- select 检测到 stdin 对应的内核缓冲有数据可读的时候,解除阻塞;
- read 函数取 2 个字节的数据到 buffer 中;
- printf 将 buffer 中的数据显示出来,并进行下次循环,阻塞到 select;
- 由于内核中还有数据未读完,select 再次解除阻塞,直至数据取完为止;
-
混用时的问题
#include <stdio.h>
#include <sys/select.h>
int main()
{
int n;
fd_set rset;
FD_ZERO(&rset);
for (;;)
{
FD_SET(fileno(stdin), &rset);
select(fileno(stdin) + 1, &rset, NULL, NULL, NULL);
n = getc(stdin);
printf("内容为:[%c]\n", n);
}
}
---
intput: 123456
output: 内容为:[1]
intput: 9
output: 内容为:[2]
output: 内容为:[3]
output: 内容为:[4]
output: 内容为:[5]
output: 内容为:[6]
output: 内容为:[
output: ]
output: 内容为:[9]
我们发现输出已经出现问题了,我们继续分析一下该问题是怎么造成的:
- 当我们输入 123456 之后,数据由用户空间缓冲复制到了内核缓冲;
- select 检测到有数据可读,解除阻塞;
- getc 函数从用户缓冲中取 1 字节数据,发现缓冲中无数据可读,于是将内核中的数据复制到用户缓冲,并取 1 字节作为输出;
- 此时由于数据已经全部复制到了用户缓冲,所以 select 进入阻塞状态(即使用户空间的缓冲中有数据可读);
- 当输出 9 并回车时,该数据又被复制到了内核空间(行缓冲),select 解除阻塞;
- getc 函数从用户缓冲中取出 1 字节数据输出(由于用户缓冲中有数据,所以 getc 便不会再从内核中复制数据);
- 由于内核中有数据,所以 select 便再解除阻塞,getc 再取 1 字节直到 9 被复制到用户缓冲并输出为止;
📚 Tips
仔细看最后的输出,你会发现 9 之后的换行符还留在用户空间缓冲中,该数据只能等下次再有数据输出到内核空间中才会得到输出。