【嵌入式Linux】Linux内核初识笔记

嵌入式Linux内核相关的基础知识,笔记中所指的内核为Linux2.6版本

授课老师:朱有鹏
时间:2018年12月1日


14.1 Linux内核

从1992年到现在,他的版本太多了。我们要选择合适版本的内核。

内核和发行版的区别:
内核只有一个,发行版有很多
官方网址:www.kernel.org
内核不包括应用程序。卖操作系统的人把应用程序和内核打包在一起,是为发行版。
发行版 = kernel + 应用程序。

1、到底什么是操作系统?
本质是一个程序,他的主要作用就是管理计算机 硬件,给应用程序提供一个运行环境。

2、操作系统核心功能

  1. 内存管理:没有操作系统,内存需要自己管理。不管就会程序间互相占用,程序多了之后内存管理非常麻烦。OS则帮我们管控这些内存
  2. 硬件设备管理
  3. 进程调度
  4. 文件系统:用来管理块设备的一种方式。存储设备由很多个扇区组成,每个扇区有约512个字节。以扇区为单位进行读写。
    如果没有文件系统。程序自己要读写扇区就得记得哪个文件在哪个扇区。文件系统就像是一个索引。为我们提供了 目录和文件名

3、操作系统的扩展功能
协议栈

协议栈是属于作系统的扩展功能
例如windows中默认不集成CAN通信协议栈,所以windows不能进行CAN通信
他有一天想要支持CAN通信,就需要专门对其进行扩展

有用的应用程序包
	比如ping,ifconfig,cd

操作系统中的工作模型

		  APP
		  
----------API-----------

	       OS

----------驱动-----------

		  硬件

内核态和用户态
我们常用的单核CPU在微观上只能做一件事,那么就分为内核态和用户态。

CPU在跑内核代码时是内核态
CPU在跑用户应用时是用户态

区别主要是权限问题。内核态可以随意访问内存,最高的访问权限。用户态不能随便去访问。

驱动故障可能导致整个内核崩溃。所以驱动必须非常小心。
驱动漏洞会使得内核不安全。注意不被别人钻空子。

“所以写驱动不是一件闹着玩的事情。写驱动出了问题不像写应用,他没有访问权限问题。出了问题是要负责的,要一定功力的喂。”

目前驱动的代码越来越多,最近这些年变化比较大的就是驱动。文件系统代码量新增不大,内存管理变化也不大。
2015年4月——安卓4.x
2011年1月——安卓3.0
之前10年发展从1.0到2.6,好多年变一个版本。现在驱动太活跃了,硬件的更新换代太快了,导致内核版本更新也变快了。


2.14.3.内核和应用程序、根文件系统的关联

2.14.3.1、应用和内核的关系
(1)应用程序不属于内核,而是在内核之上的
(2)应用程序工作在用户态,是受限制的。
(3)应用程序故障不会导致内核崩溃
(4)应用程序通过内核定义的API接口来调用内核工作
(5)总结1:应用程序是最终目标
(6)总结2:内核就是为应用程序提供底层资源管理的服务员

2.14.3.2、内核和根文件系统
(1)根文件系统提供根目录。
(2)进程1存放在根文件系统中
(3)内核启动最后会去装载根文件系统。
(4)总结:根文件系统为操作系统启动提供了很多必备的资源:根目录、进程1


模块化设计的体现
(1)配置时可裁剪。
linux内核在编译之前可以进行配置,配置时可以选择将组成内核的成千上万个模块每一个要或者不要。要了之后还有更多的一些细节的配置。
(2)模块化编译和安装。
为了操作方便,逐渐从静态的升级变成了动态的升级(不需要重启系统,更不需要重新烧录系统)。这种动态的升级也是由模块化来支持的。
(3)源码中使用条件编译。
这种在uboot中已经见过了。

