在这里插入图片描述

drivers/input/serio.c 串行输入总线抽象(Serial Input Bus Abstraction) PS/2及传统输入端口的核心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术以及它所实现的serio(Serial I/O)子系统,是为了给Linux内核提供一个标准化的、与具体硬件控制器解耦的框架,来管理传统的、基于串行字节流的输入设备。它主要解决了以下问题:

  • 抽象硬件接口:在PC体系结构中,键盘和PS/2鼠标都连接到一个称为i8042(或兼容的)键盘控制器上。这个控制器本身有其复杂的、通过I/O端口访问的底层接口。serio框架将这种底层的、特定于控制器的交互,抽象成一个简单的、通用的串行字节流接口。
  • 分离总线与设备驱动serio扮演了一个“总线”的角色。它将硬件控制器(如i8042)的驱动与连接在该控制器上的具体设备(如PS/2鼠标、AT键盘)的驱动分离开来。这使得鼠标驱动(psmouse.c)无需关心它是在和一个真实的i8042芯片通信,还是在和一个虚拟化环境模拟的控制器通信。
  • 支持多种设备类型:PS/2端口不仅可以连接标准的鼠标和键盘,还可以连接触摸板、指点杆(TrackPoint)等。serio总线允许内核探测连接在端口上的设备类型,并为其加载正确的设备驱动。

它的发展经历了哪些重要的里程碑或版本迭代?

serio框架是Linux支持PC硬件的 foundational 组件之一,其发展相对稳定,主要里程碑体现在其架构的成熟和角色的演变上。

  • 初期实现:作为Linux支持PC AT键盘和PS/2鼠标的基础,serio从内核早期就已经存在。
  • 架构确立:其最重要的里程碑是确立了**“端口驱动 -> serio总线 -> 设备驱动”**的三层架构。这使其成为一个真正的总线系统,而不是一个单一的驱动。
  • 角色的转变:随着USB(特别是USB HID类)的普及,PS/2端口和serio子系统的重要性急剧下降。其角色从主流输入接口转变为纯粹的遗留系统(Legacy System)支持
  • 虚拟化中的核心作用:尽管在物理机上已不常见,但serio在虚拟化技术中扮演着至关重要的角色。几乎所有的虚拟机(如QEMU/KVM, VMware, VirtualBox)都为客户机(Guest OS)模拟了一个标准的i8042控制器和PS/2键盘/鼠标,因为这是最通用、无需特殊驱动就能保证操作系统正常安装和使用的输入设备。这使得serio在现代服务器环境中依然被广泛使用。

目前该技术的社区活跃度和主流应用情况如何?

  • 社区活跃度serio子系统已经非常成熟和稳定,处于纯维护模式。社区不会为其添加新的功能,主要的开发活动是修复bug或进行一些细微的代码清理。
  • 主流应用
    1. 遗留硬件支持:在仍然使用PS/2接口的旧电脑或特定工业计算机上运行Linux。
    2. 虚拟化:这是其当前最主流的应用。为虚拟机提供基础的键盘和鼠标输入。

核心原理与设计

它的核心工作原理是什么?

serio的核心是一个清晰的三层驱动模型:

  1. 底层:端口驱动(Port Driver)

    • 例如 drivers/input/serio/i8042.c
    • 这个驱动知道如何与物理硬件控制器(如i8042芯片)通信。它负责读写I/O端口、注册和处理来自控制器的硬件中断。
    • 它的职责是:从硬件接收原始的字节数据,并通过serio_interrupt()函数将这些字节“喂”给serio核心;同时,它也提供一个write函数,让上层能通过它向硬件发送字节。
    • 端口驱动会创建一个或多个serio_port对象,代表控制器上的物理端口(例如,一个用于键盘,一个用于AUX/鼠标)。
  2. 中层:Serio核心(Bus Driver)

    • drivers/input/serio/serio.c 本身。
    • 它定义了struct serio对象和serio_driver接口,构成了一个迷你的总线系统。
    • 它接收来自端口驱动的字节流,并将其分发给绑定到该serio_port上的设备驱动。
    • 它负责管理设备驱动的注册和匹配。当一个serio_port被创建时,serio核心会尝试探测连接在上面的设备,并为之寻找合适的驱动。
  3. 上层:设备驱动(Device Driver)

    • 例如 drivers/input/mouse/psmouse.cdrivers/input/keyboard/atkbd.c
    • 这些驱动不关心底层的硬件控制器是什么,它们只与serio_port打交道。
    • 它们注册为serio_driver。当serio核心认为某个端口上的设备与某个驱动匹配时,就会调用该驱动的connect回调函数。
    • 设备驱动的核心任务是解析协议。它从serio核心接收字节流,并根据特定设备的协议(如PS/2鼠标的3字节数据包格式,或键盘的扫描码)来解释这些字节。
    • 最后,它将解析后的信息(如按键的按下/弹起、鼠标的相对位移)转换成标准的内核输入事件(input_event),并通过输入核心(Input Core)上报给用户空间。

