【Linux102】17-kernel/signal.c


公粽号「专注Linux」,专注Linux内核开发

Linux102系列会详细讲解Linux0.11版本中的102个文件,本文讲解linux0.11的第17个文件kernel/signal.c的文件源码。

1.kernel/signal.c的主要作用

推荐前置阅读:😉【Linux102】18-include/signal.h


这段代码是处理信号机制的核心实现,负责信号的设置获取处理


这段代码实现了 Linux 内核早期的信号机制核心功能,包括:

1.进程的信号掩码(阻塞码)的获取与设置函数sys_sgetmask()
sys_ssetmask()
2.进程的信号处理函数的注册函数sys_sigaction()
3.修改进程在收到特定信号时所采取的具体的行动的系统调用函数sys_sigaction()
4.进程的信号实际处理逻辑,包括现场保存与恢复、跳转至用户态处理函数等do_signal()

注意,有关信号操作的发送信号函数 send_sig()和通知父进程函数 tell_father()被包另一个程序 exit.c中。

程序中名称前缀 sig均是信号 signal 的简称。

2.signal()和 sigaction()的区别

signal()和 sigaction()的功能比较类似,都是改变信号原处理句柄(handler,或称为程序)。但:

signal()会返回原信号处理句柄,并且在新句柄被调用一次后句柄就会恢复到器值
sigaction()则可以进行更自由的设置

3.信号和进程的关系


注意前缀——“进程的”,说明进程和信号息息相关,具体如下:

在操作系统内核中,进程是信号的载体与处理主体,信号则是内核进程间向进程传递异步事件通知的机制,二者通过内核的信号管理模块紧密关联,二者的互动是内核实现进程控制(如终止、暂停)和异步事件通知的核心机制。


4.本源码涉及的文件

😉【Linux102】18-include/signal.h本文件定义了信号处理相关数据结构,是理解Linux内核的信号机制的必知前提。
😉【Linux102】15-include/linux/sched.h本程序主要定义了进程调度的相关数据结构,如任务数据结构等等,理解这些数据结构是理解进程调度机制的关键,也是理解内核的关键。
一句话:我们常说的进程是XXX,但是进程究竟是什么?就定义在这个文件里面!源码面前,没有秘密。
😉【【Linux102】20-include/linux/kernel.h本程序主要定义了内核中最常用的一些基础函数声明,是内核运行最基本保障,比如malloc、printk、free、tyy_write、panic等函数声明,这些函数具体的实现则分布在各个.c文件中。
😉【Linux102】21-include/asm/segment.h本程序定义了一系列用于在 Linux 内核中访问用户空间内存的内联函数,主要通过内嵌汇编操作段寄存器fs来实现内核与用户空间的数据交互。

5.do_signal()函数详解

do_signal()函数是内核系统调用(int 0x80)中断处理程序信号的预处理程序。该函数的主要作用是将信号的处理句柄插入到用户程序堆栈中。这样,在当前系统调用结束返回后就会立刻执行信号句柄程序,然后再继续执行用户的程序

图-信号处理程序的调用方式


6.sys_signal()函数详解

sys_signal()函数原型

int sys_signal(int signum, long handler, long restorer);

sys_signal()函数的参数restorer是一函数指针。该函数由Libc函数库提供用于恢复系统调用后的返回值和一些寄存器。在编译链接程序时由Libc函数库提供,用于在信号处理程序结束后清理用户态堆栈,并恢复系统调用存放在eax中的返回值。


7.如何处理信号?

进程中的信号是用于进程与内核进程之间通信的一种简单消息,通常是下表中的一个标号数值,并且不携带任何其他的信息。 例如当一个子进程终止或结束时,就会产生一个标号为17SIGCHLD信号发送给父进程,以通知父进程有关子进程的当前状态。

关于一个进程如何处理收到的信号,一般有两种做法:

进程如何处理收到的信号?
一是程序的进程不去处理,此时该信号会由系统相应的默认信号处理程序进行处理;
第二种做法是进程使用自己的信号处理程序来处理信号。

下面我们再逐行解释源码:

8.源码注释版本

