07 - Accessing OS interobjects

07 - 访问操作系统的对象

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

本讲内容:操作系统还必须给应用程序提供访问操作系统对象的机制。当然,我们可以直接以 API 的形式提供,例如 Win32 API 包含 “RegOpenKeyEx” 访问注册表。这节课我们学习 UNIX 的 “Everything is a file” 带来的方便 (和不便)。

Take-away Messages: 操作系统必需提供机制供应用程序访问操作系统对象。对于 UNIX 系统,文件描述符是操作系统中表示打开文件 (操作系统对象) 的指针;而 Windows 则更直接地提供了 handle 机制。

1. Testkit 的修复?

我其实还不会用...

看看老师会怎么讲?

快照?

1.1. 工程实现

多个 .c 文件

顺序不知道...

好吧,没听懂...

1.2. 有机会琢磨一下怎么用 Test

...让豆包教一下

1.3. 操作系统的对象

进程

  • 进程 = 状态机

  • 进程管理 API: fork, execve, exit

连续的内存段

  • 我们可以把 “连续的内存段” 看作一个对象

    • 可以在进程间共享

    • 也可以映射文件

  • 内存管理 API: mmap, munmap, mprotect, msync

2. 文件描述符

文件是操作系统常见的对象...

2.1. 文件和设备

文件:有 “名字” 的数据对象

  • 字节流 (终端,random)

  • 字节序列 (普通文件)

文件描述符

  • 指向操作系统对象的 “指针”

    • Everything is a file

    • 通过指针可以访问 “一切”

  • 对象的访问都需要指针

    • open, close, read/write (解引用), lseek (指针内赋值/运算), dup (指针间赋值)

2.2. 文件描述符:访问文件的“指针”

文件描述符 file descripter

fd = open...后续的操作都会基于 fd

进程里面有一个进程空间,需要一个 fd 指针指向地址空间的操作对象...

如果需要改变文件 都需要 fd,要改变 fd 就要使用 open 和 close...

相关 API

  • open

    • p = malloc(sizeof(FileDescriptor));

    • 进程空间里开辟一个新内存,用于放置 fd

  • close

    • delete(p);

    • 删去一个 fd 指针...

  • read/write

    • *(p.data++);

  • lseek

    • p.data += offset;

  • dup(duplicate)

    • q = p;

    • 类似指针赋值操作?

    • 用于复制一个已存在的文件描述符(file descriptor)。它的作用是创建一个新的文件描述符,该描述符指向与原文件描述符相同的文件或资源。

2.3. 文件描述符的分配

总是分配最小的未使用描述符

  • 0,1,2 是最小的 std in,std out,std error

  • 新打开的文件总是从 3 开始

    • 文件描述符是进程文件描述符表的索引

    • 关闭文件后,该描述符号可以被重新分配

进程能打开多少文件?

  • ulimit -n (进程限制)

  • sysctl fs.file-max (系统限制)

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/fd# ./fd-alloc

# 程序分配了 8 个文件描述符
# 编号从 3 到 10。这些文件描述符是系统自动分配的
# 通常从最小的可用文件描述符开始(标准输入、输出、错误分别占用 0、1、2)
Allocated file descriptor fds[0]: 3
Allocated file descriptor fds[1]: 4
Allocated file descriptor fds[2]: 5
Allocated file descriptor fds[3]: 6
Allocated file descriptor fds[4]: 7
Allocated file descriptor fds[5]: 8
Allocated file descriptor fds[6]: 9
Allocated file descriptor fds[7]: 10

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

# 程序重新分配了编号为 1、3、5、7 的文件描述符
# 由于这些文件描述符已经被关闭
# 系统会重新分配它们,编号仍然是 4、6、8、10
Reallocated file descriptor fds[1]: 4
Reallocated file descriptor fds[3]: 6
Reallocated file descriptor fds[5]: 8
Reallocated file descriptor fds[7]: 10

# 程序关闭了所有分配的文件描述符,包括之前重新分配的
Closed file descriptor fds[0]: 3
Closed file descriptor fds[1]: 4
Closed file descriptor fds[2]: 5
Closed file descriptor fds[3]: 6
Closed file descriptor fds[4]: 7
Closed file descriptor fds[5]: 8
Closed file descriptor fds[6]: 9
Closed file descriptor fds[7]: 10

2.4. 文件描述符的 offset

如果希望在文件里面追加写入内容?

文件描述符是“进程状态的”一部分

  • 保存在操作系统中;程序只能通过整数编号访问

  • 文件描述符自带一个 offset - 偏移量

  • 每次 open 之后都会得到一个独立的 offset...

Quiz: fork() 和 dub() 之后,文件描述符共享 offset 吗?

  • fork() :创建一个子进程的时候,子进程会得到父进程文件描述符的一个副本。

    • 这个副本和父进程的文件描述符指向同一个文件,而且它们的 offset 是共享的。

    • 在父进程里移动了 offset(比如读取或者写入了一些内容),子进程看到的 offset 也会跟着变。

  • dup() :复制一个文件描述符的时候,新复制出来的文件描述符和原来的文件描述符指向同一个文件,但是它们的 offset 是独立的。

    • 在新文件描述符上移动 offset 不会影响到原来的文件描述符。

  • fork() 看似优雅,实则复杂...

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/fd# ./fd-offset
# 程序使用dup复制文件描述符,写入 "A" 和 "B"
# 输出结果为 "AB",说明dup复制的文件描述符共享文件偏移量,写入操作是连续的
Content of sample.txt: AB

# 程序使用fork创建子进程,子进程写入 "D",父进程写入 "C"
# 输出结果为 "DC",说明fork创建的子进程和父进程的文件偏移量是独立的
# 子进程先写入 "D",父进程后写入 "C",因此最终内容为 "DC"
Content of sample.txt after fork: DC

