程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

linux驱动移植-tty架构

----------------------------------------------------------------------------------------------------------------------------
内核版本:linux 5.2.8
根文件系统:busybox 1.25.0
u-boot:2016.05
----------------------------------------------------------------------------------------------------------------------------

一、终端(tty)

1.1 回顾

还记的在Mini2440之uboot移植流程之linux内核启动分析(六)小节中,我们介绍过u-boot在启动linux之前,需要做一些准备工作,比如配置启动参数:

#define CONFIG_BOOTARGS        "root=/dev/mtdblock3 console=ttySAC0,115200 init=/linuxrc"

其中:

  • root 指定文件系统位置,这里指定为NAND三号分区;
  • init 指定内核启动后执行的第一个应用程序;
  • console 指定控制台使用哪个终端,计算机启动的时候,会将内核启动日志输出到控制台,这里的ttySAC0指的就是串口0;

console参数用来指定控制台使用哪个终端,控制台是与系统交互的设备,linux操作系统会接收来自控制台的输入,同时将信息输出到控制台上。

那终端又是什么?终端就是处理计算机输入输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入,典型的终端包括电传打印机、显示器键盘套件。

ttySAC0中tty是什么,tty是Teletype或Teletypewriter 的缩写,原来是指电传打字机,后来这种设备逐渐键盘和显示器取代。不管是电传打字机还是键盘显示器,都是作为计算机的终端设备存在的,所以tty也泛指计算机的终端(terminal)设备。

如果不指定console,默认设置为/dev/tty0,tty0表示当前所使用虚拟终端的一个别名。虚拟终端一共有6个,在文件系统中使用/dev/tty1~/dev/tty6表示,其输入来自键盘、同时将tty驱动的输出到显示器,他们实际上是共享同一个真实的物理控制台(也就是连接在计算机上的键盘和显示器套件)。

当然这里指定为ttySAC0,表示计算机启动的时候,内核启动日志会通过串口0输出,同时通过串口0接收用户的输入。

1.2 物理终端terminal

早期的终端(terminal) 是一台独立于计算机的机器(teleprinter即, tty),大概像下面的样子:

它终端通过线缆与计算机连接,作为输入和输出设备,将输入的数据发送到计算机, 并打印出响应。

 

在今天你很难想象程序的运行结果需要等到打印出来才能看到,teleprinter设备已经进了计算机博物馆。现在我们用tty代表计算机终端(terminal),只是沿用了历史习惯,电传打字机(teletypewriter)曾经是计算机的终端,它的缩写便是tty。

从历史上看,终端刚开始就是电传打印机,配有打印机,键盘,带有一个串口,通过串口传送数据到主机端,然后主机处理完交给终端打印出来。

为了把不同型号的电传打字机接入计算机,需要在操作系统内核安装驱动,为上层应用屏蔽所有的低层细节。

如上图所示,其中:

  • uart驱动:物理终端通过电缆连接到计算机上的 uart(通用异步接收器和发射器)。操作系统中有一个uart驱动程序用于管理字节的物理传输;
  • 行规范:上图中内核中的 Line discipline(行规范)用来提供一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),主要用来支持用户在输入时的行为(比如输错了,需要退格);
  • tty驱动:tty驱动用来进行会话管理,并且处理各种终端设备;

uart驱动、行规范和 tty驱动都位于内核中,它们的一端是终端设备,另一端是用户进程。因为在linux下所有的设备都是文件,所以它们三个加在一起被称为 "tty设备"。

1.3 控制台console

控制台的概念与终端含义非常相近,其实现在我们经常用它们表示相同的东西,但是在计算机的早期时代,它们确实是不同的东西。

一些数控设备(比如数控机床)的控制箱,通常会被称为控制台,顾名思义,控制台就是一个直接控制设备的面板,上面有很多控制按钮。

在计算机里,把那套直接连接在电脑上的键盘和显示器就叫做控制台。而终端是通过串口连接上的,不是计算机自身的设备,而控制台是计算机本身就有的设备,一个计算机只有一个控制台。

在linux系统中,/dev/console是系统控制台,是与操作系统交互的设备。计算机启动的时候,所有的信息都发送到该设备上

1.4 虚拟控制台

今天电传打字机已经进了博物馆,但linux挺仍然保留了当初tty驱动和Line discipline的设计和功能。终端不再是一个需要通过uart连接到计算机上物理设备。终端成为内核的一个模块,它可以直接向tty驱动发送字符,并从tty驱动读取响应然后打印到屏幕上。也就是说,用内核模块模拟物理终端设备,因此被称为终端模拟器(terminal emulator),或者说虚拟终端(virtual console)、在其他博客上看到的本地终端指的也是这个。 

上图是一个典型的Linux系统,虚拟终端就像过去的物理终端一样,它监听来自键盘的事件将其发送到tty驱动,并从tty驱动读取响应,通过显卡驱动将结果渲染到显示器上。tty驱动和Line discipline的行为与原先一样,但不再有UART和物理终端参与。

如何看到一个虚拟终端呢,由于在linux系统中,一切设备都是文件,虚拟终端也不例外。

/dev/tty1~/dev/tty6是这些虚拟终端在文件系统中的表示,由于这些虚拟终端的输入数据来自键盘、同时将tty驱动的输出到显示器,因此他们实际上是共享同一个真实的物理控制台。

假设你的linux系统没安装图形界面(或者默认不启用图形界面),当系统启动完成之后,你会在屏幕上看到一个文本模式的登录提示。这个界面就是虚拟终端的界面。我们可以通过CTRL+ALT+[F1~F6]组合键,在这几个虚拟终端之间切换,每切换到一个虚拟终端,这个虚拟终端就是当前的焦点终端:

该焦点终端会被内核记录为全局变量,这样只要有键盘输入,就会把输入的字符交给焦点终端。

系统中有没有什么变量可以表示焦点终端呢?当然有了,那就是/dev/console,不管你在哪里往/dev/console里写东西,这些东西总会出现在系统当前的焦点终端上!

那么系统中有没有一个叫做自己的全局变量呢?当然有,那就是/dev/tty,也就是说,无论你在哪个终端下工作,当你往/dev/tty里写东西的时候,它总是会马上出现在你的眼前。

1.5 伪终端pty

伪终端(pseudo terminal, pty)是终端的发展,为满足现在需求(比如网络登陆、xwindow窗口的管理)。它是成对出现的逻辑终端设备(即master和slave设备, 对master的操作会反映到slave上)。它是运行在用户态的终端模拟程序。

pty运行在用户态,更加安全和灵活,同时仍然保留了tty驱动和Line discipline的功能。常用的伪终端有 xterm,gnome-terminal,以及远程终端ssh。

注意:X11也叫做X Window系统,是图形化窗口管理系统 。它是诞生于Unix 、以及 OpenVMS,是传统上Unix环境中建立图形用户界面的标准工具包和协议。linux操作系统下的图形管理界面(GNOME、KDE)也是基于X11运行库基础上开发的。在图例中,X窗口系统从键盘、鼠标获取输入信息,之后将输入反馈显示于屏幕,同时把输入输出转交给伪终端(这样看来,伪终端实际上就是一个应用层的程序,类似于我们的浏览器等软件)。

pty是通过打开特殊的设备文件/dev/ptmx 创建,由一对双向的字符设备构成,称为pty master 和pty slave。其中:

  • pty slave对应文件系统/dev/pts/目录下的一个文件;
  • pty master则在内存中标识为一个文件描述符(fd);

我们以ubuntu桌面版提供的gnome-terminal为例,介绍伪终端如何与tty驱动交互。当我们在ubuntu桌面启动gnome-terminal时:

  • gnome-terminal通过打开/dev/ptmx文件创建了一个伪终端的master和slave对,它持有pty master的文件描述符/dev/ptmx,并把GUI绘制在显示器上;
  • gnome-terminal会fork一个shell子进程bash,并让bash持有pty slave的设备文件/dev/pts/[n],bash 的标准输入、标准输出和标准错误都设置为pty slave;
  • gnome-terminal 负责监听键盘事件,并将输入的字符发送到pty master;
  • Line discipline收到字符,进行缓冲。只有按下回车键时,它才会把缓冲的字符复制到pty slave;
    • gnome-terminal只会在屏幕上显示来自pty master的东西。因此,Line discipline 需要回传字符,以便让你看到你刚刚输入的内容;
  • 当按下回车键时,tty驱动负责将缓冲的数据复制到pty slave;
  • bash从标准输入读取输入的字符(例如 ls -l )。注意,bash 在启动时已经将标准输入被设置为了pty slave;
  • bash解释从输入读取的字符,发现需要运行ls;
  • bash fork出ls进程。bash fork出的进程拥有和bash相同的标准输入、标准输出和标准错误,也就是pty slave;
  • ls运行,结果打印到标准输出,也就是pty slave;
  • tty驱动将字符复制到pty  master;
  • gnome-terminal循环从pty master读取字节,绘制到用户界面上;

注意:如果我们采用cat /dev/pts/xx去读取pty salve,实际上也是可以将Line discipline缓冲区数据读取出来的,不过这样会产生竞争关系,后面的示例会说明。同理采用echo "hello slave" > /dev/pts/xx去写入pty slave,实际上也是可以将数据写入到Line discipline缓冲区。

在gnome-terminal中执行tty命令,可以查看目前使用的终端机的文件名称:

zhengyang@zhengyang:~$ tty
/dev/pts/6 

 执行 ps -l (用来查看自己的bash相关的进程)命令,也可以确认shell关联的伪终端是 pts/6:

zhengyang@zhengyang:~$ ps -l
F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  91950  91944  0  80   0 -  6128 wait   pts/6    00:00:00 bash
0 R  1000 101980  91950  0  80   0 -  7664 -      pts/6    00:00:00 ps

注意到tty这一列指出了当前进程的终端是pts/6。

1.5.1 /dev/ptmx

/dev/ptmx是一个字符设备文件,当进程打开/dev/ptmx文件时,进程会同时获得一个指向 pseudoterminal master(ptm)的文件描述符和一个在/dev/pts目录中创建的 pseudoterminal slave(pts) 设备。

通过打开/dev/ptmx 文件获得的每个文件描述符都是一个独立的ptm,它有自己关联的pts,ptmx(可以认为内存中有一个ptmx对象)在内部会维护该文件描述符和pts的对应关系,对这个文件描述符的读写都会被ptmx转发到对应的 pts。

我们可以通过lsof命令查看ptmx打开的文件描述符:

1.5.2 shell

前面我们所说的terminal,本质上是基于文本的输入输出机制,它并不理解具体的命令及其语法。

于是就需要引入shell这个玩意,shell 负责解释你输入的命令,并根据你输入的命令,执行某些动作(包括:启动其它进程)。

shell是用户空间的应用程序,通常由terminal fork出来,是 terminal 的子进程。shell用来提示用户输入,解释用户输入的字符,然后处理来自底层操作系统的输出。

通常我们使用较多的shell有bash、csh、flsh、zsh和ksh。如今影响力最大的shell是bash(没有之一)。其名称源自“Bourne-again shell”,是GNU社区对Bourne shell的重写,使之符合自由软件(GPL 协议)。

假设你想看一下/home这个目录下有哪些子目录,可以在shell 中运行了如下命令

ls /home

