计算机操作系统详细学习笔记(二):处理器管理

本文深入探讨处理器管理,包括处理器与寄存器的功能、指令与处理器模式的细节、中断的概念及处理,以及进程与多线程技术。此外,还详细讲解了处理器调度的不同层次和算法,如优先数、时间片轮转、分级调度和彩票调度算法。

前言

如果你对这篇文章可感兴趣,可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」,查看完整博客分类与对应链接。

文章目录


二、处理器管理

2.1 处理器与寄存器

2.1.1 处理器

CPU(运算单元+控制单元)

  • 指令译码器(ID):负责具体的解释指令的执行

  • 指令暂存器(IR):指令暂存在 IR 中

  • 程序计数器(PC):指向下一条要执行的指令的内存地址

  • 标志寄存器(flag):算术逻辑单元在执行结束后,会将结果汇总到标志寄存器(flag)中。

  • MAR(内存地址寄存器)、MDR(内存数据寄存器):实现对内存数据的访问。

上述所有部件均由内部总线连接通信。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NdqliGRS-1597622836110)(media/15700914003028.jpg)]

2.1.2 寄存器

用户程序可见寄存器

  • 可以使程序员减少访问主存储器的次数,提高指令执行的效率。
  • 分类
    • 数据寄存器(通用寄存器):用于存储数据,如AX、BX、CX、DX
    • 地址寄存器:索引(SI、DI)、栈指针(SP、BP)、段指针(CS、DS、SS、ES)

控制与状态寄存器

  • 用于控制处理器的操作,主要被具有特权的操作系统程序使用,以控制程序的执行。
  • 举例
    • 程序计数器(PC)、指令寄存器(IR)
  • 还需记住指令执行的关键状态
    • 条件码 CC:CPU为指令操作结果设置的位,标志正/负/零/溢出等结果。
    • 标志位:中断位、中断允许位、中断屏蔽位、处理器模式位、内存保护位…
2.1.3 程序状态字(PSW)

PSW

  • 操作系统的概念】,指记录当前程序运行的动态信息,通常包含:
    • 程序计数器、指令寄存器、条件码
    • 一系列标志位
  • 计算机系统的寄存器
    • 通常设置一组控制与状态寄存器
    • 也可以专设一个 PSW 寄存器

2.2 指令与处理器模式

2.2.1 机器指令

定义

机器指令是计算机系统执行的基本命令,是中央处理器执行的基本单元。

定性

指令完成各种算术逻辑运算、数据传输、控制流跳转特征码。

指令构成

指令由一个或多个字节组成,包括操作码字段、一个或多个操作数地址字段、以及一些表征机器状态的状态字以及特征码。

2.2.2 指令执行过程

大致流程概述

根据 PC 取指,放入 IR,并对指令译码,发出控制命令,执行微操作,完成一条指令执行。

一种指令执行步骤

  • 取指:根据 PC 从存储器或高速缓冲存储器中取指令到 IR
  • 解码:解译 IR 中的指令来决定其执行行为
  • 执行:连接 CPU,执行运算,产生结果并写回,设置CC;最后跳转指令跳转PC,其他指令递增PC
2.2.3 特权指令与非特权指令

背景

用户程序并非能够使用全部机器指令,那些与计算机核心资源相关的特殊指令会被保护。如启动 I/O 指令、置 PC 指令等。

具体分类

  • 核心资源相关的指令只能被操作系统程序使用。
  • 特权指令:只能被操作系统内核使用的指令。
  • 非特权指令:能够被所有程序使用的指令。
2.2.4 处理器模式

背景

计算机通过设置处理器模式实现特权指令管理。

模式分类

  • 计算机一般设置0、1、2、3等四种运行模式,分别对应:0操作系统内核、1系统调用、2共享库程序、3用户程序等保护级别。
  • 0模式可以执行全部指令;3模式只能执行非特权指令;其他每种运行模式可以规定执行的指令子集。

真实现状

一般来说,现代操作系统只使用0和3两种模式,对应于内核模式和用户模式。

2.2.5 模式切换

分类

仅包括 “用户模式 -> 内核模式” 和 “内核模式 -> 用户模式” 的转换。

转换方式

中断、异常或系统异常等事件导致用户程序向 OS 内核切换,触发:用户模式 -> 内核模式

  1. 程序运行时发生并响应中断
  2. 程序运行时发生异常
  3. 程序请求操作系统服务

OS内核处理完成后,调用中断返回指令,触发:内核模式 -> 用户模式

2.3 中断

2.3.1 中断的概念

定义

中断是指程序执行过程中,遇到急需处理的事件时,暂时中止CPU上现行程序的运行,转去执行相应的事件处理程序,待处理完成后再返回原程序被中断处或调度其他程序执行的过程。

定性

操作系统是 “中断驱动” ,即中断是激活操作系统的唯一方式。

中断分为广义与狭义,上述概念均指广义中断(广义中断 = 狭义中断 + 异常)。

中断、异常与系统异常

  • 狭义中断

狭义的中断指来源于处理器之外的中断事件,即与当前运行指令无关的中断事件,如 I/O 中断、时钟中断、外部信号中断等。

  • 异常

异常指当前运行指令引起的中断事件,如地址异常、算术异常、处理器硬件故障等。

  • 系统异常(trap)

系统异常指执行陷入指令而触发系统调用引起的中断事件,如请求设备、请求 I/O、创建进程等。【系统异常可以被认为是异常中的一类】

2.4 中断源

处理器硬件故障中断事件(异常)

  • 由处理器、内存储器、总线等硬件故障引起
  • 处理原则
    • 保护现场、停止设备、停止CPU,向操作员报告,等待人工干预。

程序性中断事件(异常)

  • 处理器执行机器指令引起
    • 算术异常(除数为0、操作数溢出)【简单处理】
    • 指令异常(非法指令、用户态使用特权指令、地址越界、非法存取)【终止进程】
    • 终止进程指令【终止进程】
    • 虚拟地址异常【调整内存后重新执行指令】

