独上高楼,学习Linux的六种境界

a65ce651545b69c39dccdefb56ce630b.png

  LINUX是全人类的选择, 

  今天如此, 

  未来几十年也不会变。 

  对于软件工程师来说, 

  不需要考虑是否需要学LINUX, 

  只需要考虑如何能学好LINUX……  

>>>>>>>

独上高楼

71cb52e9f670264a2f5f8f58326bf523.gif

LINUX代表未来,今天既已选择了它,未来几十年也不会变。对于今天的程序员来说,正经历着这场变化。在桌面级领域,Windows仍有比较大的占有率,但我相信目前正在进行的这场革命最后一定会让Linux成为愈发普遍的系统平台。

对于从事软件开发的各位同行来说,我们当然应该走在普通用户的前头,应该根据技术发展的方向做技术储备。因此,在我看来,今天无需考虑是否需要学习Linux,只需要考虑如何能学好Linux。

十几年前就有人问过我,怎么还在做Windows的分享。由此看出,早在十几年前就有人下断言,认为Linux会逐步成为主流。我也觉得这个预测是对的。

受国学大师王国维的启发,我把学习Linux归纳成六种境界。王国维曾在《人间词话》里把人生和治学分成三种境界。对于软件设计来说,境界也是非常重要的一个概念,甚至可以说是一种方法论。后面我会把我归纳的六种境界和王国维归纳的三种境界做一个对比,我归纳的前三种境界其实都不作数,后三种境界,我会把它对应到王国维归纳的三种境界。

2337c9271bb9fa5700b4294c5d406a8b.jpeg

如果对人生和治学不达到一定高度,也就不会有这样的感悟。

“独上高楼,望尽天涯路”在王国维眼里是第一境。所以我归纳的前三境其实都还属于零点几的版本,不算是第一境。

引子打住,接下来我们就进入第一境。

ff87d674432bde6fd8d0b79a89be2747.gif

efb984f9e6c55388d9a4d5da592e665a.png

境界一

f4b334d89830b7be1945455b01199509.gif

a729a692a0196d3f2011db9b27b46cf6.png

一提到学习,很多人就想到“我要买书”。今天关于Linux的书也是数不胜数,读书当然也是有好处的,所以我们把它归纳成第一境。买书是好事情,但是能否从书中有所领悟,就要看读书的方法和认真程度了。

除了书之外,在今天的互联网时代还有很多文章,我推荐大家去看Linux内核的官方文档。如果对Linux内核的官网文档划分三六九等,其中又有一些写得非常好的文档,例如:《Unreliable Guide To Hacking The Linux Kernel》

732bd4a65bff8a461f428217aa6fa953.png

这篇文档的标题起得非常低调,意思是:对于你想要征服Linux内核来说,我给你写的是一个不太可靠的指南。越是这样一种表面上看似不太可靠的说法,就越是值得我们一读。

7ff9f963d44d090664bbcb142bff1052.png

这篇文章的作者是Rusty Russell,他是澳大利亚的一位同行,曾任职于IBM的Linux技术中心。在我看来,写作是一门非常讲究方法的技术,写相同的内容,不同人写出来的表达方式就是不一样的。Rusty写了大量的内核代码,其文章也表现出一些很独到的观点,写得都非常言简意赅。

79de720d6c94c288fc69247925237cd0.png

在这篇不算很长的文档中,他写了很多理解内核的关键点,比如上面这一小段,实际上只有这么几十个单词,却写出了读内核代码需要理解的一个关键特征,即:关于返回值的约定。Linux内核是用C语言写的,C语言具有精悍的特征,讲究宁简勿繁,所以Linux内核的代码没有太多杂七杂八的错误判断,也没有太多冗余(如:保留一大堆参数,将来再用)。

Linux内核的代码尽可能精简,这一点在返回值上也有体现。关于返回值,Rusty提出了两点:

  1. 返回零代表成功,返回负数代表失败。

    这也是我非常喜欢的做法,但对于一些教条的编码规范来说,返回都是无符号的,如果要代表错误,需要单独在参数里设置指针来做错误码,就很别扭。

  2. 如果一个函数返回的是指针的话,没有必要单独返回一个错误码,只需把这个错误码编码到指针里。因此,内核里就有了这样几个著名的宏:

    IS_ERR()

    PTR_ERR()

    如果是一个返回指针的函数,在指针里面失败了,就同时把返回值编码进去。为了更高的可读性,就定义了这几个宏。

在Rusty的这篇文章里,类似这样短小精悍的要点总结了十几个,文章的篇幅也很短,用一句比较流行的话来说,就是真的都是干货

小结:学习内核的第一种境界就是读文字、读书、读好的文章、读内核的官方文档

703846d3d4ff43addfc78fcd9c25d068.png

境界二

9e8b3ca54676f77e46a3664abdbfb318.gif

第二种境界就是读代码,Linux内核是开源的,所以大家一定要抓住机会,利用它开源的特征,读它的源代码。

ae4a31237e19675c796f12b0c51c4fc9.png

如果你现在还没有下载Linux内核的源代码,就立刻打开kernel.org,下载一份源代码。我鼓励大家下载longterm,即LTS(长期支持版本)。最新的LTS是6.1,在此之前用的比较多的LTS是5.10或者5.15,目前在产品中如果用的是5.10,就算是比较新的了,再往前就是4.19的版本。下载好一个LTS,放在自己的电脑上解压缩,就可以随时打开查看了。

Linux内核的源代码价值连城,是全世界人类智慧的结晶,是基于全球程序员的通力合作,才产生了这样一套代码。因此,我们可以在这套代码中学到很多东西。

“ 宁广勿深 ”

        ——Linux目录树的定义原则

57361c732c8828d8c670f8e395bdb59a.png

首先是Linux内核目录树的定义原则——宁广勿深,宁可目录个数多一些,不要目录太深。现实中很多的软件项目做得不好,就是因为把目录定义得特别深,不利于浏览,在浏览目录时相当于是进、进、进,然后退出来,再进、进、进,退出来。

因此,关于源代码的目录组织推荐“宁广勿深”,同一级多一点不要紧,因为这样能够一目了然,一眼看过去就能知道这一层都有哪些内容。尽可能避免一级级往下扩散,譬如最上面只有两个目录,然后就分开了,每一级都往下扩展出多个目录,像这样的目录树在浏览时非常不方便,也无法一下看到全貌。

 读源代码

——以Panic函数为例

读Linux内核的源代码时,可以找一些自己感兴趣的函数去读,比如Panic函数就很有意思,因为它很生动,我们可以直接打开panic.c,读一下Panic函数的逻辑。

92b575b905e6d2c5ec89ec691ae2b576.png

它是Linux内核中一个比较古老的函数,早在0.1版本就有,只不过在0.1版本的时候,Panic是一个死循环,是在后面的发展中越来越复杂。与Printf的函数原型很类似,Panic有一个固定的模板,后面可以跟不确定的参数。内核代码在很多地方都有调用,调用的时候就是根据原型来传递各种参数的。如果大家对Linux的崩溃进行分析,在Linux下发生Panic时结合源代码来看,就可以一边学习源代码,一边来理解Panic的原因。

 结合产业发展读内核代码

              ——以Arch子目录为例

读内核源代码还可以结合产业的发展来读,如Arch目录,它代表着CPU厂商的发展力量。

4b634880afc0eb9f91a5fbaf4d4adab5.png

从4.4内核到5.13,再从5.13到6.16,Arch子目录的变化是很大的。

关于Arch子目录,我在庐山桃花源研习班讨论的时候,有一位格友提了一个非常好的问题:

“为什么ARM的32位和64位分成两个目录,而x86的在一个目录?”

169f97d87ccd0f3d255aec82e3f2e6c2.png

在我看来,这个问题问得非常好,它代表着做同一件事情的不同方法或是人生的不同态度。

ARM分成两个目录体现出它的年轻、无所顾忌、敢于突破自身,它认为老的32位下面有很多问题,所以它发展到64位就创建一个新的目录,丢掉了历史包袱。这就像年轻人,倘若这份工作不想做了,索性就把它抛弃掉,学一门新的技术从头再来。

而在x86的历史上有IA-64架构,在英特尔看来,希望64位就是IA-64,没想到AMD不同意,搞出了AMD64。因为AMD宣传的口号是64位和32位无缝兼容,所以没必要搞两个目录。x86之所以重,就是因为它背着所有的历史包袱,所有东西都搅在一个目录里。

bcdc8a274cca9ef87208fcbf0417c4b8.png

境界三

6a10a9eb6d61de552846ea04da2f2e37.gif

接下来的第三种境界是读“信”。信的英文是Letter,另外还有消息(Message)、电子邮件(Mail),都可以称作广义上的信。在我读大学的时候还流行过写信,下课时大家都期盼着自己能收到信。

为什么 读信和读书的感觉不一样呢?

因为信比较短,而且信有具体的场景,更具针对性,让人身临其境。对于一本厚厚的书,很多时候我们坚持不下来,不知道作者在说什么,不知道应该先读什么、后读什么。但对于一封信,通常你读的时候不会有这种感觉,因为你很快就能进到这封信的场景,立刻就能明白这封信所描述的问题,所以你就能快速理解它。因此,学习Linux内核的第三种境界就是读内核写给我们的信。

内核 怎么写信呢?

内核里面有一个著名的Printk函数,该函数是Linux内核的开发者——林纳斯所依赖的第一大调试方式。

4f055527e8ce3c88926df7f9b32b281b.png

4f8cd8ced59a8bd1f475eb7d566707a9.png

我统计了Linux 3.12的内核,里面有六万多条Printk,今天可能已经突破十万条,也就是说,在内核中有十万个主要的Printk。同时,在这些Printk里面还有大量的变量,因为Printk可以包含变量信息,所以在不同场景下执行到这里时,都会打印出一条消息。把这些消息累积在一起,就仿佛是内核时钟记录着自己走过的路径。它此刻的心情就像朋友之间写信一样,当我们读一封信的时候,可以体会到对方的经历和写信时的心情。

读内核打印出的这些Printk消息,我们也可以感受到:

  • 内核现在在做什么?

  • 内核的健康状况如何?

  • 内核有没有遇到障碍或是困难?

  • 内核之前是不是面临着危险的境地,比如触发Panic崩溃?

为什么 要重视内核的消息?

内核消息是一份非常生动的内核资料,如果你在不同的系统上启动,就会发现诸多差异。例如:在x86上看到的就会有x86的一些特征,固件和设备都和ARM的不一样。因此,每当接触一个新的Linux系统,我非常喜欢用dmesg去看内核消息,每次都有所收获。

当然,内核消息不光是启动时,当你在执行一些操作的时候,内核也会打印出消息,这时打印出的消息也是很有用的。

d72047a6ed9e9c1f0ceb22dfcfc1fc45.png

境界四

1a68f65a89a3ae52992c25d83f16b7dd.gif

第四种境界是写代码。前三种境界的第一个动词都是读,而第四种境界不再是读了,而是写。读,我们可以不必动手,用眼睛来读,但如果是写代码,就需要大家动手了,自己敲代码进去、编译执行,经历这样一个过程。

2057f9afab391d0221f214a1839c7a5a.png

这里引用一下我非常崇拜的 丹尼斯·里奇 的一句话,他是C语言的发明者,同时也是UNIX操作系统最重要的两位开创者之一。他说:“学一门编程语言的唯一方法,就是用它来写程序。”

对于这句话,我是很信服的,要学好编程,就要写代码。同理,要学习内核,也要写代码,所以我套用丹尼斯·里奇的话来描述一下学习内核:“学习内核的一种捷径就是写代码运行到内核中。”

怎么 在内核里写代码呢?

一种方法是修改内核编译,但这种方法需要的功力和时间都比较长。

另一种方法是写驱动,因为驱动程序也运行在内核空间。由于驱动是模块化的,所以我们可以先写一个小驱动跑在内核空间,就可以通过它来感受内核编程的不同,探索如何与内核进行交互、如何写内核代码,体会内核的“为君难”——内核虽然权力大,但也有很多为难之处。

fded1ecdf325409cfc4c39975f458539.png

在Windows环境下,要想把自己的代码运行在内核空间,已经有所限制,如果要把代码运行到内核空间,就必须要对代码签名。微软从Windows Vista开始,内核空间的代码必须签名,所以不太方便。如果你要搞测试签名,也需要重新启用禁止签名强制。因此,我今天正在逐步转向Linux。因为Windows环境越来越不适合开发者,有太多的规矩,而太过循规蹈矩的系统缺少生机与活力,所以我鼓励大家也像我这样转向Linux,在Linux环境下可以很轻松地加载驱动,不需要签名。当然,如果你想搭建构建驱动的环境,是有点难度的,需要做些准备工作,如:安装内核的头文件。

1bcfea2f87469df8a926f667c2858732.png

我们这里特别举一下幽兰的例子,幽兰是格蠹科技推出的一款专为程序员打造的笔记本,大家拿到手就可以构建内核驱动。对于有幽兰的各位格友,你可以立刻试一试,试成功之后,你可以再做一些修改,修改之后再编译、加载,不断重复这个过程,你就会对内核越来越熟悉。

 演示:使用幽兰构建内核驱动

https://www.bilibili.com/video/BV1Gm4y1e7Gj/

e5e089f588c58319c25132ca2056260f.png

这时候,我们再回顾一下王国维归纳的三种境界,他归纳的第一境是“昨夜西风凋碧树,独上高楼,望尽天涯路”

对于很多接触Linux的人根本没有这种感觉,在我看来,只有你亲自写一些内核代码之后,你才会有这种感觉,体会到内核的难处、内核代码的独特之处,才会知道内核代码所面临的真正挑战——巨大的代码量、各方面的巨大负担,所以要把它做好是很难的。

4b8da17c72adfa0b58e442dc151a2c5a.png

境界五

15988e17f536e4b1edd3ff302bb96a5c.gif

第五种境界就是要上调试器进行内核调试。当你真正写了内核代码之后,你就会对内核越来越了解,就会发现要学的东西还有很多,也就是“独上高楼,望尽天涯路”,到处都是茫茫一片,你很想去探索,但是时间有限,这时候就要提高效率了。

 如何提高效率?

   调试器是一个比较好的提高效率的方法。

回想起这么多年我在技术方面的坚持探索,给我最大帮助的其实就是调试器,所以我们每学一门新技术,都是想办法上调试器。最近几年,我转向ARM + Linux,也开发了好几种调试器,比如硬件方面有挥码枪这样的调试器,软件方面有Nano Code,在我出差的时候,经常把这些东西都带着。

0ded01d777705b066a1d4e3eca99b851.png

上图是我在旅途中拍摄的,照片中有两个挥码枪、一个GDK3(用于调试ARM的M核)、一个GDK8(用于调试ARM的A核,里面也有Linux)。

d551773793188ea57b7bc20ec3094594.png

这张截图是在wifi的发包函数设置断点,当该断点命中的时候,大家可以看到这是来自用户空间的一个调用,它一步步进到socket层,进入网络协议栈,再进到电路层,给设备驱动,设备驱动最后就发给物理层做物理传输了。

很多人都对网络感兴趣,无论是研究安全,还是做通信,都对网络感兴趣。如何生动地学习网络呢?调试器绝对是一种很高效的方法。为什么我们把读代码只看作是第二种境界?因为读代码很多时候都读不到真正要用的代码,读到的都是死代码。只有在调试的时候,读到的才是活的代码,因为这个代码是真正在跑的,是你在设断点断下来后切切实实看到的。

通过内核调试,我们能够提高自己理解内核的效率,设置一个断点,断下来后,立刻就能看出很多问题。

当然,我们也可以用这种方式来看调度。

Child-SP          RetAddr           Call Site
ffffff80`0f08bb00 ffffff80`090a060c lk!__switch_to+0x8 [arch/arm64/kernel/process.c @ 544]
ffffff80`0f08bb90 ffffff80`090a10d8 lk!__schedule+0x2f4 [kernel/sched/core.c @ 3437]
ffffff80`0f08bbb0 ffffff80`08082fa0 lk!preempt_schedule_irq+0x38 [./arch/arm64/include/asm/irqflags.h @ 62]
ffffff80`0f08bbb0 00000000`9200004f lk!el1_preempt+0x8 [arch/arm64/kernel/entry.S @ 665]
dt prev -y comm
Local var @ 0 Type task_struct*
   +0x768 comm : [16]char[]  "Chrome_HistoryT"
dt next -y comm
Local var @ 0 Type task_struct*
   +0x768 comm : [16]char[]  "migration/0"

这是一个抢先式调度的执行过程,是真正在做CPU上下文的切换,从一个线程切到另一个线程。

主动 让出执行权

Child-SP          RetAddr           Call Site
ffffff80`0a313d30 ffffff80`090a060c lk!cpu_switch_to [arch/arm64/kernel/entry.S @ 1067]
ffffff80`0a313de0 ffffff80`090a0c80 lk!__schedule+0x2f4 [kernel/sched/core.c @ 3437]
ffffff80`0a313e00 ffffff80`080e5f00 lk!schedule+0x38 [kernel/sched/core.c @ 4171]
ffffff80`0a313e60 ffffff80`080e147c lk!smpboot_thread_fn+0x258 [kernel/smpboot.c @ 160]
ffffff80`0a313ec0 ffffff80`08085db0 lk!kthread+0x12c [kernel/kthread.c @ 260]
ffffff80`0a313ec0 00000000`00000000 lk!ret_from_fork+0x10 [arch/arm64/kernel/entry.S @ 1104]

切线程又分成很多种场景,有的时候是主动让出执行权,可以主动调用schedule,把执行权交给调度器,由调度器去执行切换。

因等待信号量 而切换线程

Child-SP          RetAddr           Call Site
ffffff80`0d8abbb0 ffffff80`090a060c lk!cpu_switch_to [arch/arm64/kernel/entry.S @ 1067]
ffffff80`0d8abc60 ffffff80`090a0c80 lk!__schedule+0x2f4 [kernel/sched/core.c @ 3437]
ffffff80`0d8abc80 ffffff80`090a47b8 lk!schedule+0x38 [kernel/sched/core.c @ 4171]
ffffff80`0d8abd40 ffffff80`090a30e8 lk!schedule_timeout+0x1e0 [kernel/time/timer.c @ 1795]
ffffff80`0d8abdc0 ffffff80`0811a4cc lk!__down_interruptible+0xa0 [kernel/locking/semaphore.c @ 221]
ffffff80`0d8abdf0 ffffff80`011c1134 lk!down_interruptible+0x54 [kernel/locking/semaphore.c @ 85]
ffffff80`0d8abe60 ffffff80`080e147c bcmdhd!dhd_rxf_thread+0x9c [drivers/net/wireless/rockchip_wlan/rkwifi/bcmdhd/dhd_linux.c @ 6614]
ffffff80`0d8abec0 ffffff80`08085db0 lk!kthread+0x12c [kernel/kthread.c @ 260]
ffffff80`0d8abec0 00000000`00000000 lk!ret_from_fork+0x10 [arch/arm64/kernel/entry.S @ 1104]

有的时候是进入等待。比如说,这里是在等待信号量,在信号量调用down函数时,它进入等待,触发线程切换。

从空闲线程 投入工作

Child-SP          RetAddr           Call Site
ffffff80`0a323e70 ffffff80`090a060c lk!cpu_switch_to [arch/arm64/kernel/entry.S @ 1067]
ffffff80`0a323f20 ffffff80`090a104c lk!__schedule+0x2f4 [kernel/sched/core.c @ 3437]
ffffff80`0a323f40 ffffff80`080f4f50 lk!schedule_idle+0x24 [./include/asm-generic/bitops/non-atomic.h @ 106]
ffffff80`0a323fb0 ffffff80`080f5224 lk!do_idle+0x168 [kernel/sched/idle.c @ 292]
ffffff80`0a323fd0 ffffff80`08097c38 lk!cpu_startup_entry+0x24 [kernel/sched/idle.c @ 370]
ffffff80`0a324000 00000000`00000000 lk!secondary_start_kernel+0x150 [arch/arm64/kernel/smp.c @ 255]

还有一种情况是从空闲线程切到一个新线程

84b85a655560bf2a5c283ff36cce50e6.png

如果大家对内核调试的方法感兴趣,推荐大家可以用挥码枪加GDK8,这是性价比非常高的一种内核调试方案。如果大家想自己搭建内核调试的环境,无论是接串口,还是重新编译内核用网络,都比较花时间,挥码枪加GDK8相当于你拿到手立刻就连上了,五分钟之内就把内核断下来,设好符号就可以进入状态。

bfe476e28ffdc2920f596da5ab1b3e98.png

内核调试,我把它看作是学习Linux内核的第五种境界,对应到王国维所说的第二境——“衣带渐宽终不悔,为伊消得人憔悴”。当你达到这种境界,你就把学习内核当成是一种兴趣,当成是一种自然而然的事情。

beec905f1661c1ff5ac33cf7b964cc8a.png

境界六

17ef65482675728fb76767c9cdc2c61e.gif

最后一种境界很特别,它有几种称呼,我现在写到文字上的是一种比较雅的称呼——相伴。夫妻之间相伴到老,相伴是很高的一种境界,我们和Linux内核之间的最高境界也是相伴。

这种境界其实来源于NT内核,在NT内核开发的最后阶段,开发团队就开始使用这个开发中的内核作为自己的工作机。当然,最初是派了几个高手来这件事,他们先把自己的机器换成开发中的NT内核,然后在上面自己使用,立刻就发现了大量的Bug。作为软件工程师,你就把你实际开发的系统作为你所使用的系统,体验自己写的代码究竟如何,感受自己研发的产品到底是好是坏,烂有多烂,好有多好。如果进入这种境界,就很不一般了。

目前,对于整个Linux系统来说,它的可用性在某些方面仍有所欠缺,所以很多人在桌面级还是更多地依赖Windows或者Mac。在我看来,如果大家真的想学好Linux,决定致力于Linux内核,为什么你不直接把自己的工作机切到Linux上去呢?我今天正在做这件事,所以我最近一直背着两台电脑,虽然重了一点,但其实也还好,上面这台是Linux的,它很薄,分量也轻很多,因为里面是无风扇设计的。

4c0e1e63e7a6349ef9f96d3ba636eb02.png

这个过程需要有所付出,但这个付出的过程恰恰就是我们学习的过程。在这个过程中,我们能更好地来学习内核,因为在这个过程中,我们立刻就能感受到Linux环境中的一些不足。

对我来说,现在很多时候也是在开发Linux上的一些代码,所以我接下来正在努力地把我的开发环境全面切到Linux上去,也是鼓励大家跟我一起做。有的人说:“我不要做小白鼠,不要这么干,我就等你们都体验好了,然后我再切过去。”这不是开发者的心态,而是普通用户的心态。作为程序员就是要始终站在技术的前沿,学习新的技术,遇到各种问题加以解决。当然我这样说并不是没有问题硬要找问题,而是你要有一个真实的环境,这样你所遇到的问题就都是很有代表性的、很有意义和价值的,代表着潮流,然后你再钻研到这些问题里去,就是始终跟随着这个潮流。

75a3888826437352814b7beca841700f.png

进入到这种状态,我认为只要坚持就可以达到王国维所归纳的第三种境界——“众里寻他千百度,蓦然回首,那人却在灯火阑珊处”。一直坚持,整日和Linux环境朝夕相处、日夜相伴,今天学一点,明天学一点,你敲的每一条命令、执行的每一次操作、点击的每一次鼠标按键,都是在感受Linux,都是在学习Linux,都是在和内核对话。这时候,你的进步是一点一滴的,是在无形中向上前进。像这样每一天的朝夕相伴,我相信有一天你蓦然回首,就会发现你的水平不一样了,很自然地就能理解内核的各种机制。

b75f8556eb0e4d4b54ec2c8828b2f056.gif

05235eab95077bba1ac313fdc7783b07.png

购买幽兰代码本即可成为兰舍会员,与众多技术高手一起成长。

购买可前往淘宝格友小店:

https://m.tb.cn/h.Uuv7fit?tk=N1iIdn8t4CI

【盛格塾】

正心诚意,格物致知

人文情怀审视软件,以软件技术改变人生

8e081fe9ad6c083cc40764e15fe00390.png

格友公众号

81e4e90725fabd2433c3663353dcd890.png

盛格塾小程序

扫描上方二维码或在微信中搜索“盛格塾”小程序

可以阅读更多文章和有声读物

往期推荐

幽兰代码本——开源软件实践家

梦遇恶件

1954年的今天——纪念图灵逝世69周年

鹤鸣2镜像发布说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值