proc.c static struct proc* allocproc(void)

トップページ
jupiteroak.hatenablog.com


proc.c
https://github.com/mit-pdos/xv6-public/blob/master/proc.c#L73

static struct proc* allocproc(void)
{
  struct proc *p;
  char *sp;

  acquire(&ptable.lock);

  for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
    if(p->state == UNUSED)
      goto found;

  release(&ptable.lock);
  return 0;

found:
  p->state = EMBRYO;
  p->pid = nextpid++;

  release(&ptable.lock);

  // Allocate kernel stack.
  if((p->kstack = kalloc()) == 0){
    p->state = UNUSED;
    return 0;
  }
  sp = p->kstack + KSTACKSIZE;

  // Leave room for trap frame.
  sp -= sizeof *p->tf;
  p->tf = (struct trapframe*)sp;

  // Set up new context to start executing at forkret,
  // which returns to trapret.
  sp -= 4;
  *(uint*)sp = (uint)trapret;

  sp -= sizeof *p->context;
  p->context = (struct context*)sp;
  memset(p->context, 0, sizeof *p->context);
  p->context->eip = (uint)forkret;

  return p;
}

allocproc関数は、プロセスの作成・実行おいて必要となる、プロセスディスクリプタの割り当てとカーネルスタックの設定を行います。

戻り値 struct proc *p
新規に作成したプロセスに対応しているプロセスディスクリプタのアドレスです。


処理の内容

ptableから使用されていないプロセスディスクリプタを探す

クリティカルセクションの入口を定める

acquire(&ptable.lock);

ptableを排他制御するために、acquire関数を呼び出してロックを取得し、クリティカルセクションの入口とします。

ptableからUNUSEDになっているプロセスディスクリプタを探す

for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
    if(p->state == UNUSED)
      goto found;

