Linux 操作系统原理 — 内核态与用户态

目录

文章目录

  • 目录
  • Linux 的内核态与用户态
    • 系统调用
  • Shell
  • 用户态和内核态的切换
  • 进程的用户空间和内核空间的内存布局
    • 内核空间
    • 用户空间

Linux 的内核态与用户态

我们常说的 Linux 严格来说指代的是 Linux Kernel,泛指使用或裁剪标准 Linux Kernel 并在此基础之上实现各种应用程序解决方案的操作系统发行版本(e.g. RHEL、SUSE 和 Ubuntu)。一个完整的 Linux 操作系统体系架构通常由下列几个核心层级组成:

  • Applications:在操作系统上安装并运行的用户态应用程序
  • Shell:支持编程的命令行解析器
  • Libs:操作系统标准库函数
  • System Calls:暴露给用户态的内核态系统调用接口
  • Kernel:操作系统的核心,真正对接硬件平台的软件程序

Linux 操作系统原理 — 内核态与用户态_第1张图片

Linux Kernel 本质上看是一种软件,实现了进程管理器、内存管理器、文件系统、设备驱动以及网络管理组件来负责对接、管理计算机硬件平台,并通过系统调用(System Calls)为上层应用程序暴露硬件资源以提供程序运行环境。

以系统调用为边界将 Linux 操作系统的体系架构分为用户态和内核态(包括系统调用)。

操作系统的用户态和内核态实际上对应了 CPU 指令集中的非特权指令和特权指令的执行状态,CPU 划分了不同的执行级别来执行具有相应特权的指令,例如:Intel x86 CPU 具有四种不同的执行级别 [RING0, RING1, RING2, RING3],Linux 操作系统只使用了其中的 RING0 和 RING3 分别表示内核态与用户态。处于 RING3 状态的用户态代码不能直接访问处于 RING0 的内核态代码的地址空间(包括代码和数据)。

Linux 操作系统原理 — 内核态与用户态_第2张图片

我们知道有些 CPU 特权指令的操作实际是比较危险的,比如:写入系统配置文件、杀掉其他用户的进程或重启系统。所以在操作系统的设计中,为了保障操作系统的稳定性,尤其是在多用户环境中的可靠性,操作系统根据 CPU 的指令类型来抽象并实现了用户态和内核态两种代码运行模式,两种运行模式之间的切换也成为模式切换。用户态的代码被限制了可以执行的操作以及可以访问的资源范围,而内核态的代码则可以执行任何操作并且没有资源使用上的限制。

所以,**为什么要划分核心态和用户态?**简单来说:

  • 禁止用户程序和底层硬件平台直接交互
  • 禁止用户程序直接访问任意内存地址空间

Linux 进程拥有 4GB 内存地址空间,其中 3-4G 部分是内核态的地址空间,存放了整个内核的代码,所有内核模块以及内核所维护的数据。随便多说一句,这就是所谓的操作系统副本,无论是 SMP 还是 NUMA 实现的都是「单操作系统与数据库系统副本」,而 MPP 海量并行处理体系结构实现的是多操作系统与数据库系统副本,但 MPP 一般只常见于大型机。运行在 RING3 的用户程序代码可以通过系统调用主动访问 RING0 的内核代码来实现从用户态带内核态的切换。当进程陷入内核态时,被执行的内核代码会直接使用进程的内核栈资源。

例如:用户运行一个程序,该程序创建的进程开始运行在用户态,如果程序要执行诸如文件操作,网络数据发送操作等内核态操作的话,就必须通过系统调用中的 Write,Send 等功能单元完成,根本是通过调用内核代码完成的。此时,运行该进程的处理器会从 RING3 切换到 RING0 级别,然后进入 3-4GB 内核地址空间中完成内核代码的执行。执行完成后,处理器再从 RING0 切换回 RING3,进程也回到用户态。

Linux 操作系统原理 — 内核态与用户态_第3张图片

用户态的应用程序可以通过三种方式来访问内核态的资源:

  • 系统调用
  • 库函数
  • Shell 脚本

Linux 操作系统原理 — 内核态与用户态_第4张图片

系统调用

Linux 操作系统原理 — 内核态与用户态_第5张图片
系统调用是操作系统的最小功能单位,具有原子性,这些系统调用根据不同的应用场景可以进行扩展和裁剪,现在各种版本的 Unix 实现都提供了不同数量的系统调用,如 Linux 的不同版本提供了 240-260 个系统调用,FreeBSD 大约提供了 320 个(reference:UNIX 环境高级编程)。

库函数正是为了将程序员从复杂的细节中解脱出来而提出的一种有效方法。它实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如 ISO C 标准库,POSIX 标准库等。

