チラシの裏

に書いとけなんて言うけど、今どきチラシなんて無いよね

大学院は行くべきなのか

大学院に進むかどうか悩んでいる人の参考になれば幸いです(時期が悪い)

 

 

私は大学院に進みました。これといった目標もなく、なんとなく推薦が貰えたので、行ける所まで行ってみるかという軽い気持ちで進学しました。

 

結果から言うと物凄く後悔しています。しかし、大学院に行かないほうが良かったかと聞かれれば、私は大学院に行った方が良かったと思っています。なぜなら大学院に行くことで、自分は所詮凡人であることを思い知る事になり、今後の人生において冒険をしなくなるからです。(もちろん、ここで活躍できた超人は博士課程に進むべきです。それだけの強さが貴方にはあります。しかし大勢の一般人は、自分が凡人であるということを思い知らされる場所だと思います)

 

人生において冒険の価値観は人それぞれだと思います。大学を中退して起業することで成功した人間は多くいます。しかし、成功した人間以上に、大量の失敗した人間が存在します。一度しか無い人生で冒険するしないは自由ですが、私は娯楽に溢れた現代で冒険する意味は薄いと思っています。ある程度の安定した収入と休息さえあれば、十分だと思っています。

 

大学院、というか研究の世界は青天井です。小中学校の成績は所詮クラス内での順位であり、高校・大学の成績も受験を考えれば同年代内での順位です。つまり天井があるのです。閉じた世界の中で順位が決められており、1位が存在する。そういった世界なのです。

 

しかし大学院に進学(というか研究室に配属)すると途端に世界が変わります。日本だけでなく海外を含め、大学の研究者だけでなく企業の人も含め、20代の若造もいればその道数十年の超超ベテランまでごっちゃ混ぜの世界に放り込まれます。ありえないでしょ?いきなりコレって。順番というものを考えて欲しいですね。

 

大学までどれだけ良い成績を取ってようが所詮井の中の蛙、まず数十年分の先行研究に往復ビンタされ、クソみたいな内容を学会発表しては研究室のボスレベルの人間にわんさか叩かれる。そして、自分は「凡人」だという事を知るのです。

 

先に言っておきますが、これは悪いことではないと私は思っています。実際に私は大学院に進学した事を後悔していますが、これは今が辛いからであって、自分が凡人だと教えてくれた事に関しては計り知れない価値があると思っています。

 

世の中には常に上を目指す人がいます。その人達を貶めるわけではないのですが、私は正直彼らの事が理解できなくなりました。大学に入るまでは私も常に上を目指すタイプの人間でした。クラスでは常に上位を目指していたし、大学も自分が行ける範囲で、可能な限り偏差値の高い場所を選びました。就職してもそうなると思っていましたし、新入社員から出世する気まんまんでした。

 

でも考えてみてください。「上」ってどこまで行くつもりなんですか?仮に私が大学院に行かず、自分が凡人であると気付かなかった場合、新入社員から出世を目指していたと思います。そして、同様に数年後、「上」の存在を知り自分が凡人であるとわからされる事になっていたと思います。

しかし、それに気付くまでに一体どれだけの時間がかかるのでしょうか?新入社員であれば序盤は同期と比較されることでしょう。これは所詮大学までの比較対象と大差なく、数年間は自分は優れた人間だと錯覚していることでしょう。そして次第に自分の評価と他人からの評価に溝が生じ、会社は自分の実力を正しく評価してくれないと嘆くようになります。働きながら転職活動を続け、転職先でも同じことを繰り返すでしょう。それを何回も繰り返した後に、自分が凡人であることを知るのです。そのときにはもう30代40代でしょう。

 

「上」を目指しているときは盲目になりがちです。山登りの際に辛くなったら頂上だけを見ろと言われますよね。それと同じで、上を見て登っている最中は気付かないのです。

 

人生は偉くなったら勝ち、という訳ではありません。偉くなろうとする人は、偉くなりたいのではなく、幸せになりたいから偉くなろうとするものです。

偉くなった瞬間はとても幸せなものです。私にもわかります。クラスで1位になった時、大学受験に合格した時、自分の成果が褒められた時、とても幸せでした。

でも考えてみてください、その幸せ、一瞬すぎじゃありませんか?その一瞬の幸せのために、どれだけの長い期間我慢しましたか?コスパ、おかしくないですか?

他人に更に評価してもらいたいならば、今と同じ立場ではもう評価されません。今の立場を失わず、更に上を目指さないと次の評価を貰えません。そして更に上を目指そうと思った時、今以上に労力と時間を払わないといけません。もはや呪いです。ふと自分が何のために生きているのか考える余裕があるうちは良いですが、そのうち考える余裕もなくなるでしょう。これを見て彼が幸せだと思うでしょうか?

 

自分が凡人であると自覚する前はひたすら「上」を目指しがちです。「上」には何も無いという事に気付くのが遅れれば、それは呪いのように一生上を目指すことになります。大学院は、人生において比較的早い時期にそれを気付かせてくれます。授業もなく、ひたすら考えるための時間があるので、それに気付くことができます。

 

なので、私は大学院に行ったこと自体は後悔していますが、「大学院に行くべきかどうか」と聞かれれば行くべきだと答えるでしょう。私は自分が凡人であるということを踏まえた上で、慎ましく生きていこうと思っています。

 

 

 

 

 

自作OSにforkを実装する

技術記事を書こうとして5億年ぶりにQiita開いたらパスワード忘れて入れなくなった。
ちょうど最近、勝手にユーザーの閲覧履歴公開して批判されてたし、なんか戻すのも面倒になったのでこっちに書きます。

