23 Linux高级篇-Linux内核介绍&内核升级

23 Linux高级篇-Linux内核介绍&内核升级

  1. 《鸟哥的Linux私房菜 基础学习篇 第四版》1
  2. 《鸟哥的Linux私房菜 服务器架设篇 第三版》2
  3. 《韩顺平_2021图解Linux全面升级》3
  4. 《Linux0.01内核源代码及注释》4

23.1 linux-0.01内核介绍

23.1.1 为什么要阅读Linux内核?

首先给出几个理由:

  1. 爱好,就是喜欢linux(黑客精神),想深入理解linux底层运行机制,对操作系统有深入理解。
  2. 找工作面试的需要。作为开发者,不管你从事的是驱动开发,应用开发还是后台开发,你都需要了解操作系统内核的运行机制,这样才能写出更好的代码。作为开发人员不应该只局限在自己的领域,你设计的模块看起来小,但是你不了解进程的调用机制,你不知道进程为什么会阻塞、就绪、执行几个状态。那么很难写出优质的代码。

  老韩忠告,作为有追求的程序员,还是应该深入的了解一个操作系统(比如linux/unix)的底层机制,最好是源码级别的,这样你写多线程高并发程序,包括架构、优化、算法等,思想高度不一样的。当然也不是要求大家把一个非常庞大的Linux内核每一行都读懂,但至少能看几个核心的模块。

23.1.2 下载linux-0.01内核源码

  很多人害怕读Linux内核,Linux内核这样大而复杂的系统代码,阅读起来确实有很多困难,但是也不是想象的那么高不可攀。内核源代码有很多版本,2020年Linux的内核源码就至少有2700w行代码,可读性就非常差,所以建议从linux-0.01入手,总共的代码1w行左右。下面是Linux内核的下载地址:

  • 官方内核地址:https://www.kernel.org/ ,一般用来下载最新几个版本的Linux内核。
  • 微软镜像提供的Linux 0.01版本下载地址(选择“linux-0.01.tar.gz”)。

23.1.3 linux-0.01内核介绍

在阅读源码前,需要注意:

1.linux0.01的阅读需要懂c语言。
2.阅读源码前,应知道inux内核源码的整体分布情况。现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序和网络等组成。Linux内核源码的各个目录大致与此相对应。
3.在阅读方法或顺序上,有纵向与横向之分。所谓纵向就是顺着程序的调用顺序逐步进行;所谓横向,就是按模块进行。它们经常结合在一起进行。
4.对于Linux启动的代码可顺着Linux的启动顺序一步步来阅读;对于像内存管理部分,可以单独拿出来进行阅读分析。实际上这是一个反复的过程,不可能读一遍就理解。

注:Linux0.01大概是1991年左右写的,那时候网络不发达,驱动程序也几乎没有,所以Linux0.01相对来说还是比较简单的。

1. 源码目录介绍
linux内核源码阅读&目录介绍&main.c说明

  按照上一小节的方法下载linux-0.01源码后,打开目录查看,如下图所示:

图23-1 linux-0.01源码文件夹

下面这些目录/文件的含义(chatgpt):

  • boot:包含了引导加载程序的代码,用于启动内核。这里的代码负责在系统引导时加载内核到内存中并启动它。
  • fs:这个目录包含了文件系统(file system)相关的代码。linux-0.01 内核支持一些基本的文件系统操作,但功能相对有限。
  • include:包含了一些头文件,这些头文件定义了内核中使用的常量、数据结构、函数原型等。这些头文件在整个内核代码中被广泛引用。
  • init:包含了内核的初始化代码,涉及系统的启动和初始化过程。包含核心的初始化代码main.c
  • kernel:这是内核的主要代码目录,包含了实现操作系统核心功能的代码,例如进程调度、系统调用、中断处理等。
  • lib:这个目录包含了一些通用的函数库和工具函数,用于辅助内核的开发。
  • mm:这个目录包含了内存管理(memory manager)相关的代码。内存管理是操作系统的一个关键部分,它负责管理系统的内存资源,包括分配、释放和保护内存区域。
  • tools:这个目录包含了一些工具和脚本,用于构建、编译和调试内核。
  • Makefile:编译文件,有多个,每个模块都可能对应一个Makefile。

为了方便阅读源码,现在将整个源码文件夹linux-0.01,使用Xftp上传到CentOS的/opt/linuxsrc/目录下,然后就可以很方便的查看,比如下面使用tree指令查看整个目录的所有文件:

