信号

引言

信号是一种软件中断,它提供了异步事件的处理机制。这些异步事件可以来自系统外部(比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序),也可以来自系统内部(进程包含除以0的误操作)。

信号作为一种进程间通信的基本形式,而一个进程可以给另一个进程发送信号。

事件的发生是异步的,而程序对信号的处理也是异步的。信号在内核注册,收到信号时,内核从程序的其他部分异步调用信号处理函数。

一、信号的相关概念

根据进程的请求,内核会执行以下三种操作之一:

  • 忽略信号
    不采取任何操作。但是有两种信号不能被忽略:SIGKILL(kill -9)和SIGSTOP。这样做的原因是系统管理员需要能杀死或停止进程,如果进程能够选择忽略SIGKILL(使进程不能被杀死)或SIGSTOP(使进程不能被停止)将破坏这一权利。
  • 捕获并处理信号
    内核会暂停该进程正在执行的代码,并跳转到先前注册过的函数。接下来进程会执行这个函数。一旦进程从该函数返回,它会跳回到捕获信号的地方继续执行。
    经常捕获的两个信号是SIGINT和SIGTERM(kill)。
    SIGINT信号:程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl-C)时产生。例如终端能捕获该信号并返回到主提示符。
    SIGTERM信号:进程终止信号(kill),进程捕获到SIGTERM以便在结束前执行必要的清理工作,例如断开网络,或删除临时文件。
    两种信号不能被捕获:SIGKILL和SIGSTOP
  • 执行信号的默认操作
    对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。

1.1、信号标识符

每个信号都以SIG为前缀。例如SIGINT是用户按下Ctrl-C发出的信号,SIGABRT是进程调用abort()函数时产生的信号,而SIGKILL是进程被强制终止时产生的信号。

这些信号都是在头文件#include<signal.h>中定义的。
信号被预处理程序简单定义为正整数,信号从名称到整数的映射是依赖于具体实现的,并且在不同的UNIX系统中也不同,但是最开始的12个左右的信号通常是以同样的方式映射的。一个好的程序员应该总是使用信号的可读名称,而不使用它们的证书值。

可以使用kill -l命令查看系统支持的信号列表

[root@loaclhost home]# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

SIGQUIT(ctrl+\):内核会给前台进程组的进程发送该信号。默认的操作是终止进程并进行内存转储。

1.2、基本信号管理接口signal()函数

最简单古老的信号管理接口是signal()函数。该函数由ISO C89标准定义,其中只定义了信号支持的最少的共同特征,该系统调用是非常基本的。

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。
对于sighandler_t signal(int signum, sighandler_t handler)函数来说:signum 显然是信号的编号,handler 是中断函数的指针。
同样,typedef void (*sighandler_t)(int)中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。我们先来看看简单一个信号注册的代码示例吧。

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

//typedef void (*sighandler_t)(int);
void handler(int signum)
{
    if(signum == SIGIO)
        printf("SIGIO   signal: %d\n", signum);
    else if(signum == SIGUSR1)
        printf("SIGUSR1   signal: %d\n", signum);
    else
        printf("error\n");
}

int main(void)
{
    //sighandler_t signal(int signum, sighandler_t handler);
    signal(SIGIO, handler);
    signal(SIGUSR1, handler);
    printf("%d  %d\n", SIGIO, SIGUSR1);
    for(;;)
    {
        sleep(10000);
    }
    return 0;
}

我们先使用 kill 命令发送信号给之前所写的程序,结果取下:

[root@localhost home]# ./signal &
[1] 29983
[root@localhost home]# 29  10

[root@localhost home]# ps -ef |grep signal
root     29983 29923  0 14:54 pts/2    00:00:00 ./signal
root     29985 29923  0 14:54 pts/2    00:00:00 grep --color=auto signal
[root@localhost home]#
[root@localhost home]# kill -10 29983
SIGUSR1   signal: 10
[root@localhost home]# kill -29 29983
SIGIO   signal: 29
[root@localhost home]# kill -SIGIO 29983
SIGIO   signal: 29
[root@localhost home]# kill -SIGUSR1 29983
SIGUSR1   signal: 10
[root@localhost home]# kill -SIGUSR2 29983
[1]+  User defined signal 2   ./signal

