一、Linux C 信号机制(总)

一、Linux C 信号机制

概念:信号是软件层面中断。信号的响应依赖中断。

同步:程序每一步执行什么内容是确定的。

异步:程序每一步执行什么内容是未知的。事件什么时候到来未知。

​ 异步事件的处理:查询法;通知法。

1 signal

查看信号命令: 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

其中编号 1~31 叫做标准信号,32~64 叫做实时信号。

  1. SIGINT为终端中断符,即在程序运行时在终端按下 ctrl + c 引发的中断。
  • signal 用来注册当前信号的行为。
void (*signal(int signum, void (*fun)(int)))(int);
SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

示例:

/**
 * signal 函数示例
*/

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

void int_handler(int s){
    write(1, "!", 1);
}

int main(){

    //忽略 SIGINT 信号 ,SIG_IGN 为忽略信号
    //signal(SIGINT, SIG_IGN);

    //在程序未结束时,当收到SIGINT时,执行 int_handler 的动作
    //信号会打断阻塞的系统调用,当一直按住 ctrl + c 时程序会“快进”执行。
    signal(SIGINT, int_handler);

    for (int i = 0; i < 10 ; i++){
        write(1, "*" , 1);
        sleep(1);
    }


    return 0;
}

这里体现了信号会打断阻塞的系统调用,这样在许多阻塞的系统调用发生错误时可能存在假错,我们需要对错误类型判断。如下:

do{
    //尝试只读打开文件
    fd = fopen(FILENAME, O_RDONLY);
    
    if (fd < 0){ //如果打开失败
        
        if (errno ! = EINTR){ //不是被信号打断,则报错退出
            perroor("open()");
            exit(1);
        }
    }
}while (fd < 0);//直到成功打开文件跳出循环

2 信号的不可靠

信号的不可靠指的是信号的行为是不可靠的。(不是指信号会丢失,但是标准信号一定会发生丢失,而实时信号不会)。一个信号在处理这个行为的同时又有一个相同的信号到达,由于现场的布置是由内核完成的,就有可能布置在同一个位置,使得第二次的现场将第一次的现场覆盖掉。

3 可重入函数

为了解决信号的不可靠问题。可重入函数是指第一次调用还没结束时发生第二次调用不会发生错误。

所有的系统调用都是可重入的,一部分的库函数也是可重入的,例:memcpy可以用在带有信号的程序中,而许多库函数为了能够在含有信号的情况下使用也添加了function_r版本的方法,以满足需求。

4 信号的响应过程

当前内核为每一个进程维护了两个位图,一个叫信号屏蔽字(mask),一个叫pending(一般都为32位)。

mask:记录信号的状态,一般情况初始全为1。

pending:用来记录发生了那些信号,一般情况初始全为0。

==当一个进程从内核态回到用户态时(从中断中恢复到现场),会有机会进行一次计算,计算 mask & pending,==如果结果全为0,则说明没有收到信号,进程返回原地址继续执行。

如果指定位的信号为1,说明收到了该信号。将栈内的返回现场地址悄悄改成该信号的处理函数的入口,将mask和pending的该信号的指示位都修改为0并执行信号处理函数。处理函数执行完毕后,将栈内的返回现场地址改回原地址,并将mask的指示位修改为1。

信号从收到到响应有不可避免的延迟,这个延迟是由有信号的响应机制是严重依赖系统的中断才能处理信号造成的。由于信号是由pending保存,位图不能够计数也不能累加故会存在信号丢失。标准信号 的响应没有严格的顺序。

不能随意地从信号处理函数中往外跳。(setjmp, longjmp)(sigsetjmp, siglongjmp)

5 常用函数

kill
  • 发送信号
 kill - send signal to a process
SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);

DESCRIPTION
       The kill() system call can be used to send any signal to any 
       process group or process.

       If pid is positive, then signal sig is sent to the process 
       with the ID specified by pid.

       If pid equals 0, then sig is sent to every process in the 
       process group of the calling process.//组内广播

       If  pid equals -1, then sig is sent to every process for 
       which the calling process has permission to send signals, 
       except for process 1 (init), but see below.//除了一号全局广播

       If pid is less than -1, then sig is sent to every process in 
       the process group whose ID is -pid.//发送给指定进程组

       If sig is 0, then no signal is sent, but existence and 
       permission checks are still performed; this can be used
       to check for the existence of a process ID or process group 
       ID that the caller is permitted to signal.
       //检测进程/进程组是否存在,检查返回错误值

       For  a  process  to  have  permission  to  send  a signal, 
       it must either be privileged (under Linux: have the CAP_KILL 
       capability in the user namespace of the target process), or 
       the real or  effective  user ID of the sending  process  
       must  equal the real or saved set-user-ID of the target 
       process.  In the case of SIGCONT, it suffices when the 
       sending and receiving processes belong to the same session.
       (Historically, the  rules  were different; see NOTES.)
raise
  • 给自己发信号
NAME
       raise - send a signal to the caller

SYNOPSIS
       #include <signal.h>

       int raise(int sig);

DESCRIPTION
       The  raise()  function  sends  a  signal to the calling 
       process or thread.  In a single-threaded program it is
       equivalent to

           kill(getpid(), sig);//以进程为单位

       In a multithreaded program it is equivalent to

           pthread_kill(pthread_self(), sig);//以线程为单位

       If the signal causes a handler to be called, raise() will 
       return only after the signal handler has returned.
alarm
  • 时钟信号,不能用于多任务 && pause
NAME
       alarm - set an alarm clock for delivery of a signal

