七、Linux 进程

七、Linux 进程

    • [7. 1 进程相关概念](#7. 1 进程相关概念)
      • [7.1.1 进程概念](#7.1.1 进程概念)
      • [7.1.2 描述进程:PCB](#7.1.2 描述进程:PCB)
      • [7.1.3 Linux 的 PCB:`task_struct`](#7.1.3 Linux 的 PCB:task_struct)
        • [`task_struct` 内容分类](#task_struct 内容分类)
        • 详细解析
      • [7.1.4 组织进程](#7.1.4 组织进程)
      • [7.1.5 查看进程](#7.1.5 查看进程)
      • [7.1.6 系统调用获取 `PID`](#7.1.6 系统调用获取 PID)
      • [7.1.7 系统调用创建进程 fork](#7.1.7 系统调用创建进程 fork)
    • [7.2 进程状态](#7.2 进程状态)
      • [7.2.1 查看进程状态](#7.2.1 查看进程状态)
      • [7.2.2 阻塞状态](#7.2.2 阻塞状态)
      • [7.2.3 关于 swap 分区](#7.2.3 关于 swap 分区)
      • [7.2.4 Linux OS 具体进程状态](#7.2.4 Linux OS 具体进程状态)
        • [R 运行状态](#R 运行状态)
        • [S 睡眠状态](#S 睡眠状态)
        • [D 磁盘休眠状态](#D 磁盘休眠状态)
        • [T 停止状态](#T 停止状态)
        • [X 死亡状态](#X 死亡状态)
        • [Z 僵尸状态](#Z 僵尸状态)
      • [7.2.5 孤儿进程](#7.2.5 孤儿进程)
    • [7.3 进程优先级](#7.3 进程优先级)
      • [7.3.1 概念](#7.3.1 概念)
      • [7.3.2 系统进程信息](#7.3.2 系统进程信息)
      • [7.3.3 竞争独立并行并发](#7.3.3 竞争独立并行并发)
    • [7.4 进程切换](#7.4 进程切换)
    • [7.5 进程调度](#7.5 进程调度)

7. 1 进程相关概念

7.1.1 进程概念

进程(task)的概念:

  • 文字描述:程序的一个执行实例、正在执行的程序等。
  • 内核观点 :担当分配系统资源( CPU 时间、内存)的实体。

总结:进程 = 内核数据结构 + 自己程序的代码和数据

例如:执行命令、启动 APP,本质都是启动进程。

OS 内会同时运行很多的程序,每一个都要加载到内存,所以一定会存在很多的进程,而在没有启动进程之前,OS 就已经在内存中了。

了解了什么是进程,还要区分清楚什么不是进程

首先要知道:进程 = 内核对象数据结构 + 代码和数据,那么不符合这个定义的就不是进程。

我们知道:程序 = 代码 + 数据 ,而程序的本质是文件 ,所以程序本身并不是进程 。而且单独的内核对象数据结构也不是进程

OS 怎么管理进程 ?在 OS先描述 一个进程,再组织进程。

下面我们将会详细介绍,进程是如何被管理的。

7.1.2 描述进程:PCB

进程控制块Process Control Block),即 PCB 是操作系统中用于管理和控制进程运行 的关键数据结构 。它记录了操作系统所需的,用于描述进程当前情况以及控制进程运行的全部信息 。可以理解为进程属性的集合

每个进程都有一个唯一的 PCB,操作系统通过PCB来感知和管理进程的存在。

Linux 操作系统下的 PCB 是: task_struct

7.1.3 Linux 的 PCB:task_struct

task_struct 是 Linux 内核中的⼀种数据结构,它会被装载到 RAM(内存)中并且包含着进程的信息。也就是说:进程所有的属性信息直接或者间接通过 task_struct 这个结构体找到。

task_struct 内容分类

task_struct 包含了进程相关的各种信息,下面是其内容的分类:

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

一个进程执行代码会占用 CPU 资源,直到代码执行完成,才放弃占有 CPU 资源正确吗?

不正确。简单试想一下,假如代码是死循环,那么代码会永久执行下去,一直占用 CPU 资源,当 CPU 资源全被占用,那么系统就会卡死。这显然是不合理的,于是为了解决这一问题,当代计算机,都会给每一个进程分配一个时间片(基于时间片的轮转调度),时间片执行完毕,就会自动让出 CPU,让另一个进程执行

总结:一个进程没有执行完成,就有可能会把CPU出去。因为时间片到达,就会存在进程切换和调度的动作。

不同进程不断轮次占用 CPU 资源,这就叫做进程切换 。为了防止发生进程切换时,程序执行的位置/进度丢失,进程切换和调度会进行对应的保存上下文和恢复上下文数据。

上下文数据:进程对应的 CPU 内的寄存器的临时数据。

**CPU 内的寄存器硬件只有一套!**但是 CPU 内的寄存器硬件,可以在不同的时间段,保存不同进程的数据!!

7.1.4 组织进程

所有运行在系统⾥的进程都以 task_struct 双链表的形式存在内核⾥。

7.1.5 查看进程

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

在根目录/下,存在着一个保存进程信息的文件夹 proc,也就是说进程的信息可以通过 /proc 系统⽂件夹查看。例如:获取 PID 为10083的进程信息,就可以查看 /proc/10083 文件夹。

c 复制代码
ls /proc

ls /proc/1

当然查看时要有对应的权限,否则会 Permission denied,权限被拒。

  1. 大多数进程信息同样可以使⽤ topps 这些用户级工具获取
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    while(1)
    {
    	sleep(1);
    } 
    return 0;
}

运行情况:

c 复制代码
ps aux | grep test | grep -v grep
syt      2931757  0.0  0.0   2548  1404 pts/0    S+   21:49   0:00 ./test

ps axj | grep process

每次启动时的 pid 的值是不同的很正常,pid 在系统中会有一个全局变量维持 pid 的值,pid 的值是线性增长的,最后会回绕。

指令详情:

c 复制代码
ps aux / ps axj
  • a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
  • x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
  • j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息。
  • u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如用户、CPU和内存使⽤情况等。

cwd简介:

cwdCurrent Working Directory(当前工作目录)的缩写。

每个进程启动的时候,默认存在自己的 cwd。

cwd 就是您当前在命令行中的"位置",它决定了:

  • 相对路径从哪里开始解析
  • 命令默认在哪个目录执行文件操作
  • 命令的默认执行上下文

理解 cwd 对于正确使用相对路径和避免"文件未找到"错误至关重要!

chdir简介:

chdirChange Directory 的缩写,用于改变当前工作目录(cwd)的系统调用或命令。

权限考虑:确保程序有目标目录的执行权限,否则 chdir 会失败。

7.1.6 系统调用获取 PID

++只有一个进程,如何获取当前进程的其他属性?++

结构体内部元素地址知道,就转换为求结构体对象地址的问题。

意义:增加链式管理的扩展性。(例如:代码只需要维护一份)

一个进程如何获取自己的标识符

进程获取自己的标识符,就是获取自己的 task_struct 结构体内部的属性值 ,因此 OS就必须提供一个系统调用,提供给进程来获取自己的标识符PIDProcess Identifier)。

  • pid(Process Identifier)进程标识符。
  • ppid(Parent Process Identifier)父进程标识符。
c 复制代码
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    printf("pid: %d\n", getpid());
    printf("ppid: %d\n", getppid());
    return 0;
}

// 运行结果:
./test 
pid: 2932846
ppid: 2930368

7.1.7 系统调用创建进程 fork

概念简介

通过 man fork 查看 fork 的用法和功能。

因为篇幅受限,此处只展示部分内容,详情可以去看官方文档。

简要描述就是,fork 通过复制进程来创建新进程,新创建的进程是子进程,被复制的进程是父进程

关于复制过程:父子进程代码共享,数据各自开辟空间,私有⼀份(采⽤写时拷贝),简单来说:一份代码,两个进程。

返回值是 pid_t ,如果正确执行,子进程的 PID 返回给父进程,0会被返回给子进程。执行失败,父进程返回-1,不会创建子进程,错误码也会被设置。

c 复制代码
SYNOPSIS
	#include <unistd.h>
    pid_t fork(void);

DESCRIPTION
    fork() creates a new process by duplicating the calling process.  The new process is referred to as the child process.  The calling process is re‐
           ferred to as the parent process
           
RETURN VALUE
       On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  On failure, -1 is returned in the parent,  no
       child process is created, and errno is set to indicate the error.
测试示例
c 复制代码
#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
    { 
    	// parent
    	printf("I am father : %d!, ret: %d\n", getpid(), ret);
    } 
    sleep(1);
    return 0;
}


// 运行结果:
./test 
I am father : 2937451!, ret: 2937452
I am child : 2937452!, ret: 0

Linux系统中,新的进程,往往是通过父进程(bash...)的方式,创建出来的。

细节分析

fork() 的核心特点:

  1. 一次调用,两次返回(在父进程和子进程中分别返回)
  2. 写时拷贝(高效的内存管理)
  3. 进程拷贝 (子进程是父进程的副本)
  4. 并发执行 (父子进程并发运行)

返回值问题:

++为什么子进程返回0,父进程返回子进程的 pid?++

父进程:子进程 = 1 :n,父进程只有一个只需要判断创建成功与否,不需要标识。子进程可能同时存在多个,所有需要加以区分,也就是为了标识指定的一个子进程,便于控制相应的子进程。

fork();是函数,一个函数,怎么会 return 返回两次?

如果一个函数已经准备 return 了,那么它的核心功能实现了吗?答案是:已经实现了,但是缺少 return 说明,表示功能实现了。

同理:fork 创建子进程,创建完毕,就要能被 OS 调度执行了。

也就是说,fork 执行之后,在 return 之前,就已经存在父子进程了。

此时结论就水到渠成:父子进程各自执行了 return 语句。

一个id,同时接受两个不同的值?即 == 0,又 > 0?

默认情况下,fork 之后,代码和数据,一般都是父子共享的。

7.2 进程状态

7.2.1 查看进程状态

c 复制代码
ps aux / ps axj		// 查看进程

选项作用:

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

示例

c 复制代码
#include <iostream>
#include <unistd.h>

int main()
{

    std::cout << "我是一个进程: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
    std::cout << "------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------" << std::endl;
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        while(true)
        {
            std::cout << "我是fork后的子进程: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
            sleep(1);
        }
    }
    else
    {
        while(true)
        {
            std::cout << "回到父进程: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
            sleep(1);
        }
    }

    return 0;
}

7.2.2 阻塞状态

阻塞状态通常是由外部条件引起的,进程/线程因为等待某个事件的发生而无法继续执行。

"阻塞状态"是一个通用操作系统概念,它并不直接对应 Linux 中某一个特定的状态字母(如 SD)。但在 Linux 内核的实现中,阻塞状态通常体现为睡眠状态(Sleeping)

  • 特点:
    • 被动性: 进程/线程自身通常无法控制何时被阻塞,它是由于请求资源而自然进入的状态。
    • 不可预测的唤醒时间: 唤醒时间取决于外部事件何时发生(用户何时输入、数据何时从磁盘读出)。
    • 在Linux内核中 ,对应的状态通常是 TASK_UNINTERRUPTIBLE(不可中断睡眠,如等待磁盘I/O)或 TASK_INTERRUPTIBLE(可中断睡眠,如等待信号量)。

阻塞会出现什么现象?

例如:一个程序执行了 scanf() 函数等待用户输入。在用户按下回车键之前,进程等待事件完成,这个进程就处于阻塞状态

当事件完成,硬件就绪,最先知道的是操作系统。OS 是硬件的管理者,管理方法:先描述,再组织。

阻塞与运行的本质:是看 PCB 在谁提供的队列中。

7.2.3 关于 swap 分区

Swap 分区 (也称为交换分区)是 Linux 系统中用于虚拟内存管理的一块磁盘空间。

当物理内存(RAM)不足时,操作系统会将暂时不用的内存数据移动到 swap 分区,从而释放 RAM 给更需要的进程。这个过程称为换出(swapping out);

当需要再次使用这些数据时,再将其从 swap 分区读回 RAM,称为换入(swapping in)。

为什么需要 Swap?

  1. 缓解内存压力:当物理内存耗尽时,系统不会立即崩溃,而是利用 swap 作为"后备内存",保证系统继续运行。
  2. 支持内存回收与休眠
    • 系统可以将长时间未使用的内存页换出,让出更多 RAM 给文件缓存或活跃进程。
    • 系统休眠(suspend-to-disk)时,会将整个内存内容保存到 swap 分区,下次启动时恢复。
  3. 扩展可用内存:虽然 swap 比 RAM 慢得多(磁盘 vs 内存),但它让系统能运行超过物理内存大小的程序组合。

在 Linux 中,swap 可以是:

类型 说明 特点
交换分区 一个独立的磁盘分区,格式化为 swap 文件系统 性能较好(连续空间),是传统推荐方式
交换文件 在普通文件系统上创建一个大文件,作为 swap 使用 灵活,无需重新分区,可随时调整大小
多个交换区 可以同时启用多个 swap 设备/文件,并对它们设置优先级 提高性能或冗余

查看当前 swap:swapon --showfree -h

swap 分区存在的本质是:时间换空间。

不建议 swap 太大,防止过度 swap 导致系统变慢。

过分 swap 的危害:如果系统频繁换页(被称为"颠簸"),磁盘 I/O 会剧增,导致性能急剧下降。此时通常需要增加物理内存。

7.2.4 Linux OS 具体进程状态

这是 Linux 内核 3.15 版本的进程部分源代码:

c 复制代码
/*
*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 */
}

状态详情:

  • R 运行状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥。
  • S 睡眠状态(sleeping): 意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
  • D 磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T 停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。
  • X 死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
  • Z 僵尸状态 (zombie):当进程终止时,它会将自己的退出状态码(退出成功与否及任何错误码)写入其 task_struct 结构,若父进程未调用 wait()waitpid() 系统调用来回收,子进程就会变为僵尸状态。读取后或父进程本身也被终止,系统内核(init 进程)会接管并回收,该子进程才会彻底消失。

关于 T 和 t 状态:

T (Stopped) 和 t (Tracing stop) 都表示进程暂停,但触发和适用场景不同。T 通常由外部信号(如 SIGSTOP / SIGTSTP)触发,用于作业控制或系统管理;t 则由调试器触发(如 gdb),以实现断点调试、单步执行等内核跟踪功能。简单来说,一个是被动停止,一个是被动停止并用于调试。

在后续版本中又新增了:

  • I 空闲状态(Idle):用于内核空闲线程的不可中断睡眠。内核线程专用,不影响平均负载。

除此之外,还有一个常驻进程,注意概念不要混淆,,它并不属于进程的状态。**常驻进程:在后台运行且不受任何终端控制的特殊进程。**它们通常在系统启动时开始执行,并在系统关闭时才结束。常驻进程的主要目的是执行特定的系统任务,如处理网络请求、管理系统日志、监控系统状态等。

R 运行状态

一个CPU,一个调度队列。

**CPU 的进程调度算法:FIFO 算法,即先进先出(First In First Out)的调度队列。**如下图所示:

FIFO 是典型的非抢占调度算法。如果一个长时间运行的进程占据了 CPU,后续的短进程必须等待它完成。

  1. 优点

    • 实现简单,只需要一个队列。
    • 无需额外开销。没有优先级计算、时间片管理。
    • 公平(都要排队)。每个进程最终都能执行。
  2. 缺点

    • 响应较差。一个长进程会阻塞后面所有短进程,导致响应时间极差。
    • 不适合交互系统:用户键入命令后,可能因前方进程而长时间无响应。
    • 没有优先级机制。对于紧急的需要插队处理的任务没有设计。

现代通用操作系统(Linux、Windows)不会单独使用 FIFO ,而是在其基础上引入时间片轮转(RR)优先级抢占

就绪队列和进程状态

  • 在就绪队列中的进程处于 R 状态(可运行)。
  • 进程运行中若等待 I/O(如 read 磁盘),会进入 S 或 D 状态,并被移出就绪队列,放入对应设备的等待队列。
  • I/O 完成后,进程重新变为 R 状态,按照调度算法(FIFO 则放回队尾)重新加入就绪队列。
S 睡眠状态

S+的"+"表示是前台进程,此时不可以进行其他 Linux 指令操作。

运行时加上"&"就成为没有"+"的后台进程,此时可以进行其他 Linux 指令操作。

c 复制代码
kill -9 进程pid	//强制结束进程(CTRL+C也可以结束前台进程)
c 复制代码
kill -l		//列出所有信号
D 磁盘休眠状态

这种状态是 Linux 特有的进程状态。其设计目的是为了保护数据完整性和文件系统的一致性 。当进程需要与磁盘这类慢速设备进行同步的、关键的 I/O 操作 时,它会进入 D 状态。在此状态下:

  1. 进程无法被正常终止kill -9也无法强制结束。
  2. 进程不会响应任何信号:等待磁盘操作的完成。
T 停止状态

可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。

c 复制代码
kill -19 进程 pid	// 暂停
kill -18 进程 pid	// 恢复

t:追踪暂停状态:追踪一个状态,所处的状态gdb调试的时候。

X 死亡状态

这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。

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

示例

c 复制代码
#include <iostream>
#include <unistd.h>

int main()
{
    //  while :; do ps ajx | grep myprocess | grep -v grep;sleep 1;echo"#############################";done
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        std::cout << "子进程开始是Z状态: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
        sleep(3);
    }
    else
    {
        std::cout << "父进程sleeping: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
        sleep(10);
    }
    return 0;
}

进程退出时的退出信息main 函数的返回值 或者收到的信号值 。该退出信息保存到 进程自己的 task_struct 结构体中。

++检测到Z状态进程,回收Z状态进程,本质是在做什么?++

Z 僵尸状态 ,指的是只保留进程的 task_struct 结构体 ,以便未来让父进程 或者操作系统获取子进程的退出数据。

注意:子进程必须回收(最佳实践)

如果子进程不回收 ,导致进程永远处于 Z 僵尸状态 ,task_struct 结构体不会被释放,占据内存空间,最后造成内存泄露

但当对应的进程结束了,内存泄露问题就不会存在了。

7.2.5 孤儿进程

父进程如果提前退出,子进程后续退出,并进⼊ Z 状态,此时该如何处理?

父进程先退出,父进程被回收,子进程就称之为孤儿进程 。之后孤儿进程被1号 init 进程领养 ,当然由 init 进程回收

区分前后台进程:**能从键盘获取数据的是前台进程,反之是后台进程。**键盘只有一个,所以在获取输入的时候,只能有一个进程在获取键盘数据。

结论:前台进程任何时候只有一个。

示例

c 复制代码
#include <iostream>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        std::cout << "我是子进程: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
        sleep(10);
    }
    else
    {
        std::cout << "我是父进程: " << getpid() << " 我的父进程是: " << getppid() << std::endl;
        sleep(3);
    }

    // // 查看指令
    // //  while :; do ps ajx | grep myprocess | grep -v grep;sleep 1;echo"#############################";done
   	return 0;
}

7.3 进程优先级

7.3.1 概念

cpu 资源分配的先后顺序,就是指进程的优先权(priority)。 或者说:进程已经能得到某种资源的前提下,得到某种资源的先后顺序

进程优先级存在的原因:可分配资源不足,需要按照一定顺序分配资源。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

优先级和权限区分

  • 权限:能不能的问题。
  • 优先级:已经能了,先后问题。

7.3.2 系统进程信息

使用ps ‒l命令,会输出类似以下的内容

c 复制代码
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1002 3105462 3105461  0  80   0 -  2163 do_wai pts/0    00:00:00 bash
0 R  1002 3105500 3105462  0  80   0 -  2729 -      pts/0    00:00:00 ps

其中以下几个信息比较重要:

  • UID(User Identity):代表执行者的⾝份。
  • PID(Process Identity):代表这个进程的代号
  • PPID(Parent Process Identity):代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI(Priority):代表这个进程可被执行的优先级,其值越小越早被执行
  • NI(Nice):代表这个进程的nice值,其表示进程可被执行的优先级的修正数值

其中的 PRI 和 NI 其实就是 task_struct 结构体的两个整型变量。

PRI值越小越快被执行 ,那么加⼊ nice 值后,将会使得 PRI 变为:PRI(new) = PRI(old) + nice

这样,当 nice 值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在 Linux 下,就是调整进程 nice 值。

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

c 复制代码
top	// 修改 nice
	// 进⼊ top 后按"r" ‒> 输⼊进程 PID ‒> 输⼊ nice 值 

其他调整优先级的命令:nice,renice。

nice 值取值范围是[-20,19],⼀共40个级别。

关于优先级的一些常见问题:

  1. old pri 是多少?

    • Linux 下默认 PRI(old) 是80。
  2. 进程的优先级变化范围?

    • 80-20,80+19\]------\[60,99

  3. 一般会高频更改优先级吗?

    • 更改进程优先级不会(可以但一般不会)是一个高频操作。

结论:优先级变化有限。

为什么设计优先级变化有限?

**分时操作系统(**普遍),给进程分配时间片,相对公平的调度策略,较为均衡的让不同的进程的能在一段时间内,都得到 CPU 资源。所以改变优先级不能变化范围太大。

实时操作系统(一般在工业控制领域使用):保证任务在严格的时间限制(截止时间) 内完成。通常使用抢占式的静态或动态优先级调度算法(如速率单调调度RMS、最早截止时间优先 EDF)。高优先级任务可以立即抢占低优先级任务。

7.3.3 竞争独立并行并发

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

补充:父子进程是两个进程,两个进程有各自的 task_struct 结构,和相同的只读的一份代码,而数据是各自一份,所以父子进程同样具有独立性。

简单理解并行并发:

  • 并行:如果现在有多个厨师,每个厨师负责一个锅,那么每个锅都可以同时被翻炒。
  • 并发:一个厨师同时照看多个锅,他轮流翻炒每个锅里的菜,每个锅都在同时进行烹饪,但厨师在同一时刻只翻炒一个锅。

7.4 进程切换

CPU 寄存器保存指的是保存其内部的临时数据。

寄存器是共享的,但是寄存器内部的数据是进程私有的,叫做进程上下文。

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

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

7.5 进程调度

在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成本增加,我们称之为进程调度O(1)算法。

Linux 2.6内核中进程队列的数据结构之间的关系:

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

优先级

普通优先级:100〜139(可与 nice 值的取值范围对应)

实时优先级: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 指针的内容,就相当于有具有了⼀批新的活动进程。

优点

  1. 保证公平性 :通过 vruntime 和双队列机制,确保所有进程在一个周期内都能获得大致相等的 CPU 时间,有效防止了"进程饥饿"。
  2. 提高调度效率
    • 调度器在选择下一个进程时,永远只需要查看活跃队列,而活跃队列通常只包含需要立即运行的进程,这缩小了搜索范围。
    • 队列切换只是一个简单的指针交换操作,速度极快,开销极低。
  3. 实现O(1)时间复杂度:虽然 CFS 使用红黑树(查找复杂度为 O(logN)),但通过"缓存"最左节点,获取下一个进程的速度非常快。双队列模型进一步保证了操作的效率。
相关推荐
淼淼爱喝水1 小时前
Ansible 中 handler 与 notify 的作用与使用详解
linux·网络·apache·playbook
sbjdhjd1 小时前
Docker 安全优化实战手册(企业级硬核版)
linux·运维·docker·云原生·容器·eureka·kubernetes
小周技术驿站1 小时前
Linux 基础命令详解
linux·前端·chrome·ubuntu·centos
kdxiaojie1 小时前
U-Boot分析【学习笔记】(7)
linux·笔记·学习
www.021 小时前
通过 SSH 隧道将 GPT 调教为服务器专属 Agent(个人记录)
linux·服务器·vscode·gpt·大模型·ssh·api转发
张小姐的猫1 小时前
【Linux】多线程(中)—— 线程控制接口 | 线程库 | 线程局部存储
linux·运维·服务器
脆皮炸鸡7551 小时前
大山之二:文件系统(Ext系列)
linux·开发语言·经验分享·学习方法
打工人小夏1 小时前
使用finalshell在新服务器上部署前端页面
linux·服务器·前端·vue.js
Zhu7581 小时前
软件更新-openssh和openssl-centos
linux·运维·centos