《操作系统》课程提纲,王道考研b站视频提纲,更新中,欢迎补充

写在前面:

  1. 该笔记为王道考研b站《操作系统》正版视频提纲,笔记内容为个人归纳👤
  2. 笔记未更新完,欢迎同路人到GitHub上下载完整md文件,更新细节后重新建立提交分支到我的笔记项目下面,我会合并大家的提交,方便更多人学习
    GitHub链接:https://github.com/Ulrich2003/OS-Notes
    笔记名:操作系统.md
  3. ⚠️ 特别提醒:GitHub上的版本永远是最新版本,CSDN版本只能不定时更新,不能保证是最新笔记版本
    笔记内容过多
  4. 笔记内容过多,推荐使用浏览器内置「页内搜索功能」查找相关知识点,Mac系统快捷键为 ⌘ + F ,Windows下应该是ctrl + F
    在这里插入图片描述

操作系统

进程

进程的定义

程序段、数据段,PCB三部分组成了进程实体,所谓的创建进程,实质上是创建进程实体中的PCB

什么是PCB(Process Control Block)?

系统中存放进程的管理和控制信息的数据结构称为进程控制块,是进程实体的一部分,每一个进程均有一个PCB。

注意⚠️:PCB是进程存在的唯一标识

定义:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位

image-20211102145920896

进程的状态

image-20211102151119457

进程状态的转换

image-20211102152220009

进程控制

简单理解为:进程控制为要实现进程状态的转换

进程控制无非要做3类事情:

  • 更新PCB中的信息
  • 将PCB插入合适的队列
  • 分配/回收资源♻️
进程的创建

无 -> 创建态 -> 就绪态

image-20211102160425793

进程的撤销

就绪态/阻塞态/运行态 -> 终止态 -> 🈚️

image-20211102161100365

进程的阻塞和唤醒

image-20211102162758636

进程的切换(下图中事件片写错了,是时间片 ⌚️)

image-20211102165313658

进程通讯

进程是分配系统资源的单位,因此各个进程拥有的内存地址空间相互独立

为了保证安全🔐,一个进程是不能直接访问另一个进程的地址空间

共享存储

image-20211102170236664

管道通讯

image-20211102170401809

  1. 管道只能采用半双工通讯,某一段时间内只能实现单向的传输 🔌 如果要实现双向同时通讯,则需要设置两个管道

  2. 各进程要互斥地访问管道

  3. 数据以字符流的形式写入管道,当管道写满时,写进程write()系统调用将被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程read()系统调用将被阻塞。

  4. 如果没有写满,是不允许读的。如果没有读空,是不允许写的。

  5. 数据一旦被读出,就从管道中抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况

消息传递

进程间的数据交换以**格式化的消息 💿 **为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。

image-20211103204859147

  1. 直接通信方式

消息直接挂到接收进程的消息缓冲队列上

image-20211103205103826

  1. 间接通信方式

消息要先发送到中间实体(信箱📪)中,因此也称为“信箱通信方式”

image-20211103205343521

线程概念和多线程模型

📌 线程是一个基本的CPU执行单元,也是程序执行流的最小单位

📌 传统进程机制中,进程是资源分配、调度的基本单位,引入线程后,进程是资源分配的基本单位,线程是调度的基本单位

引入线程后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发的处理各种任务

📌 引入线程后,进程只作为除CPU之外的系统资源的分配单元(如打印机,内存地址空间等都是分配给进程的)

image-20211103211632449

线程的属性:

image-20211103212724976

进程的实现方式

image-20211103213955627

用户级线程(从用户视角看到的进程)

用户级进程由应用程序通过线程库实现。所有线程管理工作都由应用程序负责

用户级线程中,线程切换可以在用户态下即可完成,无须操作系统干预

在用户看来,是由多个线程,但在操作系统内核看来,并意识不到线程的存在(用户级线程对用户不透明,对操作系统透明)

内核级线程

内核级线程就是「从操作系统内核视角看到能看到的线程」

内核级线程的管理工作由操作系统内核完成。线程调度,切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成

多线程模型
多对一模型

每个用户级线程映射到一个内核级线程。每个用户进程只对应一个内核级线程。

优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高

缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行

image-20211103215836633

一对一模型

一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。

优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强,多线程可在多核处理机上并行执行。

缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。

image-20211103220830844

多对多模型

克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。

image-20211103220505416

处理机调度

调度的三个层次
高级调度(作业调度)

