[539页]
10-6 tty_io.c程序
10-6-1 功能描述
每个tty设备有3个缓冲队列,分别是读缓冲队列(read_q)、写缓冲队列(write_q)和辅助缓冲队列(secondary),定义在tty_struct结构中(include/linux/tty.h)。
对于每个缓冲队列,读操作是从缓冲队列的左端取字符,并且把缓冲队列尾巴(tail)指针向右移动。而写操作则是往缓冲队列的右端添加字符,并且也把头(head)指针向右移动。这两个指针中,任何一个若移动到超出了缓冲队列的末端,则折回到左端重新开始。
如图10-14所示。
本程序包括字符设备的上层接口函数。
主要含有终端读/写函数tty_read()和tty_write()。
读操作的行规则函数copy_to_cooked()也在这里实现。
tty_read()和tty_write()将在文件系统中用于操作字符设备文件时被调用。
例如当一个程序读/dev/tty文件时,就会执行系统调用sys_read(0(在fs/read_write.c中),而这个系统调用在判别处所读文件是一个字符设备文件时,即会调用rw_char()函数(在fs/char_dev.c中),该函数则会根据所读设备的子设备号等信息,由字符设备读写函数表(设备开关表)调用rw_tty(),最终调用到这里的终端读操作函数tty_read()。
copy_to_cooked()函数由键盘中断过程调用(通过do_tty_interrupt()),用于根据终端termios结构中设置值的字符输入/输出标志(例如INLCR、OUCLC)对read_q队列中的字符进行处理,把字符转换以字符行为单位的规范模式字符序列,并保存在辅助字符缓冲队列(规范模式缓冲队列)(secondary)中,供上述tty_read()读取。在转换处理期间,若终端的回显标志L_ECHO置位,则还会把字符放入写队列write_q中,并调用终端写函数把该字符显示在屏幕上。如果是串行终端,那么写函数将时rs_write()(在serial.c,53行)。rs_write()会把串行终端写队列中的字符通过串行线路发送给串行终端,并显示在串行终端的屏幕上。copy_to_cooked()函数最后还将唤醒等待着辅助缓冲队列的进程。函数实现的步骤如下所示:
1、 如果读队列空或者辅助队列已经满,则跳转到最后一步(第10步),否则执行以下操作。
2、 从读队列read_q的尾指针处取一字符,并且尾指针前移一字符位置。
3、 若是回车(CR)或换行(NL)字符,则根据终端termios结构中输入标志(ICRNL、INLCR、INOCR)的状态,
对该字符作相应转换。例如,如果读取的是一个回车字符并且ICRNL标志是置位的,则把它替换成换行字符。
4、 若大写小写标志IUCLC是置位的,则把字符替换成对应的小写字符。
5、 若规范模式标志ICANON是置位的,则对该字符进行规范模式处理。
a.若是删行字符(^U),则删除secondary中的一行字符(队列头指针后退,直到遇到回车或换行或队列已空为止);
b.若是擦除字符(^H),则删除secondary中头指针处的一个字符,头指针后退一个字符位置;
c.若是停止字符(^S),则设置终端的停止标志stopped=1;
d.若是开始字符(^Q),则复位终端的停止标志。
6、 如果接收键盘信号标志ISIG是置位的,则为进程生成对应键入控制字符的信号。
7、 如果是行结束字符(例如NL或^D),则辅助队列secondary的行数统计值data增1.
8、 如果本地回显标志是置位的,则把字符也放入写队列write_q中,并调用终端写函数在屏幕上显示该字符。
9、 把该字符放入辅助队列secondary中,返回第1步继续循环处理读队列中其他字符。
10、 最后唤醒睡眠在辅助队列上的进程。
在阅读下面的程序前不妨首先查看一下include/linux/tty.h头文件。在该头文件定义了tty字符缓冲队列的数据结构以及一些宏操作定义。另外还定义了控制字符的ASCII码值。
10-6-2 代码注释
/*
* linux/kernel/tty_io.c
*
* (C) 1991 Linus Torvalds
*/
/*
* 'tty_io.c'给tty终端一种非相关的感觉,不管它们是控制台还是串行终端。
* 该程序同样实现了回显、规范(熟)模式等。
*
* Kill-line问题,要谢谢John T Kohl。他同时还纠正了当 VMIN = VTIME = 0 时的问题。
*/
#include <ctype.h> // 字符类型头文件。定义了一些有关字符类型判断和转换的宏。
#include <errno.h> // 错误号头文件。包含系统中各种出错号。
#include <signal.h> // 信号头文件。定义信号符号常量,信号结构及其操作函数原型。
#include <unistd.h> // unistd.h是标准符号常数与类型文件,并声明了各种函数。
// 给出定时警告(alarm)信号在信号位图中对应的比特屏蔽位。
#define ALRMMASK (1<<(SIGALRM-1))
#include <linux/sched.h> // 调度程序头文件,定义了任务结构task_struct、任务0数据等。
#include <linux/tty.h> // tty头文件,定义了有关tty_io,串行通信方面的参数、常数。
#include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。
#include <asm/system.h> // 系统头文件。定义设置或修改描述符/中断门等嵌入式汇编宏。
// 终止进程组(向进程组发送信号)。参数pgrp指定进程组号;sig指定信号;priv权限。
// 即向指定进程组pgrp中的每个进程发送指定信号sig。只要向一个进程发送成功最后就会
// 返回0,否则如果没有找到指定进程组号pgrp的任何一个进程,则返回出错号-ESRCH,若
// 找到进程组号是pgrp的进程,但是发送信号失败,则返回发送失败的错误码。
int kill_pg(int pgrp, int sig, int priv); // kernel/exit.c,171行。
// 判断一个进程组是否是孤儿进程。如果不是则返回0;如果是则返回1。
int is_orphaned_pgrp(int pgrp); // kernel/exit.c,232行。
// 获取termios结构中三个模式标志集之一,或者用于判断一个标志集是否有置位标志。
#define _L_FLAG(tty,f) ((tty)->termios.c_lflag & f)
#define _I_FLAG(tty,f) ((tty)->termios.c_iflag & f)
#define _O_FLAG(tty,f) ((tty)->termios.c_oflag & f)
// 取termios结构终端特殊(本地)模式标志集中的一个标志。
#define L_CANON(tty) _L_FLAG((tty),ICANON) // 取规范模式标志。
#define L_ISIG(tty) _L_FLAG((tty),ISIG) // 取信号标志。
#define L_ECHO(tty) _L_FLAG((tty),ECHO) // 取回显字符标志。
#define L_ECHOE(tty) _L_FLAG((tty),ECHOE) // 规范模式时取回显擦出标志。
#define L_ECHOK(tty) _L_FLAG((tty),ECHOK) // 规范模式时取KILL擦除当前行标志。
#define L_ECHOCTL(tty) _L_FLAG((tty),ECHOCTL) // 取回显控制字符标志。
#define L_ECHOKE(tty) _L_FLAG((tty),ECHOKE) // 规范模式时取KILL擦除行并回显标志。
#define L_TOSTOP(tty) _L_FLAG((tty),TOSTOP) // 对于后台输出发送SIGTTOU信号。
// 取termios结构输入模式标志集中的一个标志。
#define I_UCLC(tty) _I_FLAG((tty),IUCLC) // 取大写到小写转换标志。
#define I_NLCR(tty) _I_FLAG((tty),INLCR) // 取换行符NL转回车符CR标志。
#define I_CRNL(tty) _I_FLAG((tty),ICRNL) // 取回车符CR转换行符NL标志。
#define I_NOCR(tty) _I_FLAG((tty),IGNCR) // 取忽略回车符CR标志。
#define I_IXON(tty) _I_FLAG((tty),IXON) // 取输入控制流标志XON。
// 取termios结构输出模式标志集中的一个标志。
#define O_POST(tty) _O_FLAG((tty),OPOST) // 取执行输出处理标志。
#define O_NLCR(tty) _O_FLAG((tty),ONLCR) // 取换行符NL转回车换行符CR-NL标志。
#define O_CRNL(tty) _O_FLAG((tty),OCRNL) // 取回车符CR转换行符NL标志。
#define O_NLRET(tty) _O_FLAG((tty),ONLRET) // 取换行符NL执行回车功能的标志。
#define O_LCUC(tty) _O_FLAG((tty),OLCUC) // 取小写转大写字符标志。
// 取termios结构控制标志集中波特率。CBAUD是波特率屏蔽码(0000017)。
#define C_SPEED(tty) ((tty)->termios.c_cflag & CBAUD)
// 判断tty终端是否已挂线(hang up),即其传输波特率是否为B0(0)。
#define C_HUP(tty) (C_SPEED((tty)) == B0)
// 取最小值宏。
#ifndef MIN
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#endif
// 下面定义tty终端使用的缓冲队列结构数组tty_queues 和 tty终端表结构数组tty_table。
// QUEUES是tty终端使用的缓冲队列最大数量。伪终端分主从两种(master和slave)。每个
// tty终端使用3个tty 缓冲队列,它们分别是用于缓冲键盘或串行输入的读队列 read_queue、
// 用于缓冲屏幕或串行输出的写队列 write_queue,以及用于保存规范模式字符的辅助缓冲队列
// secondary。
#define QUEUES (3*(MAX_CONSOLES+NR_SERIALS+2*NR_PTYS)) // 共54项。
static struct tty_queue tty_queues[QUEUES]; // tty缓冲队列数组。
struct tty_struct tty_table[256]; // tty表结构数组。
// 下面设定各种类型的tty终端所使用缓冲队列结构在tty_queues[]数组中的起始项位置。
// 8个虚拟控制台终端占用tty_queues[]数组开头24项(3 X MAX_CONSOLES)(0 -- 23);
// 两个串行终端占用随后的6项(3 X NR_SERIALS)(24 -- 29)。
// 4个主伪终端占用随后的12项(3 X NR_PTYS)(30 -- 41)。
// 4个从伪终端占用随后的12项(3 X NR_PTYS)(42 -- 53)。
#define con_queues tty_queues
#define rs_queues ((3*MAX_CONSOLES) + tty_queues)
#define mpty_queues ((3*(MAX_CONSOLES+NR_SERIALS)) + tty_queues)
#define spty_queues ((3*(MAX_CONSOLES+NR_SERIALS+NR_PTYS)) + tty_queues)
// 下面设定各种类型tty终端所使用的tty结构在tty_table[]数组中的起始项位置。
// 8个虚拟控制台终端可用tty_table[]数组开头64项(0 -- 63);
// 两个串行终端使用随后的2项(64 -- 65)。
// 4个主伪终端使用从128开始的项,最多64项(128 -- 191)。
// 4个从伪终端使用从192开始的项,最多64项(192 -- 255)。
#define con_table tty_table // 定义控制台终端tty表符号常数。
#define rs_table (64+tty_table) // 串行终端tty表。
#define mpty_table (128+tty_table) // 主伪终端tty表。
#define spty_table (192+tty_table) // 从伪终端tty表。
int fg_console = 0;
/*
* 下面是汇编程序中使用的缓冲队列结构地址表。通过修改这个表,
* 你可以实现虚拟控制台。
*/
// tty读写缓冲队列结构地址表。供rs_io.s程序使用,用于取得读写缓冲队列结构的地址。
struct tty_queue * table_list[]={
con_queues + 0, con_queues + 1, // 前台控制台读、写队列结构地址。
rs_queues + 0, rs_queues + 1, // 串行终端1读、写队列结构地址。
rs_queues + 3, rs_queues + 4 // 串行终端2读、写队列结构地址。
};
改变前台控制台。
// 将前台控制台设定为指定的虚拟控制台。
// 参数:new_console - 指定的新控制台号。
void change_console(unsigned int new_console)
{
// 如果参数指定的控制台已经在前台或者参数无效,则退出。否则设置当前前台控制台号,同
// 时更新table_list[]中的前台控制台读、写队列结构地址。最后更新当前前台控制台屏幕。
if (new_console == fg_console || new_console >= NR_CONSOLES)
return;
fg_console = new_console;
table_list[0] = con_queues + 0 + fg_console*3;
table_list[1] = con_queues + 1 + fg_console*3;
update_screen(); // kernel/chr_drv/console.c,936行。
}
如果队列缓冲区空则让进程进入可中断睡眠状态。
// 参数:queue - 指定队列的指针。
// 进程在取队列缓冲区中字符之前需要调用此函数加以验证。如果当前进程没有信号要处理,
// 并且指定的队列缓冲区空,则让进程进入可中断睡眠状态,并让队列的进程等待指针指向
// 该进程。
static void sleep_if_empty(struct tty_queue * queue)
{
cli();
while (!(current->signal & ~current->blocked) && EMPTY(queue))
interruptible_sleep_on(&queue->proc_list);
sti();
}
若队列缓冲区满则让进程进入可中断的睡眠状态。
// 参数:queue - 指定队列的指针。
// 进程在往队列缓冲区中写入字符之前需要调用此函数判断队列情况。
static void sleep_if_full(struct tty_queue * queue)
{
// 如果队列缓冲区不满则返回退出。否则若进程没有信号需要处理,并且队列缓冲区中空闲剩
// 余区长度 < 128,则让进程进入可中断睡眠状态,并让该队列的进程等待指针指向该进程。
if (!FULL(queue))
return;
cli();
while (!(current->signal & ~current->blocked) && LEFT(queue)<128)
interruptible_sleep_on(&queue->proc_list);
sti();
}
等待按键。
// 如果前台控制台读队列缓冲区空,则让进程进入可中断睡眠状态。
void wait_for_keypress(void)
{
sleep_if_empty(tty_table[fg_console].secondary);
}
复制成规范模式字符序列。
// 根据终端termios结构中设置的各种标志,将指定tty终端读队列缓冲区中的字符复制转换
// 成规范模式(熟模式)字符并存放在辅助队列(规范模式队列)中。
// 参数:tty - 指定终端的tty结构指针。
void copy_to_cooked(struct tty_struct * tty)
{
signed char c;
// 首先检查当前终端tty结构中缓冲队列指针是否有效。如果三个队列指针都是NULL,则说明
// 内核tty初始化函数有问题。
if (!(tty->read_q || tty->write_q || tty->secondary)) {
printk("copy_to_cooked: missing queues\n\r");
return;
}
// 否则我们根据终端termios结构中的输入和本地标志,对从tty读队列缓冲区中取出的每个
// 字符进行适当的处理,然后放入辅助队列secondary中。在下面循环体中,如果此时读队列
// 缓冲区已经取空或者辅助队列缓冲区已经放满字符,就退出循环体。否则程序就从读队列缓
// 冲区尾指针处取一字符,并把尾指针前移一个字符位置。然后根据该字符代码值进行处理。
// 另外,如果定义了_POSIX_VDISABLE(\0),那么在对字符处理过程中,若字符代码值等于
// _POSIX_VDISABLE的值时,表示禁止使用相应特殊控制字符的功能。
while (1) {
if (EMPTY(tty->read_q))
break;
if (FULL(tty->secondary))
break;
GETCH(tty->read_q,c);// 取一字符到c,并前移尾指针。
// 如果该字符是回车符CR(13),那么若回车转换行标志CRNL置位,则将字符转换为换行符
// NL(10)。否则如果忽略回车标志NOCR置位,则忽略该字符,继续处理其他字符。如果字
// 符是换行符NL(10),并且换行转回车标志NLCR置位,则将其转换为回车符CR(13)。
if (c==13) {
if (I_CRNL(tty))
c=10;
else if (I_NOCR(tty))
continue;
} else if (c==10 && I_NLCR(tty))
c=13;
// 如果大写转小写输入标志UCLC置位,则将该字符转换为小写字符。
if (I_UCLC(tty))
c=tolower(c);
// 如果本地模式标志集中规范模式标志CANON 已置位,则对读取的字符进行以下处理。 首先,
// 如果该字符是键盘终止控制字符KILL(^U),则对已输入的当前行执行删除处理。删除一行字
// 符的循环过程如是:如果 tty辅助队列不空,并且取出的辅助队列中最后一个字符不是换行
// 符NL(10),并且该字符不是文件结束字符(^D),则循环执行下列代码:
// 如果本地回显标志ECHO 置位,那么:若字符是控制字符(值 < 32),则往tty写队列中放
// 入擦除控制字符ERASE(^H)。然后再放入一个擦除字符ERASE,并且调用该tty写函数,把
// 写队列中的所有字符输出到终端屏幕上。 另外,因为控制字符在放入写队列时需要用2个字
// 节表示(例如^V),因此要求特别对控制字符多放入一个ERASE。最后将tty辅助队列头指针
// 后退1字节。另外,如果定义了_POSIX_VDISABLE(\0),那么在对字符处理过程中,若字符
// 代码值等于 _POSIX_VDISABLE的值时,表示禁止使用相应特殊控制字符的功能。
if (L_CANON(tty)) {
if ((KILL_CHAR(tty) != _POSIX_VDISABLE) &&
(c==KILL_CHAR(tty))) {
/* deal with killing the input line */
while(!(EMPTY(tty->secondary) ||
(c=LAST(tty->secondary))==10 ||
((EOF_CHAR(tty) != _POSIX_VDISABLE) &&
(c==EOF_CHAR(tty))))) {
if (L_ECHO(tty)) {// 若本地回显标志置位。
if (c<32)// 控制字符要删2字节。
PUTCH(127,tty->write_q);
PUTCH(127,tty->write_q);
tty->write(tty);
}
DEC(tty->secondary->head);
}
continue;// 继续读取读队列中字符进行处理。
}
// 如果该字符是删除控制字符ERASE(^H),那么:如果tty的辅助队列为空,或者其最后一个
// 字符是换行符NL(10),或者是文件结束符,则继续处理其他字符。如果本地回显标志ECHO置
// 位,那么:若字符是控制字符(值< 32),则往tty的写队列中放入擦除字符ERASE。再放入
// 一个擦除字符ERASE,并且调用该tty的写函数。最后将tty辅助队列头指针后退1字节,继
// 续处理其他字符。同样地,如果定义了_POSIX_VDISABLE(\0),那么在对字符处理过程忠,
// 若字符代码值等于 _POSIX_VDISABLE 的值时,表示禁止使用相应特殊控制字符的功能。
if ((ERASE_CHAR(tty) != _POSIX_VDISABLE) &&
(c==ERASE_CHAR(tty))) {
if (EMPTY(tty->secondary) ||
(c=LAST(tty->secondary))==10 ||
((EOF_CHAR(tty) != _POSIX_VDISABLE) &&
(c==EOF_CHAR(tty))))
continue;
if (L_ECHO(tty)) {// 若本地回显标志置位。
if (c<32)
PUTCH(127,tty->write_q);
PUTCH(127,tty->write_q);
tty->write(tty);
}
DEC(tty->secondary->head);
continue;
}
}
// 如果设置了IXON标志,则使终端停止/开始输出控制字符起作用。如果没有设置此标志,那
// 么停止和开始字符将被作为一般字符供进程读取。在这段代码中,如果读取的字符是停止字
// 符STOP(^S),则置tty 停止标志,让tty 暂停输出。同时丢弃该特殊控制字符(不放入
// 辅助队列中),并继续处理其他字符。如果字符是开始字符START(^Q),则复位tty停止
// 标志,恢复tty输出。同时丢弃该控制字符,并继续处理其他字符。
// 对于控制台来说,这里的tty->write()是console.c中的con_write()函数。因此控制台将
// 由于发现stopped=1而会立刻暂停在屏幕上显示新字符(chr_drv/console.c,第586行)。
// 对于伪终端也是由于设置了终端stopped标志而会暂停写操作(chr_drv/pty.c,第24行)。
// 对于串行终端,也应该在发送终端过程中根据终端stopped标志暂停发送,但本版未实现。
if (I_IXON(tty)) {
if ((STOP_CHAR(tty) != _POSIX_VDISABLE) &&
(c==STOP_CHAR(tty))) {
tty->stopped=1;
tty->write(tty);
continue;
}
if ((START_CHAR(tty) != _POSIX_VDISABLE) &&
(c==START_CHAR(tty))) {
tty->stopped=0;
tty->write(tty);
continue;
}
}
// 若输入模式标志集中ISIG标志置位,表示终端键盘可以产生信号,则在收到控制字符INTR、
// QUIT、SUSP 或 DSUSP 时,需要为进程产生相应的信号。 如果该字符是键盘中断符(^C),
// 则向当前进程之进程组中所有进程发送键盘中断信号SIGINT,并继续处理下一字符。 如果该
// 字符是退出符(^\),则向当前进程之进程组中所有进程发送键盘退出信号SIGQUIT,并继续
// 处理下一字符。如果字符是暂停符(^Z),则向当前进程发送暂停信号SIGTSTP。同样,若定
// 义了_POSIX_VDISABLE(\0),那么在对字符处理过程忠,若字符代码值等于_POSIX_VDISABLE
// 的值时,表示禁止使用相应特殊控制字符的功能。以下不再啰嗦了 :-)
if (L_ISIG(tty)) {
if ((INTR_CHAR(tty) != _POSIX_VDISABLE) &&
(c==INTR_CHAR(tty))) {
kill_pg(tty->pgrp, SIGINT, 1);
continue;
}
if ((QUIT_CHAR(tty) != _POSIX_VDISABLE) &&
(c==QUIT_CHAR(tty))) {
kill_pg(tty->pgrp, SIGQUIT, 1);
continue;
}
if ((SUSPEND_CHAR(tty) != _POSIX_VDISABLE) &&
(c==SUSPEND_CHAR(tty))) {
if (!is_orphaned_pgrp(tty->pgrp))
kill_pg(tty->pgrp, SIGTSTP, 1);
continue;
}
}
// 如果该字符是换行符NL(10),或者是文件结束符EOF(4,^D),表示一行字符已处理完,
// 则把辅助缓冲队列中当前含有字符行数值secondary.data增1。如果在函数tty_read()中取
// 走一行字符,该值即会被减1,参见315行。
if (c==10 || (EOF_CHAR(tty) != _POSIX_VDISABLE &&
c==EOF_CHAR(tty)))
tty->secondary->data++;
// 如果本地模式标志集中回显标志ECHO在置位状态,那么,如果字符是换行符NL(10),则将
// 换行符NL(10)和回车符CR(13)放入tty写队列缓冲区中;如果字符是控制字符(值<32)
// 并且回显控制字符标志ECHOCTL置位,则将字符'^'和字符 c+64 放入tty写队列中(也即会
// 显示^C、^H等);否则将该字符直接放入tty写缓冲队列中。最后调用该tty写操作函数。
if (L_ECHO(tty)) {
if (c==10) {
PUTCH(10,tty->write_q);
PUTCH(13,tty->write_q);
} else if (c<32) {
if (L_ECHOCTL(tty)) {
PUTCH('^',tty->write_q);
PUTCH(c+64,tty->write_q);
}
} else
PUTCH(c,tty->write_q);
tty->write(tty);
}
// 每一次循环末将处理过的字符放入辅助队列中。
PUTCH(c,tty->secondary);
}
// 最后在退出循环体后唤醒等待该辅助缓冲队列的进程(如果有的话)。
wake_up(&tty->secondary->proc_list);
}
/* 当需要发送信号SIGTTIN 或 SIGTTOU 到我们进程组中所有进程时就会调用该函数。
*
* 在进程使用默认信号处理句柄情况下,我们仅要求一个系统调用被重新启动,如果
* 有系统调用因本信号而被中断。这样做的原因是,如果一个作业正在捕获SIGTTIN
* 或 SIGTTOU信号,那么相应信号句柄并不会希望系统调用被盲目地重新启动。如果
* 没有其他方法把终端的pgrp复位到当前pgrp(例如可能由于在logout时控制终端
* 已被释放),那么我们并不希望在重新启动系统调用时掉入一个无限循环中,并且
* 总是产生 SIGTTIN 或 SIGTTOU 信号。默认的信号句柄会使得进程停止,因而可以
* 避免无限循环问题。这里假设可识别作业控制的父进程会在继续执行其子进程之前
* 把问题搞定。
*/
向使用终端的进程组中所有进程发送信号。
// 在后台进程组中的一个进程访问控制终端时,该函数用于向后台进程组中的所有进程发送
// SIGTTIN或SIGTTOU信号。无论后台进程组中的进程是否已经阻塞或忽略掉了这两个信号,
// 当前进程都将立刻退出读写操作而返回。
int tty_signal(int sig, struct tty_struct *tty)
{
// 我们不希望停止一个孤儿进程组中的进程(参见文件kernel/exit.c中第232行上的说明)。
// 因此如果当前进程组是孤儿进程组,就出错返回。否则就向当前进程组所有进程发送指定信
// 号sig。
if (is_orphaned_pgrp(current->pgrp))
return -EIO; /* don't stop an orphaned pgrp */
(void) kill_pg(current->pgrp,sig,1); / 发送信号sig。
// 如果这个信号被当前进程阻塞(屏蔽),或者被当前进程忽略掉,则出错返回。否则,如果
// 当前进程对信号sig设置了新的处理句柄,那么就返回我们可被中断的信息。否则就返回在
// 系统调用重新启动后可以继续执行的信息
if ((current->blocked & (1<<(sig-1))) ||
((int) current->sigaction[sig-1].sa_handler == 1))
return -EIO; /* Our signal will be ignored */
else if (current->sigaction[sig-1].sa_handler)
return -EINTR; /* We _will_ be interrupted :-) */
else
return -ERESTARTSYS; /* We _will_ be interrupted :-) */
/* (but restart after we continue) */
}
tty读函数。
// 从终端辅助缓冲队列中读取指定数量的字符,放到用户指定的缓冲区中。
// 参数:channel - 子设备号;buf – 用户缓冲区指针;nr - 欲读字节数。
// 返回已读字节数。
int tty_read(unsigned channel, char * buf, int nr)
{
struct tty_struct * tty;
struct tty_struct * other_tty = NULL;
char c, * b=buf;
int minimum,time;
// 首先判断参数有效性并取终端的tty结构指针。如果tty终端的三个缓冲队列指针都是NULL,
// 则返回EIO出错信息。如果tty终端是一个伪终端,则再取得另一个对应伪终端的tty结构
// other_tty。
if (channel > 255)
return -EIO;
tty = TTY_TABLE(channel);
if (!(tty->write_q || tty->read_q || tty->secondary))
return -EIO;
// 如果当前进程使用的是这里正在处理的tty终端,但该终端的进程组号却与当前进程组号不
// 同,表示当前进程是后台进程组中的一个进程,即进程不在前台。于是我们要停止当前进程
// 组的所有进程。因此这里就需要向当前进程组发送SIGTTIN信号,并返回等待成为前台进程
// 组后再执行读操作。
if ((current->tty == channel) && (tty->pgrp != current->pgrp))
return(tty_signal(SIGTTIN, tty));
// 如果当前终端是伪终端,那么对应的另一个伪终端就是other_tty。若这里tty是主伪终端,
// 那么other_tty就是对应的从伪终端,反之也然。
if (channel & 0x80)
other_tty = tty_table + (channel ^ 0x40);
// 然后根据 VTIME 和VMIN 对应的控制字符数组值设置读字符操作超时定时值time和最少需
// 要读取的字符个数minimum。在非规范模式下,这两个是超时定时值。VMIN表示为了满足读
// 操作而需要读取的最少字符个数。VTIME是一个1/10秒计数计时值。
time = 10L*tty->termios.c_cc[VTIME]; // 设置读操作超时定时值。
minimum = tty->termios.c_cc[VMIN]; // 最少需要读取的字符个数。
// 如果tty终端处于规范模式,则设置最小要读取字符数minimum 等于进程欲读字符数nr。同
// 时把进程读取nr字符的超时时间值设置为极大值(不会超时)。否则说明终端处于非规范模
// 式下,若此时设置了最少读取字符数minimum,则先临时设置进城读超时定时值为无限大,以
// 让进程先读取辅助队列中已有字符。如果读到的字符数不足 minimum 的话,后面代码会根据
// 指定的超时值time 来设置进程的读超时值 timeout,并会等待读取其余字符。参见328行。
// 若此时没有设置最少读取字符数minimum(为0),则将其设置为进程欲读字符数nr,并且如
// 果设置了超时定时值time的话,就把进程读字符超时定时值timeout设置为系统当前时间值
// + 指定的超时值time,同时复位time。 另外,如果以上设置的最少读取字符数minimum大
// 于进程欲读取的字符数nr,则让minimum=nr。即对于规范模式下的读取操作,它不受VTIME
// 和VMIN对应控制字符值的约束和控制,它们仅在非规范模式(生模式)操作中起作用。
if (L_CANON(tty)) {
minimum = nr;
current->timeout = 0xffffffff;
time = 0;
} else if (minimum)
current->timeout = 0xffffffff;
else {
minimum = nr;
if (time)
current->timeout = time + jiffies;
time = 0;
}
if (minimum>nr)
minimum = nr;// 最多读取要求的字符数。
// 现在我们开始从辅助队列中循环取出字符并放到用户缓冲区buf 中。当欲读的字节数大于0,
// 则执行以下循环操作。在循环过程中,如果当前终端是伪终端,那么我们就执行其对应的另
// 一个伪终端的写操作函数,让另一个伪终端把字符写入当前伪终端辅助队列缓冲区中。即让
// 另一终端把写队列缓冲区中字符复制到当前伪终端读队列缓冲区中,并经行规程函数转换后
// 放入当前伪终端辅助队列中。
while (nr>0) {
if (other_tty)
other_tty->write(other_tty);
// 如果tty辅助缓冲队列为空,或者设置了规范模式标志并且tty读队列缓冲区未满,并且辅
// 助队列中字符行数为0,那么,如果没有设置过进程读字符超时值(为0),或者当前进程
// 目前收到信号,就先退出循环体。否则如果本终端是一个从伪终端,并且其对应的主伪终端
// 已经挂断,那么我们也退出循环体。如果不是以上这两种情况,我们就让当前进程进入可中
// 断睡眠状态,返回后继续处理。由于规范模式时内核以行为单位为用户提供数据,因此在该
// 模式下辅助队列中必须起码有一行字符可供取用,即secondary.data起码是1才行。
cli();
if (EMPTY(tty->secondary) || (L_CANON(tty) &&
!FULL(tty->read_q) && !tty->secondary->data)) {
if (!current->timeout ||
(current->signal & ~current->blocked)) {
sti();
break;
}
if (IS_A_PTY_SLAVE(channel) && C_HUP(other_tty))
break;
interruptible_sleep_on(&tty->secondary->proc_list);
sti();
continue;
}
sti();
// 下面开始正式执行取字符操作。需读字符数nr依次递减,直到nr=0或者辅助缓冲队列为空。
// 在这个循环过程中,首先取辅助缓冲队列字符c,并且把缓冲队列尾指针tail向右移动一个
// 字符位置。如果所取字符是文件结束符(^D)或者是换行符NL(10),则把辅助缓冲队列中
// 含有字符行数值减1。 如果该字符是文件结束符(^D)并且规范模式标志成置位状态,则中
// 断本循环,否则说明现在还没有遇到文件结束符或者正处于原始(非规范)模式。在这种模
// 式中用户以字符流作为读取对象,也不识别其中的控制字符(如文件结束符)。于是将字符
// 直接放入用户数据缓冲区 buf中,并把欲读字符数减1。此时如果欲读字符数已为0则中断
// 循环。另外,如果终端处于规范模式并且读取的字符是换行符NL(10),则也退出循环。
// 除此之外,只要还没有取完欲读字符数nr并且辅助队列不空,就继续取队列中的字符。
do {
GETCH(tty->secondary,c);
if ((EOF_CHAR(tty) != _POSIX_VDISABLE &&
c==EOF_CHAR(tty)) || c==10)
tty->secondary->data--;
if ((EOF_CHAR(tty) != _POSIX_VDISABLE &&
c==EOF_CHAR(tty)) && L_CANON(tty))
break;
else {
put_fs_byte(c,b++);
if (!--nr)
break;
}
if (c==10 && L_CANON(tty))
break;
} while (nr>0 && !EMPTY(tty->secondary));
// 执行到此,那么如果 tty 终端处于规范模式下,说明我们可能读到了换行符或者遇到了文件
// 结束符。如果是处于非规范模式下,那么说明我们已经读取了nr个字符,或者辅助队列已经
// 被取空了。于是我们首先唤醒等待读队列的进程,然后看看是否设置过超时定时值time。如
// 果超时定时值time不为0,我们就要求等待一定的时间让其他进程可以把字符写入读队列中。
// 于是设置进程读超时定时值为系统当前时间jiffies + 读超时值time。当然,如果终端处于
// 规范模式,或者已经读取了nr个字符,我们就可以直接退出这个大循环了。
wake_up(&tty->read_q->proc_list);
if (time)
current->timeout = time+jiffies;
if (L_CANON(tty) || b-buf >= minimum)
break;
}
// 此时读取tty字符循环操作结束,因此复位进程的读取超时定时值timeout。如果此时当前进
// 程已收到信号并且还没有读取到任何字符,则以重新启动系统调用号返回。否则就返回已读取
// 的字符数(b-buf)。
current->timeout = 0;
if ((current->signal & ~current->blocked) && !(b-buf))
return -ERESTARTSYS;
return (b-buf);
}
tty写函数。
// 把用户缓冲区中的字符放入tty写队列缓冲区中。
// 参数:channel - 子设备号;buf - 缓冲区指针;nr - 写字节数。
// 返回已写字节数。
int tty_write(unsigned channel, char * buf, int nr)
{
static cr_flag=0;
struct tty_struct * tty;
char c, *b=buf;
// 首先判断参数有效性并取终端的tty结构指针。如果tty终端的三个缓冲队列指针都是NULL,
// 则返回EIO出错信息。
if (channel > 255)
return -EIO;
tty = TTY_TABLE(channel);
if (!(tty->write_q || tty->read_q || tty->secondary))
return -EIO;
// 如果若终端本地模式标志集中设置了TOSTOP,表示后台进程输出时需要发送信号SIGTTOU。
// 如果当前进程使用的是这里正在处理的tty终端,但该终端的进程组号却与当前进程组号不
// 同,即表示当前进程是后台进程组中的一个进程,即进程不在前台。于是我们要停止当前进
// 程组的所有进程。因此这里就需要向当前进程组发送SIGTTOU信号,并返回等待成为前台进
// 程组后再执行写操作。
if (L_TOSTOP(tty) &&
(current->tty == channel) && (tty->pgrp != current->pgrp))
return(tty_signal(SIGTTOU, tty));
// 现在我们开始从用户缓冲区buf中循环取出字符并放到写队列缓冲区中。当欲写字节数大于0,
// 则执行以下循环操作。在循环过程中,如果此时tty写队列已满,则当前进程进入可中断的睡
// 眠状态。如果当前进程有信号要处理,则退出循环体。
while (nr>0) {
sleep_if_full(tty->write_q);
if (current->signal & ~current->blocked)
break;
// 当要写的字符数nr还大于0并且tty写队列缓冲区不满,则循环执行以下操作。首先从用户
// 缓冲区中取1字节。如果终端输出模式标志集中的执行输出处理标志 OPOST 置位,则执行对
// 字符的后处理操作。
while (nr>0 && !FULL(tty->write_q)) {
c=get_fs_byte(b);
if (O_POST(tty)) {
// 如果该字符是回车符'\r'(CR,13)并且回车符转换行符标志OCRNL置位,则将该字符换成
// 换行符'\n'(NL,10);否则如果该字符是换行符'\n'(NL,10)并且换行转回车功能标志
// ONLRET置位的话,则将该字符换成回车符'\r'(CR,13)。
if (c=='\r' && O_CRNL(tty))
c='\n';
else if (c=='\n' && O_NLRET(tty))
c='\r';
// 如果该字符是换行符'\n' 并且回车标志cr_flag没有置位,但换行转回车-换行标志ONLCR
// 置位的话,则将cr_flag标志置位,并将一回车符放入写队列中。然后继续处理下一个字符。
// 如果小写转大写标志OLCUC置位的话,就将该字符转成大写字符。
if (c=='\n' && !cr_flag && O_NLCR(tty)) {
cr_flag = 1;
PUTCH(13,tty->write_q);
continue;
}
if (O_LCUC(tty))// 小写转成大写字符。
c=toupper(c);
}
// 接着把用户数据缓冲指针b前移1字节;欲写字节数减1字节;复位cr_flag标志,并将该
// 字节放入tty写队列中。
b++; nr--;
cr_flag = 0;
PUTCH(c,tty->write_q);
}
// 若要求的字符全部写完,或者写队列已满,则程序退出循环。此时会调用对应tty写函数,
// 把写队列缓冲区中的字符显示在控制台屏幕上,或者通过串行端口发送出去。如果当前处
// 理的tty是控制台终端,那么tty->write()调用的是con_write();如果tty是串行终端,
// 则tty->write()调用的是rs_write()函数。若还有字节要写,则等待写队列中字符取走。
// 所以这里调用调度程序,先去执行其他任务。
tty->write(tty);
if (nr>0)
schedule();
}
return (b-buf);
}
/*
* 呵,有时我真得很喜欢386。该子程序被从一个中断处理程序中
* 调用,并且即使在中断处理程序中睡眠也应该绝对没有问题(我
* 希望如此)。当然,如果有人证明我是错的,那么我将憎恨intel
* 一辈子J。但是我们必须小心,在调用该子程序之前需要恢复中断。
*
* 我不认为在通常环境下会处在这里睡眠,这样很好,因为任务睡眠
* 是完全任意的。
*/
tty中断处理调用函数 - 字符规范模式处理。
// 参数:tty - 指定的tty终端号。
// 将指定tty终端队列缓冲区中的字符复制或转换成规范(熟)模式字符并存放在辅助队列中。
// 该函数会在串口读字符中断(rs_io.s,109)和键盘中断(kerboard.S,69)中被调用。
void do_tty_interrupt(int tty)
{
copy_to_cooked(TTY_TABLE(tty));
}
字符设备初始化函数。空,为以后扩展做准备。
void chr_dev_init(void)
{
}
tty终端初始化函数。
// 初始化所有终端缓冲队列,初始化串口终端和控制台终端。
void tty_init(void)
{
int i;
// 首先初始化所有终端的缓冲队列结构,设置初值。对于串行终端的读/写缓冲队列,将它们的
// data字段设置为串行端口基地址值。串口1是0x3f8,串口2是0x2f8。然后先初步设置所有
// 终端的tty结构。其中特殊字符数组c_cc[]设置的初值定义在include/linux/tty.h文件中。
for (i=0 ; i < QUEUES ; i++)
tty_queues[i] = (struct tty_queue) {0,0,0,0,""};
rs_queues[0] = (struct tty_queue) {0x3f8,0,0,0,""};
rs_queues[1] = (struct tty_queue) {0x3f8,0,0,0,""};
rs_queues[3] = (struct tty_queue) {0x2f8,0,0,0,""};
rs_queues[4] = (struct tty_queue) {0x2f8,0,0,0,""};
for (i=0 ; i<256 ; i++) {
tty_table[i] = (struct tty_struct) {
{0, 0, 0, 0, 0, INIT_C_CC},
0, 0, 0, NULL, NULL, NULL, NULL
};
}
// 接着初始化控制台终端(console.c,834行)。把 con_init()放在这里,是因为我们需要根
// 据显示卡类型和显示内存容量来确定系统中虚拟控制台的数量NR_CONSOLES。该值被用于随后
// 的控制台tty 结构初始化循环中。对于控制台的tty 结构,425--430行是tty结构中包含的
// termios结构字段。其中输入模式标志集被初始化为ICRNL标志;输出模式标志被初始化为含
// 有后处理标志OPOST和把NL转换成CRNL的标志ONLCR;本地模式标志集被初始化含有IXON、
// ICANON、ECHO、ECHOCTL和ECHOKE标志;控制字符数组c_cc[]被设置含有初始值INIT_C_CC。
// 435行上初始化控制台终端tty结构中的读缓冲、写缓冲和辅助缓冲队列结构,它们分别指向
// tty 缓冲队列结构数组tty_table[]中的相应结构项。参见61--73行上的相关说明。
con_init();
for (i = 0 ; i<NR_CONSOLES ; i++) {
con_table[i] = (struct tty_struct) {
{ICRNL, /* CR转NL */
OPOST|ONLCR, /*NL转CRNL*/
0, // 控制模式标志集。
IXON | ISIG | ICANON | ECHO | ECHOCTL | ECHOKE, // 本地标志集。
0, /* console termio */ // 线路规程,0 -- TTY。
INIT_C_CC}, // 控制字符数组c_cc[]。
0, /* initial pgrp */ // 所属初始进程组pgrp。
0, /* initial session */ // 初始会话组session。
0, /* initial stopped */ // 初始停止标志stopped。
con_write, // 控制台写函数。
con_queues+0+i*3,con_queues+1+i*3,con_queues+2+i*3
};
}
// 然后初始化串行终端的tty结构各字段。450行初始化串行终端tty 结构中的读/写和辅助缓
// 冲队列结构,它们分别指向tty 缓冲队列结构数组tty_table[]中的相应结构项。参见61--
// 73行上的相关说明。
for (i = 0 ; i<NR_SERIALS ; i++) {
rs_table[i] = (struct tty_struct) {
{0, /* no translation */ // 输入模式标志集。0,无须转换。
0, /* no translation */ // 输出模式标志集。0,无须转换。
B2400 | CS8, // 控制模式标志集。2400bps,8位数据位。
0, // 本地模式标志集。
0, // 线路规程,0 -- TTY。
INIT_C_CC}, // 控制字符数组。
0, // 所属初始进程组。
0, // 初始会话组。
0, // 初始停止标志。
rs_write, // 串口终端写函数。
rs_queues+0+i*3,rs_queues+1+i*3,rs_queues+2+i*3 // 三个队列。
};
}
// 然后再初始化伪终端使用的tty结构。伪终端是配对使用的,即一个主(master)伪终端配
// 有一个从(slave)伪终端。因此对它们都要进行初始化设置。在循环中,我们首先初始化
// 每个主伪终端的tty结构,然后再初始化其对应的从伪终端的tty结构。
for (i = 0 ; i<NR_PTYS ; i++) {
mpty_table[i] = (struct tty_struct) {
{0, /* no translation */ // 输入模式标志集。0,无须转换。
0, /* no translation */ // 输出模式标志集。0,无须转换。
B9600 | CS8, // 控制模式标志集。9600bps,8位数据位。
0, // 本地模式标志集。
0, // 线路规程,0 -- TTY。
INIT_C_CC}, // 控制字符数组。
0, // 所属初始进程组。
0, // 所属初始会话组。
0, // 初始停止标志。
mpty_write, // 主伪终端写函数。
mpty_queues+0+i*3,mpty_queues+1+i*3,mpty_queues+2+i*3
};
spty_table[i] = (struct tty_struct) {
{0, /* no translation */ // 输入模式标志集。0,无须转换。
0, /* no translation */ // 输出模式标志集。0,无须转换。
B9600 | CS8, // 控制模式标志集。9600bps,8位数据位。
IXON | ISIG | ICANON, // 本地模式标志集。
0, // 线路规程,0 -- TTY。
INIT_C_CC}, // 控制字符数组。
0, // 所属初始进程组。
0, // 所属初始会话组。
0, // 初始停止标志。
spty_write, // 从伪终端写函数。
spty_queues+0+i*3,spty_queues+1+i*3,spty_queues+2+i*3
};
}
// 最后初始化串行中断处理程序和串行接口1和2(serial.c,37行),并显示系统含有的虚拟
// 控制台数NR_CONSOLES 和伪终端数NR_PTYS。
rs_init();
printk("%d virtual consoles\n\r",NR_CONSOLES);
printk("%d pty's\n\r",NR_PTYS);
}
10-6-3 控制字符VTIME、VMIN
在非规范模式下,这两个值是超时定时值和最小读取字符个数。MIN表示为了满足读操作,
需要读取的最少字符数。TIME是一个十分之一秒计数的超时计时值。
当这两个都设置的话,读操作将等待,直到至少读到一个字符,如果在超时之前收到MIN个字符,则读操作即被满足。如果在MIN个字符被收到之前就已超时,就将到此时已收到的字符返回给用户。
如果仅设置了MIN,那么在读取MIN个字符之前读操作将不返回。
如果仅设置了TIME,那么在读到至少一个字符或者定时超时后读操作将立刻返回。
如果两个都没有设置,则读操作将立刻返回,仅给出目前已读的字节数。
详细说明参见termios.h文件