- 了解tty 子系统
1.TTY的子系统
在Linux kernel中,TTY就是各类终端(Terminal)的简称。为了简化终端的使用,以及终端驱动程序的编写,Linux kernel抽象出了TTY framework:对上,向应用程序提供使用终端的统一接口;对下,提供编写终端驱动程序(如serial driver)的统一框架。TTY framework通过TTY core屏蔽TTY有关的技术细节,对上以字符设备的形式向应用程序提供统一接口。
软件框架所示:
1.1.TTY core是TTY framework的核心逻辑,功能包括:
- 以字符设备的形式,向用户空间提供访问TTY设备的接口,例如:
设备号(主, 次) 字符设备 备注
(5, 0) /dev/tty 控制终端(Controlling Terminal)
(5, 1) /dev/console 控制台终端(Console Terminal)
(4, 0) /dev/vc/0 or /dev/tty0 虚拟终端(Virtual Terminal)
(4, 1) /dev/vc/1 or /dev/tty1 同上
… … …
(x, x) /dev/ttyS0 串口终端(名称和设备号由驱动自行决定)
… … …
(x, x) /dev/ttyUSB0 USB转串口终端
… … …
- 通过设备模型中的struct device结构抽象TTY设备,并通过struct tty_driver抽象该设备的驱动,并提供相应的register接口。TTY驱动程序的编写,简化为填充并注册相应的struct tty_driver结构。
注:TTY framework弱化了TTY设备的概念,通常情况下,可以在注册TTY驱动的时候,自动分配并注册TTY设备。
- 使用struct tty_struct、struct tty_port等数据结构,从逻辑上抽象TTY设备及其“组件”,以实现硬件无关的逻辑。
- 抽象出名称为线路规程(Line Disciplines)的模块,在向TTY硬件发送数据之前,以及从TTY设备接收数据之后,进行相应的处理(如特殊字符的转换等)。
1.2 System Console Core
Linux kernel的system console主要有两个功能:
-
向系统提供控制台终端(Console Terminal) ,以便让用户登录进行交互操作。
-
提供printk功能,以便kernel代码进行日志输出。
System console core模块使用struct console结构抽象system console功能,具体的driver不需要关心console的内部逻辑,填充该接口并注册给kernel即可。
1.3 TTY Line Disciplines

