引言
本章先说明可用的文件I/O函数——打开文件、读文件、写文件等。UNIX系统种的大多数文件I/O只需要用到5个函数:open、read、write、lseek以及close。然后说明不同缓冲区长度对read和write函数的影响。
本章描述的函数被称为不带缓冲的I/O(unbuffered I/O)。术语不带缓冲指的是每个read和write都调用内核的一个系统调用。
只要涉及多个进程资源共享资源,原子操作的概念就非常重要。
文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。读写文件时,使用open返回的文件描述符标识该文件,将其作为参数传递给read或write。
UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。
函数open和openat
调用open和openat函数可以打开或创建一个文件。
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);
对于open函数,仅当创建新文件时才使用最后的可变参数。
path参数时要打开或创建的文件的名字。
oflag参数用下列一个或多个常量进行或运算。
- O_RDONLY 只读打开
- O_WRONLY 只写打开
- O_RDWR 读、写打开
- O_EXEC 只执行打开
这几个常量必须指定一个且只能指定一个。
下面是可选的。
- O_APPEND 每次写都追加到文件的尾端。
- O_CREAT 若文件不存在则创建,使用此选项,需要同时说明mode参数,用mode指定新文件的访问权限位。
- O_EXCL 如果同时指定了O_CREAT,而文件已存在则出错。
- O_NONBLOCK 如果path是一个FIFO、一个块特殊文件,则此选项为文件的本次打开操作和后续I/O操作设置为非阻塞方式。
- O_SYNC 使每次write等待物理I/O操作完成。
- O_TRUNC 如果此文件存在,而且为只写或读写方式打开,则将其长度截断为0
由open和openat返回的文件描述符一定是最小的未用描述符值。
fd参数将open和openat函数区分开,由3种情况:
1)path参数指定绝对路径名,此时,fd参数被忽略。openat函数和open函数相同。
2)path参数指定相对路径名,fd参数指定了相对路径名在文件系统中的开始地址。
3)path参数指定相对路径名,fd参数具有特殊值AT_FDCWD,此时路径名在当前工作目录中获取。
openat函数是POSIX.1新增的,希望解决两个问题:
1. 让线程可以使用相对路径名打开目录中的文件,不再只能打开当前工作目录。
2. 避免time-of-check-to-time-of-use(TOCTTOU)错误。
函数creat
可以调用creat函数创建一个新文件。
#include <fcntl.h>
int creat(const char *path, mode_t mode);
返回值:如成功,返回为只写打开的文件描述符;如出错,返回-1.
此函数等效于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
函数close
调用close函数关闭一个打开文件。
#include <unistd.h>
int close(int fd);
返回值:若成功,返回0;若出错,返回-1
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内核自动关闭它所有的打开文件。
函数lseek
每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是一个非负整数,度量从文件开始处计算的字节数。通常,读写文件都是从当前文件偏移量开始,并使偏移量增加所读写的字节数。
按系统默认情况,当打开一个文件时,除非指定O_APPEND,否则该偏移量设置为0.
可以调用lseek显示地为一个打开文件设置偏移量。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
返回值:若成功,返回新的文件偏移量;若出错,返回-1
参数offset的解释与参数whence有关:
- 若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
- 若whence是SEEK_CUR, 则该文件的偏移量设置为当前值加offset,offset可为正可负。
- 若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
若lseek成功执行,返回新的文件偏移量。可以用如下方式确定打开文件的当前偏移量。
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方式也可以用来确定文件是否可以设置偏移量,如果文件描述符指向管道、FIFOO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
某些设置的偏移量可能为负,所以比较lseek的返回值时,不要测试它是否小于0,而要而是它是否等于-1
文件偏移量可以大于文件当前长度。在这种情况下,对文件的下一次操作将加长该文件,并在文件中构成一个空洞,文件中没写过的字节都读为0.
函数read
调用read函数从打开文件中读数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
返回值:读取到得字节数,若已到文件尾,返回0;若出错,返回-1
多数情况下,实际读到得字节数少于要求读得字节数;
- 读普通文件时,在读到要求字节数之前到达了文件尾。
- 从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机制可能造成返回值效于所要求读的字节数。
- 从管道或FIFO读时,若管道包含的字节数效于要求的数量。
- 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
- 当一信号造成中断,而已经读了部分数据量时。
函数write
调用write函数向打开文件写数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
返回值:若成功,返回已写的字节数;若出错,返回-1
若其返回值与参数nbytes相同则表示成功,否则表示出错。
write出错的一个常见原因时磁盘写满,或超出了一个给定进程的文件长度限制。
对于普通文件,写操作从文件当前偏移量开始。如果在打开文件时,指定了O_APPEND选型,则在每次写操作之前,将文件偏移量设置在当前文件结尾处。
I/O的效率
对于UNIX系统而言,文件文件和二进制代码文件并无区别。
大多数文件系统为改善系统性能都采取某种预读技术。当检查到正在进行顺序读取时,系统就试图读入比应用要求的更多数据。