从操作系统到二进制安全 第二章:进程与线程 (上)

目录

一. 进程

1. 进程模型

2. 进程的创建

3. 进程的终止

4. 进程的层次结构

5. 进程的状态(非常重要)

6. 进程的实现

7.多道程序设计

二. 线程

1. 线程的使用

2. 经典线程模型

3. 线程的优势

4. POSIX线程

5.两种线程实现方式(用户态&内核态)

1.在用户态实现线程

2.在内核中实现线程

3.混合实现

6.调度程序激活机制

7.弹出式线程


本文章操作系统部分很多内容参照 工业机械出版社《现代操作系统》一书

一. 进程

1. 进程模型

       一个进程,就是一个正在执行的程序的实例,包括程序计数器,寄存器和变量的当前值。

进程,从中文上可以看作行的序的简称(很不准确,但是可以这么理解),表示程序的一种运行的状态,和程序之间有区别。

      多道程序设计:每个进程都有自己的虚拟CPU,但是实际上真正CPU在某个时间段内只能运行一个进程,并且CPU在不同的进程之前在极短的时间内不停地切换,这种快速切换被称为多道程序设计。

      (一个CPU在一个时间段内只能个运行一个进程)

        进程模型基于两种独立的概念:资源分组处理与执行

举个例子:(例子来源于《现代操作系统》一书)

