Linux 进程信号

目录

什么是信号

生活中的角度

技术中的角度

PS

信号概念

kiil -l 查看所有信号

对于信号的分析

关于信号的产生(信号产生前)

1. 通过终端按键产生信号

2. 调用系统函数向进程发信号

3. 由软件条件产生信号

4. 硬件异常产生信号

关于 core dump

信号产生中

信号相关概念

在内核中的表示

接口 sigset_t

sigset_t定义

信号集操作函数

sigprocmask

sigpending

捕捉信号(信号产生后)

内核如何实现信号的捕捉

signal 函数

sigaction 函数

关于mask

kill 函数(给任意进程发任意信号)

​raise 函数(给自己发任意信号)

abort 函数(向自己发送 SIGABRT (6号)信号) 

alarm 函数

可重入函数 

volatile 关键字

SIGCHLD 信号


C语言总结在这常见八大排序在这

作者和朋友建立的社区:非科班转码社区-CSDN社区云💖💛💙

期待hxd的支持哈🎉 🎉 🎉

最后是打鸡血环节:想多了都是问题,做多了都是答案🚀 🚀 🚀

最近作者和好友建立了一个公众号

公众号介绍:

专注于自学编程领域。由USTC、WHU、SDU等高校学生、ACM竞赛选手、CSDN万粉博主、双非上岸BAT学长原创。分享业内资讯、硬核原创资源、职业规划等,和大家一起努力、成长。(二维码在文章底部哈!

什么是信号

生活中的角度

快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
比如过红绿灯,比如收快递。
比如你买了快递,即使没来,你也知道快递来了该怎么处理。当快递员来了要你去取,但是你又有更重要的事情去做的时候,就可以不立刻去取,可以等合适时间再去,并且你知道并记住了有一个快递要去取。当你时间合适,顺利拿到快递之后,就要开始处理快递了。
而处理快递一般方式有三种
1. 执行默认动作(打开快递,使用商品)
2. 执行自定义动作(快递是零食,你要送给你的女朋友)
3. 忽略快递(快递拿上来之后,做其他事情)
进程就是你,操作系统就是快递员,信号就是快递
总结说,首先你能识别信号,其次即使信号没有产生,你也有处理信号的能力,至于处理,就是在你觉得合适的时候再去,处理有三种情况,一是默认,而是忽略,三是自定义。

技术中的角度

用户输入命令 , Shell 下启动一个前台进程。
用户按下 Ctrl - C , 这个键盘输入产生一个硬件中断,被 OS 获取,解释成信号,发送给目标前台进,前台进程因为收到信号,进而引起进程退出。

PS

1. Ctrl - C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行 , 这样 Shell 不必等待进程结束就可以接受新的命令, 启动新的进程。
2. Shell 可以同时运行一个前台进程任意多个后台进程 , 只有前台进程才能接到像 Ctrl - C 这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下 Ctrl - C 而产生一个信号 , 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止 , 所以信号相对于进程的控制流程来说是异步 (Asynchronous)的。
放到后台进程
ctl + c 就终止不了了
终止:1. kill
           2. 先 jobs 再 fg(front ground) 1 把这个后台进程放到前台 然后就可以ctl + c 了
把前台任务放到后台除了 +& 还可以 bg n

信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

kiil -l 查看所有信号

        每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2
        编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

1 - 31 属于普通信号 (分时操作系统,更强的的是公平调度)
34 - 64 属于实时信号(RT)
实时就是任务到来的时候就立马去处理(实时操作系统)(比如车载系统就需要实时操作系统)(这里信号也是一样的)
对于普通信号的特点:
1. 可以不用立马处理
2. 多相同信号来时可能只处理一个
实时信号是里面处理且不能信号丢失

对于信号的分析

因为信号产生是异步的,所以在产生信号的时候,进程可能在做更重要的事情,可以暂时不处理这个信号,但是不代表这个信号不被处理,即我们要记住这个信号。这就是存在了进程的PCB中,那么既然是在PCB中,也就是说至于OS有权利去处理PCB里面的数据,因为OS是进程的管理者,进程的所有属性的获取和设置,只能是OS来的。对于如何记住其实是PCB里面有各个信号对应的位图,OS通过写入对应的位来记录进程收到的信号。

关于信号的产生(信号产生前)

1. 通过终端按键产生信号

从硬件角度(在控制信号角度),外设是可以直接和CPU直接沟通的(之前是说的数据层面是不可以的),是通过硬件中断实现的
PS:老式计算机是CPU负责把外设的数据拷贝到内存里,现在是有一个叫DMA的硬件(芯片)负责。显卡里面也有自己的芯片叫做GPU。

2. 调用系统函数向进程发信号

signal 对特定信号进行捕捉动作

signal代码 

还有比如 在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。

        4568是 test 进程的 id 。之所以要再次回车才显示 Segmentation fault , 是因为在 4568 进程终止掉之前已经回到了Shell 提示符等待用户输入下一条命令 ,Shell 不希望 Segmentation fault 信息和用 户的输入交错在一起, 所以等用户输入命令之后才显示。
        指定发送某种信号的kill 命令可以有多种写法 , 上面的命令还可以写成 kill - SIGSEGV 4568 kill - 11 4568 , 11 是信号 SIGSEGV 的编号。以往遇 到的段错误都是由非法内存访问产生的 , 而这个程序本身没错 , 给它发SIGSEGV 也能产生段错误
kill命令是调用kill函数实现的。kill 函数可以给一个指定的进程发送指定的信号 raise 函数可以给当前进程发送指定的信号( 自己给自己发信号 )
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。

abort函数使当前进程接收到信号而异常终止

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

abort 函数(向自己发送 SIGABRT (6号)信号)

捕捉号信号并且执行特定代码

这就很有意思了,相比与 9号 信号(kill)不可以被捕捉而言, SIGABRT信号可以被捕捉,但是捕捉完了之后程序依旧会终止。

3. 由软件条件产生信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前
进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
代码演示

1s后

这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。

加上一个信号捕捉

去掉printf,顺便改一下handler要他调用就终止

结果 

发现比之前大了很多
所以说明IO非常慢!(比之前没有进行printf)

PS:操作系统就是一个死循环,是硬件推动操作系统工作(不要感觉矛盾,把操作系统比作老板,把硬件比作秘书=。=,就是秘书受老板管,但又要推动老板)
(硬件里面有时钟硬件,会每隔一小段时间就给OS(CPU)发送时钟中断,CPU就会去调用OS提供的对应中段方法,来执行操作系统的代码然后完成调度,切换,检查,内存置换等各种算法)

4. 硬件异常产生信号

即因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象。

就比如除0就会崩溃,又或者越界野指针访问。

崩溃的本质?

进程崩溃的本质就是收到了信号!

除0为例:

CPU内部,状态寄存器,当我们除0的时候,CPU内核的状态寄存器会被设置为有报错:浮点数越界,CPU内部的寄存器(硬件),OS就会识别到CPU内有报错了 -》1. 谁干的?

2. 是什么报错?(OS-》构建信号)-》向目标进程发送信号-》目标进程在合适的时候,去处理这个信号-》终止进程。

但是崩溃了进程不一定会终止,因为上面都是默认的行为,现在我们自己捕捉

捕捉了之后因为没有exit,而我们现在程序出现了问题,而我们并没,有去解决所以OS会一直帮我们发送这个信号(这就是崩溃,即不一定会终止程序) 

关于 core dump

首先解释什么是 Core Dump 。当一个进程要异常终止时 , 可以选择把进程的用户空间内存数据全部 保存到磁盘上, 文件名通常是 core, 这叫做 Core Dump 。进程异常终止通常是因为有 Bug, 比如非法内存访问导致段错误 , 事后可以用调试器检查core 文件以查清错误原因 , 这叫做 Post-mortem Debug (事后调试)。一个进程允许产生多大的core 文件取决于进程的 Resource Limit( 这个信息保存 在 PCB ) 。默认是不允许产生 core 文件的 ,因为core 文件中可能包含用户密码等敏感信息 , 不安全。在开发调试阶段可以用 ulimit 命令改变这个限制 , 允许产生core 文件。 首先用 ulimit 命令改变 Shell 进程的 Resource Limit, 允许 core 文件最大为 1024K: $ ulimit - c
1024

进程等待的 core dump(核心转储),我们之前是没有讲的,关于进程等待返回的状态码,前7位是信号的,后8位是退出状态码的,第8位就是 core dump 。

core dump 我们默认是关闭的。他的作用是:

会把进程运行中,对应的异常上下文数据,core dump 到磁盘上,方便调试。

man 7 signal(查看更详细的信号手册) 比如这些就是可以打开core dump位的

ulimit -a(默认是关闭的)

ulimit -c 100000

设置了之后就有core了
然后我们对应之前那个表发现8号信号是产生core文件的
然后我们发现多了一个这个

这个文件是说引起这个问题的进程是谁,并且此进程core cump标志位会被置1,这种机制就是核心转储(打开发现会是乱码)(core dump 默认是关的)
核心转储的本质就是 

会把进程运行中,对应的异常上下文数据,core dump 到磁盘上,方便调试。

core dump 打开之后因为异常产生的文件,makedile带上-g选项就可以之间定位到产生异常的地方

回车(直接定位问题) 

信号产生中

信号相关概念

实际执行信号的处理动作称为信号递达 (Delivery)
信号从产生到递达之间的状态 , 称为信号未决 (Pending)
进程可以选择阻塞 (Block ) 某个信号。
被阻塞的信号产生时将保持在未决状态 , 直到进程解除对此信号的阻塞 , 才执行递达的动作 .
注意 , 阻塞和忽略是不同的 , 只要信号被阻塞就不会递达 , 而忽略是在递达之后可选的一种处理动作。

在内核中的表示

每个信号都有两个标志位分别表示阻塞 (block) 和未决 (pending), 还有一个函数指针表示处理动作。信号产生时, 内核在进程控制块中设置该信号的未决标志 , 直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过 , 当它递达时执行默认处理动作。
SIGINT 信号产生过 , 但正在被阻塞 , 所以暂时不能递达。虽然它的处理动作是忽略 , 但在没有解除阻塞之前不能忽略这个信号, 因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT 信号未产生过 , 一旦产生 SIGQUIT 信号将被阻塞 , 它的处理动作是用户自定义函数 sighandler 。如果在进程解除对某信号的阻塞之前这种信号产生过多次, 将如何处理 ?POSIX.1 允许系统递送该信号一次或多次。Linux 是这样实现的 : 常规信号在递达之前产生多次只计一次, 而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

pending表让我们可以知道信号来了没有,哪个信号来了。
handler表就是对应着pending表的处理方法是一个函数指针数组(之前说的OS会修改PCB中的数据改的就是pending表)(之前用的signal函数就是修改的handler表的内容)
block是阻塞信号集,ture就是不处理该信号,也是位图和pending的一样

接口 sigset_t

从上图来看 , 每个信号只有一个 bit 的未决标志 , 0 1, 不记录该信号产生了多少次 , 阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型 sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号的“ 有效 无效 状态 , 在阻塞信号集中 有效 无效 的含义是该信号是否被阻塞 , 而在未决信号集中 有效” 无效 的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的屏蔽 应该理解为阻塞而不是忽略

sigset_t定义

信号集操作函数

sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做任何解释, 比如用 printf 直接打印 sigset_t 变量是没有意义的
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

使用直接 man 查询详细信息

函数 sigemptyset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 清零 , 表示该信号集不包含任何有效信号。
函数 sigfifillset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 置位 , 表示 该信号集的有效信号包括系统支持的所有信号。
注意 , 在使用 sigset_ t 类型的变量之前 , 一定要调 用 sigemptyset sigfifillset 做初始化 , 使信号集处于确定的状态。初始化sigset_t 变量之后就可以在调用 sigaddset sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 -1 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种 信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1

sigprocmask

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集 )(block表)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1
第三个参数是返回以前的信号屏蔽字(为了想恢复)(输出型参数,不用设置为nullptr)
如果 oset 是非空指针 , 则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针 , 则 更改进程的信号屏蔽字, 参数 how 指示如何更改。如果 oset set 都是非空指针 , 则先将原来的信号 屏蔽字备份到 oset , 然后根据set how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask, 下表说明了how参数的可选值。
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞 , 则在 sigprocmask 返回前 , 至少将其中一个信号递达。

sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程
序如下:

sigpengding获取信号的pending信号集(课件有点问题)(也是输出型参数)

                使用(去gitee上copy完善的11111位置)

程序运行时 , 每秒钟把各信号的未决状态打印一遍 , 由于我们阻塞了 SIGINT 信号 , Ctrl-C 将会 使 SIGINT 信号处于未决状态, Ctrl-\ 仍然可以终止程序 , 因为 SIGQUIT 信号没有阻塞。

捕捉信号(信号产生后)

内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数 , 在信号递达时就调用这个函数 , 这称为捕捉信号。由于信号处理函数的代码是在用户空间的, 处理过程比较复杂 , 举例如下 : 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。 当前正在执行main函数 , 这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复 main 函数的上下文继续执行 , 而是执行 sighandler 函 数 ,sighandler 和main 函数使用不同的堆栈空间 , 它们之间不存在调用和被调用的关系 , 是 两个独立的控制流程。 sighandler 函数返回后自动执行特殊的系统调用sigreturn 再次进入内核态。 如果没有新的信号要递达 , 这次再返回用户态就是恢复main函数的上下文继续执行了。

用几幅图理解

图一

图二 

signal 函数

signal 对特定信号进行捕捉动作

sigaction 函数

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction 修改hander表
sigaction 结构体目前只考虑 handler和mask
sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0, 出错则返回 - 1 signo 是指定信号的编号。若act 指针非空 , 则根据 act 修改该信号的处理动作。若 oact 指针非 空 , 则通过 oact 传出该信号原来的处理动作。act oact 指向 sigaction 结构体。
sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号 , 赋值为常数 SIG_DFL 表示执行系统默认动作, 赋值为一个函数指针表示用自定义函数捕捉信号 , 或者说向内核注册了一个信号处理函 数 , 该函数返回值为void, 可以带一个 int 参数 , 通过参数可以得知当前信号的编号 , 这样就可以用同一个函数处理多种信 号。显然, 这也是一个回调函数 , 不是被 main 函数调用 , 而是被系统所调用。

