基于mjpg-streamer-r63的源码分析之:基础知识详细解释[一]

mjpg-stream的移植 MJPG简介:  MJPG是MJPEG的缩写,但是MJPEG还可以表示文件格式扩展名.   MJPEG   全名为 “Motion Joint Photographic Experts Group”,是一种视频编码格式,   Motion JPEG技术常用与闭合电路的电视摄像机的模拟视频信号“翻译”成视频流,并存储在硬盘上。典型的应用如数字视频记录器等。MJPEG不像MPEG,不使用帧间编码, 阅读详情

***************************************************************************************************************************
作者:EasyWave                                                                                 时间:2012.07.27

类别:linux应用之mjpg-streamer分析                                         声明:转载,请保留链接

注意:如有错误,欢迎指正。这些是我学习的日志文章......

***************************************************************************************************************************

        在mjpg-streamer的开源的网络视频服务器项目中,在代码中会经常用到线程,在linux下的线程thread,下面来详细的分析和学习一下linux系统下的线程,如果是在ARM嵌入式系统中的应用程序要用到线程thread的话,需要在文件系统将libpthread-0.9.30.1.so,当然这个版本是需要看具体的arm-linux的编译器版本中所包含的版本号,只需拷贝到文件系统下的lib文件夹中,同时还需要将lib的路径配置好,这样应用程序能够找到这个libpthread-0.9.30.1.so库。这样就不会出现错误了。
一:线程的建立和退出
       相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,而且拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程则正好相反。
通过pthread_create()函数来建立线程API 定义如下(POSIX线程相关的函数和变量定义都在头文件pthread.h):

int pthread_create(pthread_t  *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void * arg)

参数:
      thread:线程id返回值(无符号长整型数,bits/pthreadtypes.h);

      attr: 默认值设置为NULL。(bits/pthreadtypes.h定义了结构体pthread_atttr_t);
     属性值包括:
            可分离状态(detached state):PTHREAD_CREATE_JOINABLE(缺省值);PTHREAD_CREATE_DETACHED。
            调度策略(scheduling policy):SCHED_OTHER(正常、非实时,缺省值)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)。后两种调度策                   略仅对超级用户有效,可通过函数pthread_setschedparam()来改变。
            调度参数(scheduling parameter):一个sched_param的机构,仅有一个整形变量表示线程运行的优先级。这个参数仅当调度策略为实时(SCHED_RR 和                               SCHED_FIFO)时才有效,可通过函数pthread_setschedparam()来设置,缺省值为0。
            继承属性( inheritsched attribute ) :PTHREAD_EXPLICIT_SCHED ( 默认值) 和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策
               略和调度参数(即attr中的值),而后者表示继承调用者线程的值。
            范围(scope):表示线程间竞争CPU 的范围,也就是说线程优先级的有效范围。POSIX 的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM 和                                                PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU 时间,后者表示仅与同进程中的线程竞争CPU 。目前LinuxThreads 仅实现了
               PTHREAD_SCOPE_SYSTEM 值。
            守护池大小(guard size):表示线程守护池的大小,该属性控制守护池(guard area)的大小,直接影响到线程堆栈。该属性能有效抑制线程堆栈指针的溢出。
            堆栈地址(stack address):创建的线程堆栈的起始地址,最小值为PTHREAD_STACK_SIZE。
            堆栈大小( stack size ) : 堆栈大小, 该大小不小于PTHREAD_STACK_SIZE,当然也受限于系统限制。


void * (*start_routine)(void *):指向线程函数的函数指针(仅含有一个void *类型的参数)。

*arg:arg是void *类型的变量,但它同样可以作为任意类型的参数传给start_routine()函数。


二:线程资源释放的方法

Linux程序设计中,创建线程时调用pthread_create()函数。第二个参数attr为线程属性指针,一般情况下,我们创建线程时,若对线程属性没有特殊要求,都将此参数设为NULL,也就是使用了线程的默认属性--分离状态(joinable,或称可接合状态)。接下来主线程必须在适当的时候调用pthread_join(),同步(join,或等待,同步)子线程,同时释放线程本身占用的资源;否则,线程资源将驻留内存,直到整个进程退出为止。若该进程不断的创建线程,则每创建一次线程都会导致内存资源的消耗,很明显,这已经构成了内存泄漏!

