本节书摘来自异步社区《Linux系统编程(第2版)》一书中的第2章,第2.2节,作者:【美】Robert Love著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.2 通过read()读文件
前面讨论了如何打开文件,现在一起来看如何读文件。在接下来的一节中,我们将讨论写操作。
最基础、最常见的读取文件机制是调用read(),该系统调用在POSIX.1中定义如下:
每次调用read()函数,会从fd指向的文件的当前偏移开始读取len字节到buf所指向的内存中。执行成功时,返回写入buf中的字节数;出错时,返回-1,并设置errno值。fd的文件位置指针会向前移动,移动的长度由读取到的字节数决定。如果fd所指向的对象不支持seek操作(比如字符设备文件),则读操作总是从“当前”位置开始。
基本用法很简单。下面这个例子就是从文件描述符fd所指向的文件中读取数据并保存到word中。读取的字节数即unsigned long类型的大小,在Linux的32位系统上是4字节,在64位系统上是8字节。成功时,返回读取的字节数;出错时,返回-1:
这个简单的实现存在两个问题:可能还没有读取len字节,调用就返回了,而且可能产生某些可操作的错误,但这段代码没有检查和处理。不幸的是,类似这样的代码非常普遍。我们一起看看如何改进它。
2.2.1 返回值
对于read()而言,返回小于len的非零正整数是合法的。在很多情况下会出现该现象:可用的字节数少于len,系统调用可能被信号打断,管道可能被破坏(如果fd指向的是管道)等。
使用read()时,还需要考虑返回值为0的情况。当到达文件末尾(end-of-file, EOF)时,read()返回0,在这种情况下,没有读取任何字节。EOF并不表示出错(因此返回值不是-1),它仅仅表示文件位置已经到达文件结尾,因此没有数据可读了。但是,如果调用是要读取len个字节,但是没有一个字节可读,调用会阻塞(sleep),直到有数据可读(假定文件描述符不是以非阻塞模式打开的,参见2.2.3小节)。注意,这种阻塞模式和返回EOF不同。也就是说,“没有数据可读”和“到达数据结尾”是两个不同的概念。对于EOF,表示到达了文件的结尾。对于阻塞模式,表示读操作在等待更多的数据——例如从socket或设备文件读取数据。
有些错误是可以恢复的。比如,当read()调用在读取任何字节之前被信号打断,它会返回-1(如果返回0,则无法和EOF的情况区分开),并把errno值设置成EINTR。在这种情况下,可以而且应该重新提交read请求。
实际上,调用read()有很多可能结果:
- 调用返回值等于len。读取到的所有len个字节都被存储在buf中。结果和预期的一致。
- 调用返回值小于len,大于0。读取到的字节被存储到buf中。这种情况有很多原因,比如在读取过程中信号中断或在读取中出错,可读的数据大于0字节小于len字节,在读取len字节之前到达EOF。再次执行read(分别更新了buf和len值)会把剩余的字节读到缓冲区中或者给出错误信息。
- 调用返回0,表示EOF,没有更多可读的数据。
- 由于当前没有数据可用,调用阻塞。在非阻塞模式下,不会发生这种情况。
- 调用返回-1,并把errno设置成EINTR。这表示在读取任何字节之前接收到信号。调用可以重新执行。
- 调用返回-1,并把errno设置成EAGAIN。这表示由于当前没有数据可用,读操作会阻塞,请求应该稍后再重新执行。这种情况只在非阻塞模式下发生。
- 调用返回-1,并把errno设置成非EINTR或EAGAIN的一个值。这表示更严重的错误。重新执行读操作不会成功。
2.2.2 读入所有字节
诚如前面所描述的,由于调用read()会有很多不同情况,如果希望处理所有错误并且真正每次读入len个字节(至少读到EOF),那么之前简单“粗暴”的read()调用并不合理。要实现这一点,需要有个循环和一些条件语句,如下:
这段代码判断处理了五种情况。循环从fd所指向的当前文件位置读入len个字节到buf中,一直读完所有len个字节或者EOF为止。如果读入的字节数大于0但小于len,就从len中减去已读字节数,buf增加相应的字节数,并重新调用read()。如果调用返回-1,并且errno值为EINTR,会重新调用且不更新参数。如果调用返回-1,并且errno设置为其他值,会调用perror(),向标准错误打印一条描述,循环结束。
读取数据采用部分读入的方式不但可行,而且还很常见。但是,由于很多开发人员没有正确检查处理这种很短的读入请求,带来了无数bug。请不要成为其中一员!
2.2.3 非阻塞读
有时,开发人员不希望read()调用在没有数据可读时阻塞在那里。相反地,他们希望调用立即返回,表示没有数据可读。这种方式称为非阻塞I/O,它支持应用以非阻塞模式执行I/O操作,因而如果是读取多个文件,以防错过其他文件中的可用数据。
因此,需要额外检查errno值是否为EAGAIN。正如前面所讨论的,如果文件描述符以非阻塞模式打开(即open()调用中指定参数为O_NONBLOCK,参见2.1.1节中的“open()调用的参数”),并且没有数据可读,read()调用会返回-1,并设置errno值为EAGAIN,而不是阻塞模式。当以非阻塞模式读文件时,必须检查EAGAIN,否则可能因为丢失数据导致严重错误。你可能会用到如下代码:
处理EAGAIN的情况和处理EINTR的方式不同(用了goto start)。也许你并不需要采用非阻塞I/O。非阻塞I/O的意义在于捕捉EAGAIN的情况,并执行其他逻辑。
2.2.4 其他错误码
其他错误码指的是编程错误或(对EIO而言)底层问题。read()调用执行失败后,可能的errno值包括:
EBADF
给定的文件描述符非法或不是以可读模式打开。
EFAULT
buf指针不在调用进程的地址空间内。
EINVAL
文件描述符所指向的对象不允许读。
EIO
底层I/O错误。
2.2.5 read()调用的大小限制
类型size_t和ssize_t是由POSIX确定的。类型size_t保存字节大小,类型ssize_t是有符号的size_t(负值用于表示错误)。在32位系统上,对应的C类型通常是unsigned int和int。因为这两种类型常常一起使用,ssize_t的范围更小,往往限制了size_t的范围。
size_t的最大值是SIZE_MAX,ssize_t的最大值是SSIZE_MAX。如果len值大于SSIZE_MAX,read()调用的结果是未定义的。在大多数Linux系统上,SSIZE_MAX的值是LONG_MAX,在32位系统上这个值是2 147 483 647。这个数值对于一次读操作而言已经很大了,但还是需要留心它。如果使用之前的读循环作为通用的读方式,可能需要给它增加以下代码:
调用read()时如果len参数为0,会立即返回,且返回值为0。