# 首先安装一下tree
[root@CentOS76 ~]# cd /opt/linuxsrc
[root@CentOS76 linuxsrc]# tree linux-0.01/
bash: tree: 未找到命令...
[root@CentOS76 linuxsrc]# sudo yum install tree
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
# 安装过程略,遇到什么选项输入y同意就行
已安装:
  tree.x86_64 0:1.6.0-10.el7                                                                           

完毕!

# 查看源码文件夹的目录树
[root@CentOS76 linuxsrc]# tree linux-0.01/
linux-0.01/
├── boot
│   ├── boot.s
│   └── head.s
├── fs
│   ├── bitmap.c
│   ├── block_dev.c
│   ├── buffer.c
│   ├── char_dev.c
│   ├── exec.c
│   ├── fcntl.c
│   ├── file_dev.c
│   ├── file_table.c
│   ├── inode.c
│   ├── ioctl.c
│   ├── Makefile
│   ├── namei.c
│   ├── open.c
│   ├── pipe.c
│   ├── read_write.c
│   ├── stat.c
│   ├── super.c
│   ├── truncate.c
│   └── tty_ioctl.c
├── include
│   ├── a.out.h
│   ├── asm
│   │   ├── io.h
│   │   ├── memory.h
│   │   ├── segment.h
│   │   └── system.h
│   ├── const.h
│   ├── ctype.h
│   ├── errno.h
│   ├── fcntl.h
│   ├── linux
│   │   ├── config.h
│   │   ├── fs.h
│   │   ├── hdreg.h
│   │   ├── head.h
│   │   ├── kernel.h
│   │   ├── mm.h
│   │   ├── sched.h
│   │   ├── sys.h
│   │   └── tty.h
│   ├── signal.h
│   ├── stdarg.h
│   ├── stddef.h
│   ├── string.h
│   ├── sys
│   │   ├── stat.h
│   │   ├── times.h
│   │   ├── types.h
│   │   ├── utsname.h
│   │   └── wait.h
│   ├── termios.h
│   ├── time.h
│   ├── unistd.h
│   └── utime.h
├── init
│   └── main.c
├── kernel
│   ├── asm.s
│   ├── console.c
│   ├── exit.c
│   ├── fork.c
│   ├── hd.c
│   ├── keyboard.s
│   ├── Makefile
│   ├── mktime.c
│   ├── panic.c
│   ├── printk.c
│   ├── rs_io.s
│   ├── sched.c
│   ├── serial.c
│   ├── sys.c
│   ├── system_call.s
│   ├── traps.c
│   ├── tty_io.c
│   └── vsprintf.c
├── lib
│   ├── close.c
│   ├── ctype.c
│   ├── dup.c
│   ├── errno.c
│   ├── execve.c
│   ├── _exit.c
│   ├── Makefile
│   ├── open.c
│   ├── setsid.c
│   ├── string.c
│   ├── wait.c
│   └── write.c
├── Makefile
├── mm
│   ├── Makefile
│   ├── memory.c
│   └── page.s
└── tools
    └── build.c

11 directories, 88 files

2. 主函数介绍

  上一小节已经提到源码目录下的init文件夹便包括了内核的主函数main.c。本小小节就来简单介绍一下main.c中的入口函数void main()。首先给出main.c的全部内容(无注释):

 * calls - which means inline code for fork too, as otherwise we
 * would use the stack upon exit from 'fork()'.
 *
 * Actually only pause and fork are needed inline, so that there
 * won't be any messing with the stack from main(), but we define
 * some others too.
 */
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall0(int,setup)
static inline _syscall0(int,sync)

#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>

#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#include <linux/fs.h>

static char printbuf[1024];

extern int vsprintf();
extern void init(void);
extern void hd_init(void);
extern long kernel_mktime(struct tm * tm);
extern long startup_time;

/*
 * Yeah, yeah, it's ugly, but I cannot find how to do this correctly
 * and this seems to work. I anybody has more info on the real-time
 * clock I'd be interested. Most of this was trial and error, and some
 * bios-listing reading. Urghh.
 */

#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)

static void time_init(void)
{
        struct tm time;

        do {
                time.tm_sec = CMOS_READ(0);
                time.tm_min = CMOS_READ(2);
                time.tm_hour = CMOS_READ(4);
                time.tm_mday = CMOS_READ(7);
                time.tm_mon = CMOS_READ(8)-1;
                time.tm_year = CMOS_READ(9);
        } while (time.tm_sec != CMOS_READ(0));
        BCD_TO_BIN(time.tm_sec);
        BCD_TO_BIN(time.tm_min);
        BCD_TO_BIN(time.tm_hour);
        BCD_TO_BIN(time.tm_mday);
        BCD_TO_BIN(time.tm_mon);
        BCD_TO_BIN(time.tm_year);
        startup_time = kernel_mktime(&time);
}

