unix系统中的大多数文件I/O只需用到5个函数:open、read、write、lseek以及close。
其中read和write函数被称为不带缓冲的I/O,不带缓冲指的是每个read和write都调用内核中一个系统调用。
文件描述符
文件标识符是非负整数。打开现存文件或新建文件时,内核会返回一个文件标识符。读写文件也需要使用文件标识符来指定等待读写的文件。
习惯上,标准输入(standard input)的文件标识符是0,标准输出(standard output)是1,标准错误(standard error)是2。
open函数
用于打开或创建文件。
#include <fcntl.h>
int open(const char *pathname, int oflag, ... /*mode_t mode */);
pathname是待打开/创建文件的路径名;oflag用于指定文件的打开/创建模式,这个参数可由以下常量(定义于 fcntl.h)通过逻辑或构成;第三个参数(...)仅当创建新文件时才使用,用于指定文件的访问权限位。
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
打开/创建文件时,至少得使用上述三个常量中的一个。以下常量是选用的:
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式
create函数
用于创建一个新文件。
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
其不足之处是它以只写方式打开所创建的文件。
等价于 open(pathname, O_WRONLY | O_CREAT |O_TRUNC);
close函数
用于关闭一个已打开的文件。
#include <unistd.h>
int close(int filedes);
当一个进程终止时,内核自动关闭它所有打开的文件。
lseek函数
用于设置一个打开的文件的偏移量。
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence);
成功时返回目前的读写位置,也就是距离文件开头多少个字节;失败时返回-1。
filedes是已打开的文件标识符;offset是偏移量,每一读写操作所需要移动的距离,单位是字节的数量,可正可负(向前移,向后移)。whence为下列其中一种:(SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2)
SEEK_SET 将读写位置指向文件头后再增加offset个位移量
SEEK_CUR 以目前的读写位置往后增加offset个位移量
SEEK_END 将读写位置指向文件尾后再增加offset个位移量
当whence值为SEEK_CUR或SEEK_END时,参数offset允许负值的出现。
read函数
用于从打开文件中读取数据。
#include <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
成功时返回所读的数据量,数据读完时返回0,失败时返回-1。
filedes是要读取数据的文件标识符;buf是所读取到的数据的内存缓冲;nbytes是需要读取的数据量。
write函数
用于向打开的文件写数据。
#include <unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes);
成功时返回所写的数据量,失败时返回-1。
filedes是要写入数据的文件标识符;buf是所要写入的数据的内存缓冲;nbytes是需要写入的数据量。
文件共享
内核使用三种数据结构表示打开的文件:
(1)每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,每个描述符占用一项。与每个文件描述符相关联的是:
(a)文件描述符标志。(b)指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
(a)文件状态标志。(b)当前文件偏移量。
(c)指向该文件v节点表项的指针。
(3)每个打开文件都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v节点还包含了该文件的i节点。i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘所在位置的指针等等。
如果两个独立进程各自打开了同一个文件,两个进程都得到一个文件表项,因为每个进程都有它自己的对该文件的当前偏移量。
可能有多个文件描述符项指向同一文件表项,如调用dup函数或fork后都会发生。
当多个进程写同一个文件是,可能产生预期不到的结果。为了避免这种情况,需要理解原子操作的概念。
原子操作
原子操作指的是由多步组成的操作,如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
1、添写至一个文件
任何一个需要多个函数调用的操作都不可能是原子操作,因为在两个函数调用之间,内核有可能会临时挂起该进程。
unix系统通过在打开文件时设置O_APPEND标志,使每次往文件末端写数据时,不需要先调用lseek再调用write。
2、pread和pwrite函数
pread相当于顺序调用lseek和read,但pread又与这种顺序调用有下列重要区别:调用pread时,无法中断其定位和读操作;不更新文件指针。
3、创建一个文件
调用open函数时使用O_CREAT和O_EXCL选项,当文件已存在是,open失败。避免先调用open时失败,再调用creat的做法。
dup和dup2函数
用来复制一个现存的文件描述符。
#include <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);
sync、fsync和fdatasync函数
传统的unix实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲区排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为延迟写。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。
为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,unix系统提供了sync、fsync和fdatasync三个函数。
#include <unsitd.h>
int fsync(int filedes);
int fdatasync(int filedes);
void sync(void);
sync函数是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。通常称为update的系统守护进程会周期性地调用sync函数。
fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
fcntl函数
#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /*int arg*/);
fcntl函数有5种功能:
(1)复制一个现有的描述符(F_DUPFD);(2)获得/设置文件描述符标记(F_GETFD或F_SETFD);
(3)获得/设置文件状态标志(F_GETFL或F_SETFL);
(4)获得/设置异步I/O所有权(F_GETOWN或F_SETOWN);
(5)获得/设置记录锁(F_GETLK、F_SETLK或F_SETLKW)。