You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 63 | 知识串讲:用一个创业故事串起操作系统原理(二)
上一节说到小马同学的公司已经创立了,还请来了周瑜和张昭作为帮手,所谓“兄弟齐心,其利断金”。可是,现在这家公司,还得从接第一个外部项目开始。
## 首个项目虽简单,项目管理成体系
![](https://static001.geekbang.org/resource/image/db/a9/dbd8785da6c3ce3fe1abb7bb5934b7a9.jpeg)
这第一个项目还是小马亲自去谈的。其实软件公司了解客户需求还是比较难的因为客户都说着接近人类的语言例如C/C++。这些咱们公司招聘的CPU小伙伴们可听不懂需要有一个人将客户需求转换为项目执行计划书CPU小伙伴们才能执行这个过程我们称为编译。
编译其实是一个需求分析和需求转换的过程。这个过程会将接近人类的C/C++语言转换为CPU小伙伴能够听懂的二进制语言并且以一定的文档格式写成项目执行计划书。这种文档格式是作为一个标准化的公司事先制定好的一种格式是周瑜从大公司里面借鉴来的称为ELF格式这个项目执行计划书有总论ELF Header的部分有包含指令的代码段的部分有包含全局变量的数据段的部分。
小马和客户聊了整整一天确认了项目的每一个细节保证编译能够通过才写成项目执行计划书ELF文件放到档案库中。此时已经半夜了。
第二天,周瑜一到公司,小马就兴奋地给周瑜说,“我昨天接到了第一个项目,而且是一个大项目,项目执行计划书我都写好了,你帮我监督、执行、管理,记得按时交付哦!”
周瑜说“没问题。”于是周瑜从父项目开始fork一个子项目然后在子项目中调用exec系统调用 然后到了内核里面通过load\_elf\_binary将项目执行计划书加载到子进程内存中交给一个CPU执行。
虽然这是第一个项目,以周瑜的项目管理经验,他告诉小马,项目的执行要保质保量,需要有一套项目管理系统来管理项目的状态,而不能靠脑子记。“项目管理系统?当然应该有了”,小马说。他在《企业经营宝典》中看到过。
于是项目管理系统就搭建起来了。在这里面所有项目都放在一个task\_struct列表中对于每一个项目都非常详细地登记了项目方方面面的信息。
![](https://static001.geekbang.org/resource/image/1c/bc/1c91956b52574b62a4418a7c6993d8bc.jpeg)
每一个项目都应该有一个ID作为这个项目的唯一标识。到时候排期啊、下发任务啊等等都按ID来就不会产生歧义。
项目应该有运行中的状态TASK\_RUNNING并不是说进程正在运行而是表示进程在时刻准备运行的状态。这个时候要看CPU小伙伴有没有空有空就运行他没空就得等着。
有时候进程运行到一半需要等待某个条件才能运行下去这个时候只能睡眠。睡眠状态有两种。一种是TASK\_INTERRUPTIBLE可中断的睡眠状态。这是一种浅睡眠的状态也就是说虽然在睡眠等条件成熟进程可以被唤醒。
另一种睡眠是TASK\_UNINTERRUPTIBLE不可中断的睡眠状态。这是一种深度睡眠状态不可被唤醒只能死等条件满足。有了一种新的进程睡眠状态TASK\_KILLABLE可以终止的新睡眠状态。进程处于这种状态中他的运行原理类似TASK\_UNINTERRUPTIBLE只不过可以响应致命信号也即虽然在深度睡眠但是可以被干掉。
一旦一个进程要结束先进入的是EXIT\_ZOMBIE状态但是这个时候他的父进程还没有使用wait()等系统调用来获知他的终止信息,此时进程就成了僵尸进程。
EXIT\_DEAD是进程的最终状态。
![](https://static001.geekbang.org/resource/image/e2/88/e2fa348c67ce41ef730048ff9ca4c988.jpeg)
另外,项目运行的统计信息也非常重要。例如,有的员工很长时间都在做一个任务,这个时候你就需要特别关注一下;再如,有的员工的琐碎任务太多,这会大大影响他的工作效率。
那如何才能知道这些员工的工作情况呢?在进程的运行过程中,会有一些统计量,例如进程在用户态和内核态消耗的时间、上下文切换的次数等等。
项目之间的亲缘关系也需要维护,任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。
![](https://static001.geekbang.org/resource/image/92/04/92711107d8dcdf2c19e8fe4ee3965304.jpeg)
另外,对于项目来讲,项目组权限的控制也很重要。什么是项目组权限控制呢?这么说吧,我这个项目组能否访问某个文件,能否访问其他的项目组,以及我这个项目组能否被其他项目组访问等等
另外,项目运行过程中占用的公司的资源,例如会议室(内存)、档案库(文件系统)也需要在项目管理系统里面登记。
周瑜同学将项目登记好然后就分配给CPU同学们说开始执行吧。
好在第一个项目还是比较简单的一个CPU同学按照项目执行计划书按部就班一条条地执行很快就完成了客户评价还不错很快收到了回款。
## 项目大了要并行,项目多了要排期
小马很开心可谓开门红。接着第二个项目就到来了这可是一个大项目要帮一家知名公司开发一个交易网站共200个页面这下要赚翻了就是时间要得比较急要求两个星期搞定。
小马把项目带回来周瑜同学说这个项目有点大估计一个CPU同学干不过来了估计要多个CPU同学一起协作了。
为了完成这个大的项目进程就不能一个人从头干到尾了这样肯定赶不上工期。于是周瑜将一个大项目拆分成20个子项目每个子项目完成10个页面一个大项目组也分成20个小组并行开发都开发完了再做一次整合这肯定比依次开发200个页面快多了。如果项目叫进程那子项目就叫线程。
在Linux里面无论是进程还是线程到了内核里面我们统一都叫任务由一个统一的结构task\_struct进行管理。
![](https://static001.geekbang.org/resource/image/75/2d/75c4d28a9d2daa4acc1107832be84e2d.jpeg)
不知道是好消息,还是坏消息,这么大一个项目还没有做完,新的项目又找上门了。看来有了前面的标杆客户,名声算是打出去了,一个项目接一个地不停。
小马是既高兴又犯愁于是找周瑜和张昭商量应该咋办。要不多招人多来几个CPU小伙伴就不搞定了可是咱们还是在创业阶段养不起这么多人。另外的办法就是人力复用一个CPU小伙伴干多个项目干不过来就加加班实在不行就996这样应该就没问题了。
一旦涉及一个CPU小伙伴同时参与多个项目就非常考验项目管理的水平了。如何排期、如何调度是一个大学问。例如有的项目比较紧急应该先进行排期有的项目可以缓缓但是也不能让客户等太久。所以这个过程非常复杂需要平衡。
对于操作系统来讲他面对的CPU的数量是有限的干活儿都是他们但是进程数目远远超过CPU的数目因而就需要进行进程的调度有效地分配CPU的时间既要保证进程的最快响应也要保证进程之间的公平。
如何调度呢周瑜能够想到的方式就是排队。每一个CPU小伙伴旁边都有一个白板上面写着自己需要完成的任务来了新任务就写到白板上做完了就擦掉。
一个CPU上有一个队列队列里面是一系列sched\_entity每个sched\_entity都属于一个task\_struct代表进程或者线程。
调度要解决的第一个问题是每一个CPU小伙伴每过一段时间都要想一下白板上这么多项目我应该干哪一个CPU的队列里面有这么多的进程或者线程应该取出哪一个来执行
![](https://static001.geekbang.org/resource/image/10/af/10381dbafe0f78d80beb87560a9506af.jpeg)
这就是调度规则或者调度算法的问题。
周瑜说,他原来在大公司的时候,调度算法常常是这样设计的。
一个是公平性对于接到的多个项目不能厚此薄彼。这个算法主要由fair\_sched\_class实现fair就是公平的意思。
另一个是优先级,有的项目要急一点,客户出的钱多,所以应该多分配一些精力在高优先级的项目里面。
在Linux里面讲究的公平可不是一般的公平而是CFS调度算法CFS全称是Completely Fair Scheduling完全公平调度。
为了公平项目经理需要记录下进程的运行时间。CPU会提供一个时钟过一段时间就触发一个时钟中断。就像咱们的表滴答一下这个我们叫Tick。CFS会为每一个进程安排一个虚拟运行时间vruntime。如果一个进程在运行随着时间的增长也就是一个个Tick的到来进程的vruntime将不断增大。没有得到执行的进程vruntime不变。
显然那些vruntime少的原来受到了不公平的对待需要给他补上所以会优先运行这样的进程。
这有点儿像让你把一筐球平均分到N个口袋里面你看着哪个少就多放一些哪个多了就先不放。这样经过多轮虽然不能保证球完全一样多但是也差不多公平。
有时候,进程会分优先级,如何给优先级高的进程多分时间呢?
这个简单就相当于N个口袋优先级高的袋子大优先级低的袋子小。这样球就不能按照个数分配了要按照比例来大口袋的放了一半和小口袋放了一半里面的球数目虽然差很多也认为是公平的。
函数update\_curr用于更新进程运行的统计量vruntime CFS还需要一个数据结构来对vruntime进行排序找出最小的那个。在这里使用的是红黑树。红黑树的节点是sched\_entity里面包含vruntime。
调度算法的本质就是解决下一个进程应该轮到谁运行的问题这个逻辑在fair\_sched\_class.pick\_next\_task中完成。
调度要解决的第二个问题是什么时候切换任务也即什么时候CPU小伙伴应该停下一个进程换另一个进程运行
一个人在做A项目在某个时刻换成做B项目去了。发生这种情况主要有两种方式。
方式一A项目做着做着里面有一条指令sleep也就是要休息一下或者等待某个I/O事件。那没办法了要主动让出CPU然后可以开始做B项目。主动让出CPU的进程会主动调用schedule()函数。
在schedule()函数中会通过fair\_sched\_class.pick\_next\_task在红黑树形成的队列上取出下一个进程然后调用context\_switch进行进程上下文切换。
进程上下文切换主要干两件事情一是切换进程空间也即进程的内存也即CPU小伙伴不能A项目的会议室里面干活了要跑到B项目的会议室去。二是切换寄存器和CPU上下文也即CPU将当期在A项目中干到哪里了记录下来方便以后接着干。
方式二A项目做着做着旷日持久实在受不了了。项目经理介入了说这个项目A先停停B项目也要做一下要不然B项目该投诉了。最常见的现象就是A进程执行时间太长了是时候切换到B进程了。这个时候叫作A进程被被动抢占。
抢占还要通过CPU的时钟Tick来衡量进程的运行时间。时钟Tick一下是很好查看是否需要抢占的时间点。 时钟中断处理函数会调用scheduler\_tick()他会调用fair\_sched\_class的task\_tick\_fair在这里面会调用update\_curr更新运行时间。当发现当前进程应该被抢占不能直接把他踢下来而是把他标记为应该被抢占打上一个标签TIF\_NEED\_RESCHED。
另外一个可能抢占的场景发生在当一个进程被唤醒的时候。一个进程在等待一个I/O的时候会主动放弃CPU。但是当I/O到来的时候进程往往会被唤醒。这个时候是一个时机。当被唤醒的进程优先级高于CPU上的当前进程就会触发抢占。如果应该发生抢占也不是直接踢走当然进程而也是将当前进程标记为应该被抢占打上一个标签TIF\_NEED\_RESCHED。
真正的抢占还是需要上下文切换也就是需要那么一个时刻让正在运行中的进程有机会调用一下schedule。调用schedule有以下四个时机。
* 对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。
* 对于用户态的进程来讲,从中断中返回的那个时刻,也是一个被抢占的时机。
* 对内核态的执行中被抢占的时机一般发生在preempt\_enable()中。在内核态的执行中有的操作是不能被中断的所以在进行这些操作之前总是先调用preempt\_disable()关闭抢占。再次打开的时候,就是一次内核态代码被抢占的机会。
* 在内核态也会遇到中断的情况,当中断返回的时候,返回的仍然是内核态。这个时候也是一个执行抢占的时机。
周瑜和张昭商定了这个规则然后给CPU小伙伴们交代之后项目虽然越来越多但是也井井有条起来。CPU小伙伴不会像原来一样火急火燎不知所从了。
可是其实对于项目的开发,这家公司还是有严重漏洞的,就是项目的保密问题,不管哪家客户将系统外包出去,肯定也不想让其他公司知道详情。如果解决不好这个问题,没人敢把重要的项目交给这家公司,小马的公司也就永远只能接点边角系统,还是不能保证温饱问题。
那接下来,小马会怎么解决项目之间的保密问题呢?欲知后事,且听下回分解。
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)