Linux驱动之tty子系统
深入linux源码探究!
1. TTY 子系统概述
在 Linux 内核中,TTY (Teletype) 子系统是 用户空间与字符设备交互的桥梁,它最早用于模拟电传打字机(Teletype),但如今主要用于 终端、串口、伪终端(PTY)等字符设备的管理。
TTY 子系统负责:
- 字符设备驱动(如串口、虚拟终端等)的管理。
- 终端 I/O 控制(如 stty 命令)。
- 行规处理(输入缓冲、回显、信号处理等)。
- 用户进程与设备的连接(如 bash 终端使用
/dev/tty
)。
2. TTY 子系统架构
Linux TTY 子系统由 四层 组成:
- 用户空间 (User Space)
shell
,minicom
,cat /dev/ttyS0
,ioctl()
- TTY 核心层 (TTY Core Layer)
tty_operations
,tty_ldisc
,tty_driver
- TTY 核心层包含线路规程
- 线路规程 (Line Discipline)
- N_TTY (标准终端), N_SLIP, N_PPP
- 低级驱动 (Low-level Drivers)
- 串口 (serial), 虚拟终端 (virtual tty), 伪终端 (pty)
(1)用户空间
用户进程可以通过 /dev/ttyX
访问 TTY 设备,常见的方式包括:
- 文件操作:
open("/dev/ttyS0")
、read/write
。 - IOCTL 控制:如
tcsetattr()
设置波特率、设置线路规程等。 - 终端模拟器:如
minicom
、screen
、tmux
。
(2)TTY 核心层
TTY 核心层提供 TTY 设备的抽象接口,包括:
struct tty_driver
:描述 TTY 设备的驱动信息。struct tty_operations
:TTY 设备操作函数,如open()
、write()
、read()
。struct tty_port
:管理 TTY 端口,包括缓冲区、引用计数。struct tty_ldisc
:描述ldisc
线路规程(一般不增删、tty核心层已经做好不同线路规程的结构体实体)
(3)线路规程(Line Discipline)
- TTY 设备的输入数据可以通过不同的 线路规程 进行处理,在 Linux 中,有多个常见的线路规程。最常用的有:
-
N_TTY
(默认)(TTY_LINE_DISC_TERMINAL):标准的文本终端处理规程。用于普通的字符终端设备,比如虚拟终端(tty、ttyS 等)。它负责处理普通的字符回显、行编辑、字符映射等。 -
N_PTY
(TTY_LINE_DISC_PTY):伪终端处理规程。用于创建伪终端设备(如 tty 或 pty),用于实现终端模拟(如终端仿真器)。N_PTY 允许一个进程模拟另一个进程的终端行为。 -
N_SERIAL
(TTY_LINE_DISC_SERIAL):用于串口通信的线路规程。该规程处理与串口设备(如 8250 串口)之间的通信,包括流控制、数据转换等。 -
N_UUCP
:用于 UUCP(Unix-to-Unix Copy Protocol)通信的线路规程。它支持特定的通信协议,通常用于较早的拨号通信。 -
N_SG
:用于高性能串口设备的线路规程。
- 一些常见的用于设置线路规划模式的 IOCTL 命令包括:
- TIOCSETD:设置 TTY 设备的线路规划。
- TIOCSERIAL:设置串口的相关属性(例如波特率、停止位等)。
- TCSETA、TCSETAW、TCSETAF 等:设置终端的
termios
配置。
- 线路规程模式是在用户层指定
tcsetattr()
->ioctl()
-> 按照传递进来的CMD参数来创建不同的线路规程模式,并且添加到对应的tty_struct
结构体中
(4)低级驱动
低级驱动负责具体设备的实现,包括:
- 串口驱动:
drivers/tty/serial/
,如8250
驱动。 - 虚拟终端:
drivers/tty/vt/
,如tty0
、tty1
。 - 伪终端(PTY):
drivers/tty/pty.c
,用于ssh
、tmux
等。
3. TTY 关键数据结构
(1)TTY 驱动结构体 tty_driver
tty_driver
的**ttys
、**ports
、**cdevs
都是在tty_alloc_driver
函数中分配得的内存空间(分别是申请指向*tty_struct
、*port
、*cdev
的数组,但是具体的port
、tty_struct
、cdev
并没有在这里分配)
定义在 include/linux/tty_driver.h
:
struct tty_driver {int magic; /* magic number for this structure */struct kref kref; /* Reference management */struct cdev **cdevs;struct module *owner;const char *driver_name;const char *name;int name_base; /* offset of printed name */int major; /* major device number */int minor_start; /* start of minor device number */unsigned int num; /* number of devices allocated */short type; /* type of tty driver */short subtype; /* subtype of tty driver */struct ktermios init_termios; /* Initial termios */unsigned long flags; /* tty driver flags */struct proc_dir_entry *proc_entry; /* /proc fs entry */struct tty_driver *other; /* only used for the PTY driver *//** Pointer to the tty data structures*/struct tty_struct **ttys;struct tty_port **ports;struct ktermios **termios;void *driver_state; // 这个地方就是tty驱动层抽象底下不同设备的驱动的指针变量/** Driver methods*/const struct tty_operations *ops;struct list_head tty_drivers;
}
- 作用:
- 注册 TTY 设备:使用
tty_register_driver()
。 - 管理多个 TTY 端口:如
/dev/ttyS0
,/dev/ttyS1
。 - 存储具体设备的相关数据指针
void *driver_state;
这个地方就是tty驱动层抽象底下不同设备的驱动的指针变量。- 比如在uart的驱动中就用
driver_state
这成员变量指向&uart_driver
用来存储uart串口的信息,便于tty_operation
中的操作集访问。
- 一个
tty_driver
会对应多个tty_port
- 在设备打开后,几乎操作的都是tty_struct来获取所有与进程和tty_port相关信息
- uart具体设备的一些结构体
struct uart_driver
struct uart_driver {struct module *owner;const char *driver_name;const char *dev_name;int major;int minor;int nr; //串口数量struct console *cons;struct uart_state *state;struct tty_driver *tty_driver;
};
struct uart_state
struct uart_state {struct tty_port port;enum uart_pm_state pm_state;struct circ_buf xmit;atomic_t refcount;wait_queue_head_t remove_wait;struct uart_port *uart_port;
};
struct uart_port
struct uart_port {...struct uart_state *state; ...const struct uart_ops *ops;...unsigned int line; /* port index */...
};
(2)TTY 设备操作 tty_operations
tty_operations
是tty核心层暴露给具体设备操作集接口,注册申请tty设备需要实现tty_operations
结构体中的函数集。
- 比如uart驱动中就实现了
tty_operations
操作集 - 如果需要注册一个tty设备就得实现
tty_operations
操作集
- 定义
定义在include/linux/tty_driver.h
:
struct tty_operations {int (*open)(struct tty_struct *tty, struct file *filp);void (*close)(struct tty_struct *tty, struct file *filp);int (*write)(struct tty_struct *tty, const unsigned char *buf, int count);...
};
-
该
tty_operations
操作集的具体用到的地方在本文第4节《tty框架的write/read流程》中的图片中有标注。 -
常见操作:
open()
:打开 TTY 设备。close()
:关闭 TTY 设备。write()
:写入数据到 TTY 设备。
(3)TTY 端口 tty_port
-
TTY 端口管理,如缓冲区、状态:
-
tty_port 需要手动分配和初始化,并且
tty_port
初始化后必须挂在到tty_driver
的ports[i]
中,或者挂在到tty_struct
中的port
字段,否则会引起crash the kernel
,(在uart驱动中,已经将tty_port
添加tty_struct
中的port
字段中了), -
或者在需要操作
tty_port
的地方能够正确找到创建的tty_port
,比如在tty_operations
的open中就需要tty_port_open()
来传入tty_port*
-
tty_port_install()
中可以将tty_struct
中的port
字段和tty_port
关联 -
重要的是
tty_struct
中的port
字段需要被填充struct port*
,如下(在tty_init_dev()中):
if (!tty->port)tty->port = driver->ports[idx];
struct tty_port {struct tty_bufhead buf; /* Locked internally */struct tty_struct *tty; /* Back pointer */struct tty_struct *itty; /* internal back ptr */const struct tty_port_operations *ops; /* Port operations */const struct tty_port_client_operations *client_ops; /* Port client operations */spinlock_t lock; /* Lock protecting tty field */int blocked_open; /* Waiting to open */int count; /* Usage count */wait_queue_head_t open_wait; /* Open waiters */wait_queue_head_t delta_msr_wait; /* Modem status change */unsigned long flags; /* User TTY flags ASYNC_ */unsigned long iflags; /* Internal flags TTY_PORT_ */unsigned char console:1, /* port is a console */low_latency:1; /* optional: tune for latency */struct mutex mutex; /* Locking */struct mutex buf_mutex; /* Buffer alloc lock */unsigned char *xmit_buf; /* Optional buffer */unsigned int close_delay; /* Close port delay */unsigned int closing_wait; /* Delay for output */int drain_delay; /* Set to zero if no pure timebased drain is needed elseset to size of fifo */struct kref kref; /* Ref counter */void *client_data;
};
(4)TTY 结构体 tty_struct
-
tty_struct
是整个tty子系统管理的核心结构体,负责将进程、tty_driver
、tty_port
联系起来。 -
tty_struct由 TTY 核心在打开 TTY 设备时在
tty_init_dev
中自动创建tty_struct
,并添加到tty_driver
的*ttys[tty->index]
中。 -
tty_port->tty
并不会存储所有tty_struct
,它只是一个指针,指向最近一次成功打开的tty_struct
。
4. tty框架的write/read流程
上面的write
/read
是在linux源码一步一步跳转查看的。具体流程应该差不多。
5. 简单虚拟tty设备框架
- 这里的
tty_port
是直接创建初始化,并挂在在tty_driver->ports[i]
中,并没有在其他install
(tty_operations
)的接口上把tty_port
挂载到tty->port
上。 - 也可以实现install接口来实现把
tty_port
挂载到tty->port
上。
- 代码说明:
- 这里代码会生成
/dev/ttyPCIE0
和/dev/ttyPCIE1
设备。 - 向其中一个设备写入数据会写入到另一个设备中,可以从另一个设备读取。
- 可以用minicom工具打开测试
sudo minicom -D /dev/ttyPCIE0
sudo minicom -D /dev/ttyPCIE1
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/serial.h>
#include <linux/serial_core.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>#define TTY_MAJOR_AUTO 0
#define VIRTUAL_TTY_MINORS 2
#define DRIVER_NAME "gps_pcie_tty"static struct tty_driver *gps_pcie_tty_driver;
static struct tty_port *gps_pcie_tty_ports[VIRTUAL_TTY_MINORS] = {NULL, NULL};static const struct tty_port_operations gps_pcie_tty_port_ops = {};static int gps_pcie_tty_open(struct tty_struct *tty, struct file *filp)
{int port_num = tty->index;if (port_num >= VIRTUAL_TTY_MINORS)return -ENODEV;// 设置 tty->porttty->port = gps_pcie_tty_ports[port_num];// 打开端口return tty_port_open(tty->port, tty, filp);
}static void gps_pcie_tty_close(struct tty_struct *tty, struct file *filp)
{tty_port_close(tty->port, tty, filp);
}static int gps_pcie_tty_write(struct tty_struct *tty, const unsigned char *buf, int count)
{int port_num = tty->index;int other_port_num = (port_num == 0) ? 1 : 0;struct tty_port *other_port = gps_pcie_tty_ports[other_port_num];struct tty_struct *other_tty = tty_port_tty_get(other_port);// 另一个tty设备if (other_tty) {// 向上递交数据tty_insert_flip_string(other_port, buf, count);tty_flip_buffer_push(other_port);tty_kref_put(other_tty);}return count;
}static const struct tty_operations gps_pcie_tty_ops = {.open = gps_pcie_tty_open,.close = gps_pcie_tty_close,.write = gps_pcie_tty_write,
};static int __init gps_pcie_tty_init(void)
{int ret;int i;gps_pcie_tty_driver = tty_alloc_driver(VIRTUAL_TTY_MINORS, TTY_DRIVER_REAL_RAW);if (IS_ERR(gps_pcie_tty_driver))return PTR_ERR(gps_pcie_tty_driver);gps_pcie_tty_driver->owner = THIS_MODULE;gps_pcie_tty_driver->driver_name = DRIVER_NAME;gps_pcie_tty_driver->name = "ttyPCIE";gps_pcie_tty_driver->major = TTY_MAJOR_AUTO;gps_pcie_tty_driver->minor_start = 0;gps_pcie_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;gps_pcie_tty_driver->subtype = SERIAL_TYPE_NORMAL;gps_pcie_tty_driver->init_termios = tty_std_termios;gps_pcie_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;tty_set_operations(gps_pcie_tty_driver, &gps_pcie_tty_ops);for (i = 0; i < VIRTUAL_TTY_MINORS; i++) {gps_pcie_tty_ports[i] = kzalloc(sizeof(struct tty_port), GFP_KERNEL);if (!gps_pcie_tty_ports[i]) {ret = -ENOMEM;goto err_free_ports;}tty_port_init(gps_pcie_tty_ports[i]);gps_pcie_tty_ports[i]->ops = &gps_pcie_tty_port_ops;gps_pcie_tty_driver->ports[i] = gps_pcie_tty_ports[i];}ret = tty_register_driver(gps_pcie_tty_driver);if (ret) {goto err_free_ports;}pr_info("gps_pcie_tty loaded\n");return 0;err_free_ports:for (i = 0; i < VIRTUAL_TTY_MINORS; i++) {if (gps_pcie_tty_ports[i]){kfree(gps_pcie_tty_ports[i]);gps_pcie_tty_ports[i] = NULL;}}if(gps_pcie_tty_driver){tty_driver_kref_put(gps_pcie_tty_driver);}return ret;
}static void __exit gps_pcie_tty_exit(void)
{int i;tty_unregister_driver(gps_pcie_tty_driver);for (i = 0; i < VIRTUAL_TTY_MINORS; i++) {if (gps_pcie_tty_ports[i] != NULL){tty_port_tty_hangup(gps_pcie_tty_ports[i], false);tty_port_destroy(gps_pcie_tty_ports[i]);kfree(gps_pcie_tty_ports[i]);gps_pcie_tty_ports[i] = NULL;}}if(gps_pcie_tty_driver){tty_driver_kref_put(gps_pcie_tty_driver);}gps_pcie_tty_driver = NULL;pr_info("gps_pcie_tty driver unloaded\n");
}module_init(gps_pcie_tty_init);
module_exit(gps_pcie_tty_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("One Code");
MODULE_DESCRIPTION("One-Code PCIE TTY DRIVER");