《ORANGE’S:一个操作系统的实现》读书笔记(三十二)文件系统(七)

发布时间:2024年01月17日

文件系统到现在为止,唯一还差的一个就是将TTY纳入文件系统的管辖,之前一直都是搁置状态,这篇文章将记录把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”之外,它干脆就是一个回音壁,输入什么即输出什么。

改造 printf

上面的代码已经可以工作了,但里面对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,在使用时一定要保证这一点成立。

欢迎关注我的公众号


?

公众号中对应文章附有当前文章代码下载说明。

文章来源:https://blog.csdn.net/zorro_z/article/details/135641984
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。