今回の内容は自作OSにforkのシステムコールを追加する話です。
なんでこんな記事を書いてるのかと言うと、就活用に以前書いたOSのソースコード整理してたら大量のTODOが出てきて、 いざTODOに取り掛かろうと思ったらコードに全くコメントが無くて読み解くのに5000兆年かかったからです。 メモ(コメント)は大事、はっきりわかんだね。




さて、今回実装するのはforkシステムコールです。
親プロセスから子プロセスが生成され、親プロセスは返り値として子プロセスのPIDを、子プロセスは返り値として0を受け取ります。よくよく考えたら不思議ですよね、システムコールを出す側としては、なんで同じプログラムを走らせてるのに返り値が変わったりするんだっていう気分です。実際私も最近まで理解してませんでしたが...


とりあえずforkシステムコールの本体となる関数を見ていきましょう。

int sys_fork(void)
{
    struct proc *np;
    struct proc *curproc = user.procp;

    if ((np = palloc()) == 0)
        goto bad;
    if ((np->pgdir = copyuvm(curproc->pgdir, curproc->size)) == 0)
        goto bad;

    np->size  = curproc->size;
    np->pproc = curproc;
    *np->tf   = *curproc->tf;

    np->tf->eax = 0;

    for (int i = 0; i < NOFILE; i++) {
        if (curproc->ofile[i]) {
            np->ofile[i] = fdup(curproc->ofile[i]);
        }
    }
    np->cwd = idup(curproc->cwd);

    np->stat = RUN;

    return np->pid;

bad:
    if (np)
        pfree(np);
    return -1;
}

はい、記述量としてはこれだけなんですが、説明がないとなんのこっちゃですね。一つずつ解説していきましょう。

まず3行目のstruct procについて。これはプロセスを管理する構造体で、次のようになっています。(今回の解説に関係のないメンバ変数は省略しています)

enum procstat { UNUSED, SET, RUN, SLEEP, ZOMB, STOP };

struct proc {
    enum procstat stat;         /* process state */
    uint32_t pid;               /* unique process id */
    struct proc *pproc;         /* parent process */
    uint32_t size;              /* size of process */
    uint32_t *pgdir;            /* page directory */
    char *kstack;               /* kernel stack */
    struct trapframe *tf;       /* trap frame */
    struct context *context;    /* context switch */
    struct inode *cwd;          /* current directory */
    struct file *ofile[NOFILE]; /* open files */
} proc[NPROC];

それぞれのメンバ変数はまぁ...使われるときに解説しましょう(面倒になった)。OSではこの構造体の配列を管理しています。

さて、4行目ではuserという構造体が使われています。この構造体はOS上で"走っている"プロセスの情報を記録しています。 user.procpは現在CPUが割り当てられているプロセス構造体のポインタです。それをcurprocという変数に移しているわけですね。

どんどん行きましょう、と言いたいところですが、6行目ではpalloc()という関数を呼び出しているようです。 この関数はproc構造体の配列から未使用のエントリを取得して、初期化して返してくれます。

struct proc *palloc()
{
    struct proc *p = 0;
    char *sp;

    pushcli();

    for (int i = 0; i < NPROC; i++) {
        if (proc[i].stat == UNUSED) {
            p = &proc[i];
            memset(p, 0, sizeof(struct proc));
            break;
        }
    }

    if (p == 0 || (p->kstack = kalloc()) == 0) {
        popcli();
        return 0;
    }

    p->stat = SET;
    p->pid  = user.nextpid++;
    popcli();

    memset(p->kstack, 0, PAGESIZE);

    sp = p->kstack + STACKSIZE;
    sp -= sizeof(struct trapframe);
    p->tf = (struct trapframe *)sp;
    sp -= sizeof(struct context);
    p->context = (struct context *)sp;

    p->context->eip  = (uint32_t)trapret;
    p->tf->eflags    = FL_IF;
    p->tf->cs        = SEG_UCODE | DPL_USER;
    p->tf->ds        = SEG_UDATA | DPL_USER;
    p->tf->es        = p->tf->ds;
    p->tf->ss        = p->tf->ds;
    p->tf->eflags    = FL_IF;
    p->ofile[STDIN]  = fget(STDIN);
    p->ofile[STDOUT] = fget(STDOUT);

    return p;
}

面倒くさくなってきましたね...玉ねぎの皮のように剥いても剥いても中身が見えないですが、これも一つずつ説明していきましょう。ここからの話はx86アーキテクチャに深く関係してきます。

palloc()の6行目ではpushcli()という関数が、17, 23行目ではpopcli()という関数が使用されています。これはx86における割り込み禁止命令cliをネスト可能にした関数です。
このOSではタイマ割り込みによるプロセスのスケジューリングが行われるため、プロセス構造体の配列などカーネル全体で共有する変数にアクセスする際は、割り込み禁止状態にしてタイマ割り込みを防ぐ必要があります。この場合cli命令を出して割り込み禁止状態にした後、禁止区間が終わったら割り込み許可命令stiで割り込みを許可します。
しかしここで面倒なのがこのcli命令はネストができないという点。例えばある関数f1, f2があり、どちらとも割り込み禁止状態で処理したい部分があったとします。ここでf1が割り込み禁止中にf2を呼び出した場合、cli@f1(割り込み禁止)→cli@f2(無意味)→sti@f2(割り込み許可)→sti@f1(無意味)という流れになってしまい、f2からf1に返った直後からf1でも割り込み許可状態になってしまうのです。これを解決するためにカーネル内でネスト用の変数を保持しておき、cli, sti命令をセットで使う限り何回呼び出しても大丈夫なようにしています。

