Linux进程控制

Linux进程控制

本节重点
  • 学习进程创建,fork/vfork
  • 学习到进程等待
  • 学习到进程程序替换, 微型shell,重新认识shell运行原理
  • 学习到进程终止,认识$?
  • 主要就是进程相关的操作代码—创建/退出/等待/程序替换
    在这里插入图片描述

进程创建

fork函数初识
  • 通过pid_t fork(void)创建出来的进程其实是父子进程代码共享数据独有
  • 假设现在在物理内存中运行着一个./main(它实际上在物理内存中是离散式存储的),这个时候我们需要创建一个pcb来实现这个程序的调度,这个pcb里面会有一个内存指针,也就是虚拟地址空间的mm_struct结构体,然后通过页表进行映射,实现虚拟地址和物理地址之间的转化,然后呢,虚拟地址空间中又有很多东西,比如说是代码段 ,初始化全局变量,未初始化全局变量,栈,堆,内核空间,环境变量,运行参数等等东西…假设我现在有一个全局变量gval,要知道,变量名其实就是一块内存的别名,分配了一块地址,其实就是有了一块全局的地址,这个全局地址其实并没有物理存储,这个物理地址经过页表映射之后呢,就会映射到物理内存中相应的物理地址,这个时候通过fork创建了一个子进程,父子进程代码共享,数据独有,子进程复制了父进程pcb中的信息,页表也一起复制了。全部都复制了,也就是说父子进程指向的是同一个程序。但是这个时候,如果子进程说我想要修改这个全局变量的值,那么为了进程的独立性,就会给子进程重新开辟一块物理内存,这个时候呢,就将页表的映射关系进行修改,指向新的内存空间,这个时候子进程中的gval就会发生变化了。这其实就是写实拷贝技术
    在这里插入图片描述
    在这里插入图片描述
  • 在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
  • 但是其实,通过fork函数创建出来的子进程本身应该具有自己一块独有的物理内存地址去存放自己的数据的,但是为了保证子进程的创建效率,我们开始的时候让父子进程指向同一块物理内存,只有当数据发生改变的时候,再给子进程重新开辟物理内存,然后再将数据拷贝到新的物理内存里面就可以了(写实拷贝技术)
  • 父进程和子进程数据独有,代码共享
    在这里插入图片描述
进程调用fork,当控制转移到内核中的fork代码后,内核做:
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度
  • 所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定,也就是说在fork之后,父子进程谁先进行这时不确定的事情,是由调度器来决定的
fork()和vfork()
  • pid_t vfork()—父子进程共用虚拟地址空间,所以父进程需要阻塞起来,也就是说,先不让父进程运行,避免调用栈混乱,fork()和vfork()的区别就在于,fork父子进程的虚拟地址空间是不相同的,而vfork的父子进程的虚拟地址空间是相同的,父子进程共用一块虚拟地址空间,然后虚拟地址空间中有很多区域,比如堆区,栈区,他们都是通用的
  • vfork()本身是为了提高创建子进程的效率,但是有了写实拷贝技术之后,创建子进程的效率就大大的提高了,所以现在也不怎么使用vfork()函数了
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
写时拷贝
  • 通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
    在这里插入图片描述
fork常规用法
  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
  • 系统中有太多的进程
  • 实际用户的进程数超过了限制
进程终止
  • 进程终止就是退出一个进程,那个举一个例子:一个函数,他肯定是要完成一些功能的,那么我们如何查看这个函数他到底有没有完成他要去完成的功能呢?函数他是有返回值的,那么我们就可以通过函数的返回值来查看函数到底有没有完成他所要完成的相应的功能
    在这里插入图片描述
  • 具体使用哪一个方式来退出,主要取决于使用的场景,或者说,在结束的时候需不需要刷新缓冲区,来根据这些需求,从而可以去选择不同的退出方式
进程退出的场景:
  • 代码运行完毕,结果正确(正常退出,结果符合预期)
  • 代码运行完毕,结果不正确(正常退出,结果不符合预期)
  • 代码异常终止(异常退出,就是说,当前的进程并没有执行完毕,只是说有一些异常的情况,导致了当前进程的退出)
进程常见退出方法
  • 从main返回
  • 调用exit
  • _exit
    在这里插入图片描述
    在这里插入图片描述
