在进程中指向一个唯一绑定的文件(设备),通过引用文件描述符可以操作特定的目标文件(设备)。
文件描述符是一个非负整数,UNIX系统shell把文件描述符0与进程的标准输入(STDIN_FILENO
)关联,把文件描述符1与进程的标准输出(STDOUT_FILENO
)关联,把文件描述符2与进程的标准错误(STDERR_FILENO
)关联,通常定义于头文件unistd.h
中。
文件描述符的作用域被进程限制(Q:多进程打开同一文件),文件描述符与文件/设备绑定,与进程绑定,相当于文件/设备与进程间数据流通的端口。
open
和openat
#include
int open(char *path, int oflags,.../* mode_t mode */);
int openat(int fd, char *path, int oflags,.../* mode_t mode */);
IOS C中,最后一个参数用
...
表示余下参数的数量和类型可变。
path: 打开文件路径
oflags: 打开方式
O_APPEND:每次写时追加到文件尾端。
O_CLOEXEC:把ED_CLOEXEC常量设置为文件描述符的标志。
O_CREAT:若文件不存在,则创建它。使用此选项时,
open/openat
需要说明mode
参数指明创建文件的访问权限。O_DIRECTORY:如果
path
引用的不是目录则出错。O_EXCL:如果同时指定了O_CREAT,且文件已经存在,则出错。
O_SYNC:使得每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。
O_TRUNC:如果此文件存在,且为只写或者读写打开,则将其长度截断为0.
O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准的termios参数值,使其符合Single UNIX Specification。
O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要文件属性被更新。
O_RSYNC:使每一个以文件描述符作为参数的read操作等待,直到所有对文件同一部分挂起的写操作都完成。
由open
或openat
返回的文件描述符一定是最小的未使用的文件描述符数值。
fd参数把open
和openat
函数区分开,共有三种可能性:
(1) path
参数指定的是绝对路径名,在这种情况下,fd
参数被忽略,openat
函数就相当于open
函数。
(2) path
参数指定的是相对路径名,fd
参数指出了相对路径名在文件系统中的开始地址。fd
参数是通过打开相对路径名所在的目录来获取的。
(3) path
参数指定相对路径名,fd
参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat
函数在操作上与open
类似。
openat
函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题,一,让线程可以以相对路径名打开目录中的文件,而不再只能打开当前工作目录。在第11章中我们会看到,同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同的线程在同一时间工作在不同的目录中。第二,可以避免time-of-check-to-time-of-use(TOCTTOU)
的错误。
TOCTTOU
错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的,因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个函数调用的结果不再有效,使得程序最终的结果是错误的。
creat
也可调用creat函数创建一个新文件。
#include
int creat(const char *path, mode_t mode);
// 返回值:若成功,返回为只写打开的文件描述符;若出错,返回-1
注意:此函数等效于:
open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
open(path, O_RDWR|O_CREAT|O_TRUNC, mode);
close
#include
int close(int fd);
// 若成功,返回0;若出错,返回-1;
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内核会自动关闭它所有的打开文件。
lseek
每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是一个非负整数,用以度量从文件开始处计算的字节数(本节稍后将对“非负”这一修饰词的某些例外进行说明)。通常,读,写操作都是从当前文件的偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND
选项,否则该偏移量被设置为0。
可以调用lseek
显式地为一个打开文件设置偏移量。
off_t lseek(int fd, off_t offset, int whence);
//返回值:若成功,返回新的文件偏移量;若出错,返回-1
对参数offset
的解释与参数whence
有关。
offset
是SEEK_SET,则将该文件的偏移量设置为距文件开始处的offset
个字节。offset
是SEEK_CUR,则将该文件的偏移量设置为当前值加offset
,offset可为正负。offset
是SEEK_END,则将该文件的偏移量设置为文件长度加offset
,offset可为正负。这种方法也可以用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个通道、FIFO或者网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
通常,文件的偏移量是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移值必须为非负值,所以在返回lseek的返回值时应当谨慎,不要测试他是否小于0,而要测试他是否等于-1;。
lseek将当前的文件偏移量记录在内核中,他不引起任何的IO操作。然后,该偏移量用于下一个读或者写操作。
文件的偏移量可以大与文件的当前长度,在这种情况下,对文件的下一次写将加长该文件,并在文件中形成一个空洞,这一点是允许的,位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占有存储空间。具体的处理方式与文件系统的实现有关。,当定位超过文件尾端之后写的,对新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分不需要分配磁盘块。
read
#include
ssize_t read(int fd, void *buf, size_t nbytes);
// 返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1;
如read
成功,则返回读到的字节数。若已到达文件的尾端,则返回0。
有多种情况可使实际读到的字节数少与要求读的字节数:
读操作是从文件的当前偏移偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。POSIX.1从几个方面对read函数原型做了更改,经典的原型定义是:
int read(int fd, char *buf, unsigned nbytes);
write
调用write
函数向打开文件写数据。
#include
ssize_t write(int fd, const void *buf, size_t nbytes);
// 返回值:若成功,返回已写字节数;若出错,返回-1;
其返回值通常与参数nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,将文件的偏移量增加实际写的字节数。
预读/高速缓存
UNIX系统支持在不同进程中共享打开文件。在介绍dup
函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。
内核使用三种数据结构表示打开文件。
(1) 进程的进程表项中都有一个记录项,这个记录项中包含一张打开的文件描述符表,每个描述符占一项。与每个描述符想关联的是:
a. 文件描述符标志(close_on_exec)。
b. 指向一个文件表项的指针。
(2) 内核为所有打开文件维持一张文件表。每个文件表包含:
a. 文件状态标志(读,写,添写,同步和非阻塞等)
b. 当前文件的偏移量。
c. 指向该文件v节点表项的指针。
(3) 每个打开的文件(设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含了文件的所有者,文件长度,指向文件实际数据块在磁盘上所在位置的指针等。
虚拟文件系统(Virtual File System),把与文件系统无关的i节点部分称为v节点。目的是对在一个计算机系统上的多文件系统类型提供支持。
假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为每个进程操作文件的方式不同。
lseek
定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置成i节点表项中的当前文件长度。lseek
函数只修改文件表项中的当前文件偏移量,不进行任何的I/O操作。dup
函数时,我们就能看到这一点。在fork
后,也发生相同的情况,此时父进程、子进程各自的每一个打开的文件描述符共享同一个文件表项。说明进程表项在进程所占内存中,由进程管理。文件表项不再进程所占内存中,由内核管理?错误
注意文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,后真则应用于指向该文件表项的任何进程中的所有描述符。
多个进程读取同一个文件能正确工作。每个进程都有它自己的文件表项,也有它自己的当前文件偏移量。但是,当多个进程同时写一个文件时,则可能产生意想不到的结果。
每个进程对于同一个文件仅仅持有一个文件表项?错误
任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核可能会临时挂起进程,在此期间,前一个函数调用的结果可能已经被修改或无效。
一般而言,原子操作(atomic operation)值的是由多步组成的一个操作,如果该操作原子的执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行一个步骤的子集。
dup
和dup2
#include
int dup(int fd);
int dup2(int fd, int fd2);
// 两函数的返回值:若成功,则反回新的文件描述符;若出错,返回-1
由dup
返回的文件描述符一定是当前可用的文件描述符中的最小数值。对于dup2
,可以用fd2
参数指定新描述符的值。如果fd2
已经打开,则先将其关闭。若fd
等于fd2
,则dup2
返回fd2
,而不关闭它。否则,fd2
的FD_CLOEXEC
文件描述符标志就被清除,这样fd2
在进程调用exec时是打开状态。
这些函数返回的新文件描述符与参数fd
共享同一个文件表项。
我们假定进程启动时执行了
newfd = dup(1);
当函数开始执行时,假定下一个可用的文件描述符是3。因为两个文件描述符指向同一个文件表项,所以它们共享同一个文件状态标志(读,写,追加等)以及同一个当前文件偏移量。
每个文件描述符都有它自己的一套文件描述符标志。正如我们将在下一节中说明的那样,新描述符的执行时关闭(close-on-exec)标志总是由dup
函数清除。
复制一个文件描述符的另一种方法是使用fcntl
函数。实际上,调用
dup(fd);
等效于
fcntl(fd, F_DUPFD, 0);
而调用
dup2(fd, fd2);
等效于
close(fd2);
fcntl(fd, F_DUPFD, fd2);
在后一种情况,dup2
并不完全等同于close
加上fcntl
。它们之间的具体区别如下。
(1) dup2
是一个原子操作,而close
和fcntl
包括两个函数调用。有可能在close
和fcntl
之间调用了信号捕获函数,它可能修改文件描述符。如果不同的线程改变了文件描述符的话,也会出现相同的问题。
(2) dup2
和fcntl
有一些不同的errno
。
sync
、fsync
、fdatasync
传统的UNIX系统实现在内核中设有缓冲区高速缓存
或页高速缓存
,大多数的磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)。
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX 系统提供了sync
、fsync
、fdatasync
。
#include
int fsync(int fd);
int fdatasync(int fd);
// 若成功,返回0,若出错,返回-1
void sync(void);
sync
只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘的操作结束。
通常,称为update
的系统守护进程周期性的调用(一般每隔30秒)sync
函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(1)
也调用sync函数。
fsync
函数只对文件描述符fd
指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync
可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写道磁盘上,
fdatasync
函数类似与fsync
,但它只影响文件的数据部分。而除数据外,fsync
还会更新文件的属性。
Q: 为什么更新文件数据和更新文件属性异步?
fcntl
fcntl
函数可以改变已经打开
文件的属性。
#include
int fcntl(int fd, int cmd, .../* int arg */);
// 返回值:若成功,则依赖于cmd;若出错,返回-1
在本节的各实例中,第三个参数总是一个整数,与上面所示的函数原型中的注释部分对应。但是在14.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
)。
这里将讨论进程表项中各文件描述符相关的文件描述符标志和文件表项中的文件状态标志。
F_DUPFD
:复制文件描述符fd
。新文件描述符作为函数值返回,它是尚未打开的各描述符中大于或等于第三个参数值(取为整型值)中各值的最小值。新描述符与fd
共享同一个文件表项。但是,新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC
文件描述符标志已被清除(这表示该描述符在exec时仍保持有效)。F_DUPFD_CLOEXEC
:复制文件描述符,设置与新文件描述符关联的F_CLOEXEC
文件描述符标志的值,返回新文件描述符。F_GETFD
:对应与fd
的文件描述符标志值作为函数值返回。F_SETFD
:设置文件描述符fd
的文件描述符标志。新标志值按第三个参数设置。F_GETFL
:对应于fd
的文件表项的文件状态标志作为函数值返回。我们在说明open
函数时,已描述了文件状态标志:
O_RDONLY
只读打开O_WRONLY
O_RDWR
O_EXEC
只执行打开O_SEARCH
只搜索打开目录O_APPEND
追加打开O_NONBLOCK
非阻塞模式O_SYNC
等待写完成(数据和属性)O_DSYNC
等待写完成,仅数据O_RSYNC
同步读和写O_SETFL
:将文件状态标志设置为第三个参数的值。可以更改的时O_APPEND
之后的文件状态标志。F_GETOWN
:获取当前接收SIGIO
和SIGUGR
信号的进程ID或进程组ID。F_SETOWN
:设置接收SIGIO
和SIGURG
信号的进程ID或进程组ID。fcntl
的返回值于命令有关。如果出错,所有命令返回-1,如果成功则返回某个其他值。
下列4个命令有特定的返回值:F_DUPFD
,F_GETFD
,F_GETFL
以及F_GETOWN
。第一个命令返回新的文件描述符,第二和第三个文件返回相应的标志,最后一个命令返回一个正的进程ID或负的进程组ID。
在修改文件描述符标志或者文件状态标志时必须谨慎,先要获取现在的标志值,然后按照期望修改它,最后设置新标志值。不能只是执行F_SETFD
或者F_SETFL
命令,这样会关闭以前设置的标志位。
开启同步写标志,将会使每次写操作都会阻塞,等待数据写入磁盘后,再返回。在UNIX系统中,通常write
只是将数据排入队列,而实际的写磁盘操作则可能在以后的某个时刻进行(写入高速缓存->推入写队列)。而数据库系统则需要使用O_SYNC
,这样依赖,当他从write
返回时就知道数据已经确实的写到了磁盘上,以免在系统异常时丢失数据。
fcntl
函数时只需要做很少的工作。ioctl
除上述外,其他I/O操作。
#include
#include
int ioctl(int fd, int request, ...);
// 返回值:若出错,返回-1;若成功,返回其他值
/dev/fd
文件描述符被映射成指向底层物理文件的符号链接。
如果一个程序要从磁盘或网卡提取数据,那么
CPU
会发出一个指令通知磁盘和网卡,之后将进程阻塞,切换到其他进程,磁盘和网卡数据抵达内存的时候,会发出中断提醒CPU
切回来。将进程阻塞,等待缓慢的I/O
传送延迟时处理其他任务,依赖的是中断。
但是内存不是I/O
,也不能用I/O
来称呼内存,内存又叫主存储器,在冯氏体系中是独立于I/O
的部分,当CPU
执行一个进程时,从内存中该进程所在的空间连续不断的取
指令,也就是说,写程序和执行程序的时候,是不考虑什么内存延迟的。
内存延迟的概念出现在CPU
主频大幅提升的时代,这时候物理上的CPU
已经比内存快了很多,也就是说取指令会有延迟,但是CPU
无法解决内存延迟,因为所有进程都在内存中,切哪一个都得等,不像I/O
,它们比内存还要慢的多,在内存里切换进程等时间等时间对等待I/O
时间而言是可以忽略不计的。所以对于内存延迟,CPU唯一的办法就是等待内存响应。没有其他办法。
为了进一步提高性能,减少内存等待时间,CPU发展出来复杂的高速缓存系统,从内存预先猜测哪些指令可能会被执行,预先将他们提取到速度与CPU差不多的高速缓存中存起来。高速缓存不命中,才会到内存中进行读取。
大多数文件系统为了改善性能都采用某种预读的方式,当检测到正在顺序读取时,就系统就试图读入比应用要求的更多的数据,并假想应用很快会读取到这些数据(减少物理I/O次数,尽量使磁盘读取,和内存读取速度相匹配)。
操作系统试图用高速缓存技术将相关文件放到主存中,重复读写同一文件,一般会从高速缓存中读写文件。
我们向文件写入数据时,内核通常将数据写入高速缓存中,再排入物理I/O写队列中,晚些时候再写入磁盘。
dup2
功能相同的函数,要求不调用fcntl
函数,并要有正确的出错处理。dup2(fd, new_fd);
new_fd
与fd
指向相同的文件表项。dup函数总是返回当前可用的最小的文件描述符。
fd1 = open(path, oflags);
fd2 = dup(fd1);
fd3 = open(path, oflags);
fd1,fd2,fd3分别文进程表中的一项文件描述符项,fd1和fd2指向相同的文件表项,fd3指向自己的文件表项,fd1,fd2,fd3的文件表项指向相同的文件v结点。
文件描述符项主要包含文件描述符,文件描述符标志,文件表项指针。文件表项主要包含文件状态标志,当前文件偏移量和文件i结点指针。文件的v结点主要包含文件属性,文件数据。
lseek
在任一位置开始读?能否用lseek
更新文件中的任一部分数据?能,不能