SYNOPSIS
       #include <unistd.h>

       unsigned int alarm(unsigned int seconds);

DESCRIPTION
       alarm() arranges for a SIGALRM signal to be delivered to the 
       calling process in seconds seconds.

       If seconds is zero, any pending alarm is canceled.

       In any event any previously set alarm() is canceled.

RETURN VALUE
       alarm()  returns the number of seconds remaining until any 
       previously scheduled alarm was due to be delivered,or zero 
       if there was no previously scheduled alarm.
//-------------------------------------------------------------
NAME
       pause - wait for signal

SYNOPSIS
       #include <unistd.h>

       int pause(void);

DESCRIPTION
       pause() causes the calling process (or thread) to sleep 
       until a signal is delivered that either terminates the
       process or causes the invocation of a signal-catching 
       function.

RETURN VALUE
       pause() returns only when a signal was caught and  the  
       signal-catching  function  returned.   In  this  case,
       pause() returns -1, and errno is set to EINTR.           
//-------------------------------------------------------------
/**
 * alarm
*/

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

#include <unistd.h>

int main(){

    alarm(5);

    while (1){
        pause();//等待一个信号打断
    }

    return 0;
}           

不要轻易使用sleep()函数,因为有些环境下sleep是由 alarm + pause封装的,会对程序中的alarm使用造成影响。

简单实验

一、定时循环,设计程序定时5s循环计数。

  1. time版本
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

int main(){

    time_t end;
    long long int count = 0;

    end = time(NULL) + 5;

    while(time(NULL) <= end){
        count ++;
    }
    printf("%lld\n", count);

    return 0;
}

time版本会浪费大量时间在time的调用(取系统时戳)导致真正用在循环的时间非常少。效率极其低下。

  1. alarm版本
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


int volatile loop = 1;//循环控制函数

void alrm_handler(int s){
    loop = 0;
}

int main(){ 

    long long int count;

    signal(SIGALRM, alrm_handler);//注册信号行为,一定要在alarm之前
    alarm(5);//时钟计时

    while(loop){//循环条件
        count++;
    }

    printf("%lld\n", count);
    return 0;
}
//使用gcc -O1 优化编译,会导致永真循环,解决方案第七行加 volatile
//让程序不轻信内存中的值,要到变量真正的位置取值。。

二、流量控制

  1. 普通版(漏桶)
#define CPS 10 //每秒读写字符
#define BUFSIZE	CPS
//最简单在循环内尾加sleep(1)

int volatile loop = 0;//循环控制函数
void alrm_handler(int s){
    alarm(1);//为自己发送时钟信号
    loop = 1;
}

signal(SIGALRM, alrm_handler);//注册信号行为
alarm(1);//时钟计时

while(1){
    //控制时间
    while(!loop){//此处存在忙等
		;//pause(); //解决忙等问题
    }
    loop = 0;
    
    //下面为实际循环内容
}
  1. 令牌桶
#define CPS 10 //每秒读写字符
#define BUFSIZE	CPS
#define BURST	100 //桶大小上限
//最简单在循环内尾加sleep(1)

//int volatile token = 0;//循环控制函数
//sig_atomic_t 信号原子类型,这种类型的取值和赋值一定是原子操作
static volatile sig_atomic_t token =0;

void alrm_handler(int s){
    alarm(1);//为自己发送时钟信号
    token++;//token累加
    if(token > BURST){
        token = BURST;//限制token大小
    }
}

signal(SIGALRM, alrm_handler);//注册信号行为
alarm(1);//时钟计时

while(1){
    //控制时间
    while(token <= 0){//此处存在忙等
		pause(); //解决忙等问题
    }
    token--;//削减令牌
    
    
    //下面为实际循环内容
}
桶令牌结构封装
  • mytbf.h
#ifndef MYTBF_H__
#define MYTBF_H__

//令牌桶最数量
#define MYTBF_MAX   1024

//令牌桶结构类型
typedef void    mytbf_t;

/**
 * @name mytbf_init
 * @brief 初始化一个令牌桶结构返回一个令牌桶结构指针,
 * 并将结构体指针存放在令牌结构体指针数组全局变量中。
 * 
 * @param cps   流控速率
 * @param burst token最大值
 * 
 * @return  
 *      NULL - fail | 
 *      mytbf_t类型的结构体指针 - succeed
*/
mytbf_t *mytbf_init(int cps, int burst);

/**
 * @name mytbf_fetchtoken
 * @brief 获取token并释放size大小的token
 * @param ptr 桶令牌结构体指针
 * @param size 需要释放的大小
 * @return 
 *      n 成功释放的大小 - succeed | 
 *      -EINVAL errn - faild
*/
int mytbf_fetchtoken(mytbf_t *, int);

/**
 * @name mytbf_returntoken
 * @brief token归还函数
 * @param ptr 桶令牌结构体指针
 * @param size 归还的token大小
 * @return
 *      size 成功累加 - succeed | 
 *      -EINVAL errn - faild     
*/
int mytbf_returntoken(mytbf_t *, int);

/**
 * @name mytbf_destory
 * @brief 销毁令牌结构体并释放空间
 * @param ptr 桶令牌结构体指针
 * @return
 *      默认不会失败 返回 0 - succeed
*/
int mytbf_destory(mytbf_t *);

#endif
  • mytbf.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

#include "mytbf.h"

//signal的返回值类型定义
typedef void (*sighandler_t)(int);

//存放令牌桶指针的全局变量
static struct mytbf_st* job[MYTBF_MAX];
//时钟信号使用标志位 0为可用 1为不可用
static int inited = 0;

