自作OS(OS5)の紹介とユーザーランド周りの機能追加について

本記事は、「自作OS Advent Calendar 2016 - Adventar」の12/13(火)の記事です。


自作OS(OS5)について改めての紹介と、今回のリリースで追加した機能の紹介です。

はじめに

OS5は、自分の理解の確認や勉強のために、フルスクラッチで作成しているOSです。
動いている様子はこんな感じです。


今回のリリースを含め、現在の主なスペックは以下のとおり。


作成状況の節目ごとに当ブログで記事にしており、そのタイミングをリリースとしています。
前回のリリースは以下の記事です。


ソースコードはGitで管理しており、GitHub上にリポジトリがあります。
リリースの際は"blog-YYYYMMDD"というタグをプッシュしています。
今回のリリースは"blog-20161213"というタグをプッシュしています。


また、これまでのリリース状況や、GitHubへのリンク、勉強会スライド等を「自作OS(OS5)のまとめ」にまとめています。


今回のリリースでソースコード行数(アセンブラとCの行数の総和)は以下の様になりました。

ブートローダ 253行(変化なし)
カーネル 1966行 → 2073行
ユーザーランド 437行 → 535行


以降では、OS5の各実装について、簡単に説明します。(今回追加した機能についても、併せて説明します。)

ビルドシステム

OS5のビルドを確認しているのはDebian上で、ツールはMakeとGCCを使用しています。


今回のリリースでGCCのオプション変更とドキュメントビルド対応を行いました。

GCCのオプション変更

開発当初使用していたPCが32ビットであったために、OS5はこれまで、32ビットのDebian上でビルドしていました。PCが64ビットに変わったあとも、特に何も考えず、QEMU上に32ビットのDebian環境を構築し、ビルドしていました。


そんな中、今年の4月に勉強会でOS5を紹介させて頂いた所、GCCのオプションを変更するパッチを書いて下さった方が居たので、マージさせていただきました。そのため、今は、amd64Debian上でビルドしています。

ドキュメントビルド対応

OS5のソースディレクトリ直下のdocディレクトリには、emacsのorg-modeの形式でドキュメントを配置しています。org-mode自体プレーンテキストであり、読み難いものでは無いですが、org-modeは整形されたテキストへの変換が可能なので、make docでこの変換を行うようにしました(要emacs)。

ブートローダ

カーネル・ユーザーランドのRAMへのロード(BIOS割り込みルーチンを使用)と、CPU設定を行っています。CPU設定では割り込みや、各種ディスクリプタテーブルの設定などを行っています。特に重要なのが16ビットのリアルモードから32ビットのプロテクトモードへの移行です。CPUのモードをプロテクトモードへ移行させた後、カーネルの先頭アドレスへジャンプします。


現状、MBR(512バイト)に収まっており、ブートローダーの多段ブートの様な事はしていません。ソースコードは、単一のアセンブラファイルです。


後述の「カーネル」でも書きますが、「ひとまずタスクは2つまで」と想定した作りこみがあり、今回のリリースで、RAMへロードするセクタ数とGDT(グローバルディスクリプタテーブル)を増やしました。

カーネル

ブートローダーからジャンプしてくると、カーネルの初期化処理が実行されます。カーネルの各機能の初期化を行った後は、カーネルは割り込み駆動で動作し、何もしない時はhlt命令でCPUを寝させるようにしています。


カーネルの構成を図にしてみると、以下のとおりです。


カーネルの各機能の実装については、今年の4月に発表させていただいた勉強会のスライドにまとめています。各機能の実装について、図を使って説明していますので、興味があれば見てみてください。


今回のリリースでは、カーネルが起動してから3つ目のタスク(*1)を使用するための修正、schedule()の整理、exitシステムコールとタスク終了イベントの追加を行いました。
(*1)カーネルではx86 CPUのデータシートの言い回しに合わせて「タスク」と呼ぶようにしていますが、現状の実態としては、「アプリケーション」と同一です。

カーネルが起動してから3つ目のタスクを使用するための修正

「ひとまずタスクは2つまで」と想定した作り込みが2つあり、修正しています。


1つ目は、ヒープ領域のサイズです。物理アドレス空間内の配置場所の問題で、これまでヒープ領域は44KBでした。メモリのアロケーション(OS5ではmem_alloc()、4KBずつ確保)を解放無しで11回行うと使いきってしまうサイズです。現状、mem_alloc()はタスクに関連する箇所でのみ、タスク1つ当たりに5回呼ばれます。ファイルシステムとタスクのエントリ追加で2回、ページディレクトリ・ページテーブル・スタック領域の確保で3回です。タスクを2つ起動させるとmem_alloc()が10回呼ばれることになり、3つ目のタスクを起動させるためのメモリを確保できなくなります。ヒープ領域の配置場所を変え、252KBまで拡張しました。


2つ目は、GDT内のCS(コードセグメント)とDS(データセグメント)のディスクリプタの配置場所の変更です。上記のスライドでも説明していますが、OS5ではx86 CPUの持つタスク管理機能を使っています。CPUのタスク管理機能を使うためには、TSS(タスクステートセグメント)を定義する必要があり、そのためのディスクリプタをGDTに配置します。これまでは、GDT内のディスクリプタの並びが

