文件输入/输出
简介
Unix系统上的绝大多数文件I/O可以通过使用下面5个函数完成:open, read, write, lseek, 和 close。本章描述的都是非缓冲I/O(unbuffered I/O)函数。非缓冲意味着每次的读、写都要陷入内核做一次系统调用。
文件描述符
文件描述符是一个非负整数。对于内核来说,所有打开的文件都用文件描述符表示。方便起见,Unix系统定义文件描述符0为所有进程的标准输入,文件描述符1为标准输出,2为标准错误。但这种方便性并不是Unix的一个内核特征。符合POSIX标准的应用程序中可以用STDIN_FILENO, STDOUT_FILENO, 和 STDERR_FILENO分别表示三者,<unistd.h>。
文件描述符的范围可以从0到OPEN_MAX,OPEN_MAX是一个实现限制。
Open函数
#include <fcntl.h>
int open(const char *pathname, int oflag, ... /*mode_t mode */ );
Returns: file descriptor if OK, 1 on error
第三个是可变参数,只有在创建新文件的时候才使用。
oflag是选项值,多个选项值直接可以用|符号连接。定义在<fcntl.h>的选项值有:
O_RDONLY 打开文件,只读
O_WRONLY 打开文件,只写
O_RDWR 打开文件,可读可写
上述三个选项有且只能有一个指定,下面的是可选参数:
O_APPEND 将写追加在文件末尾
O_CREAT 若文件不存在,则创建之,新文件的访问权限位由第三个参数指定
O_EXCL 若同时也指定了O_CREAT,而且该文件已经存在,则会产生一个错误
O_TRUNC 如果文件已经存在,并且是只写或读写文件,则将长度截短到0
O_NOCTTY 如果路径名指向一个终端设备,则不将该设备分配为进程的控制终端
O_NONBLOCK 如果路径名指向一个FIFO,块特殊文件(block special file)或字符特殊文件(character special file),这个选项为打开文件和后续的I/O设置了非块模式(the nonblocking mode)。
SUS的同步输入和输出选项:
O_DSYNC 每次写都要等上次物理I/O完成,如果不影响读取新写入的数据,则不等待文件属性更新
O_RSYNC 读操作等待文件同一区域的追加写完成后再进行
O_SYNC 等待物理I/O结束后再write,包括更新文件属性的I/O
Open返回的文件描述符是当前最小的未使用整数。
文件名和路径名截短
当新创建的文件名长度超过NAME_MAX限制时,System V早期版本的处理方式是截短文件名长度到NAME_MAX之内;但在BSD派生的系统,会返回一个错误状态,errno设置成ENAMETOOLONG。
POSIX.1标准中,常量_POSIX_NO_TRUNC 用来确定长文件名和长路径名的处理方式:截短或者返回错误。
creat函数
创建新文件
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
Returns: file descriptor opened for write-only if OK, 1 on error
该函数等价于
open (pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat函数相对于上面等价函数的不足是低效:创建的新文件是只写的,如果想要回读,我们必须调用creat,close,然后再open。
close函数
关闭打开的文件
#include <unistd.h>
int close(int filedes);
Returns: 0 if OK, 1 on error
当一个进程中止时,它的所有打开文件都将被内核自动关闭。
lseek函数
每个打开的文件都有一个相关的“当前文件偏移 current file offset”,通常是一个非负整数,该整数计量从文件头到当前位置的字节数。当文件被打开时,该偏移值初始化为0,除非指定了O_APPEND 选项。
一个文件的偏移能够通过调用lseek来显式地获得。
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence);
Returns: new file offset if OK, 1 on error
whence参数可取下面的值:
SEEK_SET,此时文件的偏移设置为从文件头开始的第offset个字节处;
SEEK_CUR,文件的偏移设置为其当前值加上offset,offset可正可负;
SEEK_END,文件的偏移设置为文件大小加上offset,offset可正可负。
语句lseek(fd, 0, SEEK_CUR);
既可用来取得当前的偏移位置,也可用来确定某个文件是否可seek。
lseek的“l”代表了“long integer”。
对于一个常规的文件来说,偏移量总是一个非负数。但也有文件的偏移量可能是负数。
文件的偏移有可能会大于当前文件大小,在这种情况下,下一次写操作将会扩展该文件(extend the file),并在文件中留下一个hole,这时,相同大小的两个文件可能会占用不同数目的磁盘块(block)。
od命令查看文件内容,-c选项可以指定以字符形式打印内容。
read函数
从一个打开的文件中读取数据
#include <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
Returns: 读取的字节数, 碰到文件结尾(EOF)返回0, 错误则是1。
读操作从文件的当前偏移开始,在成功返回后,偏移值增加实际读取的字节数目。
POSIX.1标准对read函数在几个方面做了改动,其经典定义如下:
int read(int filedes, char *buf, unsigned nbytes);
首先,为于ISO C保持一致,将第二个参数类型从char* 变换到了通用指针void*;
其次,返回值必须是一个有符号整数(ssize_t),用来返回正的字节计数(?疑惑?),0(EOF),或者1(出错)。
最后,第三个参数是无符号整数,以允许一个16位的实现可以一次读、写65534个字节。
write函数
向打开的文件写数据
#include <unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes);
Returns: number of bytes written if OK, 1 on error
返回值通常等于参数nbytes;否则写操作有错误发生。常见的错误原因有磁盘满,或者超过了对一个给定进程的文件大小限制。
I/O效率
在非缓冲I/O中选择一个合适大小的缓冲区可以提高I/O的效率。教程提供了这样一个测试程序。
文件共享
Unix系统支持打开文件(open files)在不同进程间的共享。内核定义了三种数据结构来表示一个打开文件。
1.每个进程都在进程表中有一个条目。每个这样的进程表条目是一张打开文件描述符表格,可以将其看作是一个向量(vector)——每个描述符占用一项。和文件描述符相关联的内容包括:
a.文件描述符标志
b.指向文件表条目的指针
2.内核为所有打开的文件维护一张文件表(file table)。每个文件表条目包括:
a.文件的状态标志(the file status flags),如read, write, append, sync, nonblocking
b.当前文件偏移
c.指向该文件v结点表(the v-node table)条目的指针
3.每个打开文件(或设备)都有一个v结点结构,它包含了关于文件类型的信息和指针,这些指针指向操作该文件的函数。对于绝大多数的文件,v结点包含了文件的i结点。这些信息是在文件打开的时候从磁盘读入的,需要的时候随时可用。
(注:Linux没有v结点,使用一个通用i结点结构(a generic i-node structure ))
图--打开文件的内核数据结构
图--两个独立的进程共享同一打开文件
文件操作对数据结构的影响
×每次写操作完成,文件表条目中当前文件偏移增加写入的字节数,若结果超过了当前文件大小,那么i结点中的当前文件大小更新为当前文件偏移
×文件打开时如果指定了O_APPEND标志,文件表条目中的文件状态标志要作相应的设置。当对这样的文件写操作时,当前文件偏移初始为i结点中的文件大小,这样写操作就在文件末尾进行
×使用lseek将文件定位到当前文件的结尾,文件表条目中的当前文件偏移设置为当前文件大小
×lseek函数只是修改文件表条目中的当前文件偏移,而没有发生任何I/O。
原子操作
组成原子操作的多个步骤,要么全发生,要么全不发生。
文件追加
多个进程对同一文件的追加操作(先lseek再write的方式)可能会引起意外错误,需要将多个操作合并为一个原子操作。
pread和pwrite函数
SUS中引入了两个扩展函数pread和pwrite,它们将seek和I/O合并为一个原子操作。
#include <unistd.h>
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
Returns: number of bytes read, 0 if end of file, 1 on error
调用pread等同于先调用lseek,然后read,但是
×没有方法可以中断这两个操作
×文件指针在操作结束以后也没有更新
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
Returns: number of bytes written if OK, 1 on error
创建一个文件
将判断文件是否已经存在和创建文件合并为单个原子操作,避免错误。
dup和dup2函数
某个已经存在的文件描述符可以通过下列函数之一进行复制:
#include <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);
Both return: new file descriptor if OK, 1 on error
dup返回值是最小的可用文件描述符。
dup2的第二个参数filedes2指定了新的文件描述符值。如果filedes2已经打开,则先将其关闭。如果filedes等于filedes2,那么dup2返回filedes2而无须关闭它。
返回的新的文件描述符和参数filedes共享相同的文件表条目。如图
sync, fsync, 和 fdatasync函数
对于传统的Unix系统实现,内核中都有一个buffer cache 或 page cache,磁盘I/O的数据都通过它来传递。延迟写(delayed write):当向一个文件写数据时,数据通常被内核拷贝到它的缓冲区,排好序,在将来的某个时刻写到磁盘。为确保磁盘上文件系统和buffer cache中内容的一致性,提供了sync, fsync, 和 fdatasync三个函数。
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
Returns: 0 if OK, 1 on error
void sync(void);
sync函数只是简单地将所有已修改的块缓冲排入写队列,并且返回,它不必等待磁盘写操作发生。通常Unix的write只是将数据放到写队列中,写操作在将来某时发生。为了保证有规则地清空内核的块缓冲,系统的一个守护进程(通常称作update)定期地(通常每隔30秒)调用sync。Sync命令也调用sync函数。
fsync函数在返回之前要等待磁盘写操作完成。使用fsync的目的是向应用程序,比如数据库,保证修改的数据块已经写到磁盘。
fdatasync函数类似于fsync,但它只影响文件的数据部分。fsync同时更新文件的属性。
fcntl函数
fcntl可以改变已打开文件的属性。
#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* int arg */ );
Returns: depends on cmd if OK (see following), 1 on error
fcntl的五种用途:
复制已存在的描述符(cmd = F_DUPFD),新、旧文件描述符共享同一文件表条目(file table entry),但新描述符有它自己的描述符标志,并且它的FD_CLOEXEC标志被清除。
获得/设置文件描述符标志 (cmd = F_GETFD or F_SETFD)。目前只定义了一个文件描述符标志:FD_CLOEXEC。设置时参数可以是0(默认)或者1(exec调用后关闭文件描述符)。
获得/设置文件状态标志 (cmd = F_GETFL or F_SETFL)。文件状态标志参见Open函数的参数oflag。
获得/设置异步I/O所有权 (cmd = F_GETOWN or F_SETOWN)
获得/设置记录锁(record locks) (cmd = F_GETLK, F_SETLK, or F_SETLKW)
在修改文件描述符标志或文件状态标志的时候需要先检查已设置的标志值,再根据需要进行修改,设置新的标志值。而不能简单地作F_SETFD 或 F_SETFL操作,这样可能会关闭已设置的标志位。
ioctl函数
ioctl函数是I/O操作的集大成者(The ioctl function has always been the catchall for I/O operations)。使用ioctl函数最多的是终端输入、输出。
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
#include <stropts.h> /* XSI STREAMS */
int ioctl(int filedes, int request, ...);
Returns: 1 on error, something else if OK
每个设备驱动器能够定义它们自己的ioctl命令集。不过系统也为不同种类的设备提供了通用的ioctl命令。
/dev/fd
新的系统都提供了一个/dev/fd目录,其中有命名为0,1,2等的文件。打开这样的一个文件/dev/fd/n效果等同于复制描述符n,当然前提是描述符n是打开着的。
函数调用
fd = open("/dev/fd/0", mode);
等同于
fd = dup(0);文件描述符fd和0共享相同的file table entry。
但有一点要注意:绝大多数系统忽略指定的mode,然而其它的要求它是被引文文件(如这里的/dev/fd/0,即标准输入)的mode的子集。如果描述符指定是只读的,即使我们成功调用
fd = open("/dev/fd/0", O_RDWR);
仍然不能向fd做写操作。
/dev/fd主要用在shell上,它使得程序可以通过路径名参数的形式处理标准输入和标准输出。
如命令,filter file2 | cat file1 /dev/fd/0 file3 | lpr,cat依次读入file1,标准输入(filter file2的结果),file3,在以前,/dev/fd/0是用-表示。
小结