从系统的角度分析影响程序执行性能的因素
一、Linux系统的组成
从用户的角度到系统底层的顺序来看,linux系统包含以下几个部分:
应用程序:linux系统能够执行的程序,用于完成用户所希望的功能
shell程序:用于执行用户所写的或者自带的应用程序
文件系统:用于组织磁盘上的文件,规定了文件的组织和存储方式
系统调用和公用函数库:操作系统提供的功能函数以及一些库函数
内核模块:操作系统的核心,具有虚拟内存、多任务、共享库、需求加载、可执行程序和网络功能。内核模块分为几个部分,包含进程管理,存储管理吗,文件系统管理,设备和驱动管理,网络通信等
进程管理
程序的运行过程在操作系统中被抽象成一种任务的执行,也就是进程。进程在linux下的表示形式是task_struct数据结构,叫做进程控制块。
(1)进程控制块(task_struct)
进程控制块包含了和进程相关的数据内容,包括进程的状态,进程双向链表的管理,以及控制台tty、文件系统fs的描述、进程打开文件的文件描述符files、内存管理的描述mm,还有进程间通信的信号signal的描述等。
当操作系统创建一个新的进程时,就会为这个进程创建一个进程控制块,标识这个进程的存在,并为这个进程分配相应的栈空间,将进程控制块挂载到相应的队列上(进程的状态队列,阻塞,就绪等)。
(2)进程的创建
linux下的进程,除了0号进程是通过硬编码方式来初始化进程描述符,其余都是通过复制父进程的进程描述符来完成初始化的。
初始状态下,linux在start_kernel中通过硬编码方式初始化第一个进程的进程描述符。后续的用户态进程和内核线程通过复制0号进程来完成。
进程的创建过程包含复制父进程的进程描述符相关信息,更新共享的数据结构,包含文件描述符计数,共享内存等,如果子进程需要运行其他程序,则通过加载器将可执行程序相关内容加载进子进程的运行空间。
(3)进程的切换
进程在切换前需要决定下一个运行的进程,这需要进程调度的功能。
进程调度需要:
- 记录系统中所有进程的执行情况。
- 根据进程调度策略选择下一个占有处理机的进程
- 进行上下文切换
进程的上下文包含:
- 进程的控制块
- 进程的堆栈
- 进程的地址空间和运行环境(cpu上下文和一些寄存器)
在进程状态结束、用完时间片、从系统调用或者中断结束时返回用户态时会发生进程调度。
进程的切换过程:
首先进程通过中断或者系统调用进入内核态,保存cpu现场,通过更换cs:eip寄存器切换为内核堆栈,将程序的下一个执行地址设置成系统调用程序或者中断处理程序的入口地址,之后保存现场,执行程序。
执行完成后会检查对应是否需要进程调度的标志。进程的上下文切换,包含更换页表寄存器的切换(更换为新进程的地址空间),更换内核堆栈(换为新进程的内核堆栈),加载新进程的进程控制块和cpu上下文(保存在task_struct的thread变量中),之后沿着新进程在之前进程调度时调用的schedule函数的调用栈返回到中断程序的结束位置,返回用户态。
系统调用和中断
中断的主要功能是平衡cpu和外设之间的速度差异,处理器速度一般比外设速度快很多,只有当外设处理好之后cpu才转过来处理外设。中断就是用于在cpu处理其他事情时通知cpu来处理外设。
中断分为硬件中断和软件中断,硬件中断用于外设准备好后之后的处理。而软件中断则是指令产生的中断调用,分为故障、陷阱和退出。在发生中断时,进程进入内核态,将当前执行到的位置的状态保存,将指令流切换成中断处理程序的执行,在执行完成后才会将保存的寄存器状态恢复,返回并继续之前的执行。
每个中断由0-255之间的数字来标识对应的中断向量。通过中断向量就可找到对应的中断处理程序。
在中断处理前,需要保存cpu上下文在堆栈中,以便中断处理结束后恢复现场继续执行。
中断的硬件处理过程:
- 在执行每一条指令前,检查是否发生中断或异常
- 如果发送了中断,执行以下过程
- 确定中断向量号
- 从中断向量表中获取中断处理程序的地址
- 检查是否由用户态进入了内核态,如果是,则需要将堆栈切换成内核堆栈
- 在内核堆栈中保存eflags,cs,eip寄存器的内容。将中断处理程序的入口装载。开始执行。
- 处理完成后,调用iret指令,弹出之前保存的内容。如果进程是用户态被中断的,就恢复到用户态,否则再装载ss和esp寄存器,返回之前的内核栈。
中断用于设备的处理,而系统调用是建立在中断基础上的应用,操作系统提供了很多功能,相对于用户自己写的程序更加稳定,安全,包括进程的创建,资源的分配,内存分配等。用户程序通过调用相关的命令,发出中断,陷入内核态,内核程序调用相应的系统调用处理程序来完成用户程序所希望的功能。
虚拟文件系统和设备驱动
文件系统
在磁盘上不同的文件系统有不同的组织方式,包括空闲块,已分配的块,目录文件,资源文件等。其中有一个超级块结构记录了文件系统的相关信息,如空闲块和分配块的位置,数量等。
而一个操作系统可以挂载多个不同的文件系统,这是通过内核的虚拟文件系统(vfs)完成的,对用户程序隐去各种不同文件系统的实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界面。当文件系统挂载到操作系统时,操作系统在内核中创建一个vfs超级块,将磁盘上的超级块读取进来,初始化vfs超级块。
每个进程通过open系统调用来与具体的文件建立连接。该连接用一个file数据结构来表示,结构中有个类型为file_operations的指针域f_op。将指针域f_op设置成指向某个具体的文件系统所提供的一组操作函数。当内核将一个索引节点装入内存是,会在file_operations数据结构中存放一个指向这些文件操作的指针。之后read,write等系统调用就会通过这个指针来操作实际文件系统上的内容了。
内核维持一个系统打开文件表,其中保存的是inode节点,代表的是各个文件,而进程保存了一个进程打开文件表,其中保存了各个inode节点的索引,之后就可以通过文件描述符访问系统打开文件表(访问文件)了。
设备驱动
设备在内核中是以文件的形式存在的,相应的,对应的操作也就被初始化为设备驱动程序中对应的函数。
内存管理
linux操作系统采用虚拟内存的管理方式,使得进程之间不会互相干扰。用户进程部分分段存储内容可由栈、堆、BSS段、数据段、代码段组成。 在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。
Linux系统会不时的进行页面交换操作,以保持尽可能多的空闲物理内存,即使并没有什么事情需要内存,Linux也会交换出暂时不用的内存页面。这可以避免等待交换所需的时间。
linux进行页面交换是有条件的,不是所有页面在不用时都交换到虚拟内存,linux内核根据”最近最经常使用“算法,仅仅将一些不经常使用的页面文件交换到虚拟内存。
交换空间的页面在使用时会首先被交换到物理内存,如果此时没有足够的物理内存来容纳这些页面,它们又会被马上交换出去,如此以来,虚拟内存中可能没有足够空间来存储这些交换页面,最终会导致linux出现假死机、服务异常等问题,linux虽然可以在一段时间内自行恢复,但是恢复后的系统已经基本不可用了。
模型验证
当进程读写文件时,首先调用open系统调用,通过系统调用陷入到内核,之后查询中断向量表后调用对应的中断处理程序,执行sys_open函数,这个函数会在磁盘上找到对应的文件控制块,根据不同文件系统的open函数来创建file文件类型,存储在系统打开文件表中,这个结构中存储了文件对应的信息,包括大小类型,读写指针等。进程也有一个进程打开文件表,其中存储的是系统打开文件表的索引。
之后进程使用read系统调用即可对file数据结构进行相关操作,依据返回的文件描述符就能找到对应的file文件。之后通过file文件中的file_operation数据结构存储的函数指针调用相关函数。
分析影响程序性能表现的因素
影响程序表现的因素有很多:
比如io密集型的程序会受到cpu核心数的影响,需要频繁读取的程序会受到cache命中率的影响,多线程程序会受到切换线程的频率的影响,此外还有内存大小等。
举例:
(1)多线程程序若需要频繁切换线程,应该考虑使用线程池或者协程,线程池是为了减少线程的创建和销毁,线程也需要有线程描述符和相关的线程堆栈来维持线程的运行环境。协程则是将内核线程拆分成多个用户级线程,也就是说内核不知道协程的存在,切换协程只是将当前线程的运行映像存储,切换到另一个运行映像,却不需要内核态的帮助。
(2)数组的遍历过程,尽量按照存储顺序访问,这样cache的命中率最高,防止直接从内存中读取,这样相对于从cache中读取会慢很多。
当然具体的程序需要具体分析,比如多线程程序,对于io密集型线程,线程的个数最好是cpu核心数的两倍,对于cpu密集型,最好是cpu核心数减一或者加一,这都是需要根据程序的需求来确定的。