用户态/内核态

内核态

  1. 内核态,或者说是CPU特权模式,是CPU的一种工作状态,它影响CPU对不同指令的执行结果。操作系统通过跟CPU配合,设置特权模式和用户模式,来防止应用程序进行越权的操作。
  2. 防止应用程序越权访问内存时使用了虚拟地址空间映射的技术,这是操作系统软件配合硬件的MMU共同实现的。在用户模式下,应用程序访问的内存地址是虚拟内存地址,会映射到操作系统指定的物理地址上。这个虚拟内存地址空间就是用户空间。
  3. 应用程序无法自由进入内核态,只能通过操作系统提供的接口调用进入,或者在硬件中断到来时被动进入。
  4. 应用程序通过操作系统功能来使用硬件。

提出/实现内核态/用户态是对计算机/操作系统的一种保护,限制用户态(即用户程序)的操作权限,以保证用户程序不能错误/恶意的使用计算机资源。如果不限制用户程序的权限,用户程序可能会执行一些错误行为,如修改操作系统代码区,在无使用权限下读写某I/O设备。
如果用户程序能够读写所有物理内存和寄存器,就意味着用户程序能够任意修改计算机的状态和其中的程序。所以必须要将执行用户程序和执行操作系统(调用)时做出区别,限制用户程序执行时所享有的权限,即提出了用户态/内核态。

  1. CPU必须至少有两种不同的状态:操作系统状态和应用程序状态。不同状态下,相同指令会产生不同的结果,也就保证某些任务只有操作系统能执行,某些只有应用程序能执行。
  2. 操作系统必须有办法配合CPU,设置哪些内存可以访问,哪些内存不能访问(或者说只有操作系统状态下能访问),不能访问的包括操作系统自己的代码区和数据区、中断向量表等。
  3. 应用程序状态下不能直接访问硬件设备。
  4. CPU在触发中断时需要自动切换到操作系统状态(否则无法进行多任务切换)。
  5. 操作系统状态可以自由切换到应用程序状态;应用程序状态不能任意切换到操作系统状态,但也需要有触发进入操作系统代码并切换到操作系统状态的能力(否则无法调用操作系统功能)。

对于CPU本身来说,CPU是不知道究竟哪一段代码属于应用程序、哪一段代码属于操作系统的,它没有能力识别当前执行的代码究竟应不应该有权限,因此它只负责按照程序逻辑来执行:如果指令自己要求自己进入用户模式,CPU就进入用户模式,但进去之后,就只有特定的方法才能再回到特权模式。所以并不是说进入特权模式就一定是操作系统代码了,CPU并没有这个保证。但是,我们说了,保护模式设计的目标就是为了让应用程序代码受到限制,如果应用程序的代码进入了特权模式,这个限制就完全失效了,所以操作系统设计上会使用各种各样的巧妙手段,配合CPU的功能,保障应用程序只能通过跳转到操作系统代码的方式来切换到内核态上,这样也就间接保障了内核态下执行的都是操作系统(包括驱动)的代码。

既然内存里每一个单元是否允许访问都需要能够设置,而内存的大小是不确定的,那这个设置的数量也不确定,而且会较为庞大,在寸土寸金(?)的CPU里放这么多、这么复杂的设置是很不合适的,唯一可行的方案就是通过内存自己来管理内存——使用一部分内存用来存储其他内存应该如何使用的配置。这样,实际访问内存时,就需要——
先访问内存中的内存配置,根据内存配置判断要访问的内存是否允许访问,如果不允许访问需要触发非法操作的中断,而如果允许访问则正常访问;同时,内存中的内存配置也是内存的一部分,所以内存中的内存配置也会受到内存中的内存配置的管理。

CPU中引入了一种称为MMU的单元,它可能是现代CPU最复杂的组件之一了。它能从内存中以指定格式加载配置,从而影响用户模式下访问内存的特性。为了方便进程切换,这个格式往往有复杂的数据结构,还要支持多种多样的配置功能。在用户模式下,所有内存访问经过MMU,从而对内存的访问受到了保护;在特权模式下,内存访问绕过MMU,直接访问物理内存,从而获得完整的权限。