模块化设计的好处
(1)功能可裁剪、灵活性
近些年越来越多的驱动安装不再需要重启系统,这种设计归功于模块化设计,动态升级
(2)可扩展性(动态安装卸载、新硬件支持)
(3)利于协作

14.5.选择合适版本的内核

linux内核版本变迁简史
linux0.01。初版
linux0.11。很多讲linux内核源代码解析的书都是以这个版本为原本来讲。《图解linux内核设计的艺术》
linux2.4。比较接近现代的版本,很多经典的书都是以2.4版本内核为参照的,譬如《LDD3》。linux2.4的晚期内核在前几年还会经常碰到有用的。
linux2.6早期。2.6的早期和2.4晚期内核挺像的。
linux2.6晚期。2.6的晚期内核较早期内核有一些改变,尤其是驱动相关的部分和一些头文件的位置。2.6的晚期内核目前还算是比较主流。
linux3.x 4.x

2.14.5.2、如何选择合适的内核版本
并不是越新版本的内核越好
选择SoC厂家移植版本会减少工作量

2.14.5.3、S5PV210适用的内核版本
2.6.35.7+android2.3/QT4.8.3
3.0.8+android4.0


15.LINUX内核的配置

我们用的内核是 2.6.35.7版本的 ,x210_qt文件夹下的kernel,在学习\目录下
注意:新开发板用这个内核,对x210会有触摸屏和分辨率的问题
用起来的话触摸屏肯定不会是好的
移植的时候应该会解决这个问题。也就是我们会有点活儿干

内核源码目录文件简介(单个文件):

.gitignore
.mailmap //国外大神的邮箱
copying //版权
initrd //设备树相关
Kbuild** kernel build,有点像makefile,用于管理linux内核编译的
//多个目录下有Kconfig

Makefile linux内核的总makefile
mk //九鼎,QT自己添加的。有点像cp.sh

linux的可配置性非常高,所以配置他是一个很复杂的事。为了正确配置,他不能像uboot搞一个.h文件用宏定义配置 。uboot的方式很依赖于人的记忆。
linux则需要一套比较傻瓜化的配置体系。内核发明的体系就是我们重点研究的地方。
Kconfig、Kbuild就与配置体系有关


内核源码目录(目录):
arch/ arch是architecture的缩写,意思是架构。
arch目录下是好多个不同架构的CPU的子目录,
譬如 arm这种cpu的所有文件都在arch/arm目录下,
X86的CPU的所有文件都在arch/x86目录下。

crypto/ 英文意思是加密。这个目录下放了一些各种常见的加密算法的C语言代码实现。
譬如crc32、md5、sha1等。

Documentation/ 里面放了一些文档。

drivers/ 驱动目录,里面分门别类的列出了linux内核支持的所有硬件设备的驱动源代码。
firmware/ 固件。什么是固件?固件其实是软件,不过这个软件是固话到IC里面运行的叫固件。就像S5PV210里的iROM代码。
fs/ fs就是file system,文件系统,里面列出了linux支持的各种文件系统的实现。
include/ 头文件目录,公共的(各种CPU架构共用的)头文件都在这里。
每种CPU架构特有的一些头文件在arch/arm/include目录及其子目录下。
init/ init是初始化的意思,这个目录下的代码就是linux内核启动时初始化内核的代码。
ipc/ ipc就是进程间通信,里面都是linux支持的IPC的代码实现。
kernel/ kernel就是内核,就是linux内核
这个文件夹下放的就是内核本身需要的一些代码文件。
lib/。 lib是库的意思,这里面都是一些公用的有用的库函数,注意这里的库函数和C语言的库函数不一样的。在内核编程中是不能用C语言标准库函数,这里的lib目录下的库函数就是用来替代那些标准库函数的。譬如在内核中要把字符串转成数字用atoi,但是内核编程中只能用lib目录下的atoi函数,不能用标准C语言库中的atoi。譬如在内核中要打印信息时不能用printf,而要用printk,这个printk就是我们这个lib目录下的。
mm/。 mm是memory management,内存管理,linux的内存管理代码都在这里。
net/。 该目录下是网络相关的代码,譬如TCP/IP协议栈等都在这里。
scripts/。 脚本,这个目录下全部是脚本文件,这些脚本文件不是linux内核工作时使用的,而是用来辅助对linux内核进行配置编译生产的。我们并不会详细进入分析这个目录下的脚本,而是通过外围来重点学会配置和编译linux内核即可。
security/。安全相关的代码。不用去管。
sound/。 音频处理相关的。
tools/。 linux中用到的一些有用工具
usr/。 目录下是initramfs相关的,和linux内核的启动有关,暂时不用去管。
virt/。 内核虚拟机相关的,暂时不用管。

