Linux操作系统分析与设计——基于mykernel的一个简单程序以及时间片轮转多道程序内核代码分析(学号:357)

Linux内核实验(二):shell命令解释系统 一、引言Shell命令解释程序中元字符的处理是shell中一个强大的功能,利用对各种字符的不同解释可以充分挖掘出内核的各种强大的潜能。例如利用“&”符号启动并发的后台进程,利用“<”、“>”符号启动I/O重定向,利用“|”启动管道读写等。同样也可以实现其他一些元字符的功能,如“*”通配符,“;”连接符等。二、实验内容问题A 实现一个能处理前后台运行命令的shell 问题B 实现 阅读详情

1.mykernel程序

程序部署

    sudo apt-get install qemu # install QEMU
    sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
    wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # download Linux Kernel 3.9.4 source code
    wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # download mykernel_for_linux3.9.4sc.patch
    xz -d linux-3.9.4.tar.xz
    tar -xvf linux-3.9.4.tar
    cd linux-3.9.4
    patch -p1 < ../mykernel_for_linux3.9.4sc.patch
    make allnoconfig
    make
   	qemu -kernel arch/x86/boot/bzImage

从qemu窗口中您可以看到my_start_kernel在执行,同时my_timer_handler时钟中断处理程序周期性执行。
cd mykernel 您可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c
当前有一个CPU执行C代码的上下文环境,同时具有中断处理程序的上下文环境,我们初始化好了系统环境。
您只要在mymain.c基础上继续写进程描述PCB和进程链表管理等代码,在myinterrupt.c的基础上完成进程切换代码,一个可运行的小OS kernel就完成了。
start to write your own OS kernel,enjoy it!

运行结果

实验过程截图如下:
安装qemu
解压内核源码及打补丁
编译内核结束
使用qemu运行内核

代码分析

关闭运行着的qemu,进入mykernel查看源代码,可以看到以下文件
在这里插入图片描述
打开mymain.c,看到代码
在这里插入图片描述
可以看到这里有个名为“__init my_start_kernel()”的函数,函数体力有个while死循环,不停地打印"my_start_kernel here"这句话,就解释了刚才qemu为何一直打印输出这句话。
myinterrupt.c
这个文件中只有一个名为 “my_timer_handler()”的函数,打印“my_timer_handler”这句话。
通过以上实验我们可以发现,mykernel在启动之后,系统调用my_start_kernel函数,并周期性调用my_timer_handler函数。

2.时间片轮转多道批处理程序内核代码分析

程序部署和运行

(1)获取源码,感谢孟老师在 https://github.com/mengning/mykernel 提供的代码。修改mymain.c myinterrupt.c mypcb.h三个文件。

(2)从github中copy文件,进入mykernel文件夹,覆盖原来的mymain.c myinterrupt.c 新建mypcb.h。

(3)重新编译内核,在LinuxKernel/linux-3.9.4文件夹下,执行下面的命令。

make allnoconfig          
make
qemu -kernel arch/x86/boot/bzImage

运行结果

重新编译
运行qemu

代码分析

mypcb.h : 进程控制块PCB结构体定义。

mymain.c: 初始化各个进程并启动0号进程。

myinterrupt.c:时钟中断处理和进程调度算法。

mypcb.h:

/*
 *  linux/mykernel/mypcb.h
 *
 *  Kernel internal PCB types
 *
 *  Copyright (C) 2013  Mengning
 *
 */

#define MAX_TASK_NUM        4
#define KERNEL_STACK_SIZE   1024*8

/* CPU-specific state of this task */
struct Thread {
    unsigned long		ip;
    unsigned long		sp;
};

typedef struct PCB{
    int pid;
    volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    char stack[KERNEL_STACK_SIZE];
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long	task_entry;
    struct PCB *next;
}tPCB;

void my_schedule(void);

在这个文件里,定义了 Thread 结构体,用于存储当前进程中正在执行的线程的ip和sp,PCB结构体中的各个字段含义如下

pid:进程号

state:进程状态,在模拟系统中,所有进程控制块信息都会被创建出来,其初始化值就是-1,如果被调度运行起来,其值就会变成0

stack:进程使用的堆栈

thread:当前正在执行的线程信息

task_entry:进程入口函数

next:指向下一个PCB,模拟系统中所有的PCB是以链表的形式组织起来的。