#include <linux/sched.h>   // 包含进程调度相关定义(如current进程结构体)
#include <linux/kernel.h>  // 包含内核基本函数和宏定义
#include <asm/segment.h>   // 包含段操作函数(如put_fs_byte、get_fs_byte等)
#include <signal.h>        // 包含信号相关常量和结构体定义(如sigaction、SIGKILL等)
  
void do_exit(int error_code);  // 声明进程退出函数(在exit.c中实现)
// 作用:系统调用,获取当前进程的信号掩码(被阻塞的信号集合)。
// current:指向当前运行进程的结构体指针(在 sched.h 中定义)。
// blocked:进程结构体中记录被阻塞信号的字段(位掩码,每一位代表一个信号是否被阻塞)。
int sys_sgetmask()
{
    return current->blocked;
}

// 作用:系统调用,设置当前进程的信号掩码。
// 关键逻辑:SIGKILL(终止信号)不能被阻塞,
// 因此通过& ~(1 << (SIGKILL - 1))强制清除新掩码中 SIGKILL 的阻塞位。
int sys_ssetmask(int newmask)
{
    int old = current->blocked;  // 保存旧的信号掩码

    // 设置新的信号掩码,但强制不能阻塞SIGKILL(1 << (SIGKILL - 1)是SIGKILL的位掩码)
    current->blocked = newmask & ~(1 << (SIGKILL - 1));
    return old;  // 返回旧的信号掩码
}

/**
* func descp: 保存旧的信号处理结构(save_old)
*/
// 静态内联函数:将内核空间的信号处理结构(struct sigaction)复制到用户空间。
// verify_area:检查用户空间地址的合法性(内核安全机制)。
// put_fs_byte:内核向用户空间写入数据的函数(因内核和用户空间地址空间分离,需特殊处理)。
static inline void save_old(char *from, char *to)
{
    int i;

    // 验证用户空间地址to的内存是否可写(避免内核访问非法用户地址)
    verify_area(to, sizeof(struct sigaction));
    // 逐字节将内核空间的from复制到用户空间的to(信号处理结构)
    for (i = 0; i < sizeof(struct sigaction); i++)
    {
        put_fs_byte(*from, to);  // 将内核数据写入用户空间
        from++;
        to++;
    }
}
/**
* func descp: 获取新的信号处理结构(get_new)
*/

// 静态内联函数:将用户空间定义的信号处理结构复制到内核空间。
// get_fs_byte:从用户空间读取数据到内核的函数(与put_fs_byte对应)。
static inline void get_new(char *from, char *to)
{
    int i;

    // 逐字节将用户空间的from复制到内核空间的to(信号处理结构)
    for (i = 0; i < sizeof(struct sigaction); i++)
        *(to++) = get_fs_byte(from++);  // 从用户空间读取数据到内核
}

/**
* func descp: 注册信号处理函数(sys_signal)
*/

// 系统调用:注册信号处理函数(早期的signal()系统调用实现)。
// 关键标志:
// SA_ONESHOT:信号处理函数执行一次后自动失效(需重新注册)。
// SA_NOMASK:信号处理期间不阻塞自身(允许嵌套触发同一信号)。
int sys_signal(int signum, long handler, long restorer)
{
    struct sigaction tmp;  // 临时信号处理结构

    // 检查信号编号合法性:1~32之间,且不能是SIGKILL(SIGKILL不能被捕获/修改)
    if (signum < 1 || signum > 32 || signum == SIGKILL)
        return -1;

    // 初始化临时信号处理结构
    tmp.sa_handler = (void (*)(int))handler;  // 信号处理函数(用户提供)
    tmp.sa_mask = 0;  // 信号处理期间不阻塞其他信号
    tmp.sa_flags = SA_ONESHOT | SA_NOMASK;  // 标志:一次有效+不自动阻塞自身
    tmp.sa_restorer = (void (*)(void))restorer;  // 信号处理完成后恢复现场的函数

    // 保存旧的信号处理函数(用于返回给用户)
    handler = (long)current->sigaction[signum - 1].sa_handler;
    // 更新当前进程的信号处理结构
    current->sigaction[signum - 1] = tmp;
    return handler;  // 返回旧的信号处理函数地址
}

/**
* func descp: 高级信号处理设置(sys_sigaction)
*/

