一、概述
所有执行I/O操作的系统调用都以文件描述符,一个非负整数来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道,FIFO,socket,终端,设备和普通文件。本文主要介绍IO相关的接口。
二、通用接口
通常,shell会替所有程序打开三种标准的文件描述符。如下表:
文件描述符 |
用途 |
POSIX名称 |
stdio流 |
---|---|---|---|
0 |
标准输入 |
STDIN_FILENO |
stdin |
1 |
标准输出 |
STDOUT_FILENO |
stdout |
2 |
标准错误 |
STDERR_FILENO |
stderr |
2.1、打开文件
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, .../*mode_t mode */;成功返回文件描述符,错误返回-1,并设置errno
要打开的文件由参数pathname来标识,如果pathname是一符号链接,会对其进行解引用。flags为位掩码,用于指定文件的访问模式,可选择如表所示常量之一:
标志 |
用途 |
---|---|
O_RDONLY | 以只读方式打开文件 |
O_WRONLY | 以只写方式打开文件 |
O_RDWR | 以读写方式打开文件 |
O_CLOEXEC | 设置close-on-exec标志 |
O_CREAT | 若文件不存在则创建 |
O_DIRECT | 无缓冲的输入/输出 |
O_DIRECTORY | 如果pathname不是目录,则失败 |
O_EXCL | 结合O_CREAT参数使用,专门用于创建文件 |
O_LARGEFILE | 在32位系统中使用此标志打开大文件 |
O_NOATIME | 调用read时,不修改文件最近访问时间 |
O_NOCTTY | 不要pathname指向的终端设备成为控制终端 |
O_NOFOLLOW | 对符号链接不予解引用 |
O_TRUNC | 截断文件,使其长度为0 |
O_APPEND | 总是在文件尾部追加数据 |
O_ASYNC | 当I/O操作可行时,产生信号通知进程 |
O_DSYNC | 提供同步的I/O数据完整性 |
O_NOBLOCK | 以非阻塞方式打开 |
O_SYNC | 以同步方式写入文件 |
2.2、创建文件
在早期的UNIX实现中,open函数只有两个参数,无法创建新文件,而是使用creat系统调用来创建并打开一个新文件。
#include <unistd.h>
int creat(const char *pathname, mode_t mode);成功返回文件描述符,失败返回-1
该系统调用根据pathname参数创建并打开一个文件,若文件已存在,则打开文件,并清空文件内容。由于open调用的flags参数能对文件打开方式提供更多控制,creat调用已不多见,creat调用等价于如下open调用:
fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
2.3、读取文件内容
read系统调用从文件描述符fd所指代的文件中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buffer, size_t count);成功返回读取的字节数,失败返回-1
count参数指定最多能读取的字节数。buffer参数提供存放数据的缓冲区,至少应该有count个字节大小。一次read调用所读取的字节数可能小于请求的字节数,对于普通文件而言有可能是当前读取位置靠近文件结尾。当read用于管道、FIFO、socket或终端时,也会出现读取字节数小于请求字节数的情况。例如从终端读取到换行符。
2.4、写入文件
write系统调用将数据写入一个已打开的文件中。#include <unistd.h>
ssize_t write(int fd, void *buffer, size_t count);成功返回写入的字节数,失败返回-1
write调用的参数含义与read调用类似。如果write调用成功,将返回实际写入文件的字节数,该返回值可能小于count参数,这被称为“部分写”。对磁盘文件而言,造成“部分写”的原因可能是由于磁盘已满或是进程对文件大小的限制。
对磁盘文件进行IO操作时,write调用成功并不能保证数据已写入磁盘。因为为了减少磁盘活动量和加快write调用,内核会缓存磁盘的IO操作。后面会详细介绍。
2.5、关闭文件
#include <unistd.h>
int close(int fd);成功返回0,失败返回-1
close系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当一进程终止时,将自动关闭其已打开的所有文件描述符。尽管如此,显示关闭不用的文件描述符是良好的编程习惯,尤其是对于长时间运行的服务器程序。
2.6、改变文件偏移量
对于每个打开文件,内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个read或write操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示,文件第一个字节的偏移量为0。每次read或write调用会自动对其进行调整。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);成功返回新的偏移量,失败返回-1
offset参数指定了一个以字节为单位的数值。whence参数则表明应参照哪个基点来解释offset参数,应为下列其中之一:
SEEK_SET:将文件偏移量设置为从文件头部起始点开始的offset个字节。
SEEK_CUR:相对于当前位置偏移量,将文件偏移量调整offset个字节。
SEEK_END:将文件偏移量设置为起始于文件尾部的offset个字节。即从文件最后一个字节的下一个字节算起。
如果whence参数值为SEEK_CUR或SEEK_END,offset参数可以为正数也可以为负数;如果为SEEK_SET,则offset参数必须为非负数。
3、文件描述符和打开文件之间的关系
要了解文件描述符和打开文件之间的关系,首先要了解由内核维护的三个数据结构:
- 进程级的文件描述符表
- 系统级的打开文件表
- 文件系统的i-node表
针对每个进程,内核为其维护打开文件的描述符表,该表的每一条目都记录了单个文件描述符的相关信息,主要有两条:1、控制文件描述符操作的一组标志(目前仅定义了一个,即close-on-exec);2、对打开文件句柄的引用。
内核对所有打开的文件维护有一个系统级的表格,称为打开文件表,并将表中各条目称为打开文件句柄,打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
- 当前文件偏移量
- 打开文件时所用的状态标志
- 文件访问模式
- 与信号驱动IO相关的设置
- 对该文件i-node对象的引用
若两个进程拥有多个打开的文件描述符,如下图所示:
进程A中,文件描述符1和20指向同一个打开的文件句柄(23)。这可能是通过调用dup、dup2或fcntl而形成的。
进程A的文件描述2和进程B的文件描述符2都指向同一个打开的文件句柄(73),这种情形可能在调用fork后出现,或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一进程时也会发生。
进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向inode表中的相同条目(1976),即指向同一文件。这种情况是因为每个进程各自对同一文件发起了open调用。同一进程两次打开同一文件也会发生类似情况。
通过以上描述我们可以知道:
- 两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。无论这两个描述符是分属于不同进程还是同属于一个进程
- 获取和修改打开的文件标志(例如:O_APPEND,O_NONBLOCK和O_ASYNC等),可执行fcntl的F_GETFL和F_SETFL操作,与上一条描述类似
- 文件描述符标志为进程和文件描述符私有,对这一标志的修改不会影响到同一进程或不同进程的其它文件描述符