精简的Linux系统概念模型
概念模型
Linux,全称GNU/Linux,是一种免费使用和自由传播的类UNIX操作系统。它主要由以下的多个模块组成:
进程管理
进程类似于人类,他们被产生有或多或少的生命,可以产生一个或多个子进程,最终都要死亡。一个微小的差异是进程之间没有性别差异,每个进程都只有一个父亲。从内核的观点看,进程的目的就是担当分配系统资源(cpu时间、内存等)的实体。当一个进程创建时,它几乎与父进程相同,它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享含有程序代码(正文)的页,但是它们各自有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的(反之亦然)
进程描述符:为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。例如,内核必须知道进程的优先级,它是正在CPU上运行还是因为某些事件而被阻塞,给它分配了什么样的地址空间,允许它访问哪个文件等等。这正是进程描述符的作用。进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。因为进程描述符中存放了那么多信息,所以它是相当复杂的。它不仅包含了很多进程属性的字段,而且一些字段还包括了指向其他数据结构的指针,一次类推,下图表示了Linux的进程描述符。
进程状态:主要分为可运行状态、可中断的等待状态、不可中断的等待状态、暂停状态、跟踪状态、僵死状态和僵死撤销状态。
- 可运行状态:进程要么在CPU上执行,要么准备执行。
- 可中断的等待状态:进程被挂起(睡眠),直到某个条件变为真,产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件(把进程的状态放回到TASK_RUNNING)
- 不可中断的等待状态:与可中断的等待状态类似,但有一个例外,把信号传递到睡眠进程不能改变它的状态,这种状态很少用到, 但是在一些特定条件下,这种状态还是很有用的。
- 暂停状态:进程的执行被暂停。
- 跟踪状态:进程的执行由debugger程序暂停。
- 僵死状态:进程的执行被终止。
- 僵死撤销状态:父进程刚发出wait4()或waitpid()系统调用,因而进程由系统删除。
创建进程:Linux操作系统紧紧依赖进程创建来满足用户的需求。例如,只要用户输入一条命令,shell进程就创建一个新进程,新进程执行shell的另一个拷贝。
撤销进程:进程终止了它们本该执行的代码,从某种角度上来说,这些进程“死”了。当这种情况发生时,必须通知内核以便内核释放进程所拥有的资源,包括内存、打开文件及其他如信号量这些零散的东西。
内存管理
在Linux系统中,整个系统的性能取决于如何有效地去管理内存。因此,现在所有多任务操作系统都在尽力优化对动态内存的使用,也就是说,尽可能做到当用到时分配,不需要时释放。
虚拟地址:为了充分利用和管理系统内存资源,Linux采用虚拟内存管理技术,利用虚拟内存技术让每个进程都有一定容量互不干涉的虚拟地址空间。进程初始化分配和操作的都是基于这个「虚拟地址」,只有当进程需要实际访问内存资源的时候才会建立虚拟地址和物理地址的映射,调入物理内存页。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。
中断和异常
中断:中断是指 CPU 对系统发生某事件时的这样一种响应。CPU 暂停正在执行的程序,在保留现场后自动地转去执行该事件的中断处理程序,执行完后,再返回到原程序的断点处继续执行。
下图 表示中断时 CPU 的活动轨迹。还可进一步把中断分为外中断和内中断。
- 外中断——就是我们指的中断——是指由于外部设备事件所引起的中断,如通常的磁盘中断、打印机中断等。
- 内中断——是指由于 CPU 内部事件所引起的中断,如程序出错(非法指令、地址越界)。内中断(trap)也被译为“捕获”或“陷入”。
- 异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。
- 中断则是由于系统中某事件引起的,该事件与现行指令无关。
异常:即这里的内中断。
引入这两者原因:
- 中断的引入:为了支持CPU与设备之间的并行操作。
当CPU启动设备进行输入/输出后,设备便可以独立工作,CPU转去处理与本次输入/输出不相关的事情;当设备完成输入/输出后,通过向CPU发中断报告此次输入/输出的结果,让CPU决定如何处理以后的事情。
- 异常的引入:表示CPU执行指令时本身出现的问题。
如算术溢出、除零、取数时的奇偶错,访存地址时越界或执行了“陷入指令”等,这时硬件改变了CPU当前的执行流程,转到相应的错误处理程序或异常处理程序或执行系统调用。
文件系统
在LINUX系统中有一个重要的概念:一切都是文件。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。这样带来优势也是显而易见的。
上图所示的体系结构显示了用户空间和内核中与文件系统相关的主要组件之间的关系。VFS 是底层文件系统的主要接口。这个组件导出一组接口,然后将它们抽象到各个文件系统,各个文件系统的行为可能差异很大。有两个针对文件系统对象的缓存(inode 和 dentry)。它们缓存最近使用过的文件系统对象。在这里缓冲区缓存和设备驱动的交互、以及VFS提供的系统接口暂不讨论,主要看看实现这个VFS子系统的主要结构。
I/0体系结构和设备驱动程序
I/0体系结构
为确保计算机能够正常工作,必须提供数据通路,让信息在连接到计算机的CPU、RAM、和I/O设备之间流动,这些数据通路总称为总线,担当计算机内部主通信通道的作用。
所有计算机都拥有一条系统总线,它连接大部分内部硬件设备,一种典型的系统总线是PCI(Peripheral Component Interconnect)总线。目前使用其他类型的总线也很多,例如:ISA、EISA、MCA、SCSI和USB。典型的情况是,一台计算机包括几种不同类型的总线,它们通过被称作"桥"的硬件设备连接在一起。两条高速总线用于在内存芯片上来回传送数据:前端总线将CPU连接到RAM控制器上,而后端总线将CPU连接到外部硬件的高速缓存上。主机上的桥将系统总线和前端总线连接在一起。
任何I/O设备有且仅能连接一条总线。总线的类型影响I/O设备的内部设计,也影响着内核如何处理设备。
CPU和I/O设备之间的数据通路通常称为I/O总线。80x86微处理器使用16位的地址总线对I/O设备进行寻址,而使用8位、16位或32位数据总线传递数据,每个I/O设备依次连接到I/O总线上,这种连接使用了包含3个元素的硬件组织层次:I/O端口、接口和设备控制器。
设备驱动程序
设备驱动程序是一种内核模块,负责管理硬件设备的底层 I/O 操作。设备驱动程序是使用标准接口编写的,内核可通过调用该标准接口与设备进行交互。设备驱动程序也可以是仅针对软件的,即模拟仅存在于软件中的设备,如 RAM 磁盘、总线以及伪终端。
设备驱动程序包含与设备进行通信时所需的所有特定于设备的代码。此代码包括一组用于系统其余部分的标准接口。就像系统调用接口可使应用程序不受平台特定信息影响一样,此接口可保护内核不受设备特定信息的影响。应用程序和内核其余部分需要非常少的特定于设备的代码(如果有)对此设备进行寻址。这样,设备驱动程序使得系统的可移植性更强,并更易于维护。
设备驱动程序按照处理 I/O 的方式可以分为以下三大类别:
- 块设备驱动程序-适用于可将 I/O 数据作为异步块进行处理的情况。通常,块驱动程序用于管理可物理寻址的存储介质的设备,如磁盘。
- 字符设备驱动程序-适用于针对连续的字节流执行 I/O 操作的设备。
- STREAMS 设备驱动程序-字符驱动程序的子集,将 streamio(7I) 例程集用于内核中的字符 I/O。
举例验证
这里以文件读写流程为例。
读流程:
- 应用程序发起读请求,触发系统调用read()函数,用户态切换为内核态。
- 文件系统通过目录项→inode→address_space→页缓存树,查询Page Cache是否存在。
- Page Cache不存在产生缺页中断,CPU向DMA发出控制指令。
- DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的缓冲区(read buffer)。
- DMA 磁盘控制器向 CPU 发出数据读完的信号,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
- 用户进程由内核态切换回用户态,获得文件数据。
写流程:
- 应用程序发起写请求,触发系统调用write()函数,用户态切换为内核态。
- 文件系统通过目录项→inode→address_space→页缓存树,查询Page Cache是否存在,如果不存在则需要创建。
- Page Cache存在后,CPU将数据从用户缓冲区拷贝到内核缓冲区,Page Cache变为脏页(Dirty Page),写流程返回。
- 用户主动触发刷盘或者达到特定条件内核触发刷盘,唤醒pdflush线程将内核缓冲区的数据刷入磁盘。
应用程序与影响应用程序性能的因素
应用程序
这里我们采用类似于压力测试的方法来测试这个主题。
在下面的命令中,我们启动了四个无尽循环。当然也可以通过添加数字或使用 bash 表达式,如 {1...6} 来代替 1 2 3 4 以增加循环次数:
for i in 1 2 3 4; do while : ; do : ; done & done
在命令行上输入后,将在后台启动四个无尽循环:
$ for i in 1 2 3 4; do while : ; do : ; done & done
[1] 205012
[2] 205013
[3] 205014
[4] 205015
在这种情况下,发起了作业 1-4,作业号和进程号会相应显示出来。
对应用程序性能的影响
CPU负载
$ while true; do uptime; sleep 30; done
在输出中,我们可以看到CPU平均负载是如何增加的,然后在循环结束后又开始下降。
11:25:34 up 5 days, 17:27, 2 users, load average: 0.15, 0.14, 0.08
11:26:04 up 5 days, 17:27, 2 users, load average: 0.09, 0.12, 0.08
11:26:34 up 5 days, 17:28, 2 users, load average: 1.42, 0.43, 0.18
11:27:04 up 5 days, 17:28, 2 users, load average: 2.50, 0.79, 0.31
11:27:34 up 5 days, 17:29, 2 users, load average: 3.09, 1.10, 0.43
11:28:04 up 5 days, 17:29, 2 users, load average: 3.45, 1.38, 0.54
11:28:34 up 5 days, 17:30, 2 users, load average: 3.67, 1.63, 0.66
11:29:04 up 5 days, 17:30, 2 users, load average: 3.80, 1.86, 0.76
11:29:34 up 5 days, 17:31, 2 users, load average: 3.88, 2.06, 0.87
11:30:04 up 5 days, 17:31, 2 users, load average: 3.93, 2.25, 0.97
11:30:34 up 5 days, 17:32, 2 users, load average: 3.64, 2.35, 1.04 <== 循环停止
11:31:04 up 5 days, 17:32, 2 users, load average: 2.20, 2.13, 1.01 11:31:34 up 5 days, 17:33, 2 users, load average: 1.40, 1.94, 0.98
因为所显示的负载分别代表了 1、5 和 15 分钟的平均值,所以这些值需要一段时间才能恢复到系统接近正常的状态。
我们可以宏观地看到这里程序运行时CPU的负载增加。
内存负载
编写以下脚本查看内存负载:
$ cat watch-it
#!/bin/bash
while true
do
free
sleep 30
done
运行之后:
$ ./watch-it
13:09:14 up 5 days, 19:10, 2 users, load average: 0.00, 0.00, 0.00
13:09:44 up 5 days, 19:11, 2 users, load average: 0.68, 0.16, 0.05
13:10:14 up 5 days, 19:11, 2 users, load average: 1.20, 0.34, 0.12
13:10:44 up 5 days, 19:12, 2 users, load average: 1.52, 0.50, 0.18
13:11:14 up 5 days, 19:12, 2 users, load average: 1.71, 0.64, 0.24
13:11:44 up 5 days, 19:13, 2 users, load average: 1.83, 0.77, 0.30
可以看到因为是无限循环,因此内存的负载也是一个逐渐上升的过程。
磁盘I/O负载
我们可以使用 iotop 观察受压的 I/O。注意,运行 iotop 需要 root 权限。
之前:
$ sudo iotop -o
Total DISK READ: 0.00 B/s | Total DISK WRITE: 19.36 K/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 27.10 K/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
269308 be/4 root 0.00 B/s 0.00 B/s 0.00 % 1.24 % [kworker~fficient]
283 be/3 root 0.00 B/s 19.36 K/s 0.00 % 0.26 % [jbd2/sda1-8]
之后:
Total DISK READ: 0.00 B/s | Total DISK WRITE: 0.00 B/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
269308 be/4 root 0.00 B/s 0.00 B/s 0.00 % 1.32 % [kworker~fficient]
283 be/3 root 0.00 B/s 19.36 K/s 0.00 % 0.28 % [jbd2/sda1-8]
可以看到,虽然这是一个无限循环程序,但是由于它并没有涉及到多少的IO操作,因此,这里I/O相关基本没什么变化。
总结
通过这门课的学习,我认识到了之前很多不足的点,收获颇丰,主要有以下三点:
- 我明白了学好linux不是一件一蹴而就的事,一定要能坚持使用它,特别是在使用初期,由于在linux中,用户权限很大,做任何事情都很自由,所以,我们往往需要知道做的每一步在干什么,系统做了些什么,这些需要时间去掌握。
- 在学习初期,我们一定会遇到很多困难,或者说各种困难,所以,这就要求我们能够用好搜索工具,很多百度搜不到的,我们要去Google搜,包括stackoverflow这种网站去搜索。
- 学好Linux一定要常常使用,因此,在平时的计算机相关课程中,我们最好直接采用Linux这种操作系统进行平时的编码,这样就会让我们对Linux更加熟悉,把它作为提高生产力的工具。