早速話がそれました。palloc()の8~14行目ではプロセス構造体配列から未使用なエントリを取得します。
palloc()の16行目で呼び出しているkalloc()関数は、現在使用されていない4KB空間の物理メモリアドレスを返してくれます。 このkalloc()によって確保された領域を、このプロセスがカーネル権限で動くときに使うスタック領域として使用します。 カーネル権限で動くときに使うスタックってなんだよ(困惑)っていう人のために、もう少し詳しく説明します。
プロセスがユーザー空間で実行されている場合のスタックは、プログラマにとってはお馴染みのあのスタックです。 では例えば何かしらのシステムコールを発行したとしましょう。このシステムコールを処理する際に使われるスタックはユーザー空間に存在する、先程まで使用していたスタックではありません。x86では権限の昇格が発生した際にスタックが自動で切り替わります。(もしかしたら切り替わらない方法もあるかもしれない。詳しくないです、申し訳無い)
つまりシステムコールやタイマ割り込みなどでユーザー空間からカーネル空間に切り替わる際、スタックも自動的に切り替わります。

この自動的なスタックの切り替えには、セグメントディスクリプタにTSS (Task-State Segment)をセットする必要があります。 セグメントについて話し出すと、もう明後日の方向に飛んでいくのでやめておきます。私もあんまり詳しく理解してないのでボロを出したくないですし。 というか私もわからないので詳しい人教えて下さい何でもしますから!
一応自分の理解でざっくり説明すると、権限の昇格が発生した際にはこのTSSに昇格前のレジスタが記録され、SS0がスタックセグメントレジスタに、ESP0がスタックポインタ(%esp)にセットされるという認識です。今回ユーザー空間は ring 11, カーネル空間は ring 00でやってます。SS0, ESP0が読み込まれるのはring 00を使用しているからであり、ring 01に権限が昇格する場合はSS1, ESP1が、ring 10に昇格する場合はSS2, ESP2が読み込まれると思います。

f:id:mic611:20200401185053p:plain
TSSは多分セグメントディスクリプタへのセットの仕方次第ではタスクスイッチにも使えるのかもしれない、というかそっちが本業なのかも。 詳しく知りたい方は Intel Software Developer’s Manual Vol. 3A 7-3 ~ 7-9を読んでください。というかSDMには全てが書いてあります(読み切れる量だとは言ってない)



えーと、何を話していたんでしたっけ...そうだ、palloc()の16行目の解説をこれで終えました。 palloc()の21行目ではプロセスの確保が完了したため、ステートをSET(セッティング中)に変更します。 ステートを変更したらもうこのプロセス構造体のエントリはpalloc()による確保の対象外となるため、割り込み禁止を解除して良いでしょう。 あ、そうだ。一応pidも設定しておきます。次に使うべきpidもuser構造体が管理しています。

palloc()の27~31行目ではカーネルスタック上にstruct trapframestruct contextの領域を確保しています。 構造体の解説はこの後しますが、図にするとこんな感じです。スタックはアドレスの大きい方からアドレスの小さい方に伸びていくことに注意しましょう。 ちなみにPAGESIZESTACKSIZEは両方とも4KB(0x1000)です。 f:id:mic611:20200401214544p:plain
ところで「アドレス上位」と「アドレス下位」という単語わかりにくい...わかりにくくない?この図のようにメモリ番地を図示する際はアドレスが小さい方が"上に来る"ことが多いのに、"アドレス下位"って呼ぶんですよ。これ非常にややこしくなるので自分はこれ以降も「アドレスの大きい方」「アドレスの小さい方」で統一してます。アドレス上位下位は使うとしても論文などのフォーマルな場面のみにしてほしいものですね。

さて、続くpalloc()の33~39行目では先程カーネルスタック上に確保したstruct trapframestruct contextに値をセットしています。ここで構造体の宣言を見ていきましょう。

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

struct trapframe { /* sizeof (trapframe) = 76 */
    // registers as pushed by pushal
    uint32_t edi;
    uint32_t esi;
    uint32_t ebp;
    uint32_t oesp; // useless & ignored
    uint32_t ebx;
    uint32_t edx;
    uint32_t ecx;
    uint32_t eax;

    // rest of trap frame
    uint16_t gs;
    uint16_t padding1;
    uint16_t fs;
    uint16_t padding2;
    uint16_t es;
    uint16_t padding3;
    uint16_t ds;
    uint16_t padding4;
    uint32_t trapno;

    // below here defined by x86 hardware
    uint32_t err;
    uint32_t eip;
    uint16_t cs;
    uint16_t padding5;
    uint32_t eflags;

    // below here only when crossing rings, such as from user to kernel
    uint32_t esp;
    uint16_t ss;
    uint16_t padding6;
};

このstruct contextstruct trapframeは基本的に割り込み時に使用される領域で、カーネル空間での処理やユーザー空間へ戻る際に使用します。 はい、というわけでここからx86の割り込みについてお話します。
先程、ユーザー空間からカーネル空間へ昇格する際にスタックが切り替わるという話をしました。ではプログラムカウンタはどうなっているのでしょうか。 x86ではIDT (Interrupt Descriptor Table)と呼ばれる領域に割り込みハンドラを登録することができます。CPUにはIDTR (Interrupt Descriptor Table Register)と呼ばれるレジスタがあり、 割り込みが発生した際、その割り込み番号に応じてIDT (テーブルと言われている様にディスクリプタの配列が並んでいる)に登録されたエントリを確認し、 エントリに記録された割り込みハンドラにジャンプします。 f:id:mic611:20200401232640p:plain