对于线程资源的释放,有两种实现方法:
(1) 线程创建时,默认属性是可接合的(joinable),那就需要主线程来等待,所以在创建这个线程后适当的地方必须调用pthread_join()来等待子线程结束执行,否则就会引起内存泄漏!调用pthread_join()来等待子线程执行结束,这是 Linux同步主线程和子线程的一种机制,同时释放子线程的资源(线程描述符和堆栈(thread descriptor and stack))。假如使用默认线程属性,即线程属性为joinable,而又没有调用pthread_join(),那么该进程退出前,所创建的线程占用的资源便不会被释放(kind of like a zombie process),因此造成内存泄漏。假如你不想或没有必要同步主线程和子线程,那么就把子线程属性设置为detached分离状态,那么子线程结束执行后会自行销毁其占用的资源
(2) 将线程属性设为分离状态(detached),这样子线程就属于“自灭”:子线程函数启动后跟主线程不再有“父子”关系(等待和被等待),退出线程时其资源会自动释放。注意:创建线程时,若属性参数为NULL,则线程属性默认为可接合的(joinable,即需要主线程等待的)。可以在线程创建时将其属性设为分离状态(detached),也可在线程创建后将其属性设为分离的(detached)

1):线程建立代码

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *print_msg_function( void *ptr );
int main(int argc, char *argv[])
{
    pthread_t thread1 , thread2;
    char *msg1 = "Thread 1" ;
    char *msa2 = "Thread 2" ;
    int iret1 , iret2;
    /* 创建两个线程执行相同的函数 */
    iret1 = pthread_create( &thread1, NULL, print_msg_function, (void*)msg1 );
    iret2 = pthread_create( &thread2, NULL, print_msg_function, (void*)msg2 );
    /* 等待线程完成 */
    pthread_join( thread1, NULL);
    pthread_join( thread2, NULL);
    printf("Thread 1 returns: % d\n" ,iret1 );
    printf("Thread 2 returns: % d\n" ,iret2 );
    exit(0);
}
void *print_msg_function( void *ptr )
{
    char *msg;
    msg = (char * )ptr;
    printf("% s \n" , msg);
}

编译运行:
编译:
C 编译器: cc -lpthread pthread 1 .c
或
C ++ 编译器: g++ -lpthread pthread 1 .c
运行:./a.out
结果:
Thread 1
Thread 2
Thread 1 returns: 0
Thread 2 returns: 0


2):线程设置为joinable

#include <pthread.h>
void* thread_function (void* thread_arg)
{
    /* 线程要完成的工作 */
    ……
    pthread_exit(“Exiting from the thread _function!”);
}
int main(int argc, char *argv[])
{
    pthread_t thr;
    void* thread_result;
    pthread_create (& thr, NULL, & thread_function, NULL);
    /* 主线程做的工作 */
    ……
    /* 等待线程结束退出 */
    pthread_join(thr, & thread_result);
    return 0;
}
3):线程设置为detached

/* 线程创建后,通过调用pthread_detach()来设置 */
#include <pthread.h>
void* thread_function (void* thread_arg)
{
     /* 线程要完成的工作 */
     ……
     pthread_exit(“Exiting from the thread _function!”);
}
int main(int argc, char *argv[])
{
     pthread_t thread;
     pthread_create(&thread, NULL, &thread_function, NULL);
     pthread_detach(thread); /* D o w ork here... */
     /* 主线程做的工作 */
     ……
     /* 不需要join等待线程退出 */
     return 0;
}

三:线程同步

       尽管在Posix 中同样可以使用IPC 的信号量机制来实现互斥锁mutex功能,但显然semphore的功能过于强大了,在Posix 中定义了另外一套专门用于线程同步的mutex函数

1):互斥锁创建和注销

         POSIX定义了两种方法创建互斥锁,静态方式和动态方式:
         pthread_mutex_t  mutex = PTHREA D_M UTEX_IN ITIA LIZER ;
         int  pthread _mutex_init(pthread_mutex_t  *mutex, const pthread_mutex attr_t  *mutex_attr);
         int  pthread _mutex_destroy(pthread_mutex_t  *mutex);

