6.828オペレーティングシステムlab 4実験報告:PartC

14038 ワード

プリエンプトマルチプロセス処理&プロセス間通信
lab 4の最後のステップとして、カーネルが協力しないプロセスで使用されるリソースをプリエンプトできるように変更し、プロセス間の通信を許可します.
Part I:クロック中断およびプリエンプトuser/spinテストを実行してみます.このテストでは、CPUリソースを取得するとデッドサイクルに入り、カーネルおよび親プロセスがCPUを再び取得できないサブプロセスを確立します.これは明らかにオペレーティングシステムが避ける必要があることです.カーネルが実行中のプロセスからCPUリソースを奪うことを可能にするには、ハードウェアクロックからの外部ハードウェア割り込みをサポートする必要があります.
Interrupt discipline
外部割り込みはIRQ(Interrupt Request)で表される.合計16種類のIRQがあり、picirq.cIRQ_OFFSET増加したオフセットをIDTにマッピングした.inc/trap.hにおいて、IRQ_OFFSETは32として定義される.従って、IDT[32]は、クロック割り込みの処理エントリアドレスを含む.LenovoLab 3の内容:
x 86のすべての異常は割り込みベクトル0~31で表すことができ、IDTの0~31項目に対応する.例えば、ページエラーは、割り込みベクトル14の異常を生じる.32より大きい割り込みベクトルはすべて割り込みを表します
xv 6に対して,JOSではカーネル状態で外部機器の割り込みを無効にする重要な簡略化に当たった.外部割り込みは、%eflagレジスタのFL_IFビット制御を使用する.この位置1の場合、割り込みがオンになります.私たちの簡略化のため、カーネルに入るときと離れるときだけこのビットを修正する必要があります.
割り込みが発生した場合に処理できるように、ユーザ状態でFL_IFを1に設定する必要がある.私たちはbootloaderの最初の命令cliで中断を閉じて、それから二度と開いたことがありません.
Exercise 13. Modify kern/trapentry.S and kern/trap.c to initialize the appropriate entries in the IDT and provide handlers for IRQs 0 through 15. Then modify the code in env_alloc() in kern/env.c to ensure that user environments are always run with interrupts enabled.
比較的簡単で、Lab 3のExercise 4と大同小異です.関連する定数定義はinc/trap.hで見出すことができる.kern/trapentry.Sに追加:
// IRQs
TRAPHANDLER(handler32, IRQ_OFFSET + IRQ_TIMER)
TRAPHANDLER(handler33, IRQ_OFFSET + IRQ_KBD)
TRAPHANDLER(handler36, IRQ_OFFSET + IRQ_SERIAL)
TRAPHANDLER(handler39, IRQ_OFFSET + IRQ_SPURIOUS)
TRAPHANDLER(handler46, IRQ_OFFSET + IRQ_IDE)
TRAPHANDLER(handler51, IRQ_OFFSET + IRQ_ERROR)
kern/trap.ctrap_init()に加える.
    // IRQs
    void handler32();
    void handler33();   
    void handler36();
    void handler39();
    void handler46();
    void handler51();
...
    // IRQs
    SETGATE(idt[IRQ_OFFSET + IRQ_TIMER], 0, GD_KT, handler32, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_KBD], 0, GD_KT, handler33, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL], 0, GD_KT, handler36, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, handler39, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_IDE], 0, GD_KT, handler46, 0);
    SETGATE(idt[IRQ_OFFSET + IRQ_ERROR], 0, GD_KT, handler51, 0);
kern/env.cenv_alloc()に加える.
    // Enable interrupts while in user mode.
    // LAB 4: Your code here.
    e->env_tf.tf_eflags |= FL_IF;

Handling Clock Interrupts user/spinプログラムでは,サブプロセスがオンになるとデッドサイクルに陥り,その後kernelは制御権を獲得できなくなる.ハードウェアを周期的にクロックを中断させ、kernelに制御権を強制的に渡すことで、他のプロセスに切り替えることができます.
Exercise 14. Modify the kernel's trap_dispatch() function so that it calls sched_yield() to find and run a different environment whenever a clock interrupt takes place.
この練習自体は非常に簡単ですが、kern/trap.ctrap()関数ではこの断言はできません.
    assert(!(read_eflags() & FL_IF));

