Linux 信号 (2)

1.信号周期

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作(handler)。
1.信号 产生时,内核在进程控制块中设置该信号的pending标志为1,直到信号递达才设为0。
简单点说 信号产生pening 为1 信号到达后 pending设置为0等下一个信号产生
pending 在信号发出到递达之间为1 其他时候都为0
SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
简单点说 当阻塞的时候 信号无法递达 就无法执行handler 哪怕handler是忽略 阻塞的时候信号没有递达 所以pending一直为1
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可
以依次放在一个队列里。   

2.sigset_t

sigset_t是描述信号的一个类型

sigset_t(信号集)的核心实现就是位图

每一个位的序号对应信号编号(比如第 1 位对应 1 号信号 SIGHUP,第 2 位对应 2 号信号 SIGINT,…,第 31 位对应 31 号信号)。
⚠️ 注意:0 号信号是 Linux 的 “空信号”(用于检测进程是否存在),所以 sigset_t 的第 0 位通常不用,这就是为什么 32 位里只用 31 位表示信号。

sigset_t 是一个通用的 “信号位图数据类型”,不是一个具体的集合 —— 我们可以创建不同的 sigset_t 变量(实例),分别用来表示「阻塞信号集」和「未决信号集」

阻塞集:位 = 1 → 该信号被阻塞(有效 = 阻塞)
未决集:位 = 1 → 该信号未决(有效 = 未决)

3.函数接口

#include <signal.h>

1.sigemptyset

初始化空信号集


int sigemptyset(sigset_t *set);

参数:set:指向要初始化的信号集指针。
返回值:成功返回 0,失败返回 - 1(几乎不会失败,除非参数非法)。
注意:所有信号集的初始化第一步,必须先调用此函数或sigfillset。

2. sigfillset

初始化满信号集

int sigfillset(sigset_t *set);

参数:set:指向要初始化的信号集指针。
返回值:成功返回 0,失败返回 - 1(几乎不会失败,除非参数非法)。

注意:SIGKILL和SIGSTOP即使被加入信号集,也无法被阻塞(系统强制规定)。

3. sigaddset

向信号集添加信号

int sigaddset(sigset_t *set, int signum);

set:已初始化的信号集指针。
signum:要添加的信号(如SIGINT、SIGQUIT,不能是 0)。
返回值:成功返回 0,失败返回 - 1。

4. sigdelset

从信号集删除信号

int sigdelset(sigset_t *set, int signum);

set:已初始化的信号集指针。
signum:要删除的信号(如SIGINT、SIGQUIT,不能是 0)。

注意:添加前必须先初始化信号集(sigemptyset/sigfillset)。

5. sigismember

判断信号是否在信号集中

int sigismember(const sigset_t *set, int signum);

set:已初始化的信号集指针。
signum:要判断是否存在的信号(如SIGINT、SIGQUIT,不能是 0)。

返回值:
1:信号存在于信号集中;
0:信号不存在;
-1:参数错误(如信号号非法)

6.sigprocmask

进程信号屏蔽字操作

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how:修改屏蔽字的方式(三选一):
SIG_BLOCK:将set中的信号添加到当前屏蔽字(屏蔽字 = 屏蔽字 | set);
SIG_UNBLOCK:从当前屏蔽字移除set中的信号(屏蔽字 = 屏蔽字 & ~set);
SIG_SETMASK:将当前屏蔽字直接设置为set(屏蔽字 = set)。


set:要操作的信号集(NULL 表示不修改屏蔽字,仅获取当前屏蔽字)。
oldset:保存修改前的旧屏蔽字(NULL 表示不保存)。
返回值:成功返回 0,失败返回 - 1(设置errno)。


注意:
仅对单线程进程有效,线程需使用pthread_sigmask;
SIGKILL和SIGSTOP无法被阻塞,即使加入set也无效。

7.sigpending

获取未决信号集

int sigpending(sigset_t *set);

参数:set:保存未决信号集的指针。
返回值:成功返回 0,失败返回 - 1。

8.sigfillset

int sigfillset(sigset_t *set);

sigset_t *set:指向要初始化的信号集结构体的指针(sigset_t是系统定义的信号集类型,无法直接操作,必须通过信号集函数操作)。

返回0 POSIX 标准规定,该函数总是成功,不会返回错误

调用sigfillset后,传入的信号集set会被填充系统中所有的信号

此时,用sigismember判断任意信号是否在集合中,结果都是真(1)。