它的主要优势体现在哪些方面?

  • 出色的抽象和解耦:将硬件控制、总线管理和设备协议解析完美地分离开来,使得各层驱动可以独立开发和维护。
  • 模块化和可扩展性:可以很容易地为一种新的、使用PS/2兼容协议的设备编写驱动,而无需触及底层的i8042代码。
  • 健壮的遗留支持:为古老但重要的PC标准输入设备提供了稳定可靠的支持。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 技术过时serio所服务的硬件本身是过时的。它不支持即插即用(Plug and Play)、高带宽和现代设备的丰富功能。
  • 性能有限:串行协议本身速度慢,中断驱动,效率远低于USB。
  • 功能单一:它本质上只是一个字节流传输通道,不像USB HID那样,设备可以提供详细的描述符来报告自己的功能。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

在现代系统中,serio是处理PS/2协议输入设备唯一且标准的解决方案。

  • 虚拟机基础输入:当你在KVM/QEMU或VirtualBox中创建一个新的虚拟机并开始安装操作系统时,虚拟机提供的键盘和鼠标就是通过模拟的i8042控制器和serio总线来工作的。这保证了即使在没有安装任何额外驱动的裸机环境下,客户机操作系统也能获得基本的输入能力。
  • 嵌入式或工业控制:在一些成本敏感或有特殊要求的嵌入式/工业系统中,可能仍会使用PS/2接口的键盘或条码扫描枪,因为其协议简单、实现成本低。在这些系统上运行Linux时,serio是驱动这些设备的基础。

是否有不推荐使用该技术的场景?为什么?

任何现代输入设备都不应该使用serio

  • 原因:USB、I2C、SPI、Bluetooth等现代总线为输入设备提供了远比serio强大的功能,包括:
    • 热插拔serio不支持。
    • 自动识别:USB HID设备可以通过描述符精确报告自身功能,无需驱动去猜测。
    • 高带宽:支持高回报率(high polling rate)的电竞鼠标或复杂的触摸板。
    • 统一的驱动模型:HID(Human Interface Device)类为各种输入设备(键盘、鼠标、游戏手柄、触摸屏)提供了统一的驱动模型,远比serio的模式更具可扩展性。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比 serio vs. USB HID (Human Interface Device)

特性 serio (及 psmouse/atkbd) USB HID
总线类型 简单的、点对点的、异步串行字节流。 复杂的、基于数据包的、主从式、可拓扑的总线。
设备发现 启动时进行简单的探测和类型猜测。不支持热插拔。 完全的即插即用。设备连接后会进行枚举,主机可读取其所有信息。
设备描述 。驱动程序必须内置对各种设备协议(如 IntelliMouse, ExplorerPS/2)的了解。 有(HID报告描述符)。设备会提供一个详细的报告,精确描述它发送的每个数据包的格式和含义(如“第1个字节的第0位是左键”)。
驱动模型 serio_driver,特定于serio总线。 usb_driver + hid_driver。HID类驱动是通用的,可以处理所有符合HID规范的设备,无论其物理连接是什么。
性能 。带宽和回报率受限于PS/2协议。 。从低速(1.5 Mbps)到超高速(10+ Gbps),可以满足任何输入设备的需求。
应用范围 遗留设备(PS/2键盘、鼠标、触摸板)和虚拟机 所有现代输入设备的标准(USB键盘、鼠标、游戏手柄、触摸屏等)。

Serio驱动Sysfs属性:用户空间接口与手动绑定控制

