mirror of
https://github.com/MintCN/linux-insides-zh.git
synced 2026-04-25 11:11:20 +08:00
complete
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
仍旧是与系统架构有关的初始化
|
||||
===========================================================
|
||||
|
||||
在之前的[章节](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-5.html)我们从 [arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.c)了解了特定系统架构 (在我们的例子中是 `x86_64` )的初始化内容,并且通过 `x86_configure_nx` 函数根据对[NX bit](http://en.wikipedia.org/wiki/NX_bit)的支持设置 `_PAGE_NX` 标志。正如我之前写的, `setup_arch` 函数和 `start_kernel` 都非常庞大,所以在这个和下个章节我们将继续学习关于系统架构初始化进程的内容。`x86_configure_nx` 函数的下面是 `parse_early_param`。这个函数的定义在 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 并且你可以从它的名字中了解到,这个函数解析内核命令行并且基于给定的参数 (所有的内核命令行参数你都可以在 [Documentation/kernel-parameters.txt](https://github.com/torvalds/linux/blob/master/Documentation/kernel-parameters.txt) 找到)。 你可能记得在最前面的 [章节](http://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-2.html) 我们怎样安装 `earlyprintk`。在早期我们在 [arch/x86/boot/cmdline.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cmdline.c)里面的 `cmdline_find_option` 函数和 `__cmdline_find_option`, `__cmdline_find_option_bool` 的帮助下寻找内核参数和他们的值。我们在通用内核部分不依赖于系统架构,在这里我们使用另一种方法。 如果你正在阅读linux内核源代码,你可能注意到这样的调用:
|
||||
在之前的[章节](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-5.html)我们从 [arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.c)了解了特定系统架构的初始化内容(在我们的例子中是 `x86_64` ),并且通过 `x86_configure_nx` 函数根据对[NX bit](http://en.wikipedia.org/wiki/NX_bit)的支持配置了 `_PAGE_NX` 标志位。正如我之前写的, `setup_arch` 函数和 `start_kernel` 都非常复杂,所以在这个和下个章节我们将继续学习关于系统架构初始化进程的内容。`x86_configure_nx` 函数的下面是 `parse_early_param`函数。这个函数定义在 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c) 中并且你可以从它的名字中了解到,这个函数解析内核命令行并且基于给定的参数 (所有的内核命令行参数你都可以在 [Documentation/kernel-parameters.txt](https://github.com/torvalds/linux/blob/master/Documentation/kernel-parameters.txt) 找到)。 你可能记得在最前面的 [章节](http://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-2.html) 我们是怎样安装 `earlyprintk`。在前面我们在 [arch/x86/boot/cmdline.c](https://github.com/torvalds/linux/blob/master/arch/x86/boot/cmdline.c)里面的 `cmdline_find_option` 和 `__cmdline_find_option`, `__cmdline_find_option_bool` 函数的帮助下寻找内核参数及其值。我们在通用内核部分不依赖于特定的系统架构,在这里我们使用另一种方法。 如果你正在阅读linux内核源代码,你可能注意到这样的调用:
|
||||
|
||||
```C
|
||||
early_param("gbpages", parse_direct_gbpages_on);
|
||||
@@ -23,7 +23,7 @@ early_param("gbpages", parse_direct_gbpages_on);
|
||||
```
|
||||
|
||||
这个定义可以在 [include/linux/init.h](https://github.com/torvalds/linux/blob/master/include/linux/init.h)中可以找到.
|
||||
正如你所看到的 `early_param` 宏只是调用了 `__setup_param` 宏:
|
||||
正如你所看到的, `early_param` 宏只是调用了 `__setup_param` 宏:
|
||||
|
||||
```C
|
||||
#define __setup_param(str, unique_id, fn, early) \
|
||||
@@ -35,7 +35,7 @@ early_param("gbpages", parse_direct_gbpages_on);
|
||||
= { __setup_str_##unique_id, fn, early }
|
||||
```
|
||||
|
||||
这个宏定义了 `__setup_str_*_id` 变量 (这里的 `*` 取决于被给定的函数名称) 并且把给定的命令行参数赋值给这个变量。在下一行中,我们可以看到类型为 `obs_kernel_param` 的变量 `__setup_ *` 的定义及其初始化。
|
||||
这个宏内部定义了 `__setup_str_*_id` 变量 (这里的 `*` 取决于给定的函数名称),然后把给定的命令行参数赋值给这个变量。在下一行中,我们可以看到定义了一个`obs_kernel_param` 类型的变量 `__setup_ *` 并进行初始化。
|
||||
|
||||
`obs_kernel_param` 结构体定义如下:
|
||||
|
||||
@@ -50,10 +50,10 @@ struct obs_kernel_param {
|
||||
这个结构体包含三个字段:
|
||||
|
||||
* 内核参数的名称
|
||||
* 功能取决于参数的函数
|
||||
* 根据不同的参数,选取对应的处理函数
|
||||
* 决定参数是否为 early 的标记位 (译者注:这个参数是个标记位,有0和1两种值,两种值的后续调用是不一样的)
|
||||
|
||||
注意 `__set_param` 宏定义有 `__section(.init.setup)` 属性。这意味着所有 `__setup_str_ *` 将被放置在 `.init.setup` 区段中,此外正如我们在 [include/asm-generic/vmlinux.lds.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/vmlinux.lds.h) 中看到的,他们将被放置在 `__setup_start` 和 `__setup_end` 之间:
|
||||
注意 `__set_param` 宏定义有 `__section(.init.setup)` 属性。这意味着所有 `__setup_str_ *` 将被放置在 `.init.setup` 区段中,此外正如我们在 [include/asm-generic/vmlinux.lds.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/vmlinux.lds.h) 中看到的,`.init.setup` 区段被放置在 `__setup_start` 和 `__setup_end` 之间:
|
||||
|
||||
```
|
||||
#define INIT_SETUP(initsetup_align) \
|
||||
@@ -63,7 +63,7 @@ struct obs_kernel_param {
|
||||
VMLINUX_SYMBOL(__setup_end) = .;
|
||||
```
|
||||
|
||||
现在我们知道了参数是怎样定义的,让我们一起回到 `parse_early_param` 的实现:
|
||||
现在我们知道了参数是怎样定义的,让我们一起回到 `parse_early_param` 的实现上来:
|
||||
|
||||
```C
|
||||
void __init parse_early_param(void)
|
||||
@@ -80,26 +80,29 @@ void __init parse_early_param(void)
|
||||
done = 1;
|
||||
}
|
||||
```
|
||||
`parse_early_param` 函数定义了两个静态变量。首先第一个变量 `done` 用来检查 `parse_early_param` 函数是否已经被调用,第二个变量是用来临时存储内核命令行的。在这之后我们把 `boot_command_line` 赋值到我们刚才定义的临时命令行变量中( `tmp_cmdline` ) 并且从相同的源代码文件 `main.c` 中调用 `parse_early_options` 函数。 `parse_early_options`函数从 [kernel/params.c](https://github.com/torvalds/linux/blob/master/) 中调用 `parse_args` 函数, `parse_args` 解析传入的命令行并且调用 `do_early_param` 函数。 这个 [函数](https://github.com/torvalds/linux/blob/master/init/main.c#L413) 从 ` __setup_start` 循环到 `__setup_end` ,并且如果 `obs_kernel_param` 中的 `early` 字段值为early(1) ,从 `obs_kernel_param` 调用函数(译者注:调用结构体中的第二个参数,这个参数是个函数)。在这之后所有基于早期命令行参数的服务都被创建,在 `parse_early_param` 之后的下一个调用是 `x86_report_nx` 。 正如我在这章开头所写的,我们已经用 `x86_configure_nx` 配置了 `NX-bit` 位。接下来的 [arch/x86/mm/setup_nx.c](https://github.com/torvalds/linux/blob/master/arch/x86/mm/setup_nx.c)中的 `x86_report_nx`函数仅仅打印出关于 `NX` 的信息。注意我们 `x86_report_nx` 不一定在 `x86_configure_nx` 之后调用,但是一定在 `parse_early_param` 之后调用。答案很简单: 因为内核支持 `noexec` 参数,所以我们在 `parse_early_param` 之后调用 `x86_report_nx` :
|
||||
`parse_early_param` 函数内部定义了两个静态变量。首先第一个变量 `done` 用来检查 `parse_early_param` 函数是否已经被调用过,第二个变量是用来临时存储内核命令行的。然后我们把 `boot_command_line` 的值赋值给刚刚定义的临时命令行变量中( `tmp_cmdline` ) 并且从相同的源代码文件 `main.c` 中调用 `parse_early_options` 函数。 `parse_early_options`函数从 [kernel/params.c](https://github.com/torvalds/linux/blob/master/) 中调用 `parse_args` 函数, `parse_args` 解析传入的命令行然后调用 `do_early_param` 函数。 `do_early_param` [函数](https://github.com/torvalds/linux/blob/master/init/main.c#L413) 从 ` __setup_start` 循环到 `__setup_end` ,如果循环中 `obs_kernel_param` 中的 `early` 字段值为1 ,就调用 `obs_kernel_param` 中的第二个函数 `setup_func`。在这之后所有基于早期命令行参数的服务都已经被创建,在 `parse_early_param` 之后的下一个函数调用是 `x86_report_nx` 。 正如我在这章开头所写的,我们已经用 `x86_configure_nx` 函数配置了 `NX-bit` 位。接下来 [arch/x86/mm/setup_nx.c](https://github.com/torvalds/linux/blob/master/arch/x86/mm/setup_nx.c)中的 `x86_report_nx`函数打印出关于 `NX` 的信息。注意`x86_report_nx` 函数不一定在 `x86_configure_nx` 函数之后调用,但是一定在 `parse_early_param` 之后调用。答案很简单: 因为内核支持 `noexec` 参数,所以我们一定在 `parse_early_param` 调用之后才能调用 `x86_report_nx` :
|
||||
|
||||
```
|
||||
noexec [X86]
|
||||
On X86-32 available only on PAE configured kernels.
|
||||
//在X86-32架构上,仅在配置PAE的内核上可用。
|
||||
noexec=on: enable non-executable mappings (default)
|
||||
//noexec=on:开启不可执行文件的映射(默认)
|
||||
noexec=off: disable non-executable mappings
|
||||
//noexec=off: 禁用不可执行文件的映射
|
||||
```
|
||||
|
||||
我们可以在启动的时候看到:
|
||||
|
||||

|
||||
|
||||
在这之后我们可以看到下面函数的调用:
|
||||
之后我们可以看到下面函数的调用:
|
||||
|
||||
```C
|
||||
memblock_x86_reserve_range_setup_data();
|
||||
```
|
||||
|
||||
这个函数被定义在相同的源代码文件 [arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.c) 中,并且为 `setup_data` 重新映射内存,保留内存块(你可以阅读之前的 [章节](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-5.html) 了解关于 `setup_data` 的更多内容,你也可以在[Linux kernel memory management](http://xinqiu.gitbooks.io/linux-insides-cn/content/MM/index.html) 中阅读到关于 `ioremap` and `memblock` 的内容)。
|
||||
这个函数的定义在相同的源代码文件 [arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.c) 中,然后这个函数为 `setup_data` 重新映射内存并保留内存块(你可以阅读之前的 [章节](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-5.html) 了解关于 `setup_data` 的更多内容,你也可以在[Linux kernel memory management](http://xinqiu.gitbooks.io/linux-insides-cn/content/MM/index.html) 中阅读到关于 `ioremap` and `memblock` 的内容)。
|
||||
|
||||
接下来我们来看看下面的条件语句:
|
||||
|
||||
@@ -112,7 +115,7 @@ noexec [X86]
|
||||
}
|
||||
```
|
||||
|
||||
[arch/x86/kernel/acpi/boot.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/acpi/boot.c) 中的函数 `acpi_mps_check` 取决于 `CONFIG_X86_LOCAL_APIC` 和 `CONFIG_x86_MPPARSE` 配置选项:
|
||||
[arch/x86/kernel/acpi/boot.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/acpi/boot.c) 中的函数 `acpi_mps_check` 的结果取决于 `CONFIG_X86_LOCAL_APIC` 和 `CONFIG_x86_MPPARSE` 配置选项:
|
||||
|
||||
```C
|
||||
int __init acpi_mps_check(void)
|
||||
@@ -130,13 +133,13 @@ int __init acpi_mps_check(void)
|
||||
}
|
||||
```
|
||||
|
||||
这个函数检查内置的 `MPS` 又称 [多重处理器规范]((http://en.wikipedia.org/wiki/MultiProcessor_Specification)) 表。如果设置了 ` CONFIG_X86_LOCAL_APIC` 但未设置 `CONFIG_x86_MPPAARSE` ,并且传递给内核的命令行参数是 `acpi=off`、`acpi=noirq` 或者 `pci=noacpi`,那么`acpi_mps_check` 就会打印出警告信息。如果 `acpi_mps_check` 返回1,这表示我们警用了本地 [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)
|
||||
,而且通过 `setup_clear_cpu_cap` 宏清除了当前CPU中的 `X86_FEATURE_APIC` 位。(你可以阅读[CPU masks](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-2.html)了解更多关于CPU mask的内容)。
|
||||
`acpi_mps_check` 函数检查内建的 `MPS` 又称 [多重处理器规范]((http://en.wikipedia.org/wiki/MultiProcessor_Specification)) 表。如果设置了 ` CONFIG_X86_LOCAL_APIC` 但未设置 `CONFIG_x86_MPPAARSE` ,而且传递给内核的命令行选项中有 `acpi=off`、`acpi=noirq` 或者 `pci=noacpi`,那么`acpi_mps_check` 函数就会输出出警告信息。如果 `acpi_mps_check` 返回了1,这就表示我们禁用了本地 [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)
|
||||
,而且 `setup_clear_cpu_cap` 宏清除了当前CPU中的 `X86_FEATURE_APIC` 位。(你可以阅读[CPU masks](https://xinqiu.gitbooks.io/linux-insides-cn/content/Concepts/linux-cpu-2.html)了解关于CPU mask的更多内容)。
|
||||
|
||||
早期的PCI转储
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
接下来我们使用下面的代码来转储 [PCI](http://en.wikipedia.org/wiki/Conventional_PCI) 设备:
|
||||
接下来我们通过下面的代码来转储 [PCI](http://en.wikipedia.org/wiki/Conventional_PCI) 设备:
|
||||
|
||||
```C
|
||||
#ifdef CONFIG_PCI
|
||||
@@ -145,13 +148,13 @@ int __init acpi_mps_check(void)
|
||||
#endif
|
||||
```
|
||||
|
||||
`pci_early_dump_regs` 定义在 [arch/x86/pci/common.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/common.c) 中,并且他的值是取决于内核命令行参数:`pci=earlydump` 。我们可以在[drivers/pci/pci.c](https://github.com/torvalds/linux/blob/master/arch) 中看到这个参数的定义:
|
||||
变量 `pci_early_dump_regs` 定义在 [arch/x86/pci/common.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/common.c) 中,他的值取决于内核命令行参数:`pci=earlydump` 。我们可以在[drivers/pci/pci.c](https://github.com/torvalds/linux/blob/master/arch) 中看到这个参数的定义:
|
||||
|
||||
```C
|
||||
early_param("pci", pci_setup);
|
||||
```
|
||||
|
||||
`pci_setup` 函数获得 `pci=` 之后的字符串,然后进行分析。这个函数调用了在 [drivers/pci/pci.c](https://github.com/torvalds/linux/blob/master/arch) 中用 `_weak` 修饰符定义的 `pcibios_setup` 函数,并且每种架构都重写了 `_weak` 修饰过的函数。 例如 依赖于 `x86_64` 架构的版本写在[arch/x86/pci/common.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/common.c)中:
|
||||
`pci_setup` 函数取出 `pci=` 之后的字符串,然后进行解析。这个函数调用 [drivers/pci/pci.c](https://github.com/torvalds/linux/blob/master/arch) 中用 `_weak` 修饰符定义的 `pcibios_setup` 函数,并且每种架构都重写了 `_weak` 修饰过的函数。 例如, `x86_64` 架构上的该函数版本在[arch/x86/pci/common.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/common.c)中:
|
||||
|
||||
```C
|
||||
char *__init pcibios_setup(char *str) {
|
||||
@@ -168,14 +171,14 @@ char *__init pcibios_setup(char *str) {
|
||||
}
|
||||
```
|
||||
|
||||
所以,如果 `CONFIG_PCI` 选项被设置,而且我们向内核命令行传递了 `pci=earlydump` 选项,下一个被调用的函数是在 [arch/x86/pci/early.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/early.c)中的 `early_dump_pci_devices` 。这个函数使用下面代码检查 `noearly` pci 参数:
|
||||
如果 `CONFIG_PCI` 选项被设置,而且我们向内核命令行传递了 `pci=earlydump` 选项,那么 [arch/x86/pci/early.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/early.c)中的 `early_dump_pci_devices` 函数将会被调用。这个函数像下面这样来检查pci参数 `noearly` :
|
||||
|
||||
```C
|
||||
if (!early_pci_allowed())
|
||||
return;
|
||||
```
|
||||
|
||||
同时如果条件通过则返回。每个PCI域可以承载多达 `256` 条总线,并且每条总线可以承载多达32个设备。那么接下来我们进行下面的循环:
|
||||
如果条件则返回。每个PCI域可以承载多达 `256` 条总线,并且每条总线可以承载多达32个设备。那么接下来我们进入下面的循环:
|
||||
|
||||
```C
|
||||
for (bus = 0; bus < 256; bus++) {
|
||||
@@ -193,7 +196,7 @@ for (bus = 0; bus < 256; bus++) {
|
||||
|
||||
这就是 pci 加载的全部了。我们在这里不会深入研究 `pci` 的细节,不过我们会在 `Drivers/PCI` 章节看到更多的细节。
|
||||
|
||||
完成内存解析
|
||||
内存解析的完成
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -211,14 +214,15 @@ for (bus = 0; bus < 256; bus++) {
|
||||
early_reserve_e820_mpc_new();
|
||||
```
|
||||
|
||||
让我们来一起看看上面的代码。正如你所看到的,第一个函数是 `e820_reserve_setup_data` 。这个函数和我们前面看到的 `memblock_x86_reserve_range_setup_data` 函数做的事情几乎是相同的,但是这个函数同时还会调用 `e820_update_range` 函数,向 `e820map` 中用给定的类型添加新的区域,在我们的例子中,使用的是 `E820_RESERVED_KERN` 类型。接下来的函数是 `finish_e820_parsing` 。除了这两个函数之外,我们还可以看到一些与 [e820](http://en.wikipedia.org/wiki/E820) 有关的函数。你可以在上面的清单中看到这些函数。`e820_add_kernel_range` 函数需要内核开始和结束的物理地址:
|
||||
让我们来一起看看上面的代码。正如你所看到的,第一个函数是 `e820_reserve_setup_data` 。这个函数和我们前面看到的 `memblock_x86_reserve_range_setup_data` 函数做的事情几乎是相同的,但是这个函数同时还会调用 `e820_update_range` 函数,向 `e820map` 中用给定的类型添加新的区域,在我们的例子中,使用的是 `E820_RESERVED_KERN` 类型。接下来的函数是 `finish_e820_parsing`,这个函数使用 `sanitize_e820_map` 函数对 `e820map` 进行清理。除了这两个函数之外,我们还可以看到一些与 [e820](http://en.wikipedia.org/wiki/E820) 有关的函数。你可以在上面的列表中看到这些函数。`e820_add_kernel_range` 函数需要内核开始和结束的物理地址:
|
||||
|
||||
```C
|
||||
u64 start = __pa_symbol(_text);
|
||||
u64 size = __pa_symbol(_end) - start;
|
||||
```
|
||||
|
||||
函数会检查在 `e820map` 中被标记成 `E820RAM` 的 `.text` `.data` 和 `.bss` 段,如果没有这些字段,那么就会输出错误信息。接下来的 `trm_bios_range` 函数把 `e820Map` 中的前4096个字节更新为 `E820_RESERVED` 并且调用函数 `sanitize_e820_map` 进行处理。在这之后我们使用 `e820_end_of_ram_pfn` 得到最后一个页面帧的编号,每个内存页面都有一个唯一的编号 - `页面帧编号`, `e820_end_of_ram_pfn` 函数调用 `e820_end_pfn` 函数返回这个最大的页面帧编号:
|
||||
函数会检查在 `e820map` 中被标记成 `E820RAM` 的 `.text` `.data` 和 `.bss` 区段,如果没有这些字段,那么就会输出错误信息。接下来的 `trm_bios_range` 函数把 `e820Map` 中的前4096个字节修改为 `E820_RESERVED` 并且再次调用函数 `sanitize_e820_map` 进行清理。在这之后我们使用 `e820_end_of_ram_pfn` 函数得到最后一个页帧的编号,每个内存页面都有一个唯一的编号 - `页帧号`, `e820_end_of_ram_pfn` 函数调用 `e820_end_pfn` 函数返回最大的页面帧号:
|
||||
|
||||
```C
|
||||
unsigned long __init e820_end_of_ram_pfn(void)
|
||||
{
|
||||
@@ -226,7 +230,7 @@ unsigned long __init e820_end_of_ram_pfn(void)
|
||||
}
|
||||
```
|
||||
|
||||
`e820_end_pfn` 函数读取当前系统架构的最大页面帧编号(对于 `x86_64` 架构来说 `MAX_ARCH_PFN` 是 `0x400000000` )。在 `e820_end_pfn` 函数中我们遍历整个 `e820` 插槽,并且检查 `e820` 入口是否有 `E820_RAM` 或者 `E820_PRAM` 类型,因为我们只能对这些类型计算页面帧编码,得到当前 `e820` 入口页面帧的基地址和结束地址,同时对这些地址进行检查:
|
||||
`e820_end_pfn` 函数读取特定于系统架构的最大页帧号(对于 `x86_64` 架构来说 `MAX_ARCH_PFN` 是 `0x400000000` )。在 `e820_end_pfn` 函数中我们遍历整个 `e820` 槽,并且检查 `e820` 入口是否有 `E820_RAM` 或者 `E820_PRAM` 类型,因为我们只能对这些类型计算页面帧码,得到当前 `e820` 入口页面帧的基地址和结束地址,同时对这些地址进行检查:
|
||||
|
||||
```C
|
||||
for (i = 0; i < e820.nr_map; i++) {
|
||||
@@ -260,7 +264,7 @@ for (i = 0; i < e820.nr_map; i++) {
|
||||
return last_pfn;
|
||||
```
|
||||
|
||||
接下来我们检查在循环中得到的 `last_pfn` 不得大于特定系统架构的最大页面帧编号(在我们的例子中是 `x86_64` 系统架构),输出关于最大页面帧编码的信息,并且返回它。我们可以在 `dmesg` 的输出中看到 `last_pfn` :
|
||||
接下来我们检查在循环中得到的 `last_pfn`,`last_pfn` 不得大于特定于系统架构的最大页帧号(在我们的例子中是 `x86_64` 系统架构),输出关于最大页帧号的信息,并且返回它。我们可以在 `dmesg` 的输出中看到 `last_pfn` :
|
||||
|
||||
```
|
||||
...
|
||||
@@ -268,7 +272,7 @@ for (i = 0; i < e820.nr_map; i++) {
|
||||
...
|
||||
```
|
||||
|
||||
在这之后,我们计算出了最大的页面帧编号,我们要计算 `max_low_pfn` ,这是 `小内存` 或者低于第一个4GB中的的的最大页面帧。如果安装了超过4GB的内存RAM,`max_low_pfn` 将会是 `e820_end_of_low_ram_pfn` 函数的结果,这个函数和 `e820_end_of_ram_pfn` 相似,但是有4GB自己的限制,换句话说 `max_low_pfn` 和 `max_pfn` 的值是一样的:
|
||||
在这之后,我们计算出了最大的页面帧号,我们要计算 `max_low_pfn` ,这是 `低端内存` 或者低于第一个4GB中的最大页面帧。如果系统安装了超过4GB的内存RAM,`e820_end_of_low_ram_pfn` 函数的结果将会是 `max_low_pfn` ,这个函数和 `e820_end_of_ram_pfn` 相似,但是有4GB自己的限制,换句话说 `max_low_pfn` 和 `max_pfn` 的值是一样的:
|
||||
|
||||
```C
|
||||
if (max_pfn > (1UL<<(32 - PAGE_SHIFT)))
|
||||
@@ -279,20 +283,20 @@ else
|
||||
high_memory = (void *)__va(max_pfn * PAGE_SIZE - 1) + 1;
|
||||
```
|
||||
|
||||
接下来我们通过 `__va` 宏计算 `大内存` 中的最大页面帧编号(有更高的直接内存映射上界),这个宏根据给定的物理内存返回一个虚拟地址。
|
||||
接下来我们通过 `__va` 宏计算 `高端内存` (有更高的内存直接映射上界)中的最大页帧号,并且这个宏会根据给定的物理内存返回一个虚拟地址。
|
||||
|
||||
|
||||
直接媒体结构扫描
|
||||
桌面管理接口
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
在处理完不同内存区域和 `e820` 插槽之后的下一步就是收集有关计算机的信息。我们将获得与 [桌面管理接口](http://en.wikipedia.org/wiki/Desktop_Management_Interface) 和下面函数的所有信息:
|
||||
在处理完不同内存区域和 `e820` 槽之后,接下来就该是收集计算机的相关信息了。我们将用下面的函数收集与 [桌面管理接口](http://en.wikipedia.org/wiki/Desktop_Management_Interface) 有关的所有信息:
|
||||
|
||||
```C
|
||||
dmi_scan_machine();
|
||||
dmi_memdev_walk();
|
||||
```
|
||||
|
||||
首先是定义在 [drivers/firmware/dmi_scan.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/drivers/firmware/dmi_scan.c) 中的 `dmi_scan_machine` 函数。这个函数浏览 [System Management BIOS](http://en.wikipedia.org/wiki/System_Management_BIOS) 结构,从中提取信息。制定了两种方法来获得 `SMBIOS` 表的访问权: 第一种是从 [EFI](http://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface) 的配置表获得指向 `SMBIOS` 表的指针;第二种是扫描 `0xF0000` 和 `0x10000` 地址之间的物理地址。让我们一起看看第二种方法。`dmi_scan_machine` 函数通过 `dmi_early_remap` 函数将 `0xf0000` 和 `0x10000` 之间的内存重新映射到 `early_ioremap`:
|
||||
首先是定义在 [drivers/firmware/dmi_scan.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/drivers/firmware/dmi_scan.c) 中的 `dmi_scan_machine` 函数。这个函数遍历 [System Management BIOS](http://en.wikipedia.org/wiki/System_Management_BIOS) 结构,从中提取信息。这里有两种方法来访问 `SMBIOS` 表: 第一种是从 [EFI](http://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface) 的配置表获得指向 `SMBIOS` 表的指针;第二种是扫描 `0xF0000` 和 `0x10000` 地址之间的物理地址。让我们一起看看第二种方法。`dmi_scan_machine` 函数通过 `dmi_early_remap` 函数将 `0xf0000` 和 `0x10000` 之间的内存重新映射到 `early_ioremap`:
|
||||
|
||||
```C
|
||||
void __init dmi_scan_machine(void)
|
||||
@@ -306,7 +310,7 @@ void __init dmi_scan_machine(void)
|
||||
if (p == NULL)
|
||||
goto error;
|
||||
```
|
||||
然后遍历所有的 `DMI` 头地址,并且查找 `_SM_` 字符串:
|
||||
然后迭代所有的 `DMI` 头部地址,并且查找 `_SM_` 字符串:
|
||||
|
||||
```C
|
||||
memset(buf, 0, 16);
|
||||
@@ -320,18 +324,20 @@ for (q = p; q < p + 0x10000; q += 16) {
|
||||
memcpy(buf, buf + 16, 16);
|
||||
}
|
||||
```
|
||||
`_SM_` 字符串一定在 `000F0000h` 和 `0x000FFFFF` 地址之间。在这里我们用 `memcpy_fromio` 函数向 `buf` 里面复制16个字节,这个函数和 `memcpy` 函数的作用是一样的。然后对这个缓冲区( `buf` ) 执行`dmi_smbios3_present` 和 `dmi_present` 函数。这些函数检查 `buf`的前4个字节是否是 `__SM__` 字符串,得到 `SMBIOS` 的版本号,并且获得 `_DMI_` 的属性例如 `_DMI_` 结构表长度、表的地址等等...每当其中的一个函数完成之后,你就会在 `dmesg` 的输出中看到函数的运行结果:
|
||||
`_SM_` 字符串一定在 `000F0000h` 和 `0x000FFFFF` 地址之间。在这里我们用 `memcpy_fromio` 函数向 `buf` 里面拷贝16个字节,这个函数和 `memcpy` 函数的作用是一样的。然后对这个缓冲区( `buf` ) 执行`dmi_smbios3_present` 和 `dmi_present` 函数。这些函数检查 `buf` 的前4个字节是否是 `__SM__` 字符串,获得 `SMBIOS` 的版本和 `_DMI_` 的属性例如 `_DMI_` 的结构表长度、结构表的地址等等... 在其中的一个函数完成之后,你就可以在 `dmesg` 的输出中看到它的运行结果:
|
||||
|
||||
```
|
||||
[ 0.000000] SMBIOS 2.7 present.
|
||||
[ 0.000000] DMI: Gigabyte Technology Co., Ltd. Z97X-UD5H-BK/Z97X-UD5H-BK, BIOS F6 06/17/2014
|
||||
```
|
||||
在 `dmi_scan_machine` 函数的最后,我们取消之前重新映射的内存:
|
||||
|
||||
在 `dmi_scan_machine` 函数的最后,我们释放之前重新映射的内存:
|
||||
|
||||
```C
|
||||
dmi_early_unmap(p, 0x10000);
|
||||
```
|
||||
|
||||
第二个函数是 - `dmi_memdev_walk`。正如你理解的那样,这个函数遍历整个内存设备。让我们一起看看这个函数:
|
||||
第二个函数是 - `dmi_memdev_walk`。和你想的一样,这个函数遍历整个内存设备。让我们一起看看这个函数:
|
||||
|
||||
```C
|
||||
void __init dmi_memdev_walk(void)
|
||||
@@ -347,7 +353,7 @@ void __init dmi_memdev_walk(void)
|
||||
}
|
||||
```
|
||||
|
||||
这个函数检查 `DMI` 是否可用(我们之前在 `dmi_scan_machine` 函数中得到了这个结果)并且使用 `dmi_walk_early` 和 `dmi_alloc` 函数收集关于内存设备的信息,其中 `dmi_alloc` 的定义如下:
|
||||
这个函数检查 `DMI` 是否可用(我们之前在 `dmi_scan_machine` 函数中得到了这个结果,并且保存在 `dmi_available` 中),然后使用 `dmi_walk_early` 和 `dmi_alloc` 函数收集内存设备的有关信息,其中 `dmi_alloc` 的调用如下:
|
||||
|
||||
```
|
||||
#ifdef CONFIG_DMI
|
||||
@@ -355,7 +361,7 @@ RESERVE_BRK(dmi_alloc, 65536);
|
||||
#endif
|
||||
```
|
||||
|
||||
`RESERVE_BRK` 定义在 [arch/x86/include/asm/setup.h](http://en.wikipedia.org/wiki/Desktop_Management_Interface)中,并且这个函数在 `brk` 段中保留给定大小的空间:
|
||||
定义在 [arch/x86/include/asm/setup.h](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/include/asm/setup.h) 中的 `RESERVE_BRK` 函数在 `brk` 段中预留给定大小的空间:
|
||||
|
||||
-------------------------
|
||||
init_hypervisor_platform();
|
||||
@@ -369,7 +375,7 @@ RESERVE_BRK(dmi_alloc, 65536);
|
||||
均衡多处理(SMP)的配置
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
接下来的一步是解析 [SMP](http://en.wikipedia.org/wiki/Symmetric_multiprocessing) 的配置信息。我们调用 `find_smp_config` 函数来完成这个任务,这个函数只是调用另一个函数:
|
||||
接下来的一步是解析 [SMP](http://en.wikipedia.org/wiki/Symmetric_multiprocessing) 的配置信息。我们调用 `find_smp_config` 函数来完成这个任务,这个函数内部调用另一个函数:
|
||||
|
||||
```C
|
||||
static inline void find_smp_config(void)
|
||||
@@ -378,7 +384,7 @@ static inline void find_smp_config(void)
|
||||
}
|
||||
```
|
||||
|
||||
在函数的内部,`x86_init.mpparse.find_smp_config` 就是 [arch/x86/kernel/mpparse.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/mpparse.c) 中的 `default_find_smp_config` 函数。在 `default_find_smp_config` 函数中我们扫描一些内存区域来寻找 `SMP` 的配置信息,并在找到它们的时候返回:
|
||||
在函数的内部,`x86_init.mpparse.find_smp_config` 函数就是 [arch/x86/kernel/mpparse.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/mpparse.c) 中的 `default_find_smp_config` 函数。我们调用 `default_find_smp_config` 函数扫描内存中的一些区域来寻找 `SMP` 的配置信息,并在找到它们的时候返回:
|
||||
|
||||
```C
|
||||
if (smp_scan_config(0x0, 0x400) ||
|
||||
@@ -387,14 +393,14 @@ if (smp_scan_config(0x0, 0x400) ||
|
||||
return;
|
||||
```
|
||||
|
||||
首先 `smp_scan_config` 函数定义了一些变量:
|
||||
首先 `smp_scan_config` 函数内部定义了一些变量:
|
||||
|
||||
```C
|
||||
unsigned int *bp = phys_to_virt(base);
|
||||
struct mpf_intel *mpf;
|
||||
```
|
||||
|
||||
第一个变量是我们用来扫描 `SMP` 配置内存区域的虚拟地址;第二个变量是指向 `mpf_intel` 结构体的指针。让我们一起试着去理解 `mpf_intel` 是什么吧。所有的信息都存储在多处理器配置数据结构中。`mpf_intel` 呈现了这个结构,看下来像是下面这样:
|
||||
第一个变量是我们用来扫描 `SMP` 配置内存区域的虚拟地址;第二个变量是指向 `mpf_intel` 结构体的指针。让我们一起试着去理解 `mpf_intel` 是什么吧。所有的信息都存储在多处理器配置数据结构中。`mpf_intel` 代表了这个结构,看下来像是下面这样:
|
||||
|
||||
```C
|
||||
struct mpf_intel {
|
||||
@@ -411,9 +417,9 @@ struct mpf_intel {
|
||||
};
|
||||
```
|
||||
|
||||
正如我们在文档中看到的那样 - 系统 BIOS的主要功能之一就是创建MP指针结构和MP配置表。然后操作系统必须拥有访问多处理器配置信息,并且 `mpf_intel` 存储了多处理器配置表的物理地址(看结构体的第二个参数),然后,`smp_scan_config` 在指定的内存区域中循环查找 `MP floating pointer structure` 。它检查当前浮动指针是否指向 `SMP` 签名,并且检查他的校验和,在循环中检查 `mpf->specification` 是1还是4(这个值只能是1或者是4):
|
||||
正如我们在文档中看到的那样 - 系统 BIOS的主要功能之一就是创建MP指针结构和MP配置表。然后操作系统必须可以访问关于多处理器配置的有关信息, `mpf_intel` 中存储了多处理器配置表的物理地址(看结构体的第二个变量),然后,`smp_scan_config` 在指定的内存区域中循环查找 `MP floating pointer structure` 。它检查当前字节是否指向 `SMP` 签名,然后检查签名的校验和,并且在循环中检查 `mpf->specification` 的值是1还是4(这个值只能是1或者是4):
|
||||
|
||||
```C
|
||||
```C7
|
||||
while (length > 0) {
|
||||
if ((*bp == SMP_MAGIC_IDENT) &&
|
||||
(mpf->length == 1) &&
|
||||
@@ -432,7 +438,7 @@ if ((*bp == SMP_MAGIC_IDENT) &&
|
||||
其他的早期内存初始化程序
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
在 `setup_arch` 的下一步,我们可以看到 `early_alloc_pgt_buf` 函数的调用,这个函数为早期阶段分配页表缓冲区。页表缓冲区将被防止在 `brk` 段中。让我们一起看看这个函数的实现:
|
||||
在 `setup_arch` 的下一步,我们可以看到 `early_alloc_pgt_buf` 函数的调用,这个函数在早期阶段分配页表缓冲区。页表缓冲区将被放置在 `brk` 区段中。让我们一起看看这个功能的实现:
|
||||
|
||||
|
||||
```C
|
||||
@@ -449,7 +455,7 @@ void __init early_alloc_pgt_buf(void)
|
||||
}
|
||||
```
|
||||
|
||||
首先这个函数获得页表缓冲区的大小,它的值是 `INIT_PGT_BUF_SIZE` ,这个值在目前的linux 4.0 内核中是 `(6 * PAGE_SIZE)`。因为我们已经得到了页表缓冲区的大小,我们调用 `extend_brk` 函数并且传入两个参数: size和align。你可以从他们的名称中了解到,这个函数扩展 `brk` 段。正如我们在linux 内核链接脚本中看到的,`brk`在内存中的位置恰好在 [BSS](http://en.wikipedia.org/wiki/.bss) 后面:
|
||||
首先这个函数获得页表缓冲区的大小,它的值是 `INIT_PGT_BUF_SIZE` ,这个值在目前的linux 4.0 内核中是 `(6 * PAGE_SIZE)`。因为我们已经得到了页表缓冲区的大小,现在我们调用 `extend_brk` 函数并且传入两个参数: size和align。你可以从他们的名称中猜到,这个函数扩展 `brk` 段。正如我们在linux内核链接脚本中看到的,`brk` 区段在内存中的位置恰好就在 [BSS](http://en.wikipedia.org/wiki/.bss) 区段后面:
|
||||
|
||||
```C
|
||||
. = ALIGN(PAGE_SIZE);
|
||||
@@ -461,11 +467,11 @@ void __init early_alloc_pgt_buf(void)
|
||||
}
|
||||
```
|
||||
|
||||
或者我们可以使用 `readelf` 工具来找到它:
|
||||
我们也可以使用 `readelf` 工具来找到它:
|
||||
|
||||

|
||||
|
||||
之后我们用 `_pa` 宏得到了新的 `brk`段的物理地址,我们计算页表缓冲区的基地址和结束地址。下一步因为我们之前创建了页面缓冲区,我们使用 `reserve_brk` 函数为 brk 段保留内存块:
|
||||
之后我们用 `_pa` 宏得到了新的 `brk` 区段的物理地址,我们计算页表缓冲区的基地址和结束地址。因为我们之前已经创建好了页面缓冲区,所以现在我们使用 `reserve_brk` 函数为 `brk` 区段保留内存块:
|
||||
|
||||
```C
|
||||
static void __init reserve_brk(void)
|
||||
@@ -477,7 +483,8 @@ static void __init reserve_brk(void)
|
||||
_brk_start = 0;
|
||||
}
|
||||
```
|
||||
注意在 `reserve_brk` 的最后,我们把 `_brk_start` 设置为0,因为在这之后我们不会再为它分配内存了,我们需要使用 `cleanup_highmap` 函数来取消内核映射中越界的内存区域。请记住内核映射是 `__START_KERNEL_map` 和 `_end - _text` 或者 `level2_kernel_pgt` 对内核 `_text`、`data` 和 `bss` 的映射。在 `clean_high_map` 的开始部分我们定义这些参数:
|
||||
|
||||
注意在 `reserve_brk` 的最后,我们把 `_brk_start` 赋值为0,因为在这之后我们不会再为 `brk` 分配内存了,我们需要使用 `cleanup_highmap` 函数来释放内核映射中越界的内存区域。请记住内核映射是 `__START_KERNEL_map` 和 `_end - _text` 或者 `level2_kernel_pgt` 对内核 `_text`、`data` 和 `bss` 区段的映射。在 `clean_high_map` 的开始部分我们定义这些参数:
|
||||
|
||||
```C
|
||||
unsigned long vaddr = __START_KERNEL_map;
|
||||
@@ -486,7 +493,7 @@ pmd_t *pmd = level2_kernel_pgt;
|
||||
pmd_t *last_pmd = pmd + PTRS_PER_PMD;
|
||||
```
|
||||
|
||||
现在,因为我们已经定义了内核映射的开始和结束部分,我们在循环中遍历所有内核页中间目录调墨油, 并清除不在 `_text` 和 `end` 段之间的条目:
|
||||
现在,因为我们已经定义了内核映射的开始和结束位置,所以我们在循环中遍历所有内核页中间目录条目, 并且清除不在 `_text` 和 `end` 区段中的条目:
|
||||
|
||||
```C
|
||||
for (; pmd < last_pmd; pmd++, vaddr += PMD_SIZE) {
|
||||
@@ -497,7 +504,7 @@ for (; pmd < last_pmd; pmd++, vaddr += PMD_SIZE) {
|
||||
}
|
||||
```
|
||||
|
||||
在这之后,我们使用 `memblock_set_current_limit` (你可以在[linux 内存管理 第二章节](https://github.com/MintCN/linux-insides-zh/blob/master/MM/linux-mm-2.md) 阅读关于 `memblock` 的更多内容) 函数来设置 `memblock` 分配的限制,它将是`ISA_END_ADDRESS` 或者 `0x100000` 并且它会调用 `memblock_x86_fill` 函数根据 `e820` 来填充 `memblock` 信息。你可以在内核初始化的时候看到这个函数运行的结果:
|
||||
在这之后,我们使用 `memblock_set_current_limit` (你可以在[linux 内存管理 第二章节](https://github.com/MintCN/linux-insides-zh/blob/master/MM/linux-mm-2.md) 阅读关于 `memblock` 的更多内容) 函数来为 `memblock` 分配内存设置一个界限,这个界限是`ISA_END_ADDRESS` 或者 `0x100000` ,然后调用 `memblock_x86_fill` 函数根据 `e820` 来填充 `memblock` 信息。你可以在内核初始化的时候看到这个函数运行的结果:
|
||||
|
||||
```
|
||||
MEMBLOCK configuration:
|
||||
@@ -511,14 +518,15 @@ MEMBLOCK configuration:
|
||||
reserved[0x1] [0x00000001000000-0x00000001a57fff], 0xa58000 bytes flags: 0x0
|
||||
reserved[0x2] [0x0000007ec89000-0x0000007fffffff], 0x1377000 bytes flags: 0x0
|
||||
```
|
||||
在 `memblock_x86_fill` 之后的其他函数: `early_reserve_e820_mpc_new` 在 `e820map` 中为多处理器规格表分配额外的槽, `reserve_real_mode` - 保留从 `0x0` 到1M的低内存用作实模式的跳板(用于重启等...),`trim_platform_memory_ranges` 函数删除掉以 `0x20050000`, `0x20110000` 等地址开头的内存空间。这些内存区域必须被排除在外,因为 [Sandy Bridge](http://en.wikipedia.org/wiki/Sandy_Bridge) 会在这些区域出现一些故障, `trim_low_memory_range` 保留 `memblock` 中的前4KB页面,`init_mem_mapping` 函数重新创建直接内存映射并且建立在 `PAGE_OFFSET` 处物理内存的直接映射, `early_trap_pf_init` 建立了 `#PF` 处理函数(我们将会在有关中断的章节看到它),然后 `setup_real_mode` 函数建立到 [实模式]http://en.wikipedia.org/wiki/Real_mode) 代码的跳板。
|
||||
|
||||
这就是本章的全部内容了。您可能注意到这部分并没有包括 `setup_arch` 中的所有函数 (如 "early_gart_iommu_check"、[mtrr](http://en.wikipedia.org/wiki/Memory_type_range_register) 初始化等...)。正如我已经写了很多次的, `setup_arch` 很复杂 linux 内核也很复杂。这就是为什么我不能囊括 linux 内核中的每一行。我认为我们并没有错过重要的东西, 但是你可以这样说: 每行代码都很重要。是的, 这是真的, 但是不管怎样我错过了他们, 因为我认为对于整个linux内核面面俱到是不现实的。无论如何, 我们会经常回到我们已经看到的想法, 如果有什么不熟悉的, 我们将讨论这个主题。
|
||||
除了 `memblock_x86_fill` 之外的其他函数有: `early_reserve_e820_mpc_new` 在 `e820map` 中为多处理器规格表分配额外的槽, `reserve_real_mode` - 保留从 `0x0` 到1M的低端内存用作到实模式的跳板(用于重启等...),`trim_platform_memory_ranges` 函数清除掉以 `0x20050000`, `0x20110000` 等地址开头的内存空间。这些内存区域必须被排除在外,因为 [Sandy Bridge](http://en.wikipedia.org/wiki/Sandy_Bridge) 会在这些内存区域出现一些问题, `trim_low_memory_range` 保留 `memblock` 中的前4KB页面,`init_mem_mapping` 函数重新创建内存的直接映射,然后在 `PAGE_OFFSET` 处建立物理内存的直接映射, `early_trap_pf_init` 建立了 `#PF` 处理函数(我们将会在有关中断的章节看到它),然后 `setup_real_mode` 函数建立了到 [实模式]http://en.wikipedia.org/wiki/Real_mode) 代码的跳板。
|
||||
|
||||
这就是本章的全部内容了。您可能注意到这部分并没有包括 `setup_arch` 中的所有函数 (如 `early_gart_iommu_check`、[mtrr](http://en.wikipedia.org/wiki/Memory_type_range_register) 的初始化等...)。正如我已经说了很多次的, `setup_arch` 函数很复杂,linux内核也很复杂。这就是为什么我不能对于linux 内核中的每一行面面俱到。我认为我们并没有错过重要的东西, 但是你可能会说: 每行代码都很重要。是的, 这没错, 但不管怎样我略过了他们, 因为我认为对于整个linux内核面面俱到是不现实的。无论如何, 我们会经常复习我们已经了解的, 如果有什么不熟悉的, 我们将覆盖这个主题。
|
||||
|
||||
结束语
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
这里是linux 内核初始化进程第六章节的结尾。在这一章节中,我们再次深入研究了 `setup_arch` 函数,然而这是个很长的部分,我们目前还没有完成它。没错, `setup_arch`很复杂,希望下个章节将会是这个函数的最后一个部分。(译者注:假的)。
|
||||
这里是linux 内核初始化进程第六章节的结尾。在这一章节中,我们再次深入研究了 `setup_arch` 函数,然而这是个很长的部分,我们目前还没有学习完。的确, `setup_arch`很复杂,希望下个章节将会是这个函数的最后一个部分。。
|
||||
|
||||
如果你有任何的疑问或者建议,你可以留言,也可以直接发消息给我[twitter](https://twitter.com/0xAX)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user