Linux 中的几种定时器

19 篇文章 2 订阅

0. 前言

在linux系统中定时器有分为软定时和硬件定时器。硬件定时器一般指的是CPU的一种底层寄存器,它负责按照固定时间频率产生中断信号,形成信号源。基于硬件提供的信号源,系统就可以按照信号中断来计数,计数在固定频率下对应固定的时间,根据预设的时间参数即可产生定时中断信号,这就是软定时。

本文主要整理 Linux 系统开发中常使用的软定时器,而硬件定时器涉及到硬件手册这里略过。

本文会在持续更新过程中将常用定时器逐一整理出来。

1. alarm()


#include <unistd.h>
unsigned int alarm(unsigned int __seconds);

当时间到达 __seconds秒后,进程会受到一个 SIGALRM 的信号。当 __seconds 设置为0时,当前的 alarm定时器将退出。

返回值是一个无符号整型类型。返回之前闹钟的剩余秒数,如果之前未设闹钟则返回0。

注意:

  • 每个进程只允许设置一个闹钟,重复设置会覆盖前一个闹钟;

  • 当经过指定的 __seconds 之后,信号由内核产生;

可以通过函数signal注册该信号的回调处理函数callback_fun:


#include <signal.h>

typedef void (*sighandler_t)(int);
sig_t signal(int signum, sighandler_t handler);

举例:


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

/*闹钟信号处理函数*/
void sig_handler(int signal) {
 
    printf("hello world: %d\n", signal);
}
 
/*主函数*/
int main() {
    int i;
    signal(SIGALRM, sig_handler);

    alarm(5);

    for (i = 0; i < 8; i++)
    {
        printf(" sleep % d ... \n", i);
        sleep(1);
    }

    return 0;
}

该例子中,首先通过 signale() 捕捉 SIGALRM 信号,并通过 sig_handler() 进行处理,接着通过函数 alarm() 注册了一个 5s 的定时器,然后通过一个 for 循环进行睡眠等待 SIGALRM 到来。当信号 SIGALRM 到来后进程会转到sig_handler() 处执行,执行完该函数后才会回到 main() 中继续执行。

执行结果如下:


 sleep  0 ...
 sleep  1 ...
 sleep  2 ...
 sleep  3 ...
 sleep  4 ...
hello world: 14
 sleep  5 ...
 sleep  6 ...
 sleep  7 ...

其他信号相关的信息可以查看:《进程间通信——信号(Signal)》

2. setitimer()

setitimer() 类似于 alarm(),同样是通过闹钟,只不过该函数可以精确到微秒。


#include <sys/time.h>

int getitimer(int which, struct itimerval* current_value);
int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);

参数:

  • which 定时器计时的方式:

  • ITIMER_REAL: 真实时间,时间到达时发送 SIGALRM;

  • ITIMER_VIRTUAL: 用户时间,时间到达时发送 SIGVTALRM;

  • ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达时发送 SIGPROF信号;

  • new_value 定时器新的定时值;

  • old_value 定时器旧的定时值,最开始设置为 NULL;

返回时:

  • 设置成功时,返回0;

  • 设置失败时,返回-1,并设置 errno;

下面来看下数据结构 itimerval:


#include <linux/time.h>

struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
};

struct itimerval {
    struct timeval it_interval;
    struct timeval it_value;
};

如果it_value 中两个值都为0,表示关闭定时器;如果it_value 中至少一个不为0,则表示打开定时器;

如果it_interval中两个值都为0,表示定时器只执行1次;如果 it_interval 中至少一个不为0,则表示定时器是周期性工作;

将上面的实例进行简单的修改:


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

/*闹钟信号处理函数*/
void sig_handler(int signal) {
 
    printf("hello world: %d\n", signal);
}
 
