POSIX 定时器

看到一篇非常详细的POSIX定时器的讲解,mark一下。

POSIX 定时器 主要分创建,初始化以及删除三个操作,对应的函数分别为:

timer_create()(创建定时器)
timer_settime()(初始化定时器)
timer_delete(销毁定时器)

首先需要创建一个定时器对象,设置通知类型,一般包括信号、脉冲或线程创建,并创建通知结构(结构sigevent),设置定时类型 (相对与绝对,一次与周期),最后启动它。

1. 创建定时器
int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid)
      进程可以通过调用timer_create()创建特定的定时器,定时器是每个进程自己的,不是在fork时继承的。clock_id说明定时器是基于哪个时钟的,*timerid装载的是被创建的定时器的ID。该函数创建了定时器,并将他的ID 放入timerid指向的位置中。参数evp指定了定时器到期要产生的异步通知。如果evp为NULL,那么定时器到期会产生默认的信号,对 CLOCK_REALTIMER来说,默认信号就是SIGALRM。如果要产生除默认信号之外的其它信号,程序必须将 evp->sigev_signo设置为期望的信号码。struct sigevent 结构中的成员evp->sigev_notify说明了定时器到期时应该采取的行动。通常,这个成员的值为SIGEV_SIGNAL,这个值说明在定时器到期时,会产生一个信号。程序可以将成员evp->sigev_notify设为SIGEV_NONE来防止定时器到期时产生信号。

   如果几个定时器产生了同一个信号,处理程序可以用 evp->sigev_value来区分是哪个定时器产生了信号。要实现这种功能,程序必须在为信号安装处理程序时,使用struct sigaction的成员sa_flags中的标志符SA_SIGINFO。

clock_id取值为以下:

CLOCK_REALTIME :Systemwide realtime clock. 标准POSIX定义的时钟,如果它处于省电模式,基于这个时钟的定时器可以唤醒处理器

CLOCK_SOFTTIME: 这个时钟只在处理器不处于节电模式时才激活。例如,使用CLOCK_SOFTTIME计时器休眠的应用程序不会在应用程序应该唤醒时唤醒处理器。这将允许处理器进入省电模式。

CLOCK_MONOTONIC:Represents monotonic time. Cannot be set. 这个时钟总是固定定时增加,无法调整

CLOCK_PROCESS_CPUTIME_ID :High resolution per-process timer.

CLOCK_THREAD_CPUTIME_ID :Thread-specific timer.

CLOCK_REALTIME_HR :High resolution version of CLOCK_REALTIME.

CLOCK_MONOTONIC_HR :High resolution version of CLOCK_MONOTONIC.

第二个参数:sigevent数据结构的指针。这个数据结构用于通知内核,在“触发”时,计时器应该传递什么类型的事件。

struct sigevent
 
{
 
    int sigev_notify; //notification type
 
    int sigev_signo; //signal number
 
    union sigval   sigev_value; //signal value
 
    void (*sigev_notify_function)(union sigval);
     
    pthread_attr_t *sigev_notify_attributes;
 
}
 
union sigval
 
{
 
    int sival_int; //integer value
 
    void *sival_ptr; //pointer value
 
}


通过将evp->sigev_notify设定为如下值来定制定时器到期后的行为:

SIGEV_NONE:什么都不做,只提供通过timer_gettime和timer_getoverrun查询超时信息。

SIGEV_SIGNAL: 当定时器到期,内核会将sigev_signo所指定的信号传送给进程。在信号处理程序中,si_value会被设定会sigev_value。

SIGEV_THREAD: 当定时器到期,内核会(在此进程内)以sigev_notification_attributes为线程属性创建一个线程,并且让它执行sigev_notify_function,传入sigev_value作为为一个参数。

 

2.  设置定时器
timer_create()所创建的定时器并未启动。要将它关联到一个到期时间以及启动时钟周期。使用timer_settime()的参数组合完成,timer_settime()函数用于实际启动计时器:

