はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみにインスパイアされてxv6 OSを読んでます。
UNIX V6自体はx86CPUでは動作しないため、基本的には、UNIXv6をX86アーキテクチャで動くようにしたxv6 OSのリポジトリをForkしたkash1064/xv6-public: xv6 OSのソースコードを読んでいくことにしました。
前回はmain
関数で実行されるpicinit
関数とioapicinit
関数の動きを確認しました。
今回はconsoleinit
関数の挙動を追っていきます。
もくじ
consoleinit関数
consoleinit
関数はconsole.c
で以下のように定義されています。
void consoleinit(void)
{
initlock(&cons.lock, "console");
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
cons.locking = 1;
ioapicenable(IRQ_KBD, 0);
}
まず1行目のinitlock(&cons.lock, "console");
ですが、これはメモリ割り当て・排他制御 編で確認したメモリロックのためにspinlock
構造体を初期化する関数でした。
今回使用している&cons.lock
は、以下のように定義されています。
static struct {
struct spinlock lock;
int locking;
} cons;
なお、consoleinit
関数の中ではメモリロックは行われません。
consoleread
関数やconsolewrite
関数などが実行される際に、cons
が使われてメモリロックが行われます。
続いて、以下の行を見ていきます。
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
ここで参照しているdevsw
はdevsw
構造体の配列であり、この配列はfile.c
で定義されています。
struct devsw devsw[NDEV];
また、devsw
構造体の定義はfile.h
で行われています。
// table mapping major device number to
// device functions
struct devsw {
int (*read)(struct inode*, char*, int);
int (*write)(struct inode*, char*, int);
};
extern struct devsw devsw[];
#define CONSOLE 1
ちなみにNDEV
はparam.h
で10と定義されていることがわかります。
#define NDEV 10 // maximum major device number
devsw構造体について
ここで詳しく知りたいのは、devsw
構造体が何者かという点です。
UNIXのマニュアルなどを見ると、devsw
構造体はデバイスドライバがcharacter device interfaces
を持つ場合に使用されるもののように見えます。
参考:devsw(9) - NetBSD Manual Pages
また、以下のページのように、「システムが一文字ずつデータを転送する機器に対応」した入出力インターフェースがcharacter device interfaces
に該当すると考えられます。
ここでdevsw
構造体の定義を見ると、read
とwrite
の2種類の関数ポインタが設定されています。
ここには、consoleinit
関数のように任意の関数割り当てが行われます。
引数にはいずれもinode
構造体が含まれます。
inode
構造体は、devsw
構造体と同様にfile.h
で定義されています。
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT+1];
};
inodeとは
そもそもinode
とは何かについて触れておきます。
inode
とはざっくり言うとファイル、ディレクトリなどのファイルシステムのオブジェクトに関する情報が格納されている構造体です。
inode
の持つ情報としては以下のようなものが挙げられます。
- ファイルサイズ(バイト数)
- ファイルを格納しているデバイスのデバイスID
- ファイルの所有者、グループのID
- ファイルシステム内でファイルを識別するinode番号
- タイムスタンプ
xv6OSのinode
構造体を見ても上記に近い情報が格納されています。
inode
は、システム内に一意のIDで管理されます。(xv6OSでは恐らくinum
が該当する)
割り当て可能なinode
番号には通常上限があり、もしinode
番号が枯渇した場合は、ストレージデバイスのディスク容量に空きがあっても新規にファイルの作成ができなくなります。
一般的なLinuxシステムの場合は、df -i
コマンドで各デバイスごとの使用可能なinode
の上限を確認できます。
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
udev 1007124 449 1006675 1% /dev
tmpfs 1019154 919 1018235 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 1310720 380878 929842 30% /
tmpfs 1019154 1 1019153 1% /dev/shm
tmpfs 1019154 5 1019149 1% /run/lock
tmpfs 1019154 18 1019136 1% /sys/fs/cgroup
/dev/loop0 29 29 0 100% /snap/bare/5
/dev/loop2 10847 10847 0 100% /snap/core18/2284
/dev/loop1 10836 10836 0 100% /snap/core18/2253
/dev/loop3 11776 11776 0 100% /snap/core20/1270
/dev/loop5 18500 18500 0 100% /snap/gnome-3-34-1804/72
/dev/loop4 11777 11777 0 100% /snap/core20/1328
/dev/loop6 18500 18500 0 100% /snap/gnome-3-34-1804/77
/dev/loop7 65095 65095 0 100% /snap/gtk-common-themes/1519
/dev/loop8 796 796 0 100% /snap/lxd/21835
/dev/loop9 64986 64986 0 100% /snap/gtk-common-themes/1515
/dev/loop10 796 796 0 100% /snap/lxd/21545
/dev/loop11 479 479 0 100% /snap/snapd/14295
/dev/loop12 482 482 0 100% /snap/snapd/14549
/dev/sda2 65536 320 65216 1% /boot
tmpfs 1019154 45 1019109 1% /run/user/121
tmpfs 1019154 83 1019071 1% /run/user/1000
consolewrite関数を読む
この時点ではまだinode
を使ってファイルを作成することはないので、ひとまずxv6OSのコードに戻ります。
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
devsw
配列のCONSOLE = 1
要素のwrite
とread
には、それぞれconsole.c
で定義された関数が割り当てされます。
まずはconsolewrite
関数を読んでみます。
int consolewrite(struct inode *ip, char *buf, int n)
{
int i;
iunlock(ip);
acquire(&cons.lock);
for(i = 0; i < n; i++) consputc(buf[i] & 0xff);
release(&cons.lock);
ilock(ip);
return n;
}
consolewrite
関数はターゲットとなるinode
構造体変数のポインタとconsputc
関数に引き渡す文字およびその長さが引数として与えられます。
まずiunlock
関数ですが、これはfs.c
で定義されています。
// Unlock the given inode.
void iunlock(struct inode *ip)
{
if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1) panic("iunlock");
releasesleep(&ip->lock);
}
この関数についてはファイルシステムを扱う際に詳しく見ていきますが、受け渡しされたinode
の持つ‘sleeplock‘構造体を操作してロックを解放しています。
続いてはacquire
でロックを取得した後、consputc
関数に受け渡しされた文字列を一文字ずつ流し込んでいます。
この時、与えられた文字列は0xFF
とのANDになるので、印字可能な状態が担保されます。
void consputc(int c)
{
if(panicked){
cli();
for(;;) ;
}
if(c == BACKSPACE){
uartputc('\b'); uartputc(' '); uartputc('\b');
} else{
uartputc(c);
}
cgaputc(c);
}
ここで、与えられた値を引数としてuartputc
関数が呼び出されます。
uartputc
関数はuart.c
で定義された関数で、シリアルポート(UART)への空きこみを行います。
ここでは、COM1(I/Oポート 0x3f8)
に受け取った値を書き込んでいます。
void uartputc(int c)
{
int i;
if(!uart) return;
for(i = 0; i < 128 && !(inb(COM1+5) & 0x20); i++) microdelay(10);
outb(COM1+0, c);
}
COM1+0
はデータレジスタになっており、ここに値が書き込まれると送信バッファに書き込みが行われます。
その前の行のinb(COM1+5)
はラインステータスレジスタの値の読み取りを行っています。
ラインステータスレジスタの6番目のbitはTHRE
と呼ばれるレジスタで、このbitが立っているときは送信バッファが空で、新たなデータを送信可能であることを意味します。
つまり!(inb(COM1+5) & 0x20)
の行は、ラインステータスレジスタのTHRE
をチェックして、送信バッファが使用可能でない場合はmicrodelay
関数により処理を遅延させる処理を行っているわけです。
ちなみに、BACKSPACE
が入力された場合の書き込みがuartputc('\b'); uartputc(' '); uartputc('\b');
になっているのは、カーソルを一つ戻してスペースで上書きした上で、もう一度書き込み前の位置にカーソルを戻しているイメージみたいです。
ビデオメモリの書き込み
シリアルポートへの書き込みが完了したら、最後にcgaputc
関数が呼び出されます。
cgaputc
関数では、入力値をビデオメモリに書き込んで出力します。
static void cgaputc(int c)
{
int pos;
// Cursor position: col + 80*row.
outb(CRTPORT, 14);
pos = inb(CRTPORT+1) << 8;
outb(CRTPORT, 15);
pos |= inb(CRTPORT+1);
if(c == '\n') pos += 80 - pos%80;
else if(c == BACKSPACE){
if(pos > 0) --pos;
} else{
crt[pos++] = (c&0xff) | 0x0700; // black on white
}
if(pos < 0 || pos > 25*80) panic("pos under/overflow");
if((pos/80) >= 24){ // Scroll up.
memmove(crt, crt+80, sizeof(crt[0])*23*80);
pos -= 80;
memset(crt+pos, 0, sizeof(crt[0])*(24*80 - pos));
}
outb(CRTPORT, 14);
outb(CRTPORT+1, pos>>8);
outb(CRTPORT, 15);
outb(CRTPORT+1, pos);
crt[pos] = ' ' | 0x0700;
}
ここで使用している書き込み先のcrt
はアドレス0xb8000
の領域です。
この領域はフレームバッファと呼ばれる領域です。
//PAGEBREAK: 50
#define BACKSPACE 0x100
#define CRTPORT 0x3d4
static ushort *crt = (ushort*)P2V(0xb8000); // CGA memory
参考:フレームバッファ(frame buffer)とは - IT用語辞典 e-Words
まずCRTPORT
ですが、これはCRT Controller
のレジスタである0x3D4
を指しています。
これは制御用のレジスタで、0x3D4
に対応するデータレジスタ領域は0x3D5
となります。
この2つの領域を利用してコンソール上のカーソル位置をコントロールできます。
0x3D4
に14をセットすることで、16bitで表現されるカーソルの上位8bitを制御することを指定します。
そして、15をセットした場合は、カーソルの下位8bitを制御することを指定します。
つまり、以下の行では変数pos
に現在のカーソルの16bitを格納しているわけです。
outb(CRTPORT, 14);
pos = inb(CRTPORT+1) << 8;
outb(CRTPORT, 15);
pos |= inb(CRTPORT+1);
ここで取得したカーソルの上位bitと下位bitの関係は以下のようになります。
上位8bitがカーソルの行の位置を指し、下位8bitが何文字目かを指しています。
以下の行は、改行文字が渡された場合の挙動です。
if(c == '\n') pos += 80 - pos%80;
カーソル位置が次の行の一番左端の位置になるようにpos
を加算しています。
また、BACKSPACE
が与えられた場合はカーソル位置を一つ戻します。
文字入力が与えられた場合は、16bitの文字データを格納します。
else if(c == BACKSPACE){
if(pos > 0) --pos;
} else{
crt[pos++] = (c&0xff) | 0x0700; // black on white
}
この文字データの上位8bitには、背景と文字の色の情報が保持されます。
また、下位8bitには表示する文字が指定されます。
参考画像:3.-The Screen
実際にデバッガで確認してみると、この処理によってコンソールに文字が表示されていることを確認できます。
次の処理は非常にシンプルで、最大の行数である24行をオーバーした場合に、先頭行を削除してスクロールした上で、末尾の行を空行にしています。
if((pos/80) >= 24){ // Scroll up.
memmove(crt, crt+80, sizeof(crt[0])*23*80);
pos -= 80;
memset(crt+pos, 0, sizeof(crt[0])*(24*80 - pos));
}
最後の処理は、現在のカーソル位置をCRTPORT
とCRTPORT+1
に保存しています。
outb(CRTPORT, 14);
outb(CRTPORT+1, pos>>8);
outb(CRTPORT, 15);
outb(CRTPORT+1, pos);
crt[pos] = ' ' | 0x0700;
これでconsputc
関数によるコンソールへの書き込みが完了します。
consolewrite
関数に戻ったらメモリロックの解除とinode
のロックを行って終了です。
release(&cons.lock);
ilock(ip);
consoleread関数を読む
次はconsoleread
関数を読んでいきます。
consoleread
関数は、inode
と読み取り先のポインタ、読み取るバッファサイズを引数として受け取ります。
int consoleread(struct inode *ip, char *dst, int n)
{
uint target;
int c;
iunlock(ip);
target = n;
acquire(&cons.lock);
while(n > 0){
while(input.r == input.w){
if(myproc()->killed){
release(&cons.lock);
ilock(ip);
return -1;
}
sleep(&input.r, &cons.lock);
}
c = input.buf[input.r++ % INPUT_BUF];
if(c == C('D')){ // EOF
if(n < target){
// Save ^D for next time, to make sure
// caller gets a 0-byte result.
input.r--;
}
break;
}
*dst++ = c;
--n;
if(c == '\n')
break;
}
release(&cons.lock);
ilock(ip);
return target - n;
}
consolwrite
関数同様メモリロックとinode
のロック解除を行った後、指定されたバッファサイズのループ内で以下の処理を行います。
c = input.buf[input.r++ % INPUT_BUF];
if(c == C('D')){ // EOF
if(n < target){
// Save ^D for next time, to make sure
// caller gets a 0-byte result.
input.r--;
}
break;
}
*dst++ = c;
--n;
if(c == '\n') break;
input
構造体は以下の構造体です。
#define INPUT_BUF 128
struct {
char buf[INPUT_BUF];
uint r; // Read index
uint w; // Write index
uint e; // Edit index
} input;
このinput
構造体に入ってきた値を1文字ずつ読みだして取得しているようです。
実際にこの処理が呼び出されるのは、OSの起動が完了してからです。
具体的には、シェルに入力した文字を取得する際などに使用されます。
以下の画像は、コンソールに「l」という文字を打ち込んだ際の挙動をデバッガで確認したときのものです。
ユーザの入力値がどのようにinput.buf
に格納されるかは、実際にシェルが使えるようになってから詳しく追っていこうと思います。
最後にioapicenable(IRQ_KBD, 0);
で割込みを有効化して、consoleinit
関数は終了します。
まとめ
今回はコンソールの初期化を行いました。
入出力インターフェースの仕組みについて知ることができたので非常に興味深かったです。
次回はシリアルポートを初期化するuartinit
関数から見ていきたいと思います。