UNIX环境高级编程之文件I/O
引言
UNIX系统提供了进行文件I/O操作的系统调用,下面介绍了这些文件I/O的使用方法。
文件描述符
定义
在UNIX系统中,所有打开的文件都使用一个非负整数来表示打开的文件,称为文件描述符。
标准输入、标准输出和标准错误的文件描述符
在UNIX系统中,shell把文件描述符0与标准输入相关联,把文件描述符1与标准输出相关联,把文件描述符2与标准错误相关联。文件描述符0、1和2通常被替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。这些符号常量都定义在头文件<unistd.h>中。
文件描述符的范围
文件描述符的变化范围是0 ~ OPEN_MAX - 1。OPEN_MAX的值可以通过<unistd.h>头文件的sysconf函数获得。
函数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函数
参数
open函数的path参数是要打开或创建的文件的路径,oflag参数可用来说明此函数的多个选项。oflag参数是由以下常量经过“或”运算得到的。这些常量均定义在<fcntl.h>中。
常量名 | 含义 |
---|---|
O_PDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读、写打开 |
O_EXEC | 只执行打开 |
O_SEARCH | 只搜索打开(应用于目录) |
O_APPEND | 每次写时都追加到文件的尾端 |
O_CLOEXEC | 把FD_CLOEXEC常量设置为文件描述符标志 |
O_CREAT | 若此文件不存在则创建它。使用此选项时,open函数需同时说明第三个参数mode,用mode指定该新文件的访问权限位 |
O_DIRECTOPY | 如果path引用的不是目录则出错 |
O_EXCL | 如果同时指定了O_CREAT,而文件已存在,则出错,用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建成为一个原子操作。 |
O_NOCTTY | 如果path的引用是终端设备,则不将该设备分配作为此进程的控制终端 |
O_NOFOLLOW | 如果path引用的是一个符号链接,则出错 |
O_NONBLOCK | 如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式 |
O_SYNC | 使每次write要等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O |
O_TRUNC | 如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0 |
O_DYSNC | 使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新 |
O_RSYNC | 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成 |
在表格的前5个常量中,必须指定且只能指定一定。其余的常量是可选的。
返回值
open函数返回打开或创建的文件的文件描述符,由open函数返回的文件描述符一定是最小的未用描述符数值。这一点被某些应用程序用来在标准输入、标准输出或标准错误上打开新的文件。
openat函数
fd参数将open函数与openat函数区别开,共有三种可能性。
- path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数相当于open函数。
- path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始位置。fd参数是通过打开相对路径名所在的目录来获取。
- path参数指定了相对路径名,fd参数具有特殊值AT_WDCWD。在这种情况下,路径名在当前工作目录下获取,openat函数在操作上open函数类似。
函数creat
用途
creat函数用于创建一个新文件。
该函数等价于 open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
函数原型
#include <fcntl.h>
int creat(const char *path, mode_t mode);
参数
path参数指定了新文件的路径,mode参数指定了新文件的访问权限。
返回值
如果文件创建成功,返回只写打开的文件描述符;若出错,返回-1。
不足
creat函数的一个不足之处在于它以只写方式打开所创建的文件。
函数close
用途
close函数用于关闭一个文件。
关闭一个文件会释放该进程加在文件上的所有记录锁。
当一个进程终止时,内核自动关闭它所有的打开文件。
函数原型
#include <unistd.h>
int close(int fd);
参数
fd参数是要关闭的文件的文件描述符。
返回值
若成功,返回0;若失败,返回-1。
函数lseek
当前文件偏移量
当前文件偏移量用以度量从文件开始处计算的字节数,通常是一个非负整数。当文件打开时,除非指定O_APPEND选项,否则该偏移量被设置为0.
lseek函数的用途
调用lseek函数显式地为一个打开的文件设置偏移量。
函数原型
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
返回值
若成功,返回新的文件偏移量;若出错,返回-1。
如果文件描述符指向的是一个管道,FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
参数
- fd:文件描述符
- offset:新的文件偏移量
- whence:
若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
文件空洞
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞。位于文件中但都没写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。当定位到超过文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
函数read
用途
调用read函数从打开文件中读数据。
读操作从当前的文件偏移量开始,在成功返回之前,该偏移量将增加实际读到的字节数。
函数原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
返回值
如果read成功,返回读到的字节数。如果出错,返回-1。
参数
- fd:文件描述符
- buf:指向一段缓冲区
- nbytes:想要读取的字节数
返回值比nbytes小的原因
- 读普通文件时,在读到要求字节数之前已达到了文件末尾
- 当从终端设备读时,通常一次最多读一行
- 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数
- 从管道或FIFO读时,如若管道包含的字节小于所需的数量,那么read将只返回实际可用的字节数
- 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录
- 当一信号造成中断,而已经读了部分数据量时
函数write
用途
向打开的文件中写数据。
函数原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
返回值
若成功返回已写的字节数,若出错返回-1
出错的常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
文件共享
内核用于所有I/O的数据结构
- 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 文件描述符标志
- 指向一个文件表项的指针
- 内核为所有打开文件维持一张文件表。每个文件表项包含:
- 文件状态标识(读、写、添写、同步和非阻塞等)
- 当前文件偏移量
- 指向该文件v结点表项的指针
- 每个文件(或设备)都有一个v结点结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点。i节点包含了文件的所有者、文件长度指向文件实际数据块在磁盘上所在位置的指针等。
原子操作
追加到一个文件
假设两个独立的进程A和B同时打开一个文件,进程A将文件偏移量设置为1500,这时内核挂起进程A。进程B也将文件偏移量设置为1500,并调用write写入100B的数据,并将文件偏移量设置为1600。这时内核挂起进程B,运行进程A。进程A从文件偏移量1500处写入数据,将进程B写入的数据覆盖。为了避免这种问题的发生。我们使用open函数的O_APPEND选项,使每次写操作时都将文件偏移量设置在文件末尾。
函数pread和pwrite
- 函数原型
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
- 用途
调用pread相当于先调用lseek再调用read
调用pread时,无法中断其定位和读操作
pread不更新当前文件偏移量
pwrite与pread类似
创建一个文件
open函数的O_CREAT和O_EXCL选项可以使open函数原子地创建一个文件。
函数dup和dup2
用途
dup和dup2函数用来复制一个现有的文件描述符
函数原型
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
返回值
若成功,返回新的文件描述符;若失败,返回-1
用法
- 由dup返回的新文件描述符一定是当前可用文件描述符的最小数值
- dup2函数可以用参数fd2指定新描述符的值
- 如果fd2已经打开,则先将其关闭
- 如果fd等于fd2,则dup2返回fd2,而不关闭它
- 否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程中调用exec时是打开状态
函数sync、fsync和fdatasync
延迟写
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式称为延迟写。
用途
保证磁盘上实际文件系统与缓冲区中内容的一致性。
函数原型
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
返回值
若成功,返回0;若出错,返回-1
用法
- sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
- fsync函数只对由文件描述符fd指定的一个文件起作用。并且等待写磁盘操作结束才返回。
- fdatasync函数类似于fsync,但他只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
- 通常,称为update的系统守护进程周期性地调用(一般每隔30秒)sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。
- fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
函数fcnl
用途
改变已经打开文件的属性
函数原型
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */);
功能
fcntl有以下五种功能
- 复制一个已有的描述符(cmd = F_DUPFD或F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志(cmd = F_GETFD或F_SETFD)
- 获取/设置文件状态标志(cmd = F_GETFL或F_SETFL)
- 获取/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)
- 获取/设置记忆锁(cmd = F_GETLK、F_SETLK或F_SETLKW)
/dev/fd
- /dev/fd的目录的目录项是名为0、1、2等文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。
- fd = open("/dev/fd/0", mode);等效于ds =dup(0); 大多数系统会忽略mode,另一些系统要求mode必须是所引用的文件初始打开是所使用的打开模式的一个子集。
- 也可以用/dev/fd作为路径名参数调用creat,这与调用open时用O_CREAT作为第2个参数作用相同。