// 系统调用:更灵活的信号处理设置(对应标准的sigaction()系统调用)。
// 功能:允许用户自定义信号处理函数、掩码和标志,比sys_signal更强大。
// 关键逻辑:自动处理信号掩码(若未设置SA_NOMASK,则信号处理期间自动阻塞自身)。

int sys_sigaction(int signum, const struct sigaction *action,
                  struct sigaction *oldaction)
{
    struct sigaction tmp;  // 临时保存旧的信号处理结构

    // 检查信号编号合法性(同sys_signal)
    if (signum < 1 || signum > 32 || signum == SIGKILL)
        return -1;

    // 保存当前信号的旧处理结构
    tmp = current->sigaction[signum - 1];
    // 将用户提供的新处理结构从用户空间复制到内核(更新当前进程的信号处理)
    get_new((char *)action, (char *)(signum - 1 + current->sigaction));

    // 如果需要返回旧处理结构,将其从内核复制到用户空间
    if (oldaction)
        save_old((char *)&tmp, (char *)oldaction);

    // 根据标志更新信号掩码:
    // 如果设置了SA_NOMASK,则不阻塞任何信号;否则自动阻塞当前信号
    if (current->sigaction[signum - 1].sa_flags & SA_NOMASK)
        current->sigaction[signum - 1].sa_mask = 0;
    else
        current->sigaction[signum - 1].sa_mask |= (1 << (signum - 1));

    return 0;  // 成功
}

/**
* func descp:  信号处理核心逻辑(do_signal)
*/

// 核心函数:当内核检测到进程有未处理的信号时,调用此函数处理。
// 执行流程:
// 检查信号处理函数:忽略(1)、默认(NULL)或用户自定义。
// 若为一次性处理(SA_ONESHOT),清空处理函数。
// 修改指令指针(eip),让进程从用户态的信号处理函数开始执行。
// 保存现场:将寄存器、原指令地址等压入用户栈,供处理完成后恢复。
// 阻塞sa_mask中指定的信号,避免处理期间被干扰。

void do_signal(long signr, long eax, long ebx, long ecx, long edx,
               long fs, long es, long ds,
               long eip, long cs, long eflags,
               unsigned long *esp, long ss)
{
    unsigned long sa_handler;  // 信号处理函数地址
    long old_eip = eip;  // 保存原指令地址(信号处理完成后返回)
    // 获取当前信号对应的处理结构
    struct sigaction *sa = current->sigaction + signr - 1;
    int longs;  // 栈上需要保存的长整型数据个数
    unsigned long *tmp_esp;  // 临时栈指针

    sa_handler = (unsigned long)sa->sa_handler;  // 获取处理函数地址

    // 处理函数为1:忽略信号(SIG_IGN)
    if (sa_handler == 1)
        return;

    // 处理函数为NULL:使用默认处理
    if (!sa_handler)
    {
        if (signr == SIGCHLD)  // SIGCHLD(子进程状态变化)默认忽略
            return;
        else  // 其他信号默认处理:进程退出
            do_exit(1 << (signr - 1));
    }

    // 如果是一次性处理(SA_ONESHOT),执行后清空处理函数(下次用默认行为)
    if (sa->sa_flags & SA_ONESHOT)
        sa->sa_handler = NULL;

    // 修改指令指针:跳转到信号处理函数(用户态执行)
    *(&eip) = sa_handler;

    // 计算需要保存到栈的参数个数:SA_NOMASK则7个,否则8个(多保存一个信号掩码)
    longs = (sa->sa_flags & SA_NOMASK) ? 7 : 8;
    // 调整用户栈指针:为保存参数预留空间
    *(&esp) -= longs;
    verify_area(esp, longs * 4);  // 验证栈空间合法性
    tmp_esp = esp;  // 临时栈指针

    // 将以下数据依次压入用户栈(供信号处理函数和恢复现场使用)
    put_fs_long((long)sa->sa_restorer, tmp_esp++);  // 恢复现场函数地址
    put_fs_long(signr, tmp_esp++);                  // 信号编号
    if (!(sa->sa_flags & SA_NOMASK))
        put_fs_long(current->blocked, tmp_esp++);   // 信号掩码(若需要)
    put_fs_long(eax, tmp_esp++);                    // 寄存器eax
    put_fs_long(ecx, tmp_esp++);                    // 寄存器ecx
    put_fs_long(edx, tmp_esp++);                    // 寄存器edx
    put_fs_long(eflags, tmp_esp++);                 // 标志寄存器
    put_fs_long(old_eip, tmp_esp++);                // 原指令地址(处理完成后返回)

    // 信号处理期间阻塞sa_mask中指定的信号
    current->blocked |= sa->sa_mask;
}  

