昨天在家搭建环境时发现一个问题,我安装的ubuntu 22.04.5,默认的系统启动时不会出现弹出选择内核版本的菜单,需要修改grup脚本:
# 1. 编辑GRUB默认配置文件
sudo vim /etc/default/grub
# 2. 修改以下参数
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 6.6.123"
# 3. 关闭GRUB菜单隐藏(方便后续验证)
GRUB_TIMEOUT=5 # 菜单显示5秒,默认可能是0(隐藏)
GRUB_TIMEOUT_STYLE=menu
# 4. 保存退出后,更新GRUB配置
sudo update-grub
做完如上设置就可正常切换到自己手动安装的内核版本,调试驱动安装就都正常了。

今天我走读了 ttynull 驱动源码, 这是 Linux 内核中基于 TTY 子系统实现的空终端驱动。
驱动简介
ttynull.c核心功能是模拟 /dev/ttynull 设备(类似 /dev/null 但适配 TTY 子系统):
- 写入该设备的数据被直接丢弃,仅返回写入长度;
- 无实际硬件交互,纯软件层面实现 TTY 子系统的 “空操作”;
- 注册为控制台设备,可作为系统输出的 “黑洞”;
- 它是学习 Linux 子系统架构、字符设备驱动的经典入门案例。
完整源码逐行注释
// SPDX-License-Identifier: GPL-2.0 // 内核开源许可证声明(必选)
/*
* Copyright (C) 2019 Axis Communications AB
*
* Based on ttyprintk.c:
* Copyright (C) 2010 Samo Pogacnik
*/
// 内核头文件引入
#include <linux/console.h>
#include <linux/module.h>
#include <linux/tty.h>
// TTY端口操作集(本驱动无硬件,为空实现)
static const struct tty_port_operations ttynull_port_ops;
// TTY驱动核心结构体指针(管理驱动生命周期)
static struct tty_driver *ttynull_driver;
// TTY端口结构体(管理设备打开/关闭/挂起等状态)
static struct tty_port ttynull_port;
/**
* ttynull_open - TTY设备打开函数
* @tty: 指向当前打开的TTY设备结构体
* @filp: 指向文件操作结构体(用户空间open对应的内核态结构)
* 返回值:0表示成功,负数表示错误
* 功能:复用内核tty_port的通用打开逻辑,无自定义硬件初始化
*/
static int ttynull_open(struct tty_struct *tty, struct file *filp)
{
return tty_port_open(&ttynull_port, tty, filp);
}
/**
* ttynull_close - TTY设备关闭函数
* @tty: 当前TTY设备结构体
* @filp: 文件操作结构体
* 功能:复用内核tty_port的通用关闭逻辑,无自定义硬件释放
*/
static void ttynull_close(struct tty_struct *tty, struct file *filp)
{
tty_port_close(&ttynull_port, tty, filp);
}
/**
* ttynull_hangup - TTY设备挂起处理函数
* @tty: 当前TTY设备结构体
* 功能:处理设备挂起事件(如串口断开),复用内核通用逻辑
*/
static void ttynull_hangup(struct tty_struct *tty)
{
tty_port_hangup(&ttynull_port);
}
/**
* ttynull_write - TTY设备写入函数(核心空操作)
* @tty: 当前TTY设备结构体
* @buf: 待写入的数据缓冲区(用户空间传入)
* @count: 待写入的数据长度
* 返回值:传入的count(模拟“成功写入”,实际数据被丢弃)
* 功能:不读取buf中的数据,直接返回长度,实现“写入即丢弃”
*/
static ssize_t ttynull_write(struct tty_struct *tty, const u8 *buf,
size_t count)
{
return count; // 核心:无数据处理,直接返回写入长度
}
/**
* ttynull_write_room - 告知内核设备可用写入空间
* @tty: 当前TTY设备结构体
* 返回值:固定65536(64KB),表示设备有充足写入空间
* 功能:避免内核因“空间不足”拒绝写入,无实际硬件意义
*/
static unsigned int ttynull_write_room(struct tty_struct *tty)
{
return 65536;
}
/**
* ttynull_ops - TTY设备操作集
* 功能:封装驱动实现的核心操作,未实现的接口由内核提供默认空实现
* 注:TTY子系统的核心抽象,所有TTY驱动都需实现该结构体的关键成员
*/
static const struct tty_operations ttynull_ops = {
.open = ttynull_open, // 设备打开接口
.close = ttynull_close, // 设备关闭接口
.hangup = ttynull_hangup, // 设备挂起接口
.write = ttynull_write, // 设备写入接口
.write_room = ttynull_write_room, // 写入空间查询接口
};
/**
* ttynull_device - 控制台与TTY驱动的关联函数
* @c: 控制台结构体
* @index: 设备索引(输出参数)
* 返回值:ttynull_driver(当前TTY驱动指针)
* 功能:将控制台设备关联到ttynull驱动,仅支持1个设备实例(索引0)
*/
static struct tty_driver *ttynull_device(struct console *c, int *index)
{
*index = 0; // 仅支持1个设备实例(/dev/ttynull)
return ttynull_driver;
}
/**
* ttynull_console - 控制台设备结构体
* 功能:注册ttynull为控制台类型设备,支持系统级输出(最终被丢弃)
*/
static struct console ttynull_console = {
.name = "ttynull", // 控制台名称(对应/dev/ttynull)
.device = ttynull_device, // 控制台与TTY驱动的关联函数
};
/**
* ttynull_init - 模块初始化函数(驱动加载入口)
* 返回值:0表示成功,负数表示错误
* 功能:完成TTY驱动的分配、配置、注册,以及控制台注册
* 流程:分配驱动→初始化端口→配置驱动→注册驱动→注册控制台
*/
static int __init ttynull_init(void)
{
struct tty_driver *driver;
int ret;
// 1. 分配TTY驱动结构体
// 参数1:设备实例数(1个);参数2:驱动属性
// TTY_DRIVER_RESET_TERMIOS:打开时重置终端属性
// TTY_DRIVER_REAL_RAW:原始模式(无字符处理)
// TTY_DRIVER_UNNUMBERED_NODE:不生成编号节点(仅/dev/ttynull,无ttynull1/2)
driver = tty_alloc_driver(1,
TTY_DRIVER_RESET_TERMIOS |
TTY_DRIVER_REAL_RAW |
TTY_DRIVER_UNNUMBERED_NODE);
if (IS_ERR(driver)) // 检查分配是否失败(内核分配失败返回错误码指针)
return PTR_ERR(driver); // 转换错误码并返回
// 2. 初始化TTY端口(关联空操作集)
tty_port_init(&ttynull_port);
ttynull_port.ops = &ttynull_port_ops;
// 3. 配置TTY驱动核心属性
driver->driver_name = "ttynull"; // 内核态驱动名称(标识用)
driver->name = "ttynull"; // 用户态设备名称(/dev/ttynull)
driver->type = TTY_DRIVER_TYPE_CONSOLE; // 驱动类型:控制台
driver->init_termios = tty_std_termios; // 初始化终端属性为内核默认值
// 配置输出模式:换行符转换等(符合TTY规范)
driver->init_termios.c_oflag = OPOST | OCRNL | ONOCR | ONLRET;
tty_set_operations(driver, &ttynull_ops); // 绑定驱动操作集
tty_port_link_device(&ttynull_port, driver, 0); // 关联端口与驱动
// 4. 注册TTY驱动到内核
ret = tty_register_driver(driver);
if (ret < 0)
{ // 注册失败:释放已分配的资源(内核编码规范:失败必释放)
tty_driver_kref_put(driver); // 释放驱动结构体
tty_port_destroy(&ttynull_port); // 销毁TTY端口
return ret;
}
// 5. 保存驱动指针 + 注册控制台
ttynull_driver = driver;
register_console(&ttynull_console); // 将ttynull注册为控制台设备
return 0; // 初始化成功
}
/**
* ttynull_exit - 模块退出函数(驱动卸载入口)
* 功能:释放所有资源,与初始化流程相反
* 流程:注销控制台→注销驱动→释放驱动→销毁端口
*/
static void __exit ttynull_exit(void)
{
unregister_console(&ttynull_console); // 注销控制台
tty_unregister_driver(ttynull_driver); // 注销TTY驱动
tty_driver_kref_put(ttynull_driver); // 释放驱动结构体
tty_port_destroy(&ttynull_port); // 销毁TTY端口
}
// 模块加载/卸载入口(内核模块标准声明)
module_init(ttynull_init);
module_exit(ttynull_exit);
// 模块许可证声明(必须为GPL,否则内核拒绝加载)
MODULE_LICENSE("GPL v2");
核心模块拆解
[1]. 头文件与全局变量

