【Linux系统】第八篇:Linux操作系统中的进程概念(冯诺依曼+操作系统+进程状态+进程优先级)

一、冯诺依曼体系结构(硬件方面)

冯·诺伊曼体系结构是现代计算机的基础,现在大多计算机仍是冯·诺伊曼计算机的组织结构。(下面是一张冯若依曼体系结构的图片)
在这里插入图片描述
解释:

  • 存储器: 对应的是我们电脑中的内存
  • 中央处理器CPU: 其中由运算器和控制器两个部分构成
  • 输入设备: 包括键盘、硬盘、鼠标等
  • 输出设备: 打印机、显示器等(输入设备和输出设备统称为外设)
  • 即是输入也是输出设备有:磁盘、网卡等。磁盘可以永久性存储数据

从上面这张图篇我们可以得出几点结论和cpu处理数据的过程:
1、外设并不是直接和CPU进行交互,而是先与内存进行交互,再与CPU进行交互,因为外设运行速度比较慢,CPU的运算速度是很快的,为了平衡二者之间的速度,会让CPU与介于二者运行速度之间的内存先进行交互。

2、读入数据时,输入设备将数据写入到中介内存中,然后内存把数据写入到CPU中,让CPU进行数据的处理,处理完后,CPU将数据写回到内存中,最后由内存把数据写出到输出设备中

总结:

CPU不和外设直接打交道,和内存直接打交道

举个例子:你用QQ和朋友聊天时数据的流动过程

要使用QQ,首先需要联网,假设你和你的朋友的电脑都是冯诺依曼体系结构,在你向朋友发送消息这个过程中,你的电脑当中的键盘充当输入设备、显示器和网卡充当输出设备,你朋友的电脑当中的网卡充当输入设备、显示器充当输出设备。
在这里插入图片描述

刚开始你在键盘当中输入消息,键盘将消息加载到内存,此时你的显示器就可以从内存获取消息进而显示在你自己的显示器上,此时你就能在你自己的电脑上看到你所发的消息了。
在键盘将消息加载到内存后,CPU从内存获取到消息后对消息进行各种封装,然后再将其写回内存,此时你的网卡就可以从内存获取已经封装好的消息,然后在网络当中经过一系列处理(这里忽略网络处理细节),之后你朋友的网卡从网络当中获取到你所发的消息后,将该消息加载到内存当中,你朋友的CPU再从内存当中获取消息并对消息进行解包操作,然后将解包好的消息写回内存,最后你朋友的显示器从内存当中获取消息并显示在他的电脑上。
在这里插入图片描述
注意: 同种设备在不同场景下可能属于输入设备,也可能属于输入设备。

了解上述一系列理论后,我们就可以明白一个问题:为什么程序运行之前必须先加载到内存?
因为可执行程序(文件)是在磁盘(外设)上的,而CPU只能从内存当中获取数据,所以必须先将磁盘上的数据加载到内存,也就是必须先将程序加载到内存。

二、操作系统(软件方面)

1、概念

简单理解: 操作系统是一个进行软硬件资源管理的软件。

在这里插入图片描述

2、设计操作系统的目的

  1. 对上(用户、程序员):给用户提供稳定、高效和安全的运行环境,为程序员提供各种基本功能(OS不信任任何用户,不让用户或程序员之间与硬件进行交互)
  2. 对下:管理好各种软硬件资源

3、定位

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

4、如何理解 “管理”

os完整体系构造

首先,我们肉眼可见的就是计算机实物,也就是计算机底层的硬件。这些硬件看似是一个个罗列出来的,但实际在底层都遵守冯诺依曼的组织形式。
在这里插入图片描述

而单单只有这些硬件是不够的,还需要有一个软件来对这些硬件进行管理。例如,内存何时从输入设备读取数据?读取多少数据?内存何时刷新缓冲区到输出设备?是按行刷新还是全刷新?这些都是由软件进行管理的,而这个软件就是操作系统(Operator System)。
在这里插入图片描述