9.源码完整版

/*
 *  linux/kernel/signal.c
 *
 *  (C) 1991  Linus Torvalds
 */

#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>

#include <signal.h>

void do_exit(int error_code);

int sys_sgetmask()
{
    return current->blocked;
}

int sys_ssetmask(int newmask)
{
    int old = current->blocked;

    current->blocked = newmask & ~(1 << (SIGKILL - 1));
    return old;
}

static inline void save_old(char *from, char *to)
{
    int i;

    verify_area(to, sizeof(struct sigaction));
    for (i = 0; i < sizeof(struct sigaction); i++)
    {
        put_fs_byte(*from, to);
        from++;
        to++;
    }
}

static inline void get_new(char *from, char *to)
{
    int i;

    for (i = 0; i < sizeof(struct sigaction); i++)
        *(to++) = get_fs_byte(from++);
}

int sys_signal(int signum, long handler, long restorer)
{
    struct sigaction tmp;

    if (signum < 1 || signum > 32 || signum == SIGKILL)
        return -1;
    tmp.sa_handler = (void (*)(int))handler;
    tmp.sa_mask = 0;
    tmp.sa_flags = SA_ONESHOT | SA_NOMASK;
    tmp.sa_restorer = (void (*)(void))restorer;
    handler = (long)current->sigaction[signum - 1].sa_handler;
    current->sigaction[signum - 1] = tmp;
    return handler;
}

int sys_sigaction(int signum, const struct sigaction *action,
                  struct sigaction *oldaction)
{
    struct sigaction tmp;

    if (signum < 1 || signum > 32 || signum == SIGKILL)
        return -1;
    tmp = current->sigaction[signum - 1];
    get_new((char *)action,
            (char *)(signum - 1 + current->sigaction));
    if (oldaction)
        save_old((char *)&tmp, (char *)oldaction);
    if (current->sigaction[signum - 1].sa_flags & SA_NOMASK)
        current->sigaction[signum - 1].sa_mask = 0;
    else
        current->sigaction[signum - 1].sa_mask |= (1 << (signum - 1));
    return 0;
}

void do_signal(long signr, long eax, long ebx, long ecx, long edx,
               long fs, long es, long ds,
               long eip, long cs, long eflags,
               unsigned long *esp, long ss)
{
    unsigned long sa_handler;
    long old_eip = eip;
    struct sigaction *sa = current->sigaction + signr - 1;
    int longs;
    unsigned long *tmp_esp;

    sa_handler = (unsigned long)sa->sa_handler;
    if (sa_handler == 1)
        return;
    if (!sa_handler)
    {
        if (signr == SIGCHLD)
            return;
        else
            do_exit(1 << (signr - 1));
    }
    if (sa->sa_flags & SA_ONESHOT)
        sa->sa_handler = NULL;
    *(&eip) = sa_handler;
    longs = (sa->sa_flags & SA_NOMASK) ? 7 : 8;
    *(&esp) -= longs;
    verify_area(esp, longs * 4);
    tmp_esp = esp;
    put_fs_long((long)sa->sa_restorer, tmp_esp++);
    put_fs_long(signr, tmp_esp++);
    if (!(sa->sa_flags & SA_NOMASK))
        put_fs_long(current->blocked, tmp_esp++);
    put_fs_long(eax, tmp_esp++);
    put_fs_long(ecx, tmp_esp++);
    put_fs_long(edx, tmp_esp++);
    put_fs_long(eflags, tmp_esp++);
    put_fs_long(old_eip, tmp_esp++);
    current->blocked |= sa->sa_mask;
}

10.源码图像版

kernel/signal.c源码图像版

11.源码注释版图像

kernel/signal.c源码注释图像版

12.进程信号表

Linux系统所支持的信号见表:

