一、Linux系统概念模型
(一)中断与系统调用
1. 中断
- 中断可以提高处理器的效率,即当 I/O 设备准备好数据时,通过中断,CPU 才去处理 I/O 设备准备好的数据。
- 中断分为外部中断和内部中断:
- 外部中断一般指计算机外设发出的中断。在用户进程执行时,硬件中断信号到来,进入内核态,执行这个中断对应的中断服务例程;
- 内部中断是指因为硬件出错或是运算出错所引起的中断。内部中断分为故障和陷阱。系统调用就是利用陷阱这种内部中断方式主动从用户态进入内核态的。
- 外部中断处理过程:
- 执行完每个指令之后,CPU 都要检查当前是否有外部中断信号;
- 如果检测到外部中断信号,则需要保护被中断现场的 CPU 环境(如程序状态字 PSW、程序计数器 PC、各种通用寄存器);
- 根据中断信号类型转入并执行相应的中断处理程序;
- 恢复原进程的 CPU 环境退出中断,返回源程序继续向下执行。
2. 系统调用
- 系统调用是操作系统提供给应用程序使用的接口,应用程序可以通过系统调用请求获得操作系统的服务。
- 核心态:拥有者可以访问计算机系统更多的资源的状态,可以运行 CPU 任何指令。
- 用户态:相比核心态来说,用户态拥有或者访问计算机系统的资源较少,只能运行 CPU 非特权指令。
- 在用户态,程序请求系统调用,然后从用户态进入内核态,执行对系统调用的相应处理。
(二)进程管理
1. 进程的描述
- 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配的基本单位。
- 在 Linux 内核中用一个结构体 task_struct 来描述进程,结构体中包含进程的状态,内存管理的描述 mm,还有进程间通信的信号 signal 的描述等。
2. 进程的状态
- 运行态:当进程正在被 CPU 执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态。进程可以在内核态运行,也可以在用户态运行;
- 可中断睡眠状态:当进程处于可中断等待状态时,系统不会调度该进程执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程;
- 可中断睡眠状态:与可中断睡眠状态类似。但处于该状态的进程只有被使用 wake_up() 函数明确唤醒时才能转换到可运行的就绪状态;
- 暂停状态:当进程收到信号 Sigstop、Sigtstp、Sigttin 或 Sigttou 时就会进入暂停状态;
- 僵死状态:当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。
3. 进程调度
- 进程上下文
- 上下文这个概念简单来说就是环境,对于进程而言,进程上下文就是进程执行时的环境,包含进程执行所需要的所有信息。
- 进程上下文包括用户地址空间(包括程序代码、数据、用户堆栈等);控制信息(进程描述符、内核堆栈等);进程的 CPU 上下文(相关寄存器的值)。
- Linux 进程调度策略
- Linux 使用完全公平调度算法(CFS)来进行进程调度
- CFS 算法是基于权重的动态优先级调度算法。每个进程使用 CPU 的顺序由进程已使用的 CPU 虚拟时间决定,已使用的虚拟时间越少,进程排序就越靠前,进程再次被调度执行的概率也就越高。每个进程每次占用 CPU 后能够执行的时间由进程的权重决定,并且保证在某个时间周期内运行队列里的所有进程都能够至少被调度执行一次。
- 进程调度时机
- 进程状态变换时;
- 某进程系统调用返回到用户态时;
- 某进程时间片用完;
- 中断处理后,进程返回到用户态。
(三)文件管理
1. 概念
- 文件系统,就是操作系统中实现文件管理的一组软件、被管理的文件以及为实施文件管理所需要的一些数据结构的总称。
- Linux 一切皆文件,即在 Linux 系统中的所有东西都可以通过文件的方式访问、管理。
2. 虚拟文件系统
-
虚拟文件系统(VFS),是 Linux 内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个抽象功能,允许不同的文件系统共存。系统中所有的文件系统不但依赖 VFS 共存,而且也依靠 VFS 协同工作。
-
模型
- 第一层为文件系统接口层,如 open、write、close 等系统调用接口;
- 第二层为 VFS 接口层,该层有两个接口:一个是与用户的接口;一个是与特定文件系统的接口。VFS 与用户的接口将所有对文件的操作定向到相应的特定文件系统函数上。VFS 与特定文件系统的接口主要是通过 VFS 操作来实现的;
- 第三层是具体文件系统层,提供具体文件系统的结构和实现,包括网络文件系统,如 NFS。
(四)内存管理
- Linux 采用虚拟内存技术管理系统内存。虚拟内存技术即每个进程都有独立的进程地址空间。当进程要求运行的时,不是将他的全部信息装入内存,而是将其一部分先装入内存,另一部分暂时留在外存,进程在运行过程中,要使用信息不在内存时,发生中断,由操作系统将他们调入内存,以保证进程的正常运行。
- Linux 内部的地址的映射过程为根据逻辑地址找到线性地址,再根据线性地址找到对应的物理地址。
- Linux 内核通过 task_struct 结构体来管理进程,这个结构体包含了一个进程所需的所有信息。task_struct 中有一个结构体 mm_struct,描述了一个进程的整个虚拟地址空间。每个进程正是因为都有自己的 mm_struct,才使得每个进程都有自己独立的虚拟的地址空间。
二、具体实例
使用 read 函数来读取一个文件的过程如下:
- 某一进程代码调用封装好的库函数 read();
- 进程将系统调用号 0x03(read 的系统调用号是 0x03)放入寄存器 eax 中,并且保存参数,通过 int 80 指令陷入中断;
- 在内核中首先执行 system_call 函数,接着根据系统调用号在系统调用表中查找到对应的系统调用服务例程;
- 执行该服务例程,在此过程中调用 VFS 提供的接口用来操作文件;
- 执行完毕后,转入 ret_from_sys_call 例程,从系统调用返回。
三、影响应用程序性能表现的因素
-
内存管理会影响应用程序的性能,一个典型的例子就是程序的局部性原理:
-
时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
-
空间局部性是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问。
-
如下面代码所示,函数 func1 符合局部性原理,当前数字与下一个数字间隔为 1,而 func2 函数不满足局部性原理,当前数字与下一个数字间隔为 1024,会发生缺页中断,因此效率低。运行结果:运行 1000 次函数 func1 的时间为 2075ms,而运行 1000 次函数 func2 的时间为 2523ms,也证明了上述观点。
#include <stdio.h> #include <time.h> #define N 1000 // 符合局部性原理 int func1(int a[][N]) { int sum = 0; for(int i = 0; i < N; i++) { for(int j = 0; j < N; j++) { sum += a[i][j]; } } return sum; } // 不符合局部性原理 int func2(int a[][N]) { int sum = 0; for(int j = 0; j < N; j++) { for(int i = 0; i < N; i++) { sum += a[i][j]; } } return sum; } int main() { clock_t start, finish; start = clock(); int arr[N][N]; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { arr[i][j] = i + j; } } start = clock(); for (int i = 0; i < 1000; i++) { func1(arr); } finish = clock(); printf("运行 1000 次函数 func1 的时间为: %lf(ms)\n", (finish - start) / 1000.0); start = clock(); for (int i = 0; i < 1000; i++) { func2(arr); } finish = clock(); printf("\n运行 1000 次函数 func2 的时间为: %lf(ms)\n", (finish - start) / 1000.0); return 0; }
-
-
CPU 性能,实时使用率也会影响程序的运行性能,当 CPU 使用率很高时候,运行程序,那么程序的性能就很低。
-
磁盘 I/O 也会影响程序的运行性能,磁盘的IOPS(Input/Output Per Second,即每秒钟处理的I/O请求数量)参数越大,读写磁盘多的程序性能就越强。
四、结语
学习了《Linux操作系统分析》这门课,我对 Linux 中系统调用、进程创建与切换、以及程序在 Linux 系统的执行原理等知识有了一定的了解,而不是像以前一样仅仅知道 Linux 几个操作命令。因此,十分感谢孟老师和李老师的辛勤付出!