有一位手艺好的厨师(CPU)正为他的女儿做生日蛋糕,他有蛋糕的食谱(也就是程序),厨房里有所需的原料(各种输入的数据),突然他的女儿的头被蜜蜂蛰了,计算机科学家就记录下他照着食谱做到哪里了(保存当前进程状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。当他处理好蛰伤后,这位厨师又回来做蛋糕,从他离开的那一步开始做起。这里处理器从一个进程(做蛋糕)切换到另一个进程(治疗女儿),每个进程拥有自己的程序(食谱和急救手册)。

 这里有个关键的思想:一个进程时某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被多个若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而向另一个进程提供服务。

注:如果一个程序运行了两边,算是两个进程

 由于CPU在各进程之间来回快速切换,所以每个进程执行其运算的速度是不确定的,而且当同一进程再次运行时,其运算速度通常也是不可再现的。

2. 进程的创建

首先,我们研究进程为什么被创建,导致进程创建的事件是什么。

4种主要事件会导致进程的创建:

1. 系统初始化

2. 正在运行的程序执行了创建进程的系统调用

3.用户请求创建一个进程

4.一个批处理作业的初始化

我们来一个个看这些事件。

      系统的初始化,启动操作系统的时候,通常会创建若干个进程。其中有些是前台进程,也就是同用户交互并替他们完成工作的那些进程(没有界面和与用户交互的地方,你咋用0.o)。其他的是后台进程,这些进程与用户没有关系,相反,却具有某些专门的功能(举个例子,设计一个后台进程来接收发来的电子邮件,这个进程在一天的大部分事件都在睡眠,但是当电子邮件到达时突然被唤醒了。这个进程的特殊功能就是接收发来的电子邮件,如果没有发来的邮件,那么这个进程会处于睡眠的状态,这种停留在后台处理诸如电子邮件,Web页面、新闻等活动的进程,称为守护进程,守护进程就是典型的开机启动的后台进程)。

       正在运行的程序执行了创建进程的系统调用,除了在启动阶段创建的进程之外,新的进程也可以在次之后创建(比如一个正在运行的进程经常会发出系统调用以便创建一个或多个新进程协助其工作)

        用户请求创建一个进程,顾名思义,就是用户主动地发出一个请求,比如启动一个程序或者输入一个命令,这会导致创建一个进程(比如再交互式系统中,键入一个命令或者双击一个图标启动程序,会导致开始一个新的进程,并运行所选择的程序)

        一个批处理作业的初始化,这种创建进程的情形只在大型机的批处理系统中应用。用户在这种系统中交批处理作业。在操作系统任务有资源可以运行另一个作业时,它创建一个新的进程,并运行输入队列中的下一个作业。

从技术上看,以上所有情形中,新进程都是由于一个已存在的进程执行了一个用于创建进程的系统调用而创建的

在UNIX系统中,只有一个系统调用可以创建新进程:fork。这个系统调用会创建一个与调用进程相同的副本。fork后,这两个进程(父进程和子进程)拥有相同的内存映像、相同的环境字符串和同样打开的文件。接着子进程执行execve或者一个类似的系统调用,以修改其内存映像并运行一个新的程序。

在Windows中正好相反,一个Win32函数调用CreatProcess既处理进程的创建,也负责把正确的程序装入新的进程。

但无论在Windows还是UNIX中,进程创建后,父进程和子进程有各自不同的地址空间。如果其中某个进程在其地址空间中修改了一个字,这个修改对于其他的进程,包括它的父进程都是不可见的(这个就和细胞的分裂一样,分裂后,一个细胞是看不见另一个细胞的)。

    在UNIX中,子进程的初始地址空间是父进程的一个副本,但这里涉及两个不同的地址空间,不可写的内存区是共享的。某些UNIX的实现使程序正文在两者间共享,因为他不能被修改。还有一种共享方法,叫做写时复制,用于子进程共享父进程的所有内存,就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝。

     再次强调!!!可写的内存,是不可以共享的。(虽然也不是没办法,我们后面会讨论进程共享内存的)

      而在Windows中,一开始父进程的地址空间和子进程的地址空间就是不同的。

3. 进程的终止

哲学:任何东西都不是永恒存在的

包括进程也是,进程有创建,它开始运行,完成工作,但是迟早一个进程是会终止的,进程的终止有以下四种情况:

两种自愿的情况,也就是由于进程本身退出

1. 正常退出

2. 出错退出

两种非自愿退出,也就是由于外界因素退出

3. 严重错误

4. 被其他进程杀死

1.正常退出多数进程是由于完成了它们的工作而终止,当编译器完成了所给定程序的编译后,编译器执行一条系统调用,通知操作系统该进程工作已完成,然后操作系统将进程终止(在Windows中相关的调用是ExitProcess,在UNIX中是exit)。这种终止方式是由于进程本身已经完成所以退出,所以是主动退出。

2. 出错退出:是由于进程发生严重错误,例如如果用户要编译程序text.c,但是该文件并不存在,于是编译器就会退出,并且给出错误参数时,与屏幕交互式进程通常并不会退出。相反,这些程序会弹出一个对话框,并要求用户再试一次。由于是程序本身发生了错误,所以也属于主动退出。

3.严重错误:通俗来说,就是进程引起严重的错误,比如执行了一条非法指令,引用不存在内存,或整数除0,这些引起的错误不是程序本身的错误,而且由程序引起的外部错误。在一些操作系统中(比如UNIX),进程可以通知操作系统,它希望自行处理某些错误。在这类错误中,进程收到信号(被中断),而不是在运行时程序自己发生错误,所以时非自愿的。

4.被其他进程杀死:一个进程通知操作系统杀死另一个进程,在UNIX中这个系统调用是kill,在win32中是TerminateProcess,这两种中,进程一定要先获得确定的授权,才能进行杀死操作。在某些操作系统中,如果一个进程终止时,不论是自愿的还是其他原因,由该进程所创建的所以进程也一律被立即被杀死,不过UNIX和Windows都不是这种工作方式。

4. 进程的层次结构

在某些系统中,父进程创建子进程后,父进程和子进程会以某种形式继续保持关联。子进程又会创建属于自己的子进程,则这些进程会组成一个层次结构。注意:一个进程只能有一个父进程,但是可以有0个,1个或多个子进程。

在UNIX中,进程和它的所有后裔组成进程组,用户从键盘发出一个信号后,该信号被送给当前与键盘相关的进程组中的所有成员(它们通常是当前窗口创建的所有活动进程)。每个进程可以分别捕获该信号、忽略该信号或采取默认动作,即该信号被杀死。

在Windows中没有进程层次的概念

5. 进程的状态(非常重要)

进程除了创建和终止,有以下三种状态:

1.运行态(该时刻进程实际占用CPU)。

2.就绪态(可运行,但因为其他进程正在运行而暂时停止)

3. 阻塞态(除非某种外部事件发生,否则程序不能运行)

下面我们来通过举例子的方式讨论一下这几种状态

1. 运行态:这个简单,就是一个进程正在运行,此时这个进程占用着CPU资源,而其他进程在这段时间内不能使用CPU资源

2. 就绪态:这个进程已经可以运行,但由于CPU正在被某个进程占用,所以只能处于等待的状态,等待占用CPU的进程把CPU让出来,然后这个进程在进入CPU,成为运行态。

3. 阻塞态:当操作系统发现这个进程不能运行下去的时候,会将这个进程设置为阻塞态,直到满足运行条件后,才会将其设为就绪。

不同状态之间的转换

进程的三种状态,有四种转换的方式。

注:本图取自博客:https://blog.csdn.net/Jacky_Feng/article/details/108289943

执行转化为就绪:最常见的情况就是时间片用完,也就是进程占用CPU的时间过长,导致发生时钟中断切换进程,这是操作系统保证CPU不被一个进程一直占用的方法,在一个进程运行一段时间后,会把CPU资源让给其他进程,而转换的方式,就是通过时钟中断。

执行转化为阻塞:最常见的情况就是进程发生IO申请,比如希望从管道或设备文件(如终端)读取数据时,进程可以执行一个诸如pause的系统调用来进入阻塞状态,在等待IO的同时把CPU资源让给其他进程。

阻塞转化为就绪:当一个进程阻塞后(比如由于等待IO阻塞),当某外部事件发生,比如IO完成后,程序会转为就绪态,然后等待运行。(前提是一定要有外部事件发生)

就绪转化为进行:一个进程处于就绪态时,他只需要做的事情就是等待进程调度,然后使其编程执行态。

6. 进程的实现

首先讨论两个方面:

1. 进程切换后,原先进程的状态以及一些数据保存在哪里。

2. 进程间如何实现相互切换的。

首先,我们讨论进程是如何保存状态和一些数据的

为了实现进程模型,操作系统维护了一张表(一种数据结构),称为进程表。每个进程占用一个进程表项(进程控制块),该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配情况、所打开的文件的状态、账号和调度信息,以及其他进程由运行态转换到用户态或者阻塞他必须保存的信息,从而保证该进程随后能再次启动。

以上就是经典系统中的关键字段

所有的中断,都是从保存寄存器开始,首先保存寄存器到进程表项中,然后会从堆栈中删除由中断硬件机制存入堆栈的那部分信息,并将堆栈指针指向一个由进程处理程序所使用的临时堆栈。

注:无论中断时因何而起,首先干的事情就是保存寄存器。并且,一些诸如保存寄存器值和设置堆栈指针等操作,无法用C语言这类的高级语言描述,所以这些操作通过一个短小的汇编语言例程来完成,通常该例程可以供所有的中断使用。

接着,我们来了解进程间如何实现切换的

前面提到过,进程的切换最常用的一种方式就是中断,比如我们的时钟中断一直在我们电脑中发生。而中断处理过程中有个不可或缺的部分:中断向量

中断向量:是指计算机系统中用于管理和处理硬件中断的一个数据结构。它通常是一个表(称为中断向量表,Interrupt Vector Table,IVT),其中每个条目包含一个指向特定中断处理程序(即中断服务例程,ISR)的指针。当一个中断发生时,处理器使用中断向量来找到并执行相应的中断处理程序。

简而言之,就是一张记录了中断处理程序的位置的表,每一个表项都指向了一个中断处理程序。它包含中断服务程序的入口地址

一个进程在执行过程中可能被中断数千次,但关键是每次中断后,被中断的进程都返回到与中断发生前完全相同的状态。

7.多道程序设计

采用多道程序的目的,就是提升CPU的利用率。

由于一个进程用于计算的时间占CPU的多少我们无法准确衡量,并且不同的CPU情况不同。所以我们从概率的角度去探讨这点。

假设一个进程等待IO操作的时间与其停留在内存中的时间的比是p。当内存中同时有n个进程时,则所有进程都在等待IO的概率为p^{n}(注:由于一旦有一个进程在计算,则CPU资源在其计算时间内不会造成浪费)。

所以我们可以得到CPU的利用率为:

                                          CPU利用率=   1-p^{n}

下图以n为变量的函数表示了CPU的利用率,n称为多道程序设计的道数。

这个图虽然很简单,很粗略,但是它对CPU性能的预测仍然有效。比如,假设计算机有8GB内存,操作系统及相关表格占2GB,每个用户程序也占2GB。这些内存空间允许3个用户程序同时留在内存中,若80%的时间用于IO等待,,则CPU的利用率为49%,若增加8GB字节的内存后,可以从3道程序提高到7道程设计,因而CPU利用率提高到79%,换而言之,第一次增加8GB提高了30%的利用率。但是当第二次增加8GB内存后,只能提高到91%,吞吐量的提高仅为12%。通过这一模型,计算机用户可以确定,第一次增加内存是划算的,第二次不是。

二. 线程

一个进程里面,有一个地址空间和一个控制线程。

1. 线程的使用

(线程被称为迷你进程,是计算机中最小的单位)

问:为什么人们需要线程?
答:在许多应用中同时发生着多种活动,其中某些活动随着时间的推移会被阻塞。通过将这些程序分解成可以准并行的多个顺序线程,程序设计会变得更简单。

线程的优势:

1. 一个进程里的不同线程共享同一个地址空间和所有可用数据的能力

2. 线程比进程更轻量级,所以它们比进程更容易(即更快)创建和撤销(据估计,创建一个线程要比创建一个进程快10-100倍)。有大量线程需要动态和快速修改时,这一特性是很有用的。

3. 最后,在多CPU系统中,多线程是有益的,这样的系统中,真正的并行有了实现的可能。

下面举个例子说明线程的好处,比如我们在编辑一个文档,如果没有线程的话,当我们对文档某个地方进行修改的时候,进程需要做以下工作:1.接受用户输入的信息,2.找到修改的部分,3.把修改的部分改正,4.重新格式化文档,这样会使得用户等待比较长的时间。

单线程的应用在磁盘备份或格式化文档的时候,来自键盘和鼠标的命令就会被忽略。需要用户等待,效率低

但是引入多线程后,可以解决此类问题,假设字处理软件被编写成包含有两个线程的程序,一个与用户交互,一个在后台重新进行格式处理。一旦一个地方被修改,交互线程会立刻通知格式化处理线程对整本书进行修改,而交互线程继续等待用户的输入。

在这个例子里,两个线程属于分工合作的关系。

在此基础上,还可以再加一个线程,这个线程用于实时向磁盘中备份文档。

在《现代操作系统》一书中,,给出了一个例子,也就是万维网服务器

对页面的请求发送给服务器,而所请求的页面返回给客户机。

高速缓存:在大多数web服务器中,用户对某些页面的访问频率远远大于别的页面,很明显的一个例子就是官网主页。web服务器可以把获得大量访问的页面集合保存在内存中,这样就可以避免去磁盘调用这些页面。

再web服务器中,多线程非常重要。

有以下几个重要线程:

1. 分派程序:从网络中读入工作请求

2. 工作线程:再检查请求后,提交该请求(检查请求这一步操作也可以分一个进程)

工作线程被唤醒后,它检查有关的请求是否在Web页面高速缓存之中,这个告诉缓存是所有页面都可以访问的。如果没有,该线程开始一个从磁盘调入页面的read操作,并且阻塞直到该磁盘操作完成。而分派线程这个时候可以继续从网络读入请求。

现在考虑没有多线程的情形下,web服务器会出现的问题

web服务器的主循环获得请求,检查请求,并且在取下一个请求之前完成整个工作,等待磁盘操作的时候,CPU就会空转,而是我们很忌讳的一件事情。

有一种设计方法是:服务器在表格中记录当前请求状态,然后去处理下一个事件。下一个事件可能是一个新的工作请求,或者是磁盘对先前操作的回答,

这个方法中,“顺序进程”模型消失了,进程并不是按照顺序走下来的,而是在不同的工作请求中跳来跳去,每次服务器从为某个请求工作的状态切换到另一个状态时,都必须显式地保存或重新装入相应的计算机状态。

有限状态机:每个计算都有一个被保存的状态,存在一个会发生且使得相关状态发生改变的事件集合,称为有限状态机

多线程的作用,书上给出的第三个例子,是处理极大数据量的应用,通常是:
一个输入线程

一个处理线程

一个输出线程

输入线程把数据读入缓冲区,处理线程从缓冲区中取出数据,处理数据,并把结果放在输出缓冲区中,输出线程把这些结果写到磁盘上

2. 经典线程模型

进程模型基于两种独立的概念:资源分组处理与执行

而线程的概念引入,可以使得这两种概念分开。

理解进程的一个角度,就是把相关资源集中在一起,进程有存放程序正文和数据以及其他资源的地址空间。这些资源包括打开的文件、子进程和即将发生的定时器、信号处理程序、账号信息等。。。

进程拥有一个执行线程,线程中有程序计数器,用来记录接下来要执行的指令,线程拥有寄存器(记录当前变量)和堆栈(记录执行历史)。

进程用于把资源集中到一起,而线程则是在CPU上被调度执行的实体。

   由于线程有进程的一些性质,所以线程有时被称为轻量级进程。

   在一个进程中的多个线程是共享同一个地址空间和其他资源,以一种协作的方式存在。而进程之间有独自的内存空间和资源。一个线程可读可写另一个线程的地址空间。

   当然,每个线程都有自己的程序计数器,寄存器,堆栈,状态。这些可能是共享的,也可能不是,也可能是部分共享。每个线程会调用不同的过程,从而有不同的执行历史,故线程有独立的堆栈是重要的。

   通常,一个进程会从单线程开始,这个线程有能力创建新的线程,线程的创建与终止类似于进程的创建终止。我们之后会讨论到如何创建线程。

   但是线程不同与进程的是,它不能利用时钟中断来主动让出CPU ,以便让其它线程有机会运行,这里有个线程调用:thread_yield,它允许线程自动放弃CPU,从而让另一个线程运行。

   不同线程若要并行,也要多核处理器,不然只能并发。

还有一个问题是,如果一个线程关闭了某个文件,但是另一个线程还在这个文件上进行读写操作,这个该怎么办?

3. 线程的优势

这里讨论一下线程相比于进程的优势,线程的出现就是为了提高效率,在一个进程中实现多个操作

1. 更轻量:比进程更容易创建和撤销,当大量线程要修改时,这个优势很明显体现出来了

2. 可以共享同一个进程的资源:共享是线程的一个巨大的优势,这使得线程之间的通讯变得非常容易

3. 在一个进程中实现多个操作:可以对同一个文件进行多个操作,CPU只需要在线程间切换,大大节省了时间

4. POSIX线程

这里重要的其实就是一个pthread线程包,是IEEE在IEEE标准1003.1c中定义的线程标准。这个标准定义了超过60个函数调用,这里举几个重要的

1.pthread_create  创建一个新线程

2.pthread_exit       结束调用线程

3.pthread_join       等待一个特定的线程退出

4.pthread_yield     释放CPU来运行另一个线程

5.pthread_attr_init 创建并初始化一个线程的属性结构

6.pthread_attr_destory 删除一个线程的属性结构

剩下还有些东西请读者自己依靠编程研究

5.两种线程实现方式(用户态&内核态)

1.在用户态实现线程

特点:整个线程包放在用户空间中,内核对线程包一无所知,在每个进程中会有线程表,用于跟着该进程中的线程。线程表保存了线程的寄存器、状态和其他信息

下面讨论优缺点

优点:

1.可以在不支持线程的操作系统上实现多线程

2.调度快,效率高,不用陷入内核,不用上下文切换,不用对内存缓存刷新

3.允许每个进程定制调度算法

缺点:

1.很难在不影响别的线程的情况下实现阻塞系统调用,只能将阻塞系统调用改为非阻塞系统调用,这样成本很大。

2.当一个线程故障时,会阻塞整个进程

3.当一个线程运行时,其他线程就不能运行

对于第一条缺点,比如在还没有任何键盘输入的时候,一个线程读取键盘,让该线程世界进行该系统调用时不可接受的,因为会停止所有线程(因为是在用户态实现的,所以对于单线程系统而言,从内核的角度来看它仍然是单线程,当实现阻塞系统调用时,会把这个进程下的所有线程全部阻塞(实际上在操作系统看来它只阻塞了一个线程))

对于第二条缺点,其实和第一条很像,一个线程故障,会导致操作系统认为这个进程整体都故障,所以会阻塞整个进程

对于第三点缺点,由于一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU。因为在进程内部没有时钟中断,所以不可能轮转调度。

2.在内核中实现线程

特点:

这个时侯在进程中就没有线程表了,而在内核中有记录系统中所有线程的线程表。

当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。

内核线程表保存了线程的寄存器、状态和其他信息,这些信息和在用户空间中的是一样的。

下面分析优缺点:

优点:

1.一个线程阻塞时,不会干扰其他线程

2.内核可以根据其选择,并发地运行一个进程里的不同线程(可以在线程之间互相切换)

3.内核线程不需要任何新的、非阻塞系统调用。

4.如果某个进程中的线程引起了页面故障,内核可以很方便的检查该进程是否有任何其他可运行的线程。

缺点:

1.内核中创建或撤销线程的时间代价比较大

2.对操作系统要求更高

解决第一个缺点其实也有办法,就是当某个线程被撤销时,就把它标注为不可运行,但是内核数据结构不受影响,稍后必须创建线程时,就启用某个旧线程,节省开销

3.混合实现

人们试图结合两种线程的优点,一种方法是使用内核级线程,然后用户级线程与某些或者全部内核线路多路复用起来,这样可以带来很大的灵活度。

内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。可以创建、撤销、调度这些用户级线程。每个内核级线程中都能有一个用户级线程使用CPU。

具体可以参照下图理解:

6.调度程序激活机制

解决问题:尽管内核级线程在一些关键点上优于用户级进程,但是内核级进程的速度确实很慢。

调度程序激活机制就是在保证优良特性的情况下提升其速度的方法
工作目标:模拟内核线程的功能,但是为线程包提供通常在用户空间才能实现的更好的性能和更大的灵活性。

当一个用户线程从事某种系统调用时是安全的,那就不应该进行专门的非阻塞调用或者进行提前检查。

这种办法避免了用户空间和内核空间之间不必要的转换,从而提高了效率,比如某个线程等待另一个线程的工作而阻塞,此时没有理由请求内核,这样就减少了内核->用户转换的开销。用户空间的运行时系统可以阻塞同步的线程而另外调度一个线程。

工作的基本思路:当内核了解到一个线程被阻塞之后(例如,由于执行了一个阻塞系统调用或者产生理论一个页面故障),内核通知该进程的运行时系统,并且在堆栈中以参数传递有问题的线程编号和所发生事件的一个描述。

上行调用:内核了解到一个线程被阻塞后,通知该进程的运行时系统,并在堆栈中以参数形式传递有问题的线程编号和所发生事件的描述。这种通过在已知起始地址启动运行时系统并发出通知的机制被称为上行调用,这是对信号的一种粗略模拟。

一旦如此激活,运行时系统就会重新调度其线程,过程如下:1.把当前线程标记为阻塞并从就绪表中取出另一个线程,设置其寄存器,然后启动之,2.稍后,内核知道原来的线程又可运行时(比如已经从磁盘中读入故障页面),内核就又一次上行调用运行时系统,通知它这个事件,此时运行时系统按照自己的判断,立刻重启被阻塞的线程(或者把它放入就绪表中稍后运行)。

7.弹出式线程

概念很简单:一个消息到达导致系统创建一个处理该消息的线程,这种线程称为弹出式线程

好处:由于这种线程相当新,没有历史,没有必须存储的存储器、堆栈等内容,每个线程都全新的。使用弹出式线程可以使得消息到达与处理开始之间的事件非常短。

在使用弹出式线程时要提前计划,比如哪个进程中的线程先运行?如果系统支持在内核上下文中运行线程,线程就有可能在那里运行。在内核空间中运行弹出式线程通常比用户空间中容易且快捷。而且弹出式线程可以很容易地访问所有表格和IO设备,在处理中断时有用。

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值