linux系统调用进程2信号

1. 信号理论基础

信号共性:
      简单、不能携带大量信息、满足条件才发送
信号的特质:
      信号是软件层面上的"中断"。一旦型号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,在继续执行后续指令。
      所有信号的产生以及处理全部都是有【内核】完成的
      
信号产生
 1. 按键产生,ctrl+z、ctrl+c 、 ctrl+\
 2. 系统调用产生, 如果kill 系统  系统调用函数 kill raise  abort    ,  定时器 alarm settimer
 3. 软件条件产生, sleep
 4. 硬件异常产生, 段错误、 浮点类错误、 总线错误 、SIGPIPE
 段错误: 1. 访问了不是自己内存, 比如malloc自己申请了区域,访问malloc区域外了
          2. 对只读区进行修改 char* ch="abc"  ch[0]='a'
 5.命令产生,比如kill命令  
 

信号状态:

    产生:
    未决: 产生与递达之间状态,信号被阻塞了
    递达: 产生送到内核,直接被内核处理掉

信号处理方式:  执行默认动作 、忽略  、 捕捉(自定义)


信号内核处理原理:
    linux内核进程控制块pcb是一个结构体,task_struct,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,
    还包含信号相关信息,阻塞信号和未决信号
    
    例子:
    比如ctrl+c发送2号信号,那么未决信号集把第二位设置为1,那么内核发现第二未变成1了,处理2号信号,处理[默认、忽略、捕捉(自定义)]完毕第二位变成0

未决信号集把第二位设置为1


    完毕第二位变成0


   如果由于某种原因,把2号信号设置屏蔽,那么信号屏蔽字中2号变成1,那么信号不能递到内核,那么信号不能处理
   等到屏蔽字中2号变成0,那么信号才可以被处理,未决信号集才把第二位设置为0

   信号阻塞

2. 常用信号集

    信号4要素:
    信号编号、名称、 对应事件、默认处理动作 

默认动作: 终止、忽略、终止+产生core + 暂停、继续

    man 7 signal
      Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard
       SIGQUIT       3       Core    Quit from keyboard
       SIGILL        4       Core    Illegal Instruction
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating point exception
       SIGKILL       9       Term    Kill signal
       SIGSEGV      11       Core    Invalid memory reference
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
                                     readers
       SIGALRM      14       Term    Timer signal from alarm(2)
       SIGTERM      15       Term    Termination signal
       SIGUSR1   30,10,16    Term    User-defined signal 1
       SIGUSR2   31,12,17    Term    User-defined signal 2
       SIGCHLD   20,17,18    Ign     Child stopped or terminated
       SIGCONT   19,18,25    Cont    Continue if stopped
       SIGSTOP   17,19,23    Stop    Stop process
       SIGTSTP   18,20,24    Stop    Stop typed at terminal
       SIGTTIN   21,21,26    Stop    Terminal input for background process
       SIGTTOU   22,22,27    Stop    Terminal output for background process

       SIGUSR1   30,10,16    Term    User-defined signal 1 不同操作系统对应value不一样,名称一样

1    SIGHUP:  当用户退出shell时,由该shell启动所有进程将接收这个信号,默认动作为停止进程
2    SIGINT:  相当于ctrl+c(终止/中断), 默认是终止进程
3    SIGQUIT: 相当于ctrl+\(退出), 默认是终止进程
6    SIGBUS:  总线错误,非法访问内存地址,默认终止进程产生core文件[用于gdb调试能够找到错误所在行,那么因为程序在终止的时候把错误信息写入了core文件]
8    SIGFPE:  发生致命运算错误时发生,  包括溢出,除数为0,默认终止进程产生core文件
10    SIGUSR1: 用户定义信号, 程序员可以在程序中定义并且使用该信号, 默认动作是终止进程
12    SIGUSR2: 用户定义信号, 程序员可以在程序中定义并且使用该信号, 默认动作是终止进程
*****    
9  SIGKILL: 无条件终止进程,   不能被忽略、阻塞,上面的ctrl+c信号,如果把该信号捕捉掉,那么进程无法杀死,如果是病毒,完了
19 SIGSTOP:无条件停止