void main(void)         /* This really IS void, no error here. */
{                       /* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
        time_init();
        tty_init();
        trap_init();
        sched_init();
        buffer_init();
        hd_init();
        sti();
        move_to_user_mode();
        if (!fork()) {          /* we count on this going ok */
                init();
        }
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
        for(;;) pause();
}

static int printf(const char *fmt, ...)
{
        va_list args;
        int i;

        va_start(args, fmt);
        write(1,printbuf,i=vsprintf(printbuf, fmt, args));
        va_end(args);
        return i;
}

static char * argv[] = { "-",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };

void init(void)
{
        int i,j;

        setup();
        if (!fork())
                _exit(execve("/bin/update",NULL,NULL));
        (void) open("/dev/tty0",O_RDWR,0);
        (void) dup(0);
        (void) dup(0);
        printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
                NR_BUFFERS*BLOCK_SIZE);
        printf(" Ok.\n\r");
        if ((i=fork())<0)
                printf("Fork failed in init\r\n");
        else if (!i) {
                close(0);close(1);close(2);
                setsid();
                (void) open("/dev/tty0",O_RDWR,0);
                (void) dup(0);
                (void) dup(0);
                _exit(execve("/bin/sh",argv,envp));
        }
        j=wait(&i);
        printf("child %d died with code %04x\n",j,i);
        sync();
        _exit(0);       /* NOTE! _exit, not exit() */
}

然后是其中的入口函数(有注释):

void main(void)         /* This really IS void, no error here. */
{                       /* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
        time_init();            //初始化运行时间
        tty_init();             //初始化tty终端
        trap_init();            //初始化陷阱门(硬件中断向量)
        sched_init();           //初始化调度程序
        buffer_init();          //初始化缓冲管理
        hd_init();              //初始化硬盘
        sti();                  //所有初始化工作完成后,开启中断
        move_to_user_mode();    //进入到相关的用户模式
        if (!fork()) {          /* we count on this going ok */
                init();
        }
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
        for(;;) pause();
}

大致的介绍就到这里,后续可以自行结合文档《Linux0.01内核源代码及注释.pdf》4深入阅读。注意不要求逐行阅读,先看个大概即可,注意要反复地看。

23.3 Linux内核升级

  在最开始介绍Linux系统时便提到,严格来说Linux只是一个内核,只不过在这个内核的基础上添加各种不同的软件和工具,才有了许许多多的Linux发行版,所以Linux内核对于Linux系统来说至关重要。Linux内核不断进行定期更新,包括错误修复、增强高级功能以及许多其他增强功能。虽然没有人会强迫你更新到最新的Linux内核,但以下是进行内核升级的几个理由:

  1. 性能提升:新的内核可能会提供更好的性能,包括更快的启动时间、更快的文件系统访问和更快的网络传输速度等。
  2. 安全性提升:新的内核通常修复了已知的漏洞和安全问题,提高了系统的安全性和稳定性。
  3. 新的硬件支持:新的内核可能支持更多的硬件设备和技术。
  4. 新的功能支持:新的内核通常会提供新的功能和改进,例如更好的文件系统支持、更好的虚拟化支持等。

【参考1】CSDN博文“Centos7系统内核如何更换?为什么需要更新内核?”。
【参考2】PHP中文网“您应该更新 Linux 内核的五个理由”。

23.3.1 最新版内核介绍

本小节首先来介绍一下最新版(2023/8/25)的Linux内核目录。首先是下载解压Linux内核:

################## 指令速览 #####################
cd /opt/linuxsrc
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.4.11.tar.gz
tar -zxvf linux-6.4.11.tar.gz
# 注:若下载速度太慢,也可以直接在浏览器复制上述网址下载,然后通过Xftp上传到Linux的/opt/linuxsrc目录下


################## 实际演示 #####################
[root@CentOS76 linuxsrc]# cd /opt/linuxsrc
[root@CentOS76 linuxsrc]# wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.4.11.tar.gz
--2023-08-25 09:44:47--  https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.4.11.tar.gz
正在解析主机 cdn.kernel.org (cdn.kernel.org)... 151.101.109.176, 2a04:4e42:8c::432
正在连接 cdn.kernel.org (cdn.kernel.org)|151.101.109.176|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:221659176 (211M) [application/x-gzip]
正在保存至: “linux-6.4.11.tar.gz.1”