int timer_settime (timer_t timerid,       int flags,    struct itimerspec *value,      struct itimerspec *oldvalue);
       timerid参数是从timer_create()函数调用中得到的值,创建一组计时器,然后在它们上分别调用timer_settime()来设置和启动它们。
        flags参数就是指定绝对与相对标志。如果传递常量TIMER_ABSTIME,则value所指定的时间值会被解读成绝对值(此值的默认的解读方式为相对于当前的时间)。这个经修改的行为可避免取得当前时间、计算“该时间”与“所期望的未来时间”的相对差额以及启动定时器期间造成竞争条件。在希望计时器关闭的时候传递实际的日期和时间。如果传递一个0,那么计时器将被视为相对于当前时间。
       value 参数指定时间。以下是两个数据结构的关键部分():

struct timespec
{    
    long    tv_sec,
    long    tv_nsec;
};
struct itimerspec 
{
     struct timespec it_value,//单次启动时间
     struct timespec it_interval;//定时时间
};


如同settimer(),it_value用于指定当前的定时器到期时间。当定时器到期,it_value的值会被更新成it_interval 的值。如果it_interval的值为0,则定时器不是一个时间间隔定时器,一旦it_value到期就会回到未启动状态。timespec的结构提供了纳秒级分辨率:

ovalue的值不是NULL,则之前的定时器到期时间会被存入其所提供的itimerspec。如果定时器之前处在未启动状态,则此结构的成员全都会被设定成0。

获得一个活动定时器的剩余时间:

int timer_gettime(timer_t timerid,struct itimerspec *value);
 
取得一个定时器的超限运行次数:

   有可能一个定时器到期了,而同一定时器上一次到期时产生的信号还处于挂起状态。在这种情况下,其中的一个信号可能会丢失。这就是定时器超限。程序可以通过调用timer_getoverrun来确定一个特定的定时器出现这种超限的次数。定时器超限只能发生在同一个定时器产生的信号上。由多个定时器,甚至是那些使用相同的时钟和信号的定时器,所产生的信号都会排队而不会丢失。

int timer_getoverrun(timer_t timerid);
   执行成功时,timer_getoverrun()会返回定时器初次到期与通知进程(例如通过信号)定时器已到期之间额外发生的定时器到期次数。举例来说,在我们之前的例子中,一个1ms的定时器运行了10ms,则此调用会返回9。如果超限运行的次数等于或大于DELAYTIMER_MAX,则此调用会返回DELAYTIMER_MAX。

   执行失败时,此函数会返回-1并将errno设定会EINVAL,这个唯一的错误情况代表timerid指定了无效的定时器

3. 删除定时器
int timer_delete (timer_t timerid);
   一次成功的timer_delete()调用会销毁关联到timerid的定时器并且返回0。执行失败时,此调用会返回-1并将errno设定会 EINVAL,这个唯一的错误情况代表timerid不是一个有效的定时器。

4. 示例
4.1 采用通知方式为信号(signal)的处理方式
4.1.1:使用SIGALRM信号量定时

上一篇讲到,如果使用信号量SIGALRM,(对 CLOCK_REALTIMER来说,默认信号就是SIGALRM)sleep()函数使用的就是实时时钟CLOCK_REALTIMER
所以使用信号值SIGALRM会中断sleep(int second)函数的休眠;

下面给出一个例子:

//timercreate_demo.cpp
#include <unistd.h> 
#include <stdio.h>
#include <signal.h>
#include <time.h>
 
void SignHandler(int iSignNo);
void testTimerSign();
void printTime();
 
int main() {
    testTimerSign();
    while(true){
         int left = sleep(5);
         printTime();
         printf("sleep(5)(left=%d)\n", left);
    }
    return 0; 
}
 
