Linux操作系统分析

一、精简的Linux系统概念模型

        操作系统的三大核心功能是进程管理,内存管理和文件系统。

 

 

       从宏观上来看,Linux操作系统的体系架构分为用户态和内核态。内核从本质上看是一个提供系统服务的程序,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。系统调用是操作系统提供给用户的一种服务,程序设计人员在编写程序的时候可以用来请求操作系统的服务。再往底层走,就是内核的一些驱动程序,这些驱动程序往下连接着硬件资源,以便更好的为上层提高服务。

二、中断机制

中断最初用于避免CPU轮询I/O设备,就绪状态发生时让I/O设备主动通过中断信号通知CPU,大大提高了CPU在输入输出上的工作效率,这就是硬件中断(Interruot)。后来随着中断适用范围扩大,比如解决机器运行过程出现的异常情况以及系统调用的实现等,这就产生了软件中断(Exception),又分为故障(fault)、陷阱(trap)和退出(Abort)。简而言之,中断使得CPU能从当前进程的指令流中切换出来,转去执行预定义的中断处理程序(内核代码入口)

在硬件级别上,中断的一个一般流程如下:

  1. 每个中断由0~255之间的一个数来标识,Intel称其为中断向量,中断描述符表(IDT)记录了中断向量和对应的中断处理程序入口地址。内核在中断发生前,会初始化IDT,idtr寄存器指向该表的基地址
  2. 在执行下一条指令前,CPU会检查之前有无发生中断
  3. 获取中断向量号,配合idtr寄存器中IDT表的基地址,即可获得中断处理程序的入口地址
  4. 检查是否是由用户态陷入内核态
  5. 如果是,将用户栈替换为内核栈(通过装载ss和esp寄存器),并将之前的ss,esp,eflags,eip等CPU关键上下文压栈保存;否则不需要替换ss和esp寄存器
  6. 将中断处理程序的入口地址装载进eip寄存器,即开始中断处理程序
  7. 弹出内核栈中保存的CPU关键上下文并恢复,如果之前由用户态陷入内核态,那么还会替换为用户栈

在软件级别上,中断过程被分为了上半部和下半部

  • 上半部:实现快速响应,处理更多中断请求,该部分仅简单地读取寄存器中的中断状态并清除中断标志,就像登记了一个中断事件到下半部的处理队列中。该部分不可被打断
  • 下半部:真正完成中断处理工作,允许被打断。下半部可以使用软中断、tasklet或工作队列实现

中断的一个典型应用就是系统调用,对诸如分配内存、创建进程等操作,交由用户程序来执行是很危险的。通过trap(一般是int $0x80),用户态可主动发出中断陷入内核态,中断处理程序读eax寄存器可以获得系统调用号,然后执行对应的系统调用例程,由可信任的内核来做这些操作就是安全的

三、 进程调度

进程调度的功能:

(1)记录系统中所有进程的执行情况。

(2)选择占有处理机的进程。

(3)进行进程上下文切换。—个进程的上下文(context)包括进程的状态、有 关变量和数据结构的值、机器寄存器的值和PCB以及 有关程序、数据等。 

进程调度的时机:

(1)进程状态发生变化时。

(2)当前进程时间片用完。

(3)进程从系统调用返回到用户态。

(4)中断处理后,进程返回到用户态。

Linux进程调度采取的是动态优先级法,调度的对象是可运行队列。在调度过程中,调度程序检查可运行队列中所有进程的权值, 选择其中权值最大的进程做为下一个运行进程。 

Linux系统的一般执行过程举例:

• 以32位x86系统结构linux-3.18.6为例,以系统调⽤作为特殊的中断简要总结如下。 • (1)正在运⾏的⽤户态进程X。 • (2)发⽣中断(包括异常、系统调⽤等),CPU完成以下动作。 •
• save cs:eip/ss:esp/eflags:当前CPU上下⽂压⼊进程X的内核堆栈。
• load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack):加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执⾏路径的起点。 • (3)SAVE_ALL,保存现场,此时完成了中断上下⽂切换,即从进程X的⽤户态到进程X的内核态。

• (4)中断处理过程中或中断返回前调⽤了schedule函数,其中的switch_to做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程 (本例假定为进程Y)的内核堆栈,并完成了进程上下⽂所需的EIP等寄存器状态切换。详细过程⻅前述内容。
• (5)标号1,即前述3.18.6内核的swtich_to代码第50⾏“”1:\t“ ”(地址为switch_to中的“$1f”),之后开始运⾏进程Y(这⾥进程Y曾经通过以上步骤被切换出去,因此可以 从标号1继续执⾏)。
• (6)restore_all,恢复现场,与(3)中保存现场相对应。注意这⾥是进程Y的中断处理过程中,⽽(3)中保存现场是在进程X的中断处理过程中,因为内核堆栈从进程X 切换到进程Y了。
• (7)iret - pop cs:eip/ss:esp/eflags,从Y进程的内核堆栈中弹出(2)中硬件完成的压栈内容。此时完成了中断上下⽂的切换,即从进程Y的内核态返回到进程Y的⽤户 态。
• (8)继续运⾏⽤户态进程Y。

 

四、文件系统

