进程的基本概念(上)-- 操作系统与进程

进程的概念

一. 冯诺依曼体系结构

image-20240221180215069

1.1 冯诺依曼体系结构推导

计算机的作用就是为了解决问题。首先需要将数据或问题输入到计算机当中,所以计算机必须有输入设备。计算机解决完问题后还需要将计算结果输出显示出来,所以计算机必须有输出设备。要将问题解决就需要用到中央处理器,其中运算器负责算术运算与逻辑运算,控制器控制具体的流程。

<img src

  • 输入设备:键盘,网卡,鼠标,磁盘等
  • CPU:运算器&&控制器
  • 输出设备:显卡,网卡,显示器,磁盘等

以上的各种设备之间是相互连接的,在目前是由 主板 进行连接的,设备的连接连接是一种手段,目的是让数据在设备之间流动,本质就是数据在设备之间的来回拷贝,而拷贝的整体速度,是决定计算机效率的重要指标。

输入设备和输出设备相对于中央处理器来说是非常慢的,而根据 木桶原理,那么最终整个体系所呈现出来的速度将会是很慢的。

于是就不让输入设备和输出设备直接与CPU进行交互,而在这中间加入了内存,最终形成了冯 • 诺依曼体系结构。内存比外设速度快许多,但比又CPU慢。在冯 • 诺依曼体系结构中,内存就处于慢设备和快设备之间,能够起到缓冲作用

关于冯诺依曼:

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

冯 • 诺依曼体系结构运行流程: 用户输入的数据先存储到内存当中,CPU读取数据的时候就直接从内存中读取,CPU处理完数据后又写回内存当中,然后内存再将数据输出到输出设备当中,最后由输出设备进行输出显示。

1.2 内存提高效率

image-20240221181606211

由图中的内存金子塔我们可以得知:

离CPU越近的存储单元,对CPU的传输速率越快,但单体容量越小,造价也越高

离CPU越远的存储单元,对CPU的传输速率越慢,但单体容量越大,造价也越低

**内存具有数据存储的能力。**这也使得软件有了发挥的空间,譬如操作系统可以将可能使用的数据预先加载到内存,以此来提高效率。同时

**CPU处理数据和内存加载数据是可以同时进行的。**根据统计学原理,当某个数据被访问时,那么下一次有极大可能访问其周围的数据。所以当CPU处理某一数据时,内存可以将该数据周围的数据加载进来,这也使得计算机的效率更高。

输出数据时也是一样,CPU处理完数据后直接将数据放到内存当中,当输出设备需要时再从内存中获取即可,此时CPU就可处理其他的事务(不需要等待外设处理),从而提高效率。这也就有了我们平常所说的缓冲区的概念。

1.3 具体案例理解冯 • 诺依曼体系结构

image-20240221184446824

分析两台电脑使用QQ发送信息的过程,忽略网络处理的细节从而简化模型

通过键盘输入信息并将消息加载到内存后,CPU从内存获取到消息后对消息进行各种处理,然后再将其写回内存,此时本地网卡就可以从内存获取已处理好的消息,然后在网络当中经过一系列处理,之后你朋友的网卡从网络当中获取到你所发的消息后,将该消息加载到内存当中,你朋友的CPU再从内存当中获取消息并对消息进行解包操作,然后将解包好的消息写回内存,最后你朋友的显示器从内存当中获取消息并显示在他的电脑上。

1.4 其他认识

问:磁盘与内存的区别是什么?

磁盘是一个外部存储器,是一种外设,而内存是一个内部存储器,通常由寄存器构成。

问:程序在运行时,为什么要先将程序加载到内存中去?

外设 <–> 内存 <–> CPU

外设 和 CPU 只能和 内存 打交道

程序首先是一种文件,文件要储存在磁盘当中,而一个程序要运行,就要让CPU运行,而在数据层面,CPU只和内存打交道,所以要由内存从磁盘中读取数据,再交由CPU处理,处理结束后,再由CPU加载到内存中去,最后由内存交给输出设备,可以理解为冯诺伊曼体系结构就这么规定的。

在物理层面上,各个硬件单元之间是通过总线连接的,外设与内存之间的总线为IO总线,内存与CPU之间的总线为系统总线

二. 操作系统(Operator System – OS)

2.1 操作系统的基本概念

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

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

设计OS的目的

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

定位