(補足)IDTではこの割り込みがユーザー空間から呼び出し可能かという設定(Descriptor Privilege Level)もできるので、システムコールの割り込みベクタのみring 11から受け付け、それ以外の割り込みはring 00のみから受け付けるみたいなのもできるはずです。間違ってたらごめんなさい。あと同じ割り込みでもtrapとinterruptという概念があって、trapはシステムコールなどで使われ割り込み処理時もEFLAGSのFL_IF(割り込み許可フラグ)がそのまま使われる一方でinterruptはIDEの割り込みやCPU内部割り込みなどFL_IF(割り込み許可フラグ)が降ろされた状態で割り込みハンドラにジャンプします。詳しくはIntel SDM Vol.3A 6-xやwikipediaのCPUの割り込みとか見てください。あと本当はセグメントディスクリプタと掛け合わせてアドレスが算出されたりするんですが、今回セグメントディスクリプタは全アドレスを1つとして実質使用していないので説明を省いてます。

えーっと、もう少し割り込みの説明が続きます。さっきのはx86における割り込み処理についてですが、これだけだとstruct trapframestruct contextの意味がわかりませんよね。 はい、これら構造体はOS内部で作り出すもの(?)なので、もうちょっと説明が続きます。我慢してね。
先程、IDTに割り込みハンドラを登録すると説明しました。では今回のOSではどのような関数が登録されているか説明しましょう。

.globl alltraps
.globl vector0
vector0:
    pushl $0
    pushl $0
    jmp alltraps
.globl vector1
vector1:
    pushl $0
    pushl $1
    jmp alltraps
.globl vector2
vector2:
    pushl $0
    pushl $2
    jmp alltraps
.globl vector3
    ...

はい、今回は割り込み番号Nに対してvectorNという関数(?)を用意し、割り込みが入ったらエラーコードと割り込み番号をスタックにプッシュして、全ての割り込みを同じハンドラで処理します。直接ハンドラとなる関数を登録するのもありだと思いますが、個人的にはこっちのほうが管理しやすいと感じますね。あと補足ですが、割り込みによってエラーコードをプッシュするものとしないものがある(8, 10~14, 17番の割り込みはエラーコードが付いてくる。by SDM Vol.3A 6-2)ので気を付けましょう。この実装ではエラーコードがない場合、0をプッシュしてスタックの状態を均一にしています。
さて、次は各割り込みハンドラがジャンプするalltrapsを見てみましょう。

.globl alltraps
alltraps:
    # Build trap frame.
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal
  
    # Set up data segments.
    movw $SEG_KDATA, %ax
    movw %ax, %ds
    movw %ax, %es

    # Call trap(tf), where tf=%esp
    pushl %esp
    call trap
    addl $4, %esp

# Return falls through to trapret...
.globl trapret
trapret:
    popal
    popl %gs
    popl %fs
    popl %es
    popl %ds
    addl $0x8, %esp  # trapno and errcode
    iret

はい、ここでstruct trapframeの形成をしています。struct trapframeの定義を確認すると
1. 権限の昇格が発生した際に積まれるレジスタ群 (esp, ss)
2. x86アーキテクチャの設計上必ず積まれることになるレジスタ群 (eflags, cs, eip err(一部))
3. 割り込みハンドラvectorNで積まれる割り込み番号 (trapno, err(2で積まれなかった場合))
4. alltrapsで積まれるセグメントレジスタ群 (ds, es, fs, gs) と汎用レジスタ (pushalで積まれるもの)
の順番になっていることが確認できます。

さて、alltrapsの14行目時点ではカーネルスタック上にstruct trapframeが形成されていることがわかりました。ちなみにこの構造体を示すポインタは現在のスタックポインタです。 そのためスタックポインタをプッシュしてtrap関数を呼び出すことで、C言語側ではtrap(struct trapframe *tf)として関数を作成することができます。
ここでtrap関数の説明を...してるともう永遠に終わらないので流石にやめておきましょうか。trap関数内では引数にとったstruct trapframeの値に応じて様々な処理をしています。





・・・これforkの解説記事だよね?
えーっと、とりあえずシステムコールの場合のtrapの処理だけ軽く解説します。 ユーザー空間からシステムコールを呼び出す際は、システムコール番号をeaxに入れてint T_SYSCALL(64)でソフトウェア割り込みを発生させています。