此时这里有一个问题:难道操作系统直接和底层硬件打交道吗?
举个例子,如果操作系统自己来完成键盘的读取操作,那么只要你的键盘读取方式进行了改变,那么操作系统的内核源代码就需要进行重新编译,这对操作系统来说维护成本太高了。
于是我们又在操作系统与底层硬件之间增加了一层驱动层,驱动层的主要工作就是单独去控制底层硬件的。例如,键盘有键盘驱动,网卡有网卡驱动,硬盘有硬盘驱动,磁盘有磁盘驱动。驱动简单来说就是去访问某个硬件,访问这个硬件的读、写以及硬件当前的状态等等,驱动层就是直接和硬件打交道的。而驱动一般是由硬件制造厂商提供的,或是由操作系统相关的模块进行开发的(例如网卡)。此时操作系统就只需关心何时读取数据,而不用关心数据是如何读取的了,也就是完成了操作系统与硬件之间的解耦。

在这里插入图片描述

那操作系统究竟管理些什么呢?操作系统主要进行以下四项管理:

  1. 内存管理:内存分配、内存共享、内存保护以及内存扩张等等。
  2. 驱动管理:对计算机设备驱动驱动程序的分类、更新、删除等操作。
  3. 文件管理:文件存储空间的管理、目录管理、文件操作管理以及文件保护等等。
  4. 进程管理:其工作主要是进程的调度。

在这里插入图片描述

而操作系统再往上就是我们所处的位置,在这里我们就可以用命令行或是图形化界面进行各种操作,这一层被称为用户层。
在这里插入图片描述

但操作系统为了保护自己,对上只暴露了一些接口,而不会让用户直接访问操作系统,这一系列接口被称为系统调用接口。
在这里插入图片描述

但这些系统调用接口对我们普通用户来说使用成本又太高了,因为要使用系统调用前提条件是你得对系统有一定了解。所以在系统调用接口之上又构建出了一批库,例如libc和libc++。实际上在语言级别上使用的各种库,就是封装了系统调用接口的,我们就是通过调用这些库当中的各种函数(例如printf和scanf)进行各种程序的编写。

在这里插入图片描述

理解 “管理”

管理本质: 对数据做管理
管理方法: 先描述,在组织。

要想学好操作系统,那么就必须正确理解到底什么是管理。

首先区分决策与执行的区别

  1. 决策:就拿上述的操作系统层次结构来说,其中的操作系统就具有“决策权”,可以当管理者。
  2. 执行:上述的操作系统层次结构中,驱动就是执行操作系统发布的任务的。

接下来我们举一个实例来谈谈管理。就比如学校管理(大致概括)。
现给出三个角色:学生、辅导员、校长。很明显,校长在这三个人中是管理者,学生是被管理者,那辅导员充当什么角色呢?
在这里插入图片描述
思考一下,我们在完成一件事要经过两个过程,首先就是要决定这件事要不要做或者如何去做(做决策),然后就是去执行这件事情。

校长作为管理者可以给学校制定规则来管理学校,所以校长就是那个做决策的人,但是校长做出决策后并不需要自己去执行,而是让辅导员去执行,所以辅导员的主要任务就是执行管理者的决策,我们称其为执行者
在这里插入图片描述

虽然说校长是管理学生的,但是我们在学校一般情况下是看不到校长本人的,那么校长是如何做到在不看到我们的情况下对我们进行管理的呢?

就比如:现在校长要求辅导员统计学生的各科成绩和资料,当辅导员将学生资料拿来后,校长想办法选出前三名学生给予奖学金奖励。

在这个过程中,校长根本没有见过这三名同学,就对其进行了管理,他根据的是什么?没错,他根据的是数据。

实际上,学校将我们每个学生的各种信息都进行了管理,基本信息、成绩信息以及健康信息等等。
在这里插入图片描述

每这么一套信息就描述了一名学生,校长通过对这些信息的管理就能做到对学生的管理。这么一套信息在C语言当中我们称之为抽象结构体,而在C++当中又叫做面向对象。

当学生的数量多起来了,校长就可以将全部学生的信息组织起来,当然组织的方式有很多种(链表、顺序表、树),而每种组织方式都有其自己的优势,于是就有了一门课程专门教我们管理数据的方式,那就是数据结构。这里我们假设校长以链表的形式将学生的信息组织起来。
在这里插入图片描述