11  SIGSEGV:  进程在进程无效内存访问,默认终止进程产生core文件
13    SIGPIPE:  向一个没有读端的管道写数据,默认终止进程
14  SIGALRM:   定时器超时,超时时间 有系统调用alarm 设置,默认动作终止进程
17  SIGCHLD:  子进程终止时候、子进程接收到SIGSTOP信号停止时、子进程处停止态,接收到SIGCONT后唤醒时,告诉父进程,父进程可以去收尸了,默认忽略
20  SIGTSTP:   ctrl+z. (暂停/停止)

3.  linux 信号api函数 

3.1. alarm 函数、 setitimer 函数

3.1.1 alarm函数   功能: 5秒以后收到信号,进程终止

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>

//alarm
int main00001(){

   alarm(5);  // 5s后发送SIGALRM 信号,退出程序
   sleep(1);
   int i=alarm(2);  //返回4,返回上次剩余时间
   printf("---%d---\n",i);
 //  alarm(0);    // 取消闹钟
   int j=0;
   while(1){   // 5秒以后收到信号,进程终止
	   printf("%s---%d----\n","hello....",j++);
   }
   /**
    * time ./single.o
    * 程序时间分析:
    * real	0m3.014s
      user	0m0.024s
      sys	0m1.171s
    程序运行时间= 系统时间+ 用户时间 + 等待时间
    还有1s中是等待时间, 等待cpu、内存、终端输出,终端被占用
     time ./single.o > out : 可以写如文件的数据是上面打印到终端的20倍
     cat out | wc -l
    */
	return 0;
}

学习一下关于程序运行时间分析:

上面案例:  1s中内    printf("%s---%d----\n","hello....",j++); 是   用time看,可以看出来,

系统时间+用户+系统调用 时间浪费了很多时间, 干活时间没有多少

 time ./single.o > out : 可以写如文件的数据是上面打印到终端的20倍 

全缓冲速度快一些

printf: 行缓冲 ,

3.1.2 setitimer 函数, alarm只是简单定时器,setitimer 可以设置间隔轮巡

void myfunc(){
	printf("hello world\n");
}
/**
 * setitimer 函数
 */
int main00005(){

	struct itimerval it,oldit;
	signal(SIGALRM,myfunc);
	// 2s 到了发信号
	it.it_value.tv_sec=2;
	it.it_value.tv_usec=0;
    // 没隔 5s发信号, tv_sec是秒数  tv_usec是微秒   秒+微 :时间
	it.it_interval.tv_sec=5;
	it.it_interval.tv_usec=0;
// ITIMER_REAL 自然时间 
// ITIMER_VIRTUAL:  计算进程执行时间
// ITIMER_PROF :   进程执行时间 + 调度时间
// 三种参数对应信号不一样 
	if(setitimer(ITIMER_REAL,&it,&oldit) ==-1){
		 perror("setitimer error");
		 exit(-1);
	}
	while(1){};
	return 0;
}

3.2. kill 命令、函数

/***
 * kill 命令 发送信号(不仅仅用于杀死) kill -9 pid
 * kill 函数
 */
int main00002(){
   //int kill(pid_t pid, int sig);
	// pid > 0 : 杀死指定进程
	//     = 0 : 杀死调用kill函数那个进程同时处于同一个进程组的所有进程
	//     <-1:  区绝对值,杀死该绝对值所对应的进程组合
	//     -1 : 发送信号给有权限发送的所有进程,root不可以
	__pid_t pid=fork();
	if(pid==-1){
	   perror("fork fail");
	   exit(-1);
	}else if(pid>1){
		// father
        while(1){}
	}else if(pid==0){
		// child
		sleep(2);
		// 子进程杀死父进程
		kill(getppid(),SIGKILL);
	}
	return 0;
}

  <-1:  区绝对值,杀死该绝对值所对应的进程组合