void SignHandler(int iSignNo){
    //printTime();
    if(iSignNo == SIGUSR1){
        printf("Capture sign no : SIGUSR1\n"); 
    }else if(SIGALRM == iSignNo){
        //printf("Capture sign no : SIGALRM\n"); 
    }else{
        printf("Capture sign no:%d\n",iSignNo); 
    }
}
 
void testTimerSign(){
    struct sigevent evp;  
    struct itimerspec ts;  
    timer_t timer;  
    int ret;  
    evp.sigev_value.sival_ptr = &timer;  
    evp.sigev_notify = SIGEV_SIGNAL;  
    evp.sigev_signo = SIGALRM;
    signal(evp.sigev_signo, SignHandler); 
    ret = timer_create(CLOCK_REALTIME, &evp, &timer);  
    if(ret) {
        perror("timer_create");
    } 
    ts.it_interval.tv_sec = 1;
    ts.it_interval.tv_nsec = 0;  
    ts.it_value.tv_sec = 1;
    ts.it_value.tv_nsec = 0;  
    printTime();
    printf("start\n");
    ret = timer_settime(timer, 0, &ts, NULL);  
    if(ret) {
        perror("timer_settime"); 
    } 
}
 
void printTime(){
    struct tm *cursystem;
    time_t tm_t;
    time(&tm_t);
    cursystem = localtime(&tm_t);
    char tszInfo[2048] ;
    sprintf(tszInfo, "%02d:%02d:%02d", 
        cursystem->tm_hour, 
        cursystem->tm_min, 
        cursystem->tm_sec);
        printf("[%s]",tszInfo);
}


因为timer_settime()中定时器间隔时间为1秒
于是sleep(5)每次都被打断不能按时休眠,剩余4秒未能执行;

4.1.2 示例2 SIGALRM信号量不同线程的影响
因为休眠sleep(unsigned int)为线程内操作
所以如果不同线程,信号量SIGALRM是不能中断sleep();

下面用一个例子说明:

//timercreate_demo.cpp
#include <unistd.h> 
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <pthread.h>  
 
void SignHandler(int iSignNo);
void testTimerSign();
void printTime();
void *function(void *arg);
 
int main() {
    pthread_t thread1;  
    pthread_create(&thread1,NULL,function,(char*)"111"); 
    testTimerSign();
    while(true);
    return 0; 
}
 
void SignHandler(int iSignNo){
    if(iSignNo == SIGUSR1){
        printf("Capture sign no : SIGUSR1\n"); 
    }else if(SIGALRM == iSignNo){
        //printf("Capture sign no : SIGALRM\n"); 
    }else{
        printf("Capture sign no:%d\n",iSignNo); 
    }
}
 
void testTimerSign(){
    struct sigevent evp;  
    struct itimerspec ts;  
    timer_t timer;  
    int ret;  
    evp.sigev_value.sival_ptr = &timer;  
    evp.sigev_notify = SIGEV_SIGNAL;  
    evp.sigev_signo = SIGALRM;
    signal(evp.sigev_signo, SignHandler); 
    ret = timer_create(CLOCK_REALTIME, &evp, &timer);  
    if(ret) {
        perror("timer_create");
    } 
    ts.it_interval.tv_sec = 1;
    ts.it_interval.tv_nsec = 0;  
    ts.it_value.tv_sec = 1;
    ts.it_value.tv_nsec = 0;  
    printTime();
    printf("start\n");
    ret = timer_settime(timer, 0, &ts, NULL);  
    if(ret) {
        perror("timer_settime"); 
    } 
}
 
void printTime(){
    struct tm *cursystem;
    time_t tm_t;
    time(&tm_t);
    cursystem = localtime(&tm_t);
    char tszInfo[2048] ;
    sprintf(tszInfo, "%02d:%02d:%02d", 
        cursystem->tm_hour, 
        cursystem->tm_min, 
        cursystem->tm_sec);
        printf("[%s]",tszInfo);
}
 
