理解Linux文件系统的关键一点在于一切皆文件的思想。
Linux中,文件既包括普通数据文件,又包括如设备、管道、套接字、目录、符号链接等特殊文件。
文件系统指的是常规文件和目录的组织集合,Linux支持多种文件系统,并提供虚拟文件系统(VFS)来隐藏不同文件系统的实现细节。
Linux内核维护了三种与文件相关的数据结构,也提供了一套通用的文件I/O模型对所有类型的文件进行操作。
该系列文章为《Linux/Unix系统编程手册》的学习笔记,由于该书太过冗长,属于工具书的类别,这里对书中的一些核心内容加以提炼和整理。 书中的编程练习这里不做展示和说明。
文件I/O模型
Linux提供了一套通用的文件I/O模型进行文件操作,适用于所有类型的文件,这里先列出几个重要的api(伪码)
# 打开文件
fd = open(pathname, flags, mode)
# 读取文件到缓冲区
numread = read(fd, buffer, count)
# 从缓冲区写入文件
numwrite = write(fd, buffer, count)
# 关闭文件
status = close(d)
# 设置文件偏移量
curr_off = lseek(fd, offset, whence)
# 文件控制操作,常见于非open方式打开的fd,如pipe()/socket()
status = fcntl(fd, cmd, ...)
# 复制文件描述符
newfd = dup(oldfd)
status = dup2(oldfd, newfd)
# 在制定offset处I/O
numread = pread(fd, buffer, count, offset)
numwrite = pwrite(fd, buffer, count, offset)
几点说明:
文件描述符
本质上,内核只维护一种文件类型,即字节流序列。 进程用一个很小的非负整数代指打开的文件,shell会始终打开三个文件描述符,所有shell启动的程序都会继承这三个描述符:
- fd=0 标准输入(文件流stdin)
- fd=1 标准输出(文件流stdout)
- fd=2 标准错误(文件流stderr)
open的flags参数
包括三部分内容:访问模式、创建标志和一打开文件的状态标志。常见如下:
- O_RDONLY 只读模式
- O_WRONLY 只写模式
- O_RDWR 读写模式
- O_CREAT 若文件不存在则创建,此时需指定mode参数为文件设置权限
- O_SYNC 已同步方式写入文件(每次都要写入磁盘,效率极低)
- O_APPEND 总在文件末尾添加数据(线程安全)
- O_ASYNC 信号驱动I/O
- O_NONBLOCK 非阻塞模式打开文件(open及后续的所有I/O操作全部为非阻塞)
lseek的whence参数
- SEEK_SET 文件开始
- SEEK_END 文件结尾的下一个字节
- SEEK_CUR 当前位置
注意,将文件偏移量置于文件开始为lseek(fd, 0, SEEK_SET)
,而置于结尾字节则是lseek(fd, -1, SEEK_END)
几种复制文件描述符的方式
dup会返回编号值最低的未用描述符值作为新描述符的值,如果想指定新描述符值只能显示将其先关闭。
如果newfd已经打开的情况下,dup2则会关闭newfd所指编号的文件描述符,并会忽略关闭时发生的任何错误。
newfd = fcntl(oldfd, F_DUPFD, startfd)
则会返回大于startfd的标号值最低的为用描述符值作为新描述符的值,比较灵活。
指定偏移量I/O
pread/pwrite实际效果相当于先对fd进行lseek操作再进行I/O,不过重要的一点是前者的操作是原子性的,线程安全的。
文件描述符、打开的文件句柄和i-node表
Linux内核维护着三种文件相关的数据结构
-
文件描述符 针对每个进程,内核维护的打开的文件描述符,包含对打开的文件句柄的引用。
- 打开的文件句柄
内核为所有打开的文件维护的系统级的描述表格,维护着如下信息:
- 文件偏移量
- 打开文件的flags信息
- 文件访问模式(读写模式)
- 对i-node表的引用
- i-node表
每个文件系统会为驻留在其上的所有文件维护一份i-node表,维护如下信息:
- 指向文件数据块的指针
- 指向文件所持有锁的列表的指针
- 文件类型和访问权限
- 文件属主和属组
- 指向文件的硬链接数量
- 文件大小
三个数据结构的引用关系
-
多个文件描述符指向同一打开文件句柄的情况: 同一进程内复制的文件描述符,和fork出的子进程所获得的文件描述符副本都会指向同一文件句柄。
-
多个文件句柄指向同一i-node节点: 多次open打开同一文件会产生多个打开文件句柄,并都指向同一i-node节点。
目录与链接
i-node中维护者一个指针指向储存文件内容的数据块,目录作为一种特殊的文件,其数据块中又维护着当前目录下的文件名及对应i-node编号。
注意:i-node中是不维护文件名信息的。
-
根目录 i-node节点的起始编号为1,而文件系统根目录(/)总是储存在i-node条目2中。
-
硬链接 Shell中可以通过
ln
为已存在的文件创建新的硬链接,这样将有两个指向当前文件i-node节点的指针,该文件的硬链接数量也将变为2。 -
符号链接(软连接) Shell中可以通过
ln-s
为已存在的文件创建软连接,它并不会引入新的指向i-node的指针,而是将文件名作为数据块的内容。
文件I/O缓冲
read和write系统调用在操作磁盘文件时并不会直接发起磁盘访问,而是在用户空间缓冲区与内核空间缓冲区之间不断复制数据。
使用大块缓冲区缓冲数据将减少上述复制的次数,显著提高I/O性能。
stdio库的缓冲
使用stdio库函数(如printf)进行I/O可以免去自行处理对数据的缓冲,一般来说,库函数会将所有的输入输出缓存在stdio缓冲区(用户空间缓冲区)中,在适当时机通过系统调用将其复制到内核缓冲区。
相关常见api如下:
# 设置一个文件流的缓冲模式
setvbuf(stream, buf, mode, size)
# 刷新输出流输入流数据到内核缓冲区
fflush(stream)
# 将fd相关的缓冲数据和元数据刷新到磁盘
fsync(fd)
# 将所有内核缓冲区数据刷新到磁盘
sync()
# fd与文件流的相互转换
fd = fileno(stream)
stream = fdopen(fd, mode)
缓冲类型
即setvbuf的mode参数
-
INOBF 无缓冲 所有的I/O库函数都会直接调用read或write,会忽略buf和size参数。
-
INLBF 行缓冲 对于输出流,在输出一个换行符之后缓冲数据。对于输入流,每次读取一行数据。 代指终端的流默认为行缓冲(如printf函数)。
-
INFBF 全缓冲 单次进行read或write的大小与缓冲区相同。 代指磁盘的流默认为全缓冲。
缓冲及刷新过程
(End)