この問題は非常に調べにくく、2日間の時間を無駄にした.最終的にネット上でコードを多角的に比較したところ,実はLab 3のExercise 4に残された問題であることが分かった.前の練習には影響しませんが、ここでは露出しています.実際にはSETGATEというマクロの理解が足りないためである.当時、私は注釈の理解に基づいて、SETGATEの2番目のパラメータを1に書きました.主に注釈中のistrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.によって誤導される.
SETGATE(idt[T_PGFLT], 1, GD_KT, handler14, 0);

しかしながら、SETGATEの注釈によれば、1とすると処理中断開始時にFL_IFビットを1にリセットし、0にするとFL_を保持するIFビットは変わらない.ここの需要によって、明らかに0を置くべきだ.
// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
    //   see section 9.6.1.3 of the i386 reference: "The difference between
    //   an interrupt gate and a trap gate is in the effect on IF (the
    //   interrupt-enable flag). An interrupt that vectors through an
    //   interrupt gate resets IF, thereby preventing other interrupts from
    //   interfering with the current interrupt handler. A subsequent IRET
    //   instruction restores IF to the value in the EFLAGS image on the
    //   stack. An interrupt through a trap gate does not change IF."
// - sel: Code segment selector for interrupt/trap handler
// - off: Offset in code segment for interrupt/trap handler
// - dpl: Descriptor Privilege Level -
//    the privilege level required for software to invoke
//    this interrupt/trap gate explicitly using an int instruction.
#define SETGATE(gate, istrap, sel, off, dpl)            \
{                               \
    (gate).gd_off_15_0 = (uint32_t) (off) & 0xffff;     \
    (gate).gd_sel = (sel);                  \
    (gate).gd_args = 0;                 \
    (gate).gd_rsv1 = 0;                 \
    (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \
    (gate).gd_s = 0;                    \
    (gate).gd_dpl = (dpl);                  \
    (gate).gd_p = 1;                    \
    (gate).gd_off_31_16 = (uint32_t) (off) >> 16;       \
}

この最大の穴が解決したら、後ろは簡単です.trap_dispatch()にクロック割り込みのブランチを直接追加すればよい.
    // Handle clock interrupts. Don't forget to acknowledge the
    // interrupt using lapic_eoi() before calling the scheduler!
    // LAB 4: Your code here.
    if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
        lapic_eoi();
        sched_yield();
        return;
    }

まとめると、本当に穴があいている.異常処理の内容が多すぎて、戦線が長すぎます.間違いがあってとても探しにくい.
Part II:プロセス間通信(IPC)
以前のLabでは、オペレーティングシステムがどのように各プロセスを隔離しているのか、どのようにプログラムが1台の機械を独占しているように感じているのかについて話していました.オペレーティングシステムのもう一つの重要な機能は、プロセス間の相互通信を許可することです.
IPC in JOS
2つのシステム呼び出し:sys_ipc_recvおよびsys_ipc_try_sendを実装し、通信をサポートするために2つのライブラリ関数、ipc_recvおよびipc_sendにカプセル化します.実際には、プロセス間で送信される情報は、2つの部分からなり、1つのint32_t、1つのページマッピング(オプション)である.
メッセージの送信と受信
プロセスは、sys_ipc_recvを使用してメッセージを受信する.このシステム呼び出しは、メッセージが受信されるまでプログラムを停止し、CPUリソースを譲ります.この時期、親子プロセスに限らず、いずれのプロセスも彼に情報を送信することができる.メッセージを送信するために、プロセスはsys_ipc_try_sendを呼び出し、受信者のプロセスidおよび送信する値をパラメータとする.受信者がsys_ipc_recvを呼び出した場合、メッセージは正常に送信され、0が返される.そうでない場合、E_IPC_NOT_RECVは、ターゲットプロセスがメッセージを受信していないことを示す.ipc_sendライブラリ関数は、成功するまでsys_ipc_try_sendを繰り返し実行します.
ページの転送
プロセスがsys_ipc_recvを呼び出し、仮想アドレスdstva(ユーザ空間に存在する必要がある)を提供すると、プロセスは、ページマッピングを受信することを望んでいることを示す.送信者がページを送信すると、そのページは受信者のdstvaにマッピングされる.同時に、dstvaに位置していたページマッピングが上書きされます.
プロセスがsys_ipc_try_sendを呼び出し、仮想アドレスsrcva(ユーザ空間にある必要がある)を提供すると、送信者はsrcvaにあるページを受信者に送信したいことを示し、権限はpermに設定される.
成功したIPCの後、送信者と受信者は物理ページを共有します.
Exercise 15. Implement sys_ipc_recv and sys_ipc_try_send in kern/syscall.c. Read the comments on both before implementing them, since they have to work together. When you call envid2env in these routines, you should set the checkperm flag to 0, meaning that any environment is allowed to send IPC messages to any other environment, and the kernel does no special permission checking other than verifying that the target envid is valid. Then implement the ipc_recv and ipc_send functions in lib/ipc.c .
まず、inc/env.hをよく読んで、メッセージを伝達するためのデータ構造を理解する必要があります.
    // Lab 4 IPC
    bool env_ipc_recving;       // Env is blocked receiving
    void *env_ipc_dstva;        // VA at which to map received page
    uint32_t env_ipc_value;     // Data value sent to us
    envid_t env_ipc_from;       // envid of the sender
    int env_ipc_perm;       // Perm of page mapping received

