APUE读书笔记之第三章 文件I/O
1.UNIX系统中大多数文件IO只需用到5个函数:open、read、write、lseek和close。这些函数又称为不带缓冲的IO(unbuffered I/O),不带缓冲指的是每个read跟write都调用内核中的一个系统调用(这些不带缓冲的IO属于系统调用,不属于C库函数)。
2.对于内核而言,所有打开的文件都通过文件描述符引用。每个进程(正在执行的程序)都会有3个已经打开的文件:标准输入(文件描述符0)、标准输出(文件描述符1)和标准错误(文件描述符2)。符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,都定义在头文件<unistd.h>中。
3.open和openat函数
作用:打开或者创建一个文件
头文件: #include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数原型: int open(const char *pathname, int flags, mode_t mode);
int openat(int fd,const char *pathname, int flags, mode_t mode);
参数说明:
pathname:路径名
mode_t mode:表明余下的参数的数量跟类型是可变的(对open函数而言,仅当创建新文件时才使用最后这个参数)
flags:可用来说明次函数的多个选型。用下列一个或多个常量进行“或”运算构成flags参数(这些常量定义在 <fcntl.h>中)
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR: 读写打开
上面3个常量必须指定一个且只能指定一个。下面的常量则是可选的。
O_APPEND:每次写时都追加到文件的尾端
O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志
O_CREAT:若此文件不存在则创建它。使用此选项的时候,open函数需说明第3个参数mode(openat函数需说明 第4个参数mode),用mode指定新文件的访问权限位
O_DIRECTORY:如果pathname引用的不是目录,则出错
O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则 创建它,这使测试和创建两者成为一个原子操作
O_NOCTTY:如果pathname引用的是终端设备,则不将该设备分配作为此进程的控制终端
O_NOFOLLOW:如果pathname引用的是符号链接,则出错
O_NONBLOCK:如果pathname引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次 打开操作和后续的I/O操作设置非阻塞方式
O_SYNC:使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O
O_TRUNC:如果文件存在,而且以只写或读写成功打开,则将其长度截断为0
O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification
O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件 属性被更新
fd参数把open和openat函数区分开,共有3中可能性:
1.pathname参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数
2.pathname参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相 对路径名所在的目录来获取(这种情况相当于相对路径不是当前路径,可以重新指定相对路径)
3.pathname参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获 取,openat函数在操作上与open函数类似(open函数的相对路径就是当前路径,此时两函数类似)
引入openat函数是方便一个进程内的各线程可拥有不同的当前目录,传统的chdir会影响整个进程,而使用openat只需 要在每个线程初始化时打开一个目录(调用open),然后就可以以openat在“当前目录”操作文件了,如:
int dirfd=open("/tmp");//相当于chdir到/tmp
int filefd=openat(dirfd,"myfile");//在/tmp目录下打开myfile
返回值:若成功,返回文件描述符;若失败,返回-1
4.creat函数
作用:创建一个新文件
头文件: #include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数原型: int creat(const char *pathname,mode_t mode);
返回值:若成功,返回为只写打开的文件描述符;若出错,返回-1
此函数等价于:
open(pathname,O_WRONLY | O_CREAT | O_TRUNC,mode);(因为O_CREAT的存在,对O_TRUNC而言,此文件 肯定存在)
一般可以用open函数代替creat函数
5.close函数
功能:关闭一个打开文件
头文件: #include <unistd.h>
函数原型:int close(int fd);
返回值:若成功,返回0;若出错,返回-1
关闭一个文件时还会释放该进程加在该文件上的所有记录锁
当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都利用了这一功能而不显示地用close关闭打开的文件
6.lseek函数
每一个打开的文件都有一个与其相关联的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
函数原型: off_t lseek(int fd, off_t offset, int whence);
功能:显示地为一个打开文件设置偏移量
头文件:#include <sys/types.h>
#include <unistd.h>
对参数offset的解释与参数whence有关:
若whence是SEEK_SET,则将该文件偏移量设置为距文件开始处offset个字节
若whence是SEEK_CUR,则将该文件偏移量设置为其当前值加offset,offset可正可负
若whence是SEEK_END,则将该文件偏移量设置为文长度加offset,offset可正可负
返回值:若成功,返回新的文件偏移量;若失败,返回-1
若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
curr_pos=lseek(fd,0,SEEK_CUR);
这种方法也可以用来确定所涉及的的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
通常,文件的当前偏移量应当是个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负数,所以在比较lseek的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1
因为偏移量(off_t)是带符号数据类型,所以文件的最大长度会减少一半。例如,若off_t是32位整型,则文件最大长度是2^31-1个字节
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何的I/O操作。然后,该偏移量用于下一个读或写操作
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读作为0。
文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块
od命令可以观察文件的实际内容,od -c表示以字符方式打印文件内容
7.read函数
函数原型:ssize_t read(int fd, void *buf, size_t count);
头文件: #include <unistd.h>
功能:从打开文件中读数据
返回值:如果成功,返回实际读到的字节数;如果已到达文件的尾端,返回0;如果出错,返回-1
有多种情况可使实际读到的字节数(返回值)少于要求读的字节数(count):
1.读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30个字节,而要求读100个字 节,则read返回30。下次再调用read的时候,它将返回0(文件尾端)
2.当从终端设备读时,通常一次最多读一行(第18章将介绍如何改变这一点)
3.当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数
4.当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数
5.当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录
6.当一信号造成中断,而已经读了部分数据量时。我们将在10.5节进一步讨论这种情况
读操作从文件的当前偏移量开始,在成功返回之前,该偏移量将增加实际读到的字节数
POSIX.1从几个方面对read函数的原型做了修改。经典的原型定义是:
int read(int fd,char *buf,unsigned count);
1.首先,为了与ISO C一致,第2个参数由char *改为void *。在ISO C中,类型void *用来表示通用指针
2.其次,其返回值必须是一个带符号整型(ssize_t),以保证能返回正整数字节数、0(表示文件尾端)或-1(标识出错)
3.最后,第3个参数在历史上是一个无符号整型,这允许一个16位的实现一次读或写的数据可以多达65534个字节。在1990 POSIX.1标准中,引入了新的基本系统数据类型ssize_t以提供带符号的返回值,不带符号的size_t则用于第3个参数
8.write函数
函数原型:ssize_t write(int fd, const void *buf, size_t count);
头文件: #include <unistd.h>
功能:向打开文件中写数据
返回值:如果成功,返回实际写的字节数;如果出错,返回-1
其返回值通常与count的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数
9./dev/null:外号叫无底洞,你可以向它输出任何数据,它通吃,并且不会撑着
/dev/zero:是一个输入设备,你可以用它来初始化文件,从里面读出来的数据都是0
10.文件共享
内核使用3种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响
(1)每个进程在进程表中都有一个记录项(每个进程都有一个进程表),记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a.文件描述符标志(close_on_exec)
b.指向一个文件表项的指针
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
a.文件状态标志(读、写、添写、同步和非阻塞等)
b.当前文件偏移量
c.指向该文件v节点表项的指针
(3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等
(打开文件描述符表可存放在用户空间(作为一个独立的对应于每个进程的结构,可以换出),而非进程表中)
图3-7显示了一个进程对应的3张表之间的关系。该进程有2个不同的打开文件:一个文件从标准输入打开(文件描述符0),另一个从标准输出打开(文件描述符1)
虚拟文件系统(virtual file system)
网络文件系统(NFS)
如果两个独立进程各自打开了同一文件,则有图3-8中所示的关系。
我们假定第一个进程在文件描述符3上打开了该文件,而另一个进程在文件描述符4上打开了该文件。打开该文件的每个进程都获得各自的一个文件表项,但对于一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
给出了这些数据结构后,现对前面所述的操作进一步说明。
1)在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项的当前文件长度设置为当前文件偏移量(也就是该文件加长了)
2)如果有O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这使得每次写入的数据都追加到文件的当前尾端处
3)若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度(注意,这与用O_APPEND标志打开文件是不同的)
4)lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作
可能有多个文件描述符项指向同一文件表项。3.12节讨论dup函数时,就能看到这一点。在fork后也发生同样的情况,此时父进程、子进程各自的每一个打开文件描述符共享同一文件表项。
注意,文件描述符标志跟文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程的所有描述符。说明fcntl函数时,将会了解如何获取和修改文件描述符标志跟文件状态标志
本节前面所述的一切对于多个进程读取同一文件都能正确工作。每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一文件时,则可能产生意想不到的结果。
11.原子操作
任何要求多于一个函数调用的操作都不是原子操作,因为在这两个函数调用之间,内核有可能会临时挂起进程
对于open函数的O_CREAT和O_EXCL这两个选项。如果同时指定这两个选项,而该文件已经存在时,open将失败,如果文件不存在则创建。检查文件是否存在跟创建文件这两个操作是作为一个原子操作执行的。
一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集
12.dup和dup2函数
函数原型: int dup(int oldfd);
int dup2(int oldfd, int newfd);
头文件: #include <unistd.h>
功能:复制一个现有的文件描述符
返回值:若成功,返回新的文件描述符;若出错,返回-1
由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。对于dup2,可以用newfd参数指定新文件描述符的值。如果newfd已打开,则先将其关闭。如若oldfd等于newfd,则dup2返回newfd,而不关闭它。否则,newfd的FD_CLOEXEC文件描述符标志就被清除,这样newfd在进程调用exec时是打开状态
这些函数返回的新文件描述符与之前的文件描述符共享同一个文件表项,如图3-9所示
每个文件描述符都有它自己的一套文件描述符标志。正如我们将在下一节说明的那样,新描述符的执行时关闭(close-on-exec)标志总是由dup函数清除
13.sync、fsync和fdatasync函数
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中的内容一直,UNIX系统提供了sync、fsync和fdatasync三个函数
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:若成功,返回0;若出错,返回-1
void sync(void);
sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束
通常,称为update的系统守护进程周期性的调用(一般每隔30s)sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(1)也调用sync函数
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性
14.fcntl函数
函数原型: int fcntl(int fd, int cmd, ... /*int arg */ );
头文件: #include <unistd.h>
#include <fcntl.h>
功能:可以改变已经打开文件的属性
返回值:若成功,则依赖于cmd(见下);若出错,返回-1
在本节的各实例中,第3个参数总是一个整数,与上面所示函数原型中的注释部分对应。但是在14.3节说明记录锁的时候,第3个参数则是指向结构体的指针
fcntl函数有以下5个功能:
(1)复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
(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)
先说明这11种cmd的前8种(14.3节说明后3中,它们都与记录锁有关)。参照图3-7,我们将讨论与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志
F_DUPFD:复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值(取为整 数)中各值的最小值。新的描述符与fd共享同一文件表项(见图3-9)。但是,新描述符有它自己的一套文件描述符 标志,其FD_CLOEXEC文件描述符标志被清除(这表示该描述符在exec时仍保持有效,第8章对此进行讨论)
F_DUPFD_CLOEXEC:复制文件描述符,设置与新描述符相关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符
F_GETFD:对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC
F_SETFD:对于fd设置文件描述符标志。新标志值按第3个参数设置
要知道,现在很多与文件描述符标志有关的程序并不使用常量FD_CLOEXEC,而是将此标志设置为0(系统默认,在exec时不关闭)或1(在exec时关闭)
F_GETFL:对应于fd的文件状态标志作为函数值返回。在说明open时,已描述了文件状态标志。列在图3-10中
遗憾的是,5个访问方式标志(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC以及O_SEARCH)并不各占1位(前 3个标志的值分别是0、1和2。这5个值互斥,一个文件的访问方式只能取这5个值之一)。因此首先必须用屏蔽字 O_ACCMODE取得访问方式位,然后将结果与这5个值中的每一个相比较
F_SETFL:将文件状态标志设置为第3个参数的值。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、 O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC
F_GETOWN:获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID
F_SETOWN:设置接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一 个进程组ID
fcntl的返回值与命令有关。如果出错,所以命令都返回-1,如果成功则返回某个其他值。下列4个命令有特定返回值:F_DUPFD、F_GETFD、F_GETFL以及F_GETOWN。第1个命令返回新的文件描述符,第2个和第3个命令返回相应的标志,第4个命令返回一个正的进程ID或负的进程组ID
暂时没看懂这些命令?
在修改文件描述符标志或文件状态标志时必须谨慎,先要获得现在的标志位,然后按照期望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位
语句:val |=flags;//意思是加上新的标志位,原来的标志位不清除
val &= ~flags;//意思是清除原来的标志位,只使用新的标志位
同步写标志(O_SYNC(等待写完成(数据和属性)))表示每次write都要等待,直到数据已写到磁盘再返回。和sync函数不同。(O_RSCNC:同步读和写,即等待读写完成)