《ORANGE’S:一个操作系统的实现》读书笔记(二十四)进程间通信(二)

上一篇文章记录说明了宏内核和微内核,通过选择,我们最终选择的是微内核方式。那么选择微内核后,首要的任务就比较明显了,那就是实现一个进程间通信(IPC)机制。 在开始实现IPC之前,我们先来添加两个函数assert()和panic(),以便在运行过程中随时发现错误,从而减轻调试的难度。这篇文章就来记录这两个函数的实现。

assert()

assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序,也就是说,如果表达式 expression 的值为假(为0),那么它将首先打印出一条错误信息,然后终止函数运行,否则,什么都不做。。你或许早就开始使用这个函数,但之前你使用的都是现成的assert,只要包含一个头文件,就可以方便地使用。不过现如今什么都要自力更生了,我们需要自己写一个assert函数。

代码 include/const.h,assert。

#define ASSERT
#ifdef ASSERT
void assertion_failure(char *exp, char *file, char *base_file, int line);
#define assert(exp)  if (exp) ; \
                     else assertion_failure(#exp, __FILE__, __BASE_FILE__, __LINE__)
#else
#define assert(exp)
#endif

注意其中的__FILE__、__BASE_FILE__和__LINE__这三个宏,它们的意义如下:

  • __FILE__ 将被展开成当前输入的文件。在这里,它告诉我们哪个文件中产生了异常。
  • __BASE_FILE__ 可被认为是传递给编译器的那个文件名。比如你在m.c中包含了n.h,而n.h中的某一个assert函数失败了,则__FILE__为n.h,__BASE_FILE__为m.c。
  • __LINE__ 将被展开成当前的行号。

明白了这几个宏的含义,剩下的assertion_failure()这个函数就显得容易了,它的作用就是将错误发生的位置打印出来。

代码 lib/misc.c,这是新增的文件,assertion_failure。

PUBLIC void assertion_failure(char *exp, char *file, char *base_file, int line)
{
    printl("%c  assert(%s) failed: file: %s, base_file: %s, ln%d", MAG_CH_ASSERT, exp, file, base_file, line);

    /**
     * If assertion fails in a TASK, the system will halt before 
     * printl() returns. If it happens in a USER PROC, printl() will 
     * return like a comm routine and arrive here.
     * @see sys_printx()
     * 
     * We use a forever loop to prevent the proc from going on:
     */
    spin("assertion_failure()");

    /* should never arrive here */
    __asm__ __volatile__("ud2");
}

PUBLIC void spin(char * func_name)
{
    printl("\nspinning in %s ...\n", func_name);
    while (1) {}
}

我想你一定注意到了“printl(“%c  assert(%s) failed: file: %s, base_file: %s, ln%d”, MAG_CH_ASSERT, exp, file, base_file, line);”这条语句,其中使用了格式化打印字符串,但是我们之前只实现了’%x’,其它的都没有实现,所以,我们现在需要将该打印语句中使用的格式化字符全部实现。

代码 kernel/vsprintf.c,实现格式化打印。

/**
 * 进制转换函数
 * @param val 需要转换的数字
 * @param base 进制
 */
PRIVATE char* i2a(int val, int base, char ** ps)
{
    int m = val % base;
    int q = val / base;
    if (q) {
        i2a(q, base, ps);
    }
    *(*ps)++ = (m < 10) ? (m + '0') : (m - 10 + 'A');

    return *ps;
}

PUBLIC int vsprintf(char *buf, const char *fmt, va_list args)
{
    char* p;

    va_list p_next_arg = args;
    int m;

    char inner_buf[STR_DEFAULT_LEN];
    char cs;
    int align_nr;

    for (p = buf; *fmt; fmt++) {
        if (*fmt != '%') {
            *p++ = *fmt;
            continue;
        } else {        /* a format string begins */
            align_nr = 0;
        }

        fmt++;

        if (*fmt == '%') {
            *p++ = *fmt;
            continue;
        } else if (*fmt == '0') {
            cs = '0';
            fmt++;
        } else {
            cs = ' ';
        }

        while (((unsigned char)(*fmt) >= '0') && ((unsigned char)(*fmt) <= '9')) {
            align_nr *= 10;
            align_nr += *fmt - '0';
            fmt++;
        }

        char * q = inner_buf;
        memset(q, 0, sizeof(inner_buf));

        switch (*fmt) {
            case 'c':
                *q++ = *((char*)p_next_arg);
                p_next_arg += 4;
                break;
            case 'x':
                m = *((int*)p_next_arg);
                i2a(m, 16, &q);
                p_next_arg += 4;
                break;
            case 'd':
                m = *((int*)p_next_arg);
                if (m < 0) {
                    m = m * (-1);
                    *q++ = '-';
                }
                i2a(m, 10, &q);
                p_next_arg += 4;
                break;
            case 's':
                strcpy(q, (*((char**)p_next_arg)));
                q += strlen(*((char**)p_next_arg));
                p_next_arg += 4;
                break;
            default:
                break;
        }

        int k;
        for (k = 0; k < ((align_nr > strlen(inner_buf)) ? (align_nr - strlen(inner_buf)) : 0); k++) {
            *p++ = cs;
        }
        q = inner_buf;
        while (*q) {
            *p++ = *q++;
        }
    }

    *p = 0;

    return (p - buf);
}

MAG_CH_ASSERT的宏定义在const.h中,同时在该头文件中也添加了MAG_CH_PANIC的定义(panic函数会用到)。

/* magic chars used by `printx' */
#define MAG_CH_PANIC    '\002'
#define MAG_CH_ASSERT   '\003'

注意这里使用了一个小伎俩,那就是使用了一个改进后的打印函数printl(),它其实就是一个定义成printf的宏,不过这里的printf和我们之前完成的printf是不一样的,它将调用一个叫做printx的系统调用,并最终调用函数sys_printx(),它位于tty.c中。

代码 include/proto.h,printl。

#define printl printf

现在我们来修改printf()函数,让它调用printx()。

代码 kernel/printf.c,修改调用为printx。

int printf(const char *fmt, ...)
{
    int i;
    char buf[256];

    va_list arg = (va_list)((char*)(&fmt) + 4); /* 4是参数fmt所占堆栈中的大小 */
    i = vsprintf(buf, fmt, arg);
    buf[i] = 0;
    printx(buf);

    return i;
}

开始编写printx。代码 kernel/syscall.asm。

...
_NR_printx          equ 1
...
printx:
    mov eax, _NR_printx
    mov edx, [esp + 4]
    int INT_VECTOR_SYS_CALL
    ret

printx的参数个数与以前的系统调用相比有所增加,所以需要修改kernel.asm中的sys_call。额外需要注意的是,我们新加的参数是通过edx这个参数传递的,而save这个函数中也用到了寄存器dx,所以我们同时需要修改save。

代码 kernel/kernel.asm,修改 save 和 sys_call。

save:
    pushad      ; `.
    push    ds  ;  |
    push    es  ;  | 保存原寄存器值
    push    fs  ;  |
    push    gs  ; /

    ;; 注意,从这里开始,一直到 'mov esp, StackTop',中间坚决不能用 push/pop 指令,
    ;; 因为当前 esp 指向 proc_table 里的某个位置,push 会破坏掉进程表,导致灾难性后果!

    mov     esi, edx                        ; 保存 edx, 因为 edx 里保存了系统调用的参数
                                            ; (没用栈,而是用了另一个寄存器 esi)

    mov     dx, ss
    mov     ds, dx
    mov     es, dx
    mov     fs, dx

    mov     edx, esi                        ; 恢复 edx

    mov     esi, esp                        ; esi = 进程表起始位置

    inc     dword [k_reenter]               ; k_reenter++;
    cmp     dword [k_reenter], 0            ; if(k_reenter==0)
    jne     .1                              ; {
    mov     esp, StackTop                   ;   mov esp, StackTop <--切换到内核栈
    push    restart                         ;   push restart
    jmp     [esi + RETADR - P_STACKBASE]    ;   return;
.1:                                         ; } else { 已经在内核,不需要再切换
    push    restart_reenter                 ;   push restart_reenter
    jmp     [esi + RETADR - P_STACKBASE]    ;   return;
                                            ; }

sys_call:
    call    save

    sti
    push    esi

    push    dword [p_proc_ready]
    push    edx
    push    ecx
    push    ebx
    call    [sys_call_table + eax * 4]
    add     esp, 4 * 4

    pop     esi
    mov     [esi + EAXREG - P_STACKBASE], eax
    cli

    ret

接下来修改sys_call_table[]中的系统调用,将之前使用的sys_write修改为sys_printx。

代码 kernel/global.c,修改sys_call_table。

PUBLIC system_call sys_call_table[NR_SYS_CALL] = {sys_get_ticks, sys_printx};

现在我们来编写函数sys_printx()。

代码 kernel/tty.c,sys_printx。

PUBLIC int sys_printx(int _unused1, int _unused2, char *s, PROCESS *p_proc)
{
    const char *p;
    char ch;

    char reenter_err[] = "? k_reenter is incorrect for unknown reason";
    reenter_err[0] = MAG_CH_PANIC;

    /**
     * @note Code in both Ring 0 and Ring 1~3 may invoke printx().
     * If this happens in Ring 0, no linear-physical address mapping
     * is needed.
     * 
     * @attention The value of 'k_reenter' is tricky here. When 
     * -# printx() is called in Ring 0 
     *      - k_reenter > 0. When code in Ring 0 calls printx(),
     *       an 'interrupt re-enter' will occur (printx() generates
     *       a software interrupt). Thus 'k_reenter' will be increased 
     *       by 'kernel.asm::ave' and be greater than 0.
     * -# printx() is called in Ring 1~3
     *      - k_reenter == 0
     */
    if (k_reenter == 0) { /* printx() called in Ring<1~3> */
        p = va2la(proc2pid(p_proc), s);
    } else if (k_reenter > 0) { /* printx() called in Ring<0> */
        p = s ;
    } else { /* this should NOT happen */
        p = reenter_err;
    }

    /**
     * @note if assertion fails in any TASK, then system will be halted; 
     * if it fails in a USER PROC, it'll return like any normal syscall 
     * does.
     */
    if ((*p == MAG_CH_PANIC) || (*p == MAG_CH_ASSERT && p_proc_ready < &proc_table[NR_TASKS])) {
        disable_int();
        char *v = (char*)V_MEM_BASE;
        const char *q = p + 1; /* +1: skip the magic char */

        while (v < (char*)(V_MEM_BASE + V_MEM_SIZE)) {
            *v++ = *q++;
            *v++ = RED_CHAR;
            if (!*q) {
                while (((int)v - V_MEM_BASE) % (SCREEN_WIDTH * 16)) {
                    /* *v++ = ' ' */
                    v++;
                    *v++ = GRAY_CHAR;
                }
                q = p + 1;
            }
        }

        __asm__ __volatile__("hlt");
    }

    while ((ch = *p++) != 0) {
        if (ch == MAG_CH_PANIC || ch == MAG_CH_ASSERT) {
            continue; /* skip the magic char */
        }
        out_char(tty_table[p_proc->nr_tty].p_console, ch);
    }

    return 0;
}

sys_printx首先判断首字符是否为预先设定的“Magic Char”,如果是的话,则做响应的特殊处理。我们的assertion_failure()就使用了MAG_CH_ASSERT作为“Magic Char”。当sys_printx发现传入字符串的第一个字符是MAG_CH_ASSERT时,会同时判断调用系统调用的进程是系统进程(TASK)还是用户进程(USER PROC),如果是系统进程,则停止整个系统的运转,并将要打印的字符串打印在显存的各处;如果是用户进程,则打印之后像一个普通的printx调用一样返回,届时该用户进程会因为assertion_failure()中对函数spin()的调用而进入死循环。换言之,系统进程的assert失败会导致系统停转,用户进程的失败仅仅使自己停转。

sys_printx代码中使用的proc2pid是一个宏,定义在proc.h中。

代码 include/proc.h,proc2pid。

#define proc2pid(x) (x - proc_table)

va2la()用来由虚拟地址求线性地址,它用到了ldt_seg_linear()。ldt_seg_linear()函数的作用是,每个进程都有自己的LDT,位于进程表的中间,这个函数就是根据LDT中描述符的索引来求得描述符所指向的基地址。这两个函数的代码如下所示。

代码 kernel/proc.c,va2la()和ldt_seg_linear()。

/**
 * <Ring 0~1> Calculate the linear address of a certain segment of a given proc.
 * 
 * @param p     Whose (the proc ptr).
 * @param idx   Whick (one proc has more than one segments).
 * 
 * @return The required linear address.
 */
PUBLIC int ldt_seg_linear(PROCESS *p, int idx)
{
    DESCRIPTOR *d = &p->ldts[idx];
    return d->base_high << 24 | d->base_mid << 16 | d->base_low;
}

PUBLIC void* va2la(int pid, void* va)
{
    PROCESS *p = &proc_table[pid];

    u32 seg_base = ldt_seg_linear(p, INDEX_LDT_RW);
    u32 la = seg_base + (u32)va;

    if (pid < NR_TASKS + NR_PROCS) {
        assert(la == (u32)va);
    }

    return (void*)la;
}

到这里大家应该清楚了assert()函数的实现的方法了,我们不妨来试验一下,在系统进程TTY中添加一句“assert(0);”,运行,你将看到如下左图所示的画面。再在用户进程TestC中添加一句“asser(0);”,将看到如下右图所示的画面。需要注意的是,我们新添加了misc.c文件,不要忘记修改Makefile。

panic()

panic 函数是一种在程序运行中遇到非常严重的错误时,用于引发程序崩溃的函数;它会停止当前正在执行的程序,并输出错误信息。panic 函数通常用于表示程序运行时遇到了无法修复的错误。

panic()跟assert()类似,也用到了sys_printx()和“Magic Char”,不过它更简单一些。

代码 kernel/main.c,panic。

PUBLIC void panic(const char *fmt, ...)
{
    int i;
    char buf[256];

    /* 4 is the size of fmt in this stack */
    va_list arg = (va_list)((char*)&fmt + 4);

    i = vsprintf(buf, fmt, arg);

    printl("%c !!panic!! %s", MAG_CH_PANIC, buf);

    /* should never arrive here */
    __asm__ __volatile__("ud2");
}

由于 panic 只会用在系统任务所处的Ring1或Ring0,所以sys_printx()遇到MAG_CH_PANIC就直接叫停整个系统,因为我们使用panic的时候,必是发生了严重错误的时候。

我们同样可以在TTY中试验一下 panic 的效果,比如添加这么一行:

panic("in TTY");

运行,会看到如下图所示效果。

在我们接下来的代码中,很多地方用到了assert()和panic(),其实有些地方完全可以不用这两个函数,而是以返回值的形式向上层函数传递的,但使用assert()和panic()可以减少代码量,并在第一时间通知我们哪里出了问题,作为一个试验性的操作系统,这样做比使用某种方法来“消除”错误要好。

欢迎关注我的公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值