Skip to content

把操作系统对象当成文件来访问

进程

  • 进程 = 状态机
  • 进程管理 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} 匹配 ab 或 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
    • 安卓鸿蒙是套壳
    • 访问格式和权限

上次更新于: