每个Linux开发者都应该知道的一些知识

时间:2022-03-13 13:36:57

如何学习Linux

  作为现代操作系统的代表之一,Linux操作系统非常复杂,内部有多得令人眼花缭乱的各种组件在同步运行和相互通信。对于初学者来说,我认为理解操作系统工作原理最好的方法是利用抽象的思维去理解,也就是说,你可以暂时忽略大部分细节。就像坐车一样,通常你不会去在意车内固定发动机的装配螺栓,也不会关心你走的路是谁修筑的。如果你是一名乘客,可能只会关心如何打开或关闭车门、如何系好安全带以及车要把你带到哪儿去。如果你是一名司机,就需要了解更多的细节,比如如何控制油门、刹车和换挡,以及如何处理意外情况。如果你是一名维修工程师或汽车设计师,则需要更深入地了解汽车构造及其工作原理了。

每个Linux开发者都应该知道的一些知识

  我们试一下运用“抽象思维”来理解开车这件事情,首先我们可以将“一辆汽车在路上行驶”抽象为三个部分:汽车、道路和驾驶操作。这样一来,开车这件事情就简单多了,我们几乎只需要知道驾驶操作即可。如果道路颠簸,也不会去埋怨汽车本身和自己的驾驶技术,反而我们会问这条路为什么这么烂,而我们是不是一定要走这条路。同样,在软件开发过程中,开发人员通常不用太关心他们需要使用的组件的内部结构,他们只关心能使用哪些组件,以及这些组件该怎么用。跟汽车零部件一样,每一个组件都可能包含着复杂的技术细节,但我们可以暂时忽略这些细节,而专注于这些组件在系统中发挥的功能。实际上,抽象思维形成的这种“分层思想”无论在计算机技术还是其他社会生产活动中都是适用的。

Linux操作系统的层次

  下面我们来看一下,通过抽象可以将系统分解为哪些组件,以及这些组件在用户和硬件系统之间所处的位置。
  简单来说,Linux操作系统可以大体分为三层,如下图所示,最底层是硬件系统,包括CPU、内存、硬盘、网卡等;硬件系统之上是内核,这是操作系统的核心,内核负责管理硬件系统,同时为应用程序提供操作接口;用户进程在这里表示计算机中运行的所有程序,它们运行于用户空间,由内核统一管理。

每个Linux开发者都应该知道的一些知识

  内核和用户进程之间最大的区别在于:内核运行于内核模式(kernel mode,也称内核态),用户进程运行于用户模式(user mode,也称用户态)。在内核模式中运行的代码可以不受限制地访问*处理器和内存,也就是说内核可以为所欲为,那这就非常危险了,因为内核进程可以轻而易举地使整个系统崩溃。所以为了提高系统稳定性,限制进程对*处理器和内存的访问权限,提出了用户模式的概念。
  一般我们将只有内核可以访问的空间称为内核空间,而将用户进程能够访问的空间称为用户空间。通过这种限制,即使某个用户进程运行时崩溃了,也不会对整个系统造成严重的影响。

