Linux中早期控制台初始化和注册的实现

设置早期打印功能setup_early_printk

c 复制代码
int __init setup_early_printk(char *opt)
{
        char *space;
        char buf[256];

        if (early_console_initialized)
                return -1;

        opt = strchr(opt, '=') + 1;

        strlcpy(buf,opt,sizeof(buf));
        space = strchr(buf, ' ');
        if (space)
                *space = 0;

        if (strstr(buf,"keep"))
                keep_early = 1;

        if (!strncmp(buf, "serial", 6)) {
                early_serial_init(buf + 6);
                early_console = &early_serial_console;
        } else if (!strncmp(buf, "ttyS", 4)) {
                early_serial_init(buf);
                early_console = &early_serial_console;
        } else if (!strncmp(buf, "vga", 3)) {
                early_console = &early_vga_console;
        } else {
                early_console = NULL;
                return -1;
        }
        early_console_initialized = 1;
        register_console(early_console);
        return 0;
}

1. 函数功能概述

这是一个用于设置早期打印功能的函数,在内核启动初期就提供控制台输出能力,便于调试启动过程中的问题

2. 代码分段详解

2.1. 第一段:变量声明和初始检查

c 复制代码
int __init setup_early_printk(char *opt)
{
        char *space;
        char buf[256];

        if (early_console_initialized)
                return -1;
  • __init 宏表示这个函数只在初始化阶段使用,之后内存会被释放
  • char *spacechar buf[256] 声明局部变量,buf 用于存储处理后的选项字符串
  • if (early_console_initialized) 检查全局标志,如果早期控制台已经初始化过,直接返回 -1 避免重复初始化

2.2. 第二段:参数解析

c 复制代码
        opt = strchr(opt, '=') + 1;

        strlcpy(buf,opt,sizeof(buf));
        space = strchr(buf, ' ');
        if (space)
                *space = 0;
  • strchr(opt, '=') + 1 查找参数中的等号,并指向等号后的内容(跳过参数名)
  • strlcpy(buf,opt,sizeof(buf)) 安全地复制选项字符串到缓冲区
  • strchr(buf, ' ') 查找空格位置(如果有其他参数)
  • if (space) *space = 0 如果有空格,将其替换为字符串结束符,截断后续参数

2.3. 第三段:keep 选项处理

c 复制代码
        if (strstr(buf,"keep"))
                keep_early = 1;
  • strstr(buf,"keep") 检查是否包含 "keep" 子串
  • 如果找到 "keep",设置 keep_early = 1,表示保留早期控制台不被后续的正常控制台替换

2.4. 第四段:串口控制台设置

c 复制代码
        if (!strncmp(buf, "serial", 6)) {
                early_serial_init(buf + 6);
                early_console = &early_serial_console;
        } else if (!strncmp(buf, "ttyS", 4)) {
                early_serial_init(buf);
                early_console = &early_serial_console;
  • !strncmp(buf, "serial", 6) 检查是否以 "serial" 开头
  • 如果是,调用 early_serial_init(buf + 6) 初始化串口,跳过 "serial" 前缀
  • !strncmp(buf, "ttyS", 4) 检查是否以 "ttyS" 开头(传统串口设备名)
  • 两种情况下都设置 early_console 指向串口控制台结构体

2.5. 第五段:VGA 控制台和其他情况

c 复制代码
        } else if (!strncmp(buf, "vga", 3)) {
                early_console = &early_vga_console;
        } else {
                early_console = NULL;
                return -1;
        }
  • !strncmp(buf, "vga", 3) 检查是否以 "vga" 开头
  • 如果是,设置 early_console 指向 VGA 控制台结构体
  • 如果都不匹配,设置 early_console 为 NULL 并返回 -1 表示失败

2.6. 第六段:完成初始化

c 复制代码
        early_console_initialized = 1;
        register_console(early_console);
        return 0;
}
  • early_console_initialized = 1 设置全局标志表示早期控制台已初始化
  • register_console(early_console) 向内核注册这个控制台
  • return 0 返回成功状态

3. 函数功能总结

主要功能:根据启动参数配置和初始化早期控制台,在内核启动初期提供调试输出能力

支持的控制台类型

  • 串口控制台("serial" 或 "ttyS" 开头)
  • VGA 控制台("vga" 开头)

参数格式示例

  • earlyprintk=serial,0x3f8,115200
  • earlyprintk=ttyS0,115200
  • earlyprintk=vga
  • earlyprintk=serial,keep (保留早期控制台)

返回值

  • 0:成功
  • -1:失败(已初始化或无效参数)