次に注意しなければならないのは通信の流れです.
  • ipc_recvを呼び出し、Env構造体の関連field
  • を設定する.
  • ipc_sendを呼び出し、envidを介して受信プロセスを見つけ、Envでさっき設定したfieldを読み出して通信する.
  • が最後に戻るのは、実際にはipc_sendにreg_が設定されていることです.eaxは、呼び出しが終了し、カーネル状態が終了したときに返されます.

  • 過程は簡単そうに見えるが、実は穴が多い.まず呼び出しプロセスから始めます.この部分は簡単です.
    lib部分
    int32_t
    ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
    {
        // LAB 4: Your code here.
        // panic("ipc_recv not implemented");
    
        int r;
        if (pg != NULL) {
            r = sys_ipc_recv(pg);
        } else {
            r = sys_ipc_recv((void *) UTOP);
        }
        if (r < 0) {
            // failed
            if (from_env_store != NULL) *from_env_store = 0;
            if (perm_store != NULL) *perm_store = 0;
            return r;
        } else {
            if (from_env_store != NULL) *from_env_store = thisenv->env_ipc_from;
            if (perm_store != NULL) *perm_store = thisenv->env_ipc_perm;
            return thisenv->env_ipc_value;
        }
    }
    
    void
    ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
    {
        // LAB 4: Your code here.
        // panic("ipc_send not implemented");
    
        int r;
        if (pg == NULL) pg = (void *)UTOP;
        do {
            r = sys_ipc_try_send(to_env, val, pg, perm);
            if (r < 0 && r != -E_IPC_NOT_RECV) panic("ipc send failed: %e", r);
            sys_yield();
        } while (r != 0);
    }
    

    注意すべきことは多くない.主なtrickは1つであり,ページを共有する必要がなければパラメータとしての仮想アドレスをUTOPとし,このアドレスは次のシステム呼び出し実装では無視される.
    sys_ipc_recv()
    //   
    static int
    sys_ipc_recv(void *dstva)
    {
        // LAB 4: Your code here.
        // panic("sys_ipc_recv not implemented");
        
        // wrong, because when we don't want to share page, we set dstva=UTOP
        // but we can still pass value
        // if ( (uintptr_t) dstva >= UTOP) return -E_INVAL;
        if ((uintptr_t) dstva < UTOP && PGOFF(dstva) != 0) return -E_INVAL;
    
        envid_t envid = sys_getenvid();
        struct Env *e;
        // do not check permission
        if (envid2env(envid, &e, 0) < 0) return -E_BAD_ENV;
        
        e->env_ipc_recving = true;
        e->env_ipc_dstva = dstva;
        e->env_status = ENV_NOT_RUNNABLE;
        sys_yield();
    
        return 0;
    }
    

    この関数には大きな穴があり,すでに注釈されている.
  • パラメータとしての仮想アドレスがUTOPの上にある場合、エラー報告終了ではなく無視する必要があります.この場合、受信者は、ページを共有する必要がなく、値を受信するだけであることを示す(lib/ipc.cの処理を連想する).
  • sys_ipc_try_send()
    ここでのニーズはsys_page_map()と非常に似ており、sys_page_map()を呼び出すことで解決しようと試みてきました.これにより、大量の重複コードの作成を避けることができます.しかし、その最大の違いは、ipc通信が親子プロセス間に限定されるものではないことであり、sys_page_map()が最初に設計した役割はfork()であるため、ここで使用するには、envid2env()のパラメータを変更するためにいくつかの小さな変更が必要であることである.どのように変更しますか?まず、syscall()は現在5つのパラメータをサポートしているため、パラメータの変更幅が大きすぎる場合は、パラメータを追加することは考慮されません.さらに、前のfork()部分のコードを変更する必要があります.inc/mmu.hに使用可能な権限識別ビットがあることに気づきましたが、ここで借りてもいいですか?
    // The PTE_AVAIL bits aren't used by the kernel or interpreted by the
    // hardware, so user processes are allowed to set them arbitrarily.
    #define PTE_AVAIL   0xE00   // Available for software use
    

    そして、次のように実現する.
    static int
    sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
    {
        // LAB 4: Your code here.
        // panic("sys_ipc_try_send not implemented");
    
        envid_t src_envid = sys_getenvid(); 
        struct Env *dst_e;
        if (envid2env(envid, &dst_e, 0) < 0) {
            return -E_BAD_ENV;
        }
    
        if (dst_e->env_ipc_recving == false) 
            return -E_IPC_NOT_RECV;
        
        // pass the value
        dst_e->env_ipc_value = value;
        dst_e->env_ipc_perm = 0;
    
        // pass the page
        if ((uintptr_t)srcva < UTOP) {
            // customerize 0x200 as PTE_NO_CHECK
            unsigned tmp_perm = perm | 0x200;
            int r = sys_page_map(src_envid, srcva, envid, (void *)dst_e->env_ipc_dstva, tmp_perm);
            if (r < 0) return r;
            dst_e->env_ipc_perm = perm;
        }
    
        dst_e->env_ipc_from = src_envid;
        dst_e->env_status = ENV_RUNNABLE;
        // return from the syscall, set %eax
        dst_e->env_tf.tf_regs.reg_eax = 0;
        dst_e->env_ipc_recving = false;
        return 0;
    }
    

    同時に、sys_page_map()を修正します.
    static int
    sys_page_map(envid_t srcenvid, void *srcva,
             envid_t dstenvid, void *dstva, int perm)
    {
        // Hint: This function is a wrapper around page_lookup() and
        //   page_insert() from kern/pmap.c.
        //   Again, most of the new code you write should be to check the
        //   parameters for correctness.
        //   Use the third argument to page_lookup() to
        //   check the current permissions on the page.
    
        // LAB 4: Your code here.
        // panic("sys_page_map not implemented");
    
        if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) != 0) return -E_INVAL;
        if ((uintptr_t)dstva >= UTOP || PGOFF(dstva) != 0) return -E_INVAL;
        if ((perm & PTE_U) == 0 || (perm & PTE_P) == 0 || (perm & ~PTE_SYSCALL) != 0) return -E_INVAL;
        struct Env *src_e, *dst_e;
        // add for lab4 exercise 15 for ipc.
        // customerize 0x200 as PTE_NO_CHECK
        // and we assume 0x200 is not used elsewhere, so we restore perm here.
        bool check_perm = (perm & 0x200);
        perm &= (~0x200);
        if (envid2env(srcenvid, &src_e, !check_perm)<0 || envid2env(dstenvid, &dst_e, !check_perm)<0) return -E_BAD_ENV;
        pte_t *src_ptab;    
        struct PageInfo *pp = page_lookup(src_e->env_pgdir, srcva, &src_ptab);
        if ((*src_ptab & PTE_W) == 0 && (perm & PTE_W) == 1) return -E_INVAL;
        if (page_insert(dst_e->env_pgdir, pp, dstva, perm) < 0) return -E_NO_MEM;
        return 0;
    }
    

    また、システム呼び出しにもこの2つのブランチが追加されました.
    // syscall()
        case SYS_ipc_try_send:
            retVal = sys_ipc_try_send(a1, a2, (void *)a3, a4);
            break;
        case SYS_ipc_recv:
            retVal = sys_ipc_recv((void *)a1);
            break;
    

    これでmake gradeに成功した.多核の場合もmake CPUS=2 gradeが通過する.Lab 4はこれで終わります.