/*主函数*/
int main() {
    int i;
    signal(SIGALRM, sig_handler);

    struct itimerval new_timer;
    new_timer.it_interval.tv_sec = 1;
    new_timer.it_interval.tv_usec = 0;
    new_timer.it_value.tv_sec = 2;
    new_timer.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL, &new_timer, NULL);

    for (i = 0; i < 8; i++)
    {
        printf(" sleep % d ... \n", i);
        sleep(1);
    }

    return 0;
}

该例子中,首先通过 signale() 捕捉 SIGALRM 信号,并通过 sig_handler() 进行处理,接着通过函数 setitimer() 注册了一个 1s 的定时器,2s后开始执行,此处与 alarm() 不同,alarm() 指定的定时器触发时进行 sig_handler() 处理,处理完后回到 main() 中,而此处 setitimer() 设定完定时器的时间后就回到了 main() 函数中等待定时器触发。

执行结果如下:


 sleep  0 ...
 sleep  1 ...
hello world: 14
 sleep  2 ...
hello world: 14
 sleep  3 ...
hello world: 14
 sleep  4 ...
hello world: 14
 sleep  5 ...
hello world: 14
 sleep  6 ...
hello world: 14
 sleep  7 ...
hello world: 14

2s 后定时器触发,开始处理 sig_handler() 且每个 1s 触发一次,而main函数中 for 循环也会同步执行。

3. timer_create()

另外一种依赖信号的定时器为 timer_create(),较 setitimer() 更加灵活,且时间可以精确到纳秒。


#include <time.h>

int timer_create(clockid_t clockid, struct sigevent* event, timer_t* timer_ptr);
int timer_delete(timer_t timer);

int timer_settime(timer_t timer, int flags,
                       const struct itimerspec* new_value,
                       struct itimerspec* old_value);

int timer_gettime(timer_t timer, struct itimerspec* ts);

3.1 timer_create()

timer_create() 用以创建一个 POSIX 内部定时器,将定时器的标识 ID 存放到 timer_ptr 中。

参数:

  • clockid定义了定时器计时的方法,有如下几个值 (定义在 linux/time.h 中):

  • CLOCK_REALTIME : 可设置的系统范围的实时时钟;

  • CLOCK_MONOTONIC : 单调递增的时钟,系统启动后不会被改变;

  • CLOCK_PROCESS_CPUTIME_ID : 用于测量当前进程(包括所有线程)CPU占用时间,包含用户调用和系统调用;

  • CLOCK_THREAD_CPUTIME_ID : 用于测量当前线程CPU占用时间,包含用户调用和系统调用;

  • event 指出该如何通知调用者定时器超时信息,详细的数据结构如下。

  • sigev_notify 指定定时器超时处理的方式;

  • SIGEV_NONE : 定时器超时后不使用异步通知,可能的情况是使用timer_gettime来监控定时器;

  • SIGEV_SIGNAL : 一旦超时,产生一个信号,任何时候,至多只有一个信号会发送到队列里面,可以使用timer_getoverrun来获取超时次数;

  • SIGEV_THREAD : 新建一个线程去处理,该线程执行sigev_notif_function为入口函数;

  • SIGEV_THREAD_ID : linux独有,发出一个信号,和SIG_NAL类似,只不过该信号发送到指定的线程,如果 sigev_notify 设置该值时,需要同时指定 _sigev_un._tid 的值,例如使用 gettid();

  • sigev_signo 用以指定定时器超时时发出的信号的值;例如当sigev_notify 指定 SIGEV_THREAD_ID时,需要有信号发出进行处理,此时 sigev_signo 可以设定 SIGALRM

  • timer_ptr 定时器的标识 ID;

注意:如果event被设置为NULL,相当于SIGEV_SIGNAL,信号是SIGALRM;

返回值:

  • 创建成功时,返回0;

  • 创建失败时,返回-1,并设置 errno;

下面时 sigevent 数据结构:


#include <uapi/asm-generic/siginfo.h>

typedef struct sigevent {
    sigval_t sigev_value;
    int sigev_signo;
    int sigev_notify;
    union {
        int _pad[SIGEV_PAD_SIZE];
         int _tid;

        struct {
            void (*_function)(sigval_t);
            void *_attribute;    /* really pthread_attr_t */
        } _sigev_thread;
    } _sigev_un;
} sigevent_t;