//  <-1:  区绝对值,杀死该绝对值所对应的进程组合
int main00003(){
	__pid_t pid=fork();
	if(pid==-1){
	   perror("fork fail");
	   exit(-1);
	}else if(pid>1){
		// father
        while(1){}
	}else if(pid==0){
		// child
		while(1){}
		// 子进程杀死父进程
		//kill(getppid(),SIGKILL);
	}
	return 0;
}

 ps ajx|grep hello

 kill -9 -7275

= 0 : 杀死调用kill函数那个进程同时处于同一个进程组的进程

int main00004(){
	__pid_t pid=fork();
	if(pid==-1){
	   perror("fork fail");
	   exit(-1);
	}else if(pid>1){
		// father
        while(1){}
	}else if(pid==0){
		// child
		sleep(10);
		// 杀死调用kill函数那个进程同时处于同一个进程组的进程
		kill(0,SIGKILL);
	}
	return 0;
}

案例: 杀死其他 子进程

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>

  
int main(int argc,char* argv[]) {
 int i= 0;
 pid_t pid3 , pid;
 for(i=0;i<5;i++){
  pid = fork();
  if( pid ==0){
    break;
  }
  if(i==2){
    pid3= pid;
  }
 }

 if(i<5){
    while(1){
      printf("I am child,pid=%d,ppid=%d\n",getpid(),getppid());
      sleep(3);
    }
 }else if(i ==5){
      printf("I am parent,pid=%d,i will kill pid3=%d\n",getpid(),pid3);
      sleep(5);
      kill(pid3,SIGKILL);
      while(1){  //父进程不能死
        sleep(1);
      }
 }

  return 0;
}

  raise(SIGKILL);  // 自杀 , 自己给自己发送SIGKILL 信号

int main(int argc,char* argv[]) {
  printf("i wll die\n" );
  sleep(2);
  raise(SIGKILL);  // 自杀 , 自己给自己发送SIGKILL 信号
  return 0;
}

3.3信号阻塞

    信号集操作函数:
      sigset_t  set: 自定义信号集
      sigemptyset(sigset_t* set):   清空信号集
      sigefillset(sigset_t* set):     全部置1
      sigaddset(sigset_t* set,int signum):  将一个信号添加到集合中
      sigdelset(sigset_t* set,int signum):  将一个信号从集合中移除
      sigismember(sigset_t* set,int signum):   判断一个信号是否存在集合中  1在  0不在
      
    设置阻塞信号屏蔽字和解除屏蔽: 
      int sigprocmask(int how,const sigset_t* set,sigset_t* oldset)
      how: SIG_BLOCK       设置阻塞, mask = mask|set
           SIG_UNBLOCK     取消阻塞, mask = mask & ~set
           SIG_SETMASK     用自定义set替换mask,全部会覆盖
     set:  自定义set
      oldset:  就有的mask
     查看未决信号集:
     int sigpending(sigset_t *set);
     set:传出的 未决信号集

过程分析:

set 部分自定信号集

 自定义功能集 & 阻塞信号  阻塞信号集第二位变成1

 按ctrl+c   未决信号机  位2 变 成1 ,信号阻塞,无法处理 ,ctrl+c无效

阻塞信号取消阻塞信号 

void print_set(sigset_t * set){
	int i;
    for(i=1;i<32;i++){
    	if(sigismember(set,i)){   // 判断一个信号是否存在集合中  1在  0不在
           putchar('1');
    	}else{
    	   putchar('0');
    	}
    }
    printf("\n-------------------------------------- \n");
}
 /***
  *  设置阻塞信号,进程屏蔽信号,发送信号无效
  */