2.5. Windows 中的文件描述符

Handle 把手;握把;把柄

  • 比 file descriptor 更像“指针”

  • 更好的翻译(。

  • 句柄?把柄!!!

进程创建 - 面向工程的设计

  • 默认 handle 是不继承的(和 UNIX 默认继承相反)

2.6. 操作系统都有什么文件?

Filesystem Hierarchy Standard FHS

  • enables software and user to predict the location of installed files and directories: 例如 macOS 就不遵循 FHS

2.7. 冷知识

一个 U 盘就是一个操作系统???

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

  1. 创建 UEFI 分区,并复制正确的 Loader

  2. 创建文件系统

    • mkfs (格式化)

  1. cp -ar 把文件正确复制 (保留权限)

    • 注意 fstab 里的 UUID

    • 你就得到了一个可以正常启动的系统盘!

  1. 运行时挂载必要的其他文件系统

    • 磁盘上的 /dev, /proc, ... 都是空的

    • mount -t proc proc /mount/point 可以 “创建” procfs

2.8. 任何“可读写”的东西都额可以是文件

真实的设备

  • /dev/sda

  • /dev/tty

虚拟的设备 (文件)

  • /dev/urandom (随机数), /dev/null (黑洞), ...

    • 它们并没有实际的 “文件”

    • 操作系统为它们实现了特别的 read 和 write 操作

  • procfs 也是用类似的方式实现的

管道:一个特殊的 “文件” (流)

  • 由读者/写者共享

    • 读口:支持 read

    • 写口:支持 write

匿名管道

int pipe(int pipefd[2]);
  • 返回两个文件描述符

  • 进程同时拥有读口和写口

    • 看起来没用?不,fork 就有用了 (testkit)

  1. 文件描述符是用来访问操作系统的某种指针

  2. 操作系统提供了 API 来创建对象

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/pipe# ./anonymous-pipe
[1760] Write: 'Hello, world!'
[1761] Got: 'Hello, world!'
[1760] Done.
  1. 使用 pipe 系统调用创建一个匿名管道,返回两个文件描述符: pipefds[0] (读端)和 pipefds[1] (写端)。

  2. 使用 fork 创建子进程, 子进程和父进程都继承了管道的两个文件描述符。

  3. 子进程关闭写端( pipefds[1] ),从读端( pipefds[0] )读取数据。

  4. 父进程关闭读端( pipefds[0] ),向写端( pipefds[1] )写入数据 "Hello, world!"。

  5. 父进程调用 wait 等待子进程结束。

# T1:
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/pipe# ./named-pipe read

# T2
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/pipe# ./named-pipe write "Hello, world!"

# T1
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/pipe# ./named-pipe read
Received: Hello, world!

管道通信...

管道的内容

...其实之前根本没怎么听说过,计网里面好像有一个管道的内容???但是当时真是一脸懵逼...

这里大概知道命令行的 pipe...有 read 和 write?

可以进行输出处理,统计分析,组合命令....

老师说后面会有一个 pipe 的 Lab...加油干

fd = file descriptor...终于了解了

通过 管道的 read 和 write 口分配,对父子进程进行管理?

实现了父子进程的同步

父进程会说:等我完成你再执行,子进程会听话】

2.9. 实现一切的基础!!!

进程管理

  • fork, execve, waitpid, exit

内存管理

  • mmap, munmap, mprotect, msync

文件管理

  • open, close, read, write, lseek, dup

3. Every thing is a file... ?

3.1. 一切皆文件的好处

一套 API 访问所有对象

  • 一切都可以 | grep

    • Introducing ag -g

同时,UNIX Shell 的语法广受诟病

  • 稍大一些的项目就应该用更好的语言 (Python, Rust!)

  • 但是:We all love quick & dirty!

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"}'
  • 所有一切都是基于字符串的替换

  • 很接近自然语言

  • 但是很难看...

通配符 glob patterns

  • *:匹配任意数量的字符(包括零个)

  • ?:匹配单个字符

  • []:匹配指定范围内的字符

3.2. 文件描述符适合什么?

字节流

  • 顺序读/顺序写

    • 没有数据时等待

    • 典型代表:管道

字节序列

  • 其实有一点点不方便了

    • 需要到处 lseek() 再 read/write

      • mmap?指针?

      • madvice,msync 提供精准控制...

3.3. 反思

优点

  • 优雅,文本接口,就是好用

总结

  • 文件描述符共享:父子进程通过 fork() 创建时,文件描述符指向同一个文件。

  • 文件偏移量独立:父子进程的文件偏移量是独立的,它们可以独立地读写文件。

  • 写入顺序影响结果:最终文件内容取决于父子进程写入的顺序,但它们不会互相覆盖。

缺点

  • 和各种 API 紧密耦合

  • 对高速设备不够友好

    • 额外的延迟和内存拷贝

    • 单线程 I/O

3.4. 出路:API + 封装

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

  • Windows NT: Win32 API → POSIX 子系统

    • Windows Subsystem for Linux (WSL)

  • macOS: Cocoa API → BSD 子系统

  • Fuchsia: Zircon 微内核 → POSIX 兼容层

兼容当然没法做到 100%

  • sysfs, procfs 就是没法兼容

  • 优雅的 WSL1 已经暴毙

    • “Windows Subsystem for Linux”

    • “Linux Subsystem for Windows” (wine) - 另一个方案,现在已经陷入泥潭

    • 理论上只要实现 Linux 所有的 API,就实现了完全的兼容....

3.5. OpenHarmony

只需要把上次的 APP 需要调用的 API 全都找到并且实现就好!

Last updated