操作系统的 libc 支持
操作系统的 libc 支持

之前 我们说到系统从其功能实现上的目的是: 管理各种硬件资源,实现隔离并发与虚拟化. 但是站在一般用户的角度来说, 一个操作系统要有良好的生态, 支持应用程序才能被广泛使用. 而应用程序是由编程语言开发的, 这就要求操作系统能够支持一些语言(特别对于C语言)的标准库.
“C库”, “C标准库”, “C运行时”,
gibc
/musl
/mscrt
:
- C 标准库指实现了 ISO C 标准 (C11/C17) 的函数库, 提供我们耳熟能详的 C 标准 API, 常见的实现有
glibc
或Microsoft CRT
,不常见的其他实现还有musl
和BSD libc
等. (musl
因为其标准兼容性被很多人青睐,BSD libc
确实见的就比较少了)- C 运行时一般指 C 程序启动(和终止)所需的底层代码 (
crt0.o
或crt1.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-oskernel
中 libc-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_region
对 args
的处理方式是(出于在用户栈上的字符串索引方便):
把每个参数先都放入栈中并把栈中地址记录下来 -> 放入辅助 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_at
或open_file_dir
方法, 再往下就是之前在 ArceOS 文件系统相关解析中File::_open_at
,root::lookup/create
及以下的部分 (ArceOS 的文件系统涉及架构详见 组件化操作系统 | Amiriox’s Storage)lseek
: 通过上述File
的from_fd
获取File
对象, 调用File
的seek
, 检查需要 seek 的位置是否小于文件大小, 更新self.offset
为pos
.unlink
: 分割字符串, 通过Directory::open_dir_at
获取Directory
, 调用Directory
的remove_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_impl
由get_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 不太相关, 我就暂时没有去理解