本代码片段为serio总线上的所有驱动程序定义并实现了一组sysfs属性。其核心功能是向用户空间导出一个标准的、基于文件的接口,允许用户查看驱动的描述信息,以及查询和修改驱动的绑定模式(自动或手动)。这为系统管理员和开发者提供了一种在运行时动态控制驱动行为的机制,特别是实现了将设备与驱动进行手动绑定的能力,从而覆盖内核默认的自动匹配逻辑。

实现原理分析

该代码的实现完全建立在Linux设备模型的sysfs属性框架之上。它利用了一系列的宏和回调函数来创建文件节点。

  1. 属性定义:

    • description (只读): 使用DRIVER_ATTR_RO宏定义了一个名为description的只读属性。这个宏会自动创建一个struct driver_attribute,并将其.show回调指向description_show函数。当用户cat /sys/bus/serio/drivers/<driver_name>/description时,VFS层会调用description_show。该函数会从驱动的私有结构serio_driver中读取description字符串,并将其返回给用户。
    • bind_mode (读写): 使用DRIVER_ATTR_RW宏定义了一个名为bind_mode的读写属性。这会将其.show回调指向bind_mode_show.store回调指向bind_mode_store
      • bind_mode_show: 读取serio_driver结构中的manual_bind布尔标志,并将其转换为字符串”manual”或”auto”返回。
      • bind_mode_store: 当用户向该文件写入(echo "manual" > ...)时,此函数被调用。它会解析用户写入的字符串,并相应地设置serio_driver结构中的manual_bind标志为truefalse
  2. 属性分组 (ATTRIBUTE_GROUPS):

    • serio_driver_attrs数组将所有定义的属性(descriptionbind_mode)收集起来。
    • ATTRIBUTE_GROUPS(serio_driver)宏则将这个属性数组封装成一个attribute_group
    • 这个attribute_group会被serio总线(在上一节中分析的serio_bus结构体中通过.drv_groups字段指定)自动附加到每一个注册到该总线上的驱动程序的sysfs目录中。因此,任何serio驱动在加载后,都会在/sys/bus/serio/drivers/<driver_name>/下自动拥有descriptionbind_mode这两个文件。

代码分析

description属性 (只读)

1
2
3
4
5
6
7
8
9
10
// description_show: "description" sysfs属性的show回调函数。
static ssize_t description_show(struct device_driver *drv, char *buf)
{
// 将通用的device_driver指针转换为serio_driver指针。
struct serio_driver *driver = to_serio_driver(drv);
// 将驱动的description字符串(如果存在)格式化输出到缓冲区。
return sprintf(buf, "%s\n", driver->description ? driver->description : "(none)");
}
// 使用宏定义一个名为"description"的只读驱动属性。
static DRIVER_ATTR_RO(description);

bind_mode属性 (读/写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// bind_mode_show: "bind_mode" sysfs属性的show回调函数。
static ssize_t bind_mode_show(struct device_driver *drv, char *buf)
{
struct serio_driver *serio_drv = to_serio_driver(drv);
// 根据manual_bind标志的值,返回"manual"或"auto"字符串。
return sprintf(buf, "%s\n", serio_drv->manual_bind ? "manual" : "auto");
}

// bind_mode_store: "bind_mode" sysfs属性的store回调函数。
static ssize_t bind_mode_store(struct device_driver *drv, const char *buf, size_t count)
{
struct serio_driver *serio_drv = to_serio_driver(drv);
int retval;

retval = count; // 假定成功,返回写入的字节数。
// 比较用户写入的字符串,并相应地设置manual_bind标志。
if (!strncmp(buf, "manual", count)) {
serio_drv->manual_bind = true;
} else if (!strncmp(buf, "auto", count)) {
serio_drv->manual_bind = false;
} else {
retval = -EINVAL; // 如果输入无效,返回错误。
}

return retval;
}
// 使用宏定义一个名为"bind_mode"的读写驱动属性。
static DRIVER_ATTR_RW(bind_mode);

属性分组

1
2
3
4
5
6
7
8
9
// serio_driver_attrs: 一个数组,收集了所有serio驱动的属性。
static struct attribute *serio_driver_attrs[] = {
&driver_attr_description.attr,
&driver_attr_bind_mode.attr,
NULL, // 数组以NULL结尾。
};
// 使用宏将属性数组定义为一个名为"serio_driver"的属性组。
// 这个组会被自动应用到所有注册到serio总线上的驱动。
ATTRIBUTE_GROUPS(serio_driver);

Serio总线行为实现:设备与驱动的匹配、绑定与事件通知

本代码片段是serio总线类型核心回调函数的具体实现。它定义了当设备模型核心在serio总线上进行操作时应执行的逻辑。其核心功能包括:1) 根据设备ID匹配合适的驱动程序;2) 在匹配成功后,通过调用驱动的connect方法来“绑定”设备与驱动;3) 在解绑时调用disconnect;4) 在系统关机等生命周期事件中通知驱动;5) 生成uevent事件,向用户空间(如udev)广播新设备的信息,以便自动加载驱动模块。