int main00006(){
   sigset_t set,pedset;    // 自定义信号集
   sigemptyset(&set);   // 清空信号集
   // 2	SIGINT:  相当于ctrl+c(终止/中断), 默认是终止进程
   sigaddset(&set,SIGINT);  // SIGINT 设置为1
   sigaddset(&set,SIGQUIT);  // SIGQUIT 阻塞 ctrl+\
   sigaddset(&set,SIGKILL); //无效
   sigset_t oldset;  //传出来 oldset 就得信号集
   int ret=sigprocmask(SIG_BLOCK,&set,&oldset);  //设置信号屏蔽字,阻塞  mask = mask|set
   //
   if(ret ==-1){
	   perror("sigprocmask error");
	   exit(-1);
   }
   // 查看未决信号,程序执行以后比如按ctrl+c,上面设置的是阻塞信息
   int j=0;
   while(1){
	   j++;
	   ret=sigpending(&pedset);   // 查看未决信号集
	   if(ret ==-1){
	  	   perror("sigprocmask error");
	  	   exit(-1);
	    }
	   print_set(&pedset);
	   sleep(2);
	   if(j==3){
		   // 解除阻塞 , 执行 ctlr+ c 
		   sigprocmask(SIG_UNBLOCK,&set,NULL);  // mask = mask & ~set,最终运算
		   printf("-----SIG_UNBLOCK----");
	   }
   }
	return 0;
}

 3.4.注册信号捕捉函数

信号捕捉:

sighandler_t signal(int signum,sighandler_t handler)
  * signum   要捕捉的信号
  * handler 捕捉以后回调函数, void func(int)  
  这个函数不同平台signal有不同含义,一般使用另外一个函数 sigaction 

sigaction (int signum, const struct sigaction *act ,const  struct sigaction* oldact);
signum : 要捕捉的信号
act : 传入动作
oldact:  原动作 , 如果设置以后不想要了,可以使用old恢复现场

  struct sigaction {
               void     (*sa_handler)(int);  // 设置0 执行这个回调,下面的不要
               void     (*sa_sigaction)(int, siginfo_t *, void *);  // 
               sigset_t   sa_mask;         // 执行捕捉函数期间,临时屏蔽信号集
               int        sa_flags;        // 一般写0 
               void     (*sa_restorer)(void);  // 无效
           };

必须要 中断、异常、或者系统调用才可以进入内核  while(1){}  无法进入内核

1. 用户状态, 中断 进入内核 ,2.  内核处理,  定义捕捉, 回到 应用层处理,否则默认内核处理

3.  捕捉函数进入内核, 回到main

3.4.1.signal 函数

/***
 *  signal函数, 不是 posix 标准,是 ANSI定义
 *  注册信号捕捉函数
 */

void sig_catch(int signo){
   printf("catch you !! %d\n",signo);
}
int main00007(){
    // 注册信号捕捉函数,但内核处理信号的时候调用sig_catch函数处理
	signal(SIGINT,sig_catch);
    while(1){

    }
	return 0;
}

3.4.2.sigaction 函数信号 捕捉

void sig_catch2(int signo){
	if(signo==SIGINT){
		 printf("catch you  2!! %d\n",signo);
		   sleep(10);
	}else if(signo==SIGQUIT){
		 printf("catch you 3!! %d\n",signo);
	}

}
/**
 * sigaction 函数信号 捕捉
 * 特点:可以避免信号捕捉
 */
int main00008(){
    struct sigaction act,oldact;
    act.sa_handler= sig_catch2;  //设置捕捉函数
    sigemptyset(&(act.sa_mask));  // 设置mask, 临时没有屏蔽信号, 就是 阻塞信号集
    // 功能
    // 如果函数已经捕捉到了SIGINT 信号,sa_handler在执行
    // 此时SIGINT 再次收到, 信号优先原则,那么又会调用 sa_handler函数执行,此时会陷入递归
    // 在在函数执行时候把act.sa_mask 置0, 那么在此时收到信号,不会继续执行函数、函数只执行一次
    // 函数执行完毕会恢复
    sigaddset(&act.sa_mask,SIGQUIT); //设置在函数执行期间屏蔽其他信号
    act.sa_flags=0;   //通常用0
	int ret= sigaction(SIGINT,&act,&oldact);  // 注册信号捕捉函数
//	sigaction(SIGQUIT,&act,&oldact);
	if(ret==-1){
		perror("sigaction error");
		exit(-1);
	}
	while(1){};
	return 0;
}

