深入理解操作系统(20)第八章:异常控制流(1)异常+进程(控制转移/控制流/ECF/异常表,号,类型/中断/陷阱/故障/终止/上下文切换/逻辑控制流/并发/多任务/时间片/地址空间/用户和内核模式/

1. 前沿

1.1 控制转移和控制流

从给处理器加电开始,直到你断电为止,程序计数器假设一个序列的值

A0,A1 …… An-1

其中,每个Ak是某个相应的指令Ak的地址。

每次从Ak到Ak+1的过渡称为控制转移(control transfer)。
这样的控制转移序列叫做处理器的控制流(control flow)

最简单的一种控制流是一个“平滑的”序列,其中每个Ak和Ak+1在存储器中都是相邻的。典型地,这种平滑流的突变,也就是与不相邻,是由诸如跳转、调用和返回这样一些熟悉的稃序指令造成的。要想使得程序能够对由程序变量表示的内部的程序状态中的变化做出反应,这些指令是必要的机制。

1.2 ECF 异常控制流(exceptional control flow)

但是系统也必须能够对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕茯的,而且也不一定要和程序的执行相关。

比如:
 一个硬件定时器(或计时器)会定期产生信号,这个事件必须得到处理:
 包到达网络适配器后,必须存放在存储器中:
 程序向磁盘请求数据,然后休眠,直到被通知说数据己就绪;
 当子进程终止时,创造这些子进程的父进程必须得到通知。

1.2.1 ECF定义

现代系统通过使控制流发生突变来对这些情况做出反应。

一般而言,我们把这些突变称为ECF(exceptional control flow,异常控制流)

ECF发生在计算机系统的各个层次。

比如,
 在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序;
 在操作系统层,内核通过上下文转换将控制从一个用户进程转移到另一个用户进程;
 在应用层,一个进程可以发送一个信号到另一个进程
	而接收者会将控制突然转移到它的一个信号处理程序。
	一个程序可以通过回避通常的栈规则,
	并执行到其他函数中任意位置的非本地跳转来对错误做出反应。

1.3 理解ECF很重要

作为程序员,理解ECF对你们来说很重要,这有很多原因:

1. 理解ECF将帮助你理解重要的系统概念

ECF是操作系统用来实现I/O、进程和虚拟存储器的基本机制。在你可以真正理解这些重要概念之前,你必须理解ECF。

2. 理解ECF将都助你理解应用程序是如何与操作系统交互的

应用程序通过使用一个叫做陷阱(trap)或者系统调用(system call)的ECF,向操作系统请求服务。
比如,向磁盘写数据、从网络读取数据、创建一个新进程,以及终止当前进程,都是通过应用程序提交系统调用来实现的,理解基本的系统调用机制将帮助你理解这些服务是如何提供给应用的。

3. 理解ECF将帮助你编写有趣的新应用程序

操作系统为应用程序提供了强大的ECF机制用来创建新进程、等待进程终止、通知其他进程系统中的异常事件,以及检测和响应这些事件。如果你理解这些ECF机制,那么你就能用它们来编写诸如Unix shell和web服务器之类的有趣程序了。

4. 理解ECF将帮助你理解软件异常如何工作

像C++和Java这样的语言通过try、catch以及throw语句来提供软件异常机制。软件异常允许程序进行非本地跳转(也就是,违反通常的调用/返回栈规则的跳转)来响应错误情况。非本地跳转是一种应用层ECF,在c中是通过setjmp和longjmp函数提供的。理解这些低级函数将帮助你理解高级软件异常如何得以实现。

到目前为止,对系统的学习使你己经了解应用是如何与硬件交互的。这一章的重要性在于你将开始学习你的应用是如何与操作系统交互的。有趣的是,这些交互都是围绕着ECF的。我们描述存在于一个计算机系统中所有层次上的各种形式的ECF。

1.4 本章内容

1. 我们从异常开始,异常位于硬件和操作系统交界的部分。
2. 我们还会讨论系统调用,它们是为用程序提供到操作系统的入口点的异常。
3. 然后,我们会提升抽象的层次,描述进程和信号,它们位于应用和操作系统的交界之处。
4. 最后,我们将讨论非本地跳转,这是ECF的一种应用层形式。

2. 异常

2.1 异常定义

异常是一种形式的异常控制流,它一部分是由硬件实现的,一部分是由操作系统实现的。

因为它们有一部分是由硬件实现的,所以貝体细节将随系统的不同而有所不同,然而,对于每个系统而言,基本的思想都是相同的。在这一节中我们的目的是让你对异常和异常处理有一个一般性的了解,并且帮助消除现代计算机系统的一个经常令人感到迷惑的方面。

2.1.1 异常

异常(exception)就是控制流中的突变,用来响应处理器状态中的某些变化。

下图展示了其基本的思想,
图8.1
在这里插入图片描述

在此图中,当处理器状态中一个重要的变化发生时,处理器正在执行某个当前指令Icurr。在处理器中,状态被编码为不同的位和信号。状态变化被称为事件(event),事件可能和当前指令的执行直接相关。比如,发生虚拟存储器缺页、算术溢出或者一条指令试图除以零。另一方面,事件可能和当前指令的执行没有关系。比如,一个系统定时器产生信号或者一个请求完成。

2.1.2 异常表

在任何情况中,当处理器检测到有事件发生时,

它就会通过一张叫做异常表(exception table)的跳转表,进行一个间接过程调用(异常)
跳转到一个专门设计用来处理这类事件的操作系统子程序-异常处理程序(exception handler)

当异常处理程序完成处理后,根据引起异常事件的类型,会发生以下三种情况中的一种:

1. 处理程序将控制返回给当前指令lcurr(当事件发生时正在执行的指令)
2. 处理程序将控制返回给Inext(如果没有发生异常将会执行的下一条指令)
3. 处理程序终止被中断的程序

2.2 异常处理

异常可能会难以理解,因为处理异常需要硬件和软件紧密合作。很容易搞混哪个部分执行哪个任务,让我们更详细地来看看硬件和软件的分工吧。

2.2.1 异常号

系统中可能的每种类型的异常都分配了一个惟一的非负整数的异常号(exception number)。

异常号:
 1.这些号码中的某一些是由处理器的设计者分配的
	如:被零除、缺页、存储器访问违例、断点以及算术溢出
 2.其他号码是由操作系统内核(操作系统常驻存储器的部分〕的设计者分配的
	如:统调用和来自外部I/O设备的信号

在系统启动时,操作系统分配和初始化一张称为异常表的跳转表,使得表目k包含异常k的处理程序的地址。
下图展示了一张异常表的格式。
图8.2
在这里插入图片描述

在运行时(当系统在执行某个程序时),处理器检测到发生了一个事件,并且确定了相应的异常号k。随后,处理器触发异常,方法是去是执行间接过程调用,通过异常表的表目k,转到相应的处理程序。
下图展示了处理器如何使用异常表来形成适当的异常处理程序的地址。

1. 异常号是到异常表中的索引
2. 异常表的起始地址放在一个叫做异常表基寄存器的特殊CPU寄存器里

图8.3
在这里插入图片描述

2.2.2 异常和过程调用的区别;

异常类似于过程调用,但是有一些重要的不同之处:

1. 过程调用时,在跳转到处理程序之前,处理器将返回地址压到栈中。
   然而,根据异常的类型,返回地址要么是当前指令,要么是下一条指令。
   
2. 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
   比如,一个IA32系统将包含当前条件码的EFLAGS寄存器和其他一些东西压入栈中。

3. 如果控制从一个用户程序转移到内核,所有这些项目都被压到内核栈中,
   而不是压到用户栈中。
   
4. 异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。

一旦硬件触发了异常,剩下的工作就是由异常处理程序在软件中完成。在处理程序处理了事件之后,它通过执行一条特殊的“从中断返回”指令,可选地返回到被中断的程序,该指令将适当的状态弹回到处理器的控制和数据寄存器中,将状态恢复为用户模式。如果异常中断的是一个用户程序,然后将控制返回给被中断的程序。

2.3 异常的类型

异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault)和终止(abort)。
图8.4做了一个总结
在这里插入图片描述

2.3.1 中断

中断是异步发生的,是来自处理器外部的I/O设备的信号的结果

1. 硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的
2. 硬件中断的异常处理程序常常被称为中断处理程序(interrupt handler)

下图概述了一个中断的处理。
图8.5
在这里插入图片描述

I/O设备,例如络适配器、磁盘控制器和定时器芯片,通过向处理器芯片上的一个管脚发信号,并将异常号放到系统总线上,来触发中断,这个异常号标识了引起中断的设备。
在当前指令完成执行之前,处理器注意到中断管脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。当处理程序返回时,它就将控制返回给下一条指令(也就是,如果没有发生中断,在控制流中会在当前指令之后的那条指令)。结果是程序继续执行,就好像没有发生过中断一样。

剩下的异常类型(陷阱、故障和终止)是同步发生的,是执行当前指令的结果
我们把这类指令叫做故障指令(faulting instruction)

2.3.2 陷阱(系统调用)

陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。

陷阱最重要的用途是系统调用!!!
在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用

用户程序经常需要向内核请求服务,比如读一个文件(read)、创建一个新的进程(fork)s加载一个新的程序(execve或者终止当前进程(exit)。为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的“syscall n”指令,当用户程序想要请求服务n时,可以执行这条指冷。执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。
下图概述了一个系统调用的处理。
图8.6
在这里插入图片描述

从一个程序员的角度来看,系统调用和普通的函数调用是一样的。然而,它们的实现是非常不同的。普通的函数运行在用户模式(usermode)中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式(kernelmode)中,内核模式允许系统用执行指令,并访问定义在内核中的栈。

2.3.3 故障(缺页异常/页错误)

故障由错误情况引起,它可能被故障处理程序修正。
当一个故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到故障指令,从而重新执行它。否则处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
下图述了一个故障的处理。
图8.7
在这里插入图片描述

故障的一个经典示例是缺页异常

当指令引用一个虚拟地址,而与该地址相对应的物页面不在存储器中,因此必须从磁盘中取出时,就会发生这种故障。就像我们将在第10章中看到的那样,一个页面就是虚拟存储器的一个连续的块(典型的是4KB)。缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在存储器中了,指令就可以没有故障地运行完成了

缺页参考:
页错误 Page Fault /缺页异常 详解
https://blog.csdn.net/lqy971966/article/details/106910442

2.3.4 终止

终止是不可恢复的致命错误造成的结果。

典型的是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。

终止处理程序从不将控制返回给应用程序。
如图8.8所示,处理程序将控制返回一个abort例程,该例程会终止这个应用程序。
图8.8
在这里插入图片描述

2.4 Intel 处理器中的异常

为了使描述更具体,让我们来看看Intel系统定义的一些异常。

一个pentium处理器可以有高达256种不同的异常类型。

范围0~31的号码对应的是Pentium体系结构定义的异常,因此对任何Pentium类的系统都是一样的。
范围32~255的号码对应的是操作系统定义的中断和陷阱。
图8.9展示了些示例。
图8.9
在这里插入图片描述

当应用试图除以零时,或者当一个除法指令的结果对于目标操作数来说太大了时,就会发生除法错误(异常0)。Unix不会试图从除法错误中恢复,而是选择终止程序。
Unix shell典型地会把除法错误报告为“浮点异常(Floating exception)”。

许多原因都会导致不为人知的一般保护故障(异常13)。通常是因为一个程序引用了一个未定义的虚拟存储器区域,或者因为程序试图写一个只读的文本段。Unix不会尝试恢复这类故障。Unix shell典型地将这种一般保护故障报告为“段故障(segmentation fault)。
缺页(异常14)是会重新执行故障指令的异常的一个示例。
机器检查(异常18)是在故障指令执行中检测到致命的硬件错误时发生的,机器检查处理程序从不返回控制给应用程序。
在IA32系统上.系统调用是通过一条称为INT n的陷阱指令来提供的,其中n用能是异常表中256个表目中任何一个的索引。在历史上,系统调用是通过异常128(0x80)提供的。

3. 进程

3.1 进程基本概念

异常提供基本的构造块,它允许操作系统提供进程(process)的概念,进程是计算机科学中最深刻最成功的概念之一。
当我们在一个现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的惟一程序。我们的程序好像是独占地使用处理器和存储器。处理器就好像是无间断的一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据显得好像是系统存储器中唯一的对象。这些假象都是通过进程的概念提供给我们的。

3.1.1 进程的经典定义

进程的经典定义就是一个执行中程序的实例

3.1.2 进程的上下文

系统中的每个程序都是运行在某个进程的上下文(context)中的。
上下文是由程序正确运行所需的状态组成的。

上下文包括:程序的代码和数据、它的栈、它的通用目的寄存器的内容
		   以及它的程序计数器、环境变量以及打开文件描述符的集合。

3.1.3 进程的两个关键抽象

每次用户通过向shell输人一个可执行目标文件的名字,运行一个程序时,shell会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序还能够创建新进程,且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
关于操作系统如何实现进程的细节的讨论超出了我们的范围。取而代之,我们将关注进程提供给应用程序的关键抽象:

1. 一个独立的逻辑控制流,它提供一个假象,使我们觉得我们的程序独占地使用处理器。
2. 一个私有的地址空间,它提供一个假象,使我们觉得我们的程序独占地使用存储器系统。

让我们更深人地看看这些抽象。

3.2 逻辑控制流

一个独立的逻辑控制流,它提供一个假象,使我们觉得我们的程序独占地使用处理器。

3.2.1 逻辑控制流定义

典型地,即使在系统中有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。

如果我们想用调试器单步执行我们的程序,我们会看到一系列的PC(程序计数器)的值,这些值惟一地对应于包含在我们程序的可执行目标文件中的指令或是包含在运行时动态链接到我们程序的共享对象中的指令。

这个值的序列叫做逻辑控制流。

3.2.2 例子说明-解释一个假象:程序在独占地使用处理器

考虑一个运行着三个进程的系统,如图8.10所示。处理器的一个物理控制流被分成了三个逻辑流,每个进程一个。每一个竖直方向上的列表示一个进程的逻辑流的一部分。在这个例子中,进程A运行了一会儿,然后是B开始运行到完成。然后,C运行了一会儿,A接着运行直到完成。最后,c可以运行到结束了。
图8.10
在这里插入图片描述

图8.10的关键点在于进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(preempted)(暂时挂起),与此同时其他进程开始执行。对于一个运行在这些进程之一的上下文中的程序,它看上去就像是在独占地使用处理器,惟一的反面例证是如果我们精确地测量每条指令使用的时间(参见第9章),我们将发现在我们程序中一些指令的执行之间,CPU好像会周期性地停顿(stall)。然而,每次处理器停顿,它随后继续执行我们的程序,并不改变程序存储器位置或寄存器的内容。

一般而言,和不同进程相关的逻辑流并不影响任何其他进程的状态,从这个意义上说,每个逻辑流都是与其他逻辑流相独立的。当进程使用进程间通信(IPC)机制,比如管道、套接字、共享存储器和信号量,显式地与其他进程交互时,这条规则的惟一例外就会发生。

3.2.3 并发进程,并发运行,多任务,时间片

任何逻辑流在时间上和另外的逻辑流重叠的进程被称为并发进程(concurrent process),
而这两个进程就被称为并发运行。

如图8.10中,进程A和B就是并发运行的,A和c也是。另一方面,B和c并不是并发运行的,因为B的最后一条指令是在C的第一条指令之前执行的。
进程和其他进程轮换运行的概念称为多任务(multi tasking)。一个进程执行它的控制流的一部分的每一时间段叫做时间片(time slice)。因此,多任务也叫做时间分片(time slicing)。

3.2.4 默认时间片(10ms)

默认的时间片一般是10ms;
因为设大了,系统响应变慢(调度周期长);
设小了,会明显增大进程频繁切换带来的处理器消耗;

备注:

进程不一定要一次用完时间片,可分多次使用,尽可能长时间保证可运行

3.3 私有地址空间

一个私有的地址空间,它提供一个假象,使我们觉得我们的程序独占地使用存储器系统。

进程也为每个程序提供一种假象,就好像它正在独占地使用系统地址空间。在一台n位地址的机器上,地址空间是2的n次方个可能地址的集合,0,1,2的n次方-1。一个进程为每个程序提供它自己的私有地址空间。一般而言这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写的,从这个意义上说,这个地址空间是私有的。
尽管和每个私有地址空间相关联的存储器的内容一般是不同的,但是每个这样的空间都有相同的结构。
比如,图8.11展示了一个Linux进程的地址空间的结构。
图8.11

在这里插入图片描述
详细的内核部分结构:

在这里插入图片描述

地址空间底部的四分之三是预留给用户程序的,包括通常的文本、数据、堆和栈段。
地址空间顶部的四分之一是预留给内核的。地址空间的这个部分包含内核在代表进程执行指令时使用的代码、数据和栈。
(比如,当应用程序执行一个系统调用时)

虚拟地址空间参考:
https://blog.csdn.net/lqy971966/article/details/119378416

3.4 用户模式和内核模式

为了使操作系统内核提供一个无懈可击的进程抽象。处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。

3.4.1 通过控制寄存器中的一个方式位来切换用户/内核模式

典型地,处理器是用某个控制寄存器中的一个方式位(modebit)来提供这种功能的,该寄存器描述了进程当前享有的权力。

1. 当方式位设置时,进程就运行在内核模式中(有时叫做超级用户模式)。

一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器
位置。

2. 当方式位没有设置时,进程就运行在用户模式中。

用户模式中的进程不允许执行特权指令(previleged instruction),
比如停止处理器、改变方式位的值或者发起一个I/O操作,也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。用户程序必须通过系统调用接间地访问内核代码和数据。

3.4.2 用户模式变为内核模式的惟一方法(中断、故障或者陷入系统调用)

1. 一个运行应用程序代码的进程初始时是在用户模式中的
2. 进程从用户模式变为内核模式的惟一方法是通过诸如中断、故障或者陷入系统调用这样的异常。

当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,
当它返回到应用代码时,处理器就把模式从内核模式改回到用户模式。

3.4.3 /proc文件系统(可访问内核数据结构)

Linux和Solaris提供了一种聪明的机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构的内容。

此文件系统将许多内核数据结构的内容输出为一种用户程序可以读的ASCII文件的层次结构。

比如,你可以使用Linux文件系统找出一般的系统属性,比如CPU类型(/proc/cpuinfo),或者某个进程使用的存储器段(/proc//maps)。

3.5 上下文切换

操作系统内核利用一种称为上下文切换(context switch〕的较高级形式的异常控制流来实现多任务。
上下文切换机制是建立在我们在8.1节中己经讨论过的那些较低层异常机制之上的。
内核为每个进程维持一个上下文(context)。

参考:
Linux:上下文,进程上下文和中断上下文概念,上下文切换
https://blog.csdn.net/lqy971966/article/details/119103989

3.5.1 上下文内容

上下文就是内核重新启动一个被抢占进程所需的状态。
它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描绘地址空间的页表(pagetable)、包含有关当前进程信息的进程表以及包含进程己打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。
当内核选择一个新的进程运行时,我们就说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,

3.5.2 上下文切换的工作:

上下文切换可以:

1. 保存当前进程的上下文
2. 恢复某个先前被抢占进程所保存的上下文
3. 将控制传递给这个新恢复的进程。

3.5.3 上下文切换的例子说明

当内核代表用户执行系统调用时,可以发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。
比如,如果一个read系统调用请求一个磁盘访问,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。
另一个示例是sleep系统调用,它显式地请求让调用进程休眠。一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。

3.5.4 中断也可能引发上下文切换

比如,所有的系统都有其种产生周期性定时器中断的机制,典型的为每1毫秒或侮10毫秒。每次发生定时器中断时,内核就能判定当前进程己经运行了足够长的时间了,并切换到一个新的进程。

下图展示了一对进程A和B之间上下文切换的示例。
在这个例子中,初始地,进程A运行在用户模式中,直到它通过执行系统调用陷入到内核。内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并在磁盘控制器完成从磁盘到存储器的数据传输后,要求磁盘中断处理器。

图8.12
在这里插入图片描述

磁盘取数据要用一段相对较长的时间(数量级为十几毫秒),所以内核执行从进程A到进程B的上下文切换,而不是在这个间歇时间内等待,什么都不做。注意在切换之前,内核正代表进程A在用户模式下执行指令。在切换的第一步中,内核代表进程A在内核式下执行指令。然后在某一
时刻,它开始代表进程B(仍然是内核模式下)执行指令。在切换完成之后,内核代表进程B在用户模式下执行指令。
随后,进程B在用户模式下运行一会儿,直到磁盘发出一个中断信号,表示数据己经从磁盘传送到了存储器。内核判定进程B已经运行了足够长的时间了,就执行一个从进程B到进程A的上下文切换,将控制返回给进程A中紧随在read系统调用之后的那条指令。进程A继续运行,直到下一次异常发生,依此类推。

3.5.5 高速缓存污染(pollution)和异常控制流

一般而言,硬件高速缓存存储器不能和诸如中断和上下文切换这样的异常控制流很好地交互。

如果当前进程被一个“中断"暂时中断,那么对于中断处理程序来说高速缓存是冷的。

高速缓存是冷的,意思就是:程序所需的数据不在高速缓存中

如果处理程序从主存中访问了足够多的表目,那么当被中断的进程继续时,高速缓存对它来说也是冷的。
在这种情况中,我们就说(中断)处理程序污染了高速缓存。使用上下文切换也会发生类似的现象。
当一个进程在上下文切换后续执行时,高速缓存对于应用程序而言也是冷的,必须再次热身。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值