当前位置: 首页 > news >正文

【Linux】进程状态、进程优先级、进程切换和调度

文章目录

  • 前言
  • 一. 冯诺依曼体系结构
  • 二. 操作系统
  • 三. 进程
    • 3.1 基本概念与基本操作
    • 3.2 查看进程
    • 3.3 进程标识符pid
    • 3.4 创建进程
    • 3.5 查看系统文件
  • 四. 进程状态
    • 4.1 进程状态说明
    • 4.2 进程状态查看
    • 4.3 睡眠和暂停状态
    • 4.4 僵尸进程
    • 4.5 孤儿进程
  • 五. 进程优先级
    • 5.1 基本概念
    • 5.2 查看系统进程
    • 5.3 PRI 和 NI
    • 5.4 修改进程的优先级
    • 5.5 理解竞争、独立、并行、并发
  • 六. 进程切换
  • 最后


前言

在上一篇文章中,我们详细介绍了版本控制器Git和调试器—gdb/cgdb的使用的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解进程、进程状态、进程优先级和进程切换的内容,接下来一起看看吧!


一. 冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

在这里插入图片描述
截至目前,我们所认识的计算机,都是由一个一个的硬件组件组成:

  • 输入单元:包括键盘、鼠标、扫描仪、写板等
  • 中央处理器(CPU):含有运算器和控制器等
  • 输出单元:显示器、打印机等

强调

  • 这里的存储器指的是内存
  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取
  • 所有设备都只能直接和内存打交道

重点CPU只能在内存中读取和写入数据,我们的程序要想在CPU中运行,那就必须要先加载到内存中

二. 操作系统

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。操作系统包括:

  • 内核(进程管理、内存管理、文件管理、驱动管理)
  • 其他程序(例如函数库、shell程序等等)

在这里插入图片描述
设计OS的目的:

  • 对下,与硬件交互,管理所有的软硬件资源
  • 对上,为用户程序(应用程序)提供一个良好的执行环境

在这里插入图片描述
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

计算机管理硬件:

  1. 描述起来,用struct结构体
  2. 组织起来,用链表或其他高效的数据结构

总结先描述、再组织

系统调用和库函数概念

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

那么操作系统是怎么管理进行进程管理的呢?

先把进程描述起来,再把进程组织起来!

三. 进程

3.1 基本概念与基本操作

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间、内存)的实体
  • 当前:进程=内核数据结构(task_struct)+ 自己的程序代码和数据

描述进程-PCB

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

在Linux中描述进程的结构体叫做task_struct
task_struct是Linux内核的一种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息。

task_struct
内容分类:

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

组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct双链表的形式存在内核里。
在这里插入图片描述

3.2 查看进程

  1. 进程的信息可以通过 /proc 系统文件夹查看

在这里插入图片描述

  1. 我们可以通过指令topps来查看进程信息

top
在这里插入图片描述
q退出

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{while(1){sleep(1);}return 0;
}

在这里插入图片描述
在一个窗口里面执行该程序
在这里插入图片描述
在另一个窗口里查看该进程的信息

ps -axj:查看所有进程信息;
ps - axj | head -1:查看第一行信息(第一行是显示每一列指的是什么)
ps - axj | grep test:在ps - axj查看的所有进程信息中,搜索test
&&:使用&&可以依次执行多个指令

如果我们不想让一个程序继续执行下去,想终止程序了,按下Ctrl+c即可,它的作用就是终止进程,我们也可以使用kill指令(kill -9 pid)来杀死进程。

在这里插入图片描述
在这里插入图片描述

3.3 进程标识符pid

进程标识符对于进程来说是唯一的,和其他进程是有区别的;也就是说每一个进程它的pid都不相同。

  • 进程id(PID)
  • 父进程id(PPID)

getpid():获取当前进程的pid

我们可以通过man getpid来查看man手册中 的getpid。

在这里插入图片描述

我们可以通过getppid来获得父进程的pid。

在这里插入图片描述

可以看到查询的是2号手册,这表明这里的getpidgetppid是一个系统调用。
使用时需要包含头文件<sys/types.h><unistd.h>

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{printf("pid: %d\n", getpid());printf("ppid: %d\n", getppid());return 0;
}

在这里插入图片描述

3.4 创建进程

我们可以使用系统调用fork来在程序运行过程中创建进程。

可以查看man手册来了解一下fork

在这里插入图片描述

#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    
int main()    
{    fork();                                                                                                                                                                         printf("pid: %d, ppid: %d\n", getpid(), getppid());    return 0;    
}

在这里插入图片描述
根据结果可以看到代码被执行了两次,分别是父进程和子进程。pid为22510的就是父进程,pid为22511的就是子进程。

