操作系统之进程与进程控制

时间:2021-03-29 20:52:50

一、进程概念

引子 程序运行在并发环境中的问题

(1)运行过程不确定
(2)结果不可再现

1.进程定义

  进程是程序在某个数据集合上的一次运行活动。数据集合是指软硬件环境,多个进程共存或共享的环境。

2.进程的特征

(1)动态性
  进程是程序的一次执行过程,动态产生且动态消亡;
(2)并发性
  进程同其他进程一起向前推进;
(3)异步性
  进程按照各自的速度向前推进(每一个进程按照自定逻辑,不考虑其他进程的运行,各自占用CPU);
(4)独立性
  进程是系统分配资源和调度CPU的单位(但是有了线程后,操作系统调度CPU的单位就变成了线程)。

3.进程与程序的区别

(1)进程是动态的:程序的一次执行过程;
(2)程序是静态的:一组指令的有序集合;
(3)进程是暂存的:在内存中短期驻留;
(4)程序是长存的:可以在存储介质上长期保存;
(5)一个程序可能有多个进程。

4.进程的分类

(1)按照使用资源的权限进行分类
  ①系统进程:系统内核相关的进程;
  ②用户进程:运行于用户态的进程。
(2)按照对CPU的依赖性进行分类
  ①CPU型进程:主要用于计算;
  ②I/O型进程:主要用于I/O操作。

二、进程状态

1.进程的状态

(1)运行状态(Running)
  进程已经占用CPU,在CPU上运行。
(2)就绪状态(Ready)
  具备运行条件但是由于没有CPU可用,所以暂时不能运行。
(3)阻塞状态(Block)也叫等待状态(Wait)
  由于等待某项服务完成或者等待某个信号而不能运行的状态,比如等待系统调用,I/O操作等等。

2.进程的三态模型

操作系统之进程与进程控制

 

(1)就绪->运行:进程调度;
(2)运行->就绪:时间片到或者被强占;
(3)运行->阻塞:请求服务后等待响应,或者等待某个信号的到来;
(4)阻塞->就绪:请求的服务已经完成,或者等待的信号已经到来。

3.进程的五态模型

操作系统之进程与进程控制

 

(1)新建状态
  用户向系统提交程序后,在进程建立之前的过程。
(2)终止状态
  进程撤出系统的过程。
4.Linux中的状态定义
(1)可运行态
  ①就绪:在就绪队列中等待调度;
  ②运行:正在运行。
(2)阻塞(等待)态
  ①浅度(可中断)阻塞:能被其他进程的信号或者时钟唤醒;
  ②深度(不可中断)阻塞:不能被其他进程通过信号和时钟唤醒,一般是用来请求文件服务,I/O服务,系统服务。
(3)僵死态
  进程终止运行,释放大部分资源。
(4)挂起态
  进程被挂起,暂停。可用于调试进程。

三、进程控制块(Process Control Block,PCB)

1.进程控制块的定义

(1)描述进程状态、资源、与相关进程关系的数据结构;
(2)PCB是进程的标志。对于操作系统来说,它通过PCB来感知和管理进程;
(3)进程创建时会建立PCB,进程撤出时会销毁PCB。

2.PCB中的基本成员

(1)name(ID):进程名称(标识符,0~32768的正整数);
(2)status:状态;
(3)next_pcb:指向下一个PCB的指针;
(4)start_addr:程序地址;
(5)p_priority:优先级;
(6)cpu status:现场保留区(堆栈);
(7)comminfo:进程通信;
(8)processfamily:家族;
(9)own resource:资源。
  不同的操作系统PCB成员会有所区别,但是一些基本成员都是相同的,可能只是命名上的区别而已。进程控制块应包含以下三类信息:标识信息、现场信息、控制信息。

3.Linux中的进程控制块

Linux的进程控制块一般包含如下信息:
(1)进程状态;
(2)调度信息;
(3)标识符;
(4)内部进程通信信息;
(5)链接信息;
(6)时间和计时器;
(7)文件系统;
(8)虚拟内存信息;
(9)处理器信息。

