【从零开始撸内核驱动源码】:ttynull驱动
本文约1400字,之前的两篇帖子 Linux内核驱动源码走读之编译内核及外部驱动实操指南|Linux内核驱动安装失败问题调试及解决方法都是下班回家远程到公司电脑弄的,但是公司外网8点就断开了,以前在家基本不想写代码调试,只看看书,所以影响不大。但是明天上完班就放假了,还是在家里的电脑上重新搭建了开发环境,ubuntu22.04(自带的内核版本是6.8.0), 官网没有找到这个版本的内核源码,下了个最接近的内核源码版本6.6.123,后续走读驱动源码就用这个开发环境了。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。
昨天在家搭建环境时发现一个问题,我安装的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
补充Linux内核驱动安装失败问题调试及解决方法中最后一步,如果reboot虚拟机不弹出选择内核版本的菜单,做完如上设置就可正常切换到自己手动安装的内核版本,调试驱动安装就都正常了。

今天我走读了ttynull驱动源码, 这是Linux 内核中基于 TTY 子系统实现的空终端驱动。
一 驱动简介
ttynull.c核心功能是模拟 /dev/ttynull 设备(类似 /dev/null 但适配 TTY 子系统):
写入该设备的数据被直接丢弃,仅返回写入长度;
无实际硬件交互,纯软件层面实现 TTY 子系统的 “空操作”;
注册为控制台设备,可作为系统输出的 “黑洞”;
它是学习 Linux TTY 子系统架构、字符设备驱动的经典入门案例。
二 完整源码逐行注释
// 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的通用打开逻辑,无自定义硬件初始化*/staticintttynull_open(struct tty_struct *tty, struct file *filp){return tty_port_open(&ttynull_port, tty, filp);}/*** ttynull_close - TTY设备关闭函数* @tty: 当前TTY设备结构体* @filp: 文件操作结构体* 功能:复用内核tty_port的通用关闭逻辑,无自定义硬件释放*/staticvoidttynull_close(struct tty_struct *tty, struct file *filp){tty_port_close(&ttynull_port, tty, filp);}/*** ttynull_hangup - TTY设备挂起处理函数* @tty: 当前TTY设备结构体* 功能:处理设备挂起事件(如串口断开),复用内核通用逻辑*/staticvoidttynull_hangup(struct tty_struct *tty){tty_port_hangup(&ttynull_port);}/*** ttynull_write - TTY设备写入函数(核心空操作)* @tty: 当前TTY设备结构体* @buf: 待写入的数据缓冲区(用户空间传入)* @count: 待写入的数据长度* 返回值:传入的count(模拟“成功写入”,实际数据被丢弃)* 功能:不读取buf中的数据,直接返回长度,实现“写入即丢弃”*/staticssize_tttynull_write(struct tty_struct *tty, const u8 *buf,size_t count){return count; // 核心:无数据处理,直接返回写入长度}/*** ttynull_write_room - 告知内核设备可用写入空间* @tty: 当前TTY设备结构体* 返回值:固定65536(64KB),表示设备有充足写入空间* 功能:避免内核因“空间不足”拒绝写入,无实际硬件意义*/staticunsignedintttynull_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驱动的分配、配置、注册,以及控制台注册* 流程:分配驱动→初始化端口→配置驱动→注册驱动→注册控制台*/staticint __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 - 模块退出函数(驱动卸载入口)* 功能:释放所有资源,与初始化流程相反* 流程:注销控制台→注销驱动→释放驱动→销毁端口*/staticvoid __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 驱动(如串口驱动、虚拟终端驱动)打下基础。
下一篇将介绍单独编译内核自带的驱动,敬请期待。
我们在走读内核驱动源码的过程中,争取将内核、驱动等编译过程中的Makefile相关配置都梳理一遍
以上为全文内容。
这里是女程序员的笔记本
15年+嵌入式软件工程师兼二胎宝妈
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助
夜雨聆风
