深入解析Linux内核Console框架及虚拟UART控制台的实现

发布于:2024-12-06 ⋅ 阅读:(43) ⋅ 点赞:(0)

往期内容

本专栏往期内容:Uart子系统

  1. UART串口硬件介绍
  2. 深入理解TTY体系:设备节点与驱动程序框架详解
  3. Linux串口应用编程:从UART到GPS模块及字符设备驱动
  4. 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
  5. IMX 平台UART驱动情景分析:注册篇
  6. IMX 平台UART驱动情景分析:open篇
  7. IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析
  8. IMX 平台UART驱动情景分析:write篇–从 TTY 层到硬件驱动的写操作流程解析
  9. 深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现
    10.Linux 内核日志系统—printk的机制与应用

interrupt子系统专栏:

  1. 专栏地址:interrupt子系统
  2. Linux 链式与层级中断控制器讲解:原理与驱动开发
    – 末片,有专栏内容观看顺序

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统

  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

    – 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
    – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有专栏内容观看顺序

img

1.内核代码

Linux 4.9.88

本文深入探讨Linux内核的Console框架,解析其设计思路及作用,特别是在TTY框架基础上构建Console接口的意义。从内核日志输出与人机交互需求出发,剖析Console结构体的关键字段和功能。同时,结合具体代码实例,展示如何编写虚拟UART控制台驱动,包括控制台的初始化、日志写入、与TTY设备的绑定等过程。通过对驱动注册、命令行参数解析及设备关联的详细介绍。

2.console介绍

Linux kernel的console框架,主要提供“控制台终端”的功能,用于:

1)kernel日志信息(printk)的输出。

2)实现基础的、基于控制台的人机交互。

Linux TTY framework(5)_System console driver

既然已经有了TTY框架,为什么要多出来一个console框架,为什么不能直接使用TTY driver的接口实现console功能?

TTY框架的核心功能,就是管理TTY设备,并提供访问TTY设备的API(如数据收发)。而console的两个功能需求,“日志输出”就是向TTY设备发送数据,“控制台人机交互”就是标准的TTY功能。因此从功能上看,完全可以直接使用TTY框架的API啊。

不过,既然存在,一定有其意义。内核之所以要抽象出console框架,思路如下:

1)Linux kernel有一个很强烈的隐性规则----内核空间的代码不应该直接利用用户空间接口访问某些资源,例如kernel代码不应该直接使用文件系统接口访问文件(虽然它可以)。回到本文的场景里面,TTY框架通过字符设备(也即文件系统)向用户空间提供接口,那么kernel的代码(如printk),就不能直接使用TTY的接口访问TTY设备,怎么办呢?开一个口子,从kernel里面再拉出一套接口,这就是console框架。

2)console框架构建在TTY框架之上,大部分的实现(特别是访问硬件的部分)都和TTY框架复用。

3)系统中可以有多个TTY设备,只有那些附加了console驱动的设备,才有机会成为kernel日志输出的目的地,有机会成为控制台终端。因此,console框架变相的成为管理TTY设备的一个框架。

4)驱动工程师在为某个TTY设备编写TTY driver的时候,会根据实际的需求,评估该TTY设备是否可能成为控制台设备,如果可能,则同时为其编写system console driver,使其成为候选的控制台设备。系统工程师在系统启动的时候,可以通过kernel命令行参数,决定printk会在哪些候选设备上输出,那个候选设备最终会成为控制台设备。示意图如下:

img

3.console结构体分析

include\linux\console.h:

struct console {
    char    name[16];                    // 1. 控制台设备的名称
    void    (*write)(struct console *, const char *, unsigned);   // 2. 输出日志的函数指针
    int     (*read)(struct console *, char *, unsigned);          // 3. 读取输入的函数指针
    struct tty_driver *(*device)(struct console *, int *);        // 4. 返回关联的tty设备的函数指针
    void    (*unblank)(void);             // 5. 唤醒控制台设备的函数指针
    int     (*setup)(struct console *, char *);                   // 6. 设置控制台参数的函数指针
    int     (*match)(struct console *, char *name, int idx, char *options); // 7. 匹配控制台设备的函数指针
    short   flags;                       // 8. 控制台设备的标志位
    short   index;                       // 9. 控制台的索引号
    int     cflag;                       // 10. 配置标志
    void    *data;                       // 11. 控制台的自定义数据
    struct   console *next;              // 12. 指向下一个控制台的指针(链表)
};

