tty 层

作者:Greg Kroah-Hartman

欢迎来到一个名为“Driving Me Nuts”(让我抓狂)的新专栏。在这里,我们将探索不同的 Linux 内核驱动子系统,并尝试理解它们提供的各种不同接口以及对驱动程序的期望。如果有人想了解任何特定的子系统,请给我发送电子邮件。

有很多关于 Linux 内核编程和 Linux 驱动程序编程的优秀参考资料(参见“资源”部分)。本专栏假设您过去至少浏览过这些资料,或者手边有这些资料作为参考。

首先,让我们研究一下内核的 tty 层。所有 Linux 用户在命令提示符下输入或使用串行端口连接时都会使用此层。

每个 tty 驱动程序都需要创建一个 struct tty_driver 来描述自身,并将该结构注册到 tty 层。struct tty_driver 在 include/linux/tty_driver.h 文件中定义。清单 1 [可在 ftp.linuxjournal.com/pub/lj/listings/issue100/5896.tgz 获取] 显示了 2.4.18 内核版本中该结构的样貌。这是一个相当庞大且令人望而生畏的结构,所以让我们尝试将其分解成更小的部分。

“magic”字段应始终设置为 TTY_DRIVER_MAGIC。tty 层使用它来验证它是否真的在处理 tty 驱动程序。

driver_name 和 name 字段用于描述您的驱动程序,driver_name 应设置为具有描述性的内容,因为它将显示在 /proc/tty/drivers 文件中。name 字段用于指定驱动程序的 /dev 或 devfs 名称库。例如,内核串行驱动程序将 driver_name 字段设置为 serial,如果未启用 devfs,则将 name 字段设置为 ttyS,如果启用了 devfs,则设置为 tts/%d。如果启用了 devfs,它将在为您的驱动程序创建新的设备节点时使用 name 字段。name 的 %d 部分将在设备在 tty 子系统中注册时填充设备的次要编号。

只有当您的设备不从次要编号 0 开始时,name_base 字段才是必要的。对于几乎所有驱动程序,这都应设置为 0。

major、minor_start 和 num 字段用于描述分配给 tty 层的驱动程序的主/次要编号。major 字段应设置为分配给您的驱动程序的主设备号。如果您正在创建新的驱动程序,请阅读 Documentation/devices.txt 文件,了解如何为您的驱动程序获取新的主设备号。对于任何想了解什么驱动程序使用什么主/次要编号对的人来说,该文件也是很好的参考资料。minor_start 字段用于指定您的设备的第一个次要编号在哪里。如果您为您的驱动程序分配了整个主设备号,则应将其设置为 0。num 字段描述了分配给您的驱动程序的不同次要编号的数量。

因此,如果您将主设备号 188 的全部都分配给了您的驱动程序,那么您的驱动程序应将这些字段设置为

  • major: 188,

  • minor_start: 0,

  • num: 255,

type 和 subtype 字段描述了您的驱动程序对 tty 层来说是什么类型的 tty 驱动程序。type 字段可以设置为以下值

  • TTY_DRIVER_TYPE_SYSTEM:tty 子系统内部使用,用于通知自身正在处理内部 tty 驱动程序。如果使用此值,则 subtype 应设置为 SYSTEM_TYPE_TTY、SYSTEM_TYPE_CONSOLE、SYSTEM_TYPE_SYSCONS 或 SYSTEM_TYPE_SYSPTMX。任何正常的 tty 驱动程序都不应使用此类型。

  • TTY_DRIVER_TYPE_CONSOLE:仅由控制台驱动程序使用。不要将其用于任何其他驱动程序。

  • TTY_DRIVER_TYPE_SERIAL:任何串行类型驱动程序使用。如果使用此值,则 subtype 应设置为 SERIAL_TYPE_NORMAL 或 SERIAL_TYPE_CALLOUT,具体取决于您的驱动程序的类型。这是 type 字段最常见的设置之一。

  • TTY_DRIVER_TYPE_PTY:伪终端接口 (pty) 使用。如果使用此值,则 subtype 需要设置为 PTY_TYPE_MASTER 或 PTY_TYPE_SLAVE。