自愿性中断事件(系统调用)

  • 处理器执行陷入指令请求OS服务引起(请求分配外设、请求I/O)
  • 处理流程
    • 陷入OS、保护现场,根据功能号查入口地址,跳转具体处理程序。

I/O中断事件(狭义中断)

  • 来源于外围设备报告I/O状态的中断事件
    • I/O完成:调整进程状态,释放等待进程
    • I/O出错:等待人工干预
    • I/O异常:等待人工干预

外部中断事件

  • 由外围设备发出的信号引起的中断事件
    • 时钟中断、间隔时钟中断:记时与时间片处理。
    • 设备报到与结束中断:调整设备表。
    • 键盘/鼠标信号中断:根据信号作出相应反应。
    • 关机/重启动中断:写回文件,停止设备与CPU。

2.5 中断系统

2.5.1 中断系统概述

定义

  • 中断系统是计算机系统中响应和处理中断的系统,包括硬件子系统和软件子系统两部分。
    • 硬件子系统:负责中断响应
    • 软件子系统:负责中断处理

中断响应处理与指令执行周期

  • 在指令执行周期最后增加一个微操作,以响应中断
  • 注意:指令执行过程中不允许发生中断,指令具有原子性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ObgmluZv-1597622836114)(media/15700914194414.jpg)]

2.5.2 中断装置

定义

  • 计算机系统中发现并响应中断 / 异常的硬件装置称为中断装置。

分类

  • 由于中断源的多样性,存在多种中断装置,用于处理不同类型中断,一般因计算机而异,通常有:
  1. 处理器外的中断:由中断控制器发现和响应。
  2. 处理器内的异常:由指令的控制逻辑和实现线路发现和响应,响应机制称为陷阱。
  3. 请求OS服务的系统异常:处理器执行陷入指令时直接触发,响应机制称为系统陷阱。
2.5.3 中断控制器

定义

  • CPU中的一个控制部件,包括中断控制逻辑线路和中断寄存器。
    • 中断寄存器:中断类型、中断来源

执行过程

  1. 外部设备向其发出中断请求 IRQ,在中断寄存器中设置已发生的中断。
  2. 指令处理结束前,会检查中断寄存器,若有不被屏蔽的中断产生,则改变处理器内操作的顺序,引出操作系统中的中断处理程序。

特点

  • 检查响应中断(由CPU完成)的过程是一个同步的过程,而中断的请求由外围设备发生,是一个异步的过程。
2.5.4 陷阱与系统陷阱

定义

  • 指令的逻辑实现线路的一部分。

执行过程

  1. 执行指令出现异常后,会根据异常情况转向操作系统的异常处理程序。
  2. 出现虚拟地址异常后,需要重新执行指令,往往越过陷阱独立设置页面异常处理程序。
  3. 执行陷入指令后,越过陷阱处理,触发系统陷阱,激活系统调用处理程序。
2.5.5 中断响应过程

发现中断源,提出中断请求

  1. 发现中断寄存器中记录的中断【发现】
  2. 决定这些中断是否被屏蔽【是否屏蔽】
  3. 当有多个要响应的中断源时,根据规定的优先级选择一个【优先级】

中断当前程序的执行

  • 保存当前程序 PSW/PC 到核心栈(硬件保护)

转向操作系统的中断处理程序

2.5.6 中断的处理

中断处理程序(软件保护)

  • 定性

OS 处理中断事件的控制程序,主要任务是处理中断事件和恢复正常操作。

  • 主要功能
  1. 保护未被硬件保护的处理器状态(硬件只保护了 PSW/PC)
  2. 分析被中断进程的 PSW 中断码字段,识别中断源
  3. 分别处理发生的中断事件

恢复正常操作

  1. 恢复原进程】处理完毕后直接返回被中断进程(如时钟中断)
  2. 恢复另一个进程】中断当前进程运行,调整进程队列,启动进程调度,选择下一个执行的进程。(如输入输出进程)
2.5.7 中断系统处理流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5RbOcBC-1597622836115)(media/15700914434459.jpg)]

解析

  1. 运行进程(硬件):在最后一个周期检测有无中断发生

  2. 交换新旧PSW(硬件)

    • 中断当前程序运行,保存当前程序的 PSW/PC 到核心栈

    • 并将OS内核系统的PC置到程序PC寄存器中,使得OS内核程序得以占用内核程序运行

  3. 分析中断源(软件)

    • 根据中断码字段,识别中断源
  4. 分别处理各类中断事件(软件)

    • 恢复自己的进程,或恢复其它进程
  5. 恢复PSW(硬件)

    • 弹出要恢复进程的PSW

2.6 多中断的响应与处理

2.6.1 中断屏蔽

特点(有选择的响应中断)

  • 当计算机检测到中断时,中断装置通过中断屏蔽位决定是否响应已发生的中断。
2.6.2 中断优先级

特点(有优先度的响应中断)

  • 当计算机同时检测到多个中断时,中断装置响应中断的顺序。

例子(不同OS有不同的中断优先级)

  • 故障中断 > 自愿性中断 > 程序性中断 > (时钟中断)外部中断 > I/O中断 > 重启关机中断
2.6.3 中断的嵌套处理

特点

  • 中断处理过程中,可以再响应其它中断
  • 改变中断处理次序,先响应的可能后处理

限制

  • 中断响应处理有硬件要求,通常限制在 3 层之内

2.7 进程及其状态

2.7.1 进程的概念

进程的提出

  • OS为了全方位地管理计算机系统中运行的程序,为正在运行的程序建立了一个管理实体 —— 进程

定义

  1. 进程是 (一个具有一定独立功能的程序关于某个数据集合的) 一次运行活动。
  2. 进程是OS进行资源分配和调度的一个独立单位。

进程分为 5 个实体部分

  1. (OS管理运行程序的)数据结构 P
  2. (运行程序的)内存代码 C
  3. (运行程序的)内存数据 D
  4. (运行程序的)通用寄存器信息 R
  5. (OS控制程序执行的)程序状态字信息 PSW