void trap(struct trapframe *tf)
{
    if (tf->trapno == T_SYSCALL) {
        memmove(tf, user.procp->tf, sizeof(struct trapframe));
        tf->eax = syscall(tf->eax);
        return;
    }
    ...

ここのmemmove()はシステムコール中にuser構造体からstruct trapframeが触れた方が便利だったから入れてます。 まぁそこは重要じゃなくて、大事なのはシステムコールの返り値をtf->eaxに入れている点です。 x86におけるeaxは特別なレジスタで、関数の返り値を入れる際に使用されることが多いです。(あと演算速度も早いらしい)

ここでtf->eaxにセットされた値はalltrapscall trap終了後、trapretにてポップされます。 trapretではalltrapsで積み上げたレジスタ群を全て復元し、iret命令で割り込みからのリターンを行っています。

自分がシステムコールという関数を呼び出したと思い込んでいる一般ユーザー空間くんですが、実際にはシステムコールの割り込み要求が出されていて、その割り込みハンドラからシステムコールカーネル内で実行されているわけですね。このカーネル内の処理が終わったらカーネルシステムコールの返り値をさりげなくeaxにセットすることで、何も知らないユーザー空間くんはあたかも関数を呼び出したかのように錯覚するわけです。ここが一番言いたかった。ここがforkを理解する上で一番重要です。


さて、palloc()に戻りましょう。 palloc()ではstruct contextのプログラムカウンタeiptrapretのアドレスを入れていますね。これはどういうことでしょうか?
答えは簡単で、本来であればstruct trapframealltrapsによって形成されるのですが、palloc()内部で新しく確保したプロセス構造体に、擬似的なstruct trapframeを生成して直接trapretにジャンプするとあら不思議、新しいプロセスの誕生です。


え、struct contextの説明が入ってないからなんでcontext->eiptrapret入れただけでtrapretにジャンプするかわからない?しょうがねぇな...
struct contextはプロセスを切り替える時のみに使用する構造体です。プロセスの切り替えはsleep()などにより自主的に行われる場合もあれば、タイマ割り込みなどで強制的に行われる場合もあります。どっちにしろカーネルではsched()関数が呼び出され、次に実行されるプロセスとの切り替えが行われます。

void sched()
{
    /* 次に実行するプロセスを決める */

    /* メモリ空間を切り替える */

    swtch(&oldproc->context, newproc->context);
}

.globl swtch
swtch:
    movl 4(%esp), %eax
    movl 8(%esp), %edx

    # Save old callee-saved registers
    pushl %ebp
    pushl %ebx
    pushl %esi
    pushl %edi

    # Switch stacks
    movl %esp, (%eax)
    movl %edx, %esp

    # Load new callee-saved registers
    popl %edi
    popl %esi
    popl %ebx
    popl %ebp
    ret

ちなみに今回カーネル空間は共有しているのでカーネルのコードを書く上では気にしなくていいです。そのため、プロセスの切り替えはカーネルスタックの切り替えだけで十分です。
なぜなら最後のret命令ではスタック上に保存されたsched()を呼び出した際のリターンアドレスへジャンプするからです。 そして先程述べたcontext->eipというのは、まさにこのリターンアドレスを示しています。
要するにpalloc()で確保したプロセスのcontext->eiptrapretを入れておけば、新しく確保されたこのプロセスがsched()により実行される際、swtch()関数から直接trapretにリターンし、擬似的に生成されたstruct trapframeからユーザー空間を構築してくれるというわけです。当然このstruct trapframeeipにアドレスを入れておけば、そのアドレスからユーザー空間が再開されるので、execvを実装する際にはここにELFのエントリーアドレスを入れたりするわけです。今回はforkなのでしませんが。

長かったですね...pallocの説明はこれで終わりです。一応最後にstdinstdoutのファイル構造体を開いていますが、これについてはまた今度説明しましょう。少なくとも私はもう書き疲れました。


多分もう戻って確認するのも面倒だと思うので、もう一度sys_fork()を貼っておきましょう

int sys_fork(void)
{
    struct proc *np;
    struct proc *curproc = user.procp;

    if ((np = palloc()) == 0)
        goto bad;
    if ((np->pgdir = copyuvm(curproc->pgdir, curproc->size)) == 0)
        goto bad;

    np->size  = curproc->size;
    np->pproc = curproc;
    *np->tf   = *curproc->tf;

    np->tf->eax = 0;

    for (int i = 0; i < NOFILE; i++) {
        if (curproc->ofile[i]) {
            np->ofile[i] = fdup(curproc->ofile[i]);
        }
    }
    np->cwd = idup(curproc->cwd);

    np->stat = RUN;

    return np->pid;

bad:
    if (np)
        pfree(np);
    return -1;
}

まだ6行目ってマジ?

copyuvm()については次回説明することにしましょう。次回はexecvを説明する予定です。次回も何もこの2つしか説明する予定はないのですが・・・
ここでは簡単に全てのメモリ空間をコピーする関数とだけ伝えておきます。要するに親プロセスのメモリ空間を新しく確保したプロセスに全部コピーするわけですね。

はい次行きましょう次。11, 12行目のサイズや親プロセスの登録は飛ばして良いでしょう。大事なのは13行目のstruct trapframeのコピーですね。 先程カーネル空間からユーザー空間に戻る際、struct trapframeから全てが復元されるという話をしました。これを完全にコピーするということは、メモリ空間だけでなく 現在のユーザー空間における実行アドレスやレジスタの値も完全にコピーするということです。まさにforkの真髄ですね。


15行目。ここが今回の記事で一番伝えたかった場所です。このsys_fork()の返り値は新プロセスのPIDですが、これは親プロセス(forkシステムコールを呼び出したプロセス)のtrapframe->eaxに保存されます。これによって親プロセスはシステムコールという「関数」からの返り値として子プロセスのPIDを受け取るわけです。
ですが子プロセスは返り値として0を受け取りますよね。それはまさにこの一行によるものです。子プロセスは親プロセスのメモリ空間やstruct trapframeなど全てをコピーしてきましたが、唯一、trapframe->eaxだけ親プロセスと異なるのです。そしてこれはシステムコールの返り値として使用されるのです。これだけは伝えたかった。満足です。

まぁ後は流れで、現在開いてるファイルやカレントディレクトリの参照数をインクリメントしたりしてます。

最後、子プロセスの全設定が終わったら状態をSET(セット中)からRUN(実行可能状態)に変更することで、sched()関数の選択対象となりました。 親プロセスは子プロセスのPIDを受け取り、子プロセスはいつかsched()によって実行された際には親プロセスと同じ場所から始まるものの、唯一システムコールの返り値だけが違うという状況になりましたとさ。めでたしめでたし。

自宅のサーバーを新調した話

画像多め注意!(手遅れ)

数年前からラズパイ3をサーバー代わりに使ってたんですが、Celeronで組み直しました。

理由は主に3つあって
1. CPUのパワー不足
2. ファイルサーバーが作りたかった
3. 今が買い時おじさん「今が買い時

1に関しては金盾回避用にShadowsocksR鯖を建てたんですが、暗号化の処理が重くてCPUがボトルネックになって通信速度が出ない状態になっていたため

2に関しては主にDropboxのせいなんですが、勝手に3台までに制限させられたのでしぶしぶnextcloudに移行。GoogleDriveやOneDriveはアレなファイル入れると垢BANされるそうなので、それは困るという話でやめた
ラズパイはSDカードで動作させてたんですが、SDカードは基本的に信頼出来ないので、ファイルサーバーとして使うなら流石にディスクを外付けしたくなりますね...
でも小さいのが取り柄なラズパイに本体よりでかいディスクを付けるの不格好だし、HDDは電源供給怪しいし、USB2.0にSSD繋ぐのもなんかもったいなくてやりたくないし...って事で買い替えました。

3に関してはメモリとSSDがクソ安だったからです。
今は時期が悪いおじさん「流石に今は時期が良い」

御託はいいからさっさと組むぞ!!!!!



登場するパーツ達


CPU: Celeron G3900 - ¥4380

最近のIntelCPU値上がりしすぎて、5000円以下だとこれしかなかった...
まぁ秋葉原で中古とか漁れば普通にあるんだろうけど、格安帯ってあんま中古でも値段変わらない印象あるからこれでいいかな。
個人用なので性能的には必要十分。ラズパイよりよっぽど高性能(当たり前)。
Unix Benchmarks - single - multi - watt
Raspberry Pi 3B - 312.2 - 779.1 - 5~7W
Celeron G3900 - 1213.1 - 2410.5 - 40~50W
こうやって見るとラズパイのワットパフォーマンス凄いな...

ASRock Desk Mini 110/B/BB - ¥12723

省スペースかつディスクが2台のる小型ベアボーンって時点で、多分これしか選択肢無いっすね。
ACアダプタが大きいのが玉に瑕だが、このサイズでマザボ/ケース/電源と考えると激安。ASRock最強!ASRock最強!ASRock最強!

MEM: DDR4-2133 4GBx1 - ¥3080

最近メモリ安くなりすぎて草。2年前に2万円で16GB買ったワイ涙目。
ゲームもしなければそもそもXも起動しないのであまりメモリ使わんかな...一応2スロあるので、足りなかったら追加すればいいやって事で最小の4GB。

SSD: Transcend 240GB - ¥4200

SSDも安くなりすぎて草。2年前に1万円で240GB買ったワイ涙目。
現時点で一番コスパが良いのは500GBなんだけど、そんなに使う予定が無かったので240GB。
怪しいブランドや廉価版なら3000円ぐらいに抑えられたんだけども、今回はファイルサーバー用なので一応TranscendのSSDにしました。
容量大きいほうが寿命良さそうなので、普通に500GB買えばよかったかなと少し後悔...

HDD: HGST 500GB

自宅に転がってたやつ。稼働時間10000超えてる(大丈夫か)



f:id:mic611:20190605163605j:plain



開封

f:id:mic611:20190605171148j:plain

これマジ?本体に対してACアダプタ(とケーブル)がデカすぎるだろ...

いや、思った以上にデカかった。予想の5倍ぐらいあるぞ。小型PCとは一体...うごご



f:id:mic611:20190605171813j:plain 裏面端子はこんな感じ。今回はLANケーブルと電源しか繋がないです。
角のネジを外してボードを引っ張り出します。


f:id:mic611:20190605172300j:plain DeskMini 110は2.5インチのディスク2枚に加えて、M.2 NVMeのSSDにも対応していますね。
にしてもこのチップの上に発熱ヤバイもの載せていいんか?って感じはしますが...
今回は使用するCPUが非力な点、SATA SSDで速度は十分な点、排熱に不安がある点から使いません。


f:id:mic611:20190605173511j:plain CPUくんを開封
今回は費用節約のためリテールクーラーを使用します。
寝室に置いてるんですが数週間動かした感じ、まぁ夏場なら外がうるさいので寝るときも気にならないですね。
冬場でうるさく感じたらnoctuaの静音ファンに切り替えるかも...(CPUより高いので多分無い)
冷却性能ではフルロード時でも60度超えないので良い感じ。ちょっとうるさいけど。


f:id:mic611:20190605174422j:plain Intel CPUは初めて組むんですが、これはCPU側にピンが無いので凄く良いですね。
Ryzenで組んだ時はCPU側のピン曲げそうでラーメン食ってるときより汗流しました。


f:id:mic611:20190605174727j:plain 次にクーラーとメモリを取り付けます。
プッシュピン型のクーラーは取り付けやすくて良いですね。
ネジタイプの奴だと本当にハマらなくて、数時間泣きながらネジ回す羽目になるので最高です。

一方メモリの硬さは一級品でしたね。50年に1度の出来栄えぐらいの硬さです。指切った
あとスロットが2つあったけど、1枚の場合どっちに付けるか書いてなかったですね。どっちでも良いのかな?


f:id:mic611:20190605175554j:plain ディスクを取り付けて行きます。
見にくいですが、ボードの真ん中の楕円穴に露出してる部分にSATAポートが2つあります。
これデザインした人頭おかしい・・・(褒め言葉)
ディスクは1台なら4隅のデッパリに引っ掛けるだけで良いんですが、2台目は裏からネジで固定する必要があるんですね...
f:id:mic611:20190605181149j:plain (取り外してまた付けるの)あ~めんどくせマジで....
HDDは振動をなるべく減らすためにガッチリとネジ止めしました。


f:id:mic611:20190605181801j:plainf:id:mic611:20190605182427j:plain

(組み立て)工事完了です・・・診断も全部パスしたので多分大丈夫でしょう。
CeleronにはIntelハイッテルのシールも付属で付いてるので、Intel教の人も安心ですね私は付けませんでしたが
微妙にLEDが眩しい気がしますが...まぁいいでしょう。私としては正常起動時は青より緑が好きなんですけどね。






セットアップのためにUSBにOSイメージを入れてブートさせます。

今回はOSとして宗教上の理由でDebian GNU/Linuxを採用します
「サーバーのくせにRed Hat系じゃないとかww」「FreeBSD is GOD.」
という異教徒の方々はここで帰ってもらって大丈夫です。

まずネットからDebianのネットワークインストール用イメージを落としてきて、USBに焼き込みます。

そしてBoot Menuから華麗に起動!!!!




































GNU GRUB version 2.02~beta3-5+deb9u1

Minimal BASH-like line editing is supported. For the first word, TAB lists possible command completions. Anywhere else TAB lists possible device or file completions.
grub>


 

 

デデドン!(絶望)

試しにUbuntu Desktopのイメージでブートしてみたら普通にGRUBが起動したので、多分ハードウェアの問題じゃなくてソフトウェアの問題ですね・・・とりあえず手動ブートしてみましょう

grub> ls
(hd0), (hd0,msdos1), (hd1) 
grub> ls /
.disk EFI boot
grub> ls (hd0,msdos1)/
.disk boot css dists doc efi firmware install install.amd isolinux pics pool tools ...
grub> set root=(hd0,msdos1)
grub>



どうやらUSBメモリはhd0でSSDはhd1らしい
そしてルートディレクトリが変な所になってるようなので再セット
さてさて、これでgrub.cfgを読ませれば行けるかな

grub> configfile /boot/grub/grub.cfg
can't find command 'linux'
can't find command 'initrd'

grub>

 

あれ? 

 

grub> linux /install.amd/vmlinuz
can't find command 'linux'
grub> initrd /install.amd/initrd.gz
can't find command 'initrd'
grub>

 

f:id:mic611:20190605183847j:plain

冷静にinsmodコマンドを実行する。

 

grub> insmod linux
file '/boot/grub/x86_64-efi/linux.mod' not found
grub> ls /boot/grub/x86_64-efi
acpi.mod ... linux.mod ... zfscrypt.mod
grub>

/boot/grub/x86_64-efi/linux.modの存在は確認できるんだが、insmodでパスを省略すると正しいパスでもファイルが見つからないっぽい(なぜ)

grub> insmod /boot/grub/x86_64-efi/linux.mod
module 'video.mod' isn't loaded
grub>

そしてフルパスでinsmodを実行すると何故か正しく動くらしい(これもうわかんねぇな...)
ただし依存関係のモジュールは自動でロードしてくれないので、手動でロードする必要アリ。
insmodしてみて足りないって言われたモジュールを再帰的に入れるか、バイナリから直接依存modを見つけ出すかしてください。
私は面倒だったので足りないのを一つ一つ手入力で入れました(情弱)







linuxコマンドとinitrdコマンドが使えるようになったので、今度こそとgrub.cfgを読み込ませますが今度は画面がブラックアウト。
まさかと思いgrub.cfgを確認する。  

if loadfont $prefix/font.pf2 ; then
  set gfxmode=800x600
  insmod efi_gop
  insmod efi_uga
  insmod video_bochs
  insmod video_cirrus
  insmod gfxterm
  insmod png
  terminal_output gfxterm
fi

if background_image /isolinux/splash.png; then
  set color_normal=light-gray/black
  set color_highlight=white/black
else
  set menu_color_normal=cyan/blue
  set menu_color_highlight=white/blue
fi

insmod play
play 960 440 1 0 4 440 1
set theme=/boot/grub/theme/1
menuentry --hotkey=g 'Graphical install' {
    set background_color=black
    linux    /install.amd/vmlinuz vga=788 --- quiet 
    initrd   /install.amd/gtk/initrd.gz
}
menuentry --hotkey=i 'Install' {
    set background_color=black
    linux    /install.amd/vmlinuz vga=788 --- quiet 
    initrd   /install.amd/initrd.gz
}
submenu --hotkey=a 'Advanced options ...' {
...

oh...これはinsmodでコケてる奴ですね...一行一行全部手動で入れましょう...









結局grub.cfgを手動でパス補完して入力したら無事Install画面が起動しました。
めでたしめでたし。

f:id:mic611:20190605185154j:plain

 

 

 

mast→coins単位変換一覧

転学類する人の参考用に。

正直編入生でもかなりガバガバ(卒研がワイルドカードらしい)のに、
転類はそれ以上にガバらしいので、とりあえず申請しとけ感がいなめない。

というか80単位上限とか、3年次科目に変換できないとか言う制限は、
編入生に対してズルいという声が多く上がったために制限が入ったそうな(担任談)

つまり転類は基本的に何しても許されるので、とりあえず申請しとけ...(チキって後悔した人並感)

f:id:mic611:20180605101447j:plain

筑波大学転学類制度の実体験とオススメ科目

この記事は、mast Advent Calendar 2018 の記事です(大嘘)

大学生活に関係することだし、アドベントカレンダーにすべきなんだろうけど、内容が内容なので辞めておきました。(載せる人脈が無かっただけ)


転類しました。

ネット上に全然筑波大の転類情報が残ってないので色々残しておきます。

合わせて読んでおきたい : https://togetter.com/li/1103694

ぶっちゃけ自分語りだけど許して

 


# 基本情報

2年次mast → 3年次coins(情報システム専攻)

一般入学(前期)

成績はそこそこ悪くないぐらい

 


# 転類の流れ

1. 10月上旬に申請資料を学務から貰う。

学務によっては9月から貰えるところもあるらしい。3学の学務(?)では貰えたらしい(伝聞)。

ちなみに春日の学務は死ぬほど頑固で、10月になるまで一切くれなかった。そんなんだから嫌われるんや。

 

2. 10月中旬までに提出する。

ぶっちゃけこれが転類の最難関。1週間ちょいでクラス担任と学類長と(他学群に行くなら学群長も)面談してハンコを貰わないといけない。

さらに親の署名とハンコも必要。(実家が遠い人は死ぬ)

更にA4 1枚の申請理由書を書き上げる。ちなみに秋AB学期の授業はもう始まってる。

例年用紙は同じなので既に持ってる人から貰って夏休み中に先に書き上げて親の印鑑と署名も貰うべき。担任や学類長へのアポも先に取るのが良い。

 

3. 12月頭に書類選考結果が出る。

書類選考(一次試験)合格なら二次試験の日程が書かれた紙と受験票が渡される。

自分の場合は、転類志願先の学類長室で20分の面談が指定された。

多分履修見て空きコマに指定してくれてると思う。

 

4. 12月中旬~下旬に二次試験がある。

面接は必須で、履修状況とかいろいろ判別して筆記試験ありの場合もあるらしい。

面接は受験みたいな礼儀とかを重視するタイプじゃなくて、かなりフランク。

学類長+2人と自分で机を囲んで、転類したい理由、転類先でやりたい事とかを聞かれる。ほのぼのした雰囲気。

ところでcoinsの学類長室、音が外にガバガバ漏れてたんですが大丈夫なんですかね...面接前に「ヤバイ、成績表紛失したどうしよう」とか、面接後に「じゃあ、合格でいいですかね..」とか漏れてたんですがそれは...

成績は見られるけど、判断の材料にはされないっぽい。自分は上記の通り、教員が成績表を紛失したまま合格貰ったので。

 

5. 1月中旬に結果が出る。

ぶっちゃけネタバレされてたからアレ。

 

6. 単位変換や、学生証更新、主専攻申請をする。

合格してから4/6ぐらいまで、一切何の連絡もない。学生証は3/31で期限切れ。

周りが履修を登録する頃になってやっと連絡がくる。そんで5日後(授業開始前日)に単位変換とかするから、シラバスとか成績証明書とか持ってこいと。

単位変換を待ってから履修を組むべきじゃない。自分である程度変換の目安を付けておいて、数パターン履修を予め決めておくべき。

単位変換は会議を通して判断されるので、結果が出るのは履修締め切り後。正直どうかしてる。

3年次転類は実質編入扱いされるので、編入生とは仲良くなれる。(コミュ力があれば)

転類したいけど留年したくないなら1年次45単位取って、CAP開放して2年次55単位取らないとキツイ。


7. 編入オリエンテーションの感想

4/10に編入オリエンテーションをやるから、転類のお前も来いと学務からメールが4日前に来た。

メールでは10日午後と11日午前にオリエンテーションをやるとの事だったが、実際には10日全日だった。ガバガバ

オリエンテーションでは主に単位変換や卒業要件の説明。4年次以上の編入生がTAとして沢山来てる。

説明が終わったら、編入生とTAで同じ高専同士集まって単位変換の会議。

ただ転学類は編入とは違うので正直説明が全く当てはまらず意味不明。

具体的には

  • 既に取った転類先の科目はどういう扱いになるのか。(科目番号は同じだが、全部基礎科目扱いになってる)
  • 既に取った体育や総合、英語などの科目はどういう扱いになるのか。
  • 3年次以上の科目は原則変換できないというが、3年次の科目を既に履修してた場合はどうなるか。
  • そもそも申請用紙が他学校からの編入を想定したものなので、転類をこの書類で申請して良いのか?

教員とTAも前例がないからわからないと、たらい回しにされる。強いメンタルが必要。

結論としては、既に取った転類先の科目は申請書に書かなくても良いそうです。

自分で単位認定申請書を作成し、成績証明書と一緒にクラス担任に面談。その後、紙媒体と電子媒体(USBメモリなど)を両方提出。

オリエンテーションが4/10で申請の締め切りが4/16,結果が確定するのは5月以降。

ちなみに授業開始は4/12で春A履修締切が4/25。頑張れ。

 

 

 

# Q&A


Q. 1年次→2年次で転類したいんだけど

A. トンネル入学と疑われるから難しいらしい。

一度申請した学類は二度と申請できないシステム上、2年次に転類先の授業を沢山取って2→3で転類すべき。

ちなみに1年次で転類する場合は入学試験の結果が重視される。 成績は AC>推薦>後期>前期 らしい。

esysからbresに転類した方からの意見

 

 

質問があればTwitterでリプかコメントくれればQ&Aに反映します。