文件和文件描述符

Everything is a File

访问操作系统中的对象

  • 文件:有 “名字” 的对象
  • 字节流 (终端) 或字节序列 (普通文件;包括 /proc/*)

文件描述符

  • 指向操作系统对象的 “指针”
    • 通过指针可以访问 “everything”
  • 对象的访问都需要指针
    • open, close, read/write (解引用), lseek (指针内赋值/运算), dup (指针间赋值)

应用程序:访问文件

通过系统调用

  • open, read, write, mmap, …
  • (当然可以;我们实现过很多这样的程序了)

真的是这样吗?

  • 求证:strace readelf -h /bin/ls
  • 还有更多有趣的例子
    • LC_ALL=zh_CN.UTF-8 strace readelf -h a.txt
    • “不是 ELF 文件 - 它开头的 magic 字节错”?
      • (需要 language-pack-zh-hans)
      • 计算机世界没有魔法!

更多的细节 (1)

文件是 “虚拟磁盘”

  • 把磁盘的一部分映射到地址空间,再自然不过了
    mmap(addr, length, prot, flags, fd, offset);

一些细节问题

  • 映射的长度超过文件大小会发生什么?
    • 启发:langchain (self-validation) 是趋势
    • RTFM 看来还是有用的 (“Errors”): SIGBUS
      • ftruncate 可以改变文件大小

更多的细节 (2)

文件访问的 offset

  • 文件的读写自带 “游标”
    • 省去了进程保存文件读/写的位置

Offset 管理

  • read/write: 会自动维护 offset
  • lseek: 修改 offset 位置
    • 对比:mmap file,实现 append 是个噩梦

更多的细节 (3)

mmap, lseek, ftruncate 互相交互的情况

  • 假设初始时文件大小为 2MB
    • lseek to 3 MiB (SEEK_SET)
      • 这时候能写入吗?
    • ftruncate to 1 MiB
      • 这时候 offset 在哪里?

水面下的冰山

  • 当多个机制互相作用时,系统就变得复杂

更多的细节 (4)

文件描述符在 fork 时会被子进程继承

  • 父子进程共用 offset?
  • 父子进程拥有独立 offset?

请你做一次操作系统的设计者

  • 哪一种方案更合理?

偏移量管理:行为

操作系统的每一个 API 都可能和其他 API 有交互

  1. open 时,获得一个独立的 offset
  2. dup 时,两个文件描述符共享 offset
  3. fork 时,父子进程共享 offset
  4. execve 时文件描述符不变
  5. O_APPEND 打开,偏移量永远在最后 (无论是否 fork)
    • modification of the file offset and the write operation are performed as a single atomic step

A fork() in the road

  • (在当时) 好的设计可能成为系统演化过程中的包袱

实现文件

Everything is a File

文件描述符可以访问 “一切”

  • 操作系统内核是如何实现的?
  • 如你所料:一个 switch-case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(f->type == FD_PIPE){
r = piperead(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
return -1;
r = devsw[f->major].read(1, addr, n);
} else if(f->type == FD_INODE){
ilock(f->ip);
if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
f->off += r;
iunlock(f->ip);
} else {
panic("fileread");
}

如果是 /proc/[pid]/maps?

syscall 时,我们有 current

  • current 指向了 memory areas 的数据结构
    • < 6.1: rbtree
    • ≥ 6.1: maple tree (B-tree)
  • 要记得上锁
  • 上锁就可能出性能问题

文件 = 实现了文件操作的 “Anything”

设备驱动程序

回顾:I/O 设备

一个能与 CPU 交换数据的接口/控制器

  • 寄存器被映射到地址空间

c27-3-1.webp

操作系统:Everything is a file

  • 只需要一个 struct file_operations 的实现

设备驱动程序

一个 struct file_operations 的实现

  • 把系统调用 “翻译” 成与设备能听懂的数据
    • 就是一段普通的内核代码

例子

  • devfs 中的 “虚拟” 文件
    • /dev/pts/[x] - pseudo terminal
    • /dev/zero, /dev/null (实现), /dev/random, …
  • procfs 中的 “虚拟文件”

驱动 Nuclear Launcher

我们也可以实现一个

  • 把对 /dev/nuke0 “路由” 我们的 file_operations
  • 向 GPIO 的 memory-mapped address 写入正确的电平

配置设备

设备不仅仅是数据,还有配置

  • 打印机的卡纸、清洁、自动装订……
    • 一台几十万的打印机可不是那么简单
  • 键盘的跑马灯、重复速度、宏编程……
  • 磁盘的健康状况、缓存控制……

两种实现方法

  • 控制作为数据流的一部分 (ANSI Escape Code)
  • 提供一个新的接口 (request-response)

ioctl

The ioctl() system call manipulates the underlying device parameters of special files. In particular, many operating characteristics of character special files (e.g., terminals) may be controlled with ioctl() requests. The argument fd must be an open file descriptor.

“非数据” 的设备功能几乎全部依赖 ioctl

  • “Arguments, returns, and semantics of ioctl() vary according to the device driver in question”

堆叠的冗余代码

  • 设备的复杂性是无法降低的
    • “就是有那么多功能”
    • UNIX 的负担:复杂的 “hidden specifications”
      • 另一个负担:procfs

例子

  • 终端:为什么 libc 能 “智能” 实现 buffer mode?
  • 网卡,GPU,……
  • KVM Device