my_schedule声明,在myinterrupt.c中实现,在mymain.c中的各个进程函数会根据一个全局变量的状态来决定是否调用它,从而实现主动调度。

mymain.c:

/*
 *  linux/mykernel/mymain.c
 *
 *  Kernel internal my_start_kernel
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>


#include "mypcb.h"

tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;

void my_process(void);


void __init my_start_kernel(void)
{
    int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    /*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
        task[i].pid = i;
        task[i].state = -1;
        task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
        task[i].next = task[i-1].next;
        task[i-1].next = &task[i];
    }
    /* start process 0 by task[0] */
    pid = 0;
    my_current_task = &task[pid];
	asm volatile(
    	"movl %1,%%esp\n\t" 	/* set task[pid].thread.sp to esp */
    	"pushl %1\n\t" 	        /* push ebp */
    	"pushl %0\n\t" 	        /* push task[pid].thread.ip */
    	"ret\n\t" 	            /* pop task[pid].thread.ip to eip */
    	"popl %%ebp\n\t"
    	: 
    	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
	);
}   
void my_process(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%10000000 == 0)
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
            if(my_need_sched == 1)
            {
                my_need_sched = 0;
        	    my_schedule();
        	}
        	printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}

my_start_kernel 是系统启动后最先调用的函数,在这个函数里完成了0号进程的初始化和启动,并创建了其它的进程PCB,以方便后面的调度。在模拟系统里,每个进程的函数代码都是一样的,即 my_process 函数,my_process 在执行的时候,会打印出当前进程的 id,从而使得我们能够看到当前哪个进程正在执行。

myinterrupt.c:

/*
 *  linux/mykernel/myinterrupt.c
 *
 *  Kernel internal my_timer_handler
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

/*
 * Called by timer interrupt.
 * it runs in the name of current running process,
 * so it use kernel stack of current running process
 */
void my_timer_handler(void)
{
#if 1
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
        my_need_sched = 1;
    } 
    time_count ++ ;  
#endif
    return;  	
}

void my_schedule(void)
{
    tPCB * next;
    tPCB * prev;

    if(my_current_task == NULL 
        || my_current_task->next == NULL)
    {
    	return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<\n");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {
    	my_current_task = next; 
    	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  
    	/* switch to next process */
    	asm volatile(	
        	"pushl %%ebp\n\t" 	    /* save ebp */
        	"movl %%esp,%0\n\t" 	/* save esp */
        	"movl %2,%%esp\n\t"     /* restore  esp */
        	"movl $1f,%1\n\t"       /* save eip */	
        	"pushl %3\n\t" 
        	"ret\n\t" 	            /* restore  eip */
        	"1:\t"                  /* next process start here */
        	"popl %%ebp\n\t"
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	); 
 	
    }
    else
    {
        next->state = 0;
        my_current_task = next;
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
    	/* switch to new process */
    	asm volatile(	
        	"pushl %%ebp\n\t" 	    /* save ebp */
        	"movl %%esp,%0\n\t" 	/* save esp */
        	"movl %2,%%esp\n\t"     /* restore  esp */
        	"movl %2,%%ebp\n\t"     /* restore  ebp */
        	"movl $1f,%1\n\t"       /* save eip */	
        	"pushl %3\n\t" 
        	"ret\n\t" 	            /* restore  eip */
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	);          
    }   
    return;	
}

my_time_handler()函数,是个时间片轮转,周期性地发出中断信号,也就是my_need_sched。my_start_kernel()完成每个进程初始化,每个进程的任务都是my_process(),由于这个函数中有个无限循环,任务永远不会结束;并且启动了0号进程。任务需要调度时根据任务链表顺序进行调度。

总结

参考了不少同学的博客,收获很大,这里也引用某位学姐的博客来总结一下。

1.怎么理解中断上下文和进程上下文切换?

可以想象个场景,例如,我们工作时突然来了一个打扰(如电话),需要把手头工作进行到哪里了保存在大脑里,方便接完电话后继续回到工作状态。不论是中断上下文还是进程上下文的切换,都是基于这个原理。

2.怎么理解内核态和用户态?

打个比方,图书馆中,读者可以自由地检索目录、阅读书籍,却无权直接去书库取书,也无权修改图书馆的数据库。而工作人员就在特权状态,可以访问任意资源。这样的划分是为了系统的稳定和安全,也是免去了用户和系统底层打交道的麻烦。

