读《莱昂氏UNIX源代码分析》

在unix早期的代码中,schedule和swap两个核心任务都是由0号进程来负责的,这个朴实的设计就是unix系统最最原始的设计,因为unix在开始设计的时候十分清楚进程应该做什么不应该做什么,应该做它本职的工作,而诸如调度和置换之类的任务不应该由用户进程负责,但是linux后来颠覆了这个想法,毕竟频繁的切换带来的开销已经基本抵消了分工设计带来的优雅,于是就将调度工作分担给了各个进程本身,而置换工作仍旧由 内核进程来完成但是却不是0号进程,而0号进程最终退化成了一个idle进程。

我们从原始的unix 6的代码中可以看到schedule的实现,该实现中一共进行了两次切换而不是像linux中的那样仅仅一次,起初切换到0号进程,实际上就是切换了一下堆栈,核心堆栈切换到了0号进程就可以说当前的进程是0号进程了,代码执行流依旧往下走,切换堆栈并没有修改pc寄存器的值,因此进行了第一次切换以后的代码就成了0号进程执行的了,0号进程接下来做什么呢?其实很简单,就是在所有被换进内存的进程中找一个最值得运行的,这就涉及到了具体的调度策略。待0号进程选择好了下一个进程之后,最后进行第二次切换,也就是切换到这个被选中的进程,切换的过程是简单的,就是核心栈的切换,然后就是切换页表,实际上那个时候的页表被叫做区表,系统还没有实现完全的虚拟内存管理,PDP-11机器使用寄存器来实现类似X86页表的功能,在切换的过程中,将负责“虚拟页面”和物理页面映射的寄存器修改一下就可以了,因为一个时刻只有一个进程是运行态,所以进程的核心栈所在的位置是一个确定的位置,unix的这个设计非常好,系统变得简单,只需要将这个确定位置的“虚拟内存”地址指向新进程的物理内存地址就可以了,物理内存由malloc分配,注意和标准c库的malloc的区别和联系,区别是功能不同,联系是实现思想相同。这里不深谈其它的地址映射,实际上内核空间的代码是共享的,而用户空间的不同,PDP-11机器分别用两组寄存器来进行内核空间和用户空间的地址映射,由此可见内核空间的映射是相同的,并且和后来的linux一样,也用了一一的线性映射。这里可以看出代码和数据的区别,虽然在调度函数里面执行的是一个函数的代码,但是却是不同的3个进程在执行,所以正是数据将进程区别了开来,而不是代码,数据又被分为堆栈等等,因此切换了堆栈就是切换了进程,当然这里的堆栈是核心堆栈。这个设计被后来的windows所采用,一直到现在的Windows NT都直接体现了古老的unix的这一思想,将核心堆栈等等一些核心的数据结构放置于一个固定的内存地址,而linux却不是这样,这得益于linux中task_struct这一精巧结构的实现,在windows中和unix一样也是将进程的控制块分为了类似U区和proc结构,并且unix自从设计之初,优先级的概念就很重要,后来windows的IRQL也是借鉴了这一思想,说说看,linux是类unix系统吗?除了API一样之外几乎完全颠覆了unix的一切,倒不如说windows的某些设计和unix很相似,当然这里不谈微内核和大内核。

通观unix 6代码的fork和newproc函数的实现,简直叫一个妙啊,很简单,很明了,在父进程中直接返回子进程的pid,而子进程直接挂入进程队列,待到系统进行调度的时候也就是switch的时候,子进程会返回switch的返回值,也就是0,switch的返回值简直就是一石三鸟啊!unix在当时其实还没有完全实现虚拟存储,仅仅是很朴素的虚拟内存的概念,因此并没有后来复杂的请求调页机制,而是整个进程的换入换出,负责这件事的就是0号进程最后的那个无限大循环。在linux的请求调页实现中,专门有一个内核线程负责页面的换入和换出,当然必须处理的就是缺页异常,linux靠缺页处理以及kswapd实现了请求调页。

总之,如果你想深入理解操作系统原理,那么务必看一下《莱昂氏UNIX源代码分析》这一本书,这本书的分量可以任何讲操作系统的书都要重,从中你可以得到操作系统最精髓的部分,比现如今动则五六百页的书要简单得多,简单就是美。只求理解精髓!10000行左右的代码竟然诠释了如此一个庞然大物,并且作为后面几乎所有操作系统的蓝本一直发展壮大着,直到现在。

你可能感兴趣的:(读《莱昂氏UNIX源代码分析》)