总结:这么多目录跟我们关系很紧密的就是arch和drivers目录,然后其他有点相关的还有include、block、mm、net、lib等目录。

这里放一张3.14.43内核的源码目录截图,可以看到与2.6版本的差别不是很大=v=

3.14内核源码目录


【内核配置 内核编译】

1 Makefile
	检查交叉编译工具链和ARCH
	ARCH = arm	
	CROSS_COMPILE   ?= /usr/local/arm/arm-2009q3/bin/arm-none-linux-				gnueabi-
	剩下的部分都是正确的
	
2 make x210ii_qt_defconfig				//99%配置
	configuration written to .config成功
	配置完了之后就会产生一个.config/目录,这个目录很重要
	这个目录ls是看不见的,ls -a即可

3 make menuconfig					//还是配置
	注意:第2步配置时失败是不能进行到这一步的
	报错!需要安装ncurses库!apt-get install libncurses5-dev
	画面像是BIOS,我们EXIT就配置完毕了

4 make
	这里用的kernel是九鼎配置好了的,是没有报错的
	一般通过了drivers/下的编译那么就没什么问题了
	编译时间比较长。
	得到zImage,拿到UBOOT中的TFTP去下载,那就没问题了

	可能出现:代码本身的错误 和 未经distclean的错误。

5 编译完成的镜像在 arch/arm/boot目录下,得到的镜像名为zImage

【内核烧写——这里方法随意,本次笔记中是使用了tftp】

Linux端:
	桥接,	虚拟网络编辑器,本地
ifconfig
	把要烧写的东西放进/tftpboot目录下

uboot端:
	
	设置uboot环境变量
		print
		set xxx xxxx	或 set xxx “xxx ; xx”
		ipaddr = 与linux同一个网段的任意ip
		serverid = linux ip

	以上都做好了之后 tftp 30008000 zImage

注意:第三步出现了错误信息
//这是由于没安装ncurses库,需要装这个软件。
*** Unable to find the ncurses libraries or the
*** required header files.
*** ‘make menuconfig’ requires the ncurses libraries.


*** Install ncurses (ncurses-devel) and try again.



15.5.menuconfig的使用和演示

2.15.5.1、使用说明解释
(1)make ,menuconfig中本身自带的提示就有所有的用法,这里只要全部理解就可以了。
(2)menuconfig中间的选择区中有很多个选择项,每个选择项对应.config文件中的一个配置项,每一个选择项都可以被选择和配置操作,选择区中的每一项都是有子目录的,将光标放在选择项上按Enter键可以进入子目录(子目录可能还会有子目录)。选择区太短放不下所有的一个目录层级的选项,可以用箭头按键的向上箭头和向下箭头来上翻和下翻。

注:在menuconfig中操作相关的几个键盘按键,主要是;Enter、ESC、四个方向箭头按键。还有一些特殊字符按键,如/ ?
向上和向下箭头,主要用来在选择项菜单中目录浏览时上下翻
回车,主要作用是选中并且执行select/exit/help。
ESC,主要作用是返回上一层
向左和向右箭头,主要作用是在菜单选项(select、exit、help)间切换。

