1.精简的Linux系统概念硬件
我们假设我们的系统只有CPU、内存、硬盘、IO设备这四类设备,主要的设计与分析也是在这四类硬件之间的交互调度。并且,对于硬盘具体的分区、格式化、寻道过程不做分析;对IO设备的读写默认为中断的方式,不涉及DMA交互,默认可以一次性从IO设备缓存中读取所有的数据;对CPU只分析涉及进程调度的寄存器,不考虑cache。对于其他计算机硬件比如总线系统,不做分析。
我们从进程管理、内存管理和文件系统三个方面分析模型。
首先,我们需要明确这四类设备是如何连接起来的,也就是它们之间谁与谁是直接交互,谁与谁是间接交互,具体模型如下图:
CPU、IO、内存和硬盘通过总线系统交互,总线系统的具体细节不做分析。大部分应用程序都是储存在硬盘中,只有少部分引导程序是固化在内存中完成开机动作。操作系统完成启动后,将应用程序加载到内存中,并且开始进程的调度轮转。同时,IO设备也在异步工作,随时发起中断通知CPU接收数据。
(1)进程的描述与创建
Linux中,用于描述进程的结构体为task_struct,本文使用的是Linux5.8.1版本的源码,结构体部分内容如下图所示:
task_struct结构体十分复杂,从注释我们可以看到,包括表示进程运行状态的state、栈指针stack、表示当前使用的cpu、父进程指针real_parent。对于task_struct还有很多有意思的设计,比如结尾处的结构体thread_struct thread,在x86上这个结构体包含可变大小的结构,所以必须放在task_struct的结尾并且该成员变量之后不能再定义其他任何成员变量。
分析了进程的描述,接下来就是进程创建。在linux中,有三个进程是特殊进程,分别是0、1、2号进程其中0号进程是是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换。在linux内核启动过程中由set_task_stack_end_magic(&init_task)创建,位于init/main.c/start_kernel函数中。之后0号进程会通过kernel_thread创建1号和2号进程,1号进程是所有用户进程的祖先,而2号进程负责所有内核线程的调度和管理。用户态下所有的进程都是通过fork相应的父进程产生的。
(2)进程切换与中断
linux中以时间片轮转的方式让所有进程共享CPU,那么当一个进程使用完自己的时间片之后就需要让出CPU给其他进程使用。那么在切换过程中,就需要保存进程上下文。linux进程上下文包括进程的虚拟地址空间和硬件上下文。进程硬件上下文包含了当前cpu的一组寄存器的集合,arm64中使用task_struct结构的thread成员的cpu_context成员来描述,包括x19-x28,sp, pc等。进程地址空间使用mm_struct结构体来描述,这个结构体被嵌入到task_struct中。5.8.1版本中,在__schedule函数中调用context_switch完成进程切换,下一个进程的task_struct通过函数pick_next_task获取。
中断是linux系统的重要组成部分,中断主要分为硬件中断、软件中断与异常。其中硬件中断通常为IO设备发起的,请求CPU处理进程需要的IO数据;软件中断例如进程调用系统调用,需要从用户态切换到内核态;而异常可以进一步分为错误(比如缺页)、陷阱(比如断点)和终止。中断的处理过程与进程切换类似,只不过中断的部分上下文是由硬件直接保存。在处理中断的过程中不应发生上下文切换,因为中断处理的过程是在当前进程堆栈执行的。linux中断具有优先级,并且高优先级中断可以抢占低优先级中断
(3)内存管理
linux的内存从宏观上分为内核区与用户区。内核进程可以访问所有内存,而用户进程只能访问用户区。在编写linux内核模块时,如果内核模块需要访问用户区内存,需要从用户区内存复制数据,而不是直接访问。在内存管理时,内存按段、页来分割,以方便管理。同时通过虚拟内存和硬盘空间,让所有程序共享整个内存。基于LRU的调度算法和程序的连续性规则,将长期不用的内存页面置换到硬盘上,将需要的内存页面置换到内存中,从而造成每个进程都拥有块完整内存的假象。从进程的角度看,内存分为数据段、代码段、BSS段、堆和栈。栈是进程的运行空间,代码段保存了进程的指令集合,BBS和数据段保存了进程的静态变量等,而堆用于进程动态申请内存。
(4)文件系统
与熟悉的windows不同,linux没有CDEF盘,而是使用文件来管理。在linux中,一切皆文件。Linux世界中的所有、任意、一切东西都可以通过文件的方式访问、管理。而管理这些文件需要一些数据结构来描述文件,文件控制块FCB是系统为管理文件而设置的。
同时,虚拟文件系统(Virtual File System, 简称 VFS)在linux的文件管理中扮演了重要的角色。VFS是 Linux 内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个 抽象功能,允许不同的文件系统共存。VFS直接与底层驱动打交道,负责将数据从缓冲区写入硬盘,负责抽象底层复杂的接口,向上层提供统一的调用接口。因为IO是一件非常耗时的事情,每一次调用write写文件都立刻写硬盘非常浪费时间。鉴于程序的连续性,每一次调用write都不是立刻将数据写入硬盘,而是写入缓冲区中,等待系统统一将数据写入硬盘,这样可以减少硬盘寻道浪费的时间,加快了IO速度。但是也因为这种特性,为一些需要实时保存数据的应用带来一些麻烦,比如Redis数据库。这些应用都可以设置实时写入硬盘来保证数据的完整性。
当我们写C代码准备读文件时,会引发下列过程:
1.调用C lib库的read api,给出具体的参数;
2.C lib库中read调用系统调用触发中断,用户进程挂起,CPU从用户态转到内核态;
3.在内核态中,开始在内存中查找目标文件是否存在,如果存在则开始读取数据;
4.在读取过程中有可能部分内容不在内存中,发生缺页中断,从硬盘置换需要的页面进入内存,如果此刻内存容量不足还要按照算法置换部分页面出去并对脏页进行处理;
5.读取完数据,返回用户态,继续用户进程。
在一般的应用程序中,影响程序表现的因素有:
1.是否有大量的IO操作,IO操作是否是异步;
2.是否有网络操作,此时网络通信质量也会影响应用程序表现;
3.是否申请临界资源,比如打印机,mysql连接池等;
4.数据结构是否合理,数组遍历顺序是否合理;如果数据较大但是按列遍历,可能会频繁触发缺页中断导致表现不佳;过着数据结构设计不合理,比如C中struct手动对齐不合理导致浪费过多内存;
5.内存申请与释放是否正确。使用没有自动GC的语言,对内存申请释放不合理对导致严重的内存泄露;
6.文件使用是否正确,例如日志型应用程序打开一个上GB的日志文件,显然是设计上的不合理;
7.应用程序本身算法设计是否合理,比如大量的递归、不合理的sleep、大量同步操作、临界区未上锁导致bug、重复地复制对象;
8.应用程序本身的编程语言也会影响程序表现。
原文:https://www.cnblogs.com/kirei1991/p/14747507.html