案例捕捉 setitimer函数信号:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <signal.h>

void catch_sig(int num){
  printf("catch=%d sig\n",num);
}
  
int main(int argc,char* argv[]) {
 
 // 注册一下捕捉函数
  struct sigaction act;
  act.sa_flags = 0;
  act.sa_handler = catch_sig;
  sigemptyset(&(act.sa_mask));  // 设置mask 
  sigaction(SIGALRM,&act,NULL);

  // settimer
  struct itimerval myit = {{3,0} ,{5,0}};
  setitimer(ITIMER_REAL , &myit, NULL);
  while(1){
    printf("hello\n");
    sleep(1);
  }

  return 0;
}

捕捉信号以后,如果捕捉函数中有耗时操作, 临时屏蔽信号 无效

如下代码:按下ctrl+c 捕捉,耗时函数 , 在按下ctrl+c 无效 ,该信号被屏蔽了,按下Ctrl+\ 临时屏蔽,等耗时函数执行完毕,ctrl+\ 释放执行

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <signal.h>

void catch_sig(int num){
  printf("begin--catch=%d sig\n",num);
  sleep(10);
  printf("end--catch=%d \n",num);
}
  
  // 捕捉函数屏蔽自己的信号, 按下ctrl+c 捕捉了,一直按下去,ctrl+c无效果,屏蔽了
  // 设置了临时屏蔽信号,捕捉到信号的时候    sa_mask  , 
 //  catch_sig sleep(5) 以后临时屏蔽信号 送过来了 
int main(int argc,char* argv[]) {
  // 注册一下捕捉函数
  struct sigaction act;
  act.sa_flags = 0;
  act.sa_handler = catch_sig;
  sigemptyset(&(act.sa_mask));  // 设置mask 

  sigaddset(&act.sa_mask,SIGQUIT);  //临时屏蔽 ctrl+ \ 信号

  sigaction(SIGINT,&act,NULL); //注册

  while(1){
    printf("who kill me\n");
    sleep(1);
  }
  return 0;
}

3.4.3.sigaction 函数信号 回收子进程   

SIGCHLD 信号 子进程暂停或者终止,默认是忽略 , 父进程干别的是,有信号了回收子进程就行了, 父进程可以干其他的了

void sig_catch3(int signo) {
	__pid_t pid = wait(NULL);
	printf("---sig_catch3---%d--", signo);
	if (pid == -1) {
		perror("wait fail");
		exit(-1);
	}
}
 /***
  *  为什么会出现僵尸进程:
  *  如果一次有多个子进程死了,发送SIGCHLD 信号, 但是 sigaction 的捕捉函数默已经被调用,在wait,
  *  你们其他的 信号失效,没有被回收  出现阻塞
  *
  */
int main00009() {
	int i;
	for (i = 0; i < 15; i++) {
		__pid_t pid = fork();
		if (pid == 0) {
			break;
		}
	}
	if (i == 15) {
		printf("parent ..pid.. %d", getpid());
		struct sigaction act, oldact;
		act.sa_handler = sig_catch3;  //设置捕捉函数
		sigemptyset(&(act.sa_mask));  // 设置mask
		act.sa_flags = 0;   //通常用0
		int ret = sigaction(SIGCHLD, &act, &oldact);  // 注册信号捕捉函数
		if (ret == -1) {
			perror("sigaction error");
			exit(-1);
		}
		while (1) {
		};
	} else {
        printf("child --- %d--parent --%d \n",getpid(),getppid());
	}

	return 0;
}