这样就可以一下把 2 3号都拦住

sigaction的更大的意义就在于防止操作系统嵌套式的去执行同一个信号处理

关于mask

当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flflags 字段包含一些选项 , 本章的代码都
sa_flflags 设为 0,sa_sigaction 是实时信号的处理函数。

就是说不允许我们的普通信号被重复提交

PS:
删掉一个进程 可以 kill -9 +pid 
killall + 进程名也可以

kill 函数(给任意进程发任意信号)

 raise 函数(给自己发任意信号)

abort 函数(向自己发送 SIGABRT (6号)信号) 

这就很有意思了,相比与 9号 信号不可以被捕捉而言, SIGABRT信号可以被捕捉,但是捕捉完了之后程序依旧会终止 

alarm 函数

1s后

 

(进程收到信号默认是结束进程的,所以进程跑1s就结束了)
加上一个信号捕捉

顺便改一下handler要他调用就终止结果 

发现比之前大了很多
所以说明IO非常慢!(比之前没有进行printf) 

PS

操作系统就是一个死循环,是硬件推动操作系统工作(不要感觉矛盾,把操作系统比作老板,把硬件比作秘书=。=,就是秘书受老板管,但又要推动老板)
(硬件里面有时钟硬件,会每隔一小段时间就给OS(CPU)发送时钟中断,CPU就会去调用OS提供的对应中段方法,来执行操作系统的代码然后完成调度,切换,检查,内存置换等各种算法)