[2]. TTY 操作集(tty_operations)
它是 TTY子系统 的核心抽象接口,所有 TTY 驱动都需实现关键成员;
本驱动仅实现必要的 5 个接口,其余接口(如read)由内核提供默认空实现(返回-ENODEV);
write 是核心:通过 “返回长度不处理数据” 实现空驱动的核心功能。
[3]. 模块初始化与退出
初始化流程(ttynull_init)图如下所示:

说明:注册失败时需逐层释放资源(驱动→端口),符合内核 “申请失败必释放” 的编码规范。
退出流程(ttynull_exit):
与初始化流程完全相反:先注销高层接口(控制台),再释放底层资源(驱动→端口),避免资源泄漏。
[4]. 控制台注册
ttynull_console 将驱动注册为控制台设备,支持系统通过 printk 等接口输出数据;
ttynull_device 实现控制台与 TTY 驱动的关联,确保系统输出能路由到 ttynull 驱动。
关键知识点总结
[1]. TTY 子系统核心概念
- tty_driver: TTY 驱动的核心抽象,描述驱动名称、类型、操作集等
- tty_port: 管理 TTY 设备的状态(打开 / 关闭 / 挂起),解耦驱动与硬件
- tty_operations: 驱动需实现的操作接口,是 TTY 子系统与驱动的交互层
- console: 系统控制台抽象,支持内核 / 应用输出到 TTY 设备
[2]. 空驱动设计的巧妙之处
- 复用内核逻辑: 无硬件交互时,直接复用 tty_port 提供的通用接口(open/close/hangup);
- 最小化实现: 仅实现核心的 write 接口,其余接口用默认实现;
- 无资源无处理: ttynull_port_ops 为空实现,因无硬件需管理。
[3]. 内核编码规范
- 必须声明
MODULE_LICENSE("GPL"),否则内核标记为 “tainted” 并拒绝加载;
- 资源申请失败时必须释放已申请的资源,避免内存泄漏;
- 函数命名遵循 “驱动名 + 功能”(如 ttynull_write),增强可读性;
- 返回值规范:ssize_t 类型返回长度,错误时返回负的 errno(如 PTR_ERR(driver))。
学习价值
- 理解 TTY 子系统的分层架构:驱动层(tty_driver)→ 端口层(tty_port)→ 硬件层;
- 掌握内核字符设备驱动的极简实现范式:无硬件时如何复用内核通用逻辑;
- 熟悉内核模块的资源管理规范:申请 / 释放的对称式设计;
- 为学习复杂 TTY 驱动(如串口驱动、虚拟终端驱动)打下基础。
在系统与内核驱动的学习道路上,持续的实践与源码走读是关键。希望这份详细的 ttynull 驱动源码分析,能帮助你更好地理解 Linux 内核的工作机制。本文及更多深度技术内容也可以在 云栈社区 找到。