这个函数在内核启动早期阶段至关重要,为后续的启动过程提供了可靠的输出和调试通道

初始化早期串口控制台early_serial_init

c 复制代码
static __init void early_serial_init(char *s)
{
        unsigned char c;
        unsigned divisor;
        unsigned baud = DEFAULT_BAUD;
        char *e;

        if (*s == ',')
                ++s;

        if (*s) {
                unsigned port;
                if (!strncmp(s,"0x",2)) {
                        early_serial_base = simple_strtoul(s, &e, 16);
                } else {
                        static int bases[] = { 0x3f8, 0x2f8 };

                        if (!strncmp(s,"ttyS",4))
                                s += 4;
                        port = simple_strtoul(s, &e, 10);
                        if (port > 1 || s == e)
                                port = 0;
                        early_serial_base = bases[port];
                }
                s += strcspn(s, ",");
                if (*s == ',')
                        s++;
        }

        outb(0x3, early_serial_base + LCR);     /* 8n1 */
        outb(0, early_serial_base + IER);       /* no interrupt */
        outb(0, early_serial_base + FCR);       /* no fifo */
        outb(0x3, early_serial_base + MCR);     /* DTR + RTS */

        if (*s) {
                baud = simple_strtoul(s, &e, 0);
                if (baud == 0 || s == e)
                        baud = DEFAULT_BAUD;
        }

        divisor = 115200 / baud;
        c = inb(early_serial_base + LCR);
        outb(c | DLAB, early_serial_base + LCR);
        outb(divisor & 0xff, early_serial_base + DLL);
        outb((divisor >> 8) & 0xff, early_serial_base + DLH);
        outb(c & ~DLAB, early_serial_base + LCR);
}

1. 函数功能概述

这是一个用于初始化早期串口控制台的函数,在内核启动初期设置串口参数

1.1. 第一段:变量声明和初始设置

c 复制代码
static __init void early_serial_init(char *s)
{
        unsigned char c;
        unsigned divisor;
        unsigned baud = DEFAULT_BAUD;
        char *e;
  • static __init:静态函数,初始化后内存可释放
  • c:用于临时存储LCR(线路控制寄存器)值
  • divisor:波特率分频器
  • baud:波特率,默认 DEFAULT_BAUD(通常是9600)
  • e:字符串转换结束指针

1.2. 第二段:参数解析 - 跳过前导逗号

c 复制代码
        if (*s == ',')
                ++s;
  • 如果参数字符串以逗号开头,跳过这个逗号
  • 处理格式如:,0x3f8,115200

1.3. 第三段:解析串口基地址 - 十六进制格式

c 复制代码
        if (*s) {
                unsigned port;
                if (!strncmp(s,"0x",2)) {
                        early_serial_base = simple_strtoul(s, &e, 16);
                }
  • 检查字符串非空
  • 如果以"0x"开头,按十六进制解析串口基地址
  • simple_strtoul:简单字符串转无符号长整型
  • 例如:0x3f8 → 串口1的标准地址

1.4. 第四段:解析串口基地址 - 十进制或设备名格式

c 复制代码
                else {
                        static int bases[] = { 0x3f8, 0x2f8 };

                        if (!strncmp(s,"ttyS",4))
                                s += 4;
                        port = simple_strtoul(s, &e, 10);
                        if (port > 1 || s == e)
                                port = 0;
                        early_serial_base = bases[port];
                }
  • bases[]:预定义的串口基地址数组
    • 0x3f8:COM1
    • 0x2f8:COM2
  • 如果以"ttyS"开头,跳过前4个字符
  • 按十进制解析端口号(0或1)
  • 如果端口号无效(>1或解析失败),默认使用端口0
  • 根据端口号选择对应的基地址

1.5. 第五段:移动到波特率参数

c 复制代码
                s += strcspn(s, ",");
                if (*s == ',')
                        s++;
        }
  • strcspn(s, ","):查找下一个逗号的位置
  • 移动指针到逗号位置,然后跳过逗号
  • 准备解析波特率参数

1.6. 第六段:串口基本配置 - 8N1格式

c 复制代码
        outb(0x3, early_serial_base + LCR);     /* 8n1 */
  • LCR:线路控制寄存器
  • 0x3 = 0b00000011:8位数据,无校验,1位停止位(8N1)
  • 设置串口数据帧格式

1.7. 第七段:禁用中断

c 复制代码
        outb(0, early_serial_base + IER);       /* no interrupt */
  • IER:中断使能寄存器
  • 0:禁用所有中断(早期启动不需要中断)

1.8. 第八段:禁用FIFO

c 复制代码
        outb(0, early_serial_base + FCR);       /* no fifo */
  • FCR:FIFO控制寄存器
  • 0:禁用FIFO缓冲区

1.9. 第九段:设置调制解调器控制

c 复制代码
        outb(0x3, early_serial_base + MCR);     /* DTR + RTS */
  • MCR:调制解调器控制寄存器
  • 0x3 = 0b00000011:设置DTR和RTS信号线

1.10. 第十段:解析波特率参数

c 复制代码
        if (*s) {
                baud = simple_strtoul(s, &e, 0);
                if (baud == 0 || s == e)
                        baud = DEFAULT_BAUD;
        }
  • 如果还有参数字符串,解析波特率
  • simple_strtoul(s, &e, 0):自动检测进制(十进制/十六进制)
  • 如果解析失败或为0,使用默认波特率

1.11. 第十一段:计算波特率分频器

c 复制代码
        divisor = 115200 / baud;
  • 计算分频值:基准频率115200Hz / 目标波特率

1.12. 第十二段:进入分频器设置模式

c 复制代码
        c = inb(early_serial_base + LCR);
        outb(c | DLAB, early_serial_base + LCR);
  • 读取当前LCR值
  • c | DLAB:设置DLAB位
  • 进入分频器编程模式

1.13. 第十三段:设置分频器低字节和高字节

c 复制代码
        outb(divisor & 0xff, early_serial_base + DLL);
        outb((divisor >> 8) & 0xff, early_serial_base + DLH);
  • DLL:分频器锁存低字节
  • DLH:分频器锁存高字节
  • 分别设置分频器的低8位和高8位

1.14. 第十四段:退出分频器设置模式

c 复制代码
        outb(c & ~DLAB, early_serial_base + LCR);
}
  • c & ~DLAB:清除DLAB位
  • 恢复正常操作模式,分频器开始生效