void *function(void *arg){  
    char *m;  
    m = (char *)arg;  
    while(true) {
        while(true){
            int left = sleep(3);
            printTime();
            printf("sleep(3)(left=%d)\n", left);
            }
    }
}


注意编译的时候添加 -lpthread

输出结果: 

可以看到主线程的定时器中的信号量SIGALRM是无法中断子线程thread1的休眠;

4.1.3 示例3 使用SIGUSR1信号量定时
 

//signalDemo.cpp
//compile : gcc signalDemo.cpp -o testSignal -lrt
#include <unistd.h>
#include <stdio.h> 
#include <signal.h>
#include <time.h>
 
void testTimerSign();
void SignHandler(int iSignNo);
void printTime();
 
int main() {
    testTimerSign();
    while(true) {
        sleep(1); 
    }
    return 0; 
}
 
void SignHandler(int iSignNo){
    printTime();
    if(iSignNo == SIGUSR1){
        printf("Capture sign No.=SIGUSR1\n"); 
    }else{
        printf("Capture sign No.=%d\n",iSignNo); 
    }
}
 
void testTimerSign(){
    struct sigevent evp;  
    struct itimerspec ts;  
    timer_t timer;  
    int ret;  
    evp.sigev_value.sival_ptr = &timer;  
    evp.sigev_notify = SIGEV_SIGNAL;  
    evp.sigev_signo = SIGUSR1;  
    signal(evp.sigev_signo, SignHandler); 
    ret = timer_create(CLOCK_REALTIME, &evp, &timer);  
    if(ret) {
        perror("timer_create");
    } 
    ts.it_interval.tv_sec = 1; // the spacing time  
    ts.it_interval.tv_nsec = 0;  
    ts.it_value.tv_sec = 2;  // the delay time start
    ts.it_value.tv_nsec = 0;  
    printTime();
    printf("start\n");
    ret = timer_settime(timer, 0, &ts, NULL);  
    if(ret) {
        perror("timer_settime"); 
    } 
}
 
void printTime(){
    struct tm *cursystem;
    time_t tm_t;
    time(&tm_t);
    cursystem = localtime(&tm_t);
    char tszInfo[2048] ;
    sprintf(tszInfo, "%02d:%02d:%02d", 
        cursystem->tm_hour, cursystem->tm_min, 
        cursystem->tm_sec);
    printf("[%s]",tszInfo);
}


输出结果:

4.1.4 示例4 信号量为signaction的处理函数
 

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
 
#define CLOCKID CLOCK_REALTIME
 
void sig_handler(int signo)
{
    printf("timer_signal function! %d\n", signo);
}
 
int main()
{
    //XXX int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    // signum--指定的信号编号,可以指定SIGKILL和SIGSTOP以外的所有信号编号
    // act结构体--设置信号编号为signum的处理方式
    // oldact结构体--保存上次的处理方式
    //
    // struct sigaction 
    // {
    // void (*sa_handler)(int);            //信号响应函数地址
    // void (*sa_sigaction)(int, siginfo_t *, void *);   //但sa_flags为SA——SIGINFO时才使用
    // sigset_t sa_mask;         //说明一个信号集在调用捕捉函数之前,会加入进程的屏蔽中,当捕捉函数返回时,还原
    // int sa_flags;
    // void (*sa_restorer)(void);    //未用
    // };
    //
    timer_t timerid;
    struct sigevent evp;
 
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = sig_handler;
    act.sa_flags = 0;
 
    // XXX int sigaddset(sigset_t *set, int signum);  //将signum指定的信号加入set信号集
    // XXX int sigemptyset(sigset_t *set);            //初始化信号集
    
    sigemptyset(&act.sa_mask);
 
    if (sigaction(SIGUSR1, &act, NULL) == -1)
    {
        perror("fail to sigaction");
        exit(-1);
    }
 
    memset(&evp, 0, sizeof(struct sigevent));
    evp.sigev_signo = SIGUSR1;
    evp.sigev_notify = SIGEV_SIGNAL;
    if (timer_create(CLOCK_REALTIME, &evp, &timerid) == -1)
    {
        perror("fail to timer_create");
        exit(-1);
    }
 
    struct itimerspec it;
    it.it_interval.tv_sec = 2;
    it.it_interval.tv_nsec = 0;
    it.it_value.tv_sec = 1;
    it.it_value.tv_nsec = 0;
    if (timer_settime(timerid, 0, &it, 0) == -1)
    {
        perror("fail to timer_settime");
        exit(-1);
    }
 
    pause();
 
    return 0;
}


