操作系统学习笔记_03_进程的概念与操作

本文深入解析进程的概念,探讨进程与程序的区别,详细阐述进程的状态、内存管理及操作系统如何通过进程控制块(PCB)进行进程调度。同时,文章介绍了进程创建、识别、执行和终止等操作,以及fork()和exec*()系统调用在UNIX系统中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.进程的概念和状态

进程的概念常常和程序糅合在一起,但两者实际是不一样的。首先,“进程”所包含的内容比“程序”丰富。“程序”这个概念可以用“一段代码”来概括,这段代码可能由不同的语言书写,它有可能是可以直接执行的(比如机器码),有可能是可以直接被解释器执行的(比如MATLAB代码),也有可能暂时不能被执行,需要编译成机器码之后才能执行(比如C++代码)。项目是由代码包装而成的可执行文件。例如,C语言代码在gcc环境下编译形成项目由如下步骤组成:C语言代码经过前端编译之后形成汇编代码,编译器后端将汇编码翻译为可执行的机器码,然后机器码依托其他文件(如库文件等)包装成为可执行的项目。在项目被执行时,项目将被装入内存。此时,项目的载体称为进程,执行进程就是对项目的内容进行解读,并按照内容执行操作。一种狭义的说法称进程为“正在执行的一段代码”,实际上进程信息的载体不仅包括执行代码(文本段),还有程序计数器和处理器寄存器(保存当前活动状态)、进程栈(包括临时数据)、数据段(包括全局变量)和堆(进程运行期间动态分配的内存)等,这些信息在进程执行时都存储在内存中。单独从项目与代码、进程对应的角度来看,项目可能由一段或多段代码生成,也可能申请一个或多个进程来执行项目,他们之间都没有一一对应的关系,所以上面的说法是不准确的。这种说法的可取之处在于指出了进程动态的特点:代码和指令是静态的操作,相当于工具;进程通过程序计数器取下一条执行的代码或下一项执行的指令,相当于工具的使用者。

内存中的进程

在进程执行时,操作系统为进程规定了不同的状态,以便于操作系统对进程进行管理和维护。这些状态包括新建(进程正在被创建),运行(正在执行指令),等待(等待某事件发生,比如收到I/O事件或其他事件的信号),就绪(等待处理器分配)和终止(执行完毕)。在同一时刻,进程只能处于同一状态。因为处理器的数目总是有限,每个处理器上每时每刻都只有一个进程处于运行状态,而其他很多的进程都处于就绪或等待状态。

进程的相关信息在操作系统中被保存在进程控制块(下文简称PCB)中。PCB是一种存储进程信息的数据结构,其内容包括进程状态、程序计数器、CPU寄存器、CPU调度信息、内存管理信息、流水信息和I/O信息等。进程的PCB保存在内核中。当进程因系统调用而中断执行时,其运行信息仍然由PCB保存。在进程恢复执行时,操作系统从PCB读取进程原先的状态,重新装入进程。从这个角度看,PCB相当于进程在内核中的映射:进程在中断时占用资源的状态原样保存在用户内存中,而执行状态保存在内核的PCB中,体现出内核控制进程、进程支配资源的层级结构。

CPU在进程之间的切换
以P0为例,CPU因为任何原因而要暂停执行当前进程时,就将进程信息存入PCB,然后暂停。在开始执行一个进程时,通过加载PCB中的信息来获取该进程的状态,以P1为例。

2.进程的操作

现代操作系统(以下以UNIX为例)要求通过进程来访问和利用资源,因此可以说所有的计算机操作都由进程完成,如文件操作、内存访问与读写和流水信息记录等。操作系统通常为进程提供识别、创建、执行和终止等操作,通过特定的系统调用来实现。

进程识别

不同的进程具有不同的进程控制符(下简称PID),在需要识别特定进程时,比对PID即可。为了实现这一点,操作系统在创建进程时赋予进程不同(unique)的PID,并通过系统调用管理:系统调用getpid()可用于获得进程的PID.