内核模式和用户模式

  实际上,内核模式和用户模式是需要处理器支持的。内核程序和用户程序的本质区别在于:除了可以执行大部分通用指令,内核程序还可以执行特权指令。说到计算机指令,就不得不提到RISC(Reduced Instruction Set Computer,精简指令集)和CISC(Complex Instruction Set Computer,复杂指令集),我们知道Intel的x86架构芯片采用的是CISC,而ARM架构芯片则采用RISC。也就是说,内核模式和用户模式之间的切换以及模式的实现依托于CPU指令集架构。
  Intel的x86处理器通过Ring级别来进行访问控制,共分为4个级别,即Ring0~Ring3。Ring0层拥有的权限最高,Ring3层拥有的权限最低。按照Intel原来的设想,应用程序工作在Ring3层,只能访问Ring3层的空间;操作系统工作在Ring0层,可以访问所有层的空间;而其他驱动程序工作与Ring1和Ring2层,每一层只能访问本层和权限更低层的数据。这种设计可以有效保障操作系统的稳定性和安全性。但是现代操作系统,包括Windows和Linux都没有采用4层权限,只使用了Ring0和Ring3层,对应于内核空间和用户空间。因此,驱动一旦加载,就运行在Ring0层,拥有与操作系统内核一样的权限。
  和x86架构不同,ARM没有Ring0~Ring3,也不存在Root模式和非Root模式。众所周知,ARM有7种工作模式,即usr(用户模式,User)、fiq(快速中断模式,FIQ)、irq(外部中断模式,IRQ)、svc(管理模式,Supervisor)、abt(数据访问中止模式,Abort)、und(未定义指令中止模式,Undef)和sys(系统模式,System)。除了用户模式以外的其他6种处理器模式都称为特权模式(Privileged Modes)。在特权模式下,程序可以访问所有的系统资源,也可以任意地进行处理器模式切换。
  除此之外,还有在ARM v6中引入的Security Extensions带来的Monitor模式,以及在ARM v7中引入的Virtualization Extensions带来的Hyp模式。对于ARM v8架构则更为复杂一些,它定义了两种执行状态(Execution State),分别是AArch32状态和 AArch64状态。同时定义了4个异常等级(Exception Level)来进行权限控制,分别是EL0~EL3。对于AArch32,ARMv8定义了9种PE模式(也就是上面提到的9种工作模式)来确定执行权限,而不使用EL;而对于AArch64,则不支持PE模式。(更多关于处理器架构的信息,请查阅相关手册)。

内存的作用

  除了CPU,内存可以说是是硬件系统中最为重要的部分。内存中存储的是0或1这样的比特数据,内核和进程也都是运行在内存里面的,它们在内存中就是一系列的比特数据集合,所有外围设备的数据输入和输出都通过内存完成。而CPU就像一个操作员一样处理内存中的数据,它从内存读取指令和数据,然后将运算结果写回内存。Linux内核几乎所有的操作都和内存有关,例如:将内存划分为很多区块,并且一直维护着这些区块的状态信息;每一个进程拥有自己的内存区块,并且由内核保证每个进程只使用它自己的内存区块。

Linux内核

  Linux内核采用的是整体式结构(Monolithic),整个内核是一个单独的、非常大的程序,这样虽然能够使系统的各个部分直接沟通,提高系统相应速度,但与嵌入式系统存储容量小、资源有限的特点不相符合。因此,在嵌入式系统中经常采用的是另一种称为微内核(Microkernel)的体系结构,即内核本身只提供一些最基本的操作系统功能,如任务调度、内存管理、中断处理等,而类似于文件系统和网络协议等附加功能则运行在用户空间中,并且可以根据实际需要进行取舍。这样可以大大减小内核的体积,便于维护和移植。

