文件系统到现在为止,唯一还差的一个就是将TTY纳入文件系统的管辖,之前一直都是搁置状态,这篇文章将记录把TTY纳入文件系统。
终于到这一天了,我们会像Linux中的做法一样,将TTY纳入文件系统的管辖范围,完成统一大业。
让我们来简单地看一下TTY的现状,在输入输出系统篇章中的wirte()系统调用已经被废弃,取而代之的是进程间通信篇章中写的printx()。而且write()也有了新的用途,它用来对文件进行写操作。自从我们开始实现IPC和文件系统,几乎所有在屏幕上打印字符的操作都是用printx()实现的。同时我们也很久没有在控制台上输入过什么字符了——控制台除了回显一下之外再无作为。
所以如今的TTY如同一座几乎废弃的工厂,一片萧条。不过这种情况马上要改变了,我们马上要做的就是将它跟文件系统连接起来。
我们不妨先来想像一下TTY和文件系统连接后的情形。从用户角度来看,读写TTY和读写普通文件将不会有差别,都是通过调用read()和write()来实现。普通文件和特殊文件的分辨是由文件系统来完成的,而且我们在实现文件系统时已经编写了判断文件属性的代码了,并将字符设备特殊文件交给其驱动程序——TTY——来处理。
写入TTY跟写入普通文件是很类似的,不同之处只是在于TTY不需要进行任何端口操作,只是写入显存就可以了。而对于读出操作,TTY和普通文件则有着很大不同。TTY收到进程读请求的消息之后并不能马上返回数据,因为此时还没有任何输入呢,这时候需要用户输入字符,等输入结束之后,TTY才可以返回给进程。这个过程我们面临两个问题。一是怎样才算是“输入结束”,是每一次键盘敲击之后都算结束呢,还是等敲击回车才算结束,或者其它;二是是否要让文件系统等待输入过程结束。
对于第一个问题,前人已经给了我们答案。面向字符和面向行的需求是分别存在的,所以通常TTY的操作有两种模式,分别叫生模式(Raw Mode)和熟模式(Cooked Mode),有时也被称作非规范模式(Uncanonical Mode)和规范模式(Canonical Mode)。在生模式下,字符被原原本本地向上传递,而在熟模式下,TTY负责一定的解释工作,比如将Backspace解释为删除前一字符、将回车解释为输入完毕等。
完全实现两种模式是一件烦琐的事情,按照一贯的原则,我们这里只实现熟模式,也就是说,每次等回车出现,才算完成一次输入。
第二个问题的答案比较明显。由于键盘输入可能耗时很久,这段时间内我们当然不能让文件系统闲着,可能有一大堆的进程想要读写磁盘文件呢,所以TTY需要马上向文件系统发送消息以示返回。而文件系统则完全应该阻塞想要得到输入的进程,一直到输入结束。
这两个问题解决,我们就大致上了解了读写TTY的过程了。假设进程P要求读取TTY,它会发送消息给文件系统,文件系统将消息传递给TTY,TTY记下发出请求的进程号等信息之后立即返回,而文件系统这时并不对P解除阻塞,因为结果还未准备好。在接下来的过程中,文件系统像往常一样等待来自任何进程的请求。而TTY则会将键盘输入复制进P传入的内存地址,一旦遇到回车,TTY就告诉文件系统,P的请求已被满足,文件系统会解除对P的阻塞,于是整个读取工作结束。
写TTY的过程则很简单。P发消息给文件系统,文件系统传递给TTY,TTY收到消息立即将字符写入显存(保持P和FS进程的阻塞),完成后发消息给文件系统,文件系统再发消息给P,整个过程结束。
好了,现在我们就来修改TTY任务,代码如下所示。
代码 kernel/tty.c,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++) {
do {
tty_dev_read(tty);
tty_dev_write(tty);
} while (tty->ibuf_cnt);
}
send_recv(RECEIVE, ANY, &msg);
int src = msg.source;
assert(src != TASK_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;
}
}
}
...
/**
* Get chars from the keyboard buffer if the TTY::console is the 'current' console.
*
* @see keyboard_read()
*
* @param tty Ptr to TTY.
*/
PRIVATE void tty_dev_read(TTY * tty)
{
if (is_current_console(tty->console)) {
keyboard_read(tty);
}
}
/**
* Echo the char just pressed and transfer it to the waiting process.
*
* @param tty Ptr to a TTY struct.
*/
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) {
if (ch >= ' ' && ch <= '~') { /* printable */
out_char(tty->console, ch);
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) {
out_char(tty->console, '\n');
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;
}
}
}
}
/**
* Invoked when task TTY receives DEV_READ message.
*
* @note The routine will return immediately after setting some members of
* TTY struct, telling FS to suspend the proc who wants to read. The real
* transfer (tty buffer -> proc buffer) is not done here.
*
* @param tty From which TTY the caller proc wants to read.
* @param msg The MESSAGE just received.
*/
PRIVATE void tty_do_read(TTY* tty, MESSAGE* msg)
{
/* tell the tty: */
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 */
msg->type = SUSPEND_PROC;
msg->CNT = tty->tty_left_cnt;
send_recv(SEND, tty->tty_caller, msg);
}
/**
* Invoked when task TTY receives DEV_WRITE message.
*
* @param tty To which TTY the caller proc is bound.
* @param msg The MESSAGE.
*/
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);
}
跟输入输出系统篇章中的TTY任务相比,代码复杂了许多,原因在于现在的输入和输出都是面向进程的,而原先的TTY自顾自地接受输入并马上输出。
作为驱动程序,TTY接收并处理DEV_OPEN、DEV_READ和DEV_WRITE消息,这跟硬盘驱动是类似的。
DEV_OPEN基本上是个摆设,收到此消息我们直接返回,因为实在没有什么可OPEN的。DEV_READ和DEV_WRITE分别由对应的函数tty_do_read()和tty_do_write()来处理。从tty_do_read()的内容可以看出,结构体TTY的成员增加了若干,因为我们要保存发送读请求的进程的一些信息。新的TTY结构如下代码所示。
代码 include/tty.h,TTY结构。
typedef struct s_tty
{
u32 ibuf[TTY_IN_BYTES]; /* TTY input buffer */
u32* ibuf_head; /* the next free slot */
u32* ibuf_tail; /* the val to be processed by TTY */
int ibuf_cnt; /* how many */
int tty_caller;
int tty_procnr;
void* tty_req_buf;
int tty_left_cnt;
int tty_trans_cnt;
struct s_console * console;
}TTY;
tty_caller用来保存向TTY发送消息的进程(通常这个进程应该是FS)的进程号。tty_procnr用来保存请求数据的进程(下文中我们称它为进程P)的进程号。tty_req_buf保存进程P用来存放读入字符的缓冲区的线性地址。tty_left_cnt保存P想读入多少字符。tty_trans_cnt保存TTY已经向P传送了多少字符。
在tty_do_read()将结构体的这些成员赋值之后,马上向文件系统发送了一个SUSPEND_PROC消息,文件系统需要处理它。
代码 fs/main.c,文件系统处理SUSPEND_PROC消息。
/**
* <Ring 1> The main loop of TASK FS.
*/
PUBLIC void task_fs()
{
...
switch (fs_msg.type) {
case OPEN:
fs_msg.FD = do_open();
break;
case CLOSE:
fs_msg.RETVAL = do_close();
break;
case READ:
case WRITE:
fs_msg.CNT = do_rdwt();
break;
case UNLINK:
fs_msg.RETVAL = do_unlink();
break;
case RESUME_PROC:
src = fs_msg.PROC_NR;
break;
...
/* reply */
if (fs_msg.type != SUSPEND_PROC) {
fs_msg.type = SYSCALL_RET;
send_recv(SEND, src, &fs_msg);
}
}
}
文件系统在收到SUSPEND_PROC消息之后,并不像处理完READ或WRITE消息之后那样向进程P发送消息,而是不理不睬,径自开始下一个消息处理的循环,留下P独自等待。一直到TTY发送了RESUME_PROC消息,文件系统才会通知P,让它继续运行。这一过程我们已经说明过。
我们回头接着来看task_tty()。TTY处理完DEV_READ消息之后,来到第15行继续下一个循环,这里tty_dev_read()将从键盘缓冲区将字符读入,在接下来的tty_dev_write()中,这些字符将被送入进程P的缓冲区,直到读入一个回车(\n)或者已经传输了足够多的字符(由tty_left_cnt指定)。
注意,传输字符的过程并非在一个循环中就可以完成,因为人的手很慢而机器处理的速度很快。所以很可能发生的事情是,人通过键盘输入了一个字符“a”,被tty_dev_read()读出并由tty_dev_write()传给P,由于tty->ibuf_cnt这时变成0,所以tty_dev_write()返回,程序就来到了task_fs()继续继续接收消息了。而所有这些完成之后,人的手才慢吞吞地输入了下一个字符“b”。
那么问题随之出现了,如果这时没有人给TTY发消息,TTY就会在第22行永远等下去,即便我们敲了再多字符,TTY也不加理会了,这可不行。所以我们还是用TASK_HD中使用的方法,借助从不停歇的时钟来唤醒TTY:
代码 kernel/clock.c,时钟中断唤醒TTY。
PUBLIC void clock_handler(int irq)
{
...
if (key_pressed) {
inform_int(TASK_TTY);
}
...
}
每次时钟中断发生时,系统都判断key_pressed这一变量,它是在键盘中断程序中指定的:
代码 kernel/keyboard.c,键盘中断。
PUBLIC void keyboard_handler(int irq)
{
...
key_pressed = 1;
}
也就是说,每次键盘敲击都会通过key_pressed这一变量反映出来,随后的时钟中断根据它来唤醒TTY,这样TTY就又来到代码第15行进行下一次循环。
跟读取TTY的过程相比,写入的过程相对简单,一个tty_do_write()就解决了。
到这里TTY就改写完毕了,下面就来改造一个用户进程,让它使用新的机制来读写控制台,代码如下所示。
代码 kernel/main.c,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 */
}
原先进程跟TTY之间的对应关系由进程表中的nr_tty成员来实现的,如今我们只需要打开文件就可以了,所以nr_tty再无用处,我们可以删去。
好了,现在我们可以编译运行了,运行结果如下图所示。
怎么样,看着还不错吧?几乎可以被人误认为是个shell了。不过这个假shell太傻了,除了能应对一个“hello”之外,它干脆就是一个回音壁,输入什么即输出什么。
上面的代码已经可以工作了,但里面对write()的调用多少有点丑陋,输出一个“{”居然需要三个参数。是时候改造printf了,其实这个工作很简单,代码如下所示。
代码 kernel/printf.c,printf。
/**
* low level print
*
* @param fmt The format string
*
* @return The number of chars printed.
*/
PUBLIC int printl(const char *fmt, ...)
{
int i;
char buf[STR_DEFAULT_LEN];
va_list arg = (va_list)((char*)(&fmt) + 4); /* 4是参数fmt所占堆栈中的大小 */
i = vsprintf(buf, fmt, arg);
buf[i] = 0;
printx(buf);
return i;
}
/**
* The most famous one.
*
* @param fmt The format string
*
* @return The number of chars printed.
*/
PUBLIC int printf(const char *fmt, ...)
{
int i;
char buf[STR_DEFAULT_LEN];
va_list arg = (va_list)((char*)(&fmt) + 4); /* 4是参数fmt所占堆栈中的大小 */
i = vsprintf(buf, fmt, arg);
int c = write(1, buf, i);
assert(c == i);
return i;
}
我们把原来的printf()改做printl(),然后将printf()用write()系统调用重写了一遍。没有把直接使用系统调用的printl()删掉,这意味着之前的printl()调用没有变化。今后在内核以及任务中我们仍然会使用printl(),只有在用户进程中,我们才使用依赖于文件系统的printf()。现在我们就把TestB修改一下。
代码 kernel/main.c,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) {
printf("$ ");
int r = read(fd_stdin, rdbuf, 70);
rdbuf[r] = 0;
if (strcmp(rdbuf, "hello") == 0) {
printf("hello world!\n");
} else {
if (rdbuf[0]) {
printf("{%s}\n", rdbuf);
}
}
}
assert(0); /* never arrive here */
}
改造之后的函数好看多了,只是一定要注意,printf()默认调用它的进程已经打开了控制台文件,并且fd为1,在使用时一定要保证这一点成立。
欢迎关注我的公众号
?
公众号中对应文章附有当前文章代码下载说明。