前言
本文从计算机是如何运行程序开始说起,说明什么是内核空间(kernel space)和用户空间(user space),以及两者区别;
Prerequisite
回到根本——计算机是如何运行程序的
参考我的这篇文章;
我们看到的程序是C、C++、JAVA这类高级语言,需要经过编译被转换为机器能够识别的01机器码,每条机器码都是很简单的指令,比如将两数相加、对两数作比较等,机器码的语言是硬性规定在CPU硬件中的,不可以随意改变。每种CPU(例如非常流行的Intel x86)都有属于自己的机器码,与其他CPU的机器代码不兼容。
程序其实就是一条条的基本指令和数据,静静存储在硬盘中,目前什么都干不了;
当需要运行程序的时候,硬盘中的程序就会被实例化到内存,也就是把程序的指令、数据都放到我们的RAM主存上(更具体的原理在这里就不细究了,属于OS的事情),这样CPU才能执行程序,这段程序也才变为了一个进程(当然也可以是多个进程);
进程与线程在本节开头的文章里有,不过对本文不是重点;
CPU以"取出/执行"的模式运行程序:
- 按顺序取出第一条指令;
- 执行指令;
- 继续取出下一条指令,往复循环;
当然有些指令,比如跳转(if、while就是通过跳转实现的),会影响其执行顺序;
操作系统(OS, Operating System)就像一个监管程序,在计算机启动时(“boots up”)开始运行,让用户浏览和运行程序。OS在后台组织所有程序的运行,给每个程序提供自己的内存区域,使其不干扰其他程序或整个系统(现代计算机基本就是采用前面介绍的虚拟地址空间技术);
虚拟地址空间、虚拟内存
现代处理器采用的是虚拟地址寻址的方式,也就是平时访问的内存地址其实都是虚拟地址,而虚拟地址又指向实际的物理地址,需要由操作系统和CPU硬件翻译成物理地址,才能访问该地址的值;
采用这种方式的原因:
-
如果应用程序可以直接访问物理内存,寻址物理内存的每一个细节,容易破坏操作系统;
-
再者,如果要运行多个进程,理想状态下是各个进程所占物理内存互不干扰,但谁也无法保证程序没有BUG,如果误操作到其他进程,就会导致程序出现运行问题;
当然也是有解决方案的,比如限定进程的内存访问界限,但这样会带来额外的寻址开销,这里不展开;
-
且不管进程间的内存干扰,每个进程都占据一部分内存,就会导致内存不足;
这里同样也有一些解决方案,比如手动覆盖技术、自动交换技术等,当然也同样存在各自的缺陷,这里也不细究;
那么为了解决上述问题,就有了虚拟内存技术,也成为了现代处理器内存寻址的解决方案;
虚拟内存地址空间被分成多个块,每块都有连续的地址空间,可以被分给应用程序;物理空间也同时被分成很多快,这些块大小和虚拟地址空间的块大小一致;系统会自动将虚拟地址空间映射到物理地址空间;进程只需要请求和操作虚拟内存,从每个进程的角度来看,内存中只存在操作系统内核以及本进程,自己独占了整个内存。
虚拟内存技术拥有自动覆盖技术和自动交换技术,使得能够运行比当前空闲内存空间还要大的程序,并借助外存获得更多的空闲内存空间;
再细说一下虚拟内存技术下CPU的寻址方式
倒也和本文不是那么相关,写在这篇博客下面了;
正文
正文算是对这篇文章的总结、简化和补充,有时间可以好好看看原文;
到底什么是内核空间
System memory in Linux can be divided into two distinct regions: kernel space and user space. Kernel space is where the kernel (i.e., the core of the operating system) executes (i.e., runs) and provides its services.”
– Kernel Space Definition, The Linux Information Project 2005.
这段引用也就很好的说明了,内核空间其实就是计算机内存的一部分,内核/用户空间是由操作系统划分的(实际的内存就还是一整块内存,没这种划分);
但是让我们继续深入,本文以UNIX家族来说明;
RAM中预留的空间
Prerequisite中我们已经讲了计算机运行程序的方式,OS将程序的指令加载进RAM,CPU再从RAM取指令……
但如果所有程序都能访问整块内存,包括其他程序和操作系统用到的那部分内存,就会大大提高系统故障率;那么作为监管者的OS就需要避免这种情况,同时优化内存的使用。
确实有技术可以让不同程序共享一块内存,但也不常见,我们不讨论共享内存的情况,要求不同程序间不应当共享内存。但不同程序还是会有一些必须共享的资源,比如显示器、网络连接、硬盘、打印机等,这些资源都会由OS进行保护和管理,这也正是OS的作用之一:管理所有的硬件并控制对他们的访问。这些属于OS的程序,如中断管理、进程调度、内存管理、硬件驱动等,都在RAM中属于OS的那部分,普通的用户进程是无法直接访问的,这部分属于OS的空间也就是所谓的Kernel space。
换句话说:内核空间就是OS划分的主内存的一部分,包含OS内核所有的指令。
注意到User Program1使用的内存有分块,这里简单做一下解释:
boot进程
现在我们有一台计算机,开机的时候都发生了什么?
由于处理器并没有多聪明,不可能自己知道我上电以后应该执行什么,所以必须得给处理器指定上电以后要干的第一件事情,然后逐步唤醒管家OS,点亮机器;
这部分我们先跳过吧
保护内核空间
MMU、虚拟内存空间、分页、页表、分段……这里不细究;总之就是保证了内核空间与用户空间的安全隔离;
访问内核空间
所以用户程序正常情况下是不能访问内核空间的,如果想访问内核空间,执行内核空间的指令,通常的方法就是使用系统调用(system call);
先说中断
处理器连接了很多外设,键盘、网卡、鼠标……可以用轮询的模式,不停的查看哪里有信号,但是就会浪费大量时间去轮询;
所以用的更多的模式是中断模式,每次键盘敲击、网卡收到数据包,都会发起中断,处理器就会保存当前状态到特定寄存器,以便处理完中断以后回来继续执行;随后处理器就会跳转到内核空间特定的内存地址,也就是“陷入内核态”,调用那个位置的异常处理程序,根据中断去做对应的处理。处理完以后就根据方才保存在寄存器中的状态继续做刚才的事情。
中断分硬中断和软中断,暂且理解为硬中断就是键盘、网卡这类硬件发出的电信号,软中断就是软件模拟硬中断的模式发出的中断信号。
系统调用
到这就不难理解系统调用了,我们可以把系统调用理解为一种软中断,当用户进程需要访问内核空间特定的资源时,就需要通过特殊的指令向处理器发起中断;处理器就会将该用户进程提供的信息填充到特定寄存器(太多的话就存到一块内存或者将这些信息入栈,不细究);
发起中断的参数取决于要在内核空间进行的行为,中断处理程序会根据传入的参数,采取适当的行动。
如果该进程的要求不合理的话,这适当的行动也可能是拒绝请求甚至关闭发起中断的进程(给出core dump警告,很熟悉吧)。比如说试图往未分配给该进程的内存地址写数据。
下图举例说明了syscall的整个过程,这其中也有一些混淆的概念。malloc()是C库的一个函数,封装了OS API的一个函数sbrk(),(没错,他俩都不算系统调用),实际的系统调用是封装在sbrk()里的sbrk系统调用,这会发起中断,之后“陷入内核态”,在内核空间执行对应的指令。
结语
自己的学习笔记和理解,难免有错误或补充还请指出,多多交流,感谢!