参数:
*mutex:pthread_mutex_t结构体;
*mutex_attr:指定互斥锁属性(属性的问题,大家可以到网络上去搜索啦,这里不多将了),如果为NULL,则使用缺省属性。Linux 实现中,

用宏PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁;动态方式是采用pthread_mutex_init()函数来初始化互斥锁。销毁一个互斥锁即意味着释放它所占用的资源,且要求互斥锁当前处于开放状态。但在Linux 中,互斥锁并不占用任何资源,因此销毁互斥锁pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有
其他动作。

2):锁操作

        锁操作主要包括加锁(调用函数pthread_mutex_lock())、解锁(调用函数pthread_mutex_unlock() ) 和测试加锁( 调用函数pthread_mutex_trylock())三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
        int pthread _mutex_lock(pthread_mutex_t   *m utex)
        int pthread _mutex_unlock(pthread_mutex_t   *m utex)
        int pthread _mutex_trylock(pthread_mutex_t   *m utex)

pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

线程锁操作示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *function_count(void);
pthread_mutex_t  mutex1 = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
int main(int argc, char *argv[])
{
    int rc1 , rc2;
    pthread_t thread1 , thread2;
    /* 创建两个独立的线程,分别都调用函数 function_count */
    if( (rc1=pthread_create( &thread1, NULL,(void *)function_count, NULL)) )
    {
         printf("Thread creation failed : % d\n" , rc1 );
    }
    if( (rc2=pthread_create( &thread2, NULL,(void *)function_count, NULL)) )
    {
        printf("Thread creation failed : % d\n" , rc2);
    }
    /* 等待线程结束 */
    pthread_join( thread1, NULL);
    pthread_join( thread2, NULL);
    exit(0);
}
void *function_count(void)
{
    pthread_mutex_lock( &mutex1 );
    counter++;
    pthread_mutex_unlock( &mutex1 );
    printf("C ounter value: % d\n" ,counter);
}

四:条件变量

       条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待“条件变量的条件成立”而挂起;另一个线程使“条件成立”(给出条件成立信号)。为了防止竞争,条件变量总是和互斥锁结合起来使用。

1):POSIX定义了两种方法创建条件变量,静态方式和动态方式:
       pthread_cond_t cond = PTH READ_COND_INITIA LIZER;
       int pthread _cond_init(pthread_cond_t  *cond, pthread_condattr_t  *cond_attr);
       int pthread _cond_destroy(pthread_cond_t  *cond);

参数:
*cond: pthread_cond_t的结构体;
*cond_attr:指定条件变量属性。尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。Linux实现中,用宏PTHREAD_COND_INITIALIZER静态初始化条件变量;动态方式是采用pthread_cond_init()函数来初始化条件变量。注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程等待该条件变量时才能注销该条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。

2):条件变量的等待和激发
       条件变量的等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

int pthread _cond_wait(pthread_cond_t  *cond, pthread_mutex_t  *m utex)
int pthread _cond_timedwait(pthread_cond_t  *cond, pthread_mutex_t  *m utex, const struct tim espec *abstim e)
int pthread_cond_signal(pthread_cond_t  *cond)
int pthread_cond_broadcast(pthread_cond_t  *cond)

         无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求条件等待pthread_cond_wait()(或pthread_cond_timedwait()。mutex互斥锁必须为普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),而且调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock())。在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁;条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入
pthread_cond_wait()后的解锁动作对应(实际上pthread_cond_wait()完成的操作包括:--解锁--挂起等待--加锁--,与pthread_cond_wait()前后的加锁、解锁正好对应)。
激发条件有两种形式:pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

        具体的实例这个就不说了,自己可以到网上去搜索。但主要的一点是:为了防止竞争,条件变量总是和互斥锁结合起来使用。这个在mjpg-streamer的代码中得到了体现!

 

下一篇博客将分析getopt_long_only和getopt_long函数的应用:基于mjpg-streamer-r63的源码分析之:基础知识详细解释[二]

 

开源程序mjpg-streamer在arm开发板上的简单应用 mjpg-streamer这个程序依赖libjpeg这个库,因此需要先移植libjpeg库 (1) libjpeg 的移植(例如移植这个版本:libjpeg-turbo-1.2.1): tar xzvf libjpeg-turbo-1.2.1.tar.gz cd libjpeg-turbo-1.2.1 mkdir tmp ./configure --prefix=/work/cross_ 阅读详情