4.进程的切换

(1)进程的上下文

  Context,指进程运行环境,CPU环境(比如各个寄存器的取值)等等。进程的上下文由三部分组成:用户级上下文(程序、数据、共享存储区、用户栈,它们占用进程的虚拟地址空间)、寄存器上下文(由各个寄存器组成)、系统级上下文(PCB、核心栈等)。
(2)进程切换过程
  ①进程被从堆栈调度到CPU运行;
  ②进程被从CPU调度到堆栈暂停运行。

四、进程控制的概念

1.进程控制的概念

  在进程生存期间,对其全部行为的控制。典型的控制行为有创建进程、阻塞进程、唤醒进程、撤销进程。

2.进程控制行为之一——进程创建

(1)功能
  创建一个具有指定标识的进程。
(2)参数
  进程标识、优先级、进程起始地址、CPU初始状态、资源需求等等。
(3)创建进程的过程
  ①创建一个空白PCB;
  ②获得并赋予进程标识符ID;
  ③为进程分配空间;
  ④初始化PCB;
  ⑤把进程插入到相应的进程队列,一般放到就绪队列中。

3.进程控制行为之二——进程撤销

(1)功能
  撤销一个指定的进程,收回进程所占用的资源,撤销该进程的PCB。
(2)进程撤销的时机/时间
  ①正常结束;
  ②异常结束;
  ③外界因素。
(3)参数
  被撤销的进程名(ID)。
(4)进程撤销的过程
  ①在PCB队列中找到要撤销进程的PCB;
  ②获取该进程的状态;
  ③若该进程处于运行态,则立即终止该进程(先检查该进程是否有子进程,若有子进程,则先撤销子进程,递归执行此操作);
  ④释放进程占有的资源;
  ⑤将进程的PCB从PCB队列中移除。

4.进程控制行为之三——进程阻塞

(1)功能
  阻塞进程的执行。
(2)阻塞的时机/事件
  ①请求系统服务:操作系统不能立即满足进程的请求,导致进程不能满足运行条件;
  ②启动某种操作:进程启动某操作,阻塞等待该操作完成(比如进行I/O操作);
  ③新数据尚未到达:该进程要获得另外一个进程的中间结果,该进程需要等待另一个进程运算结束;
  ④进程正常结束:进程完成任务后,自我阻塞,等待新任务到达。
(3)参数
  阻塞原因。
(4)进程阻塞的实现
  ①停止运行;
  ②将PCB“运行态”改为“阻塞态”;
  ③出入相应原因的阻塞队列;
  ④转到调度程序(把系统控制权交给调度模块)。

5.进程控制行为之四——进程唤醒

(1)功能
  唤醒处于阻塞队列中的某个进程。
(2)引起唤醒的时机/事件
  ①系统服务满足进程要求;
  ②I/O事件完成;
  ③新数据到达;
  ④进程向系统或I/O提出新请求(服务)。
(3)参数
  进程ID。

6.进程控制原语

  由若干指令构成的具有特定功能的函数,具有原子性,其操作不可分割。以上四种进程控制行为(创建、撤销、阻塞、唤醒)都被封装为原语,以保证其运行的完整性。

五、Linux下的进程控制

1.创建进程

  创建进程的函数原型是pid_t fork(void);
  pid_t是一个整数类型,即fork()函数会返回新进程的ID号(0~32768的整数)。
  例如:pid_t pid = fork();

注意:
(1)新进程是当前进程的子进程。
(2)父进程和子进程
  ①父进程:fork()的调用者;
  ②子进程:新建的进程。
(3)子进程是父进程的复制(相同的代码,相同的数据,相同的堆栈),除了ID号和时间信息外,两者完全相同。
(4)子进程和父进程可以并发运行。

请问下面的程序会输出什么结果?

//文件名为test.c
int main(void)
{
    fork();
    printf("Hello World!\n");

    return 0;
}

 

答案:

  操作系统之进程与进程控制

  一个是子进程输出的,一个是父进程输出的。