struct console 是 Linux 内核中用于表示控制台设备(如屏幕、串口、虚拟终端等)的结构体。它定义了控制台设备的名称、读写操作、设备设置等功能,每个控制台需要根据自己的特点填写相应的字段来实现特定的行为。

  1. char name[16];

    • 功能:控制台设备的名称,最多为 16 个字符。console的name和index:/dev/name+index
    • 作用:标识控制台的类型,例如虚拟终端、串口等。
    • 可能的赋值
.name = "ttyS",    // 表示串口设备
.name = "tty0",    // 表示虚拟终端设备
  1. void (*write)(struct console *, const char *, unsigned);

    • 功能:写日志的函数指针。当内核想要输出数据到控制台时,会调用这个函数。
    • 作用:实际执行数据输出到设备。
    • 可能的赋值
.write = uart_console_write,    // 串口设备的写入函数
  1. int (*read)(struct console *, char *, unsigned);

    • 功能:读取控制台输入的函数指针,用于读取用户输入。
    • 作用:如果控制台设备支持从用户那里接收输入,则实现该函数;否则通常为 NULL
    • 可能的赋值
.read = NULL,   // 不支持输入
  1. struct tty_driver *(*device)(struct console *, int *);

    • 功能:返回与控制台设备关联的 tty_driver 结构体,用于处理与终端设备的通信。
    • 作用:关联控制台与底层的 TTY 驱动。获取该console对应的TTY driver,用于将console和对应的TTY设备绑定,这样控制台终端就可以和console共用同一个TTY设备了。
    • 可能的赋值
.device = uart_console_device,   // 串口设备的 TTY 驱动获取函数
    // APP访问/dev/console时通过这个函数来确定是哪个(index)设备
    // 举例:
    // a. cmdline中"console=ttymxc1"
    // b. 则注册对应的console驱动时:console->index = 1
    // c. APP访问/dev/console时调用"console->device"来返回这个index
  1. void (*unblank)(void);

    • 功能:唤醒控制台设备的函数。用于在设备空闲时唤醒控制台。
    • 作用:用于显示设备的电源管理(如唤醒屏幕)。
    • 可能的赋值
.unblank = fb_console_unblank,   // Framebuffer 控制台的唤醒函数
  1. int (*setup)(struct console *, char *);

    • 功能:设置控制台设备的初始化参数。用于初始化console的回调函数,console driver可以在该回调函数中对硬件做出现动作。可以不实现,如果实现,则必须返回0,否则该console不可用。
    • 作用:初始化控制台时,配置必要的参数(如串口波特率、屏幕分辨率等)。
    • 可能的赋值
.setup = uart_console_setup,    // 串口控制台的初始化函数
  1. int (*match)(struct console *, char *name, int idx, char *options);

    • 功能:用于匹配控制台设备。
    • 作用:用于注册多个控制台时,选择正确的设备。
    • 可能的赋值
.match = uart_console_match,    // 匹配串口控制台
  1. short flags;

    • 功能:控制台的标志位,用于控制设备的行为。

    • 作用:控制台设备的属性,例如是否启用等。

    • 常见标志

      • CON_ENABLED: 表示设备已启用。
      • CON_ANYTIME: 表示设备随时可用,即使 CPU 不是在线状态。
      • CON_BOOT:该console是一个临时console,只在启动的时候使用,kernel会在真正的console注册后,把它注销掉。
      • CON_CONSDEV:表示该console会被用作控制台终端(和/dev/console对应),对应命令行中的最后一个,例如“console=ttyXS0 console=ttyUSB2”中的ttyUSB2。
      • CON_PRINTBUFFER:如果设置了该flag,kernel在该console被注册的时候,会将那些被缓存到buffer中的之前的日志,统统输出到该console上。通常注册的console,如串口console,都会设置该flag,以便可以看到console注册前的日志输出。(常用)
    • 可能的赋值

.flags = CON_ENABLED | CON_ANYTIME,   // 控制台已启用且随时可用
  1. short index;

    • 功能:控制台的索引号,表示设备的序号或设备号。
    • 作用:用于区分多个相同类型的设备(如 tty0, tty1 等)。
    • 可能的赋值
