人生は勉強ブログ

https://github.com/dooooooooinggggg

プロセスの概要 UNIX V6の実装を読んだ

研究室にて,UNIX V6のソースコードを読んでみる本を輪講した.

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

この輪講を通して,OS(オペレーティングシステム)のイメージ(どこでどのように動いているのか等)が以前に比べてより鮮明になったかなと感じている.

今回の記事では,OSの機能のうち,プロセスに関することをカーネルソースコードを意識しつつ書いていこうと思う.

なぜUNIX V6なのか

OSに関する本を読んでみたいという気持ちがあった.でもLinuxだと難しいし,本も分厚い,挫折しそうと,と危惧していたところ,ファカルティの方から,U NIX V6ならソースコード短い(全部で1万行)し,やってみれば,との助言をいただいたので,やることにした.

書籍の紹介にも,

UNIX V6は1975年にベル研究所からリリースされたOSですが、近年のOSにも通じる、OSの基本的なアイディアが詰まっており、 デバイスドライバも含めて約10,000行という、カーネルの全体を理解するのが難しくないボリュームに収まっています。

とあるので,やることにした.

全ての要素をここで詳細に書くと,それはもう本を買って読めばいいじゃんとなるので,なるべく抽象的に書けるように気をつける.

ちなみに,Linuxカーネルソースコードは144MBあるらしい.これは,オライリーの大型本にして,40000ページ分.「詳解Linuxカーネル」40冊分.

OSの主な機能

まずはOSが何をやっているか?であるが,大まかに分けると以下のようになる.(本の目次まま)

プロセス

プログラムの実行に関して

前提として,プログラムの実行に関して記述する.

通常のプログラムは,プログラマがソースを書いて, (それがコンパイル型言語であろうと,インタプリタ型言語であろうと,)どうにかして各プロセッサの命令に落とし込む.

例えばC言語の場合,ソースコードgccなどでコンパイルして実行ファイルを作り,実行する.

実行すると,実行ファイルはメモリ上に展開され,プロセスとして実行される.

実行中のプロセスの仮想アドレス空間をみてみると,その中にはテキストセグメントとデータセグメントが存在し,テキストセグメントの命令に沿ってプログラムが進んでいく.

テキストセグメントは通常,プロセスの仮想メモリ空間上の,最下位(0x0)に位置し,ReadOnlyである.

データセグメントにはデータ領域とスタック領域があり,データ領域はテキストセグメントのすぐ後のある位置からスタートし,最上位アドレスに向けて伸びていく.

スタック領域は,仮想アドレス空間上の最上位からスタートし,最下位アドレスに向けて伸びていく.

今回のアーキテクチャでは,データセグメントの下位アドレスに,PPDA(Per Process Data Area)というカーネルにしか見えない領域が物理的に確保されている.

カーネルの実体

カーネルも,捉え方によっては一つの巨大なプログラム,プロセスであると捉えることができる.

物理アドレスの最下位アドレスに,カーネルのテキストセグメントは存在からである.

この巨大なプログラムが,直接CPUなどのハvードウェアそ制御したり,プロセスの管理をしたりしている.

カーネル内部には,プロセスを開始する関数が書かれており,ユーザーがプログラムを実行しようとすると,この関数が呼ばれる.

プロセスのリストは,カーネル内のグローバル変数,proc[50] (procというのは,プロセスの情報が入っている構造体)として管理されており, 仮想アドレス空間を構築したりした後に,この配列に書き込むことで,プロセスの一覧を保持している.(こう書くと,カーネルも実行中のプログラムなんだなと考えることができる)

仮想アドレス空間