进程创建

进程的创建和管理通过维护进程树实现。在UNIX中,PID为1的init进程在装入系统时就产生并开始执行,其任务是为用户创建进程。正在执行的进程通过系统调用创建新进程,前者和后者分别称为父进程和子进程。新创建的进程还可以创建其他子进程,所以可以用进程树表示进程间的创建关系;init进程就是这棵树的根节点,是所有用户进程的根进程。然而,进程在内核存储中的实际组织方式并不是树,而是双向链表,其中每个进程都存储有自身父进程和全体子进程的PID.

用于创建进程的系统调用是fork().由fork()产生的子进程由原来进程的地址空间的副本组成,前者完全复制父进程的程序计数器、文件状态(进程打开的文件在内核结点中由列表存储,所以状态可以克隆)和程序代码,并获得独享的内存空间,将父进程在用户内存的资源完全拷贝过来。在内核中,子进程结点完全从父进程克隆过来,只改变PID、运行时间和继承关系。

除此之外,还需要系统调用exec*()辅助管理进程。exec*()是一族系统调用的总称,它们将当前进程映像替换成新的程序文件,所以exec*()不产生新进程,而是造成原进程内容的变化。具体而言,在进程调用exec*()时,进程转为执行参数所指示的程序。此时原来的程序代码不复存在(被新的指令代码替换),进程改为执行新的指令;原来的执行信息不复保留,内存和寄存器资源被重置。进程只保留原来的PID、继承关系和运行时间等信息,在新的指令执行完毕后直接终止。

此处额外说明一点:系统调用的设计理念就是尽量少进行硬件操作(如访问内存),因为所需时间长。所以上面介绍的fork()和exec*()在内核中的实际操作就是本着这样的理念设计的。

在系统实际运行时,fork()和exec*()经常搭配使用,以实现父进程中包含子进程的效果。为了实现上述操作,需要父进程执行wait()或waitpid()系统调用以暂停执行,在子进程通过exit()终止时方被唤醒,继续执行。具体而言,程序使用一个针对fork()的条件判断(父子进程中fork()的返回值不同)来使父子进程执行不同指令。父进程执行到wait()后建立一个信号接收器,之后保存状态进入等待。子进程在执行到exit()后释放所有资源,只保留最少的信息(PID、运行时间和退出信息),并发送SIGCHLD信号给父进程。父进程在接受信号后被唤醒(如果是由waitpid()产生的接收器则只接受特定子进程的信号),接收器默认将信号移除,并销毁发信的子进程。从用户的视角看,这种操作使得父进程在创建子进程后的某时刻挂起,待后者结束后按原样执行。另外,UNIX中默认先运行父进程,所以这种操作也提供了调换执行顺序的选项。如果子进程发送信号早于父进程建立接收器,则信号将被保留,待接收器建立之后立即被接收并执行销毁。

如果父进程wait()早于子进程结束,父进程就一直wait(),至子进程终止,父进程接收到信号,kill子进程

如果父进程wait()晚于子进程结束,子进程终止之后保留信号,父进程wait()时立即接收到信号,kill子进程

在一种特殊情况下,如果在子进程运行结束前父进程已经终止,子进程就会变成孤儿进程。这样做会使得子进程和进程树失去联系,使得操作系统无法控制其终止所带来的影响。UNIX为此提供了重定父址的操作,在每个进程终结时将其所有子进程树的根进程确定为init进程的子进程,即选择init进程作为这些孤儿进程的继父。init进程周期性地执行接收信号操作,保证这些僵尸进程(执行完的子进程)能及时被销毁。

参考文献:

[1] Abraham Silberschatz. 操作系统概念:Java实现:第7版:翻译版. 高等教育出版社, 2010.1;

[2] 韩其睿. 操作系统原理. 清华大学出版社, 2013.8.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值