美文网首页
C 迷你系列(六)select 与 stdio 混用所带来的问题

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

作者: Tubetrue01 | 来源:发表于2021-08-09 18:49 被阅读0次

    引言

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

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

    1. 输入回车之后,数据从用户缓冲复制到了内核缓冲(行缓冲);
    2. select 检测到 stdin 对应的内核缓冲有数据可读的时候,解除阻塞;
    3. read 函数取 2 个字节的数据到 buffer 中;
    4. printf 将 buffer 中的数据显示出来,并进行下次循环,阻塞到 select;
    5. 由于内核中还有数据未读完,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]
    
    

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

    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 混用所带来的问题

          本文链接:https://www.haomeiwen.com/subject/jsttbltx.html