//保存 SIGALRM 信号的原始行为
static sighandler_t alrm_handler_save;

//令牌桶结构体
struct mytbf_st {
    //流控速率
    int csp;
    //令牌最大值 累加上限
    int burst;
    //令牌大小
    int token;

    //指针在 job[MYTBF_MAX] 存放位置
    int pos;
};

/**
 * @name get_free_pos
 * @brief 获取令牌指针数组的空缺位置
 * @param void
 * @return
 *      pos 空缺位置数组下标 - succeed | 
 *      -1 无空缺位置 - fail
*/
static int get_free_pos(void){
    int i = 0;

    //遍历数组
    for(i = 0; i < MYTBF_MAX; i++){
        //如果存在空指针 返回数组下标
        if(job[i] == NULL){
            return i;
        }
    }
    //无空缺位置 返回-1
    return -1;
}

/**
 * @name alrm_handler
 * @brief 时钟信号的处理函数,桶令牌的累加处理
*/
static void alrm_handler(int s){

    //设置下一次的时钟信号
    alarm(1);

    //遍历桶令牌指针数组
    for(int i = 0; i < MYTBF_MAX; i++){
        
        //指针非空时对token进行累加计数
        if(job[i] != NULL){
            job[i]->token += job[i]->cps;
            if(job[i]->token > job[i]->burst){
                job[i]->token = job[i]->burst;
            }
        }
    }

}

/**
 * @name module_unload
 * @brief 桶令牌解挂函数,恢复SIGALRM信号的原始行为,并清空时钟信号。
 * @param void
 * @return void
*/
static void module_unload(void){
    //恢复SIGALRM信号的原始行为
    signal(SIGALRM, alrm_handler_save);
    //并清空时钟信号
    alarm(0);
    //释放令牌空间,并将指针指空
    for(int i = 0; i < MYTBF_MAX; i++){
        free(job[i]);
        job[i] = NULL;
    }
}

/**
 * @name module_load
 * @brief 重新定义SIGALRM信号的行为,保留SIGALRM信号的原始行为,并设置时钟信号。
 * @param void
 * @return void
*/
static void module_load(void){

    //重新定义SIGALRM信号的行为,保留SIGALRM信号的原始行为
    alrm_handler_save = signal(SIGALRM, alrm_handler);
    //设置时钟信号
    alarm(1);

    //异常退出处理(钩子函数)
    atexit(module_unload);
}

/**
 * @name mytbf_init
 * @brief 初始化一个令牌桶结构返回一个令牌桶结构指针,
 * 并将结构体指针存放在令牌结构体指针数组全局变量中。
 * 
 * @param cps   流控速率
 * @param burst token最大值
 * 
 * @return  
 *      NULL - fail | 
 *      mytbf_t类型的结构体指针 - succeed
*/
mytbf_t *mytbf_init(int cps, int burst){
    //声明结构体指针
    struct mytbf_st *me;
    //桶令牌指针位置
    int pos;

    //判断时钟是否可用
    if(!inited){
        //初始化时钟信号,和时钟触发机制
        module_load();
        //将inited置1,时钟信号以初始化,时钟信号不再初始化。
        inited = 1;
    }

    //获取令牌指针数组中的空缺位置
    pos = get_free_pos();

    //数组已满 无法初始化 返回NULL
    if(pos < 0){
        return NULL;
    }

    //申请空间并初始化结构体成员
    me = malloc(sizeof(*me));
    //分配内存失败 返回空指针
    if (me == NULL){
        return NULL;
    }
        
    me->burst = burst;
    me->csp = cps;
    me->pos = pos;
    me->token = 0;

    //将指针存入 指针数组空缺位置中
    job[pos] = me;

    //返回结构体指针
    return me;

}

/**
 * @name mytbf_fetchtoken
 * @brief 获取token并释放size大小的token
 * @param ptr 桶令牌结构体指针
 * @param size 需要释放的大小
 * @return 
 *      n 成功释放的大小 - succeed | 
 *      -EINVAL errn - faild
*/
int mytbf_fetchtoken(mytbf_t *ptr, int size){
    
    //指针类型强转
    struct mytbf_st *me = ptr;
    //释放大小
    int n;

    //size的合法性判断
    if(size <= 0){
        return -EINVAL;
    }

    //当token无内容时 等待时钟信号触发
    while(me->token <= 0){
        pause();
    }

    //选择 token和size的最小值释放空间
    n = min(me->token, size);
    me->token -= n;

    //返回成功释放空间的大小
    return n;

}


/**
 * @name mytbf_returntoken
 * @brief token归还函数
 * @param ptr 桶令牌结构体指针
 * @param size 归还的token大小
 * @return
 *      size 成功累加 - succeed | 
 *      -EINVAL errn - faild     
*/
int mytbf_returntoken(mytbf_t *ptr, int size){
    
    //指针类型强转
    struct mytbf_st *me = ptr;

    //size的合法性判断
    if(size <= 0){
        return -EINVAL;
    }

    //token增加
    me->token += size;
    //判断token是否溢出
    if(me->token > me->burst){
        me->token = me->burst;
    }

    //成功返回
    return size;

}

/**
 * @name mytbf_destory
 * @brief 销毁令牌结构体并释放空间
 * @param ptr 桶令牌结构体指针
 * @return
 *      默认不会失败 返回 0 - succeed
*/
int mytbf_destory(mytbf_t *ptr){
    //指针类型强转
    struct mytbf_st *me = ptr;
    //释放指针指向的结构体
    free(ptr);
    //将待删除位置指针指控
    job[me->pos] = NULL;
    
    //成功返回
    return 0;
}