文件系统(File System)是文件存放在磁盘等存储设备上的组织方法,其主要体现在对文件和目录的组织上。

目录提供了管理文件的一个方便而有效的途径,用户可以从一个目录切换到另一个目录,而且可以设置目录和文件的权限,设置文件的共享程度。

Linux 文件系统利用树形结构管理文件。每个节点有多个指针,指向下一层节点或者文件的磁盘存储位置。文件节点还附有文件的操作信息(metadata),包括修改时间,访问权限等。

用户的访问权限通过能力表(Capability List)和访问控制表(Access Control List)实现。前者从文件角度出发,标注了每个用户可以对该文件进行何种操作。后者从用户角度出发,标注了某用户可以以什么权限操作哪些文件。

Linux 的文件权限分为读、写和执行,用户组分为文件拥有者,组和所有用户。可以通过命令对三组用户分别设置权限。

 

五、内存管理

Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间(具体的原因请看硬件基础部分)。

在讨论进程空间细节前,这里先要澄清下面几个问题:

      第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。

     第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。

    第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。

 

进程内存的分配与回收

创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。进程对内存区域的分配最终都会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap()),

内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况, do_mmap()函数都会将一个地址区间加入到进程的地址空间中--无论是扩展已存在的内存区域还是创建一个新的区域。

同样,释放一个内存区域应使用函数do_ummap(),它会销毁对应的内存区域。

 

系统物理内存管理 

虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址的转换工作需要通过查询页表才能完成,概括地讲,地址转换需要将虚拟地址分段,使每段虚地址都作为一个索引指向页表,而页表项则指向下一级别的页表或者指向最终的物理页面。

每个进程都有自己的页表。进程描述符的pgd域指向的就是进程的页全局目录。

 

物理内存管理(页管理)

Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数个4k(在i386体系结构中)大小的页,从而分配和回收内存的基本单位便是内存页了。利用分页管理有助于灵活分配内存地址,因为分配时不必要求必须有大块的连续内存,系统可以东一页、西一页的凑出所需要的内存供进程使用。虽然如此,但是实际上系统使用内存时还是倾向于分配连续的内存块,因为分配连续内存时,页表不需要更改,因此能降低TLB的刷新率(频繁刷新会在很大程度上降低访问速度)。

六、影响Linux系统性能的因素

应用程序的性能很大程度上跟存储方式和系统中断有着密切的关系,不同的存储方式可能有着不同的性能开销,例如对于一个二维数组,有两种方式进行数据保存,一种是行优先存储,一种是列优先存储:

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 //第一种代码
 9 func First() {
10     test := [1000][1000]int{}
11     for i := 0 ; i < 1000; i++{
12         for j := 0 ; j < 1000 ; j++{
13             test[i][j] = 1;
14         }
15     }
16 }
17 // 第二种代码
18 func Second() {
19     test := [1000][1000]int{}
20     for j := 0 ; j < 1000; j++{
21         for i := 0 ; i < 1000 ; i++ {
22             test[i][j] = 1;
23         }
24     }
25 }
26 // 测试
27 func test(a func()) {
28     start := time.Now()
29     a()
30     end := time.Now()
31     fmt.Println(time.Duration(end.Sub(start)) * time.Millisecond)
32 }
33 
34 func main() {
35     test(First)
36     test(Second)
37 }

​ 测试结果:

​ 由以上结果可以发现:方法一比方法二快。

​ 原因:由于数组是连续存储的,且根据Linux的内存管理机制,可以得到,第一种以行优先为主的存储方式相对第二种以列优先的存储方式拥有更少的缺页中断,从而拥有更少的开销,从而性能更好。

影响Linux性能的因素

硬件层面:

  硬盘与内存互相读写时,硬盘的持续传输速度,随机小文件(4K)传输速度,延迟等。内存的容量,频率,时序,带宽,延迟等。CPU的频率,IPC(Instruction Per Clock),核心数,缓存等。以及程序可能用到的各种额外计算硬件如GPU,TPU,乃至各种专用集成电路ASIC的性能,延迟等。连接各硬件的PCI总线接口的速度也很可能成为瓶颈。以及网卡的吞吐量,网络的带宽和延迟等。

软件层面:

  操作系统对于各种硬件资源的调度的合理性,各硬件的驱动程序对硬件性能的发挥,在多核心CPU上,实现了多线程运行的程序会比单线程要快,使用的处理器指令集会影响性能表现,程序本身的代码的优化(包括编译器的优化)在数据的组织和计算处理上会影响性能表现。对于一个特定的程序,操作系统给它的优先级不同也会影响性能表现。它的性能瓶颈往往出现在这个程序会大量使用的资源上,比如cpu计算密集,或网络带宽敏感,或硬盘IO密集等

七、致谢

非常感谢孟老师和李老师精彩的课程,孟老师翩翩风度和庄周梦蝶的精妙比喻,李老师的热情幽默,都让枯燥的代码世界充满了诗情画意,不仅教授了学生许多实用的知识,也熏陶了学生的精神世界。

 

在课程即将结束之际,我衷心祝愿孟老师和李老师的课都越讲越好,两位令人尊敬的老师都身体健康,万事如意。

posted @ 2021-05-18 00:28  青萍剑气三千万  阅读(230)  评论(0编辑  收藏  举报