2. 串口寄存器说明

寄存器 偏移 功能
DLL 0x0 分频器锁存低字节(DLAB=1时)
DLH 0x1 分频器锁存高字节(DLAB=1时)
IER 0x1 中断使能寄存器(DLAB=0时)
FCR 0x2 FIFO控制寄存器
LCR 0x3 线路控制寄存器
MCR 0x4 调制解调器控制寄存器

3. 支持的参数格式示例

  1. 十六进制地址0x3f8,115200
  2. 设备名ttyS0,9600ttyS1,57600
  3. 数字端口0,1152001,19200
  4. 默认配置 :空字符串或,使用COM1和9600波特率

向内核注册一个控制台设备register_console

c 复制代码
void register_console(struct console * console)
{
        int     i;
        unsigned long flags;

        if (preferred_console < 0)
                preferred_console = selected_console;

        /*
         *      See if we want to use this console driver. If we
         *      didn't select a console we take the first one
         *      that registers here.
         */
        if (preferred_console < 0) {
                if (console->index < 0)
                        console->index = 0;
                if (console->setup == NULL ||
                    console->setup(console, NULL) == 0) {
                        console->flags |= CON_ENABLED | CON_CONSDEV;
                        preferred_console = 0;
                }
        }

        /*
         *      See if this console matches one we selected on
         *      the command line.
         */
        for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) {
                if (strcmp(console_cmdline[i].name, console->name) != 0)
                        continue;
                if (console->index >= 0 &&
                    console->index != console_cmdline[i].index)
                        continue;
                if (console->index < 0)
                        console->index = console_cmdline[i].index;
                if (console->setup &&
                    console->setup(console, console_cmdline[i].options) != 0)
                        break;
                console->flags |= CON_ENABLED;
                console->index = console_cmdline[i].index;
                if (i == preferred_console)
                        console->flags |= CON_CONSDEV;
                break;
        }

        if (!(console->flags & CON_ENABLED))
                return;

        /*
         *      Put this console in the list - keep the
         *      preferred driver at the head of the list.
         */
        acquire_console_sem();
        if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
                console->next = console_drivers;
                console_drivers = console;
        } else {
                console->next = console_drivers->next;
                console_drivers->next = console;
        }
        if (console->flags & CON_PRINTBUFFER) {
                /*
                 * release_console_sem() will print out the buffered messages
                 * for us.
                 */
                spin_lock_irqsave(&logbuf_lock, flags);
                con_start = log_start;
                spin_unlock_irqrestore(&logbuf_lock, flags);
        }
        release_console_sem();
}

1. 函数功能概述