mian.c

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

#include "mytbf.h"

#define CPS     10
#define BURST   100
#define BUFSIZE 1024


int main(){

    mytbf_t *tbf;
    int size, len;

    //所有要实现的内容之前调用init
    tbf = mytbf_init(CPS,BURST);
    if(tbf == NULL){
        exit(0);//错误退出
    }

    
    while(1){
        //向token询问 BUFSIZE 个使用权限,返回size个可使用的权限
        size = mytbf_fetchtoken(tbf, BUFSIZE);

        //下面使用 size个可使用权限
        //例如读 size个大小的字符 确定读到 len个大小的字符

        if(size - len > 0){
            //归还剩余token
            mytbf_returntoken(tbf, size-len);
        }



    }

    //销毁令牌桶
    mytbf_destory(tbf);

    return 0;
}
思考题
  • 使用单一计时器,构造一组函数,实现任意数量的计时器。

数据结构构造:

成员数据类型描述
secint时间
func函数指针r行为函数入口
argchar*字符串

每次对sec的值自减,当sec为0时调用函数并传入参数。

setitimer
  • setitimer
NAME
       getitimer, setitimer - get or set value of an interval timer

SYNOPSIS
       #include <sys/time.h>

       int getitimer(int which, struct itimerval *curr_value);
       int setitimer(int which, const struct itimerval *new_value,
                     struct itimerval *old_value); //误差不累计

DESCRIPTION
       These  system  calls provide access to interval timers, that is, timers that initially expire at some point in the future, and (optionally) at regular intervals after that.  When a timer expires, a signal is generated for the calling process, and the timer is reset to the specified interval (if the interval is nonzero).

       Three  types of timers—specified via the which argument—are provided, each of which counts against a different clock and generates a different signal on timer expiration:
		//实际时间
       ITIMER_REAL    This timer counts down in real (i.e., wall clock) time.  
           			  At each expiration, a  SIGALRM  signal is generated.
		//用户模式 (所有线程)
       ITIMER_VIRTUAL This  timer  counts down against the user-mode CPU time 
                      consumed by the process. (The measurement includes CPU time 
                      consumed by all threads in the process.)  At each  expiration,
                      a SIGVTALRM signal is generated.
	  //系统+用户(所有线程)
       ITIMER_PROF    This  timer counts down against the total (i.e., both user and
                      system) CPU time consumed by the process.  (The measurement 
                      includes CPU time consumed by all threads in the process.)  At
                      each expiration, a SIGPROF signal is generated.
                      process. (The measurement includes CPU time consumed by all 
                      threads in the process.)  At  each expiration, a SIGPROF signal
                      is generated.

                      In  conjunction with ITIMER_VIRTUAL, this timer can be used to 
                      profile user and system CPU time consumed by the process.

       A process has only one of each of the three types of timers.

       Timer values are defined by the following structures:

           struct itimerval {
               struct timeval it_interval; //周期  /* Interval for periodic timer */
               struct timeval it_value;  //初相位  /* Time until next expiration */
           };

           struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };
//tv_sec 和 tv_usec 到 it_value 的赋值时原子化的操作
abort
  • abort 给自己发送一个SIGABRT 信号
NAME
       abort - cause abnormal process termination //导致进程异常终止

SYNOPSIS
       #include <stdlib.h>

       void abort(void);

DESCRIPTION
       The  abort() first unblocks the SIGABRT signal, and then raises that signal for the calling process (as though raise(3) was called).  This results in the abnormal termination of the process unless the  SIGABRT  signal  is caught and the signal handler does not return (see longjmp(3)).

       If  the SIGABRT signal is ignored, or caught by a handler that returns, the abort() function will still terminate the process.  It does this by restoring the default disposition for SIGABRT and then raising  the  signal for a second time.

RETURN VALUE
       The abort() function never returns.
system
  • system
NAME
       system - execute a shell command

SYNOPSIS
       #include <stdlib.h>

       int system(const char *command);

       system - execute a shell command

SYNOPSIS
       #include <stdlib.h>

       int system(const char *command);

DESCRIPTION
       The system() library function uses fork(2) to create a child process that executes the shell command specified in command using execl(3) as follows:

           execl("/bin/sh", "sh", "-c", command, (char *) 0);

       system() returns after the command has been completed.
		
       /* 在带信号的程序中使用 要 block SIGCHLD 并 忽略掉 SIGINT 和 SIGQUIT */
       During execution of the command, SIGCHLD will be blocked, and SIGINT and  
SIGQUIT  will  be  ignored, in  the process  that  calls  system()  (these  signals  
will  be handled according to their defaults inside the child process that executes 
command).

       If command is NULL, then system() returns a status indicating whether a shell is available on the system

6 信号集

相关函数

  • sigemptyset
//sigset_t 信号集类型
NAME
       sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations

SYNOPSIS
       #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);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember():
           _POSIX_C_SOURCE

DESCRIPTION
       These functions allow the manipulation of POSIX signal sets.

       sigemptyset() initializes the signal set given by set to empty, with all signals excluded from the set.

       sigfillset() initializes set to full, including all signals.

       sigaddset() and sigdelset() add and delete respectively signal signum from set.

       sigismember() tests whether signum is a member of set.

       Objects  of  type  sigset_t must be initialized by a call to either sigemptyset() or sigfillset() before being passed to the functions sigaddset(), sigdelset() and sigismember() or the additional glibc functions described below (sigisemptyset(), sigandset(), and sigorset()).  The results are undefined if this is not done.

