南京大学 操作系统 (JYY) 学习笔记:访问操作系统的对象与“一切皆文件”

写在前面:这是本系列的第七篇。

我们已经知道,进程从 execve 后的初始状态开始,可以通过 mmap 改变自己的地址空间,通过 fork 创建新的进程,再通过 execve 执行新的程序------我们慢慢已经开始理解"操作系统上的应用生态"并没有魔法了。

本讲内容:操作系统还必须给应用程序提供访问操作系统对象的机制。Windows 提供了具体的 Handle (句柄) 机制,而 UNIX 则贯彻了极致的浪漫主义:"Everything is a file"。这节课我们来领略这种设计带来的方便(和不便)。

课前反思:Testkit 与工程实现

关于上节课的 Testkit,其实我还不太会用。

多个 .c 文件交织在一起,执行顺序让人摸不着头脑,快照 (Snapshot) 机制也没完全听懂。有机会一定要自己琢磨一下怎么用 Test(或者让豆包/Kimi 详细拆解教一下)。

在系统编程中,测试框架是重中之重。


操作系统的对象

操作系统里到底有哪些"对象"供我们操作?

  • 进程 (Process):
    • 进程 = 状态机。
    • 进程管理 API: fork, execve, exit, waitpid
  • 连续的内存段 (Memory Segments):
    • 我们可以把"连续的内存段"看作一个对象。
    • 它可以在进程间共享,也可以直接映射到文件上。
    • 内存管理 API: mmap, munmap, mprotect, msync

文件描述符 (File Descriptor)

文件是操作系统中最常见的对象。

文件和设备

  • 文件:有"名字"的数据对象。
    • 字节流: 终端 (Terminal)、随机数发生器 (random) 等,数据像水流一样,读过去就没了。
    • 字节序列: 硬盘上的普通文件,可以通过指针随意前后移动定位。
  • 文件描述符 (fd):
    • 它是指向操作系统对象的"指针"。
    • UNIX 的核心哲学:Everything is a file
    • 只要掌握了这个"指针",你就可以访问系统里的"一切"。
    • 对象的访问全靠这个指针:open (获取指针), close (销毁指针), read/write (解引用/读写数据), lseek (指针内偏移运算), dup (指针间赋值)。

文件描述符的底层逻辑

fd = open(...),后续所有的操作都会基于这个 fd

进程里面有一个进程地址空间,内核为你维护了一个"文件描述符表"。fd 本质上就是这个表里的一个整数索引(Index)。