进程的共享

  1. 共享D:不同程序在相同数据集上运行 —— 构成两个共享数据的交往进程

  2. 共享C

    • 相同代码在不同数据集上运行 —— 构成两个共享代码的无关进程
    • 共享的代码称为可再入程序,如编辑器。可再入程序必须是纯代码的,不能带任何局部数据区。
  3. 注意

    • 前述的程序与数据集均是内存级的。
    • 在不同时段中针对(同一个外存数据文件)运行(同一个外存程序文件),意味着完全不同的(P,C,D,R,PSW),因此两次运行构成两个不同的进程。

概念级的进程状态

  1. 运行态
    • 指进程占有处理器运行
  2. 就绪态
    • 指进程具备运行条件等待处理器运行
  3. 等待态
    • 指进程由于等待资源、输入输出、信号等而不具备运行条件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5yEwGp1W-1597622836117)(media/15700914621062.jpg)]

进程挂起的概念

原因:OS无法预期进程的数目与资源需求,计算机系统在运行过程中可能出现资源不足的情况

运行资源不足的表现:性能低、死锁

解决办法(进程挂起)

  • 剥夺某些进程的内存及其他资源,调入OS管理的对换区,不参加进程调度,待适当时候再调入内存、恢复资源、参与运行

挂起态与等待态的区别:后者占有已申请到资源处于等待,前者没有任何资源。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ECYkKbJ-1597622836118)(media/15700914804210.jpg)]

2.8 进程的数据描述

2.8.1 进程控制块(PCB)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zJbM1HPg-1597622836119)(media/15700915246241.jpg)]

标识信息

功能:用于存放唯一标识该进程的信息

存放信息:系统分配的标识号(进程组标识号)、用户定义的进程名(进程组名)

现场信息

功能:用于存放该进程运行时的处理器现场信息

处理器现场信息:

  • (用户可见寄存器内容)数据寄存器、地址寄存器
  • (控制与状态寄存器内容)PC、IR、PSW
  • (栈指针内容)核心栈与用户栈指针

控制信息

功能:用于存放与管理、调度进程相关的信息

管理、调度进程相关信息:

  • (调度相关信息)状态、等待事件 / 原因、优先级
  • (进程组成信息)代码 / 数据地址、外存映像地址
  • (队列指引元)进程队列指针、父子兄弟进程指针
  • (通信相关信息)消息队列、信号量、锁
  • (进程特权信息)内存访问权限、处理器特权
  • (处理器使用信息)占用的处理器、时间片、处理器使用时间 / 已执行总时间、记账信息
  • (资源清单信息)正占有的资源、已使用的资源
2.8.2 进程映像(Process Image)

定义

某一时刻进程的内容及其执行状态集合。

进程映像是内存级的物理实体,又被称为进程的内存映像。

组成部分

  1. 进程控制块
    保存进程的标识信息、状态信息和控制信息

  2. 进程程序块
    进程执行的程序空间

  3. 进程数据块
    进程处理的数据空间,包括数据、处理函数的用户栈和可修改的程序

  4. 核心栈
    进程在内核模式下运行时使用的堆栈,中断或系统过程使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZBNnJwY-1597622836119)(media/15700915519325.jpg)]

2.8.3 进程上下文

出现原因

进程的执行需要环境支持,包括 CPU 现场和 Cache 中的执行信息

上下文分类

OS 中的进程物理实体和支持进程运行的环境共同构成进程上下文。

  1. 用户级上下文
    用户程序块 / 用户数据区 / 用户栈 / 用户共享内存

  2. 寄存器上下文
    PSW / 栈指针 / 通用寄存器

  3. 系统级上下文
    PCB / 内存区表 / 核心栈

特点

进程上下文刻画了进程的执行情况

2.9 进程的管理

2.9.1 概念级 OS 进程管理软件
  1. 系统调用 / 中断 / 异常处理程序

  2. 队列管理模块

  3. 进程控制模块

  4. 进程调度程序(独立进程居多)

  5. 进程通信程序(多个程序包)

2.9.2 进程实现的队列模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-srbRbuMK-1597622836120)(media/15700945577701.jpg)]

2.9.3 队列管理模块
  1. 进程管理的核心模块

  2. 多种进程队列,如就绪队列、等待队列、先进先出队列、优先队列

  3. 队列中进程通过 PCB 中的队列指引元索引连接

2.9.4 进程的控制与管理

进程的创建

进程表加一项,申请PCB并初始化,生成标识,建立映像,分配资源,移入就绪队列

进程撤销

从队列中移除,归还资源,撤销标识,回收PCB,移除进程表项

进程阻塞

保存现场信息,修改PCB,移入等待队列,调度其他进程执行

进程唤醒(对应阻塞)

等待队列中移出,修改PCB,移入就绪队列(该进程优先级高于运行进程则触发抢占)

进程挂起

修改状态并移出入相关队列,收回内存等资源送至缓冲区

进程激活(对应挂起)

分配内存,修改状态并出入相关队列

2.9.5 原语与进程控制原语

目的:为了避免发生错误,对进程控制过程中涉及OS核心数据修改的部分采用原语实现。

原语定义

由若干条指令构成的完成某种特定功能的程序,执行上具有原子性,执行过程中不可以被中断、分割。即进入原语前把中断关掉,原语结束后把中断打开,因此原语必须是短小精悍的代码段。

分类

进程控制原语、进程通信原语

2.10 模式切换与进程切换

2.10.1 模式切换

概念

即用户模式和内核模式之间的相互切换。

分类

  1. 用户模式 ⟶ \longrightarrow 内核模式:中断用户进程执行而触发(中断、异常、系统调用)
  2. 内核模式 ⟶ \longrightarrow 用户模式:将控制权交还用户进程而触发(中断返回指令)

基本工作任务

正向切换(中断装置):

  • 处理器模式转为内核模式
  • 保存当前进程的PC/PSW值到核心栈
  • 转向中断/异常/系统调用处理程序

逆向切换(中断返回指令):

  • 从待运行进程核心栈中弹出PSW/PC值
  • 处理器模式转为用户模式