为什么会出现僵尸进程: 如果一次有多个子进程死了,发送SIGCHLD 信号, 但是 sigaction 的捕捉函数默已经被调用,在wait,你们其他的 信号失效,没有被回收  出现阻塞


  解决方法:

void sig_catch4(int signo) {
	int status;
	pid_t wpid;
	// 没有子进程了回收失败
// -1 没有子进程可以回收了
	while((wpid=waitpid(-1,&status,WNOHANG)!=-1)){
	    if(WIFEXITED(status)){
	    	  printf("WIFEXITED----%d\n",WEXITSTATUS(status));
	    }
	}
	printf("---sig_catch3---%d--", signo);

}
/***
 *  解决方法:循环回收
 */
int main00010() {
	int i;
	for (i = 0; i < 15; i++) {
		__pid_t pid = fork();
		if (pid == 0) {
			break;
		}
	}
	if (i == 15) {
		// 问题,如果子进程代码先执行,那么sigaction 还没有注册,
		printf("parent ..pid.. %d", getpid());
		struct sigaction act, oldact;
		act.sa_handler = sig_catch4;  //设置捕捉函数
		sigemptyset(&(act.sa_mask));  // 设置mask
		act.sa_flags = 0;   //通常用0
		int ret = sigaction(SIGCHLD, &act, &oldact);  // 注册信号捕捉函数
		if (ret == -1) {
			perror("sigaction error");
			exit(-1);
		}
		while (1) {
		};
	} else {
        printf("child --- %d--parent --%d \n",getpid(),getppid());
	}

   // else if(i<15){
  // 子进程
   //  }

	return 0;
}

问题: 如果父进程还没有注册SIGCHLD  , 子进程已经死了,解决方法

未决信号变成1了, 阻塞取消,信号才被处理

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <signal.h>

void sig_catch4(int signo) {
  pid_t wpid;
  // 没有子进程了回收失败
// -1 没有子进程可以回收了
  while((wpid=waitpid(-1, NULL ,WNOHANG)  > 0)){
      // if(WIFEXITED(status)){
      //     printf("WIFEXITED----%d\n",WEXITSTATUS(status));
      // }

      printf("---sig_catch3---%d-- \n ", wpid);
  }
}
/***
 *  解决方法:循环回收
 */
int main() {
 int i;
 pid_t pid;
// 屏蔽信号
   sigset_t myset,oldset;    // 自定义信号集
   sigemptyset(&myset);   // 清空信号集
   sigaddset(&myset,SIGCHLD);  
   int ret=sigprocmask(SIG_BLOCK,&myset,&oldset);  //设置信号屏蔽字,阻塞  mask = mask|set
   //
   if(ret ==-1){
     perror("sigprocmask SIGCHLD  error");
     exit(-1);
   }


 
  for (i = 0; i < 10; i++) {
    pid = fork();
    if (pid == 0) {
      break;
    }
  }
  if (i == 10) {
    printf("parent ..pid.. %d \n", getpid());
    sleep(2);
    // 问题,如果子进程代码先执行,那么sigaction 还没有注册,
    struct sigaction act;
    act.sa_flags = 0;   //通常用0
    sigemptyset(&(act.sa_mask));  // 设置mask
    act.sa_handler = sig_catch4;  //设置捕捉函数
    
    int ret = sigaction(SIGCHLD, &act, NULL);  // 注册信号捕捉函数
    if (ret == -1) {
      perror("sigaction error");
      exit(-1);
    }
  
    sigprocmask(SIG_SETMASK,&oldset,NULL);


    while(1){
   //    printf("x");   //进入系统调用,才会执行中断
       sleep(1);
    }

 //   sleep(100);

     
  }else if(i<10){
       printf("child -%d---> %d\n",  i,  getpid() );
  } 

   // else if(i<15){
  // 子进程
   //  }

  return 0;
}


 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值