tty子系统

转自:https://www.cnblogs.com/jliuxin/p/14129303.html#_label4

  1. 概念介绍:终端
    在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可以理解为1个链接, 它链接到当前正在使用的控制台终端, 比如现在通过Alt+F2切换到/dev/tty2对应的控制台终端, 然后输入命令echo test > /dev/tty0, 就会在/dev/tty2对应的控制台终端上看到test. 如果用Alt+F3切换到另一个终端, 在做同样的动作, 也会在F3对应的终端上看到test. 但是不论在哪个终端, 输入命令echo test > /dev/tty2, 都只会在Alt+F2对应的终端上看到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的tty1终端上显示出来。 内核启动完毕之后,在文件系统的启动过程中,会初始化一些程序(比如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)。其中,M与N的命名方式如下:
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
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
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 module
owner

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 char
name
会出现在/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的作用
    回到顶部

  1. 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_PPS (Pulse-Per-Second) line discipline.
*

  • int(*receive_buf2)(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.
*If assigned, prefer this function for automatic flow control.
*/
5.3 主要API说明

tty_register_ldisc

头文件: include/linux/tty.h
实现文件: drivers/tty/tty_ldisc.c
int tty_register_ldisc(int disc, struct tty_ldisc_ops *new_ldisc)
 代码逻辑很简单, 把new_ldisc存储到tty_ldiscs数组中, 参数disc代表new_ldisc在数组中的ID.
tty_set_ldisc

头文件: include/linux/tty.h
实现文件: drivers/tty/tty_ldisc.c
int tty_set_ldisc(struct tty_struct *tty, int ldisc)
 当用户空间调用ioctl设置ldis时, 该函数会被调用.
函数的逻辑也很简单, 根据参数ldisc, 从池子里面选择对应的ldis, 然后替换tty_struct->ldisc.
tty_ldisc_N_TTY

tty_ldisc_N_TTY并不是一个API, 它是内核系统默认的ldis. 这个ldis并不会对数据做额外的处理, 它就像一个管道, 联通tty core和tty driver.
如果用户空间没有显示配置用哪一个ldis, 默认使用的就是tty_ldisc_N_TTY.

这里有两个问题:
tty_ldisc_N_TTY是谁定义的, 何时会把自己添加到池子里面?
代码逻辑上是怎么把tty_ldisc_N_TTY做为默认的ldis的?

首先看第一个问题: tty_ldisc_N_TTY是谁定义的, 谁注册的:
实现文件: drivers/tty/n_tty.c
struct tty_ldisc_ops tty_ldisc_N_TTY = {
.magic = TTY_LDISC_MAGIC,
.name = “n_tty”,
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.chars_in_buffer = n_tty_chars_in_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.fasync = n_tty_fasync,
.receive_buf2 = n_tty_receive_buf2,
};

注册函数: drivers/tty/tty_ldisc.c
void tty_ldisc_begin(void)
{
/* Setup the default TTY line discipline. */
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
}
 drivers/tty/tty_io.c中的void __init console_init(void)函数会调用这里的tty_ldisc_begin, 然后tty_ldisc_begin调用tty_register_ldisc向池子里面添加一个ldis

再来看第二个问题: 代码逻辑上是怎么把tty_ldisc_N_TTY做为默认的ldis的:
我们知道, 当用户空间对一个tty节点执行open操作时, tty core里面对应的open函数会被调用.
tty core的open函数会创建一个tty_struct结构体, 并且会在此时选择默认的ldis, 并把这个ldis赋值给tty_struct->ldisc.

具体的代码流程是:
drivers/tty/tty_io.c : tty_open -> tty_init_dev -> alloc_tty_struct -> tty_ldisc_init.

tty_ldisc_init是在drivers/tty/tty_ldisc.c中定义的, 代码如下:
void tty_ldisc_init(struct tty_struct *tty)
{
struct tty_ldisc *ld = tty_ldisc_get(tty, N_TTY);
if (IS_ERR(ld))
panic(“n_tty: init_tty”);
tty->ldisc = ld;
}
 tty_ldisc_get(tty, N_TTY): 根据参数N_TTY(其实就是一个ID, 对应数组中的某个元素), 获取到对应的ldis, 然后赋值给tty->ldisc.
回到顶部
6. serial core

6.1 简介

serial core主要是针对串口驱动. 绝大多数ARM的CPU, 都有串口控制器, 在CPU的芯片手册里面, 一般叫UART或者USART.

为什么会有serial core的存在呢? 主要目的是为了让编写串口驱动变得更加容易.
当你需要编写一个串口驱动时, 你只需要向serial core子系统注册即可, serial core会帮你向tty driver子系统进行注册. 当然你也可以直接向tty driver子系统注册一个串口驱动, 这样就相当于绕过了serial core, 一般不推荐这样做.

serial core里面也涉及到几个自己的数据结构, 为了理清这些数据结构的意义, 我们先来看看串口在硬件上的特点:
一般一个ARM的CPU上会有多个UART/USART控制器, 它们在芯片手册上一般叫做USART1, USART2, USART3…
在Serial core里面, 用一个struct uart_driver结构体代表一个CPU的所有USART控制器; 用一个struct uart_state结构体代表CPU的某一个具体的控制器(例如USART1).

先来一张数据结构的关系图, 然后我们再来理清这些数据结构的对应关系:

好多数据结构…, 别着急, 我们来慢慢分析:
 一个CPU(假设有N个USART控制器)对应一个uart_driver, 也对应一个tty_driver.
其中uart_driver->nr = tty_driver->num = N.
 一个USART控制器对应一个uart_state, 也对应一个tty_port, 也对应一个uart_port.
tty_driver->major + tty_driver->minor_start定义了起始设备号, 一个控制器也对应一个设备号, 同时也对应一个/dev/下的节点.

理解了上面描述的对应关系, 再来看下面关于数据结构的介绍就会轻松很多了.
6.2 主要数据结构

uart_driver

头文件: include/linux/serial_core.h
struct uart_driver
Comment
struct module*owner

const chardriver_name
最终会赋值给tty_driver->driver_name, 此字段的作用如果忘记了, 请回头看看tty_driver数据结构的介绍
const char
dev_name
最终会赋值给tty_driver->name, 此字段的作用如果忘记了, 请回头看看tty_driver数据结构的介绍
int major
最终会赋值给tty_driver->major
int minor
最终会赋值给tty_driver->minor_start
int nr
最终会赋值给tty_driver->num
struct consolecons
暂时不清楚具体作用
/

  • these are private; the low level driver should not
  • touch these; they should be initialised to NULL
    /
    struct uart_state
    state
    注意代码注释, 当我们打算实现一个uart_driver结构体时, 这个字段应该为NULL.
    serial core会负责帮我们分配/释放uart_state空间.
    注意, 一个uart_driver下可以”挂接”多个uart_state
    struct tty_driver*tty_driver
    同样, 这个字段应该设置为NULL
    serial core会负责调用alloc_tty_driver来创建tty_driver结构体.
    uart_state

头文件: include/linux/serial_core.h
uart_state, 前文说过, 它对应CPU上某个具体的控制器, 例如USART1.
struct uart_state
Comment
struct tty_portport
一个uart_state对应一个tty_port, 也就是说每个USART控制器都会对应一个tty_port.
tty_port的作用是当硬件收到数据时, 把数据存储在tty_port里面, 然后再转移到ldis里面.
每个USART控制器, 从硬件的角度来说, 都可以收到自己的数据, 因此每个控制器都需要一个tty_port, 否则多个硬件往一个tty_port里面存储数据就会出现数据混乱
struct uart_port*uart_port
一个uart_state也对应一个uart_port.
是不是在想uart_port和tty_port有什么关系? uart_port是不是类似tty_port, 负责把硬件的收到的数据送给ldis?
答案是否定的, 在uart_state中, 已经有tty_port这个结构体来负责把硬件收到的数据转送给ldis, 没有必要再设计一个uart_port来做同样的事情.
uart_port的主要作用是跟硬件控制器打交道, CPU上会有多个USART, 每个USART多少有点不一样, 至少每个USART的寄存器地址就不一样, 中断号不一样. 而且不同的USART也可能有不同的波特率等等, 因此每一个硬件控制器, 都需要一个uart_port的结构体来描述它.
因为uart_port是负责跟具体的硬件控制器打交道, 因此往硬件控制器发送数据和从硬件控制器获取数据, 都需要经过uart_port. uart_port接收到数据之后, 会通过uart_port->uart_state->tty_port(看见了吗, uart_port并不能直接获取到tty_port, 从这里也可以看出, 它与tty_port属于同等级别, 都隶属于uart_state)找到tty_port, 然后把数据存储到tty_port中.
后文会详细介绍uart_port结构体
struct circ_bufxmit
一个uart_state也对应一个xmit.
xmit就是一个环形缓冲区, 大小一般是(#define UART_XMIT_SIZE PAGE_SIZE)
xmit的作用是当用户空间调用write操作往串口发送数据时, tty子系统经过层层调用, 会把数据存储在xmit里面, 然后通知硬件开始发送数据.
使用缓冲区的原因是串口采用的是串行传输, 速度比较慢, 用缓冲区的话用户空间就不会被阻塞.
enum uart_pm_statepm_state
休眠相关, 暂不分析
uart_port

一个具体的USART硬件控制器对应一个uart_port.
uart_port是用来描述硬件的, 主要作用是负责与硬件控制器打交道, 控制硬件发送数据, 从硬件接收数据等.

既然uart_port是用来描述硬件的, 那么它应该描述硬件的哪些信息呢?
首先, 得有硬件的寄存器地址, 中断号等.
其次, 还得有操作硬件的接口函数, 例如操作硬件发送数据, 从硬件接收数据等等, 这一部分功能其实是用uart_ops描述的.

下面我们来看看uart_port的数据结构, 挺长的, 我们只截取几个重点:
头文件: include/linux/serial_core.h
struct uart_port
Comment
spinlock_tlock
/* port lock /
unsigned longiobase
unsigned char __iomem
membase
硬件寄存器地址, 我们可以用I/O Port标准提供的in/out, read/write方式来访问硬件
resource_size_tmapbase
resource_size_tmapsize
除了上述方式, 我们也可以通过I/O Memory的方式来访问硬件寄存器.
事实上, I/O Port方式在X86架构中比较流行, 在ARM中, 更多的时候用的是I/O Memory的方式
unsigned intirq;/* irq number /
unsigned longirqflags;/
irq flags */
(*handle_irq)(struct uart_port *)
中断相关
中断号, 中断标志
中断处理函数
(*handle_break)(struct uart_port *)
处理break信号

struct serial_rs485 rs485
(*rs485_config)(struct uart_port *, struct serial_rs485 rs485)
485相关
485的标志位, (是否使能485等等)
485的配置函数
const struct uart_ops
ops
指向uart_ops
………
还有很多, 就不一一细说了
(*serial_in)(struct uart_port *, int)
(*serial_out)(struct uart_port *, int, int)
(*set_termios)(struct uart_port *, ……)
(*set_mctrl)(struct uart_port *, unsigned int)
(*startup)(struct uart_port *port)
(*shutdown)(struct uart_port *port)
(*throttle)(struct uart_port *port)
(*unthrottle)(struct uart_port *port)
(*pm)(struct uart_port *, unsigned int state, …)
这些接口函数的功能与uart_ops里面重复了, 基本上都是使用uart_ops里面的函数, 这里都没有用到
uart_ops

uart_ops算是uart_port的子结构, 它是用于描述如何操作一个USART硬件控制器来收发数据的.

头文件: include/linux/serial_core.h
/*

  • This structure describes all the operations that can be done on the

  • physical hardware. See Documentation/serial/driver for details.
    */
    struct uart_ops {
    unsigned int (*tx_empty)(struct uart_port *);
    void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
    unsigned int (*get_mctrl)(struct uart_port *);
    void (*stop_tx)(struct uart_port *);
    void (*start_tx)(struct uart_port *);
    void (*throttle)(struct uart_port *);
    void (*unthrottle)(struct uart_port *);
    void (*send_xchar)(struct uart_port *, char ch);
    void (*stop_rx)(struct uart_port *);
    void (*enable_ms)(struct uart_port *);
    void (*break_ctl)(struct uart_port *, int ctl);
    int (*startup)(struct uart_port *);
    void (*shutdown)(struct uart_port *);
    void (*flush_buffer)(struct uart_port *);
    void (*set_termios)(struct uart_port *, struct ktermios *new,
    struct ktermios *old);
    void (*set_ldisc)(struct uart_port *, struct ktermios *);
    void (*pm)(struct uart_port *, unsigned int state,
    unsigned int oldstate);

    /*

    • Return a string describing the type of the port
      */
      const char *(*type)(struct uart_port *);

    /*

    • Release IO and memory resources used by the port.
    • This includes iounmap if necessary.
      */
      void (*release_port)(struct uart_port *);

    /*

    • Request IO and memory resources used by the port.
    • This includes iomapping the port if necessary.
      */
      int (*request_port)(struct uart_port *);
      void (*config_port)(struct uart_port *, int);
      int (*verify_port)(struct uart_port *, struct serial_struct *);
      int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
      #ifdef CONFIG_CONSOLE_POLL
      int (*poll_init)(struct uart_port *);
      void (*poll_put_char)(struct uart_port *, unsigned char);
      int (*poll_get_char)(struct uart_port *);
      #endif
      };
      ops的调用流程梳理

从第二章开始到现在, 我们已经介绍过很多种ops, 下面我们梳理一下它们的调用流程.

从用户空间到硬件:
用户空间 -> tty core(file_operations tty_fops) -> tty line discipline(tty_ldisc_ops) -> tty driver(tty_operations) -> uart port(uart_ops)

从硬件到用户空间:
上述流程反过来即可
6.3 主要API说明

uart_register_driver

如果我们想编写一个串口驱动, 需要准备好uart_driver结构体, 然后调用该API向serial core进行注册.
该API会进一步向tty driver子系统注册, 下面我们分析一下该API的实现细节

头文件: include/linux/serial_core.h
实现文件: drivers/tty/serial/serial_core.c
int uart_register_driver(struct uart_driver *drv)
 drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL)
分配drv->nr个uart_state空间
 normal = alloc_tty_driver(drv->nr)
申请一个tty_driver结构体normal, 随后会初始化normal, 我们看看几个重要的地方:
 normal->flags= TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV
flags设置了TTY_DRIVER_DYNAMIC_DEV标志, 设置这个标准意味着什么? 不记得了可以回头看看第3章. 它意味着当调用tty_register_driver向tty driver子系统进行注册时, 并不会创建字符设备驱动, 也不会生成设备节点, 也不会在/sys/class/tty下创建子目录.
为什么? 回想一下uart_driver这个结构体作用: 它并不对应某个具体的USART硬件控制器, 它好像一个容器, 里面可以存放多个硬件控制器(也就是uart_port). 而设备节点一般就代表某一个硬件控制器, 例如/dev/ttyS0对应的是USART0这个控制器, /dev/ttyS1对应的USART1这个控制器, 因此设备节点不应该在此时创建, 而应该在uart_port注册的时候创建.
 tty_set_operations(normal, &uart_ops)
设置normal的tty_operations, 也就是uart_ops. uart_ops是在serial_core.c中实现的. tty line discipline就是与这个uart_ops打交道的.
 for (i = 0; i < drv->nr; i++) {
….
tty_port_init(port);
port->ops = &uart_port_ops;
}
初始化tty_port, 并设置其ops为uart_port_ops, uart_port_ops也是在serial_core.c中实现的. 当硬件收到数据之后, 就是通过这个uart_port_ops把数据转存到tty line discipline里面的
 retval = tty_register_driver(normal)
向tty driver子系统注册
uart_add_one_port

针对某一个具体的USART硬件控制器, 当我们准备好uart_port和uart_port->uart_ops结构体之后, 就可以调用该API向serial core子系统添加一个port了.

头文件: include/linux/serial_core.h
实现文件: drivers/tty/serial/serial_core.c
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
 当对uart_port做完一系列的初始化之后, 最终会调用tty_port_register_device_attr, 这是tty driver子系统的一个API, 该API最终会导致注册字符设备驱动, 生成设备节点, 创建/sys/class/tty下的子目录
回到顶部
7. 驱动设计指导规范

在大多情况下, 我们的驱动开发工作主要都是开发串口驱动, 即向serial core子系统进行注册. 因为本节主要介绍编写串口驱动的主要步骤.

在3.1节我们介绍过如何编写一个tty driver, 其实编写串口驱动和编写tty driver很相似.
只不过当你编写tty driver时, 是直接向tty driver子系统进行注册. 而编写串口驱动时, 是向serial core子系统注册, 然后serial core再向tty driver子系统进行注册.
7.1 代码文件结构

编写串口驱动, 你一定需要引用serial_core.h这个头文件.
文件路径: include/linux/serial_core.h, 这个头文件定义了你需要实现的数据结构, 定义了serial core子系统提供给你的API.

同时, 你可能需要看看drivers/tty/serial/serial_core.c这个C文件, 以辅助理解API的意义.
7.2 串口驱动编写流程

注册uart_driver
定义一个struct uart_driver结构体, 给结构体的相应变量赋值, 然后调用uart_register_driver进行注册即可.

思考一个问题? 在什么时机调用uart_register_driver, 是不是弄个platform_driver, 然后在driver的probe函数里面调用uart_register_driver? 答案是否定的.
添加一个uart_port
定义一个struct uart_port和struct uart_ops结构体, 然后调用uart_add_one_port进行注册.

同样, 思考一个问题, 何时调用uart_add_one_port, 会不会有platform_driver?
答案是肯定的. 因为uart_port是用于描述一个具体的USART控制器的. USART控制器属于ARM CPU的片上外设, 所有的片上外设都会挂接在platform bus下面, 有一个paltform device描述此外设的硬件资源, 还有一个platform driver用于描述如何操作这个外设.
所以, 对于每一个USART控制器, 都会有一个与之对于的platfrom device, 这些device共用同一个platfrom driver, 在driver的probe函数里面, 调用uart_add_one_port向serial core子系统添加一个USART控制器.

不过uart_register_driver就不是在probe函数里面被调用的, 因为它并不对应某一个具体的片上外设, 不会有与之对于的platform device. 一般我们会定义一个module_init函数, 在module_init函数里面调用uart_register_driver

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值