RETURN VALUE
       sigemptyset(), sigfillset(), sigaddset(), and sigdelset() return 0 on success and -1 on error.

       sigismember() returns 1 if signum is a member of set, 0 if signum is not a member, and -1 on error.

       On error, these functions set errno to indicate the cause of the error.

7 信号屏蔽字/pending集

相关函数

  • sigprocmask
NAME
       sigprocmask, rt_sigprocmask - examine and change blocked signals

SYNOPSIS
       #include <signal.h>

       /* Prototype for the glibc wrapper function */
       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

       /* Prototype for the underlying system call */
       int rt_sigprocmask(int how, const kernel_sigset_t *set,
                          kernel_sigset_t *oldset, size_t sigsetsize);

       /* Prototype for the legacy system call (deprecated) */
       int sigprocmask(int how, const old_kernel_sigset_t *set,
                       old_kernel_sigset_t *oldset);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigprocmask(): _POSIX_C_SOURCE

DESCRIPTION
       sigprocmask()  is  used  to fetch and/or change the signal mask of the calling thread.  The signal mask is the
       set of signals whose delivery is currently blocked for the caller (see also signal(7) for more details).

       The behavior of the call is dependent on the value of how, as follows.

       SIG_BLOCK
              The set of blocked signals is the union of the current set and the set 
              argument.

       SIG_UNBLOCK
              The signals in set are removed from the current set of blocked signals. 
              It is permissible  to  attempt to unblock a signal which is not 
              blocked.

       SIG_SETMASK
              The set of blocked signals is set to the argument set.

       If oldset is non-NULL, the previous value of the signal mask is stored in
       oldset.

       If  set is NULL, then the signal mask is unchanged (i.e., how is ignored), but
       the current value of the signal
       mask is nevertheless returned in oldset (if it is not NULL).

       A set of functions for modifying and inspecting variables of type sigset_t 
       ("signal  sets")  is  described  in sigsetops(3).

       The use of sigprocmask() is unspecified in a multithreaded process; see 
       pthread_sigmask(3).

示例:

/**
 * 信号屏蔽 函数示例
 * 
*/

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

void int_handler(int s){
    write(1, "!", 1);
}

int main(){
    //set 信号集    oset 旧状态     saveset 原始状态
    sigset_t set, oset, saveset;

    signal(SIGINT, int_handler);

    //置空信号集
    sigemptyset(&set);
    //添加 SIGINT信号到信号集中
    sigaddset(&set, SIGINT);

    //采集原始信号集中的信号状态 保存到 saveset
    sigprocmask(SIG_UNBLOCK, &set, &saveset);

    for (int j = 0; j < 1000; j++){
        //阻塞信号集中的信号,保存旧的状态到oset
        sigprocmask(SIG_BLOCK, &set, &oset);
        for (int i = 0; i < 5 ; i++){
            write(1, "*" , 1);
            sleep(1);
        }
        write(1, "\n", 1);
        //从oset中恢复旧的状态
        sigprocmask(SIG_SETMASK, &oset, NULL);
    }
    //结束前恢复原始状态
    sigprocmask(SIG_SETMASK, &saveset, NULL);
    return 0;
}
  • sigpending
NAME
       sigpending, rt_sigpending - examine pending signals
	//从内核中获取pending的状态,基本无用,因为信号响应的机制。
SYNOPSIS
       #include <signal.h>

       int sigpending(sigset_t *set);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigpending(): _POSIX_C_SOURCE
SYNOPSIS
       #include <signal.h>

       int sigpending(sigset_t *set);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigpending(): _POSIX_C_SOURCE

DESCRIPTION
       sigpending() returns the set of signals that are pending for delivery to the calling thread (i.e., the signals which have been raised while blocked).  The mask of pending signals is returned in set.

RETURN VALUE
       sigpending() returns 0 on success and -1 on error.  In the event of an error, errno is  set  to  indicate  the
       cause.

8 扩展

  • sigsuspend 信号驱动
NAME
       sigsuspend, rt_sigsuspend - wait for a signal

SYNOPSIS
       #include <signal.h>

       int sigsuspend(const sigset_t *mask);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigsuspend(): _POSIX_C_SOURCE

DESCRIPTION

SYNOPSIS
       #include <signal.h>

       int sigsuspend(const sigset_t *mask);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigsuspend(): _POSIX_C_SOURCE

DESCRIPTION
       sigsuspend()  temporarily replaces the signal mask of the calling process with the mask given by mask and then
       suspends the process until delivery of a signal whose action is to invoke a signal handler or to  terminate  a
       process.

       If  the  signal terminates the process, then sigsuspend() does not return.  If the signal is caught, then sig‐
       suspend() returns after the signal handler returns, and the signal mask is restored to the  state  before  the
       call to sigsuspend().

       It  is  not  possible  to  block  SIGKILL  or  SIGSTOP; specifying these signals in mask, has no effect on the
       process's signal mask.

例:

/**
 * 信号驱动 函数示例
 * 
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void int_handler(int s){
    write(1, "!", 1);
}

int main(){
    //set 信号集    oset 旧状态     saveset 原始状态
    sigset_t set, oset, saveset;

    signal(SIGINT, int_handler);

    //置空信号集
    sigemptyset(&set);
    //添加 SIGINT信号到信号集中
    sigaddset(&set, SIGINT);

    //采集原始信号集中的信号状态 保存到 saveset
    sigprocmask(SIG_UNBLOCK, &set, &saveset);

    sigprocmask(SIG_BLOCK, &set, &oset);//
    for (int j = 0; j < 1000; j++){
        //阻塞信号集中的信号,保存旧的状态到oset
        //sigprocmask(SIG_BLOCK, &set, &oset);
        for (int i = 0; i < 5 ; i++){
            write(1, "*" , 1);
            sleep(1);
        }
        write(1, "\n", 1);
    /*      
        //非原子化操作,信号容易在pause之前被相应,无法完成驱动任务
        //从oset中恢复旧的状态
        sigprocmask(SIG_SETMASK, &oset, NULL);       
        pause();
    */
        //解决方案
        sigsuspend(&oset);
        /** 相当于一下内容地原子化操作
         * 最外层循环 28L 添加
         * sigprocmask(SIG_BLOCK, &set, &oset);
         * 
         * 内循环此处添加
         * sigset_t tmpset;
         * sigprocmask(SIG_SETMASK, &oset, &tmpset);
         * pause();
         * sigprocmask(SIG_SETMASK, &tmpset, NULL);
         */
    }
    //结束前恢复原始状态
    sigprocmask(SIG_SETMASK, &saveset, NULL);
    return 0;
}

//效果每行打印5个*后停止,在接收到 ctrl + c 的信号后驱动继续打印一行,
//并保证能在打印过程中等待响应阻塞的信号
  • sigaction

代替 signal 函数,解决signal在处理多个信号时容易发生重入的问题。

解决无法判断信号来源

//通过信号关闭文件描述符以及日志
void xxx_exit(int s){
    /*
    if(s == SIGINT){
    
    }
    else if(s == SIGQUIT){
    
    }
    else if(s == SIGTERM){
    
    }
    else;
    
    */
    
    fclose(fp);
    closelog();
    exit(0);
}

int main(){
    ...
        
    signal(SIGINT, xxx_exit);
    signal(SIGQUIT, xxx_exit);
    signal(SIGTERM, xxx_exit);
    ...
}
//有重入的危险,当信号处理函数在响应的时候容易被再次响应

sigaction说明

NAME
       sigaction, rt_sigaction - examine and change a signal action

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

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigaction(): _POSIX_C_SOURCE

       siginfo_t: _POSIX_C_SOURCE >= 199309L

DESCRIPTION
       The  sigaction()  system call is used to change the action taken by a process on receipt of a specific signal. (See signal(7) for an overview of signals.)

       signum specifies the signal and can be any valid signal except SIGKILL and SIGSTOP.

       If act is non-NULL, the new action for signal signum is installed from act.  If oldact is non-NULL, the previous action is saved in oldact.

       The sigaction structure is defined as something like:

           struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               //void* 执行信号处理函数而被打断的现场,实质上这里的void* 是ucontext_t *
               //int getcontext(ucontext_t *ucp);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };
//sa_handler 和 sa_sigaction 在很多结构中是共用体不能够同时指定
//sa_mask 在响应信号时希望那些信号被阻塞
//sa_flags 特殊指令
       On some architectures a union is involved: do not assign to both sa_handler and sa_sigaction.
           
       The  sa_restorer  field  is  not  intended for application use.  (POSIX does 
       not specify a sa_restorer field.) Some further details of the purpose of this 
       field can be found in sigreturn(2).

       sa_handler specifies the action to be associated with signum and  may  be  
       SIG_DFL  for  the  default  action, SIG_IGN  to ignore this signal, or a 
       pointer to a signal handling function.  This function receives the signal 
       number as its only argument.

       If SA_SIGINFO is specified in sa_flags, then sa_sigaction (instead of 
       sa_handler)  specifies  the  signal-handling function for signum.  This 
       function receives three arguments, as described below.

       sa_mask  specifies  a mask of signals which should be blocked (i.e., added to 
       the signal mask of the thread in which the signal handler is invoked) during 
       execution of the signal handler.  In addition,  the  signal  which triggered 
       the handler will be blocked, unless the SA_NODEFER flag is used.

       sa_flags  specifies a set of flags which modify the behavior of the signal. 
       It is formed by the bitwise OR of zero or more of the following:

           SA_NOCLDSTOP
                  If signum is SIGCHLD, do not receive notification  when  child  
                  processes  stop  (i.e.,  when  they receive  one  of SIGSTOP, 
                  SIGTSTP, SIGTTIN, or SIGTTOU) or resume (i.e., they receive 
                  SIGCONT) (see wait(2)).  This flag is meaningful only when 
                  establishing a handler for SIGCHLD.

           SA_NOCLDWAIT (since Linux 2.6)
                  If signum is SIGCHLD, do not transform children into zombies when 
                  they terminate.  See  also  waitpid(2).  This flag is meaningful 
                  only when establishing a handler for SIGCHLD, or when setting that 
                  signal's disposition to SIG_DFL.

                  If the SA_NOCLDWAIT flag is set when establishing a handler for 
                  SIGCHLD, POSIX.1 leaves it unspecified  whether  a  SIGCHLD signal 
                  is generated when a child process terminates.  On Linux, a SIGCHLD 
                  signal is generated in this case; on some other implementations, it 
                  is not.

           SA_NODEFER
                  Do not prevent the signal from being received from within its own 
                  signal  handler. This  flag  is meaningful  only when establishing 
                  a signal handler.  SA_NOMASK is an obsolete, nonstandard synonym 
                  for this flag.

           SA_ONSTACK
                  Call the signal handler on an alternate signal stack provided by 
                  sigaltstack(2).  If  an  alternate stack  is  not available, the 
                  default stack will be used.  This flag is meaningful only when 
                  establishing a signal handler.

           SA_RESETHAND
                  Restore the signal action to the default upon entry to the signal 
                  handler.  This flag is meaningful  only  when  establishing a 
                  signal handler.  SA_ONESHOT is an obsolete, nonstandard synonym 
                  for this flag.

           SA_RESTART
                  Provide behavior compatible with BSD signal semantics by making 
                  certain  system  calls  restartable across  signals.   This  flag 
                  is meaningful only when establishing a signal handler.  See 
                  signal(7) for a discussion of system call restarting.

           SA_RESTORER
                  Not intended for application use. This flag  is  used  by  C 
                  libraries  to  indicate  that  the sa_restorer  field  contains  
                  the  address  of a  "signal  trampoline".  See sigreturn(2) for 
                  more details.

           SA_SIGINFO (since Linux 2.2)
                  The signal handler takes three arguments, not one.  In  this  
                  case,  sa_sigaction  should  be  setinstead of sa_handler.  This 
                  flag is meaningful only when establishing a signal handler.