标号名 称说 明默认操作
1SIGHUP(Hangup)当你不再控制终端时,或者当你关闭 Xterm 或断开 modem 连接时内核会产生该信号。后台程序常用其重新读取配置文件(Abort)挂断控制终端或进程
2SIGINT(Interrupt)来自键盘的终端,通常终端驱动程序会将其与 Ctrl + C 绑定(Abort)程序被终止
3SIGQUIT(Quit)来自键盘的终端,通常终端驱动程序会将其与 Ctrl + \ 绑定(Dump)程序被终止并产生 dump core 文件
4SIGILL(Illegal Instruction)程序出错或者执行了非法操作指令(Dump)程序被终止并产生 dump core 文件
5SIGTRAP(Breakpoint/Trace Trap)调试用,跟踪断点(Dump)程序被终止并产生 dump core 文件
6SIGABRT(Abort)放弃执行,异常结束(Dump)程序被终止并产生 dump core 文件
6SIGIOT(IO Trap)同 SIGABRT(Dump)程序被终止并产生 dump core 文件
7SIGUNUSED(Unused)没有使用-
8SIGFPE(Floating Point Exception)浮点异常(Dump)程序被终止并产生 dump core 文件
9SIGKILL(Kill)程序被终止,该信号不能被捕获或忽略,想立刻终止进程就发此信号,程序无清理机会(Abort)程序被终止
10SIGUSR1(User Defined Signal 1)用户定义的信号(Abort)进程被终止
11SIGSEGV(Segmentation Violation)当程序引用无效内存时产生,比如访问没有映射的内存、写只读内存等(Dump)程序被终止并产生 dump core 文件
12SIGUSR2(User Defined Signal 2)保留给用户程序用于 IPC 或其他目的(Abort)进程被终止
13SIGPIPE(Pipe)当程序向一个套接字或管道写时,由于没有读者而产生该信号(Abort)进程被终止
14SIGALRM(Alarm)该信号会在用户调用 alarm 系统调用所设置的延迟秒数到后产生,常用于到期提醒(Abort)进程被终止
15SIGTERM(Terminate)用于和善地要求一个程序终止,是 kill 的默认信号,与 SIGKILL 不同,该信号能被捕获做退出前清理(Abort)进程被终止
16SIGSTKFLT(Stack Fault on Coprocessor)协处理器堆栈错误(Abort)进程被终止
17SIGCHLD(Child)父进程发出,停止或终止子进程,可改变含义挪作他用(Ignore)子进程停止或结束
18SIGCONT(Continue)该信号致使被 SIGSTOP 停止的进程恢复运行,可被捕获(Continue)恢复进程的执行
19SIGSTOP(Stop)停止进程的运行,该信号不可被捕获或忽略(Stop)停止进程运行
20SIGTSTP(Terminal Stop)向终端发送停止进程,该信号可以被捕获或忽略(Stop)停止进程运行
21SIGTTIN(TTY Input on Background)后台程序试图从一个不再被控制的终端上读取数据,此时进程将被停止,直到收到 SIGCONT(Stop)停止进程运行
22SIGTTOU(TTY Output on Background)后台进程试图向一个不再被控制的终端上输出数据,此时进程将被停止,直到收到 SIGCONT(Stop)停止进程运行


汇编语言

😉【汇编语言】1—基础硬件知识

😉【汇编语言】2—寄存器基础知识

😉【汇编语言】3-寄存器与内存的交互

😉【汇编语言】4-第一个完整的汇编程序

😉【汇编语言】5-[BX]和loop指令

😉【汇编语言】6-包含多个段的程序

😉【汇编语言】7-灵活的5大寻址方式

😉【汇编语言】8-1-数据的位置

😉【汇编语言】8-2-数据的长度

😉【汇编语言】8-数据处理的两个基本问题(位置与长度)

😉【DOSBox】1-debug

😉【DOSBox】2-debug可执行文件

😉【DOSBox】3-完整开发流程


C语言

本系列将直击C语言的本质基础,流利处理出各个易错、实用的实战点,并非从零开始学习C。

😉【C语言】C Token(C89 C99 C11)

😉【C语言】指针基础

😉【C语言】数组基础

😉【C语言】结构体对齐

😉【C语言】华为C语言进阶测试

😉【C语言】触发刷新到磁盘的方式总结

😉【C语言】C语言文件操作的mode详解

😉【C语言】C语言文件知识全讲解

