Linux TTY framework(4)_TTY driver
作者:wowo 发布于:2016-10-25 22:40 分类:TTY子系统
1. 前言
本文将从驱动工程师的角度去看TTY framework:它怎么抽象、管理各个TTY设备?它提供了哪些编程接口以方便TTY driver的开发?怎么利用这些接口编写一个TTY driver?等等。
注1:话说介绍各个framework的时候,我一直比较喜欢用provider、consumer等概念,因为非常生动、易懂。不过在TTY framework的官方俗语中,压根没有provider、consumer等概念,为了不混淆试听,就算了吧。
注2:TTY framework在Linux kernel中算得上一个比较繁琐、庞杂的framework了,再加上现在很少有人会直接去写一个TTY driver,因此本文只是介绍一些概念性的东西,以加深对TTY及其driver的理解,为后续学习serial framework打基础。一些细节的东西,大家可参考callme_friend同学写的"TTY驱动分析[2]”,特别是其中的一些图示,很清晰!
注3:本文所使用的kernel版本为“X Project”初始的“Linux 4.6-rc5”版本。
2. 关键数据结构
注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的概念(参考2.1的介绍),却着重突出driver。由struct tty_driver所代表的TTY driver,几乎大包大揽了TTY device有关的所有内容,如下:
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中,即可完成驱动的设计。当然,我们不需要关心struct tty_driver中的所有字段,下面我们捡一些重点的字段一一说明。
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,可参考2.5小节的介绍;
flags,可参考2.6小节的介绍;
ops,tty driver的操作函数集,可参考2.7小节的介绍;
driver_state,可存放tty driver的私有数据。
2)内部使用的字段
ttys,一个struct tty_struct类型的指针数组,可参考2.3小节的介绍;
ports,一个struct tty_port类型的指针数组,可参考2.4小节的介绍;
termios,一个struct ktermios类型的指针数组,可参考2.5小节的介绍。
2.3 TTY struct(struct tty_struct)
TTY struct是TTY设备在TTY core中的内部表示。
从TTY driver的角度看,它和文件句柄的功能类似,用于指代某个TTY设备。
从TTY core的角度看,它是一个比较复杂的数据结构,保存了TTY设备生命周期中的很多中间变量,如:
dev,该设备的struct device指针;
driver,该设备的struct tty_driver指针;
ops,该设备的tty操作函数集指针;
index,该设备的编号(如tty0、tty1中的0、1);
一些用于同步操作的mutex锁、spinlock锁、读写信号量等;
一些等待队列;
write buffer有关的信息;
port,该设备对应的struct tty_port(可参考2.4小节的介绍)
等等。
由于编写TTY driver的时候不需要特别关心struct tty_struct的内部细节,这里不再详细介绍。
2.4 TTY port(struct tty_port)
在TTY framework中TTY port是一个比较难理解的概念,因为它和TTY struct类似,也是TTY device的一种抽象。那么,既然有了TTY struct,为什么还需要TTY port呢?先看一下kernel代码注释的解释:
/* include/linux/tty.h */
/*
* Port level information. Each device keeps its own port level information
* so provide a common structure for those ports wanting to use common support
* routines.
*
* The tty port has a different lifetime to the tty so must be kept apart.
* In addition be careful as tty -> port mappings are valid for the life
* of the tty object but in many cases port -> tty mappings are valid only
* until a hangup so don't use the wrong path.
*/
我的理解是:
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(struct ktermios)
说实话,在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中的定义和注释,写的很清楚):
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不需要关心具体的业务逻辑,只需要根据实际的硬件情况,实现这些操作接口即可。
听着还挺不错啊(framework的中心思想一贯如此,大家牢记即可),不过不能高兴的太早,看过这个数据结构之后,估计心会凉半截:
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, 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);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
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, 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 (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
#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
const struct file_operations *proc_fops;
};
这也太复杂了吧?确实如此,好在终端设备正在慢慢的退出历史舞台,看这篇文章的同学们,可能这一辈子都不会直接去写一个TTY driver。所以,这里我也不打算详细介绍,仅仅出于理解TTY framework的目的,做如下说明:
这些操作函数的操作对象,基本上都是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驱动分析[2]”,本文不再详细介绍了。
3. 提供的用于编写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.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 */
static inline int tty_insert_flip_char(struct tty_port *port,
unsigned char ch, char flag)
{
….
}static inline int tty_insert_flip_string(struct tty_port *port,
const unsigned char *chars, size_t size)
{
return tty_insert_flip_string_fixed_flag(port, chars, TTY_NORMAL, size);
}
注5:本文没有涉及TTY buffer、TTY flip有关的知识,大家知道有这么回事就行了。
4. TTY driver的编写步骤
说实话,TTY driver的编写步骤,就和“把大象放进冰箱”一样“简单”:
步骤1:实现TTY设备有关的操作函数集,并保存在一个struct tty_operations变量中。
步骤2:调用tty_alloc_driver分配一个TTY driver,并根据实际情况,设置driver中的字段(包括步骤1中的struct tty_operations变量)。
步骤3:调用tty_register_driver将driver注册到kernel。
步骤4:如果需要动态注册TTY设备,在合适的时机,调用tty_register_device或者tty_register_device_attr,向kernel注册TTY设备。
步骤5:接收到数据时,调用tty_insert_flip_string或者tty_insert_flip_char将数据交给TTY core;TTY core需要发送数据时,会调用driver提供的回调函数,在那里面访问硬件送出数据即可。
好吧,相信任何人看到上面的步骤之后,都没办法去写一个完整的TTY driver(我也是)。好在这是一个幸福的时代,我们很少需要直接去写这样的一个driver。那么本文的目的又是什么呢?权当是一个科普吧,如果你真的打算去写一个TTY driver,不至于无从下手。
5. 参考文档
[1] Linux TTY framework(2)_软件架构
[2] TTY驱动分析(链接失效,TODO)
[3] TTY各数据结构关系图,http://www.wowotech.net/content/uploadfile/201505/eb451430746679.gif((链接失效,TODO))
[4] POSIX terminal interface,https://en.wikipedia.org/wiki/POSIX_terminal_interface#CITEREFZlotnick1991
原创文章,转发请注明出处。蜗窝科技,www.wowotech.net。
标签: Linux Kernel 内核 driver tty tty_struct tty_port

