LinuxTTY 子系统4(基于Linux6.6)---console driver介绍
一、 概述
在Linux内核中,console
框架是一个负责将信息从内核输出到终端、屏幕、串口等设备的子系统。控制台(console)是操作系统中与用户交互的最基本方式之一,Linux内核通过控制台将重要的内核日志信息、错误消息、调试信息等输出给用户。控制台也通常是系统启动时默认的用户界面,直到图形界面或其他更高级的终端被加载。
控制台的组成
Linux的控制台框架由多个组件构成,主要包括:
- 控制台设备(Console Device):负责输出信息到终端或物理设备。可以是屏幕、串口、虚拟终端等。
- 控制台操作结构(Console Operations):定义了控制台的行为,包括输出字符、输入字符、刷新屏幕等操作。
- 控制台管理(Console Management):用于管理多个控制台设备的注册和切换。
二、设计思路
既然已经有了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会在哪些候选设备上输出,那个候选设备最终会成为控制台设备。示意图如下:
三、 核心数据结构
理解了console框架的设计思路之后,再来看它的实现,就很简单了。其核心数据结构为struct console,如下:
include/linux/console.h
struct console {
char name[16];
void (*write)(struct console *co, const char *s, unsigned int count);
int (*read)(struct console *co, char *s, unsigned int count);
struct tty_driver *(*device)(struct console *co, int *index);
void (*unblank)(void);
int (*setup)(struct console *co, char *options);
int (*exit)(struct console *co);
int (*match)(struct console *co, char *name, int idx, char *options);
short flags;
short index;
int cflag;
uint ispeed;
uint ospeed;
u64 seq;
unsigned long dropped;
void *data;
struct hlist_node node;
/* nbcon console specific members */
bool (*write_atomic)(struct console *con,
struct nbcon_write_context *wctxt);
bool (*write_thread)(struct console *con,
struct nbcon_write_context *wctxt);
void (*driver_enter)(struct console *con, unsigned long *flags);
void (*driver_exit)(struct console *con, unsigned long flags);
atomic_t __private nbcon_state;
atomic_long_t __private nbcon_seq;
struct printk_buffers *pbufs;
struct task_struct *kthread;
struct rcuwait rcuwait;
struct irq_work irq_work;
};
该数据结构中经常被使用的字段有:
name,该console的名称,配合index字段,可用来和命令行中的“console=xxx”中的“xxx”匹配,例如:
1.如果name为“ttyXS”,index为大于等于0的数字(例如2),则可以和“console=ttyXS2”匹配;
2.如果name为“ttyXS”,index为小于0的数字(例如-1),则可以和“console=ttySXn“(n=0,1,2…)任意一个匹配。
write,如果某个console被选中作为printk的输出,则kernel printk模块会调用write回调函数,将日志信息输出到。
device,获取该console对应的TTY driver,用于将console和对应的TTY设备绑定,这样控制台终端就可以和console共用同一个TTY设备了。
setup,用于初始化console的回调函数,console driver可以在该回调函数中对硬件做出现动作。可以不实现,如果实现,则必须返回0,否则该console不可用。
flags,指示属性的flags,常用的包括:
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注册前的日志输出。
CON_ENABLED,表示该console正在被使用。
四、 接口说明
4.1 console driver的注册接口
对具体的console driver来说,只需要关心console的注册/注销接口即可:
include/linux/console.h
extern void register_console(struct console *);
extern int unregister_console(struct console *);
在正确填充struct console变量之后,通过register_console接口将其注册到kernel中即可。该接口将会完成如下的事情:
1.检查该console的name和index,确认之前没有注册过,否则注册失败;
2.如果该console为boot console(CON_BOOT),确认是第一个注册的boot console,否则注册失败;
3.如果系统中从来没有注册过console,则将第一个被注册的、可以setup成功(.setup为NULL或者返回0)的console作为正在使用的console,并使能之(CON_ENABLED);
4.command line中的“console=xxx”最对比,使能那些在命令行中指定的console;
5.查找在command line中指定的最后一个console,并置位其CON_CONSDEV flag,表明选择它为控制台console。
4.2 用户层面接口
系统console的使用控制,主要由命令行参数(一般都是bootloader传递而来的)指定,总结如下:
1)如果某个console只需要在启动的时候使用,则需要在注册console的时候,置位其CON_BOOT标志,例如[3]中介绍的early console。
2)如果系统中注册了多个console,可以通过命令行参数指定使用哪个或者哪些,kernel的日志将会输出到所有在命令行指定了的console上面。
3)命令行中指定的最后一个console,将会作为控制台console,应用程序打开/dev/console将会打开该console对应的TTY设备(由.device回调返回的tty driver指定)。
五、console driver的编写步骤
5.1、创建控制台驱动模块
创建一个新的内核模块,来实现控制台功能。
初始化内核模块
创建一个源文件(例如 my_console.c
),并编写基本的模块框架:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/console.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Console Driver");
static int __init my_console_init(void) {
printk(KERN_INFO "My Console Driver initialized.\n");
return 0;
}
static void __exit my_console_exit(void) {
printk(KERN_INFO "My Console Driver exited.\n");
}
module_init(my_console_init);
module_exit(my_console_exit);
这个代码只是初始化了一个简单的内核模块,在加载和卸载时输出日志信息。
定义控制台操作结构体
控制台操作结构体 console_operations
定义了控制台的 I/O 函数。常见的操作包括输入(读取)、输出(写入)等。你需要实现这些操作。
static struct console my_console;
static int my_console_write(struct console *co, const char *s, unsigned int count) {
unsigned int i;
for (i = 0; i < count; i++) {
// 输出每个字符到硬件
printk(KERN_INFO "%c", s[i]); // 这里仅输出到内核日志,实际可以写入硬件
}
return count;
}
static struct console my_console = {
.name = "my_console", // 控制台名称
.write = my_console_write, // 写操作
.flags = CON_PRINTBUFFER, // 将输出定向到内核日志
};
static int __init my_console_init(void) {
printk(KERN_INFO "My Console Driver initialized.\n");
// 注册控制台
register_console(&my_console);
return 0;
}
static void __exit my_console_exit(void) {
printk(KERN_INFO "My Console Driver exited.\n");
// 注销控制台
unregister_console(&my_console);
}
module_init(my_console_init);
module_exit(my_console_exit);
5.2、实现控制台输入处理(可选)
除了输出功能,还可以实现输入处理。输入通常是由设备中断或轮询方式实现的。
例如,可以添加一个简单的输入处理函数:
static int my_console_read(struct console *co, char *buf, unsigned int count) {
// 在这里实现读取输入的代码
// 假设每次返回一个字符,实际可以从设备缓存中读取数据
return 0; // 返回读取的字节数
}
static struct console my_console = {
.name = "my_console", // 控制台名称
.write = my_console_write, // 写操作
.read = my_console_read, // 读操作
.flags = CON_PRINTBUFFER, // 打印到内核日志
};