把操作系统对象当成文件来访问
进程
- 进程 = 状态机
- 进程管理 API: fork, execve, exit
连续的内存段
- 我们可以把 “连续的内存段” 看作一个对象
- 可以在进程间共享
- 也可以映射文件
- 内存管理 API: mmap, munmap, mprotect, msync
操作系统肯定还有其他对象的!
文件
- ***有名字的数据段,字节的序列(普通文件)***
- Linux 终端命令有关设备文件的介绍
``` sh
$ cd /dev $ ls -l /dev/null crw-rw-rw- 1 root root 1, 3 Mar 21 14:46 /dev/null $ touch /tmp/sb.c $ ls -l /tmp/sb.c -rw-r--r-- 1 czc czc 0 Mar 21 14:48 /tmp/sb.c ``` - 开头的那一段crw中的c意为character device - 类似的还有b开头的块(block)设备,可通过ls -l | grep ^b查看
read系统调用- ssize_t read(int fd, void buf[.count], size_t count);
- read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
- 但是不是这样的,需要
open系统调用打开文件赋值给一个文件描述符
什么是文件描述符
- 指向操作系统对象的 “指针”
- Everything is a file
- 通过指针可以访问 “一切”
- 对象的访问都需要指针
open, close, read/write (解引用), lseek (指针内赋值/运算), dup (指针间赋值)- 比如一开始0,1,2指向操作系统中同一个对象(终端)可以
ls -l proc/<pid>/fd查看文件描述符- 此时使用read访问没有指向的文件描述符(比如3),read失败会返回-1
- open会在地址空间找到没有用过的一个文件描述符,并分配指针指向,隐式传入文件描述符参数。
- close就是解除指针的指向。
- dup是指针的浅拷贝,
dup(1)备份一份1号文件描述符,返回值是复制的文件描述符号,比如返回4,现在4就指向原来1的位置,就可以任意修改1的重定向。
- 012总是标准输入、输出和错误
- 新打开的文件从 3 开始分配
- 文件描述符是进程文件描述符表的索引
- 关闭文件后,该描述符号可以被重新分配
- 查看打开文件的限制
- ulimit -n (进程限制)通常是shell的配置决定
- sysctl fs.file-max (系统限制)通常通过内核计算,内核的内存自动计算
sh
czc@Starrys:~/ocaml$ ulimit -n
1048576
czc@Starrys:~/ocaml$ sysctl fs.file-max
fs.file-max = 1619867- 示例:fd-alloc
sh
./fd-alloc
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
Closed file descriptor fds[1]: 4
Closed file descriptor fds[3]: 6
Closed file descriptor fds[5]: 8
Closed file descriptor fds[7]: 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- fork()之后会怎么样呢?
- fork之后其实文件描述符保留,指向相同的位置。
- 文件描述符其实是一个流式的,读就读走,写就覆盖。
- 实际上文件描述符指向的是offset,从而指向一个对象。比如write写入后,offset也要更新到相应的位置
- dup()以后确实是共享offset
- 保证文件描述符指向相同的offset从而书写。共享了之后两个文件描述符用同一个offset,会同步更新,所以二者不会互相覆盖。一个写完"hello"offset已经往后推了,然后另一个写"world"
c
// Duplicate the file descriptor
int fd2 = dup(fd1);
if (fd2 < 0) {
perror("Failed to duplicate file descriptor");
close(fd1);
exit(EXIT_FAILURE);
}
// Write "A" to the file
write(fd1, "A", 1);
// Write "B" using the duplicated file descriptor
write(fd2, "B", 1);
// Close both file descriptors
close(fd1);
close(fd2);- 那么fork()呢?
- 实验表明确实也是共享的offset
c
pid_t pid = fork();
if (pid < 0) {
perror("Failed to fork");
close(fd);
exit(EXIT_FAILURE);
} else if (pid == 0) {
// Child process
write(fd, "D", 1);
close(fd);
exit(EXIT_SUCCESS);
} else {
// Parent process
wait(NULL); // Wait for child to finish
write(fd, "C", 1);
close(fd);句柄 Handle(Windows 的文件描述符)
- 据传是第一次翻译的语境是在一个句子中,所以译为句子的句柄,以后沿用了这个翻译。
- 默认是不继承的,使用“最小权限原则”
- 所以linux有一个权限设置
fcntl(fd, F_SETFD, FD_CLOEXEC)来设置为exe时不继承。
对象
- FHS原则(File Hierarchy Standard)
- unix下一切都是文件。设备文件,系统参数……
mount -t proc proc .命令执行系统调用创建对象。- 神奇的命令,mount可以挂载文件系统。
- 管道:一个特殊的文件(流)
- 一个写口write port,一个读口read port。
- 也是文件描述符,
int pipe(int pipefd[2]);返回两个文件描述符。 - read读不到数据,会等待直到有数据进入。
- 管道联合fork使用
- fork之后mem和reg都复制了,fd获得浅拷贝。比如写了一个3->w,4->r,fork后这两个文件描述符的指向不变
- 可以父进程close(3),子进程close(4),此时可以从子进程往父进程写数据,且父进程因为read等待一定读到数据。同时实现父子进程的同步
- 可不可以不关?
- 管道的读取按照MSGSIZE读取,写端写满一定大小(4KB),读端才会读取。
- 此时就可以实现管道符“|”等状态。
- 创建命名的管道 (FIFO)
- 当创建匿名管道的时候,pipe系统调用可以创建一个匿名的管道对象,当所有持有该管道的文件描述符消亡以后,该管道没有人引用,会被操作系统自动回收
- 可以在文件系统下创建一个管道。使用mkfifo函数创建一个命名管道对象
c
#define PIPE_NAME "/tmp/my_pipe"
if (mkfifo(PIPE_NAME, 0666) == -1) {
if (errno != EEXIST) {
perror("mkfifo");
return 1;
}
int fd = open(PIPE_NAME, O_RDONLY);- 一切皆文件。都可以使用
| grep,使用cat打印。ag -g ls **/*可以查看当前目录下的所有文件- 危险:makefile中使用
rm -rf a $(ROOT_DIR)/不小心ROOT_DIR没赋值,直接就挂了。
常见的通配符包括:
*:匹配任意数量的字符(包括零个)?:匹配单个字符[]:匹配指定范围内的字符 实际中常用的扩展包括:**:递归匹配任意层级的子目录{}:匹配多个模式,如{a,b,c}匹配a、b或c!(pattern):排除指定模式
- 其实Unix shell是一个很好的编程语言。
- 但是更接近自然语言,会出现二义性quick&dirty
- 你去买两个西瓜,如果看到香蕉,就买一个二义性
文件描述符适合什么
字节流
- 顺序读/顺序写
- 没有数据时等待
- 典型代表:管道
字节序列
- 其实就有一点点不方便了,字节序列拥有offset这样的游标。
- 需要到处 lseek 再 read/write
off_t lseek(int fd, off_t offset, int whence);- mmap 不香吗?指针指哪打哪,使用memcopy就能结束
- madvise, msync 提供了更精细的控制
文件和各种 API 紧密耦合
- A fork() in the road
- 没有问题是加一层抽象解决不了的
- lib.c把各种api调用翻译成上层系统使用的api
- WSL1已经挂了。早期的兼容问题很大。执行系统调用的适合会把linux的调用翻译成windows的。
- Linux看起来api很少,所有syscall都实现后就能兼容linux。
- 但是实现,比如read,就需要知道什么范围下的文件是可以读取的。
- linux的文件、设备、驱动全部都很难实现。
- 同样还有一个Linux Subsystem for Windows(wine)
- 要达到面向应用程序的实现,就必须考虑硬件的接口
- OpenHarmony
- 安卓鸿蒙是套壳
- 访问格式和权限