早期的计算机,比如运行DOS的IBM PC或者早期的大型机IBM 709, 是没有内核这种概念的, 从软盘、磁带或者纸带加载程序之后程序就可以完全的控制整个计算机硬件.
后来随着历史的发展,出现了一种需求:多个人同时访问一台计算机。为了满足这种需求,出现了现代化的操作系统:分时操作系统。早期流行并且至今产生深远影响的分时操作系统就是Unix。
这里有几个概念:
微软也编写了关于虚拟内存的文档: 虚拟地址空间 - Windows drivers
4. 特权级与系统调用:许多指令,比如设置页表或者中断向量表的地址,修改CPU的工作模式,都属于特权指令,普通程序禁止执行, CPU使用特权级(又称RING或者异常级别)表示当前正在执行的进程的权限.低权限的程序执行高权限的指令或者访问高权限的内存会触发异常.
这是AARCH64(arm64)的特权级(Exception level)描述图权限级最低的,就是普通的用户态进程(EL0) , 最高的,就是操作系统内核(EL1)
其实上图有4层结构,EL2用于虚拟机管理程序,而虚拟机可以执行多个操作系统。(实际上手机不使用此功能),最高的EL3就是CPU的固件。而右侧的部分为可信计算环境,处理诸如加密,指纹等机密信息. 据华为PPT,当前鸿蒙微内核实际上运行于此(不是电视上那个鸿蒙OS).
为了允许用户态进程访问高权限资源,操作系统内核提供了通过陷阱调用系统功能的入口,即系统调用. 打开文件(open)写文件(write)都是系统调用。
可以看到,用户态程序和内核之间存在天然的界限.
需要注意的一点是,操作系统会附带一些重要的系统进程,比如Linux的init, 但是这些也只是用户态程序, 操作系统内核也许对其有特殊的标记,但是对于CPU来说和其他进程没有区别.
下面讨论一下Windows,比较使用量很广.
上图中,Windows的内核主要部分即ntoskrnl.exe,也称内核映像. 处于商业的考虑,Windows实际上有4种不同的内核映像.而smss.exe则是第一个用户态程序.
Windows的内核代号为NT, 其架构图为
这个图很复杂,我们也不必搞懂...紫色的内核模式和蓝色的用户模式区分很明显.
其中一个很小的kernel mode drivers(内核模式驱动程序),看起来不起眼,实际上是NT内核中体积最大的部分.
诸多微软官方和第三方的驱动程序数不胜数的硬件,通过这种方式被Windows兼容,通过Windows update和即插即用(PnP)机制,做到了最佳的体验。
Windows是一个闭源操作系统,也基本上不管驱动的编写(除去CPU和那些已经标准化的协议)。硬件制造商(或者毒瘤软件)想要编写驱动程序,需要安装Visual studio,Windows SDK和HDK(硬件开发工具),打开MSDN(现在改名了),照着文档里的函数接口写。
使用C/C++编写Windows驱动编写完的驱动包括.sys驱动程序和inf描述文件,交给微软进行兼容性测试,随后就能获得签名并发布了。微软同时保证,这些接口不会发生变化,即保证了二进制兼容性(编译好的驱动即使系统更新也不会出现问题).
当然也有例外(逃
上图都是驱动的问题....
前面提到了虚拟内存的概念,Windows大多数驱动都是内核模式驱动程序(.sys),少数是用户模式驱动程序(.dll), 微软在这里提到了区别: 用户模式和内核模式 - Windows drivers
运行 Windows 的计算机中的处理器有两个不同模式:用户模式 和内核模式 。 根据处理器上运行的代码的类型,处理器在两个模式之间切换。 应用程序在用户模式下运行,核心操作系统组件在内核模式下运行。 虽然许多驱动程序以内核模式运行,但某些驱动程序可能以用户模式运行。
启动用户模式应用程序时,Windows 会为该应用程序创建进程 。 进程为应用程序提供专用的“虚拟地址空间” 和专用的“句柄表” 。 由于应用程序的虚拟地址空间为专用空间,因此一个应用程序无法更改属于其他应用程序的数据。 每个应用程序都隔离运行,如果一个应用程序发生故障,则故障仅局限于该应用程序。 其他应用程序和操作系统不会受该故障的影响。
除了专用之外,用户模式应用程序的虚拟地址空间也受到限制。 在用户模式下运行的处理器无法访问为操作系统保留的虚拟地址。 限制用户模式应用程序的虚拟地址空间可防止应用程序更改以及可能损坏关键的操作系统数据。
在内核模式下运行的所有代码都共享单个虚拟地址空间。 这意味着内核模式驱动程序不会与其他驱动程序和操作系统本身隔离。 如果内核模式驱动程序意外写入错误的虚拟地址,则属于操作系统或其他驱动程序的数据可能会受到安全威胁。 如果内核模式驱动程序发生故障,整个操作系统就会发生故障。
所谓的发生故障,就是蓝屏. 由于显卡驱动故障的概率很高, 使用用户模式驱动程序会减少蓝屏, 微软也使用特殊的Windows显示驱动框架(WDDM)强制要求显卡驱动部分实现在用户模式下. 在Windows10中, Win+Ctrl+Shift+B快捷键即可重启显卡驱动和图像堆栈, 而放在XP的时代,显卡驱动出现故障就会直接蓝屏.
正是因为Windows支持用户模式驱动程序,因此Windows属于混合内核.
我们再讨论一下GNU/Linux这边的情况, Linux仅仅是操作系统的内核, Busybox组成的initrd, Systemd, GNU libC,Xorg等等用户态进程组成了一个系统.
Linux的“官方网站”为The Linux Kernel Archives
Github上有Linus Torvalds先生维护的Linux分支的镜像torvalds/linux
广告一下我维护的Linux 0.11的代码, 可以方便的在现代系统上研究12101111/Linux-0.11
Linux是一个Unix-like(即没有Unix商标,但是兼容Unix的POSIX API)的内核, 当年还是大学生的Linus买到了一台低配版386兼容机,不想用落后的DOS系统,而想用Unix.但当时的Unix是一款昂贵的商业产品.
当时计算机教学使用的系统叫Minix,是Andrew Stuart "Andy" Tanenbaum编写的一个流行微内核Unix-like系统,这个系统随者作者的书《操作系统:设计与实现》分发,但是不是自由软件.
当时另一个流行的Intel 386平台的Unix系统为FreeBSD,不过当时此项目正在和AT&T公司(即Unix所有者)打官司, 而且不兼容没有387协处理器的机器, 因此Linus自己照着Andy的课本和Intel手册写出了Linux, 使用GPLv2协议成为自由软件, 并通过互联网开始协作开发.
Linus还就微内核和宏内核的问题和Andy 论战过: 塔能鲍姆-托瓦兹辩论
Linux不仅如标准的宏内核一样所有功能位于内核空间中, 而且由于GPLv2, 所有驱动程序的源代码(理论上)都需要开源, 因此Linux很少有下载驱动这一操作, 因为所有驱动都是开源的, Linux源码里已经有了.后来Linux具有了模块系统,驱动可以按需加载, 不需要为了删除不需要的驱动而重新编译内核了.
和Windows不同的是, Linux的驱动接口经常变动, 因此即使编译好的内核模块(.ko文件)经常升级Linux版本就不能用了, 因此驱动只能随着主线更新, 如果没有人维护就会从主线中剔除.
部分arm芯片厂商的内核版本就很老,就是因为它们既不想把源代码交给社区维护,也追不上社区升级版本号的速度.
由于Linux并没有用户模式驱动的功能,因此编写差劲的开源nVidia驱动(虽然开发者已经很努力了)和同样编写差劲却又不开源的nVidia闭源驱动经常造成kernel panic(等价于Windows蓝屏)
那么微内核到底是什么一种东西呢.
微内核将传统宏内核中的驱动程序,甚至包括许多功能,比如文件系统, 网络, GUI等等, 都变成用户态的进程(服务),而内核中只保留最重要的功能: 进程管理,内存管理, 进程间通信, 以及硬件抽象层(HAL).
而Linux, 即使是驱动源码独立于内核仓库,具有明显的模块化特征,具有稳定的驱动接口,也不能是微内核,唯一的标准就是用户态的驱动程序和系统服务
传统Unix内核(左)和微内核(右)假如把NT改造为微内核,那么至少需要从内核中删除内核模式驱动程序和GDI/WindowManagerGoogle的Fuchsia OS的 Zircon内核也是微内核, 详见: 聞其詳:许中兴博士演讲:Fuchsia OS 简介完整实录及幻灯片下载
Redox, Rust编写的微内核操作系统, 可以看到各种驱动的服务进程从理论上将, 微内核将驱动独立于内核空间, 大大降低了驱动故障的风险, 提升了安全性, 热更新驱动热重启驱动也变得可能.
微内核的代码大大的简化(少去多半的驱动代码), 维护成本下降, 只要提供一个合理的驱动接口即可.
但有一个重要的问题: 这样做的效率会降低.
从开销上计算, 函数调用<<系统调用<进程间通信(IPC)
函数调用(同一特权级): 压栈,跳转目标地址,弹栈, 返回栈中的返回地址. 函数调用能很好的被CPU的流水线感知.
系统调用(不同特权级): 清空流水线,TLB刷新,保留现场(用户态程序各个寄存器的值)可能的进程调度,转向执行其他进程,复制用户空间里的数据到内核空间(字符串等系统调用参数),处理,将结果复制回用户空间,恢复现场,清空流水线,TLB刷新
进程间通信: 多次系统调用,可能需要多次复制消息体,可能发生进程调度。
最大的问题,就在IPC的性能上.
根据目前的硬件,系统调用时,如果要传递寄存器存不下的东西,比如内存中的一段字符串,那么内核是不能直接读取用户态的内存的(页表不同),内核需要先把用户提供的内存地址翻译到内核中的地址,然后内核分配一块内核使用的内存,将其复制进去,然后处理. 返回数据也类似.
再比如内核处理网卡的数据,并将此数据传给浏览器.
宏内核的过程: 网卡发来中断, CPU执行网卡驱动设置的处理程序,要求网卡使用DMA将数据存到内核中,然后返回.DMA完成后,再次中断,系统的IP子系统分析已经存储的IP数据报,确定是TCP数据,根据端口号唤醒正在等待recv()的浏览器,将数据复制到recv参数里的缓冲区. 整个过程只发生一次数据的复制,两次中断.
那么微内核怎么办呢, 这里以最傻的微内核为例: 网卡发来中断, CPU执行HAL设置的处理程序, HAL确定这个中断应该被/bin/e1000d这个进程处理,于是唤醒/bin/e1000d, 把网卡发来的中断信息发给/bin/e1000d, e1000d将“写网卡内存映射的寄存器.....要求网卡进行DMA..."这种消息发给内核,内核收到之后执行,然后DMA结束之后把内存数据复制给e1000d,e1000d处理之后使用IPC把IP数据报发送给/bin/ipd,ipd处理后把TCP数据发送给浏览器。
实际上发生了多次数据的复制,页表的修改,多次进程调度,效率远低于宏内核。
通过简化代码,虽然能提高IPC的性能,但远不如函数调用快。实际上上述例子中TCP/IP协议栈即使是微内核也不应该放在用户态中。
而进一步的提速,可能需要对现有的内存保护模型进行修改,避免过多的复制, 而硬件的修改,则是比软件的革新难数倍.
原文:https://www.cnblogs.com/jinanxiaolaohu/p/11617578.html