C 迷你系列(六)select 与 stdio 混用所带来的问题

引言

在 《UNIX 网络编程》一书 135 页的末尾提到关于 select 与 stdio 相关函数混用的问题。这里我把它单独拿出来,以一个简单的例子说明一下。避免之后的使用中出现类似的问题。

问题根源

两者的缓冲区:

  • 系统 I/O 在内核空间中存在缓冲,而在用户空间没有;
  • stdio 系列函数除了在内核空间中有缓存,在用户空间也有缓冲;

缓冲区类型:

  • 全缓冲(大部分缓冲都是这类型)
  • 行缓冲(例如:stdio、stdout)
  • 无缓冲(例如:stderr)

而具体的问题则是出现在 select 只会检测内核空间中的缓冲区,无法感知用户空间中的缓冲区。当数据从内核空间复制到用户空间的时候,即使该描述符对应的缓存空间有数据,select 也不会再给通知。如图:


image.png

示例

  • 正常输出

#include 
#include 
#include 
#include 

#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 字节内容就是最后的换行符。

我们分析一下从我们输出完并按下回车到显示时,都发生了什么:

  1. 输入回车之后,数据从用户缓冲复制到了内核缓冲(行缓冲);
  2. select 检测到 stdin 对应的内核缓冲有数据可读的时候,解除阻塞;
  3. read 函数取 2 个字节的数据到 buffer 中;
  4. printf 将 buffer 中的数据显示出来,并进行下次循环,阻塞到 select;
  5. 由于内核中还有数据未读完,select 再次解除阻塞,直至数据取完为止;
  • 混用时的问题

#include 
#include 

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]

我们发现输出已经出现问题了,我们继续分析一下该问题是怎么造成的:

  1. 当我们输入 123456 之后,数据由用户空间缓冲复制到了内核缓冲;
  2. select 检测到有数据可读,解除阻塞;
  3. getc 函数从用户缓冲中取 1 字节数据,发现缓冲中无数据可读,于是将内核中的数据复制到用户缓冲,并取 1 字节作为输出;
  4. 此时由于数据已经全部复制到了用户缓冲,所以 select 进入阻塞状态(即使用户空间的缓冲中有数据可读);
  5. 当输出 9 并回车时,该数据又被复制到了内核空间(行缓冲),select 解除阻塞;
  6. getc 函数从用户缓冲中取出 1 字节数据输出(由于用户缓冲中有数据,所以 getc 便不会再从内核中复制数据);
  7. 由于内核中有数据,所以 select 便再解除阻塞,getc 再取 1 字节直到 9 被复制到用户缓冲并输出为止;

Tips

仔细看最后的输出,你会发现 9 之后的换行符还留在用户空间缓冲中,该数据只能等下次再有数据输出到内核空间中才会得到输出。

你可能感兴趣的:(C 迷你系列(六)select 与 stdio 混用所带来的问题)