9.sigaction

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

为进程指定的信号(SIGKILL和SIGSTOP除外)设置新的处理方式(包括处理函数、处理时的阻塞信号集、行为规则),或获取该信号当前的处理配置,同时可保存原有配置。

signum:要处理的信号(除SIGKILL/SIGSTOP外)。
act:指向struct sigaction的指针,包含新的信号处理配置(NULL 表示不修改)。
oldact:保存旧的处理配置(NULL 表示不保存)

成功:返回0。
失败:返回-1,并设置全局变量errno标识错误原因,常见错误码:
EINVAL:signum是无效信号(如0、SIGKILL),或sa_flags包含非法标志。
EFAULT:act或oldact指向的内存地址无效(如空指针、未授权内存)。
EINTR:函数调用被其他信号中断(极少发生)。

struct sigaction {
    // 1. 普通信号处理函数(二选一:与sa_sigaction互斥)
    void (*sa_handler)(int);
    // 2. 带额外信息的信号处理函数(需配合SA_SIGINFO标志)
    void (*sa_sigaction)(int, siginfo_t *, void *);
    // 3. 处理信号时的阻塞掩码(信号集)
    sigset_t sa_mask;
    // 4. 信号处理的行为标志(位图,可组合)
    int sa_flags;
    // 5. 废弃成员(现代系统无需使用)
    void (*sa_restorer)(void);
};
1. sa_handler:普通信号处理函数
作用
指定信号的基础处理逻辑,是一个函数指针,参数为信号编号。

注意事项
sa_handler与sa_sigaction是互斥关系:若使用sa_handler,则sa_flags不需要设置SA_SIGINFO;若设置了SA_SIGINFO,内核会优先使用sa_sigaction。

2.sa_sigaction:带额外信息的信号处理函数
void (*sa_sigaction)(int signum, siginfo_t *info, void *ucontext);

核心辅助结构体:siginfo_t
siginfo_t是保存信号附加信息的关键结构体,核心成员如下(POSIX 标准):

typedef struct siginfo {
    int si_signo;    // 信号编号(与signum一致)
    int si_code;     // 信号触发的原因码(如SI_USER表示由用户进程发送,SI_KERNEL表示由内核发送)
    pid_t si_pid;    // 发送信号的进程PID(仅对进程间信号有效,如kill、sigqueue发送的信号)
    uid_t si_uid;    // 发送信号的进程的真实UID(同上)
    void *si_value;  // 信号携带的自定义数据(由sigqueue函数发送,kill函数不支持)
    // 其他成员(如si_addr:触发SIGSEGV的无效内存地址,si_status:子进程退出状态等)
} siginfo_t;

作用
定义一个信号集,当当前信号的处理函数正在执行时,这个信号集中的信号会被临时阻塞(无法被处理),直到处理函数执行完毕。

3. sa_mask:处理信号时的阻塞掩码


作用
定义一个信号集,当当前信号的处理函数正在执行时,这个信号集中的信号会被临时阻塞(无法被处理),直到处理函数执行完毕。

4. sa_flags:信号处理的行为标志


作用
设置信号处理的行为规则,是一个位图(可以通过按位或|组合多个标志)。

5. sa_restorer:废弃成员


作用
早期 UNIX 系统中用于恢复进程上下文的函数指针,现代系统中已被内核替代,无需手动设置,也不要使用(该成员在部分系统中甚至被省略)。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

// SIGINT信号的自定义处理函数
// 【打印顺序12.5(条件触发)】:仅当用户按下Ctrl+C且解除SIGINT阻塞时,才会执行此函数的打印
// 该打印会插入在“打印顺序12”和“打印顺序13”之间
void sigint_handler(int signum) {
    printf("\nHandler: Received SIGINT(%d)\n", signum);
}

// 辅助函数:打印信号集中SIGINT和SIGQUIT的状态
// 被多次调用,每次调用都会输出对应信号状态,顺序随主流程编号
void print_sigset(const sigset_t *s) {
    printf("  SIGINT: %s | SIGQUIT: %s\n", 
           sigismember(s, SIGINT) ? "YES" : "NO", 
           sigismember(s, SIGQUIT) ? "YES" : "NO");
}