当你输入这串命令并敲回车键,shell会拿到这一行,然后它会分析出,空格前面的ls是一个外部命令,空格后面的/home是该命令的参数。然后shell会启动这个外部命令对应的进程,并把上述参数作为该进程的启动参数。

刚才提到了【外部命令】这个词汇,顺便解释一下:通俗地说,“内部命令”就是内置在shell中的命令;而“外部命令”则对应了某个具体的【可执行文件】。

当你在shell中执行“外部命令”,shell会启动对应的可执行文件,从而创建出一个“子进程”;而如果是“内部命令”,就【不】产生子进程。

那么,如何判断某个命令是否为“外部命令”?
比较简单的方法是——用如下方式来帮你查找。如果某个命令能找到对应的可执行文件,就是“外部命令”;反之则是“内部命令”。

whereis 命令名称
1.5.3 配置tty设备

内核使用tty驱动来处理terminal和shell之间的通信。Line discipline是tty的一个逻辑组件。Line discipline主要有以下功能:

  • 用户输入时,字符会被回传到pty master;
  • Line discipline会在内存中缓冲这些字符。当用户按回车键时,它才将这些字符发送到pty slave;
  • Line discipline 可以拦截处理一些特殊的功能键,例如:
    • 当用户按ctrl+c时,它向连接到pty slave的进程发送kill -2(SIGINT)信号;
    • 当用户按ctrl+w 时,它删除用户输入的最后一个字;
    • 当用户按ctrl+z 时,它向连接到pyt slave的进程发送kill -STOP信号;
    • 当用户按退格键时,它从缓冲区中删除该字符,并向pty master发送删除最后一个字符的指令;

我们可以使用命令行工具stty查询和配置tty,包括Line discipline规则。在 termina 执行stty -a命令:

root@zhengyang:~# stty -a
speed 38400 baud; rows 65; columns 121; line = 0;
intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q;
stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc

可以通过stty raw命令来禁用所有的Line discipline规则,这样的终端被称为raw terminal。像vi这样的编辑器会将终端设置为raw ,因为它需要自己处理字符。下面介绍的远程终端也是需要一个raw terminal,同样会禁用所有的Line discipline规则。

1.5.4 远程终端ssh

我们经常通过ssh连接到一个远程主机,这时候远程主机上的ssh server 就是一个伪终端pty,它同样持有 pty master,但 ssh server不再监听键盘事件,以及在屏幕上绘制输出结果,而是通过tcp连接,向ssh client发送或接收字符。

1.5.5 示例

一般情况下我们通过远程连接的方式执行命令时,进程的标准输入、标准输出和标准错误输出都会绑定到伪终端上,下面是一个简单的demo程序:

#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("PID : %d\n", getpid());
    sleep(200);  // 单位秒

    printf("\n");
    return 0;
}

把这段代码保存在文件mydemo.c中,然后执行下面的命令编译并执行该程序:

demo程序输出了自己进程的PID,现在另外开一个终端执行lsof 命令:

可以看到进程的0u(标准输入)、1u(标准输出)、2u(标准错误输出)都绑定到了伪终端 /dev/pts/20上。

注:FD为文件描述符。NAME为文件系统挂载点名称。

下面,我们思考一个问题,如果我们尝试去读取一个/dev/pty/xx设备,将会产生什么影响,我们打开两个终端,并在右侧终端输入1-9这几个数字:

我们发现右侧终端输入的数字只显示了12589,另一半数字显示在了左侧的终端。这主要是因为/dev/pts/21除了被右侧的bash设置为标准输入、标准输出、标准错误输出外,还被左侧终端打开的cat进程设置为输入,我们使用lsof名称查看/dev/pts/21文件被哪些进程打开了:

由于除了右侧终端fork的子进程bash外,还存在左侧终端cat进程读取dev/pts/21,因此产生了竞争关系,导致无法预测输入内容被哪个进程读取到。一旦被cat读到了,那么按下的键将不会显示在当前的终端中。

我们可以通过w命令看看有哪些人登录在机器上,然后去cat他们的/dev/pts/xxx,他们一定会以为自己的键盘坏了!(小提示,当用户登录的时候,使用的/dev/pts/xx文件权限将设置为仅自己读写,owner 设置为自己,所以这个恶作剧必须要 root 才行!)。

1.6 总结

至此我们可以得出这样的结论:现在所说的终端已经不是物理终端了,而是软件模拟终端。如今的终端大致可以分为以下几类:

  • 控制台:/dev/console是系统控制台终端,用于与操作系统交互;/dev/console是一个虚拟的设备,它需要映射到真正的tty上,比如通过内核启动参数“console=ttySCA0”就把console映射到了串口0;
  • 虚拟终端:/dev/ttyn,是运行在内核态的软件模拟终端;当用户在无图形界面的linux系统登录时,使用的是虚拟终端,使用CTRL + ALT+ [F1 - F6]组合键时,我们就可以切换到tty1、tty2、tty3等上面去;
  • 伪终端:/dev/pts/xx, 是运行在用户态的软件模拟终端,比如远程登录ssh、以及我们所使用的ubuntu系统图形界面中的终端;
  • 串行端口终端:dev/ttySn,这一类是使用计算机串行端口连接的终端设备;

二、tty驱动框架

tty子系统框架如下图:

tty驱动框架主要包括:

  • tty core:称之为tty核心,主要是向用户提供统一的软件接口,比如tty_register_device、tty_register_driver函数;
  • Line discipline:称之为tty线路规程,主要从上下两层接收数据,并按照一定协议进行转换,比如ppp或者蓝牙协议;
  • tty driver:该层主要用于实现各类终端的驱动,用以控制实际硬件设备,用于收发数据;

三、tty核心数据结构

学习tty驱动,首先要了解驱动框架涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。

3.1 struct tty_driver

struct tty_driver用来描述一个tty驱动,定义在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;
} __randomize_layout;

其中部分参数含义如下:

  • magic:表示这个给这个结构体的“幻数”,设为TTY_DRIVER_MAGIC,在alloc_tty_driver函数中被初始化;
  • owner:这个驱动的模块拥有者;
  • cdevs:字符设备指针数组,每个元素都是一个字符设备指针,这些字符设备的文件操作集被设置为tty_fops;数组长度为num;
  • driver_name:驱动名称;
  • name:驱动的设备节点名称;
  • major:主设备号;
  • minor_start:开始次设备号;
  • num:分配的字符设备数量,这字符设备具有相同的主设备号,仅仅次设备号不一样;
  • type:tty驱动的类型;设备类型包括TTY_DRIVER_TYPE_PTY、TTY_DRIVER_TYPE_SERIAL、TTY_DRIVER_TYPE_CONSOLE、TTY_DRIVER_TYPE_SYSTEM;
  • subtype:tty驱动的子类型;
  • init_termios:初始化线路设置;为一个termios结构体,这个成员被用来提供一个线路设置集合;
  • termios:用于保存当前的线路设置,这些线路设置控制当前波特率、数据大小、数据流控设置等;
  • flags:tty驱动标志;
  • driver_state:保存tty驱动私有数据;
  • proc_entry:proc文件系统入口;
  • other:仅对pty驱动又意义;
  • ttys:指针数组,数组长度为num;
    ports:指针数组,数组长度为num;
  • termios:指针数组,数组长度为num;
  • ops:tty字符设备文件操作集;
  • tty_drivers:双向链表节点,用于构建双向链表,注册tty_driver时会将tty_driver添加到双向链表tty_drivers;

3.2 struct tty_struct

strutc tty_struct结构体用于描述tty设备。定义在include/linux/tty.h:

struct tty_struct {
        int     magic;
        struct kref kref;
        struct device *dev;
        struct tty_driver *driver;
        const struct tty_operations *ops;
        int index;

        /* Protects ldisc changes: Lock tty not pty */
        struct ld_semaphore ldisc_sem;
        struct tty_ldisc *ldisc;

        struct mutex atomic_write_lock;
        struct mutex legacy_mutex;
        struct mutex throttle_mutex;
        struct rw_semaphore termios_rwsem;
        struct mutex winsize_mutex;
        spinlock_t ctrl_lock;
        spinlock_t flow_lock;
        /* Termios values are protected by the termios rwsem */
        struct ktermios termios, termios_locked;
        struct termiox *termiox;        /* May be NULL for unsupported */
        char name[64];
        struct pid *pgrp;               /* Protected by ctrl lock */
        struct pid *session;
        unsigned long flags;
        int count;
        struct winsize winsize;         /* winsize_mutex */
        unsigned long stopped:1,        /* flow_lock */
                      flow_stopped:1,
                      unused:BITS_PER_LONG - 2;
        int hw_stopped;
        unsigned long ctrl_status:8,    /* ctrl_lock */
                      packet:1,
                      unused_ctrl:BITS_PER_LONG - 9;
        unsigned int receive_room;      /* Bytes free for queue */
        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;          /* protects tty_files list */
        struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

        int closing;
        unsigned char *write_buf;
        int write_cnt;
        /* If the tty has a pending do_SAK, queue it here - akpm */
        struct work_struct SAK_work;
        struct tty_port *port;
} __randomize_layout;

其中部分参数含义如下:

  • flags:标示tty设备的当前状态,包括TTY_THROTTLED、TTY_IO_ERROR、TTY_OTHER_CLOSED、TTY_EXCLUSIVE、 TTY_DEBUG、TTY_DO_WRITE_WAKEUP、TTY_PUSH、TTY_CLOSING、TTY_DONT_FLIP、 TTY_HW_COOK_OUT、TTY_HW_COOK_IN、TTY_PTY_LOCK、TTY_NO_WRITE_SPLIT等;
  • dev:设备驱动模型中的设备,设备的设备号和driver->cdevs[index]字符设备一致,可以将tty_struct看做为device的子类;
  • driver:指向与之关联的tty_driver;
  • index:存储在driver->ttys指针数组的第几个成员;
  • termios_rwsem:tty设备读写信号量,用于实现进程同步问题,解决多个进程同时读写同一个tty设备的资源竞争问题;
  • ldisc_sem:tty设备线路规程信号量,同一时间只能一个进程获取到线路规程ldisc;
  • ldisc:tty设备的线路规程;
  • ops:tty字符设备文件操作集;
  • write_wait:写等待队列头,头节点为struct wait_queue_head_t类型。等待队列中的元素为struct wait_queue_t类型;
  • read_wait:读等待队列头,头节点为struct wait_queue_head_t类型。等待队列中的元素为struct wait_queue_t类型;
  • stopped:是否停止tty设备;
  • write_buf:写缓冲区;
  • write_cnt:写缓冲区大小;
  • driver_data、disc_data为数据指针,用于存储tty驱动和线路规程的“私有”数据。
3.2.1 struct n_tty_data

disc_data存放时tty设备接收到的数据,其数据类型为struct n_tty_data:

struct n_tty_data {
        /* producer-published */
        size_t read_head;
        size_t commit_head;
        size_t canon_head;
        size_t echo_head;
        size_t echo_commit;
        size_t echo_mark;
        DECLARE_BITMAP(char_map, 256);

        /* private to n_tty_receive_overrun (single-threaded) */
        unsigned long overrun_time;
        int num_overrun;

        /* non-atomic */
        bool no_room;

        /* must hold exclusive termios_rwsem to reset these */
        unsigned char lnext:1, erasing:1, raw:1, real_raw:1, icanon:1;
        unsigned char push:1;