简单的总结一下,我们通过 signal 函数注册一个信号处理函数,分别注册了两个信号(SIGIO 和 SIGUSER1);
随后主程序就一直“长眠”了;
通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps 命令查看了之前所写的程序的 PID,通过 kill 函数来发送。
对于已注册的信号,使用 kill 发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。

那么,已经可以设置信号处理函数了,信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为 SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可。

signal(SIGINT, SIG_IGN);
signal(SIGINT, SIG_DFL);

在此还有两个问题需要说明一下:

  • 当执行一个程序时,所有信号的状态都是系统默认或者忽略状态的。除非是 调用exec进程忽略了某些信号。exec 函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不会改变 。
  • 当一个进程调动了 fork 函数,那么子进程会继承父进程的信号处理方式。

等待信号

出于调试和延时代码的目的,POSIX定义了pause()系统调用,它可以使进程睡眠,直到进程接收到处理或终止进程的信号:

#include<unistd.h>
int pause(void);
  • 作用:使调用进程(线程)进入休眠状态(就是挂起);直到接收到信号且信号函数成功返回 pause函数才会返回
  • 返回值:始终返回-1,并将errno设置为EINTR。如果内核发出的信号被忽略,进程不会被唤醒。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>
// 实验说明: 执行程序的过程中按CTL + C不能是程序退出, 而是执行我们安装的信号处理函数

void sig_ctlc(int sig)		// sighandler_t
{
	printf("sig = %d: sig_ctlc func\n", sig);
	getchar();
	printf("sig_ctlc return\n");
}
int main(int argc, char **argv)
{
	// 安装信号 
	if(SIG_ERR == signal(SIGINT, sig_ctlc))
		perror("SIGINT install err\n");

	// signal(SIGINT, SIG_IGN);	// 忽略SIGINT信号
	// signal(SIGINT, SIG_DFL);	// 对于SIGINT信号,使用默认处理函数

	printf("pause before\n");
	// 1.当我们没有发送信号时pause会阻塞
	// 2.当进程接收到到信号时不会立刻返回, 只有当信号处理函数返回时, pause才会返回-1.
	pause();
	printf("pause after\n");
	return 0;
}
执行效果:
	book@gui_hua_shu:/work/nfs_root/qt_fs_new/2system_pro/sig$ ./a.out
	pause before
	^Csig = 2: sig_ctlc func	// 发送了ctl +c 信号
	c							// 输入c\n
	sig_ctlc return
	pause after

1.3、信号发送函数

kill 的函数原型:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

正如之前所说的,信号的处理需要有接受者,显然发送者必须要知道发给谁,根据 kill 函数的远行可以看到,pid 就是接受者的 pid,sig 则是发送的信号的类型。

关于 kill 函数,还有一点需要额外说明,上面的程序限定了 pid 必须为大于0的正整数,其实 kill 函数传入的 pid 可以是小于等于0的整数。

  • pid>0:给pid进程发送sig信号;
  • pid=0:将会把信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限想这些进程发送信号。
  • pid=-1:将该信号发送给发送进程的有权限向他发送信号的所有进程。
  • pid<-1:会给进程组pid发送sig信号;

成功返回0,失败返回-1并将errno设置为下列值之一:

  • EINVAL:由sig指定的信号无效
  • EPERM:调用进程没有权限想指定的进程发送信号
  • ESRCH:pid进程不存在,或进程使僵尸进程。

权限

有CALL_KILL权限的进程(通常是根用户的进程)可以给任何进程发送信号;
如果没有这种权限,用户只能给自己持有的进程发送信号。

重入

进程捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如信号处理程序没有调用exit或longjmp),则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程在何处执行。如果程序正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,则可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一张链接表,而插入执行信号处理程序时,进程可能正在更改此链接表。又例如,若进程正在执行getpwnam这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数,则返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖。
在这里插入图片描述
没有列入表10-3中的大多数函数是不可重入的,其原因为:
(a)已知它们使用静态数据结构;
(b)它们调用malloc或free;
(c)它们是标准I/O函数。

应当了解,即使信号处理程序调用的是列于表10-3中的函数,但是由于每个线程只有一个errno变量,所以信号处理程序可能会修改其原先值。因此,作为一个通用的规则,当在信号处理程序中调用表10-3中列出的函数时,应当在其前保存、在其后恢复errno。

若在信号处理程序中调用一个不可重入函数,则其结果是不可预测的。

二、信号集

2.1、阻塞信号

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
    在这里插入图片描述