实现原理分析

该代码是Linux设备模型中“总线-驱动-设备”范式的经典实现,它将通用逻辑与驱动的特定实现分离开来。

  1. 匹配机制 (serio_bus_match):

    • 这是设备模型进行自动绑定的第一步。当一个新serio设备或新serio驱动被注册时,内核会遍历所有未绑定的另一方,并调用serio_bus_match
    • 此函数首先检查manual_bind标志,允许用户通过sysfs手动绑定,从而跳过自动匹配。
    • 核心匹配逻辑在serio_match_port中。它会遍历驱动程序提供的id_table(一个serio_device_id数组)。对于表中的每一项,它会将type, proto, extra, id字段与设备的相应ID字段进行比较。SERIO_ANY作为一个通配符,可以匹配任何值。只要找到表中的一项完全匹配,函数就返回成功。
  2. 绑定与解绑 (serio_driver_probe, serio_driver_remove):

    • Probe: 当serio_bus_match返回成功后,设备模型会调用serio_driver_probe。此函数是一个简单的“中间人”,它从devicedevice_driver结构中提取出serioserio_driver指针,然后调用serio_connect_driverserio_connect_driver负责获取锁,并最终调用具体驱动程序提供的.connect()方法,将serio端口的控制权正式移交给驱动。
    • Remove: 当设备或驱动被移除时,serio_driver_remove被调用。它通过serio_disconnect_driver(在此代码段中未显示,但功能与serio_connect_driver相反)来调用驱动的.disconnect()方法,让驱动释放所有资源并断开与端口的连接。
  3. 生命周期管理 (serio_shutdown):

    • 当系统关机时,设备模型会为每个设备调用总线的.shutdown方法。serio_shutdown会进一步调用驱动的.cleanup()方法,为设备提供一个在系统断电前执行最后清理操作的机会。
  4. 用户空间通知 (serio_uevent):

    • 当一个serio设备被注册时,此函数被调用以生成一个uevent。它会将设备的ID信息(type, proto, id, extra)打包成环境变量。
    • 最关键的一步是创建MODALIAS变量。它将设备ID编码成一个标准格式的字符串,如serio:ty01pr00id00ex00。用户空间的udev服务会监听这些uevent,并使用MODALIAS的值去匹配modprobe数据库,从而找到并自动加载能够处理该设备的内核模块。

代码分析

设备/驱动匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// serio_match_port: 检查一个serio端口是否与给定的ID表匹配。
static int serio_match_port(const struct serio_device_id *ids, struct serio *serio)
{
// 遍历驱动提供的ID表。
while (ids->type || ids->proto) {
// 检查type, proto, extra, id四个字段是否匹配。SERIO_ANY是通配符。
if ((ids->type == SERIO_ANY || ids->type == serio->id.type) &&
(ids->proto == SERIO_ANY || ids->proto == serio->id.proto) &&
(ids->extra == SERIO_ANY || ids->extra == serio->id.extra) &&
(ids->id == SERIO_ANY || ids->id == serio->id.id))
return 1; // 找到匹配项,返回1 (成功)。
ids++;
}
return 0; // 遍历完ID表仍未找到匹配项,返回0 (失败)。
}