请问下面的程序会输出什么结果?

//文件名为fork_2.c
int main(void)
{
    pid_t pid;
    pid = fork();
    
    if(pid == 0)
    {
        printf("pid == 0\n");
    }
    else
    {
        printf("pid != 0\n");
    }

    return 0;
}

 

答案:
  操作系统之进程与进程控制

  为什么if和else两个分支都被执行了呢?

  因为fork()函数执行后,创建了一个新的进程,子进程是父进程的复制,所以相同的代码会执行两次。在子进程中fork()函数会返回0,在父进程中,fork()函数会返回子进程的id号。所以在子进程中,会执行if分支,在父进程中,会执行else分支。但谁先谁后不确定。

2.fork()函数的执行步骤

  由于子进程是父进程的复制,所以子进程中也会有创建子进程的语句,如果不加以限制,就会形成递归创建,但实际上并不是这样的。
  实际流程是:父进程创建了子进程后,子进程中“创建进程”语句及其前面的语句都不再执行。并发运行的是fork()语句后的语句。

   操作系统之进程与进程控制

 

在linux的源码中我们可以找到fork函数:

//linux内核中的fork函数
...
copy_files(clone_flags,p);    //克隆文件
copy_fs(clone_flags,p);    //克隆文件系统
copy_mm(clone_flags,p);    //克隆内存信息
...

  我们可以看到有三条语句,用于拷贝进程的所有信息,这也解释了为什么说子进程是父进程的复制。

3.子进程如何执行与父进程不同的功能。

  Linux中 ,init进程(初始化进程)是所有其他进程的父进程,那么是不是就说所有的进程都执行与init进程相同的功能呢?并不是。Linux中某些子进程和父进程的执行并不是完全相同的。它是如何做到的呢?这是因为exec函数簇的存在,它是若干函数的集合。其功能是让子进程具有和父进程完全不同的新功能。

4.一个进程控制的实例

  编写一个代码,创建两个子进程,第一个子进程打印“I am the 1st subprocess!”,第二个子进程打印“I am the 2rd subprocess”,父进程打印“I am the parent process”。要求实现先打印第一个子进程的内容,再打印第二个子进程的内容,最后打印父进程的内容。

 1 int main()
 2 {
 3   int p1, p2; //进程ID
 4  
 5   p1 = fork();
 6   switch(p1)
 7   {
 8     case -1:  //进程创建失败时执行下列语句,因为进程创建失败fork函数会返回-1
 9         printf("Error\n");  
10         exit(1);
11     case 0:  //子进程中执行下列语句,因为在子进程中fork函数会返回0
12         execl("/bin/echo","echo", "I am the 1st subprocess!", NULL);  //使用exec函数簇中的execl函数将子进程一的功能进行改造
13         exit(1);
14     default:  //父进程中执行下列语句,因为在父进程中fork函数会返回子进程的ID号(大于0,小于32768)
15         wait(NULL);  //用于进程同步,wait函数的作用是:父进程在此处暂停运行,等待一个子进程结束后,再从此处继续向下运行
16         break;
17   }
18  
19   p2 = fork();
20   switch(p2)
21   {
22     case -1:  //进程创建失败时执行下列语句,因为进程创建失败fork函数会返回-1
23         printf("Error\n");
24         exit(1);
25     case 0:  //子进程中执行下列语句,因为在子进程中fork函数会返回0
26         execl("/bin/echo", "echo", "I am the 2rd subprocess", NULL);  //使用exec函数簇中的execl函数将子进程二的功能进行改造
27         exit(1);
28     default:  //父进程中执行下列语句,因为在父进程中fork函数会返回子进程的ID号(大于0,小于32768)
29         wait(NULL);  //用于进程同步,wait函数的作用是:父进程在此处暂停运行,等待一个子进程结束后,再从此处继续向下运行
30         break;
31   }
32  
33   printf("I am the parent process\n");
34  
35   return 0;
36 }

 

运行结果:

  操作系统之进程与进程控制