【Linux】玩转操作系统,深入刨析进程状态与调度机制

1. 进程排队

  1. 进程排队:指的是操作系统将等待资源的进程组织成队列,以便按照特定的调度策略来决定哪个进程可以获取资源并继续执行,即:将进程放入到等待资源的队列中进行排队。

进程排队,表示进程不是一直在运行的,是在等待某种软硬件"资源"。eg:scanf,等待键盘资源。

即使进程被调度到CPU上执行,它也不会一直运行下去。在多任务操作系统中,进程的执行是分时的,每个进程会分配到一定的时间片来执行,当时间片用完,操作系统会暂停运行当前进程。

  1. 进程排队,一定是进程的task_struct(PCB)进行排队。

  2. 进程的task_struct(PCB)可以放入到多种数据结构中。

进程的PCB既可以被放入到全局的双链表中,又可以被放入到特定的队列中。当进程被放入到全局的双链表中,OS会在进程的每个PCB中创建链表节点的对象;当进程被放入到特定的队列中,OS会在队列的数据结构中创建或复用双链表节点,在PCB中设置队列节点的链接。

image.png

2. 进程状态的表述

前言:教材中描述进程状态的模型和实际操作系统(如:Linux)中实现的进程状态模型可能会有差异。

2.1. 进程状态

  1. 进程状态:本质是一个整形类型的变量,它定义在task_struct结构体中,用来描述进程的不同的不同状态。
  2. 进程状态,决定着进程的后续操作和它在操作系统的行为。

image.png

2.2 运行状态

运行状态:进程已经准备好随时被调度。即:正在CPU上执行以及在运行队列中等待被调度的进程。

💡Tips:一个CPU一个运行队列。
屏幕截图 2024-07-20 104921.png

2.3. 阻塞状态

阻塞状态:进程暂时无法继续执行,在等待某种软硬件"资源"。

💡Tips:每个硬件都有自己的队列!

  1. 当进程在等待软硬件资源的时候,资源没有就绪,操作系统就会先将进程的task_struct设置为阻塞状态、再将task_struct链入到等待资源的等待队列中。

  2. 状态的变迁,引起的是PCB会被操作系统变迁到不同的队列中!
    image.png

  3. 硬件的就绪状态,本质是硬件的资源准备好执行任务的状态,操作系统作为硬件的管理者,负责监控和管理这些硬件资源的状态。

硬件资源就绪,对于键盘的输入,通常是指键盘的中断机制已触发,表明有新的输入数据已经准备好了。

键盘的输入处理流程如下:硬件资源就绪(键盘输入准备完毕)→ OS通过驱动程序将数据从外设(键盘)搬到内存(内核输入缓冲区)→ 用户程序通过系统调用接口(scanf)将数据从内存搬到用户空间。

2.4. 挂起状态

挂起状态:进程被暂时停止执行,它的状态被保存,以便在将来某个时刻恢复。分为内部挂起、外部挂起。

  1. 外部挂起:也称为阻塞挂起。当系统资源紧张,如:内存空间不足,OS会将进程的代码和数据保存到磁盘的swap分区,以释放内存供其他进程使用,此时进程状态为挂起状态。当内存资源变的可用时,OS会将对应的代码和数据重新加载到内存中,并将其状态由挂起变为运行,以便它可以被调度执行。

  2. 唤出过程:当内存资源紧张时,OS会将内存中数据(等待资源的进程的代码和数据)交换到磁盘的swap分区中,以释放内存空间,这个过程称为唤出。

  3. 唤入过程:当需要恢复执行此进程时,就会将磁盘的swap分区保存的内容回到内存中,使进程可以继续执行,这个过程称为唤出。

image.png

问1:创建一个新进程,是先创建进程的PCB,还是先把进程的代码和数据加载到内存中?

答:先创建进程的PCB,再把进程的代码和数据加载到内存中,确保了OS对进程的创建和管理。

问2:磁盘的swap分区的大小是越大越好吗?

答:不是,会导致性能下降、资源浪费等问题。与内存相比,磁盘速度慢的多,如果swap分区很大,会导致OS过度依赖它,而频繁的唤入唤出操作会显著降低系统的性能,因为磁盘的IO速度远低于内存的读写速度。

  • swap分区不能设置过大,通常大小等于内存大小或内存大小的一半。

3. 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 */
};

image.png

3.1. 运行状态R