在整个计算机软硬件架构中,操作系统的定位是:进行软硬件资源管理的软件

2.2 操作系统的上下结构

image-20240221185034838

其实可以将将整个系统类比为一个银行系统进行理解。

  • 底层硬件就是银行内的基础设施,如座椅、电脑、保险箱等
  • 使用这些硬件的员工我们就可以看作是 驱动程序
  • 管理员工的就是银行行长即 操作系统
  • 系统调用接口就相当于银行的服务窗口
  • 人工办理业务就可以看作用户调用接口

操作系统中还包含着许多系统进程,可以看作是银行提供业务处理的员工。顾客和员工也只能通过窗口进行办理业务,也就是必须通过系统调用接口才能访问内部,而不能直接操作内核、硬件等。

但在现实情况中,有些用户并不会使用窗口(第一次来银行的客户),这时外面的工作人员就会询问你要办理什么业务,这些工作人员就可以看作是用户调用接口。工作人员了解到你要办什么业务后,会告诉你具体告诉你要如何领票排队、具体去哪个窗口。所以用户操作接口内部还是会调用系统调用接口的。

问:我们可以发现操作系统并不直接与硬件交互,就像行长不会修电脑。但是为什么要设计成这样呢?

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

2.3 管理理念: 先描述,再组织

先描述,再组织 – 先定义类对象,再组织数据结构

以校园中的管理举例,校园中可以大致分为三类人: 学生(被管理者)、老师(执行者)、校长(决策者)

在校园中学生几乎是不会看见校长的(操作系统不直接与硬件交互),校长只进行决策。可是校长不与学生交互,是如何管理学生的呢?

譬如校长想选出三名学生代表在元旦晚会上发表讲话,就让老师将成绩单拿给他,校长从中选出了成绩前三名的学生并让老师告诉这三名学生。在这个过程中,校长并没有与学生交互却管理到了学生,就是根据数据进行的管理。

实际上,学校将我们每个学生的各种信息都进行了管理,基本信息、成绩信息以及健康信息等等。每一套信息就描述了一名学生,校长通过对这些信息的管理就能做到对学生的管理。当学生的数量较多时,校长可以将全部学生的信息组织起来(链表、顺序表、树等)。此时校长对各个学生的管理,实际上就是对数据结构的增删查改,这也就体现了管理的本质不是对事物的管理,而是对数据的管理

计算机管理硬件

  1. 描述起来,用struct结构体

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

系统调用和库函数概念

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

三. 进程

操作系统是如何管理进程的呢,我们通过先描述,再组织的原则,先来了解什么是进程?

3.1 基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体

当代码进行编译链接等操作后就会生成一个可执行程序,这个可执行程序本质上也是一个文件,存放在磁盘上。当使这个可执行程序运行起来,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程

并且进程与程序并不一定是对应的,一个程序可以同时运行多次,也就有了多个进程。

3.2 描述进程 – PCB

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

我们的电脑上存在着大量的进程,这时就需要操作系统来进行管理。如何管理呢?先描述,再组织

操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB,本质上是一个结构体),并将这些PCB以双向链表的形式组织起来。

image-20240221224922488

task_struct – PCB的一种

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

**task_ struct内容分类 **

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

组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里

3.3 查看进程

(1)通过系统目录查看

在根目录下有一个名为proc的系统目录,该目录中包含大量进程信息。其中有些子目录的目录名为数字,这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。若想查看PID为1的进程的进程信息,则查看名字为1的文件夹即可。

image-20240221225318495

(2)通过ps命令查看

可使用 man 1 ps 命令来查看ps帮助文档

在实际当中我们通常会使用下面几条语句来查看进程

ps axj | head -1 && ps axj | grep 进程名 | grep -v grep

while :; do ps axj | head -1 && ps axj | grep 进程名 | grep -v grep; sleep 1;done;

image-20240221230512450

(3)通过系统调用获取进程标示符

  • 进程id(PID)
  • 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("pid: %d\n", getpid());
    printf("ppid: %d\n", getppid());
    return 0;
}