3.2 timer_settime()

timer_settime() 用以启动或停止一个定时器。

参数:

  • timer,由 timer_create() 创建,为定时器的唯一标识;

  • flags,设置定时器时间标识

  • 0,启动一个相对定时器,基于当前时间 + 指定的 new_value;

  • TFD_TIMER_ABSTIME,使用绝对时间的定时器,由参数 new_value 决定;

  • TFD_TIMER_CANCEL_ON_SET,如果实时时钟发生改变,退出绝对时间定时器;

  • new_value,定时器新的定时值,根据flags 不同有不同的含义;

  • old_value,定时器旧的定时值,最开始设置为NULL,如果为非NULL,表示之前设置过;

返回值:

  • 创建成功时,返回0;

  • 创建失败时,返回-1,并设置 errno;

下面来看下数据结构 itimerspec:


#include <uapi/linux/time.h>

struct timespec {
    time_t tv_sec;
    long   tv_nsec;
};

struct itimerspec {
    struct timespec it_interval;  /*定时器的时间周期,interval*/
    struct timespec it_value;     /*定时器的时间值,timeout*/
};

如果it_value 中两个值都为0,表示关闭定时器;如果it_value 中至少一个不为0,则表示打开定时器;

如果it_interval中两个值都为0,表示定时器只执行1次;如果 it_interval 中至少一个不为0,则表示定时器是周期性工作;

3.3 timer_delete()

用以删除定时器。参数时 timer_create() 时创建的 timer 唯一标识;

3.4 timer_gettime()

timer_gettime() 用于查询 timer 对应定时器设定的当前时间值。

参数:

  • timer,由 timer_create() 创建,为定时器的唯一标识;

  • ts,返回的当前时间值;

返回值:

  • 查询成功时,返回0;

  • 查询失败时,返回-1,并设置 errno;

3.5 举例


#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
#include <string.h>

/*闹钟信号处理函数*/
void timer_process(int signal) {
 
    printf("hello world: %d\n", signal);
}

timer_t timer_;

bool create_timer(sigset_t *sigset) {
    struct sigevent sevent;

    sigemptyset(sigset);
    sigaddset(sigset, SIGALRM);
    if (sigprocmask(SIG_BLOCK, sigset, NULL)) {
        printf("sigprocmask failed: %s\n", strerror(errno));
        return false;
    }

    sevent.sigev_notify = SIGEV_SIGNAL;
    sevent.sigev_signo = SIGALRM;
    if (timer_create(CLOCK_MONOTONIC, &sevent, &timer_)) {
        printf("timer_create failed: %s\n", strerror(errno));
        return false;
    }

    return true;
}

bool start() {
    struct itimerspec new_timer;


    new_timer.it_value.tv_sec = 2;
    new_timer.it_value.tv_nsec = 0;
    new_timer.it_interval.tv_sec = 1;
    new_timer.it_interval.tv_nsec = 0;

    if (timer_settime(timer_, 0, &new_timer, NULL)) {
        printf("timer_settime failed: %s\n", strerror(errno));
        return false;
    }

    return true;
}

/*主函数*/
int main() {
    sigset_t sigset;
    int signum;
    
    if (!create_timer(&sigset)) {
        printf("timer creation failed!\n");
        return 0;
    }
    
    start();
    
    while (true) {
        if (sigwait(&sigset, &signum) == -1) {
            printf("sigwait failed: %s\n", strerror(errno));
        }

        timer_process(signum);
    }

    return 0;
}

该例子核心处理有三个地方:

  • create_timer() 封装了创建 timer 的过程,这里使用信号集指定 SIGALRM 信号,接着使用 timer_create() 创建定时器;

  • start() 封装了定时器时间的设定,主要是调用 timer_settime() 启动定时器;

  • while() 循环利用sigwait() 进行 SIGALRM 等待,如果捕获到信号,则会调用 timer_process() 进行处理流程;