signal 重入问题的解决

//通过信号关闭文件描述符以及日志
void xxx_exit(int s){
    /*
    if(s == SIGINT){
    
    }
    else if(s == SIGQUIT){
    
    }
    else if(s == SIGTERM){
    
    }
    else;
    
    */
    
    fclose(fp);
    closelog();
    exit(0);
}

int main(){
    struct sigaction sa;
    struct sigaction sa_int;
    struct sigaction sa_quit;
    struct sigaction sa_term;
    ...
/*        
    //SIGINT    
    sa_int.sa_handler = xxx_exit;
    sigemptyset(&sa_int.sa_mask);
    sigaddset(sa_int.sa_mask, SIGQUIT | SIGTERM);
    sa_int.sa_flags = 0;
    sigaction(SIGINT, &sa_int, NULL);
    //SIGQUIT
    sa_quit.sa_handler = xxx_exit;
    sigemptyset(&sa_quit.sa_mask);
    sigaddset(sa_quit.sa_mask, SIGINT | SIGTERM);
    sa_quit.sa_flags = 0;
    sigaction(SIGQUIT, &sa_quit, NULL);
    //SIGTERM
    sa_term.sa_handler = xxx_exit;
    sigemptyset(&sa_term.sa_mask);
    sigaddset(sa_term.sa_mask, SIGQUIT | SIGTERM);
    sa_term.sa_flags = 0;
    sigaction(SIGTERM, &sa_term, NULL);
*/
    //简化写法
    sa.sa_handler = xxx_exit;
    sigemptyset(&sa.sa_mask);
    sigaddset(sa.sa_mask, SIGINT | SIGQUIT | SIGTERM);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    
    //signal(SIGINT, xxx_exit);
    //signal(SIGQUIT, xxx_exit);
    //signal(SIGTERM, xxx_exit);
    ...
}

使用三参的信号处理函数,处理信号

		void     (*sa_sigaction)(int, siginfo_t *, void *);
//结构体类型说明
		The siginfo_t data type is a structure with the following fields:

           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 */
               long     si_band;      /* Band event (was int in
                                         glibc 2.3.2 and earlier) */
               int      si_fd;        /* File descriptor */
               short    si_addr_lsb;  /* Least significant bit of address
                                         (since Linux 2.6.32) */
               void    *si_lower;     /* Lower bound when address violation
                                         occurred (since Linux 3.19) */
               void    *si_upper;     /* Upper bound when address violation
                                         occurred (since Linux 3.19) */
               int      si_pkey;      /* Protection key on PTE that caused
                                         fault (since Linux 4.6) */
               void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */
           }
		
//si_code 信号来源字段
       For a regular signal, the following list shows the values which can be placed in si_code for any signal, along with the reason that the signal was generated.

流控处理,修改版(mytbf.c)

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/time.h>
#include "mytbf.h"

//存放令牌桶指针的全局变量
static struct mytbf_st* job[MYTBF_MAX];
//时钟信号使用标志位 0为可用 1为不可用
static int inited = 0;

//保存 SIGALRM 信号的原始行为
static struct sigaction alrm_sa_save;


//令牌桶结构体
struct mytbf_st {
    //流控速率
    int csp;
    //令牌最大值 累加上限
    int burst;
    //令牌大小
    int token;

    //指针在 job[MYTBF_MAX] 存放位置
    int pos;
};

/**
 * @name get_free_pos
 * @brief 获取令牌指针数组的空缺位置
 * @param void
 * @return
 *      pos 空缺位置数组下标 - succeed | 
 *      -1 无空缺位置 - fail
*/
static int get_free_pos(void){
    int i = 0;

    //遍历数组
    for(i = 0; i < MYTBF_MAX; i++){
        //如果存在空指针 返回数组下标
        if(job[i] == NULL){
            return i;
        }
    }
    //无空缺位置 返回-1
    return -1;
}

/**
 * @name alrm_action
 * @brief 时钟信号的处理函数,桶令牌的累加处理
*/
static void alrm_action(int s, siginfo_t *infop, void *unused){

	//判断信号来源是否来自内核
    if(infop->si_code != SI_KERNEL){
        return ;
	}
    
    //遍历桶令牌指针数组
    for(int i = 0; i < MYTBF_MAX; i++){
        
        //指针非空时对token进行累加计数
        if(job[i] != NULL){
            job[i]->token += job[i]->cps;
            if(job[i]->token > job[i]->burst){
                job[i]->token = job[i]->burst;
            }
        }
    }

}