実行中のプログラムは,以上のようにの仮想アドレス空間にて実行されている.これを実現しているのは,MMU(`Memory Management Unit)という機構で,これについては後述する.

仮想アドレス空間という方式を採用することで,

  • 各プログラムが実際にどの物理アドレスに書き込んだり読み込んだりしているか意識する必要がない
  • 各プログラムが別のプログラムのメモリ領域をみることができない

というメリットがある.

カーネルは,プログラムを実行する際に,仮想アドレス空間を構築する,これはつまりMMUの値を設定し,proc構造体か,その子分みたいな存在のuser構造体にこの値を保存する.

この,「仮想アドレス空間を構築」という処理も,実際にみてみると,空いているメモリ空間をfor文(!)で探し見つけたらbreakして,というような実装になっている.

MMU

Memory Management Unitは,(この本の環境である)UNIX V6及びPDP-11/40という環境下においては,PAR(Page Address Register)とPDR(Page Description Register)という2つ1組のレジスタによって実現されている. PARは12bitのベースアドレスの情報を保持し,PDRは14bitの様々な情報を保持している.このPAR & PDRはカーネルモード用とユーザーモード用にそれぞれ8個,計16個用意されている. これは実行中のプロセスのみに使用されて,実行されていないプロセスにおいては,user構造体の所定の位置にこのレジスタの値が保存される.

簡単に言い換えると,仮想アドレスと物理アドレスを変換してくれる,ブラックボックスだと思えばいい.

システムコール

システムコールは,ユーザープログラム中で,特殊な権限をもつ命令を安全に実行できるように用意された,いわばOSが提供しているAPIのようなもの.

標準出力にprintする際にも,内部では,システムコールが発行されている.

システムコールがユーザープログラム中から呼ばれると,プロセッサのモードがカーネルモードに切り替わり,処理が終わると,制御がユーザープログラム側に戻ってくる.

基本的に重要な処理の場合が多いため,それをAPIとして隠蔽することで,安全性を高めている.

プロセスの切り替え

プロセスは,割り込みなどの処理により,カーネルによって,切り替わえられる..

実際に,プロセスの切り替えを実行している関数はswtch()という関数から始まる一連の処理だが,このとき,どのようにプロセスが切り替わるのか.また,その際に,実行中のプロセスの情報はどこに保持されるのか.

swtch()関数:https://minnie.tuhs.org//cgi-bin/utree.pl?file=V6/usr/sys/ken/slp.c

/*
 * This routine is called to reschedule the CPU.
 * if the calling process is not in RUN state,
 * arrangements for it to restart must have
 * been made elsewhere, usually by calling via sleep.
 */
swtch()
{
    static struct proc *p;
    register i, n;
    register struct proc *rp;

    if(p == NULL)
        p = &proc[0];
    /*
    * Remember stack of caller
    */
    savu(u.u_rsav);
    /*
    * Switch to scheduler's stack
    */
    retu(proc[0].p_addr);

loop:
    runrun = 0;
    rp = p;
    p = NULL;
    n = 128;
    /*
    * Search for highest-priority runnable process
    */
    i = NPROC;
    do {
        rp++;
        if(rp >= &proc[NPROC])
            rp = &proc[0];
        if(rp->p_stat==SRUN && (rp->p_flag&SLOAD)!=0) {
            if(rp->p_pri < n) {
                p = rp;
                n = rp->p_pri;
            }
        }
    } while(--i);
    /*
    * If no process is runnable, idle.
    */
    if(p == NULL) {
        p = rp;
        idle();
        goto loop;
    }
    rp = p;
    curpri = n;
    /*
    * Switch to stack of the new process and set up
    * his segmentation registers.
    */
    retu(rp->p_addr);
    sureg();
    /*
    * If the new process paused because it was
    * swapped out, set the stack level to the last call
    * to savu(u_ssav).  This means that the return
    * which is executed immediately after the call to aretu
    * actually returns from the last routine which did
    * the savu.
    *
    * You are not expected to understand this.
    */
    if(rp->p_flag&SSWAP) {
        rp->p_flag =& ~SSWAP;
        aretu(u.u_ssav);
    }
    /*
    * The value returned here has many subtle implications.
    * See the newproc comments.
    */
    return(1);
}

これを解決する仕組みがコンテキストスイッチである.コンテキストスイッチは,各レジスタが保持している値を,切り替えられるプロセスのuser構造体に退避させる.その逆も同じで,プロセスが再開されるとき,user構造体からレジスタの値を復帰し,プロセスが切り替えられる直前の状態を復元する.

レジスタには,プログラムカウンタなどの情報も含まれるため,退避と復元が可能になっている.これらの処理は,savu(),retu(),aretu()という関数に実装されている. savu()がuser構造体への保存,retu(),aretu()が,値の復元を担当する.

savu(),retu(),aretu():https://minnie.tuhs.org//cgi-bin/utree.pl?file=V6/usr/sys/conf/m40.s

_savu:
    bis $340,PS
    mov (sp)+,r1
    mov (sp),r0
    mov sp,(r0)+
    mov r5,(r0)+
    bic $340,PS
    jmp (r1)

_aretu:
    bis $340,PS
    mov (sp)+,r1
    mov (sp),r0
    br  1f

_retu:
    bis $340,PS
    mov (sp)+,r1
    mov (sp),KISA6
    mov $_u,r0

感想

自分の中で整理するためにまとめてみたが,正直本を読んでもらった方が早い気がする.

とてもいい本で,内容も重すぎない割に,得るものが非常に多い.

一個難点があるとすれば,古いプロセッサなので,実際に動かしたりできないという点. UNIX V6がx86で動くように調整されたxv6というOSもあるらしいので,時間がある時に触ってみようと思う.

参考:xv6 を読む