4. timerfd

这是以文件描述符的形式监听时间变化,通常跟select/poll/epoll 配合使用。timerfd 涉及三个接口函数:


#include <sys/timerfd.h>

int timerfd_create(clockid_t clockid, int flags);

int timerfd_settime(int fd, int flags,
                        const struct itimerspec* new_value,
                        struct itimerspec* old_value);

int timerfd_gettime(int fd, struct itimerspec* current_value);

4.1 timerfd_create()

timerfd_create() 用以创建一个定时器描述符。

参数:

  • clockid定义了定时器计时的方法,有如下几个值 (定义在 linux/time.h 中):

  • CLOCK_REALTIME : 可设置的系统范围的实时时钟;

  • CLOCK_MONOTONIC : 单调递增的时钟,系统启动后不会被改变;

  • CLOCK_PROCESS_CPUTIME_ID : 用于测量当前进程(包括所有线程)CPU占用时间,包含用户调用和系统调用;

  • CLOCK_THREAD_CPUTIME_ID : 用于测量当前线程CPU占用时间,包含用户调用和系统调用;

  • flags 描述符创建标识

  • TFD_NONBLOCK 以非阻塞形式打开描述符,节约额外调用 fcntl() 函数;

  • TFD_CLOEXEC 为新打开的描述符设置 close-on-exec 选项,在 fork + exec后新进程自动关闭该 fd。同样的可以节约额外 open() 调用;

返回值:

  • 创建成功时,返回新的 fd;

  • 创建失败时,返回-1,并设置 errno;

4.2 timerfd_settime()

timerfd_settime() 用以启动或停止一个定时器。

参数:

  • fd,由 timerfd_create() 创建,为定时器的文件描述符;

  • flags,设置定时器时间标识

  • 0,启动一个相对定时器,基于当前时间 + 指定的 new_value;

  • TFD_TIMER_ABSTIME,使用绝对时间的定时器,由参数 new_value 决定;

  • TFD_TIMER_CANCEL_ON_SET,如果实时时钟发生改变,退出绝对时间定时器;

  • new_value,定时器新的定时值,根据flags 不同有不同的含义;

  • old_value,定时器旧的定时值,最开始设置为NULL,如果为非NULL,表示之前设置过;

返回值:

  • 创建成功时,返回0;

  • 创建失败时,返回-1,并设置 errno;

下面来看下数据结构 itimerspec:


#include <uapi/linux/time.h>

struct timespec {
    time_t tv_sec;
    long   tv_nsec;
};

struct itimerspec {
    struct timespec it_interval;  /*定时器的时间周期,interval*/
    struct timespec it_value;     /*定时器的时间值,timeout*/
};

如果it_value 中两个值都为0,表示关闭定时器;如果it_value 中至少一个不为0,则表示打开定时器;

如果it_interval中两个值都为0,表示定时器只执行1次;如果 it_interval 中至少一个不为0,则表示定时器是周期性工作;

4.3 timerfd_gettime()

timerfd_gettime() 用于查询 fd对应定时器设定的当前时间值。

参数:

  • fd,由 timerfd_create() 创建,为定时器的文件描述符;

  • current_value,返回的当前时间值;

返回值:

  • 查询成功时,返回0;

  • 查询失败时,返回-1,并设置 errno;

4.4 read() 和 close()

timerfd 归根就是一个文件描述符,当配合 poll/epoll 收到监听定时器超时,需要通过read() 读取文件描述符中的buffer,该buffer 是一个无符号 8bytes 的整型数(uint64_t),表示该定时器超时的次数。如果没有超时,read() 将会进行阻塞,阻塞到下一次定时器超时。另外,如果提供的buffer 大小 < 8bytes,read() 将返回EINVAL,read()成功则返回 8.

在不需要定时器的时候,记得通过 close() 进行关闭。

4.5 举例


#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/eventfd.h>
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <unistd.h>

#define EPOLL_SIZE_HINT    128