int main() {
    sigset_t set, mask, pending;  // 定义信号集变量
    struct sigaction sa;          // 定义信号动作结构体

    // ====================== 打印顺序1:信号集初始化(空集) ======================
    sigemptyset(&set);            // 初始化信号集为空
    printf("After sigemptyset:\n");  // 打印提示
    print_sigset(&set);            // 打印信号集状态(SIGINT和SIGQUIT均为NO)

    // ====================== 打印顺序2:添加SIGINT到信号集 ======================
    sigaddset(&set, SIGINT);      // 向信号集添加SIGINT
    printf("\nAfter sigaddset(SIGINT):\n");  // 打印提示
    print_sigset(&set);            // 打印状态(SIGINT=YES,SIGQUIT=NO)

    // ====================== 打印顺序3:添加SIGQUIT到信号集 ======================
    sigaddset(&set, SIGQUIT);     // 向信号集添加SIGQUIT
    printf("\nAfter sigaddset(SIGQUIT):\n");  // 打印提示
    print_sigset(&set);            // 打印状态(SIGINT=YES,SIGQUIT=YES)

    // ====================== 打印顺序4:从信号集删除SIGQUIT ======================
    sigdelset(&set, SIGQUIT);     // 从信号集删除SIGQUIT
    printf("\nAfter sigdelset(SIGQUIT):\n");  // 打印提示
    print_sigset(&set);            // 打印状态(SIGINT=YES,SIGQUIT=NO)

    // ====================== 打印顺序5:将所有信号加入信号集 ======================
    sigfillset(&set);             // 把所有信号加入信号集
    printf("\nAfter sigfillset:\n");  // 打印提示
    print_sigset(&set);            // 打印状态(SIGINT=YES,SIGQUIT=YES)
    // ====================== 打印顺序6:判断SIGINT是否在信号集中 ======================
    if (sigismember(&set, SIGINT)) {  // 判断SIGINT是否在集合中(必然成立)
        printf("  sigismember: SIGINT in set\n");  // 打印提示
    }

    // ====================== 打印顺序7:设置SIGINT处理函数成功 ======================
    sa.sa_handler = sigint_handler;  // 指定自定义处理函数
    sigemptyset(&sa.sa_mask);        // 处理信号时的阻塞集为空
    sa.sa_flags = 0;                 // 无特殊标志
    if (sigaction(SIGINT, &sa, NULL) == -1) {  // 设置SIGINT的处理动作
        perror("sigaction");
        exit(1);
    }
    printf("\nsigaction: Set SIGINT handler ok\n");  // 打印设置成功提示

    // ====================== 打印顺序8:阻塞SIGINT成功 ======================
    sigemptyset(&mask);              // 初始化掩码集为空
    sigaddset(&mask, SIGINT);        // 掩码集添加SIGINT
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {  // 阻塞SIGINT
        perror("sigprocmask");
        exit(1);
    }
    printf("\nsigprocmask: Blocked SIGINT\n");  // 打印阻塞成功提示

    // ====================== 打印顺序9:提示用户按下Ctrl+C ======================
    printf("Press Ctrl+C within 5s...\n");  // 打印提示语
    sleep(5);  // 暂停5秒,等待用户操作(按或不按Ctrl+C)

    // ====================== 打印顺序10:打印未决信号集状态 ======================
    sigpending(&pending);  // 获取当前未决信号集
    printf("\nsigpending: Pending signals:\n");  // 打印提示
    print_sigset(&pending);  // 打印未决信号状态(用户按了则SIGINT=YES,否则NO)
    // ====================== 打印顺序11:提示SIGINT是否未决 ======================
    printf("  SIGINT %s pending\n", sigismember(&pending, SIGINT) ? "is" : "not");

    // ====================== 打印顺序12:解除SIGINT阻塞成功 ======================
    if (sigprocmask(SIG_UNBLOCK, &mask, NULL) == -1) {  // 解除SIGINT阻塞
        perror("sigprocmask");
        exit(1);
    }
    printf("\nsigprocmask: Unblocked SIGINT\n");  // 打印解除阻塞提示
    // 【关键触发点】若用户之前按下了Ctrl+C,此时内核会立即调用sigint_handler
    // 执行“打印顺序12.5”:输出Handler的内容,之后再执行打印顺序13
    // 若用户未按,则直接执行打印顺序13

    // ====================== 打印顺序13:解除阻塞后打印未决信号集状态 ======================
    sigpending(&pending);  // 再次获取未决信号集
    printf("\nsigpending after unblock:\n");  // 打印提示
    print_sigset(&pending);  // 打印状态(SIGINT必然为NO,因为已处理或未产生)

    return 0;
}

