05.【Linux系统编程】进程(进程概念、进程状态(注意僵尸和孤儿)、进程优先级、进程切换和调度)
目录
1. 冯诺依曼体系结构
1.1 冯诺依曼体系结构
1.2 存储器的作用(所有设备都只能直接和内存打交道)
1.3 冯·诺依曼体系结构的特点、影响、选择原因
2. 操作系统(Operator System)
2.1 概念
2.2 设计OS的目的
2.3 核心功能
2.4 如何理解 "管理"
2.5 系统调用和库函数概念
3. 进程
3.1 基本概念与基本操作
3.1.1 描述进程-PCB
3.1.2 task_ struct的成员,进程列表
3.1.3 查看进程
3.1.4 杀死进程
3.1.5 通过系统调用获取进程标示符( getpid(),getppid() )
3.1.6 通过系统调用创建进程-fork初识
3.2 进程状态(内核根据进程状态进行管理)
3.2.1 各种进程状态概念
3.2.2 进程管理(根据不同状态,链接到不同的队列进行细分)
3.2.3 再谈PCB的结构,各种队列如何链接?
3.2.4 进程状态查看(命令)
3.2.5 理解运行、阻塞、挂起状态
3.2.6 Z(zombie)-僵尸进程
3.2.7 孤儿进程
3.3 进程优先级
3.3.1 基本概念
3.3.2 查看系统进程
3.3.3 PRI and NI(进程优先级)
3.3.4 修改进程优先级(进程饥饿解释)
3.3.5 基于时间片的分时操作系统
3.3.6 补充概念-竞争、独立、并行、并发
3.4 进程切换
3.4.1 进程切换概念
3.4.2 进程调度队列,调度算法O(1)
1. 冯诺依曼体系结构
1.1 冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是由一个个的硬件组件组成
• 输入单元:包括键盘,鼠标,话筒,摄像头...网卡,磁盘(外存)等
• 输出单元:显示器,网卡,磁盘,打印机等
• 中央处理器(CPU):含有运算器和控制器等
• 存储器:内存
Input/Output → IO:站在存储器的角度理解IO
1.2 存储器的作用(所有设备都只能直接和内存打交道)
关于冯诺依曼,必须强调几点:
1. 这里的存储器指的是内存
2. 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)(数据层面)
3. 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
4. 总之,所有设备都只能直接和内存打交道。
比如:软件运行流程:“操作系统将磁盘上的程序加载到内存,CPU从内存中自动地、连续地取出指令并执行,执行过程中与内存频繁交换数据,最终结果由输出设备从内存中获取并呈现给用户。”
1.3 冯·诺依曼体系结构的特点、影响、选择原因
1.3.1 冯·诺依曼体系结构:计算机的“基本法” (特点)
冯·诺依曼体系结构规定了一台通用计算机必须具备的五大部件(运算器、控制器、存储器、输入设备、输出设备),并遵循两大核心原则:
存储程序:程序指令和数据以二进制形式共同存储在同一个存储器中。
顺序执行:CPU自动地从存储器中取出指令并执行(通过程序计数器PC)。
它的核心作用是实现了“通用性”。通过更换存储器中的程序,同一台硬件计算机就能解决完全不同的问题,从而成为了一个“万能机器”。这是软件和硬件产业能够分离和发展的根本前提。
1.3.2 木桶效应:性能的“必然代价”(后果)
木桶效应指系统整体性能受限于其最慢的部件。
“短板”:慢速的外设(如硬盘、网络、键盘)(毫秒级)。
“长板”:高速的CPU和内存(纳秒级)。
这个效应是冯诺依曼结构带来的直接结果。因为冯氏结构规定:1.CPU所需的所有指令和数据必须来自内存。2.内存的所有内容最终必须来自外设(硬盘)。
这就强制性地将最快的部件(CPU)和最快的部件(外设)耦合在了一起,导致前者必须等待后者,从而让外设的速度决定了系统的最终体验性能。
结论:木桶效应是追求“通用性”所必须接受的性能代价。
1.3.3 软件执行流程:“基本法”的具体实践
在冯诺依曼结构下,软件的运行遵循一个固定的流程,这个流程清晰地展示了木桶效应:
1. 加载:操作系统将程序和数据从慢速外设(硬盘) 加载到快速内存中。
2. 执行:CPU从内存中取指令、解码执行。执行过程中不断与内存交换数据。
3. 输出:结果数据写回内存,最后由输出设备(如显卡)从内存中读取并呈现。
4. 瓶颈:在第1步和第3步,系统速度被外设这块“短板”严重制约。如果内存不足,系统还会用硬盘做虚拟内存,导致更频繁地访问外设,使瓶颈恶化。
1.3.4 为何选择这种结构?—— 权衡与智慧
既然有性能瓶颈,为什么还要这么设计?
答案在于权衡。计算机架构师在绝对速度和通用性/成本/易用性之间选择了后者。
如果追求绝对速度:需要为每个任务设计专用硬件(如ASIC)。这会导致硬件极度复杂、成本高昂、无法编程。
如果追求通用性:就采用冯诺依曼结构。接受性能瓶颈,但获得了:
更简单的CPU设计:CPU只需理解如何与内存交互,无需理解所有外设的复杂协议。
更低的成本:硬件可以大规模标准化生产。
无限的灵活性:通过编程,机器可以获得任何功能。
** ecosystem繁荣**:催生了独立的软件产业。
现代计算机所做的一切优化(如CPU缓存、DMA、SSD),都不是为了推翻冯诺依曼结构,而是在接受其“通用性”的前提下,去尽力弥补它带来的“性能短板”。
总结:当代计算机,是性价比的产物。
2. 操作系统(Operator System)
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS),操作系统是一款进行软硬件 管理 的软件。笼统的理解,操作系统包括:
• 内核(进程管理,内存管理,文件管理,驱动管理)
• 其他程序(例如函数库,shell程序等等)
2.2 设计OS的目的
• 对下,与硬件交互,管理所有的软硬件资源。(手段)
• 对上,为用户程序(应用程序)提供一个良好的执行环境。(目的)
2.3 核心功能
• 在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件.
2.4 如何理解 "管理"
管理:先描述(类),再组织(数据结构)。
• 管理的例子 - 学生,辅导员,校长
• 描述被管理对象
• 组织被管理对象
总结
计算机管理硬件
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
2.5 系统调用和库函数概念
• 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
• 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。(库函数和系统调用属于上下层的关系)
那在还没有学习进程之前,就问大家,操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!
3. 进程
我们执行的所有的指令,工具,自己的程序,运行起来全部都是进程。
命令行解释器本质是一个进程,os会给每一个登录用户分配一个bash。
3.1 基本概念与基本操作
• 进程的课本概念:程序的一个执行实例,正在执行的程序等
• 内核观点:担当分配系统资源(CPU时间,内存)的实体。
• 当前:进程 = 内核数据结构(task_struct) + 自己的程序代码和数据
电脑开机之后,操作系统一直运行在内存中,操作系统中有一个东西叫做内核。当我们运行一个可执行程序之后: 1.可执行程序的代码和数据运行在内存中(由内核分配);2.可执行程序的地址和状态信息等存放在进程控制块(PCB)中,PCB是操作系统内核管理的数据结构。用户对进程的操作:实际是用户通过系统调用与内核交互,内核对PCB发出增删改查指令,由CPU去执行这些增删查改的操作,从而控制进程 → 从而实现用户对进程的管理。
内核:内核就是物理内存的“上帝”和唯一的总管理者,进程 代码和数据 的分配是由内核分配的。进程列表(PCB)是由内核管理的,在内核的内部。
操作系统:主要由内核和其他部分组成。
系统调用:用户和内核的翻译官。
进程:= 内核数据结构(task_struct) + 自己的程序代码和数据
进程的执行:CPU执行的是“代码和数据”部分,而内核管理的是“进程列表”(PCB),但内核通过管理PCB,来控制CPU何时、如何执行哪份“代码和数据”。
3.1.1 描述进程-PCB
• 进程信息被放在一个叫做进程控制块(PCB)的数据结构中,可以理解为进程属性的集合。
• Linux 操作系统下的PCB 是: task_struct,是描述进程的结构体。(task_struct-PCB的一种)
• task_struct 是Linux 内核的一种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息。
3.1.2 task_ struct的成员,进程列表
• 标示符: 描述本进程的唯一标示符,用来区别其他进程。
• 状态: 任务状态,退出代码,退出信号等。
• 优先级: 相对于其他进程的优先级。
• 程序计数器: 程序中即将被执行的下一条指令的地址。
• 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
• 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
• I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
• 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
• 其他信息
• 具体详细信息后续会介绍
可以在内核源代码里找到task_ struct结构体。所有运行在系统里的进程都以task_struct 双链表的形式存在内核里。(即:进程列表是全部PCB连接起来)
3.1.3 查看进程
我们执行的所有的指令,工具,自己的程序,运行起来全部都是进程。
1. 大多数进程信息同样可以使用top和ps这些用户级工具来获取
// myproc.c
#include<stdio.h>
#include<unistd.h>int main()
{while(1){printf("进程运行\n");sleep(1);}return 0;
}
看资源用 aux,看关系用 ajx
# 查看进程
ps aux | grep myproc
# ps ajx | head -1;ps aux | grep myproc # 查看进程,ps ajx | head -1打印表头,ps ajx | grep myproc打印myproc进程
ps ajx | head -1;ps ajx | grep myproc
2. 进程的信息可以通过/proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看/proc/1 这个文件夹。
查看proc目录的对应进程pid号的文件,可见有cwd和exe两个符号链接。
cwd:current working directory,进程当前的工作目录。我们使用fopen创建文件时,如果不添加文件目录,则默认会在当前进程目录下创建。
exe:进程的绝对路径
3.1.4 杀死进程
# 方法1:
Ctrl+c# 方法2:
kill [PID]
kill -9 [PID]
#kill [PID] 是 “请求进程正常退出”,kill -9 [PID]是 “强制进程立即死亡”。
#实际使用中,建议先尝试 kill [PID],仅在进程无响应时再用 kill -9 [PID]。
3.1.5 通过系统调用获取进程标示符( getpid(),getppid() )
• 进程id(PID)
• 父进程id(PPID)
命令行解释器本质是一个进程,os会给每一个登录用户分配一个bash。
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h> // 包含getpid和getppidint main()
{while(1){printf("我是子进程pid: %d,我的父进程是ppid:%d\n", getpid(), getppid());sleep(1);}return 0;
}
3.1.6 通过系统调用创建进程-fork初识
• fork有两个返回值:一次调用,两次返回 父进程返回子进程PID(>0);子进程返回0;失败返回-1。
• 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
• 父子进程各有一份PCB。
1. 为什么fork函数会返回两次?
#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;
}// 父子进程各自打印
// hello proc : 901!, ret: 902 // 父进程fork返回子进程的PID
// hello proc : 902!, ret: 0 // 子进程fork返回0
2. 为什么else if(id==0)和else同时成立,同时运行
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int gval = 100;int main()
{printf("父进程开始运行,pid: %d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);sleep(5);// childwhile(1){sleep(1);printf("子进程修改变量: %d->%d", gval, gval+10);gval+=10; // 修改printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());}}else{//fatherwhile(1){sleep(1);printf("我是一个父进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);}}return 0;
}
结论
进程具有独立性
用户程序的代码和数据部分,代码共享,数据私有。数据通过“写时拷贝”技术表现得像是各自私有,只有在修改时才会真正拥有独立的副本。
3.2 进程状态(内核根据进程状态进行管理)
3.2.1 各种进程状态概念
• 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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 */ // TASK_RUNNING (状态全称)"S (sleeping)", /*1 */ // TASK_INTERRUPTIBLE"D (disk sleep)", /*2 */ // TASK_UNINTERRUPTIBLE "T (stopped)", /*4 */ // TASK_STOPPED"t (tracing stop)", /*8 */ // TASK_TRACED"X (dead)", /*16 */ // EXIT_ZOMBIE "Z (zombie)", /*32 */ // EXIT_DEAD
};
• R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
• S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
• D磁盘休眠状态(Disk sleep): 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
• T停止状态(stopped): 可以通过kill指令发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
• t跟踪暂停 (tracing stop)
• X死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
• 僵尸状态(Zombies)
可以通过修改task_struct中表示状态的变量的值来修改状态。
3.2.2 进程管理(根据不同状态,链接到不同的队列进行细分)
前面我们在3.1.2讲到,进程列表是所有的PCB连接起来的。而对于进程管理要根据每个进程的状态区分。3.2.1节我们提到进程有很多种不同的状态。而不同的状态要通过不同的队列进行更加细致的管理。这种在进程列表下,又根据不同状态划分不同队列的结构类似网状结构。
状态符号 | 中文名 | 所在队列 | 解释 |
R (running) | 运行/就绪 | 就绪队列 (调度队列) | 正在CPU上运行或正在就绪队列中等待CPU调度。这是唯一会被调度器考虑的队列。 |
S (sleeping) | 可中断睡眠, 浅睡眠 | 等待队列 | 进程在等待某个事件(如I/O完成、信号量),等待可以被信号中断。会挂在对应的事件等待队列上。 |
D (disk sleep) | 不可中断睡眠 ,深度睡眠 | 等待队列 | 进程通常在等待磁盘I/O等硬件操作,睡眠状态不可被信号中断(防止数据丢失)。会挂在对应的硬件操作等待队列上。 |
T (stopped) | 暂停 | 无 | 进程被调试器(gdb)或使用kill的SIGSTOP等信号暂停,可见T状态。它不再参与调度,只是“挂起”,直到收到SIGCONT信号。 |
t (tracing stop) | 跟踪暂停 | 无 | 进程被调试器跟踪而暂停(类似于T (stopped)),是调试时的一种特殊状态。同样不参与调度。使用gdb的断点,运行到断点处暂停可见t状态。 |
Z (zombie) | 僵尸 | 无 | 进程已终止,但其退出状态尚未被父进程读取(wait())。其PCB残留,资源已释放,绝对不参与调度。 |
X (dead) | 死亡 | 无 | 进程的最终状态,PCB即将被系统销毁回收。不存在于任何队列中。 |
3.2.3 再谈PCB的结构,各种队列如何链接?
PCB的链式结构和我们的双向链表类似但不相同。它通过不同的结构体对象来链接进程列表或者不同的队列。
struct task_struct {// ... 很多其他字段 ...struct list_head tasks; // 用于连接全局进程列表struct sched_entity se; // 用于连接调度器的就绪队列(CFS红黑树)struct sched_rt_entity rt; // 用于实时调度器的队列// 当进程睡眠时,会使用这个字段连接到等待队列wait_queue_entry_t wait_queue_entry;// ... 很多其他字段 ...
};
3.2.4 进程状态查看(命令)
ps aux / ps axj 命令
• a:显示一个终端所有的进程,包括其他用户的进程。
• x:显示没有控制终端的进程,例如后台运行的守护进程。
• j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
• u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等
# 循环查看带头进程信息
while :; do ps ajx | head -1;ps ajx | grep myproc; sleep 1; done
R+,'+'表示在前台运行,没有‘+’则表示在后台运行。
3.2.5 理解运行、阻塞、挂起状态
1. 运行状态:进程在调度队列中,进程的状态都是running;
2. 阻塞 (Blocking),别名:睡眠 (Sleeping)
概念:
原因:进程自身的行为导致的。进程在运行过程中,需要等待某个事件发生或某个资源(键盘,显示器,网卡,磁盘,摄像头,话筒...)可用,而主动停止执行。S和D状态都是阻塞状态(睡眠状态)。
触发者:进程自己(通过系统调用,如 read, wait)。
例子:进程请求读取磁盘文件,在数据读回来之前,它无法继续执行,于是主动阻塞自己。
进程尝试获取一个已被其他进程持有的锁,于是主动阻塞自己,进入等待队列。
进程等待网络数据包到达。
状态:在Linux中,对应 S (TASK_INTERRUPTIBLE) 和 D (TASK_UNINTERRUPTIBLE) 状态。
资源:进程仍持有已分配的资源(如已打开的文件描述符),并且通常仍在内存中。
结束条件:等待的事件发生(如I/O完成、锁被释放)。
比喻:你在厨房烧水,水开需要时间。于是你主动离开灶台(放弃CPU)去看电视(进入阻塞状态)。等水壶叫了(事件发生),你再回来继续做饭(恢复执行)。
3. 挂起 (Suspending),别名:换出 (Swapping Out)
原因:系统资源不足导致的。通常是物理内存不足,操作系统需要将内存中暂时不能运行的进程(可能是阻塞态的进程)强制移动到磁盘的交换空间(Swap Space),以 freeing up 内存给其他进程使用。
触发者:操作系统(内核)。
例子:系统内存严重不足,内核选择将一个正在睡眠、优先级较低的进程换出到磁盘。
状态:挂起本身不是一个独立的状态,它是一个附加状态。一个进程可以是:阻塞且挂起、就绪且挂起。
资源:进程不占用物理内存(被换到磁盘上了),但仍然保留在进程列表中,PCB仍在内存。
结束条件:操作系统需要再次运行该进程时,会将其换入(Swapping In)回内存。
比喻:你的工作台(内存)太小,摆满了各种项目材料(进程)。现在有一个新项目急需开工,但没地方了。你只好把一個暂时不做的半成品项目(被阻塞的进程)打包成一个箱子,放到仓库(磁盘)里去。等以后需要继续做这个项目时,再把它从仓库搬回工作台。
补:操作系统如何对各种硬件资源管理?先描述,再组织!,像管理进程一样管理硬件设备。每个设备都有一个等待队列。比如假设硬件设备管理的结构体类型为struct device{},这个结构体里面会有struct task_struct *wait_queue这样一个对象作为等待队列。
补:进程状态的变化,表现之一,就是要在不同的队列中进行流动,本质都是数据结构的增删改查。
3.2.6 Z(zombie)-僵尸进程
1. 概念
• 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
• 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
举例:子进程运行5次后结束,但不给父进程信息。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>int main()
{pid_t id = fork();if(id == 0){// childint count = 5;while(count--){printf("我是子进程,我正在运行:%d\n",count);sleep(1);}}else{while(1){printf("我是父进程,我正在运行..\n");sleep(1);}}return 0;
}
新开终端使用以下命令查看进程状态,可见子进程为Z状态。
while :; do ps ajx | head -1;ps ajx | grep myproc; sleep 1; done
2. 僵尸进程的危害:
• 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
• 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
• 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
• 内存泄漏?是的!
3.2.7 孤儿进程
孤儿进程:父进程先退出,子进程就称之为“孤儿进程”
领养:孤儿进程被1号init/systemd进程(操作系统)领养,当然也要由init/systemd进程回收。孤儿进程会“脱离原终端的控制”。因此,原终端的控制命令(如Ctrl+C)无法结束它,必须使用kill命令。(kill -9 PID)
领养原因:父进程如果提前退出,那么子进程后退出,进入Z僵尸状态,造成内存泄露。
3.3 进程优先级
3.3.1 基本概念
概念:进程的到CPU资源的先后顺序,就是指进程的优先权(priority)。
原因:CPU资源固定,导致要通过优先级确认谁先谁后的问题。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
实现:优先级PRI的设置,值越小优先级越高,值越大优先级越低。
3.3.2 查看系统进程
在linux或者unix系统中,用 ps –l ( ps -al )命令则会类似输出一些进程信息,其中的几个重要信息如下:
名称 | 意义 |
UID | 代表执行者的身份,代表user名的编号,使用 ls -ln 查看(当我访问文件时系统会根据UID判断我是拥有者、所属组、还是other) |
PID | 代表这个进程的代号 |
PPID | 代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号 |
PRI | priority,代表这个进程可被执行的优先级,其值越小越早被执行(默认80) |
NI | 代表这个进程的nice值(默认0) |
3.3.3 PRI and NI(进程优先级)
PRI:进程的优先级,通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
NI:nice值,表示进程可被执行的优先级的修正数值。
实际优先级:PRI(new) = PRI(默认) + nice
优先级调整:在Linux下,调整进程优先级,就是调整进程nice值。(nice为负,PRI值变小,优先级变高,进程越早被执行,反之亦然)
nice其取值范围是-20至19,一共40个级别。(60 ~ 99)
在实际操作系统中总共有140个优先级:
• 普通优先级:100~139(将我们自己的优先级60~99依次对应到100~139)
• 实时优先级:0~99(不关心)
3.3.4 修改进程优先级(进程饥饿解释)
关键字 | 命令 |
top | 进入top后按“r”–>输入进程PID–>输入nice值 |
nice | nice -n <nice值> <命令> |
renice | renice <新nice值> -p <进程PID> |
优先级在系统进程中即可查看:ps -al( ps -l )
普通用户只能设置 0~19 的 nice 值(值越大优先级越低)。
root 用户可设置 -20~19 的所有值(需谨慎使用高优先级)。
优先级设置不合理,会导致优先级低的进程,长时间得不到CPU的资源,进而导致:进程饥饿。
3.3.5 基于时间片的分时操作系统
这是一种允许多个用户通过多个终端(通常是键盘和显示器)同时交互式地使用同一台主机的操作系统。它将CPU的运行时间划分成极小的、固定长度的时间段,每个时间段称为一个时间片。操作系统以轮转的方式,将CPU时间片分配给每个就绪的用户任务。
为什么需要它?—— 主要目标
公平性 (Fairness):防止任何一个用户进程或用户长期垄断CPU资源,确保所有交互式用户都能在可接受的时间内得到系统响应。
交互性 (Interactivity):极短的时间片保证了系统对用户操作的响应速度极快(通常在1-3秒内),从而提供了良好的交互体验。
高效性 (Efficiency):通过在进程因I/O而阻塞时快速切换到其他就绪进程,极大地减少了CPU的空闲时间,提高了CPU利用率。
举例:
一个生动的比喻:围棋裁判
想象一个围棋裁判(操作系统) 和一群棋手(用户进程)。
裁判负责控制比赛节奏。
他规定每位棋手每次只能思考10秒钟(时间片)。
一位棋手(进程A)下了一步棋后,裁判立刻按下计时器。10秒一到,无论这位棋手是否想好了下一步,裁判都会要求他暂停(剥夺CPU),然后立刻转向下一位棋手(进程B),给他10秒时间。
如果一位棋手在5秒内就快速下了一步(进程主动阻塞,如进行I/O操作),裁判不会傻等剩下的5秒,而是立刻转向下一位棋手。
通过这种方式,裁判在众多棋手之间快速轮转。从整个房间的视角看,所有棋盘都在同步推进,每个棋手都感觉裁判在持续关注自己。这就是分时系统的“魔法”。
3.3.6 补充概念-竞争、独立、并行、并发
• 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
• 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
• 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
• 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。(时间片技术是实现并发的主要方式。)
3.4 进程切换
3.4.1 进程切换概念
CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器,并开始下一个任务的运行, 这一过程就是context switch。
举例:
死循环进程如何运行?一旦一个进程占有CPU,不会把自己的代码直接跑完,当时间片时间到达后,CPU就会切换执行下一个进程。本次没执行完的死循环进程保存上下文数据,重新放入队列中,下一次轮到死循环进程时,再把上下文数据恢复,继续向后运行。(死循环进程不会一直占用CPU)
参考一下Linux内核0.11代码(由tss对象专门保存CPU寄存器中的数据)
3.4.2 进程调度队列,调度算法O(1)
下图是Linux2.6内核中进程队列的数据结构,之间关系也已经给大家画出来,方便大家理解
调度队列将需要执行的进程分为活跃进程和过期进程,活跃进程是在活跃队列的进程,过期进程是在过期队列的进程。
1. 进程调度解释:由于我们的系统是分时操作系统,在活跃队列中,操作系统会给每个进程固定的时间片,让每个进程排队接收CPU的处理。每个进程的时间片截止后去到过期队列的对应位置。当运行队列全部执行结束后,将活跃队列和过期队列进行swap(&active,&expired),交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!继续执行活跃队列,重复以上步骤。
2. 活跃队列:时间片还没有结束的所有进程都按照优先级放在该队列(active指针永远指向活跃队列)
3. 过期队列:过期队列上放置的进程,都是时间片耗尽的进程,当活跃队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。(expired指针永远指向过期队列)
4. 活跃队列/过期队列的结构中变量的解释:
nr_active:总共有多少个运行状态的进程
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大 提高查找效率!
queue[140]:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!结构参照哈希桶,但存放条件不同,进程队列按照优先级存放,哈希桶按照哈希值存放。
5. 进程调度的过程:
1. 从根据bitmap得到第一个非空队列的下标x,开始遍历queue[x],该队列必定为优先级最高的队列。
2. 依次运行此队列的每一个进程,之后再根据bitmap找到下一个非空队列,继续执行!
此种方法在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!
注意:
1. 就绪队列&运行队列,活跃队列&过期队列
就绪队列一个概念,运行队列是这个概念在Linux系统中实现的肉身。
活跃队列和过期队列是构成这个肉身的两个最重要的器官,即运行队列的两部分。
我们说“调度器从就绪队列中挑选进程”,在O(1)调度器的具体实现上,就是“调度器先从活跃队列中挑选进程;如果活跃队列空了,就交换指针,然后从新的活跃队列(原来的过期队列) 中挑选进程”。
2. 一个CPU拥有一个runqueue,如果有多个CPU就要考虑进程个数的负载均衡问题
3. 为什么要设置过期队列?
如果没有过期队列,一个进程a在时间片截止时就会再次根据自己的优先级回到活跃队列,如果此进程后面还有优先级低的进程b在等待,那么因为a的优先级高,a就类似做了插队的一个行为,下次CPU还是执行优先级高的进程a,就会导致高优先级的进程a一直占用CPU资源,而低优先级的进程b长时间得不到CPU的资源就会导致:进程饥饿!!
补:进程饥饿的负面影响(AI提供)
1. 系统性能下降和响应迟缓:从用户角度看,某些应用程序会“卡住”、“无响应”。你点击了某个窗口,但它迟迟没有反应,因为它的进程正在饥饿中。系统的整体吞吐量可能会下降,因为资源没有被公平高效地分配。
2. 用户体验极差:这是最直接的影响。用户无法与饥饿的进程进行交互,会导致挫败感。例如,你无法输入文字,无法点击按钮。
3. 可能引发更严重的问题(如死锁):如上文所述,饥饿有时是死锁的前奏或组成部分。一个低优先级进程因饥饿而无法运行去释放资源,会间接导致依赖该资源的其他高优先级进程也被阻塞。
4. 服务失效:如果饥饿的进程是系统关键服务或网络服务器的后台工作者,会导致服务超时、请求被丢弃,从而造成服务实质上的不可用。