LinuxTTY 子系统3

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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值