        /* shared by producer and consumer */
        char read_buf[N_TTY_BUF_SIZE];
        DECLARE_BITMAP(read_flags, N_TTY_BUF_SIZE);
        unsigned char echo_buf[N_TTY_BUF_SIZE];

        /* consumer-published */
        size_t read_tail;
        size_t line_start;

        /* protected by output lock */
        unsigned int column;
        unsigned int canon_column;
        size_t echo_tail;

        struct mutex atomic_read_lock;
        struct mutex output_lock;
};

定义的read_buf缓冲区是线性数组,但是却是作为环形缓冲区使用的;

read_head成员是环形缓冲区空闲位置的开始,产生数据的进程从read_head位置开始往缓冲区写入数据;

read_tail成员是环形缓冲区保存数据位置的开始,读取数据的进程从read_tail位置开始从缓冲区读取数据;

tty->read_buf[]    // 环形缓冲区;
tty->read_tail    // 指向缓冲区当前可以读取的第一个字符;
tty->read_head    // 指向缓冲区当前可以写入的第一个地址;

3.3 struct tty_port

strutc tty_port用于描述一个端口。定义在include/linux/tty.h:

struct tty_port {
        struct tty_bufhead      buf;            /* Locked internally */
        struct tty_struct       *tty;           /* Back pointer */
        struct tty_struct       *itty;          /* internal back ptr */
        const struct tty_port_operations *ops;  /* Port operations */
        const struct tty_port_client_operations *client_ops; /* Port client operations */
        spinlock_t              lock;           /* Lock protecting tty field */
        int                     blocked_open;   /* Waiting to open */
        int                     count;          /* Usage count */
        wait_queue_head_t       open_wait;      /* Open waiters */
        wait_queue_head_t       delta_msr_wait; /* Modem status change */
        unsigned long           flags;          /* User TTY flags ASYNC_ */
        unsigned long           iflags;         /* Internal flags TTY_PORT_ */
        unsigned char           console:1,      /* port is a console */
                                low_latency:1;  /* optional: tune for latency */
        struct mutex            mutex;          /* Locking */
        struct mutex            buf_mutex;      /* Buffer alloc lock */
        unsigned char           *xmit_buf;      /* Optional buffer */
        unsigned int            close_delay;    /* Close port delay */
        unsigned int            closing_wait;   /* Delay for output */
        int                     drain_delay;    /* Set to zero if no pure time
                                                   based drain is needed else
                                                   set to size of fifo */
        struct kref             kref;           /* Ref counter */
        void                    *client_data;
};

3.4 struct tty_operations

tty_operations中的成员函数与 tty_driver中的同名成员函数意义完全一致,描述的是tty字符设备文件操作集,其定义在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);
        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);
        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 *, void *);
} __randomize_layout;

3.5 struct tty_ldisc

struct tty_ldisc定义在include/linux/tty_ldisc.h:

struct tty_ldisc {
        struct tty_ldisc_ops *ops;
        struct tty_struct *tty;
};

tty线路规程是由tty_ldisc_ops和tty_struct组成,其中tty_ldisc_ops为线路规程操作集,定义如下:

struct tty_ldisc_ops {
        int     magic;
        char    *name;
        int     num;
        int     flags;

        /*
         * The following routines are called from above.
         */
        int     (*open)(struct tty_struct *);
        void    (*close)(struct tty_struct *);
        void    (*flush_buffer)(struct tty_struct *tty);
        ssize_t (*read)(struct tty_struct *tty, struct file *file,
                        unsigned char __user *buf, size_t nr);
        ssize_t (*write)(struct tty_struct *tty, struct file *file,
                         const unsigned char *buf, size_t nr);
        int     (*ioctl)(struct tty_struct *tty, struct file *file,
                         unsigned int cmd, unsigned long arg);
        int     (*compat_ioctl)(struct tty_struct *tty, struct file *file,
                                unsigned int cmd, unsigned long arg);
        void    (*set_termios)(struct tty_struct *tty, struct ktermios *old);
        __poll_t (*poll)(struct tty_struct *, struct file *,
                             struct poll_table_struct *);
        int     (*hangup)(struct tty_struct *tty);

        /*
         * The following routines are called from below.
         */
        void    (*receive_buf)(struct tty_struct *, const unsigned char *cp,
                               char *fp, int count);
        void    (*write_wakeup)(struct tty_struct *);
        void    (*dcd_change)(struct tty_struct *, unsigned int);
        int     (*receive_buf2)(struct tty_struct *, const unsigned char *cp,
                                char *fp, int count);

        struct  module *owner;

        int refcount;
};

3.6 关系图

以tty串口设备为例,下图是tty_driver、tty_struct、tty_operations、tty_ldisc、tty_ldisc_ops之间的关系图:

 四、tty驱动API

linux内核提供了一组函数用于操作tty_driver结构体及tty设备。

4.1 注册tty设备

tty_register_device用于注册tty设备,函数定义在drivers/tty/tty_io.c:

/**
 *      tty_register_device - register a tty device
 *      @driver: the tty driver that describes the tty device
 *      @index: the index in the tty driver for this tty device
 *      @device: a struct device that is associated with this tty device.
 *              This field is optional, if there is no known struct device
 *              for this tty device it can be set to NULL safely.
 *
 *      Returns a pointer to the struct device for this tty device
 *      (or ERR_PTR(-EFOO) on error).
 *
 *      This call is required to be made to register an individual tty device
 *      if the tty driver's flags have the TTY_DRIVER_DYNAMIC_DEV bit set.  If
 *      that bit is not set, this function should not be called by a tty
 *      driver.
 *
 *      Locking: ??
 */

struct device *tty_register_device(struct tty_driver *driver, unsigned index,
                                   struct device *device)   // 传入NULL
{
        return tty_register_device_attr(driver, index, device, NULL, NULL);
}

/**
 *      tty_register_device_attr - register a tty device
 *      @driver: the tty driver that describes the tty device
 *      @index: the index in the tty driver for this tty device
 *      @device: a struct device that is associated with this tty device.
 *              This field is optional, if there is no known struct device
 *              for this tty device it can be set to NULL safely.
 *      @drvdata: Driver data to be set to device.
 *      @attr_grp: Attribute group to be set on device.
 *
 *      Returns a pointer to the struct device for this tty device
 *      (or ERR_PTR(-EFOO) on error).
 *
 *      This call is required to be made to register an individual tty device
 *      if the tty driver's flags have the TTY_DRIVER_DYNAMIC_DEV bit set.  If
 *      that bit is not set, this function should not be called by a tty
 *      driver.
 *
 *      Locking: ??
 */
struct device *tty_register_device_attr(struct tty_driver *driver,
                                   unsigned index, struct device *device,
                                   void *drvdata,
                                   const struct attribute_group **attr_grp)
{
        char name[64];
        dev_t devt = MKDEV(driver->major, driver->minor_start) + index;   // 设备号
        struct ktermios *tp;
        struct device *dev;
        int retval;

        if (index >= driver->num) {        // 判断索引号
                pr_err("%s: Attempt to register invalid tty line number (%d)\n",
                       driver->name, index);
                return ERR_PTR(-EINVAL);
        }

        if (driver->type == TTY_DRIVER_TYPE_PTY)   // 设备类型 伪终端
                pty_line_name(driver, index, name);
        else
                tty_line_name(driver, index, name);  // 设置name为 driver->name+index

        dev = kzalloc(sizeof(*dev), GFP_KERNEL);  // 动态申请struct device
        if (!dev)
                return ERR_PTR(-ENOMEM);

        dev->devt = devt;               // 初始化设备号
        dev->class = tty_class;         // 类tty_class名称为tty
        dev->parent = device;
        dev->release = tty_device_create_release;
        dev_set_name(dev, "%s", name);  // 设置dev->init_name为driver->name+index
        dev->groups = attr_grp;
        dev_set_drvdata(dev, drvdata);   // 设置dev->p->driver_data=drvdata

        dev_set_uevent_suppress(dev, 1);

        retval = device_register(dev);   // 注册设备 会在/sys/class/tty类文件下创建名字为driver->name+index的文件夹
        if (retval)
                goto err_put;

        if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {  // 如果未指定TTY_DRIVER_DYNAMIC_ALLOC
                /*
                 * Free any saved termios data so that the termios state is
                 * reset when reusing a minor number.
                 */
                tp = driver->termios[index];   // 释放driver->termios[index]
                if (tp) {
                        driver->termios[index] = NULL;
                        kfree(tp);
                }

                retval = tty_cdev_add(driver, devt, index, 1);  // 注册1个字符设备,主设备号为driver->major,次设备号数为driver->minor_start + index
                if (retval)
                        goto err_del;
        }

        dev_set_uevent_suppress(dev, 0);
        kobject_uevent(&dev->kobj, KOBJ_ADD);

        return dev;

err_del:
        device_del(dev);
err_put:
        put_device(dev);

        return ERR_PTR(retval);
}

仅有tty_driver是不够的,驱动必须依附于设备,tty_register_device函数用于注册关联于tty_driver的设备,index为设备的索引(范围是0~driver->num)。

该函数主要流程如下:

  • 动态申请struct device,并初始化初始化成员:
    • 初始化设备编号devt:主设备号为driver->major,次设备号为driver->minor_start+index;
    • 初始化class为tty_class,类名称为tty;
    • 初始化parent为device;
    • 初始化release为tty_device_create_release;
    • 初始化init_name为为driver->name+index;
    • 初始化groups为attr_grp;
    • 设置dev->p->driver_data为drvdata;
  • 调用device_register注册设备,会在/sys/class/tty类文件下创建名字为driver->name+index的文件夹;
  • 如果未指定TTY_DRIVER_DYNAMIC_ALLOC,则调用tty_cdev_add注册字符设备:
    • 调用cdev_alloc态分配字符设备,并赋值给driver->cdevs[xxx],主设备号为driver->major,次设备号数为driver->minor_start + index;文件操作集为tty_fops;
    • 调用cdev_add将字符设备添加到内核;

4.2 注销tty设备

tty_unregister_device用于卸载tty设备,函数定义在drivers/tty/tty_io.c:

/**
 *      tty_unregister_device - unregister a tty device
 *      @driver: the tty driver that describes the tty device
 *      @index: the index in the tty driver for this tty device
 *
 *      If a tty device is registered with a call to tty_register_device() then
 *      this function must be called when the tty device is gone.
 *
 *      Locking: ??
 */

void tty_unregister_device(struct tty_driver *driver, unsigned index)
{
        device_destroy(tty_class,
                MKDEV(driver->major, driver->minor_start) + index);
        if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
                cdev_del(driver->cdevs[index]);
                driver->cdevs[index] = NULL;
        }
}

4.3 分配tty驱动

alloc_tty_driver用于分配tty驱动,函数定义在include/linux/tty_driver.h:

/*
 * DEPRECATED Do not use this in new code, use tty_alloc_driver instead.
 * (And change the return value checks.)
 */
static inline struct tty_driver *alloc_tty_driver(unsigned int lines)  // 
{
        struct tty_driver *ret = tty_alloc_driver(lines, 0);
        if (IS_ERR(ret))
                return NULL;
        return ret;
}

/* Use TTY_DRIVER_* flags below */
#define tty_alloc_driver(lines, flags) \
                __tty_alloc_driver(lines, THIS_MODULE, flags)