(4)通过系统调用创建进程 – 初识fork

  • 运行 man fork 认识fork
  • fork有两个返回值 (在Linux中,同一个变量名可以表示不同的内存)
    • 给父进程返回子进程的pid
    • 给子进程返回0
  • 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    int ret = fork();
    printf("hello proc : %d!, ret: %d\n", getpid(), ret);
    sleep(1);
    return 0;
}
  • fork 之后通常要用 if 进行分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    int ret = fork();
    if(ret < 0)
    {
        perror("fork");
        return 1;
    }
    else if(ret == 0)
    {
        //child
    	printf("I am child : %d!, ret: %d\n", getpid(), ret);
    }
    else
    { 
        //father
    	printf("I am father : %d!, ret: %d\n", getpid(), ret);
    }
    sleep(1);
    return 0;
}

(5)demo代码

// 父子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("before fork: I am a prcess, pid: %d, ppid: %d\n", getpid(), getppid());

    sleep(5);
    printf("开始创建进程啦!\n");
    sleep(1);
    pid_t id = fork();
    
    if(id < 0) 
        return 1;
    else if(id == 0)
    {
        // 子进程
        printf("after fork, 我是子进程: I am a prcess, pid: %d, ppid: %d, return id: %d, &id: %p\n", 					getpid(), getppid(), id, &id);
        sleep(1);
    }
    else
    {
        // 父进程
        printf("after fork, 我是父进程: I am a prcess, pid: %d, ppid: %d, return id: %d, &id: %p\n", 					getpid(), getppid(), id, &id);
        sleep(1);
    }
    sleep(2);
    return 0;
}
// 多进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

const int num = 10;

void work()
{
    int cnt = 12;
    while(cnt)
    {
        printf("child %d is running, cnt: %d\n", getpid(), cnt);
        cnt--;
        sleep(1);
    }
}

int main()
{
    for(int i = 0; i < num; i++)
    {
        pid_t id = fork();
        if(id < 0) break;
        if(id == 0) 
        {
            work();
            exit(0);
        }
        printf("father creat child process success, child pid: %d\n",id);
        sleep(1);
    }
    // 执行到这里就只有父进程了
    sleep(10);

    return 0;
}

3.4 进程状态

image-20240221232754314

(1)源码

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

(2)状态详解

  • R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里

    所有处于运行状态的进程(即可被调度的进程),都被放到运行队列当中。当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。一个进程处于运行状态(running),并不意味着进程一定处于运行当中。运行状态表明一个进程要么在运行中,要么在运行队列里。即可以同时存在多个R状态的进程

  • S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))

    譬如当进程循环向屏幕输出时,由于CPU的处理速度极快,但显示器的速度较慢,导致进程需等待显示器这个资源(CPU此时会处理别的进程)。此时该进程会在运行状态和睡眠状态不断切换,但由于CPU的高速导致我们观测时大概率会看见睡眠状态

  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

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

  • T停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

  • t暂停状态(tracing stop):是一种被追踪的暂停状态,通常处于被调试的状态下。

  • Z僵尸状态(zombie):是一种特殊状态,详细描述请看下文。

  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

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

显示状态时有个+号表示该进程是前台进程,若没有则是后台进程。处于该睡眠状态下的进程是可以被杀死的,譬如使用kill命令发送信号

kill -命令编号	//通过 kill -l 查看命令手册

(3)阻塞状态

进程运行时是被CPU调度的。即进程在调度时是需要用到CPU资源的,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的

image-20240221233251632

在运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,都有各自对应的资源等待队列

image-20240221233307580

对应到Linux中的状态,阻塞即 睡眠状态S 与 磁盘休眠状态D

**(4)Z(zombie)-僵尸进程 **

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(0);// 让子进程直接退出
    }
    // 剩下的只有父进程了
    int cnt = 10;
    while(cnt)
    {
        cnt--;
        printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(1);
    }

    wait(NULL);
   	printf("father wait child done...\n");
    sleep(5);

    return 0;
}

image-20240221234632675

僵尸进程危害

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

(5)孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号init进程领养,当然要有init进程回收喽。
// 孤儿进程
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int cnt = 500;
        while(cnt)
        {
            printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(0);// 让子进程直接退出
    }
    // 剩下的只有父进程了
    // 父进程比子进程先结束
    int cnt = 5;
    while(cnt)
    {
        cnt--;
        printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(1);
    }

    return 0;
}

父进程比子进程先结束,剩下的子进程仍处于僵尸状态,需要有人去领养他,释放他的内存,所以当某一进程的父进程比其先结束,则该进程将会被1号系统进程所领养,在系统进程结束时,释放该进程

image-20240221235522092

3.5 进程优先级

(1)基本概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。

(2)优先级存在的原因

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