对于fork的返回值:如果创建子进程成功,就返回子进程的pid给父进程,返回0给子进程;如果创建失败就返回-1给父进程,子进程未创建

这样我们是不是就可以根据fork的返回值来判断,当前进程是父进程还是子进程;然后进行分流,让父进程和子进程执行不同的代码。

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

在这里插入图片描述

在这里插入图片描述

循环查看进程信息的指令:

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

fork函数为什么会有两个返回值?

一个父进程它可以有多个子进程,而一个子进程只能有一个父进程;
那在父进程中,它怎么知道其子进程是哪一个进程呢?所以在父进程中要存储下来我们子进程的pid;
我们要实现父子进程的分流,那只能通过fork的返回值来判断父子进程。

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

在这里插入图片描述
根据运行结果可以看到:在子进程修改x时,是不会修改父进程的x的,虽然父子进程的代码和数据是共享的,但是当数据被修改时会发生写时拷贝,这样父子进程的数据就互不影响了

那么父进程的父进程20534又是什么呢?
在这里插入图片描述

我们可以看到20534这个进程其实是-bash

在Linux系统中,bash是最常用的命令行解释器(shell);
它是用户与操作系统之间的接口,负责将我们的指令翻译成内核理解的指令
bashshell的一种实现,简单来说bash就是Linux中的命令行解释器;

3.5 查看系统文件

我们知道系统文件/proc里存放了所有进程信息,我们运行了一个程序,其中pid为2041,我们看看2041这个文件存放了什么
在这里插入图片描述
可以看到这个文件存放了很多东西:

在这里插入图片描述

我们重点看cwdexe

cwd:存储当前可执行程序的绝对路径(指向进程的当前工作目录)
exe:指向进程启动时使用的可执行文件的完整路径(进程由哪一个可执行程序启动)。

当我们执行程序,使用创建一个文件并且没有指定路径时,我们知道这个文件肯定默认存放在当前目录下的,那么程序如何知道当前路径?

所以cwd存储的就是当前进程的工作目录,这样程序就知道当前目录了。

exe中存储的是进程由哪一个可执行程序启动(进程启动时使用的可执行程序的完整路径)

如果可执行程序删除掉了,那么exe就会标红:

在这里插入图片描述

四. 进程状态

在这里插入图片描述

4.1 进程状态说明

进程状态详解如下:

操作系统层面的进程状态

  1. 运行状态(Running)
    进程正在CPU上执行,或已准备好执行、等待调度器分配CPU时间片。

  2. 就绪状态(Ready)
    进程具备运行条件,等待CPU资源分配,通常与运行状态统一为TASK_RUNNING状态。

  3. 阻塞状态(Blocked/Waiting)
    进程因等待某事件(如I/O操作完成、信号量释放)而暂停执行,进入睡眠状态。

  4. 挂起状态(Suspended)
    当系统资源不足时,操作系统将进程从内存换出到外存,进程处于挂起状态,分为就绪挂起阻塞挂起

Linux系统中的进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

下面的状态在kernel源代码里定义:

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

Linux内核中,进程状态更细致,使用不同的状态标识符表示:

状态标识状态名称描述
R运行状态(Running/Runnable)进程正在运行或处于就绪队列中,等待CPU调度。
S可中断睡眠状态(Interruptible Sleep)进程等待某事件(如I/O完成),可被信号唤醒。
D不可中断睡眠状态(Uninterruptible Sleep)进程等待硬件操作(如磁盘I/O),不响应任何信号,通常不可被杀死。
Z僵尸状态(Zombie)进程已终止,但其父进程尚未回收其资源,进程表项仍存在。
T停止状态(Stopped)进程被信号(如SIGSTOP)暂停,等待恢复信号(如SIGCONT)。
t追踪停止状态(Tracing Stopped)进程被调试器(如gdb)跟踪并暂停。
X死亡状态(Dead)进程即将被完全销毁,通常不可见。

进程状态转换

进程在其生命周期中,会根据系统事件和调度策略在不同状态之间转换:

  • 运行 → 阻塞:进程请求I/O或等待事件,主动放弃CPU。
  • 运行 → 就绪:时间片用完,调度器选择其他进程运行。
  • 阻塞 → 就绪:等待的事件发生,进程被唤醒。
  • 运行/就绪 → 挂起:系统资源紧张,进程被换出内存。
  • 挂起 → 就绪/阻塞:资源充足或事件触发,进程被换入内存。