这个函数返回tty_driver指针,其参数为要分配的设备数量,lines会被赋值给tty_driver的num成员。

4.3.1 __ tty_alloc_driver

__tty_alloc_driver函数定义在drivers/tty/tty_io.c,第一个参数为申请的tty字符设备数量,比如,某某芯片的介绍:”多达3个UART接口“,那么这个lines就是3;

/**
 * __tty_alloc_driver -- allocate tty driver
 * @lines: count of lines this driver can handle at most
 * @owner: module which is responsible for this driver
 * @flags: some of TTY_DRIVER_* flags, will be set in driver->flags
 *
 * This should not be called directly, some of the provided macros should be
 * used instead. Use IS_ERR and friends on @retval.
 */
struct tty_driver *__tty_alloc_driver(unsigned int lines, struct module *owner,
                unsigned long flags)
{
        struct tty_driver *driver;
        unsigned int cdevs = 1;
        int err;

        if (!lines || (flags & TTY_DRIVER_UNNUMBERED_NODE && lines > 1))
                return ERR_PTR(-EINVAL);

        driver = kzalloc(sizeof(struct tty_driver), GFP_KERNEL);  // 动态申请内存
        if (!driver)
                return ERR_PTR(-ENOMEM);

        kref_init(&driver->kref);
        driver->magic = TTY_DRIVER_MAGIC;
        driver->num = lines;  // tty字符设备数量
        driver->owner = owner;
        driver->flags = flags;

        if (!(flags & TTY_DRIVER_DEVPTS_MEM)) {   // 未指定pts mem标志
                driver->ttys = kcalloc(lines, sizeof(*driver->ttys),   // 初始化指针数组,数组长度为lines,每个元素都是struct tty_struct *
                                GFP_KERNEL);
                driver->termios = kcalloc(lines, sizeof(*driver->termios),  // 初始化指针数组,数组长度为lines,每个元素都是struct ktermios*
                                GFP_KERNEL);
                if (!driver->ttys || !driver->termios) {
                        err = -ENOMEM;
                        goto err_free_all;
                }
        }

        if (!(flags & TTY_DRIVER_DYNAMIC_ALLOC)) {  // 未自定该标志
                driver->ports = kcalloc(lines, sizeof(*driver->ports),  // 初始化指针数组,数组长度为lines,每个元素都是struct tty_port*
                                GFP_KERNEL);
                if (!driver->ports) {
                        err = -ENOMEM;
                        goto err_free_all;
                }
                cdevs = lines;
        }

        driver->cdevs = kcalloc(cdevs, sizeof(*driver->cdevs), GFP_KERNEL); // 初始化指针数组,数组长度为lines,每个元素都是struct cdev*
        if (!driver->cdevs) {
                err = -ENOMEM;
                goto err_free_all;
        }

        return driver;
err_free_all:
        kfree(driver->ports);
        kfree(driver->ttys);
        kfree(driver->termios);
        kfree(driver->cdevs);
        kfree(driver);
        return ERR_PTR(err);
}

该函数主要就是进行一些初始化工作:

  • 动态分配tty_driver;
  • 初始化tty_driver成员:
    • 设置成员num;
    • 初始化成员cdevs;
    • 初始化成员ttys;
    • 初始化成员ports;
    • 初始化成员termios;
    • 等等;

4.4 注册tty驱动

tty_register_driver用于注册tty驱动,函数定义在drivers/tty/tty_io.c:

/*
 * Called by a tty driver to register itself.
 */
int tty_register_driver(struct tty_driver *driver)
{
        int error;
        int i;
        dev_t dev;
        struct device *d;

        if (!driver->major) {     // 如果没有指定主设备号,动则态申请
                error = alloc_chrdev_region(&dev, driver->minor_start,
                                                driver->num, driver->name);
                if (!error) {
                        driver->major = MAJOR(dev);
                        driver->minor_start = MINOR(dev);
                }
        } else {
                dev = MKDEV(driver->major, driver->minor_start);
                error = register_chrdev_region(dev, driver->num, driver->name);
        }
        if (error < 0)
                goto err;

        if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC) {  // 指定动态分配
                error = tty_cdev_add(driver, dev, 0, driver->num);  // 注册driver->num个字符设备,相同的主设备号
                if (error)
                        goto err_unreg_char;
        }

        mutex_lock(&tty_mutex);        // 获取互斥锁
        list_add(&driver->tty_drivers, &tty_drivers);   // 添加到双向链表中
        mutex_unlock(&tty_mutex);      // 释放互斥锁

        if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {  // 未指定TTY_DRIVER_DYNAMIC_DEV
                for (i = 0; i < driver->num; i++) {
                        d = tty_register_device(driver, i, NULL);  // 注册tty设备
                        if (IS_ERR(d)) {
                                error = PTR_ERR(d);
                                goto err_unreg_devs;
                        }
                }
        }
        proc_tty_register_driver(driver);  // 向proc文件系统注册driver
        driver->flags |= TTY_DRIVER_INSTALLED;
        return 0;

err_unreg_devs:
        for (i--; i >= 0; i--)
                tty_unregister_device(driver, i);

        mutex_lock(&tty_mutex);
        list_del(&driver->tty_drivers);
        mutex_unlock(&tty_mutex);

err_unreg_char:
        unregister_chrdev_region(dev, driver->num);
err:
        return error;
}

注册tty驱动成功时返回0,参数为由alloc_tty_driver 分配的tty_driver结构体指针。

tty_register_driver注册过程主要做了以下事情:

  • 申请设备号;
  • 如果指定了标志位TTY_DRIVER_DYNAMIC_ALLOC,则调用tty_cdev_add注册driver->num个字符设备,相同的主设备号;
    • 调用cdev_alloc态分配字符设备,并赋值给driver->cdevs[xxx],主设备号为driver->major,次设备号数为driver->minor_start + index;文件操作集为tty_fops;
    • 调用cdev_add将字符设备添加到内核;
  • 将当前tty_driver添加到全局双向链表tty_drivers;
  • 如果未指定TTY_DRIVER_DYNAMIC_DEV,调用tty_register_device注册tty设备,一共注册driver->num个struct device设备:
    • 设备class为tty_calss(名称为tty) ;
    • 设备名称为driver->name+编号,会在文件系统下创建设备节点文件/dev/设备名称;
  • 调用proc_tty_register_driver向 proc 文件系统添加driver ;
4.4.1 tty_cdev_add

tty_cdev_add函数主要就是动态分配struct cdev,并初始化ops为tty_fops;owner为driver->owner,同时将其添加到内核:

static int tty_cdev_add(struct tty_driver *driver, dev_t dev,
                unsigned int index, unsigned int count)
{
        int err;

        /* init here, since reused cdevs cause crashes */
        driver->cdevs[index] = cdev_alloc();             // 动态分配字符设备
        if (!driver->cdevs[index])
                return -ENOMEM;
        driver->cdevs[index]->ops = &tty_fops;          // 字符设备文件操作集
        driver->cdevs[index]->owner = driver->owner;
        err = cdev_add(driver->cdevs[index], dev, count);  // 添加字符设备到内核 第二个参数起始设备号  第三个参数为次设备数量
        if (err)
                kobject_put(&driver->cdevs[index]->kobj);
        return err;
}

函数第一个参数为tty_driver,第二个参数为起始设备编号,第三个参数为drivers->cdevs元素索引,第四个参数为次设备数量。

这里我们需要留意的地方是字符设备的文件操作集被初始化为了tty_fops,比如当我们对字符设备/dev/ttySn进行读写的时候实际上调用的就是操作集中的方法。

4.4.2 tty_fops

tty_fops定义在drivers/tty/tty_io.c:

static const struct file_operations tty_fops = {
        .llseek         = no_llseek,
        .read           = tty_read,
        .write          = tty_write,
        .poll           = tty_poll,
        .unlocked_ioctl = tty_ioctl,
        .compat_ioctl   = tty_compat_ioctl,
        .open           = tty_open,
        .release        = tty_release,
        .fasync         = tty_fasync,
        .show_fdinfo    = tty_show_fdinfo,
};

用户空间的任何操作,首先对接到的是这个地方,也就是文件 tty_io.c中的这个操作集。

4.4.3 proc_tty_register_driver

函数最后调用proc_tty_register_driver向proc文件系统添加driver ;定义在fs/proc/proc_tty.c文件:

/*
 * This function is called by tty_register_driver() to handle
 * registering the driver's /proc handler into /proc/tty/driver/<foo>
 */
void proc_tty_register_driver(struct tty_driver *driver)
{
        struct proc_dir_entry *ent;

        if (!driver->driver_name || driver->proc_entry ||
            !driver->ops->proc_show)
                return;

        ent = proc_create_single_data(driver->driver_name, 0, proc_tty_driver,
                               driver->ops->proc_show, driver);
        driver->proc_entry = ent;
}

4.5 注销tty驱动

tty_unregister_driver用于注销tty驱动,函数定义在drivers/tty/tty_io.c:

/*
 * Called by a tty driver to unregister itself.
 */
int tty_unregister_driver(struct tty_driver *driver)
{
#if 0
        /* FIXME */
        if (driver->refcount)
                return -EBUSY;
#endif
        unregister_chrdev_region(MKDEV(driver->major, driver->minor_start),
                                driver->num);
        mutex_lock(&tty_mutex);
        list_del(&driver->tty_drivers);
        mutex_unlock(&tty_mutex);
        return 0;
}

4.6 设置tty驱动操作

tty_set_operations函数用于设置tty驱动操作,定义在drivers/tty/tty_io.c:

void tty_set_operations(struct tty_driver *driver,
                        const struct tty_operations *op)
{
        driver->ops = op;
};

函数会将tty_operations结构体中的函数指针拷贝给tty_driver对应的函数指针,在具体的tty驱动中,通常会定义1个设备特定的 tty_operations。

五、tty_open

我们在调用tty_register_driver进行tty_driver注册的时候,该函数内部会调用tty_register_device注册tty字符设备,并且将tty字符设备文件操作集设置为tty_fops。

这一小节,我们将对tty_fops文件操作集进行分析。tty_fops定义在drivers/tty/tty_io.c:

static const struct file_operations tty_fops = {
        .llseek         = no_llseek,
        .read           = tty_read,
        .write          = tty_write,
        .poll           = tty_poll,
        .unlocked_ioctl = tty_ioctl,
        .compat_ioctl   = tty_compat_ioctl,
        .open           = tty_open,
        .release        = tty_release,
        .fasync         = tty_fasync,
        .show_fdinfo    = tty_show_fdinfo,
};

这里我们以open函数为例进行讲解。下图为tty_open函数调用过程,比较复杂,后面我们一一分析:

5.1 tty_open

当我们打开一个tty设备时,即对设备节点/dev/ttyN进行open操作的时候,tty_open函数会被调用:

static int tty_open(struct inode *inode, struct file *filp)
{
        struct tty_struct *tty;
        int noctty, retval;
        dev_t device = inode->i_rdev;
        unsigned saved_flags = filp->f_flags;

        nonseekable_open(inode, filp);

retry_open:
        retval = tty_alloc_file(filp);
        if (retval)
                return -ENOMEM;

        tty = tty_open_current_tty(device, filp);  // 获取当前进程对应控制终端所对应的tty_struct指针
        if (!tty)
                tty = tty_open_by_driver(device, inode, filp);  //进入这里  获取当前设备对应的tty_struct 

        if (IS_ERR(tty)) {
                tty_free_file(filp);
                retval = PTR_ERR(tty);
                if (retval != -EAGAIN || signal_pending(current))
                        return retval;
                schedule();
                goto retry_open;
        }

        tty_add_file(tty, filp);

        check_tty_count(tty, __func__);
        tty_debug_hangup(tty, "opening (count=%d)\n", tty->count);

        if (tty->ops->open)
                retval = tty->ops->open(tty, filp);   // 重点
        else
                retval = -ENODEV;
        filp->f_flags = saved_flags;

        if (retval) {
                tty_debug_hangup(tty, "open error %d, releasing\n", retval);

                tty_unlock(tty); /* need to call tty_release without BTM */
                tty_release(inode, filp);
                if (retval != -ERESTARTSYS)
                        return retval;

                if (signal_pending(current))
                        return retval;

                schedule();
                /*
                 * Need to reset f_op in case a hangup happened.
                 */
                if (tty_hung_up_p(filp))
                        filp->f_op = &tty_fops;
                goto retry_open;
        }
        clear_bit(TTY_HUPPED, &tty->flags);

        noctty = (filp->f_flags & O_NOCTTY) ||
                 (IS_ENABLED(CONFIG_VT) && device == MKDEV(TTY_MAJOR, 0)) ||
                 device == MKDEV(TTYAUX_MAJOR, 1) ||
                 (tty->driver->type == TTY_DRIVER_TYPE_PTY &&
                  tty->driver->subtype == PTY_TYPE_MASTER);
        if (!noctty)
                tty_open_proc_set_tty(filp, tty);
        tty_unlock(tty);
        return 0;
}

针对tty_open接口而言,主要实现如下几个功能:

  • 若打开的tty设备为控制终端,则通过调用tty_open_current_tty获取当前进程对应控制终端所对应的tty_struct指针;
  • 若1中没有找到对应tty_struct,则调用tty_open_by_driver根据字符设备号从tty_drivers链表中查找已注册的tty_driver,若该tty_driver与对应tty端口的tty_struct已完成绑定,则获取对应的tty_struct指针;
  • 若以上两步均没有获取到tty端口对应的tty_struct,则说明该tty端口对应的tty_struct还没有创建,则调用tty_init_dev完成tty_struct的创建,并完成tty_struct与tty_driver的绑定、tty_struct与tty_port、tty_struct与ldisc、tty_struct与tty device的绑定操作, 并调用tty_ldisc_setup,进行线路规程的打开(如termios的设置,ldisc的使能、ldisc缓存的初始化等)等等;
  • 调用tty->ops->open函数;
5.1.1 tty_open_current_tty

tty_open_current_tty函数用于获取当前进程对应控制终端所对应的tty_struct指针;

/**
 *      tty_open_current_tty - get locked tty of current task
 *      @device: device number
 *      @filp: file pointer to tty
 *      @return: locked tty of the current task iff @device is /dev/tty
 *
 *      Performs a re-open of the current task's controlling tty.
 *
 *      We cannot return driver and index like for the other nodes because
 *      devpts will not work then. It expects inodes to be from devpts FS.
 */
static struct tty_struct *tty_open_current_tty(dev_t device, struct file *filp)
{
        struct tty_struct *tty;
        int retval;

        if (device != MKDEV(TTYAUX_MAJOR, 0))  // 主设备号不等于TTYAUX_MAJOR,直接返回   实际上走这里
                return NULL;

        tty = get_current_tty();  // 返回当前进程的控制终端
        if (!tty)
                return ERR_PTR(-ENXIO);

        filp->f_flags |= O_NONBLOCK; /* Don't let /dev/tty block */
        /* noctty = 1; */
        tty_lock(tty);
        tty_kref_put(tty);      /* safe to drop the kref now */

        retval = tty_reopen(tty);
        if (retval < 0) {
                tty_unlock(tty);
                tty = ERR_PTR(retval);
        }
        return tty;
}

如果主设备号不等于TTYAUX_MAJOR,函数直接返回0;tty_reopen函数内部调用了tty_ldisc_reinit。

5.1.2 tty_open_by_driver

tty_open_by_driver函数根据字符设备号从tty_drivers链表中查找已注册的tty_driver,若该tty_driver与对应tty端口的tty_struct已完成绑定,则获取对应的tty_struct指针;

若没有获取到tty端口对应的tty_struct,则说明该tty端口对应的tty_struct还没有创建,则调用tty_init_dev完成tty_struct的创建,并完成tty_struct与tty_driver的绑定、tty_struct与tty_port、tty_struct与ldisc、tty_struct与tty device的绑定操作, 并调用tty_ldisc_setup,进行线路规程的打开(如termios的设置,ldisc的使能、ldisc缓存的初始化等)等等;

/**
 *      tty_open_by_driver      -       open a tty device
 *      @device: dev_t of device to open
 *      @inode: inode of device file
 *      @filp: file pointer to tty
 *
 *      Performs the driver lookup, checks for a reopen, or otherwise
 *      performs the first-time tty initialization.
 *
 *      Returns the locked initialized or re-opened &tty_struct
 *
 *      Claims the global tty_mutex to serialize:
 *        - concurrent first-time tty initialization
 *        - concurrent tty driver removal w/ lookup
 *        - concurrent tty removal from driver table
 */
static struct tty_struct *tty_open_by_driver(dev_t device, struct inode *inode,
                                             struct file *filp)
{
        struct tty_struct *tty;
        struct tty_driver *driver = NULL;
        int index = -1;
        int retval;

        mutex_lock(&tty_mutex);
        driver = tty_lookup_driver(device, filp, &index);  // 根据设备号从tty_drivers链表中查找已注册的tty_driver,由于已经注册了tty_driver,所以这个能找到
        if (IS_ERR(driver)) {
                mutex_unlock(&tty_mutex);
                return ERR_CAST(driver);
        }

        /* check whether we're reopening an existing tty */
        tty = tty_driver_lookup_tty(driver, filp, index);  // 返回driver->ttys[idx];
因为tty_init_dev函数在这之前还未被调用,而且目前为止未发现其他位置初始化tty_struct并放入ttys[],所以这里返回NULL 
if (IS_ERR(tty)) { // 返回NULL mutex_unlock(&tty_mutex); goto out; } if (tty) { if (tty_port_kopened(tty->port)) { tty_kref_put(tty); mutex_unlock(&tty_mutex); tty = ERR_PTR(-EBUSY); goto out; } mutex_unlock(&tty_mutex); retval = tty_lock_interruptible(tty); tty_kref_put(tty); /* drop kref from tty_driver_lookup_tty() */ if (retval) { if (retval == -EINTR) retval = -ERESTARTSYS; tty = ERR_PTR(retval); goto out; } retval = tty_reopen(tty); if (retval < 0) { tty_unlock(tty); tty = ERR_PTR(retval); } } else { /* Returns with the tty_lock held for now */ tty = tty_init_dev(driver, index); // 执行这个 分配一个tty设备,即strucr tty_struct,并进行初始化各个成员 mutex_unlock(&tty_mutex); } out: tty_driver_kref_put(driver); return tty; }
5.1.3 tty->ops->open

在tty_open函数的最后,调用tty->ops->open函数,以串口驱动为例,ops被设置为uart_ops,open函数为uart_open。这个我们在串口驱动中去介绍。

5.2 tty_init_dev 

tty_init_dev 在函数tty_open_by_driver中被调用,在这个函数内部主要做了两件事情:

  • 调用alloc_tty_struct分配tty_struct;
  • 调用tty_driver_install_tty设置driver->ttys[tty->index]成员为tty_struct;
/**
 *      tty_init_dev            -       initialise a tty device
 *      @driver: tty driver we are opening a device on
 *      @idx: device index
 *      @ret_tty: returned tty structure
 *
 *      Prepare a tty device. This may not be a "new" clean device but
 *      could also be an active device. The pty drivers require special
 *      handling because of this.
 *
 *      Locking:
 *              The function is called under the tty_mutex, which
 *      protects us from the tty struct or driver itself going away.
 *
 *      On exit the tty device has the line discipline attached and
 *      a reference count of 1. If a pair was created for pty/tty use
 *      and the other was a pty master then it too has a reference count of 1.
 *
 * WSH 06/09/97: Rewritten to remove races and properly clean up after a
 * failed open.  The new code protects the open with a mutex, so it's
 * really quite straightforward.  The mutex locking can probably be
 * relaxed for the (most common) case of reopening a tty.
 */

struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx)
{
        struct tty_struct *tty;
        int retval;

        /*
         * First time open is complex, especially for PTY devices.
         * This code guarantees that either everything succeeds and the
         * TTY is ready for operation, or else the table slots are vacated
         * and the allocated memory released.  (Except that the termios
         * may be retained.)
         */

        if (!try_module_get(driver->owner))
                return ERR_PTR(-ENODEV);

        tty = alloc_tty_struct(driver, idx); // 动态分配tty_struct  tty->driver=driver、tty->ops = driver->ops  
        if (!tty) {
                retval = -ENOMEM;
                goto err_module_put;
        }

        tty_lock(tty);
        retval = tty_driver_install_tty(driver, tty); // 设置driver->ttys[tty->index]成员为tty
        if (retval < 0)
                goto err_free_tty;

        if (!tty->port)
                tty->port = driver->ports[idx];

        WARN_RATELIMIT(!tty->port,
                        "%s: %s driver does not set tty->port. This will crash the kernel later. Fix the driver!\n",
                        __func__, tty->driver->name);

        retval = tty_ldisc_lock(tty, 5 * HZ);
        if (retval)
                goto err_release_lock;
        tty->port->itty = tty;

        /*
         * Structures all installed ... call the ldisc open routines.
         * If we fail here just call release_tty to clean up.  No need
         * to decrement the use counts, as release_tty doesn't care.
         */
        retval = tty_ldisc_setup(tty, tty->link);
        if (retval)
                goto err_release_tty;
        tty_ldisc_unlock(tty);
        /* Return the tty locked so that it cannot vanish under the caller */
        return tty;

err_free_tty:
        tty_unlock(tty);
        free_tty_struct(tty);
err_module_put:
        module_put(driver->owner);
        return ERR_PTR(retval);

        /* call the tty release_tty routine to clean out this slot */
err_release_tty:
        tty_ldisc_unlock(tty);
        tty_info_ratelimited(tty, "ldisc open failed (%d), clearing slot %d\n",
                             retval, idx);
err_release_lock:
        tty_unlock(tty);
        release_tty(tty, idx);
        return ERR_PTR(retval);
}
5.2.1 alloc_tty_struct

alloc_tty_struct函数用于分配一个tty_struct,并进行成员的初始化:

/**
 *      alloc_tty_struct
 *
 *      This subroutine allocates and initializes a tty structure.
 *
 *      Locking: none - tty in question is not exposed at this point
 */

struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
        struct tty_struct *tty;

        tty = kzalloc(sizeof(*tty), GFP_KERNEL);  // 动态分配内存
        if (!tty)
                return NULL;

