第14讲 C标准库的实现
系统调用的封装,内存空间管理
不会有人想用汇编来写操作系统吧,不会有人想直接用系统调用构建吧。 不会真的有人在oj上写题,不用printf用write系统调用吧。
对系统调用进行封装,是自然而然的想法。
重新认识libc
一个例子
execve的pathname是绝对路径,但是你希望使用PATH下的路径可以吗?你想不按照规定来?没门。
execlp帮助你去遍历env的PATH,挨个执行execve看看能不能找到合适的pathname。高情商api,这才是人民群众喜闻乐见的api
封装纯粹的计算,比如memset。 标准库要正确,并快。兼容很多cpu
多个线程同时memset怎么办?库函数要考虑的问题比自己写的要多得多。
标准库只对“标准库内部数据”的线程安全性负责
更多标准库需要做的:排序,查找,atoi,atol,atoll
计算就更复杂了 怎样在IEEE754这个浮点数优化计算,754在-1到1之间很密,怎么利用这个特性,怎么避免-INF精度爆炸的问题。
封装操作系统的对象
在UNIX的世界里,实际上就是封装文件描述符
FILE * 背后是一个文件描述符
|
|
窥探glibc的内部实现
|
|
返回了一个3 的文件描述符,并在3这里做了一个write的系统调用
我们甚至可以在gdb上p *fp
或者p *stdin
,会发现stdin的filenum=0
|
|
怪?
这就是UNIX
封装更多的东西
err,error,perror
为什么你在很多地方都能看到No such file or directory?
我们也可以打印一个这样的error msg
|
|
原来大家和我用的是同一个标准库
|
|
下一个关于env的魔术
|
|
链接的时候会找到这个**environ
那么状态机重置以后**environ
会变成什么?
我们通过gdb一步一步看一下哪一步**environ
被赋值的
静态链接结果:
|
|
是__libc_start_main()赋值的
动态链接结果:
|
|
封装地址空间
malloc和free
标准库的实现要考虑什么?
用户的使用场景
- 小对象的创建/分配迅速(需要考虑并发)
- 大的数组和对象拥有更长的生命周期(不太需要并发)
设置两套系统,考虑fast path和slow path
- fast path
- 性能极好、并行度极高、覆盖大部分情况
- 但有小概率会失败 (fall back to slow path)
- slow path
- 不在乎那么快
- 但把困难的事情做好
- 计算机系统里有很多这样的例子 (比如 cache)
人类也是这样的系统
Daniel Kahneman. Thinking, Fast and Slow. Farrar, Straus and Giroux, 2011.
小内存:Segregated List
分配: Segregated List (Slab)
每个 slab 里的每个对象都一样大 每个线程拥有每个对象大小的 slab fast path → 立即在线程本地分配完成 slow path → pgalloc() 两种实现 全局大链表 v.s. List sharding (per-page 小链表)
回收
直接归还到 slab 中 注意这可能是另一个线程持有的 slab,需要 per-slab 锁 (小心数据竞争) 大内存:一把大锁保平安 Buddy system (1963)
如果你想分配 1, 2, 3, 4, … 个连续的页面? 例如:64 KB/页面 那就 first fit 或者 best fit 吧…… 你只需要一个数据结构解决问题
区间树;线段树……
更多标准库的设计推荐阅读libc的手册