【Linux】进程优先级和进程切换

目录

  • 一、孤儿进程
    • [1.1 如果父进程先退出,那么子进程会怎样?](#1.1 如果父进程先退出,那么子进程会怎样?)
    • [1.2 为什么要被领养?为什么要被操作系统领养?](#1.2 为什么要被领养?为什么要被操作系统领养?)
  • 二、进程优先级
    • [2.1 优先级是什么?](#2.1 优先级是什么?)
    • [2.2 为什么要有优先级?](#2.2 为什么要有优先级?)
    • [2.3 Linux下是怎么设计的?](#2.3 Linux下是怎么设计的?)
  • 三、进程切换
    • [3.1 CPU上下文切换](#3.1 CPU上下文切换)
    • [3.2 进程是如何组织的?](#3.2 进程是如何组织的?)
      • [3.2.1 就一个进程而言,该如何获取当前进程的其它属性呢?](#3.2.1 就一个进程而言,该如何获取当前进程的其它属性呢?)
      • [3.2.2 为什么要这样设计呢?](#3.2.2 为什么要这样设计呢?)
    • [3.3 Linux 2.6 内核 进程 O(1) 调度队列](#3.3 Linux 2.6 内核 进程 O(1) 调度队列)

个人主页:矢望

个人专栏:C++LinuxC语言数据结构

一、孤儿进程

1.1 如果父进程先退出,那么子进程会怎样?

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();

    if(id == 0)
    {
        while(1)
        {
            printf("我是子进程,pid:%d, ppid:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("我是父进程,pid:%d, cnt = %d\n", getpid(), cnt);
            sleep(1);
        }
    }

    return 0;
}

如上面的代码,我们编译运行程序后,先将父进程退出,然后观察子进程的状态。

如上图,父进程退出后,bash立刻把父进程回收了,所以没有看到父进程的Z状态,此时子进程的父进程变成1了,并且它由前台进程变成了后台进程

首先,父进程先退出,子进程就称为孤儿进程 。其次,1号进程就是操作系统(操作系统的一部分)

也就是说孤儿进程被系统领养了

1.2 为什么要被领养?为什么要被操作系统领养?

如果一个子进程,它的父进程先行退出了,那么如果这个孤儿进程在某一时刻它自己也退出了,此时由于没有父进程进行回收,它就会永远处于Z状态,这就导致了内存泄漏 。如果孤儿之后不被领养,那么操作系统中的Z状态会越来越多,内存泄漏越来越严重。所以领养孤儿进程是为了给孤儿进程的退出进行善后,被操作系统领养,是因为操作系统是最高管理者,孤儿进程退出后,操作系统会自动回收它

扩充:进程变成孤儿进程之后,OS会自动将这个进程变成后台进程

扩充:区分前后台进程:谁能从键盘获取数据谁就是前台进程。键盘只有一个,所以前台进程任何时刻只能有一个,而后台进程可以有多个

二、进程优先级

2.1 优先级是什么?

进程优先级:进程在已经能得到某种资源的前提下,得到某种资源的先后顺序

权限是能不能的问题,优先级是已经能了,得到先后的问题。

2.2 为什么要有优先级?

原因:资源不足,分配资源时要设置优先级,从而决定进程获得某种资源的先后顺序。

2.3 Linux下是怎么设计的?

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

显示系统中更详细的进程信息:ps -l,带上a就把所有进程全部显示出来,包括后台进程。

UID:代表执行者的身份。

其中 ls -ln使用数字ID而不是名称 。就可以看到UID

辨别进程是谁启动的,就可以使用UID来辨别

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

2.3.1 PRI 和 NI

PRI,即进程的优先级,或者说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。

NI呢? 就是 nice 值了,其表示进程可被执行的优先级的修正数值

PRI值越小越快被执行,那么加入 nice 值后,将会使得PRI变为:PRI(new) = PRI(old) + nice注意:这里PRI(old)默认就是80

nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。

所以,调整进程优先级,在Linux下,就是调整进程nicenice 的取值范围是-20 ~ 19,一共 40 个级别。

2.3.2 修改进程的优先级

输入top,进入 top 后按r -> 输入进程 PID -> 输入要修改的 nice 值完成修改。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {
        printf("我是一个进程,pid:%d\n", getpid());
        sleep(1);
    }

    return 0;
}

编译运行举例:

所以修改进程的优先级是修改进程的 nice 值。

扩充:设置负优先级-20到-1需要root权限。不建议高频修改进程优先级,一般不要更改优先级,让调度器自己工作通常是更好的选择

如上图,切换成root账号就可以修改了。

nice 的取值范围是-20 ~ 19,优先级的取值范围是[60, 99],一共 40 个级别。当你设置的nice值超过这个范围,会更改为最接近的数值,例如你设置为100,则它会变为19

其它调整优先级的命令:nice、renice

为什么优先级的变化范围是有限的?

我们一般使用的操作系统都是分时操作系统 ,它能够很好的满足人和互联网的需求,它的特点是给进程分配时间片,以相对公平公正的调度策略,较为均衡的让不同的进程在一段时间内都能得到CPU的资源 。这就需要我们优先级在一个可控范围内改变,不能让用户将优先级改变的波动太大,避免进程饥饿现象的发生,即总是执行优先级高的,而忽略优先级低的进程。

补充概念

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

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

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

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

三、进程切换

3.1 CPU上下文切换

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

寄存器是共享的,但寄存器里面的数据本质是进程私有的,叫做进程上下文


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

3.2 进程是如何组织的?

铺垫

C语言中,任何变量的地址数字,是众多开辟字节中,地址数据值最小的那个

cpp 复制代码
struct A
{
	int a;
	int b;
	int c;
	double d;
};
struct A obj;

如上是一个结构体,现在我只知道结构体成员变量c的地址,如何求出结构体变量的实际地址呢?

公式:&c - &((struct A*)0 -> c)

  • 重新设计双链表
    基于前面的铺垫,这里我们重新设计一下双链表。

    如上图所示,这是重新设计的双链表。这个双链表和之前我们所学到的双链表不同,链接的指针不是 struct task_struct* 类型,而是简单的一个双链表结构体指针 struct link* 类型,所以它并不指向PCB的地址,而指向独立设计的结构体对象的地址。

3.2.1 就一个进程而言,该如何获取当前进程的其它属性呢?

我们回想前面铺垫部分的内容,当我们知道结构体内部元素的地址时,我们就可以求出结构体对象的地址,而知道地址之后,就可以利用这个地址,访问我们想访问的结构体里面的内容struct link* 中存放的就是 task_structstruct link 结构体成员的地址。

3.2.2 为什么要这样设计呢?

这样设计,我们只需要设计好这个链表的增、删、查、改,之后如果我们想通过链表管理其它的数据结构,我们依旧可以使用这个链表,例如管理硬件设施等。所以只需要维护这一份代码,就可以扩展链式管理的范围

另外,假设我们想要进行增删查改操作,以头插为例,我们的所有类型都是struct link*类型。我们可以设计一个接口insert_link(head, struct link* xxx);,这样当我们申请了一个新的PCB要头插时,这时候就可以insert_link(head, &(x -> node));头插新申请的PCB内部的link结构体对象的地址。

以下是Linux内核2.6.18中的简单链表信息:

cpp 复制代码
struct list_head{
	struct list_head *next, * prev;
};

// 在 task_struct 中的结构体对象
struct list_head tasks;

Linux内核会将所有的进程task_struct统一放在一张双链表中

这没有问题,那么进程不是还可以在运行队列、阻塞队列等里面吗?这又是如何设计的呢?

cpp 复制代码
struct task_struct
{
	// ...
	struct list_head tasks;
	// ...
	struct list_head run_queue;
	// ...
};

如上,task_struct中可以有多个链式结构,所以一个进程既可以在全局的双链表里,又可以在运行队列里,一个进程可以同时在多个链式结构里

拓展而言,一个task_struct中可以包含二叉树、红黑树、哈希表等基础数据结构,一个进程可以同时属于它们,甚至不同的结构体对象之间都可以使用这些链式结构等的数据结构相连接

cpp 复制代码
struct task_struct {
    // ...
    struct list_head tasks;           // 双向链表
    struct list_head run_queue;       // 运行队列链表
    struct rb_node vruntime_node;     // CFS调度器的红黑树节点
    struct hlist_node pid_links[PIDTYPE_MAX];  // PID哈希表节点
    // ...
};

// 内核中的实际红黑树节点定义
struct rb_node {
    unsigned long  __rb_parent_color;
    struct rb_node *rb_right;
    struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));

3.3 Linux 2.6 内核 进程 O(1) 调度队列

每一个CPU都有一个调度队列struct runqueue{},在这个调度队列中有一个queue[140]的数组。其中这个数组中100~139下标中存放的都是普通优先级 ,而0~99下标存放的是实时优先级(我们不关心)。

其中普通优先级有40个,而我们的PRI的取值是[60, 99]也是40个,之后就可以把60映射到100,而99映射到139,所以优先级数字本质是数组下标

将来这140个下标中存储的是140个子队列,将来选择进程时,先根据进程的优先级确定queue数组下标,然后根据数组下标中的队列FIFO进行进程的选择,所以根据优先级选择进程的时候本质是一个哈希的过程,一旦确定在那个数组下标队列,剩下的工作就是FIFO

如上图,一个CPU在选择进程进行调度时,会从0下标到139下标依次遍历,只要下标不为空就执行这个下标对应队列中的第一个进程,这个进程就是要执行的优先级最高的进程。

  • 位图操作提高效率

遍历queue[140]的时间复杂度是常数,但是还是有些低效了,有没有更好的方法呢?

可以通过位图来提高效率,通过创建大于140的比特位,在每一个比特位上通过1来标识该下标位置有进程,0来标识该下标位置没有进程。我们寻找下标最小的1就行了,这个下标里面的队列就是当前优先级最高的队列。

在内核中是通过long bitmap[5]来标记的,long类型是四字节,一个long类型占据32bite位,也就是5long类型就可以覆盖完成140个位置。从0开始遍历bitmap数组,如果该下标位置bitmap[i]0,说明queue[]中这32个下标位置没有进程,如果不是0,说明优先级最高的那一批进程就在该下标中,再仔细检查这32个比特位即可,这样就提高了效率

内核中又通过一个变量nr_active来记录现在CPU中有多少个进程在运行。所以在检查bitmap之前会先检查nr_active是否为0,如果为0就不用找了。

上述我们所讲的内核中叫做优先级数组

cpp 复制代码
struct prio_array_t {
    unsigned int nr_active;         /* 活跃进程总数 */
    unsigned long bitmap[5]; /* 位图,标记哪些优先级有进程 */
    struct list_head queue[140];  /* 每个优先级的进程队列 */
};

Linux 2.6.18的内核源码当中,其实是有两个一摸一样的queue[140]的,它们是这样表现的:

cpp 复制代码
struct prio_array_t{
	unsigned int nr_active;         /* 活跃进程总数 */
    unsigned long bitmap[5]; /* 位图,标记哪些优先级有进程 */
    struct list_head queue[140];  /* 每个优先级的进程队列 */
};

struct prio_array_t array[2];

其中array[0]里面存储的是活跃进程 ,也就是时间片还没有耗尽的进程,array[1]里面是过期进程,也就是时间片耗尽的进程。

另外源码中有两个struct prio_array_t*类型的指针,一个叫做active,它指向array[0],另一个是expired,它指向array[1]

在详细展开之前,先提出两个子问题:

1、假如有优先级为61的进程要被执行,但是总是有优先级为60的进程加入,那么会造成进程饥饿吗?

2、要把一个进程由85改到65该怎么办,是直接在活跃队列改吗,需要断开链接吗?

好,接下来我们来详细展开,

如上,active指针指向array[0]expired指针指向array[1]CPU在选择进程进行调度时,会在活跃进程中选择优先级靠前的进程进行调度,当这个进程的时间片耗尽之后,注意此时,这个进程不会再回到活跃进程中,而是会链入到过期队列中,链入到它的优先级对应的下标队列中,也就是说,活跃队列里面的进程越调度越少

当活跃队列里面的进程全部调度完成之后,那么过期队列之后就会变成活跃队列,也就是会执行swap(&active, &expired),更换指针指向就可以了,这样就可以继续调度了,就可以在两个队列之间辗转腾挪

接下来回答之前的两个问题,

第一个问题:当新的进程来的时候,会直接将它放在过期队列里,这也符合我们所说的先来后到,等到下一轮调度时,如果它的优先级高它就先调度,这种一下子调度完活跃队列的机制有效解决了进程饥饿问题

第二个问题:(85对应下标12565对应下标105)如果我们直接改优先级,那我们还要将它断链,然后链入到新的优先级队列里,这样该成本太高,甚至会破坏调度机制!所以我们会修改nice值为-15,将这个改变记录下来,这一次调度的优先级会保持不变,等到这次执行完成,要链入过期队列时,此时就可以重新计算优先级了,然后链入到指定下标,反正都要重新链入,所以利用这个机会更改是最佳的,这也是设置nice的原因。

这就是Linux O(1) 调度算法

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
Configure-Handler2 小时前
ubuntu 22.04 配置VNC远程连接
linux·运维·ubuntu
一个平凡而乐于分享的小比特2 小时前
Makefile 源码编译系统详解
linux·makefile
木卫二号Coding2 小时前
在 Ubuntu 上安装 noVNC
linux·运维·ubuntu
爱吃苹果的梨叔2 小时前
NTP 网络时间服务器硬件驯服技术说明(投标技术响应说明)
linux·运维·服务器·网络·嵌入式硬件·tcp/ip
有时.不昰沉默2 小时前
ubuntu 20.04 启动直接进入 tty1,而非 图形界面
linux·运维·ubuntu·tty1
济6172 小时前
linux 系统移植(第七期)----U-Boot 图形化配置--添加自定义菜单-- Ubuntu20.04
linux·运维·服务器
松涛和鸣3 小时前
DAY56 ARM Cortex-A Bare Metal
linux·服务器·c语言·开发语言·arm开发·数据库
星陨773 小时前
OpenStack私有云平台API接口练习
linux·运维·网络·openstack
别再下雨辽3 小时前
开发板通过 VSCode Remote-SSH 反向转发复用 PC 代理排障总结
linux·ide·笔记·vscode·ssh