        kref_init(&tty->kref);
        tty->magic = TTY_MAGIC;
        if (tty_ldisc_init(tty)) {   // 初始化tty设备的线路规程,设置tty->ldisc=ld   ld->ops=tty_ldiscs[0]
                kfree(tty);
                return NULL;
        }
        tty->session = NULL;
        tty->pgrp = NULL;
        mutex_init(&tty->legacy_mutex);
        mutex_init(&tty->throttle_mutex);
        init_rwsem(&tty->termios_rwsem);
        mutex_init(&tty->winsize_mutex);
        init_ldsem(&tty->ldisc_sem);
        init_waitqueue_head(&tty->write_wait);
        init_waitqueue_head(&tty->read_wait);
        INIT_WORK(&tty->hangup_work, do_tty_hangup);
        mutex_init(&tty->atomic_write_lock);
        spin_lock_init(&tty->ctrl_lock);
        spin_lock_init(&tty->flow_lock);
        spin_lock_init(&tty->files_lock);
        INIT_LIST_HEAD(&tty->tty_files);
        INIT_WORK(&tty->SAK_work, do_SAK_work);

        tty->driver = driver;
        tty->ops = driver->ops;           // 这里  
        tty->index = idx;
        tty_line_name(driver, idx, tty->name);
        tty->dev = tty_get_device(tty);  // 获取tty->driver->cdevs[tty->index]字符设备对应的device

        return tty;
}

这里首先动态分配tty设备,即tty_struct:

  • 调用tty_ldisc_init初始化tty设备的线路规程:
    • 动态分配线路规程ld,结构为struct tty_ldisc;
    • 设置ld成员ops为tty_ldiscs[0],即tty_ops;
    • 设置ld成员tty为tty;
    • 设置tty的线路规程为ld,即tty->ldisc=ld;
  • 成员driver为driver;
  • 设置其操作集ops为driver->ops;
  • 设置index,即其位于driver->ttys指针数组中的索引;
  • 设置dev,从tty_class类下根据设备号查找对应的device;
5.2.2 tty_driver_install_tty

tty_driver_install_tty函数用于初始化driver->ttys[tty->index]成员为tty:

int tty_standard_install(struct tty_driver *driver, struct tty_struct *tty)
{
        tty_init_termios(tty);
        tty_driver_kref_get(driver);
        tty->count++;
        driver->ttys[tty->index] = tty;
        return 0;
}


/**
 *      tty_driver_install_tty() - install a tty entry in the driver
 *      @driver: the driver for the tty
 *      @tty: the tty
 *
 *      Install a tty object into the driver tables. The tty->index field
 *      will be set by the time this is called. This method is responsible
 *      for ensuring any need additional structures are allocated and
 *      configured.
 *
 *      Locking: tty_mutex for now
 */
static int tty_driver_install_tty(struct tty_driver *driver,
                                                struct tty_struct *tty)
{
        return driver->ops->install ? driver->ops->install(driver, tty) :
                tty_standard_install(driver, tty);
}
5.2.3 tty_ldisc_setup

tty_ldisc_setup函数位于drivers/tty/tty_ldisc.c,该函数调用了tty_ldisc_open,其本质就是调用线路规程ld的操作集中的open函数,即n_tty_open函数;

/**
 *      tty_ldisc_setup                 -       open line discipline
 *      @tty: tty being shut down
 *      @o_tty: pair tty for pty/tty pairs
 *
 *      Called during the initial open of a tty/pty pair in order to set up the
 *      line disciplines and bind them to the tty. This has no locking issues
 *      as the device isn't yet active.
 */

int tty_ldisc_setup(struct tty_struct *tty, struct tty_struct *o_tty)
{
        int retval = tty_ldisc_open(tty, tty->ldisc);
        if (retval)
                return retval;

        if (o_tty) {
                /*
                 * Called without o_tty->ldisc_sem held, as o_tty has been
                 * just allocated and no one has a reference to it.
                 */
                retval = tty_ldisc_open(o_tty, o_tty->ldisc);
                if (retval) {
                        tty_ldisc_close(tty, tty->ldisc);
                        return retval;
                }
        }
        return 0;
}

5.3 其他函数

5.3.1 tty_ldisc_get

tty_ldisc_get用于获取一个线路规程的引用,这里首先动态分配一个tty_ldisc结构体,然后从tty_ldiscs全局指针数组获取第0个线路规程操作集,赋值给tty_ldisc的ops成员。

/**
 *      tty_ldisc_get           -       take a reference to an ldisc
 *      @disc: ldisc number
 *
 *      Takes a reference to a line discipline. Deals with refcounts and
 *      module locking counts.
 *
 *      Returns: -EINVAL if the discipline index is not [N_TTY..NR_LDISCS] or
 *                       if the discipline is not registered
 *               -EAGAIN if request_module() failed to load or register the
 *                       the discipline
 *               -ENOMEM if allocation failure
 *
 *               Otherwise, returns a pointer to the discipline and bumps the
 *               ref count
 *
 *      Locking:
 *              takes tty_ldiscs_lock to guard against ldisc races
 */

#if defined(CONFIG_LDISC_AUTOLOAD)
        #define INITIAL_AUTOLOAD_STATE  1
#else
        #define INITIAL_AUTOLOAD_STATE  0
#endif
static int tty_ldisc_autoload = INITIAL_AUTOLOAD_STATE;

static struct tty_ldisc *tty_ldisc_get(struct tty_struct *tty, int disc)
{
        struct tty_ldisc *ld;
        struct tty_ldisc_ops *ldops;

        if (disc < N_TTY || disc >= NR_LDISCS)
                return ERR_PTR(-EINVAL);

        /*
         * Get the ldisc ops - we may need to request them to be loaded
         * dynamically and try again.
         */
        ldops = get_ldops(disc);   // 获取 tty_ldiscs[disc]  tty_ldiscs是指针数组,每个元素指向struct tty_ldisc_ops 
        if (IS_ERR(ldops)) {
                if (!capable(CAP_SYS_MODULE) && !tty_ldisc_autoload)
                        return ERR_PTR(-EPERM);
                request_module("tty-ldisc-%d", disc);
                ldops = get_ldops(disc);
                if (IS_ERR(ldops))
                        return ERR_CAST(ldops);
        }

        /*
         * There is no way to handle allocation failure of only 16 bytes.
         * Let's simplify error handling and save more memory.
         */
        ld = kmalloc(sizeof(struct tty_ldisc), GFP_KERNEL | __GFP_NOFAIL);  // 动态分配tty设备线路规程
        ld->ops = ldops;
        ld->tty = tty;

        return ld;
}

那tty_ldiscs全局指针数组是在何时初始化的呢?实际上tty_ldiscs是在drivers/tty/n_tty.c文件中以模块形式注入内核的。

void __init n_tty_init(void)
{
        tty_register_ldisc(N_TTY, &n_tty_ops);  //N_TTY = 0
}/* Line disc dispatch table */
static struct tty_ldisc_ops *tty_ldiscs[NR_LDISCS];  // 长度为30

/**
 *      tty_register_ldisc      -       install a line discipline
 *      @disc: ldisc number
 *      @new_ldisc: pointer to the ldisc object
 *
 *      Installs a new line discipline into the kernel. The discipline
 *      is set up as unreferenced and then made available to the kernel
 *      from this point onwards.
 *
 *      Locking:
 *              takes tty_ldiscs_lock to guard against ldisc races
 */

int tty_register_ldisc(int disc, struct tty_ldisc_ops *new_ldisc)
{
        unsigned long flags;
        int ret = 0;

        if (disc < N_TTY || disc >= NR_LDISCS)
                return -EINVAL;

        raw_spin_lock_irqsave(&tty_ldiscs_lock, flags);
        tty_ldiscs[disc] = new_ldisc;
        new_ldisc->num = disc;
        new_ldisc->refcount = 0;
        raw_spin_unlock_irqrestore(&tty_ldiscs_lock, flags);

        return ret;
}

这里默认初始化了N_TTY线路规程,即将tty_ldiscs第0个元素设置为&n_tty_ops,比如对于串口驱动所使用的的线路规程操作集就为这个。

static struct tty_ldisc_ops n_tty_ops = {
        .magic           = TTY_LDISC_MAGIC,
        .name            = "n_tty",
        .open            = n_tty_open,
        .close           = n_tty_close,
        .flush_buffer    = n_tty_flush_buffer,
        .read            = n_tty_read,
        .write           = n_tty_write,
        .ioctl           = n_tty_ioctl,
        .set_termios     = n_tty_set_termios,
        .poll            = n_tty_poll,
        .receive_buf     = n_tty_receive_buf,
        .write_wakeup    = n_tty_write_wakeup,
        .receive_buf2    = n_tty_receive_buf2,
};
5.3.2 tty_ldisc_open

tty_ldisc_open函数用于打开一个tty设备的线路规程,其本质就是调用线路规程ld的操作集中的open函数,即n_tty_open函数;

/**
 *      tty_ldisc_open          -       open a line discipline
 *      @tty: tty we are opening the ldisc on
 *      @ld: discipline to open
 *
 *      A helper opening method. Also a convenient debugging and check
 *      point.
 *
 *      Locking: always called with BTM already held.
 */

static int tty_ldisc_open(struct tty_struct *tty, struct tty_ldisc *ld)
{
        WARN_ON(test_and_set_bit(TTY_LDISC_OPEN, &tty->flags));
        if (ld->ops->open) {   
                int ret;
                /* BTM here locks versus a hangup event */
                ret = ld->ops->open(tty);
                if (ret)
                        clear_bit(TTY_LDISC_OPEN, &tty->flags);

                tty_ldisc_debug(tty, "%p: opened\n", ld);
                return ret;
        }
        return 0;
}

六、tty_write

当应用向tty设备节点写入数据的时候,还是先对应到字符设备的write的调用,也就是tty_write。下图为tty_write函数调用过程,后面我们一一分析:

6.1 tty_write

函数定义在drivers/tty/tty_io.c:

/**
 *      tty_write               -       write method for tty device file
 *      @file: tty file pointer
 *      @buf: user data to write
 *      @count: bytes to write
 *      @ppos: unused
 *
 *      Write data to a tty device via the line discipline.
 *
 *      Locking:
 *              Locks the line discipline as required
 *              Writes to the tty driver are serialized by the atomic_write_lock
 *      and are then processed in chunks to the device. The line discipline
 *      write method will not be invoked in parallel for each device.
 */

static ssize_t tty_write(struct file *file, const char __user *buf,
                                                size_t count, loff_t *ppos)
{
        struct tty_struct *tty = file_tty(file);  // 从文件描述符file的私有数据结构中获得tty_struct
        struct tty_ldisc *ld;
        ssize_t ret;

        if (tty_paranoia_check(tty, file_inode(file), "tty_write"))
                return -EIO;
        if (!tty || !tty->ops->write || tty_io_error(tty))
                        return -EIO;
        /* Short term debug to catch buggy drivers */
        if (tty->ops->write_room == NULL)
                tty_err(tty, "missing write_room method\n");
        ld = tty_ldisc_ref_wait(tty);  // 尝试获取信号量,获取成功返回tty设备的线路规程 如果获取不到返回NULL
        if (!ld)
                return hung_up_tty_write(file, buf, count, ppos);
        if (!ld->ops->write)
                ret = -EIO;
        else
                ret = do_tty_write(ld->ops->write, tty, file, buf, count);
        tty_ldisc_deref(ld);  // 释放信号量
        return ret;
}

调用到了 do_tty_write,第一个参数是线路规程的write,也就是ld为N_TTY规程的write,我们来看看 do_tty_write。

