Linux进程切换以及调度算法

news2024/11/18 1:50:25

目录

Linux进程切换以及调度算法

Linux进程切换介绍

前置知识

进程切换过程分析

调度算法

补充知识


Linux进程切换以及调度算法

Linux进程切换介绍

前面提到,分时操作系统下,CPU会在多个进程中做高速切换以实现多个进程看似同时执行的,但是问题是为什么CPU可以做到多个进程来回切换而不会影响每一个进程的执行,这就是进程切换需要思考的问题

基本认知:CPU切换进程是根据每一个进程对应的时间片决定切换的时间,一旦时间片到了,CPU就会切换到另一个进程,如此往复。所以,在上面的过程中,就会出现一些进程并没有执行完毕但是CPU进行了切换

前置知识

在CPU中,有许多寄存器,每一种寄存器对应着自己的功能:

  1. 通用寄存器(General-Purpose Registers)
    • 这些寄存器可以用于多种用途,如存储中间计算结果、指针或整数数据等
  2. 状态寄存器(Status Registers)标志寄存器(Flag Registers)
    • 存储条件码或其他状态信息,如零标志、进位标志、溢出标志等,这些标志常用于决定程序分支或中断处理
  3. 程序计数器(Program Counter, PC)
    • 也称为指令指针(Instruction Pointer),它指向当前正在执行或即将执行的指令的位置
  4. 指令寄存器(Instruction Register, IR)
    • 暂存当前正在执行的指令
  5. 地址寄存器(Address Registers)
    • 用于存储内存地址
  6. 数据寄存器(Data Registers)
    • 用于暂存从内存读取的数据或写入内存的数据
  7. 段寄存器(Segment Registers)
    • 在某些架构中用于存储内存段的基址
  8. 控制寄存器(Control Registers)
    • 用于配置处理器的工作模式和其他控制功能
  9. 链接寄存器(Link Registers)返回地址寄存器(Return Address Register)
    • 用于保存返回地址,通常在函数调用期间使用

例如,下面的图中展示了部分寄存器的作用:

每一个CPU有自己的一套寄存器,而每一套寄存器并不会在进程切换时保存每一个进程切换前的数据,而正在被调度的进程在CPU寄存器里面的瞬时数据也被称为上下文数据

进程切换过程分析

以下面的图为例:

当前CPU正在执行一个进程,假设现在继续出现一个新的进程如下,此时状态如下:

如果下一刻操作系统切换进程为进程2执行,就会出现下面的情况:

进程2因为要执行,导致进程1执行的记录被抹除,如果此时下一次进程1被切换为继续执行就会找不到上一次执行的数据,为了防止出现这种问题,在Linux中,每一个PCB结构中会存在一个TSS结构,该结构用于存储被切换前的最后一步对应的上下文数据

有了TSS结构,就可以实现每一次进程切换时,尽管CPU的寄存器中的数据被下一个寄存器抹除,但是下一次执行之前被切换的进程依旧可以从被切换的那一瞬间对应的数据开始继续执行,整体大致执行思路如下图所示:

Linux最初源码中的TSS结构:

struct tss_struct {
    long    back_link;    /* 16 high bits zero */
    long    esp0;
    long    ss0;        /* 16 high bits zero */
    long    esp1;
    long    ss1;        /* 16 high bits zero */
    long    esp2;
    long    ss2;        /* 16 high bits zero */
    long    cr3;
    long    eip;
    long    eflags;
    long    eax,ecx,edx,ebx;
    long    esp;
    long    ebp;
    long    esi;
    long    edi;
    long    es;        /* 16 high bits zero */
    long    cs;        /* 16 high bits zero */
    long    ss;        /* 16 high bits zero */
    long    ds;        /* 16 high bits zero */
    long    fs;        /* 16 high bits zero */
    long    gs;        /* 16 high bits zero */
    long    ldt;        /* 16 high bits zero */
    long    trace_bitmap;    /* bits: trace 0, bitmap 16-31 */
    struct i387_struct i387;
};

调度算法