特殊状态说明

  • 僵尸进程(Zombie)
    子进程已结束,但父进程未调用wait()waitpid()回收其信息,导致进程表项残留。

  • 不可中断睡眠(D状态)
    进程处于内核关键路径(如磁盘I/O),为避免数据不一致,不响应任何信号,通常需要等待I/O完成或重启系统解决。

  • 挂起状态
    当内存不足或系统负载过高时,操作系统将部分进程换出到外存,以释放内存资源。

4.2 进程状态查看

  • 查看进程状态
    使用ps命令,如ps auxps -el,可查看进程当前状态标识。

  • 分析D状态进程
    通过/proc/<pid>/wchan查看进程等待的内核函数,或使用cat /proc/<pid>/stack查看内核堆栈。

  • 处理僵尸进程
    找到其父进程,确保父进程正确调用wait(),或终止父进程使僵尸进程被init进程收养并回收。

ps aux / ps axj 命令
  • a:显示一个终端所有的进程,包括其它用户的进程。
  • x:显示没有控制终端的进程,例如后台运行的守护进程。
  • j:显示进程归属的进程组ID、会话ID、父进程ID、以及与作业控制相关的信息。
  • u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。

在这里插入图片描述

4.3 睡眠和暂停状态

S状态为可中断睡眠状态,意味着进程在等待事件完成;D状态为不可中断睡眠状态,即磁盘休眠状态,这个状态的进程通常会等待IO的结束。

大部分的进程都是处于S状态的;因为我们的CPU资源有限,如果所有的进程都处于运行态,那CPU会响应不过来。

模拟一下S状态:

#include <stdio.h>                                                                                                                                                                                        int main()    
{    int x;    scanf("%d",&x);    return 0;    
}

在这里插入图片描述
在这里插入图片描述

T状态为停止状态,等待恢复信号;t状态为追踪停止状态,进程被调试器(如gdb)跟踪并暂停。

kill -l:可以查看所有kill指令的参数选项与对应信号效果
kill -19 [pid]:暂停对应进程
kill -18 [pid]:让对应进程继续运行

模拟一下T状态:
在这里插入图片描述

在这里插入图片描述

模拟一下t状态:
在这里插入图片描述
在这里插入图片描述

补充:

问题1:Linux下的进程状态S和S+有什么不同吗?

有区别“+”号表示“前台进程组”属性,不是另一种基本状态,而是附加在状态字母后的修饰标记。

状态含义
S可中断睡眠(sleeping),即进程正在等待某事件,可被信号唤醒,但属于后台进程组;终端里 Ctrl-C 杀不死它,也收不到终端输入
S+同样是可中断睡眠,但处于前台进程组
① 会占用当前终端,Shell 必须等它结束才返回提示符;
② 能收到终端产生的信号(如 Ctrl-C 可中断);
③ 在 ps 的 STAT 列里用 S+ 表示

因此看到 S+ 时,可以理解为“正在终端前台等待某事件,可中断”;而 S 则是一般后台(或非终端)睡眠进程。其他字母(R、D、T…)带不带 + 也遵循同样规则:有 + 就是前台进程组,无 + 则不是 。

问题2:怎么将进程状态S+变为S?

在 Linux 里,把 S+(前台睡眠)变成 S(后台睡眠)的本质是 让该进程脱离“前台进程组”
最常用、最安全的做法就是 在 Shell 里把它“放后台”。下面给出几种常见场景,按由简到繁排序,任选其一即可。


  1. 临时挂起 + 后台继续(已经启动的情况)
    Ctrl-Z          # 把前台进程暂停(状态瞬间变成 T)
    bg              # Shell 把它切到后台继续运行(状态变成 S)
    
    此时进程不再占用终端,Shell 立即返回提示符;ps 里 STAT 列的 + 消失,只剩 S

  1. 启动时直接后台(还没启动的情况)
    ./your_program &
    
    末尾一个 & 就让进程从诞生起就处于后台,STAT 直接是 S(或者 R)。

小结
只要让进程不再占用当前终端的“前台进程组”,+ 号就会消失;S+S 最常见、最无需改代码的做法就是:
Ctrl-Z 挂起 → bg 放回后台

演示:
Ctrl-Z挂起
在这里插入图片描述
可以看到进程状态由S+变为T
在这里插入图片描述
再输入bg将该进程放回后台执行
在这里插入图片描述
此时进程状态由T变为S,表明该进程属于后台进程组
在这里插入图片描述

此时想要Ctrl+c杀死进程,发现不行了,因为Ctrl+c杀死不了后台进程
在这里插入图片描述

我们可以手动输入kill -19 [pid]来杀死后台进程
在这里插入图片描述
此时后台进程就被杀死了
在这里插入图片描述

4.4 僵尸进程

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