6.2 do_tty_write

/*
 * Split writes up in sane blocksizes to avoid
 * denial-of-service type attacks
 */
static inline ssize_t do_tty_write(
        ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t),
        struct tty_struct *tty,
        struct file *file,
        const char __user *buf,
        size_t count)
{
        ssize_t ret, written = 0;
        unsigned int chunk;

        ret = tty_write_lock(tty, file->f_flags & O_NDELAY);   // 非堵塞I/O
        if (ret < 0)
                return ret;

        /*
         * We chunk up writes into a temporary buffer. This
         * simplifies low-level drivers immensely, since they
         * don't have locking issues and user mode accesses.
         *
         * But if TTY_NO_WRITE_SPLIT is set, we should use a
         * big chunk-size..
         *
         * The default chunk-size is 2kB, because the NTTY
         * layer has problems with bigger chunks. It will
         * claim to be able to handle more characters than
         * it actually does.
         *
         * FIXME: This can probably go away now except that 64K chunks
         * are too likely to fail unless switched to vmalloc...
         */
        chunk = 2048;                                   // 数据切片大小
        if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))  // 写数据时不分批标志
                chunk = 65536;
        if (count < chunk)
                chunk = count;

        /* write_buf/write_cnt is protected by the atomic_write_lock mutex */
        if (tty->write_cnt < chunk) {         // 写数据大小不足chunk
                unsigned char *buf_chunk;

                if (chunk < 1024)
                        chunk = 1024;

                buf_chunk = kmalloc(chunk, GFP_KERNEL);  // 在内核申请缓冲区,用于保存需要写的数据
                if (!buf_chunk) {
                        ret = -ENOMEM;
                        goto out;
                }
                kfree(tty->write_buf);
                tty->write_cnt = chunk;       // 写缓冲区大小
                tty->write_buf = buf_chunk;   // 写缓冲区
        }
        /* Do the write .. */
        for (;;) {                           // 对数据进行切片,分批写入,每次写入大小为chunk  
                size_t size = count;
                if (size > chunk)           
                        size = chunk;
                ret = -EFAULT;
                if (copy_from_user(tty->write_buf, buf, size))  // 从用户空间拷贝数据到内核空间
                        break;
                ret = write(tty, file, tty->write_buf, size);    // 写入tty设备  
                if (ret <= 0)
                        break;
                written += ret;  // 已经写入数据大小
                buf += ret;       // 下一次传输需要写入的数据
                count -= ret;     // 剩余需要写入大小
                if (!count)
                        break;
                ret = -ERESTARTSYS;
                if (signal_pending(current))  // 如果有信号处理,跳出处理信号,然后便会进入对应的signal处理函数
                        break;
                cond_resched();  // 进程调度
        }
        if (written) {
                tty_update_time(&file_inode(file)->i_mtime);
                ret = written;
        }
out:
        tty_write_unlock(tty);
        return ret;
}

do_tty_write 做了几个事情:

  • 首先计算出一个叫做chunk的值,这个值用于限制每次最大从用户空间copy多少数据到内核空间;这里实际上就是考虑一次写入大量数据的时候,比如数据为5kb时,将需要写入的数据进行切片,然后分片写入,第一次传入2kb,第二次传输2kb,第三次传输1kb;
  • 调用copy_from_user循环拷贝用户的buffer数据到内核空间的 tty->write_buf;
  • 调用write(ld->ops_write)将数据传递到下一层,也就是线路规程;
  • 调用signal_pending函数进行信号处理;该函数仅检查当前进程是否有信号处理(不会在这里处理信号),返回不为0表示有信号需要处理;
  • 调用cond_resched主动让出CPU,下次被调度的时候,继续执行,判断用户需要传输的数据是否传输完毕,未完成,则继续走2~5的流程;

那么接下来我们要看到 N_TTY 线路规程的wirte函数了,即n_tty_write。

6.3 n_tty_write

n_tty_write的实现定义在drivers/tty/n_tty.c:

/**
 *      n_tty_write             -       write function for tty
 *      @tty: tty device
 *      @file: file object
 *      @buf: userspace buffer pointer
 *      @nr: size of I/O
 *
 *      Write function of the terminal device.  This is serialized with
 *      respect to other write callers but not to termios changes, reads
 *      and other such events.  Since the receive code will echo characters,
 *      thus calling driver write methods, the output_lock is used in
 *      the output processing functions called here as well as in the
 *      echo processing function to protect the column state and space
 *      left in the buffer.
 *
 *      This code must be sure never to sleep through a hangup.
 *
 *      Locking: output_lock to protect column state and space left
 *               (note that the process_output*() functions take this
 *                lock themselves)
 */

static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
                           const unsigned char *buf, size_t nr)
{
        const unsigned char *b = buf;
        DEFINE_WAIT_FUNC(wait, woken_wake_function);  // 定义一个等待队列元素wait_queue_t,func设置为woken_wake_function,private为当前进程
        int c;
        ssize_t retval = 0;

        /* Job control check -- must be done at start (POSIX.1 7.1.1.4). */
        if (L_TOSTOP(tty) && file->f_op->write != redirected_tty_write) {
                retval = tty_check_change(tty);
                if (retval)
                        return retval;
        }

        down_read(&tty->termios_rwsem);   // 获取信号量,获取不到,则进程进入睡眠状态,为了解决多个进程同时竞争一个tty设备的问题

        /* Write out any echoed characters that are still pending */
        process_echoes(tty);

        add_wait_queue(&tty->write_wait, &wait);  // 实现将等待队列元素wait插入等待队列第一个元素的位置
        while (1) {
                if (signal_pending(current)) {    // 信号处理
                        retval = -ERESTARTSYS;
                        break;
                }
                if (tty_hung_up_p(file) || (tty->link && !tty->link->count)) {
                        retval = -EIO;
                        break;
                }
                if (O_OPOST(tty)) {
                        while (nr > 0) {
                                ssize_t num = process_output_block(tty, b, nr);
                                if (num < 0) {
                                        if (num == -EAGAIN)
                                                break;
                                        retval = num;
                                        goto break_out;
                                }
                                b += num;
                                nr -= num;
                                if (nr == 0)
                                        break;
                                c = *b;
                                if (process_output(c, tty) < 0)
                                        break;
                                b++; nr--;
                        }
                        if (tty->ops->flush_chars)
                                tty->ops->flush_chars(tty);
                } else {
                        struct n_tty_data *ldata = tty->disc_data;

                        while (nr > 0) {
                                mutex_lock(&ldata->output_lock);
                                c = tty->ops->write(tty, b, nr);  // 以串口驱动为例,ops被设置为uart_ops,open函数为uart_write
                                mutex_unlock(&ldata->output_lock);
                                if (c < 0) {
                                        retval = c;
                                        goto break_out;
                                }
                                if (!c)  // 该次写入返回0,写入失败
                                        break;
                                b += c;
                                nr -= c;  // 剩余需要写入的数据大小
                        }
                }
                if (!nr)  // 0 无数据需要写入  
                        break;
                if (tty_io_nonblock(tty, file)) {  // 非堵塞I/O
                        retval = -EAGAIN;
                        break;
                }
                up_read(&tty->termios_rwsem);   // 释放信号量

                wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);  // 将当前进程的状态设置成TASK_INTERRUPTIBLE,并调用schedule_timeout(timeout),让出CPU
// 进程睡眠后不主动唤醒
down_read(
&tty->termios_rwsem); // 再次获取信号量 } break_out: remove_wait_queue(&tty->write_wait, &wait); // 从等待队列中移除wait if (nr && tty->fasync) set_bit(TTY_DO_WRITE_WAKEUP, &tty->flags); up_read(&tty->termios_rwsem); // 释放信号量 return (b - buf) ? b - buf : retval; }

在n_tty_write中:

  • 定义了等待队列元素,将当前进程加入到tty->write_wait写等待队列;
  • 调用 tty->ops->write(tty, b, nr),进行循环写入;以串口驱动为例,ops被设置为uart_ops,write函数为uart_write。
  • 如果数据写入失败,并且是堵塞模式,调用wait_woken,设置当前进程为TASK_INTERRUPTIBLE类型(可打断),让出CPU,超时时间设置为MAX_SCHEDULE_TIMEOUT;
  • 从等待队列中移除当前进程;

七、tty_read

当应用从tty设备节点读取数据的时候,还是先对应到字符设备的read的调用,也就是tty_read。下图为tty_write函数调用过程,后面我们一一分析:

 

7.1 tty_read

函数定义在drivers/tty/tty_io.c:

/**
 *      tty_read        -       read method for tty device files
 *      @file: pointer to tty file
 *      @buf: user buffer
 *      @count: size of user buffer
 *      @ppos: unused
 *
 *      Perform the read system call function on this terminal device. Checks
 *      for hung up devices before calling the line discipline method.
 *
 *      Locking:
 *              Locks the line discipline internally while needed. Multiple
 *      read calls may be outstanding in parallel.
 */

static ssize_t tty_read(struct file *file, char __user *buf, size_t count,
                        loff_t *ppos)
{
        int i;
        struct inode *inode = file_inode(file);
        struct tty_struct *tty = file_tty(file);  // 从文件描述符file的私有数据结构中获得tty_struct
        struct tty_ldisc *ld;

        if (tty_paranoia_check(tty, inode, "tty_read"))
                return -EIO;
        if (!tty || tty_io_error(tty))
                return -EIO;

        /* We want to wait for the line discipline to sort out in this
           situation */
        ld = tty_ldisc_ref_wait(tty);  // 尝试获取信号量,获取成功返回tty设备的线路规程 如果获取不到返回NULL
        if (!ld)
                return hung_up_tty_read(file, buf, count, ppos);
        if (ld->ops->read)
                i = ld->ops->read(tty, file, buf, count); // 直接调用n_tty_read
        else
                i = -EIO;
        tty_ldisc_deref(ld);   // 释放信号量

        if (i > 0)
                tty_update_time(&inode->i_atime);

        return i;
}

tty_read直接调用了ld->ops->read函数,也就是n_tty_read。

7.2 n_tty_read

n_tty_read函数位于drivers/tty/n_tty.c:

/**
 *      n_tty_read              -       read function for tty
 *      @tty: tty device
 *      @file: file object
 *      @buf: userspace buffer pointer
 *      @nr: size of I/O
 *
 *      Perform reads for the line discipline. We are guaranteed that the
 *      line discipline will not be closed under us but we may get multiple
 *      parallel readers and must handle this ourselves. We may also get
 *      a hangup. Always called in user context, may sleep.
 *
 *      This code must be sure never to sleep through a hangup.
 *
 *      n_tty_read()/consumer path:
 *              claims non-exclusive termios_rwsem
 *              publishes read_tail
 */

static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
                         unsigned char __user *buf, size_t nr)
{
        struct n_tty_data *ldata = tty->disc_data;  // 接收到的数据
        unsigned char __user *b = buf;              // 用户空间读缓冲区  
        DEFINE_WAIT_FUNC(wait, woken_wake_function);   // 定义一个等待队列元素wait_queue_t,func设置为woken_wake_function,private为当前进程
        int c;
        int minimum, time;
        ssize_t retval = 0;
        long timeout;
        int packet;
        size_t tail;

        c = job_control(tty, file);
        if (c < 0)
                return c;

        /*
         *      Internal serialization of reads.
         */
        if (file->f_flags & O_NONBLOCK) {  //  用于一些数据的初始化和校验。把数据拷贝到用户空间分两种情况处理:一种是标准模式,另一种是非标准模式。
                                           //  在标准模式下,如果没有设置O_NONBLOCK,读操作只有在遇到文件结束符或者各行的字符都已编辑完毕后才返回。
                if (!mutex_trylock(&ldata->atomic_read_lock))
                        return -EAGAIN;
        } else {
                if (mutex_lock_interruptible(&ldata->atomic_read_lock))
                        return -ERESTARTSYS;
        }

        down_read(&tty->termios_rwsem);  // 获取信号量,获取不到,则进程进入睡眠状态,为了解决多个进程同时竞争一个tty设备的问题

        minimum = time = 0;
        timeout = MAX_SCHEDULE_TIMEOUT;
        if (!ldata->icanon) {
                minimum = MIN_CHAR(tty); // 获取termios.c_cc[VMIN]数组的值,作为本次读取操作能够读取到的最大数据量;
                if (minimum) {
                        time = (HZ / 10) * TIME_CHAR(tty);
                } else {
                        timeout = (HZ / 10) * TIME_CHAR(tty);
                        minimum = 1; // 设置minimum的值是1,即缓冲区中的数据量超过1个,就要唤醒读取进程;
                }
        }
        packet = tty->packet;
        tail = ldata->read_tail;      //  指向缓冲区当前可以写入的第一个地址;

        add_wait_queue(&tty->read_wait, &wait);   // 实现将等待队列元素wait插入等待队列第一个元素的位置
        while (nr) {    // 数据未读取完
                /* First test for status change. */
                if (packet && tty->link->ctrl_status) {
                        unsigned char cs;
                        if (b != buf)
                                break;
                        spin_lock_irq(&tty->link->ctrl_lock);
                        cs = tty->link->ctrl_status;
                        tty->link->ctrl_status = 0;
                        spin_unlock_irq(&tty->link->ctrl_lock);
                        if (put_user(cs, b)) {
                                retval = -EFAULT;
                                break;
                        }
                        b++;
                        nr--;
                        break;
                }

                if (!input_available_p(tty, 0)) {  // 判断是否有数据可以读取,如果没有进入该分支
                        up_read(&tty->termios_rwsem);      // 释放信号量
                        tty_buffer_flush_work(tty->port);  // 将数据从tty缓冲区刷新到线路规程 
                        down_read(&tty->termios_rwsem);    // 获取信号量,获取不到,则进程进入睡眠状态,为了解决多个进程同时竞争一个tty设备的问题
                        if (!input_available_p(tty, 0)) {  // 判断是否有数据可以读取,如果没有进入该分支
                                if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {
                                        retval = -EIO;
                                        break;
                                }
                                if (tty_hung_up_p(file))
                                        break;
                                /*
                                 * Abort readers for ttys which never actually
                                 * get hung up.  See __tty_hangup().
                                 */
                                if (test_bit(TTY_HUPPING, &tty->flags))
                                        break;
                                if (!timeout)
                                        break;
                                if (tty_io_nonblock(tty, file)) {  // 非堵塞模式,读取不到数据直接返回
                                        retval = -EAGAIN;
                                        break;
                                }
                                if (signal_pending(current)) {    // 信号处理
                                        retval = -ERESTARTSYS;
                                        break;
                                }
                                up_read(&tty->termios_rwsem);    // 释放信号量

                                timeout = wait_woken(&wait, TASK_INTERRUPTIBLE,
                                                timeout);  // 将当前进程的状态设置成TASK_INTERRUPTIBLE,并调用schedule_timeout(timeout),让出CPU,进程睡眠后不主动唤醒

                                down_read(&tty->termios_rwsem); // 获取信号量,获取不到,则进程进入睡眠状态,为了解决多个进程同时竞争一个tty设备的问题
                                continue;
                        }
                }
           // 在堵塞模式下,如果缓冲区中没有数据可以读取,当前进程要休眠等待,直到缓冲区有数据可以读取时才会被唤醒;
           // 假定此时缓冲区中没有数据,当前进程进入休眠;之后缓冲区有数据时,当前进程被唤醒并调度运行;
            if (ldata->icanon && !L_EXTPROC(tty)) {
           //  在规范模式下,缓冲区中的字符是经过加工了的,要累积到一个缓冲行才会唤醒等待读出的进程(缓冲行,即碰到’\n’字符);
            //  此时的读取操作在canon_copy_from_read_buf()函数中完成
                        retval = canon_copy_from_read_buf(tty, &b, &nr);  // 重点
                        if (retval)
                                break;
                } else {
                        int uncopied;

                        /* Deal with packet mode. */
                        if (packet && b == buf) {
                                if (put_user(TIOCPKT_DATA, b)) {
                                        retval = -EFAULT;
                                        break;
                                }
                                b++;
                                nr--;
                        }
                      // 在非规范模式下,缓冲区中的字符是未经加工的,不存在缓冲行的概念,在原始模式可以把字符’\0’复制到用户空间,这里使用copy_from_read_buf()函数进行成片的拷贝;
                       // 由于缓冲区是环形的,缓冲的字符可能跨越环形缓冲区的结尾,被分割成两部分,所以要使用copy_from_read_buf()函数两次;
                        uncopied = copy_from_read_buf(tty, &b, &nr); 
                        uncopied += copy_from_read_buf(tty, &b, &nr);   
                        if (uncopied) {
                                retval = -EFAULT;
                                break;
                        }
                }

                n_tty_check_unthrottle(tty);

                if (b - buf >= minimum)
                        break;
                if (time)
                        timeout = time;
        }
        if (tail != ldata->read_tail)
                n_tty_kick_worker(tty);
        up_read(&tty->termios_rwsem);  // 释放信号量

        remove_wait_queue(&tty->read_wait, &wait);  // 从等待队列移除wait
        mutex_unlock(&ldata->atomic_read_lock);

        if (b - buf)
                retval = b - buf;

        return retval;
}