2.怎么理解系统调用?

图书馆中,读者想要借书时,只能填写借书单,由工作人员完成登记和取书工作。“借书单”起到了一个“接口”的作用。在系统中运行的应用程序通过系统调用与内核通信,也是通过系统调用界面陷入内核。而“接口”是实现的关键。

基于Linux操作系统C语言开发的多人聊天室程序设计与实现.docx 里面附有源代码,加上详细的设计步骤,使用了C语言开发,功能有服务器和客户端的消息接收与发送,以及退出功能 立即下载

相关推荐

基于嵌入式实时操作系统的程序设计技术.pdf

基于嵌入式实时操作系统的程序设计技术,全面掌握操作系统的软件开发

实验一 命令解释程序设计

一、实验目的 1、探索、理解并掌握操作系统命令解释器的设计原理和实现机,基于 Linux 内核进行相应命令解释程序的设计和实现; 2、通过本实验掌握Linux 操作系统平台及GCC编译器的编译、调试方法; 3、掌握虚拟机平台的使用、管理。 二、实验内容 利用C语言编写一个微型命令解释程序minishell.c,该程序可接收并解释以下命令: (1) dir 列出当前目录 (2) cop file1 file2 拷贝文件 (3) era filename 删除文件 (4) disp ...

guleilei0的博客 1179

Linux 操作系统内核实验-Shell命令解释系统设计

Shell命令解释系统设计: 问题A: 实现一个能处理前后台运行命令的shell 问题B: 实现一个带有管道功能的shell 问题C: 实现一个能处理I/O重定向的shell 问题D: 实现一个能在一行上处理多条命令的shell

Linux内核设计与实现 读书笔记

第三章 进程管理 1. fork系统调用从内核返回两次: 一次返回到子进程,一次返回到父进程 2. task_struct结构是用slab分配器分配的,2.6以前的是放在内核栈的栈底的;所有进程的task_struct连在一起组成了一个双向链表 3. 2.6内核的内核栈底放的是thread_info结构,其中有指向task_struct的指针; 4. current宏可以找到当前进程的...

weixin_30535167的博客 149

Linux命令解释程序的设计.zip

Linux命令解释程序的设计.zip

fibplus-8.0.0.zip

FIBPlus 是一套面向 Delphi / C++Builder 的 Firebird、InterBase 数据库访问组件,特点是直接走原生客户端 API,不经 BDE/ODBC,速度快且可控性强。8.0.0 版本主要适配 Delphi 2009 及以后的版本(Delphi 6–2007 需用 7.9.x),核心组件包括 TpFIBDatabase、TpFIBTransaction、TpFIBDataSet 等,支持精细事务管理、本地缓存、BLOB 处理和备份恢复等数据库服务。该库原为 Devrace 的商业组件,现由社区在 GitHub 的 madorin/fibplus 项目开源维护,建议从官方仓库获取源码而非第三方网盘压缩包。安装时解压后用对应版本的工程组(10.4+ 统一为 `FIB.groupproj`)Build 全部包,再 Install `dclFIB` 并在 Library 路径加入 Source 目录即可。新项目中若需官方技术支持,也可考虑 FireDAC、IBDAC 等更现代的替代方案。

《Linux内核设计与实现》读书总结

Linux内核设计与实现 进程管理 进程:处于执行器的程序,包含代码段,打开的文件,信号,内核内部数据,内存地址空间,多个线程,存放全局变量的数据段 线程:进程中活动的对象,内核调度的单位,包含独立程序计数器,进程栈,一组进程寄存器 Linux对进程与线程不做特殊的区分 进程描述符:task_struct的数据结构,位于内核双向循环链表的任务队列中;进程描述符包含:打开的文件,地...

c295655244的博客 1888

基于K8S + apisix/istio 实现云原生模块化微服务.自带代码生成器,能够生成对表的增删改查的逻辑代码

基于K8S + apisix/istio 实现云原生模块化微服务.自带代码生成器,能够生成对表的增删改查的逻辑代码.是标准Maven项目,只依赖springboot,没有hibernate,struts,ibatis,数据库调优可以使用druid输出慢sql,比分析xml中的语句更直观,springrain所有的sql语句都使用Finder封装管理,只要查看Finder的引用即可.