注意:进程处于僵尸状态,它并没有被完全销毁,操作系统只是释放了进程的所有资源,并没有销毁进程的task_struct

例子:

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

在这里插入图片描述
在这里插入图片描述

僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。父进程如果一直不读取,那子进程就一直处于Z状态
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

4.5 孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z状态后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程
  • 孤儿进程被1号init/systemd进程领养,当然要有init/systemd进程回收

例子:

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

在这里插入图片描述
在这里插入图片描述
父进程先运行结束,子进程就变成了孤儿进程

父进程退出后,子进程的父进程就变成了1(1号进程可以理解为操作系统)

在这里插入图片描述

孤儿进程的危害

  • 占用系统资源,孤儿进程被 init 进程接管后不会自动退出,会持续占用 PID、内存、文件描述符等资源,累积过多会耗尽资源。
  • 拖慢系统性能,大量孤儿进程会增加 CPU 调度开销和内存占用,导致系统响应变慢、负载升高,严重时引发卡顿或崩溃。
  • 隐藏程序异常,孤儿进程的出现往往是父进程异常退出(如崩溃、未正确回收子进程)导致的,若忽视可能遗漏程序的逻辑漏洞或运行故障。

五. 进程优先级

5.1 基本概念

  • CPU资源分配的先后顺序,就是指进程的优先权(priority)
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能。
  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

为什么要存在进程优先级?

CPU资源有限,需要通过优先级确定运行的先后顺序

5.2 查看系统进程

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

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

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

在使用ls -l时,它会显示出来文件的拥有者、所属组,但都是以用户名的形式显示出来的;

我们可以通过ls -ln命令,查看文件时将用户名以数字形式(UID)显示出来

在这里插入图片描述

5.3 PRI 和 NI

进程优先级为什么要用这两个值来表示呢?

  • PRI:表示进程的优先级,默认是80
  • NI:表示进程优先级的修正数值,称为这个进程的nice
  1. PRI的值越小,进程的优先级越高
  2. 加入nice值后,PRI变为:PRI(new) = PRI(old)+nice
  3. nice值为负值的时候,那么该进程的优先级值将变小,即优先级会变高,则越快被执行
  4. 在Linux下,调整进程优先级,就是调整进程的nice
  5. nice值的取值范围为:-20至19,一共40个级别

需要强调一点的是,进程的nice值不是进程的优先级,它们不是一个概念,但是进程的nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正数据。

5.4 修改进程的优先级

我们修改进程优先级,并不是直接修改进程的优先级值,而是通过修改nice值来改变进程的优先级

top命令更改已存在进程的nice

  • 输入top指令
  • 进入top后按"r",输入进程PID,输入修改后的nice

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
修改nice值为10后,PRI值就变为 80 + 10 = 90

修改过一次进程的NI值之后,再次修改会发现,无法进行修改了;
这是因为操作系统为了防止恶意修改进程优先级,只允许普通用户对进程修改一次优先级;
root超级用户可以多次修改进程优先级。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

nicerenice

除了使用top来修改进程的NI值之外,我们还可以使用nicerenice来修改

nice

nice -n 10 ./code

这个指令就是在./code程序运行时,将nice修改为10

在这里插入图片描述
在这里插入图片描述

renice

nice指令是在程序启动时(创建进程时),就给定NI值;而renice指令则是修改已有进程的NI值,从而修改进程优先级

renice -n NI-p PID

在这里插入图片描述
在这里插入图片描述

我们还可以通过系统调用,在代码中查看和修改进程的优先级

getpriority:获取当前进程的优先级;
setpriority:修改当前进程的优先级。

在这里插入图片描述

进程优先级的范围:

NI值的取值范围: [-20,19](一共40个数据)
进程优先级的范围:[60 , 99](一共40个优先级)

在这里插入图片描述

在这里插入图片描述

5.5 理解竞争、独立、并行、并发

  • 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

六. 进程切换

CPU上下文切换:其实际含义是任务切换,或者CPU寄存器切换。当多任务内核决定运行另外的任务时,它保存正在运行任务的当前状态,也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中,入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器,并开始下一个任务的运行,这一过程就是context switch。

在这里插入图片描述
参考一下Linux内核0.11代码:
在这里插入图片描述

注意