2.10.2 进程切换

概念

进程切换指从正在运行的进程中收回处理器,让待运行进程来占有处理器运行。

进程切换实质上就是被中断运行进程与待运行进程的上下文切换。因此进程切换必须在OS内核模式下完成,需要模式切换。

具体处理过程

(保存旧的 ⟶ \longrightarrow 找个新的 ⟶ \longrightarrow 让新的上)

  1. 【保存旧的】(中断/异常等触发)正向模式切换并压入PSW/PC

  2. 保存被中断进程现场信息、处理具体中断

  3. 把被中断进程的系统堆栈指针SP值保存到PCB、调整被中断进程的PCB信息、将PCB加入相关队列

  4. 【找个新的】选择下一个占用CPU运行的进程

  5. 【让新的上】修改被选中进程的PCB信息,如进程状态

  6. 设置被选中进程的地址空间,恢复存储管理信息,恢复被选中进程的SP值以及现场信息

  7. (中断返回指令触发)逆向模式转换并弹出

发生时机

  1. 进程进入等待态(阻塞式系统调用、虚拟地址异常)

  2. 进程进入就绪态(时间片/IO中断、更高优先级进程插队)

  3. 进程进入终止态

2.11 多线程技术概述

2.11.1 单线程结构进程

概念

由资源部分 + 一个执行序列组成,非常经典、传统的结构。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLUwSEKz-1597622836121)(media/15701014076883.jpg)]

缺点

在并发程序设计中,存在进程切换开销大、进程通信开销大的问题,降低了并行计算的效率。

2.11.2 多线程结构进程

特点

出现了多个执行序列,提高了并发运算的效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-owYG6WDH-1597622836121)(media/15701017369033.jpg)]

多线程环境下进程的概念

在多线程环境中,进程是操作系统中进行保护和资源分配的独立单位,主要包含以下两个部分。

  1. 用来容纳进程映像的虚拟地址空间
  2. 对进程、文件和设备的存取保护机制

多线程环境下线程的概念

线程是进程的一条执行路径,是调度的基本单位,同一个进程中的所有线程共享进程获得的主存空间和资源。主要包括以下内容:

  1. 线程执行状态
  2. 受保护的线程上下文,当线程不运行时,用于存储现场信息
  3. 独立的程序指令计数器
  4. 执行堆栈
  5. 容纳局部变量的静态存储器

多线程环境下线程的状态与调整

线程状态:运行、就绪和睡眠,无挂起态

线程操作:孵化(创建)、封锁(进入等待态)、活化(结束等待态)、剥夺(离开CPU)、指派(分配CPU)、结束

OS感知线程环境下:

  • 处理器调度对象是线程
  • 进程没有三状态(或只有挂起态)

OS不感知线程环境下:

  • 处理器调度对象仍是进程
  • 由用户来调度线程

并发多线程程序设计的优点

快速线程切换、减少系统开销、线程通信易于实现、并行程度提高、节省内存空间

2.12 KLT与ULT

2.12.1 KLT

概念

KLT(Kernel-Level Threads),内核级线程机制。

线程管理的所有工作都由OS内核来做,OS直接调度KLT,OS提供了一个应用程序设计接口API,供开发者使用KLT。

特点

  1. 进程中的一个线程被阻塞了,内核能调度同一进程的其它线程占有处理器运行。
  2. 多处理器环境中,内核能同时调度同一进程中多个线程并行执行。
  3. 内核自身也可用多线程技术实现,能提高操作系统的执行速度和效率。
  4. 应用程序线程在用户态运行,线程调度和管理在内核实现,在同一进程中,控制权从一个线程传送到另一个线程时需要模式切换。(开销较大)
2.12.2 ULT

概念

ULT(User-Level Threads),用户级线程。

用户空间运行的线程库,提供多线程应用程序的开发和运行,任何应用程序均需通过线程库进行程序设计,再与线程库连接后运行。

线程管理的所有工作都由应用程序完成,内核没有意识到线程的存在。

特点

  1. 所有线程管理数据均在进程用户空间中,线程切换不需要内核模式。(降低切换开销)
  2. 允许进程定义调度算法,使调度更精准。
  3. 可移植性强,能运行在任何OS上。
  4. 【缺点1】不能利用多处理器的优点,OS调度进程,仅有一个ULT能执行。
  5. 【缺点2】一个ULT的阻塞,将引起整个进程的阻塞。
2.12.3 Jacketing 技术

改造系统调用,将阻塞式系统调用改造成非阻塞式的。

当线程陷入系统调用时,执行jacketing程序,使OS把控制权切换到用户线程调度程序,来选择下一个线程执行。

2.12.4 ULT vs. KLT

【相同点】进程是资源保护的单位,一个进程中可以有多个线程,且线程编程接口是内置的,且线程间可以共享资源。

【不同点】KLT线程调度由OS完成。ULT线程调度由用户定义,处理器仍然是进程调度,且利用jacketing技术进行进程调度和线程调度的切换。

【各自优势】ULT适用于解决逻辑并行性(用户定义线程调度),KLT适用于解决物理并行性(真正的多核并行线程)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b1S16g7O-1597622836122)(media/15701036574822.jpg)]

2.13 多线程实现的混合策略

2.13.1 多线程实现的混合式策略

OS通过内核来支持内核级多线程,由OS程序库来支持用户级多线程。

将ULT和KLT进行多对多的绑定,使得逻辑并行性和物理并行性都得到较好的体现。

特点

  1. 组合了KLT与ULT
  2. 线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行
  3. 一个应用中的多个ULT被映射到一些KLT上
  4. 程序员可以针对特定应用和机器调节内核级线程的数目,以达到整体最佳结果
2.13.2 线程混合式策略下的线程状态

特点

  1. KLT三态,系统调度负责
  2. ULT三态,用户调度负责
  3. 活跃态ULT代表绑定了一个KLT,而KLT有可能为三态中的任意一态
  4. 活跃态ULT运行时可激活用户调度
  5. 非阻塞系统调用可使用Jacketing启动用户调度,调整活跃态ULT
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2mouaXlM-1597622836123)(media/15701047713535.jpg)]

