分类:
2012-11-27 23:26:18
本文为你展示基于“串口通信设备”的TTY设备驱动程序内部结构,并讲述这些驱动是如何实现链路层通信协议(包括ppp和slip)的应用。本文的代码基于2.4内核,大部分适用于2.2和2.0[注]。
注:本文的代码虽然基于2.4内核,但是结构原理在现在内核版本仍然存在。
串口驱动误解当我们说【串口驱动】的时候,我们第一个想到的东西是/dev/ttyS0,因为它是大家所熟知的串口通信的设备文件(至少在PC系统上如此)。由于/dev/ttyS0是一个字符型的设备文件,大家都以为串口驱动是字符设备驱动,这种推断不能叫错,只是不够准确。【串口驱动】不是一般的字符驱动,有两点可以证明:
串口驱动的“串口”从分类就看出来它是一种特殊的驱动类型,归于字符驱动大类下。
查看 /proc/devices ,你会发现ttyS驱动的主设备号是4,这是一个善意的谎言:文本模式控制台设备的主设备号也是4。事实上,2.0版内核用了一个通用的名字“ttyp”代表主设备号4。
串口设备之所以与一般的字符设备(比如并口打印机或磁带机)存在不同是因为串口被用来实现高一级的抽象——tty,串口设备为tty设备提供了串行通信信道。tty的名字来源早期的一种串口应用设备——电传打字机(tele-type)。
tty设备驱动子系统tty设备是什么样的设备呢?tty的概念其实已经被泛化,不只是指处理数字文字(alphanumeric)字符的终端设备,例如基于VGA和帧
缓冲式的文本控制台、xterm虚拟终端等。[tty设备驱动子系统]以下图那样的一种结构实现了对tty设备的泛化。某特定tty设备驱动的特殊部分以
注册的方式注册入通用的子系统部分,从而实现一支特定tty设备驱动。注册方式的好处是设备驱动的特殊部分可以以内核模块形式(kernel
module)实现。
Figure 1
图中展示出有三部分是可注册的,串行通信设备驱动(serial.c)、线路规则(n_tty.c)和[tty设备驱动子系统]本身 (tty_io.c)。可以这样理解,tty设备是一种特殊的字符设备,所以它需通过[字符设备驱动子系统](fs/devices.c)注册自身。[串 行通信设备驱动]是不能被用户直接使用的,它必须抽象为一个tty设备,然后配置使用默认的线路规则——n_tty.c,经[tty设备驱动子系统]在系 统中注册为字符设备。
由此可见,[tty设备驱动子系统]被划分为三块,数据流从用户空间到串行设备间夹着一层tty。虽然如此,tty_io.c本身只是桥梁,实际的 tty操作是线路规则完成,线路规则(line discipline)是一支定义串口线如何使用的软件模块。Linux默认是线路规则是N_TTY,N_TTY是标准字符终端IO处理规则。
为何如此复杂?这种分层设计看上去就很复杂,有必要吗?复杂的回报是灵活性。当tty设备应用种类纷繁时,很有必要。为新串行设备编写驱动比一般字符设备要复杂,有了这种通用抽象模型,为编写串口设备驱动省下很多功夫。
更重一点是,[tty设备驱动子系统]把线路规则也卸下,进一步的抽象后,线路规则也可替换。这样,串行设备驱动根本不知道数据是从哪来的,它只管收发就行了。
PPP和slip如果你使用modem(使用PPP链路协议)拨号上网,或者用SLIP互联PC和你的掌上PDA,你就体会到上面提到的复杂性。ppp和SLIP实现了各自的线路规则,当它们中任何一个应用要运行,tty设备必须在它们的线路规则模块间切换,来构造不同的tty设备。
有趣的是,SLIP驱动向[tty设备驱动子系统]和[网络子系统]同时注册,前者是线路规则N_SLIP,后者网络设备slip0。当tty设备 切换为N_SLIP后,串行通信数据经TCP/IP协议栈与用户空间通信。当tty设备被切为TCP/IP网络设备后,其它用户进程没法读写 基于/dev/ttyS的tty设备。这也说明了为什么网络通道建立后,slattach 和 pppd 都不能退出的原因。
关键数据结构[tty设备驱动子系统]由三个主要的数据结构构建:
struct tty_struct:这是[tty设备]的核心表征数据结构,结构体内内嵌(通过指针)余下的两个关键数据结构。每当有新的tty设备被打开使用 时,tty_struct都被创建一个新实例,直到设备被关闭。注意实际代码里(tty_io.c)有很多复杂的操作,例如在打开和关闭tty设备的过程 中对的termios设定的保存与恢复(至少基于串口的tty设备如此)。
struct tty_driver:这是驱动本身的表征,定义设备号,定义并实现设备的使用接口等,在设备打开的时,get_tty_driver函数负责关联设备与设备驱动。tty_driver结构成员大略如下:
struct tty_ldisc:线路规则模块由tty_struct的ldisc域指定。在设备打开的时候,ldisc默认指向n_tty,用户空间程序可以通过ioctl切换tty设备的线路规则。tty_ldisc结构成员大略如下:
作为一般的tty设备驱动程序开发者,可以不必关心tty_struct的实现,只管tty_driver 和 tty_ldisc就行了。因为当应用系统使用新串行通信设备,我们一般实现tty_driver就行了;如果应用系统有传统的串口,并且想开发新上层应 用,那实现一支新线路规则模块就行了。例如,假设你手头有一块特殊的键盘,使用标准RS-232串口,那么需要实现一支新线路规则模块就可以了,因为整套 的串口tty设备驱动有了,键盘驱动已经有了。新线路规则模块完成与input子系统和通用键盘驱动的通信。
设备的读写数据流
Figure 3
图三展示了数据是如何从用户空间流向硬件接口并返回的。从图可知,“写入数据”是比较直观,“读出数据”需要解释一下。“读数据”稍微复杂,因为 “读数据”时硬件和用户空间没有直接的调用关系。因为只有用户空间调用硬件,硬件不能调用用户空间代码,所以只能把收到的数据存放起来,等用户空间程序来 读。你可能已经猜到解决办法——使用缓冲区:硬件收到的数据会存放到内核的缓冲区并保持着,直到有用户空间程序把它们读走。当某用户程序读取缓冲区,而缓 冲区为空的时候,用户程序被置入睡眠状态,只有当缓冲区收到数据后才被唤醒。
注意,其实“写数据”的操作一样有缓冲区。只是“写操作”是一步接一步由上而下,控制流比较简单直接,不像读操作有数据传输的延迟,写操作的缓冲区只是为了“软化”硬件传输操作和软件控制操作的边界。为了精简图示,图中没有画出写操作缓冲区。
对tty设备而言,[读缓冲区]应该集成在tty的数据结构内,虽然这样设计会让tty_struct变得相当肥大,但是没有理由表明缓冲区可以放在其它地方。tty设备传输不能没有缓冲,而tty设备是动态创建的,所以即使缓冲占有空间,至少不会浪费空间。
tty设备被设计使用两种类型的缓冲区,内核开发者选择了在线路规则模块内使用普通的缓冲区(tty buffer),而低层硬件驱动使用翻转式缓冲区(flip_buffer),使用翻转式缓冲,硬件驱动可以尽可能快的接收传入的数据,不必与上层同步操 作:flip_buffers由硬件独占使用,flip_buffers的数据被tty_flip_buffer_push函数转存入tty buffer。
所谓的翻转式缓冲区其实就是两块一样大小的可以来回写入数据的物理缓冲区块。在底层的中断处理函数返回前,函数flush_to_ldisc会被调 用来翻转缓冲区的读写指针,翻转的具体实现可参考drivers/char/tty_io.c中的flush_to_ldisc()。
如何切换自定的线路规则模块[tty设备驱动子系统]提供了线路规则模块的注册接口(tty_register_ldisc()),定义了线路规则驱动结构 (tty_ldisc ),驱动开发者可以自定线程路规则。线路规则有一个数值标识,有一个符号名。例如,缺省的tty线路规则的标识值是N_TTY,PPP的标识值是 N_PPP,这些值在include/linux/tty.h 内静态定义。值得注意的是,目前的设计没有保留可用的标识值给本地自定使用,因此你只能手动在系统中“偷”一个值来使用。例如到目前为止,N_MOUSE 还没有被广泛使用,你可以用它来自定一个线路规则 。
要切换到N_MOUSE,在用户程序中使用以下代码: