往期内容
本专栏往期内容:Uart子系统
- UART串口硬件介绍
- 深入理解TTY体系:设备节点与驱动程序框架详解
- Linux串口应用编程:从UART到GPS模块及字符设备驱动
- 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
- IMX 平台UART驱动情景分析:注册篇
- IMX 平台UART驱动情景分析:open篇
- IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析
- IMX 平台UART驱动情景分析:write篇–从 TTY 层到硬件驱动的写操作流程解析
- 深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现
10.Linux 内核日志系统—printk的机制与应用
interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发
– 末片,有专栏内容观看顺序pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
目录
1.内核代码
Linux 4.9.88
- kernel/printk.c📎printk.c
- include/linux/kernel.h📎kernel.h
- kernel/printk/internal.h📎internal.h
- drivers/tty/serial/imx.c📎imx.c
本文深入探讨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会在哪些候选设备上输出,那个候选设备最终会成为控制台设备。示意图如下:
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 内核中用于表示控制台设备(如屏幕、串口、虚拟终端等)的结构体。它定义了控制台设备的名称、读写操作、设备设置等功能,每个控制台需要根据自己的特点填写相应的字段来实现特定的行为。
char name[16];
- 功能:控制台设备的名称,最多为 16 个字符。console的name和index:/dev/name+index
- 作用:标识控制台的类型,例如虚拟终端、串口等。
- 可能的赋值:
.name = "ttyS", // 表示串口设备
.name = "tty0", // 表示虚拟终端设备
void (*write)(struct console *, const char *, unsigned);
- 功能:写日志的函数指针。当内核想要输出数据到控制台时,会调用这个函数。
- 作用:实际执行数据输出到设备。
- 可能的赋值:
.write = uart_console_write, // 串口设备的写入函数
int (*read)(struct console *, char *, unsigned);
- 功能:读取控制台输入的函数指针,用于读取用户输入。
- 作用:如果控制台设备支持从用户那里接收输入,则实现该函数;否则通常为
NULL
。 - 可能的赋值:
.read = NULL, // 不支持输入
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
void (*unblank)(void);
- 功能:唤醒控制台设备的函数。用于在设备空闲时唤醒控制台。
- 作用:用于显示设备的电源管理(如唤醒屏幕)。
- 可能的赋值:
.unblank = fb_console_unblank, // Framebuffer 控制台的唤醒函数
int (*setup)(struct console *, char *);
- 功能:设置控制台设备的初始化参数。用于初始化console的回调函数,console driver可以在该回调函数中对硬件做出现动作。可以不实现,如果实现,则必须返回0,否则该console不可用。
- 作用:初始化控制台时,配置必要的参数(如串口波特率、屏幕分辨率等)。
- 可能的赋值:
.setup = uart_console_setup, // 串口控制台的初始化函数
int (*match)(struct console *, char *name, int idx, char *options);
- 功能:用于匹配控制台设备。
- 作用:用于注册多个控制台时,选择正确的设备。
- 可能的赋值:
.match = uart_console_match, // 匹配串口控制台
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, // 控制台已启用且随时可用
short index;
- 功能:控制台的索引号,表示设备的序号或设备号。
- 作用:用于区分多个相同类型的设备(如
tty0
,tty1
等)。 - 可能的赋值:
.index = -1, // 动态分配索引
int cflag;
- 功能:控制台的配置标志,通常与串口或终端设备的特定设置相关。
- 作用:配置控制台的行为,如波特率、校验位等。
- 可能的赋值:
.cflag = B9600 | CS8 | CREAD, // 例如串口配置9600波特率,8位字符
void *data;
- 功能:通用指针,用于存储与控制台相关的自定义数据。
- 作用:开发者可以使用它存储设备专用的上下文或数据结构。
- 可能的赋值:
.data = custom_data, // 自定义数据
struct console *next;
- 功能:指向下一个控制台的指针。
- 作用:内核中可能会有多个控制台设备,这个指针将控制台设备串成链表,供内核遍历使用。
- 可能的赋值:不需要手动赋值,内核会自动管理链表中的顺序。
struct console
主要用于描述一个控制台设备的特性,包括其名称、读写操作、设备管理等。开发者实现一个控制台时,需为其成员赋值,特别是 write
和 setup
函数来实现数据输出和设备初始化等功能。
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
: 字符串的长度。
工作原理:
循环遍历每个字符:
- 函数使用
for
循环遍历传入的字符串(s
),每次取出一个字符进行处理。
- 函数使用
将字符存入
txbuf
缓冲区:- 每次调用
txbuf_put(s[i])
将当前字符存入txbuf
缓冲区。txbuf_put
函数负责判断缓冲区是否已满,如果满了则不再存储字符,并提前结束函数。
- 每次调用
提前退出:
- 如果
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 设备。
工作原理:
获取 UART 驱动的指针:
- 通过
co->data
获取与该控制台关联的 UART 驱动(struct uart_driver *p
),co->data
在控制台结构体中初始化时赋值为virt_uart_drv
。设置控制台的索引:
- 通过
*index = co->index;
将控制台的索引号传递回上层调用函数。返回
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 /* 查看信息 */