.index = -1,   // 动态分配索引
  1. int cflag;

    • 功能:控制台的配置标志,通常与串口或终端设备的特定设置相关。
    • 作用:配置控制台的行为,如波特率、校验位等。
    • 可能的赋值
.cflag = B9600 | CS8 | CREAD,   // 例如串口配置9600波特率,8位字符
  1. void *data;

    • 功能:通用指针,用于存储与控制台相关的自定义数据。
    • 作用:开发者可以使用它存储设备专用的上下文或数据结构。
    • 可能的赋值
.data = custom_data,   // 自定义数据
  1. struct console *next;

    • 功能:指向下一个控制台的指针。
    • 作用:内核中可能会有多个控制台设备,这个指针将控制台设备串成链表,供内核遍历使用。
    • 可能的赋值:不需要手动赋值,内核会自动管理链表中的顺序。

struct console 主要用于描述一个控制台设备的特性,包括其名称、读写操作、设备管理等。开发者实现一个控制台时,需为其成员赋值,特别是 writesetup 函数来实现数据输出和设备初始化等功能。

4.console驱动注册过程

4.1 处理命令行参数

kernel\printk\printk.c中,可以看到如下代码:

__setup("console=", console_setup);

这是用来处理u-boot通过设备树传给内核的cmdline参数,比如cmdline中有如下代码:

console=ttymxc0,115200  console=ttyVIRT0

对于这两个"console=xxx"就会调用console_setup函数两次,构造得到2个数组项:

struct console_cmdline
{
    char	name[16];			/* Name of the driver	    */
    int	index;				/* Minor dev. to use	    */
    char	*options;			/* Options for the driver   */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
    char	*brl_options;			/* Options for braille driver */
#endif
};

static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];

在cmdline中,最后的"console=xxx"就是"selected_console"(被选中的console,对应/dev/console):
在这里插入图片描述

4.2 register_console

console分为两类,它们通过console结构体的flags来分辨(flags中含有CON_BOOT):

  • bootconsoles:用来打印很早的信息
  • real consoles:真正的console

可以注册很多的bootconsoles,但是一旦注册real consoles时,所有的bootconsoles都会被注销,并且以后再注册bootconsoles都不会成功。


被注册的console会放在console_drivers链表中,谁放在链表头部?

  • 如果只有一个real consoles,它自然是放在链表头部
  • 如果有多个real consoles,“selected_console”(被选中的console)被放在链表头部

放在链表头有什么好处?APP打开"/dev/console"时,就对应它。

uart_add_one_port
    uart_configure_port
        register_console(port->cons);

在正确填充struct console变量之后,通过register_console接口将其注册到kernel中即可。该接口将会完成如下的事情:

检查该console的name和index,确认之前没有注册过,否则注册失败;

如果该console为boot console(CON_BOOT),确认是第一个注册的boot console,否则注册失败;

如果系统中从来没有注册过console,则将第一个被注册的、可以setup成功(.setup为NULL或者返回0)的console作为正在使用的console,并使能之(CON_ENABLED);

和command line中的“console=xxx”最对比,使能那些在命令行中指定的console;

查找在command line中指定的最后一个console,并置位其CON_CONSDEV flag,表明选择它为控制台console。

4.3 open /dev/console

drivers\tty\tty_io.c中,代码调用过程如下:

tty_open
    tty = tty_open_by_driver(device, inode, filp);
        driver = tty_lookup_driver(device, filp, &index);
            case MKDEV(TTYAUX_MAJOR, 1): {
                struct tty_driver *console_driver = console_device(index);  
                // 这就实现了通过打开/dev/console,就能获取到与绑定的tty_driver


/* 从console_drivers链表头开始寻找
 * 如果console->device成功,就返回它对应的tty_driver
 * 这就是/dev/console对应的tty_driver
 */ 
struct tty_driver *console_device(int *index)
{
    struct console *c;
    struct tty_driver *driver = NULL;

    console_lock();
    for_each_console(c) {
        if (!c->device)
            continue;
        driver = c->device(c, index);   //返回与控制台设备关联的 tty_driver 结构体,用于处理与终端设备的通信。
        if (driver)
            break;
    }
    console_unlock();
    return driver;
}

5.编写console驱动

编写的代码:📎virtual_uart.c

5.1 编写

1)如果希望该console可以作为系统控制台(/dev/console),则必须先实现该console对应的TTY设备的TTY driver。

