进程这个概念是在多道程序设计中引入的,在此之前的是批处理程序,所以进程主要是用来解决程序不能并发执行从而导致 CPU 利用率低下这个问题的。
进程管理,就是在程序之上抽象出了进程的概念,然后通过进程状态、上下文切换、中断、调度等等手段,最终实现使程序在多道程序环境下能并发执行,并能对并发执行的程序加以控制和描述。
Process,一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
一个进程包含以下信息:
进程的特点
进程与程序的联系
进程与程序的区别
进程表存储在内核空间中,包含进程 ID 和进程控制块信息,以下是典型的进程表中的一些字段(都在 PCB 中):
操作系统在进程表中维护指向每个进程 PCB 的指针, 以便它可以快速访问 PCB。
在操作系统中,是用 PCB(Process Control Block,进程控制块)数据结构来描述进程的基本情况以及运行变化的过程。PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
PCB 具体包含以下三类信息:
进程标识信息:
进程控制信息:
处理机状态信息保存区:
基本的进程状态有五个:创建、就绪、阻塞、运行、结束。除此之外,还有两个挂起状态,共计七个状态。
如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间。所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到外存,等需要再次运行的时候,再从外存换入到物理内存。
挂起状态就是用来描述进程没有占用实际的物理内存空间的情况。
导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:
- 通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程。
- 用户希望挂起一个程序的执行,比如在 Linux 中用
Ctrl+Z
挂起进程;
进程的状态变迁:
进入挂起状态的方式:操作系统自动挂起、用户主动挂起、睡眠。
离开挂起状态的方式:操作系统自动激活、用户主动激活、睡眠到期自动唤醒。
通过链表的方式,把相同状态的进程的 PCB 链在一起,组成各种状态队列。比如就绪队列、阻塞队列、运行队列等。
当一个进程的状态发生变化时,它的 PCB 从一个状态队列中脱离出来,加入到另一个状态队列中去。
除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。
一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源(不可写的内存区域是共享的,如代码段),当子进程被终止时,其在父进程处继承的资源应当还给父进程。终止父进程时同时也会终止其所有的子进程。子进程可以视为父进程的副本,不共享地址空间。
注意:Linux 操作系统对于终止有子进程的父进程,会把子进程交给 1 号进程接管。本文所指出的进程终止概念是宏观操作系统的一种观点,最后怎么实现当然是看具体的操作系统。
创建进程的过程如下:
进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill
掉)。
进程可以通过系统调用自愿终止,也可以让操作系统终止进程。
终止进程的过程如下:
当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。
阻塞进程的过程如下:
进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。
如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。
唤醒进程的过程如下:
进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行。
常见的进程上下文切换场景:
在 UNIX 中,进程可以创新新的进程,且两者之间存在父子关系,每一个子进程都有一个父进程,父进程可以有零个或多个子进程。
子进程的所有资源都继承父进程,相当于父进程的一个副本(除代码段共享以外),不共享地址空间。
除了进程 0(即 PID=0 的交换进程,Swapper Process)以外的所有进程都是由其他进程使用系统调用 fork 创建的,这里调用 fork 创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程 0 以外的进程都只有一个父进程,但一个进程可以有多个子进程。
操作系统内核以进程标识符(Process Identifier,即 PID)来识别进程。进程 0 是系统引导时创建的一个特殊进程,在其调用 fork 创建出一个子进程(即 PID=1 的进程 1,又称 init)后,进程 0 就转为交换进程(有时也被称为空闲进程),而进程 1( init 进程)就是系统里其他所有进程的祖先。
进程和它的所有子女和后裔进程共同组成一个进程组。
Windows 中没有父子进程的概念。
父进程结束后仍在运行的子进程。孤儿进程将被 init 进程所收养。
没有被父进程回收的子进程。僵尸进程会造成内存泄露。
当一个子进程结束运行时,子进程的退出状态会返回给操作系统,系统则以 SIGCHLD 信号将子进程结束的事件告知父进程,此时子进程的 PCB 仍驻留在内存中。一般来说,收到 SIGCHLD 后,父进程会使用 wait/waitpid 系统调用以获取子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的 PCB;而如果父进程没有这么做的话,子进程的 PCB 就会一直驻留在内存中,即成为僵尸进程。
使用 fork 函数,函数的返回值:如果返回值大于零,表明处于父进程上下文环境中,返回值是子进程的ID.如果返回值是零,表明处于子进程上下文环境中.其他返回值(小于零)表明调用 fork 函数出错,仍处于父进程上下文环境中。fork 函数被调用一次,但返回两次,两次的返回值不同,子进程的返回值是 0,父进程的返回值是新进程的进程 ID。
子进程在创建后和父进程同时执行,竞争系统资源,谁先谁后,取决于内核所使用调度算法。
copy-on-write,当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
好处:既能实现多进程资源的独立性,又能按需分配,提高内存空间的利用率。
操作系统设置了两种运行级别,即用户态和内核态,分别采用 CPU 的 R0 和 R3 两种特权级,来使用不同级别的指令集。
当进程/线程运行在用户空间时就处于用户态,运行在内核空间时就处于内核态。
用户进程不能直接执特权指令,只能发起使用特权指令的请求,然后由操作系统代替用户执行指令。
从用户态切换到内核态的三种途径:系统调用、中断、异常。
在 PSW(Program Status Word,程序状态字)中有1个二进制位用来控制 CPU 处于内核态还是用户态。
进程不区分用户进程和内核进程,而是说进程运行在用户态和内核态。
运行在用户态的进程不能直接访问内核空间的数据结构和程序。当我们在系统中执行一个进程时,大部分时间是运行在用户态的,在需要操作系统帮助完成某些没有权力完成的工作时就会切换到内核态执行(比如操作硬件)。
进程是程序在一个数据集合上的一次动态执行过程。在程序之上抽象出进程的概念,既能够实现程序的并发执行,也方便进行资源的分配和管理。
在内核空间中有一张进程表,包含进程ID和进程控制块信息。
进程有创建、就绪、运行、阻塞、结束五个基本状态,以及就绪挂起和阻塞挂起两个状态,总共七个状态。
不同状态的进程,通过链表的方式组织起来就成了状态队列,比如就绪队列、阻塞队列、运行队列等。
CPU 往往会在多个进程中切换运行,进程需要将信息保存在 PCB 中,之后就可以继续运行了,即进程的上下文切换。
进程运行在用户态或内核态,大部分时间都运行在用户态,但是不能直接访问内核空间,不具备对硬件的操作权限和部分 CPU 指令,所以在需要完成某些没有权利完成的工作时,需要切换到内核态由操作系统执行。