本文主要介绍几个系统中比较重要的概念。
我们倡导面对任何问题的时候,先要弄明白系统的设计目标。
1、进程的调度 -- 吞吐 和 响应
我们在思考一个系统的调度器时,要理解任何操作系统的调度器设计只追求两个目标:吞吐率大 和 低延时 。
吞吐率大:势必要把更多的时间花费在 真实的有用功 上,而不是把时间浪费在频繁的 进程上下文切换 上。
低延时:要求把优先级高的进程可以随时抢占进来,打断别人,强行插队。但是抢占会引起上下文切换,
上下文切换的时间,本身对吞吐率来说,是一个消耗,这个消耗可以降低到 2us 或者更低(这看起来没什么?),
但是上下文切换更大的消耗不是切换本身,而是切换会引起大量的cache miss。你明明weibo跑的很爽,现在切过去微信,那么CPU的cache是不太容易命中微信的。
不抢肯定响应差,抢了吞吐会下降。Linux不是一个完全照顾吞吐的系统,也不是一个完全照顾响应的系统,它作为一个软实时的操作系统,
实际上是想达到某种平衡,同时也提供给用户一定的配置能力,在内核编译的时候,Kernel Features ---> Preemption Model
选项实际上可以让我们编译内核的时候,是倾向于支持吞吐,还是支持响应:
越往上面选,吞吐越好,越好下面选,响应越好。服务器你一个月也难得用一次鼠标,而桌面则显然要求一定的响应,这样可以保证UI行为的表现较好。但是Linux即便选择的是最后一个选项“Preemptible Kernel (Low-Latency Desktop)”,它仍然不是硬实时的。因为,在Linux有三类区间是不可以抢占调度的,这三类区间是:
-
中断
-
软中断
-
持有类似spin_lock这样的锁而锁住该CPU核调度的情况 (该核的调度会被关掉)
如下图,一个绿色的普通进程在T1时刻持有spin_lock进入一个critical section(该核调度被关),绿色进程T2时刻被中断打断,而后T3时刻IRQ1里面唤醒了红色的RT进程(如果是硬实时RTOS,这个时候RT进程应该能抢入),之后IRQ1后又执行了IRQ2,到T4时刻IRQ1和IRQ2都结束了,红色RT进程仍然不能执行(因为绿色进程还在spin_lock里面),直到T5时刻,普通进程释放spin_lock后,红色RT进程才抢入。从T3到T5要多久,鬼都不知道,这样就无法满足硬实时系统的“可预期”的确定性的延迟性,因此Linux不是硬实时操作系统。
Linux的preempt-rt补丁试图把中断、软中断线程化,变成可以被抢占的区间,而把会关本核调度器的spin_lock替换为可以调度的mutex,它实现了在T3时刻唤醒RT进程的时刻,RT进程可以立即抢占调度进入的目标,避免了T3-T5之间延迟的非确定性。
2、进程的类型 -- CPU 消耗性 和 IO 消耗型
IO 消耗型进程 :(狂睡,等I/O,CPU利用率低),等待I/O请求,提交I/O请求。比如系统读写文件(磁盘I/O),鼠标,键盘都属于。I/O消耗型任务对延迟比较敏感,应该被优先调度。
CPU消耗型进程:(狂算,CPU利用率高),优先级比IO进程低。
比如,你正在疯狂编译安卓,而等鼠标行为的用户界面老不工作(正在狂睡),但是鼠标一点,我们应该优先打断正在编译的进程,而去响应鼠标这个I/O,这样电脑的用户体验才符合人性。
3、进程的内存 -- 分配和占据
Linux 作为一个把应用程序员当傻逼的操作系统,它必须允许应用程序犯错。
所以这类问题就不要问了:
进程malloc了内存,还没有free就挂了,那么我前面分配的内存没有释放,是不是就泄露掉了?
明确的说,这是不可能的,Linux内核如果这么傻,它是无法应付乱七八糟各种开源有漏洞的软件的,所以进程死的时候,肯定是资源被内核释放掉了,这类傻问题,你明白linux的出发点,就不会再去问了。
同样的,你在应用程序里面malloc成功的那一刻,你真的拿到内存了吗?
malloc成功,不要以为你真的拿到了内存,这个时候你的 vss ( 虚拟地址空间 ,virtual set size ) 会增大,但是你的 rss ( 驻留在内存条上的内存,占用的物理内存,resident set size ) 会随着写到每一页而缓慢增大,所以分配成功的那一刻,你顶多是被忽悠了,和你实际占用还是不占用,暂时没有半毛钱的关系。
举例来说明,如下图,最初的堆是8KB, 这8KB 也写过了,所以堆的 vss 和 rss 都是 8KB 。此后,我们调用 brk() 把堆变大到 16KB, 但是实际上它占据的内存 rss 还是 8KB ,因为第3页还没有写,根本没有真正的从内存条上拿到内存。直到写到第3页,堆的 rss 才变成 12KB, 这就是 linux 针对 app 的 lazy 分配机制,它的出发点,当然也是防止应用程序傻逼了。
代码段的内存、堆的内存、栈的内存都是这样懒惰地拿到,demanding page。
我们有一台1GB内存的32位Linux系统,我们关闭swap,同时透过修改overcommit_memory为1来允许申请不超过进程虚拟地址空间的内存:
$ sudo swapoff -a
$ sudo sh -c 'echo 1 >/proc/sys/vm/overcommit_memory'
此后,我们的应用可以申请一个超级大的内存(比实际内存还大):
上述程序在1GB的电脑上面运行,申请2GB内存可以申请成功,但是在写到一定程度后,系统出现out-of-memory,上述程序对应的进程作为oom_score最大(最该死的)的进程被系统杀死。
4、进程的资源 -- 隔离和共享
Linux 的 某一个进程究竟耗费了多少内存?
这个问题很复杂,除了上面的 vss ,rss 外,还有 pss 和 uss ,这些都是 Linux 不同于 RTOS 的显著特点之一。Linux 各个进程既要做到隔离,但是隔离中又要实现共享,比如1000个进程都要用到 libc ,libc的代码段显然在内存中只有1份。
下面的一幅图上有3个进程,pid为1044的 bash、pid为1045的 bash和pid为1054的 cat。每个进程透过自己的页表,把虚拟地址空间指向内存条上面的物理地址,每次切换一个进程,即切换一份独特的页表。
仅从此图而言,进程1044的 vss 和 rss 分别是:
vss= 1+2+3
rss= 4+5+6
但是是不是“4+5+6”就是1044这个进程耗费的内存呢?这显然也是不准确的,因为4明显被3个进程指向,5明显被2个进程指向,坏事是大家一起干的,不能1044一个人背黑锅。
这个时候,就衍生出了一个pss(按比例计算的驻留内存, Proportional Set Size )的概念,仅从这一幅图而言,进程1044的pss为:
pss= 4/3 +5/2 +6
最后,还有进程1044独占且驻留的内存 uss(Unique Set Size ),仅从此图而言,
uss = 6
所以,分析Linux,我们不能模棱两可地停留于表面,或者想当然地说:“Linux的进程耗费了多少内存?”因为这个问题,又是一个要靠装逼来回答的问题,“dependon…”。
本文参考: