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

2023-11-02

【摘要】本文详细解读了linux系统下的tty子系统的深层次原理和架构,并参考了LDD3中的代码实例讲述了无硬件下实现一个简单的tty设备驱动模块的编写。对了解tty子系统及下部串口驱动模块的学习有较好的参考价值。

1、tty设备简介

tty一词源于Teletypes,或Teletypewriters,它是最早出现的一种终端设备,类似电传打字机,由Teletype公司生产。最初tty是指连接到Unix系统上的物理或者虚拟终端。终端是一种字符型设备,通常使用tty来统称各种类型的终端设备。随着时间的推移,当通过串行口能够建立起终端连接后,这个名字也用来指任何的串口设备。

它还有多种类,例如串口(ttySn、ttySACn、ttyOn)、USB到串口的转换器(ttyUSBn),还有需要特殊处理才能正常工作的调制解调器(比如传统的WinModem类设备)等。tty虚拟设备支持虚拟控制台,它能通过键盘及网络连接或者通过xterm会话登录到计算机上。

其实起初终端和控制台都不是个人电脑的概念,而是多人共用的小型中型大型计算机上的概念。终端为主机提供了人机接口,每个人都通过终端使用主机的资源。终端有字符终端和图形终端两种。一台主机可以连很多终端。控制台是一种特殊的人机接口, 是人控制主机的第一人机接口。

而主机对于控制台的信任度高于其他终端。对此还可以结合内核启动代码中init进程打开/dev/console和执行两次sys_dup(0),以及标准输入、标准输出、标准出错,还有就是进程fork后的标准输入输出的复制情况来一起理解。而个人计算机只有控制台,没有终端。当然愿意的话,可以在串口上连一两台字符哑终端。

但是linux按POSIX标准把个人计算机当成小型机来用,在控制台上通过getty软件虚拟了六个字符哑终端(或者叫虚拟控制台终端tty1-tty6)(数量可以在/etc/inittab里自己调整)和一个图型终端,在虚拟图形终端中又可以通过软件(如rxvt)再虚拟无限多个伪终端(pts/0等)

但这全是虚拟的,虽然用起来一样,但实际上没有物理实体。所以在个人计算机上,只有一个实际的控制台,没有终端,所有终端都是在控制台上用软件模拟的。要把个人计算机当主机再通过串口或网卡外连真正的物理终端也可以,论成本,谁又会怎么做呢。

终端按照其自身能力分类,可以分为:

  1. 哑终端(瘦客户端)

早期的计算机终端是通过串行RS-232通信的,它只能解释有限数量的控制码(CR,LF等),但没有能力处理执行特殊的转义序列功能(如清行、清屏或控制光标的位置)。简单来说就是处理能力有限的终端机,他们一般基本上只具有和机械电传打字机类似的有限功能。这种类型的终端称为哑终端。

现在仍然在现代类Unix系统上得到支持,通过设置环境变量TERM=dumb。哑终端有时用来指任何类型的通过RS-232连接的传统计算机终端,不对数据进行本地处理或本地执行用户程序的串行通信终端。哑终端有时也指功能有限,只有单色文本处理能力或直接传输每一个键入的字符而不等待主机轮询的公共计算机终端。

  1. 智能终端(胖客户端)

智能终端就是有能力处理转义序列,也就是说处理能力较强的终端机。


Linux系统的终端设备一般分为控制台、伪终端pty、串口终端(/dev/ttySn)和其它类型4种。

1.控制台

1.1 系统控制台(/dev/console)

/dev/console是系统控制台,是与操作系统交互的设备。系统所产生的信息会发送到该设备上。平时我们看到的PC只有一个屏幕和键盘,它其实就是控制台。目前只有在单用户模式下,才允许用户登录控制台/dev/console。(可以在单用户模式下输入tty命令进行确认)。

console有缓冲的概念,为内核提供打印输出。内核把要打印的内容装入缓冲区__log_buff,然后由console来决定打印到哪里(比如是tty0还是ttySn等)。console指向激活的终端。历史上,console指主机本身的屏幕和键盘,而tty指用电缆链接的其它位置的控制台。

某些情况下console和tty0是一致的,就是当前所使用的是虚拟终端,也是激活虚拟终端。所以有些资料中称/dev/console是到/dev/tty0的符号链接,但是这样说现在看来是不对的:根据内核文档,在2.1.71之前,/dev/console根据不同系统设定,符号链接到/dev/tty0或者其他tty*上,在2.1.71版本之后则完全由内核代码内部控制它的映射。

如果一个终端设备要实现console功能,必须向内核注册一个struct console结构,一般的串口驱动中都会有。如果设备要实现tty功能,必须要向内核的tty子系统注册一个struct tty_driver结构,注册函数在drivers/tty/tty_io.c中。一个设备可以同时实现console和tty_driver,一般串口都这么做。

1.2 当前控制台(/dev/tty)

这是应用程序中的概念,如果当前进程有控制终端(Controlling Terminal),那么/dev/tty就是当前进程控制台的设备文件。对于你登录的shell,/dev/tty就是你使用的控制台,设备号是(5,0)。不过它并不指任何物理意义上的控制台,/dev/tty会映射到当前设备(使用命令tty可以查看它具体对应哪个实际物理控制台设备)。输出到/dev/tty的内容只会显示在当前工作终端上(无论是登录在ttyn中还是pty中)。

你如果在控制台界面下(即字符界面下)那么dev/tty就是映射到dev/tty1-6之间的一个(取决于你当前的控制台号),但是如果你现在是在图形界面(Xwindows),那么你会发现现在的/dev/tty映射到的是/dev/pts的伪终端上。/dev/tty有些类似于到实际所使用终端设备的一个联接

你可以输入命令tty,显示当前映射终端如:/dev/tty1或者/dev/pts/0等。也可以使用命令ps -ax来查看其他进程与哪个控制终端相连。

在当前终端中输入 echo “tekkaman” > /dev/tty ,都会直接显示在当前的终端中。

1.3 虚拟控制台 (/dev/ttyn)

/dev/ttyn是进程虚拟控制台,他们共享同一个真实的物理控制台。如果在进程里打开一个这样的文件且该文件不是其他进程的控制台时,那该文件就是这个进程的控制台。

进程printf数据会输出到这里。在PC上,用户可以使用alt+Fn切换控制台,现在不知道怎么回事我用Ctrl + Alt + Fn才能切换。这没具体看过为啥。可能是Linux没有继承UNIX这方面的传统罢了。看起来感觉存在多个屏幕,这种虚拟控制台对应tty1~n,其中:/dev/tty1代表第1个虚拟控制台;当使用ALT+F2进行切换时,系统的虚拟控制台为/dev/tty2 ,当前控制台(/dev/tty)则指向/dev/tty2

在UNIX系统中,计算机显示器通常被称为控制台(console)。它仿真了类型为Linux的一种终端,并且有一些设备特殊文件与之相关联:tty0、tty1、tty2等。当你在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。

读者可以登录到不同的虚拟控制台上去,因而可以让系统同时有几个不同的会话存在。

比较特殊的是/dev/tty0,他代表当前虚拟控制台,其实就是当前所使用虚拟控制台的一个别名。因此不管当前正在使用哪个虚拟控制台(注意:这里是虚拟控制台,不包括伪终端),系统信息都会重定位到/dev/tty0上。

只有系统或超级用户root可以向/dev/tty0进行写操作。tty0是系统自动打开的,但不用于用户登录。在Framebuffer设备没有启用的系统中,可以使用/dev/tty0访问显卡。

image-20221021232808244

2. 伪终端pty

伪终端(Pseudo Terminal)是终端的发展,为满足现在需求(比如网络登陆、xwindow窗口的管理)。它是成对出现的逻辑终端设备(即master和slave设备, 对master的操作会反映到slave上)。它多用于模拟终端程序,是远程登陆(telnet、ssh、xterm等)后创建的控制台设备。

简单说主终端和类似sshd,telnetd等用户空间的远程协议处理进程连接,而从终端则和shell之类的实际进程连接。在处理远程登录的时候,一般都是由远程协议处理进程打开主终端和从终端,然后就在远程网络终端和本机shell之间建立了一条双向通道(远程网络终端(套接字)<—>本机协议处理进程<—>主终端<—>从终端<—>shell)。

在这个“打开主从终端建立连接”的语义以及其实现上,有着不同的标准,总的来说有三种方式,分别是SVR4的方式,BSD的方式以及linux的方式。

在“建立连接”的语义上SVR4的方式使用“流”来建立这条连接,而BSD和linux则是自动建立的。

在“打开主从终端”的语义上,SVR4和linux是自动确定主终端并打开主终端后自动确定从终端,而BSD则必须手工确定和打开主终端。

可见linux处理伪终端的方式是结合SVR4和BSD两种UNIX标准的结果,linux不仅实现这种有意义的最佳组合,而且分别实现了SRV和BSD的两种方式的接口,如果编译CONFIG_LEGACY_PTYS宏,则可以使用BSD的方式,如果编译CONFIG_UNIX98_PTYS,则实现SRV4的接口。

  • BSD接口:较简单,master为/dev/pty [p-za-e] [0-9a-f];slave为 /dev/tty [p-za-e] [0-9a-f] ,它们都是配对的出现的。例如/dev/ptyp3和/dev/ttyp3。但由于在编程时要找到一个合适的终端需要逐个尝试,所以逐渐被放弃。
  • Unix 98接口(SRV4):仅使用一个**/dev/ptmx作为master设备,任何sshd,telnetd之类的进程都可以只使用这一个终端设备文件,在每次打开操作时会得到一个master设备fd,并在/dev/pts/目录下得到一个slave设备(如 /dev/pts/3和/dev/ptmx)**,这样就避免了逐个尝试的麻烦。在ptmx_open中,不仅系统可以自动分配一个主终端,而且还为该主终端绑定了一个从终端,主终端设置到file结构体的private_data字段上,之后诸如sshd,telnetd之类的进程读写/dev/ptmx文件时,虽然它们读写的是同一个文件,可是由于file结构体不再它们之间共享,因此它们取到的file->private_data也就不同了

由于可能有好几千个用户登陆,所以/dev/pts/* 是动态生成的,不象其他设备文件是构建系统时就已经产生的硬盘节点(如果未使用devfs、udev、mdev等) 。**第一个用户登陆,设备文件为/dev/pts/0,第二个为/dev/pts/1,以此类推。**它们并不与实际物理设备直接相关。现在大多数系统是通过此接口实现pty。

我们在X Window下打开的终端或使用telnet或ssh等方式登录Linux主机,此时均通过pty设备。例如,如果某人在网上使用telnet程序连接到你的计算机上,则telnet程序就可能会打开/dev/ptmx设备获取一个fd。此时一个getty程序就应该运行在对应的/dev/pts/* 上。当telnet从远端获取了一个字符时,该字符就会通过ptmx、pts/* 传递给 getty程序,而getty程序就会通过pts/* 、ptmx和telnet程序往网络上返回“login:”字符串信息。这样,登录程序与telnet程序就通过“伪终端”进行通信。

  • telnet<—>/dev/ptmx(master)<—>pts/*(slave)<—>getty

如果一个程序把 pts/* 看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对的另一个/dev/ptmx上,而/dev/ptmx则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行互相交流,这很象是逻辑设备对之间的管道操作。对于pts/* ,任何设计成使用一个串行端口设备的程序都可以使用该逻辑设备。但对于使用/dev/ptmx的程序,则需要专门设计来使用/dev/ptmx逻辑设备。通过使用适当的软件,就可以把两个甚至多个伪终端设备连接到同一个物理串行端口上。

image-20221021235742300

  • Tmux Server打开一对伪终端,自己持有主设备,将次设备继承给它Fork出来的Bash,此一对进程进入后台,不再归属任何终端。
  • 一旦Tmux Client运行于某个SSH终端,它会把当前终端的pts传递给Tmux Server,从而让Tmux Server作为一个数据代理传递输入和输出。
  • 一旦Tmux Client运行,它便成为了当前Bash的前台进程,通过重定向之后,当前Tmux Client便成为了Bash0的前端,接收Bash0的输入和输出。文件句柄转交后的流程如下:
    1. 有人在远端的Windows主机上敲入一个字符“a”;
    2. 字符“a”经由SSH客户端加密后传输到Linux SSH服务器SSHd并解密;
    3. 字符“a”通过SSHd的ptmx写入
    4. Tmux Server从pts/2将字符“a”读出并写入ptmx;
    5. Bash0将字符“a”从pts/1读出并执行;
    6. Bash0将-bash: a: command not found按原路返回给Windows。

3. 串口终端(/dev/ttySn)

串行端口终端(Serial PortTerminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间串行端口设备通常被称为终端设备,那时它的最大用途就是用来连接终端,所以这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0)、/dev/tts/1(或/dev /ttyS1)等,设备号分别是(4,0)、(4,1)等(对应于win系统下的COM1、COM2等)。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。

我们可以在命令行提示符下键入:echo "tekkaman" > /dev/ttyS1会把“tekkaman”发送到连接在ttyS1(COM2)端口的设备上。

在2.6以后的内核后,一些三星的芯片将串口终端设备节点命名为ttySACn。TI的Omap系列芯片从2.6.37开始,芯片自带的UART设备开始使用专有的的omap-uart驱动,故设备节点命名为ttyOn,以区别于使用8250驱动时的设备名“ttySn”。这其中包括笔者用到的这个S3C2440。所以我们在Uboot启动参数中要设置console = ttySAC0才可以。这一句的意思其实就是把ttySAC0当做我们的控制台终端。

image-20221021232849202

4. 其它类型

还针对很多不同的字符设备存在有很多其它种类的终端设备特殊文件,例如针对ISDN设备的**/dev/ttyIn**终端设备等。


2、tty子系统

在Linux kernel中,tty驱动不像于spi,iic等那么架构简单,它是一个庞大的系统,它的框架大体如下图一。我们作为普通的驱动开发移植人员,不会从零写tty驱动,一般都是厂家根据现有的tty驱动和自家芯片修改,拿到板子按照厂家的配置,串口应该使能直接使用的。但是开发的过程中也有可能需要用到串口,一般会修改serial驱动,这样我们不会动tty_core层。

Linux tty子系统包含:tty核心(tty_core),tty线路规程(tty_line_discipine)和tty驱动(tty_driver)。tty核心是对整个tty设备的抽象,对用户提供统一的接口,tty线路规程是对传输数据的格式化,tty驱动则是面向tty设备的硬件驱动。其整体框架如下图所示:

image-20221021212536798

tty 核心从用户获取将要发送给 tty 设备的数据,接着传递它到 tty 线路规程驱动,再传递到 tty 驱动。 这个 tty 驱动转换数据为可以发送给硬件的格式。

从 tty 硬件收到的数据通过 tty 驱动向上回流,先进入 tty 线路规程驱动,再进入 tty 核心, 最后被用户获取。有时 tty 驱动直接和 tty 核心通讯,并且 tty 核心直接发送数据到 tty 驱动。但通常 tty 线路规程在它们 2 者之间修改数据格式以适配某种协议规则(蓝牙、PPP等)。

1. tty核心

tty 核心提供一个非常容易的方式给任何 tty 驱动来维护一个文件在/proc/tty/driver 目录中。如果驱动定义了read_proc 或者 write_proc 函数,这个文件被创建。接着, 任何在这个文件上的读或写调用被发送给这个驱动.
这些函数的格式如同标准的 /proc 文件处理函数:

/* 打印出当前注册的端口号的read_proc函数示例*/
static int tiny_read_proc(char *page, char **start, off_t off, int count, int *eof, void *data)
{
    struct tiny_serial *tiny;
    off_t begin = 0;
    int length = 0;
    int i;
    length += sprintf(page, "tinyserinfo:1.0 driver:%s\n", DRIVER_VERSION);
    for (i = 0; i < TINY_TTY_MINORS && length < PAGE_SIZE; ++i) 
    {
        tiny = tiny_table[i];
        if (tiny == NULL)
            continue;
        length += sprintf(page+length, "%d\n", i);
        if ((length + begin) > (off + count))
            goto done;
        if ((length + begin) < off) {
            begin += length;
            length = 0;
        }
    }
    *eof = 1;
    done:
    if (off >= (length + begin))
        return 0;
    *start = page + (off-begin);
    return (count < begin+length-off) ? count : begin + length-off;
}

当 tty 驱动被注册时, 或者当单个 tty 设备被创建时,tty 核心处理所有的 sysfs 目录和设备创建。 依赖在 struct tty_driver 中的TTY_DRIVER_NO_DEVFS 标志。单个目录一直包含 dev 文件, 它允许用户空间工具来决定分配给设备的主次号。

它还包含一个 device 和 driver 符号连接,如果一个指向有效的 struct device 的指针被传递给读 tty_register_device的调用。除了这 3 个文件, 对单个 tty 驱动不可能在这个位置创建新的sysfs 文件。这个会可能在将来的内核发行中改变。

2. tty线路规程

线路(行)规程规定了键盘,串口,打印机,显示器等输入输出设备和用户态Shell等程序之间的行为规范,键盘上的按键事件被行规程解释成了Shell可以理解的输入并给出相应的输出。人们要想操作计算机,这套规程是必不可少的,它事实上规定了**信息从外部进入计算机的规范**。

当我们在一个终端上按下按键L的时候,终端只是把字母L回显了回来,紧接着按下按键S,依然是回显字母S,随后我们按下回车键,回显的不再是回车键,而是列出并显示了当前目录下的所有文件,这些规则就是线路规程。

可以使用stty命令来展示你的终端行规程的配置。

下图显示了Linux下线路规程在整个体系结构中的位置:

image-20221021231521648

  • SLIP

    • SLIP就是**Serial Line Internet Protocol(串行线路网际协议)**可能现在很少有人再使用它了,毕竟现如今都是以太网和光纤的天下了,谁还会用串口线来传输网络数据包。但是它在以前很长一段时间一直作为连接运行TCP/IP协议的主机的专用链路存在的。

    • 我们知道,TCP/IP是对等层通信协议,但是最终的数据包不得不通过某种物理介质传输,因此还需要一种纵向的协议才可以让对等层通信得以实现。我们把横向的对等层协议叫做**通信协议,而纵向的协议叫做传输协议,行规程事实上就是一种传输协议,SLIP实际上就是一种行规程**,SLIP行规程把串口线上传输的字符解释成IP数据报文并向上递送。这种行规程和TCP/IP的关系如下所示:

      image-20221021232308002

    • 可以看得出,SLIP作为一中行规程,并没有把解析后的数据继续提交到与之绑定的TTY缓冲区,而是将解析后的数据作为IP数据报直接递送给了TCP/IP协议栈来处理。

    • 广义地来讲,其实像以太网规范这种也可以叫做某种行规程,毕竟它也是约定IP层软件和传输介质之间行为规范的协议,起到的均是对数据格式化的作用,但由于如今超高速传输早就完完全全基于比特流序列,不再通过定义以字符为边界的块来封装数据,所以再叫做行规程就有点词不达意了,但本质上都是一样的,都是定义在某种介质上如何把数据包封装的协议。

  • 串口、键盘和显示器在tty系统中的层次

    image-20221022000618863

    • 当通过键盘在命令行输入“ls+回车”后屏幕显示结果的内在过程如下:
      1. keyborad 输入“l”,通过串口发送给驱动;
      2. 驱动发送给line discipline,并在line discipline中的buffer里储存起来。
      3. 然后通过line discipline返回给显示驱动,并在终端显示。
      4. keyborad 输入“s”,之后的过程同l
      5. 最后键盘输入回车,传递到line discipline层后,解析为把之前buffer中的字符串ls作为命令传给应用层的进程执行
      6. 用户进程(shell)将得到的命令结果返回给行规程(line discipline)。
      7. 行规程再返回给显示驱动,并在终端上显示。

应用程序通过open/read/write等标准接口访问硬件。引入行规程之后还要设置行规程,通过/ioctl或者其他封装好的函数去设置行规程。

3. tty驱动/tty设备

有 3 种不同类型 tty 驱动: 控制台串口, 和 pty。控制台和 pty 驱动已经被编写进内核,所以任何使用 tty 核
心来与用户和系统交互的需要我们自己编写新驱动的只有串口驱动。

通过查看/proc/tty/drivers文件,可找到当前被加载到内核中的有哪些类型的 tty 驱动和设备。缺省的串口驱动创建一个文件在这个目录中来展示许多串口特定的硬件信息:

驱动的名子 节点名子 主设备号 次设备号范围 驱动的类型

/dev/tty /dev/tty 5 0 system:/dev/tty
/dev/console /dev/console 5 1 system:console
/dev/ptmx /dev/ptmx 5 2 system
/dev/vc/0 /dev/vc/0 4 0 system:vtmaster
usbserial /dev/ttyUSB 188 0-254 serial
serial /dev/ttyS 4 64-67 serial
pty_slave /dev/pts 136 0-255 pty:slave
pty_master /dev/ptm 128 0-255 pty:master


通过查看/sys/class/tty文件,可找到所有当前注册到内核中的 tty 设备目录,目录下有一个 “dev” 文件包含有分配给那个 tty 设备的主次编号,和该驱动对应的设备及驱动的符号链接。

/sys/class/tty/
|-- console
| -- dev |-- ptmx | – dev
|-- tty
| -- dev |-- tty0 | – dev

