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 盘就是一个操作系统???
只要拷对了文件,操作系统就能正常执行啦!
创建 UEFI 分区,并复制正确的 Loader
创建文件系统
mkfs (格式化)
cp -ar 把文件正确复制 (保留权限)
注意 fstab 里的 UUID
你就得到了一个可以正常启动的系统盘!
运行时挂载必要的其他文件系统
磁盘上的 /dev, /proc, ... 都是空的
mount -t proc proc /mount/point 可以 “创建” procfs
2.8. 任何“可读写”的东西都额可以是文件
真实的设备
/dev/sda
/dev/tty
虚拟的设备 (文件)
/dev/urandom (随机数), /dev/null (黑洞), ...
它们并没有实际的 “文件”
操作系统为它们实现了特别的 read 和 write 操作
甚至可以通过 /sys/class/backlight 控制屏幕亮度
procfs 也是用类似的方式实现的
管道:一个特殊的 “文件” (流)
由读者/写者共享
读口:支持 read
写口:支持 write
匿名管道
int pipe(int pipefd[2]);
返回两个文件描述符
进程同时拥有读口和写口
看起来没用?不,fork 就有用了 (testkit)
文件描述符是用来访问操作系统的某种指针
操作系统提供了 API 来创建对象
root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec7/pipe# ./anonymous-pipe
[1760] Write: 'Hello, world!'
[1761] Got: 'Hello, world!'
[1760] Done.
使用 pipe 系统调用创建一个匿名管道,返回两个文件描述符: pipefds[0] (读端)和 pipefds[1] (写端)。
使用 fork 创建子进程, 子进程和父进程都继承了管道的两个文件描述符。
子进程关闭写端( pipefds[1] ),从读端( pipefds[0] )读取数据。
父进程关闭读端( pipefds[0] ),向写端( pipefds[1] )写入数据 "Hello, world!"。
父进程调用 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