linux TTY子系统(2) -- 软件框架

  • 了解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

猜你喜欢

转载自blog.csdn.net/weixin_41028621/article/details/109330542