TTY设备
一、TTY设备
在*nix中,tty设备用来抽象串口类型的设备,它位于字符驱动之下,抽象了串口设备需要的特性、功能,抽象后的一个tty设备即可表示一个串行输入、输出接口(比如控制台口,串口、pty设备接口)。
TTY的实现由两部分组成:
- Tty core:它以统一一致的方式来处理流向某个tty设备的数据以及来自某个tty设备的数据,并向用户空间提供了统一一致的用户接口,向底层即真实的设备驱动提供了统一一致的编程接口。利用这些特性,可以很容易为一个新的串口设备编写驱动程序以及用户程序。
- Line discipline:它是线路规程的意思。正如它的名字一样,它表示的是这条终端”线程”的输入与输出规范设置。主要用来进行输入/输出数据的预处理,写入设备的数据要先经过它的处理才会被发送给真实的设备驱动,从设备接收的数据也会先经过它的处理才会进入到tty core的处理逻辑。
用户空间,tty以及真实的设备驱动之间的关系大致如图所示:
1.1 初始化
tty的初始化包括几部分:
- 尽快将控制台口给初始化起来以使得内核启动过程中可以将log打印到终端
- 创建tty class
- 初始化tty设备
1.1.1 初始化控制台口
在start_kernel会很快的调用tty中的console_init函数,它会完成:
- 调用tty_ldisc_begin将line discipline tty_ldisc_N_TTY注册到系统中
- 调用所有位于__con_initcall_start和__con_initcall_end之间的控制台口初始化函数
位于__con_initcall_start和__con_initcall_end之间的控制台口初始化函数是通过console_initcall声明的,只要一个初始化函数被加了给前缀,它就会被放入该区域,该区域和其它初始化区域一样定义于头文件“vmlinux.lds.h”中。
一个使用console_initcall声明的函数至少需要条用register_console将其注册为控制台口,这样内核的输出才能被送到该设备。
1.1.2 创建tty_class
在系统启动中,还会调用tty_class_init来创建一个tty class,它是tty设备的class。
1.1.3 初始化tty设备
在系统启动阶段完成的最后一件属于tty的初始化动作就是调用tty_init完成tty的初始化。该函数会完成/dev/tty/和/dev/console的初始化。对这两个设备,都分别会顺序完成:
- 调用cdev_init来初始化对应的字符设备数据结构并将设备操作和数据结构关联起来
- 调用cdev_add将设备添加到系统中,(所有的字符设备最终都添加到了cdev_map中)
- 调用register_chrdev_region将该字符设备需要使用的设备号注册到系统中,所有字符设备所使用的设备号都保存在chrdevs数据结构中
- 调用device_create创建设备,并将设备添加到系统中
1.2 与上层以及下层的接口
tty设备是串口类设备的抽象,它以统一一致的方式来处理流向某个tty设备的数据以及来自某个tty设备的数据,并向用户空间提供了统一一致的用户接口,向底层即真实的设备驱动提供了统一一致的编程接口。
1.2.1 与上层(实际上就是tty设备的使用者)的接口
tty设备是字符设备,类似于其它的设备,在*niux中,它也被看做一个文件,因而也就有其相应的文件操作,它向其用户提供的接口就是通过文件操作提供的。tty核心提供了两个文件操作集:static const struct file_operations tty_fops和static const struct file_operations console_fops以及一个假的文件操作集static const struct file_operations hung_up_tty_fops,tty设备的文件操作指向其中一个。
如果一个用户想要使用某个tty设备, 只要该tty对应的设备之后,就可以使用其中的文件操作来读写tty设备了。tty核心会把相应的操作转换为对设备的读写。
在文件操作中,打开文件操作是比较关键的,由于在打开后即可对打开的文件句柄进行操作,因而打开操作就至少需要找到对应的设备,找到对应的文件操作指针,在文件打开后,就可以直接对文件句柄进行操作了,因而我们简单分析下tty open的操作:
- 首先尝试打开当前的tty
- 如果没有打开就尝试找到设备驱动,然后根据驱动去查找tty
- 首先根据设备号信息查找驱动程序
- 如果驱动程序提供了lookup函数就利用它来查找相应的tty,否则取tty_driver->ttys的对应于该设备号的变量
- 如果找到了对应的tty,就调用tty_reopen快速打开它(实际上就是增加引用计数)
- 否则
- 申请并初始化一个tty数据结构,其中initialize_tty_struct会把该tty的line discipline设置为tty_ldisc_N_TTY,并且把设备驱动程序的操作集赋给tty。
- 否则就调用tty_driver_install_tty,将tty和设备驱动关联起来,对应于前边查找时的处理,如果设备驱动程序提供了install函数,则调用它来保存这种关联关系,否则就直接将tty保存在tty_driver->ttys的对应于该设备号的位置
- 调用tty_ldisc_setup,进行调用line discipline提供的open函数打开tty
- 将该文件指针挂到tty数据结构的tty_files链表中
- 调用设备的open函数打开设备
从上述打开过程可以看到,打开操作将tty和真实的物理设备已经需要使用的line discipline都关联了起来,因而之后对文件句柄进行操作的时候就可以找到对应的line discipline和设备驱动程序。
可以看到在打开一个tty设备后,tty的line discipline固定的就是tty_ldisc_N_TTY,如果想改变这一点,需要使用ioctl命令TIOCSETD来实现。实际上由于tty也被当做一个文件来操作,因而其对上层提供的接口也和普通文件相同,但是我们知道串口有很多参数设置,这都是通过ioctl实现的。
1.2.2 与下层(即真实的设备驱动之间)的接口
Tty设备是对实际串口设备的抽象,实际的操作仍然要由相应的硬件来完成,为了达到这个目的,tty核心向下层提供了一些注册接口让真实设备驱动程序可以将其注册到tty上,以供tty使用。通过EXPORT_SYMBOL关键字即可找到tty对外提供的所有接口。我们这里只关注最关键的:
该函数式tty向下层提供的最主要的接口,它主要完成如下工作:
- 将该设备驱动所支持的设备的设备号注册到系统中
- 将该设备驱动程序添加到tty_drivers中,即注册到系统中。之后tty可以根据使用的设备的信息(使用的是设备号)从tty_drivers中找到所要使用的哪个驱动程序
- 如果该驱动程序没有设置TTY_DRIVER_DYNAMIC_DEV标记,则调用tty_register_device(该函数最终会调用device_register将设备注册到系统中),将设备添加到系统中。
1.2.2.2 tty_register_device和tty_register_device_attr
这两个函数向系统中注册一个新的tty设备,前一个是后一个的包装器函数。对于这两个函数有一个要求,该设备的驱动程序需要设置TTY_DRIVER_DYNAMIC_DEV标记,如果驱动程序没有设置该标记,则不应对该设备调用这两个函数。因为对于没有设置这个标记的驱动程序,当调用tty_register_driver时,该函数会调用tty_register_device完成tty设备的注册。
1.2.2.3 tty_insert_flip_char和tty_insert_flip_string
这两个函数用于驱动将接受到的数据放入tty缓存。
1.2.2.4 tty_insert_flip_string_flags和tty_insert_flip_string_fixed_flag
用于将标记放入tty缓存。
1.2.2.5 tty_prepare_flip_string和tty_prepare_flip_string_flags
为接收的字符申请一片内存区域,它们会返回所申请的大小以及指向可用缓存起始位置的指针。它们用于驱动程序想自己将数据拷贝到tty缓存的场合,一般使用tty_insert_flip_char,tty_insert_flip_string。
1.2.2.6 tty_flip_buffer_push和tty_schedule_flip
将数据从tty buffer转移到tty line discipline中。二者不同之处在于如果设置了low_latency则后者 不工作,而前者直接调用flush_to_ldisc来刷出数据到line discipline。如果没有设置low_latency,则二者都是将刷出数据到line discipline的工作添加到工作队列system_wq中,随后由工作者线程调用flush_to_ldisc。
需要注意的是由于flush_to_ldisc不能在中端上下文被调用,因此如果设置了low_latency,则不应该在中断上下文调用tty_flip_buffer_push。
它在没有设置low_latency时,会调用flush_work将数据从tty buffer刷到line discipline,flush_work会等待任务完成,因为它会阻塞,因而它不能在中端上下文被调用。该函数一般由读任务来调用,典型的在tty_ldisc_N_TTY的读以及poll中会调用到它。
为tty port初始化缓存数据结构
1.2.2.9 tty_buffer_request_room
为tty申请缓存
1.3 文件读/写
1.3.1 文件读
Tty的读操作如下
- 找到文件对应的tty,在tty设备打开时,已经将文件和tty数据结构关联起来了
- 找到与tty关联的line discipline,然后调用其读函数进行读
可以看出,tty的读依赖于line discipline的读操作,当前的tty设计中,当设备驱动程序发现有数据输入时,在它从设备中读出数据后需要调用tty_insert_flip_char或者tty_insert_flip_string将数据放入tty的缓存中,然后驱动程序需要调用tty_flip_buffer_push将数据从tty buffer中转移到tty line discipline中(通过调用line discipline的receive_buf函数)。因而line discipline只需要操作自己缓存中的数据即可。
Tty提供的缓存为每个输入的字符缓存了字符本身以及其对应的标记信息。
1.3.2 文件写
Tty的写逻辑很简单:
- 找到文件对应的tty,在tty设备打开时,已经将文件和tty数据结构关联起来了
- 找到与tty关联的line discipline,然后调用do_tty_write执行
- 从用户空间拷贝数据
- 调用line discipline的写函数将进行数据写操作
- 循环前两步直到写完成或者被信号打断
执行写操作时,tty没有提供公共的缓存机制,因而line discipline也是直接对数据进行处理后即发送给驱动,这里需要驱动提供的一个接口就是write_room,它用来获取驱动的缓存中还有多少空间,如果驱动不提供该函数,则tty会认为有一个一定大小的空间可用(代码中返回的是2048)。
二、Tty line discipline
从tty核心的描述中也可以看出,tty discipline在文件读写中起了很大的作用,读写操作都需要经过它,它主要用来进行输入/输出数据的预处理,写入设备的数据要先经过它的处理才会被发送给真实的设备驱动,从设备接收的数据也会先经过它的处理才会进入到tty core的处理逻辑。
它的实现本身非常简单,主要提供的几个对外的接口是:
- tty_register_ldisc:注册一个新的line discipline到系统中,注册的line discipline都会保存在tty_ldiscs中,注册到系统中的line discipline才能被使用
- tty_unregister_ldisc:用于解除一个line discipline的注册
- tty_set_ldisc:设置tty的line discipline,如果该tty存在旧的line discipline,还会做一些清除工作
- tty_ldisc_setup:用于打开line discipline
- tty_ldisc_release:用于释放tty对应的line discipline
- tty_ldisc_init:初始化tty的line discipline,当前代码固定初始化为tty_ldisc_N_TTY
- tty_ldisc_begin:将line discipline tty_ldisc_N_TTY注册到系统中。
tty line discipline对驱动程序来说是透明的,只有tty core才知道它的存在,并且会使用它。驱动程序完全不知道它的存在。