运行状态R:进程正在运行或者准备运行。它可能正在CPU上执行,或者在运行队列中等待被调度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LUNW5u4E-1722041903277)(https://i-blog.csdnimg.cn/direct/c1daab5586244550a8fb60956666b8aa.png)]

  1. grep是单独指令,是程序,只有被运行,变成进程,它才会执行过滤操作,所以它的状态总是运行状态R。

  2. 前台进程(带+),可以被键盘(ctrl + c)终止;后台进程(不带+),不可以被键盘终止,可以通过发信号(kill -9 进程的PID),才可终止后台进程 ;前台进程 -> 后台进程:./程序 & 。

3.2. 可中断睡眠状态S

可中断睡眠S:也称为浅度睡眠或睡眠状态,意味着进程正在等待某个事件的完成,如: I/O操作、定时器到期(sleep函数)等,它可以被信号中断,如:ctrl + c、kill -9终止进程。

💡Tips:可中断睡眠睡眠S == 阻塞状态。

#include<stdio.h>
#include<unistd.h>
 
int main()
{
    while(1)
    {
        printf("I am a process, id: %d\n", getpid());
        sleep(1);
    }
}

在这里插入图片描述

问:为什么这个进程处于睡眠状态?

答:sleep函数会将进程由运行状态变为睡眠状态,等待指定的时间,进程会被OS唤醒。printf函数的执行速度通常是非常快的,因为CPU的执行速度很快,而printf是向显示器打印,需要访问外设,printf可能会等待I/O操作完成时使进程短暂的进入睡眠状态。

3.3. 不可中断睡眠状态D

不可中断睡眠D:也称为深度睡眠或磁盘休眠状态,它通常会等待硬件操作(如:磁盘I/O)完成,不能响应信号,直到它所等待的事件完成。

💡Tips:不可中断睡眠睡眠D == 阻塞状态。

eg:当一个进程发起磁盘的写入操作时,当内存资源十分吃紧,如果此进程处于睡眠状态S,OS就会将此进程杀掉,可能会导致数据丢失,所以此进程的状态被设置于不可中断睡眠状态D,OS不可以杀掉此进程,进程等待磁盘的结果以作出相应的应答。

3.4. 停止状态T

停止状态T:意味着进程已经被暂停或者正在被追踪,通常是因为接受到一个信号,或者是在调试器中被断点所拦截。

💡Tips:停止状态T == 阻塞状态。

  1. 可以通过发送SIGSTOP信号(kill -19)给进程来停止进程,通过发送SIGCONT信号(kill -18)让处于暂停状态的进程继续运行。

image.pngimage.png

3.5. 死亡状态X

死亡状态X:意味着进程已经终止,且资源已经被回收,它只是一个返回状态,不再存在于进程列表中。

3.6. 僵尸状态Z

僵尸状态:进程已经退出,但其父进程尚未调用wait( )来收集其退出信息。即: 进程是已退出但未清理的状态,等待父进程回收资源。

  1. 前提:子进程比父进程先退出。只要子进程退出了,父进程还在运行,但父进程没有读取子进程退出信息,子进程就进入Z状态,当父进程回收了子进程的资源,子进程由Z状态变为X状态。

处于僵尸状态的进程会一直保存在进程列表中。通常情况下,进程先变为Z状态,再变为X状态。

  1. 当一个Shell( bash )会话结束时,如果没有显示的调用wait( )回收子进程的资源,而Shell作为父进程,会自动调用wait( )来清理所有已经终止的子进程的资源,避免留下僵尸进程,后台进程除外。

  2. 当进程退出后,先会释放进程的代码和数据,进程的PCB不能被释放,因为它里面存储着结果数据,只有当父进程完成了资源的回收和状态的读取,PCB才会被释放。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<wait.h> 

int main()
{
    pid_t id = fork();
    if(id < 0) return 1;
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt--)
        {
            printf("I am child process, pid: %d, ppid:%d\n",getpid(    ),getppid()); 
            sleep(1);
        }
        exit(0);
    }
    else
    {
        //父进程
        int cnt = 10;
        while(cnt--)
        {
            printf("I am father process, pid: %d, ppid:%d\n",getpid    (),getppid());
            sleep(1);
        }
    }
    wait(NULL);
                                                                     
    printf("father wait child done...\n");
    sleep(5);
}
//现象:父子进程同时运行5s, 5s后子进程退出(变Z状态), 父进程在运行5s, 5s后父进程回收子进程资源(子进程由Z->X状态), 5s后父进程退出

