awakeBird Back-end Dev Engineer

Linux基础(二)文件系统及文件I/O

2018-09-08

理解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)


Comments

Content