输出结果:

4.2 采用新线程派驻的通知方式
4.2.1 示例5 简单函数线程的处理方式

 

#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
 
#define CLOCKID CLOCK_REALTIME
 
void timer_thread(union sigval v)
{
    printf("timer_thread function! %d\n", v.sival_int);
}
 
int main()
{
    // XXX int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid);
    // clockid--值:CLOCK_REALTIME,CLOCK_MONOTONIC,CLOCK_PROCESS_CPUTIME_ID,CLOCK_THREAD_CPUTIME_ID
    // evp--存放环境值的地址,结构成员说明了定时器到期的通知方式和处理方式等
    // timerid--定时器标识符
    timer_t timerid;
    struct sigevent evp;
    memset(&evp, 0, sizeof(struct sigevent));    //清零初始化
 
    evp.sigev_value.sival_int = 111;        //也是标识定时器的,这和timerid有什么区别?回调函数可以获得
    evp.sigev_notify = SIGEV_THREAD;        //线程通知的方式,派驻新线程
    evp.sigev_notify_function = timer_thread;    //线程函数地址
 
    if (timer_create(CLOCKID, &evp, &timerid) == -1)
    {
        perror("fail to timer_create");
        exit(-1);
    }
 
    // XXX int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value,struct itimerspec *old_value);
    // timerid--定时器标识
    // flags--0表示相对时间,1表示绝对时间,通常使用相对时间
    // new_value--定时器的新初始值和间隔,如下面的it
    // old_value--取值通常为0,即第四个参数常为NULL,若不为NULL,则返回定时器的前一个值
    
    //第一次间隔it.it_value这么长,以后每次都是it.it_interval这么长,就是说it.it_value变0的时候会装载it.it_interval的值
    //it.it_interval可以理解为周期
    struct itimerspec it;
    it.it_interval.tv_sec = 1;    //间隔1s
    it.it_interval.tv_nsec = 0;
    it.it_value.tv_sec = 1;        
    it.it_value.tv_nsec = 0;
 
    if (timer_settime(timerid, 0, &it, NULL) == -1)
    {
        perror("fail to timer_settime");
        exit(-1);
    }
 
    pause();
 
    return 0;
}
/*
 * int timer_gettime(timer_t timerid, struct itimerspec *curr_value);
 * 获取timerid指定的定时器的值,填入curr_value
 *
 */


输出结果:

4.2.2 示例6  使用pthread 创建线程的处理方法

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <error.h>
#include <errno.h>
#include <string.h>
#include <pthread.h> 
#include <sys/wait.h>
  
 
void timer_thread(union sigval v)  
{  
    printf("timer_thread function! %d\n", v.sival_int);  
}  
 
int init_timer(timer_t *timerid, struct sigevent *evp, struct itimerspec *it)
{
    if ( !evp || !it )
        return -1;
 
    memset(evp, 0, sizeof(struct sigevent));   //清零初始化  
  
    evp->sigev_value.sival_int = 111;        //也是标识定时器的,这和timerid有什么区别?回调函数可以获得  
    evp->sigev_notify = SIGEV_THREAD;        //线程通知的方式,派驻新线程  
    evp->sigev_notify_function = timer_thread;   //线程函数地址  
 
    if (timer_create(CLOCK_REALTIME, evp, timerid) == -1)  
    {  
        perror("fail to timer_create");  
        return -1;;  
    }  
 
    printf("timer_create timerid = %d\n", *timerid);
    it->it_interval.tv_sec = 1;  // 后续按照该时间间隔 
    it->it_interval.tv_nsec = 0;  
    it->it_value.tv_sec = 3;     // 最初开始时间间隔 
    it->it_value.tv_nsec = 0;  
 
    return 0;
}
 
