99年4月19日執筆

Linusになろう! -- 君にも書けるOS kernel

はじめに

本稿は今話題のOS Linuxについての解説です。最近のLinuxはWindowsの対抗たりえるかどうかという政治的な面、また高性能かつ軽量なOSとして語られることが多く、OS自身の技術的な側面を語られることはあまりありません。また、発表されてからもう8年くらい経ち、その間改良に改良を重ねた結果、かなり大きなプログラムとなってしまっていて、技術解説をするにも容易なものではありません。さらにLinuxの成功の結果、Linusは「神格化」された存在となってしまっています。

しかし、このLinuxはごく初期にはLinusという他の人よりもちょっと好奇心と実行力のある一人の青年が自力で書いた「手作りのOS」であり、また「個人の著作物」です。直接的な意味でも比愈的な意味でも「神の手」によって作られたものではありません。

ここでの解説は、この「手作りのOS」であるLinuxの解説です。

なお本解説で使うLinuxのバージョンは0.01です。これはLinusが初めて人に見せたバージョンのものです。まだOSとしての機能は不足していますし、間違いも大量にあります。しかし、ここで敢えてこのバージョンを取り上げたのは、

  • Linus個人だけのコードである。つまり個人というものが直接見えて来る
  • 初期バージョンの魅力。作者の手の跡の生々しさが感じられる
  • 小さい。そのため、追うのも理解するのも容易である。またこれくらい の規模なら、「いっちょやったるか」と思うことも可能である

ということからです。

なお、本稿では技術解説記事ではありますが、厳密な技術解説を行うことを目的としていません。カーネルのような「神の領域」と思われるようなプログラムを、「これなら自分にも書けそうだ」と思えるガイドやエールとなるための説明を目標としています。細かい技術的な情報を得るためには本稿を参照しながらソースコードを参照すれば良いでしょう。またこのバージョンは全部を読み通すことは困難ではありません。

Ver 0.01の時代背景

初めにこの頃の時代背景を少しだけ説明しておきたいと思います。

初期のLinux自身の歴史については、以下に示しておきます。

初期のLinux年表
1991/4頃 Linusがi386の勉強を初める
1991/6頃 カーネルらしい動作をするプログラムが出来る
1991/7/3 comp.os.minixにPOSIXについての質問をpostする
1991/8/25 comp.os.minixに新しいOSの機能についてのpostをする
1991/9中旬 Ver 0.01
1991/10/5 Ver 0.02
1991/10下旬 Ver 0.03。gccのセルフコンパイルが可能になった
1991/11頃 Ver 0.10
1991/12/25 仮想記憶のサポート
1992/1/5 Ver 0.12
1992/1/12 タネンバウムとflame warが始まる
1992/3 Ver 0.95

時代的な背景を書くなら、この頃はPC/ATはCPUがそろそろ386や486になろうとしていた頃で、また日本ではDOS/Vというものが出始めて、それまで「アメリカのパソコン」であったPC互換機がちゃんと日本語が使えるようになった頃です。しかし、まだ32bitのCPUを活用するようなOSはあまりなく、SCO UNIX等が買えるようなお大尽がUNIXを使うか、DOS extenderのようなものを使うかしかありませんでした。ただ、そろそろアメリカではWindows 3.xが出る頃です。

またその頃、Dr. Dobbs Journalでは、BSDコードのうちfreeになったものだけをベースにし、不足している部分を書き足すということを行い、BSDをPC上で動かすという記事をJolits夫妻が書いていました。これがほぼ同じ頃に公開された 386BSDです。

当時のPCは日本ではメモリは8MB CPUは486 HDDは200MBくらいというのが標準的なものでした。つまり、ハードウェアは32bit時代。商用OSではいまだ16bitが主流、それでは飽き足らない人が32bitを摸索し始めたという時代です。このような時代背景から、Linuxは最初から32bitでスタートしています。

カーネルの基本機能

Linuxの解説に入る前に、まずカーネルの基本構成についておさらいします。