😉【C语言】从extern到头文件包含的最优实践

😉【C语言】C语言的关键字与重载机制

😉【C语言】长字符串的2种处理方式

😉【C语言】C语言嵌入汇编程序

😉【C语言】指针数组 VS 数组指针 原来这么简单!

😉【C语言】深浅拷贝、传参、赋值 本质剖析

😉【C语言】find-in-linux递归搜索文件名函数

😉【C陷阱与缺陷】-1-词法陷阱

😉【C陷阱与缺陷】-2-语法陷阱

😉【C陷阱与缺陷】-3-语义陷阱


Linux101系列

专注讲解Linux中的常用命令,共计发布100+文章。

😉【Linux101-1】ls

😉【Linux101-1】ls -l命令输出结果全解析

😉【Linux101-2】cd

😉【Linux101-3】cat

😉【Linux101-4】tac

😉【Linux101-5】head

😉【Linux101-6】tail

😉【Linux101-7】pwd

😉【Linux101-8】touch

😉【Linux101-9】cal

😉【Linux101-10】bc

😉【Linux101-11】df

😉【Linux101-12】uname

😉【Linux101-13】mkdir

😉【Linux101-14】gzip

😉【Linux101-15】tar

😉【Linux101-16】lsof

😉【Linux101-17】du

😉【Linux101-18】stat

😉【Linux101-19】top

😉【Linux101-20】echo


Linux102系列

本系列将精讲Linux0.11内核中的每一个文件,共计会发布100+文章。

😉【Linux102】1-Makefile

😉【Linux102】2-Makefile.header

😉【Linux102】3-system.map

😉【Linux102】4-bootsect.s

😉【Linux102】5-setup.s

😉【Linux102】6-head.s

😉【Linux102-D】/boot

😉【Linux102】7-main.c

😉【Linux102】8-kernel/asm.s

😉【Linux102】9-kernel/traps.c

😉【Linux102】10-kernel/printk.c

😉【Linux102】11-kernel/vsprintf.c

😉【Linux102】12-include/stdarg.h

😉【Linux102】13-kernel/mktime.c

😉【Linux102】14-kernel/system_call.s

😉【Linux102】15-include/linux/sched.h

😉【Linux102】18-include/signal.h

😉【Linux102】19-include/sys/types.h

😉【Linux102】20-include/linux/kernel.h

😉【Linux102】21-include/asm/segment.h

😉【Linux102】22-include/linux/head.h

😉【Linux102】23-include/linux/mm.h

😉【Linux102】24-include/linux/fs.h

😉【Linux102】25-include/errno.h

😉【Linux102】26-include/sys/wait.h

😉【Linux102】27-include/inux/tty.h

😉【Linux102】28-include/termios.h

😉【Linux102】29-kernel/panic.c

😉【Linux102】30-include/sys/times.h

😉【Linux102】31-include/sys/utsname.h

😉【Linux102】32-include/stddef.h


Linux内核精讲系列

和Linux内核102系列不同,本系列将会从全局描绘Linux内核的各个模块,而非逐行源码分析,适合想对Linux系统有宏观了解的家人阅读。

😉【Linux】学习Linux前必备的知识点

😉【Linux】Linux内核对进程的内存抽象

😉【Linux】Linux概述1-linux对物理内存的使用

😉【Linux】软件从写下到运行的全部流程

😉【Linux】CPU的三寻址:实模式、保护模式、长模式

😉【Linux】实模式与保护模式的寻址, 这次讲明白了!

😉【Linux】linux0.11的源码目录架构

😉【Linux】Makefile机制及基础详解

😉【Linux】编译并运行Linux0.11

😉【Linux】“进进内网文”—Linux的内核结构全貌

😉【Linux】linux的中断机制

😉【Linux】linux进程描述



关于小希

😉嘿嘿嘿,我是小希,专注Linux内核领域,同时讲解C语言汇编等知识。

我的微信:C_Linux_Cloud,期待与您学习交流!

加微信请备注哦


小希的座右铭:别看简单,简单也是难。别看难,难也是简单。我的文章都是讲述简单的知识,如果你喜欢这种风格:

不妨关注、评论、转发,让更多朋友看到哦~~~🙈

下一期想看什么?在评论区留言吧!我们下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值