本章学习文件I/O。
不带缓冲指的是每个读写操作都会调用内核中的一个系统调用。
File descriptor
相当于win下的文件句柄,一般0:stdin,1:stdout,2:stderr,在依从POSIX标准的系统中,使用宏STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO来确定,最好不用硬编码。
文件描述符的取值范围是0~OPEN_MAX。
open function
#include <fcntl.h>
int open(const char *pathname, into flag, … /*mode_t mode */)
flag包括O_RDONLY, O_WRONLY和O_RDWR(以上3选1),和O_APPEND, O_CREAT, O_EXCL(测试是否存在),O_TRUNC, O_NOCITY(终端相关),O_NONBLOCK(非阻塞,对FIFO、块文件或字符设备文件有效),SUS扩展选项:O_DYSNC(每次write等待物理I/O完成,属性除外),O_RSYNC(读同步),O_SYNC(写操作,包括属性,在不同系统上实现不太一样)。
open操作返回的文件描述符一定是未用的最小文件描述符。
如果文件名长度大于NAME_MAX有的系统会直接截断,有的会返回出错。_POSIX_NO_TRUNC确定了遵从哪种行为,如果=1,那么出错,设置errno;否则直接截断。
creat function
令人蛋碎的create函数,别问为啥没有最后的e,这个问题去问ken大神。
int creat(const char *pathname, mode_t mode)
这个函数现在一般不用了,等价于open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode),之所以存在,是对旧系统的兼容。
close function
#include <unistd.h>
int close(int filedes),同上面的一样,失败返回-1
lseek function
off_t lseek(int filedes, off_t offset, int whence)
通过whence参数描述具体的行为,可以是SEEK_SET, SEEK_CUR, SEEK_END,具体意思和ISO C中一致。如果是确定当前偏移量,可以用lseek(fd,0,SEEK_CUR)
off_t的实现根据平台有所变化,一般有32位和64位两种。可以使用sysconf确认,这里标准有些混乱。
注意:某些设备的偏移量可能是负值,因此对结果,确认失败与-1比较是否相等比确认负值更为妥当。
如果写入内容超出文件末端,文件大小会被扩大,但是两块内容之间的空洞是否占用空间是由具体实现决定的。
read function
sszie_t read(int filedes, void *buf, size_t nbytes);//posix.1
int read(int filedes, char *buf, unsigned nbytes);//经典
返回读到的字节数(或-1),读到EOF则返回0
write function
sszie_t write(int filedes, const void *buf, size_t nbytes)
类似read,失败的原因多半是磁盘空间已经写满,或文件大小超限。
对于Unix系统,字符和二进制没有区别…换句话说,一切皆字符。
buf大小的选择,一般与文件系统的block长度相关,可以通过实验得出恰当值。
文件共享
系统有一张进程表,每个进程在其中有一个记录项,记录项包含一张该进程打开的所有文件的描述符表,这个表格主要包含两项内容:文件描述符标志和对应的文件表项指针;后者包含文件状态标志、文件偏移量和指向该文件v节点表项的指针。v节点包含了文件类型和对此文件进行各种操作的函数的指针,此外还可能包含了文件的i节点(索引节点)。i节点是文件的固有属性,包含文件的所有者、文件长度、文件所在的设备和指向文件实际数据块在磁盘所在位置的指针。
以上描述根据实现不同而有所不同,Linux没有v节点,采用了一个独立于文件系统的i节点和一个依赖于文件系统的i节点。
一个给定的文件只有一个v节点表项,但是打开该文件的每个独立进程都有自己的文件表项(以维持各自的偏移量)。当写入的偏移量超出文件长度,i节点中的对应信息也会更新为文件偏移量。
可能有多个文件描述符项指向同一个文件表项;文件状态标志会影响所有指向该文件表项的任何进程中的描述符。
原子操作
所谓原子操作,意思就是不能被打断的操作,要么执行,要么不执行,不能被暂停的操作,主要用于多线程同步。
SUS的扩展原子I/O,包括:
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset)
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset)
注意这里直接将偏移量做为参数,因此相当于将lseek和read/write合并为一个原子(当然,不更新文件偏移量指针)
open(fd, O_CREAT | O_EXCL)也可以作为一个原子,意思是如果不存在就创建文件,否则返回-1,O_TRUNC也有类似的效果。
dup和dup2
复制文件描述符的函数(命名方式非常糟糕)。
int dup(int filedes);
int dup(int fiedes1, int filedes2)
前者一定返回可用的最小文件描述符,后者可以通过filedes2指定(如果filedes2已经打开,需要先将其关闭,如果两个参数相等就不用关闭了)。
dup后的描述符共享文件表项,所以需要使用原子函数读写。
另外,可以使用万用函数fcntl进行复制,dup1: fcntl( fd, P_DUPFD, 0); dup2: close(fd2); fcntl(fd1, F_DUPFD,fd2),注意后者不是原子操作。
sysc, fsync和fdatasync
用于高速缓存的读写同步。
sync最简单,会将所有修改过的块缓冲区排入写队列,然后返回(作用于全局),并不等待写完成。系统守护进程会周期地调用该函数维护系统。
fsync(fd),作用于单一文件描述符,等待写操作结束。
fdatasync类似于fsync,但是只更新数据,不更新属性。(并非所有系统都支持)
fctnl function
字面意思就是文件操作函数,万用函数。主要用来改变已经打开文件的性质。
#include <fctnl.h>
int fctnl(int filedes, int cmd, … /* int arg */ )
行为依赖于cmd,包括:
- F_DUPFD,复制文件描述符,前面有相关描述,从略;
- F_GETFD, F_SETFD,获得/设置文件描述符标记,现在只有一个:FD_CLOEXEC(执行时是否关闭);
- F_GETFL, F_SETFL,获得/设置文件状态标志,即O_xxx一系列open函数使用的标志;
- F_GETOWN, F_SETOWN,获得/设置异步I/O所有权,见14章;
- F_GETLK, F_SETLK, F_SETLKW,获得/设置记录锁;
对于F_GETFL/F_SETFL,有个比较蛋碎的地方,它们共占一位,如果需要取得具体的,先要把结果与O_ACCMODE相与,然后再和3个读取方式逐一比较;其他的就直接位与就行了,=1说明对应标志置位。
ioctl function
字面意思是I/O控制函数,万用函数。
#include <unistd.h> /*system V*/
#include <sys/ioctl.h> /*BSD and Linux*/
#include <stropts.h> /*XSI STREAMS*/
int ioctl(int filedes, int request, …)
这货是SUS的一个扩展,功能非常多。通常最后一个参数是指向一个变量或结构的指针。除了这个头文件之外,还需要其操作设备的头文件。每个设备驱动都可以自定义自身的ioctl命令,因此它的用法非常多。
/dev/fd
非POSIX.1特性。打开/dev/fd/n,相当于复制描述符n(假设n是打开的),主要用于shell。
有些系统可能直接提供/dev/stdin, /dev/stdout和/dev/stderr来表述标准流。
有些bash shell中,可以使用 '-'来表述标准输入,但是如果有/dev/fd/0或/dev/stdin,显然使用后者更清晰。