ps -l

image-20240221230549489

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

(3)PRI and NI

  • PRI即进程的优先级,就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • NI即nice值,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行,所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是[-20, 19],一共40个级别,进而PRI的范围即为[60, 99].

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

问:Linux为什么调整优先级要受限制?

如果不加限制,将某一进程的优先级调整的非常高,别人的进程优先级非常低,由于优先级高的先得到资源,而资源是源源不断的,就会导致常规进程很难享受到资源,进而导致进程饥饿,任何分时系统,都要较为公平的调度进程

**(4)PRI vs NI **

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

**(5)查看进程优先级的命令 **

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

  • top

    top命令就相当于Windows操作系统中的任务管理器,它能够动态实监测系统中进程资源占用情况

  • 进入top后按“r”–>输入进程PID–>输入nice值

(6)进程与进程之间的关系

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

3.6 进程的调度与切换

**(1)概念:**当一个进程放在cpu上运行时,是必须要把进程的代码跑完才会进行下一个进程吗?答案肯定是 不对。现在的操作系统都是基于时间片轮转执行的

与分时操作系统对应的就是实时操作系统,实时操作系统比如车载操作系统,当一个进程的优先级很高必须执行完才能执行下一个,如刹车进程,必须执行完,才能执行下一个,要不然就坑用户了,所以他的进程优先级可以很低。

分时操作系统却不会这样,分时操作系统的优先级是有范围的( [60,99] ),这样能保证每个进程都能被调度到,能够公平的照顾到每个进程,同时也照顾到了进程饥饿的问题。

(2)进程调度

image-20240303115703986

寄存器是cpu内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

进程在cpu中执行中要产生大量的数据,这些数据会被存储到寄存器上,但是当时间片耗尽时,那么这些数据该怎么办?

简单来说会从cpu上剥离下来,数据存储到pcb上。其中有一个关键的寄存器叫eip,它会记录当前cpu执行到了哪一行代码的下一行。

当cpu首次执行该进程到时间片耗尽的时候,cpu内部所产生的临时数据存放在寄存器中,这些数据我们叫进程硬件的上下文,硬件上下文让我们的进程得以保存并存储到pcb上,当进程之后在被调度时,首先就要恢复硬件的上下文,从上次结束的地方再继续执行。

image-20240303115823480

cpu寄存器只有一套,但是里面存储的进程的数据却可以有很多套,虽然数据存储在一个共享的cpu设备里,但是某个进程的数据,都是被某个进程所私有的!!!

(3)进程切换

Linux的优先级是可以被修改的,并且范围是[60,99]

image-20240303120015287

普通的运行队列都是FIFO的机制,Linux下的调度算法是考虑到优先级,考虑到效率和考虑到饥饿的问题。

image-20240303120037329

首先Linux下有一个这样的数组,它的类型是结构体指针,他的大小是140。[0,99]号元素我们现在不谈[100,139]号元素,我们发现这对应的不就是nice值修改的范围。至此我们发现运行对列的数组下有存储着不同优先级队列的地址,这时cpu在调度的时候就考虑到了优先级的问题。根据优先级的高低来运行调度进程。

image-20240303120222115

在运行队列的上面我们有bitmap[5],它的类型是int,也就是说有32 * 5 = 160 个比特位,我们用每一个bit位的位置来对应每一个队列,因为只有140个队列,所以还剩下20个bit位置我们不用。bit位的内容来对应该队列是否为有内容。这样考虑到了调度时的效率问题。

image-20240303120245607

在runqueue上还存在着两个指针,查看英文释义,一个是活跃的,一个是到期的。活跃的意思也就是该指针指向的队列正在被cpu运行,而到期的则没有被运行。

image-20240303120401069

当cpu执行完活跃队列的进程之后,就会执行过期进程。而在执行活跃进程中又来的新进程则被按照优先级安插到了过期队列中。这样一共有两个队列,通过指针的不停切换就实现了调度的公平性。一定程度上解决了进程饥饿的问题。

nr_active 表示有多少个活跃的队列。如果没有直接切换。

image-20240303130434943

image-20240303120538635

这两个指针是怎么切换的呢?简单的来说有一个结构体数组,里面分别存放着两个队列。

把第一个元素(地址)给到active ,第二个给到 expired,然后等到活跃的进行运行完,交换两个指针的内容,实现切换。

image-20240303120608318

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小张a_3168

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值