线路规程(Line Disciplines)在TTY framework中是一个非常优雅的设计,可以把它看成设备驱动和应用接口之间的一个适配层。从字面意思理解,就是辅助TTY driver,将我们通过TTY设备键入的字符转换成一行一行的数据,当然,实际情况远比这复杂,例如存在如下的Line Disciplines(以n_为前缀):
$ ls drivers/tty/n_*
drivers/tty/n_gsm.c drivers/tty/n_r3964.c drivers/tty/n_tracesink.c drivers/tty/n_tty.c
drivers/tty/n_hdlc.c drivers/tty/n_tracerouter.c drivers/tty/n_tracesink.h
1.4 TTY Drivers以及System Console Drivers
最后,对内核以及驱动工程师来说,更关注的还是具体的TTY设备驱动。主要的TTY driver有两类:
-
虚拟终端(Virtual Terminal,VT)驱动,位于drivers/tty/vt中,负责实现VT有关的功能。
-
串口终端驱动,也即serial subsystem,位于drivers/tty/serial中。
2.内部结构
计算机为了支持这些teletype,于是设计了名字叫做TTY的子系统,内部结构如下:
- UART driver对接外面的UART设备。
- Line discipline主要是对输入和输出做一些处理,可以理解它是TTY driver的一部分。
大多数用户都会在输入时犯错,所以退格键会很有用。这当然可以由应用程序本身来实现,但是根据UNIX设计“哲学”,应用程序应尽可能保持简单。为了方便起见,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),这些命令在行规范(line discipline)内默认启用。高级应用程序可以通过将行规范设置为原始模式(raw mode)而不是默认的成熟或准则模式(cooked and canonical)来禁用这些功能。大多数交互程序(编辑器,邮件客户端,shell,及所有依赖curses或readline的程序)均以原始模式运行,并自行处理所有的行编辑命令。行规范还包含字符回显和回车换行(译者注:\r\n 和 \n)间自动转换的选项。如果你喜欢,可以把它看作是一个原始的内核级sed(1)。
另外,内核提供了几种不同的行规范。一次只能将其中一个连接到给定的串行设备。行规范的默认规则称为N_TTY(drivers/char/n_tty.c,如果你想继续探索的话)。其他的规则被用于其他目的,例如管理数据包交换(ppp,IrDA,串行鼠标),但这不在本文的讨论范围之内。 - TTY driver用来处理各种终端设备。
- 用户空间的进程通过TTY driver来和终端打交道。
用户可能想要同时运行多个程序,并且一次只与其中一个交互。如果一个程序进入无限循环,用户可能想要终止或挂起它。在后台启动的程序应该能够独立运行,直到它们尝试向终端写入(被挂起)。同样,用户的输入应该指向前台程序。对于这些功能,操作系统是在TTY驱动程序( TTY driver drivers/char/tty_io.c)中实现的。
在操作系统中,如果已经进程有执行上下文,我们就说它是“活着的”(有一个执行上下文),这也意味着它可以独立执行操作。而TTY驱动程序不是“活”的; 在面向对象的术语中,TTY驱动程序是被动对象(passive object)。它有一些数据字段和一些方法,但让它做某事的唯一方法是当它的某个方法从别的进程的上下文或内核中断处理程序中调用时。行规范(line discipline)同样是一个被动对象。
3.TTY设备
对于每一个终端,TTY driver都会创建一个TTY设备与它对应,如果有多个终端连接过来,那么看起来就是这个样子的:
当驱动收到一个终端的连接时,就会根据终端的型号和参数创建相应的tty设备(上图中设备名称叫ttyS0是因为大部分终端的连接都是串行连接),由于每个终端可能都不一样,有自己的特殊命令和使用习惯,于是每个tty设备的配置可能都不一样。比如按delete键的时候,有些可能是要删前面的字符,而有些可能是删后面的,如果没配置对,就会导致某些按键不是自己想要的行为,这也是我们在使用模拟终端时,如果默认的配置跟我们的习惯不符,需要做一些个性化配置的原因。
后来随着计算机的不断发展,teletype这些设备逐渐消失,我们不再需要专门的终端设备了,每个机器都有自己的键盘和显示器,每台机器都可以是其它机器的终端,远程的操作通过ssh来实现,但是内核TTY驱动这一架构没有发生变化,我们想要和系统中的进程进行I/O交互,还是需要通过TTY设备,于是出现了各种终端模拟软件,并且模拟的也是常见的几种终端,如VT100、VT220、XTerm等。
可以通过命令toe -a列出系统支持的所有终端类型,可以通过命令infocmp来比较两个终端的区别,比如infocmp vt100 vt220将会输出vt100和vt220的区别。
4.程序如何和TTY打交道
在讨论TTY设备是如何被创建及配置之前,我们先来看看TTY是如何被进程使用的:
#先用tty命令看看当前bash关联到了哪个tty
dev@debian:~$ tty
/dev/pts/1
#看tty都被哪些进程打开了
dev@debian:~$ lsof /dev/pts/1
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 907 dev 0u CHR 136,1 0t0 4 /dev/pts/1
bash 907 dev 1u CHR 136,1 0t0 4 /dev/pts/1
bash 907 dev 2u CHR 136,1 0t0 4 /dev/pts/1
bash 907 dev 255u CHR 136,1 0t0 4 /dev/pts/1
lsof 1118 dev 0u CHR 136,1 0t0 4 /dev/pts/1
lsof 1118 dev 1u CHR 136,1 0t0 4 /dev/pts/1
lsof 1118 dev 2u CHR 136,1 0t0 4 /dev/pts/1
通过上面的lsof可以看出,当前运行的bash和lsof进程的stdin(0u)、stdout(1u)、stderr(2u)都绑定到了这个TTY上。
5.上层访问分析
- drivers/char/mem.c
- drivers/tty/tty_io.c
5.1.tty 初始化
chr_dev_init->tty_init:
int __init tty_init(void)
{
cdev_init(&tty_cdev, &tty_fops);
if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
panic("Couldn't register /dev/tty driver\n");
device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), NULL, "tty");
cdev_init(&console_cdev, &console_fops);
if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
panic("Couldn't register /dev/console driver\n");
consdev = device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 1), NULL,
"console");
if (IS_ERR(consdev))
consdev = NULL;
else
WARN_ON(device_create_file(consdev, &dev_attr_active) < 0);
return 0;
}
在/dev/下生成console和tty两个设备结点,他们对应的fops分别是tty_fops和console_fops。
476 static const struct file_operations tty_fops = {
477 .llseek = no_llseek,
478 .read = tty_read,
479 .write = tty_write,
480 .poll = tty_poll,
481 .unlocked_ioctl = tty_ioctl,
482 .compat_ioctl = tty_compat_ioctl,
483 .open = tty_open,
484 .release = tty_release,
485 .fasync = tty_fasync,
486 .show_fdinfo = tty_show_fdinfo,
487 };
489 static const struct file_operations console_fops = {
490 .llseek = no_llseek,
491 .read = tty_read,
492 .write = redirected_tty_write,
493 .poll = tty_poll,
494 .unlocked_ioctl = tty_ioctl,
495 .compat_ioctl = tty_compat_ioctl,
496 .open = tty_open,
497 .release = tty_release,
498 .fasync = tty_fasync,
499 };
执行echo “peng” > /dev/ttyS*,会先调用tty_open然后调用tty_write,最后调用tty_release。
5.2.执行读写操作
- read
当在用户空间执行read操作时, 此处的read函数将会被调用. read的主要目的是把数据从内核空间返回给用户空间.
当硬件(例如串口)收到了数据之后, 会通过tty_port存储到tty line discipline里面. 这里的read操作就是从tty_ldisc获取数据, 然后返回给用户空间.
848 static ssize_t tty_read(struct file *file, char __user *buf, size_t count,
849 loff_t *ppos)
850 {
851 int i;
852 struct inode *inode = file_inode(file);
853 struct tty_struct *tty = file_tty(file);
854 struct tty_ldisc *ld;
855
856 if (tty_paranoia_check(tty, inode, "tty_read"))
857 return -EIO;
858 if (!tty || tty_io_error(tty))
859 return -EIO;
860
861 /* We want to wait for the line discipline to sort out in this
862 situation */
863 ld = tty_ldisc_ref_wait(tty);
864 if (!ld)
865 return hung_up_tty_read(file, buf, count, ppos);
866 if (ld->ops->read)
867 i = ld->ops->read(tty, file, buf, count); //重点
868 else
869 i = -EIO;
870 tty_ldisc_deref(ld);
871
872 if (i > 0)
873 tty_update_time(&inode->i_atime);
874
875 return i;
876 }
代码很简单, 调用ld->ops->read读取数据. 这里简单是因为的主要的逻辑都是在tty line discipline中处理的,
- write
当在用户空间执行write操作时, 此处的write函数将会被调用. write的主要目的是把数据从用户空间传递到内核空间, 然后通过硬件发送出去.write的逻辑也很简单, 收到用户空间的数据之后, 调用tty line discipline发送数据, tty line discipline会调用tty_driver->ops->write函数把数据通过硬件发送出去.
1024 static ssize_t tty_write(struct file *file, const char __user *buf,
1025 size_t count, loff_t *ppos)
1026 {
1027 struct tty_struct *tty = file_tty(file);
1028 struct tty_ldisc *ld;
1029 ssize_t ret;
1030
1031 if (tty_paranoia_check(tty, file_inode(file), "tty_write"))
1032 return -EIO;
1033 if (!tty || !tty->ops->write || tty_io_error(tty))
1034 return -EIO;
1035 /* Short term debug to catch buggy drivers */
1036 if (tty->ops->write_room == NULL)
1037 tty_err(tty, "missing write_room method\n");
1038 ld = tty_ldisc_ref_wait(tty);
1039 if (!ld)
1040 return hung_up_tty_write(file, buf, count, ppos);
1041 if (!ld->ops->write)
1042 ret = -EIO;
1043 else
1044 ret = do_tty_write(ld->ops->write, tty, file, buf, count);
1045 tty_ldisc_deref(ld);
1046 return ret;
1047 }
do_tty_write会调用ld->ops->write, ld->ops->write最终会调用tty_driver->ops->write函数把数据通过硬件发送出去.详细调用流程:
-->write()
-->file_operation.tty_write
-->do_tty_write(ld->ops->write, tty, file, buf, count) /* tty_ldisc->tty_ldisc_ops->write */
-->tty_ldisc_ops.ldisc_write
-->tty->driver->ops->write (tty, tbuf->buf, tbuf->count) /* tty_struct->tty_driver->tty_operations->write */
-->tty_operations.uart_write
-->uart_start(tty);
-->__uart_start(tty);
-->port->ops->start_tx(port); /* uart_port->uart_ops->start_tx */
-->uart_ops.serial8250_start_tx //硬件实现函数
1630 static void serial8250_start_tx(struct uart_port *port)
1631 {
1632 struct uart_8250_port *up = up_to_u8250p(port);
1633 struct uart_8250_em485 *em485 = up->em485;
1634
1635 serial8250_rpm_get_tx(up);
1636
1637 if (em485 &&
1638 em485->active_timer == &em485->start_tx_timer)
1639 return;
1640
1641 if (em485)
1642 start_tx_rs485(port);
1643 else
1644 __start_tx(port);
1645 }
至此消息也发送出去了,从消息流程可以看出来消息是经过ldisc线路规程层,然后tty层,然后到硬件驱动层。
5.3.tty line discipline
- tty_ldiscs[NR_LDISCS]
drivers/tty/tty_ldisc.c
/* Line disc dispatch table */
static struct tty_ldisc_ops *tty_ldiscs[NR_LDISCS];
其实就是一个全局结构体数组. 池子的大小是数组大小, 也就是NR_LDISCS.当用tty ldis子模块提供的API注册一个ldis时, 被注册的ldis就是存储在这个数组里面.
- struct tty_ldisc
struct tty_ldisc {
struct tty_ldisc_ops *ops;
struct tty_struct *tty;
};
这个结构体很简单, *tty是用来指向tty_struct结构体的, 主要是要实现tty_ldisc_ops这个结构体.
- tty_ldisc_ops
169 struct tty_ldisc_ops {
170 int magic;
171 char *name;
172 int num;
173 int flags;
174
175 /*
176 * The following routines are called from above.
177 */
178 int (*open)(struct tty_struct *);
179 void (*close)(struct tty_struct *);
180 void (*flush_buffer)(struct tty_struct *tty);
>>181 ssize_t (*read)(struct tty_struct *tty, struct file *file,
>>182 unsigned char __user *buf, size_t nr);
>>183 ssize_t (*write)(struct tty_struct *tty, struct file *file,
>>184 const unsigned char *buf, size_t nr);
185 int (*ioctl)(struct tty_struct *tty, struct file *file,
186 unsigned int cmd, unsigned long arg);
187 long (*compat_ioctl)(struct tty_struct *tty, struct file *file,
188 unsigned int cmd, unsigned long arg);
189 void (*set_termios)(struct tty_struct *tty, struct ktermios *old);
190 __poll_t (*poll)(struct tty_struct *, struct file *,
191 struct poll_table_struct *);
192 int (*hangup)(struct tty_struct *tty);
193
194 /*
195 * The following routines are called from below.
196 */
197 void (*receive_buf)(struct tty_struct *, const unsigned char *cp,
198 char *fp, int count);
199 void (*write_wakeup)(struct tty_struct *);
200 void (*dcd_change)(struct tty_struct *, unsigned int);
201 int (*receive_buf2)(struct tty_struct *, const unsigned char *cp,
202 char *fp, int count);
203
204 struct module *owner;
205
206 int refcount;
207 };
系统调用console_init(kernel/printk/printk.c)-> n_tty_init(); // Setup the default TTY line discipline
drivers/tty/n_tty.c:
2499 void __init n_tty_init(void)
2500 {
2501 tty_register_ldisc(N_TTY, &n_tty_ops);
2502 }
调用tty_register_ldisc 注册n_tty_ops.
2468 static struct tty_ldisc_ops n_tty_ops = {
2469 .magic = TTY_LDISC_MAGIC,
2470 .name = "n_tty",
2471 .open = n_tty_open,
2472 .close = n_tty_close,
2473 .flush_buffer = n_tty_flush_buffer,
2474 .read = n_tty_read,
2475 .write = n_tty_write,
2476 .ioctl = n_tty_ioctl,
2477 .set_termios = n_tty_set_termios,
2478 .poll = n_tty_poll,
2479 .receive_buf = n_tty_receive_buf,
2480 .write_wakeup = n_tty_write_wakeup,
2481 .receive_buf2 = n_tty_receive_buf2,
2482 };
接着读写分析:
具体细节看:http://www.wowotech.net/tty_framework/435.html