Unix系统中的大多数文件IO只需用到5个函数:open、read、write、lseek以及close。这些函数是不带缓冲的IO,不带缓冲指的是每个read和write都调用内核中的一个系统调用,它们不是ISO/C的组成部分,而是POSIX和SUS的东西。
文件描述符——
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数,按照惯例,文件描述符0、1、2分别表示的是标准输入、标准输出、标准出错,在依从POSIX的应用程序中,这3个幻数应当替换为符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,这些常量定义在头文件
open函数——
#include <fcntl.h>
int open(const char *pathname, int flags[, mode_t mode]);
调用open函数可以打开或创建一个文件,若成功则返回文件描述符,一定是最小的未用描述符数值,若出错则返回-1。pathname是要打开或创建文件的名字,当整个路径名超过PATH_MAX或路径名中任一文件名超过NAME_MAX时,若_POSIX_NO_TRUNC有效,则返回出错状态,并将errno设置为ENAMETOOLONG,否则名字可能被截断。flags参数可用来说明函数的多个选项,用一个或多个常量进行“或”运算构成,常量如O_RDONLY等以大写字母O和一个下划线开头。mode参数仅当创建新文件时才使用,用于指定该新文件的访问权限位,与进程的umask值有关。
create函数——
#include <fcntl.h>
int create(const char *pathname, mode_t mode);
调用create函数也可以创建一个新文件,等效于如下open函数:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
close函数——
#include <unistd.h>
int close(int fd);
调用close函数关闭一个打开的文件,成功返回0,出错返回-1。当一个进程终止时,内核会自动关闭它打开的所有文件,这样可不必显式地调用close关闭打开的文件,文件关闭时还会释放该进程加在它身上的所有记录锁。
lseek函数——
#include <unistd.h>
off_t lseek(int fd, off_t offset, in whence);
调用lseek函数为一个打开的文件设置偏移量,成功返回新的文件偏移量,出错返回-1。lseek仅将当前的文件偏移量记录在内核中,它并不引起任何IO操作,然后该偏移量用于下一个读或写操作。whence参数可以是SEEK_SET、SEEK_CUR、SEEK_END中的一个,分别表示偏移量的基准位置为文件开头、文件当前位置、文件结尾。offset可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,位于文件中但没有写过的字节都被读为0,然而文件中的空洞并不要求在磁盘上占用存储区。
read函数——
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
调用read函数从打开的文件中读取数据,成功返回读到的字节数,到达文件末尾返回0,出错返回-1。
write函数——
#include <unistd.h>
ssize_t write(int fd, cosnt void *buf, size_t count);
调用write函数向打开的文件中写入数据,成功返回写入的字节数,出错返回-1。
IO效率——
《UNIX环境高级编程》一书描述了不同的buffer长度对IO效率的影响,例子中使用read和write函数复制一个文件,文件长度为103316352字节,buffer长度从1到512k,测试了不同buffer长度下的用户CPU时间、系统CPU时间、时钟时间以及循环次数,结果显示,buffer长度为4096字节时效率最高,即一个block的大小。大多数文件系统为改善其性能都采用某种预读技术,当检测到正进行顺序读取时,系统就试图读入比应用程序所要求的更多数据,并假想应用程序很快就会读这些数据。
文件共享——
UNIX系统支持在不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件,分别是进程表记录项、文件表记录项、v节点,同一个文件共享一个v节点表项。文件共享必然是一个原子操作,即这个操作要么不执行,要么一次性执行完毕,执行过程中没有任何干扰,例如执行lseek和read函数,本意是先定位文件偏移量再读取数据,如果两个函数执行之间插入了别的操作就有可能带来副作用,正确的做法是对这个操作加锁,或者是使用一个等效的替代函数pread,这个函数就好像是把lseek和read当作一个原子操作一样。
文件操作——
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup和dup2两个函数都可以用来复制一个现有的文件描述符,它们共享同一个文件表项,这个操作还可以由fcntl函数实现。
#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);
传统UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘IO都通过缓冲进行。当将数据写入文件时,内核通常先将数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其它磁盘数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的IO操作。这种输出方式被称为延迟写。延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束,通常称为update的系统守护进程会周期性调用sync函数。fsync和fdatasync对指定的文件描述符起作用,并且等待写磁盘操作结束,后者只影响文件的数据部分。
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
fcntl函数可以改变已打开文件的性质,重点是cmd参数,功能包括现有描述符的复制,获得和设置文件描述标记、文件状态标记、异步IO所有权、记录锁。
#include <sys/ioctl.h>
int ioctl(int fd, int request, …);
ioctl函数是IO操作的杂物箱,不能用上面列出的函数表示的IO操作都能用ioctl表示。终端IO是ioctl的最大使用方面。
/dev/fd目录——
/dev/fd目录下是一些名为0、1、2等的文件,打开文件/dev/fd/n等效于复制描述符n,共享同一文件表项。/dev/fd文件主要由shell使用,如下两个命令行等效:
cat file | cat - > tmp
cat file | cat /dev/fd/0 > tmp
cat命令把命令行参数的字符“-”解释为标准输入,等效于使用/dev/fd/0,而后者提高了文件名参数的一致性,看起来更加清晰。