从系统的角度分析影响程序执行性能的因素

1. Linux系统概念模型

学过操作系统的同学应该都知道,操作系统有五大功能 —— 处理机管理、存储器管理、设备管理、文件管理、进程管理。操作系统向下实现了对硬件资源的管理,向上提供了系统调用以为系统程序和应用程序提供一个良好的执行环境。那么接下来我们就针对操作系统的几个功能对linux系统进行一个简单的剖析。

1.1 处理机管理

处理机管理的主要目标就是提高处理机的利用率,使其尽可能地处于工作状态。而其涉及到的技术主要就是多道和分时,对于单个处理机的系统,处理机在进程之间按照一定的规则不断切换,这种执行方式就称为并发执行,操作系统通过并发机制,对处理机进行分配和调度,以实现对计算机资源的充分使用。

1.2 设备管理

设备管理也可以称为I/O管理,其主要任务为:

  • 选择和分配I/O设备以便进行数据传输操作
  • 控制I/O设备和CPU(内存)之间进行交换数据
  • 为用户提供一个友好的透明接口,把用户和设备硬件特性分离,使得用户在编写程序时不需要考虑具体设备,由系统按照用户的要求来对设备的工作进行控制
  • 提高设备与设备之间、CPU与设备之间、进程与进程之间的并发程度,以提高资源利用率

在这一功能模块中,主要涉及到的技术有:

  • 缓冲技术 —— 以解决CPU与I/O设备间速度不匹配的矛盾
  • 四种I/O控制方式 —— 程序I/O、中断、DMA、I/O通道

1.3 文件管理

在linux中一切皆文件,这是通过linux中的虚拟文件系统(VFS)实现的,包括上面讲到的设备管理,其实每一个设备都是一个文件,当我们要注册一个设备的时候,linux就会在/dev/目录下创建一个设备文件,该文件会关联到相应的操作函数。在本功能模块本应该讲到inode索引等文件系统中的重要内容,但因为不是本博文的重点,因此略过。

1.4 内存管理

内存管理是linux操作系统中极其重要的一个内容,其包括虚拟内存管理、物理内存管理。

1.4.1 虚拟内存管理

虚拟内存管理,也就是进程内存管理。对于32位linux操作系统,其使用3G/1G虚拟内存布局,即高1G空间为内核空间、低3G空间为用户空间

1.4.2 物理内存管理

1.5 进程管理

进程管理是Linux操作系统的核心,其中涉及到进程的描述、创建、切换。

在Linux系统中,进程是由一个名为task_struct的结构所描述的,该结构中包含了表示一个进程所必需的数据,所有进程由双向链表链起来。在Linux进程树中,init_task为第一个进程(0号进程),它的初始化是通过硬编码方式固定下来的,除此之外,所有的其他进程均通过do_fork复制复制进程的方式初始化的。在Linux系统中,创建一个新的进程的基本过程为:通过fork系统调用复制当前进程,再通过exec执行新的程序,且这里还涉及到Linux系统的写时复制的优化策略。

进程调度是进程管理的核心,那么进程调度的时机有:

  1. 当前进程的时间片用完
  2. 进程状态发生改变,如进程需要等待某个事件
  3. 就绪态队列处有高优先级进程到来
  4. 系统调用或中断处理退出

1.6 系统调用

涉及到Linux就必须要讲到系统调用,讲到系统调用还需要讲到中断。为了保证系统资源的安全使用,应用程序是不直接使用系统资源的,它需要通过系统调用来实现对系统资源的访问。于是,Linux在内核中实现了大量的系统调用,glibc也同样封装了大量的系统调用API,也即库函数,因此,程序员可以通过调用库函数来调用系统调用从而实现对系统资源的访问。

  • 中断

    Linux用户态程序想要进入内核态则需要通过中断进入,早期使用int 0x80进入,后面实现了syscall/sysexit优化了int 0x80不必要的权限检测问题。

    Linux中维护了一张中断向量表,每一个中断向量指向一个中断服务例程,用户程序想要使用系统资源时,需要通过0x80号中断进入相应的中断服务例程。

  • 系统调用

    上面讲到用户程序通过中断进入中断服务例程,那么0x80号中断服务例程做什么呢?

    其实Linux系统还维护了一张系统调用表,每一个系统调用号对应的系统调用项中的内容为指向系统调用的函数指针,0x80号中断服务例程接收从用户态传来的系统调用号(eax),并去系统调用表中找到对应的系统调用的函数地址并执行该系统调用