Shell

Shell 是一个特殊的应用程序,俗称命令行,本质上是一个命令解释器,它下通系统调用,上通各种应用,通常充当着一种“胶水”的角色,来连接各个小功能程序,让不同程序能够以一个清晰的接口协同工作,从而增强各个程序的功能。

同时,Shell 是可编程的,它可以执行符合 Shell 语法的文本,这样的文本称为 Shell 脚本,通常短短的几行 Shell 脚本就可以实现一个非常大的功能,原因就是这些 Shell 语句通常都对系统调用做了一层封装。为了方便用户和系统交互,一般的,一个 Shell 对应一个终端,终端是一个硬件设备,呈现给用户的是一个图形化窗口。我们可以通过这个窗口输入或者输出文本。这个文本直接传递给 Shell 进行分析解释,然后执行。

用户态和内核态的切换

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。

所以,为了减少有限资源的访问和使用冲突,Unix/Linux 的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel 的 X86 架构的 CPU 提供了 0 到 3 四个特权级,数字越小,特权越高。

Linux 操作系统中主要采用了 0 和 3 两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。

很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数 malloc(),它具体是使用 sbrk() 系统调用来分配内存,当malloc() 调用 sbrk() 的时候就涉及一次从用户态到内核态的切换,类似的函数还有 printf(),调用的是 wirte() 系统调用来输出字符串,等等。

用户程序除了通过系统调用主动触发模式切换之外,还可能会被动的进行。总的来说模式切换有两种触发手段:

  • (软中断)系统调用:这时用户态进程要传递很多变量或参数值给内核,内核态运行时也要保存用户进程的一些寄存器值和变量等等。所谓的「进程上下文」,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值以及运行环境等。

  • (硬中断)外围设备中断:硬件可以通过触发中断信号令内核调用中断处理程序从而进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的「中断上下文」,就是硬件传递过来的这些参数和内核需要保存的当前被中断执行的进程环境。

由此可见,处理器总处于以下状态中的一种:

  1. 运行进程上下文的内核态,内核代表进程运行在内核地址空间
  2. 运行中断上下文的内核态,内核代表硬件运行在内核地址空间
  3. 用户态,运行在用户地址空间

Linux 操作系统原理 — 内核态与用户态_第6张图片

发生从用户态到内核态的切换,一般存在以下三种情况:

  1. 系统调用。
  2. 异常事件: 当 CPU 正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
  3. 外围设备的中断:当外围设备完成用户的请求操作后,会向 CPU 发出中断信号,此时,CPU 就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。

注意:系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断,这是操作系统为用户特别开放的一种中断,如 Linux int 80h 中断。所以,从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。

进程的用户空间和内核空间的内存布局

Linux 操作系统原理 — 内核态与用户态_第7张图片

内核空间

内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在该区域进行读写或直接调用内核代码定义的函数的。

上图左侧区域为内核进程对应的虚拟内存,按访问权限可以分为进程私有和进程共享两块区域:

  1. 进程私有的虚拟内存:每个进程都有单独的内核栈、页表、task 结构以及 mem_map 结构等。
  2. 进程共享的虚拟内存:属于所有进程共享的内存区域,包括物理存储器、内核数据和内核代码区域。

用户空间

每个普通的用户进程都有一个单独的用户空间,处于用户态的进程不能访问内核空间中的数据,也不能直接调用内核函数的 ,因此要进行系统调用的时候,就要将进程切换到内核态才行。

用户空间包括以下几个内存区域:

  • 运行时栈:由编译器自动释放,存放函数的参数值,局部变量和方法返回值等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存储到栈顶,调用结束后调用信息会被弹出并释放掉内存。栈区是从高地址位向低地址位增长的,是一块连续的内在区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
  • 运行时堆:用于存放进程运行中被动态分配的内存段,位于 BSS 和栈中间的地址位。由卡发人员申请分配(malloc)和释放(free)。堆是从低地址位向高地址位增长,采用链式存储结构。频繁地 malloc/free 造成内存空间的不连续,产生大量碎片。当申请堆空间时,库函数按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
  • 代码段:存放 CPU 可以执行的机器指令,该部分内存只能读不能写。通常代码区是共享的,即其他执行程序可调用它。假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。
  • 未初始化的数据段:存放未初始化的全局变量,BSS 的数据在程序开始执行之前被初始化为 0 或 NULL。
  • 已初始化的数据段:存放已初始化的全局变量,包括静态全局变量、静态局部变量以及常量。
  • 内存映射区域:例如将动态库,共享内存等虚拟空间的内存映射到物理空间的内存,一般是 mmap 函数所分配的虚拟内存空间。

你可能感兴趣的:(Linux,操作系统原理)