12% [======>                                                       ] 27,768,960  7.32MB/s 剩余 
# 剩下的下载页面就不显示了,遇到选项就选择默认即可。
[root@CentOS76 linuxsrc]# tar -zxvf linux-6.4.11.tar.gz
# 解压过程省略,会稍微等一会
linux-6.4.11/virt/lib/
linux-6.4.11/virt/lib/Kconfig
linux-6.4.11/virt/lib/Makefile
linux-6.4.11/virt/lib/irqbypass.c
[root@CentOS76 linuxsrc]# ls
linux-0.01  linux-6.4.11  linux-6.4.11.tar.gz
[root@CentOS76 linuxsrc]# ls linux-6.4.11
arch   COPYING  Documentation  include   ipc      kernel    MAINTAINERS  net     samples   sound  virt
block  CREDITS  drivers        init      Kbuild   lib       Makefile     README  scripts   tools
certs  crypto   fs             io_uring  Kconfig  LICENSES  mm           rust    security  usr

可以看到相比于linux-0.01,linux-6.4.11多出来非常多的内容。下面就来介绍linux-6.4.11源码中各目录的含义:

[root@CentOS76 linuxsrc]# ls linux-6.4.11
arch   COPYING  Documentation  include   ipc      kernel    MAINTAINERS  net     samples   sound  virt
block  CREDITS  drivers        init      Kbuild   lib       Makefile     README  scripts   tools
certs  crypto   fs             io_uring  Kconfig  LICENSES  mm           rust    security  usr
  • arch:包含体系结构特定的代码,其中每个子目录代表一个特定的硬件体系结构(例如x86、ARM、PowerPC等)。
  • COPYING:可能是一个包含 Linux 内核的版权信息和许可证条款的文件。
  • Documentation:包含了内核的文档,其中包括关于内核配置、开发指南、API文档等内容。
  • include:包含内核头文件,定义了内核中使用的常量、数据结构和函数原型。
  • ipc:包含进程间通信(IPC)相关的代码,支持不同的进程间通信机制。
  • kernel:包含核心内核代码,涵盖进程调度、中断处理、系统调用等。
  • MAINTAINERS:可能是一个包含维护者列表的文件,列出了负责维护和审核特定代码部分的开发者。
  • net:包含网络协议栈的代码,涉及网络协议和网络设备驱动程序。
  • samples:包含示例代码的目录,这些示例代码可以用来演示如何使用某些内核功能或特性。
  • sound:包含声音子系统的代码,支持音频设备和功能。
  • virt:包含虚拟化相关的代码,支持不同的虚拟化平台。
  • block:包含块设备层的代码,涉及与块设备(如硬盘)相关的逻辑。
  • CREDITS:通常包含了对于内核开发和贡献的致谢名单。
  • drivers:这是一个庞大的目录,包含了各种硬件设备的驱动程序,例如网络、存储、显卡等。
  • init:包含内核初始化代码,涉及启动过程和系统初始化。但不像linux-0.01的init目录只包含main.c,还因为版本升级而包含很多其他的文件。
  • Kbuild:通常是一个用于构建和编译内核的脚本文件,它可能包含了一些构建规则和选项。
  • lib:这个目录包含了一些通用的函数库和工具函数,用于辅助内核的开发。
  • README:这个文件通常包含了关于内核的基本信息、编译和安装指南等。
  • scripts:可能包含一些用于辅助构建、编译和调试内核的脚本文件。
  • tools:包含一些工具和脚本,用于构建、分析和调试内核。
  • certs:可能是包含用于存储数字证书的目录,这些证书可能用于验证内核模块或其他安全相关操作。
  • crypto:包含加密相关的代码,用于支持不同的加密算法和加密操作。
  • fs:包含文件系统的代码,支持各种文件系统,如EXT4、FAT、NTFS等。
  • io_uring:可能是与 I/O uring 相关的代码,I/O uring 是一种用于高效异步 I/O 操作的机制。
  • Kconfig:通常是用于配置内核选项的配置文件,这些选项可以通过配置工具进行设置。
  • LICENSES:可能包含不同的软件许可证文件,用于指定内核中包含的代码的许可证。
  • mm:包含内存管理相关的代码,用于管理物理内存和虚拟内存。
  • rust:可能包含使用 Rust 编程语言编写的内核模块或代码。
  • security:包含安全子系统的代码,用于实施访问控制和安全策略。
  • usr:包含用户空间的文件,如测试程序和示例文件。

