Skip to content

File I/O

数据从文件传输到内存空间称为文件输入(file input),反之从内存空间传输到文件称为文件输出(file output)。统称为文件 I/O(file I/O)。

高级语言的类库往往提供了方便的接口操作文件 I/O,不过有的时候没有充分的控制力或者效率不够高,这时就需要使用系统调用来进行文件 I/O。

从 Unix 诞生之初,就采用了一种通用的 I/O 模型,目的是消除不同设备以及不能访问方式的差异。也就是说,一个能够对磁盘文件进行读写的程序,同样可以对终端、网络接口或者外围设备等设备进行读写。从程序的视角看,访问这些设备并没有差别。因此我们说 Unix 提供了设备无关的 I/O。

文件权限

每一个进程都有一个文件创建掩码(file creation mask),也称为 umask,它决定了新创建的文件的默认权限。当进程创建一个文件时,该文会被赋予一组初始权限。创建文件的函数往往有一个模式(mode)参数,允许进程去设置这些初始权限。不过 umask 会对这些权限进行掩码操作,从而最终决定了新文件的权限——进程试图设置的权限减去被 umask 移除的权限。

umask 是一个反向掩码,比特 1 表示禁止(去掉相应的权限),比特 0 表示允许。掩码一共 9 比特,三个一组,分别表示用户(user)、组(group)和其他人(others)的读、写、执行权限。常见的 umask 值如下:

0 0 0   0 1 0   0 1 0
r w x   r w x   r w x
user    group   others
由于 3 比特二进制和八进制紧密关联,因此常常用八进制表示 umask 的值,比如上面的例子是 022022 是一个常用的 umask 值,这意味着用户所在组、其他人没有写的权限。当调用函数创建文件时,模式是 mode,那么最终的权限就是 mode & ~umask。例如,如果 umask022,而创建文件时指定的模式是 0662,那么最终的权限就是 0662 & ~022 = 0640,即用户有读写权限,组有读权限,其他人没有任何权限。

使用 umask 命令可以查看当前的设置。umask -S 参数可以以符号方式显示不会修改哪些权限。umask xxx 命令可以设置新的 umask 值。

进程启动的时候会使用当前 shellumask 值作为默认值,子进程也会继承这个值。

进程的 User ID

每个文件都有访问权限控制,而用户只能通过启动某个程序来访问文件。因此文件权限要决定哪些用户进程能访问、如何访问。每个进程至少与一个用户 ID(user IDUID)相关联。Linux 上每个进程有四个用户 ID,分别是

  • 实际用户 ID(real user IDRUID
  • 有效用户 ID(effective user IDEUID
  • 保存的设置用户 ID(saved set-user IDSUID
  • 文件系统用户 ID(filesystem user IDFSUID

Unix 上,内核确定有没有权限使用的是有效用户 ID。Linux 上使用文件系统用户 ID 来确定访问权限,但是由于文件系统用户 ID 总是等于有效用户 ID,因此 Linux 上的访问权限检查和 Unix 上是一样的。

通常情况下,运行一个程序,所创建的进程被分配一个有效用户 ID 和实际用户 ID,都是当前用户的用户 ID,因此是相同的。不过有的时候进程可以拥有不同的有效用户 ID 和实际用户 ID,此时有效用户 ID 赋予程序的特权往往要大于实际用户 ID。因此一个程序可以使用程序文件所有者的用户 ID 作为有效用户 ID 来运行,而不是运行这个程序的用户的用户 ID。

文件模式(file mode)包含 12 个比特,最高位称为 setuid,如果这个比特被设置了,那么非程序拥有者运行这个程序,所创建的进程使用该程序的拥有者的用户 ID 作为有效用户 ID,而不是运行这个程序的用户的用户 ID。通过 ls -l 可以查看这个比特是否被设置了,如果设置了用户执行权限显示的是 s 而不是 x。启用了 setuid 的程序通常需要临时把有效用户 ID 改成实际用户 ID,然后再恢复有效用户 ID。保存的设置用户 ID 就是用来保存原来的有效用户 ID 的。这样的程序被称为 setuid 程序。常见的例子是系统软件比如 passwd。下面是 passwd 的权限:

ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 118168 Apr 19  2025 /usr/bin/passwd
当我们在一个终端上运行 passwd 命令时,所创建的进程的有效用户 ID 是 root,而不是当前用户的用户 ID。打开另一个终端可以查看
ps -o euid,ruid,pid,args -C passwd
 EUID  RUID     PID COMMAND
    0  1000    3611 passwd
RUID 是当前用户的用户 ID,而 EUIDroot 的用户 ID。

I/O 机制

进程访问文件之前,首先要与文件建立连接。连接(connection)是管理和控制进程访问文件的一个对象,包含文件偏移量(file offset)它指向文件执行操作的位置、各种标志(flag)和控制读写的模式(mode)、文件的位置等等。为了创建这个对象,进程执行 open 来打开文件。POSIX 规范讲这种连接对象(connection object)称为打开文件描述(open file description, OFD)。

有三种打开文件的模式:只读(read mode)、只写(write mode)和读写(read-write mode),这些事是访问模式(access mode)。

打开文件之后返回一个标识符,称为文件描述符(file descriptor),它是一个非负整数,是新创建的 OFD 的引用。多个进程可以打开同一个文件,每个进程都有自己的文件描述符。一个进程可以打开同一个文件多次,每次都会创建一个新的 OFD,每个 OFD 都有自己的文件偏移量、标志和模式,一个进程还可以有多个文件描述符指向同一个 OFD。Unix 会有锁机制确保只有一个进程打开。

下图是内核的内存打开文件表(in-memory open file table)的示意图,这个表也被称为文件结构表(file structure table)。这个表的每一项有很多字段,其中一个是指向表示真实文件的 inode 的指针。进程 X 打开了两个文件,OFD 是 1 和 m 位置,同时复制了文件描述符,使得 2 和 n 指向同一个 OFDOFD 3 和 m 指向同一个 inode 3,打开的是同一个文件。

当不再使用一个文件时,进程可以执行 close 来关闭文件。关闭文件会释放文件描述符和 OFD,如果没有其他的文件描述符指向这个 OFD,那么内核会释放这个 OFD