总结

  • KLT:OS直接调度KLT,物理并行性好
  • ULT:OS调度进程,用户调度ULT,逻辑并行性好
  • 混合式线程:ULT可以直接分配到CPU上,也可以和KLT进行多对多匹配,实现了物理并行性与逻辑并行性共存
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAdIwCau-1597622836123)(media/15701049646546.jpg)]

2.14 处理器调度

2.14.1 处理器调度的层次

高级调度(长程调度、作业调度)

决定一个程序能否加入到执行的进程池中。

低级调度(短程调度、进程调度)

决定哪一个可用进程占用处理器执行。

中级调度(平衡负载调度)

当OS负荷过大时,可以将一些进程进入挂起状态,决定主存中的可用进程集合。

处理器调度层次与关键状态转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UutIefIm-1597622836124)(media/15704267669898.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-du8tk6NF-1597622836125)(media/15704269143160.jpg)]

从中可以发现三级调度在进程状态转换方面的主要功能,其中低级调度仅仅只是从就绪态到运行态的转换。而等待态 ⟶ \longrightarrow 就绪态,运行态 ⟶ \longrightarrow 等待态,运行态 ⟶ \longrightarrow 终止态则不由调度决定,而由程序本身决定。

2.14.2 高级调度

分时OS中的功能

  1. 决定是否接受一个终端用户的连接
  2. 决定命令能否被系统接纳并构成进程
  3. 决定新建态进程是否加入就绪进程队列

批处理OS中的功能

高级调度在批处理OS中又称为作业调度,功能是按照某种原则从后备作业队列中选取作业进入内存,并为作业做好运行前的准备工作和完成后的善后工作。

2.14.3 中级调度

目的

引入该调度的主要目的是为了提高内存利用率和作业吞吐率。

功能

中级调度决定哪些进程被允许驻留在主存中参与竞争处理器及其他资源,起到短期调整系统负荷的作用。

中级调度把一些进程换出主存,从而使之进入“挂起”状态,不参与进程调度,以平顺系统的负载。

2.14.4 低级调度

概念

低级调度即处理器调度,按照某种原则把处理器分配给就绪态进程或内核级线程。

进程调度程序

实现处理器调度的程序,是OS的最核心部分,是所有进程的父进程。其处理器调度策略优劣直接影响整个OS的性能。

主要功能

  1. 记住进程或内核级线程的状态
  2. 决定某个进程或内核级线程什么时候获得处理器,以及占用多长时间
  3. 把处理器分配给进程或内核级线程
  4. 收回处理器

2.15 处理器调度算法

2.15.1 调度算法原则

资源利用率(越高越好)

需要令CPU或其他资源的使用率尽可能高且能够并行工作。

响应时间(越短越好)

使交互式用户的响应时间尽可能小,或尽快处理实时任务。

响应时间 = 等待时间+运行时间(从提交第一个请求到产生第一个响应所用时间)

响应比 = (等待时间+运行时间)/ 运行时间。

周转时间(越短越好)

提交给系统开始到执行完成获得结果为止的这段时间间隔称周转时间。

周转时间 = 结束时间-提交时间

等待时间 = 周转时间-CPU使用时间

  • 周转时间与响应时间的辨析

    1. 在批处理系统中,产生第一次响应时,就是作业完成了。而在分时系统中,时间片结束后,就认为产生了第一个响应,而作业不一定完成了。
    2. 因此二者仅在批处理系统中相等。
  • 例题
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d6jyWZ54-1597622836126)(media/15704298292750.jpg)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZXNIHUb9-1597622836127)(media/15704298613667.jpg)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eHg8m81d-1597622836128)(media/15704298818009.jpg)]
    因此选 D。此题完美解释了分时系统中响应时间和周转时间的差别。

吞吐量(越多越好)

单位时间处理的进程数。

公平性

确保每个用户每个进程获得合理的CPU份额或其他资源份额。

2.15.2 优先数调度算法

基础概念

根据分配给进程的优先数决定运行进程的先后次序。

分类

  1. 抢占式优先数调度算法:一旦高优先级出现,则必须占用CPU,不存在等待。

  2. 非抢占式优先数调度算法:CPU空闲之后才去选择一个高优先级的。

优先数的确定准则

进程负担任务的紧迫程度、进程交互性、进程使用外设频率、进入系统的时间

与进入系统时间相关的优先数

  1. 计算时间短优先
  2. 剩余计算时间短优先
  3. 响应比高者优先( 响应比 = 等待时间 + 执行时间 执行时间 响应比=\displaystyle\frac{等待时间+执行时间}{执行时间} 响应比=执行时间等待时间+执行时间
  4. 先来先服务(先进队则先被选择)
    • 多用于高级调度;低级调度中则并不常用。
2.15.3 时间片轮转调度算法

算法概念

根据各个进程进入就绪队列的时间,先后轮流占有CPU一个时间片。时间片到了则采用时间片中断中断进程。

时间片长短确定

选择长短合适的时间片,过长则退化为先来先服务算法,过短则调度开销大。

类别

单时间片、多时间片、动态时间片

2.15.4 分级调度算法

算法概念

分级调度算法又称为多队列策略,反馈循环队列。

基本思想

  1. 建立多个不同优先级的就绪进程队列
  2. 多个就绪进程队列间按照优先数调度
  3. 高优先级就绪进程,分配的时间片短
  4. 单个就绪进程队列中进程的优先数和时间片相同(先进先出)

举例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5vd26ff-1597622836128)(media/15704314117896.jpg)]

三个优先级的队列,外设进入高队列,磁盘进入中队列,如果时间片超时则进入低队列。

分级算法的分级原则

  • 一般分级原则

    • 外设访问、交互性、时间紧迫程度、系统效率、用户立场…
  • 现代操作系统的实现模型(结合时间片+优先数+分级队列)

    • 多个高优先级的实时进程队列,如:硬实时、网络、软实时
    • 多个分时任务的进程队列,根据基准优先数和执行行为调整
    • 队列数可能多达 32~128 个