0 使用禁止
1 CS(カーネル権限用)
2 DS(カーネル権限用)
3 TSS(カーネルタスク)
4 TSS(1つ目のユーザタスク用)
5 TSS(2つ目のユーザタスク用)
6 CS(ユーザ権限用)
7 DS(ユーザ権限用)

となっていたのですが、タスク生成時にTSSをGDTへの配置する場所はインクリメントしていく実装なので、3つ目のタスクのTSSは「CS(ユーザ権限用)」を上書きしてしまいます。そこで、以下の配置へ変更しました。(今思えば、ユーザ権限用のCS・DSを追加する際にTSSの配置換えを行うべきでした。。。)

0 使用禁止
1 CS(カーネル権限用)
2 DS(カーネル権限用)
3 CS(ユーザ権限用)
4 DS(ユーザ権限用)
5 TSS(カーネルタスク)
6 TSS(1つ目のユーザタスク用)
7 TSS(2つ目のユーザタスク用)
schedule()の整理

sched.cのschedule()がスパゲッティになっていたので整理しました。schedule()を汎用的にしようと思うあまり、schedule()にcause_idという引数を渡すようにしてどこから呼ばれたかを判別し、呼び出し元に応じた処理を行っていました。しかし、これがschedule()をスパゲッティにしてしまう要因であり、呼び出し元に依存する処理は呼び出し元で行うほうがシンプルと考え、schedule()のcause_id引数は廃止し、呼び出し元へ処理を移動しました。

exitシステムコールとタスク終了イベントの追加

exitシステムコールを追加しました。このシステムコールが実行されると、タスクをランキューから外し、タスクのために確保していたメモリを解放します。


また、OS5には、あるイベントが発生するまでタスクを待機させる(ランキューから外す)機能があります。今回のリリースで、タスク終了イベントを追加したので、他のタスクの終了を待つことができるようになりました。なお、現状、このイベントを使用するのはシェルのみで、複数のタスクの終了を待つという状況はないので、「どのタスクの終了を待つか」という実装はありません。

ユーザーランド

アプリケーションとしては、shellとuptime、whoareyouという3つです。現状、カーネルの動作確認程度のものでしかないです。


shellはその名の通りシェルで、CUIを提供します。shellの組み込みコマンドとしては、echoとメモリ/IOへの直接read/writeのコマンド(readb,readw,readl,ioreadb,writeも同様のコマンド名)、そしてbgというコマンドがあります。bgは今回のリリースで追加したコマンドで、引数で指定したコマンドをバックグラウンド実行します。(これまでは、実行したコマンドの終了を待つことができなかったので、常にバックグラウンド実行でした。)uptime・whoareyouもshellから起動します。これらのコマンド名をshell上で入力すると、shellはexecシステムコール(OS5ではexecをシステムコールとしています)を使用して実行します。


uptimeマルチタスクの動作確認をするためのコマンドです。コンソール画面の右上で、16進数で起動時間をカウントし続けます。自ら終了することがないコマンドなので、バックグラウンド実行しないとプロンプトが帰ってこなくなります。


whoareyouは今回のリリースで新たに追加したコマンドです。argcとargvによりコマンドライン引数を受け取れることを確認するためのコマンドです。


また、今回のリリースでは、main()をエントリポイントとする変更や、静的ライブラリの仕組みも導入しており、よく見るCのソースコードのようにアプリケーションを書けるようになりました。
例えば、whoareyouのソースコードは以下のとおりです。

#include <app.h>
#include <kernel.h>
#include <string.h>
#include <console.h>

int main(int argc, char *argv[])
{
	if ((argc >= 2) && !str_compare(argv[1], "-v"))
		put_str("Operating System 5\r\n");
	else
		put_str("OS5\r\n");
	exit();

	return 0;
}


話は変わって、ユーザーランドのファイルシステムイメージは、makeの過程で、シェルスクリプトで作成します。ファイルシステムは、簡単に、ファイル名とバイナリのみを管理するだけのもので、シェルスクリプトでバイナリを並べて連結しています。ファイルシステムについても詳しくは上述のスライドをご覧ください。


これまでをまとめると、アプリケーションが実行されるまでの流れは以下のとおりです。

  1. ファイルシステムイメージをブートローダーがRAM上の決まったアドレスへロード
  2. カーネルは、初期化の過程でファイルシステムが配置されているRAM上の領域をチェック
  3. カーネルは、ファイルシステム上の1つ目のファイルを、カーネル起動後に実行する最初のアプリとして実行する(ここでshellが実行される)

おわりに

OS5と今回のリリースについて、紹介してみました。


まだまだソースコード行数も大したことなくGUIも無いようなOSですが、自分の興味のままに、作っていきたいなと思っています。


# 実は、GUIという程ではないですが、グラフィックモードで動作させるパッチはあります。
# (ブートローダーでビデオモードをグラフィックのモードへ変更し、カーネルでVRAM空間をアプリケーションのメモリ空間へマップするだけです。)
# - https://github.com/cupnes/os5/tree/test_gui