按一定的原则从外存上处于后备队列的作业中挑选一个(或多个)作业,给他们分配内存等必要资源,并建立相应的进程(建立PCB),以使他们获得竞争处理机的权利。

image-20211104123616950

高级调度是辅存(外存)与内存之间的调度。每个作业只调入一次,调出一次。 作业调入时会建立相应的PCB,作业调出时才会撤销PCB。高级调度主要是🈯️调入问题,因为只有调入的时机需要操作系统来确定,但调出的时机必然是作业运行结束才调出。

中级调度(内存调度)

引入虚拟存储技术之后,可将暂时不能运行的进程调至外存等待⌛️。等它重新具备了运行条件且内存又稍有空闲时,再重新调入内存。

这么做的目的是为了提高内存利用率和系统吞吐量。

暂时调到外存等待的进程状态为挂起状态,值得注意的是,PCB并不会一起调到外存,而是会常驻在内存中。PCB中会记录进程数据在外存中存放到位置📝,进程状态等信息,操作系统通过内存中的PCB来保持对各个进程的监控、管理。被挂起的进程会被放到挂起队列中。

image-20211104125125244

中级调度,就是要决定将哪个处于挂起状态的进程重新调入内存。一个进程可能会被多次调出,调入内存,因此中级调度发生的频率要比高级调度更高。

低级调度(进程调度)

主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。

📌 进程调度是操作系统中最基本的一种调度,在一般操作系统中都必须配置进程调度。进程调度频率很高,一般几十毫秒一次。

image-20211104130759663

进程的挂起态和七状态模型

暂时调到外存等待的进程状态称为挂起状态

挂起态又可以进一步细分为就绪挂起,阻塞挂起两种状态

image-20211104125726372

image-20211104130950595

进程调度的时机、切换的过程

进程调度(低级调度),就是按照某种算法重就绪队列中选择一个进程为其分配处理机

image-20211104132435027

进程调度的方式
非剥夺方式

又称为非抢占方式,只允许进程主动放弃处理机,在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。

剥夺调度方式

又称抢占方式。如果有一个更重要,更急迫的进程需要使用处理机,则立刻暂停正在执行的过程,将处理机分配给更重要紧迫的那个进程

“狭义的进程调度”与“进程切换”的区别

狭义的进程调度指的是从就绪 ☑️ 队列中选一个要运行的进程(这个进程可以是刚刚被暂停执行的进程 ⏸️,也可能是另一个进程,后一种情况就需要进程切换)

进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。

广义的进程调度包含了选择一个进程和进程切换两个步骤。

进程切换的过程主要完成了✅:

  1. 对原来运行进程各种数据的保存
  2. 对新的进程的各种数据的恢复

注意⚠️:进程切换是有代价的,如果过于频繁的进行进程调度、切换,会使整个系统的效率降低。使系统大部分时间都花在了进程切换上。

调度算法的评价指标

image-20211109160530450

等待时间

image-20211109150714283

FCFS、SJF、HRRN 调度算法

image-20211109161347453

FCFS(First Come First Serve)

算法思想:公平角度考虑

算法规则:先来先服务

用于作业调度时:考虑的是哪个作业先到达后备队列

用于进程调度时:考虑的是哪个进程先到达就绪队列

是否🉑️抢占 ? F

优点:公平,算法实现简单

缺点:排在长作业后面的短作业需要等待很长的时间⌛️,带权周转时间很大,对短作业来说作业体验非常不好👎。

是否会导致饥饿 ? F

SJF(Shortest Job First)

算法思想:追求最少的平均等待时间,最少的平均周转时间、最少的平均带权周转时间

算法规则:短作业优先

🉑️用于作业调度,也可进程调度。用于进程调度时称为“短进程优先”(Shortest Process First)算法

是否🉑️抢占 ? SJF是非抢占式算法,但是也有抢占式版本 – 最短剩余时间优先算法(SRTN,Shortest Remaining Time Next)

优缺点:

优点:“最短的” (不严谨) 平均等待时间⌛️,平均周转时间

缺点:不公平。对短作业有利,对长作业不利。可能产生饥饿现象。另外,作业/进程的运行时间是由用户提供的,并不一定真实,不一定真正做到短作业优先

长作业/进程如果因为有源源不断的短作业/进程到来,可能会产生“饿死”现象

做题细节
  1. 如果题目没有特别说明,所提到的“短作业/进程优先算法”默认是非抢占式的
HRRN(Highest Response Ratio Next)

算法思想:综合考虑作业/进程的等待时间和要求服务的时间

算法规则:每次调度时先计算🧮各个进程的响应比,选择响应比高的作业/进程为其服务。

