在系统编程中,尤其是文件 I/O 操作中,我们经常需要处理大量数据的读取和写入。为了提高效率,常常使用缓冲机制。本文将详细探讨缓冲性质函数和非缓冲性质函数的区别,以及为何不能在同一文件描述符上交替使用这两种类型的函数。


一、什么是缓冲性质函数?

缓冲性质函数是指那些在内部维护一个缓冲区,用于存储从文件描述符读取的数据的函数。这些函数一次读取多个字节的数据到缓冲区中,然后根据用户的请求从缓冲区中返回所需的数据。这种机制可以减少系统调用的次数,从而提高 I/O 操作的效率。

例如,假设我们有一个 rio_readnb 函数,其功能是从文件描述符 rp 读取最多 n 字节的数据到内存位置 usrbuf。该函数可能会如下实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ssize_t rio_readnb(int fd, void *usrbuf, size_t n) {
    // 内部维护一个缓冲区
    static char buffer[BUFFER_SIZE];
    static size_t buffer_count = 0;
    static char *buffer_ptr = buffer;

    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;

    while (nleft > 0) {
        if (buffer_count == 0) {
            // 缓冲区为空,从文件描述符读取数据填充缓冲区
            if ((nread = read(fd, buffer, sizeof(buffer))) < 0) {
                if (errno == EINTR) {
                    continue; // 处理信号中断,重试读取
                } else {
                    return -1; // 读取错误
                }
            } else if (nread == 0) {
                break; // 文件结束
            }
            buffer_ptr = buffer;
            buffer_count = nread;
        }

        // 从缓冲区读取数据到用户缓冲区
        size_t cnt = (nleft < buffer_count) ? nleft : buffer_count;
        memcpy(bufp, buffer_ptr, cnt);
        bufp += cnt;
        buffer_ptr += cnt;
        buffer_count -= cnt;
        nleft -= cnt;
    }

    return (n - nleft); // 返回读取的字节数
}

二、什么是非缓冲性质函数?

非缓冲性质函数则不维护任何内部缓冲区,而是每次调用时直接从文件描述符读取数据并返回给用户。这些函数每次调用都触发系统调用,从而直接从文件描述符读取数据。

例如,假设我们有一个 rio_readn 函数,其功能是直接从文件描述符读取 n 字节的数据到内存位置 usrbuf,可能会如下实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
ssize_t rio_readn(int fd, void *usrbuf, size_t n) {
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;

    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {
            if (errno == EINTR) {
                nread = 0; // 处理信号中断,重新读取
            } else {
                return -1; // 读取错误
            }
        } else if (nread == 0) {
            break; // 文件结束
        }
        nleft -= nread;
        bufp += nread;
    }

    return (n - nleft); // 返回读取的字节数
}

三、缓冲性质函数与非缓冲性质函数为何不能交替使用?

缓冲性质函数和非缓冲性质函数在同一文件描述符上交替使用会导致数据读取的不一致和错误。这是因为这两种类型的函数在处理文件描述符时的内部机制不同。

3.1 数据不一致的问题

缓冲性质函数在读取数据时,会一次性从文件描述符读取多个字节的数据到内部缓冲区,然后再从缓冲区返回数据给用户。这意味着文件描述符的读指针会因为读取操作而移动到缓冲区数据的末尾位置。

如果接着调用非缓冲性质函数,由于它不维护缓冲区,会直接从文件描述符的当前位置读取数据。这会导致以下问题:

  1. 跳过数据:缓冲性质函数已经读取但尚未处理的数据会被非缓冲性质函数跳过,导致数据丢失。
  2. 重复读取:非缓冲性质函数移动了文件描述符的读指针,缓冲性质函数的缓冲区数据可能重复被读取。

3.2 示例说明

假设文件内容如下(每个字母代表一个字节):

1
abcdef
  1. 缓冲性质函数 rio_readnb 读取 4 个字节

    • 内部缓冲区读取 abcd,并返回 abcd
    • 缓冲区现在可能还包含剩余的 ef
  2. 接着调用非缓冲性质函数 rio_readn 读取 2 个字节

    • 直接从文件描述符当前位置(即 ef)读取。
    • 返回 ef
  3. 再次调用 rio_readnb 读取 2 个字节

    • 缓冲区已失效或被跳过,因为 rio_readn 直接从文件描述符读取,缓冲区未更新。
    • 可能导致数据不一致或错误读取。

3.3 正确的使用方法

为了避免上述问题,应该遵循以下原则:

  1. 保持一致性:在处理同一文件描述符时,要么始终使用缓冲性质函数,要么始终使用非缓冲性质函数。
  2. 避免交替使用:不要在同一文件描述符上交替使用这两种类型的函数,以确保数据读取的准确性和一致性。

结论

缓冲性质函数和非缓冲性质函数在处理文件描述符时的内部机制不同,交替使用可能导致数据不一致和错误。因此,在编写系统 I/O 操作时,应注意选择一致的函数类型,以确保数据读取的正确性和程序的稳定性。