进入main函数后,Linux内核执行硬件检测及初始化工作,即便在此之前BIOS已经对大部分硬件设备进行了相应的初始化,然而Linux并不依赖于BIOS,而是以特定的方式重新初始化相关设备,这样做的目的是为了增强可移植性及健壮性。需要强调的一点是,此时C语言仍旧运行在实模式状态下。
拷贝启动参数
进入arch\x86\boot\Main.c文件的main函数后,做的第一件事就是将从Kernel boot sector中偏移0x1f1处起始的hdr头变量拷贝至指定内存中。
- /* First, copy the boot header into the "zeropage" */
- copy_boot_params();
/* First, copy the boot header into the "zeropage" */
copy_boot_params();
可以看出该指定内存被称为“零页”,之所以这么称呼,是因为在保护模式下启用分页机制后,它是整个内存单元的第一个分页。注意启动头(boot header)与系统启动(二)中的安装头(setup header)均指的是hdr变量。下面是它的具体实现:
- /*
- * Copy the header into the boot parameter block. Since this
- * screws up the old-style command line protocol, adjust by
- * filling in the new-style command line pointer instead.
- */
- struct old_cmdline {
- u16 cl_magic;
- u16 cl_offset;
- };
- const struct old_cmdline * const oldcmd =
- (const struct old_cmdline *)OLD_CL_ADDRESS;
/*
* Copy the header into the boot parameter block. Since this
* screws up the old-style command line protocol, adjust by
* filling in the new-style command line pointer instead.
*/
struct old_cmdline {
u16 cl_magic;
u16 cl_offset;
};
const struct old_cmdline * const oldcmd =
(const struct old_cmdline *)OLD_CL_ADDRESS;
首先解释下注释:拷贝boot header的过程无法和老式命令行协议谐调,所以需要通过填充新类型命令行指针调整。如上所示,拷贝过程中首先将指向常量的结构体指针常量赋值为OLD_CL_ADDRESS,其值在arch\x86\include\asm\Setup.h文件中定义如下:
- #define OLD_CL_ADDRESS 0x020 /* Relative to real mode data */
#define OLD_CL_ADDRESS 0x020 /* Relative to real mode data */
从old_cmdline结构体的定义来看,我们就可大致猜测出这是针对老式命令行定义的变量类型,其中分别包含命令行魔数(cl_magic)及偏移(cl_offset)两个字段。以下是对命令行的简要介绍(见Documentation\x86中的boot.txt文件):
- 内核命令行(THE KERNEL COMMAND LINE)
内核命令行是bootloader与内核通信的一种重要的方式,该命令行由bootloader提供并且它的一些选项和bootloader相关。任何选项不应该从内核命令行中删除,即便它对于内核来说并不具有实际的意义。以下是内核命令行中选项的典型格式:
——vga=<mode>,这里的<mode>可以是一个整数(在C语言中的十进制、八进制或是十六进制格式),或者是字符串"normal"(使用0xFFFF表示)、"ext"(0xFFFE表示)以及"ask"(0xFFFD表示)之一。这个值应该被填入vid_mode域,并由内核所使用。更多有关命令行选项的详细信息参见boot.txt文件中的SPECIAL COMMAND LINE OPTIONS一节。
如果协议版本不是2.02或者更高(注意现在所使用的协议版本为2.10),那么内核命令行将会遵守如下约定:
1、在偏移0x0020处,字段"cmd_line_magic"被填入魔数0xA33F
2、在偏移0x0022处,字段"cmd_line_offset"被填入内核命令行的起始地址,该地址相对实模式内核的起始地址而言。
根据以上描述,由于指针常量oldcmd被设置为OLD_CL_ADDRESS,因此在老版本的内核中,该指针所指向的结构体old_cmdline包含值为0xA33F的字段cl_magic。接着往下执行:
- BUILD_BUG_ON(sizeof boot_params != 4096);
- memcpy(&boot_params.hdr, &hdr, sizeof hdr);
BUILD_BUG_ON(sizeof boot_params != 4096);
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
其中BUILD_BUG_ON在arch\x86\boot\Boot.h文件中定义如下:
- #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
该宏表示当condition为真时,对其进行两次取反操作,因此将1-2*1=-1作为数组的维数,然而这是不合法的,换言之,若condition为真则将引发编译时错误。使用该宏的目的就是通过编译器来保证启动参数boot_params所占的字节数为4096。随后将Kernel boot sector中的hdr变量拷贝至结构体boot_params的hdr字段中,注意这里的boot_params是一个全局变量,且该变量同样被定义在arch\x86\boot\Main.c文件中。而结构体的类型定义则位于arch\x86\include\asm\Bootparam.h文件中,这里不再将其列出。在拷贝完成后执行如下判断:
- if (!boot_params.hdr.cmd_line_ptr &&
- oldcmd->cl_magic == OLD_CL_MAGIC) {
- /* Old-style command line protocol. */
- /*执行语句省略*/
- 。。。。。。
- }
if (!boot_params.hdr.cmd_line_ptr &&
oldcmd->cl_magic == OLD_CL_MAGIC) {
/* Old-style command line protocol. */
/*执行语句省略*/
。。。。。。
}
其中OLD_CL_MAGIC在arch\x86\include\asm\Setup.h文件中定义如下:
- #define OLD_CL_MAGIC 0xA33F
#define OLD_CL_MAGIC 0xA33F
该判断语句测试已被拷贝至boot_params的hdr变量中的cmd_line_ptr字段是否为0,并且oldcmd中的cl_magic字段的值是否为0xA33F,若这两者都满足则执行括号内的语句,而由前文描述可知,只有在老版本的内核中cl_magic字段的值才为0xA33F,因此后续的语句实际上是针对老式的内核做相应的调整工作,具体细节不再剖析。
控制台初始化
在完成对启动参数的拷贝之后,接着调用如下函数初始化启动阶段的控制台。
- /* Initialize the early-boot console */
- console_init();
/* Initialize the early-boot console */
console_init();
在arch\x86\boot\Early_serial_console.c文件中定义如下:
- void console_init(void)
- {
- parse_earlyprintk();
- if (!early_serial_base)
- parse_console_uart8250();
- }
void console_init(void)
{
parse_earlyprintk();
if (!early_serial_base)
parse_console_uart8250();
}
而以上两个函数又将对其他函数进行层层嵌套调用,可以在源文件中看到console_init函数调用的子过程整整占据了一个文件的大小,对其中用到的主要函数详细剖析将会花费大量的篇幅,以至深入到细节之中后可能会被弄得晕头转向,所以我在这里首先高屋建瓴地给出这些函数之间的调用关系,在对各个子函数分析完毕之后不妨再回顾一下它们之间的依赖关系,这样就会对控制台的初始化方式有更为深刻的理解。以下是函数之间依赖关系的示意图:
图1
首先从simple_strtoull入手。它位于arch\x86\boot\String.c文件中:
- /**
- * simple_strtoull - convert a string to an unsigned long long
- * @cp: The start of the string
- * @endp: A pointer to the end of the parsed string will be placed here
- * @base: The number base to use
- */
- unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base)
/**
* simple_strtoull - convert a string to an unsigned long long
* @cp: The start of the string
* @endp: A pointer to the end of the parsed string will be placed here
* @base: The number base to use
*/
unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base)
从以上注释即可看出,该函数主要用于实现将一个字符串转换为无符号长整型。其中cp为待转换字符串的指针,endp则存放指向被解析的字符串的末尾的指针,base是被转换后的数所使用的基底。
- unsigned long long result = 0; /* 存放转换后的结果 */
- if (!base)
- base = simple_guess_base(cp);
unsigned long long result = 0; /* 存放转换后的结果 */
if (!base)
base = simple_guess_base(cp);
首先判断base的值是否为0,若是则调用simple_guess_base函数,该函数与simple_strtoull位于同一个文件中且定义如下:
- static unsigned int simple_guess_base(const char *cp)
- {
- if (cp[0] == '0') { /*待解析的字符串的第一个字符为0*/
- if (TOLOWER(cp[1]) == 'x' && isxdigit(cp[2]))
- return 16;
- else
- return 8;
- } else { /*不为零*/
- return 10;
- }
- }
- /*其中宏TOLOWER定义如下:*/
- /* Works only for digits and letters, but small and fast */
- #define TOLOWER(x) ((x) | 0x20)
- /*26个英文字符中任意一个字符的大小写之差为32,也即十六进制的0x20*/
- /*使用或运算的好处是无论字符x为大写还是小写,均将其转换为小写字符*/
static unsigned int simple_guess_base(const char *cp)
{
if (cp[0] == '0') { /*待解析的字符串的第一个字符为0*/
if (TOLOWER(cp[1]) == 'x' && isxdigit(cp[2]))
return 16;
else
return 8;
} else { /*不为零*/
return 10;
}
}
/*其中宏TOLOWER定义如下:*/
/* Works only for digits and letters, but small and fast */
#define TOLOWER(x) ((x) | 0x20)
/*26个英文字符中任意一个字符的大小写之差为32,也即十六进制的0x20*/
/*使用或运算的好处是无论字符x为大写还是小写,均将其转换为小写字符*/
函数首先检测待解析的字符串的第一个字符是否为0,为0则该字符串可能是十六进制或八进制,反之则为十进制。若cp[0]为0,那么将第二个字符转换为小写形式判断是否为字符'x',并调用isxdigit()函数对cp[2]进行检测,isxdigit()在文件arch\x86\boot\Ctype.h文件中定义如下:
- static inline int isxdigit(int ch)
- {
- if (isdigit(ch))
- return true;
- if ((ch >= 'a') && (ch <= 'f'))
- return true;
- return (ch >= 'A') && (ch <= 'F');
- }
- /*isdigit函数定义如下:*/
- static inline int isdigit(int ch)
- {
- return (ch >= '0') && (ch <= '9'); /*当条件 '0'≤ch≤'9' 成立时返回真*/
- }
static inline int isxdigit(int ch)
{
if (isdigit(ch))
return true;
if ((ch >= 'a') && (ch <= 'f'))
return true;
return (ch >= 'A') && (ch <= 'F');
}
/*isdigit函数定义如下:*/
static inline int isdigit(int ch)
{
return (ch >= '0') && (ch <= '9'); /*当条件 '0'≤ch≤'9' 成立时返回真*/
}
所以当被测试的参数ch满足条件 '0'≤ch≤'9','a'≤ch≤'f'或是'A'≤ch≤'F'时,isxdigit返回真,否则返回假。根据以上分析可知,simple_guess_base函数根据如下规则判断待解析的字符串的基底:当cp[0]='0'时,进一步判断cp[1]是否等于'x'以及cp[2]是否为'0'~'9'或'a'~'f'(其中'a'~'f'也可以为大写),若这两个条件满足则返回16,表明字符串按16进制进行转换,反之则返回8;若cp[0]≠'0'则返回10。在simple_strtoull函数中确保待转换的基底非零后,继续执行:
- if (base == 16 && cp[0] == '0' && TOLOWER(cp[1]) == 'x')
- cp += 2; /*当待转换的基底为16,且cp[0]为'0',cp[1]为'x'或是'X',那么将当前指针cp指向其后的第二个字符*/
- while (isxdigit(*cp)) { /*转换之前首先判断当前字符是否在'0'~'f'之间*/
- unsigned int value;
- /*判断是否在'0'~'9'之间,条件成立则减去'0'得到对应数值
- 若不成立则做相应修正——减去'a'并加上10*/
- value = isdigit(*cp) ? *cp - '0' : TOLOWER(*cp) - 'a' + 10;
- if (value >= base) /*若得到的数值不小于基底则退出*/
- break;
- result = result * base + value; /*将结果乘上基底并累加当前得到的数值*/
- cp++; /*使字符指针自增*/
- }
- if (endp)
- *endp = (char *)cp; /*将第一个未被转换的字符的指针存入*endp中*/
- return result;
if (base == 16 && cp[0] == '0' && TOLOWER(cp[1]) == 'x')
cp += 2; /*当待转换的基底为16,且cp[0]为'0',cp[1]为'x'或是'X',那么将当前指针cp指向其后的第二个字符*/
while (isxdigit(*cp)) { /*转换之前首先判断当前字符是否在'0'~'f'之间*/
unsigned int value;
/*判断是否在'0'~'9'之间,条件成立则减去'0'得到对应数值
若不成立则做相应修正——减去'a'并加上10*/
value = isdigit(*cp) ? *cp - '0' : TOLOWER(*cp) - 'a' + 10;
if (value >= base) /*若得到的数值不小于基底则退出*/
break;
result = result * base + value; /*将结果乘上基底并累加当前得到的数值*/
cp++; /*使字符指针自增*/
}
if (endp)
*endp = (char *)cp; /*将第一个未被转换的字符的指针存入*endp中*/
return result;
根据上述分析,函数的作用正如其函数名所示,用于将字符串转换为相应的无符号长整型,不过待转换的进制不能超过16。根据图1所示,接着对cmdline_find_option具体分析,其实现位于arch\x86\boot\Boot.h文件中:
- static inline int cmdline_find_option(const char *option, char *buffer, int bufsize)
- {
- return __cmdline_find_option(boot_params.hdr.cmd_line_ptr, option, buffer, bufsize);
- }
static inline int cmdline_find_option(const char *option, char *buffer, int bufsize)
{
return __cmdline_find_option(boot_params.hdr.cmd_line_ptr, option, buffer, bufsize);
}
cmdline_find_option实际是对__cmdline_find_option的一层封装,后者在arch\x86\boot\Cmdline.c文件中定义如下:
- /*
- * Find a non-boolean option, that is, "option=argument". In accordance
- * with standard Linux practice, if this option is repeated, this returns
- * the last instance on the command line.
- *
- * Returns the length of the argument (regardless of if it was
- * truncated to fit in the buffer), or -1 on not found.
- */
- int __cmdline_find_option(u32 cmdline_ptr, const char *option, char *buffer, int bufsize)
/*
* Find a non-boolean option, that is, "option=argument". In accordance
* with standard Linux practice, if this option is repeated, this returns
* the last instance on the command line.
*
* Returns the length of the argument (regardless of if it was
* truncated to fit in the buffer), or -1 on not found.
*/
int __cmdline_find_option(u32 cmdline_ptr, const char *option, char *buffer, int bufsize)
由注释可知,这个函数的作用是查找一个非布尔类型的选项,选项的格式为"option=argument"。与标准Linux实践一致,如果这个选项重复出现,那么将返回命令行的最后一个实例。函数的返回值为argument的长度,而不管这个值在缓冲区中是否被截断,返回-1表示对应的选项未找到。如前所示,在cmdline_find_option中调用该函数时,填充的第一个参数是已被拷贝至boot_params中的hdr变量的cmd_line_ptr字段,而该字段确实存放指向内核命令行的指针,这可以在系统启动(二)的表格中看到其概要解释,并且选项的格式也和前文中所举的实例相同。下面是__cmdline_find_option的具体实现:
- addr_t cptr; /* unsigned int */
- char c;
- int len = -1;
- const char *opptr = NULL;
- char *bufptr = buffer; /*bufptr指向存放argument的缓冲区*/
- enum {
- st_wordstart, /* Start of word/after whitespace */
- st_wordcmp, /* Comparing this word */
- st_wordskip, /* Miscompare, skip */
- st_bufcpy /* Copying this to buffer */
- } state = st_wordstart;
- /*若函数的形参cmdline_ptr为0或超出第一个MB的内存空间,那么返回-1指示不存在命令行或不可访问*/
- if (!cmdline_ptr || cmdline_ptr >= 0x10 0000)
- return -1; /* No command line, or inaccessible */
- /*取cmdline_ptr中最右端的4个位*/
- cptr = cmdline_ptr & 0xf;
- set_fs(cmdline_ptr >> 4);
addr_t cptr; /* unsigned int */
char c;
int len = -1;
const char *opptr = NULL;
char *bufptr = buffer; /*bufptr指向存放argument的缓冲区*/
enum {
st_wordstart, /* Start of word/after whitespace */
st_wordcmp, /* Comparing this word */
st_wordskip, /* Miscompare, skip */
st_bufcpy /* Copying this to buffer */
} state = st_wordstart;
/*若函数的形参cmdline_ptr为0或超出第一个MB的内存空间,那么返回-1指示不存在命令行或不可访问*/
if (!cmdline_ptr || cmdline_ptr >= 0x10 0000)
return -1; /* No command line, or inaccessible */
/*取cmdline_ptr中最右端的4个位*/
cptr = cmdline_ptr & 0xf;
set_fs(cmdline_ptr >> 4);
上述定义的枚举类型在其后的“选项”查找中是一大亮点,而set_fs函数在arch\x86\boot\Boot.h文件中实现如下:
- static inline void set_fs(u16 seg)
- {
- asm volatile("movw %0,%%fs" : : "rm" (seg));
- }
static inline void set_fs(u16 seg)
{
asm volatile("movw %0,%%fs" : : "rm" (seg));
}
set_fs函数所实现的功能是将形参seg移入fs寄存器,注意fs寄存器为16位,所以set_fs(cmdline_ptr>>4)这条语句实际上只是将cmdline_ptr>>4所得结果的低端的16位存入fs寄存器中,之所以要这么做,是因为当前仍然处于实模式,对内存的寻址方式是通过seg*16+offset的方式实现的,其中的offset正是变量cptr中的值。设置好段寄存器及偏移量之后,紧接着便是读取命令行字符串并做适当的处理以返回所需的结果,处理的方式是通过一个while循环来实现的,循环头如下所示:
- while (cptr < 0x10000 && (c = rdfs8(cptr++)))
while (cptr < 0x10000 && (c = rdfs8(cptr++)))
之所以在while循环体内需要判断cptr<0x10000是为了确保整个命令行在一个64KB的段内,其后的rdfs8()函数在arch\x86\boot\Boot.h文件中定义如下:
- static inline u8 rdfs8(addr_t addr)
- {
- u8 v;
- /* 等价于 movb %fs:addr , v */
- asm volatile("movb %%fs:%1,%0" : "=q" (v) : "m" (*(u8 *)addr));
- return v;
- }
static inline u8 rdfs8(addr_t addr)
{
u8 v;
/* 等价于 movb %fs:addr , v */
asm volatile("movb %%fs:%1,%0" : "=q" (v) : "m" (*(u8 *)addr));
return v;
}
可以看到这个函数实际上就是读取fs寄存器所指向的段中偏移量为addr所在的内存字节,而根据前述分析,逻辑地址fs:cptr确实已被正确设置为指向命令行所在的内存。所以循环头每次都读取当前指向的内存字节并使偏移量自增,接着便将所得到的字节交由循环体进行处理,循环体中的语句定义如下:
- switch (state) {
- case st_wordstart: /* Start of word/after whitespace */
- if (myisspace(c)) /*判断当前字符是否 <= ' '*/
- break; /*若是则退出switch分支结构读取下一个内存字节*/
- /* else */
- state = st_wordcmp;
- opptr = option; /*option为函数形参,指向待查找的选项*/
- /* fall through */
- case st_wordcmp: /* Comparing this word */
- if (c == '=' && !*opptr) { /*判断当前字符是否为'='以及opptr所指向的内存单元是否非空*/
- len = 0; /*若是则准备将选项所对应的参数拷贝至对应的缓冲区*/
- bufptr = buffer;
- state = st_bufcpy;
- } else if (myisspace(c)) { /*判定当前字符是否<=' '*/
- state = st_wordstart; /*若是则设置相应的状态值,在st_wordstart中跳过一系列不相关字符*/
- } else if (c != *opptr++) { /*将当前字符与opptr所指向的选项比较*/
- state = st_wordskip; /*若命令行中的当前选项与所要查找的选项不符,则设置state为st_wordskip*/
- } /*若当前字符均不满足之前的条件,说明当前字符正确匹配,继续该过程*/
- break;
- static inline int myisspace(u8 c)
- {
- return c <= ' '; /* Close enough approximation */
- }
switch (state) {
case st_wordstart: /* Start of word/after whitespace */
if (myisspace(c)) /*判断当前字符是否 <= ' '*/
break; /*若是则退出switch分支结构读取下一个内存字节*/
/* else */
state = st_wordcmp;
opptr = option; /*option为函数形参,指向待查找的选项*/
/* fall through */
case st_wordcmp: /* Comparing this word */
if (c == '=' && !*opptr) { /*判断当前字符是否为'='以及opptr所指向的内存单元是否非空*/
len = 0; /*若是则准备将选项所对应的参数拷贝至对应的缓冲区*/
bufptr = buffer;
state = st_bufcpy;
} else if (myisspace(c)) { /*判定当前字符是否<=' '*/
state = st_wordstart; /*若是则设置相应的状态值,在st_wordstart中跳过一系列不相关字符*/
} else if (c != *opptr++) { /*将当前字符与opptr所指向的选项比较*/
state = st_wordskip; /*若命令行中的当前选项与所要查找的选项不符,则设置state为st_wordskip*/
} /*若当前字符均不满足之前的条件,说明当前字符正确匹配,继续该过程*/
break;
static inline int myisspace(u8 c)
{
return c <= ' '; /* Close enough approximation */
}
整个循环体使用一个switch分支语句来处理读取到的字节,总共分为4种情况,每种情况都由之前所定义的枚举值加以区分,上述代码段示出了前两种情况。
- 在st_wordstart情形中,根据当前字符是否≤' '从而跳过一些列不相关字符——主要是空格符,其后将状态值state赋值为st_wordcmp并将opptr赋值为option,这里的option是该函数的形参,由调用者决定需要在命令行中查找的选项。由于此时当前字符可能与形参的第一个字符匹配,因此直接陷入st_wordcmp情况中。
- 再次注意选项的格式为"option=argument",所以进入st_wordcmp情形后首先判断当前字符是否为'=',以及opptr指向的内存单元是否存放'\0'——表明已经匹配完成,如果这两者都满足那么可以进行其后的值拷贝工作——将状态state设置为st_bufcpy。可以看到在第三个条件分支中有如下判断语句"c!=*opptr++",若此条件成立,那么说明命令行中的当前选项并非所要查找的,因此将state设置为st_wordskip,并且由于此时opptr已经执行自增操作,所以当下次陷入此种情况时仍需指向option所在的首地址,这也就是为什么要在st_wordstart情况的最后执行"opptr=option"赋值语句的原因所在。
我们接着看下面的两种情形:
- case st_wordskip: /* Miscompare, skip */
- if (myisspace(c))
- state = st_wordstart;
- break;
- case st_bufcpy: /* Copying this to buffer */
- if (myisspace(c)) {
- state = st_wordstart;
- } else {
- if (len < bufsize-1)
- *bufptr++ = c; /*将当前字符存入bufptr所指向的缓冲区,并将指针值自增*/
- len++; /*正确记录缓冲区中字符串的长度*/
- }
- break;
- }
case st_wordskip: /* Miscompare, skip */
if (myisspace(c))
state = st_wordstart;
break;
case st_bufcpy: /* Copying this to buffer */
if (myisspace(c)) {
state = st_wordstart;
} else {
if (len < bufsize-1)
*bufptr++ = c; /*将当前字符存入bufptr所指向的缓冲区,并将指针值自增*/
len++; /*正确记录缓冲区中字符串的长度*/
}
break;
}
- 如枚举值的名称st_wordskip所示,这种情形主要用于跳过命令行中的其他选项。在该情形中仍需首先判断当前字符是否≤' ',以确保在跳过一个选项之后重新遇到不相关字符时将state设置为st_wordstart,因为选项的匹配是通过st_wordstart陷入第二种情形st_wordcmp所完成的。
- 最后一种情形st_bufcpy主要用于将option所对应的值拷贝至用于存放结果的缓冲区中,注意拷贝之前首先判断字符的长度len<bufsize-1是否成立,若成立则不再存储当前字符,这意味着字符串的长度如果超出缓冲区的大小,那么将会发生截断,然而这种情形下仍能确保len正确记录字符串的长度。
然而在完成拷贝之后仍将执行循环,除非循环头中的判断条件cptr < 0x10000 && (c = rdfs8(cptr++))不成立,这意味着所寻址的内存已超出64KB大小的段,或是当前读到的字符为'\0'——标识命令行的结束。这种处理方式正如该函数的注释所示,如果一个选项在命令行中重复出现,那么将返回最后一个选项所对应的值。退出循环之后,这个函数如下做一些适当的善后工作:
- if (bufsize)
- *bufptr = '\0';
- return len;
if (bufsize)
*bufptr = '\0';
return len;
在缓冲区中作为结果的字符串后存入'\0'字符,并返回整个字符串的大小。
以上是对simple_strtoull以及cmdline_find_option函数的简要剖析。接下来在对early_serial_init,parse_earlyprintk,probe_baud,parse_console_uart8250这几个函数剖析之前,首先需要明白串行端口通信的基本概念。