评论:
2017-06-15 16:07
看了你的系列文章,受益匪浅,非常感谢!我最近调试终端碰到点问题想请教一下。忘不吝赐教。我现在使用HDMI输出,配置framebuffer console使信息输出到显示器,但是输出显示碰到一个问题就是显示器字符刷新慢,我通过键盘输入字符,字符不会马上在屏幕上显示完整,而是慢慢才会显示完整,感觉刷新很慢。通过
echo kkkkkkk > /dev/tty1,kkkkkkk也是慢慢才显示完整,请问你知道这会是哪方面的问题吗?非常感谢!
2017-06-15 18:27
因此,基于当前的分辨率、刷新率、fb的位数,你可以算算显示一个字符的时间(估计快不到哪里去)。
2017-04-10 11:50
2017-04-10 16:05
具体你可以查查termios的flag,例如:
http://man7.org/linux/man-pages/man3/tcsetattr.3.html
功能
最新评论
文章分类
随机文章
文章存档
- 2024年2月(1)
- 2023年5月(1)
- 2022年10月(1)
- 2022年8月(1)
- 2022年6月(1)
- 2022年5月(1)
- 2022年4月(2)
- 2022年2月(2)
- 2021年12月(1)
- 2021年11月(5)
- 2021年7月(1)
- 2021年6月(1)
- 2021年5月(3)
- 2020年3月(3)
- 2020年2月(2)
- 2020年1月(3)
- 2019年12月(3)
- 2019年5月(4)
- 2019年3月(1)
- 2019年1月(3)
- 2018年12月(2)
- 2018年11月(1)
- 2018年10月(2)
- 2018年8月(1)
- 2018年6月(1)
- 2018年5月(1)
- 2018年4月(7)
- 2018年2月(4)
- 2018年1月(5)
- 2017年12月(2)
- 2017年11月(2)
- 2017年10月(1)
- 2017年9月(5)
- 2017年8月(4)
- 2017年7月(4)
- 2017年6月(3)
- 2017年5月(3)
- 2017年4月(1)
- 2017年3月(8)
- 2017年2月(6)
- 2017年1月(5)
- 2016年12月(6)
- 2016年11月(11)
- 2016年10月(9)
- 2016年9月(6)
- 2016年8月(9)
- 2016年7月(5)
- 2016年6月(8)
- 2016年5月(8)
- 2016年4月(7)
- 2016年3月(5)
- 2016年2月(5)
- 2016年1月(6)
- 2015年12月(6)
- 2015年11月(9)
- 2015年10月(9)
- 2015年9月(4)
- 2015年8月(3)
- 2015年7月(7)
- 2015年6月(3)
- 2015年5月(6)
- 2015年4月(9)
- 2015年3月(9)
- 2015年2月(6)
- 2015年1月(6)
- 2014年12月(17)
- 2014年11月(8)
- 2014年10月(9)
- 2014年9月(7)
- 2014年8月(12)
- 2014年7月(6)
- 2014年6月(6)
- 2014年5月(9)
- 2014年4月(9)
- 2014年3月(7)
- 2014年2月(3)
- 2014年1月(4)
2017-06-19 13:50