从main返回
  • 一个程序从main函数作为入口的函数,如果main函数运行完了,那么我们的程序也就算是运行完毕了,所以第一种方式就是在main函数中return,就可以退出一个进程,比如说:这样写代码是可以打印出来change world的
    在这里插入图片描述
  • 但是如果在main函数中加上一个return -1(return 0),那么就不会打印了,比如说像这样子
    在这里插入图片描述
  • 就什么都不会打印,但是需要注意的是return只有在main函数中有退出进程的作用,在普通的函数中,return只是退出函数,而不具有退出进程的含义。
调用exit
  • 使用man exit来查看exit的手册----用man查看手册
  • man手册有不同的手册,用的最多是是man1,man2,和,man3
  • 其中1号手册是命令手册,2号手册是系统调用手册,3号手册是库函数手册
    在这里插入图片描述
    在这里插入图片描述
  • man 2—_exit
  • 我们使用man 2 exit看到的其实不是纯粹的exit,看到的其实是_exit,man 3的时候看到的才是真正的exit
  • 其头文件为#include<stdlib.h>
    在这里插入图片描述
  • man 3—使一个进程终止—exit
  • 其会使得一个正常的进程终止,并且返回值返回给父进程8
    在这里插入图片描述
exit函数—void exit(int status);
#include <unistd.h>
void exit(int status);

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  • 执行用户通过 atexit或on_exit定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用_exit
_exit函数—void _exit(int status);
#include <unistd.h>
void _exit(int status);
//参数:status 定义了进程的终止状态,父进程通过wait来获取该值
场景展示

在这里插入图片描述

  • 在function里面有return,然后在main函数中调用了function函数,但是这个 程序仍然是可以打印出nihao的,也就是说在普通函数中使用return是无法退出进程,只有在main函数中使用return才是可以退出进程的。但是在普通的函数中如果使用exit(0)的话,就会发现nihao就无法打印出来了。也就是说,在任意位置调用exit(0)都会退出进程,他和main是不一样的。
  • status是退出返回值,他和return会面的那个数据是一样的道理
  • 在任意位置调用exit都会退出进程,包括普通函数中调用exit
    在这里插入图片描述
  • 同样的,使用_exit(0)也不会打印出来,也可以用来退出进程
  • 同样没有打印出来,程序依然是提前退出了
    在这里插入图片描述
  • 下面的这个代码就只会打印leihoua,就不会去打印nihao了,因为打印完leihoua,就已经退出进程了,下面的东西就不会再继续去执行了
  • printf函数没有换行符,那么在想要打印的时候,就不会刷新文件的缓冲区,所以是不会打印出来的
    在这里插入图片描述
  • 下面的这个代码,数据并没有立即被打印,是在3秒钟之后才会打印出来的,3秒钟之后调用了main函数中的return退出进程
  • 因为printf没有加\n,所以是不会对缓冲区进行刷新的,只会三3秒之后才能打印出来,是在3秒种之后退出的时候才打印出来的,因为return会刷新文件的缓冲区
    在这里插入图片描述
  • 通过上述可以看出exit和_exit的区别在于退出的时候是否会刷新文件的缓冲区,我们要根据我们的需求来选择相应的退出进程的方式。
    在这里插入图片描述
进程等待
  • 等待子进程退出,获取子进程的返回值,释放子进程的资源,避免产生僵尸进程
    在这里插入图片描述
进程等待必要性
  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
如何避免僵尸进程的产生—使用进程等待
  • 使用进程等待
进程等待的方法
  • wait方法
  • waitpid方法
wait方法—等待任意一个子进程的退出
  • 因为wait是等待任意一个子进程的退出,那么如果有很多个子进程都退出了,那么其实wait也只能等待其中一个子进程的退出,因为其只会有一个返回值,只能返回一个返回值
  • wiat函数还是一个阻塞函数,也就是说,如果这个时候没有子进程退出的话,wait就会一直等待
  • wait是阻塞等待的
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

在这里插入图片描述
在这里插入图片描述

  • wait不仅仅是处理刚退出的子进程,就算你已经退出一段时间了,我也还是可以对你进行处理的。