相关 API 的内存视角:

  • open: 进程空间(内核态)里开辟一个新内存,用于放置文件状态信息,并返回一个索引 fd 给用户态。(类似 p = malloc(sizeof(FileDescriptor));
  • close: 删去这个文件表项的引用。(类似 delete(p);
  • read/write: 顺着指针读写数据,并自动推进偏移量。(类似 *(p.data++);
  • lseek: 手动修改偏移量。(类似 p.data += offset;
  • dup (duplicate): 复制一个已存在的文件描述符。创建一个新的 fd 整数,但它指向内核中同一个 底层文件资源。(类似 q = p; 的指针赋值)。

文件描述符的分配策略

规则:总是分配当前最小的未使用描述符。

  • 0,1,2 是系统默认保留的:标准输入 (stdin),标准输出 (stdout),标准错误 (stderr)。
  • 新打开的文件,总是从 3 开始分配。
  • 关闭文件后,该描述符号可以被立即重新分配。
  • 进程能打开多少文件?
    • 使用 ulimit -n 查看进程级限制。
    • 使用 sysctl fs.file-max 查看系统级总限制。
bash 复制代码
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/fd# ./fd-alloc

# 程序分配了 8 个文件描述符,编号从 3 到 10。
Allocated file descriptor fds[0]: 3
Allocated file descriptor fds[1]: 4
...
Allocated file descriptor fds[7]: 10

# 程序关闭了编号为 1、3、5、7 (即 fd 4, 6, 8, 10) 的文件描述符
Closed file descriptor fds[1]: 4
...

# 程序重新分配了 4 个文件描述符。由于 4,6,8,10 被释放,系统优先复用它们!
Reallocated file descriptor fds[1]: 4
Reallocated file descriptor fds[3]: 6
...

避坑指南:文件描述符的 Offset (偏移量)

如果希望在文件里面追加写入内容,偏移量 offset 就非常关键。

文件描述符不仅是一个编号,它在操作系统内核中对应着一个"打开文件表项 (Open File Table Entry)",里面记录了非常关键的 offset(当前读写到了哪里)。每次独立的 open 调用,都会产生一个拥有独立 offset 的表项。

🤖 ** 极客勘误:fork()dup() 之后的 Offset 共享问题**

原笔记中 AI 对 dup() 的解释有误。在 Linux 内核底层,真实情况是这样的:

  • fork(): 创建子进程时,子进程完整复制了父进程的 fd 数组。父子进程的 fd 指向内核中同一个 "打开文件表项"。因此,父子进程共享 Offset! (这就是为什么父子进程同时 printf 输出到文件时,内容不会互相覆盖,而是顺着往下写)。
  • dup(): 复制当前进程的一个 fd(比如将 fd 3 复制为 fd 4)。这两个 fd 同样指向内核中同一个 "打开文件表项"。因此,dup()** 生成的两个 fd 也共享 Offset!** 在 fd 3 上写入内容,fd 4 的偏移量也会跟着往前走。
  • 只有当你对同一个文件调用两次 open() 时,才会生成两个完全独立的 Offset。
bash 复制代码
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/fd# ./fd-offset

# 程序使用 dup 复制文件描述符,分别写入 "A" 和 "B"。
# 输出结果为 "AB",说明 dup 复制的文件描述符共享 offset,写入操作是连续追加的。
Content of sample.txt: AB

# 程序使用 fork 创建子进程,子进程写入 "D",父进程写入 "C"。
# 输出结果为 "DC" (或 "CD")。它们依然共享 offset,不会互相覆盖同一位置。
Content of sample.txt after fork: DC

Windows 中的文件描述符:Handle (句柄)

  • Handle (把手;握把;把柄): 比 File Descriptor 更像"指针"。
  • 这是一个非常形象的翻译。你在操作一个黑盒,操作系统递给你一个"把手",你抓着这个把手就能控制黑盒。
  • 进程创建的面向工程设计: Windows 默认情况下,Handle 是不继承给子进程的(这与 UNIX 默认继承 fd 恰恰相反,各有优劣,Windows 的设计在工程上更容易避免资源泄漏)。

操作系统里都有什么文件?

  • FHS (Filesystem Hierarchy Standard):
    文件系统层次结构标准。它使得软件和用户能够预测安装文件和目录的位置(比如 /etc 放配置,/bin 放程序,/var 放日志)。有趣的是,macOS 就不严格遵循 FHS。

冷知识:一个 U 盘就是一个操作系统?

只要拷对了文件,操作系统就能正常执行!

  1. 创建 UEFI 分区,并复制正确的 Loader (引导程序)。
  2. 创建文件系统 (mkfs 格式化)。
  3. 使用 cp -ar 把 Linux 根目录的文件正确复制过去(保留权限)。注意修改 fstab 里的磁盘 UUID。
  4. 你就得到了一个可以正常启动的系统盘!
  5. 运行时挂载必要的其他文件系统:磁盘上的 /dev, /proc 其实都是空目录,是在系统启动后通过 mount -t proc proc /proc 动态在内存里"凭空捏造"出来的!

任何"可读写"的东西都可以是文件

  • 真实的设备:
    /dev/sda (硬盘), /dev/tty (终端显示器)。
  • 虚拟的设备 (文件):
    /dev/urandom (随机数生成器), /dev/null (数据黑洞)。
    它们在硬盘上并没有实际的"文件",是操作系统内核为它们实现了特别的 readwrite 函数指针。甚至你可以通过往 /sys/class/backlight 里面 write 数字来直接控制屏幕亮度!

匿名管道 (Anonymous Pipe):一个特殊的"文件流"

这是实现进程间通信的基础。

c 复制代码
int pipe(int pipefd[2]);
  • 返回两个文件描述符:pipefd[0] 是读口,pipefd[1] 是写口。
  • 看起来单个进程自己写自己读没啥用?
  • 精髓在于配合 fork()
    父进程 pipe() 后,fork() 出子进程。子进程继承了这两个管口。然后父进程关闭读口(只写),子进程关闭写口(只读)。一根单向的跨进程数据管道就此打通!
bash 复制代码
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/pipe# ./anonymous-pipe
[1760] Write: 'Hello, world!'
[1761] Got: 'Hello, world!'
[1760] Done.

之前在计网里听得一脸懵逼的"管道",其实在命令行里天天用(比如 ls | grep txt)。它的底层就是 pipe + fork + dup2(把标准输入/输出重定向到管口上)。


"Everything is a file" 的哲学反思

实现一切的基础架构:

  • 进程管理: fork, execve, waitpid, exit
  • 内存管理: mmap, munmap, mprotect, msync
  • 文件管理: open, close, read, write, lseek, dup

优点:优雅,文本接口,就是好用

  • 一套 API 访问所有对象。 一切都可以用 grep 去搜索!
  • Unix Shell 的语法虽然经常被诟病太老旧(复杂的逻辑应该用 Python/Rust),但是:We all love quick & dirty!
bash 复制代码
# 查看所有进程的 fd,纯字符串文本处理的狂欢
ls -l /proc/*/fd/* 2>/dev/null | awk '{print $(NF-2), $(NF-1),$NF}'
# 统计所有进程的内存占用
grep -s VmRSS /proc/*[0-9]/status | awk '{sum += $2} END {print sum " kB"}'

所有一切都是基于字符串的替换,很接近自然语言,非常极客。

缺点:对高速设备不够友好

  • 万物皆文件,意味着你要到处 lseek() 然后再 read/write,这会产生额外的系统调用延迟和内存拷贝。
  • 传统的 fd 是单线程同步 I/O。对于现代每秒百万次读写的高速 NVMe 固态硬盘和万兆网卡,这种 API 成了瓶颈。(这也是为什么现代引入了 io_uring 等零拷贝的高级机制)。

终局:API 与封装兼容 (Another Level of Indirection)

"Any problem in computer science can be solved with another level of indirection." (Butler Lampson)

(计算机科学中的任何问题,都可以通过增加一个间接层来解决。)

不同的操作系统底座不同,但可以通过兼容层(套壳)来互相运行对方的程序:

  • Windows NT: Win32 API → POSIX 子系统 (早期的 WSL1 就是把 Linux 系统调用实时翻译成 Windows 系统调用,极其优雅但维护难度极大,最终暴毙,换成了现在跑真虚拟机的 WSL2)。
  • macOS: Cocoa API → 极其坚固的 BSD Unix 子系统。
  • Fuchsia: Zircon 微内核 → POSIX 兼容层。

OpenHarmony 的野望

理论上,只要你用代码实现并伪装出 Linux 所有的 API(提供同样的系统调用编号和行为),你就能在这个新的系统上毫无缝隙地运行所有的 Android/Linux 应用!

只需要把原应用需要调用的 API 全都找到,然后在底层用自己的内核逻辑替换并实现它。这就是现代操作系统的"移花接木"之术!

相关推荐
寒月小酒6 小时前
第五章 生成集成 和第六章 RAG评估(all-in-rag学习)
数据库·学习
摇滚侠6 小时前
《RocketMQ 官网》阅读笔记 RocketMQ 消息队列 MessageQueue 消息 Messagege
笔记·rocketmq
魔城烟雨6 小时前
从零开始学习betaflight《5-坐标系》
学习
笔夏7 小时前
【移芯平台CAT1模组】烧写固件
stm32·单片机·学习
憧憬成为java架构高手的小白7 小时前
黑马八股--spring框架学习
java·学习·spring
Ztt6666666667 小时前
【无标题】
经验分享·笔记·其他·百度·微信公众平台
依然范特东7 小时前
强化学习笔记4--TD算法
笔记
AOwhisky7 小时前
Linux(CentOS)系统管理入门笔记(第十五期)——文件系统与存储管理(上篇):设备识别、挂载与文件查找
linux·运维·笔记·centos·文件系统
晓梦林8 小时前
[极客大挑战 2019]PHP学习笔记
笔记·学习·php