1.8 函数调用堆栈

在函数调用的时候,如何保证子程序执行完成后可以返回到之前的位置继续执行这是一个问题,这里就涉及到函数调用堆栈的使用。

在32位系统中,函数参数是通过堆栈传递的,那么函数调用过程中函数堆栈是怎样变化的呢?

1.7 模型验证

考虑read函数在系统中是如何运行的:

  • 用户程序首先调用lib库中的read函数
  • lib库中的read函数封装了系统调用sys_read,此时库函数在保存好sys_read系统调用号以及参数后,陷入0x80中断
  • 0x80号中断服务例程根据eax寄存器里的系统调用号找到系统调用sys_read的地址,并转去执行系统调用
  • 在中断处理子程序中,通过虚拟文件系统读取相应的信息存储到内核空间中,然后拷贝至用户空间,在此期间,进程可挂起,转去执行就绪队列中的其他进程
  • 读文件完成,系统通过中断通知CPU,该进程由等待态转为就绪态等待调度。
  • 调度该进程,进程继续执行,系统调用返回,用户程序获得读取的信息。

2. 应用程序在linux系统中的执行过程及影响其性能表现的因素分析

接下来我们将一个应用程序放入该系统中来系统性的梳理影响程序性能表现的因素,因为考虑到要系统性的梳理,这里就抽象出来一个应用程序了。

一个可执行文件在系统中以进程的方式执行,首先可执行文件被加载到内存中,然后系统分配内存资源完成进程的创建,在这个过程中,会有如下因素影响进程的运行性能:

  • 可执行文件本身的大小。这里系统所做的优化就是GOT表和PLT表分别实现了动态链接和延迟绑定,一方面提高了装载速度,另一方面减少了不必要的内存损耗
  • 进程创建的方式,系统做了COW优化,即在fork子进程时不直接复制父进程的所有资源,只允许读,只有在一个进程要写的时候才拷贝所有内容分配给子进程。

进程在创建完成后会加入到就绪队列中,这里涉及到进程调度的问题:

  • Linux目前采用CFS(完全公平调度)来实现进程调度

进程在执行过程中,其性能表现更多地是与它的任务有关:

  • 若进程是一个IO密集型程序,那么进程更多是处于挂起状态,等待I/O操作完成后再调度执行,此时频繁地进程调度和上下文切换也会影响性能
  • 对于外设的I/O请求,如何处理也会影响性能表现。上面讲到,I/O控制有四种技术:轮询、中断、DMA、I/O通道,其中轮询最慢,最耗费系统资源,中断是最常用的方式,DMA可以大幅度减少cpu的负担,只需要CPU把数据传输的地址和长度设置好,CPU就可以解放了,通道一般用于大型计算机中,它算是一个I/O处理机,可以完全解放CPU
  • 程序中是否进行了大量的函数调用,当发生函数调用时,系统要维护每个函数的函数调用堆栈,这也需要系统资源,因此大量的函数调用或太深的函数嵌套都会影响程序性能,即:能用循环则不用递归。
  • 程序是否符合局部性原理,举个例子来说,堆排的平均复杂度和最坏复杂度均为\(O(nlogn)\),而快排最坏复杂度为\(O(n^2)\),那为什么人们更喜欢快排呢?因为在数据量足够大时,堆排的操作相对于快排来说,快排更符合局部性原理,因为堆排每访问一个子节点都是将下标x2

除了上述所说的,还有其他方面的优化,例如在进入系统调用时,低版本的系统使用int 0x80进入中断,其弊端在于不必要的CPL和DPL的比较,这就浪费了好几个时钟周期,因此Intel和AMD分别实现了快速系统调用sysenter/stsexitsyscall/sysret

posted @ 2021-05-16 21:34  bunner  阅读(196)  评论(0编辑  收藏  举报