2.15.5 彩票调度算法

基本思想

为进程发放针对系统各种资源(如CPU时间)的彩票;当调度程序需要做出决策时,随机选择一张彩票,持有该彩票的进程将获得系统资源。

OS发展到后期时出现的一个算法,非常有意义。

功能

比如一个服务器、十个客户机的程序。一开始给每个客户机10张彩票,不给服务器彩票。而当客户机请求服务器时,客户机则将其彩票交换到服务器上去。

当十个客户机都将彩票交换到服务器时,服务器直接获得CPU资源。因此彩票调度算法非常有意义,是根据具体运行过程中的需求进行分配。

2.16 内核调度算法

2.16.1 内核调度程序

应用程序类型

  1. IO 消耗型:网络IO、磁盘IO

  2. CPU 消耗型

优先队列

struct prio_array{
    unsigned int nr_active;  //等待执行的进程数
    unsigned long bitmap[BITMAP_SIZE]; //用位的方式,表示每个优先级队列的状态
    struct list_head queue[MAX_PRIO]; //对应于bitmap,存储所有等待运行的进程
}

unsigned long bitmap[BITMAP_SIZE] 解析:

  • 一个 unsigned long 在内核中有 32 32 32 位,其用位的方式,表示某个优先级上有没有待处理的队列,是实现快速找到最高待处理优先进程的关键。如果我定义了四种优先级,我只需要四位就能表示某个优先级上有没有进程要运行,例如优先级是 2 2 2 3 3 3 上有进程,那么就应该是 0110 0110 0110

  • #define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long)),因此一共 140 140 140 种优先级,BITMAP_SIZE 为 4 4 4 32 ∗ 5 = 160 > 140 32*5=160>140 325=160>140,包含了 140 140 140 位优先级。

如何快速找到优先级最高的队列

idx = sched_find_first_bit(array->bitmap);

// 返回值为最高优先级所在队列的序号,queue = array->queue + idx; 即可取到要处理的进程队列
static inline int sched_find_first_bit(unsigned long *b)
{
	if (unlikely(b[0]))
		return __ffs(b[0]);
	if (unlikely(b[1]))
		return __ffs(b[1]) + 32;
	if (unlikely(b[2]))
		return __ffs(b[2]) + 64;
	if (b[3])
		return __ffs(b[3]) + 96;
	return __ffs(b[4]) + 128;
}

static inline int __ffs(int x)
{
	int r = 0;
 
	if (!x)
		return 0;
	if (!(x & 0xffff)) {
		x >>= 16;
		r += 16;
	}
	if (!(x & 0xff)) {
		x >>= 8;
		r += 8;
	}
	if (!(x & 0xf)) {
		x >>= 4;
		r += 4;
	}
	if (!(x & 3)) {
		x >>= 2;
		r += 2;
	}
	if (!(x & 1)) {
		x >>= 1;
		r += 1;
	}
	return r;
}

CPU 上的 runqueue

struct runqueue {
	spinlock_t lock;   //自旋锁,nginx里解决惊群现象时也是用这个。
	/*
	 * nr_running and cpu_load should be in the same cacheline because
	 * remote CPUs use both these fields when doing load calculation.
	 */
	unsigned long nr_running;
#ifdef CONFIG_SMP
	unsigned long cpu_load;
#endif
	unsigned long long nr_switches;
 
 
	/*
	 * This is part of a global counter where only the total sum
	 * over all CPUs matters. A task can increase this counter on
	 * one CPU and if it got migrated afterwards it may decrease
	 * it on another CPU. Always updated under the runqueue lock:
	 */
	unsigned long nr_uninterruptible;
 
 
	unsigned long expired_timestamp;
	unsigned long long timestamp_last_tick;
	task_t *curr, *idle;
	struct mm_struct *prev_mm;
	prio_array_t *active, *expired, arrays[2]; //优先级队列,runqueue中一共有两个
	int best_expired_prio;
	atomic_t nr_iowait;
	... ...
};

自旋锁与普通锁的区别:

  • 【普通锁】使用普通锁时,你去试图拿一把锁,结果发现已经被别人拿走了,你就在那睡觉,等别人锁用完了叫你起来。所以如果有一个人拿住锁了,一百个人都在门前睡觉等。当之前的人用完锁回来后,会叫醒所有100个等锁的人,然后这些人开始互相抢,抢到的人拿锁进去,其他的人继续等。

  • 【自旋锁】自旋锁不同,当他去拿锁发现锁被别人拿走了,他在那不睡觉的等,稍打个盹就看看自己主动看看锁有没有还回来,即反复检查锁变量是否可用。自旋锁效率高,但自旋锁仅适用于锁使用者保持锁时间比较短的情况下,因为反复检查锁的过程非常降低效率。(忙式等待)

调度函数

runqueue中的两个优先队列:

  • a c t i v e active active 是还有时间片的进程队列,而 e x p i r e d expired expired 是时间片耗尽必须重新分配时间片的进程队列。
array = rq->active;
if (unlikely(!array->nr_active)) {
	/*
	 * Switch the active and expired arrays.
	 */
	schedstat_inc(rq, sched_switch);
	rq->active = rq->expired;
	rq->expired = array;
	array = rq->active;
	rq->expired_timestamp = 0;
	rq->best_expired_prio = MAX_PRIO;
} else
	schedstat_inc(rq, sched_noswitch);

当所有运行进程的时间片都用完时,就把 a c t i v e active active e x p i r e d expired expired 队列互换指针,而时间片耗尽的进程在出 a c t i v e active active 队列入 e x p i r e d expired expired 队列时,已经单独的重新分配好新时间片了。

s c h e d u l e schedule schedule 调度函数

