操作系统的 libc 支持

操作系统的 libc 支持

../_images/app-software-stack.png

之前 我们说到系统从其功能实现上的目的是: 管理各种硬件资源,实现隔离并发与虚拟化. 但是站在一般用户的角度来说, 一个操作系统要有良好的生态, 支持应用程序才能被广泛使用. 而应用程序是由编程语言开发的, 这就要求操作系统能够支持一些语言(特别对于C语言)的标准库.

“C库”, “C标准库”, “C运行时”, gibc/musl/mscrt:

  • C 标准库指实现了 ISO C 标准 (C11/C17) 的函数库, 提供我们耳熟能详的 C 标准 API, 常见的实现有 glibcMicrosoft CRT, 不常见的其他实现还有 muslBSD libc 等. (musl 因为其标准兼容性被很多人青睐, BSD libc确实见的就比较少了)
  • C 运行时一般指 C 程序启动(和终止)所需的底层代码 (crt0.ocrt1.o), 也有(我个人认为)有些混淆视听的说法将其等同于 C 标准库. Windows 中 msvcrt.dll 将其明确区分于标准库, Linux 通常不提及 C 运行时(glibc/musl 并未将其拆分)
  • C 库通常指任何 C 标准库或 “当前系统上的 C 标准库”, 例如 “gentoo 的 C 库是 musl” 是说 gentoo 发行版的默认 C 标准库库是 musl.

标准库和内核之间的接口通常是系统调用, 因此这里分析支持 libc 主要是分析其需要的系统调用, 以及 Starry Next 及 ArceOS 对这些系统调用的具体实现.

oscomp/testsuits-for-oskernellibc-test 测例为基础, 对于 libc-test 的各个测例所需系统调用的实现:

涉及到内核态的测例:

(即直接需求系统调用的测例)

argv

sys_execve 系统调用中, sys_execve 的参数除了需要执行应用程序的路径还有命令行参数 argv 和环境变量 envp, 将来自用户的 C 的 UserPtr 指针转为 &str 并转换为 String 类型 , 传递给 sys_execve 的底层实现 load_user_app, 在 app_stack_region 创建用户栈空间时传入, 最后映射到 uspace. 具体 app_stack_regionargs 的处理方式是(出于在用户栈上的字符串索引方便): 把每个参数先都放入栈中并把栈中地址记录下来 -> 放入辅助 Vec 中 -> 把整个记录了字符串起始地址的 Vec 放入用户栈 (此处可参阅 rcore 第七章 内容). 当然除此之外还有地址对齐等额外必要工作

env

和 argv 传递的方式相同. 不过这里需要注意的是, C 规定的全局变量 environ 是在 libc 初始化阶段 _start 时初始化的, 通过读取内核在用户栈上放置的 envp 来初始化 environ

clock_gettime

调用硬件抽象层 axhal 提供的架构无关抽象 axhal::time 获取时间. axhal::time 主要是依靠架构相关的 platform::time 获取时间

fdopen

fdopen 要把一个文件描述符转换为 FILE * 文件流, 主要还是用户态操作. open_at 是打开一个路径为文件描述符, fdopen 不会涉及 open_at 系统调用. 但是这个测例里有 write, ftello, fseeko, close, 所以需要实现如下系统调用:

  • openat: 判断参数类型, 最后都是调用 arceos_posix_api::fs::add_file_or_directory_fd. 这个函数用于创建一个 Directory 对象, 并更新 fd_table. 这个函数会接收一个 lambda, 这个 lambda 捕获 Directory::from_fd 返回的 Directory 对象, 调用其 open_file_atopen_file_dir 方法, 再往下就是之前在 ArceOS 文件系统相关解析中 File::_open_at, root::lookup/create 及以下的部分 (ArceOS 的文件系统涉及架构详见 组件化操作系统 | Amiriox’s Storage)
  • lseek: 通过上述 Filefrom_fd 获取 File 对象, 调用 Fileseek, 检查需要 seek 的位置是否小于文件大小, 更新 self.offsetpos.
  • unlink: 分割字符串, 通过 Directory::open_dir_at 获取 Directory, 调用 Directoryremove_file 从目录下删除 filename

scanf / fscanf

  • read:
    • 如果实现了 fd 这个 feature, 就调用 arceos_posix_api::imp::get_file_like, 从 FD_TABLE 中找 fd 对应的实现了 FileLike trait 的对象, 并调用这个 trait 下的 read. 常见的 Stdin Stdout File Socket 都实现了这个 trait. File 类型的 read 是维护 self.offset 并且访问其中 VfsNodeRef 的内容; 而 Pipe 类型的 read 遵循 POSIX read 标准:
      • 管道中无数据, 写端已关闭: 返回 EOF
      • 管道中无数据, 写端未关闭: 说明可能还有数据将要达到, 应当阻塞, yield 或者 spin
      • 管道中有数据: 尽可能多地读完并返回大小, read_byte 单纯是访问内部数组
    • 否则就只能读 fd=1 的 stdin, 直接 stdio::stdin().read().
  • write 是类似的.

ungetc

  • ungetc 用于把一个字符退回到缓冲区, 使得下次读入从这个字符开始读入. 直接 LOG=debug (或者 strace 也行) 可以看到需要 readv (Read Vector) 系统调用. 分别对每个 vector 读入 (read_implget_file_like(fd).read(buf) 实现) 相应大小即可

memstream

open_memstream 打开一个内存流, 内存流就像文件流一样只是数据都在(堆)内存中, 所以相当于测试 brk mmap munmap 等用户态内存管理相关的系统调用. ArceOS 的用户态内存管理参考这里 .

大多只涉及用户态的测例

basename

单纯的字符串处理, 应该是不涉及什么 syscall

clocale_mbfuncs

这个测例测试 setlocale 和一些字符串转换函数如 mbrtowc 等, 后者基本都是字符串操作, 不涉及 syscall, setlocale 可能是直接操作进程用户地址空间的内存逻辑段或者操作段寄存器设置 tls 之类的, 大概也不涉及 syscall

dirname

basename 的互补, 由单纯的字符串处理

fnmatch

判断字符串是否匹配某一模式的, 感觉也还是用户态操作

iconv_open

初始化字符集转换描述符, 由于这个和 os 不太相关, 我就暂时没有去理解

inet_pton