前言
应用层通常专注于API的使用,认为将其看成是一个黑盒即可,但了解操作系统的一些机制将有助于我们更好地使用它,也有助于避免犯错误。
unix系统
1969年,unix从贝尔实验室诞生,由一个失败的操作系统multics发展而来,它发布时带上了源码,以至于很多组织都对其进一步改进。它由C语言编写,且只有几百个系统调用,秉承一切皆文件的设计思想。
linux系统
由芬兰大学生linus在1991年开发,基于Intel80386微处理器,开发完后就在因特网发布了源码。它借鉴了unix的很多设计思想,但它的实现完全不同,算是一个不同的操作系统。开源协议非常自由,可以自由修改,但需要将改过的源码继续发布出来。
linux特点
它是单内核,与之相反的是微内核。
模块化设计,支持动态加载内核模块。
支持对称多处理机制(SMP),多个处理器需要考虑共享资源问题。
抢占式多任务操作系统,内核对进程的调度属于抢占式。
支持内核线程,但它不区分线程与进程,对他来说所有进程都一样。
什么是操作系统
操作系统负责基本的功能和系统管理,包括内核、设备驱动、启动引导、shell、界面、文件管理、其它系统工具。应用程序通常通过库函数,库函数通过系统调用让内核去完成各种任务。
CPU活动状态
运行于用户空间,执行用户进程。
运行于内核空间,处于进程上下文,表示执行某个进程。
运行于内核空间,处于中断上下文,此时与任务进程都无关,它在处理某个中断。
进程&线程
进程是处于执行期的程序,它包含各种资源,比如文件、信号、内核数据、处理器状态、内存空间、执行线程、全局数据段等。进程提供了虚拟处理器和虚拟内存,看起来进程就像自己独享相应资源。
内核将所有进程保存在任务队列中,它是一个双向循环链表,该链表中每个节点的类型都是task_struct进程描述符结构,处于
所有进程都是PID为1的init进程的后代,内核启动的后阶段启动init进程,它负责读取系统的初始化脚本并执行其它相关程序。每个进程都有一个父进程,每个进程也都可以有零个或多个子进程。fork()用于创建子进程,它通过拷贝当前进程来创建一个子进程,父子进程区别在于PID、PPID、某些资源、统计量不同。fork()使用了写时拷贝机制,父进程和子进程共享同一个拷贝,只有需要写入时数据才会被复制。fork()的实际开销就是复制父进程的页表以及给子进程创建进程描述符。exec()负责读取可执行文件并将其载入空间开始运行。进程调用exit()系统调用时进程被终结。
线程是进程的活动对象,每个线程都拥有独立的程序计数器、进程栈和一组寄存器。线程能在同一程序里面共享内存地址空间,共享文件和其它资源。从内核角度来说linux并没有线程这个概念,它将线程当成是进程来看待。内核线程没有独立的地址空间,只运行在内核空间,从不切换到用户空间。
进程的调度
进程调度器的目的就是大限度地利用CPU时间,只要有可执行的任务就要尽量去执行它们,但我们知道系统中几乎总是任务数大于处理器个数,所以肯定某个时刻有任务得不到执行。多任务系统能并发地执行多个进程,一个CPU时是一对多,多个CPU时是多对多,所有任务看起来就像是同时在运行一样。
多任务系统分为抢占式和非抢占式,linux是抢占式调度,调度程序来决定进程的挂起和执行,每个进程能执行的时间为时间片,现代系统一般都有一定的策略来动态分配时间片。非抢占式则是由进程自己主动停止,否则将一直运行下去,调度程序无法决定进程的运行停止,如果某个进程一直运行则会使系统奔溃。
调度策略
进程可分为I/O密集型和处理器密集型。前者大部分时间进行各种I/O请求,通常这些进程只运行很短一段时间,然后就在I/O上阻塞了。而后者则是大部分时间都用于代码执行上,不停地运行而且没有什么I/O请求。当然有些进程既是I/O密集型也是处理器密集型。对于前者我们希望时间片少一点,对于后者我们希望时间片多一点,调度策略就是要在两个矛盾中找到平衡,使得进程能够响应迅速又能大系统利用率。
基于优先级的调度是基本的一种调度算法,它把进程进行了分级,优先级高的先运行,优先级低的后运行,相同优先级的进程则轮流运行。某些系统中优先级高的进程将更频繁运行,且时间片也更长。调度程序选择时间片未用尽且优先级高的进程来执行。linux有两种优先级范围,种是nice值,从-20到+19,默认为0,值越大优先级越低。第二种是实时优先级,默认范围是0到99,值越高表示优先级越高。任何实时进程的优先级都高于普通进程,也就是说nice优先级和实时优先级是两个不同维度。
时间片是进程在被抢占前所能持续运行的时间,默认的时间片大小不容易确定,时间片太长会使交互响应变差,时间片太短会增加进程切换带来的损耗。也就是前面说到的矛盾,I/O密集型希望时间片短点,而处理器密集型则希望时间片长点。
CFS调度器
linux采用了完全公平调度(CFS)算法,它是一个针对普通进程的调度器,linux中称为SCHED_NORMAL,而POSIX则称为SCHED_OTHRER,实现类在kernel/sched_fair.c中。CFS允许每个进程运行一段时间、循环轮转、选择运行少的进程作为待运行进程,CFS计算所有可运行进程总数作为基础,再计算一个进程应该运行多久,而不是靠优先级来计算时间片。CFS有一个小时间片(小粒度),默认是1ms。也就是说就算有无穷大个进程,每个进程少也能有1ms时间片。CFS确保给每个进程公平的处理器使用比。
CFS不再简单使用时间片的概念,而是必须要维护每个进程运行的时间记账,以此确保每个进程公平分配。
CFS选择下一个待运行进程时它会挑一个具有小运行时间的进程。
睡眠(被阻塞)的进程处于不可执行状态,它将从可执行红黑树中移除,放到等待队列中。等待队列是简单链表结构,
唤醒,与睡眠刚好相反,进程被设置为可执行状态,并且从等待队列中移到可执行红黑树中。
上下文切换,即从一个可执行线程切换到另一个可执行进程,由kernel/sched.c的context_switch函数负责。将虚拟内存从上一个进程映射到新进程,保存恢复栈信息和寄存器信息以及其它相关信息。
内核提供了need_resched标志来表明是否需要重新执行一次调度,内核无论是咋中断处理程序还是在系统调用后返回都会检查这个歌表示,如果被设置了则内核会选择一个更适合的进程投入运行。
实时调度
linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。前者是一种简单的先入先出调度算法,它不使用时间片。处于SCHED_FIFO级别的进程会比任何SCHED_NORMAL级别的进程先得到调度。一旦某个SCHED_FIFO级别的进程处于可执行状态,它将会一直执行,直到它自己阻塞或显示释放CPU。只有更高优先级的SCHED_FIFO或SCHED_RR进程才能抢占它。两个相同优先级的SCHED_FIFO级别的进程会轮流执行,其它普通进程只能等它变为不可运行状态后才有机会执行。
SCHED_RR大致与SCHED_FIFO相同,但它是耗尽事先分配的时间后就不再继续执行,即SCHED_RR是带了时间片的SCHED_FIFO。时间片只是相对于同一优先级的进程,低优先级的进程无法抢占SCHED_RR任务,即使它的时间片耗尽也不行。
放弃处理器时间
linux 通过sched_yield()系统调用来放弃当前进程的处理器时间,让给其他待执行进行,对于实时进程,它会将进程从活动队列中移到其优先级队列的后面。早期linux的yield语义不一样,只会将进程放到优先级队列的末尾,放弃的时间不会太长。
本公众号专注于人工智能、读书与感想、聊聊数学、计算机科学、分布式、机器学习、深度学习、自然语言处理、算法与数据结构、Java深度、Tomcat内核等。