举例子
  • 先写一个僵尸进程形成的原因出来
  • 子进程5秒钟之后退出,如果父进程什么都不做的话,那么子进程就会成为僵尸进程
    在这里插入图片描述
    在这里插入图片描述
  • 5秒钟之后,子进程成为了僵尸进程
    在这里插入图片描述
  • 父进程在创建完子进程其实就一直在打印我们所让他打印的内容了
  • 让子进程先睡上5秒钟,5秒钟之后,子进程退出,子进程先于父进程退出了,那么子进程从而变成了僵尸进程,那么有没有什么方式可以使得子进程不会变成僵尸进程?
  • 我们可以使用wait,因为现在并不关心他的返回值,所以我们可以传一个NULL进去,意思就是先不获取进程的返回值,这个时候通过查看进程的方式进行查看就会发现了,并没有产生僵尸进程
  • 如果我们传进去一个NULL,表示我们这个时候是不想获取到返回值的
    在这里插入图片描述
    在这里插入图片描述
  • 那么,在修改了之后,其实就没有僵尸进程的产生了
    在这里插入图片描述
  • 或者也可以定义一个status,先获取到,不关注其到底是什么
    在这里插入图片描述
  • 使用了wait之后,就会发现,已经没有僵尸进程了,没有使用wait之前的僵尸进程已经不复存在了。
    在这里插入图片描述
  • 下面的代码是先获取到返回值,然后再退出的
    在这里插入图片描述
  • 下面代码的运行结果为:执行就会出错,因为这里根本就没有子进程,还在这等待啥啊,所以wait直接报错返回
    在这里插入图片描述
    在这里插入图片描述
  • 父子进程都是各自运行各自的
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该
子进程的ID。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • WNOHANG是一个宏
  • 非阻塞就是如果当前不具备完成功能的条件,我不会在这等待,我会直接报错返回,而阻塞状态则是,如果当前不具备完成功能的条件,那么我会在这一直等着
  • waitpid相较于wait来说会更加的灵活一些,既可以阻塞,也可以不阻塞
  • 非阻塞和阻塞的优点和缺点:非阻塞不用一直等待,可以在不满足完成功能的条件的时候去感悟一些其他的事情,可以充分的利用时间,对资源的利用流程更高,但是通常需要循环操作,所以说流程可能会复杂一些。
  • 不使用wait了,使用waitpid看一看,第一个参数是-1,也就是说等待任意一个子进程的退出
  • wait和waitpid都是等待自己的子进程退出
    在这里插入图片描述
    在这里插入图片描述
  • 返回的是退出的子进程的pid
    在这里插入图片描述
  • 如果我要指定一个进程去退出的话,直接就报错退出了,如果想要指定某个进程的话,就可以把第一个参数随意更改,比如说改成7890之类的
    在这里插入图片描述
  • 会直接报错退出,就好比说去幼儿园的时候不接自己的孩子,非要去接别人家的孩子是一个道理
    在这里插入图片描述
  • 0表示的是阻塞等待
    在这里插入图片描述
  • 当然也可以把0改成WNOHANG,意思就是非阻塞的意思,也就是说,不要再继续等待了
    在这里插入图片描述
  • 运行结果,和上面的不一样,这里的返回值是0,并不是-1
    在这里插入图片描述
  • 子进程5秒钟之后退出的时候,因为没有使用waitpid去处理他,所以子进程成为僵尸进程了,所以需要像下面这样做:可以更加充分的利用资源,也就是说,我不一直在这等着,我也可以去做一些别的事情,干完事情之后,我再重新回过头来看,你到底有没有退出
  • 这就是非阻塞的一种常规用法
  • 也就是说非阻塞一般要进行循环处理,如果不进行循环处理的话,那么就很有可能会产生僵尸进程
    在这里插入图片描述
    在这里插入图片描述
  • 下面来打印一下返回值
    在这里插入图片描述
    在这里插入图片描述
  • 运行结果:
    在这里插入图片描述
    在这里插入图片描述
如何获取进程的返回值

在这里插入图片描述

  • 先去获取一下返回值
    在这里插入图片描述
  • 返回值是25534,而并不是99,那么返回25534的原因在于,十进制的99是16进制的63,十进制的25534是16进制的6300
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 运行结果如下所示:
  • 由上面的运行结果可以知道,返回值其实是用了一个字节在进行保存的
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 向右边移动8位之后的结果
    在这里插入图片描述
    在这里插入图片描述
  • 获取返回值的那个语句的意思就是只要低八位,其他位全部都不要,其他位全部置0。
    在这里插入图片描述
  • 按位与表达式的意思是----只保留方框内的数据,其他的都不要了,那么与出来的结果也就是00001001,也就是返回值的9,那么也就是获取到返回值了
    在这里插入图片描述
    在这里插入图片描述
  • 运行结果如下所示:
    在这里插入图片描述