这个函数用于向内核注册一个控制台设备,是内核控制台子系统的核心注册函数

1.1. 第一段:变量声明和初始检查

c 复制代码
void register_console(struct console * console)
{
        int     i;
        unsigned long flags;

        if (preferred_console < 0)
                preferred_console = selected_console;
  • i:循环计数器
  • flags:用于保存中断状态的变量
  • preferred_console:首选控制台索引
  • selected_console:已选控制台索引
  • 如果首选控制台未设置,使用已选控制台

1.2. 第二段:自动选择第一个可用控制台

c 复制代码
        if (preferred_console < 0) {
                if (console->index < 0)
                        console->index = 0;
                if (console->setup == NULL ||
                    console->setup(console, NULL) == 0) {
                        console->flags |= CON_ENABLED | CON_CONSDEV;
                        preferred_console = 0;
                }
        }
  • 如果仍然没有首选控制台:
    • 如果控制台索引为负,设为0
    • 如果控制台不需要setup或setup成功:
      • 设置 CON_ENABLED(启用)和 CON_CONSDEV(控制台设备)标志
      • 设置首选控制台为0

1.3. 第三段:检查命令行匹配的控制台

c 复制代码
        for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) {
                if (strcmp(console_cmdline[i].name, console->name) != 0)
                        continue;
  • 遍历命令行中指定的控制台配置
  • 比较控制台名称,不匹配则继续循环

1.4. 第四段:索引检查和设置

c 复制代码
                if (console->index >= 0 &&
                    console->index != console_cmdline[i].index)
                        continue;
                if (console->index < 0)
                        console->index = console_cmdline[i].index;
  • 如果控制台索引已设置且不匹配,跳过
  • 如果控制台索引未设置,使用命令行中的索引

1.5. 第五段:控制台初始化和启用

c 复制代码
                if (console->setup &&
                    console->setup(console, console_cmdline[i].options) != 0)
                        break;
                console->flags |= CON_ENABLED;
                console->index = console_cmdline[i].index;
  • 如果控制台有setup函数,调用它进行初始化
  • 如果setup失败,跳出循环
  • 设置 CON_ENABLED 标志
  • 设置控制台索引

1.6. 第六段:设置首选控制台标志

c 复制代码
                if (i == preferred_console)
                        console->flags |= CON_CONSDEV;
                break;
        }
  • 如果当前索引是首选控制台,设置 CON_CONSDEV 标志
  • 跳出循环(找到一个匹配的就够了)

1.7. 第七段:检查控制台是否启用

c 复制代码
        if (!(console->flags & CON_ENABLED))
                return;
  • 如果控制台没有被启用,直接返回
  • 这是函数的早期退出点

1.8. 第八段:获取控制台信号量

c 复制代码
        acquire_console_sem();
  • 获取控制台信号量,保护控制台链表操作
  • 防止并发修改控制台驱动程序链表

1.9. 第九段:插入控制台到链表头部

c 复制代码
        if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
                console->next = console_drivers;
                console_drivers = console;
        }
  • 如果是控制台设备或链表为空:
    • 将新控制台插入链表头部
    • 更新全局 console_drivers 指针

1.10. 第十段:插入控制台到链表第二个位置

c 复制代码
        else {
                console->next = console_drivers->next;
                console_drivers->next = console;
        }
  • 如果不是首选控制台且链表不为空:
    • 插入到链表第二个位置
    • 保持首选控制台在头部

1.11. 第十一段:处理打印缓冲区

c 复制代码
        if (console->flags & CON_PRINTBUFFER) {
                spin_lock_irqsave(&logbuf_lock, flags);
                con_start = log_start;
                spin_unlock_irqrestore(&logbuf_lock, flags);
        }
  • 如果设置了 CON_PRINTBUFFER 标志:
    • 保存中断状态并获取日志缓冲区锁
    • 设置控制台起始位置为日志起始位置
    • 恢复中断状态并释放锁
    • 这允许新控制台输出之前的缓冲消息

1.12. 第十二段:释放信号量并返回

c 复制代码
        release_console_sem();
}
  • 释放控制台信号量
  • 函数结束

2. 关键标志位说明

标志 含义
CON_ENABLED 控制台已启用,可以接收输出
CON_CONSDEV 这是首选的控制台设备
CON_PRINTBUFFER 应该输出之前的缓冲消息

3. 函数功能总结

主要功能:向内核注册一个控制台驱动程序,并按照优先级将其插入到控制台链表中。