image-20211116144811887

🉑️用于作业调度,也可用于进程调度

是否可抢占?非抢占式的算法,因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比

优点:综合考虑了等待时间和运行时间⌚️

等待时间相同时:要求服务时间短的优先(SJF)

要求服务时间相同时,等待时间长的优先(FCFS)

对于长作业来说,等待时间越长,响应比会越来越大,这样有效避免长作业饥饿的问题

是否会导致饥饿:❌

时间片轮转(RR, Round-Robin)

算法思想:公平,轮流为各进程服务;让每个进程在一定时间间隔内都可以得到相应

算法规则:按照各进程到达顺序,轮流让各个进程执行一个时间片。如果在一个时间片内没有执行完,则剥夺处理机,将进程重新放入就绪队列队尾重新排队

用于进程调度(只有作业放入内存建立相应的进程后,才能被分配处理机时间片)

可否抢占:☑️

优点:公平,适合分时操作系统

缺点:高频率进程切换下有一定开销,不区分任务的紧急程度‼️

是否会导致饥饿:❌

优先级调度算法

算法思想:越来越多的应用场景需要根据任务的紧急程度来处理顺序

算法规则:每个作业/进程都有自己的优先级,调度时选择优先级最高的作业/进程

既可以用于作业调度,也可以用于进程调度,甚至,还会用于I/O调度

是否可以抢占? 都有

优点:适用于实时操作系统

缺点:若源源不断🈶️高优先级进程到来,则可能导致饥饿

是否会导致饥饿:☑️

多级反馈队列调度算法

算法思想:对其他调度算法的折中权衡

算法规则:

1.设置多级就绪队列,各级队列优先级从高到低,时间片从小到大

2.新进程到达时先进入第1⃣️级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果已经是在最下级,则重新放回改队列队尾

3.只有第k级队列为空时,才会为k+1级队头的进程分配时间片用于进程调度

可否抢占:☑️

image-20211118194333583

优缺点: 对各类进程相对公平,每个新到达的进程都可以很快就得到响应

是否会导致饥饿:☑️

进程同步,进程互斥

互斥共享方式

系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源

我们把一个时间段内只允许一个进程使用的资源称为临界资源,如摄像头,打印机,此外还有许多变量,数据,内存缓冲区等都属于临界资源

对临界资源的互斥访问,可以在逻辑上分为四部分

image-20211119084604352

进程互斥需要遵循的原则
  1. 空闲让进。临界区为空闲时,允许请求进入临界区进程立即进入临界区
  2. 忙则等待。
  3. 有限等待。对请求访问的进程,必须保证能在有限时间内进入临界区(不饥饿)
  4. 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待
进程互斥的软件实现方法

❌单标志法:

image-20211119085901657

❌双标志先检查法:

image-20211119090649817

❌双标志后检查法:

image-20211119091235933

❎ Peterson算法:

image-20211119092138583

进程互斥的硬件实现方法

中断屏蔽方法

使用开/关中断指令来实现

优点:简单高效

缺点:只适用于单处理机;只适用于操作系统内核进程

TSL (TestAndSet)指令

执行过程中不允许被中断,只能一气呵成

优点:实现简单;适用于多处理机环境

缺点:不满足让权等待原则,可能会出现忙等

Swap指令

执行过程中不允许被中断,只能一气呵成,逻辑上和TSL并没有太大区别

优点:实现简单;适用于多处理机环境

缺点:不满足让权等待原则,可能会出现忙等

同时共享方式

系统中的某些资源,允许一个时间段内由多个进程“同时”对他们进行访问

信号量机制 📶

目的:方便实现进程互斥,进程同步

什么是信号量❓信号量就是一个变量(可以是一个整数,也可以是更复杂的变量),可以用一个信号量来表示系统中某种资源的数量(🌰 系统中有一台打印机🖨️,就可以设置一个初值为1的信号量)

什么是信号量机制 📶❓

用户进程可通过使用操作系统提供的一对原语来对信号量进程操作

什么是一对原语❓

  • wait(S)原语:对系统资源的申请
  • signal(S)原语:对系统资源的释放

image-20211122111001604

什么是原语❓是一种特殊程序段,执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的

整型信号量示意图:

image-20211122110945962

⚠️ 整型信号量存在的问题:不满足“让权等待⌛️“原则,会发生”忙等“

‼️ 记录型信号量 !important

P操作可以理解为🈸️请系统资源

V操作可以理解为释放系统资源

王道相关视频:https://www.bilibili.com/video/BV1YE411D7nH?p=21