int mEpollFd = -1;

int fd_process()
{
    struct epoll_event eventItems[EPOLL_SIZE_HINT];
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_SIZE_HINT, -1);
        
    int timerFd = -1;
    int eventIndex = 0;
    uint64_t readCounter;

    if (eventCount < 0) {
        printf("Poll failed with an unexpected error: %s\n", strerror(errno));
        return -1;
    }

    for (; eventIndex < eventCount; ++eventIndex) {
        timerFd = eventItems[eventIndex].data.fd;

        int retRead = read(timerFd, &readCounter, sizeof(uint64_t));
        if (retRead < 0) {
            printf("read %d failed...\n", timerFd);

            continue;
        } else {
            printf("SUCCESS.....\n");
        }
    }
    
    return 0;
}

/*主函数*/
int main()
{
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    
    int fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK);
    if (fd < 0) {
        printf("Could not create timer fd: %s\n", strerror(errno));
        return 0;
    }

    itimerspec timerSet;
    timerSet.it_interval.tv_sec = 1;
    timerSet.it_interval.tv_nsec = 0;
    timerSet.it_value.tv_sec = 2;
    timerSet.it_value.tv_nsec = 0;
    if (timerfd_settime(fd, 0, &timerSet, NULL) != 0) {
        printf("timerfd_settime failed: %s\n", strerror(errno));
        close(fd);
        return 0;
    }

    struct epoll_event eventItem;
    memset(&eventItem, 0, sizeof(epoll_event));
    eventItem.events = EPOLLIN | EPOLLET;
    eventItem.data.fd = fd;
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, &eventItem);
    if (result != 0) {
        printf("Could not add timer fd(%d) to epoll instance: %s\n", fd, strerror(errno));
    }
    
    while(true) {
        if (fd_process() < 0)
            break;
    }

    return 0;
}

在main() 函数中创建了epoll,通过 timerfd_create() 创建了 timerfd,并通过 timerfd_settime() 创建定时器,定时器的周期为1s,2s 后工作。最后通过 while 循环等待定时器工作 fd_process(),执行结果为:


SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....
SUCCESS.....

因为定时器周期为 1s,所以每隔 1s 会通过 read() 读取到定时器超时。

详细的 epoll 使用原理可以查看:《Linux 中的 epoll 原理及使用》

参考:

http://t.zoukankan.com/houjun-p-4885148.html