1:PCB进程控制块中函数有信号屏蔽状态字(block)信号未决状态字(pending)还有是否忽略标志(或是信号处理函数);block状态字、pending状态字 64bit;

2:信号屏蔽状态字(block),1代表阻塞、0代表不阻塞;信号未决状态字(pending)的1代表未决,0代表信号可以抵达了;它们都是每一个bit代表一个信号,比如,bit0代表信号SIGHUP;

3:比如向进程发送SIGINT,内核首先判断信号屏蔽状态字是否阻塞,如果该信号被设为为了阻塞的,那么信号未决状态字(pending)相应位制成1;若该信号阻塞解除,信号未决状态字(pending)相应位制成0;表示信号此时可以抵达了,也就是可以接收该信号了。

4:屏蔽状态字用户可以读写,未决状态字用户只能读;这是信号设计机制。

注意:

阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作!

对于信号来说,信号编号小于等于31(1 ~ 31)的信号允许在递达之前丢失,之后的信号(34 ~ 64)为实时信号,不允许丢失。

2.2、信号集函数

Linux使用数据结构sigset_t来表示一组信号。其定义如下:

typedef struct {
unsigned long sig[_NSIG_WORDS]} sigset_t

由此可见,实际上是一个长整形数组,数组的每一个元素的每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。

linux提供如下一组函数来设置、修改、查询、删除信号集:

#include <signal.h> 
 int sigemptyset(sigset_t *set);//初始化当前的信号集,并将所有信号排除在外 
 int sigfillset(sigset_t *set);//将所有信号的信号集设置为满 
 int sigaddset(sigset_t *set, int signum);//往信号集中增添信号 
 int sigdelset(sigset_t *set, int signum);//从信号集中删除某个信号
 int sigismember(const sigset_t *set, int signum);//检测信号是否在信号集中 
  // sigemptyset(), sigfillset(), sigaddset(), and sigdelset()这些函数均是成功返回0,失败返回-1 
  //sigismember()是一个布尔函数,如果某种信号在信号集中,则返回1,没在信号集中返回0,出错返回-1 

2.3、进程信号掩码

对于信号集分配好内存空间,需要使用初始化函数来初始化。初始化完成后,可以在该集合中添加、删除特定的信号。

如下函数用于设置或查看进程的信号掩码:

#include <signal.h> 
int sigprocmask(int how, const sigset_t * _set, sigset_t * _oldset); 

_set参数指定新的信号掩码
_oldset参数输出原来的信号掩码
如果_set不为NULL,则how参数指定设置进程信号掩码的方式

成功返回0,失败返回-1

参数how的可选值如下所示:
在这里插入图片描述

2.4、被挂起的信号

设置进程信号掩码后,被挂起的信号将不能被进程接收。如果进程发送一个被屏蔽的信号,则操作系统将信号设为一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则他能立即被进程接收到。
如下函数能获取当前被挂起的信号集:

#include <signal.h> 
int sigpending(sigset_t *set); //读取当前进程的未觉信号集 
//成功返回0,失败返回-1 

2.5、设置信号阻塞、未达的步骤:

  • 分配内存空间sigset sigset bset;
  • 置空sigemptyset(&bset);
  • 添加信号sigaddset(&bset, SIGINT);
  • 添加其他需要管理的信号…
  • 设置信号集中的信号处理方案(此处为解除阻塞)sigprocmask(SIG_UNBLOCK, &bset, NULL);

2.6、测试实例

  1 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 void printsigset(sigset_t *sig)
  5 {
  6     int i;
  7     for(i=0;i<32;++i)
  8     {
  9         if(sigismember(sig,i))
 10         {
 11             printf("1");
 12         }
 13         else
 14         {
 15             printf("0");
 16         }
 17     }
 18     printf("\n");
 19 }
 20 int main()
 21 {
 22     sigset_t s,p;
 23     sigemptyset(&s);
 24     sigemptyset(&p);
 25 
 26     sigaddset(&s,SIGTSTP);
 27     sigprocmask(SIG_BLOCK,&s,NULL);
 28     while(1)
 29     {
 30         sigpending(&p);
 31         printsigset(&p);
 32         sleep(1);
 33     }
 34     return 0;
 35 }

分析如下:
在这里插入图片描述
结果如下:
在这里插入图片描述
说明:
程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGTSTP信号,按Ctrl-Z将会 使SIGTSTP信号处于未决状态,按Ctrl-C仍然可以终止程序,因SIGINT信号没有阻塞。

