当谈到系统调用(system call)时,我们首先映入脑海的差不多就是软中断、内核态、用户态。开宗明义第一章,我想让大家先要重新认识一下『系统调用』这个词。
其实系统调用这个词有两种理解,有些资料把open、read、write、fork等man手册第二页的函数称之为系统调用,其实这个只是真正系统调用的封装函数(wrapper functions),我姑且称之为广义上的系统调用。每个系统调用的封装函数都会通过软中断陷入内核态然后调用对应的真正的系统调用。且一般会一一对应。比如fork函数内部会调用sys_fork。而后者其实才是真正的准确意义上的系统调用,由内核提供的服务,姑且称之为狭义的系统调用。
系统调用的封装函数其实是glibc实现的,而真正的系统调用是内核中的实现。看到这里你可能有点凌乱了。来,让我们看一下epoll的man手册,末尾有一个版本声明:
VERSIONS
The epoll API was introduced in Linux kernel 2.5.44. Support was added to glibc in version 2.3.2.
翻译过来就是Linux内核的2.5.44版本引入了epoll,被glibc支持是在glibc的2.3.2版本。
这里特地提了一下glibc的版本,想表达的是即使你的Linux内核版本支持了epoll系统调用,但是你的glibc的版本不够,你还是不能直接使用epoll开头的那几个函数的。那么为什么要这么麻烦呢,需要glibc封装一层?其实这主要是因为系统调用是实际调用的时候,涉及到一些汇编指令(下文会介绍)
说到这,即便我说清了系统调用一词的两种理解,却依旧忽略了一点,那就是:(广义上的)系统调用的具体实现是和内核架构相关的。我上面描述的过程基本上就是基于Linux阐述的,然而纵观整个Unix-like的操作系统家族,其实还有不一样的声音。说到这就不得不提单内核与微内核之争了。
争还是吵?单内核与微内核的论战
1992年爆发过『单内核』与『微内核』之争!挑起这次争论的是国外的谭教授——谭宁邦(Tanenbaum)。争论的另一方是Linux的发明者林纳斯(Linus)。彼时林纳斯还是个初出茅庐的小伙子,一年之前他曾在校园网上发布了Linux内核。
单内核即Monokernel(全称:Monolithic kernel)。也被译作宏内核,个人感觉宏内核一词可能让人不明就里,以下都称单内核。Linux正基于单内核,这是比较古老的内核架构。内核提供多项服务,我们常用的文件IO、内存管理、网络相关的系统调用全部运行在内核态,都运行在同一地址空间之中。
老谭直斥Linux采用单内核是在开历史倒车!OS技术倒退回七十年代。此前老谭也曾开源一个类Unix的OS,名曰MINIX(表示Mini Unix)。而MINIX则基于微内核。
微内核即Microkernel(简写:μ-kernel)提出时间比单内核要晚,在学术界而言无疑是初生的朝阳。微内核基于模块化的设计,将内核功能简化到最少,仅提供少量基础功能,更多的功能运行在用户态,不同服务运行在不同的地址空间,常用的服务(比如IO、内存管理)通过IPC调用来组合提供。无疑从这个层面上讲微内核的扩展性更强,增加新功能无需重新编译内核。并且由于内核服务间的隔离,使得OS更安全,一个服务挂掉,不会影响其他服务。而单内核中一个服务的异常可能让整个内核挂掉。但问题也显而易见,那就是大量的IPC,性能必然受影响。
微内核的思想其实和后来大型分布式系统中SOA、微服务的概念不谋而合。然而历史却并不相似,站在二十一世纪的第三个十年回望,Linux成功空前。时至今日,不管是MINIX还是其他,都鲜有扛起微内核大旗的OS被广泛使用(去年华为鸿蒙高调宣布采用单微内核架构,能走多远我们拭目以待吧)。当然Linux的成功与采用单内核架构其实并无强关联,Linux的成功主要归功于其出色的开源社区运营模式(观点出自《大教堂与集市》)。
老谭和小林的这场论战在上个世纪90年代引起轩然大波,两个阵营各有多位技术大牛出面战队,时至今日关于这场论战的记录,也时常见诸报端。论战持续时间很长,从技术本身的争辩到后来双方互相呛声,多年以后二人也曾公开表达和解,表达双方只是技术之争,不涉及任何私人攻击。
在论战之前这一老一小,其实也并非没有交集,老谭曾出版过讲解Unix以及操作系统的书籍,并随书附赠了MINIX的源码。小林在发明Linux之前,确有通过这本书和MINIX的代码进行学习操作系统的知识。所以某种意义上说,老谭算得上是小林的半个老师。
这场论战距离今天将近三十年了,是争是吵,早已难以分清,也无需纠结。这里不再展开具体细节,各位感兴趣可以很容易在互联网上找到当年的蛛丝马迹。
内核态、用户态与CPU的特权等级
Intel x86 CPU的架构,对于所运行执行的指令,划分了4个不同特权等级:ring 0、ring 1、ring 2、ring 3,通常被称为保护环(protection ring)。从ring0到ring3,特权级别依次降低。Linux使用了ring0和ring3两个特权等级。运行在ring0的程序被称之为内核态(Kernel Space)程序,运行在ring3等级的程序被称为用户态(User
Space)程序。
图片来自网络
系统调用与软中断
好了,我们已经大概知道了什么是用户态,什么是内核态。那么这和系统调用又有什么关系呢?
请看下一张图:
图片来自网络
可以发现,在用户态和内核态的边界上画着线表示的就是系统调用!也就是说不管是单内核还是微内核,运行在用户态的应用程序,想使用某些内核态才能执行的功能,必须要经过系统调用来实现。所以你需要明白:进程从用户态陷入了内核态,这是目的,而使用系统调用,仅仅是达成该目的的手段。因果要理清。
再来解释一下什么是软中断。要说软中断,先说一下中断(interrupt),如果你大学的时候有听过《计算机组成与体系结构》这门课的话,那么你应该会记得。中断本身是一个硬件概念,就是打断CPU,让其执行一下其他任务,比如键盘中断、打印机中断、定时器中断等。软中断本就是从软件层面模拟了这一中断操作。
网上很多资料大多会提到使用128号软中断指令(int $0x80)来使进程从用户态陷入内核态,执行完毕后调用iret指令重回用户态。但其实这是比较传统、比较老的做法。后来随着CPU(Intel和AMD)的升级,Linux内核一般会使用快速系统调用(Fast system calls)的sysenter/syscall指令来代替int $0x80,使用sysexit/sysret代替iret。在运行软中断指令的时候,会用一个寄存器来存储具体的系统调用号,比如在Linux上read和write的系统调用号分别为0和1。
单内核与微内核上的系统调用有什么不同呢?
单就系统调用的实现原理来说,没有不同。所谓的差异其实是体现在系统调用的封装函数的种类上!前面谈到,单内核内核提供许多服务。以Linux为代表,其系统调用十分繁多,有三百多种。可以查看:filippo.io/linux-syscal
而微内核OS,则没有这么多系统调用。比如MINIX其实只有三个系统调用(的封装函数):
- 发送:_send()
- 接收:_receive()
- 发收一体:_sendrec()
(基于老版本MINIX,最新的MINIX不清楚是否扩展了)
这当然不是说MINIX就不支持open()、read()、write()和fork()了。只不过是在MINIX系统上它们都是本质都是通过_send()、_receive()、_sendrec() 实现的。所以准确来讲MINIX上和Linux的open()、read()、write()、fork()这些系统调用封装函数对标的是_send()、_receive()、_sendrec()。
信息过多,难免有遗漏或讹误,欢迎大家批评指正!