/**
 * @name module_unload
 * @brief 桶令牌解挂函数,恢复SIGALRM信号的原始行为,并清空时钟信号。
 * @param void
 * @return void
*/
static void module_unload(void){
    
    //itimer计时器变量参数
    struct itimerval itv;
    
    //还原SIGINT信号状态
	sigaction(SIGALRM, &alrm_sa_save, NULL);
    /* if error */
    
    //清空定时器
    itv.it_interval.tv_sec = 0;
    itv.it_interval.tv_usec = 0;
    itv.it_value.tv_sec = 0;
    itv.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL, &itv, NULL);
    /* if error */    
    
    for(int i = 0; i < MYTBF_MAX; i++){
        free(job[i]);
        job[i] = NULL;
    }
}

/**
 * @name module_load
 * @brief 重新定义SIGALRM信号的行为,保留SIGALRM信号的原始行为,并设置时钟信号。
 * @param void
 * @return void
*/
static void module_load(void){
	//sigaction参数
    struct sigaction sa;
    //定时器参数
    struct itimerval itv;
    
    //定义信号处理函数以及响应机制
    sa.sa_sigaction = alrm_action;
    sigemptyset(&sa.sa_mask);
    
    //sa_sigaction instead of sa_handler
    sa.sa_flags = SA_SIGINFO;
    //注册信号行为
	sigaction(SIGALRM, &sa, &alrm_sa_save);
    /* if error */
    
    //设置定时器
    itv.it_interval.tv_sec = 1;
    itv.it_interval.tv_usec = 0;
    itv.it_value.tv_sec = 1;
    itv.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL, &itv, NULL);
    /* if error */
    
    //异常退出处理(钩子函数)
    atexit(module_unload);
}

/**
 * @name mytbf_init
 * @brief 初始化一个令牌桶结构返回一个令牌桶结构指针,
 * 并将结构体指针存放在令牌结构体指针数组全局变量中。
 * 
 * @param cps   流控速率
 * @param burst token最大值
 * 
 * @return  
 *      NULL - fail | 
 *      mytbf_t类型的结构体指针 - succeed
*/
mytbf_t *mytbf_init(int cps, int burst){
    //声明结构体指针
    struct mytbf_st *me;
    //桶令牌指针位置
    int pos;

    //判断时钟是否可用
    if(!inited){
        //初始化时钟信号,和时钟触发机制
        module_load();
        //将inited置1,时钟信号以初始化,时钟信号不再初始化。
        inited = 1;
    }

    //获取令牌指针数组中的空缺位置
    pos = get_free_pos();

    //数组已满 无法初始化 返回NULL
    if(pos < 0){
        return NULL;
    }

    //申请空间并初始化结构体成员
    me = malloc(sizeof(*me));
    //分配内存失败 返回空指针
    if (me == NULL){
        return NULL;
    }
        
    me->burst = burst;
    me->csp = cps;
    me->pos = pos;
    me->token = 0;

    //将指针存入 指针数组空缺位置中
    job[pos] = me;

    //返回结构体指针
    return me;

}

/**
 * @name mytbf_fetchtoken
 * @brief 获取token并释放size大小的token
 * @param ptr 桶令牌结构体指针
 * @param size 需要释放的大小
 * @return 
 *      n 成功释放的大小 - succeed | 
 *      -EINVAL errn - faild
*/
int mytbf_fetchtoken(mytbf_t *ptr, int size){
    
    //指针类型强转
    struct mytbf_st *me = ptr;
    //释放大小
    int n;

    //size的合法性判断
    if(size <= 0){
        return -EINVAL;
    }

    //当token无内容时 等待时钟信号触发
    while(me->token <= 0){
        pause();
    }

    //选择 token和size的最小值释放空间
    n = min(me->token, size);
    me->token -= n;

    //返回成功释放空间的大小
    return n;

}


/**
 * @name mytbf_returntoken
 * @brief token归还函数
 * @param ptr 桶令牌结构体指针
 * @param size 归还的token大小
 * @return
 *      size 成功累加 - succeed | 
 *      -EINVAL errn - faild     
*/
int mytbf_returntoken(mytbf_t *ptr, int size){
    
    //指针类型强转
    struct mytbf_st *me = ptr;

    //size的合法性判断
    if(size <= 0){
        return -EINVAL;
    }

    //token增加
    me->token += size;
    //判断token是否溢出
    if(me->token > me->burst){
        me->token = me->burst;
    }

    //成功返回
    return size;

}

/**
 * @name mytbf_destory
 * @brief 销毁令牌结构体并释放空间
 * @param ptr 桶令牌结构体指针
 * @return
 *      默认不会失败 返回 0 - succeed
*/
int mytbf_destory(mytbf_t *ptr){
    //指针类型强转
    struct mytbf_st *me = ptr;
    //释放指针指向的结构体
    free(ptr);
    //将待删除位置指针指控
    job[me->pos] = NULL;
    
    //成功返回
    return 0;
}

9 实时信号

如果一个进程同时收到标准信号和实时信号,优先响应标准信号。

实时信号存放文件 /usr/include/x86_64-linux-gnu/bits/signum.h

//signum.h
//这两个是未定义信号留给用户的
#undef  SIGUSR1
#define SIGUSR1         10
#undef  SIGUSR2
#define SIGUSR2         12


//signal.h
#define SIGRTMIN        (__libc_current_sigrtmin ())
#define SIGRTMAX        (__libc_current_sigrtmax ())

实时信号不发生丢失需要排队,其他用法与标准信号完全一致。

查看实时信号排队上限 ulimit -a

wangs7_@WangQ7:/mnt/c/Users/MrWangS7$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7823 #实时信号排队上限
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7823
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值