--------------------------------------------------------
完善 TTY 设备文件
--------------------------------------------------------
——TTY 是为进程 P 服务的,进程 P 没有请求的时候,或者以 0 长度为请求的时候,TTY 从键盘接收到的数据会被丢弃!丢弃的意思是,既不会往用户的缓冲区里面送,也不会输出到屏幕上!——这也是为什么你在 TTY0 一顿操作猛如虎,但是切换到 TTY1 之后输入 H 还是 H,因为它进行了丢弃而不显示!
这很重要,因为我们面临一个问题,就是当没有进程绑定到 TTY 的时候,那么我们能够在屏幕上输出字符并打印吗?我们阻止这种行为,我们要的效果是只要能在 TTY 上打印字符,那么按下回车之后必须假定有个进程要对屏幕上的字符串进行处理!因此,当没有进程为 TTY 的 tty->tty_left_cnt 赋值的时候,此时 tty->tty_left_cnt 将为 0,不会显示字符到 Console,就像 TTY 无效一样!
从我们的描述中,我们已经能够察觉,此时 TTY 的行为依赖进程的行为,因此进程将和 TTY 绑定在一起,进程在使用 TTY 设备的时候,必须提供一个缓冲区,然后设计好处理 console 上字符的程序!
·tty_do_write 函数
PRIVATE void tty_dev_write(TTY *tty)
{
while (tty->ibuf_cnt)
{
char ch = *(tty->ibuf_tail);
tty->ibuf_tail++;
if (tty->ibuf_tail == tty->ibuf + TTY_IN_BYTES)
tty->ibuf_tail = tty->ibuf;
tty->ibuf_cnt--;
if (tty->tty_left_cnt) /* 如果进程 P 不想从 TTY 接收任何字符的话,那么读到的字符既 */
{ /* 不会输出到屏幕,也不会写入 P 的缓冲区 */
if (ch >= ' ' && ch <= '~')
{
out_char(tty->console, ch);
// 字符先解析出来放到 tty 的缓冲区中
// 然后再放入进程的缓冲区中并打印在当前控制台上
void *p = tty->tty_req_buf + tty->tty_trans_cnt;
phys_copy(p, (void *)va2la(TASK_TTY, &ch), 1);
tty->tty_trans_cnt++; /* 已读数目 ++ */
tty->tty_left_cnt--; /* 需读数目 -- */
}
else if (ch == '\b' && tty->tty_trans_cnt)
{
out_char(tty->console, ch);
tty->tty_trans_cnt--; /* 解析出退格键有意思了 */
tty->tty_left_cnt++;
}
if (ch == '\n' || tty->tty_left_cnt == 0) /* 读够进程需求的字符数之后就唤醒 P 进程了 */
{
out_char(tty->console, '\n');
// 该让 tty 唤醒进程 P 了
MESSAGE msg;
msg.type = RESUME_PROC;
msg.PROC_NR = tty->tty_procnr;
msg.CNT = tty->tty_trans_cnt;
send_recv(SEND, tty->tty_caller, &msg);
tty->tty_left_cnt = 0;
}
}
}
}
·task_tty 函数
PUBLIC void task_tty()
{
TTY *tty;
MESSAGE msg;
init_keyboard();
for (tty = TTY_FIRST; tty < TTY_END; tty++)
init_tty(tty);
select_console(0);
while (1)
{
for (tty = TTY_FIRST; tty < TTY_END; tty++)
{
tty_dev_read(tty);
tty_dev_write(tty);
}
// 扫描码已经被 keyboard_handler 放入 kb_in 缓冲区了
// 没有人唤醒 task_tty ,那么是不会读取 kb_in 的扫描码并解析出字符的!
send_recv(RECEIVE, ANY, &msg);
int src = msg.source;
assert(src != TASK_TTY);
// 请求三个 tty 中的哪个 TTY 的数据
TTY *ptty = &tty_table[msg.DEVICE];
switch (msg.type)
{
case DEV_OPEN:
reset_msg(&msg);
msg.type = SYSCALL_RET; /* 除了进行了一次消息通讯之外,什么都不做 */
send_recv(SEND, src, &msg);
break;
case DEV_READ:
tty_do_read(ptty, &msg);
break;
case DEV_WRITE:
tty_do_write(ptty, &msg);
break;
case HARD_INT:
/**
* waked up by clock_handler -- a key was just pressed
* @see clock_handler() inform_int()
*/
key_pressed = 0;
continue;
default:
dump_msg("TTY::unknown msg", &msg);
break;
}
}
}
题外话:我个人感觉键盘这里险象环生啊,但或许因为机制配合得好,所以很多陷阱都避免了!
比如:
PRIVATE u8 get_byte_from_kb_buf()
{
u8 scan_code;
// 1
while (kb_in.count <= 0)
{
}
disable_int(); /* for synchronization */
scan_code = *(kb_in.p_tail);
kb_in.p_tail++;
if (kb_in.p_tail == kb_in.buf + KB_IN_BYTES)
kb_in.p_tail = kb_in.buf;
kb_in.count--;
enable_int(); /* for synchronization */
return scan_code;
}
严格来说,如果上面的 while 循环会被执行到,那么就会出现一个问题——文件系统阻塞,因为我们将阻塞在这里:
那么其后的接收文件系统的消息并立即唤醒文件系统的事情就做不了了,但比较有趣的是:
因此,保证了当没有键盘输入的时候 task_tty 可以正常循环而不会阻塞!因此,代码:
while (kb_in.count <= 0){}
根本就可以删掉,因为根本执行不到那里!
另一个问题:
如果你手速够快,可以在极短的时间内输入 hello 并按下回车,那么完事大吉,因为你已经将 P 需要的数据准备好并唤醒 P 了,之后的 send_recv 将准备接收 P 的下一次 read 消息,而 P 也会立即发起 read 请求!但是,如果你不够快呢(显然你快不过机器),那么在接收到一个字符 h 之后就 send_recv ... 此时从哪里 recv 呢(进程 P 还在那里因为 recv 阻塞着呢)?此时,task_tty 和 P 将永久阻塞!
解决的办法就是在按键的时候唤醒 task_tty!
·tty_do_read 函数
PRIVATE void tty_do_read(TTY *tty, MESSAGE *msg)
{
// 将 tty_dev_write 的缓冲区设置为此进程的
// 将可以从 kb_in 解析出字符送入显示器并送入此进程的缓冲区
tty->tty_caller = msg->source; /* who called, usually FS */
tty->tty_procnr = msg->PROC_NR; /* who wants the chars */
tty->tty_req_buf = va2la(tty->tty_procnr, msg->BUF); /* where the chars should be put */
tty->tty_left_cnt = msg->CNT; /* how many chars are requested */
tty->tty_trans_cnt = 0; /* how many chars have been transferred */
// 这里很重要,这里唤醒 FS,让 FS 可以继续接收消息
msg->type = SUSPEND_PROC;
msg->CNT = tty->tty_left_cnt;
send_recv(SEND, tty->tty_caller, msg);
}
——此函数有两个地方需要注意的,但都很重要(就是本节的主题)
1)
tty->tty_left_cnt = msg->CNT
这句代码使能 TTY 的输出功能,TTY 的解析功能一直都没有死去,只是需要设置 tty->tty_left_cnt 才能显示输出并拷贝到进程的缓冲区,看起来就像 TTY 活了一样,并且进程 P 开始工作了
2)
msg->type = SUSPEND_PROC;
msg->CNT = tty->tty_left_cnt;
send_recv(SEND, tty->tty_caller, msg);
这三句代码引出了 【进程 P】、【task_fs】、【task_tty】之间的工作机制:
a、进程 P 想读:
进程 P 发消息给 task_fs ,task_fs 发消息给 task_tty,task_tty 接收到 FS 的消息之后立马唤醒 FS,让 FS 可以正常工作,当 TTY 从用户接收到数据之后通知 FS,FS 唤醒进程 P!
b、进程 P 想写:
进程 P 发消息给 task_fs ,task_fs 发消息给 task_tty,TTY 直接将用户缓冲区中的数据显示到屏幕上,然后发消息给 task_fs,task_fs 发消息给进程 P(如果 task_tty 拷贝进程发来的数据很慢,文件系统可能会有适当的阻塞)!这是下面函数的作用:
·tty_do_write 函数
PRIVATE void tty_do_write(TTY *tty, MESSAGE *msg)
{
char buf[TTY_OUT_BUF_LEN];
char *p = (char *)va2la(msg->PROC_NR, msg->BUF);
int i = msg->CNT;
int j;
while (i)
{
// 以两个字符为单位进行接收并显示到屏幕上
int bytes = min(TTY_OUT_BUF_LEN, i);
phys_copy(va2la(TASK_TTY, buf), (void *)p, bytes);
for (j = 0; j < bytes; j++)
out_char(tty->console, buf[j]);
i -= bytes;
p += bytes;
}
msg->type = SYSCALL_RET;
send_recv(SEND, msg->source, msg);
}
·TestB 函数
void TestB()
{
char tty_name[] = "/dev_tty1";
int fd_stdin = open(tty_name, O_RDWR);
assert(fd_stdin == 0);
int fd_stdout = open(tty_name, O_RDWR);
assert(fd_stdout == 1);
char rdbuf[128];
while (1)
{
write(fd_stdout, "$ ", 2);
int r = read(fd_stdin, rdbuf, 70);
rdbuf[r] = 0;
if (strcmp(rdbuf, "hello") == 0)
{
write(fd_stdout, "hello world!\n", 13);
}
else
{
if (rdbuf[0])
{
write(fd_stdout, "{", 1);
write(fd_stdout, rdbuf, r);
write(fd_stdout, "}\n", 2);
}
}
}
assert(0); /* never arrive here */
}
运行
如果切换到 tty2,那么按键不会有输出,因为 tty2 的 tty_left_cnt 为 0,不会将字符显示到屏幕上!