时间片:当代计算机都是分时操作系统,每个进程都有它合适的时间片(其实就是一个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来。

在这里插入图片描述

一个CPU拥有一个runqueue

  • 如果有多个CPU就要考虑进程个数的负载均衡问题

优先级

  • 普通优先级:100~139
  • 实时优先级:0~99

活动队列

  • 时间片还没有结束的所有进程都按照优先级放在该队列
  • nr_active:总共有多少个运行状态的进程
  • queue[140]:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
  • 从该结构中,选择一个最合适的进程,过程是怎么的呢?
  1. 从0下表开始遍历queue[140]
  2. 找到第一个非空队列,该队列必定为优先级最高的队列
  3. 拿到选中队列的第一个进程,开始运行,调度完成!
  4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
  • bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

在这里插入图片描述

过期队列

  • 过期队列和活动队列结构一模一样
  • 过期队列上放置的进程,都是时间片耗尽的进程
  • 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

active指针和expired指针

  • active指针永远指向活动队列
  • expired指针永远指向过期队列
  • 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
  • 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!

总结

  • 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!
struct rq {spinlock_t lock;/** nr_running and cpu_load should be in the same cacheline because* remote CPUs use both these fields when doing load calculation.*/unsigned long nr_running;unsigned long raw_weighted_load;
#ifdef CONFIG_SMPunsigned long cpu_load[3];
#endifunsigned long long nr_switches;/** This is part of a global counter where only the total sum* over all CPUs matters. A task can increase this counter on* one CPU and if it got migrated afterwards it may decrease* it on another CPU. Always updated under the runqueue lock:*/unsigned long nr_uninterruptible;unsigned long expired_timestamp;unsigned long long timestamp_last_tick;struct task_struct *curr, *idle;struct mm_struct *prev_mm;struct prio_array *active, *expired, arrays[2];int best_expired_prio;atomic_t nr_iowait;
#ifdef CONFIG_SMPstruct sched_domain *sd;/* For active balancing */int active_balance;int push_cpu;struct task_struct *migration_thread;struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS/* latency stats */struct sched_info rq_sched_info;/* sys_sched_yield() stats */unsigned long yld_exp_empty;unsigned long yld_act_empty;unsigned long yld_both_empty;unsigned long yld_cnt;/* schedule() stats */unsigned long sched_switch;unsigned long sched_cnt;unsigned long sched_goidle;/* try_to_wake_up() stats */unsigned long ttwu_cnt;unsigned long ttwu_local;
#endifstruct lock_class_key rq_lock_key;
};
/*
* These are the runqueue data structures:
*/
struct prio_array {unsigned int nr_active;DECLARE_BITMAP(bitmap, MAX_PRIO+1); /* include 1 bit for delimiter */struct list_head queue[MAX_PRIO];
};

最后

本篇关于进程、进程状态、进程优先级和进程切换的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!

http://www.dtcms.com/a/618213.html

相关文章:

  • 【Android】View 的工作原理
  • 行人跌倒智能检测系统:YOLOv8/V5/V6/V7 多模型 + PySide6 界面 深度学习 多场景适配 大数据 (建议收藏)✅
  • 山东网络推广图片福州seo网站管理
  • C#中Task的详细用法
  • 自己怎么做企业网站建设免费代理服务器ip地址
  • 前端 css selector 的层叠 优先级与继承
  • 基于python二手房数据分析系统 可视化 Scrapy 爬虫 链家二手房数据 Django框架 基于用户的协同过滤推荐 二手房推荐系统 (源码)✅
  • Rust 内部可变性的访问器模式
  • ThinkPHP8学习篇(十二):模型关联(二)
  • 药品行业做网站windows wordpress
  • 【读代码】LightRAG轻量级知识图谱增强检索系统的架构与实现
  • arm架构设备使用FISCO BCOS上搭建多机区块链网络
  • 【Android】LRU 与 Android 缓存策略
  • 使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 26--数据驱动--参数化处理 Excel 文件 3
  • 第41节:第三阶段总结:打造一个AR家具摆放应用
  • 建设网站流程2022年最新新闻播报稿件
  • 网站地图的作用长沙网站开发设计
  • 【读代码】最新端侧TTS模型NeuTTS-Air
  • 做装修网站多少钱四川成都住建局官网
  • Microsoft 远程桌面app,支持挂机宝,云主机服务器
  • 基于MATLAB的粒子群优化(PSO)算法对25杆桁架结构进行优化设计
  • 智能驾驶:从感知到规控的自动驾驶系统全解析
  • 练习项目:基于 LangGraph 和 MCP 服务器的本地语音助手
  • 在 VMware 的 Ubuntu 22.04 虚拟机和 Windows 主机之间设置共享剪贴板
  • 淄博专业网站建设哪家专业公司装修设计工程
  • 金融网站的设计中和阗盛工程建设有限公司网站
  • 《JavaScript基础-Day.4》笔记总结
  • 关于C++中的预编译指令
  • 做网站的重要性深圳程序开发
  • 其他落地手册:facebook实现与音视频剖析