转自:https://www.cnblogs.com/jliuxin/p/14129303.html#_label4
- 概念介绍:终端 在Linux系统中, 与终端相关的概念很容易混淆. 首先,有终端的概念, 然后有各种类型的终端(串口终端, 伪终端, 控制台终端, 控制终端), 还有一个概念叫console.
那么什么是终端呢? 控制台终端是什么? 什么是console? 为了澄清这些问题, 我们来依次介绍这些概念. 1.1 终端
大家都知道, 由于最初的计算机价格昂贵, 因此, 一台计算机通常由多个人同时使用. 以前有这种设备可以连接到一台电脑, 只有显示器和键盘,还有简单的处理电路,本身没有处理计算机信息的能力, 他负责连接到一台正常的电脑(通常通过串口) ,然后登录计算机并操作计算机。当然,当时的计算机操作系统是多任务多用户的操作系统。只有显示器和键盘才能通过串口连接到计算机的设备称为终端.
终端的主要目的是提供人机交互的接口, 让他人通过终端控制本机. 在Linux系统中, tty是终端子系统. tty一词源于Teletypes, 它是最早出现的终端设备,类似于电传打字机。tty-core是终端子系统的核心. tty-core上层由字符设备驱动, 由字符设备驱动, 终端子系统/dev在目录下创建各种目录tty节点, 下面将介绍这些节点. 有了这些节点, 本机可以通过终端控制。.
如何控制? 想象一下现在有一块树莓派的板子, 系统启动后, 在/dev创建节点. 然后一个程序提供了控制本机的能力, 比如getty, 它在板上工作. getty首先会提示用户登录, 例如,它将输出到终端节点 “login:” 字符串, 然后字符串通过节点进入tty-core. 注意, 这个时候"login:"只存在于板上Linux内核中, 没有人能看到它. tty-core收到字符串后该怎么办? 它需要将字符串发送给用户, 怎么发送? 可选择树莓派串口, 然后用户在其他机器上,如XP电脑上, 以某种工具为例SecureCRT打开这个串口, 就可以看见"login:"了. 然后用户输入登录用户名, 沿原路反馈getty程序, getty验证输入的用户名是否为有效名称, 然后提示用户输入密码 ……, 实现人机交互, 控制本机的功能.
串口在这样一个过程中扮演了什么角色呢? 它是传输数据的载体. 根据载体的不同, 终端可以分为串行端口终端, 伪终端, 控制台终端. 1.2 控制台
在理解了终端的概念后, 让我们来看看什么是控制台. 简单来讲, 控制台就是Linux显示子系统 输入子系统.
以树莓派板为例, 假如板子上接了HDMI显示器, 插上了USB鼠标键盘, 那么HDMI 鼠标键盘是板的控制台.
控制台和终端一样, 具有输入/输出功能. 例如,系统可以通过串口打印/获取信息; 信息也可以通过控制台的显示系统打印, 通过输入系统获取信息.
在理解了控制台的概念后, 只需了解后面的控制台终端.
另外, 在Linux内核中还有一个概念叫做console子系统, 虽然console翻译成控制台, 但在本文的语义环境中, 请区分console和控制台: console与内核有关printk机制相关, 控制台代表(显示 子系统输入. 1.3 不同类型的终端
根据载体的不同, 终端可分为多种类型. 下面分别介绍 串行端口终端(//dev/ttySn)
很好理解, 它是载体为串口的终端. 设备节点名通常是/dev/ttyS0等, 也有USB转串口类型的终端, 节点名通常是/dev/ttyUSB0等. 控制台终端(//dev/console, /dev/tty0, /dev/tty1 …)
我们理解控制台是什么, 控制台本身具有接收输入和显示输出的功能, 但其输入通常是输入子系统(键盘, 鼠标等). 其输出通常是显示系统. 控制台终端就是把控制台的输入/输出功能做为载体, 用它来创建终端.
控制台终端的节点名为: /dev/tty0, /dev/tty1 …, /dev/tty6
以树莓派板为例, 接上USB鼠标, HDMI显示器, 启动系统. 系统启动后, 就会在HDMI看到一个登录界面. 这个登录界面就是getty程序在/dev/tty1上创建的. 按下Alt F1 - F6, 您可以看到6个登录界面, 这些登录界面是getty分别在/dev/tty1-6上创建的. /etc/inittab中会控制getty在哪些控制台终端上登录程序?.
/dev/tty0可以理解为一个链接, 它链接到目前正在使用的控制台终端, 比如现在通过Alt F2切换到/dev/tty对应的控制台终端, 然后输入命令echo test > /dev/tty0, 就会在/dev/tty2对应的控制台终端上看到test. 如果用Alt F3切换到另一个终端, 做同样的动作, 也会在F在对应的终端上看到test. 但无论在哪个终端, 输入命令echo test > /dev/tty2, 都只会在Alt F在相应的终端上看到test. 无论当前系统使用哪个控制台终端, 系统相关信息将发送到/dev/tty0. 只有系统或超级用户root可以向/dev/tty0进行写操作.
/dev/console也可以理解为链接. 只有在单用户模式下才能在/dev/console上登陆. 通过bootargs,可以告诉Linux在系统启动阶段,printk信息将打印到哪里。 树莓派, 如果console=/dev/ttyS0, 115200 console=/dev/tty1,则串口和HDMI上都会看到printk打印信息。
不过当Linux内核启动后,有应用程序open /dev/console得到的是最后一次引入的值。例如,上例是/dev/tty1. 以上例子为基础,无论您在任何终端上输入echo test > /dev/console, 最终都会在HDMI的tty显示在终端上。 内核启动完毕之后,在文件系统的启动过程中,会初始化一些程序(比如ssh, alsa-lib等),这些程序的输出信息将定位为/dev/console这就是为什么我们只能在上面HDMI看看这些信息的原因。 伪终端(pty)
伪终端主要用于通过网络控制本机.
以telnet为例. 树莓派板上, 会有一个telnet保护过程, 通过网络进行保护(TCP/IP协议)与其他机器通信, 监控是否有其他机器想通过telnet连接到本机, 收到连接请求后, 守护进程会fork一个子过程, 控制本机的程序在子过程中运行(例如getty). 接着getty打开伪终端节点(//dev/ttyp2), 我们称这个节点为设备节点(s2). 然后getty就会往s2发送一个"login:"字符串. 当这个字符串被传递到tty-core里面之后, 下一步该送到哪里? 如果是串行端口终端, 可通过串口发出. 但在伪终端中, 字符串将发送到另一个节点(/dev/ptyp2), 我们称这个节点为主设备节点(m2). telnet读取守护过程m2中的数据, 然后通过TCP/IP协议发送给其他机器.
因此: 伪终端是成对出现的逻辑设备(s2/m2), 假终端载体不是真正的硬件, 但是软件编写的逻辑设备(m2).
伪终端在表现形式上与上述终端最大的不同, 也就是说,它总是成对出现, 而不是单一的。分为伪终端主设备(/dev/ptyMN)伪终端从设备。dev/ttyMN)。其中,MN的命名方式如下: M: p q r s t u v w x y z a b c d e 共16 个 N: 0 1 2 3 4 5 6 7 8 9 a b c d e f 共16 个 这样默认支持256。这种命名方式存在一些问题,终端的最大数量也受到限制,因此Linux核心引入了一种新的命名方法:UNIX98_PTYS
UNIX98_PTYS 设备节点(//dev/ptmx)作为所有伪终端的主要设备。当有进程打开/dev/ptmx时间是/dev/pts/在目录下生成相应的从设备。此时,主设备(1)和从设备(N)有一对多的关系. 控制终端(tty)
/dev/tty这个终端没有载体,可以理解为链接,链接到当前流程打开的实际终端。在当前流程的命令行中输入tty可以查看/dev/tty对应的终端getty该程序从设备/dev/pts在/5上,然后输入tty当命令显示为/dev/pts/5 1.4 了解系统中存在的终端
/proc/tty/drivers: showing the name of the driver, the default node name, the major number for the driver, the range of minors used by the driver, and the type of the tty driver
cat /proc/tty/drivers name of the driver default node name major number range of minors type of the tty driver /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 40 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 pty_slave /dev/ttyp 3 0-255 pty:slave pty_master /dev/pty 2 0-255 pty:master unknown /dev/tty 4 1-63 console
/proc/tty/driver/files contains individual files for some of the tty drivers, if they implement that functionality. The default serial driver creates a file in this directory that shows a lot of serial-port-specific information about the hardware
/sys/class/tty All of the tty devices currently registered and present in the kernel have their own subdirectory under /sys/class/tty. Within that subdirectory, there is a “dev” file that contains the major and minor number assigned to that tty device. If the driver tells the kernel the locations of the physical device and driver associated with the tty device, it creates symlinks back to them 回到顶部 2. tty子系统架构介绍
所有的终端节点都是字符设备驱动, 因此最上层是字符设备驱动.
字符设备驱动下面是tty子系统, 先贴一张图
tty core是对终端这个概念的抽象, 它实现了各种不同类型的终端的通用功能 tty driver是载体的驱动程序,比如我们用串口作为载体,则tty driver就是串口的驱动。 driver只用关心如何把数据发给硬件(比如串口, 就是发送寄存器)以及如何从硬件接收数据,core会考虑如何以统一的形式与用户空间交互,交互的数据格式是怎样的。这里的数据格式是指软件上的概念,可以理解成协议,比如是否需要封装头部,头部信息是怎样的。
当core收到数据之后,它会传递给tty line discipline, discipline发给driver,driver在把数据变成硬件可以接受的格式,从硬件发送出去。反过来当硬件收到数据之后,driver会把这个数据写到一个缓冲区, 然后把缓冲区的数据推送到discipline的缓冲区里面,用户空间会通过read接口从discipline的缓冲区里面读取数据。
core也可以直接和driver交互而不用通过discipline。不过通常都会有一个discipline存在。 tty line discipline的主要目的是对传输的数据进行一些协议上的解封/封装, 比如PPP或者Bluetooth。 从driver的角度来看,它不知道数据是core直接给它的还是经过discipline之后再给它的。driver只知道把收到的数据发给硬件和从硬件中读取数据,不清楚数据是否封装了一些协议。这种设计也是符合逻辑的,硬件只知道一个bit一个bit的传输和接收数据,才不管传输的数据代表什么意思
理解了这3个概念, 我们就知道如果要添加一个串行端口终端,那么就需要做一个串口驱动,这个驱动要符合tty driver的规范,也就是按照tty driver的要求,实现必要的接口函数,然后向tty core注册,接下来就万事大吉了。对于其他类型的载体,比如虚拟终端或者控制台终端,也是一样,实现一个tty driver并注册即可。
嵌入式SOC中,串口一般叫UART或者USART,每个芯片的数据手册里面一般都有一章节来描述这个模块。不同的芯片厂商,比如Atmel和TI,它们的UART模块多少有点不一样,但是绝大部分都是一样的,比如都有start/stop bit,波特率,等。因此Linux内核中又抽象出了一个概念: Serial core
Serial core: Serial core在tty driver下面, 它把串口设备的一些通用的东西抽象出来了,这样对于不同的厂商的UART模块,就不需要从头到尾完全实现一遍tty driver要求的接口,只需要定义一个简单的UART driver,然后向Serial Core注册,接下来Serial Core就会把自己封装成tty driver的形式,向tty core进行注册,从而完成添加一个串行端口终端的动作。简化了串行端口终端驱动的开发。
接下来, 我们首先介绍一下tty driver, 它是一个承上启下的模块: 对上, 它与tty core交互 对下, 它提供接口给serial core
然后我们在介绍tty core, 接着是serial core, 最后是tty line discipline. 回到顶部 3. tty driver
3.1 简介
如果你想编写一个终端驱动, 需要遵循如下步骤: 首先, 创建一个struct tty_driver结构体. 内核代码提供了一个API (alloc_tty_driver), 专门用于创建这个结构体, 给该结构体分配内存. struct tty_driver tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS); 然后, 定义一个tty_operations结构体, 并编写相应的实现函数: 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, }; 然后, 初始化刚刚创建的tiny_tty_driver /* 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 driver子系统提供的API, 注册该driver. / register the tty driver */ retval = tty_register_driver(tiny_tty_driver);
当注册完成无误之后, 你会发现如下变化: /dev/ 可能为在/dev/下面出现多个tiny_tty_driver->name开头的设备节点, 例如/dev/ttty0, /dev/ttty1. 为什么说可能而不是一定? /dev/下面到底会出现几个节点? 为什么是以tiny_tty_driver->name开头的? 这些疑问将在本节详细分析中找到答案. /proc/tty/drivers 该文件中会多出一行 $ cat /proc/tty/drivers tiny_tty /dev/ttty 240 0-3 serial 此行数据依次是: tiny_tty_driver->driver_name , tiny_tty_driver->name , 主设备号 , 次设备号范围 , tiny_tty_driver->type /sys/class/tty 会在该目录下出现多个子目录, 子目录的名称以tiny_tty_driver->name打头. 同样, 本节后面的内容会介绍为什么.
上述就是编写终端驱动的基本步骤. 不管你是编写串行终端驱动, 还是虚拟终端驱动, 或是控制台终端驱动, 都应遵循上述步骤. 不过对于串行终端驱动, serial core已经帮你完成了上述步骤, 你只需要向serial core子系统进行注册即可. 3.2 主要数据结构
根据前一节的简介, 我们提炼出几个主要的数据结构, 分别介绍它们: tty_driver, tty_operations. tty_driver
在tty子系统中, tty_driver用于描述一个tty驱动. 要编写一个终端驱动, 必须定义一个tty_driver结构体. 然后用此结构体向tty子系统进行注册.
头文件: include/linux/tty_driver.h struct tty_driver Comment intmagic magic number for this structure struct kref kref 引用计数 struct cdev cdevs 描述一个字符设备驱动的结构体. 在本文开头介绍过, 所有的终端节点(/dev/ttyxx)都是字符设备驱动 struct moduleowner
const chardriver_name 会出现在/proc/tty/drivers中的第一列. The driver_name variable should be set to something short, descriptive, and unique among all tty drivers in the kernel const charname 会出现在/dev/ , /sys/class/tty/ , /proc/tty/drivers的第二列. intname_base name的起始编号, 一般情况下默认是0 /dev/下的节点名和/sys/class/tty/下的目录名是由(name+name_base)组成的. 例如name=ttty, name_base=0, 组合之后就是ttty0 intmajor 该driver的主设备号. 与字符设备驱动相关. intminor_start 该driver的次设备号的起始值. 与字符设备驱动相关. major+minor_start就是该driver的起始设备号 unsigned intnum 表示该driver在注册字符设备驱动的时候, 可以注册几个次设备. 次设备的设备号从(major+minor_start)开始递增. 假如num = 3, 如果使能了创建设备节点, 则/dev/下会多出来3个节点, /sys/class/tty/下也会多出来3个文件夹 shorttype 该driver的类型, 以下几种类型之一: include/linux/tty_driver.h /* tty driver types / #define TTY_DRIVER_TYPE_SYSTEM0x0001 #define TTY_DRIVER_TYPE_CONSOLE0x0002 #define TTY_DRIVER_TYPE_SERIAL0x0003 #define TTY_DRIVER_TYPE_PTY0x0004 #define TTY_DRIVER_TYPE_SCC0x0005/ scc driver / #define TTY_DRIVER_TYPE_SYSCONS0x0006 shortsubtype 该driver的子类型, 以下几种子类型之一 include/linux/tty_driver.h / system subtypes (magic, used by tty_io.c) */ #define SYSTEM_TYPE_TTY0x0001 #define SYSTEM_TYPE_CONSOLE0x0002 #define SYSTEM_TYPE_SYSCONS0x0003 #define SYSTEM_TYPE_SYSPTMX0x0004
/* pty subtypes (magic, used by tty_io.c) */ #define PTY_TYPE_MASTER0x0001 #define PTY_TYPE_SLAVE0x0002
/* serial subtype definitions */ #define SERIAL_TYPE_NORMAL1 struct ktermios init_termios 如果用户空间要配置一个终端的波特率, 起始/停止位, 奇偶校验等参数时, 一般会准备一个termios结构体, 然后把这个结构体设置到内核驱动里面. init_termios代表该driver的初始termios unsigned longflags 该driver的flags, 可以用以下几种类型相或(|) flags决定了在向tty子系统进行注册时, 系统会采取何种动作, 例如是否创建/dev/节点等等. flags的定义在include/linux/tty_driver.h, 针对每一个flag的意思, 该文件中也有详细的注释: #define TTY_DRIVER_INSTALLED0x0001 #define TTY_DRIVER_RESET_TERMIOS0x0002 #define TTY_DRIVER_REAL_RAW0x0004 #define TTY_DRIVER_DYNAMIC_DEV0x0008 #define TTY_DRIVER_DEVPTS_MEM0x0010 #define TTY_DRIVER_HARDWARE_BREAK0x0020 #define TTY_DRIVER_DYNAMIC_ALLOC0x0040 #define TTY_DRIVER_UNNUMBERED_NODE0x0080 struct proc_dir_entry *proc_entry proc系统相关, 用于生成/proc/ tty/driver/下的文件 struct tty_driver *other only used for the PTY driver struct tty_struct **ttys 指针, 指向tty_struct结构体, tty_struct将会在tty core一节中详细介绍 struct tty_port **ports 指针, 指向tty_port结构体, tty_port将会在tty core一节中详细介绍 struct ktermios **termios 用于链接与该driver相关的所有的termios void *driver_state
const struct tty_operations *ops 与该driver关联的ops, 后面会专门介绍这个结构体 struct list_head tty_drivers tty driver子系统中会有一个全局链表头, 挂载所有注册的driver. 这里的tty_drivers用于把自己挂接到全局的链表头下 tty_operations
tty_operations用于描述一个tty_driver的操作函数.
仔细观察这些操作函数的参数, 会发现它们都与struct tty_struct这个结构体有关系. tty_struct是用于描述当一个tty设备被open之后, 所有与之相关的状态. 换言之, tty_struct是一个run-time阶段的数据结构. 我们会在tty core一节详细介绍这个结构体.
头文件: include/linux/tty_driver.h struct tty_operations Comment struct tty_struct * (*lookup)(struct tty_driver *driver,struct inode *inode, 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)
#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
const struct file_operations *proc_fops
3.3 主要API说明
根据3.1节的简介, 我们知道最主要的一个API: tty_register_driver. 该API里面还会调用其它几个API : tty_register_device, proc_tty_register_driver 下面我们分别介绍它们. tty_register_driver
若想编写一个终端驱动, 首先会准备好tty_driver和tty_operations结构体, 然后调用tty_register_driver向tty driver子系统进行注册.
下面我们详细分析一下, tty_register_driver里面到底做了哪些事情.
头文件: include/linux/tty.h 实现文件: drivers/tty/tty_io.c int tty_register_driver(struct tty_driver *driver) 用动态/静态的方式分配主次设备号, 并赋值给driver-> major, driver-> minor_start. if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC), 则调用tty_cdev_add向字符设备驱动子系统注册一个字符设备驱动. tty_cdev_add封装了字符设备驱动的一些API, 我们看下这个函数的细节: static int tty_cdev_add(struct tty_driver driver, dev_t dev, unsigned int index, unsigned int count) { / init here, since reused cdevs cause crashes */ cdev_init(&driver->cdevs[index], &tty_fops); driver->cdevs[index].owner = driver->owner; return cdev_add(&driver->cdevs[index], dev, count); } 当cdev_add成功返回之后, 字符设备驱动就已经注册成功了. 不过请注意, 此时并不会自动在/dev/创建设备节点. 后面会有其它的代码来创建设备节点. 另外需要特别注意tty_fops这个结构体, 它是内核系统定义的一个结构体(在drivers/tty/tty_io.c中). 假设/dev/下已经创建了设备节点, 当我们在用户空间调用open/read/write/close等操作时, 最终就会映射到tty_fops这个结构体上. list_add(&driver->tty_drivers, &tty_drivers); tty_drivers是drivers/tty/tty_io.c中定义的一个全局链表头, 这里把”driver”挂接到这个全局链表头下. if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)), (如果flags没有定义TTY_DRIVER_DYNAMIC_DEV) for (i = 0; i < driver->num; i++) (针对每一个num) 调用一次tty_register_device. tty_register_device会在/dev/目录下创建1个对应的字符设备驱动节点, 同时也会在/sys/class/tty目录下创建1个对应的子目录. for循环总共调用(driver->num)次tty_register_device, 所以/dev/下就会出现(driver->num)个设备节点, /sys/class/tty下也会出现(driver->num)个子目录. 至于为什么tty_register_device为什么能创建设备节点, 节点名是什么? 以及为什么会在/sys/class/tty下创建子目录, 目录名是什么? 后文会详解分析这个API, 届时会找到答案. proc_tty_register_driver(driver) proc_tty_register_driver会在/proc/tty/driver/目录下创建一个子目录, 子目录的名称是tty_driver->driver_name. 后文会专门介绍一下proc_tty_register_driver这个API. driver->flags |= TTY_DRIVER_INSTALLED 设置flags标志, 代表该driver已经被正确注册了. tty_register_device
tty_register_device主要用于生成设备节点和/sys/class/tty下的子目录. 在编写终端驱动时, 当调用tty_register_driver向tty子系统注册时, 如果没有设置TTY_DRIVER_DYNAMIC_DEV, 则会自动调用tty_register_device; 如果设置了TTY_DRIVER_DYNAMIC_DEV, 也可以在后面再手动调用tty_register_device来创建设备节点和class下的子目录.
下面我们详细分析一下, tty_register_device里面到底做了哪些事情.
头文件: include/linux/tty.h 实现文件: drivers/tty/tty_io.c struct device *tty_register_device(struct tty_driver *driver, unsigned index, struct device *device) { return tty_register_device_attr(driver, index, device, NULL, NULL); }
直接调用tty_register_device_attr, 来看看tty_register_device_attr: struct device *tty_register_device_attr(struct tty_driver *driver, unsigned index, struct device *device, void *drvdata, const struct attribute_group **attr_grp) if (index >= driver->num), 则返回错误. 说明传过来的index参数不能大于driver本身的num数 if (driver->type == TTY_DRIVER_TYPE_PTY) pty_line_name(driver, index, name); else tty_line_name(driver, index, name) 根据driver的type, 若是PTY, 则调用pty_line_name; 若是TTY, 则调用tty_line_name. pty_line_name和tty_line_name的目的就是设置名称, 结果存储在name变量中. 它们内部会调用sprintf, 格式化输出名称. 具体细节可以看代码. 例如, 如果type是TTY, 则name最终可能是结果是 (“%s%d”, driver->name, index + driver->name_base). 这个name很重要, /dev/下的节点名和/sys/class/tty/下的子目录名都是靠它决定的. if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)), 则调用tty_cdev_add注册字符设备驱动. 这里与tty_register_driver里面的if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)遥相呼应. 如果tty_register_driver中已经注册了字符设备驱动, 那么这里就不需要再次注册了. 接着分配一个struct device结构体, 给dev->devt, dev->class等赋值, 设置dev的name. 然后调用device_register(dev)注册该device. device_register在《设备模型》一文中详细介绍过, 它会创建设备节点, 创建class下的子目录. 具体细节请看《设备模型》中的对应章节. proc_tty_register_driver & /proc/tty/drivers
proc是Linux系统中的一个子模块, 跟sysfs有点类似, 也算一个虚拟的文件系统. 如果你向proc进行注册, 注册成功之后, 用户空间就可以通过/proc/xxx与你的”proc driver”交互.
一般情况下, 我们会通过系统提供一些调试信息给到用户空间, 因此我们把proc界定为调试技术, 会在《调试技术》一文中详细介绍它.
这里, 我们只是简单看下tty子系统向proc注册了些什么东西.
头文件: include/linux/tty.h 实现文件: fs/proc/proc_tty.c
/proc/tty/drivers文件是如何生成的? proc_tty.c的初始化函数里面会创建这个文件, 代码如下: /*
- Called by proc_root_init() to initialize the /proc/tty subtree / void __init proc_tty_init(void) {
if (!proc_mkdir(“tty”, NULL)) return; proc_mkdir(“tty/ldisc”, NULL); / Preserved: it’s userspace visible / /
- /proc/tty/driver/serial reveals the exact character counts for
- serial links which is just too easy to abuse for inferring
- password lengths and inter-keystroke timings during password
- entry. */ proc_tty_driver = proc_mkdir_mode(“tty/driver”, S_IRUSR|S_IXUSR, NULL); proc_create(“tty/ldiscs”, 0, NULL, &tty_ldiscs_proc_fops); proc_create(“tty/drivers”, 0, NULL, &proc_tty_drivers_operations); } 前文说过, 我们可以通过cat /proc/tty/drivers, 来查看系统中注册了多少个tty driver. 如何实现的呢? cat操作最终会映射到proc_tty_drivers_operations, 该operations最终会扫描tty_drivers这个全局链表头下的所有driver, 然后把它们的信息反馈到用户空间.
proc_tty_register_driver tty_register_driver里面会调用此API, 此API的代码细节如下: /*
-
This function is called by tty_register_driver() to handle
-
registering the driver’s /proc handler into /proc/tty/driver/ */ void proc_tty_register_driver(struct tty_driver *driver) { struct proc_dir_entry *ent;
if (!driver->driver_name || driver->proc_entry || !driver->ops->proc_fops) return;
ent = proc_create_data(driver->driver_name, 0, proc_tty_driver, driver->ops->proc_fops, driver); driver->proc_entry = ent; } 如果tty_driver结构体定义了ops->proc_fops, 则会在/proc/tty/driver/目录下创建一个文件, 文件的名称是driver->driver_name. 我们可以cat此文件, 以便获取一些必要的信息. cat操作最终会映射到driver->ops->proc_fops. void *driver_state的作用 回到顶部
- tty core
前一节我们介绍了如何向tty driver子系统注册一个终端驱动. 驱动注册成功之后, 用户空间就可以通过tty core子系统提供的接口与驱动交互了. 本小节, 我们从用户空间的角度, 来看看tty core子系统的内部逻辑. 4.1 简介
前文说过, 所有的终端设备, 从用户空间的角度来看, 都是字符设备驱动. 在注册tty_driver时, tty_register_driver会调用tty_cdev_add来注册字符设备驱动, 然后在tty_register_device中会创建设备节点.
tty_cdev_add在注册字符设备驱动时, 使用的ops是drivers/tty/tty_io.c中实现的struct file_operations tty_fops. 用户空间的open/read等操作, 最终就会映射到tty_fops上.
另外, 在介绍tty_driver这个结构体时, 我们也提到了tty_struct, tty_port, ktermios这几个结构体.
上述这些数据结构我们划归到tty core子系统, 将在本节详细介绍它们. 4.2 主要数据结构
tty_struct
关于tty_struct和ktermios, 先看看官方代码中的一段解释:
- Where all of the state associated with a tty is kept while the tty
- is open. Since the termios state should be kept even if the tty
- has been closed — for things like the baud rate, etc — it is
- not stored here, but rather a pointer to the real state is stored
- here
tty_struct是用于表示一个tty设备被open之后的状态, 当设备被close之后, 这个结构体就消失了. 这是它与tty_driver的区别. termios在tty_driver注册的时候, 就已经有一个初始值了. tty设备被open之后, 可以修改termios的值. 设备被close之后, 它并不会消失. 举个例子: 假设我们open了一个tty设备, 然后把它的波特率设置为了9600, 如果我们close了这个设备, 然后在重新打开, 波特率不变, 还是9600.
头文件: include/linux/tty.h struct tty_struct Comment intmagic magic number for this structure struct kref kref 引用计数 struct device *dev tty_register_device中会创建一个device, 这里的dev指向那个被创建的device struct tty_driver *driver 对应的tty_driver const struct tty_operations *ops tty_driver对应的那个tty_operations. 应该可以直接通过driver->ops访问到它啊, 为什么要在这里把它单独提出来呢? 在tty设备被open的时候, 会把driver->ops 赋值给 tty_struct->ops. 在需要的时候, 可以将tty_struct->ops重新赋值而不必更改driver->ops. int index 一个tty_driver可以对应tty_driver->num个设备. 这里的 0 <= index <= tty_driver->num struct ld_semaphore ldisc_sem; struct tty_ldisc ldisc ldisc指向该tty_struct对应的tty line discipline. ldisc_sem是一个互斥锁, 用于互斥对ldisc的访问. 例如假设我们想更改tty_struct->ldisc, 则需要先获取锁ldisc_sem 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 */ 该tty_struct对应的ktermios char name[64] 该tty_struct的name, 它的值是 sprintf(p, “%s%d”, driver->name, index + driver->name_base) struct pid pgrp;/ Protected by ctrl lock */ struct pid session; pid相关 unsigned long flags 该tty_struct对应的flag, 以下几种子类型之一 注意对flags的修改必须使用set_bit/clear_bit这样的原子操作, 以避免并发访问导致的各种问题 include/linux/tty.h #define TTY_THROTTLED 0/ Call unthrottle() at threshold min / #define TTY_IO_ERROR 1/ Cause an I/O error (may be no ldisc too) / #define TTY_OTHER_CLOSED 2/ Other side (if any) has closed / #define TTY_EXCLUSIVE 3/ Exclusive open mode / #define TTY_DEBUG 4/ Debugging / #define TTY_DO_WRITE_WAKEUP 5/ Call write_wakeup after queuing new / #define TTY_OTHER_DONE6/ Closed pty has completed input processing / #define TTY_LDISC_OPEN 11/ Line discipline is open / #define TTY_PTY_LOCK 16/ pty private / #define TTY_NO_WRITE_SPLIT 17/ Preserve write boundaries to driver / #define TTY_HUPPED 18/ Post driver->hangup() / #define TTY_LDISC_HALTED22/ Line discipline is halted / int count 在用户空间, 可以对一个tty设备节点open多次, 多次open在内核空间只对应1个tty_struct. count代表被open的次数. 在open/re-open时++, 在close时– struct winsize winsize;/ winsize_mutex / 该tty_struct对应的窗口的size. 注意这个参数不像ktermios, 在tty_struct消失的时候, 它也会消失. 为什么不把它单独拿出呢? 因为应用程序几乎每次在open时, 都会设置winsize, 因此这里没必要保存. 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 */ 一般会有一个buffer用来存储用户空间给过来的数据, 这个参数应该是指的buffer的剩余size. int flow_change 具体目的暂不清楚 struct tty_struct *link 具体目的暂不清楚 struct fasync_struct fasync 异步通知机制相关. 例如用户空间可以丢一段数据下来, 但是不用再那里等着, 可以继续执行其它程序. 当内核把这段数据传输完之后, 通知用户空间. int alt_speed;/ For magic substitution of 38400 bps */ 一个变量, 具体意义暂不清楚 wait_queue_head_t write_wait; wait_queue_head_t read_wait; 两个等待队列. 在《竞争与阻塞》一文中有详细介绍等待队列 struct work_struct hangup_work 一个工作队列 void *disc_data 与tty line discipline相关 void *driver_data
struct list_head tty_files 在《字符设备驱动》一文中我们讲过, 每次open操作, 内核空间都会创建一个对应的struct file. 但是对tty设备的多次open操作, 内核只会有一个struct tty_struct. 这里的tty_files是一个链表头, 所有的struct file都会挂接在这个链表头下 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 指向一个tty_port ktermios
Ktermios主要用于用户空间配置tty设备, 配置其波特率, 奇偶校验等等.
头文件: include/uapi/asm-generic/termbits.h
具体的细节和每个字段的意思, 直接查看源代码即可, 这里不多说了. struct ktermios { tcflag_t c_iflag; /* input mode flags / tcflag_t c_oflag; / output mode flags / tcflag_t c_cflag; / control mode flags / tcflag_t c_lflag; / local mode flags / cc_t c_line; / line discipline / cc_t c_cc[NCCS]; / control characters / speed_t c_ispeed; / input speed / speed_t c_ospeed; / output speed */ }; tty_port
回头看一下tty_operations这个结构体, 会发现它只有write函数, 但是没有read函数. 当用户空间想发送数据时, write函数会被调用, 它会操作硬件(例如串口)把数据发送出去. 但是当硬件收到数据的时候, 它是如何传递给用户空间的呢?
tty_port的作用就在于此, 你可以简单的把它理解为一块buffer. 当硬件收到数据之后, 它会把收到的数据存储在tty_port的buffer里面, 然后用户空间会从tty_port的buffer读取数据.
在继续下面的文章之前, 让我们先梳理一下 /dev/设备节点, tty_driver, tty_struct, tty_port这几者之间的关系. 一个tty_driver对应(tty_driver->num)个设备节点 一个设备节点在被open之后, 对应一个tty_struct 一个tty_struct对应一个tty_port
与tty_port相关的主要代码和数据结构如下:
头文件: include/linux/tty.h struct tty_port数据结构的细节就不仔细介绍了, 直接放下代码在这里. struct tty_port_operations { /* Return 1 if the carrier is raised */ int (*carrier_raised)(struct tty_port port); / Control the DTR line */ void (*dtr_rts)(struct tty_port port, int raise); / Called when the last close completes or a hangup finishes IFF the port was initialized. Do not use to free resources. Called under the port mutex to serialize against activate/shutdowns */ void (*shutdown)(struct tty_port port); / Called under the port mutex from tty_port_open, serialized using the port mutex / / FIXME: long term getting the tty argument out of this would be good for consoles */ int (*activate)(struct tty_port *port, struct tty_struct tty); / Called on the final put of a port */ void (*destruct)(struct tty_port *port); };
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 / 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 close_wait; / Close waiters / wait_queue_head_t delta_msr_wait; / Modem status change / unsigned long flags; / TTY flags ASY_/ 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 */ };
源文件 : drivers/tty/tty_port.c : 该C文件提供了对于tty_port的一些API, 包括: void tty_port_init(struct tty_port *port) : *port参数指向一块已经分配好的空间, 该API用于初始化这块空间 tty_port_link_device : 用于把tty_port和tty_driver关联起来, 也就是让tty_driver-> ports[index] = tty_port tty_port_register_device tty_port_register_device_attr …….
drivers/tty/tty_buffer.c : 该C文件提供了操作tty_port->buf的一些API, 主要就是对buffer的处理, 该C文件对应的头文件主要是include/linux/tty_flip.h: void tty_buffer_init(struct tty_port *port): 主要用于初始化tty_port->buf, 注意这里并没有给buffer分配空间 tty_buffer_request_room: 它会调用tty_buffer_alloc, 给buffer分配存储空间 tty_buffer_set_limit : 设置buffer的size限制 tty_insert_flip_char : 往buffer里面插入一个字符 tty_insert_flip_string : 往buffer里面插入字符串 tty_buffer_space_avail : 获取buffer的剩余空间 tty_flip_buffer_push : 把数据从tty_port->buf搬移到tty line discipline. 前文提到过, 用户空间会从tty_port读取硬件收到的数据, 实际上是从tty line discipline里面读取的 4.3 主要API说明
tty core这个子模块好像没有向内核其它子模块提供什么接口, 它主要的功能是向用户空间提供了字符设备驱动接口. 因此本节我们主要看看这些字符设备驱动的接口函数.
由于这些接口函数的细节实现太过繁琐, 加之我们在项目中主要工作是集中在驱动这块, 对tty core只需大致了解即可, 因此本节只会做些粗略介绍, 大致理清代码逻辑.
tty core提供的字符设备驱动, 最重要的是下面这个ops: 代码路径: 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, }; 我们主要分析一下open/read/write/release这几个函数. open
当我们在用户空间open一个tty的设备节点时, 此处的open函数将会被调用.
open函数的主要功能是创建并初始化tty_struct结构体. 如果用户空间重复打开同一个tty设备节点, open函数并不会创建新的tty_struct, 只会执行tty_reopen操作, 在reopen里面, tty_struct->count++. 还有一个特殊情况: /dev/tty这个设备节点, 还记得它的作用吗? 如果你对这个设备节点执行open操作, 内核空间会创建一个新的tty_struct结构体吗? (答案是不会). 并且内核空间会直接通过(current->signal->tty)获取已经创建好的tty_struct. 还记得current吗? 它是struct task_struct类型的指针, 通过current, 我们可以得到进程的详细信息.
下面我们来看看这个函数的代码细节: static int tty_open(struct inode *inode, struct file *filp) { struct tty_struct *tty; struct tty_driver *driver = NULL; dev_t device = inode->i_rdev;
......
tty = tty_open_current_tty(device, filp);
if (!tty) {
mutex_lock(&tty_mutex);
driver = tty_lookup_driver(device, filp, &noctty, &index);
if (IS_ERR(driver)) {
retval = PTR_ERR(driver);
goto err_unlock;
}
/* check whether we're reopening an existing tty */
tty = tty_driver_lookup_tty(driver, inode, index);
if (IS_ERR(tty)) {
retval = PTR_ERR(tty);
goto err_unlock;
}
if (tty) {
mutex_unlock(&tty_mutex);
tty_lock(tty);
/* safe to drop the kref from tty_driver_lookup_tty() */
tty_kref_put(tty);
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);
mutex_unlock(&tty_mutex);
}
tty_driver_kref_put(driver);
}
…
} tty_open_current_tty : 当对/dev/tty节点执行open操作时, 这个函数就会起作用, 它会调用get_current_tty, 从(current->signal->tty)获取已经创建好的tty_struct tty_lookup_driver : 通过设备号找到对应的tty_driver. 能猜到实现逻辑吗? 也很简单, 首先我们已知设备号, 然后所有的tty_driver都被挂载到全局链表头tty_drivers下面了, 逐个扫描链表头下面挂接的所有的tty_driver, 对比设备号, 即可找到对应的tty_driver. tty_driver_lookup_tty : 查看tty_deriver->ttys[idx]是否为NULL, 如果不为空, 则证明是重复open同一个tty设备节点, 直接执行tty_reopen操作即可, 不用创建新的tty_struct. tty_init_dev : 创建tty_struct结构体. 它会调用alloc_tty_struct分配并初始化tty_struct. read
当我们在用户空间执行read操作时, 此处的read函数将会被调用. read的主要目的是把数据从内核空间返回给用户空间. 前文我们说过, 当我们的硬件(例如串口)收到了数据之后, 会通过tty_port存储到tty line discipline里面. 这里的read操作就是从tty_ldisc获取数据, 然后返回给用户空间.
直接贴一下代码吧: 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); struct tty_ldisc *ld;
if (tty_paranoia_check(tty, inode, "tty_read"))
return -EIO;
if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))
return -EIO;
/* We want to wait for the line discipline to sort out in this
situation */
ld = tty_ldisc_ref_wait(tty);
if (ld->ops->read)
i = ld->ops->read(tty, file, buf, count);
else
i = -EIO;
tty_ldisc_deref(ld);
if (i > 0)
tty_update_time(&inode->i_atime);
return i;
} 代码很简单, 调用ld->ops->read读取数据. 这里简单是因为的主要的逻辑都是在tty line discipline中处理的, 我们在介绍这一节时在详细描述. write
当我们在用户空间执行write操作时, 此处的write函数将会被调用. write的主要目的是把数据从用户空间传递到内核空间, 然后通过硬件发送出去.
write的逻辑也很简单, 收到用户空间的数据之后, 调用tty line discipline发送数据, tty line discipline会调用tty_driver->ops->write函数把数据通过硬件发送出去.
代码如下: 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); struct tty_ldisc *ld; ssize_t ret;
......
ld = tty_ldisc_ref_wait(tty);
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会调用ld->ops->write, ld->ops->write最终会调用tty_driver->ops->write函数把数据通过硬件发送出去. release
用户空间执行close操作时, 会导致release函数被调用. 不过不是每次close操作都会导致release函数被调用, 如果设备节点被open了多次, 那么只有最后一次close操作才会导致release函数被调用, 这个逻辑是字符设备驱动控制的, 具体细节可以回头看看《字符设备驱动》一文.
所以一旦这里的release函数被执行, 就代表所有的open操作都已经close了. release函数里面会释放tty_struct这个结构体.
代码就不贴了. 回到顶部 5. tty line discipline
5.1 简介
tty line discipline(后文简称ldis)的作用我们在第2章大致介绍过, 现在我们再来它在tty子系统中的地位. tty子系统中, tty core以字符设备驱动的形式负责与用户空间交互; tty driver则负责操作底层硬件, 以一个个bit的方式通过硬件收发数据. 当用户空间想要发送数据时, 会先把数据传递给tty core, tty core会把数据转交给ldis; ldis此时可以对数据做一些封装(一般是软件协议上的头的封装), ldis封装完数据之后, 会把数据交给tty driver通过硬件外发; tty driver不关心数据的具体内容, 它只管一个个bit的把数据发出去. 反过来, 当硬件收到数据之后, tty driver会把这些数据存储在tty_port->buffer中, 然后在某个时候, 在把tty_port->buffer中的数据搬移到ldis中; ldis收到数据后, 会对数据做一些解封处理(一般是软件协议上的头的解封); 当用户空间调用read接口获取数据时, tty core会从ldis中取出数据, 然后交给用户空间.
综合来看, ldis的作用就是对数据进行一些软件协议层面的处理, 与具体硬件无法, 主要与协议相关. 所以说一个ldis就是用来实现一种协议的, 例如PPP 或者 Bluetooth.
理解了ldis的地位, 接下来我们分析一下软件层面上的架构. 首先, 你可以把ldis理解为一个池子, 你可以通过ldis子模块提供的API向这个池子添加(注册)ldis. 这个池子会存储很多个ldis, 每个ldis对应一种协议的实现, 当我们想使用ldis的时候, 就会从这个池子里面选择一种ldis.
那么, 接下来的一个问题是谁会负责从这个池子里面选取ldis呢? 答案是tty core, 原因是它会通过ldis发送数据, 从ldis接收数据, 因此它必须知道该用哪个ldis.
还有一个问题, tty core是在何时来选择ldis的呢? 默认情况下, 当用户空间调用open操作打开某一设备节点时, tty core的open函数将会被调用, 在tty core的open函数里面, 会从ldis的池子里面选择一个ldis. 另外, 用户空间也可以通过ioctl指定tty core使用某一个ldis.
问题继续, tty core知道用哪个ldis了, 那么它应该要把这个ldis存储起来, 以便后面随时使用. 存储在哪里呢? 你应该已经知道了, 答案是tty_struct-> ldisc, 在tty core的open函数里面, 会创建tty_struct结构体, 然后选取默认的ldis, 然后把获取到的ldis存储在tty_struct-> ldisc.
对于内核的这种设计, 有啥想说的? 只能说设计的very good! 一方面是体现了软件分层的思想, 不同的子模块负责不同的事情; 另一方面, 模块与模块之前低耦合, tty core与ldis并没有必然的联系, 它可以选择使用任何一个ldis, 用户空间也设置tty core所使用的ldis. 这样的设计也符合逻辑, 例如用户空间想发送一个字符A, 那么用户空间可以选择通过蓝牙(其中一个ldis)发送出去, 也可以选择通过其它方式(另外一个ldis)发送出去. 5.2 主要数据结构
tty_ldiscs[NR_LDISCS]
实现文件: drivers/tty/tty_ldisc.c /* Line disc dispatch table */ static struct tty_ldisc_ops *tty_ldiscs[NR_LDISCS]; 它就是我们前文说的那个池子, 其实就是一个全局结构体数组. 池子的大小是数组大小, 也就是NR_LDISCS. 当我们用tty ldis子模块提供的API注册一个ldis时, 被注册的ldis就是存储在这个数组里面.
NR_LDISCS是在include/uapi/linux/tty.h中定义的. uapi说明用户空间也会引用此头文件, 用户空间引用此头文件的目的是为了设置tty core使用哪一个ldis, 在设置的时候, 用户空间指定一个ID即可, 这个ID对应数组的某个元素. #define NR_LDISCS 30
/* line disciplines / #define N_TTY 0 #define N_SLIP 1 #define N_MOUSE 2 #define N_PPP 3 #define N_STRIP 4 #define N_AX25 5 #define N_X25 6 / X.25 async / #define N_6PACK 7 #define N_MASC 8 / Reserved for Mobitex module kaz@cafe.net / #define N_R3964 9 / Reserved for Simatic R3964 module / #define N_PROFIBUS_FDL 10 / Reserved for Profibus / #define N_IRDA 11 / Linux IrDa - http://irda.sourceforge.net/ / #define N_SMSBLOCK 12 / SMS block mode - for talking to GSM data / / cards about SMS messages / #define N_HDLC 13 / synchronous HDLC / #define N_SYNC_PPP 14 / synchronous PPP / #define N_HCI 15 / Bluetooth HCI UART / #define N_GIGASET_M101 16 / Siemens Gigaset M101 serial DECT adapter / #define N_SLCAN 17 / Serial / USB serial CAN Adaptors / #define N_PPS 18 / Pulse per Second / #define N_V253 19 / Codec control over voice modem / #define N_CAIF 20 / CAIF protocol for talking to modems / #define N_GSM0710 21 / GSM 0710 Mux / #define N_TI_WL 22 / for TI’s WL BT, FM, GPS combo chips / #define N_TRACESINK 23 / Trace data routing for MIPI P1149.7 / #define N_TRACEROUTER 24 / Trace data routing for MIPI P1149.7 */ #define NR_LDISCS30, 说明这个池子最多可以存储30个ldis 另外内核已经规定了大部分ldis对应的ID, 例如#define N_PPP 3, 说明实现PPP这个协议的ldis对应的ID是3. tty_ldisc
如果我们想自己编写一个ldis, 则必须实现一个tty_ldisc_ops结构体, 然后向tty ldis子模块注册. 注册过程中, tty ldis子模块会创建一个tty_ldisc结构体, 并把这个结构体存入tty_ldiscs[NR_LDISCS]这个数组的某个位置.
头文件: include/linux/tty_ldisc.h struct tty_ldisc { struct tty_ldisc_ops *ops; struct tty_struct *tty; }; 这个结构体很简单, *tty是用来指向tty_struct结构体的, 主要是要实现tty_ldisc_ops这个结构体. tty_ldisc_ops
头文件: include/linux/tty_ldisc.h 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 (*chars_in_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);
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);
unsigned int (*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);
void (*fasync)(struct tty_struct *tty, int on);
int (*receive_buf2)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
struct module *owner;
int refcount;
}; 各个接口函数的意思在tty_ldisc.h里面已经给出了说明, 我们直接贴出来: /*
- This structure defines the interface between the tty line discipline
- implementation and the tty routines. The following routines can be
- defined; unless noted otherwise, they are optional, and can be
- filled in with a null pointer.
- int(*open)(struct tty_struct *);
*This function is called when the line discipline is associated *with the tty. The line discipline can use this as an *opportunity to initialize any state needed by the ldisc routines. *
- void(*close)(struct tty_struct *);
*This function is called when the line discipline is being *shutdown, either because the tty is being closed or because *the tty is being changed to use a new line discipline *
- void(*flush_buffer)(struct tty_struct *tty);
*This function instructs the line discipline to clear its *buffers of any input characters it may have queued to be *delivered to the user mode process. *
- ssize_t (*chars_in_buffer)(struct tty_struct *tty);
*This function returns the number of input characters the line *discipline may have queued up to be delivered to the user mode *process. *
- ssize_t (*read)(struct tty_struct * tty, struct file * file,
- unsigned char * buf, size_t nr);
*This function is called when the user requests to read from *the tty. The line discipline will return whatever characters *it has buffered up for the user. If this function is not *defined, the user will receive an EIO error. *
- ssize_t (*write)(struct tty_struct * tty, struct file * file,
- const unsigned char * buf, size_t nr);
*This function is called when the user requests to write to the *tty. The line discipline will deliver the characters to the *low-level tty device for transmission, optionally performing *some processing on the characters first. If this function is *not defined, the user will receive an EIO error. *
- int(*ioctl)(struct tty_struct * tty, struct file * file,
- unsigned int cmd, unsigned long arg);
*This function is called when the user requests an ioctl which *is not handled by the tty layer or the low-level tty driver. *It is intended for ioctls which affect line discpline *operation. Note that the search order for ioctls is (1) tty *layer, (2) tty low-level driver, (3) line discpline. So a *low-level driver can “grab” an ioctl request before the line *discpline has a chance to see it. *
- long(*compat_ioctl)(struct tty_struct * tty, struct file * file,
-
unsigned int cmd, unsigned long arg);
*Process ioctl calls from 32-bit process on 64-bit system *
- void(*set_termios)(struct tty_struct *tty, struct ktermios * old);
*This function notifies the line discpline that a change has *been made to the termios structure. *
- int(*poll)(struct tty_struct * tty, struct file * file,
- poll_table *wait);
*This function is called when a user attempts to select/poll on a *tty device. It is solely the responsibility of the line *discipline to handle poll requests. *
- void(*receive_buf)(struct tty_struct *, const unsigned char *cp,
-
char *fp, int count);
*This function is called by the low-level tty driver to send *characters received by the hardware to the line discpline for *processing. is a pointer to the buffer of input *character received by the device. is a pointer to a *pointer of flag bytes which indicate whether a character was *received with a parity error, etc. may be NULL to indicate *all data received is TTY_NORMAL. *
- void(*write_wakeup)(struct tty_struct *);
*This function is called by the low-level tty driver to signal *that line discpline should try to send more characters to the *low-level driver for transmission. If the line discpline does *not have any more data to send, it can just return. If the line *discipline does have some data to send, please arise a tasklet *or workqueue to do the real data transfer. Do not send data in *this hook, it may leads to a deadlock. *
- int (*hangup)(struct tty_struct *)
*Called on a hangup. Tells the discipline that it should *cease I/O to the tty driver. Can sleep. The driver should *seek to perform this action quickly but should wait until *any pending driver I/O is completed. *
- void (*fasync)(struct tty_struct *, int on)
*Notify line discipline when signal-driven I/O is enabled or *disabled. *
- void (*dcd_change)(struct tty_struct *tty, unsigned int status)
*Tells the discipline that the DCD pin has changed its status. *Used exclusively by the N_PP