23.3.2 Linux内核升级

  上一小节已经简单介绍了Linux最新版内核的内容,本小节就来介绍如何对当前的Linux发行版进行内核升级。“内核升级”有专门的指令完成,非常简单,唯一需要我们掌握的知识点是内核的兼容性问题。下表给出了不同的CentOS7发行版与Linux内核的对应关系。

图23-2 CentOS7内核版本(来源:维基百科-CentOS)

也就是说,内核升级时不能跨越大版本,比如我们当前的版本是CentOS7.6(3.10.0-957),那么内核最多升级到同为CentOS7的CentOS7.9(3.10.0-1160)。而不能选择现在的最新版Linux内核linux-6.4.11(2023-8-25)。

当然正如前面所说,实际上我们在进行内核升级时,无需自己挑选Linux内核,有相应的指令可以帮我们迅速匹配并升级:

################## 指令速览 #####################
uname -a            # 查看当前的内核版本
yum info kernel -q  # 检测内核版本,显示可以升级的内核
yum update kernel   # 升级内核
yum list kernel -q  # 查看已经安装的内核


################## 实际演示 #####################
# 1. 查看当前的内核版本
[root@CentOS76 linuxsrc]# uname -a
Linux CentOS76 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

# 2. 检测内核版本,显示可以升级的内核
[root@CentOS76 linuxsrc]# yum info kernel -q
已安装的软件包
名称    :kernel
架构    :x86_64
版本    :3.10.0
发布    :1160.el7
大小    :64 M
源    :installed
来自源:anaconda
简介    : The Linux kernel
网址    :http://www.kernel.org/
协议    : GPLv2
描述    : The kernel package contains the Linux kernel (vmlinuz), the core of any
         : Linux operating system.  The kernel handles the basic functions
         : of the operating system: memory allocation, process allocation, device
         : input and output, etc.

可安装的软件包
名称    :kernel
架构    :x86_64
版本    :3.10.0
发布    :1160.95.1.el7
大小    :52 M
源    :updates/7/x86_64
简介    : The Linux kernel
网址    :http://www.kernel.org/
协议    : GPLv2
描述    : The kernel package contains the Linux kernel (vmlinuz), the core of any
         : Linux operating system.  The kernel handles the basic functions
         : of the operating system: memory allocation, process allocation, device
         : input and output, etc.


# 3. 升级内核
[root@CentOS76 linuxsrc]# yum update kernel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirrors.ustc.edu.cn
 * extras: mirrors.ustc.edu.cn
 * updates: mirrors.ustc.edu.cn
正在解决依赖关系
--> 正在检查事务
---> 软件包 kernel.x86_64.0.3.10.0-1160.95.1.el7 将被 安装
--> 解决依赖关系完成

依赖关系解决

=======================================================================================================
 Package             架构                版本                               源                    大小
=======================================================================================================
正在安装:
 kernel              x86_64              3.10.0-1160.95.1.el7               updates               52 M

事务概要
=======================================================================================================
安装  1 软件包

总计:52 M
安装大小:66 M
Is this ok [y/d/N]: y
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  正在安装    : kernel-3.10.0-1160.95.1.el7.x86_64                                                 1/1 
  验证中      : kernel-3.10.0-1160.95.1.el7.x86_64                                                 1/1 

已安装:
  kernel.x86_64 0:3.10.0-1160.95.1.el7                                                                 

完毕!

# 4. 查看已经安装的内核
[root@CentOS76 linuxsrc]# yum list kernel -q
已安装的软件包
kernel.x86_64                              3.10.0-1160.el7                                    @anaconda
kernel.x86_64                              3.10.0-1160.95.1.el7                               @updates 

# 5. 查看当前的内核版本
[root@CentOS76 linuxsrc]# uname -a
Linux CentOS76 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
# 注意到显示的内核还是升级前的内核,只是下次重启时会选择进入哪个内核

升级内核完成后重启。重启后的页面如下图所示,现在选择哪个,系统都可以正常使用,原来的用户、安装的软件都在,内核升级后完全兼容以前版本的所有功能。

图23-3 内核升级后重启

  1. 《鸟哥的Linux私房菜 基础学习篇 第四版》 ↩︎

  2. 《鸟哥的Linux私房菜 服务器架设篇 第三版》 ↩︎

  3. 《韩顺平_2021图解Linux全面升级》 ↩︎

  4. 《Linux0.01内核源代码及注释》 ↩︎ ↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

虎慕

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

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

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

打赏作者

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

抵扣说明:

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

余额充值