进程是操作系统的基本概念之一,它是操作系统分配资源的基本单位,也是程序执行过程的实体。程序是代码和数据的集合,本身是一个静态的概念,而进程是程序的一次执行的实体,是一个动态的概念。
上面讲解的进程的概念是Linux体系的概念,非常抽象且难以理解。我们可以把整个操作系统比喻成一家软件公司,内存就是产品设计人员,CPU就是研发人员。公司每年都会接很多项目,每个项目的开展都离不开产品设计人员和研发人员。每个项目对公司来说都是一个进程,都需要产品设计人员与技术人员的参与,项目有开始,也有结束。
从进程视角来看 ,每个进程都有独立的内存地址空间。这个地址空间至少包括两部分内容:一部分是内核,另一部分是用户的应用程序。图1-1所示为进程结构示意图,图中有8个进程,每个进程拥有内存的整个虚拟地址空间。这个虚拟地址空间被分成了两部分:上半部分是所有进程都共享的内核资源,里面放着一份内核代码和数据;下半部分是应用程序,它们相互独立、互不干扰。
从内存视角来看 ,系统物理内存的大小是固定的,例如常见的4核8GB内存的服务器,其物理内存始终是8GB。有一部分物理内存是长期分配给系统内核使用的,有一部分是分配给进程间通信使用的,其他部分通过虚拟内存技术分配给每个进程使用,如图1-2所示。
图1-1 进程结构示意图
图1-2 进程共享内存
内核使用的空间是共享的,各个进程可以通过系统内核来操作这部分内存空间。进程通信空间是给多个进程或者多个线程间通信用的。每个进程可以在执行数据写入时申请自己独立的内存物理空间。
从CPU视角来看 ,所有的进程与线程都是需要执行的任务。Linux会将需要执行的任务放进CPU的任务队列,任务队列按照优先级进行调度。在同一个时刻一个CPU只能执行一个进程或线程的任务。当执行中的任务需要等待其他资源时,就会将任务从CPU任务队列中移除,任务会进入等待状态。CPU任务调度如图1-3所示。
为了更好地管理进程,Linux制定了进程描述符(Process Descriptor)来详细地描述进程的状态和资源以及所做的事情。Linux的进程描述符是task_struct类型的数据结构,它详细描述了进程的所有信息。进程描述符的实现如代码清单1-1所示。
图1-3 CPU任务调度
代码清单1-1 进程描述符
在上述代码中,state字段描述了当前进程或线程的状态信息,字段值描述了进程当前所处的状态。在当前的Linux系统中进程有如下状态:可运行状态、可中断的等待状态、不可中断的等待状态、暂停状态、僵死状态。详细进程状态描述如表1-1所示。
表1-1 进程状态描述
进程状态变迁如图1-4所示。
图1-4 进程状态变迁
Linux为每个进程都分配了一个唯一的数字标识,这个标识称为PID(ProcessID,进程标识符)。PID是32位的无符号整数,存放在进程描述符的pid字段中。Linux允许的最大PID为32767。
task_struct中定义了4个字段来表示进程之间的关联关系,详细信息如表1-2所示。
表1-2 进程关联关系
task_struct中有4个字段prio、static_prio、normal_prio、rt_priority来表示进程的优先级,详细信息如表1-3所示。
表1-3 进程优先级
static_prio是普通进程的静态优先级,值越小表示优先级越高。rt_priority是实时进程的优先级,值越大表示优先级越高。由于static_prio与rt_priority的单位不同,一个是值越小优先级越高,另一个是值越大优先级越高,所以normal_prio统一成值越小优先级越高。prio是动态优先级,在系统进行任务调度的时候,调度器会根据prio来进行任务调度。对于实时进程,prio就等于normal_prio,对于普通进程,可以临时调整prio来提高优先级。
Linux创建一个进程大致需要经历初始化进程描述符、申请内存空间、设置进程初始状态、将进程加入调度队列等过程。为了完整地描述一个进程,操作系统设计了非常复杂的数据结构。在进程创建的时候需要申请大量的内存空间,同时需要复制大量父进程的资源,整个过程的效率非常低下。为了提升进程创建效率,Linux构造了写时复制技术。当子进程被创建的时候,Linux内核并不会立即将父进程的所有内容复制给子进程,而是只复制一些基础信息。当父进程空间的内容发生变化时,会通过写时复制技术同步给子进程。写时复制技术允许父、子进程读取相同的物理页,只要两者有一个试图更改页的内容,内核就会把这个页的内容复制到新的物理页上,并把这个页分给正在写的进程。
Linux提供了3种创建进程的函数,分别是clone、fork、vfork。clone函数是最基础的创建进程的系统调用,可以通过各种flag标识指明子进程的基础属性、堆栈等。fork函数是通过clone函数来实现的,可以通过一系列的参数标志来指明父、子进程需要的共享资源。fork函数创建的子进程需要完全复制父进程的内存空间,但是得益于写时复制技术,进程创建的过程加快。vfork函数也是基于clone函数实现的,是对fork函数的优化。因为fork函数需要复制父进程的内存空间,虽然fork函数采用写时复制技术提升了性能,但是这种不必要的复制的代价是比较高昂的,所以vfork函数可以指定flag告诉clone函数是否共享父进程的虚拟内存空间,以加快进程的创建过程。
进程创建好之后,内核必须有能力挂起正在CPU运行的进程,并切换其他进程到CPU上执行,这个过程被称为上下文切换。上下文切换的过程包含硬件上下文切换和软件上下文切换。
虽然每个进程都可以有自己的物理内存地址空间,但所有进程共用CPU寄存器,因此上下文切换的时候需要首先保证能进行硬件上下文切换。硬件上下文切换主要是通过汇编指令,保存当前进程的CPU的一些寄存器数据,然后恢复下一个进程的CPU的一些寄存器数据。当进程被切换出去时,Linux进程描述符中的thread字段就会保存该进程的硬件上下文。thread数据结构包含了大部分CPU寄存器数据。
进程地址空间指的是进程所拥有的虚拟地址空间,是Linux内核通过数据结构描述出来的,是虚拟的内存地址空间。而CPU访问的指令和数据需要落实到实际的物理地址。软件上下文切换主要完成从进程的虚拟地址空间切换到物理空间。如果即将执行的进程是内核进程,则不需要进行内存空间切换,因为所有的内核进程共用相同的物理空间。