在Linux中,前面提到了每一个进程PCB是用双向链表链接的,但是如果只是使用双向链表结构作为调度队列,那么CPU在每一次调度时都需要从前往后遍历链表,其时间复杂度就是$O(N)$。实际上,在操作系统下,如果一个算法的时间复杂度超过了$O(N)$就已经算效率比较低的,所以为了降低时间复杂度,在调度队列中,除了有双向链表外,还使用一个类似于哈希表的结构,而每一个进程PCB就是哈希桶的节点,哈希表结构示意图如下:

当用户创建一个进程后,就会链接到后面40个空间的其中一个空间下面,这里也就解释了为什么前面进程优先级部分NI值的区间为[-20, 19],一共40个可选值,而根据进程优先级的计算公式:PRI=初始PRI+NI,可以得出,进程优先级PRI的区间为[-60, 99],所以真实的链接位置下标计算公式可以理解为:进程优先级- 60 + 100

假设现在一个进程的优先级为61,则链接位置下标就是61-60+100=101,示意图如下:

对于调度进程来说,如果只有一个调度队列,那么势必会出现优先级高的因为下标较小而一直被先执行,优先级低的可能一直不会被执行,所以为了保证调度尽量公平调度,在Linux底层的运行队列存在着两个调度队列,并且结构相同,对应运行队列中的结构如下:

// task_t
typedef struct task_struct task_t;

// prio_array_t
struct prio_array {
    int nr_active;
    unsigned long bitmap[BITMAP_SIZE];
    struct list_head queue[MAX_PRIO];
};
typedef struct prio_array prio_array_t;
// list_head
struct list_head {
    struct list_head *next, *prev;
};
// BITMAP_SIZE
#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))
// MAX_PRIO
#define MAX_USER_RT_PRIO    100
#define MAX_RT_PRIO        MAX_USER_RT_PRIO

#define MAX_PRIO        (MAX_RT_PRIO + 40)

struct runqueue {
    // ...
    task_t *curr, *idle;
    // ...
    prio_array_t *active, *expired, arrays[2];
    // ...
};

runqueue结构中,curr即为前面提到过的head指针,idle即为前面提到的tail指针,本次主要关注prio_array_t类型的三个变量,分别代表CPU当前的调度队列结构、已经执行的进程所在的队列(过期队列)结构和包含两个调度队列结构的数组

prio_array_t本质是prio_array结构,以下是该结构的介绍:

prio_array结构中存在一个nr_active变量,该变量表示当前处于运行状态的进程数量,而bitmap是一个用于映射下标的数组,而queue即为调度队列,每一个queue元素即为一个有前驱指针和后驱指针的节点,MAX_PRIO即为140(前面提到的调度队列的大小),而BITMAP_SIZE通过计算后得到值为5

long类型在C语言中占4个字节,与 int类型一致

所以上面结构可以转化为下面的直观形式:

struct runqueue {
    // ...
    task_struct *curr, *idle;
    // ...
    prio_array *active, *expired, arrays[2];
    // ...
};

struct prio_array {
    int nr_active;
    unsigned long bitmap[5];
    struct list_head queue[140];
};

下面是结构的简化图:

  • 关于activeexpire指针:

如果只有一个调度队列,那么就会出现因为优先级导致部分进程被调度后依旧会回到当前调度队列的同一位置,下一次CPU再调度,又会继续按照优先级先调度该进程,从而导致其他进程无法被调度,这个现象也被称为进程饥饿(Process Starvation)问题,所以就需要两个调度队列。在CPU调度进程时会调度active指针指向的arrays结构,此时被调度过的进程会从active指向的调度队列转移到expired指向的调度队列,如果active指针指向的arrays结构中的nr_active为0,则代表当前调度队列已经没有待调度的进程,此时expired指针的arrays结构中的nr_active一定不为0,所以此时再进行调度只需要交换active指针和expired指针的值即可实现active指针继续指向待被调度的进程所在的arrays结构,从而实现了每一个进程都被调度。

CPU调度进程的三种情况:

  1. 进程状态为终止状态:直接退出调度队列
  2. 到达时间片规定的时间:从active转移到expired
  3. 新进程:默认插入到expired,防止因为优先级过高导致其他进程尽管先产生还是后执行的情况
  • 关于bitmap映射数组下标:

使用bitmap目的是为了快速获取到queue数组中哪些位置有进程,基本思路如下:

在C语言中,long类型占4个字节,所以一共有32个二进制位,而bitmap数组的大小为5,所以有$5 \times 32 = 160$个二进制位,足以覆盖140个下标。因为有无进程刚好只有两个状态,所以用二进制表示再适合不过,对应的0在比特位中的位置表示对应的下标没有进程,1表示对应的下标有进程。如果bitmap的某一个元素为0,则其32个二进制位每一位一定为0,此时对应的调度队列下标一定没有进程,否则就代表存在进程,对应的判断方式如下:

for(int i = 0; i < 5; i++) {
    if(bitmap[i] == 0) {
        continue;
    }
    else {    
        // ...
    }
}

例如,在一个整数的二进制中,获取其中有多少个1的方法(统计一共多少个进程)如下:

while (x) {
    count++;
    x &= (x - 1);
}

// 例如10
// 1010 & 1001 = 1000
// 1000 & 0111 = 0
// 此时count为2

上面的方法中,因为每一个进程所在的调度队列遍历是$O(1)$级别,获取每一个进程也是$O(1)$级别,所以在Linux中也被称为内核O(1)调度算法,这个算法是Linux 2.6.x版本中引入的算法,后来引入了 Completely Fair Scheduler (CFS),它是一种更加公平的调度算法,旨在为所有进程提供公平的 CPU 时间份额。CFS 已经成为现代 Linux 内核默认的调度算法之一,并且在许多方面优于O(1)调度算法

补充知识

每一个进程根据自己的状态不同会处于不同的队列,例如处于运行状态队列、处于等待队列等,为了保证可以在多个队列中,在Linux对应的进程PCB中不会直接使用双向链表的前驱指针和后继指针单独作为PCB的成员,而是将一个节点的结构作为成员,源码如下,其中list_head即为每一个节点的结构:

struct task_struct {
    // ...
    struct list_head run_list;
    // ...

    struct list_head tasks;
    struct list_head ptrace_children;
    struct list_head ptrace_list;

    // ...

    struct list_head children;    /* list of my children */
    struct list_head sibling;    /* linkage in my parent's children list */
    // ...
};

进程PCB在链接时示意图如下:

上面的结构不是直接链接下一个节点的地址,而是链接下一个节点的内部成员,这种做法最大的优势就是提供了更好的扩展性:当有多个节点结构时,就可以让当前进程PCB根据节点结构链接到对应的队列中。但是这种做法也有另外的问题:如果想通过当前PCB访问下一个PCB中的其他成员就不可以直接通过访问下一个PCB节点访问

对于上面的问题,提供解决思路:

因为可以访问到下一个PCB的节点结构,所以就获取到了下一个PCB中的节点地址,在C语言结构体中,结构体的成员对应的地址依次增大,对应的偏移量也在增大。利用这个特点,通过偏移量就可以获取到PCB的地址,再通过PCB的地址就可以访问每一个成员

以下面的结构为例:

#include <stdio.h>

struct A {
    int a;
    char b;
    float c;
    double d;
};

int main()
{
    struct A t;
    printf("&t = %p\n", &t);
    printf("a = %p\n", &(t.a));
    printf("b = %p\n", &(t.b));
    printf("c = %p\n", &(t.c));
    printf("d = %p\n", &(t.d));
}

输出结果:
&t = 0x7ffd1ca8bc20
a = 0x7ffd1ca8bc20
b = 0x7ffd1ca8bc24
c = 0x7ffd1ca8bc28
d = 0x7ffd1ca8bc30

可以看到结构体起始地址与第一个成员的地址相同,假设现在只知道成员d的地址,那么想知道偏移量就必须知道结构体的起始地址,可以假设将0地址作为结构体A的起始地址,则有(struct A*)0,而通过这个方式获取d成员的地址就是&((struct A*)0->d),此时获取到的d成员的地址就是d相对于地址0的偏移量,有了偏移量,就可以通过实际d成员的地址减去偏移量计算出结构体A的实际起始地址,获得了结构体的起始地址就可以通过->访问到其他成员的地址,上面的过程综合在一起:

((struct A*)(&d - &(((struct A*)0)->d)))->其他成员

// 定义成宏使其更有通用性
#define getStart(type, x) ((type*)(&x - &(((type*)0)->x)))

上面的过程中,之所以可以将0地址作为结构体A的起始地址,是因为尽管结构体A的起始地址不在0位置,但是0位置假设作为A类型的一个成员,只要不写入,编译器就不会报错,自然也不会有非法访问的情况。

在计算真实地址中的偏移量时,例如前面实际的地址中就是0x7ffd1ca8bc30(d的实际地址)-0x7ffd1ca8bc20(结构体A的起始地址)= 0x000000000010(是一个计算出的常量)。

现在假设结构体A的起始地址为0,而因为0x000000000010是一个不变的量,所以当结构体A的起始地址为0时,自然d的地址就变为了0x000000000010。此时000000000010-0x0x000000000000也是d在结构体A的起始地址为0时的偏移量,简化为0x000000000010。

使用d的实际地址减去偏移量就是0x7ffd1ca8bc30-0x000000000010=0x7ffd1ca8bc20,获取到的就是结构体A的实际起始地址。

整个过程中将0地址作为结构体A的起始地址就是为了方便计算出偏移量,因为本身并不知道结构体A的实际起始地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2176265.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

会员办理--足浴店系统开发代码———未来之窗行业应用跨平台架构