处理逻辑

  1. 自动选择:如果没有指定首选控制台,选择第一个可用的
  2. 命令行匹配:根据内核命令行参数匹配和配置控制台
  3. 链表管理:维护一个有序的控制台驱动程序链表
  4. 缓冲区重放:可选地输出启动早期的缓冲消息

关键特性

  • 线程安全的链表操作
  • 灵活的控制台选择机制
  • 支持多个控制台同时运行
  • 保持首选控制台在输出链的头部

这个函数是Linux内核控制台子系统的核心,负责管理所有控制台驱动程序的注册和优先级排序

找到目标字符串的位置strcspn

c 复制代码
size_t strcspn(const char *s, const char *reject)
{
        const char *p;
        const char *r;
        size_t count = 0;

        for (p = s; *p != '\0'; ++p) {
                for (r = reject; *r != '\0'; ++r) {
                        if (*p == *r)
                                return count;
                }
                ++count;
        }

        return count;
}

1. 函数功能概述

strcspn 函数计算字符串 s 开头连续不包含 reject 中任何字符的字符个数,如果reject字符串只有一个字符,就是找到源字符串中reject字符的第一个位置

1.1. 第一段:函数声明和变量定义

c 复制代码
size_t strcspn(const char *s, const char *reject)
{
        const char *p;
        const char *r;
        size_t count = 0;
  • size_t:无符号整数类型,用于表示大小和计数
  • const char *s:要扫描的源字符串
  • const char *reject:包含要拒绝的字符集合
  • p:用于遍历源字符串 s 的指针
  • r:用于遍历拒绝字符集 reject 的指针
  • count:计数器,记录连续不包含拒绝字符的字符数

1.2. 第二段:外层循环 - 遍历源字符串

c 复制代码
        for (p = s; *p != '\0'; ++p) {
  • p = s:初始化指针 p 指向源字符串开头
  • *p != '\0':循环条件,直到遇到字符串结束符
  • ++p:每次循环后指针向后移动一个字符
  • 这个循环逐个检查源字符串中的每个字符

1.3. 第三段:内层循环 - 检查拒绝字符集

c 复制代码
                for (r = reject; *r != '\0'; ++r) {
  • r = reject:初始化指针 r 指向拒绝字符集开头
  • *r != '\0':循环条件,直到拒绝字符集结束
  • ++r:每次循环后指针向后移动一个字符
  • 这个循环检查当前源字符是否在拒绝字符集中

1.4. 第四段:字符匹配检查

c 复制代码
                        if (*p == *r)
                                return count;
  • *p == *r:比较当前源字符 *p 和当前拒绝字符 *r
  • 如果相等,表示当前源字符在拒绝字符集中
  • return count:立即返回当前计数(不包含这个匹配的字符)

1.5. 第五段:计数递增

c 复制代码
                ++count;
        }
  • 只有在内层循环完整执行(当前字符不在拒绝字符集中)时才会执行
  • ++count:增加计数器

1.6. 第六段:处理整个字符串都有效的情况

c 复制代码
        return count;
}
  • 如果整个源字符串都不包含任何拒绝字符,返回总计数
  • 此时 count 等于源字符串的长度

2. 函数功能总结

主要功能:计算字符串开头连续不包含指定字符集中任何字符的字符个数。

返回值含义

  • 返回从字符串开头到第一个拒绝字符之前的字符数量
  • 如果字符串开头没有拒绝字符,返回字符串长度

典型应用场景

  1. 字符串分割:找到第一个分隔符的位置
  2. 词法分析:识别标识符或数字的起始部分
相关推荐
可爱又迷人的反派角色“yang”1 小时前
ansible剧本编写(三)
linux·网络·云计算·ansible
石像鬼₧魂石6 小时前
内网渗透靶场实操清单(基于 Vulhub+Metasploitable 2)
linux·windows·学习·ubuntu
橘子真甜~7 小时前
C/C++ Linux网络编程15 - 网络层IP协议
linux·网络·c++·网络协议·tcp/ip·计算机网络·网络层
拾贰_C8 小时前
【Linux | Windows | Terminal Command】 Linux---grep | Windows--- findstr
linux·运维·服务器
阿华hhh9 小时前
Linux系统编程(标准io)
linux·开发语言·c++
石像鬼₧魂石9 小时前
Kali Linux 网络端口深度扫描
linux·运维·网络
alengan9 小时前
linux上面写python3日志服务器
linux·运维·服务器
Rose sait10 小时前
【环境配置】Linux配置虚拟环境pytorch
linux·人工智能·python
叶之香11 小时前
CentOS/RHEL 7、8安装exfat和ntfs文件系统
linux·运维·centos