相关推荐

基于mjpg-streamer-r63的源码分析之:基础知识详细解释[一].pdf

基于mjpg-streamer-r63的源码分析之:基础知识详细解释,详细解释其中的代码,知道代码怎么工作的

mjpg-streamer-r63 交叉编译成功 RT5350

今天静下心来,看看shang

2719

从数据手册到代码:一步步解读SC7A20寄存器配置与加速度值换算原理

本文详细解析了SC7A20三轴加速度传感器的寄存器配置与加速度值换算原理。从数据手册出发,逐步拆解关键寄存器设置,并推导原始ADC值到物理加速度的完整换算过程,帮助开发者深入理解传感器底层工作原理,提升嵌入式开发效率。

weixin_29179311的博客 366

基于mjpg-streamer-r63的源码分析之:基础知识详细解释[二]

在mjpg-streamer的程序中,经常会看到getopt_long_only这个函数,这是一个抓取或者说是解析命令行参数的一个linux下的API函数。

嵌入式与人工智能天地 4958

Wio-LR1121多频段LoRa模块技术解析与应用实践

LoRa技术作为低功耗广域物联网(LPWAN)的核心通信方案,通过独特的扩频调制技术实现远距离传输与强抗干扰能力。其工作原理基于CSS(Chirp Spread Spectrum)调制,支持动态调整扩频因子(SF)和带宽(BW)来平衡传输距离与数据速率。Wio-LR1121模块作为Semtech LR1121芯片的载体,创新性地整合了sub-GHz、S-Band和2.4GHz多频段支持,配合仅1.35μA的睡眠电流,为跨境物联网部署提供了硬件基础。在智慧农业、工业监测等场景中,开发者可通过SPI接口快速集成

weixin_33742618的博客 415

ADI DSP开发环境(CCES)下的程序烧录问题(以ADSP-SC589为例)

在CCES环境下将程序烧录到flash主要有两种方式。首先在对项目编译产生文件的属性设置为Release。 在编译完成后,找到每个CORE单独生成的DXE文件: 文件通常位于项目目录下每个core的Release文件中。安装SC589评估板的驱动(ADSP-SC5xx_EZ-KIT_Lite-Rel2.0.0.exe)后,在安装目录下可以找到SPI FLASH芯片的驱动程序(sc589_w25q128fv_dpia_Core1.dxe): 把文件copy到CCES 的目录下由于SC58x处理器的特殊性

daijingxin的博客 3725

基于mjpg-streamer-r63的源码分析之:基础知识详细解释

基于mjpg-streamer-r63的源码分析之:基础知识详细解释[一] http://blog.csdn.net/wavemcu/article/details/7795561 基于mjpg-streamer-r63的源码分析之:基础知识详细解释[二] http://blog.csdn.net/wavemcu/article/details/7795563

u013172071的专栏 637

SAP Gateway Foundation OData V4 服务管理深度解析,从 Service Repository 到 Service Group 的完整运行逻辑

本文解析了SAP S/4HANA中OData V4 API的技术架构,重点阐述Service Repository、Service与Service Group的三层管理体系。以SalesOrder和PurchaseOrder API为例,说明不同服务可能属于特定repository(如srvd_a2x)而非默认DEFAULT库。Service Group作为业务模型组合单元,支持跨服务引用和统一配置,其发布状态直接影响服务可用性。文中对比了后台管理事务/IWBEP/V4_ADMIN与网关端/IWFND/V4

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 82

mjpg-streamer移植与源码分析

1. 如何将mjpg-streamer移植到开发板上 文件系统:fs_mini_mdev_new_auto_wifi_ap.tar.bz2 (1)  libjpeg 的移植 tar xzvf libjpeg-turbo-1.2.1.tar.gz cd libjpeg-turbo-1.2.1 mkdir tmp ./configure --prefix=/work/jz2

xyc310898673的专栏 902

本地Pycharm连接远程服务器详细配置过程(直接在本地使用服务器显卡,很棒)

相信很多人都遇见过这种情况:实验室成员使用同一台服务器,每个人拥有自己的独立账号,我们可以使用服务器更好的配置完成实验,毕竟自己哪有money拥有自己的3090呢。

NorthSmile的博客 4万+