// serio_bus_match: serio总线的match回调函数,由设备模型核心调用。
static int serio_bus_match(struct device *dev, const struct device_driver *drv)
{
struct serio *serio = to_serio_port(dev);
const struct serio_driver *serio_drv = to_serio_driver(drv);

// 如果设备或驱动被标记为手动绑定,则跳过自动匹配。
if (serio->manual_bind || serio_drv->manual_bind)
return 0;

// 调用辅助函数进行ID表匹配。
return serio_match_port(serio_drv->id_table, serio);
}

核心绑定与生命周期回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// serio_connect_driver: 连接一个驱动到serio端口。
static int serio_connect_driver(struct serio *serio, struct serio_driver *drv)
{
// 使用guard宏自动加锁和解锁,保护serio->drv指针。
guard(mutex)(&serio->drv_mutex);

// 调用具体驱动的connect方法。
return drv->connect(serio, drv);
}

// serio_reconnect_driver: 重新连接一个驱动。
static int serio_reconnect_driver(struct serio *serio)
{
guard(mutex)(&serio->drv_mutex);

// 如果有驱动已连接且该驱动支持reconnect,则调用它。
if (serio->drv && serio->drv->reconnect)
return serio->drv->reconnect(serio);

return -1;
}

// serio_driver_probe: serio总线的probe回调函数。
static int serio_driver_probe(struct device *dev)
{
struct serio *serio = to_serio_port(dev);
struct serio_driver *drv = to_serio_driver(dev->driver);

// 连接匹配到的驱动。
return serio_connect_driver(serio, drv);
}

// serio_driver_remove: serio总线的remove回调函数。
static void serio_driver_remove(struct device *dev)
{
struct serio *serio = to_serio_port(dev);

// 断开驱动连接。
serio_disconnect_driver(serio);
}

// serio_cleanup: 清理serio端口。
static void serio_cleanup(struct serio *serio)
{
guard(mutex)(&serio->drv_mutex);

// 如果有驱动已连接且该驱动支持cleanup,则调用它。
if (serio->drv && serio->drv->cleanup)
serio->drv->cleanup(serio);
}

// serio_shutdown: serio总线的shutdown回调函数。
static void serio_shutdown(struct device *dev)
{
struct serio *serio = to_serio_port(dev);

serio_cleanup(serio);
}

用户空间事件通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SERIO_ADD_UEVENT_VAR: 一个宏,用于简化向uevent环境添加变量的操作。
#define SERIO_ADD_UEVENT_VAR(fmt, val...) \
do { \
int err = add_uevent_var(env, fmt, val); \
if (err) \
return err; \
} while (0)

// serio_uevent: serio总线的uevent回调函数。
static int serio_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const struct serio *serio;

if (!dev)
return -ENODEV;

serio = to_serio_port(dev);

// 添加独立的ID字段作为环境变量。
SERIO_ADD_UEVENT_VAR("SERIO_TYPE=%02x", serio->id.type);
SERIO_ADD_UEVENT_VAR("SERIO_PROTO=%02x", serio->id.proto);
SERIO_ADD_UEVENT_VAR("SERIO_ID=%02x", serio->id.id);
SERIO_ADD_UEVENT_VAR("SERIO_EXTRA=%02x", serio->id.extra);

// 添加最重要的MODALIAS变量,用于驱动模块的自动加载。
SERIO_ADD_UEVENT_VAR("MODALIAS=serio:ty%02Xpr%02Xid%02Xex%02X",
serio->id.type, serio->id.proto, serio->id.id, serio->id.extra);

// 如果有固件ID,也将其添加。
if (serio->firmware_id[0])
SERIO_ADD_UEVENT_VAR("SERIO_FIRMWARE_ID=%s",
serio->firmware_id);

return 0;
}
#undef SERIO_ADD_UEVENT_VAR

Serio总线注册:串行输入设备的驱动模型骨架

本代码片段是Linux内核中serio(Serial Input/Output)子系统的核心初始化部分。其主要功能是定义并向内核的统一设备模型注册一个名为serio的新总线类型(bus_type)。这个serio总线为一类简单的、基于字节流的串行输入设备(如传统的PS/2键盘、鼠标、触摸屏,以及某些触摸板和手写板)提供了一个标准化的驱动模型。它充当了底层端口驱动(如i8042 PS/2控制器驱动)和上层设备驱动(如PS/2键盘驱动)之间的抽象层和粘合剂。

实现原理分析