|-- ttyUSB0
| |-- dev
| |-- device
-> …/…/…/devices/pci0000:00/0000:00:09.0/usb3/3-1/3-1:1.0/ttyUSB0
| `-- driver -> …/…/…/bus/usb-serial/drivers/keyspan_4

  • struct tty_driver定义

    struct tty_driver {
    	int	magic;		//这个结构的"魔术"值. 应当一直设为 TTY_DRIVER_MAGIC
    	struct cdev cdev;
    	struct module	*owner;		//驱动的模块拥有者
    	const char	*driver_name;	//驱动的名子, 用在 /proc/tty 和 sysfs.
    	const char	*name;			//驱动的节点名
    	int	name_base;				//当创建设备名子时,使用的起始数字
    	int	major;					//驱动的主编号
    	int	minor_start;	//驱动的开始次编号. 这常常设为 name_base 的相同值
    	int	minor_num;		/* number of *possible* devices */
    	int	num;			/* number of devices allocated */
    	short	type;		/* type of tty driver */
    	short	subtype;	//其值依赖于type
    	struct ktermios init_termios; //当创建设备时的初始化 struct termios 值
    	int	flags;		/* tty driver flags */
    	int	refcount;	/* for loadable tty drivers */
    	struct proc_dir_entry *proc_entry; /* /proc fs entry ,它由 tty 核心创建如果驱动实现了
    write_proc 或者 read_proc 函数 */
    	struct tty_driver *other; /* 指向一个 tty 从驱动. 这只被 pty 驱动使用, 并且不应当被其他的tty 驱动使用 */
    
    	/*
    	 * Pointer to the tty data structures
    	 */
    	struct tty_struct **ttys;
    	struct ktermios **termios;
    	struct ktermios **termios_locked;
    	void *driver_state;	/* tty 驱动的内部状态. 应当只被 pty 驱动使用 */
    	
    	/*
    	 * Interface routines from the upper tty layer to the tty
    	 * driver.	Will be replaced with struct tty_operations.
    	 */
    	int  (*open)(struct tty_struct * tty, struct file * filp);
    	void (*close)(struct tty_struct * tty, struct file * filp);
    	int  (*write)(struct tty_struct * tty, const unsigned char *buf, int count);
    	void (*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, struct file * file,
    		    		unsigned int cmd, unsigned long arg);
    	long (*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);
    	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);
    	void (*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);
        /*在对/proc/tty/driver目录中的文件的读写回调函数*/
    	int (*read_proc)(char *page, char **start, off_t off,
    			  		 int count, int *eof, void *data);
    	int (*write_proc)(struct file *file, const char __user *buffer,
    			  		  unsigned long count, void *data);
    	int (*tiocmget)(struct tty_struct *tty, struct file *file);
    	int (*tiocmset)(struct tty_struct *tty, struct file *file,
    					unsigned int set, unsigned int clear);
    	struct list_head tty_drivers;
    };
    
    • owner:为了防止 tty 驱动在 tty 端口打开时被卸载,一般取值为THIS_MODULE

    • driver_name :驱动名,它出现在 /proc/tty/drivers 文件, 以及 /sys/class/tty 类目录下。

    • name:节点名,出现在/dev树中,通过在这个字串的后面追加在使用的 tty 设备号来创建多个 tty 设备。另外还在/sys/class/tty 目录中创建一个设备名子。如果 devfs 被使能,内核中的串口驱动设置这个 name 成员为 tts/ ,如果没有使能,则为ttyS/

      # 当driver_name = "tiny_tty"; name = "ttty"; devfs_name = "tts/ttty%d"; 时:
      $ cat /proc/tty/drivers
      tiny_tty /dev/ttty 240 0-3 serial
      
      $ tree /sys/class/tty/ttty*
      /sys/class/tty/ttty0
      `-- dev
      /sys/class/tty/ttty1
      `-- dev
      /sys/class/tty/ttty2
      `-- dev
      /sys/class/tty/ttty3
      `-- dev
      
      $ cat /sys/class/tty/ttty0/dev
      240:0
      
    • major 变量描述这个驱动的主编号是什么

    • type、subtype :声明这个驱动是什么类型:

      • TTY_DRIVER_TYPE_SYSTEM :仅由 tty 子系统内部使用的驱动(非"正常"的tty 驱动),其可能的子类型有
        • SYSTEM_TYPE_TTY
        • SYSTEM_TYEP_CONSOLE
        • SYSTEM_TYPE_SYSCONS
        • SYSTEM_TYPE_SYSPTMX
      • TTY_DRIVER_TYPE_CONSOLE :仅被控制台驱动使用
      • TTY_DRIVER_TYPE_SERIAL :串行类型接口驱动,其可能的子类型有
        • SERIAL_TYPE_NORMAL (最常使用)
        • SERIAL_TYPE_CALLOUT(现已淘汰)
      • TTY_DRIVER_TYPE_PTY :伪控制台接口驱动 ,其可能的子类型有
        • PTY_TYPE_MASTER
        • PTY_TYPE_SLAVE
    • flags:指示驱动的当前状态和它是什么类型 tty 驱动。你必须使用的位掩码宏为:

      • TTY_DRIVER_RESET_TERMIOS :表示当最后一个进程关闭tty设备时,自动复位termios 设置。
      • TTY_DRIVER_REAL_RAW :表示线路规程不必查看从tty 驱动收到的每个字符,但保证证发送奇偶或者坏字符通知给线路规程。因为它以一种更快的方式来处理接收到的字符,所以大部分tty驱动会设置该标识。
      • TTY_DRIVER_NO_DEVFS :表示当注册tty驱动时,tty 核心不创建任何devfs 条目给这个 tty 设备。这对任何动态创建和销毁次设备的驱动(USB-到-串口 驱动, USB 猫驱动, USB 蓝牙 tty 驱动, 以及标准串口设备)都是有益的。
      • TTY_DRIVER_INSTALLED:该标志由tty核心在tty驱动被注册后自动设置,且不得由tty驱动设置
  • 自定义的tty设备结构体

    struct tiny_serial
    {
        struct tty_struct *tty; 	/* pointer to the tty for this device */
        int open_count;				/* number of times this port has been opened */
        struct semaphore sem; 		/* locks this structure */
        struct timer_list *timer;
    };
    

1. 驱动的创建和初始化

任何一个 tty 驱动的主要数据结构是 struct tty_driver。它用来注册和注销一个 tty 驱动到 tty 内核,在内核头文件<linux/tty_driver.h> 中描述 。

  • alloc_tty_driver()

    /* allocate the tty driver */
    tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS);
    if (!tiny_tty_driver)
    	return -ENOMEM;
    
  • tty_set_operation()

    tiny_open()
    {
        ...
    }
    
    tiny_close()
    {
        ...
    }
    
    tiny_write()
    {
        ...
    }
    
    tiny_write_room()
    {
        ...
    }
    
    tiny_set_termios()
    {
        ...
    }
    
    static struct tty_operations serial_ops = {
        .open = tiny_open,
        .close = tiny_close,
        .write = tiny_write,
        .write_room = tiny_write_room,
        .set_termios = tiny_set_termios,
    };
    ...
    /* initialize the tty driver */
    tiny_tty_driver->owner = THIS_MODULE;
    tiny_tty_driver->driver_name = "tiny_tty";
    tiny_tty_driver->name = "ttty";
    tiny_tty_driver->devfs_name = "tts/ttty%d";
    tiny_tty_driver->major = TINY_TTY_MAJOR;
    tiny_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
    tiny_tty_driver->subtype = SERIAL_TYPE_NORMAL;
    tiny_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS;
    tiny_tty_driver->init_termios = tty_std_termios;
    tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
    tty_set_operations(tiny_tty_driver, &serial_ops);
    
  • tty_operations

    int (*open)(struct tty_struct * tty, struct file * filp);	//open 函数.
    void (*close)(struct tty_struct * tty, struct file * filp);	//close 函数.
    int (*write)(struct tty_struct * tty, const unsigned char *buf, int count);//write 函数.
    
    /*
     * 单字节写函数. 这个函数被 tty 核心调用当单个字节被写入设备. 如果
     * 一个 tty 驱动没有定义这个函数, write 函数被调用来替代, 当 tty
     * 核心想发送一个单个字节.
    */
    void (*put_char)(struct tty_struct *tty, unsigned char ch);
    
    /* 刷新数据到硬件的函数.*/
    void (*flush_chars)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    
    int (*write_room)(struct tty_struct *tty);	//指示多少缓冲空闲的函数
    
    int (*chars_in_buffer)(struct tty_struct *tty);	//指示多少缓冲满数据的函数.
    
    int (*ioctl)(struct tty_struct *tty, struct file * file,
                 unsigned int cmd, unsigned long arg);	//ioctl 函数
    
    //当设备的 termios 设置已被改变时.这个函数被 tty 核心调用
    void (*set_termios)(struct tty_struct *tty, struct termios * old);
    
    /*
     * 数据抑制函数. 这些函数用来帮助控制 tty 核心的输入缓存. 这个抑制
     * 函数被调用当 tty 核心的输入缓冲满. tty 驱动应当试图通知设备不应
     * 当发送字符给它. unthrottle 函数被调用当 tty 核心的输入缓冲已被
     * 清空, 并且它现在可以接收更多数据. tty 驱动应当接着通知设备可以
     * 接收数据. stop 和 start 函数非常象 throttle 和 unthrottle 函数,
     * 但是它们表示 tty 驱动应当停止发送数据给设备以及以后恢复发送数据.
     */
    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);	//挂起函数
    
    /*
     * 线路中断控制函数. 当这个 tty 驱动在 RS-232 端口上要打开或关闭线路的 BREAK 状态,
     * 这个函数被调用. 如果状态设为 -1, BREAK 状态应当打开. 如果状态设为 0, BREAK 状态应当关闭.
     * 如果这个函数由 tty 驱动实现, tty 核心将处理 TCSBRK, TCSBRKP, TIOCSBRK, 和 TIOCCBRK
     * ioctl. 否则, 这些 ioctls 被发送给驱动 ioctl 函数.
     */
    void (*break_ctl)(struct tty_struct *tty, int state);
    
    
    void (*flush_buffer)(struct tty_struct *tty);	//刷新缓冲和丢失任何剩下的数据.
        
    void (*set_ldisc)(struct tty_struct *tty);	//设置线路规程的函数. 当 tty 核心已改变这个 tty 驱动的线路规程,这个函数被调用. 它通常不用并且不应当被一个驱动定义.
    
    /*
     * 发送 X-类型 字符 的函数. 这个函数用来发送一个高优先级 XON 或者
     * XOFF 字符给 tty 设备. 要被发送的字符在 ch 变量中指定.
     */
    void (*send_xchar)(struct tty_struct *tty, char ch);
    
    /* proc 读和写函数.*/
    int (*read_proc)(char *page, char **start, off_t off, int count, int *eof, void data);
    int (*write_proc)(struct file *file, const char *buffer, unsigned long count, void *data);
    
    int (*tiocmget)(struct tty_struct *tty, struct file *file); //获得当前的特定 tty 设备的线路设置. 
    
    /* 设置当前的特定 tty 设备的线路设置. set 和 clear 包含了去设置或者清除的不同的线路设置 */
    int (*tiocmset)(struct tty_struct *tty, struct file *file, unsigned int set, unsigned int clear);
    

2. 驱动的注册/注销

  • tty_register_driver() / tty_unregister_driver()

    /* register the tty driver */
    retval = tty_register_driver(tiny_tty_driver);
    if (retval)
    {
        printk(KERN_ERR "failed to register tiny tty driver");
        put_tty_driver(tiny_tty_driver);
        return retval;
    }
    
    /* unregister the tty driver */
    tty_unregister_driver(tiny_tty_driver);
    
    • 此时,内核在sysfs中为该驱动创建了所有可能的整个范围内的次设备。
    • 当未指定TTY_DRIVER_NO_DEVFS 标志时,驱动接下来调用函数注册它控制的tty设备
  • tty_register_device() / tty_unregister_device()/

    /* register the tty devices */
    for (i = 0; i < TINY_TTY_MINORS; ++i)
    	tty_register_device(tiny_tty_driver, i, NULL);
    
    /* unregister the tty devices */
    for (i = 0; i < TINY_TTY_MINORS; ++i)
    	tty_unregister_device(tiny_tty_driver, i);
    
    • 第1个参数是一个指向设备所属的 struct tty_driver。
    • 第2个参数是设备的次编号;
    • 第3个参数是一个指向这个 tty 设备所绑定的 struct device。如果这个 tty设备没绑定到任何一个 struct device, 这个参数可被设为 NULL。

3. struct termios

这个 struct termios 结构用来描述某个 tty 设备的一个特定端口当前的线路规程设置(波特率, 数据大小, 数据流控…)。在tty_driver结构体中的init_termios变量就是该结构类型,用于描述一个完全的线路规程设置,且驱动初始化时会调用一个标准的数值集合(tty_std_termios)来初始化该init_termios变量。

struct termios tty_std_termios = {
    .c_iflag = ICRNL | IXON,					//输入模式标志
    .c_oflag = OPOST | ONLCR,					//输出模式标志
    .c_cflag = B38400 | CS8 | CREAD | HUPCL,	//控制模式标志
    .c_lflag = ISIG | ICANON | ECHO | ECHOE |	//本地模式标志
        	   ECHOK | ECHOCTL | ECHOKE | IEXTEN,
    .c_cc = INIT_C_CC							//一个控制字符数组
};
  • 关于各模式标志的详情可参考Linux 发布中的 termios 手册页;
  • 内核提供了一套宏定义来获得不同的位,这些宏定义在头文件 include/linux/tty.h 中定义;

当一个用户要改变一个 tty 设备的线路设置或者获取当前线路设置时,要么调用用户空间下的termios 系列库函数,要么直接对节点调用ioctl。tty核心将自动为大部分 termios 用户空间函数转换为一个对驱动节点的 ioctl 调用。大量的不同的 tty ioctl 调用接着被 tty 核心转换为一个对 tty 驱动的单个set_termios 函数调用。set_termios 调用需要决定哪个线路设置它被请求来改变, 接着在 tty 设备中做这些改变。

  • set_termios

    • 一个 set_termios 函数应当做的第一件事情是决定任何事情是否真的需要改变,可使用下面的代码完成 :

      #define RELEVANT_IFLAG(iflag) ((iflag) & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK))
      
      unsigned int cflag;
      cflag = tty->termios->c_cflag;
      /* check that they really want us to change something */
      if (old_termios)	//old_termios 变量在它被存取之前,需要被检查是否指向一个有效的结构
      {
      	if ((cflag == old_termios->c_cflag) && 
              (RELEVANT_IFLAG(tty->termios->c_iflag) == RELEVANT_IFLAG(old_termios->c_iflag))) {
      		printk(KERN_DEBUG " - nothing to change...\n");
      		return;
      	}
      }
      
      /* 为查看需要的字节大小, CSIZE 位掩码可用来从 cflag 变量区分出正确的位
       *  PARODD 位掩码可用来决定是否奇偶应当是奇或者偶
       */
      switch (cflag & CSIZE)
      {
          case CS5:
              printk(KERN_DEBUG " - data bits = 5\n");
              break;
          case CS6:
              printk(KERN_DEBUG " - data bits = 6\n");
              break;
          case CS7:
              printk(KERN_DEBUG " - data bits = 7\n");
              break;
          default:
          case CS8:
              printk(KERN_DEBUG " - data bits = 8\n");
              break;
      }
      
      /* 为决定需要的奇偶值, PARENB 位掩码可对 cflag 变量检查来告知是否任何奇偶要被设置. */
      if (cflag & PARENB)
          if (cflag & PARODD)
          	printk(KERN_DEBUG " - parity = odd\n");
          else
          	printk(KERN_DEBUG " - parity = even\n");
      else
        	printk(KERN_DEBUG " - parity = none\n");
      /* 请求的停止位也可使用 CSTOPB 位掩码从 cflag 变量中来知道 */
      if (cflag & CSTOPB)
      	printk(KERN_DEBUG " - stop bits = 2\n");
      else
      	printk(KERN_DEBUG " - stop bits = 1\n");
      
      /* 为确定是否用户要求硬件流控,CRTSCTS 位掩码用来对 cflag 变量检查 */
      if (cflag & CRTSCTS)
      	printk(KERN_DEBUG " - RTS/CTS is enabled\n");
      else
      	printk(KERN_DEBUG " - RTS/CTS is disabled\n");
      
      /* 确定软件流控的不同模式和不同的起停字符是有些复杂 */
      if (I_IXOFF(tty) || I_IXON(tty))
      {
          unsigned char stop_char = STOP_CHAR(tty);
          unsigned char start_char = START_CHAR(tty);
          /* if we are implementing INBOUND XON/XOFF */
          if (I_IXOFF(tty))
          	printk(KERN_DEBUG " - INBOUND XON/XOFF is enabled, "
          			"XON = %2x, XOFF = %2x", start_char, stop_char);
          else
          	printk(KERN_DEBUG" - INBOUND XON/XOFF is disabled");
          /* if we are implementing OUTBOUND XON/XOFF */
          if (I_IXON(tty))
          	printk(KERN_DEBUG" - OUTBOUND XON/XOFF is enabled, "
          			"XON = %2x, XOFF = %2x", start_char, stop_char);
          else
              printk(KERN_DEBUG" - OUTBOUND XON/XOFF is disabled");
      }
      
      /* 最后, 波特率需要确定. tty 核心提供了一个函数, tty_get_baud_rate, 来帮助做这个 */
      printk(KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty));
      
      • 至此,tty 驱动已经确定了所有的不同的线路设置, 它可以基于这些值正确设置硬件.
  • tiocmget

    • 当核心需要知道当前的特定tty 设备的控制线的物理值(串口的DTR和RTS线的值),tty 驱动中的 iocmget 函数被 tty 核心所调用。

    • 如果 tty 驱动不能直接读串口的 MSR 或者 MCR 寄存器, 一个它们的拷贝应当在本地保持. 许多 USB-到-串口 驱动必须实现这类的"影子"变量.

      static int tiny_tiocmget(struct tty_struct *tty, struct file *file)
      {
          struct tiny_serial *tiny = tty->driver_ data;
          unsigned int result = 0;
          unsigned int msr = tiny->msr;
          unsigned int mcr = tiny->mcr;
          result = ((mcr & MCR_DTR) ? TIOCM_DTR : 0) | 	/* DTR is set */
                  ((mcr & MCR_RTS) ? TIOCM_RTS : 0 ) | 	/* RTS is set */
                  ((mcr & MCR_LOOP) ? TIOCM_LOOP : 0) | 	/* LOOP is set */
                  ((msr & MSR_CTS) ? TIOCM_CTS : 0) | 	/* CTS is set */
                  ((msr & MSR_CD) ? TIOCM_CAR : 0)| 		/* Carrier detect is set */
                  ((msr & MSR_RI) ? TIOCM_RI : 0) | 		/* Ring Indicatoris set */
                  ((msr & MSR_DSR) ? TIOCM_DSR : 0); 		/* DSR is set */
          return result;
      }
      
  • tiocmset

    • 当核心要设置一个特定tty 设备的控制线值,tty 驱动中的 tiocmset 函数被 tty 核心调用。

    • tty 核心通过传递 set 和 clear 告知 tty 驱动设置什么值和清理什么 ,这些变量包含一个应当改变的线路设置的位掩码。

      static int tiny_tiocmset(struct tty_struct *tty, struct file *file,
                               unsigned int set , unsigned int clear)
      {
          struct tiny_serial *tiny = tty->driver_data;
          unsigned int mcr = tiny->mcr;
          if (set & TIOCM_RTS)
          	mcr |= MCR_RTS;
          if (set & TIOCM_DTR)
          	mcr |= MCR_RTS;
          if (clear & TIOCM_RTS)
          	mcr &= ~MCR_RTS;
          if (clear & TIOCM_DTR)
          	mcr &= ~MCR_RTS;
          /* set the new MCR value in the device */
          tiny->mcr = mcr;
          return 0;
      }
      

4. ioctls()

当 ioctl() 被在设备节点上调用,struct tty_driver 中的 ioctl 函数被 tty 核心调用。如果这个 tty 驱动不知道如何处理传递给它的 ioctl 值, 它应当返回 -ENOIOCTLCMD 来试图让 tty 核心实现一个通用的调用版本。

2.6 内核定义了大约 70 个不同的 tty ioctls,大部分的 tty 驱动不全部处理它们, 但是处理一个小的子集。下面讲述一个更通用的 tty ioctls 列表,以及如何实现它们:

cmd 涵义
TIOCSERGETLSR 获得 tty 设备的线路状态寄存器( LSR )的值
TIOCGSERIAL 获得串口线信息,调用者传递一个指向一个大的 serial_struct 结构的指针,这个结构应当由 tty 驱动填充正确的值
TIOCSSERIAL 设置串口线路信息,一个指向 struct serial_struct 的指针被传递给这个调用
TIOCMIWAIT 等待 MSR 改变
TIOCGICOUNT 中断计数(已经产生多少串口线中断)
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
                      unsigned int cmd, unsigned long arg)
{
    struct tiny_serial *tiny = tty->driver_data;
    //获得串口线信息
    if (cmd == TIOCGSERIAL)
    {
        struct serial_struct tmp;
        if (!arg)
        	return -EFAULT;
        memset(&tmp, 0, sizeof(tmp));
        tmp.type = tiny->serial.type;
        tmp.line = tiny->serial.line;
        tmp.port = tiny->serial.port;
        tmp.irq = tiny->serial.irq;
        tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ;
        tmp.xmit_fifo_size = tiny->serial.xmit_fifo_size;
        tmp.baud_base = tiny->serial.baud_base;
        tmp.close_delay = 5*HZ;
        tmp.closing_wait = 30*HZ;
        tmp.custom_divisor = tiny->serial.custom_divisor;
        tmp.hub6 = tiny->serial.hub6;
        tmp.io_type = tiny->serial.io_type;
        if (copy_to_user((void __user *)arg, &tmp, sizeof(tmp)))
        	return -EFAULT;
        return 0;
    }
    /* arg 参数包含用户在等待的事件类型,当实现这个ioctl时不要使用interruptible_sleep_on调用.
     * 在 tty 驱动的代码中能知道 MSR 寄存器改变的某些地方, 
     * 必须调用wake_up_interruptible(&tp->wait);以便这个代码能正常工作:
     */
    if (cmd == TIOCMIWAIT)
    {
    	DECLARE_WAITQUEUE(wait, current);
        struct async_icount cnow;
        struct async_icount cprev;
        cprev = tiny->icount;
        while (1) {
            add_wait_queue(&tiny->wait, &wait);
            set_current_state(TASK_INTERRUPTIBLE);
            schedule();
            remove_wait_queue(&tiny->wait, &wait);
            /* see if a signal woke us up */
            if (signal_pending(current))
            	return -ERESTARTSYS;
            cnow = tiny->icount;
            if (cnow.rng == cprev.rng &&
                cnow.dsr == cprev.dsr &&
            	cnow.dcd == cprev.dcd &&
            	cnow.cts == cprev.cts)
            {
                return -EIO; /* no change => error */
            }            	
            if(((arg & TIOCM_RNG) && (cnow.rng !=cprev.rng)) ||
            	((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr))||
            	((arg & TIOCM_CD) && (cnow.dcd != cprev.dcd)) ||
            	((arg & TIOCM_CTS)&& (cnow.cts != cprev.cts)) )
            {
            	return 0;
            }
            cprev = cnow;
        }
    }
    /* 这个调用通过arg向内核传递一个serial_icounter_struct 的指针,常常和之前的 IOCMIWAIT ioctl 调用结合使用 */
    if (cmd == TIOCGICOUNT)
    {
        struct async_icount cnow = tiny->icount;
        struct serial_icounter_struct icount;
        icount.cts = cnow.cts;
        icount.dsr = cnow.dsr;
        icount.rng = cnow.rng;
        icount.dcd = cnow.dcd;
        icount.rx = cnow.rx;
        icount.tx = cnow.tx;
        icount.frame = cnow.frame;
        icount.overrun = cnow.overrun;
        icount.parity = cnow.parity;
        icount.brk = cnow.brk;
        icount.buf_overrun = cnow.buf_overrun;
        if (copy_to_user((void __user *)arg, &icount, sizeof(icount)))
        	return -EFAULT;
        return 0;
    }
    return -ENOIOCTLCMD;
}        

5. open()和close()

  • 当一个用户对某个 tty 设备节点调用 open 时, tty 核心调用tty_operationsopen 中注册的open函数。该函数使用tty_struct和file指针作为参数,并将一些数据(可以基于端口次编号来引用的静态数组中的数据 )到tty_struct中,以便被用于后续的close, write 使用。下面的例子显示了tiny_tty 驱动中的open函数保存一个指针(用户自定义设备结构体tiny)到 tty 结构中。

    static int tiny_open(struct tty_struct *tty, struct file *file)
    {
        struct tiny_serial *tiny;		//用户自定义的设备结构体tiny_serial
        struct timer_list *timer;
        int index;
        
        /* initialize the pointer in case something fails */
        tty->driver_data = NULL;
        
        /* get the serial object associated with this tty pointer */
        index = tty->index;
        tiny = tiny_table[index];
        if (tiny == NULL)
        {
            /* first time accessing this device, let's create it */
            tiny = kmalloc(sizeof(*tiny), GFP_KERNEL);
            if (!tiny)
            	return -ENOMEM;
            init_MUTEX(&tiny->sem);
            tiny->open_count = 0;
            tiny->timer = NULL;
            tiny_table[index] = tiny;
        }
        down(&tiny->sem);
        
        /* save our structure within the tty structure */
        /* tiny_serial 结构被保存在 tty 结构中. 
         * 允许 tiny_write,tiny_write_room, 和 tiny_close 函数来获取 tiny_serial 结构和正确操作它
         */
        tiny->tty = tty;
        tty->driver_data = tiny;	
        
        /* trace the tty device opened times */
        ++tiny->open_count;
        if (tiny->open_count == 1)
        {
            /* 若设备是第一次被打开, 还需要做任何必要的硬件初始化和内存分配工作 */
            ...
        }
        return 0;
    }
    
  • 用户对前面使用 open 调用而创建的文件句柄调用 close 时,tty 核心调用tty_operationsopen 中注册的close函数。因为 open 函数可被多次调用, close 函数也可多次调用. 因此这个函数应当跟踪它被调用的次数来决定是否硬件应当在此次真正被关闭。

    static void do_close(struct tiny_serial *tiny)
    {
        down(&tiny->sem);
        if (!tiny->open_count)
        {
            /* port was never opened */
            goto exit;
        }
        --tiny->open_count;
        if (tiny->open_count <= 0)
        {
            /* The port is being closed by the last user. */
            /* Do any hardware specific stuff here */
            ...
            /* shut down our timer */
            del_timer(tiny->timer);
        }
    exit:
        up(&tiny->sem);
    }
    
    
    static void tiny_close(struct tty_struct *tty, struct file *file)
    {
        struct tiny_serial *tiny = tty->driver_data;
        if (tiny)
            do_close(tiny);
    }
    
    • close 函数没有返回值,因为它不被认为会失败。

6. write

用户在有数据发送给硬件时,write 函数被调用。但不是所有的写程序要求的字符都可以在调用写函数时被发送,因为还需要考虑速度匹配和 tty 硬件的缓冲区容量大小。函数返回能够发送给硬件的字符数,如果发生写错误则返回负的错误码。

  • write()

write 函数被要求可在中断上下文和用户上下文中被调用。所以tty驱动不应当调用任何可引起睡眠的函数(copy_from_user、kmalloc、printk等)。内核提供了一个函数calling_in_interrupt来判断驱动是否处于在中断上下文中。

下面的代码显示了一个简单的写函数,因为没有实际的tty硬件,故我们将要写的数据记录到内核调试日志中

static int tiny_write(struct tty_struct *tty, const unsigned char *buffer, int count)
{
    struct tiny_serial *tiny = tty->driver_data;
    int i;
    int ret = -EINVAL;
    if (!tiny)
    	return -ENODEV;
    down(&tiny->sem);
    if (!tiny->open_count)
        /* port was not opened */
        goto exit;
    /* fake sending the data out a hardware port by
    * writing it to the kernel debug log.
    */
    printk(KERN_DEBUG "%s - ", __FUNCTION__);
    for (i = 0; i < count; ++i)
    	printk("%02x ", buffer[i]);
    printk("\n");
exit:
    up(&tiny->sem);
    return ret;
}

如果tty 驱动在 tty_struct 中没有实现 put_char 函数,tty 核心用一个数据大小为 1 来使用 write 函数回调。这普遍发生在tty 核心想转换一个新行字符为一个换行,此时write函数不得将数据进行缓冲和重试。但因为 write 函数不知道是否它在被调用来替代 put_char,因此,一些终端类型(例如很多USB-到-串口 )不能正确工作。

  • write_room()

    • 当 tty 核心想知道多少空间在写缓冲中时,可调用此函数。随着字符不断从写缓冲清空以及调用写函数时添加字符到写缓冲,这个数字时时改变的。

      static int tiny_write_room(struct tty_struct *tty)
      {
          struct tiny_serial *tiny = tty->driver_data;
          int room = -EINVAL;
          if (!tiny)
          	return -ENODEV;
          down(&tiny->sem);
          if (!tiny->open_count)
          {
              /* port was not opened */
              goto exit;
          }
          /* calculate how much room is left in the device */
          ...		
      exit:
          up(&tiny->sem);
          return room;
      }
      

7. 其它缓冲函数

  • chars_in_buffer
    • 当 tty 核心想知道还有多少字符仍然保留在 tty驱动的写缓冲中等待被发送时,调用此函数。
    • 虽然该函数不是tty驱动必须实现的,但如果驱动能够在它发送字符串到硬件之前有能力存储它们到一个写缓存中,那就应该实现它。 另外,在具有缓冲能力的情况下,推荐驱动还要实现下面3个函数,虽然不是必须的。
  • flush_chars()
    • 当 tty 核心使用 put_char 函数已发送了许多字符给 tty 驱动,且 tty 驱动还未启动发送这些字符到硬件时调用。该函数是非阻塞的,即不用等待发送完毕后才返回。
  • wait_until_sent()
    • 该函数与flush_chars()函数的使用情况类似,区别在于它是阻塞的,它会等待字符发送完毕或达到超时时间。当这个超时时间定为0时,函数会一直阻塞等待。
  • flush_buffer()
    • 当 tty 驱动要刷新所有仍然在写缓冲的数据时,该函数被调用。注意,这些被清空的数据直接丢弃,而不是发送到硬件。

8. read哪去了

tty 核心提供一个缓冲逻辑用来存放从硬件收到的数据,但该缓冲区溢出时,并不会通知tty核心或用户。该缓冲区用 struct tty_flip_buffer 来表示。

一个 flip 缓冲是一个包含 2 个主要数据数组的结构. 从 tty 设备接收到的数据被存储于第一个数组. 当这个数组满, 任何等待数据的用户被通知数据可以读. 当用户从这个数组读数据, 任何新到的数据被存储在第 2 个数组。当那个数组被读空, 数据再次刷新给用户, 并且驱动开始填充第 1 个数组。

为试图阻止数据丢失, 一个 tty 驱动可以监视到来的数组多大,并且,如果它添满了,要及时告知 tty 驱动刷新缓冲,而不是等待下一个可用的机会。

tty 驱动 只需要关心struct tty_flip_buffer 结构中的 cout变量,它表示当前缓冲里还可用来接收数据的空间大小。如果这个值等于值 TTY_FLIPBUF_SIZE,这个 flip 缓冲需要被刷新到用户。一个使用cout变量的例子如下:

for (i = 0; i < data_size; ++i)
{
    if (tty->flip.count >= TTY_FLIPBUF_SIZE)
    	tty_flip_buffer_push(tty);
    /*将从 tty 驱动接收来的要发送给用户的字符添加到 flip 缓冲*/
    tty_insert_flip_char(tty, data[i], TTY_NORMAL);
}
tty_flip_buffer_push(tty);
  • tty_insert_flip_char()

    • 该函数将从 tty 驱动接收来的要发送给用户的字符添加到 flip 缓冲。
    • 第1个参数是数据应当保存入的struct tty_struct;
    • 第 2 个参数是要保存的字符;
    • 第 3 个参数是字符类型标志。
      • TTY_NORMAL: 这个是一个正常的被接收的字符.
      • TTY_BREAK、TTY_PARITY、 TTY_OVERRUN:这是一个特殊类型的指示错误接收数据的字符,具体取值取决于错误类型。
  • tty_flip_buffer_push()

    • 当数据被加到 flip 缓冲, 或者当 flip 缓冲满时, tty 驱动调用该函数。
    • 如果 tty 驱动可高速接收数据,且 tty->low_latency标志被设置, 对 tty_flip_buffer_push 的调用被立刻执行。否则,该函数会会调度它自己,在之后的一个时间点将数据推出缓冲。

9. tty_struct

tty_struct 变量被 tty 核心用来保持当前的特定 tty 端口的状态

/*  <linux/tty.h> */
struct tty_struct {
	int	magic;
	struct tty_driver *driver;	//当前控制这个 tty 设备的 tty_driver 结构
	int index;
	struct tty_ldisc ldisc;		//给 tty 设备的线路规程
	struct mutex termios_mutex;
	struct ktermios *termios, *termios_locked;	//指向 tty 设备的当前 termios 设置的指针
	char name[64];
	struct pid *pgrp;
	struct pid *session;
	unsigned long flags;		//tty 设备的当前状态
	int count;
	struct winsize winsize;
    /*
     * stopped: 指示tty 设备是否 被停止. tty 驱动可以设置这个值
     * hw_stopped:指示tty 设备是否 已经停止. tty 驱动可以设置这个值
     * low_latency:指示tty 设备是否能够高速接收数据. tty驱动可以设置这个值.
     */
	unsigned char stopped:1, hw_stopped:1, flow_stopped:1, packet:1;    
	unsigned char low_latency:1, warned:1;
	unsigned char ctrl_status;
	unsigned int receive_room;	/* Bytes free for queue */

	struct tty_struct *link;
	struct fasync_struct *fasync;
	struct tty_bufhead buf;
	int alt_speed;		/* For magic substitution of 38400 bps */
	wait_queue_head_t write_wait;	//当tty驱动可以接收更多数据时,应当唤醒它
	wait_queue_head_t read_wait;
	struct work_struct hangup_work;
	void *disc_data;
	void *driver_data;		//用来存储对于 tty 驱动本地的数据. 这个变量不被 tty 核心修改.
	struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096
	
	/*
	 * The following is data for the N_TTY line discipline.  For
	 * historical reasons, this is included in the tty structure.
	 */
	unsigned int column;
	unsigned char lnext:1, erasing:1, raw:1, real_raw:1, icanon:1;
	unsigned char closing:1;	//指示tty 设备是否在关闭端口当中. tty 驱动可以设置这个值
	unsigned short minimum_to_wake;
	unsigned long overrun_time;
	int num_overrun;
	unsigned long process_char_map[256/(8*sizeof(unsigned long))];
	char *read_buf;
	int read_head;
	int read_tail;
	int read_cnt;
	unsigned long read_flags[N_TTY_BUF_SIZE/(8*sizeof(unsigned long))];
	int canon_data;
	unsigned long canon_head;
	unsigned int canon_column;
	struct mutex atomic_read_lock;
	struct mutex atomic_write_lock;
	unsigned char *write_buf;
	int write_cnt;
	spinlock_t read_lock;
	/* If the tty has a pending do_SAK, queue it here - akpm */
	struct work_struct SAK_work;
};

几乎它的所有条目都只被 tty 核心使用,只有几个例外是被tty驱动使用的,下面一一讲解

  • **flags **:tty 设备的当前状态. 这是一个位段变量, 并且通过下面的宏定义存取
    • TTY_THROTTLED :仅由tty核心设置,表示驱动中有抑制函数被调用
    • TTY_IO_ERROR :当它不想任何数据被读出或写入时,驱动设置该标志。常用在设备被关闭时。
    • TTY_OTHER_CLOSED :当端口已经被关闭时,仅由 pty 驱动使用来设置。
    • TTY_EXCLUSIVE :由 tty 核心设置来指示一个端口在独占模式并且只能一次由一个用户存取。
    • TTY_DO_WRITE_WAKEUP :线路规程的 write_wakeup 函数被允许来被调用,常常在tty_driver 调用 wake_up_interruptible 函数的同一时间被调用
    • TTY_DONT_FLIP :被缺省的 tty 线路规程用来通知 tty 核心,表示不应当改变 flip 缓冲
    • TTY_HW_COOK_OUT :如果它没有被tty 驱动设置, 线路规程成块拷贝驱动的输出,如果被设置, 它通知线路规程应当"烹调"发送给它的输出。这个标志应当通常不被 tty 驱动设置
    • TTY_HW_COOK_IN :几乎和设置在驱动中的 flag 变量中的 TTY_DRIVER_REAL_RAW 标志一致。这个标志通常应当不被 tty 驱动设置.
    • TTY_PTY_LOCK :pty 驱动用来加锁和解锁一个端口
    • TTY_NO_WRITE_SPLIT :如果设置, tty 核心不将对 tty 驱动的写分成正常大小的块

3、一个完整的tty驱动源代码

/*
 * Tiny TTY driver
 *
 * This driver shows how to create a minimal tty driver.  It does not rely on
 * any backing hardware, but creates a timer that emulates data being received
 * from some kind of hardware.
 */

#include <linux/config.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/wait.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>
#include <linux/serial.h>
#include <asm/uaccess.h>


#define DRIVER_VERSION "v2.0"
#define DRIVER_AUTHOR "Greg Kroah-Hartman <greg@kroah.com>"
#define DRIVER_DESC "Tiny TTY driver"

/* Module information */
MODULE_AUTHOR( DRIVER_AUTHOR );
MODULE_DESCRIPTION( DRIVER_DESC );
MODULE_LICENSE("GPL");

#define DELAY_TIME		HZ * 2	/* 2 seconds per character */
#define TINY_DATA_CHARACTER	't'

#define TINY_TTY_MAJOR		240	/* experimental range */
#define TINY_TTY_MINORS		4	/* only have 4 devices */

struct tiny_serial {
	struct tty_struct	*tty;		/* pointer to the tty for this device */
	int			open_count;	/* number of times this port has been opened */
	struct semaphore	sem;		/* locks this structure */
	struct timer_list	*timer;

	/* for tiocmget and tiocmset functions */
	int			msr;		/* MSR shadow */
	int			mcr;		/* MCR shadow */

	/* for ioctl fun */
	struct serial_struct	serial;
	wait_queue_head_t	wait;
	struct async_icount	icount;
};

static struct tiny_serial *tiny_table[TINY_TTY_MINORS];	/* initially all NULL */


static void tiny_timer(unsigned long timer_data)
{
	struct tiny_serial *tiny = (struct tiny_serial *)timer_data;
	struct tty_struct *tty;
	int i;
	char data[1] = {TINY_DATA_CHARACTER};
	int data_size = 1;

	if (!tiny)
		return;

	tty = tiny->tty;

	/* send the data to the tty layer for users to read.  This doesn't
	 * actually push the data through unless tty->low_latency is set */
	for (i = 0; i < data_size; ++i) {
		if (tty->flip.count >= TTY_FLIPBUF_SIZE)
			tty_flip_buffer_push(tty);
		tty_insert_flip_char(tty, data[i], TTY_NORMAL);
	}
	tty_flip_buffer_push(tty);

	/* resubmit the timer again */
	tiny->timer->expires = jiffies + DELAY_TIME;
	add_timer(tiny->timer);
}

static int tiny_open(struct tty_struct *tty, struct file *file)
{
	struct tiny_serial *tiny;
	struct timer_list *timer;
	int index;

	/* initialize the pointer in case something fails */
	tty->driver_data = NULL;

	/* get the serial object associated with this tty pointer */
	index = tty->index;
	tiny = tiny_table[index];
	if (tiny == NULL) {
		/* first time accessing this device, let's create it */
		tiny = kmalloc(sizeof(*tiny), GFP_KERNEL);
		if (!tiny)
			return -ENOMEM;

		init_MUTEX(&tiny->sem);
		tiny->open_count = 0;
		tiny->timer = NULL;

		tiny_table[index] = tiny;
	}

	down(&tiny->sem);

	/* save our structure within the tty structure */
	tty->driver_data = tiny;
	tiny->tty = tty;

	++tiny->open_count;
	if (tiny->open_count == 1) {
		/* this is the first time this port is opened */
		/* do any hardware initialization needed here */

		/* create our timer and submit it */
		if (!tiny->timer) {
			timer = kmalloc(sizeof(*timer), GFP_KERNEL);
			if (!timer) {
				up(&tiny->sem);
				return -ENOMEM;
			}
			tiny->timer = timer;
		}
		tiny->timer->data = (unsigned long )tiny;
		tiny->timer->expires = jiffies + DELAY_TIME;
		tiny->timer->function = tiny_timer;
		add_timer(tiny->timer);
	}

	up(&tiny->sem);
	return 0;
}

static void do_close(struct tiny_serial *tiny)
{
	down(&tiny->sem);

	if (!tiny->open_count) {
		/* port was never opened */
		goto exit;
	}

	--tiny->open_count;
	if (tiny->open_count <= 0) {
		/* The port is being closed by the last user. */
		/* Do any hardware specific stuff here */

		/* shut down our timer */
		del_timer(tiny->timer);
	}
exit:
	up(&tiny->sem);
}

static void tiny_close(struct tty_struct *tty, struct file *file)
{
	struct tiny_serial *tiny = tty->driver_data;

	if (tiny)
		do_close(tiny);
}	

static int tiny_write(struct tty_struct *tty, 
		      const unsigned char *buffer, int count)
{
	struct tiny_serial *tiny = tty->driver_data;
	int i;
	int retval = -EINVAL;

	if (!tiny)
		return -ENODEV;

	down(&tiny->sem);

	if (!tiny->open_count)
		/* port was not opened */
		goto exit;

	/* fake sending the data out a hardware port by
	 * writing it to the kernel debug log.
	 */
	printk(KERN_DEBUG "%s - ", __FUNCTION__);
	for (i = 0; i < count; ++i)
		printk("%02x ", buffer[i]);
	printk("\n");
		
exit:
	up(&tiny->sem);
	return retval;
}

static int tiny_write_room(struct tty_struct *tty) 
{
	struct tiny_serial *tiny = tty->driver_data;
	int room = -EINVAL;

	if (!tiny)
		return -ENODEV;

	down(&tiny->sem);
	
	if (!tiny->open_count) {
		/* port was not opened */
		goto exit;
	}

	/* calculate how much room is left in the device */
	room = 255;

exit:
	up(&tiny->sem);
	return room;
}

#define RELEVANT_IFLAG(iflag) ((iflag) & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK))

static void tiny_set_termios(struct tty_struct *tty, struct termios *old_termios)
{
	unsigned int cflag;

	cflag = tty->termios->c_cflag;

	/* check that they really want us to change something */
	if (old_termios) {
		if ((cflag == old_termios->c_cflag) &&
		    (RELEVANT_IFLAG(tty->termios->c_iflag) == 
		     RELEVANT_IFLAG(old_termios->c_iflag))) {
			printk(KERN_DEBUG " - nothing to change...\n");
			return;
		}
	}

	/* get the byte size */
	switch (cflag & CSIZE) {
		case CS5:
			printk(KERN_DEBUG " - data bits = 5\n");
			break;
		case CS6:
			printk(KERN_DEBUG " - data bits = 6\n");
			break;
		case CS7:
			printk(KERN_DEBUG " - data bits = 7\n");
			break;
		default:
		case CS8:
			printk(KERN_DEBUG " - data bits = 8\n");
			break;
	}
	
	/* determine the parity */
	if (cflag & PARENB)
		if (cflag & PARODD)
			printk(KERN_DEBUG " - parity = odd\n");
		else
			printk(KERN_DEBUG " - parity = even\n");
	else
		printk(KERN_DEBUG " - parity = none\n");

	/* figure out the stop bits requested */
	if (cflag & CSTOPB)
		printk(KERN_DEBUG " - stop bits = 2\n");
	else
		printk(KERN_DEBUG " - stop bits = 1\n");

	/* figure out the hardware flow control settings */
	if (cflag & CRTSCTS)
		printk(KERN_DEBUG " - RTS/CTS is enabled\n");
	else
		printk(KERN_DEBUG " - RTS/CTS is disabled\n");
	
	/* determine software flow control */
	/* if we are implementing XON/XOFF, set the start and 
	 * stop character in the device */
	if (I_IXOFF(tty) || I_IXON(tty)) {
		unsigned char stop_char  = STOP_CHAR(tty);
		unsigned char start_char = START_CHAR(tty);

		/* if we are implementing INBOUND XON/XOFF */
		if (I_IXOFF(tty))
			printk(KERN_DEBUG " - INBOUND XON/XOFF is enabled, "
				"XON = %2x, XOFF = %2x", start_char, stop_char);
		else
			printk(KERN_DEBUG" - INBOUND XON/XOFF is disabled");

		/* if we are implementing OUTBOUND XON/XOFF */
		if (I_IXON(tty))
			printk(KERN_DEBUG" - OUTBOUND XON/XOFF is enabled, "
				"XON = %2x, XOFF = %2x", start_char, stop_char);
		else
			printk(KERN_DEBUG" - OUTBOUND XON/XOFF is disabled");
	}

	/* get the baud rate wanted */
	printk(KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty));
}

/* Our fake UART values */
#define MCR_DTR		0x01
#define MCR_RTS		0x02
#define MCR_LOOP	0x04
#define MSR_CTS		0x08
#define MSR_CD		0x10
#define MSR_RI		0x20
#define MSR_DSR		0x40

static int tiny_tiocmget(struct tty_struct *tty, struct file *file)
{
	struct tiny_serial *tiny = tty->driver_data;

	unsigned int result = 0;
	unsigned int msr = tiny->msr;
	unsigned int mcr = tiny->mcr;

	result = ((mcr & MCR_DTR)  ? TIOCM_DTR  : 0) |	/* DTR is set */
             ((mcr & MCR_RTS)  ? TIOCM_RTS  : 0) |	/* RTS is set */
             ((mcr & MCR_LOOP) ? TIOCM_LOOP : 0) |	/* LOOP is set */
             ((msr & MSR_CTS)  ? TIOCM_CTS  : 0) |	/* CTS is set */
             ((msr & MSR_CD)   ? TIOCM_CAR  : 0) |	/* Carrier detect is set*/
             ((msr & MSR_RI)   ? TIOCM_RI   : 0) |	/* Ring Indicator is set */
             ((msr & MSR_DSR)  ? TIOCM_DSR  : 0);	/* DSR is set */

	return result;
}

static int tiny_tiocmset(struct tty_struct *tty, struct file *file,
                         unsigned int set, unsigned int clear)
{
	struct tiny_serial *tiny = tty->driver_data;
	unsigned int mcr = tiny->mcr;

	if (set & TIOCM_RTS)
		mcr |= MCR_RTS;
	if (set & TIOCM_DTR)
		mcr |= MCR_RTS;

	if (clear & TIOCM_RTS)
		mcr &= ~MCR_RTS;
	if (clear & TIOCM_DTR)
		mcr &= ~MCR_RTS;

	/* set the new MCR value in the device */
	tiny->mcr = mcr;
	return 0;
}

static int tiny_read_proc(char *page, char **start, off_t off, int count,
                          int *eof, void *data)
{
	struct tiny_serial *tiny;
	off_t begin = 0;
	int length = 0;
	int i;

	length += sprintf(page, "tinyserinfo:1.0 driver:%s\n", DRIVER_VERSION);
	for (i = 0; i < TINY_TTY_MINORS && length < PAGE_SIZE; ++i) {
		tiny = tiny_table[i];
		if (tiny == NULL)
			continue;

		length += sprintf(page+length, "%d\n", i);
		if ((length + begin) > (off + count))
			goto done;
		if ((length + begin) < off) {
			begin += length;
			length = 0;
		}
	}
	*eof = 1;
done:
	if (off >= (length + begin))
		return 0;
	*start = page + (off-begin);
	return (count < begin+length-off) ? count : begin + length-off;
}

#define tiny_ioctl tiny_ioctl_tiocgserial
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
                      unsigned int cmd, unsigned long arg)
{
	struct tiny_serial *tiny = tty->driver_data;

	if (cmd == TIOCGSERIAL) {
		struct serial_struct tmp;

		if (!arg)
			return -EFAULT;

		memset(&tmp, 0, sizeof(tmp));

		tmp.type		= tiny->serial.type;
		tmp.line		= tiny->serial.line;
		tmp.port		= tiny->serial.port;
		tmp.irq			= tiny->serial.irq;
		tmp.flags		= ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ;
		tmp.xmit_fifo_size	= tiny->serial.xmit_fifo_size;
		tmp.baud_base		= tiny->serial.baud_base;
		tmp.close_delay		= 5*HZ;
		tmp.closing_wait	= 30*HZ;
		tmp.custom_divisor	= tiny->serial.custom_divisor;
		tmp.hub6		= tiny->serial.hub6;
		tmp.io_type		= tiny->serial.io_type;

		if (copy_to_user((void __user *)arg, &tmp, sizeof(struct serial_struct)))
			return -EFAULT;
		return 0;
	}
	return -ENOIOCTLCMD;
}
#undef tiny_ioctl

#define tiny_ioctl tiny_ioctl_tiocmiwait
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
                      unsigned int cmd, unsigned long arg)
{
	struct tiny_serial *tiny = tty->driver_data;

	if (cmd == TIOCMIWAIT) {
		DECLARE_WAITQUEUE(wait, current);
		struct async_icount cnow;
		struct async_icount cprev;

		cprev = tiny->icount;
		while (1) {
			add_wait_queue(&tiny->wait, &wait);
			set_current_state(TASK_INTERRUPTIBLE);
			schedule();
			remove_wait_queue(&tiny->wait, &wait);

			/* see if a signal woke us up */
			if (signal_pending(current))
				return -ERESTARTSYS;

			cnow = tiny->icount;
			if (cnow.rng == cprev.rng && cnow.dsr == cprev.dsr &&
			    cnow.dcd == cprev.dcd && cnow.cts == cprev.cts)
				return -EIO; /* no change => error */
			if (((arg & TIOCM_RNG) && (cnow.rng != cprev.rng)) ||
			    ((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr)) ||
			    ((arg & TIOCM_CD)  && (cnow.dcd != cprev.dcd)) ||
			    ((arg & TIOCM_CTS) && (cnow.cts != cprev.cts)) ) {
				return 0;
			}
			cprev = cnow;
		}

	}
	return -ENOIOCTLCMD;
}
#undef tiny_ioctl

#define tiny_ioctl tiny_ioctl_tiocgicount
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
                      unsigned int cmd, unsigned long arg)
{
	struct tiny_serial *tiny = tty->driver_data;

	if (cmd == TIOCGICOUNT) {
		struct async_icount cnow = tiny->icount;
		struct serial_icounter_struct icount;

		icount.cts	= cnow.cts;
		icount.dsr	= cnow.dsr;
		icount.rng	= cnow.rng;
		icount.dcd	= cnow.dcd;
		icount.rx	= cnow.rx;
		icount.tx	= cnow.tx;
		icount.frame	= cnow.frame;
		icount.overrun	= cnow.overrun;
		icount.parity	= cnow.parity;
		icount.brk	= cnow.brk;
		icount.buf_overrun = cnow.buf_overrun;

		if (copy_to_user((void __user *)arg, &icount, sizeof(icount)))
			return -EFAULT;
		return 0;
	}
	return -ENOIOCTLCMD;
}
#undef tiny_ioctl

/* the real tiny_ioctl function.  The above is done to get the small functions in the book */
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
                      unsigned int cmd, unsigned long arg)
{
	switch (cmd) {
	case TIOCGSERIAL:
		return tiny_ioctl_tiocgserial(tty, file, cmd, arg);
	case TIOCMIWAIT:
		return tiny_ioctl_tiocmiwait(tty, file, cmd, arg);
	case TIOCGICOUNT:
		return tiny_ioctl_tiocgicount(tty, file, cmd, arg);
	}

	return -ENOIOCTLCMD;
}

static struct tty_operations serial_ops = {
	.open = tiny_open,
	.close = tiny_close,
	.write = tiny_write,
	.write_room = tiny_write_room,
	.set_termios = tiny_set_termios,
};

static struct tty_driver *tiny_tty_driver;

static int __init tiny_init(void)
{
	int retval;
	int i;

	/* allocate the tty driver */
	tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS);
	if (!tiny_tty_driver)
		return -ENOMEM;

	/* initialize the tty driver */
	tiny_tty_driver->owner = THIS_MODULE;
	tiny_tty_driver->driver_name = "tiny_tty";
	tiny_tty_driver->name = "ttty";
	tiny_tty_driver->devfs_name = "tts/ttty%d";
	tiny_tty_driver->major = TINY_TTY_MAJOR,
	tiny_tty_driver->type = TTY_DRIVER_TYPE_SERIAL,
	tiny_tty_driver->subtype = SERIAL_TYPE_NORMAL,
	tiny_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS,
	tiny_tty_driver->init_termios = tty_std_termios;
	tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
	tty_set_operations(tiny_tty_driver, &serial_ops);

	/* hack to make the book purty, yet still use these functions in the
	 * real driver.  They really should be set up in the serial_ops
	 * structure above... */
	tiny_tty_driver->read_proc = tiny_read_proc;
	tiny_tty_driver->tiocmget = tiny_tiocmget;
	tiny_tty_driver->tiocmset = tiny_tiocmset;
	tiny_tty_driver->ioctl = tiny_ioctl;

	/* register the tty driver */
	retval = tty_register_driver(tiny_tty_driver);
	if (retval) {
		printk(KERN_ERR "failed to register tiny tty driver");
		put_tty_driver(tiny_tty_driver);
		return retval;
	}

	for (i = 0; i < TINY_TTY_MINORS; ++i)
		tty_register_device(tiny_tty_driver, i, NULL);

	printk(KERN_INFO DRIVER_DESC " " DRIVER_VERSION);
	return retval;
}

static void __exit tiny_exit(void)
{
	struct tiny_serial *tiny;
	int i;

	for (i = 0; i < TINY_TTY_MINORS; ++i)
		tty_unregister_device(tiny_tty_driver, i);
	tty_unregister_driver(tiny_tty_driver);

	/* shut down all of the timers and free the memory */
	for (i = 0; i < TINY_TTY_MINORS; ++i) {
		tiny = tiny_table[i];
		if (tiny) {
			/* close the port */
			while (tiny->open_count)
				do_close(tiny);

			/* shut down our timer and free the memory */
			del_timer(tiny->timer);
			kfree(tiny->timer);
			kfree(tiny);
			tiny_table[i] = NULL;
		}
	}
}

module_init(tiny_init);
module_exit(tiny_exit);

【参考文章】

  • Linux终端和Line discipline图解

  • tty驱动初步了解学习
    river->init_termios = tty_std_termios;
    tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
    tty_set_operations(tiny_tty_driver, &serial_ops);

    /* hack to make the book purty, yet still use these functions in the

    • real driver. They really should be set up in the serial_ops
    • structure above… */
      tiny_tty_driver->read_proc = tiny_read_proc;
      tiny_tty_driver->tiocmget = tiny_tiocmget;
      tiny_tty_driver->tiocmset = tiny_tiocmset;
      tiny_tty_driver->ioctl = tiny_ioctl;

    /* register the tty driver */
    retval = tty_register_driver(tiny_tty_driver);
    if (retval) {
    printk(KERN_ERR “failed to register tiny tty driver”);
    put_tty_driver(tiny_tty_driver);
    return retval;
    }

    for (i = 0; i < TINY_TTY_MINORS; ++i)
    tty_register_device(tiny_tty_driver, i, NULL);

    printk(KERN_INFO DRIVER_DESC " " DRIVER_VERSION);
    return retval;
    }

static void __exit tiny_exit(void)
{
struct tiny_serial *tiny;
int i;

for (i = 0; i < TINY_TTY_MINORS; ++i)
	tty_unregister_device(tiny_tty_driver, i);
tty_unregister_driver(tiny_tty_driver);

/* shut down all of the timers and free the memory */
for (i = 0; i < TINY_TTY_MINORS; ++i) {
	tiny = tiny_table[i];
	if (tiny) {
		/* close the port */
		while (tiny->open_count)
			do_close(tiny);

		/* shut down our timer and free the memory */
		del_timer(tiny->timer);
		kfree(tiny->timer);
		kfree(tiny);
		tiny_table[i] = NULL;
	}
}

}

module_init(tiny_init);
module_exit(tiny_exit);






【参考文章】

- [Linux终端和Line discipline图解](https://blog.csdn.net/dog250/article/details/78818612)
- [tty驱动初步了解学习](https://blog.csdn.net/zzsddre/article/details/125392085)
- LLD3
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一文彻底讲清Linux tty子系统架构及编程实例 的相关文章

  • 在Linux上编译C# + WPF以便在Windows上运行

    我有一个 C 应用程序 其中某些部分是使用 WPF 编写的 Mono 不支持 可以在 Linux 上编译这个应用程序吗 最终 该应用程序将在 Windows 上运行 但它是更大框架的一部分 并且我们的整个构建过程在 Linux 上运行 因此
  • Locale.getDefault() 始终返回 en

    unix 机器上的服务器始终使用 en 作为默认区域设置 以下是区域设置输出 LANG en US LC CTYPE C LC NUMERIC C LC TIME C LC COLLATE C LC MONETARY C LC MESSAG
  • 何时使用 pthread 条件变量?

    线程问题 看来 只有在其他线程调用 pthread cond notify 之前调用 pthread cond wait 时 条件变量才起作用 如果在等待之前发生通知 那么等待将被卡住 我的问题是 什么时候应该使用条件变量 调度程序可以抢占
  • 从 PL/SQL 调用 shell 脚本,但 shell 以 grid 用户而非 oracle 身份执行

    我正在尝试使用 Runtime getRuntime exec 从 Oracle 数据库内部执行 shell 脚本 在 Red Hat 5 5 上运行的 Oracle 11 2 0 4 EE CREATE OR REPLACE proced
  • awk 子串单个字符

    这是columns txt aaa bbb 3 ccc ddd 2 eee fff 1 3 3 g 3 hhh i jjj 3 kkk ll 3 mm nn oo 3 我可以找到第二列以 b 开头的行 awk if substr 2 1 1
  • 是否可以在Linux上将C转换为asm而不链接libc?

    测试平台为Linux 32位 但也欢迎 Windows 32 位上的某些解决方案 这是一个c代码片段 int a 0 printf d n a 如果我使用 gcc 生成汇编代码 gcc S test c 然后我会得到 movl 0 28 e
  • 强制卸载 NFS 安装目录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • 使用 grep 查找包含所有搜索字符串的行

    我有一个文件 其中包含很多与此类似的行 id 2796 some model Profile message type MODEL SAVE fields account 14 address null modification times
  • 如何使用 xterm.js 创建基于 Web 的终端以 ssh 进入本地网络上的系统

    我偶然发现了这个很棒的图书馆xterm js https xtermjs org 这也是 Visual Studio Code 终端的基础 我有一个非常普遍的问题 我想通过基于网络的终端 不在网络中 可能位于 aws 服务器上 访问本地网络
  • 如何禁用 GNOME 桌面屏幕锁定? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 如何阻止 GNOME 桌面在几分钟空闲时间后锁定屏幕 我已经尝试过官方手册了在红帽 https access redhat com doc
  • 如何在 shell 脚本中并行运行多个实例以提高时间效率[重复]

    这个问题在这里已经有答案了 我正在使用 shell 脚本 它读取 16000 行的输入文件 运行该脚本需要8个多小时 我需要减少它 所以我将其划分为 8 个实例并读取数据 其中我使用 for 循环迭代 8 个文件 并在其中使用 while
  • gdb查找行号的内存地址

    假设我已将 gdb 附加到一个进程 并且在其内存布局中有一个文件和行号 我想要其内存地址 如何获取文件x中第n行的内存地址 这是在 Linux x86 上 gdb info line test c 56 Line 56 of test c
  • 如何使用GDB修改内存内容?

    我知道我们可以使用几个命令来访问和读取内存 例如 print p x 但是如何更改任何特定位置的内存内容 在 GDB 中调试时 最简单的是设置程序变量 参见GDB 分配 http sourceware org gdb current onl
  • 添加要在给定命令中运行的 .env 变量

    我有一个 env 文件 其中包含如下变量 HELLO world SOMETHING nothing 前几天我发现了这个很棒的脚本 它将这些变量放入当前会话中 所以当我运行这样的东西时 cat env grep v xargs node t
  • arm64和armhf有什么区别?

    Raspberry Pi Type 3 具有 64 位 CPU 但其架构不是arm64 but armhf 有什么区别arm64 and armhf armhf代表 arm hard float 是给定的名称Debian 端口 https
  • 如何在Linux内核源代码中打印IP地址或MAC地址

    我必须通过修改 Linux 内核源代码来稍微改变 TCP 拥塞控制算法 但为了检查结果是否正确 我需要记录 MAC 或 IP 地址信息 我使用 PRINTK 函数来打印内核消息 但我感觉很难打印出主机的MAC IP地址 printk pM
  • CentOS:无法安装 Chromium 浏览器

    我正在尝试在 centOS 6 i 中安装 chromium 以 root 用户身份运行以下命令 cd etc yum repos d wget http repos fedorapeople org repos spot chromium
  • Linux 可执行文件与 OS X“兼容”吗?

    如果您在基于 Linux 的平台上用 C 语言编译一个程序 然后将其移植以使用 MacOS 库 它会工作吗 来自编译器的核心机器代码在 Mac 和 Linux 上兼容吗 我问这个问题的原因是因为两者都是 基于 UNIX 的 所以我认为这是真
  • 安装J语言的JQt IDE,出现错误

    我一直按照这里的说明进行操作 http code jsoftware com wiki System Installation Linux http code jsoftware com wiki System Installation L
  • jpegtran 优化而不更改文件名

    我需要优化一些图像 但不更改它们的名称 jpegtran copy none optimize image jpg gt image jpg 但是 这似乎创建了 0 的文件大小 当我对不同的文件名执行此操作时 大小仍然完全相同 怎么样 jp

随机推荐

  • 【机器视觉】——裂纹检测笔记

    目录 传统算法处理裂缝的基本思路 第一种思路 第二种思路 第三种思路 CPP代码 halcon代码 python代码 Matlab代码 深度学习缺陷检测 裂缝检测文献 传统算法处理裂缝的基本思路 第一种思路 1 先转换彩色图为灰度图 2 进
  • MySQL 8.0 隐藏索引

    隐式索引 最明显的一个作用类似 索引回收站 例如数据库长时间运行后 会积累很多索引 做数据库优化时 想清理掉没什么用的多余的索引 但可能删除某个索引后 数据库性能下降了 发现这个索引是有用的 就要重新建立 对于较大的表来说 删除 重建索引的
  • python筑基——基础知识作业汇总,学习笔记

    作业一 语法 变量 输 输出 基本运算 基本数据类型 字符串 类型转换 1 计算整型50乘以10再除以5的商并使用print输出 result 50 10 5 print result 2 判断整型8是否大于10的结果并使用print输出
  • codeforces 825 E Minimal Labels

    Problem codeforces com contest 825 problem E Reference 看第 5 条评论 Meaning 给出一个n个结点的DAG 找一个给结点编号的序列 且满足3个条件 编号为 1 n 每个编号出现且
  • 第三章:Linux环境基础开发工具使用

    系列文章目录 文章目录 系列文章目录 前言 一 yum 1 三板斧 2 扩展 3 软件包名称 二 vim 1 vim基本模式 2 vim基本操作 插入模式 命令模式 底行模式 注释 三 gcc g 1 预处理 2 编译 3 汇编 4 链接
  • Python opencv 机器学习 2.knn k近邻 ocr识别数字 使用digits.png(opencv自带)

    import cv2 import numpy as np from matplotlib import pyplot as plt 识别数字OCR img cv2 imread digits png gray cv2 cvtColor i
  • 见习网工之综合实验

    需求一 信息中心配置Eth trunk实现链路冗余 SW1 interface Eth Trunk1 mode lacp static least active linknumber 1 trunkport GigabitEthernet
  • 【每日一练】在JSX中使用条件渲染

    条件渲染 技术方案 三元表达式 逻辑 运算 1 三元表达式 满足条件才渲染一个span标签 const flag true function App return div flag div h1 span 我是JackWoot span h
  • 自动化测试如何做?接口自动化测试如何才能做好?

    前言 接口自动化测试常用框架 Python requests pytest yaml alluer Jenkins 接口自动化测试的目的 自动化测试的主要目的是用来回归测试的 当代码有变化时 有可能影响不应该变化的逻辑 这个时候为了确认这种
  • There was a problem importing one of the Python modules required to run yum

    为什么80 的码农都做不了架构师 gt gt gt 最近从python 2 6 升级到python2 7 导致 yum 不可用 原因主要是yum 不支持python27 因此需要更改yum的可用路径 which yum 查看下yum的安装路
  • js拖拽实现

  • 强化学习的A3C算法应用(训练Atari游戏)

    A3C算法的全称是Asynchronous Advantage Actor Critic 异步优势执行者 评论者算法 这个算法和优势执行者 评论者算法的区别在于 在执行过程中不是每一步都更新参数 而是在回合结束后用整个轨迹进行更新 因此可以
  • 多个git合并,并保留log历史记录

    面临的需求是 将多个git仓库作为一个单独目录 整合到一个新的git仓库中 并且保留历史记录 1 新建一个summary仓库 新建一个summary仓库 用于整合一系列git仓库 git clone
  • openwrt编译x86固件

    x86 openwrt固件编译 2017年十月四日我在珠海 中秋之际写下这篇文章 祝各位看官花好月圆 有情人终成眷属 最近一直在玩Openwrt 以前上学的时候接触一丁丁 但是只限于烧写别人编译好的固件 这次要真刀实干了 学习了一周各种百度
  • 专访用自己姓氏命名编译器YC++的创始人

    在CSDN的论坛里看到了这样的一条帖子 请使用中国人开发的C C 编译器 网页浏览器内核 并提供了该软件的下载地址 从大家的跟帖来看很多人 是很有兴趣的 但是作者并没有留下太多的介绍说明类的文字 为了一探究竟 我拨通了作者留下的电话并完成了
  • Ubuntu 16.04设置root用户登录图形界面

    Ubuntu默认的是root用户不能登录图形界面的 只能以其他用户登录图形界面 这样就很麻烦 因为权限的问题 不能随意复制删除文件 用gedit编辑文件时经常不能保存 只能用vim去编辑 下面以Ubuntu 16 04版为例说明 1 打开终
  • STM32实战项目:HAL_RCC_OscConfig中程序卡死问题解决办法

    STM32实战项目经验 HAL RCC OscConfig中程序卡死问题解决办法 工程环境 STM32CUBEIDE STM32F405VG 现象复现 项目中一个是IAP程序 另一个是APP程序 两个程序都是使用STM32CubeIDE生成
  • 搜索题目综合

    BFS 1 小X学游泳 题解 枚举每一个点作为连通块的起点 求得连通块大小 然后打擂台求最值即可 参考代码 include
  • element-ui和element-plus的自定义列表格用法

    前言 element plus 这个 UI 组件库 虽说基本和 vue2 element ui 差不多 但还是有点区别 以至于按emenent ui的写法来写会发现报错 下面我将讲解一下element ui和element plus的自定义
  • 一文彻底讲清Linux tty子系统架构及编程实例

    摘要 本文详细解读了linux系统下的tty子系统的深层次原理和架构 并参考了LDD3中的代码实例讲述了无硬件下实现一个简单的tty设备驱动模块的编写 对了解tty子系统及下部串口驱动模块的学习有较好的参考价值 1 tty设备简介 tty一