linux内核中的三种编译方法:编入、去除、模块化
编入:编译链接到zImage
去除:不编译到zImage中
模块化:将模块仍然编译,但不链接到zImage中,而是单独链接成一个内核模块.ko
这样可以动态加载和卸载(未来驱动经常这么搞,很简单的)

menuconfig是从Kconfig中来的。 在Kconfig中删除DM9000的项,menuconfig就没有这项了。
图像是从ncurses库中获取的。是一种文字式的伪图形界面。
注意:Y/N/M状态是没有的,在.config中


以后可能会自己在驱动移植中添加Kconfig中的项,添加到内核的配置项目中

“menuconfig”表示 菜单(本身属于一个菜单,但又有子菜单),config表示项目(无子菜单)
目录关系:一个menuconfig后跟着的所有config就是所有的子菜单

source “xx/xxx/Kconfig”	引入其他的Kconfig
			目录中的每一个Kconfig都会包含子目录的Kconfig

举个栗子:

menuconfig NETDEVICES
    default y if UML
    depends on NET
    bool "Network device support"
    ---help---
      You can say N here if you don't intend to connect your Linux box to
      any other computer at all.

NETDEVICES前面加 CONFIG_ 就构成了.config中的配置项名字。

【自己的驱动:】
自己在内核中添加了一个文件夹,需要添加一个Kconfig
上一层目录 的 Kconfig中,需要source这个Kconfig

tristate “三态”,对应Y/N/M
bool “布尔”,对应Y/N


Linux 的链接脚本由.s文件生成,目的 也是高度的可配置。

搜索文件中的关键字方法
grep “TEXT_OFFSET” * -nR

另外注意:在head.S中,有很多的宏看起来没有定义,实际上是定义了的

【操作系统启动的前提条件】
如果把内核比喻成一个复杂机器,那么start_kernel函数就是把这个机器的众多零部件组装在一起形成这个机器,让他具有可以工作的基本条件。

【操作系统去哪了】
(1)rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd
(2)调用schedule函数开启了内核的调度系统,从此linux系统开始转起来了。
rest_init最终调用cpu_idle函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle。这个函数里面肯定是死循环。

什么是内核线程
(1)进程和线程。简单来理解,一个运行的程序就是一个进程。所以进程就是任务、进程就是一个独立的程序。独立的意思就是这个程序和别的程序是分开的,这个程序可以被内核单独调用执行或者暂停。
(2)在linux系统中,线程和进程非常相似,几乎可以看成是一样的。实际上我们当前讲课用到的进程和线程的概念就是一样的。
(3)进程/线程就是一个独立的程序。应用层运行一个程序就构成一个用户进程/线程,那么内核中运行一个函数(函数其实就是一个程序)就构成了一个内核进程/线程。
(4)所以我们kernel_thead函数运行一个函数,其实就是把这个函数变成了一个内核线程去运行起来,然后他可以被内核调度系统去调度。说白了就是去调度器注册了一下,以后人家调度的时候会考虑你。

截至目前为止,我们一共涉及到3个内核进程/线程。
因此这里涉及到的三个进程分别是linux系统的进程0、进程1、进程2.

(4)我们在ubuntu下ps -aux可以看到当前系统运行的所有进程,可以看出进程号是从1开始的。为什么不从0开始,因为进程0不是一个用户进程,而属于内核进程。
(5)三个进程
进程0 idle进程, 叫空闲进程,也就是死循环。
进程1 kernel_init 函数就是进程1,这个进程被称为init进程。
进程2 kthreadd 函数就是进程2,这个进程是linux内核的守护进程。
这个进程是用来保证linux内核自己本身能正常工作的。

进程0 系统态,内核启动的时候手动构成的
进程1 系统态,由进程0 ,fork产生
进程2 用户态,每个进程都是fork诞生的


【进程1 : init:开始是内核态,后来转变为用户态】
init进程完成了从内核态向用户态的转变