该代码的实现遵循了Linux设备模型的标准范式,即通过定义和注册一个bus_type结构来创建一个新的总线。

  1. 总线类型定义 (serio_bus):

    • 代码的核心是serio_bus这个bus_type结构体的静态实例。这个结构体通过一系列函数指针(回调函数)来定义总线的行为。
    • .name = "serio": 定义了总线的名字。在bus_register被调用后,内核会在sysfs中创建对应的目录/sys/bus/serio
    • .match = serio_bus_match: 这是最重要的回调之一。当一个新的serio设备或serio驱动被注册到内核时,设备模型核心会调用此函数,以确定驱动是否支持该设备。匹配逻辑通常基于设备提供的ID表。
    • .probe = serio_driver_probe, .remove = serio_driver_remove: 当.match成功后,设备模型会调用总线的.probe函数来绑定设备和驱动。这个总线级别的probe通常会进一步调用具体驱动提供的probe方法。.remove则执行相反的解绑操作。
    • 其他回调如.uevent, .shutdown, .pm则分别定义了总线在处理热插拔事件、系统关机和电源管理时的标准行为。
  2. 总线注册与注销:

    • serio_init(): 这是一个内核模块初始化函数,通过subsys_initcall宏被注册,在系统启动的早期被调用。它的唯一工作就是调用bus_register(&serio_bus),将上面定义的serio_bus结构体正式注册到设备模型核心。从这一刻起,Linux内核就“知道”了serio总线的存在,其他驱动可以通过serio_register_portserio_register_driver来注册设备和驱动。
    • serio_exit(): 这是模块卸载函数,它调用bus_unregister(&serio_bus)来执行相反的操作,从内核中移除serio总线及其所有相关结构。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// serio_bus: 定义了 "serio" 总线类型的核心结构体。
const struct bus_type serio_bus = {
.name = "serio", // 总线的名字,将在sysfs中显示为 /sys/bus/serio
.drv_groups = serio_driver_groups, // 驱动程序的sysfs属性组
.match = serio_bus_match, // 匹配设备与驱动的回调函数
.uevent = serio_uevent, // 处理热插拔事件的回调函数
.probe = serio_driver_probe, // 绑定设备与驱动的回调函数
.remove = serio_driver_remove, // 解绑设备与驱动的回调函数
.shutdown = serio_shutdown, // 系统关闭时的回调函数
#ifdef CONFIG_PM
.pm = &serio_pm_ops, // 电源管理操作的回调函数
#endif
};
// 导出serio_bus符号,使得其他模块(如端口驱动和设备驱动)可以引用它。
EXPORT_SYMBOL(serio_bus);

// serio_init: serio总线的初始化函数。
static int __init serio_init(void)
{
int error;

// 向内核的设备模型核心注册serio总线。
error = bus_register(&serio_bus);
if (error) {
pr_err("Failed to register serio bus, error: %d\n", error);
return error;
}

return 0;
}

// serio_exit: serio总线的退出/清理函数。
static void __exit serio_exit(void)
{
// 从内核的设备模型核心注销serio总线。
bus_unregister(&serio_bus);

/*
* 此时不应有任何未完成的事件,但相关的工作(work)可能
* 仍在调度队列中,所以直接取消它。
*/
cancel_work_sync(&serio_event_work);
}

// 将serio_init注册为子系统初始化调用,在内核启动早期执行。
subsys_initcall(serio_init);
// 将serio_exit注册为模块退出调用,在模块卸载时执行。
module_exit(serio_exit);

Linux 内核 serport(drivers/input/serio/serport.c)全面解析

[drivers/input/serio/serport.c] [serport:串口到 serIO 的桥接驱动] [把一个 TTY 串口“包装成”一个 serio 端口,让 serio/input 生态能像处理 PS/2 那样处理串口侧的字节流设备]

介绍

serport 的核心思想是:利用 TTY 的 line discipline(线路规程)机制,把“串口收发的字节流”接到 serio 子系统上。
这样,serio 侧就能用统一的方式把数据分发给上层 input 驱动(例如各种基于 serio 的协议/设备驱动),而不是每个设备都单独绑在 TTY/serial 上。


历史与背景