分析linux的shell程序,linux 命令解释程序的实现(shell)

操作系统的实验,实现简单的命令解析和系统调用。实验内容:(1)parse应提供一个命令提示符,如parse>,表示接收用户的输入,每次执行完成后再打印下一个命令提示符parse>。当用户没有输入时,parse需要一直处于随时等待输入状态,同时再屏幕上显示一些必要的信息。(2)如果用户提交了一个内部命令,parse应当按照下面的描述执行相应动作。quit:结束所有的子进程并退出par...

weixin_36482579的博客 696

工艺部2026年终总结及2027工作规划PPT.pptx

制造业生产、工艺、设备、质量全套 PPT 资料,包含月度经营复盘、季度数据分析、部门年终总结规划、产线与检验规范、TPM 设备保全、质量意识、质量总监高阶培训等课件。内容兼顾经营指标偏差分析、述职汇报、现场制度宣贯、岗位技能教学。 适合操作工、检验员、设备维保员、工艺 / 品质 / 设备工程师、生产主管、各部门部长、质量总监、厂长、内部培训师、精益专员使用,覆盖一线执行、骨干分析、部门负责人述职、高管进阶全层级,可直接用于工厂汇报、内部培训、述职复盘。

wordbuddy破甲经验分享

wordbuddy破甲经验分享

全桥LLC谐振(变频+移相混合控制)变换器闭环仿真模型(Simulink仿真实现)

内容概要:本文详细介绍了基于Simulink平台构建的全桥LLC谐振变换器闭环仿真模型,采用变频控制与移相控制相结合的混合调制策略,实现高效电能转换与高精度输出稳压。模型完整涵盖了系统建模、谐振腔参数设计、软开关技术实现、双闭环控制系统(如电压外环与频率/相位内环)设计及仿真验证全过程,重点突出零电压开通(ZVS)与零电流关断(ZCS)的实现机制,以显著降低开关损耗,提升变换器效率与动态响应性能。配套资源包含完整的Simulink仿真文件、详细的参数设计报告及核心参考文献,为深入理解LLC谐振变换器的工作机理、控制策略优化及其在开关电源、新能源发电等领域的工程应用提供了坚实的技术支撑。; 适合人群:具备电力电子技术、自动控制理论基础,从事高频开关电源、新能源变换器、车载充电机或相关方向研究的研究生、科研人员及电气工程师。; 使用场景及目标:①系统学习全桥LLC谐振变换器的精确建模与仿真方法;②掌握变频+移相混合控制策略的设计原理与实现技巧;③开展高效率、高功率密度电源变换器的科研攻关与工程项目开发;④深入理解软开关技术在实际电路中的应用与优势; 阅读建议:建议结合所提供的仿真模型与设计报告进行动手实践,重点分析控制器参数整定过程、谐振元件(Lr, Cr, Lm)的参数匹配关系,并仔细观察仿真波形,以深刻把握系统的稳态特性、瞬态响应能力及控制稳定性,从而全面提升对复杂谐振变换器系统的设计与分析水平。

双闭环PWM整流器VOC控制 +SVPWM调制仿真(Simulink仿真实现)

内容概要:本文详细介绍了一种基于Simulink的双闭环PWM整流器采用虚拟同步发电机控制(VOC)策略与空间矢量脉宽调制(SVPWM)技术的仿真模型。该模型在dq同步旋转坐标系下构建电压定向控制系统,实现了电压外环与电流内环的双闭环控制结构,重点阐述了VOC控制原理、SVPWM调制算法的实现方法及其在Simulink环境中的模块化搭建过程。通过该仿真平台,能够有效实现单位功率因数运行、直流母线电压稳定控制以及优异的动态响应性能,适用于电力电子变换器、新能源并网系统及电机驱动等领域的控制策略验证与性能评估。; 适合人群:电气工程、自动化、电力电子与电力传动等相关专业的高年级本科生、研究生,以及从事电力电子系统仿真、新能源并网控制研究的科研人员和技术工程师。; 使用场景及目标:①深入理解三相PWM整流器在dq坐标系下的数学建模与控制机理;②掌握VOC控制策略的核心思想及其相较于传统矢量控制的优势;③学习SVPWM调制技术的实现流程与Simulink建模技巧;④通过仿真实验验证双闭环控制系统的静态精度与动态稳定性,提升对先进电力电子控制系统的分析与设计能力。; 阅读建议:建议结合电力电子技术、现代控制理论和电机控制相关基础知识进行学习,按照仿真模型的结构逐步分析各功能模块的信号流向与参数配置,重点关注锁相环(PLL)、坐标变换、PI控制器设计及SVPWM模块的实现细节,并可通过调整控制参数观察系统响应变化,以深化对控制策略动态特性的理解。