直流无刷电机FOC控制算法 理论到实践 —— 理论(二)

在上一章节,我们对于FOC大体有了一个介绍,本博文将详细描述有关Clark变化、Park变化、反Park变化以及SVPWM的理论实现原理,也即相关数学运算实现。

爱出名的狗腿子 1万+

远程视频监控之应用篇(mjpg-streamer)

通过对mjpg-streamer的源码分析,让小伙伴们知道整个软件的流程。也介绍了如何移植,和设置参数。

若云流风的专栏 1万+

扫地机器人设计与实现——基于嵌入式系统的智能家居解决方案

本文介绍了基于嵌入式技术的扫地机器人设计与实现。通过利用STM32单片机、传感器、驱动系统等硬件组件,并结合路径规划与控制算法、远程控制等软件技术,设计出了一款具有智能避障、路径规划、自动充电等功能的扫地机器人。系统的开发不仅提高了清洁效率,也为进一步研究和优化扫地机器人提供了实践基础。

2201_76125393的博客 1638

mjpeg-streamer的使用记录

文章目录1 简介2 源码下载2.1 mjpeg-streamer下载2.2 libjpeg的下载3 依赖安装3.1 libjpeg的安装4 内核配置4.1 UVC驱动4.2 USB 驱动5 编译6 测试 1 简介 mjpg-streamer是一个命令行应用程序,可将JPEG帧从一个或多个输入插件复制到多个输出插件。 它可用于通过基于IP的网络将JPEG文件从网络摄像头流式传输到各种类型的查看器,如...

Stoneshen的博客 3398

【数据集】全球陆地表面反照率Albedo

【数据集】全球陆地表面反照率Albedo

WW、forever的博客 1452

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

Linux下MJPG-Streamer视频服务器搭建

编译jpeg库 1.解压源码 tar xvf jpegsrc.v8b.tar.gz 2.配置源码 cd jpeg-8b ./configure --prefix=/usr/local/jpeg 3.编译 make 4.安装 make install 5.添加库文件 cd /etc/ld.so.conf.d/ 添加jpeg.co

u010439291的专栏 3078

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

分享一个用QT实现的Mjpeg-streamer客户端(简易版)

mainWindow代码如下(由于篇幅问题,子窗口代码不贴出了,有需要源码的可以留下邮箱): /* * Author : 博客园 Lance# */ #include "mainwindow.h" #include "ui_mainwindow.h" #include <QUrl> #include <QPixmap> #include <...

aiduan6474的博客 1770

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Linux下使用mjpg-streamer实现监控

现在需要让ZC301摄像头在Linux主机上跑起来,使用的是mjpg-streamer视频流服务器。 本人菜鸟,刚开始玩摄像头。通过三天不断的失败和查找之后才发现,2.6.38以后的内核版本已经移除了对 v4l1 的支持,也就是说在高版本内核的系统中的/usr/include目录下找不到头文件,会导致执行./configure的时候会出现错误:“configure: error: Could no

心系欣 6606

mjpg-streamer使用介绍

前几天自己没事玩了一下mjpg-streamer,感觉还不错,这里结合网上的一些资料和自己的心得把主要步骤贴出来供大家参考一下: ps:我在ubuntu11.10和12.04里面都是过,按照以下步骤都没什么问题,我的内核版本是3.2.0的,摄像头使用的是ZC0301PL,开发板是杂牌的,ARM11,核心芯片是S3C6410。PC上也是可以运行的,并不一定非要使用开发板,根据需要修改一下Mak

ASKLW的博客 5819

mjpg-streamer项目源码分析

前一段时间自己买了个开发板(GT2440的),可是我没有够相应的买cmos摄像头,可是又想做下国嵌的usb视频采集和传输的哪个项目,没办法,只好网上找找相关的项目,最终发现了mjpg-streamer这个开源项目。看了blog们的文章,有种激动,于是自己问同学借了个usb摄像头,试了试,挺好使的,而且处理速度上也挺好的,就开始想了解这个项目是怎么工作了(研究了好几天哦)。 下面是...

pengrui18的专栏 2万+
上一篇: Android内核的根文件系统
下一篇: 基于mjpg-streamer-r63的源码分析之:基础知识详细解释[二]
wang-bob
博客等级 码龄20年 356粉丝 112原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值