(1)一个进程2种状态。init进程刚开始运行的时候是内核态,它属于一个内核线程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。因为init进程自身完成了从内核态到用户态的过度,因此后续的其他进程都可以工作在用户态下面了

【内核态下做了什么】
重点就做了一件事情,就是挂载根文件系统并试图找到用户态下的那个init程序
init进程要把自己转成用户态就必须运行一个用户态的应用程序(这个应用程序名字一般也叫init)要运行这个应用程序就必须得找到这个应用程序,要找到它就必须得挂载根文件系统,因为所有的应用程序都在文件系统中。

内核源代码中的所有函数都是内核态下面的,执行任何一个都不能脱离内核态。应用程序必须不属于内核源代码,这样才能保证自己是用户态。也就是说我们这里执行的这个init程序和内核不在一起,他是另外提供的。提供这个init程序的那个人就是根文件系统。

【用户态下做了什么】
init进程大部分有意义的工作都是在用户态下进行的。init进程对我们操作系统的意义在于:其他所有的用户进程都直接或者间接派生自init进程。

【init进程的单行线】
如何从内核态跳跃到用户态?
init进程在内核态下面时,通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了。注意这个跳跃过程中进程号是没有改变的,所以一直是进程1.这个跳跃过程是单向的,也就是说一旦执行了init程序转到了用户态下整个操作系统就算真正的运转起来了,以后只能在用户态下工作了,用户态下想要进入内核态只有走API这一条路了。


16.11 init进程的功能

【1 init进程构建了用户交互界面】
(1)init进程是其他用户进程的老祖宗。linux系统中一个进程的创建是通过其父进程创建出来的。根据这个理论只要有一个父进程就能生出一堆子孙进程了。
(2)init启动了login进程、命令行进程、shell进程
(3)shell进程启动了其他用户进程。命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序,每一个应用程序的运行就是一个进程。

【2 打开控制台】
linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。

这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.
这三个文件描述符就是所谓的:
标准输入、标准输出、标准错误。

进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符!

【3 挂载根文件系统】
(1)prepare_namespace函数中挂载根文件系统
(2)根文件系统在哪里?根文件系统的文件系统类型是什么? uboot通过传参来告诉内核这些信息。
uboot传参中的root=/dev/mmcblk0p2 rw 这一句就是告诉内核根文件系统在哪里
uboot传参中的rootfstype=ext3这一句就是告诉内核rootfs的类型。
(3)如果内核挂载根文件系统成功,
则会打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.
如果挂载根文件系统失败,
则会打印:No filesystem could mount root, tried: yaffs2
(4)如果内核启动时挂载rootfs失败,则后面肯定没法执行了,肯定会死。内核中设置了启动失败休息5s自动重启的机制,因此这里会自动重启,所以有时候大家会看到反复重启的情况。

(5)如果挂载rootfs失败,可能的原因有:
最常见的错误就是uboot的bootargs设置不对。
rootfs烧录失败(fastboot烧录不容易出错,以前是手工烧录很容易出错)
rootfs本身制作失败的。(尤其是自己做的rootfs,或者别人给的第一次用)

【执行用户态下的进程1程序】

(1)上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,这个程序就是用户空间的进程1.找到后用run_init_process去执行他
(2)我们如何确定init程序是谁?
方法是:
先从uboot传参cmdline中看有没有指定,如果有指定先执行cmdline中指定的程序。cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序。这里的指定方式就表示我们rootfs的根目录下面有个名字叫linuxrc的程序,这个程序就是init程序。
如果uboot传参cmdline中没有init=xx或者cmdline中指定的这个xx执行失败,还有备用方案。第一备用:/sbin/init,第二备用:/etc/init,第三备用:/bin/init,第四备用:/bin/sh。
如果以上都不成功,则认命了,挂掉了=.=

=======================================

16.12.cmdline常用参数()

