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

2023-10-28

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

linux TTY子系统(2) -- 软件框架 的相关文章

随机推荐

  • java 基础-关键字 final修饰例子详解

    关于Java中的final final有三种使用场景 分别是修饰变量 方法和类 无论哪种修饰 一旦声明为final类型 你将不能改变这个引用了 编译器会检查代码 如果你试图再次初始化 编译器会报错 下面我来具体说说每一种修饰场景 1 修饰变
  • Istio 实现 ext-authz 外部扩展鉴权以及对接基于 k8s 的微服务

    Istio 实现 ext authz 外部扩展鉴权以及对接基于 k8s 的微服务 可以实现基于 redis 的 token 鉴权以及实现 rbac 鉴权 转载请注明来源 https janrs com vrsr Istio 的外部鉴权本质是
  • Android Studio安装Findbugs及生成报告

    安卓小白 安卓项目 客户要求提供代码静态检查结果 领导交代 可以使用Findbugs插件完成 以下为Android Studio中的安装使用步骤 Findbugs安装 1 Setting gt Plugins 在Marketplace下查找
  • 从全球座舱电子市场与产业看汽车级Linux(AGL)android-auto

    锋影 e mail 174176320 qq com 2016年全球汽车仪表市场规模大约77亿美元 比2015年增长9 预计到2020年汽车仪表市场规模达95亿美元 汽车仪表可以简单分为五大类 第一类简称D1 机械指针型 第二类D2 指针与
  • elementUI树状多选表格

    树状多选表格 需回显已选择的 可控制展开收起
  • 在线Java 动态运行Java源代码-执行器

    当我们通过类加载器获得Class后 就可以通过常用反射手段 调用类方法了 反射调用方法的要素 Class类 方法名 方法参数 方法返回值 Class类 已经通过前面的类加载器获取到了 方法名 需要调用的类中的methodName 通过cla
  • Qt中解决中文乱码的方法----编码

    如只是提供给本地用户使用 无需国际化 先调用下面两个函数之一 QTextCodec textc QTextCodec codecForName gbk QTextCodec textc QTextCodec codecForName utf
  • Ptyhon爬虫实战(七):爬取汽车公告网上的批次排量等信息

    网址 http www cn357 com notice 直接上代码 coding utf 8 import re import requests def getHtml url try page requests get url html
  • TCP三次握手四次断开

    转载地址 www 51niux com IP协议是网络层的主要协议 为上层传输层提供无连接 无状态 不可靠的服务 优点是简单高效 无状态是指各个IP报文是独立传送的 不同步传输状态的信息 所以容易发生重复和乱序的情况 不可靠是指IP协议不能
  • 编译 MXNet 模型

    本篇文章译自英文文档 Compile MXNet Models 作者是 Joshua Z Zhang Kazutaka Morita 更多 TVM 中文文档可访问 TVM 中文站 本文将介绍如何用 Relay 部署 MXNet 模型 首先安
  • java 部署普通部署jar 包脚本

    PORT 8888 PID lsof t i PORT if n PID then echo PORT PORT already use PID PID start stop 终止进程 kill 9 PID 检查进程是否终止成功 sleep
  • Kibana 安装(Windows)

    Kibana 安装 Windows 环境准备 下载 安装配置 启停 卸载 问题 最近因为工作需要 要对ElasticSearch 简称ES 中的一批数据做数据分析挖掘 找出数据中潜在的关系 以及部分数据的分布及趋势等 总的来说就是一个数据分
  • matlab画拟合直方图的脚本

    都是一些简单的作图参数 可以自己按照matlab官方的指导去改style https ww2 mathworks cn help stats histfit html Function Plot a histogram with fitti
  • 记一次vue2中使用keep-alive时导致的页面图表、数据紊乱的问题

    问题概述 当在组件中使用kepp alive时 keep alive中如果有其他的缓存页面 会导致从其他页 面跳转至缓存页面时造成页面数据紊乱 渲染紊乱的问题 主要出现在echarts图表样式上 问题触发或复显条件 多个页面被keep al
  • #vue# 【五】vue中文本长度超出显示省略号...及悬浮显示全部文本

    vue中字符串文本长度超出显示省略号 及悬浮显示全部文本 需求 显示不下的文本用省略号代替 并且鼠标悬停在存放文本的标签里面时 即系悬浮 会有一个div显示该单元格的全部文本信息 思路 1 在需要设置的文本标签处 加入定宽 多出内容隐藏 设
  • WSL安装和配置

    WSL的安装和配置 一 什么是WSL 二 WSL的安装 1 Linux子系统安装环境配置 2 安装Linux发行版 3 安装WIndows终端 可选 4 待补充 wsl路径问题 三 WSL中使用adb 待补充 座位单独一篇文章 四 配置安装
  • react非受控组件useRef方法

    效果图如下 代码如下 div div
  • espcms5.7.13 sql注入漏洞复现

    espcms5 7 13 sql注入漏洞复现 作者 admin 时间 2021 06 28 分类 漏洞复现 使用代码审计工具自动审计 找到select语句 双击进入 adminsoft control citylist php文件可以看到
  • J-LINK 操作使用指南

    一 安装J LINK驱动 我们提供的驱动版本有v6 14d的版本 默认配置安装即可 安装完成后 将J LINK插入电脑在设备管理器中将会显示J LINK端口 二 固件下载及配置 J LINK安装完成后 进入J FLASH界面如下 配置J F
  • linux TTY子系统(2) -- 软件框架

    了解tty 子系统 1 TTY的子系统 在Linux kernel中 TTY就是各类终端 Terminal 的简称 为了简化终端的使用 以及终端驱动程序的编写 Linux kernel抽象出了TTY framework 对上 向应用程序提供