a. 为什么要有僵尸状态Z?

  • 创建进程是希望这个进程执行特定任务,子进程在完成工作后,往往需要向父进程报告结果或者状态信息(子进程必须由结果数据保存在自己的PCB中) , 从而使父进程能够根据子进程的执行结果作出适当的响应。

b. 什么是僵尸状态Z?

  • 进程已经退出,但是当前进程的状态需要自己维持住,供父进程读取。

c. 如果父进程不读取子进程的退出数据呢?

  • 僵尸状态的进程会一直存在,而维护退出状态本质是用数据维护,存储在进程的PCB,所以task_struct对象也要一直存在,而数据结构对象本身就要占用内存,会造成内存泄漏。

4. 孤儿进程

孤儿进程:父进程比子进程先退出,子进程就称为"孤儿进程"。

  • 孤儿进程被PID为1的进程(操作系统)领养,由OS回收其资源。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

int main()
{
    pid_t id = fork();
    if(id < 0) return 1;
    else if(id == 0)
    {
        //子进程
        int cnt = 10;
        while(cnt--)
        {
            printf("I am child process, pid: %d, ppid:%d\n",getpid(    ),getppid()); 
            sleep(1);
        }
        exit(0);
    }
    else
    {
        //父进程
        int cnt = 5;
        while(cnt--)
        {
            printf("I am father process, pid: %d, ppid:%d\n",getpid    (),getppid());
            sleep(1);
        }
    }                                                       
}

5. 优先级

优先级:CPU资源分配的先后顺序。

  1. 前提:进程要访问某种资源,进程通过一定的方式(排队),确认享受资源的先后顺序。

  2. 为什么要存在优先级?因为资源相对过少。

  3. 优先级本质是一个整数类型的变量,数值越小,优先级越高,具有优先执行的权力。

  4. 优先级PRI(new) = 优先级PRI(old) + nice。

Linux系统允许用户调整进程的优先级,但不能直接修改pri,而是修改进程的nice值。

nice值:是进程优先级的修正数据,!=进程优先级;优先级PRI(old)为默认优先级(值为80)。

  1. Linux优先级默认值为80,优先级的范围为[60 , 99],nice值的范围为[-20,19],一共40个。

Linux为什么调整优先级是要受限制的?

答:如果不受限制,则用户能够随意提高自己的进程优先级调整的非常高,那么很可能他们会过度提升自己的进程,使其始终处于高优先级的状态,导致常规进程难以得到CPU资源,产生进程饥饿现象。

💡Tips:任何分时系统(基于时间片进行轮转执行),确保了进程调度的公平性和系统的稳定性!

ps -l

  • 功能:显示当前登录用户的所有进程的信息。