这项技术为了解决什么问题而诞生?

  • 早期/特定硬件里存在“看起来像 PS/2/serio 风格协议,但物理层走串口”的设备或适配器。
  • 直接在串口驱动或用户态写协议,会导致上层 input 生态(serio 的解析/驱动复用)无法复用。
  • serport 让“串口承载的字节流”能够复用 serio 的设备模型与驱动绑定方式。

重要里程碑或迭代方向(从常见实现形态可归纳)

  • 从“直接串口协议驱动”转向“serio 框架复用”:通过 line discipline 把串口变成 serio 端口。
  • 与更现代的串口设备框架并存:现在很多“串口外设”更倾向用 serdev 或专用驱动模型;serport 更偏“兼容/特定场景复用 serio”的路径。

社区活跃度与主流应用情况

  • 它属于主线的一部分,但在通用 PC/服务器场景里并不算高频使用;更常见于一些“协议复用 serio”的特定平台/适配器场景。
  • 你在嵌入式里遇到它,多半是:已有上层 serio 驱动想复用,底层接入却是 UART/TTY。

核心原理与设计

核心工作原理是什么?

把它当成两条“数据通道”就很好理解:

  1. RX:串口 → serio
  • TTY 收到数据时,line discipline 的 receive_buf(或等价回调)被调用。
  • serport 将接收到的字节逐个(或批量)送入 serio core(通常是调用 serio 的接收入口),从而触发上层 serio 驱动解析。
  1. TX:serio → 串口
  • 上层 serio 驱动要往设备发命令时,会走 serio 的 write 回调。
  • serport 在这个回调里把字节写回对应的 TTY(例如调用 tty->ops->write 或类似路径),最终发到串口线上。
  1. 生命周期:打开/关闭/绑定
  • 选择某个 line discipline(例如通过用户态对 tty 设置 ldisc),触发 open:创建并注册一个 struct serio 端口对象。
  • close:注销 serio 端口、停止收发、释放资源。

主要优势体现在哪些方面?

  • 复用 serio/input 生态:上层驱动不用关心底层是 i8042 还是串口,只要是 serio 端口就能工作。
  • 设备模型更清晰:serio 的“端口—设备—驱动”绑定关系比“纯 TTY 字节流”更容易做驱动复用与拆分。
  • 对用户态更统一:用户态通常不需要自己解析协议,只要配置好 ldisc/加载驱动即可。

已知劣势、局限性或不适用性

  • 依赖 line discipline:这在现代系统里属于相对“老派”的接入方式;运维/产品化时要处理好谁来设置 ldisc、何时设置、权限如何控制。
  • 字节流 + 时序敏感:如果上层协议对时序敏感,TTY 层的缓冲/调度可能带来抖动;需要看驱动是否做了节流/队列控制。
  • 不适合“本质就是普通串口外设”的场景:如果设备协议不是 serio 生态的一部分,用 serdev/专用驱动往往更直接、维护成本更低。

使用场景

首选解决方案的场景(举例)

  • 你手上有一套现成的 serio 设备驱动(或依赖 serio 的 input 解析),但硬件接入不是 PS/2 控制器而是 UART。
  • 产品里需要把“串口承载的输入设备”纳入统一的 input 框架,同时希望复用 serio 的上层驱动。

不推荐使用的场景?为什么?

  • 设备协议与 serio 无关,只是 UART 设备:更推荐 serdev 或在 tty/uart 上直接写内核驱动(更符合当前主流串口设备模型)。
  • 需要非常严格的时序/低延迟确定性:TTY/ldisc 的缓冲与调度可能不满足,需要更底层的串口驱动协作或专用硬件接口。

对比分析

下面选两个最容易混淆的“相邻方案”对比:

serport vs 直接在 TTY 上做协议驱动

  • 实现方式

    • serport:tty 字节流 → serio 端口 → serio 驱动解析
    • 直接协议驱动:tty 字节流 → 你自己的协议解析 → input 上报
  • 性能开销:serport 多一层 serio 分发;但换来驱动复用与清晰分层。纯协议驱动少一层,但容易重复造轮子。

  • 资源占用:两者都要有缓冲与状态;serport 额外维护 serio 端口对象。

  • 隔离级别:serport 把“传输层”和“设备解析层”隔离更清晰。

  • 启动速度:serport 取决于何时设置 ldisc 并注册 serio;直接驱动取决于何时绑定 tty。