可重入函数 

main 函数调用 insert 函数向一个链表 head 中插入节点 node1, 插入操作分为两步 , 刚做完第一步的 时候 , 因为硬件中断使进程切换到内核, 再次回用户态之前检查到有信号待处理 , 于是切换 到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2, 插入操作的 两步都做完之后从sighandler返回内核态 , 再次回到用户态就从 main 函数调用的 insert 函数中继续 往下执行 , 先前做第一步之后被打断, 现在继续做完第二步。结果是 ,main 函数和 sighandler 先后 向链表中插入两个节点 , 而最后只有一个节点真正插入链表中了。
像上例这样 ,insert 函数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数 , 这称为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为 不可重入函数 , 反之 ,如果一个函数只访问自己的局部变量或参数, 则称为可重入 (Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的 :
调用了 malloc free, 因为 malloc 也是用全局链表来管理堆的。
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。

volatile 关键字

因为一直使CPU高速计算而且发现没有改变,所以就可能优化,直接从寄存器里面拿值,但是当我们发送信号后改变的是内存的值,这就可能会出问题

volatile -- 保持内存的可见性(强制编译器访问内存数据)

SIGCHLD 信号

讲过用 wait waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻 塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不 能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
其实 , 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。
事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调 用 sigaction SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不 会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略 通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可 用。请编写程序验证这样做不会产生僵尸进程。
(虽然都是忽略(不调用和调用这个函数),但是他毕竟是个函数,还会进程后续处理,不会让子进程僵尸,会让子进程退出,并且不会给父进程发送信号)

直接就自动回收了 

经过测试发现(验证了)暂停和终止都会给父进程发这个信号
而且解除暂停也会 

先捕捉子进程退出后的信号,然后执行捕捉的函数,不去影响父进程

但是上面有问题

有多个子进程的话上面只会执行一次(因为同时退出的时候发送的都是同一个信号,所以OS会把blocks表置为1(收到一个信号就会把那个信号对应的block置1),pengding表置为1,只会执行一次)
改法,循环去waitpid 

当没有需要等待的时候就等待失败就退出了
但是还有问题,比如 

但是我们发现前几个进程退出之后,虽然后3个子进程还在跑,但是父进程不跑了,是因为后面的子进程没有退出的话父进程是阻塞等待,改成WNOHANG就可以了。 

改一改代码 

最后的最后,创作不易,希望读者三连支持💖

赠人玫瑰,手有余香💖

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来45

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值