2.16.12.1、格式简介
(1)格式就是由很多个项目用空格隔开依次排列,每个项目中都是项目名=项目值
(2)整个cmdline会被内核启动时解析,解析成一个一个的项目名=项目值的字符串。这些字符串又会被再次解析从而影响启动过程。
2.16.12.2、root=
(1)这个是用来指定根文件系统在哪里的
(2)一般格式是root=/dev/xxx(一般如果是nandflash上则/dev/mtdblock2,如果是inand/sd的话则/dev/mmcblk0p2)
(3)如果是nfs的rootfs,则root=/dev/nfs。

2.16.12.3、rootfstype=
(1)根文件系统的文件系统类型,一般是jffs2、yaffs2、ext3、ubi
2.16.12.4、console=
(1)控制台信息声明,譬如console=/dev/ttySAC0,115200表示控制台使用串口0,波特率是115200.
(2)正常情况下,内核启动的时候会根据console=这个项目来初始化硬件,并且重定位console到具体的一个串口上,所以这里的传参会影响后续是否能从串口终端上接收到内核的信息。

2.16.12.5、mem=
(1)mem=用来告诉内核当前系统的内存有多少

2.16.12.6、init=
(1)init=用来指定进程1的程序pathname,一般都是init=/linuxrc

2.16.12.7、常见cmdline介绍
(1)console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
第一种这种方式对应rootfs在SD/iNand/Nand/Nor等物理存储器上。这种对应产品正式出货工作时的情况。

(2)root=/dev/nfs nfsroot=192.168.1.141:/root/s3c2440/build_rootfs/aston_rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200

第二种这种方式对应rootfs在nfs上,这种对应我们实验室开发产品做调试的时候。


16.13.内核中架构相关代码简介

2.16.13.1、内核代码基本分为3块
(1)arch。 本目录下全是cpu架构有关的代码
(2)drivers 本目录下全是硬件的驱动
(3)其他 相同点是这些代码都和硬件无关,因此系统移植和驱动开发的时候这些代码几乎都是不用关注的。

2.16.13.2、架构相关的常用目录名及含义
(1)mach。(mach就是machine architecture)。arch/arm目录下的一个mach-xx目录就表示一类machine的定义,这类machine的共同点是都用xx这个cpu来做主芯片。(譬如mach-s5pv210这个文件夹里面都是s5pv210这个主芯片的开发板machine);mach-xx目录里面的一个mach-yy.c文件中定义了一个开发板(一个开发板对应一个机器码),这个是可以被扩展的。
(2)plat(plat是platform的缩写,含义是平台)plat在这里可以理解为SoC,也就是说这个plat目录下都是SoC里面的一些硬件(内部外设)相关的一些代码。
在内核中把SoC内部外设相关的硬件操作代码就叫做平台设备驱动。
(3)include。这个include目录中的所有代码都是架构相关的头文件。(linux内核通用的头文件在内核源码树根目录下的include目录里)

2.16.13.3、补充
(1)内核中的文件结构很庞大、很凌乱(不同版本的内核可能一个文件存放的位置是不同的),会给我们初学者带来一定的困扰。
(2)头文件目录include有好几个,譬如:
kernel/include 内核通用头文件
kernel/arch/arm/include 架构相关的头文件
kernel/arch/arm/include/asm
kernel\arch\arm\include\asm\mach
kernel\arch\arm\mach-s5pv210\include\mach
kernel\arch\arm\plat-s5p\include\plat
(3)内核中包含头文件时有一些格式

#include <linux/kernel.h>		kernel/include/linux/kernel.h
#include <asm/mach/arch.h>		kernel/arch/arm/include/asm/mach/arch.h
#include <asm/setup.h>			kernel\arch\arm\include\asm/setup.h
#include <plat/s5pv210.h>		kernel\arch\arm\plat-s5p\include\plat/s5pv210.h

(4)有些同名的头文件是有包含关系的,有时候我们需要包含某个头文件时可能并不是直接包含他,而是包含一个包含了他的头文件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

谦谦青岫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值