三、高级信号管理

3.1、sigaction()

上文中signal()函数是非常基础的,它是c标准库的一部分,因此我们必须对它运行的操作系统能力做最小的假设,它只提供了最低限度的信号管理标准。作为另一种选择,POSIX 定义了sigaction()系统调用,它提供了更强大的信号管理能力。除此以外,当信号处理程序运行时,可以用它来阻塞特定信号的接收,也可以用它来获取信号发送时各种操作系统和进程状态信息:

#include <signal.h>
int sigaction(int signum, 
				const struct sigaction *act, 
				struct sigaction *oldact);

struct sigaction {
   void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一

这个函数的原版帮助信息,可以通过man sigaction来查看。

sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中:

  • 第一个参数signum应该就是注册的信号的编号;
  • 第二个参数act如果不为空说明需要对该信号有新的配置;
  • 第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

在这里额外说一下struct sigaction结构体中的 sa_mask 成员,设置在其的信号集中的信号,会在捕捉函数调用前设置为阻塞,并在捕捉函数返回时恢复默认原有设置。这样的目的是,在调用信号处理函数时,就可以阻塞默写信号了。在信号处理函数被调用时,操作系统会建立新的信号阻塞字,包括正在被递送的信号。因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。

sigaction 的时效性:
当对某一个信号设置了指定的动作的时候,那么,直到再次显式调用 sigaction并改变动作之前都会一直有效。

关于结构体中的 flag 属性的详细配置,在此不做详细的说明了,只说明其中一点。如果设置为 SA_SIGINFO 属性时,说明了信号处理程序带有附加信息,也就是会调用 sa_sigaction 这个函数指针所指向的信号处理函数。否则,系统会默认使用 sa_handler 所指向的信号处理函数。在此,还要特别说明一下,sa_sigactionsa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

关于void (*sa_sigaction)(int, siginfo_t *, void *);处理函数来说还需要有一些说明。void* 是接收到信号所携带的额外数据;而struct siginfo这个结构体主要适用于记录接收信号的一些相关信息。

 siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

其中的成员很多,si_signo 和 si_code 是必须实现的两个成员。可以通过这个结构体获取到信号的相关信息。
关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时,在si_int或者si_ptr成员中也保存了对应的数据。

那么,kill 函数发送的信号是无法携带数据的,我们现在还无法验证发送收的部分,那么,我们先来看看发送信号的高级用法后,我们再来看看如何通过信号来携带数据。

3.2、信号发送函数——高级版

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
 };

使用这个函数之前,必须要有几个操作需要完成

  • 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
  • sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。

sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。

sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。

但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。

接收端:

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

//void (*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum, siginfo_t * info, void * context)
{
    if(signum == SIGIO)
        printf("SIGIO   signal: %d\n", signum);
    else if(signum == SIGUSR1)
        printf("SIGUSR1   signal: %d\n", signum);
    else
        printf("error\n");
    
    if(context)
    {
        printf("content: %d\n", info->si_int);
        printf("content: %d\n", info->si_value.sival_int);
    }
}

int main(void)
{
    //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    struct sigaction act;
    
    /*
     struct sigaction {
     void     (*sa_handler)(int);
     void     (*sa_sigaction)(int, siginfo_t *, void *);
     sigset_t   sa_mask;
     int        sa_flags;
     };
     */
    act.sa_sigaction = handler;
    act.sa_flags = SA_SIGINFO;
    
    sigaction(SIGIO, &act, NULL);
    sigaction(SIGUSR1, &act, NULL);
    for(;;)
    {
        sleep(10000);
    }
    return 0;
}

发送端:

#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <unistd.h>


int main(int argc, char** argv)
{
    if(4 != argc)
    {
        printf("[Arguments ERROR!]\n");
        printf("\tUsage:\n");
        printf("\t\t%s <Target_PID> <Signal_Number> <content>\n", argv[0]);
        return -1;
    }
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);

    if(pid > 0 && sig > 0)
    {
        //int sigqueue(pid_t pid, int sig, const union sigval value);
        union sigval val;
        val.sival_int = atoi(argv[3]);
        printf("send: %d\n", atoi(argv[3]));
        sigqueue(pid, sig, val);
    }
    else
    {
        printf("Target_PID or Signal_Number MUST bigger than 0!\n");
    }
    
    return 0;
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值