LinuxTTY 子系统3(基于Linux6.6)---tty driver介绍
一、概述
在 Linux 中,TTY 框架提供了一套模块化的架构来抽象和管理各种终端设备。它通过一系列的数据结构和编程接口,允许开发者为不同的硬件设备(如串行端口、虚拟终端、伪终端等)编写 TTY 驱动程序。下面将详细讲解 TTY 框架是如何管理 TTY 设备的,以及如何利用提供的编程接口开发一个 TTY 驱动。
二、 关键数据结构
注4:阅读本章内容时可对照callme_friend画的的TTY个数据结构的关系图[3]以加深理解。
2.1 TTY device
Linux TTY framework的核心功能,就是管理TTY设备,以方便应用程序使用。于是,问题来了,Linux kernel是怎么抽象TTY设备的呢?答案很尴尬,kernel并不认为TTY device是一个设备,这很好理解:
比如,熟悉的串口终端,串口控制器(serial controller)是一个实实在在的硬件设备,一个控制器可以支持多个串口(serial port),软件在串口上收发数据,就相当于在驱动“串口终端”。此处的TTY device,就是从串口控制器中抽象出来的一个数据通道;
再比如,常用的网络终端,只有以太网控制器(或者WLAN控制器)是实实在在的设备,sshd等服务进程,会基于网络socket,虚拟出来一个数据通道,软件在这个通道上收发数据,就相当于在驱动“网络终端”。
因此,从kernel的角度看,TTY device就是指那些“虚拟的数据通道”。
另外,由于TTY driver在linux kernel中出现的远比设备模型早,所以在TTY framework中,没有特殊的数据结构用于表示TTY设备。当然,为了方便,kernel从设备模型和字符设备两个角度对它进行了抽象:
1)设备模型的角度
为每个“数据通道”注册了一个stuct device,以便可以在sysfs中体现出来,例如:
/sys/class/tty/tty
/sys/class/tty/console
/sys/class/tty/ttyS0
2)字符设备的角度
为每个“数据通道”注册一个struct cdev,以便在用户空间可以访问,例如:
/dev/tty
/dev/console
/dev/ttyS0
2.2 TTY driver
从当前设备模型的角度看,TTY framework有点奇怪,它淡化了device的概念,却着重突出driver。由struct tty_driver所代表的TTY driver,几乎大包大揽了TTY device有关的所有内容,如下:
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;
/*
* Driver methods
*/
const struct tty_operations *ops;
struct list_head tty_drivers;
}
原则上来说,在编写TTY driver的时候,只需要定义一个struct tty_driver变量,并根据实际情况正确填充其中的字段后,注册到TTY core中,即可完成驱动的设计。
1)需要TTY driver关心的字段
driver_name,该TTY driver的名称,在软件内部使用;
name,该TTY driver所驱动的TTY devices的名称,会体现到sysfs以及/dev/等文件系统下;
major、minor_start,该TTY driver所驱动的TTY devices的在字符设备中的主次设备号。因为一个tty driver可以支持多个tty device,因此次设备号只指定了一个start number;
num,该driver所驱动的tty device的个数,可以在tty driver注册的时候指定,也可以让TTY core自行维护,具体由TTY_DRIVER_DYNAMIC_DEV flag决定(可参考“”中的介绍);
type、subtype,TTY driver的类型,具体可参考“include/linux/tty_driver.h”中的定义;
init_termios,初始的termios;
flags,可参考2.6小节的介绍;
ops,tty driver的操作函数集;
driver_state,可存放tty driver的私有数据。
2)内部使用的字段
ttys,一个struct tty_struct类型的指针数组;
ports,一个struct tty_port类型的指针数组;
termios,一个struct ktermios类型的指针数组。
2.3 TTY struct
TTY struct是TTY设备在TTY core中的内部表示。
从TTY driver的角度看,它和文件句柄的功能类似,用于指代某个TTY设备。
从TTY core的角度看,它是一个比较复杂的数据结构,保存了TTY设备生命周期中的很多中间变量,如:
include/linux/tty.h
struct tty_struct {
struct kref kref;
int index;
struct device *dev;
struct tty_driver *driver;
struct tty_port *port;
const struct tty_operations *ops;
struct tty_ldisc *ldisc;
struct ld_semaphore ldisc_sem;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
struct ktermios termios, termios_locked;
char name[64];
unsigned long flags;
int count;
unsigned int receive_room;
struct winsize winsize;
struct {
spinlock_t lock;
bool stopped;
bool tco_stopped;
unsigned long unused[0];
} __aligned(sizeof(unsigned long)) flow;
struct {
struct pid *pgrp;
struct pid *session;
spinlock_t lock;
unsigned char pktstatus;
bool packet;
unsigned long unused[0];
} __aligned(sizeof(unsigned long)) ctrl;
bool hw_stopped;
bool closing;
int flow_change;
struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock;
int write_cnt;
unsigned char *write_buf;
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
struct work_struct SAK_work;
} __randomize_layout;
dev,该设备的struct device指针;
driver,该设备的struct tty_driver指针;
ops,该设备的tty操作函数集指针;
index,该设备的编号(如tty0、tty1中的0、1);
一些用于同步操作的mutex锁、spinlock锁、读写信号量等;
一些等待队列;
write buffer有关的信息;
port,该设备对应的struct tty_port等等。
由于编写TTY driver的时候不需要特别关心struct tty_struct的内部细节。
2.4 TTY port
在TTY framework中TTY port是一个比较难理解的概念,因为它和TTY struct类似,也是TTY device的一种抽象。那么,既然有了TTY struct,为什么还需要TTY port呢?先看一下kernel代码注释的解释:
include/linux/tty_port.h
struct tty_port {
struct tty_bufhead buf;
struct tty_struct *tty;
struct tty_struct *itty;
const struct tty_port_operations *ops;
const struct tty_port_client_operations *client_ops;
spinlock_t lock;
int blocked_open;
int count;
wait_queue_head_t open_wait;
wait_queue_head_t delta_msr_wait;
unsigned long flags;
unsigned long iflags;
unsigned char console:1;
struct mutex mutex;
struct mutex buf_mutex;
unsigned char *xmit_buf;
DECLARE_KFIFO_PTR(xmit_fifo, unsigned char);
unsigned int close_delay;
unsigned int closing_wait;
int drain_delay;
struct kref kref;
void *client_data;
};
TTY struct是TTY设备的“动态抽象”,保存了TTY设备访问过程中的一些临时信息,这些信息是有生命周期的:从打开TTY设备开始,到关闭TTY设备结束;
TTY port是TTY设备固有属性的“静态抽象”,保存了该设备的一些固定不变的属性值,例如是否是一个控制台设备(console)、打开关闭时是否需要一些delay操作、等等;
另外(这一点很重要),TTY core负责的是逻辑上的抽象,并不关心这些固有属性。因此从层次上看,这些属性完全可以由具体的TTY driver自行维护;
不过,由于不同TTY设备的属性有很多共性,如果每个TTY driver都维护一个私有的数据结构,将带来代码的冗余。所以TTY framework就将这些共同的属性抽象出来,保存在struct tty_port数据结构中,同时提供一些通用的操作接口,供具体的TTY driver使用;
因此,总结来说:TTY struct是TTY core的一个数据结构,由TTY core提供并使用,必要的时候可以借给具体的TTY driver使用;TTY port是TTY driver的一个数据结构,由TTY core提供,由具体的TTY driver使用,TTY core完全不关心。
2.5 Termios
说实话,在Unix/Linux的世界中,终端(terminal)编程是一个非常繁琐的事情,为了改善这种状态,特意制订了符合POSIX规范的应用程序编程接口,称作POSIX terminal interface[3]。POSIX terminal interface操作的对象,就是名称为termios的数据结构(在用户空间为struct termios,内核空间为struct ktermios)。以kernel中的struct ktermios为例,其定义如下:
include/uapi/asm-generic/termbits.h
struct ktermios {
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
c_cflag,可以控制TTY设备的一些特性,例如data bits、parity type、stop bit、flow control等(例如串口设备中经常提到的8N1);
c_ispeed、c_ospeed,可以分别控制TTY设备输入和输出的速度(例如串口设备中的波特率);
其它,暂不介绍。
2.6 tty driver flags
TTY driver在注册struct tty_driver变量的时候,可以提供一些flags,以告知TTY core一些额外的信息,例如:
include/linux/tty_driver.h
struct tty_driver {
struct kref kref;
struct cdev **cdevs;
struct module *owner;
const char *driver_name;
const char *name;
int name_base;
int major;
int minor_start;
unsigned int num;
short type;
short subtype;
struct ktermios init_termios;
unsigned long flags;
struct proc_dir_entry *proc_entry;
struct tty_driver *other;
/*
* Pointer to the tty data structures
*/
struct tty_struct **ttys;
struct tty_port **ports;
struct ktermios **termios;
void *driver_state;
/*
* Driver methods
*/
const struct tty_operations *ops;
struct list_head tty_drivers;
} __randomize_layout;
TTY_DRIVER_DYNAMIC_DEV:如果设置了该flag,则表示TTY driver会在需要的时候,自行调用tty_register_device接口注册TTY设备(相应地回体现在字符设备以及sysfs中);如果没有设置,TTY core会在tty_register_driver时根据driver->num信息,自行创建对应的TTY设备。
2.7 TTY操作函数集
TTY core将和硬件有关的操作,抽象、封装出来,形成名称为struct tty_operations的数据结构,具体的TTY driver不需要关心具体的业务逻辑,只需要根据实际的硬件情况,实现这些操作接口即可。
include/linux/tty_driver.h
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
ssize_t (*write)(struct tty_struct *tty, const u8 *buf, size_t count);
int (*put_char)(struct tty_struct *tty, u8 ch);
void (*flush_chars)(struct tty_struct *tty);
unsigned int (*write_room)(struct tty_struct *tty);
unsigned int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, const struct ktermios *old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
int (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
int (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *m, void *driver);
} __randomize_layout;
说明:
这些操作函数的操作对象,基本上都是struct tty_struct类型的指针,这也印证了我们在2.3中所说的----TTY struct是TTY设备的操作句柄;
当然,具体的TTY driver,可以从struct tty_struct指针中获取足够多的有关该TTY设备的信息,例如TTY port等;
TTY core会通过“.write“接口,将输出信息送给终端设备并显示。因此具体的TTY driver需要实现该接口,并通过硬件操作将数据送出;
既然有“.write“接口,为什么没有相应的read接口?TTY设备上的输入信息,怎么经由TTY core送给Application呢?
三、提供的用于编写TTY driver的API
在提供了一系列的数据结构的同时,TTY framework向下封装了一些API,以方便TTY driver的开发,具体如下。
3.1 TTY driver有关的API
用于struct tty_driver数据结构的分配、初始化、注册等:
/* include/linux/tty_driver.h */
extern struct tty_driver *__tty_alloc_driver(unsigned int lines,
struct module *owner, unsigned long flags);
extern void put_tty_driver(struct tty_driver *driver);
extern void tty_set_operations(struct tty_driver *driver,
const struct tty_operations *op);
extern struct tty_driver *tty_find_polling_driver(char *name, int *line);
extern void tty_driver_kref_put(struct tty_driver *driver);
/* Use TTY_DRIVER_* flags below */
#define tty_alloc_driver(lines, flags) \
__tty_alloc_driver(lines, THIS_MODULE, flags)
/* include/linux/tty.h */
extern int tty_register_driver(struct tty_driver *driver);
extern int tty_unregister_driver(struct tty_driver *driver);
tty_alloc_driver,分配一个struct tty_driver指针,并初始化那些不需要driver关心的字段:
lines,指明该driver最多能支持多少个设备,TTY core会根据该参数,分配相应个数的ttys、ports、termios数组;
flags,请参考2.6小节的说明;
tty_set_operations,设置TTY操作函数集;
tty_register_driver,将TTY driver注册给TTY core。
3.2 TTY device有关的API
如果TTY driver设置了TTY_DRIVER_DYNAMIC_DEV flag,就需要自行注册TTY device,相应的API包括:
include/linux/tty_driver.h
extern struct device *tty_register_device(struct tty_driver *driver,
unsigned index, struct device *dev);
extern struct device *tty_register_device_attr(struct tty_driver *driver,
unsigned index, struct device *device,
void *drvdata,
const struct attribute_group **attr_grp);
extern void tty_unregister_device(struct tty_driver *driver, unsigned index);
tty_register_device,分配并注册一个TTY device,最后将新分配的设备指针返回给调用者:
driver,对应的TTY driver;
index,该TTY设备的编号,它会决定该设备在字符设备中的设备号,以及相应的设备名称,例如/dev/ttyS0中的‘0’;
dev,可选的父设备指针 ;
tty_register_device_attr,和tty_register_device类似,只不过可以额外指定设备的attribute。
3.3 数据传输有关的API
当TTY core有数据需要发送给TTY设备时,会调用TTY driver提供的.write或者.put_char回调函数,TTY driver在这些回调函数中操作硬件即可。
当TTY driver从TTY设备收到数据并需要转交给TTY core的时候,需要调用TTY buffer有关的接口,将数据保存在缓冲区中,并等待Application读取,相关的API有:
include/linux/tty_flip.h
/**
* tty_insert_flip_char - add one character to the tty buffer
* @port: tty port
* @ch: character
* @flag: flag byte
*
* Queue a single byte @ch to the tty buffering, with an optional flag.
*/
static inline size_t tty_insert_flip_char(struct tty_port *port, u8 ch, u8 flag)
{
struct tty_buffer *tb = port->buf.tail;
int change;
change = !tb->flags && (flag != TTY_NORMAL);
if (!change && tb->used < tb->size) {
if (tb->flags)
*flag_buf_ptr(tb, tb->used) = flag;
*char_buf_ptr(tb, tb->used++) = ch;
return 1;
}
return __tty_insert_flip_string_flags(port, &ch, &flag, false, 1);
}
static inline size_t tty_insert_flip_string(struct tty_port *port,
const u8 *chars, size_t size)
{
return tty_insert_flip_string_fixed_flag(port, chars, TTY_NORMAL, size);
}
四、 TTY driver的编写步骤
步骤 1:实现 TTY 设备的操作函数集
在 Linux 中,TTY 驱动通过 struct tty_operations
来定义操作,包括设备打开、关闭、读写、配置等操作。你需要为每个操作编写一个相应的函数,然后将这些函数保存到 struct tty_operations
结构中。
#include <linux/tty.h>
#include <linux/fs.h>
static int my_tty_open(struct tty_struct *tty, struct file *file) {
printk(KERN_INFO "Opening TTY device\n");
return 0; // 成功
}
static void my_tty_close(struct tty_struct *tty, struct file *file) {
printk(KERN_INFO "Closing TTY device\n");
}
static ssize_t my_tty_read(struct file *file, char __user *buf, size_t count) {
printk(KERN_INFO "Reading from TTY device\n");
return count; // 返回读取的字节数
}
static ssize_t my_tty_write(struct file *file, const char __user *buf, size_t count) {
printk(KERN_INFO "Writing to TTY device\n");
return count; // 返回写入的字节数
}
static struct tty_operations my_tty_ops = {
.open = my_tty_open,
.close = my_tty_close,
.read = my_tty_read,
.write = my_tty_write,
};
步骤 2:分配 TTY driver 并设置 struct tty_operations
变量
使用 tty_alloc_driver()
来分配一个 TTY 驱动结构 tty_driver
,并根据需要设置该结构的字段,包括注册的操作函数集 my_tty_ops
。
static struct tty_driver *my_tty_driver;
static int __init my_tty_driver_init(void) {
int ret;
// 分配 tty_driver 结构
my_tty_driver = tty_alloc_driver(1, GFP_KERNEL); // 1 表示驱动支持的设备数
if (!my_tty_driver) {
printk(KERN_ERR "Failed to allocate TTY driver\n");
return -ENOMEM;
}
// 设置 tty_driver 结构
my_tty_driver->driver_name = "my_tty";
my_tty_driver->name = "ttyMy"; // 设备名称
my_tty_driver->major = 240; // 主设备号
my_tty_driver->minor_start = 0; // 子设备号起始值
my_tty_driver->type = TTY_DRIVER_TYPE_SYSTEM; // 驱动类型
my_tty_driver->subtype = TTY_DRIVER_MODEM; // 驱动子类型
my_tty_driver->init_termios = tty_std_termios; // 默认终端配置
my_tty_driver->ops = &my_tty_ops; // 设置操作函数集
// 注册 tty 驱动
ret = tty_register_driver(my_tty_driver);
if (ret) {
printk(KERN_ERR "Failed to register TTY driver\n");
tty_free_driver(my_tty_driver);
return ret;
}
printk(KERN_INFO "TTY driver registered\n");
return 0;
}
步骤 3:将 TTY 驱动注册到内核
如上所示,调用 tty_register_driver()
来注册 TTY 驱动到内核中。这个过程会使得该驱动能够管理 TTY 设备,并允许用户空间程序访问 TTY 设备。
ret = tty_register_driver(my_tty_driver); // 注册 TTY 驱动
if (ret) {
printk(KERN_ERR "Failed to register TTY driver\n");
tty_free_driver(my_tty_driver);
return ret;
}
步骤 4:动态注册 TTY 设备
在 Linux 中,设备可以是静态注册的(在驱动初始化时完成),也可以是动态注册的。动态注册的方式允许在运行时创建 TTY 设备。你可以调用 tty_register_device()
或 tty_register_device_attr()
来动态注册设备。
static int __init my_tty_device_init(void) {
struct tty_device *tty_dev;
int ret;
// 创建和注册设备
ret = tty_register_device(my_tty_driver, 0, NULL); // 0 表示设备编号
if (ret) {
printk(KERN_ERR "Failed to register TTY device\n");
return ret;
}
printk(KERN_INFO "TTY device registered\n");
return 0;
}
tty_register_device()
将 TTY 设备与已经注册的 TTY 驱动进行关联。可以根据实际需求调整设备的数量和参数。
步骤 5:接收和发送数据
当设备接收到数据时,你需要将数据交给 TTY core 进行处理。TTY core 会调用你提供的回调函数来发送数据。这是通过 tty_insert_flip_char()
或 tty_insert_flip_string()
实现的。
- 接收数据:在你的驱动程序中,你需要调用
tty_insert_flip_char()
或tty_insert_flip_string()
来将数据从硬件传输到 TTY core。这些函数将数据插入到 TTY 的缓冲区,并触发相应的回调。
static void my_receive_data(struct tty_struct *tty, const unsigned char *data, int length) {
int i;
// 将数据交给 TTY core
for (i = 0; i < length; i++) {
tty_insert_flip_char(tty, data[i], TTY_NORMAL); // 插入字符数据
}
tty_flip_buffer_push(tty); // 推送数据到用户空间
}
- 发送数据:当需要从 TTY 发送数据到硬件时,TTY core 会调用你的驱动的
write
回调函数(即my_tty_write
)。
static ssize_t my_tty_write(struct file *file, const char __user *buf, size_t count) {
struct tty_struct *tty = file->private_data;
int i;
for (i = 0; i < count; i++) {
// 从用户空间数据写入硬件
my_send_char_to_hardware(buf[i]);
}
return count;
}
驱动卸载
当模块卸载时,记得注销 TTY 驱动并释放相关资源:
static void __exit my_tty_driver_exit(void) {
tty_unregister_driver(my_tty_driver); // 注销 TTY 驱动
tty_free_driver(my_tty_driver); // 释放 TTY 驱动资源
printk(KERN_INFO "TTY driver unregistered\n");
}
module_init(my_tty_driver_init);
module_exit(my_tty_driver_exit);