たいていのOSカーネルの機能の概略は以下のようになっています。

  • 初期化処理
  • デバイス操作
  • ファイル操作
  • メモリ管理
  • プロセス管理
  • 時間管理
  • 各種インターフェイス

このレベルのことについては特にUNIX系OSに限られたことではなく、内容の差こそあれ、いわゆる汎用OSには共通の事項です。また汎用でないOSであっても、一部の機能が除かれているだけで、基本的な部分はだいたい同じだと思って構いません。

マルチタスクなOSの場合、時間管理とプロセス管理は深く関係していて、タイマからの割り込みがあった場合、時間の更新を行うとともに、実行しているプロセスの切り換えを行います。また、各種プロセスは独立したものとして管理されますから、プロセス間の情報のやりとり、例えばシステムコールの要求が発生した場合等には、同様にプロセスの切り換えが発生します。この時にどんなプロセス に切り換えるかを決定するために、

  • スケジュール管理

が必要になります。

さらに最近のネットワークを含んだOSの場合、カーネル内部にもネットワークに関する処理が入っています。しかし、これはシステムコール的便利さとか、スケジューリング上の都合といったことによる部分が大きく、カーネル内にあることはあまり本質的ではありません。もっともカーネルとしてどんな機能を持つのが本質かという議論はいろいろあって、いまだに結論は出ていません。そのようにしてカーネルの機能をどんどん外部に追い出したものをマイクロカーネルと言いますが、それで本当の意味で成功した例はいまだにありません(やや偏見気味)。

それでは、それぞれについて説明したいと思います。なお、最初に言いましたように、以下の説明では特に断りのない限り、Ver 0.01のソースツリーを前提としています。この当時のLinuxはi386のアーキテクチャにベタベタに依存していますから、きちんと読むためにはかなりi386アーキテクチャについての知識が必要です。その知識のない人は「まぁそんなものだ」と読み飛ばしておけば良いのですが、実際に動くカーネルを書くとなると、この辺はちゃんと勉強しておく必要があります。

初期化処理

たいていのカーネルの場合、初期化処理の前に「起動」という処理が必要になり ます。

初期のLinuxの場合、カーネルはフロッピから読み込まれる前提となっています。この前提は現在も有効で、最新のカーネルでさえ、その先頭には自分自身をフロッピーから読み込むためのコードが入っています。この処理がboot/boot.sに書かれています。

このコードの動作については、冒頭に図のようなコメントがあります。

boot.s(部分)
|
|   boot.s
|
| boot.s is loaded at 0x7c00 by the bios-startup routines, and moves itself
| out of the way to address 0x90000, and jumps there.
|
| It then loads the system at 0x10000, using BIOS interrupts. Thereafter
| it disables all interrupts, moves the system down to 0x0000, changes
| to protected mode, and calls the start of system. System then must
| RE-initialize the protected mode in it's own tables, and enable
| interrupts as needed.
|
| NOTE! currently system is at most 8*65536 bytes long. This should be no
| problem, even in the future. I want to keep it simple. This 512 kB
| kernel size should be enough - in fact more would mean we'd have to move
| not just these start-up routines, but also do something about the cache-
| memory (block IO devices). The area left over in the lower 640 kB is meant
| for these. No other memory is assumed to be "physical", ie all memory
| over 1Mb is demand-paging. All addresses under 1Mb are guaranteed to match
| their physical addresses.
|

| 1.44Mb disks:
sectors = 18
| 1.2Mb disks:
| sectors = 15
| 720kB disks:
| sectors = 9

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

BOOTSEG = 0x07c0
INITSEG = 0x9000
SYSSEG  = 0x1000            | system loaded at 0x10000 (65536).
ENDSEG  = SYSSEG + SYSSIZE

entry start
start:
    mov ax,#BOOTSEG
    mov ds,ax
    mov ax,#INITSEG
    mov es,ax
    mov cx,#256
    sub si,si
    sub di,di
    rep
    movw
    jmpi    go,INITSEG
