在Linux系统中,终端设备非常重要,没有终端设备,系统将无法向用户反馈信息,Linux中包含控制台、串口和伪终端3类终端设备。
14.1终端设备
在Linux系统中,终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。tty是Teletype的缩写,Teletype是最早出现的一种终端设备,很像电传打字机,是由Teletype公司生产的。Linux中包含如下几类终端设备:
1、串行端口终端(/dev/ttySn)
串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。这些串行端口所对应的设备名称是/dev/ttyS0(或/dev/tts/0)、/dev/ttyS1(或/dev/tts/1)等,设备号分别是(4,0)、(4,1)等。
在命令行上把标准输出重定向到端口对应的设备文件名上就可以通过该端口发送数据,例如,在命令行提示符下键入: echo test > /dev/ttyS1会把单词“test”发送到连接在ttyS1端口的设备上。
2.伪终端(/dev/pty/)
伪终端(Pseudo Terminal)是成对的逻辑终端设备,并存在成对的设备文件,如/dev/ptyp3和/dev/ttyp3,它们与实际物理设备并不直接相关。如果一个程序把ttyp3看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对应的ptyp3上,而ptyp3则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行互相交流,使用ttyp3的程序会认为自己正在与一个串行端口进行通信。
以telnet 为例,如果某人在使用telnet程序连接到Linux系统,则telnet程序就可能会开始连接到设备ptyp2上,而此时一个getty程序会运行在对应的ttyp2端口上。当telnet从远端获取了一个字符时,该字符就会通过ptyp2、ttyp2传递给 getty程序,而getty程序则会通过ttyp2、ptyp2和telnet程序返回“login:”字符串信息。这样,登录程序与telnet程序 就通过伪终端进行通信。通过使用适当的软件,可以把2个或多个伪终端设备连接到同一个物理串行端口上。
3.控制台终端(/dev/ttyn, /dev/console)
如果当前进程有控制终端(Controlling Terminal)的话,那么/dev/tty就是当前进程的控制终端的设备特殊文件。可以使用命令“ps –ax”来查看进程与哪个控制终端相连使用命令“tty”可以查看它具体对应哪个实际终端设备。/dev/tty有些类似于到实际所使用终端设备的一个联接。
在UNIX系统中,计算机显示器通常被称为控制台终端(Console)。它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty0、tty1、tty2等。当用户在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。tty1–tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上。因此不管当前正在使用哪个虚拟终端,系统信息都会发送到控制台终端上。用户可以登录到不同的虚拟终端上去,因而可以让系统同时有几个不同的会话期存在。只有系统或超级用户root可以向/dev/tty0进行写操作。
在Linux 中,可以在系统启动命令行里指定当前的输出终端,格式如下:
console=device, options
device指代的是终端设备,可以是tty0(前台的虚拟终端)、ttyX(第X个虚拟终端)、ttySX(第X个串口)、lp0(第一个并口)等。options指代对device进行的设置,它取决于具体的设备驱动。对于串口设备,参数用来定义为:波特率、校验位、位数,格式为BBBBPN,其中BBBB表示波特率,P表示校验(n/o/e),N表示位数,默认options是9600n8。
用户可以在内核命令行中同时设定多个终端,这样输出将会在所有的终端上显示,而当用户调用open()打开/dev/console时,最后一个终端将会返回作为当前值。例如:
console=ttyS1, 9600 console=tty0
定义了2个终端,而调用open()打开/dev/console时,将使用虚拟终端tty0。但是内核消息会在tty0 VGA虚拟终端和串口ttyS1上同时显示。
通过查看/proc/tty/drivers文件可以获知什么类型的tty设备存在以及什么驱动被加载到内核,这个文件包括一个当前存在的不同 tty 驱动的列表,包括驱动名、缺省的节点名、驱动的主编号、这个驱动使用的次编号范围,以及 tty 驱动的类型。例如,下面给出了一个/proc/tty/drivers文件的例子。
14.2终端设备驱动结构
Linux内核中 tty的层次结构下图所示,包含tty核心、tty线路规程和tty驱动。
tty 线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式,例如 PPP 和 Bluetooth。
tty设备发送数据的流程为:tty核心从一个用户获取将要发送给一个 tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传递到tty驱动,tty驱动将数据转换为可以发送给硬件的格式。
接收数据的流程为: 从tty硬件接收到的数据向上交给tty驱动,进入tty线路规程驱动,再进入 tty核心,在这里它被一个用户获取。尽管大多数时候tty核心和tty之间的数据传输会经历tty线路规程的转换,但是tty驱动与tty核心之间也可以直接传输数据。
tty设备驱动
在linux内核中已经实现了tty驱动层,
图14.2显示了与tty相关的主要源文件及数据的流向。tty_io.c定义了tty 设备通用的file_operations结构体并实现了接口函数tty_register_driver()用于注册tty设备,它会利用 fs/char_dev.c提供的接口函数注册字符设备,与具体设备对应的tty驱动将实现tty_driver结构体中的成员函数。同时 tty_io.c也提供了tty_register_ldisc()接口函数用于注册线路规程,n_tty.c文件则实现了tty_disc结构体中的成员。
fs/char_dev.c |
tty_io.c |
xxx_tty.c |
n_tty.c |
/dev/ttyS0 |
DB9 |
注册字符设备 Struct_file operations |
tty_register_driver() Struct tty_driver |
tty_register_ldisc Struct tty_disc |
图14.2 tty主要源文件关系及数据流向
从图14.2可以看出,特定tty设备驱动的主体工作是填充tty_driver结构体中的成员,实现其中的成员函数,tty_driver结构体的定义如代码清单14.1。
代码清单14.1 tty_driver结构体
1 struct tty_driver
2 {
3 int magic;
4 struct cdev cdev; /* 对应的字符设备cdev */
5 struct module *owner; /*这个驱动的模块拥有者 */
6 const char *driver_name;
7 const char *devfs_name;
8 const char *name; /* 设备名 */
9 int name_base; /* offset of printed name */
10 int major; /* 主设备号 */
11 int minor_start; /* 开始次设备号 */
12 int minor_num; /* 设备数量 */
13 int num; /* 被分配的设备数量 */
14 short type; /* tty驱动的类型 */
15 short subtype; /* tty驱动的子类型 */
16 struct termios init_termios; /* 初始线路设置 */
17 int flags; /* tty驱动标志 */
18 int refcount; /*引用计数(针对可加载的tty驱动) */
19 struct proc_dir_entry *proc_entry; /* /proc文件系统入口 */
20 struct tty_driver *other; /* 仅对PTY驱动有意义 */
21 ...
22 /* 接口函数 */
23 int(*open)(struct tty_struct *tty, struct file *filp);
24 void(*close)(struct tty_struct *tty, struct file *filp);
25 int(*write)(struct tty_struct *tty, const unsigned char *buf, int count);
26 void(*put_char)(struct tty_struct *tty, unsigned char ch);
27 void(*flush_chars)(struct tty_struct *tty);
28 int(*write_room)(struct tty_struct *tty);
29 int(*chars_in_buffer)(struct tty_struct *tty);
30 int(*ioctl)(struct tty_struct *tty, struct file *file, unsigned int cmd,
31 unsigned long arg);
32 void(*set_termios)(struct tty_struct *tty, struct termios *old);
33 void(*throttle)(struct tty_struct *tty);
34 void(*unthrottle)(struct tty_struct *tty);
35 void(*stop)(struct tty_struct *tty);
36 void(*start)(struct tty_struct *tty);
37 void(*hangup)(struct tty_struct *tty);
38 void(*break_ctl)(struct tty_struct *tty, int state);
39 void(*flush_buffer)(struct tty_struct *tty);
40 void(*set_ldisc)(struct tty_struct *tty);
41 void(*wait_until_sent)(struct tty_struct *tty, int timeout);
42 void(*send_xchar)(struct tty_struct *tty, char ch);
43 int(*read_proc)(char *page, char **start, off_t off, int count, int *eof,
44 void *data);
45 int(*write_proc)(struct file *file, const char __user *buffer, unsigned long
46 count, void *data);
47 int(*tiocmget)(struct tty_struct *tty, struct file *file);
48 int(*tiocmset)(struct tty_struct *tty, struct file *file, unsigned int set,
49 unsigned int clear);
50
51 struct list_head tty_drivers;
52 };
tty_driver结构体中的magic表示给这个结构体的“幻数”,设为 TTY_DRIVER_MAGIC,在alloc_tty_driver()函数中被初始化。name与driver_name的不同在于后者表示驱动的名字,用在/proc/tty 和 sysfs中,而前者表示驱动的设备节点名。type 与subtype描述tty驱动的类型和子类型,subtype的值依赖于type,type成员的可能值为 TTY_DRIVER_TYPE_SYSTEM(由tty子系统内部使用,subtype 应当设为SYSTEM_TYPE_TTY、SYSTEM_TYEP_CONSOLE、SYSTEM_TYPE_SYSCONS或SYSTEM_TYPE_SYSPTMX,这个类型不应当被任何常规tty驱动使用)、TTY_DRIVER_TYPE_CONSOLE(仅被控制台驱动使用)、TTY_DRIVER_TYPE_SERIAL(被任何串行类型驱动使用,subtype 应当设为 SERIAL_TYPE_NORMAL或SERIAL_TYPE_CALLOUT)、TTY_DRIVER_TYPE_PTY(被伪控制台接口pty使用,此时subtype需要被设置为 PTY_TYPE_MASTER 或 PTY_TYPE_SLAVE)。init_termios 为初始线路设置,为一个termios结构体,这个成员被用来提供一个线路设置集合。termios 用于保存当前的线路设置,这些线路设置控制当前波特率、数据大小、数据流控设置等,这个结构体包含tcflag_t c_iflag(输入模式标志)、tcflag_t c_oflag(输出模式标志)、tcflag_t c_cflag(控制模式标志)、tcflag_t c_lflag(本地模式标志)、cc_t c_line(线路规程类型)、cc_t c_cc[NCCS](一个控制字符数组)等成员。驱动会使用一个标准的数值集初始化这个成员,它拷贝自tty_std_termios变量,tty_std_termos在tty核心中的定义如代码清单14.2。
代码清单14.2 tty_std_termios变量
1 struct termios tty_std_termios =
2 {
3 .c_iflag = ICRNL | IXON, /* 输入模式 */
4 .c_oflag = OPOST | ONLCR, /* 输出模式 */
5 .c_cflag = B38400 | CS8 | CREAD | HUPCL, /* 控制模式 */
6 .c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK |
7 ECHOCTL | ECHOKE | IEXTEN, /* 本地模式 */
8 .c_cc = INIT_C_CC /* 控制字符,用来修改终端的特殊字符映射 */
9 };
tty_driver 结构体中的major、minor_start、minor_num表示主设备号、次设备号及可能的次设备数,name表示设备名(如ttyS),第 23~49行的函数指针实际和tty_operations结构体等同,它们通常需在特定设备tty驱动模块初始化函数中被赋值。put_char()为单字节写函数,当单个字节被写入设备时这个函数被 tty 核心调用,如果一个 tty 驱动没有定义这个函数,将使用count参数为1的write()函数。flush_chars()与wait_until_sent()函数都用于刷新数据到硬件。write_room()指示有多少缓冲区空闲,chars_in_buffer()指示缓冲区中包含的数据数。当 ioctl(2)在设备节点上被调用时,ioctl()函数将被 tty核心调用。当设备的 termios 设置被改变时,set_termios()函数将被tty核心调用。throttle ()、unthrottle()、stop()和start()为数据抑制函数,这些函数用来帮助控制tty 核心的输入缓存。当 tty 核心的输入缓冲满时,throttle()函数将被调用,tty驱动试图通知设备不应当发送字符给它。当 tty 核心的输入缓冲已被清空时,unthrottle()函数将被调用暗示设备可以接收数据。stop()和start()函数非常像throttle()和 unthrottle()函数,但它们表示 tty 驱动应当停止发送数据给设备以及恢复发送数据。
当 tty驱动挂起 tty设备时,hangup()函数被调用,在此函数中进行相关的硬件操作。当tty 驱动要在 RS-232 端口上打开或关闭线路的 BREAK 状态时,break_ctl()线路中断控制函数被调用。如果state状态设为-1,BREAK 状态打开,如果状态设为 0,BREAK 状态关闭。如果这个函数由 tty驱动实现,而tty核心将处理TCSBRK、TCSBRKP、TIOCSBRK和 TIOCCBRK这些ioctl命令。flush_buffer()函数用于刷新缓冲区并丢弃任何剩下的数据。set_ldisc()函数用于设置线路规程,当 tty 核心改变tty驱动的线路规程时这个函数被调用,这个函数通常不需要被驱动定义。send_xchar()为X-类型字符发送函数,这个函数用来发送一个高优先级 XON 或者 XOFF 字符给 tty设备,要被发送的字符在第2个参数ch中指定。read_proc()和write_proc()为/proc 读和写函数。tiocmget()函数用于获得tty 设备的线路设置,对应的tiocmset()用于设置tty设备的线路设置,参数set和clear包含了要设置或者清除的线路设置。
Linux内核提供了一组函数用于操作tty_driver结构体及tty设备,包括:
• 分配tty驱动
struct tty_driver *alloc_tty_driver(int lines);
这个函数返回tty_driver指针,其参数为要分配的设备数量,line会被赋值给tty_driver的num成员,例如:
xxx_tty_driver = alloc_tty_driver(XXX_TTY_MINORS);
if (!xxx_tty_driver) //分配失败
return -ENOMEM;
• 注册tty驱动
int tty_register_driver(struct tty_driver *driver);
注册tty驱动成功时返回0,参数为由alloc_tty_driver ()分配的tty_driver结构体指针,例如:
retval = tty_register_driver(xxx_tty_driver);
if (retval) //注册失败
{
printk(KERN_ERR "failed to register tiny tty driver");
put_tty_driver(xxx_tty_driver);
return retval;
}
• 注销tty驱动
int tty_unregister_driver(struct tty_driver *driver);
这个函数与tty_register_driver ()对应,tty驱动最终会调用上述函数注销tty_driver。
• 注册tty设备
void tty_register_device(struct tty_driver *driver, unsigned index,
struct device *device);
仅有tty_driver是不够的,驱动必须依附于设备,tty_register_device()函数用于注册关联于tty_driver的设备,index为设备的索引(范围是0~driver->num),如:
for (i = 0; i < XXX_TTY_MINORS; ++i)
tty_register_device(xxx_tty_driver, i, NULL);
• 注销tty设备
void tty_unregister_device(struct tty_driver *driver, unsigned index);
上述函数与tty_register_device()对应,用于注销tty设备,其使用方法如:
for (i = 0; i < XXX_TTY_MINORS; ++i)
tty_unregister_device(xxx_tty_driver, i);
• 设置tty驱动操作
void tty_set_operations(struct tty_driver *driver, struct tty_operations *op);
上述函数会将tty_operations结构体中的函数指针拷贝给tty_driver对应的函数指针,在具体的tty驱动中,通常会定义1个设备特定的 tty_operations,tty_operations的定义如代码清单14.3。tty_operations中的成员函数与 tty_driver中的同名成员函数意义完全一致,因此,这里不再赘述。
代码清单14.3 tty_operations结构体
1 struct tty_operations
2 {
3 int (*open)(struct tty_struct * tty, struct file * filp);
4 void (*close)(struct tty_struct * tty, struct file * filp);
5 int (*write)(struct tty_struct * tty,
6 const unsigned char *buf, int count);
7 void (*put_char)(struct tty_struct *tty, unsigned char ch);
8 void (*flush_chars)(struct tty_struct *tty);
9 int (*write_room)(struct tty_struct *tty);
10 int (*chars_in_buffer)(struct tty_struct *tty);
11 int (*ioctl)(struct tty_struct *tty, struct file * file,
12 unsigned int cmd, unsigned long arg);
13 void (*set_termios)(struct tty_struct *tty, struct termios * old);
14 void (*throttle)(struct tty_struct * tty);
15 void (*unthrottle)(struct tty_struct * tty);
16 void (*stop)(struct tty_struct *tty);
17 void (*start)(struct tty_struct *tty);
18 void (*hangup)(struct tty_struct *tty);
19 void (*break_ctl)(struct tty_struct *tty, int state);
20 void (*flush_buffer)(struct tty_struct *tty);
21 void (*set_ldisc)(struct tty_struct *tty);
22 void (*wait_until_sent)(struct tty_struct *tty, int timeout);
23 void (*send_xchar)(struct tty_struct *tty, char ch);
24 int (*read_proc)(char *page, char **start, off_t off,
25 int count, int *eof, void *data);
26 int (*write_proc)(struct file *file, const char __user *buffer,
27 unsigned long count, void *data);
28 int (*tiocmget)(struct tty_struct *tty, struct file *file);
29 int (*tiocmset)(struct tty_struct *tty, struct file *file,
30 unsigned int set, unsigned int clear);
31 };
终端设备驱动都围绕tty_driver结构体而展开,一般而言,终端设备驱动应包含如下组成:
• 终端设备驱动模块加载函数和卸载函数,完成注册和注销tty_driver,初始化和释放终端设备对应的tty_driver结构体成员及硬件资源。
• 实现tty_operations结构体中的一系列成员函数,主要是实现open()、close()、write()、tiocmget()、tiocmset()等函数.