TTY子系统

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, 最终都会在HDMItty1终端上显示出来。 内核启动完毕之后,在文件系统的启动过程中,会初始化一些程序(比如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

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发给driverdriver在把数据变成硬件可以接受的格式,从硬件发送出去。反过来当硬件收到数据之后,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,每个芯片的数据手册里面一般都有一章节来描述这个模块。不同的芯片厂商,比如AtmelTI,它们的UART模块多少有点不一样,但是绝大部分都是一样的,比如都有start/stop bit,波特率,等。因此Linux内核中又抽象出了一个概念: Serial core

 

Serial core: Serial coretty 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 char*driver_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

driverflags, 可以用以下几种类型相或(|)

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_drivertty_operations结构体, 然后调用tty_register_drivertty 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_driversdrivers/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_drivertty子系统注册时, 如果没有设置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)

根据drivertype, 若是PTY, 则调用pty_line_name; 若是TTY, 则调用tty_line_name.

pty_line_nametty_line_name的目的就是设置名称, 结果存储在name变量中. 它们内部会调sprintf, 格式化输出名称. 具体细节可以看代码.

例如, 如果typeTTY, 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等赋值, 设置devname. 然后调用device_register(dev)注册该device.

device_register在《设备模型》一文中详细介绍过, 它会创建设备节点, 创建class下的子目录. 具体细节请看《设备模型》中的对应章节.

proc_tty_register_driver & /proc/tty/drivers

procLinux系统中的个子模块, 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/<foo>

*/

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的作用

4.   tty core

前一节我们介绍了如何向tty driver子系统注册一个终端驱动.

驱动注册成功之后, 用户空间就可以通过tty core子系统提供的接口与驱动交互了.

本小节, 我们从用户空间的角度, 来看看tty core子系统的内部逻辑.

4.1             简介

前文说过, 所有的终端设备, 从用户空间的角度来看, 都是字符设备驱动.

在注册tty_driver, tty_register_driver会调用tty_cdev_add来注册字符设备驱动, 然后在tty_register_device中会创建设备节点.

 

tty_cdev_add在注册字符设备驱动时, 使用的opsdrivers/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_structktermios, 先看看官方代码中的一段解释:

* 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的区别.

termiostty_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_structname, 它的值是

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在内核空间只对应1tty_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_portbuffer里面, 然后用户空间会从tty_portbuffer读取数据.

 

在继续下面的文章之前, 让我们先梳理一下 /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_porttty_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 : 设置buffersize限制

         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 coreopen函数将会被调用, tty coreopen函数里面, 会从ldis的池子里面选择一个ldis. 另外, 用户空间也可以通过ioctl指定tty core使用某一个ldis.

 

问题继续, tty core知道用哪个ldis, 那么它应该要把这个ldis存储起来, 以便后面随时使用. 存储在哪里呢? 你应该已经知道了, 答案是tty_struct-> ldisc, tty coreopen函数里面, 会创建tty_struct结构体, 然后选取默认的ldis, 然后把获取到的ldis存储在tty_struct-> ldisc.

 

对于内核的这种设计, 有啥想说?

只能说设计的very good! 一方面是体现了软件分层的思想, 不同的子模块负责不同的事情; 另一方面, 模块与模块之前低耦合, tty coreldis并没有必然的联系, 它可以选择使用任何一个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, 说明这个池子最多可以存储30ldis

         另外内核已经规定了大部分ldis对应的ID, 例如#define N_PPP       3, 说明实现PPP这个协议的ldis对应的ID3.

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.  <cp> is a pointer to the buffer of input

*character received by the device.  <fp> is a pointer to a

*pointer of flag bytes which indicate whether a character was

*received with a parity error, etc. <fp> 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.  <cp> is a pointer to the buffer of input

*character received by the device.  <fp> is a pointer to a

*pointer of flag bytes which indicate whether a character was

*received with a parity error, etc. <fp> 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 coretty driver.

如果用户空间没有显示配置用哪一个ldis, 默认使用的就是tty_ldisc_N_TTY.

 

这里有两个问题:

    1. tty_ldisc_N_TTY是谁定义的, 何时会把自己添加到池子里面?
  1. 代码逻辑上是怎么把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 coreopen函数会创建一个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主要是针对串口驱动. 绝大多数ARMCPU, 都有串口控制器, CPU的芯片手册里面, 一般叫UART或者USART.

 

为什么会有serial core的存在呢? 主要目的是为了让编写串口驱动变得更加容易.

当你需要编写一个串口驱动时, 你只需要向serial core子系统注册即可, serial core会帮你向tty driver子系统进行注册. 当然你也可以直接向tty driver子系统注册一个串口驱动, 这样就相当于绕过了serial core, 一般不推荐这样做.

 

serial core里面也涉及到几个自己的数据结构, 为了理清这些数据结构的意义, 我们先来看看串口在硬件上的特点:

一般一个ARMCPU上会有多个UART/USART控制器, 它们在芯片手册上一般叫做USART1, USART2, USART3

Serial core里面, 用一个struct uart_driver结构体代表一个CPU的所有USART控制器; 用一个struct uart_state结构体代表CPU的某一个具体的控制器(例如USART1).

 

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

好多数据结构, 别着急, 我们来慢慢分析:

         一个CPU(假设有NUSART控制器)对应一个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 char*driver_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 console*cons

暂时不清楚具体作用

/*

* 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_porttty_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结构体, 然后调用该APIserial 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->nruart_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_drivertty driver子系统进行注册时, 并不会创建字符设备驱动, 也不会生成设备节点, 也不会在/sys/class/tty下创建子目录.

为什么? 回想一下uart_driver这个结构体作用: 它并不对应某个具体的USART硬件控制器, 它好像一个容器, 里面可以存放多个硬件控制器(也就是uart_port). 而设备节点一般就代表某一个硬件控制器, 例如/dev/ttyS0对应的是USART0这个控制器, /dev/ttyS1对应的USART1这个控制器, 因此设备节点不应该在此时创建, 而应该在uart_port注册的时候创建.

        tty_set_operations(normal, &uart_ops)

设置normaltty_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, 并设置其opsuart_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_portuart_port->uart_ops结构体之后, 就可以调用该APIserial 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 串口驱动编写流程

  1. 注册uart_driver

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

 

思考一个问题? 在什么时机调用uart_register_driver, 是不是弄个platform_driver, 然后在driverprobe函数里面调用uart_register_driver? 答案是否定的.

  1. 添加一个uart_port

定义一个struct uart_portstruct 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, driverprobe函数里面, 调用uart_add_one_portserial core子系统添加一个USART控制器.

 

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

posted @ 2020-12-13 17:37  johnliuxin  阅读(1724)  评论(0编辑  收藏  举报