image-20211122114346261

  • S.value表示某种资源数量,S.L指向等待该资源的队列
  • P操作中,一定是先S.value–,之后可能需要执行block原语
  • V操作中,一定是先S.value++,之后可能需要执行wakeup原语
  • 可以用记录型信号量实现系统资源的“申请”和“释放”
  • 可以用记录型信号量实现进程互斥,进程同步

注意⚠️:题目中出现P(S) , V(S)的操作,除非特殊声明,否则默认S为记录型信号量

用信号量实现进程互斥,同步,前驱关系

视频链接🔗 https://www.bilibili.com/video/BV1YE411D7nH?p=22

生产者-消费者问题

image-20211122142046926

P22-P32 暂且跳过

计算机内存

内存基础知识

装入模块装入内存

image-20211130120316515

装入的三种方式:

绝对装入:在编译时,如果知道程序将放在内存中的哪个位置,编译程序将产生绝对地址的目标代码装入程序按照装入模块中的地址,将程序和数据装入内存。

绝对装入只适用于单道程序程序环境

静态重定位:又称可重定位装入。编译,链接后🔗的地址都是从0开始的,指令中使用的地址,数据存放的地址都是相对于起始地址而言的逻辑地址。装入时进行“重定位”,将逻辑地址变换为物理地址(地址变换是在装入时一次完成的✅)

⚠️静态重定位的特点是在一个作业装入内存时,必须分配其要求的全部内存空间,如果没有足够的内存空间,就不能装入该作业。作业一旦装入内存后,在运行期间就不能再移动,也不能再申请内存空间。

❗️动态重定位(常用):又称动态运行时装入。编译,链接后的装入模块的地址都是从0开始的,装入程序把装入模块装入内存后,并不会立即把逻辑地址转换为物理地址,而上把地址转换推迟到程序真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址,这种方式需要一个重定位寄存器的支持。

特点✅:可以将程序分配到不连续的存储区中,在程序运行前只需要装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存:便于程序段的分享,可以向用户提供一个比存储空间大得多的地址空间。

采用动态重定位时允许程序在内存中发生移动

链接的三种方式
  1. 静态链接🔗:在程序运行之前,先将各目标模块以及他们所需的库函数连接成一个完整的可执行文件(装入模块)。之后不再拆开。

image-20211130113900757

  1. 装入时动态链接:将各个目标模块装入内存时,边装入边链接的链接方式🔗

image-20211130114237480

  1. 运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享

image-20211130114443701

内存管理的概念

内存保护
  1. 在CPU中设置一对上,下限寄存器,存放进程的上,下限地址。进程的指令要访问某个地址时,CPU检查是否越界。

image-20211130122117448

image-20211130122125954
  1. 采用重定位寄存器(也叫基址寄存器)和界地址寄存器(也叫限长寄存器)进行越界检查。重定位寄存器中存放的是进程起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址

image-20211130122717361

内存覆盖技术

覆盖思想:将程序分为多个段,常用的段常驻内存,不常用的段在需要时调入内存。

内存中分为一个“固定区”和若干个“覆盖区”

需要常驻的内存的段存放在“固定区”中,调入后就不再调出(除非运行结束)

image-20211208184812265

覆盖技术现在基本已经过时了 ❌

内存交换技术

内存空间紧张时,把内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存

image-20211208185309711

具有对换功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。

1⃣️ 文件区主要用于存放文件,追求存储空间的利用率,对文件空间的管理采用离散分配方式 📃

2⃣️ 对换区空间只占磁盘空间的小部分,被换出的进程数据存放于对换区,主要追求换入换出速度,采用连续分配方式

image-20211208191030641

⚠️ 注意 :PCB会常驻内存,不会被换出外存

内存分配管理方式

单一连续分配

在单一连续分配方式中,内存被分为系统区用户区。系统区通常用于内存的低地址部分,用于存放操作系统相关数据。用户区用于存放用户进程相关数据。内存中只能🈶️一道用户程序,用户程序独占整个用户区空间。

优点:实现简单,🈚️无外部碎片 🧩 可以采用覆盖技术扩充内存;不一定需要采取内存保护(MS-DOS)

缺点:只能用于单用户,单任务的操作系统;🈶️内部碎片 🧩;存储器的利用率很低

image-20211208192223137

固定分区分配

将用户空间划分为若干个固定大小的分区 🧷,在每个分区中只装入一道作业