2)定义一个console变量,并根据实际情况填充对应的字段,包括name、index、setup(可选)、write、device(可选)等。

3)调用register_console将其注册到kernel中即可。

/*
 * Interrupts are disabled on entering
 */
static void virt_uart_console_write(struct console *co, const char *s, unsigned int count)
{
	int i;
	for (i = 0; i < count; i++)
		if (txbuf_put(s[i]) != 0)
			return;
}
struct tty_driver *virt_uart_console_device(struct console *co, int *index)
{
	struct uart_driver *p = co->data;
	*index = co->index;
	return p->tty_driver;
}

static struct console virt_uart_console = {
	.name		= "ttyVIRT",
	.write		= virt_uart_console_write,
	.device		= uart_console_device,
	.flags		= CON_PRINTBUFFER,
	.index		= -1,
};

static struct uart_driver virt_uart_drv = {
	.owner          = THIS_MODULE,
	.driver_name    = "VIRT_UART",
	.dev_name       = "ttyVIRT",
	.major          = 0,
	.minor          = 0,
	.nr             = 1,
	.cons           = virt_uart_console,
};

virt_uart_console_write 函数:

功能:

virt_uart_console_write 函数是控制台(console)用于输出日志信息到虚拟 UART(串行端口)的核心部分。它负责将要输出的数据从内核传输到串行设备的缓冲区 txbuf 中。

参数解释:

  • struct console *co: 指向当前控制台的指针。
  • const char *s: 需要输出的字符串数据。
  • unsigned int count: 字符串的长度。

工作原理:

  1. 循环遍历每个字符

    • 函数使用 for 循环遍历传入的字符串(s),每次取出一个字符进行处理。
  2. 将字符存入 txbuf 缓冲区

    • 每次调用 txbuf_put(s[i]) 将当前字符存入 txbuf 缓冲区。txbuf_put 函数负责判断缓冲区是否已满,如果满了则不再存储字符,并提前结束函数。
  3. 提前退出

    • 如果 txbuf 缓冲区已满,txbuf_put 返回非 0,表示无法继续写入字符,函数立即退出。

该函数将日志信息逐字节地写入 txbuf 缓冲区中,txbuf 缓冲区用于存储要发送的数据,稍后会将其通过 UART 设备发送出去。由于虚拟 UART 是模拟的串行设备,所以数据先放入一个环形缓冲区,最终由串口驱动发送出去。


virt_uart_console_device 函数:

功能:

virt_uart_console_device 函数返回与虚拟 UART 控制台设备关联的 tty_driver,用于连接 TTY
层和 UART 层。这是控制台设备注册的一部分,表示它将会使用 TTY 子系统。

参数解释:

  • struct console *co: 指向当前控制台的指针。
  • int *index: 返回控制台设备的索引号,用于区分不同的虚拟 UART 设备。

工作原理:

  1. 获取 UART 驱动的指针

    • 通过 co->data 获取与该控制台关联的 UART 驱动(struct uart_driver *p),co->data 在控制台结构体中初始化时赋值为 virt_uart_drv
  2. 设置控制台的索引

    • 通过 *index = co->index; 将控制台的索引号传递回上层调用函数。
  3. 返回 tty_driver

    • 最后返回 p->tty_driver,这是与当前 UART 驱动关联的 TTY 驱动。

该函数用于将控制台和 TTY 驱动关联起来。它返回的是 UART 驱动对应的 tty_driver,这使得虚拟 UART 可以与 TTY 层进行通信,处理如字符输入输出、终端控制等功能。

其余的代码介绍看前面编写虚拟的UART驱动程序中有介绍(深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现_uart调试-CSDN博客

5.2 上机

主要是修改下cmdlien就行,剩下的驱动编译安装、设备树替换编译等看前面的上机介绍就行了

在uboot执行:
setenv mmcargs setenv bootargs console=${console},${baudrate} root=${mmcroot} console=ttyVIRT0
boot

启动linux后确认:
cat /proc/cmdline

安装驱动程序:
echo "7 4 1 7" > /proc/sys/kernel/printk
insmod virtual_uart.ko
cat /proc/consoles  /* 确认有没有ttyVIRT0 */

cat /proc/virt_uart_buf  /* 查看信息 */

echo hello > /dev/console
cat /proc/virt_uart_buf  /* 查看信息 */

网站公告

今日签到

点亮在社区的每一天
去签到

热门文章