XQO_I_HJQOJ~HJ{N96`XFI5.png
UID:代表执行者的身份、PID:进程的唯一标识符、PPID:子进程的父进程的唯一标识符、PRI:进程的优先级、NI:进程优先级的修正数据。

top -> r -> 输入进程的PID -> 输入nice值

  • 功能:用top命令更改已存在进程的nice值,从而调整进程的优先级。

优先级.gif

renice nice 进程的PID

  • 功能:更改已存在进程的nice值,从而调整进程的优先级。

6. Linux的调度与切换

6.1. 四个概念

现代操作系统可以分成不同类别,分时操作系统和实时操作系统是两种重要的类型。

对于分时OS,进程都是基于时间片进行轮转执行的。每个就绪进程在轮到自己时,将会获得一个时间片来执行,如果一个进程在时间片结束时还没有执行完,它就会立即被暂停,CPU将被分配给下个进程,造成多个进程在"同时"运行的假象。

对于实时OS,采用的是优先级驱动的调度策略,要求OS对用户有超高响应,其中一旦高优先级的进程一旦就绪,将立即抢占CPU执行,如:车载系统。

  1. 竞争性:系统中进程的数量多、而CPU资源少量,所以进程之间对于共享资源是具有竞争属性的。

  2. 独立性:多进程在运行期间互不影响。每个进程在执行时都有自己的私有执行环境,包括代码、数据等资源,它们各自独享自己的执行资源。

  3. 并行:多个进程在多个CPU下,分别同时进行运行。

  4. 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,多个进程看起来同时执行。

6.2. 进程切换

进程切换 = 保护进程的硬件上下文 + 进程的硬件上下文恢复。是OS在多任务环境中实现进程间公平调度和并发执行的关键机制。

  1. 进程在运行的过程中,会产生大量的临时数据,小部分被存储在CPU的寄存器中,大部分被存储在内存中。

  2. 进程的硬件上下文:在某一时刻,CPU执行进程时,所有硬件状态信息的集合。如:CPU内部的寄存器中存储的所有临时数据等。

  3. 保存进程的硬件上下文:当一个进程的时间片结束、主动放弃CPU(等待某种资源scanf)、有更高优先级的进程就绪需要暂时停止执行,OS会保存该进程的硬件上下文到进程的PCB中。

  4. 进程的硬件上下文恢复:在保存完当前进程的上下文后,OS会选择下一个要运行的进程,如果该进程是第二次被调度,就会将该进程的PCB中上下文重新加载到CPU上,CPU会从上次的运行位置继续运行。

💡Tips:所有的保存都是为了最终的恢复,所有的恢复都是为了继续上次的运行位置继续执行。

  1. CPU中的寄存器从物理上是被所有进程共享,在任意时刻,CPU的寄存器由当前正在执行的进程独占使用,而寄存器内部保存的数据,是被该进程私有的。即:CPU中的寄存器只能有一套,寄存器内部保存的数据可以有多套。

💡Tips:寄存器 != 寄存器的内容。

6.3. 进程调度

在这里插入图片描述

背景:Linux的进程调度O(1)算法,需要考虑优先级、进程饥饿、以及效率等问题,这主要是通过是特定的数据结构和算法设计来实现的。

一、####数据结构

  1. queue[140]

这是个优先级数组(prio_array),它是一个长度为140的指针数组,每个元素包含一个链表头,该链表包含具有相同优先级的所有就绪进程,即:每个元素对应一个优先级队列,每个队列中的进程具有相同的优先级,它维护了140个优先级队列。

实时优先级:0~99,用于实时进程,以确保它们能够及时响应并完成任务。

普通优先级:100~139,通过nice值[-20,19]可以调整进程在该范围内的优先级,用于非实时进程,遵循时间片轮转调度策略,确保进程公平的共享CPU资源。image.png

  1. bitmap[5]

这是一个位图(prio_map),用于快速检测哪些优先级队列是非空的。它是一个5个整数的数组,每个整数有32个比特位,共有5*32=160个比特位 > 140,位图操作都是在比特级别进行的,时间复杂度为O(1),非常高效。

位图中的每个比特位的位置,对应着一个优先级队列;每个比特位的内容,表示这个队列是否为空。即:检测某个优先级队列中是否有进程,转为为检测对应比特位是否为1。

  1. nr_active

这是一个计数器,用于记录当前活跃进程队列中进程的总数。

struct q
{
    int nr_active;
    int bitmap[5];
    task_struct queue[140];
}

二、####算法实现

  1. 优先级

通过优先级数组和位图,调度器,可快速定位到最高优先级的非空队列。当需要调度下一个进程时,调度器首先会遍历位图,直到找到’第n位’为1才会停下,再访问queue数组中的下标值为’第n位’的元素,取出该队列中第一个进程进行运行。

  1. 避免进程饥饿

OS会将时间片耗尽的进程放到过期队列中,不让他们一直占用活跃队列的位置,确保其他进程被调度到CPU上执行;新增进程也会被加入到过期队列,不论这个新进程的优先级高低,避免了较高优先级的进程抢占CPU资源。而CPU只会执行活跃队列中的进程,当活跃队列上的进程都被执行完毕,两队列交换。

  1. 效率

因为bitmap[5]数组的存在,调度器只需要遍历bitmap[5]数组,而不是整个queue[140]数组,大大减少了遍历的次数。位图的查询操作时间复杂度为O(1),意味着无论多少个优先级队列,调度器只需要遍历bitmap[5],就可以快速找到非空优先级队列,大大提高了查找效率。

三、活跃进程队列、过期进程队列

  1. 活跃进程队列:包含了当前准备就绪,可以立即被执行的进程。

  2. 过期进程队列:包含了时间片耗尽,但未执行完毕的进程。

四、active指针、expired指针

  1. active指针永远指向活跃队列、expired指针永远指向过期队列。

  2. 进程的时间片一直存在,就会使活跃队列的进程一直在减少、过期队列的进程一直在增多。

struct q  
{
    int nr_active;
    int bitmap[5];
    task_struct queue[140];
}

struct q array[2]; 

struct q* active = &array[0];    //活跃队列
struct q* expired = &array[1];  //过期队列

swap(&active, &expired); //更改的只是指针变量的内容,队列内容并未改变

image.png

  • 105
    点赞
  • 92
    收藏
    觉得还不错? 一键收藏
  • 91
    评论
评论 91
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值