此时校长对各个学生的管理,实际上就变成了对这个链表的增删查改。当有新生时直接向该链表加入一个结点,当学生毕业后直接将学生信息从该链表当中移除即可。

5、系统调用和库函数概念

  1. 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  2. 系统调用在使用上,功能比较基础,但对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发,就比如我们经常使用的cin,cout等等。

三、进程管理

1、进程的概念

我们在书本上可以看到有关进程的概念有以下几点:

一个运行起来(加载到内存)的程序 - - - 进程
在内存中的程序 - - - 进程
进程和程序相比,进程具有动态属性

从以上几点看来,我们对进程的概念还是不大理解,也区分不了程序与进程的区别。

程序的本质:就是文件,在磁盘上放着。

那进程呢?
只要写过代码的都知道,当你的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。
在这里插入图片描述

2、描述进程- - -PCB

系统中可以同时存在大量进程,使用命令ps aux便可以显示系统当中存在的进程。
在这里插入图片描述

当我们开机的时候启动的第一个程序就是我们的操作系统(即操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?

这时我们就应该想到管理的六字真言:先描述,再组织。 操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
  • 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

task_struct-PCB的一种

进程控制块(PCB)是描述进程的,在C++当中我们称之为面向对象,而在C语言当中我们称之为结构体,既然Linux操作系统是用C语言进行编写的,那么Linux当中的进程控制块必定是用结构体来实现的。

  • PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。

task_ struct内容分类

task_struct就是Linux当中的进程控制块,task_struct当中主要包含以下信息:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
  • 其他信息。

既然进程信息被放在一个叫做进程控制块(PCB)中,那操作系统会将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以链表的形式组织起来。
在这里插入图片描述
这样一来,操作系统只要拿到这个链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条链表的一系列操作。
在这里插入图片描述
例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到链表当中。而退出一个进程实际上就是先将该进程的PCB从该链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。

总的来说,操作系统对进程的管理实际上就变成了对链表的增、删、查、改等操作。

从上述的讲解来说,我们可以知道进程是由什么组成的:

进程 = 内核数据结构(task_struct)+ 进程对应的磁盘代码和数据

3、查看进程

通过系统目录查看

进程信息可通过查看系统目录/proc的方式查看:
在这里插入图片描述
Linux 系统提供以查看文件的方式查看进程。每个进程创建之初都会在/proc目录下创建一个目录,并以进程编号命名。进程结束时自动消失。

通过ps命令查看

查看进程命令:

ps axj | grep myproc

能够显示出所有有关myproc的进程信息。
在这里插入图片描述

ps axj | head -1 && ps axj | grep myproc

这个指令可以带上进程的小标题。
在这里插入图片描述

4、通过系统调用获取进程标示符(PID/PPID)

先了解两个系统调用的函数:
作用:获取进程编号
在这里插入图片描述

子进程getpid()

getpid()获取子进程的进程编号。
我们可以通过一段代码来进行测试。

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>                              
int main()    
{
    
           
    printf("我是一个进程,我的ID:%d\n",getpid());
    sleep(1);             
    return 0;
}  

在这里插入图片描述

父进程getppid()

getpid()获取进程的父进程编号。

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>                              
int main()    
{
    
           
    printf("我是一个进程,我的ID:%d,PPID: %d\n",getpid(),getppid());
    sleep(1);             
    return 0;
}  

在这里插入图片描述

5、通过系统调用创建进程-fork初始

fork函数创建子进程

fork是一个系统调用级别的函数,其功能就是创建一个子进程。
在这里插入图片描述
先看下面一段代码:

#include <stdio.h>      
#include <unistd.h>      
   
int main()      
{
    
        
    fork();    
    printf("MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid());                                                                                                               
    return 0;                                 
}  

代码运行结果如下:
在这里插入图片描述
运行结果是打印两行数据,我们可以发现第二行的PPID与第一行打印的PID编号相同,也就是说这两个进程之间是父子关系,而且下面那个进程是子进程。

每出现一个进程,操作系统就会为其创建PCB,fork函数创建的进程也不例外。

我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?

我们看看以下代码的运行结果:
在这里插入图片描述
运行结果:
在这里插入图片描述
实际上,使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)。

小贴士: 使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。

使用if进行分流

上面说到,fork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么创建子进程就没有什么意义了。
实际上,在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事。

而使用if进行分流之前,我们需要先了解fork()函数的返回值。

fork函数的返回值:
在这里插入图片描述

1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
2、如果子进程创建失败,则在父进程中返回 -1。

既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。
例如,以下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    
        
                                                                    
    pid_t id=fork();                                                
    if(id==0)                                                       
    {
    
                                                                   
        //子进程    
        while(1)    
        {
    
                                                                                                                           
            printf("我是一个子进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);    
            sleep(1);
        }                                                                      
    }                                                                                                  
    else if(id>0)                                                                                      
    {
    
                                                                                                      
        //父进程                                                                                       
        while(1)                                                                                       
        {
    
                                                                                                  
            printf("我是一个父进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);      
            sleep(1);
        }                                                                                              
    }                                                                                                  
    else                                                                                               
    {
    
     
    }
    return 0;
}

fork创建出子进程后,子进程会进入到 if 语句的循环打印当中,而父进程会进入到 else if 语句的循环打印当中。
在这里插入图片描述

四、进程的状态

进程在不同的队列中,表示不同的状态。

Linux操作系统的源代码当中对于进程状态有如下定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
    
    
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。

在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。

1、运行状态-R

#include <stdio.h>    
int main()    
{
    
        
    while(1);                           
    return 0;    
}

在这里插入图片描述
一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。

小贴士: 所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。

状态后面有+号的表示前台进程,没有+号表示后台进程。前台进程在执行时,用户无法继续输入指令除非ctrl+c终止程序;后台进程在执行的过程中,用户可以输入指令且ctrl+c无法杀掉该进程。可以使用kill -9 PID的指令杀掉该进程。

2、浅度睡眠状态-S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。

例如执行以下代码:

#include <stdio.h>    
int main()    
{
    
        
    int a=0;    
    while(1)    
    {
    
        
        printf("%d\n",a++);               
    }                                  
    return 0;                          
} 

在这里插入图片描述
虽然数值一直在打印,但是printf函数需要访问显示器,大部分时间在等显示器IO就绪,只有小部分时间在执行打印代码。所以该代码呈现睡眠状态。
需要访问外设的,一般属于睡眠状态。

而处于浅度睡眠状态的进程是可以被杀掉的,我们可以使用kill命令将该进程杀掉。
在这里插入图片描述

3、深度睡眠状态-D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

4、暂停状态-T

可以输入kill -19 PID来让一个进程进入暂停状态。
在这里插入图片描述
T暂停状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。(kill -18 PID)
在这里插入图片描述
小贴士: 使用kill命令可以列出当前系统所支持的信号集。

kill -l

在这里插入图片描述

5、追踪暂停状态-t

我们在使用gdb对一个可执行文件调试的时候,程序运行到断点处,程序会进入t追踪暂停状态(tracing stop):表示该进程正在被追踪
在这里插入图片描述

6、死亡状态-X

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。

7、僵尸状态-Z

当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)。

首先,僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。

那么进程怎样进入僵尸状态?

模拟子进程正常退出,父进程不回收子进程(不读取子进程的返回信息)的场景(也可以使用kill -9 PID杀掉子进程):
在这里插入图片描述
通过下面命令循环打印进程信息

while :; do ps axj | head -1 && ps axj | grep mypro | grep -v grep; sleep 1; done

我们可以发现子进程已经变成了僵尸状态。旁边的译为死者,进程已经死亡,但是还未被回收,这就是僵尸状态。

僵尸进程的退出结果会写在PCB中,一个进程退出了,它的代码和数据会被释放,但是它的PCB是不会被释放,如果父进程不回收这块资源,那么会造成系统的内存泄漏。那我能不能手动杀掉这个僵尸进程来手动释放僵尸资源?不可以,因为僵尸进程已经死亡,无法手动杀掉进程。

在Z状态的进程被回收后,进程状态变为X死亡状态(dead):父进程读取完子进程的返回信息后,收尸速度太快了,我们看不到,进程死亡状态立马被它的父进程回收。

五、僵尸进程

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

前面也说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程。

例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    
    
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0)
	{
    
     
		//child
		int count = 5;
		while(count){
    
    
			printf("I am child process。PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){
    
     //father
		while(1)
		{
    
    
			printf("I am father process。PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else
	{
    
     //fork error}
	return 0;
} 

运行该代码后,我们可以通过以下监控脚本,每隔一秒对该进程的信息进行检测。

while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; done

在这里插入图片描述
僵尸进程的危害

  1. 僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
  2. 僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
  3. 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
  4. 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。

六、孤儿进程

在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

例如,对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    
    

    pid_t id = fork();
    if(id == 0)
    {
    
    
        while(1)
        {
    
            
        	printf("我是一个子进程:pid: %d, ppid: %d\n",getpid(),getppid());
	        sleep(2);
        }   
    }
    else if(id > 0)
    {
    
    
    	int count = 5;
        while(count)
        {
    
     
            printf("我是一个夫进程:pid: %d, ppid: %d\n",getpid(),getppid());       
            sleep(1);
            count--}
        printf("父进程退出.......\n");
        exit(1);
    }
	else{
    
     //fork error
	}
    return 0;
}

观察代码运行结果,在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了。
在这里插入图片描述

七、进程优先级

1、基本概念

什么是优先级?

优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。

优先级存在的原因?

优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。

2、查看系统进程

在Linux或者Unix操作系统中,用ps -l命令会类似输出以下几个内容:

ps -l

在这里插入图片描述
我们很容易注意到其中的几个重要信息,有下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

3、PRI与NI

  • PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
  • NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
  • PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
  • 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。
  • 调整进程优先级,在Linux下,就是调整进程的nice值。
  • NI的取值范围是-20至19,一共40个级别。
  • 所以Linux中进程的权限范围为【80-20,80+19】,数字越小,优先级越高。

注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。

4、查看进程优先级信息

当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。

ps -al

在这里插入图片描述
注意: 在Linux操作系统中,初始进程一般优先级PRI默认为80NI默认为0

5、通过top命令更改进程的nice值

top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。
在这里插入图片描述

使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID

在这里插入图片描述

输入进程PID并回车后,会要求你输入调整后的nice值。
在这里插入图片描述

输入nice值回车后按“q”即可退出,如果我们这里输入的nice值为15,那么此时我们再用ps命令查看进程的优先级信息,即可发现进程的NI变成了15,PRI变成了95(80+NI)。
在这里插入图片描述
我们这里修改的是子进程的nice值。

注意: 若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限。

[wyt@VM-20-4-centos test]$ sudo top

然后操作和上方一致,修改nice值时,填写负数。
在这里插入图片描述

6、通过renice命令更改进程的nice值

使用renice命令,后面跟上更改后的nice值和进程的PID即可。
在这里插入图片描述
可能看到这里我们有点疑惑,为什么开始是90,当我们修改nice值为-10后,新的PRI值是70,而不是80呢?其实修改nice值后,每次默认是从PRI的默认值80开始计算的。所以才为70.

八、四个重要概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。

  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。

  • 并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。

  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

什么是进程切换?
cpu中有一个eip寄存器(PC指针),指向下一条指令的地址。

进程在运行的时候,占有CPU,会产生很多的临时数据,归属于当前进程。虽然CPU内部仅有一套寄存器硬件,但是寄存器中保存的数据属于当前进程。(寄存器是共享的,但是数据是各进程私有的)

进程在运行的时候都有自己的时间片,这个时间一到,即使进程还没有被执行完毕,但是会被操作系统剥离CPU,腾出CPU让下一个进程上来跑一跑。

那么这个进程下次再回到CPU继续运行时,操作系统是如何知道这个进程的代码被执行到哪里了?

首先,进程在切换的时候,需要进行上下文保护,一些临时数据被保存至PCB里;进程在恢复运行的时候,要进行上下文的恢复,后续该进程回到CPU运行时,将加载这些数据。通过PC指针继续运行下一行代码。

猜你喜欢

转载自blog.csdn.net/m0_58124165/article/details/129134213