init_termios 字段用于在首次创建设备时设置设备的初始 termios(线路设置和速度)。

flags 字段设置为以下位值的混合,具体取决于驱动程序的需求

  • TTY_DRIVER_INSTALLED:如果设置了此位,则驱动程序无法自行向 tty 层注册,因此请勿使用此值。

  • TTY_DRIVER_RESET_TERMIOS:如果设置了此位,则每当最后一个进程关闭设备时,tty 层将重置 termios 设置。这对于控制台和 pty 驱动程序很有用。

  • TTY_DRIVER_REAL_RAW:如果设置了此位,则表示驱动程序保证在行驱动程序未要求通知奇偶校验或中断字符的情况下,将这些字符的通知设置为行驱动程序。这通常为所有驱动程序设置,因为它允许稍微优化行驱动程序。

  • TTY_DRIVER_NO_DEVFS:如果设置了此位,则调用 tty_register_driver() 将不会创建任何 devfs 条目。这对于任何动态创建和销毁次要设备的驱动程序都很有用,具体取决于物理设备是否存在。设置此位的驱动程序示例包括 USB 转串行驱动程序、USB 调制解调器驱动程序和 USB 蓝牙 tty 驱动程序。

refcount 字段是指向 tty 驱动程序中整数的指针。tty 层使用它来处理驱动程序的正确引用计数,tty 驱动程序不应触及它。

proc_entry 字段不应由 tty 驱动程序本身设置。如果 tty 驱动程序实现了 write_proc 或 read_proc 函数,则此字段将包含为其创建的驱动程序的 proc_entry 字段。

other 字段仅由 pty 驱动程序使用,任何其他 tty 驱动程序都不应使用。

现在我们有一些指向不同 tty 结构的指针。table 字段是指向 tty_struct 指针数组的指针。termios 和 termios_locked 字段是指向 struct termios 指针数组的指针。所有这些数组的条目数都应与您在上面设置的次要字段相同。tty 层使用它们来正确处理不同的次要设备,您的 tty 驱动程序不应触及它们。

driver_state 字段仅由 pty 驱动程序使用,任何其他 tty 驱动程序都不应使用。

tty_driver 结构中有很多不同的函数指针。tty 层使用这些函数指针在想要执行某些操作时调用 tty 驱动程序。并非所有函数指针都必须由 tty 驱动程序定义,但其中一些是必需的。

当对分配给您的 tty 驱动程序的设备节点调用 open(2) 时,tty 层会调用 open 函数。tty 层使用指向分配给此设备的 tty_struct 结构的指针和文件指针来调用此函数。tty 驱动程序必须设置此字段才能正常工作(否则,在调用 open(2) 时,将向用户返回 -ENODEV)。

当对先前通过调用 open(2) 创建的文件指针调用 release(2) 时,tty 层会调用 close 函数。这意味着应关闭设备。

当数据要发送到您的 tty 设备时,tty 层会调用 write 函数。数据可能来自用户空间或内核空间(如果数据来自用户空间,则将设置 from_user 字段)。此函数应返回实际写入设备的字符数。必须为 tty 驱动程序设置此函数。

当要将单个字符写入设备时,tty 层会调用 put_char 函数。如果设备中没有空间发送字符,则可能会忽略该字符。如果 tty 驱动程序未定义此函数,则当 tty 层想要发送单个字符时,将调用 write 函数。

当 tty 层使用 put_char 函数向 tty 驱动程序发送了多个字符后,将调用 flush_chars 函数。tty 驱动程序应告知设备发送串行线路中剩余的所有数据。

当 tty 层想要知道 tty 驱动程序的写入缓冲区中有多少可用空间时,将调用 write_room 函数。随着字符从写入缓冲区中清空,此数字将随时间变化。

当 tty 层想要知道 tty 驱动程序的写入缓冲区中还有多少字符要发送时,将调用 chars_in_buffer 函数。

当对设备节点调用 ioctl(2) 时,tty 层会调用 ioctl 函数。它允许 tty 驱动程序实现特定于设备的 ioctl。如果驱动程序不支持请求的 ioctl,则应返回 -ENOIOCTLCMD。这允许 tty 层在可能的情况下实现 ioctl 的通用版本。

当设备的 termios 设置已更改时,tty 层会调用 set_termios 函数。然后,tty 驱动程序应根据 termios 结构的不同字段更改设备的物理设置。tty 驱动程序应能够处理在调用此函数时旧变量可能设置为 NULL 的情况。

throttle 和 unthrottle 函数用于帮助控制 tty 层输入缓冲区的溢出。当 tty 层的输入缓冲区即将满时,将调用 throttle 函数。tty 驱动程序应尝试向设备发出信号,表明不再向其发送字符。当 tty 层的输入缓冲区已清空,现在可以接受更多数据时,将调用 unthrottle 函数。然后,tty 驱动程序应向设备发出信号,表明可以接收数据。

stop 和 start 函数与 throttle 和 unthrottle 函数非常相似,但它们表示 tty 驱动程序应停止向设备发送数据,然后在稍后恢复发送数据。

当 tty 驱动程序应挂断 tty 设备时,将调用 hangup 函数。

当 tty 驱动程序要打开或关闭 RS-232 端口上的 BREAK 状态时,将调用 break_ctrl 函数。如果 state 设置为 -1,则应打开 BREAK 状态。如果 state 设置为 0,则应关闭 BREAK 状态。如果 tty 驱动程序实现了此函数,则 tty 层将处理 TCSBRK、TCSBRKP、TIOCSBRK 和 TIOCCBRK ioctl。否则,这些 ioctl 将发送到 tty 驱动程序的 ioctl 函数。

当 tty 驱动程序要刷新其写入缓冲区中仍然存在的所有数据时,将调用 flush_buffer 函数。这意味着其中剩余的任何数据都将丢失,并且不会发送到设备。

当 tty 层更改了 tty 驱动程序的线路规程时,将调用 set_ldisc 函数。此函数通常不再使用,不应设置。

当 tty 层希望 tty 驱动程序的写入缓冲区中的所有挂起数据都发送到设备时,将调用 wait_until_sent 函数。该函数应在完成之前不返回,并允许休眠以实现此目的。

send_xchar 函数用于向 tty 设备发送高优先级的 XON 或 XOFF 字符。

如果驱动程序想要实现 /proc/tty/driver/<name> 条目,则使用 read_proc 和 write_proc 函数;<name> 将设置为上面描述的 name 字段。如果设置了这两个函数中的任何一个,则将创建该条目,并且任何 read(2) 或 write(2) 调用都将传递给相应的函数。

最后,next 和 prev 字段由 tty 层用于将所有不同的 tty 驱动程序链接在一起,tty 驱动程序不应触及它们。

没有读取?

在上面的函数列表中可能突出的一件事是缺少要由 tty 驱动程序实现的读取函数。tty 层包含一个缓冲区,当对 tty 设备节点调用 read(2) 时,它使用该缓冲区将数据发送到用户空间。每当 tty 驱动程序从设备接收到任何数据时,都需要填充此缓冲区。因为 tty 数据不会在用户请求或需要时显示,所以此模型是必要的。这样,tty 层会缓冲任何接收到的数据,并且各个 tty 驱动程序不必担心阻塞直到数据出现在 tty 线路中。

微型 tty 驱动程序

现在我们已经介绍了所有不同的字段,那么哪些字段对于启动并运行基本的 tty 驱动程序实际上是必要的呢?清单 2 是最简 tty 驱动程序的示例。完成那里显示的步骤后,创建 tiny_open、tiny_close、tiny_write 和 tiny_write_room 函数,您就完成了微型 tty 驱动程序的编写。

清单 2. 最小 TTY 驱动程序

有关微型 tty 驱动程序的示例实现,请参见清单 3 [可在 ftp.linuxjournal.com/pub/lj/listings/issue100/5896.tgz 获取]。此 tty 驱动程序创建一个定时器,每两秒钟将数据放入 tty 层,以模拟真实硬件。当在 SMP 机器上运行时,它还正确处理锁定设备结构。

数据流

当调用 tty 驱动程序的 open 函数时,驱动程序应将一些数据保存在传递给它的 tty_struct 变量中。这样,当稍后调用 close、write 和其他函数时,tty 驱动程序将知道正在引用哪个设备。如果未完成此操作,则驱动程序可以从 MINOR(tty->device) 函数中获取密钥,该函数返回设备的次要编号。

如果您查看 tiny_open 函数,tiny_serial 结构会保存在 tty 驱动程序中。这允许 tiny_write、tiny_write_room 和 tiny_close 函数检索 tiny_serial 结构并正确地操作它。

对于同一设备,tty 驱动程序的 open 和 close 函数可以多次调用,因为不同的用户程序连接到该设备。这可能允许一个进程读取数据,而另一个进程写入数据。为了正确处理所有事情,您应记录端口已打开或关闭的次数。当端口首次打开时,您可以执行必要的硬件初始化和内存分配。当端口最后一次关闭时,您可以执行正确的硬件关闭并释放任何已分配的内存。有关如何计算端口打开次数的示例,请参见 tiny_open() 和 tiny_close() 函数。

当从硬件接收到数据时,需要将其放入 tty 设备的翻转缓冲区中。这可以使用以下代码段完成

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], 0);
}
tty_flip_buffer_push(tty);

此示例确保在添加数据时 tty 翻转缓冲区中没有缓冲区溢出。对于以非常高的速率接受数据的驱动程序,应设置 tty->low_latency 标志,这将导致最后一次调用 tty_flip_buffer_push() 在调用时立即执行。在示例驱动程序中,tty_timer 函数将一个字节的数据插入到 tty 的翻转缓冲区中,然后重新提交定时器以再次调用。这模拟了从硬件设备接收到的缓慢字符流。

当要将数据发送到硬件时,将调用 write 函数。重要的是,write 调用检查 from_user 标志,以防止它意外地尝试将用户空间缓冲区直接复制到内核空间。write 函数允许返回短写入。这意味着设备无法发送所有请求的数据。调用 write(2) 函数的用户空间程序有责任正确检查返回值,以确定是否真的发送了所有数据。在用户空间中进行此检查比内核驱动程序休眠直到可以发送所有请求的数据要容易得多。

tty 接口随时间推移

从 2.0、2.2 和 2.4 内核版本开始,tty 层一直非常稳定,随着时间的推移,仅添加了少量功能。因此,为 2.0 编写的 tty 驱动程序可以在 2.4 上成功运行,几乎无需更改。在整个 2.5 内核系列中,tty 层已被标记为重写,因此本文可能描述了不再真实的内容。如有疑问,请阅读您要开发的内核版本的 include/tty_driver.h 文件。此外,请查看 driver/char 内核目录中的任何 tty 驱动程序,以获取有关如何实现此处未涵盖的某些函数的示例。

结论

我们已经介绍了 tty 层的基本知识,解释了 2.4 内核树中 tty_driver 结构中的所有不同字段,并指出了驱动程序需要实现的字段。tiny_tty.c 驱动程序(参见清单 3 [可在 ftp.linuxjournal.com/pub/lj/listings/issue100/5896.tgz 获取])是一个很好的示例,说明了非常简化的 tty 驱动程序如何成功工作。欢迎在将来将此代码用作您自己的 tty 驱动程序的示例。

资源

The tty Layer
Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。
加载 Disqus 评论