asmlinkage void __sched schedule(void)
{
	long *switch_count;
	task_t *prev, *next;
	runqueue_t *rq;
	prio_array_t *array;
	struct list_head *queue;
	unsigned long long now;
	unsigned long run_time;
	int cpu, idx;
 
 
	/*
	 * Test if we are atomic.  Since do_exit() needs to call into
	 * schedule() atomically, we ignore that path for now.
	 * Otherwise, whine if we are scheduling when we should not be.
	 */
	if (likely(!(current->exit_state & (EXIT_DEAD | EXIT_ZOMBIE)))) {先看看当前运行进程的状态
		if (unlikely(in_atomic())) {
			printk(KERN_ERR "scheduling while atomic: "
				"%s/0x%08x/%d\n",
				current->comm, preempt_count(), current->pid);
			dump_stack();
		}
	}
	profile_hit(SCHED_PROFILING, __builtin_return_address(0));
 
 
need_resched:
	preempt_disable();
	prev = current;
	release_kernel_lock(prev);
need_resched_nonpreemptible:
	rq = this_rq(); //这行找到这个CPU对应的runqueue,再次强调,每个CPU有一个自己的runqueue
 
 
	/*
	 * The idle thread is not allowed to schedule!
	 * Remove this check after it has been exercised a bit.
	 */
	if (unlikely(current == rq->idle) && current->state != TASK_RUNNING) {
		printk(KERN_ERR "bad: scheduling from the idle thread!\n");
		dump_stack();
	}
 
 
	schedstat_inc(rq, sched_cnt);
	now = sched_clock();
	if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG))
		run_time = now - prev->timestamp;
	else
		run_time = NS_MAX_SLEEP_AVG;
 
 
	/*
	 * Tasks with interactive credits get charged less run_time
	 * at high sleep_avg to delay them losing their interactive
	 * status
	 */
	if (HIGH_CREDIT(prev))
		run_time /= (CURRENT_BONUS(prev) ? : 1);
 
 
	spin_lock_irq(&rq->lock);
 
 
	if (unlikely(current->flags & PF_DEAD))
		current->state = EXIT_DEAD;
	/*
	 * if entering off of a kernel preemption go straight
	 * to picking the next task.
	 */
	switch_count = &prev->nivcsw;
	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
		switch_count = &prev->nvcsw;
		if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
				unlikely(signal_pending(prev))))
			prev->state = TASK_RUNNING;
		else {
			if (prev->state == TASK_UNINTERRUPTIBLE)
				rq->nr_uninterruptible++;
			deactivate_task(prev, rq);
		}
	}
 
 
	cpu = smp_processor_id();
	if (unlikely(!rq->nr_running)) {
go_idle:
		idle_balance(cpu, rq);
		if (!rq->nr_running) {
			next = rq->idle;
			rq->expired_timestamp = 0;
			wake_sleeping_dependent(cpu, rq);
			/*
			 * wake_sleeping_dependent() might have released
			 * the runqueue, so break out if we got new
			 * tasks meanwhile:
			 */
			if (!rq->nr_running)
				goto switch_tasks;
		}
	} else {
		if (dependent_sleeper(cpu, rq)) {
			next = rq->idle;
			goto switch_tasks;
		}
		/*
		 * dependent_sleeper() releases and reacquires the runqueue
		 * lock, hence go into the idle loop if the rq went
		 * empty meanwhile:
		 */
		if (unlikely(!rq->nr_running))
			goto go_idle;
	}
 
 
	array = rq->active;
	if (unlikely(!array->nr_active)) { //上面说过的,需要重新计算时间片时,就用已经计算好的expired队列了
		/*
		 * Switch the active and expired arrays.
		 */
		schedstat_inc(rq, sched_switch);
		rq->active = rq->expired;
		rq->expired = array;
		array = rq->active;
		rq->expired_timestamp = 0;
		rq->best_expired_prio = MAX_PRIO;
	} else
		schedstat_inc(rq, sched_noswitch);
 
 
	idx = sched_find_first_bit(array->bitmap); //找到优先级最高的队列
	queue = array->queue + idx;
	next = list_entry(queue->next, task_t, run_list);
 
 
	if (!rt_task(next) && next->activated > 0) {
		unsigned long long delta = now - next->timestamp;
 
 
		if (next->activated == 1)
			delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
 
 
		array = next->array;
		dequeue_task(next, array);
		recalc_task_prio(next, next->timestamp + delta);
		enqueue_task(next, array);
	}
	next->activated = 0;
switch_tasks:
	if (next == rq->idle)
		schedstat_inc(rq, sched_goidle);
	prefetch(next);
	clear_tsk_need_resched(prev);
	rcu_qsctr_inc(task_cpu(prev));
 
 
	prev->sleep_avg -= run_time;
	if ((long)prev->sleep_avg <= 0) {
		prev->sleep_avg = 0;
		if (!(HIGH_CREDIT(prev) || LOW_CREDIT(prev)))
			prev->interactive_credit--;
	}
	prev->timestamp = prev->last_ran = now;
 
 
	sched_info_switch(prev, next);
	if (likely(prev != next)) { //表面现在正在执行的进程,不是选出来的优先级最高的进程
		next->timestamp = now;
		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;
 
 
		prepare_arch_switch(rq, next);
		prev = context_switch(rq, prev, next); //所以需要完成进程上下文切换,把之前的进程信息CACHE住
		barrier();
 
 
		finish_task_switch(prev);
	} else
		spin_unlock_irq(&rq->lock);
 
 
	prev = current;
	if (unlikely(reacquire_kernel_lock(prev) < 0))
		goto need_resched_nonpreemptible;
	preempt_enable_no_resched();
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
		goto need_resched;
}
2.16.2 负载均衡