从具体设计上来说,最直接的想法就是用户模式和特权模式都使用相同的内存地址,只是在用户模式下设置哪些内存可访问,哪些不可访问。这种方法是否可行呢?实际上是可行的,不过略有一些缺陷:

  1. 在保护模式出现之前,编译器都是针对实模式设计的,在编译过程中,使用哪些内存地址范围、内存的什么位置放什么数据,都完全是编译器可以自己决定的。即使是保护模式出现之后,操作系统的部分也需要相同的编译方式。如果应用程序的编译需要放弃这一套逻辑,改成所有地址都由操作系统分配,那现有的汇编程序和编译器都需要重写,这个代价难以接受。
  2. 应用程序经常会需要使用一大片连续的内存空间,比如说涉及数组的一系列算法。如果内存空间全部都是动态分配的,那有些程序可能会不断地申请小块小块的空间,从而让内存空间碎片化,没有连续成片的内存。等这些程序退出之后,释放出来的内存都是小块、不连续的,操作系统就没法让其他应用程序使用连续成片的内存了。
  3. 安全上有隐患,虽然应用程序没法读取其他内存,但是应用程序可以知道哪些内存已经被其他应用程序用了,于是可以从内存地址的分配上分析出一些信息,例如当前操作系统可能执行了哪些其他应用程序,这些应用程序可能处于什么状态等等。还有可能因为CPU实现的bug导致应用程序能以意想不到的方式读取到不应当能读取的数据。
  4. 现代操作系统希望支持一些高级的内存管理方式,例如虚拟内存——将一部分不使用的内存暂时放在磁盘上,这样可以用较少的内存支撑更多的应用程序;写时复制——两个应用程序使用相同的内存块,希望能暂时使用同一个物理内存地址,但是其中一个需要修改的时候再将它复制成两份独立的内存块,从而节约内存。

现代MMU通常使用虚拟地址空间的技术来解决这个问题,也就是你说的“用户空间”。在用户模式下,所有访问内存的地址实际上都是虚拟地址,它与实际的物理地址是对应不上的。这样,即便两个应用程序使用了相同的地址,它们也可以做到互不干扰,只需要通过技术手段让它们实际映射到不同的物理地址就行了。MMU和操作系统通过称作页表的数据结构来实现虚拟地址到物理地址的映射,一般来说在x86-64系统中,内存按照4KB的大小分成页,每个地址对齐的页可以独立从任意一个虚拟地址段,映射到任意一个物理内存地址段,两个起始地址的低12位都是0(也就是所谓地址对齐,这样任意一个虚拟地址映射到物理地址时,最低12位不需要动)。页表的结构在每次进入用户模式之前都可以重新设置,这样切换进程之后,页表发生了变化,同一个虚拟地址就会映射到不同的物理地址上,这就同时实现了多个目标:

  1. 应用程序有独立的虚拟地址空间。
  2. 应用程序只能访问已经映射了的虚拟地址空间,未映射的物理地址无法访问(实现了保护内存)。
  3. 页表和中断向量表,理所当然不会被映射出来。
  4. 部分RISC(x86是CISC)的架构上,内存和外部设备有统一的地址空间,不映射外设的地址,也就阻止了对外设的访问。
  5. 应用程序看来连续的内存,在物理内存上不需要是连续的,内存使用的效率很高。
  6. 以某些方式访问某些页面时可以触发操作系统的中断,操作系统可以趁这个机会修改页表,这就给操作系统实现高级内存管理功能打下了基础。

最后我们来说一下应用程序怎么访问外部设备的问题。我们说了,用户模式下应用程序无法直接访问硬件设备,但如果完全没法利用硬件设备,那就太不方便了。这两者的权衡是,应用程序通过操作系统使用硬件,也就是说应用程序给操作系统发起请求,操作系统处理请求时将请求转发到硬件,硬件响应后,再将请求转发回应用程序。

许多硬件使用中断和DMA来传输信号或数据。这种情况下,操作系统开始操作后,到硬件操作完成前会有一段空闲时间,这时候操作系统可以将当前应用程序挂起,先去执行其他的应用程序。当硬件操作完成时,会触发中断,中断向量表在内存中,是操作系统提前设置好的,指向了操作系统自己的代码;同时,这个中断也会立即强迫CPU进入特权模式。这时候操作系统就有机会来处理硬件返回的数据了,同时根据进程优先级,可以将之前挂起的进程重新切换回来重新开始继续执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值