3.2 文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个文件或者创建一个新文件时,内核向进程返回一个文件描述符。
UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准出错相关联。可以替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO
3.3 open函数
调用open函数可以打开或者创建一个文件
1: #include<fcntl.h>2: int open(const char *pathname,int flags,..../*mode_t mode*/);3: //返回值:若成功返回文件描述符,若出错返回-1
对于open函数而言,仅当创建新文件时才使用第三个参数
pathname是要打开或创建的文件的名字。oflag参数可以用来说明此函数的多个选项。用下列一个或者多个常量或运算构成flag参数
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
在这三个常量中必须指定一个且只能指定一个,下面常量是可以选择的:
O_APPEND 每次写时都追加到文件尾端。
O_CREAT 若此文件不存在则创建它。使用此选项时,需要第三个参数mode
O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则会出错。用从可以判断一个文件是否存在,如果不存在,创建此文件,这使得测试和创建文件成为一个原子操作
O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其文件长度截短为0
O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件打开操作和后续的IO操作设置非阻塞模式。
O_DSYNC 使每次IO等待物理IO完成,但是如果写操作不影响刚读取刚写入的数据,则不等待文件属性被更新
O_RSYNC 使每一个以文件描述符作为参数的read操作等待,直至任何对文件同一部分进行的未决写操作都完成
O_SYNC 使每次write都等到物理IO操作完成,包括由write操作引起的文件属性更新所需的IO
由open函数返回的文件描述符一定是最小的未用描述符数值
3.5 close函数
可以调用close函数关闭一个打开的文件
1: #include<unistd.h>2: int close(int filedes);3: //返回值:若成功返回0,出错返回-
关闭一个文件时,还会释放该进程加在该文件上的所有记录锁
当一个进程终止时,内核会自动关闭它所有打开的文件。
3.6 lseek函数
每一个打开的文件都有一个与其相关的”当前文件偏移量“。通常读、写文件都从当前文件偏移量处开始
可以调用lseek函数显示地为一个打开的文件夹件设置其偏移量
1: #include<unistd.h>2: off_t lseek(int filedes,off_t offset,int whence)3: //若成功返回新的文件偏移量,出错返回-1
对参数off_t的解释与参数whence的值有关
若whence是SEEK_SET,则将该文件的偏移量设置为距离文件开头处offset个字节
若whence是SEEK_CUR,则将文件偏移量设置为当前值加上offset,offset可正可负
若whence是SEEK_END,则将文件偏移量设置为文件长度加上offset,offset可正可负
若lseek成功执行,则返回新的文件偏移量,为此可以用下面的方式检查打开文件的当前偏移量:
off_t currpos;
currpos=lseek(int filedes,0,SEEK_CUR);
如果文件描述符引用的是一个FIFO、管道或者网络套接字,则lseek返回-1,并将errno设置为ESPIPE
lseek仅将当前的文件偏移量记录在内核中,并不引起IO操作。然后该偏移量用于读、写操作
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,位于文件中,但没有被写过的字节都被读为0
文件上的空洞并不在磁盘上占用存储区。
3.7 read函数
使用read函数从打开文件中读数据
1: #include<unistd.h>2: ssize_t read(int filedes,void *buf,size_t nbytes);3: //返回值:若成功则返回读到的字节数,若已经到文件尾则返回0,若出错返回-1
若read成功,则返回读到的字节数。如果已经到达文件尾,则返回0.
有多种情况实际读到的字节数少于要求读的字节数:
读普通文件时,在读到要求字节数之前已经到达文件尾。
从终端设备读时,通常一次最多读一行
当从网络中读时,网络中的缓冲结构可能构成返回值小于要求读的字节数
当从管道或FIFO读时,如管道包含的字节数少于所需的量,那么read只返回实际读到的字节数。
当从某些面向记录的设备读时(例如磁带),一次最多返回一个记录
当某一信号造成中断,而已经读了部分数据量时。
3.8 write函数
使用write函数向已经打开的文件写数据
1: #include<unistd.h>2: ssize_t write(int filedes,const void *buf,size_t nbytes);3: //返回值:若成功则返回已写的字节数,若出错返回-1
返回值一般与nbytes相同否则表示出错
write出错的一个常见原因是:磁盘已经写满,或者文件长度已经超过进程的文件长度限制
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件的偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数
3.10文件共享
UNIX系统支持在不同的进程中共享打开的文件。
内核使用三种结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程的影响
(1)每一个进程在进程表中有一个记录项,记录项中包含有一张打开的文件描述符表,可以将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a.文件描述符标志(close_on_exec)
b.指向一个文件表项的指针
(2)内核为所有打开文件维持一张文件表,每个文件表项包含:
a.文件状态标志(读、写、添写、同步和非阻塞等,)
b.当前文件偏移量
c.指向该文件v节点表项的指针
(3)每一个打开的文件都有一个v节点结构,v节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v节点还包含了该文件的i节点。这些信息是在打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如:i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上位置的指针。
如果两个进程各自打开了同一个文件,则有下图的安排。打开该文件的每一个进程都得到一个文件表项,但对于一个文件只有一个v节点表项。每个进程都有自己的一个文件表项的理由是:每个进程都有它自己的对该文件的当前偏移量
(1)在完成每一个write后,在文件表项中的当前文件偏移量即增加所写的字节数。这使当前文件偏移量超过了当前文件的长度,则在i节点表项中的当前文件长度被设置为当前文件偏移量
(2)如果用O_APPEND标志打开了一个文件,则相应的标志也被设置到文件表项的文件状态标志中。每次对这种具有填写标志的文件执行写操作时,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。
(3)若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。
(4)lseek函数只修改文件表项中的当前文件偏移量,没有进行任何实际IO操作
可能有多个文件描述符项指向同一文件表项。在fork之后,此时的父、子进程对于每一个打开文件描述符共享同一个文件表项。
注意,文件描述符标志和文件状态标志在作用域方面的区别,前者只用于一个进程的一个文件描述符,而后者适用于指向该给定文件表项的任何进程的所有文件描述符。
3.11 原子操作
一般而言,原子操作指的是由多步组成的操作。如果该操作原子地执行,则要么执行完所有的步骤,要么一步也不执行,不可能执行所有步骤中的一个子集。
3.12 dup和dup2函数
下面两个函数可用来复制一个现存的文件描述符
1: #include<unistd.h>2: int dup(int filedes);3: int dup2(int filedes1,int filedes2);4: //返回值:若成功返回新的文件描述符,若出错返回-1
由dup函数返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新文件描述符的数值。如果filedes2应经打开,则先将其关闭。如若filedes1和filedes2相等,则dup2返回filedes2,不关闭filedes2.
这些函数返回的新的文件描述符与参数filedes共享同一文件表项。下图显示了这种情况:
每个文件描述符都有自己的文件描述符标志。新描述符的执行关闭(close_on_exec)标志总是由dup函数清除。
3.13 sync、fsync和fdatasync函数
传统的UNIX实现在内核中设有缓冲区高速缓存或者页面高速缓存,大多数磁盘IO都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后等待其到达队首,才进行实际的IO操作。这种方式被称为延迟写。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟会造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区告诉缓存中的内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数
1: #include<unistd.h>2: int fsync(int filedes);3: int fdatasync(int filedes);4: //返回值:若成功则返回0,若出错则返回-1
5: void sync(void);
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等实际写磁盘操作结束
fsync函数只对有filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回.
fdatasync函数类似于fsync,但它只影响文件的数据部分.而除了数据部分,fsync还会同步更新文件的属性.
3.14 fcntl函数
fcntl函数可以改变已打开文件的性质.
1: #include<fcntl.h>2: int fcntl(int filedes,int cmd,..../* int arg */);3: //返回值:若成功则依赖于cmd,若出错则返回-1
fcntl函数有5种功能:
(1)复制一个现有的文件描述符(cmd=F_DUPFD)
(2)获得或设置文件描述符标记(cmd=F_GETFD或F_SETFD)
(3)获得或设置文件状态标志(cmd=F_GETFL或F_SETFL)
(4)获得或者设置异步IO所有权(cmd=F_GETOWN或F_SETOWN)
(5)获得或设置记录锁(cmd=F_GETLK或F_SETLK、F_SETLKW)
F_DUPFD 复制文件描述符filedes。新文件描述符作为函数值返回。它是未打开的各描述符中大于或等于第三个参数值中各值的最小值新描述符与filedes共享同一文件表项。但是,新文件描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除。
F_GETFD 对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
F_SETFD 对于filedes设置文件描述符标志。新标志按第三个参数设置。
F_GETFL 对应于filedes的文件状态标志作为函数值返回。在说明open函数时,已说明了文件状态标志。
fcntl的文件状态标志
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
O_APPEND 每次写时追加
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读、写
F_SETFL 将文件状态标识设置为第三个参数值。
F_GETOWN 取当前接受SIGIO和SIGURG信号的进程ID或进程组ID。
F_SETOWN 设置接受SIGIO和SIGURG信号的进程ID或者进程组ID
习题:
当读、写磁盘文件时,上文描述的函数是否有缓冲机制?
所有磁盘IO都要经过内核的块缓冲,
所谓无缓冲IO是指用户进程不对函数进行自动缓冲,每一次read/write进行一次系统调用