LLC谐振变换器恒压恒流双竞争闭环控制simulink仿真(附说明文档)

内容概要:本文提供了一套针对LLC谐振变换器恒压恒流双竞争闭环控制的完整Simulink仿真资源,涵盖详细的仿真模型、设计说明文档及配套PPT讲解材料。该资源聚焦于电力电子领域中LLC谐振DC-DC变换器的核心控制策略,重点实现了在不同负载条件下恒压(CV)与恒流(CC)模式之间的无缝切换与双闭环竞争控制机制,确保系统具备高稳定性与动态响应性能。仿真平台基于Matlab/Simulink构建,适用于半桥或全桥LLC拓扑结构,内容包括谐振参数设计、变频控制(PFM)、软启动策略、反馈环路建模及闭环稳定性分析等关键技术环节,具有较强的工程实践价值和科研复现意义。; 适合人群:面向电力电子、电气工程及其自动化等相关专业的研究生、科研人员以及从事开关电源、新能源变换器研发的工程师,尤其适合具备基本电路理论和Simulink仿真操作经验的技术人员;; 使用场景及目标:①深入理解LLC谐振变换器的工作原理及其在恒压恒流输出控制中的实现方法;②支撑科研项目中对模式切换逻辑、软开关实现、系统抗扰能力与稳定性分析等关键问题的研究;③作为毕业设计、学术论文复现或工业级电源产品原型开发的重要技术参考;; 其他说明:资源配套提供了详尽的参数设计指导与PPT解析,有助于快速掌握建模思路与控制器整定方法,建议使用者结合自身应用场景调整谐振腔参数与PI控制器增益,以实现最优性能,同时可通过公众号“荔枝科研社”获取持续的技术支持与扩展资料。

储能双向Buck-Boost+模型预测电流控制+电压下垂控制仿真【附参考文献】(Simulink仿真实现)

内容概要:本文系统介绍了基于Simulink平台构建的储能双向Buck-Boost变换器仿真模型,融合模型预测电流控制与电压下垂控制策略,面向锂电池储能系统的高效充放电管理。该模型深入实现了双闭环控制架构的设计与动态性能分析,涵盖系统建模、关键控制算法实现及稳定性验证,并配套提供参考文献以支撑理论深度与技术复现性。资源包还整合了多种典型电力电子拓扑的仿真案例,如各类DC-DC变换器、谐振变换器、PWM整流器等,充分体现了其在新能源发电、微电网能量调度及电力系统稳定控制中的广泛应用价值和技术延展性。; 适合人群:适用于电气工程、自动化、电力电子及相关专业的高校研究生、科研人员以及从事能源系统仿真与控制设计的工程技术人员;要求具备一定的Simulink/MATLAB操作能力及电力电子与自动控制基础知识;; 使用场景及目标:①深入理解双向Buck-Boost电路在储能系统中的工作机理与能量双向流动控制方法;②掌握模型预测控制(MPC)与下垂控制在实际电力变换系统中的建模流程与参数整定技巧;③为科研课题、学位论文或工程项目提供高可靠性、可复用的仿真原型与技术参考方案; 阅读建议:建议结合文中列出的参考文献与网盘提供的完整仿真资源,按照目录结构循序渐进地学习各模块案例,重点剖析控制逻辑搭建、调节器设计与系统动态响应特性,通过动手仿真与参数调试深化对控制策略性能与鲁棒性的理解。

产业园区在推动科技成果落地时,如何高效匹配企业与技术供给方.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

上一篇: 将UDP客户端和服务端集成到MenuOS里并跟踪分析UDP协议中数据包的收发处理过程
下一篇: Java数组声明及初始化
There Is No Code
博客等级 码龄10年 254粉丝 51原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

There Is No Code

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

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

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

打赏作者

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

抵扣说明:

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

余额充值