### 回答1: 在Linux C实现定时器,我们可以使用信号处理机制来实现。具体步骤如下: 1. 引入头文件:包括<sys/time.h> 和 <signal.h>。 2. 定义信号处理函数:在信号处理函数定义定时器到期时的操作。例如,可以在信号处理函数输出一条定时器到期的消息。 3. 设置定时器:使用setitimer函数来设置定时器的间隔时间和初始启动时间,并指定信号处理函数。setitimer函数需要传入一个结构体itimerval作为参数,该结构体包含两个成员:it_value代表第一次到期的时间,it_interval代表后续到期的时间间隔。 4. 阻塞信号:使用sigaction函数阻塞相关信号,以免在处理定时器到期时被其他信号打断。 5. 开启定时器:使用信号处理函数来触发定时器,并在定时器到期时执行相关操作。 以下是一个简单的示例代码: ```C #include <stdio.h> #include <sys/time.h> #include <signal.h> void timer_handler(int signum) { printf("Timer expired!\n"); } int main() { struct itimerval timer; // 设置定时器间隔为2秒,并初始化定时器 timer.it_value.tv_sec = 2; timer.it_value.tv_usec = 0; timer.it_interval.tv_sec = 2; timer.it_interval.tv_usec = 0; // 设置信号处理函数 signal(SIGALRM, timer_handler); // 设置定时器 setitimer(ITIMER_REAL, &timer, NULL); // 阻塞相关信号 sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGALRM); sigprocmask(SIG_BLOCK, &mask, NULL); // 循环等待定时器到期 while(1) { pause(); } return 0; } ``` 以上代码,我们通过设置定时器的间隔时间和初始启动时间来控制定时器的重复触发。在信号处理函数,我们通过输出一条消息来表示定时器到期的事件。在主函数,我们首先设置信号处理函数,然后设置定时器并开启定时器,并最后通过循环等待定时器到期来保持程序的运行。 ### 回答2: 在Linux C编程,实现定时器可以使用信号机制来达到目的。下面是一个简单的例子,展示了如何实现一个定时器。 首先,需要包含头文件`<unistd.h>`和`<signal.h>`,以便使用相关的函数和宏定义。 然后,定义一个用于处理定时器的信号处理函数,例如命名为`timer_handler`。在这个函数,可以执行特定的操作作为定时器触发后的处理逻辑。在下面的例子,我们只是简单地打印一条消息。 接下来,创建一个`timer_t`类型的变量,用于存储定时器的ID。可以使用`timer_create`函数创建一个新的定时器,并传入相关的参数,如定时器类型、信号处理函数等。 然后,使用`timer_settime`函数设置定时器的时间参数,包括初始时间和间隔时间。在下面的例子,我们将定时器设置为3秒后启动,并且每隔5秒触发一次。 最后,使用`sleep`函数使程序暂停,以便触发定时器。在实际应用,可以根据需要将这个定时器与其他功能集成。 下面是一个完整的例子代码: ```c #include <unistd.h> #include <signal.h> #include <stdio.h> void timer_handler(int signum) { printf("Timer expired.\n"); } int main() { // 创建一个新的定时器 timer_t timerid; struct sigevent sev; struct itimerspec its; sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGALRM; sev.sigev_value.sival_ptr = &timerid; timer_create(CLOCK_REALTIME, &sev, &timerid); // 设置定时器参数 its.it_value.tv_sec = 3; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 5; its.it_interval.tv_nsec = 0; timer_settime(timerid, 0, &its, NULL); // 暂停程序,等待定时器触发 sleep(20); return 0; } ``` 在上述的例子,我们创建了一个3秒后启动的定时器,并且每隔5秒触发一次。程序将在主函数的`sleep`函数处暂停20秒,期间定时器会触发三次,并打印"Timer expired."的消息。 ### 回答3: 在Linux C,我们可以使用`timer_create()`函数来创建一个定时器。该函数接受四个参数:一个时钟ID,一个用于保存定时器 ID 的变量,一个结构体指针以指定定时器的属性,以及一个可选的回调函数。 要创建一个定时器,首先需要定义一个 `timer_t` 类型的变量来保存定时器 ID。然后,要使用 `timer_create()` 函数创建定时器,并将定时器 ID 保存到该变量。 接下来,需要定义一个结构体变量来指定定时器的属性。可以使用 `struct sigevent` 结构体,并根据需要设置其成员变量。例如,我们可以将 `sigev_notify` 成员设置为 `SIGEV_THREAD`,以指定定时器到期时,将调用一个线程执行回调函数。 在回调函数,可以执行想要执行的操作。可以在回调函数做一些计算、输出等,或者执行某个函数或方法。 接下来,我们需要使用 `timer_settime()` 函数来启动定时器,并设置执行回调函数的时间间隔。此函数接受四个参数:定时器 ID、指定定时器何时到期的结构体指针、一个用于保存之前定时器设置的结构体指针,并通过第四个参数来指定相对于哪个时间来设置定时器。 综上所述,实现定时器的步骤如下: 1. 定义 `timer_t` 类型的变量来保存定时器 ID。 2. 使用 `timer_create()` 函数创建定时器。 3. 定义一个结构体变量来指定定时器的属性。 4. 在回调函数定义要执行的操作。 5. 使用 `timer_settime()` 函数启动定时器,并设置执行回调函数的时间间隔。 需要注意的是,创建定时器的函数及相关数据结构在`<time.h>`头文件声明。在使用定时器时,可能还需要使用信号和线程相关的函数和数据结构。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

私房菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值