回顾之前的

在这里插入图片描述
在这里插入图片描述

如何才能知道子进程到底是正常退出还是异常退出的呢?

在这里插入图片描述

  • 便于事后调试,就是为了在事后,让我们了解到,到底是哪里出现了问题,从而导致了我们程序的异常退出而不是正常退出
  • 在操作系统中,信号的体现,其实就是一个数字
    在这里插入图片描述
  • core dump标志默认是关闭的,也就是说,core dump他是存在不同的声音的,因为有的人觉得core dump挺好的,因为其方便了后续的调试,但是也有人说,core dump这个东西比较危险,因为他保存了程序的一些运行的信息,那么这些信息里面可能会包含有隐私性的东西,有可能会涉及到数据的泄漏问题,并且每次程序异常退出的时候,都会产生一个core dump文件,core dump文件的大小取决于代码中处理的数据有多少,如果程序非常大的话,就会对内存非常的不友好。所以core dump默认是关闭的,所以就只能说他是否开启
  • core dump标志和是否正常退出是没有什么关系的
  • 操作系统给进程发出一个信号,那么现在进程就知道了发生了什么样子的事情,到底时什么形式的信号,其实是并不重要的,在操作系统中的体现,信号就是一个数字
  • 比如说下图,在进行调试的时候,就会看到有这么一句话,程序接收到了一个信号,发生了段错误
    在这里插入图片描述
  • 调试起来之后,可以看到,程序接收到了一个信号,发生了段错误
    在这里插入图片描述
  • 下面这一步是加载core dump文件,他就会告诉我们,程序被终止,因为一个11号的信号,发生了段错误,同时我们可以使用bt来查看或者说是快速定位程序出现错误的位置
    在这里插入图片描述
    在这里插入图片描述
如果进程正常退出了,再去获取程序的返回值
  • 如果进程正常退出了,再去获取程序的返回值,如果程序不是正常退出的,那么就没有必要去获取他的返回值了
  • 那么我们就要去查看低7位的值到底是什么了,通过是否0来判断进程是否是正常退出的
  • 如果不是正常退出的,就不用去获取返回值了,不是正常退出的话,那么获取返回值其实也没什么意义
    在这里插入图片描述
  • 如果是正常退出的话,再去获取返回值,如果不是正常退出的话,那么其实就没有必要再去获得返回值了,所以我们用0x7f&status,看得到的结果是多少,如果得到的结果是0的话,再去获取返回值,如果得到的结果不是0的话,就没有必要取获取返回值了
    在这里插入图片描述
  • 考虑到位运算其实是有些复杂的,所以操作系统直接提供了一些接口供我们使用
  • 我们使用man waitpid 来查看一下这些方便我们使用的接口
  • 下面这个是用来判断进程是否是正常退出的,用下面的接口也是同样可以获取到返回值的
    在这里插入图片描述
    在这里插入图片描述
  • 获取低8个比特位里面的数据
    在这里插入图片描述
  • 如果是正常退出的,那么就去获取返回值就好了,同样用宏来获取返回值的结果
  • 先看其是不是正常退出的,如果是正常退出的,再去获取返回值,否则的话,就不必去获取返回值了
    在这里插入图片描述
  • 运行结果如下所示:
  • 结果就是我们获取到了返回值,两种方法获取到的返回值是一样的,没有什么却别
    在这里插入图片描述在这里插入图片描述
程序替换

在这里插入图片描述

  • 创建一个子进程的目的是什么
    在这里插入图片描述
  • 那么如何去解决这种缺陷呢?也就是说,怎么才能把子进程要干的事情分摊出去呢?
  • 那么,就需要使用到程序替换这个方法
  • 程序替换就是说,将另一个程序加载到内存中,然后让原有的pcb不再去调度运行原先的程序,而是去调度这个新的程序
    在这里插入图片描述
  • 那么我现在假如说是想让子进程去干别的事情的话,我可以不使用代码分流的方法,我可以用别的方式,假设我现在磁盘上还有一个test方法,这个时候我把test加载到内存里去,让子进程的页表映射到test,不要让其再去映射main了,那么这个时候子进程的pcb调度的就是这个test的程序,这就是是程序替换—让pcb不去调度原先的程序,让pcb去调度运行一个新的程序。
    在这里插入图片描述