go: mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov sp,#0x400           | arbitrary value >>512

    mov ah,#0x03            | read cursor pos
    xor bh,bh
    int 0x10
    
    mov cx,#24
    mov bx,#0x0007          | page 0, attribute 7 (normal)
    mov bp,#msg1    
    mov ax,#0x1301          | write string, move cursor
    int 0x10

| ok, we've written the message, now
| we want to load the system (at 0x10000)

    mov ax,#SYSSEG
    mov es,ax               | segment of 0x010000
    call    read_it
    call    kill_motor

| if the read went well we get current cursor position ans save it for
| posterity.

    mov ah,#0x03            | read cursor pos
    xor bh,bh
    int 0x10                | save it in known place, con_init fetches
    mov [510],dx            | it from 0x90510.
        
| now we want to move to protected mode ...

    cli                     | no interrupts allowed !

| first we move the system to it's rightful place

    mov ax,#0x0000
    cld                     | 'direction'=0, movs moves forward
do_move:
    mov es,ax               | destination segment
    add ax,#0x1000
    cmp ax,#0x9000
    jz  end_move
    mov ds,ax               | source segment
    sub di,di
    sub si,si
    mov     cx,#0x8000
    rep
    movsw
    j   do_move

| then we load the segment descriptors

end_move:

    mov ax,cs               | right, forgot this at first. didn't work :-)
    mov ds,ax
    lidt    idt_48          | load idt with 0,0
    lgdt    gdt_48          | load gdt with whatever appropriate

| that was painless, now we enable A20

    call    empty_8042
    mov al,#0xD1            | command write
    out #0x64,al
    call    empty_8042
    mov al,#0xDF            | A20 on
    out #0x60,al
    call    empty_8042