目的:为了解决多道程序系统中装入多道程序,且这些程序之间又不会互相干扰。

  • 分区大小相等分配法:缺乏灵活性,但是🈶️很多适合一台计算机控制多个相同对象的场合

  • 分区大小不等分配法:增加灵活性,可以满足不同大小的进程

image-20211208193436980

操作系统需要建立一个分区说明表,来实现各个分区的分配与回收♻️

优点:无外部碎片

缺点:当用户程序太大,所有分区都不能满足需求,就不得不采用覆盖技术来解决问题,但这又会降低性能; 会产生内部碎片,内存利用率低

动态分区分配

动态分区分配又可称为可变分区分配。这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区。

  • 空闲分区表
  • 空闲分区链

image-20211208202201123

动态分区分配没有内部碎片 🧩,但是有外部碎片

内部碎片:分配给某进程中的内存区域中,如果有些部分没有用上

外部碎片:是指内存中某些空闲分区由于太小而难以利用(🉑️通过紧凑技术来解决外部碎片)

image-20211208203602235

动态分区分配算法
  • 首次适应:从头到尾找适合的分区

  • 最佳适应算法

算法思想:空闲分区按容量递增次序链接,找到大小能满足要求的第一个空闲分区

❌ 缺点:每次都选择最小的分区进行分配,会留下来越来越多,很小,难以利用的内存块。因此会产生很多外部碎片 🧩

  • 最坏适应算法

算法思想:对标上面那种算法,空闲分区按容量递减次序链接,每次分配内存时顺序查找到大小能满足要求的第一个空闲分区(分配到相对比较大分区)

缺点:每次选最大的分区进行分配,这种方法会导致较大连续空闲区被迅速用完,如果之后有“大进程”到达,就没有内存分区可用了。

  • 邻近适应:由首次适应演变而来,每次从上次查找结束位置开始查找
基本分页存储管理的基本概念

将内存空间分为一个个大小相等的分区(比如每个分区4kb),每个分区就是一个「页框」,或称为「页帧、内存块,物理块」,每个页框有一个编号,称为「页框号」或「内存块号、页帧号、物理块号」,页框号从0开始。

将用户进程的地址空间也分为与页框大小相等的一个个区域,称为「页」或「页面」。每个页面也有一个编号,称为「页号」,页号也是从0开始的

image-20211213174634204

相关计算:

image-20211213175858093

image-20211213175936107

什么是页表 ❓

为了知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表

1⃣️ 一个进程对应一张页表

2⃣️ 进程的每一页对应一个页表项

3⃣️ 每个页表项由「页号」和「块号」组成

4⃣️ 页表记录进程页面和实际存放的内存块之间的对应关系

image-20211213181329317

基础地址变换机构

基本地址变换机构 可以借助进程的页表将逻辑地址转换为物理地址

通常会在系统中设置一个「页表寄存器PTR」,存放页表在内存中的起始地址F和页表长度M。

具有快表的地址变换机构

局部性原理

时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很可能再次执行;如果某个数据被访问过,不久后该数据可能再次被访问

空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问

**快表:**又称联想寄存器(TLB),是一种访问速度比内存快很多的高速缓冲存储器📃,用来存放当前访问的若干页表项,以加速地址变换的过程。内存中的页表也经常被称为慢表

两级页表的原理,地址结构

image-20211213191712770

image-20211213200607961

n级页表,访问次数应该是n+1次

基本分段存储管理方式

分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。

image-20211213201426045

段号的位数决定了每个进程最多可以分为几个段

段内地址位数决定了每个段最大长度是多少

image-20211213201745383

image-20211213201911379

image-20211213201934077

什么是段表❓❓

程序分为多个段,各段离散地装入内存,为了保证程序能正常运行,就必须能从物理内存中找到各个逻辑段存放位置。为此,需为每个进程建立一张段映射表,简称为“段表”

image-20211213202257086

分段,分页管理的对比🆚

页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。

段是信息的逻辑单位,分段的主要目的是为了更好的满足用户需要。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名

页的大小📃固定且由系统决定🧷,段的长度却不固定,决定于用户编写的程序。

分页的用户进程地址空间是维的,程序员只需给出一个记忆符即可表示一个地址

分段的用户进程地址空间是二维的,程序员在标识一个地址 时,既要给出段名,也要给出段内地址

image-20211213211516892

分段🆚分页更容易实现信息的共享和保护

不能被修改的代码称为纯代码或可重入代码,这样的代码是可以共享的。🉑️修改的代码是不能被共享的

只需要让各进程的段表项指向同一个段即可实现共享

image-20211213212314107

image-20211213213010263

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值