serport vs serdev(现代串口设备框架)

  • 实现方式

    • serport:基于 tty/ldisc
    • serdev:基于设备模型(把 UART 视为可枚举的设备端口,驱动直接绑定)
  • 主流程度:serdev 更贴近当前串口外设驱动主流写法;serport 更偏“为了复用 serio”。

  • 适配成本:你已有 serio 上层驱动时,serport 可能改动更少;若从零写新驱动,serdev 往往更自然。


总结

关键特性

  • 用 line discipline 把一个串口 TTY 变成 serio 端口;
  • RX 把串口数据喂给 serio core,TX 把 serio 写请求送到 tty;
  • 适合“底层是串口、上层想复用 serio 驱动/模型”的场景。

serport_ldisc / serport_init / serport_exit:TTY 行规注册与模块生命周期管理


serport_ldisc:定义一个 TTY 行规(line discipline)操作集

作用与原理(关键点)

tty_ldisc_ops 是 TTY 核心用于调度“行规”的方法表。行规位于 TTY 驱动(底层收发)上层用户态读写/协议解析之间,负责把原始字节流解释为特定语义(这里的语义是 “input/鼠标类数据流”),并通过回调接入接收路径、读写路径、ioctl 等控制路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** @brief 串口输入设备使用的 TTY 行规操作集(方法表)。 */
static struct tty_ldisc_ops serport_ldisc = {
.owner = THIS_MODULE, /**< 用于模块引用计数关联,防止仍被使用时卸载。 */
.num = N_MOUSE, /**< 行规编号:TTY 核心用该编号索引并绑定行规。 */
.name = "input", /**< 行规名称:用于可读性标识与管理。 */
.open = serport_ldisc_open, /**< 行规被绑定到某个 tty 时的初始化入口。 */
.close = serport_ldisc_close, /**< 行规从 tty 解绑时的清理入口。 */
.read = serport_ldisc_read, /**< 上层对该 tty 读时的行规读入口(可选实现)。 */
.ioctl = serport_ldisc_ioctl, /**< 行规私有控制接口入口。 */
#ifdef CONFIG_COMPAT
.compat_ioctl = serport_ldisc_compat_ioctl, /**< 兼容层 ioctl(32/64 兼容场景)入口。 */
#endif
.receive_buf = serport_ldisc_receive, /**< 底层收到数据后上送到行规的入口:最核心的数据接收路径。 */
.hangup = serport_ldisc_hangup, /**< 线路挂断/端口异常时的处理入口。 */
.write_wakeup = serport_ldisc_write_wakeup /**< 底层可写时唤醒上层的入口(流控/异步写相关)。 */
};

serport_init:模块加载时注册行规

作用与原理(关键点)

  • tty_register_ldisc()serport_ldisc 作为一个可被选择/绑定的行规注册到 TTY 核心的行规表中。
  • 若注册失败,当前模块初始化失败;此时该行规不会被系统识别。
  • __init 表示该函数用于初始化阶段;若编译为内建(非模块),其代码段可能在初始化后被回收(具体取决于内核配置与链接方式)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 模块初始化:向 TTY 核心注册本模块提供的行规。
*
* @return 0 成功;负 errno 失败。
*/
static int __init serport_init(void)
{
int retval; /**< 保存 tty_register_ldisc 的返回码。 */

retval = tty_register_ldisc(&serport_ldisc); /**< 将行规方法表注册到 TTY 子系统。 */
if (retval)
printk(KERN_ERR "serport.c: Error registering line discipline.\n"); /**< 仅记录错误,错误码由返回值向上层传播。 */

return retval;
}

serport_exit:模块卸载时注销行规

作用与原理(关键点)

  • tty_unregister_ldisc() 把此前注册的行规从 TTY 核心注销。
  • 若仍存在 tty 正在使用该行规,正确性依赖于 TTY 核心与 .owner = THIS_MODULE 的引用计数/约束机制,避免“正在被调用的回调代码被卸载”。
1
2
3
4
5
6
7
/**
* @brief 模块退出:从 TTY 核心注销本模块提供的行规。
*/
static void __exit serport_exit(void)
{
tty_unregister_ldisc(&serport_ldisc); /**< 注销行规,移除其在 TTY 核心中的可绑定性。 */
}