内容很多,大致的内容是:

  • 定义了等待队列元素,将当前进程加入到tty->write_wait写等待队列;
  • check有没有可用的数据没有的话:
    • 非堵塞模式,直接返回、;
    • 堵塞模式下,设置当前进程为TASK_INTERRUPTIBLE,并调用schedule_timeout进入睡眠;那什么时候会唤醒该进程呢,以串口驱动为例,一般当串口有数据来的时候会通过 wake_up_interruptible(&tty->read_wait)唤醒该进程,从而继续读取数据;
  • 有数据的话:
    • 在规范模式,调用canon_copy_from_read_buf把数据通过copy_to_user给用户空间;
    • 在非规范模式,调用copy_from_read_buf函数两次将数据拷贝到用户空间;
  • 从等待队列移除当前进程;

7.3 canon_copy_from_read_buf 

canon_copy_from_read_buf函数定义在drivers/tty/n_tty.c,canon_copy_from_read_buf函数只有在规范模式下会被调用,该函数按缓冲行将数据从tty缓冲区中读取到用户空间;通过copy_to_user函数完成数据拷贝:

/**
 *      canon_copy_from_read_buf        -       copy read data in canonical mode
 *      @tty: terminal device
 *      @b: user data
 *      @nr: size of data
 *
 *      Helper function for n_tty_read.  It is only called when ICANON is on;
 *      it copies one line of input up to and including the line-delimiting
 *      character into the user-space buffer.
 *
 *      NB: When termios is changed from non-canonical to canonical mode and
 *      the read buffer contains data, n_tty_set_termios() simulates an EOF
 *      push (as if C-d were input) _without_ the DISABLED_CHAR in the buffer.
 *      This causes data already processed as input to be immediately available
 *      as input although a newline has not been received.
 *
 *      Called under the atomic_read_lock mutex
 *
 *      n_tty_read()/consumer path:
 *              caller holds non-exclusive termios_rwsem
 *              read_tail published
 */

static int canon_copy_from_read_buf(struct tty_struct *tty,
                                    unsigned char __user **b,  // 用户空间缓冲区
                                    size_t *nr)                // 需要读取的字节数 
{
        struct n_tty_data *ldata = tty->disc_data;  // 接收到的数据
        size_t n, size, more, c;
        size_t eol;
        size_t tail;
        int ret, found = 0;

        /* N.B. avoid overrun if nr == 0 */
        if (!*nr)
                return 0;

        n = min(*nr + 1, smp_load_acquire(&ldata->canon_head) - ldata->read_tail);

        tail = ldata->read_tail & (N_TTY_BUF_SIZE - 1); 
        size = min_t(size_t, tail + n, N_TTY_BUF_SIZE); 

        n_tty_trace("%s: nr:%zu tail:%zu n:%zu size:%zu\n",
                    __func__, *nr, tail, n, size);

        eol = find_next_bit(ldata->read_flags, size, tail);
        more = n - (size - tail);
        if (eol == N_TTY_BUF_SIZE && more) {
                /* scan wrapped without finding set bit */
                eol = find_next_bit(ldata->read_flags, more, 0);
                found = eol != more;
        } else
                found = eol != size;

        n = eol - tail;
        if (n > N_TTY_BUF_SIZE)
                n += N_TTY_BUF_SIZE;
        c = n + found;

        if (!found || read_buf(ldata, eol) != __DISABLED_CHAR) {
                c = min(*nr, c);
                n = c;
        }

        n_tty_trace("%s: eol:%zu found:%d n:%zu c:%zu tail:%zu more:%zu\n",
                    __func__, eol, found, n, c, tail, more);

        ret = tty_copy_to_user(tty, *b, tail, n);
        if (ret)
                return -EFAULT;
        *b += n;
        *nr -= n;

        if (found)
                clear_bit(eol, ldata->read_flags);
        smp_store_release(&ldata->read_tail, ldata->read_tail + c);

        if (found) {
                if (!ldata->push)
                        ldata->line_start = ldata->read_tail;
                else
                        ldata->push = 0;
                tty_audit_push();
        }
        return 0;
}

无论是规范还是不规范的模式下,都是从ldata->read_buf[xxx]里获取数据传到用户空间的,那么是什么时候或是什么函数将数据填入这个数组中呢?

以S3C2440串口驱动为例,读数据是会调用中断,通过tty_schedule_flip将数据搬至线路规程层。

参考文章

[1]S3C2440 Linux UART 串口驱动-----1

[2]Linux UART 驱动 Part-1 (底层对接)

[3]Linux UART 驱动 Part-2 (tty 层流程)

[4]Linux串口驱动分析及移植

[5]Linux系统TTY串口驱动实例详解

[6]Console & TTY Driver

[7]彻底理解Linux的各种终端类型以及概念

[8]tty驱动初步了解学习

[9]解密TTY

[10]Linux 终端(TTY)

[11]Linux 伪终端(pty)

[12]终端、Shell、tty 和控制台(console)有什么区别?

[13]TTY 到底是个什么玩意?

[14]你知道Linux 终端、终端模拟器和伪终端之间的区别吗?(推荐)

[15]扫盲 Linux&UNIX 命令行——从“电传打字机”聊到“shell 脚本编程”

[17]基于Linux的tty架构及UART驱动详解

[18]一文彻底讲清Linux tty子系统架构及编程实例

posted @ 2023-03-05 23:37  大奥特曼打小怪兽  阅读(457)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步