int start_timer(timer_t *timerid, struct itimerspec *it)
{
    if (it == NULL){
        return -1;
    }
    if (timer_settime(*timerid, 0, it, NULL) == -1)  
    {  
        perror("fail to timer_settime");  
        return -1;
    }  
    
    return 0;
}
 
void* thread_func(void *param)
{
    int a = *(int *)param;
    while(1){
        sleep(1);
        printf("This is thread..\n");
    }
 
    a = 100;
    printf("param = %d\n", a);
}
 
int main(int argc, const char *argv[])
{
    pid_t pid = 0;
    pthread_t thread;
    timer_t timerid = 0;
    int ret;
    struct sigevent evp;  
    struct itimerspec it;  
 
    int a = 10;  
#if 0
 
    int ret = init_timer(&timerid, &evp, &it);
    if (ret < 0){
        printf("init_timer failed\n");
        return -1;
    }
#endif
  
    if ((pid = fork()) < 0)
    {
        printf("fork failed.\n");
        return -1;
    }
    else if ( pid == 0){
        printf("child proc..\n");
 
        ret = pthread_create(&thread, NULL, thread_func, &a);//最后一个参数为void*类型,传递变量的地址,其实其他的类型也遵循这个原则
 
        int ret = init_timer(&timerid, &evp, &it);
        if (ret < 0){
            printf("init_timer failed\n");
            return -1;
        }
        sleep(2);
        printf("child timer_Id addr = %d\n", timerid);
        start_timer(&timerid, &it);
 
        sleep(10);
        exit(0);
    }
    else{
        printf("I'm parent proc..\n");
        printf("parent timer_Id addr = %d\n", timerid);
        printf("pthread_id = %d\n", thread);
        do {
            ret = waitpid(pid, NULL, WNOHANG);
            if (ret == 0){
                printf("No child exit\n");
                sleep(2);
            }
            else if (ret == pid){
                printf("Successly get child %d\n", pid);
            }
            else
                printf("something error\n");
        }while(ret == 0);
    
        /*ret = waitpid(pid, NULL, 0);*/
        /*if (ret == pid)*/
            /*printf("successly get child %d\n", pid);*/
 
    }
    pause();
    return 0;
}
输出结果:

 

5.  参考链接:
QNX-----定时器的使用

timer_create()建立定时器剖析

POSIX定时器:timer_settime()

linux下定时器timer_create()的使用

linux下定时器的使用--timer_create等系列
————————————————
版权声明:本文为CSDN博主「ppipp1109」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/p942005405/article/details/100883568

  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
POSIX定时器库中,有三个主要的函数用于操作定时器,它们分别是timer_create()、timer_settime()和timer_delete()。 这些函数可以帮助我们创建定时器、设置定时器的时间和操作定时器的删除。 此外,POSIX定时器库还提供了一套API来处理定时器事件。通过产生一个sigevent事件,来通知进程定时器事件的产生。这样,我们可以在定时器到期时执行一些特定的操作。 在POSIX定时器库中,我们还可以使用CLOCK_REALTIME时钟,它是一个系统范围的实时时钟。基于这个时钟的定时器可以在处理器处于省电模式时唤醒处理器并触发相应的事件。 通过使用这些函数和时钟,我们可以实现各种定时器功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [POSIX 定时器](https://blog.csdn.net/Little_Eyelash/article/details/117918411)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [POSIX定时器](https://blog.csdn.net/m0_52152959/article/details/110354542)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值