信号集操作阶段:依次打印空集、添加 SIGINT、添加 SIGQUIT、删除 SIGQUIT、填充集的状态,验证信号集操作的正确性。
设置信号处理函数:提示 SIGINT 处理函数设置成功。
阻塞 SIGINT 并等待:提示用户 5 秒内按 Ctrl+C。
如果用户按了 Ctrl+C:SIGINT 被阻塞,进入未决状态,sigpending会显示 SIGINT 是未决的。
如果用户没按 Ctrl+C:sigpending会显示 SIGINT 未决。
解除 SIGINT 阻塞:此时未决的 SIGINT 会被立即处理(执行sigint_handler,打印信号信息),随后再次检查未决信号集,SIGINT 已不存在。

4.信号处理

信号一般是在进程从内核态返回用户态的时候处理

进程通过sigaction()/signal()函数,向内核 “登记”:当收到SIGQUIT信号(对应键盘的Ctrl+\,默认动作是终止进程并生成核心转储文件)时,不执行内核的默认动作,而是执行进程自己写的sighandler函数(用户空间代码)。
内核会记录这个 “信号 - 处理函数” 的映射关系,后续收到该信号时,会按这个映射处理。

进程原本在用户态执行main函数的业务逻辑,触发以下场景时会切换到内核态(这是进入内核的唯一合法途径):
中断:比如用户按Ctrl+\触发键盘硬件中断、硬盘读写完成的中断;
异常:比如进程访问非法内存、除零错误;
系统调用:比如进程调用read()/sleep()等接口。

内核先处理完上述中断 / 异常(比如处理键盘中断、修复异常);
内核准备从内核态返回用户态,恢复main函数执行前,会执行一个关键操作:检查当前进程的未决信号队列(是否有已发送、未阻塞、待处理的信号,即 “信号递达”);
此时内核发现SIGQUIT信号需要处理(递达),且进程已注册了自定义处理函数,于是内核决定改变返回路径。

内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数,sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

sighandler执行完毕后,并不会直接回到main函数,而是触发一个内核预定义的特殊系统调用sigreturn:
这个系统调用是 “自动执行” 的(无需程序员手动写),作用是告诉内核:信号处理函数执行完了,请恢复原流程;
执行sigreturn的过程,就是进程从用户态再次切换到内核态的过程,内核会处理这个系统调用的逻辑。

内核处理完sigreturn系统调用后,会再次检查进程的未决信号队列:
若没有新的信号需要处理,内核会恢复之前保存的main函数的执行上下文;
进程从内核态切换回用户态,继续执行main函数中被中断的那一行代码。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,

这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。

如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需(sigaction函数) 要额外屏蔽的信号,

当信号处理函数返回时自动恢复原来的信号屏蔽字。

这段话是解释普通信号重复产生时通常只会被处理一次(接收一个) 的核心依据

普通信号

信号屏蔽导致阻塞:当第一个普通信号的处理函数被调用时,内核自动将该信号加入进程的信号屏蔽字。此时你发送的后 2 个相同普通信号,会被内核标记为 **“未决状态”**(无法递达,即阻塞)。


普通信号的未决队列是 “单值型”:内核对于普通信号的未决状态,只记录 “是否存在该信号未决”,不会记录产生的次数。也就是说,不管阻塞期间来了 1 个、2 个还是 100 个相同的普通信号,内核都只记 “有 1 个该信号未决”。


阻塞结束后的处理:当信号处理函数返回,屏蔽字恢复后,内核会处理这个 “未决的普通信号”(仅 1 次),之后未决状态被清除。原本的 2 个被阻塞的信号,其实已经被合并成了 1 个,因此只会处理 1 次,剩下的相当于 “消失了”。

实时信号

信号屏蔽导致阻塞:和普通信号一样,第一个实时信号的处理函数调用时,该信号被自动屏蔽,后 2 个相同实时信号会进入未决状态(阻塞)。
实时信号的未决队列是 “队列型”:内核对于实时信号的未决状态,会按产生顺序排队记录,保留每一个信号的实例(只要队列没满)。也就是说,阻塞期间来了 2 个实时信号,内核会在未决队列里存 2 个该信号的节点。
阻塞结束后的处理:当屏蔽字恢复后,内核会按照排队顺序,依次处理这 2 个未决的实时信号,最终 3 个信号都会被处理(第一个处理函数执行时处理,后两个阻塞后依次处理)。