程序替换的本质

在这里插入图片描述

  • pcb这个进程是没变的,变得只是它运行的程序,运行的程序不是原先的那个程序了,是新的映射的程序
  • pcb是没有被释放的,pcb还是原来的那个pcb
被替换掉的程序代码和数据还在吗?
  • 如果父进程还在继续使用的话,那么这些代码和数据就还是在的,因为本质上来说,这些东西都是属于人家父进程的,如果父进程也不再继续使用的话,那么这些东西就是不在了,就会从内存释放掉。
替换原理
  • 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。
如何在代码中完成程序替换,让一个进程调度运行另一个程序

在这里插入图片描述

  • 通过使用 man execl来查看操作系统所提供的一系列的接口
    在这里插入图片描述
  • 下面这个代码会打印出来nihaoa
    在这里插入图片描述
  • 运行结果
    在这里插入图片描述
  • 查看ls所在的位置
    在这里插入图片描述
    在这里插入图片描述
  • 第0个参数是ls他自己,第一个参数是-l 如果说没有参数了,那么就以空作为结尾
  • 第一个参数是带有路径的新的程序的名称,就是使用这个程序替换进程正在调度运行的程序(path参数的含义),然后后面紧跟着的是程序的运行参数,没有参数了的话,就以空作为结尾,因为execl是一个不定参的函数
  • 下面的代码,不仅会打印nihaoa,还会打印出来ls目录下文件的详细信息
    在这里插入图片描述
  • 运行结果,出现这种结果的原因就是,代码现会去打印nihaoa,然后继续向下走,碰到了execl函数,然后就回去进行程序替换,对当前调用的进程进行程序替换,去运行ls程序,这就是程序替换
    在这里插入图片描述
  • 查看运行参数的方式,argc表示的是当前的程序有多少个运行的参数,而argv这个字符串指针数组用来存储当前程序的运行参数,可以去查看当前程序都有哪些运行参数
    在这里插入图片描述
  • 运行结果,运行起来就可以看到这个程序的参数了,0号参数是程序自身
    在这里插入图片描述
  • 程序参数的赋予方式 ,这种方式是在给程序增加参数(程序运行参数)在这里插入图片描述在这里插入图片描述
  • 运行结果,因为mytest现在只有1个参数
    在这里插入图片描述
  • 因为exec程序中去执行了mytest程序,所以会多出来一个参数
    在这里插入图片描述
程序替换之后,当前进程运行完替换后的程序就会退出,并不会又回去运行原先的程序
函数解释
  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。
函数的区别

在这里插入图片描述
在这里插入图片描述

l和v的区别

在这里插入图片描述

  • 举例子:
  • execv在赋予参数的时候,只有一个参数去赋予,这个参数就是字符指针数组,通过字符指针数组去赋予参数
  • 如果没有参数了的话,就给成空,记住,参数是一定要以空来结尾的
    在这里插入图片描述
  • 运行结果,一定要注意最后一个参数总是需要以空来作为结尾
    在这里插入图片描述
  • 最后一个参数一定要为空,或者说,参数一定要以空作为结尾
    在这里插入图片描述
有没有p的区别

在这里插入图片描述

  • 下面的程序是无法运行的,因为没有路径
  • 带上一个p就可以执行成功了,因为有p的话,就可以不用带路径了
    在这里插入图片描述
有没有e的区别

在这里插入图片描述

  • 使用main函数的第三个参数去打印环境变量(test文件)
    在这里插入图片描述
  • exec文件
    在这里插入图片描述
  • 运行结果就会打印当前所有的环境变量
    在这里插入图片描述
  • 但是如果重新带有一个e和新的环境变量的话
    在这里插入图片描述
  • 就会发现原有的环境变量都没有了,只有我们新传进去的两个环境变量
    在这里插入图片描述
函数区别的演示总结:
  • 前五个全部都是库函数,最后一个是系统调用接口,也就是上面的五个,其实都是封装了最后一个从而实现的,都是为了便于实现
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
回顾

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值