function fun_会员查询_事务_新增(){var 未来之窗vos对话框_内容 ;var title"test";var 未来之窗vos对话框_id"hjksgfjkkhkj";CyberWin_Dialog.layer(未来之窗vos对话框_内容,{type:"frame",title:"新增会员",move:false,width:"…

828华为云征文|WordPress部署

目录 前言 一、环境准备 二、远程连接 三、WordPress简介 四、WordPress安装 1. 基础环境安装 ​编辑 2. WordPress下载与解压 3. 创建站点 4. 数据库配置 总结 前言 WordPress 是一个非常流行的开源内容管理系统&#xff08;Content Management System, CMS&#xf…

FTP 服务器 linux安装

文章目录 前言一、了解二、安装启动匿名连接 三、创建用户1. 创建系统用户2. 连接3. 连接不上&#xff1f; 5004. 还是连接不上&#xff1f; 5005. 还还还是连不上&#xff1f;530 补充关于创建用户useradd 命令如何设置用户不能登录shell不用系统指定的家目录 vsftpd 配置chro…

Python | Leetcode Python题解之第443题压缩字符串

题目&#xff1a; 题解&#xff1a; class Solution:def compress(self, chars: List[str]) -> int:def reverse(left: int, right: int) -> None:while left < right:chars[left], chars[right] chars[right], chars[left]left 1right - 1n len(chars)write lef…

Android15音频进阶之新播放器HwAudioSource(八十六)

简介: CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布:《Android系统多媒体进阶实战》🚀 优质专栏: Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏: 多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+…

面试扩展知识点

1.C语言中分为下面几个存储区 栈(stack): 由编译器自动分配释放堆(heap): 一般由程序员分配释放&#xff0c;若程序员不释放&#xff0c;程序结束时可能由OS回收全局区(静态区): 全局变量和静态变量的存储是放在一块的&#xff0c;初始化的全局变量和静态变量在一块区域&#…

线性表二——栈stack

第一题 #include<bits/stdc.h> using namespace std; stack<char> s; int n; string ced;//如何匹配 出现的右括号转换成同类型的左括号&#xff0c;方便我们直接和栈顶元素 char cheak(char c){if(c)) return (;if(c]) return [;if(c}) return {;return \0;/…

MySQL 高级 - 第十五章 | MySQL 事务日志

目录 第十五章 MySQL 事务日志15.1 redo 日志15.1.1 为什么需要 redo 日志15.1.2 redo 日志的优点与特点15.1.3 redo 的组成15.1.4 redo 的整体流程15.1.5 redo log 的刷盘策略15.1.6 不同刷盘策略演示15.1.7 写入 redo log buffer 过程15.1.8 redo log file 15.2 undo 日志15.…

lombok详细教程(详解)

Lombok 极速上手 此笔记来自于b站白马 背景 ⚠️ 开始学习Lombok前至少需要保证完成 JavaSE 课程中的注解部分&#xff0c;本课程采用的版本为 Java17 我们发现&#xff0c;在以往编写项目时&#xff0c;尤其是在类进行类内部成员字段封装时&#xff0c;需要编写大量的 get…

看480p、720p、1080p、2k、4k、视频一般需要多大带宽呢?

看视频都喜欢看高清&#xff0c;那么一般来说看电影不卡顿需要多大带宽呢&#xff1f; 以4K为例&#xff0c;这里引用一位网友的回答&#xff1a;“视频分辨率4092*2160&#xff0c;每个像素用红蓝绿三个256色(8bit)的数据表示&#xff0c;视频帧数为60fps&#xff0c;那么一秒…

基于微信小程序爱心领养小程序设计与实现(源码+参考文档+定制开发)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

excel统计分析(5): 非线性回归分析

非线性回归模型分类 非线性回归分析和预测模型包括&#xff1a;指数、对数、幂函数、多项式等。 &#xff08;1&#xff09;指数回归模型 指数回归模型适用于因变量随自变量的增加而迅速增长或减少的情况。 Yβ0⋅e^(β1⋅X) 其中&#xff0c;e是自然对数的底数&#xff0c;…

锐捷 NBR 1300G路由器 越权CLI命令执行漏洞

漏洞描述 锐捷NBR 1300G路由器 越权CLI命令执行漏洞&#xff0c;guest账户可以越权获取管理员账号密码 漏洞复现 FOFA title"锐捷网络 --NBR路由器--登录界面" 请求包 POST /WEB_VMS/LEVEL15/ HTTP/1.1 Host: Connection: keep-alive Content-Length: 73 Autho…

[SAP ABAP] SELECTION-SCREEN

SELECTION-SCREEN用来调节系统生成的画面 REPORT z437_test_2024.TABLES: mara, zdbt_sch_437.SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001. " Title1 PARAMETERS:p_1 DEFAULT A,p_2 TYPE char10. SELECTION-SCREEN END OF BLOCK b1.SELECTION-SCREEN …

深入理解同步和异步与reactor和proactor模式

在现代网络编程中&#xff0c;I/O 设计模式对于提高性能和资源利用率至关重要。本文将探讨两种主要的网络 I/O 设计模式&#xff1a;同步 I/O 和异步 I/O&#xff0c;以及它们的实现方式。 同步 I/O 同步 I/O 模式要求用户通过系统调用函数&#xff0c;如 read(), write(), c…

Win10系统使用mstsc远程电脑的时候发现隔一段时间就无法使用剪贴板_rdpclip---Windows运维工作笔记055

最近在使用温湿系统的远程桌面功能的时候发现,每当使用一段时间的时候,这个时候远程桌面功能的粘贴板就没办法使用了。 正常情况下,不管我一个电脑远程了多少台电脑,那么这些电脑之间都是可以使用粘贴板的,可以用来从一个电脑中截了图,然后粘贴到另一个电脑中。 但是现…

【C++笔试强训】如何成为算法糕手Day6

学习编程就得循环渐进&#xff0c;扎实基础&#xff0c;勿在浮沙筑高台 循环渐进Forward-CSDN博客 目录 循环渐进Forward-CSDN博客 第一题&#xff1a;大数加法 思路&#xff1a; 第二题&#xff1a;链表相加 思路&#xff1a; 第三题&#xff1a;大数乘法 思路&#xf…

第五十八周周报 FE-GNN

文章目录 week58 FE-GNN摘要Abstract一、大数据相关1. 完全分布式zookeeper2. 污水处理过程2.1 污水处理的基本方法2.2 污水处理基本工艺流程 二、文献阅读1. 题目2. Abstract3. 文献解读3.1 Introduce3.2 创新点 4. 网络框架4.1 特征子空间平坦化4.2 结构化主成分4.3 结论 5. …

class 027 堆结构常见题目

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。 这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐. https://space.bilibili.com/8888480?spm_id_f…

C++入门基础知识88(实例)——实例13【求一个数的阶乘】

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///C爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于求一个数的阶乘的相关内容&#xff01; …