1.文件描述符
所有执行I/O操作的系统调用都以文件描述符(一个非负整数)来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道、FIFO、socket、终端、设备、普通文件。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。每个进程,文件描述符都自成一套。
标准流(标准文件描述符)
3中标准的文件描述符:
当linux启动后,会自动打开三个文件,就是标准输入、标准输出、标准错误。标准输入流默认是键盘,标准输出流默认是终端,向错误流写数据,终端的默认做法是打印出错误内容,当然这些流可以更改的。
-
fprintf(stdout, "input someting") <=> printf("input someting")
- 向标准输出流(终端程序)输出一个字符串
-
fscanf(stdin, "%d",&a) <=> scanf("%d", &a)
- 向标准输入流(键盘)读入一个数据
-
fprintf(stderr, "a error occur")
- 向标准错误流写入一个错误信息
重定向标准流
-
./demo.out 1>>a.txt
输出流重定向- 将1代表的标准输出流重定向(>>)到a.txt文件
-
./demo.out 1>>a.txt
等价于./demo.out >>a.txt
-
./demo.out >>a.txt
输出流中的内容是追加的,追加到结尾 -
./demo.out >a.txt
输出流中的内容是覆盖的,再次写入会覆盖之前的内容
-
./demo.out <a.txt
输入流重定向
2.I/O模型
I/O的4个主要系统调用:
-
fd = open(pathname,flags,mode)
打开或创建一个新文件- flags标志
- 位掩码参数mode指定了新创建文件的权限,若open()并未指定O_CREAT标志,则忽略该参数
- S_IRUSER
- S_IWUSER
- 返回文件描述符值
- SUSv3规定,如果open()成功,必须保证其返回值为进程未用文件描述符中数值最小者,如果文件描述符0未使用,那么open一定会使用此文件描述符打开文件。
- 错误处理
- open()返回-1,错误号errno标识错误原因
- EACCES
- EISDIR
- EMFILE
- ENFILE
- ENOENT
- EROFS
- ETXTBSY
- flags标志
-
numread = read(fd,buffer,count)
读取fd所指代的文件中之多count字节的数据,并存储到buffer中- count参数指定最多能读取的字节数
- buffer参数提供用来存放输入数据的内存缓存地址
- 返回
- 遇到文件结束(EOF)则返回0
- 出错返回 -1
- 正确返回存放读取的字节数
numwritten = write(fd,buffer,count)
-
status = close(fd)
- 文件描述符属于有限资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。
3.改变文件偏移量:lseek()
off_t lseek(int fd, off_t offset, int whence)
- offset参数指定了一个以字节为单位的数值
- whence参数则表明赢参照哪个基点来解释offset参数,应为下列其中之一:
- SEEK_SET:文件头部开始
- SEEK_CUR:当前文件偏移量处
- SEEK_END:文件结尾
4.通用I/O模型以外的操作:ioctl()、fcntl()
ioctl()
ioctl()系统调用又为执行文件和设备操作提供了一种多用途机制。
-
int ioctl(int fd, int request,...);
- request指定了将在fd上执行的控制操作
- 第三个参数...(argp)可以是任意数据类型,根据request的参数值来确定argp所期望的类型。通常情况,argp指向整数或结构的指针
fcntl()
fcntl()系统调用对一个打开的文件描述符执行一些列控制操作
-
int fcntl(intn fd, int cmd, ...)
- cmd参数所支持的操作范围很广
5.原子操作和竞争条件
原子操作:将某一系统调用所要完成的各个动作作为不可中断的操作,一次性加以执行,期间不会为其他进程或线程所中断。所有的系统调用都是以原子操作方式执行的。
举例:当同时制定O_EXCL与O_CREAT作为open()标志位时,如果要打开已经存在的文件,就会返回一个错误,这提供了一种机制,对文件是否存在的检查和创建文件属于同一原子操作。区别于先检查文件再创建可能会造成其他进程在这个过程中抢占资源。
O_EXCL确保调用者就是文件的创建者。
O_APPEND标志,确保多个进程在对同一文件追加数据时不会覆盖彼此的输出。
6.打开文件的状态标志
获取访问模式和状态标志
fcntl()的用途之一是针对一个打开的文件,获取或修改其访问模式和状态标志(这些值是通过open()调用的flag参数设置的),应将fcntl()的cmd参数设置为F_GETTFL,并且获取的标志中总是包含O_LARGEFILE标志
flags = fcntl(fd, F_GETFL);
要判断是否包含某一标志位,只需要将flags于其相&即可。如下可以判断文件是否以同步方式打开:
if (flags & O_SYNC)
判定文件的访问模式稍微复杂一点,因为O_RDONLY(0) O_WRONLY(1) O_RDWR(2)这三个常量并不与打开文件状态标志中的单个比特位对应,需使用掩码O_ACCMODE与flag相与
accessMode = flags & O_ACCMODE
if (accessMode == O_WRONLY) {...}
修改访问模式和状态标志
使用fcntl()的F_SETFL来修改,允许更改的标志有:
- O_APPEND
- O_NONBLOCK
- O_NOATIME
- A_ASYNC
- O_DIRECT
适用的场景:
- 文件不是由调用程序打开的,所以无法使用open来控制这些标志(文件是3个标准描述符,这些描述符在程序启动之前就被打开)
- 文件描述符获取是通过open之外的系统调用,比如pipe()、socket()
修改标志位代码如下:
flags = fcntl(fd, F_GETFL)
flags |= O_APPEND
if(fcntl(fd, F_SETFL, flags) == -1) { errExit()}
7.文件描述符和打开文件之间的关系
文件描述符和打开的文件不一定是一一对应的关系,多个文件描述符可以指向同一打开文件。这些文件描述符可在相同或不同的进程中打开。
内核维护的3个数据结构:
- 进程级的文件描述符表
- 系统级的打开文件表
- 文件系统的i-node表
针对每个进程,内核为其维护打开的文件描述符表,每一条记录的相关信息:
- 控制文件描述符操作的一组标志
- 对打开文件句柄的引用
内核对所有打开的文件维护一个系统级的描述表格(打开文件表),并将表中各条目称为打开文件句柄,一个打开文件句柄存储了与一个打开文件相关的全部信息:
- 当前文件偏移量
- 打开文件时所使用的状态标识(flags参数)
- 文件访问模式(只读只写等)
- 与信号驱动I/O相关的设置
- 对该文件i-node对象的引用
文件系统会为驻留其上的所有文件建立一个i-node表:
- 文件类型(例如,普通文件、套接字、FIFO)和访问权限
- 一个指针,指向该文件所持有的锁的列表
- 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
文件描述符、打开的文件句柄、i-node的关系:
总结以下要点:
- 不同文件描述符(1和2)可以指向同一打开文件句柄,可能是通过调用dup() dup2()或fcntl()形成的
- 不同进程文件描述符可以指向同一打开文件句柄,可能调用fork()出现
- 不同的文件句柄指向同一i-node表条目,换言之,指向同一文件,可能因为每个进程各自对同一文件发起了open调用
- 两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量
- 文件描述符标志(close_on_exec标志)为进程和文件描述符所私有
8.复制文件描述符
int dup(int oldfd)
dup()调用复制一个打开的文件描述符oldfd,并返回一个新描述符,二者都指向同一打开文件句柄。系统会保证新描述一定是编号值最低的未用文件描述符
int dup2(int oldfd, int newfd)
dup2()系统调用会为oldfd参数所指定的文件描述符创建副本,其编号由newfd参数指定,如果newfd已经打开,那么dup2会将其先关闭
newfd = fcntl(oldfd, FU_DUPFD, startfd)
该调用为oldfd创建一个副本,且将使用大于等于startfd的最小未使用值作为描述符编号。
文件描述符的正、副本之间共享同一打开文件句柄所含的文件偏移量和状态标志,新的文件描述符有其自己一套文件描述符标志,且其close-onexec标志(FD_CLOEXEC)总是处于关闭
int dup3(int oldfd, int newfd, int flags)
dup3与dup2相同,只是增加了一个附加参数flag
9.在文件特定偏移量出的I/O:pread()和pwrite()
ssize_t pread(int fd, void *buf, size_t count, off_t offset)
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset)
相比于read()和write(),会直接设置offset参数,是一个原子操作,且性能更好
10.分散输入和集中输出:readv()和writev()
readv()和writev()系统调用分别实现了分散输入和集中输出的功能
ssize_t readv(int fd, const struct iovec *iov, int iovcnt)
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
这些系统调用并非只对单个缓冲区进行读写操作,而是一次即可传输多个缓存区的数据。数组iov定义了一组用来传输数据的缓冲区。iovcnt指定了iov的成员个数,iov中的数据结构:
struct iovec {
void *iov_base;
size_t iov_len;
}
下图展示关系:
分散输入
从文件描述符fd所指代的文件中读取一片连续的字节,然后将其散置于iov指定的缓冲区中,这一散置动作从iov[0]开始依次填满每个缓冲区。是原子性操作。
集中输出
将iov所指定的缓冲区中的数据拼接起来,然后写入fd中。
在指定offset处分散输入和集中输出
preadv()、pwirtev()
11.截断文件:truncate()和ftruncate()
truncate()和ftruncate()系统调用将文件大小设置为length指定长度
int truncate(const char *pathname, off_t length)
int ftruncate(int fd, off_t length)
若长度大于length则丢弃超出部分,若小于length,则在文件尾追加一系列字节或一个文件空洞。
12.非阻塞I/O
打开文件时指定O_NONBLOCK标志的作用:
- 若open()未能立即打开文件,则返回错误,而非陷入阻塞
- 调用open()成功后,后续I/O操作也是非阻塞的
由于管道、FIFO、套接字、设备都支持非阻塞模式,因无法通过open()设置标志,只能通过fcntl()的F_SETFL命令来修改。
由于内核缓冲区保证了普通文件I/O陷入阻塞,故而打开普通文件会忽略O_NONBLOCK标志
13.大文件I/O
LFS规范定义了一套扩展功能,允许在32位系统中运行的进程来操作无法以32位表示的大文件。
14./dev/fd 目录
对于每个进程,内核都提供一个特殊的虚拟目录/dev/fd/n,n是与进程中的打开文件描述符相对应的编号。
打开/dev/fd目录中的一个文件等同于复制相应的文件描述符:
fd = open("/dev/fd/1", O_WRONLY)
fd = dup(1)
/dev/fd实际上是一个符号链接,链接到linux所专有的/proc/self/fd目录
15.创建临时文件
mkstemp()、tmpfile()