LispでOSを書く

(このエントリは、Lisp Advent Calendar 2012 の22日目である)

ELIS復活祭のとき、ELISのTCP/IPプロトコルスタックを書いたという方とお話する機会があった。ELISのプロトコルスタックはもちろんLispで書かれていた。その方がおっしゃるには、「C言語はよい。BSDからソースコードを持ってくればいいのだから。しかし、Lispで書かれたプロトコルスタックなどなかった。自分で書くしかなかった」ということだった。それにしても、LispでOSを書くというのは、いったいどんな感じなのだろう?

OS記述言語としてのLisp

UnixがCで書かれて以来、OSは、伝統的にCとその派生言語で書かれることになった。BSDLinuxを含むUnix-likeなシステムはもちろんCで書かれている。Windows NTはC++を使っている。BeOSC++で書いたし、MacOS XObjective-Cを使っているのかもしれない。もしかすると、IBMメインフレームなどでは事情が異なるのかもしれない。

Cとその仲間たちは、いわゆる高級言語の中でも抽象化のスペクトルのだいぶ下の方に位置している。つまり、ビットを操作するような処理が得意なのだ。だから、デバイスのレジスタを直接叩くような処理はCの得意とするところだ。

OSを構成するサブシステムの中でも、プロトコルスタックファイルシステムは、デバイスを叩くというよりは、ある種のロジックを実装するものだ。80年代から90年代にかけて研究されたマイクロカーネルでは、これらのサブシステムはユーザプロセスとして実現されていた。

ソフトウェア工学の言葉を使うなら、OSのコア(マイクロカーネル)はデバイスへアクセスする「メカニズム」を提供する。上位のサブシステムは、そのデバイスをどのように使うか(例えば、ファイルシステムをどう構成するか)という「ポリシー」を実装する。

複雑なロジックを実装するためには、できるだけ抽象化のレベルの高い言語を使うべきだ。そして、もっとも抽象化のレベルの高い言語(のひとつ)はLispである。したがって、OSのサブシステムも、ある部分はELISのようにLispで書いてもいいはずだ。

FUSE + libfuse + Gauche + c-wrapper

LispでOSのサブシステムを書くというのがどんな感じなのか、試してみる方法をご紹介したい。私はCommon LispよりもSchemeの方が慣れているので、Gaucheを使うことにしよう。

1から新しいOSを設計するのは、楽しいかもしれないが時間もかかる。Linuxには、FUSE(Filesystem in Userspace)という、ファイルシステムをユーザ空間で実装するための仕組みがある。この仕組みを使って、Gaucheファイルシステムを書いてみたい。

FUSEにつて簡単に説明しよう。Linuxカーネル複数ファイルシステムを持っている。ext3, ext4, btrfsなどなどだ。それぞれのファイルシステムの開発者のために、カーネルファイルシステムとの間に標準的なインタフェースが決まっている。このインタフェースをユーザ空間からアクセスできるようにする仕組みがFUSEだ。

最近のLinuxカーネルなら、FUSEは標準で組み込まれている。/dev/fuseカーネルへのインタフェースだ。基本的には、普通のファイルアクセスのシステムコールを使って/dev/fuseを操作すればよいのだが、これをもう少し簡単に行うためのライブラリとして、libfuseがある。

libfuseはCのライブラリだ。libfuseをGaucheで使うにはCへのバインディングを書けばよいのだが、もっと簡単な方法は…そう、魔法のc-wrapperである。

hellofs (The Hello Filesystem)

ストレージを操作する本物のファイルシステムではなく(これは大仕事だ)、偽物のファイルシステム(Pseudo Filesystem)を実装してみる。これは、/procや/sysの一種と考えてよい。このファイルシステムは、FUSEのサンプルプログラムをほぼそのままSchemeで書きなおしたものだ(若干の手抜きをしたので、あまりいい例ではないかもしれない)。
#!/usr/bin/env gosh
# hellofs.scm (The Hello Filesystem)

(use gauche.uvector)
(use c-wrapper)

(c-load "fuse.h"
:cppflags "-DFUSE_USE_VERSION=26"
:cppflags-cmd "pkg-config --cflags fuse"
:import '(fuse_operations
fuse_main_compat2
fuse_main
NULL)
)
(c-load "stdio.h")
(c-load "string.h")
(c-load "errno.h")
(c-load "fcntl.h")
(c-load-library "libfuse")

(define hello-path "/hello")
(define hello-str "Hello world!\n")

(define (hello-getattr path stbuf)
(memset stbuf 0 (c-sizeof (c-struct 'stat)))
(cond ((string=? (cast path) "/")
(set! (ref stbuf 'st_mode) (logior S_IFDIR #o755))
(set! (ref stbuf 'st_nlink) 2)
0)
((string=? (cast path) hello-path)
(set! (ref stbuf 'st_mode) (logior S_IFREG #o444))
(set! (ref stbuf 'st_nlink) 1)
(set! (ref stbuf 'st_size) (string-length hello-str))
0)
(else (- ENOENT))))

(define (hello-readdir path buf filler offset fi)
(if (not (string=? (cast path) "/"))
(- ENOENT)
(begin (filler buf "." NULL 0)
(filler buf ".." NULL 0)
(filler buf ((#/^\/(.*)$/ hello-path) 1) NULL 0)
0)))

(define (hello-open path fi)
(if (not (string=? (cast path) hello-path))
(- ENOENT)
(if (not (= (logand (ref fi 'flags) 3) O_RDONLY))
(- EACCESS)
0)))

(define (hello-read path buf size offset fi)
(memcpy buf hello-str (string-length hello-str))
(string-length hello-str))

(define hello-operators (make (c-struct 'fuse_operations)))

(set! (ref hello-operators 'getattr) hello-getattr)
(set! (ref hello-operators 'readdir) hello-readdir)
(set! (ref hello-operators 'open) hello-open)
(set! (ref hello-operators 'read) hello-read)

(define (main args)
(fuse_main (length args)
args
(ptr hello-operators)
NULL
))
マウントすると、マウントポイントにhelloというファイルが現れる。このファイルにはお馴染みの文字列が格納されている。
# mkdir /tmp/hello
# ./hellofs.scm /tmp/hello -d -s
...
# ls /tmp/hello
hello
# cat /tmp/hello/hello
Hello world!
マウントするときに、シングルスレッドモード(-sオプション)を指定することに注意して欲しい。libfuseは、その中で新たなスレッドを生成する。このスレッドはGaucheのスレッドではないので、この中からSchemeの関数をコールバックすることはできない。

FUSEを使ってストレージを操作するようなファイルシステムを作りたいのなら、デバイスノードを読み書きすればよい。また、ユーザ空間でできることは何でもできる。例えば、EvernoteGoogle Driveにアクセスするようなファイルシステムを作ることもできるはずだ。

* * * 

OSのカーネルはますます大きく複雑になっている。Unixの第6版のソースコードは、印刷したものをブリーフケースで楽に持ち歩くことができるほど小さかった。今日のLinuxソースコードを印刷して持ち歩くことは、不可能ではないが紙の無駄である。

 Unix第6版の時代からプログラミング言語の世界は大きく進歩した(もちろんLispUnix第6版の時代から存在したのだが)。ところが、今日のOS開発者は、これら進歩的プログラミング言語の恩恵に与っていない。 

そろそろ、OS開発にも、次の世代のプログラミング言語を使ってもよいのではないかと思う。ELISは商業的には成功しなかった。しかし、ELISの開発過程で多くの知識が蓄積されたはずだ。(非常に失礼な言い方だが)ELISの開発者がまだ生きている今こそ、そのチャンスである(失礼しました)。