| well, that went ok, I hope. Now we have to reprogram the interrupts :-(
| we put them right after the intel-reserved hardware interrupts, at
| int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
| messed this up with the original PC, and they haven't been able to
| rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
| which is used for the internal hardware interrupts as well. We just
| have to reprogram the 8259's, and it isn't fun.

    mov al,#0x11            | initialization sequence
    out #0x20,al            | send it to 8259A-1
    .word   0x00eb,0x00eb   | jmp $+2, jmp $+2
    out #0xA0,al            | and to 8259A-2
    .word   0x00eb,0x00eb
    mov al,#0x20            | start of hardware int's (0x20)
    out #0x21,al
    .word   0x00eb,0x00eb
    mov al,#0x28            | start of hardware int's 2 (0x28)
    out #0xA1,al
    .word   0x00eb,0x00eb
    mov al,#0x04            | 8259-1 is master
    out #0x21,al
    .word   0x00eb,0x00eb
    mov al,#0x02            | 8259-2 is slave
    out #0xA1,al
    .word   0x00eb,0x00eb
    mov al,#0x01            | 8086 mode for both
    out #0x21,al
    .word   0x00eb,0x00eb
    out #0xA1,al
    .word   0x00eb,0x00eb
    mov al,#0xFF            | mask off all interrupts for now
    out #0x21,al
    .word   0x00eb,0x00eb
    out #0xA1,al

| well, that certainly wasn't fun :-(. Hopefully it works, and we don't
| need no steenking BIOS anyway (except for the initial loading :-).
| The BIOS-routine wants lots of unnecessary data, and it's less
| "interesting" anyway. This is how REAL programmers do it.
|
| Well, now's the time to actually move into protected mode. To make
| things as simple as possible, we do no register set-up or anything,
| we let the gnu-compiled 32-bit programs do that. We just jump to
| absolute address 0x00000, in 32-bit protected mode.

    mov ax,#0x0001          | protected mode (PE) bit
    lmsw    ax              | This is it!
    jmpi    0,8             | jmp offset 0 of segment 8 (cs)

これを読むと、一旦BIOSによって0x7c00にロードされた後、自分自身で0x90000に移し、その後BIOSを使ってOS本体を0x10000からロードし、全ての割り込みを止めてからそれを0x0000からにロードしなおます。その後ハードウェアや割り込みテーブル等を初期化してプロテクトモードに移行し、ロードしたコードの先頭にジャンプします。

この本体の先頭はboot/head.sにあります。ここでは32bitコードが動くために必要最低限の初期化を行い、カーネルとしての実際の初期化は、init/main.cの中で行います。

init/main.cの中ではデバイス等の初期化を行った後に、updateを起動します。この後、現在のカーネルですとinitを起動するのですが、この当時のLinuxではいきなりシェルが起動されるようになっています。

デバイス管理

この当時のLinuxはデバイスドライバは独立したディレクトリを持っていません。 kernel/に諸々のカーネルモジュールと一緒に格納されています。

サポートされているデバイスも少なく、このディレクトリを見る限りでは、

console.c
keyboard.s
rs_io.s
serial.c
hd.c
tty_io.c

あたりがデバイスドライバです。

これらの中身を理解するには、ハードウェアの知識が必須となりますので、ハードウェアの知識のない人は各種レジスタの操作については「そんなものだ」としてながめておいて下さい。

これらのうち、hd.cはHDDを操作するデバイスドライバで、これはブロックデバイスとしてfs/block_dev.cの中にあるテーブルに登録されます。

Linuxの場合、ブロックデバイスでないデバイスはキャラクタデバイスとなります。

この頃のデバイスドライバとのインターフェイスはかなり直接的なインターフェイスになっていて、しばらく後のインターフェイスが、file_operationsという構造体を経由してアクセスすることになっていたのと異なります。リストにVer 2.2系で使われているfile_operationsの構造を示します。

file_operations
struct file_operations {
    loff_t  (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    int     (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int     (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int     (*mmap) (struct file *, struct vm_area_struct *);
    int     (*open) (struct inode *, struct file *);
    int     (*flush) (struct file *);
    int     (*release) (struct inode *, struct file *);
    int     (*fsync) (struct file *, struct dentry *);
    int     (*fasync) (int, struct file *, int);
    int     (*check_media_change) (kdev_t dev);
    int     (*revalidate) (kdev_t dev);
    int     (*lock) (struct file *, int, struct file_lock *);
};

具体的には下のリストのように、関数のポインタをそれぞれのメンバに設定して初 期化します。

Ver 2.2系のhd.c
static struct file_operations hd_fops = {
    NULL,           /* lseek - default */
    block_read,     /* read - general block-dev read */
    block_write,    /* write - general block-dev write */
    NULL,           /* readdir - bad */
    NULL,           /* poll */
    hd_ioctl,       /* ioctl */
    NULL,           /* mmap */
    hd_open,        /* open */
    NULL,           /* flush */
    hd_release,     /* release */
    block_fsync     /* fsync */
};

ですから、例えば読み出しがしたければ、(*hd_fops.read)といったような呼び出しを行うわけです。

しかし当時の構造では、このような構造体は存在せず、「どの処理を選択するか」は使う関数ポインタを選ぶのではなく、処理をする側に「どの処理を行うか」というフラグを与えることによって選択するようになっています。いろいろと考えれば現在のような実装が都合が良く、またかなり早い時期にそのような実装になっていたのですが、この当時の規模くらいのカーネルであれば、どっちの方法を選択するかは、単なる趣味の問題でしょう。

キャラクタデバイスでは、同じようなインターフェイスのためのテーブルは、fs/char_dev.cの中にあります。また、ioctlを実装するための同様のテーブルが、fs/ioctl.cの中にあります。

ファイル管理

この当時のファイル管理の機能としては、

  1. minix fsの実装
  2. デバイス操作のインターフェイス
  3. パイプの実装

といったものが挙げられます。

minix fsの実装そのものに関しは、とてもここで説明出来る程簡単なものではないので、実際のコードを見ながらOperating Systems: Design and Implementation(旧版についてはASCIIから「MINIXオペレーティングシステム」第2版についてはプレンティスホールから「オペレーティングシステム」という題で邦訳が出ています)と実際のコードを参照しながら読むのが一番の近道ですが、それだけでは寂しいので、ここでもちょっとだけ解説しておきます。

minix fsの基本的な構造は、「ブロックの管理はi-nodeを使う。ディスク上の空き領域管理はビットマップで行う。名前とブロック(実際にはi-node)の対応には、16byte固定長のディレクトリエントリを持つディレクトリで行われる」といったものです。またこれらの管理情報はスーパーブロックという領域に管理情報の元を置いています。

minix fsのinodeの構造は、次のリストに示すとおりです。

inodeの構造
struct d_inode {
    unsigned short i_mode;
    unsigned short i_uid;
    unsigned long i_size;
    unsigned long i_time;
    unsigned char i_gid;
    unsigned char i_nlinks;
    unsigned short i_zone[9];
};

mode, uid, size, time, gid, nlinksはそれぞれ、ファイルモード、ユーザID、ファイルサイズ、最終更新時間、グループID、リンクカウントです。zoneはデータブロックまたは他のinodeをポイントするものとなっています。このポイントの様子を図×に示します。

ファイルシステムのためのの構造体はinclude/linux/fs.hにあります。この中でinodeがd_inodeとm_inodeとありますが、これはディスク上とメモリ上との表現するものの違いです。

openの手順としては、

  1. ファイル構造体のテーブルの空きを探す
  2. ファイル名からinodeを発見する
  3. inodeの情報のうち必要なものをファイル構造体のテーブルに格納する
  4. そのインデクスをファイル番号として返す

というようになっています。

read/writeするには、ファイル構造体の内容を元にしてinodeをたどってブロック番号を求め、それに対してread/writeを行います。

このようなことを頭に置いてfsのコードを見てみましょう。この頃のLinuxはかなり単純なので、わりと簡単に追えると思います。また、読み書きのためのバッファの管理として、buffer.cというモジュールがありますが、これ自体もかなり教科書的なプログラムです。

パイプの実装についてですが、この当時のパイプは現在のそれと違い、「フロー制御を持ったキュー」といった考えて実装されています。

execの実装(fs/exec.c)はかなり謎ですが、

  1. 空間にプログラムを読み込む
  2. 引数の処理を行う
  3. 空間の準備を行う
  4. エントリポイント等をセットする

という処理を行い、次回そのプロセスに処理が回って来た時に実行が出来るように準備しておきます。

○メモリ管理

メモリ管理モジュールはmmというディレクトリにあります。この頃のLinuxは仮想記憶をサポートしていませんので、ページングのための処理はありません。

しかし、この当時からページ保護機構を使ったcopy on writeは実装されていました。このあたりがどのように実装されているかは、i386アーキテクチャの解説を読んで下さい。i386アーキテクチャがわかれば、memory.cの中にあるページテーブルの構成やビット操作の意味がわかると思います。

基本的な考え方は、メモリが要求されるとページを割り当てますが、最初はそのページは書き込み禁止にしておきます。また、forkのように空間の複写が発生するような処理が要求された場合、実際の複写は行わず、ページテーブルを新規に作り、実際のページは共有させておきます。プログラムが何らかの理由でページに書き込みをしようとした場合、ページフォルトのトラップが発生します。そこで初めて新しくページを割り当て、ページの中身をコピーし、その後書き込みを行います。このようにすることによって、forkを高速に実行したり、メモリの節約をしたりすることが可能です。

この頃のLinuxの他の部分の単純さに比べて、この部分はかなり凝ったものとなっています。またこの後かなり早い時期に仮想記憶が実現されていたりします。これは元々Linusが初めたのが、i386アーキテクチャの勉強であり、その成果としてLinuxが作られたことが原因です。同じようなことは、システムコールインターフェイスやスケジュール管理にあるコンテキストスイッチにもあります。

プロセス管理

この当時のLinuxのプロセス構造体をリストに示します。

プロセス構造体
struct i387_struct {
    long    cwd;
    long    swd;
    long    twd;
    long    fip;
    long    fcs;
    long    foo;
    long    fos;
    long    st_space[20];   /* 8*10 bytes for each FP-reg = 80 bytes */
};

struct tss_struct {
    long    back_link;      /* 16 high bits zero */
    long    esp0;
    long    ss0;            /* 16 high bits zero */
    long    esp1;
    long    ss1;            /* 16 high bits zero */
    long    esp2;
    long    ss2;            /* 16 high bits zero */
    long    cr3;
    long    eip;
    long    eflags;
    long    eax,ecx,edx,ebx;
    long    esp;
    long    ebp;
    long    esi;
    long    edi;
    long    es;             /* 16 high bits zero */
    long    cs;             /* 16 high bits zero */
    long    ss;             /* 16 high bits zero */
    long    ds;             /* 16 high bits zero */
    long    fs;             /* 16 high bits zero */
    long    gs;             /* 16 high bits zero */
    long    ldt;            /* 16 high bits zero */
    long    trace_bitmap;   /* bits: trace 0, bitmap 16-31 */
    struct i387_struct i387;
};

struct task_struct {
/* these are hardcoded - don't touch */
    long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    long counter;
    long priority;
    long signal;
    fn_ptr sig_restorer;
    fn_ptr sig_fn[32];
/* various fields */
    int exit_code;
    unsigned long end_code,end_data,brk,start_stack;
    long pid,father,pgrp,session,leader;
    unsigned short uid,euid,suid;
    unsigned short gid,egid,sgid;
    long alarm;
    long utime,stime,cutime,cstime,start_time;
    unsigned short used_math;
/* file system info */
    int tty;                /* -1 if no tty, so it must be signed */
    unsigned short umask;
    struct m_inode * pwd;
    struct m_inode * root;
    unsigned long close_on_exec;
    struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
    struct desc_struct ldt[3];
/* tss for this task */
    struct tss_struct tss;
};

これを見ると、minixとは随分とかけ離れていることと、i386に依存したtss_structというメンバがあることがわかります。それ以外の要素については、UNIXのプロセスがどのような動きをしているか知っている人であれば、あまり細かい説明をされなくても、何となく理解出来るのではないかと思います。

tss_struct, i387_structについては、i386アーキテクチャに依存していますので、その関係の本を読んで下さい。

この中にはユーザIDのように、直接スケジューリングとは関係のないものも含まれています。実際にどれが使われているかについては、kernel/sched.cの中を見るとわかると思います。

実際のスケジューリングを行っているのは、scheduleという関数です。この頃のスケジュール処理は優先度だけを意識したラウンドロビンになっています。単純なのは良いのですが、実行効率はイマイチです。

コンテキストスイッチはi386のタスク管理を使った方法で、include/linux/sched.hの中にswitch_toというマクロで書かれています。

インターフェイス

システムコールインターフェイスはkernel/system_call.sの中に書かれています(リストに部分)。中身は基本的にレジスタを退避してシステムコール実体のcallですが、戻ってから再スケジューリングを行ったり、シグナルの処理を行ったりもしています。

システムコール実体はテーブルになっていて、そのテーブルはinclude/linux/sys.hの中で定義されています。アプリケーションからのシステムコールを行う方法は、EAXレジスタにシステムコール番号をセットしてから、0x80番のソフトウェア割り込みを発生させることによって実現されており、その設定はsched_initの中で行われています。

i386のアーキテクチャにより、割り込み発生によってコンテキストスイッチが行わます。つまり、この時にモードがユーザからカーネルに遷移するわけです。

たいていのシステムコールの実体の手続きはsys_<システムコール名>という規則で命名されています。ちゃんと実装されたシステムコールはあちこちのモジュールのエントリポイントが存在していますが、その他のものに関しては、kernel/sys.cの中にあります。これを見ると、この当時のLinuxには未実装のシステムコールが大量にあったことがわかります。

system_call
.align 2
bad_sys_call:
    movl $-1,%eax
    iret
.align 2
reschedule:
    pushl $ret_from_sys_call
    jmp _schedule
.align 2
_system_call:
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    push %fs
    pushl %edx
    pushl %ecx                  # push %ebx,%ecx,%edx as parameters
    pushl %ebx                  # to the system call
    movl $0x10,%edx             # set up ds,es to kernel space
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx             # fs points to local data space
    mov %dx,%fs
    call _sys_call_table(,%eax,4)
    pushl %eax
    movl _current,%eax
    cmpl $0,state(%eax)         # state
    jne reschedule
    cmpl $0,counter(%eax)       # counter
    je reschedule
ret_from_sys_call:
    movl _current,%eax          # task[0] cannot have signals
    cmpl _task,%eax
    je 3f
    movl CS(%esp),%ebx          # was old code segment supervisor
    testl $3,%ebx               # mode? If so - don't check signals
    je 3f
    cmpw $0x17,OLDSS(%esp)      # was stack segment = 0x17 ?
    jne 3f
2:  movl signal(%eax),%ebx      # signals (bitmap, 32 signals)
    bsfl %ebx,%ecx              # %ecx is signal nr, return if none
    je 3f
    btrl %ecx,%ebx              # clear it
    movl %ebx,signal(%eax)
    movl sig_fn(%eax,%ecx,4),%ebx   # %ebx is signal handler address
    cmpl $1,%ebx
    jb default_signal           # 0 is default signal handler - exit
    je 2b                       # 1 is ignore - find next signal
    movl $0,sig_fn(%eax,%ecx,4) # reset signal handler address
    incl %ecx
    xchgl %ebx,EIP(%esp)        # put new return address on stack
    subl $28,OLDESP(%esp)
    movl OLDESP(%esp),%edx      # push old return address on stack
    pushl %eax                  # but first check that it's ok.
    pushl %ecx
    pushl $28
    pushl %edx
    call _verify_area
    popl %edx
    addl $4,%esp
    popl %ecx
    popl %eax
    movl restorer(%eax),%eax
    movl %eax,%fs:(%edx)        # flag/reg restorer
    movl %ecx,%fs:4(%edx)       # signal nr
    movl EAX(%esp),%eax
    movl %eax,%fs:8(%edx)       # old eax
    movl ECX(%esp),%eax
    movl %eax,%fs:12(%edx)      # old ecx
    movl EDX(%esp),%eax
    movl %eax,%fs:16(%edx)      # old edx
    movl EFLAGS(%esp),%eax
    movl %eax,%fs:20(%edx)      # old eflags
    movl %ebx,%fs:24(%edx)      # old return addr
3:  popl %eax
    popl %ebx
    popl %ecx
    popl %edx
    pop %fs
    pop %es
    pop %ds
    iret

まとめ

以上でLinux Ver 0.01の簡単な解説を終わります。ここではページの関係や解説者の力量の関係で、あまり細かいところに入ることは出来ませんでしたが、これくらいの指針があれば、実際のソースを追うのも、それ程難しいことではないと思います。全体でも高々1万行程ですから、巨大という程の規模でもありませんので、ぜひとも実際に追いかけてみて下さい。そうすればOSカーネルの基本的な構造だけではなく、雲の上の存在ではないLinusの姿も見えて来ると思います。また、これを見れば、意外に簡単そうだということがわかると思います。freeOSがこれだけ隆勢になっている現在、実用的な意味で同じようなものを作る必要はないかも知れませんが、UNIXとはまた別の方向のものを作ってみるのも面白いと思います。

なお、この説明の中で「○○のコードを見て下さい」という部分が多数あります。Ver 0.01のソースは手元にある人は少ないと思います。しかし、幸いなことにtsx-11には/pub/linux/kernels/Histricというディレクトリに古いカーネルソースが置いてあります。これをgetすればまさしくLinusが彼だけの手で書いたカーネルを見ることが出来ます。この説明を読むだけではなく、実際にソースを読んでみて下さい。