每个Linux开发者都应该知道的一些知识

  对于Linux这样一个宏内核操作系统来说,一个完整的Linux内核主要由五个子系统组成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。

  • 进程调度(SCHED)控制进程对CPU的访问。当需要选择下一个进程运行时,由调度程序选择最值得运行的进程。可运行进程实际上是仅等待CPU资源的进程,如果某个进程在等待其它资源,则该进程是不可运行进程。Linux使用了比较简单的基于优先级的进程调度算法选择新的进程。
  • 内存管理(MM)允许多个进程安全的共享主内存区域。Linux 的内存管理支持虚拟内存,即在计算机中运行的程序,其代码,数据,堆栈的总量可以超过实际内存的大小,操作系统只是把当前使用的程序块保留在内存中,其余的程序块则保留在磁盘中。必要时,操作系统负责在磁盘和内存间交换程序块。内存管理从逻辑上分为硬件无关部分和硬件有关部分。硬件无关部分提供了进程的映射和逻辑内存的对换;硬件相关的部分为内存管理硬件提供了虚拟接口。
  • 虚拟文件系统(Virtual File System,VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口,VFS提供了多达数十种不同的文件系统。虚拟文件系统可以分为逻辑文件系统和设备驱动程序。逻辑文件系统指Linux所支持的文件系统,如ext2,fat等,设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。
  • 网络接口(NET)提供了对各种网络标准的存取和各种网络硬件的支持。网络接口可分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。网络设备驱动程序负责与硬件设备通讯,每一种可能的硬件设备都有相应的设备驱动程序。
  • 进程间通讯(IPC)支持进程间各种通信机制。进程间通信主要用于控制不同进程之间在用户空间的同步、数据共享和交换。由于不用的用户进程拥有不同的进程空间,因此进程间的通信要借助于内核的中转来实现。一般情况下,当一个进程等待硬件操作完成时,会被挂起;当硬件操作完成,进程被恢复执行,而协调这个过程的就是进程间的通信机制。

  Linux内核子系统的结构如下图所示,处于中心位置的进程调度,所有其它的子系统都依赖它,因为每个子系统都需要挂起或恢复进程。一般情况下,当一个进程等待硬件操作完成时,它被挂起;当操作真正完成时,进程被恢复执行。例如,当一个进程通过网络发送一条消息时,网络接口需要挂起发送进程,直到硬件成功成功地完成消息的发送,当消息被成功的发送出去以后,网络接口给进程返回一个代码,表示操作的成功或失败。其他子系统以相似的理由依赖于进程调度。

每个Linux开发者都应该知道的一些知识

  各个子系统之间的依赖关系如下:
  进程调度与内存管理之间的关系:这两个子系统互相依赖。在多道程序环境下,程序要运行必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。
  进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间,还可以存取共同的内存区域。
  虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持RAMDISK设备。
  内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程(swapd)定期由调度程序调度,这也是内存管理依赖于进程调度的唯一原因。当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,同时,挂起当前正在运行的进程。
  除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的过程。例如:分配和释放内存空间的过程,打印警告或错误信息的过程,还有系统的调试例程等等。

内核管理的四个方面

  Linux内核负责管理以下四个方面:

  • 进程:内核决定哪个进程可以使用CPU。
  • 内存:内核管理所有的内存,为进程分配内存,管理进程间的共享内存以及空闲内存。
  • 设备驱动程序:作为硬件系统(如磁盘)和进程之间的接口,内核负责操控硬件设备。
  • 系统调用和支持:进程通常使用系统调用和内核进行通信。

进程管理

  进程管理设计进程的启动、暂停、恢复和终止。启动和终止比较直观,但是要解释清楚进程在执行过程中如何使用CPU则相对复杂一些。
  在现代操作系统中,很多进程貌似都是同时运行的。例如,你可以同时在桌面打开Web浏览器和电子表格应用程序。然而,虽然它们表面上看是同时运行,但实际上这些应用程序背后的进程并不完全是同时运行的。
  我们设想一下,在只有一个CPU的计算机系统中,可能会有很多进程可以使用CPU,但是在任何一个特定的时间段内只能有一个进程可以使用CPU。所以实际上是多个进程轮流使用CPU,每个进程使用一段时间后就暂停,然后让另一个进程使用,依次轮流,时间单位是毫秒级。一个进程让出CPU使用权给另一个进程称为上下文切换(context switch)。
  进程在其时间段内有足够的时间完成主要的计算工作(实际上,进程通常在单个时间段内就能完成它的工作)。由于时间段非常短,短到我们根本察觉不到,所以在我们看来,系统是在同时运行多个进程(我们称之为多任务执行)。
  内核负责上下文切换。我们来看看下面的场景,以便理解它的工作原理。
  (1)CPU为每个进程计时,到时即停止进程,并切换至内核模式,由内核接管CPU控制权。
  (2)内核记录下当前CPU和内存的状态信息,这些信息在恢复被停止的进程时需要用到。
  (3)内核执行上一个时间段内的任务(如从输入输出设备获得数据,磁盘读写操作等)。
  (4)内核准备执行下一个进程,从准备就绪的进程中选择一个执行。
  (5)内核为新进程准备CPU和内存。
  (6)内核将新进程执行的时间段通知CPU。
  (7)内核将CPU切换至用户模式,将CPU控制权交给新进程。
  上下文切换回答了一个十分重要的问题,即内核是在什么时候运行的。答案就是:内核是在上下文切换时的时间段间隙中运行的
  在多CPU系统中,情况要稍微复杂一些。如果新进程将在另一个CPU上运行,内核就不需要让出当前CPU的使用权。不过为了将所有CPU的使用效率最大化,内核会使用一些其他的方式来获取CPU控制权。

内存管理

  内核在上下文切换过程中管理内存,这是一项十分复杂的工作,因为内核要保证以下所有条件:
  (1)内核需要自己的专有内存空间,其他的用户进程无法访问;
  (2)每个用户进程有自己的专有内存空间;
  (3)一个进程不能访问另一个进程的专有内存空间;
  (4)用户进程之间可以共享内存;
  (5)用户进程的某些内存空间可以是只读的;
  (6)通过使用磁盘交换,系统可以使用比实际内存容量更多的内存空间。
  新型的CPU提供MMU(Memory Management Unit,内存管理单元),MMU使用了一种叫作虚拟内存的内存访问机制,即进程不是直接访问内存的实际物理地址,而是通过内核使得进程看起来可以使用整个系统的内存。当进程访问内存的时候,MMU截获访问请求,然后通过内存映射表(或称为内存页面表,page table)将要访问的内存地址转换为实际的物理地址。内核需要初始化、维护和更新这个地址映射表。例如,在上下文切换时,内核将内存映射表从被移出进程转给被移入进程使用。

设备驱动程序和设备管理

  对于设备来说,内核的角色比较简单。通常设备只能在内核模式中被访问(例如用户进程请求内核关闭系统电源),因为设备访问不当有可能会让系统崩溃。另一个原因是不同设备之间没有一个统一的编程接口,即使同类设备也是如此(比如两个不同的网卡)。所以设备驱动程序传统意义上来说是内核的一部分,它们尽可能为用户进程提供统一的接口,以简化开发人员的工作。

系统调用和系统支持

  内核还对用户进程提供其他功能。例如,系统调用(system call或syscall)为进程执行一些它们不擅长或无法完成的工作。打开、读取和写文件这些操作都涉及系统调用。
  fork()和exec()这两个系统调用对于我们了解进程如何启动很重要。
  (1)fork():当进程调用fork()时,内核创建一个和该进程几乎一模一样的副本。
  (2)exec():当进程调用exec(program)时,内核启动program来替换当前的进程。
  除了init以外,Linux中的所有用户进程都是通过fork()来启动的。除了创建现有进程的副本以外,大多数情况下你还可以使用exec()来启动新的进程。一个简单的例子是你在命令行运行ls命令来显示目录内容。当你在终端窗口中输入ls时,终端窗口中的shell调用fork()创建一个shell的副本,然后该副本调用exec(ls)来运行ls。
  除了传统的系统调用,内核还为用户进程提供其他很多功能,最为常见的是虚拟设备。虚拟设备对于用户进程而言是物理设备,但其实它们都是通过软件实现的。因此从技术角度来说,它们并不需要存在于内核中,但是实际上它们很多都存在于内核中。例如:内核的随机数生成器(/dev/random)这样的虚拟设备,如果由用户进程来实现,难度要大很多。

用户空间

  前面提到过,内核分配给用户进程的内存我们称之为用户空间。因为一个进程简单来说就是内存中的一个状态。(用户空间也可以指所有用户进程占用的所有内存)
  Linux中大部分的操作都发生在用户空间中。虽然从内核的角度来说所有进程都是一样的,但是实际上它们执行的是不同的任务。相对于系统组件,用户进程位于一个基础服务层中。底层的基础服务层中提供了上层应用程序所需的工具服务(也称为中间件),比如邮件、打印和数据库服务。顶层组件则可专注于完成用户交互和复杂的功能。当然,组件之间也是可以相互调用的。
  虽然这里提到底层、顶层、中间层等概念,但实际上它们在用户空间里并没有明显的界限。其实很多用户空间的组件也比较难分类,比如Web服务器和数据库,你可以把它们归为上层组件,也可以归为中间层组件。
  另外,从技术上来说,用户进程还是需要通过使用系统调用打开设备的方式来访问虚拟设备,所以进程总是避免不了要和系统调用打交道。