Unix环境高级编程——文件I/O
3.1 引言
首先,Unix/Linux I/O操作及相关知识是十分基础的,但是必须需要掌握的。其次,这里所说的函数是不带缓冲的I/O。不带缓冲指的是每个类似read和write函数都调用内核的一个系统调用,这些不带缓冲的I/O函数不是ISO C的组成部分,即不为标准I/O库的函数。但是,它们是POSIX和Single Unix Specification的组成部分。
3.2 什么是文件描述符?
内核(kernel)利用文件描述符(file descriptor)来访问文件,文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
惯例上,Unix系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误输出相关联。这是shell以及很多应用程序的惯例与Unix内核无关。在依据POSIX的应用程序中,上面数字0、1、2应当被改变为相应的变量。
3.3 open函数
调用open函数打开或创建一个文件。
#include <fcntl.h>
//若成功返回文件描述符,出错返回-1。
int open(const char *pathname, int oflag, ...);
对于open函数而言,只有创建新的文件才使用第三个参数。
参数pathname 指向欲打开的文件路径字符串。下列是参数flags 所能使用的标志位:
-
- O_RDONLY 以只读方式打开文件。
- O_WRONLY 以只写方式打开文件。
- O_RDWR 以可读写方式打开文件。
上述三种标志位是互斥的,也就是不可同时使用,但可与下列的标志位利用OR(|)运算符组合。
-
- O_CREAT 若欲打开的文件不存在则自动建立该文件。
- O_EXCL 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。此外,若O_CREAT与O_EXCL同时设置,并且欲打开的文件为符号连接,则会打开文件失败。
- O_NOCTTY 如果欲打开的文件为终端机设备时,则不会将该终端机当成进程控制终端机。
- O_TRUNC 若文件存在并且以可写的方式打开时,此标志位会令文件长度清为0,而原来存于该文件的资料也会消失。
- O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
- O_NONBLOCK 以不可阻断的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。
另外POSIX和Single Unix Specification定义了其他几组可选标志变量,但有些Unix/Linux并没有很好的支持,具体说明请看相关标准。
由open函数返回的文件描述符一定是最小的没有被使用的数值。
3.4 creat函数
调用creat函数来创建一个新的文件。
#include <fcntl.h>
//若成功返回文件描述符,出错返回-1。
int creat(const char *pathname, mode_t mode);
此外,creat函数等效与:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat不足在于它只能以只写方式打开创建的文件,若需要创建一个文件并读写之,可以用下列方式:
open(pathname, O_RDWR | O_CREAT | O_TRUNC, mode);
3.5 close函数
调用close函数关闭一个以及打开的文件。
#include <unistd.h>
//若成功返回文件描述符,出错返回-1。
int creat(int filedes);
关闭一个文件还会释放该进程加在该文件上所有的记录锁。
当一个进程终止时,内核自动关闭它所有打开的文件。很多程序利用这一点不显式用close关闭打开文件。
3.6 lseek函数
调用lseek显式地为一个打开的文件设置偏移量。
每个打开的文件都有一个与其相关联的“当前文件偏移量”。它通常为一个非负整数,用以度量从开始计算的字节数(有特例,在后面会说道)。通常,读、写操作都从当前文件偏移量开始,并使偏移量增加所读写的字节数。默认情况下,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量设置为0。
#include <unistd.h>
//若成功返回新的文件偏移量,出错返回-1。
off_t lseek(int filedes, off_t offset, int whence);
对参数offset的解释与whence有关。
-
- 若whence为SEEK_SET,将该文件的偏移量设置为距文件开始处offset个字节。
- 若whence为SEEK_CUR,将该文件的偏移量设置为当前值加offset,offset可正可负。
- 若whence为SEEK_CUR,将该文件的偏移量设置为文件长度加offset,offset可正可负。
通常,文件的当前偏移量应当为一个非负整数。但是,某些设备可能允许负的偏移量(比如说磁带刻录机等设备)。因为偏移量可能是负值,所以在比较lseek的返回值应当谨慎,不要测试它是否小于0,而要测试是否等于-1。
lseek只保存在内存中,它对I/O操作没有任何影响。偏移量用于下一个读或写操作的位置。
文件偏移量可以大于文件的当前长度,在这种情况在,对该文件的下一次写将加长该文件,并在文件中构成一个空洞(即文件某些部分为空)。位于文件中但没有被写过的字节都为0。
文件中的空洞不要求在磁盘上占用存储区。具体的处理方式与文件系统的实现有关,当定位到超出文件尾端之后,对于新写的数据需要分配数据块,但对原文件尾端和开始位置之间的部分不需要分配磁盘块。
由于lseek使用的编译量是用off_t表示的,所以允许off_t根据各自平台自行选择合适的数据类型。如今大多数平台提供两组接口以处理文件偏移量:一组使用32位,一组使用64位。
3.7 read函数
调用read函数从打开文件中读取数据。
#include <unistd.h>
//若成功返回读到的字节数,若以到文件结尾返回0,出错返回-1。
off_t read(int filedes, void *buf, size_t nbytes);
有很多种情况使得读到的字节数小于要求的字节数(到达文件结尾、网络延迟等问题)。读操作从文件的当前偏移量开始,在成功返回之前,该偏移量将增加实际读到的字节数。
3.8 write函数
调用write函数打开的文件写数据。
#include <unistd.h>
//若成功返回已写的字节数,出错返回-1。
off_t read(int filedes, void *buf, size_t nbytes);
其返回值通常与参数nbytes的值相同,否则表示出错。Write出错的的一个常见原因是磁盘写满,或者已经超过了一个给定进程的文件长度限制。
3.9 文件共享
Unix系统支持在不同进程共享打开文件。
内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,每 个描述符占用一项。与每个文件描述符相关联的是:
-
- 文件描述符标志。
- 指向一个文件表项的指针。
内核为所有打开文件维持一张文件表。每个文件表项包含:
-
- 文件状态标志。
- 当前文件偏移量。
- 指向该文件v节点表项的指针。
每个打开文件都有一个v节点结构。v节点包含了文件类型和对比文件进行各种 操作的函数的指针。
在完成每个write后,文件表项中的当前文件偏移量即增加所写的字节数。如果这使得当前文件偏移量超过当前文件长度,则i节点表项中的当前文件长度被设置为当前文件偏移量。
如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添加写标志的文件执行写操作时,在文件爱你表项中的当前长度偏移量首先被设置为i节点表项中的文件长度。
若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为节点表项中的当前文件长度。
3.10 dup和dup2函数
两个函数都用来复制一个现存的文件描述符。
#include <unistd.h>
//若成功返回新的文件描述符,出错返回-1。
int dup(int filedes);
off_t dup2(int filedes, int filedes2);
由dup返回的文件描述符一定是可用文件描述符中最小的数值。用dup2则可以用filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。若filedes等于fildes2,则dup2直接返回filedses2而不关闭它。
3.11 sync、fsync和fdatasync函数
延迟写减少磁盘读写次数,但却降低了文件内容的更新速度。当系统发生故障或者关机时,这种延迟可能导致文件内容丢失。为了保证磁盘文件与缓冲区文件内容的一致性,Unix提供sync、fsync和fdatasync三个函数。
#include <unistd.h>
//若成功返回新的文件描述符,出错返回-1。
//等待文件操作结束返回,常用与数据库操作。
int fsync(int filedes);
//类似与fsync,但它只会影响文件数据部分。而fsync会修改文件爱你属性。
int fdatasync(int filedes);
//将所有修改过的缓冲区写入队列就返回,不等待实际磁盘文件操作结束。
void sync(int filedes);
3.12 fcntl函数
fcntl可以改变已打开的文件的性质。
#include <unistd.h>
int fcntl(int filedes, int cmd, ...);
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD)。
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)。
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。