ptableが持つプロセスディスクリプタの配列proc[NPROC](#define NPROC 64)から、UNUSEDの状態(p->state == UNUSED)になっているプロセスディスクリプタを探します。p->state == UNUSEDが真となる場合→UNUSEDの状態になっているプロセスディスクリプタが見つかった場合は、foundラベルへジャンプします。

クリティカルセクションの出口を定める(UNUSEDになっているプロセスディスクリプタがみつからなかった場合)

release(&ptable.lock);
return 0;

UNUSEDになっているプロセスディスクリプタがみつからなかった場合は、release関数を呼び出してロックを解放し、クリティカルセクションの出口とします。最後に、戻り値を0にして処理を終了します。

クリティカルセクションの出口を定める(UNUSEDになっているプロセスディスクリプタがみつかった場合)

found:
  p->state = EMBRYO;
  p->pid = nextpid++;
  release(&ptable.lock);

UNUSEDになっているプロセスディスクリプタがみつかった場合は、これから作成するプロセスの状態を設定し(p->state = EMBRYO)、プロセスIDを割り当てます(p->pid = nextpid++)。その後、release関数を呼び出してロックを解放し、クリティカルセクションの出口とします。

カーネルスタックを設定する

カーネルスタックとして利用するメモリ領域を割り当てる

if((p->kstack = kalloc()) == 0){
    p->state = UNUSED;
    return 0;
  }

kalloc関数を呼び出して、カーネルスタックとして利用する4KBのメモリ領域を割り当てます。
4KBのメモリ領域の割り当てに成功した場合は、プロセスディスクリプタpのkstackメンバにカーネルスタックの上限値となるアドレスを保存します。
(p->kstack = kalloc()) == 0 が真となる場合→4KBのメモリ領域の割り当てに失敗した場合は、プロセスディスクリプタの状態をUNUSEDに設定し(p->state = UNUSED)、0を戻り値として処理を終了します。

カーネルスタックのボトムアドレスを取得する

sp = p->kstack + KSTACKSIZE;

カーネルスタックのボトムアドレスp->kstack + KSTACKSIZE(#define KSTACKSIZE 4096)を変数spに代入します。これ以降の処理では、変数spをスタックポインタのように扱っていきます。

カーネルスタックの状態を設定する

プロセッサがuserinit関数やfork関数で新規に作成されたプロセスを実行するとき、

1、プロセッサの実行するプロセスは、ユーザモードからカーネルモードへの移行が完了した状態
2、プロセッサのハードウェアコンテキストは、forkret関数の実行を開始する状態

になっている必要があります。そのために、

①ユーザモード時のハードウェアコンテキスト(ユーザモードからカーネルモードへ移行する時にカーネルスタックに退避される)
②forkret関数を実行開始する時のハードウェアコンテキスト(新規に作成されたプロセスは最初にforkret関数を実行する)

の2つのハードウェアコンテキストを予めカーネルスタックに用意しておきます。このようなカーネルスタックを設定することによって、userinit関数やfork関数で新規に作成されたプロセスは、scheduler関数やsched関数でプロセッサの実行対象として選ばれた際に、正常に起動することができるようになります。

①ユーザモード時のハードウェアコンテキストをカーネルスタックに予め用意しておく

ユーザモードからカーネルモードへの移行(システムコール、例外、割り込み)で最初に起こることは、ユーザモードのハードウェアコンテキストをカーネルスタックに退避させることです。まずは、ユーザモードのハードウェアコンテキストがカーネルスタックに退避されている状態を再現します(allocproc関数では、データ構造のみで具体的な値は設定しません)。


・ユーザモードのハードウェアコンテキストについて
カーネルスタックに退避されているユーザモードのハードウェアコンテキストはtrapframe構造体として表現されています。
x86.h

struct trapframe {
  // registers as pushed by pusha
  uint edi;
  uint esi;
  uint ebp;
  uint oesp;      // useless & ignored
  uint ebx;
  uint edx;
  uint ecx;
  uint eax;

  // rest of trap frame
  ushort gs;
  ushort padding1;
  ushort fs;
  ushort padding2;
  ushort es;
  ushort padding3;
  ushort ds;
  ushort padding4;
  uint trapno;

  // below here defined by x86 hardware
  uint err;
  uint eip;
  ushort cs;
  ushort padding5;
  uint eflags;

  // below here only when crossing rings, such as from user to kernel
  uint esp;
  ushort ss;
  ushort padding6;


・ユーザモードのハードウェアコンテキストがカーネルスタックに退避されている状態を再現する

sp -= sizeof *p->tf;
p->tf = (struct trapframe*)sp;

trapframe構造体のサイズ分(sizeof *p->tf)プッシュした時のカーネルスタックのトップアドレスを求め、そのトップアドレスをプロセスディスクリプタpのtfメンバ(trapframe構造体へのポインタ)に保存します。これにより、プロセスディスクリプタpに対応しているプロセスにおいて、このトップアドレスから始まるカーネルスタックの領域をtrapframe構造体として扱うことができます。

スタックの状態

アドレス 退避されている値
下位側
sp→ p->kstack + KSTACKSIZE
上位側

↓ sp -= sizeof *p->tf;
↓ p->tf = (struct trapframe*)sp;

アドレス 退避されている値
下位側
sp→  p->tf ediレジスタにセットされていた値
             esiレジスタにセットされていた値
            ebpレジスタにセットされていた値
...   ...          ...
             eflagsレジスタにセットされていた値
             espレジスタにセットされていた値
  p->kstack + KSTACKSIZE ssレジスタにセットされていた値
上位側


・trapretルーチンのアドレスをスタックにプッシュした状態をつくる

sp -= 4;
(uint*)sp = (uint)trapret;

4バイトサイズのアドレスをプッシュした時のカーネルスタックのトップアドレス(sp -= 4)を求めます。
求めたトップアドレスspと関節参照演算子を用いることで、trapframe構造体で表現されたハードウェアコンテキストの上にtrapretルーチンのアドレス(4バイトサイズ)を格納します。
本来の方法(割り込み、例外、 システムコールなど)でユーザモードからカーネルモードへ移行した場合は、割り込み・例外ハンドラ内(trapasm.S)でcall trapという命令(割り込み・例外サービスルーチンであるtrap関数を呼び出す命令)を実行するので、trapframe構造体で表現されたハードウェアコンテキストの上に call trapの次にある命令のアドレス(リターンアドレス)がプッシュされます。しかし今回のような場合は、本来の方法(割り込み、例外、 システムコールなど)と違って、call trapという命令(割り込み・例外サービスルーチンであるtrap関数を呼び出す命令)を実行していないため、代わりにtrapretルーチンのアドレスを格納することで辻褄を合わせています。

スタックの状態

アドレス 退避されている値
下位側
sp→  p->tf ediレジスタにセットされていた値
             esiレジスタにセットされていた値
            ebpレジスタにセットされていた値
...   ...          ...
             eflagsレジスタにセットされていた値
             espレジスタにセットされていた値
  p->kstack + KSTACKSIZE ssレジスタにセットされていた値
上位側

↓ sp -= 4;
↓ (uint*)sp = (uint)trapret;

アドレス 退避されている値
下位側
sp→           trapretルーチンのアドレス
  p->tf ediレジスタにセットされていた値
             esiレジスタにセットされていた値
            ebpレジスタにセットされていた値
...   ...          ...
             eflagsレジスタにセットされていた値
             espレジスタにセットされていた値
  p->kstack + KSTACKSIZE ssレジスタにセットされていた値
上位側
②forkret関数を実行開始する時のハードウェアコンテキストをカーネルスタックに予め用意しておく

forkret関数を実行開始する時のハードウェアコンテキストをカーネルスタックに予め用意しておきます。
このハードウェアコンテキストは、scheduler関数やsched関数内で呼び出されるswtch関数によって、プロセッサのハードウェアコンテキストとして復帰されます。


・forkret関数を実行開始する時のハードウェアコンテキスト
コンテキストスイッチの際に操作対象となるハーウェアコンテキスト(forkret関数を実行開始する時のハードウェアコンテキスト)は、context構造体として表現されています。
proc.h

struct context {
  uint edi;
  uint esi;
  uint ebx;
  uint ebp;
  uint eip;
};


・forkret関数を実行開始する時のハードウェアコンテキストがカーネルスタックに退避されている状態を再現する

sp -= sizeof *p->context;
p->context = (struct context*)sp;

context構造体のサイズ分(sizeof *p->context)プッシュした時のカーネルスタックのトップアドレスを求め、そのトップアドレスをプロセスディスクリプタpのcontextメンバ(context構造体へのポインタ)に保存します。これにより、プロセスディスクリプタpに対応しているプロセスにおいて、このトップアドレスから始まるカーネルスタックの領域をcontext構造体として扱うことができます。


・forkret関数を実行開始する時のハードウェアコンテキストを退避させたスタック領域を初期化する

memset(p->context, 0, sizeof *p->context);

memset関数を呼び出して、forkret関数を実行開始する時のハードウェアコンテキストを退避させたスタック領域(先頭アドレスp->context・サイズ(sizeof *p->contex)のメモリ領域)を0で初期化します。


・eipレジスタにセットされていた値が退避されるメモリ領域にforkret関数のアドレスを格納する

p->context->eip = (uint)forkret;

eipレジスタにセットされていた値が退避されるメモリ領域(p->context->eip)に、forkret関数のアドレスを格納します。
これにより、swtch関数がハードウェアコンテキストを復帰させた直後に、forkret関数から処理を開始することができます。

プロセスディスクリプタのアドレスを戻り値としてリターンする

return p;