负载均衡的两个实现方式

  • 【方式 1 1 1】当一个处理器上的任务全部结束之后,每 1 m s 1ms 1ms,它会通过系统调用到最忙的处理器上获得任意一个任务执行,调用 l o a d load load_ b a l a n c e balance balance 函数。

  • 【方式 1 1 1 缺点】方式 1 1 1 不适用于运行队列一直不为空的情况。例如, c p u 0 cpu0 cpu0 上一直有 10 10 10 个可运行进程, c p u 1 cpu1 cpu1 上一直有 1 1 1 个可运行进程,显然 c p u 0 cpu0 cpu0 上的进程们得到了不公平的对待,它们拿到 c p u cpu cpu 的时间要小得多,第 1 1 1 种情况下的 l o a d _ b a l a n c e load\_balance load_balance 函数也一直不会调用。所以,实际上,每经过一个时钟节拍,内核会调用 s c h e d u l e r _ t i c k scheduler\_tick scheduler_tick 函数,这个函数会做许多事,例如减少当前正在执行的进程的时间片,在函数结尾处则会调用 r e b a l a n c e _ t i c k rebalance\_tick rebalance_tick 函数,该函数决定以什么样的频率执行负载均衡。

  • 【方式 2 2 2】而在全部处理器都有任务在执行时,则由时钟每 200 m s 200ms 200ms 通过系统调用去检查,若发现在 L i n u x Linux Linux 标准下不均衡,则会发生处理器之间的就绪任务迁移。

  • 【方式 2 2 2 具体实现】当 i d l e idle idle 标志位是 S C H E D _ I D L E SCHED\_IDLE SCHED_IDLE 时,表示当前 C P U CPU CPU 处理器空闲,就会以很高的频率来调用 l o a d _ b a l a n c e load\_balance load_balance 1 1 1 2 2 2 个时钟节拍),反之表示当前 C P U CPU CPU 并不空闲,会以很低的频率调用 l o a d _ b a l a n c e load\_balance load_balance 10 10 10 100 m s 100ms 100ms)。

r e b a l a n c e _ t i c k rebalance\_tick rebalance_tick 函数

static void rebalance_tick(int this_cpu, runqueue_t *this_rq,
			   enum idle_type idle)
{
	unsigned long old_load, this_load;
	unsigned long j = jiffies + CPU_OFFSET(this_cpu);
	struct sched_domain *sd;
 
	/* Update our load */
	old_load = this_rq->cpu_load;
	this_load = this_rq->nr_running * SCHED_LOAD_SCALE;
	/*
	 * Round up the averaging division if load is increasing. This
	 * prevents us from getting stuck on 9 if the load is 10, for
	 * example.
	 */
	if (this_load > old_load)
		old_load++;
	this_rq->cpu_load = (old_load + this_load) / 2;
 
	for_each_domain(this_cpu, sd) {
		unsigned long interval;
 
		if (!(sd->flags & SD_LOAD_BALANCE))
			continue;
 
		interval = sd->balance_interval;
		if (idle != SCHED_IDLE)
			interval *= sd->busy_factor;
 
		/* scale ms to jiffies */
		interval = msecs_to_jiffies(interval);
		if (unlikely(!interval))
			interval = 1;
 
		if (j - sd->last_balance >= interval) {
			if (load_balance(this_cpu, this_rq, sd, idle)) {
				/* We've pulled tasks over so no longer idle */
				idle = NOT_IDLE;
			}
			sd->last_balance += interval;
		}
	}
}
2.16.3 多核调度算法的优化

对任务的分配进行优化

使同一应用程序的任务尽量在一个核上执行,以便达到有共享数据的任务能够尽量在一个核上进行,而共享数据量少或者没有的任务尽量在不同核上进行。这样,可以显著地降低 cache 的缺失率,进而在很大程度上提升了系统的整体性能。

对任务的共享数据优化。

由于CMP体系结构共享二级缓存,可以考虑改变任务在内存中的数据分布,使任务在执行时尽量增加二级缓存的命中率。

对任务的负载均衡优化。

当任务在调度时,出现了负载不均衡,考虑将较忙处理器中与其他任务最不相关的任务迁移,以达到数据的冲突量最小。

2.16.4 绑定进程与CPU

CPU 亲和性( a f f i n i t y affinity affinity

CPU 亲和性,即进程要在某个给定的CPU上尽量长时间地运行而不被迁移到其他处理器的倾向性。 L i n u x Linux Linux 内核进程调度器天生就具有被称为 软CPU亲和性 的特性,这意味着进程通常不会在处理器之间频繁迁移。

硬亲和性

2.6 2.6 2.6 版本 L i n u x Linux Linux 内核还包含了一种机制,它让开发人员可以编程实现 硬CPU亲和性。这意味着应用程序可以显式地指定进程在哪个(或哪些)处理器上运行。

c p u s _ a l l o w e d cpus\_allowed cpus_allowed

L i n u x Linux Linux 内核中,所有的进程都有一个相关的数据结构,称为 t a s k _ s t r u c t task\_struct task_struct。这个结构非常重要,其中与 亲和性( a f f i n i t y affinity affinity)相关度最高的是 c p u s _ a l l o w e d cpus\_allowed cpus_allowed 位掩码。这个位掩码由 n n n 位组成,与系统中的 n n n 个逻辑处理器一一对应。 具有 4 4 4 个物理 C P U CPU CPU 的系统可以有 4 4 4 位。如果这些 C P U CPU CPU 都启用了超线程,那么这个系统就有一个 8 8 8 位的位掩码。

如果为给定的进程设置了给定的位,那么这个进程就可以在相关的 C P U CPU CPU 上运行。因此,如果一个进程可以在任何 C P U CPU CPU 上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是 1 1 1。实际上,这就是 L i n u x Linux Linux 中进程的缺省状态。

L i n u x Linux Linux 内核 A P I API API 提供了一些方法,让用户可以修改位掩码或查看当前的位掩码:

s c h e d _ s e t _ a f f i n i t y ( ) sched\_set\_affinity() sched_set_affinity() (用来修改位掩码)
s c h e d _ g e t _ a f f i n i t y ( ) sched\_get\_affinity() sched_get_affinity() (用来查看当前的位掩码)
注意, c p u _ a f f i n i t y cpu\_affinity cpu_affinity 会被传递给子线程,因此应该适当地调用 s c h e d _ s e t _ a f f i n i t y sched\_set\_affinity sched_set_affinity


资料来源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Gene_INNOCENT

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

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

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

打赏作者

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

抵扣说明:

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

余额充值