5.再谈volatile

volatile是编程语言(C/C++/CUDA 等)中的类型修饰符,核心作用是禁止编译器对变量进行激进的优化,强制每次读写该变量时都直接操作内存(物理内存 / 共享内存 / 寄存器),而非缓存到 CPU/GPU 的寄存器中。

编译器优化就会直接从cpu寄存器读取 而不是从内存中读取

不需要访问内存 所以效率更高

写变量 改变的是内存的 不会改变cpu的

比如下面这个例子

flag被优化了

ctrl c的时候

handler函数会改变内存中flag的值

但是对于cpu寄存来说flag还是0

循环就无法停止

我们发送信号就无法达到我们想要的效果

要解决这个问题也很简单 给flag加上关键字volatile

#include<iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

volatile int flag = 0;

void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    flag = 1;
}

int main()
{
    signal(2, handler);
    // 在优化条件下, flag变量可能被直接优化到CPU内的寄存器中
    while(!flag); // flag 0,!flag 真
    cout << "process quit normal" << endl;
    return 0;
}

6.SIGCHLD信号

触发条件:当子进程发生以下状态变化时,内核会自动向其父进程发送 SIGCHLD 信号:
子进程终止(如调用exit、被信号杀死);
子进程被暂停(如收到SIGSTOP信号);
子进程被恢复运行(如收到SIGCONT信号)

SIGCHLD 的默认处理动作是 “忽略(SIG_IGN)”—— 即父进程若不主动处理该信号,内核不会强制父进程做任何操作。
但这会带来问题:若子进程终止后,父进程既不处理 SIGCHLD,也未调用wait/waitpid回收子进程资源,子进程会变成僵尸进程(Z 状态),残留 PCB(进程控制块)占用系统资源。

SIGCHLD 的主要价值是让父进程 “异步感知子进程的状态变化”,从而在不阻塞自身工作的前提下,回收子进程资源:
父进程可以捕获 SIGCHLD 信号,在信号处理函数中调用waitpid(推荐)或wait,主动回收已终止的子进程,避免僵尸进程产生。

前面讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进 程结束等待清理(也就是轮询的方式)

采用第一种方式,父进程阻塞了就不 能处理自己的工作了;
采用第二种方式,父 进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号 的处理函数,
这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理
函数中调用wait清理子进程即可。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigactionSIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。
系统默认的忽 略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。
此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void handler(int sig)
{
    pid_t id;
    while( (id = waitpid(-1, NULL, WNOHANG)) > 0 ){
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;

    if( (cid = fork()) == 0 ){
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }

    while(1){
        printf("father proc is doing some thing!\n");
        sleep(1);
    }

    return 0;
}

像这个代码子进程结束就没有zombie状态

下面这个代码也不会有zombie状态

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

int main()
{
    signal(17, SIG_IGN);
    pid_t cid;

    if ((cid = fork()) == 0) {
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }

    while (1) {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }

    return 0;
}

父进程显式将SIGCHLD信号(17 号信号)的处理动作设为SIG_IGN(忽略),在 Linux 系统下,内核会自动回收该父进程的子进程资源。

虽然SIGCHLD信号默认处理方式也是忽略 但是和我们signal忽略本质上是不一样的

这是两者最关键的区别:
默认忽略SIGCHLD:
父进程未主动设置SIGCHLD的处理方式,使用系统默认的 “忽略” 动作。此时子进程终止后,若父进程未调用wait/waitpid回收资源,子进程会残留为僵尸进程(Z 状态)—— 其 PCB(进程控制块)仍占用系统资源,直到父进程退出后由 init 进程回收。
(本质:默认忽略只是 “父进程不处理 SIGCHLD 信号”,但内核不会自动回收子进程资源)

显式设置SIG_IGN(Linux 系统):
父进程通过signal(SIGCHLD, SIG_IGN)或sigaction主动将SIGCHLD设为 “忽略”。

此时子进程终止后,Linux 内核会自动回收其子进程的资源,不会产生僵尸进程。

(本质:这是 Linux 的特例,显式忽略SIGCHLD会触发内核的自动回收逻辑)

 信号通知的感知
两者的父进程都不会收到SIGCHLD信号(因为处理动作是 “忽略”),但资源处理的逻辑完全不同:
默认忽略:信号被忽略,但子进程资源未被回收;
显式SIG_IGN:信号被忽略,且子进程资源被内核自动回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值