ブートローダーからジャンプしてくると、カーネルの初期化処理が実行されます。カーネルの各機能の初期化を行った後は、カーネルは割り込み駆動で動作し、何もしない時はhlt命令でCPUを寝させるようにしています。
カーネルの各機能の実装については、2016年4月に発表させていただいた勉強会のスライドにまとめています。各機能の実装について、図を使って説明していますので、興味があれば見てみてください。
リスト3.1: kernel/Makefile
1: CFLAGS = -Wall -Wextra 2: CFLAGS += -nostdinc -nostdlib -fno-builtin -c 3: CFLAGS += -Iinclude 4: CFLAGS += -m32 5: 6: .S.o: 7: gcc $(CFLAGS) -o $@ $< 8: .c.o: 9: gcc $(CFLAGS) -o $@ $< 10: 11: kernel.bin: sys.o cpu.o intr.o excp.o memory.o sched.o fs.o task.o \ 12: syscall.o lock.o timer.o console_io.o queue.o common.o \ 13: debug.o init.o kern_task_init.o 14: ld -m elf_i386 -o $@ $+ -Map System.map -s -T sys.ld -x 15: 16: sys.o: sys.S 17: cpu.o: cpu.c 18: intr.o: intr.c 19: excp.o: excp.c 20: memory.o: memory.c 21: sched.o: sched.c 22: fs.o: fs.c 23: task.o: task.c 24: syscall.o: syscall.c 25: lock.o: lock.c 26: timer.o: timer.c 27: console_io.o: console_io.c 28: queue.o: queue.c 29: common.o: common.c 30: debug.o: debug.c 31: init.o: init.c 32: kern_task_init.o: kern_task_init.c 33: 34: clean: 35: rm -f *~ *.o *.bin *.dat *.img *.map 36: 37: .PHONY: clean
カーネルをコンパイルするルールを記載したMakefileです。OS5のカーネルはLinux等と同じくモノリシックカーネルです。そのため、コンパイルが完了すると単一のバイナリができあがります。(このバイナリを上位のMakefileでブートローダーやユーザーランドと結合します。)
アセンブラのソースコードファイルの拡張子が、ブートローダーでは boot/boot.s と小文字の's'でしたが、sys.o: sys.Sのターゲット指定の通り、大文字の'S'になっています。また、アセンブラのビルド時に使用するコマンドも、ブートローダーではasコマンドでしたが、ここではgccを使用しています。これは、プリプロセスで展開されるマクロを記述するためです(kernel/sys.S で#includeマクロを使っています)。
ブートローダーのMakefileでも書きましたが、.S.oや.c.oといった書き方をしている(6~9行目)ため、各ターゲットをsys.o: sys.Sのように指定せねばならず、冗長ですね。
リスト3.2: kernel/sys.ld
1: OUTPUT_FORMAT("binary");
2:
3: SECTIONS
4: {
5: . = 0x7e00;
6: .text : {*(.text)}
7: .rodata : {
8: *(.strings)
9: *(.rodata)
10: *(.rodata.*)
11: }
12: .data : {*(.data)}
13: .bss : {*(.bss)}
14:
15: . = 0x7e00 + 0x91fe;
16: .sign : {SHORT(0xbeef)}
17: }
boot/boot.s の説明で、「OS5では、カーネルは0x0000 7e00から配置するように決めています。」と書いていましたが、「決めている」のがこのファイルです。5行目の. = 0x7e00;で、カーネルのバイナリは0x0000 7e00から配置されることを指定しています。そして、5行目に続いて.text : {*(.text)}と記載しており、カーネルのバイナリの先頭にテキスト領域を配置することを指定しています。
また、15行目の. = 0x7e00 + 0x91fe;で、カーネル領域のサイズを決めています。上位のMakefileでは「ブートローダー・カーネル・ユーザーランドのバイナリを単にcatで連結している」旨を説明しました。カーネルサイズの変化によってユーザーランドの開始アドレスが変化することが無いよう、カーネルサイズを固定しています。0x91feは10進数で37374です。2バイトのマジックナンバー(0xbeef)を含め、37376バイト(36.5KB)がカーネルで使用できる最大サイズです。また、0x7e00 + 0x91fe + 2(マジックナンバー) = 0x11000なので、ユーザーランドの開始アドレスは0x0001 1000です。
マジックナンバーに0xbeef(肉)等、16進数で表せる英単語を使用すると、16進数でメモリダンプしたときに確認できて便利です。このような表記法は"Hexspeak"と呼ばれ、0xBAADF00D("bad food")が「Microsoft WindowsのLocalAlloc関数の第一引数にLMEM_FIXEDを渡して呼び出してメモリを確保した場合に、ヒープに確保されたメモリが初期化されていないことを表す値として使用されている。」*1等、他にも様々なものがあります。詳しくは、Wikipedia等を参照してみると面白いです。
[*1] Hexspeak - Wikipedia: https://ja.wikipedia.org/wiki/Hexspeak
リスト3.3: kernel/include/asm/cpu.h
1: #ifndef _ASM_CPU_H_ 2: #define _ASM_CPU_H_ 3: 4: #define GDT_SIZE 16 5: 6: #endif /* _ASM_CPU_H_ */
後述する kernel/sys.S で#includeされるヘッダーファイルです。#define1つだけなので先に紹介してしまいます。
ここでは、GDTのサイズを定義しています。GDTはC言語からも参照することがあるので、ヘッダーファイルに分離しています。
リスト3.4: kernel/sys.S
1: #include <asm/cpu.h> 2: 3: .code32 4: 5: .text 6: 7: .global kern_init, idt, gdt, keyboard_handler, timer_handler 8: .global exception_handler, divide_error_handler, debug_handler 9: .global nmi_handler, breakpoint_handler, overflow_handler 10: .global bound_range_exceeded_handler, invalid_opcode_handler 11: .global device_not_available_handler, double_fault_handler 12: .global coprocessor_segment_overrun_handler, invalid_tss_handler 13: .global segment_not_present_handler, stack_fault_handler 14: .global general_protection_handler, page_fault_handler 15: .global x87_fpu_floating_point_error_handler, alignment_check_handler 16: .global machine_check_handler, simd_floating_point_handler 17: .global virtualization_handler, syscall_handler 18: 19: movl $0x00080000, %esp 20: 21: lgdt gdt_descr 22: 23: lea ignore_int, %edx 24: movl $0x00080000, %eax 25: movw %dx, %ax 26: movw $0x8E00, %dx 27: lea idt, %edi 28: mov $256, %ecx 29: rp_sidt: 30: movl %eax, (%edi) 31: movl %edx, 4(%edi) 32: addl $8, %edi 33: dec %ecx 34: jne rp_sidt 35: lidt idt_descr 36: 37: pushl $0 38: pushl $0 39: pushl $0 40: pushl $end 41: pushl $kern_init 42: ret 43: 44: end: 45: jmp end 46: 47: keyboard_handler: 48: pushal 49: call do_ir_keyboard 50: popal 51: iret 52: 53: timer_handler: 54: pushal 55: call do_ir_timer 56: popal 57: iret 58: 59: exception_handler: 60: popl excp_error_code 61: pushal 62: call do_exception 63: popal 64: iret 65: 66: /* interrupt 0 (#DE) */ 67: divide_error_handler: 68: jmp divide_error_handler 69: iret 70: 71: /* interrupt 1 (#DB) */ 72: debug_handler: 73: jmp debug_handler 74: iret 75: 76: /* interrupt 2 */ 77: nmi_handler: 78: jmp nmi_handler 79: iret 80: 81: /* interrupt 3 (#BP) */ 82: breakpoint_handler: 83: jmp breakpoint_handler 84: iret 85: 86: /* interrupt 4 (#OF) */ 87: overflow_handler: 88: jmp overflow_handler 89: iret 90: 91: /* interrupt 5 (#BR) */ 92: bound_range_exceeded_handler: 93: jmp bound_range_exceeded_handler 94: iret 95: 96: /* interrupt 6 (#UD) */ 97: invalid_opcode_handler: 98: jmp invalid_opcode_handler 99: iret 100: 101: /* interrupt 7 (#NM) */ 102: device_not_available_handler: 103: jmp device_not_available_handler 104: iret 105: 106: /* interrupt 8 (#DF) */ 107: double_fault_handler: 108: jmp double_fault_handler 109: iret 110: 111: /* interrupt 9 */ 112: coprocessor_segment_overrun_handler: 113: jmp coprocessor_segment_overrun_handler 114: iret 115: 116: /* interrupt 10 (#TS) */ 117: invalid_tss_handler: 118: jmp invalid_tss_handler 119: iret 120: 121: /* interrupt 11 (#NP) */ 122: segment_not_present_handler: 123: jmp segment_not_present_handler 124: iret 125: 126: /* interrupt 12 (#SS) */ 127: stack_fault_handler: 128: jmp stack_fault_handler 129: iret 130: 131: /* interrupt 13 (#GP) */ 132: general_protection_handler: 133: jmp general_protection_handler 134: iret 135: 136: /* interrupt 14 (#PF) */ 137: page_fault_handler: 138: popl excp_error_code 139: pushal 140: movl %cr2, %eax 141: pushl %eax 142: pushl excp_error_code 143: call do_page_fault 144: popl %eax 145: popl %eax 146: popal 147: iret 148: 149: /* interrupt 16 (#MF) */ 150: x87_fpu_floating_point_error_handler: 151: jmp x87_fpu_floating_point_error_handler 152: iret 153: 154: /* interrupt 17 (#AC) */ 155: alignment_check_handler: 156: jmp alignment_check_handler 157: iret 158: 159: /* interrupt 18 (#MC) */ 160: machine_check_handler: 161: jmp machine_check_handler 162: iret 163: 164: /* interrupt 19 (#XM) */ 165: simd_floating_point_handler: 166: jmp simd_floating_point_handler 167: iret 168: 169: /* interrupt 20 (#VE) */ 170: virtualization_handler: 171: jmp virtualization_handler 172: iret 173: 174: /* interrupt 128 */ 175: syscall_handler: 176: pushl %esp 177: pushl %ebp 178: pushl %esi 179: pushl %edi 180: pushl %edx 181: pushl %ecx 182: pushl %ebx 183: pushl %eax 184: call do_syscall 185: popl %ebx 186: popl %ebx 187: popl %ecx 188: popl %edx 189: popl %edi 190: popl %esi 191: popl %ebp 192: popl %esp 193: iret 194: 195: ignore_int: 196: iret 197: 198: .data 199: idt_descr: 200: .word 256*8-1 /* idt contains 256 entries */ 201: .long idt 202: 203: gdt_descr: 204: .word GDT_SIZE*8-1 205: .long gdt 206: 207: .balign 8 208: idt: 209: .fill 256, 8, 0 /* idt is uninitialized */ 210: 211: gdt: 212: .quad 0x0000000000000000 /* NULL descriptor */ 213: .quad 0x00cf9a000000ffff /* 4GB(r-x:Code, DPL=0) */ 214: .quad 0x00cf92000000ffff /* 4GB(rw-:Data, DPL=0) */ 215: .quad 0x00cffa000000ffff /* 4GB(r-x:Code, DPL=3) */ 216: .quad 0x00cff2000000ffff /* 4GB(rw-:Data, DPL=3) */ 217: .fill GDT_SIZE-5, 8, 0 218: 219: excp_error_code: 220: .long 0x00000000
カーネルのエントリ部分のソースコードです。 boot/boot.s 286行目のljmp $8, $0x7e00でジャンプする先はこのファイルの先頭です。3行目に.code32と書かれている通り、ここからは32ビットの命令です。
まず、19行目でスタックポインタを0x00080000に設定しています。そして、21行目でカーネルとユーザーランド動作中に使用するGDT(グローバルディスクリプタテーブル)を設定しています。gdt_descrラベルの内容は203~205行目にあります。ここで定数GDT_SIZEを使うために、asm/cpu.hをincludeしています。
23~35行目で割り込みハンドラの設定をしています。IDTの全256エントリをignore_intハンドラで初期化しています。ignore_intは195~196行目にあり、内容はiretでreturnするだけです。なお、スタックポインタを表す0x00080000は定数化すべきですね。同じ値を2度も書いているし、KERN_STACK_BASEとかの定数名にしておけば、この値がカーネルのスタックのベースアドレスであると一目でわかります。
その後、37~42行目で、関数からreturnするときと同じようにスタックに値を積み、ret命令でkern_init関数(kernel/init.c)へジャンプしています。スタックポインタの整合性さえ取れていれば、jmp命令でもよさそうですが、C言語の世界の関数呼び出しは、ret命令でcall命令でジャンプしret命令で戻る、という流れなので、一部のアセンブラコードで違ったことをするより、C言語の関数呼び出しの形式に統一しています。
以降は、個別の割り込みハンドラです。kernel/sys.Sでは割り込みハンドラの出入口部分のみで、メインの処理はC言語で記述された関数を呼び出しています。システムコールのソフトウェア割り込み(128番の割り込み)のみ、pushal/popalを使わず、汎用レジスタを一つずつpush/popしているのは、EAXをシステムコールの戻り値に使っているからです。
リスト3.5: kernel/init.c
1: #include <stddef.h>
2: #include <cpu.h>
3: #include <intr.h>
4: #include <excp.h>
5: #include <memory.h>
6: #include <console_io.h>
7: #include <timer.h>
8: #include <kernel.h>
9: #include <syscall.h>
10: #include <task.h>
11: #include <fs.h>
12: #include <sched.h>
13: #include <kern_task.h>
14: #include <list.h>
15: #include <queue.h>
16: #include <common.h>
17:
18: int kern_init(void)
19: {
20: extern unsigned char syscall_handler;
21:
22: unsigned char mask;
23: unsigned char i;
24:
25: /* Setup console */
26: cursor_pos.y += 2;
27: update_cursor();
28:
29: /* Setup exception handler */
30: for (i = 0; i < EXCEPTION_MAX; i++)
31: intr_set_handler(i, (unsigned int)&exception_handler);
32: intr_set_handler(EXCP_NUM_DE, (unsigned int)÷_error_handler);
33: intr_set_handler(EXCP_NUM_DB, (unsigned int)&debug_handler);
34: intr_set_handler(EXCP_NUM_NMI, (unsigned int)&nmi_handler);
35: intr_set_handler(EXCP_NUM_BP, (unsigned int)&breakpoint_handler);
36: intr_set_handler(EXCP_NUM_OF, (unsigned int)&overflow_handler);
37: intr_set_handler(EXCP_NUM_BR, (unsigned int)&bound_range_exceeded_handler);
38: intr_set_handler(EXCP_NUM_UD, (unsigned int)&invalid_opcode_handler);
39: intr_set_handler(EXCP_NUM_NM, (unsigned int)&device_not_available_handler);
40: intr_set_handler(EXCP_NUM_DF, (unsigned int)&double_fault_handler);
41: intr_set_handler(EXCP_NUM_CSO,
42: (unsigned int)&coprocessor_segment_overrun_handler);
43: intr_set_handler(EXCP_NUM_TS, (unsigned int)&invalid_tss_handler);
44: intr_set_handler(EXCP_NUM_NP, (unsigned int)&segment_not_present_handler);
45: intr_set_handler(EXCP_NUM_SS, (unsigned int)&stack_fault_handler);
46: intr_set_handler(EXCP_NUM_GP, (unsigned int)&general_protection_handler);
47: intr_set_handler(EXCP_NUM_PF, (unsigned int)&page_fault_handler);
48: intr_set_handler(EXCP_NUM_MF,
49: (unsigned int)&x87_fpu_floating_point_error_handler);
50: intr_set_handler(EXCP_NUM_AC, (unsigned int)&alignment_check_handler);
51: intr_set_handler(EXCP_NUM_MC, (unsigned int)&machine_check_handler);
52: intr_set_handler(EXCP_NUM_XM, (unsigned int)&simd_floating_point_handler);
53: intr_set_handler(EXCP_NUM_VE, (unsigned int)&virtualization_handler);
54:
55: /* Setup devices */
56: con_init();
57: timer_init();
58: mem_init();
59:
60: /* Setup File System */
61: fs_init((void *)0x00011000);
62:
63: /* Setup tasks */
64: kern_task_init();
65: task_init(fshell, 0, NULL);
66:
67: /* Start paging */
68: mem_page_start();
69:
70: /* Setup interrupt handler and mask register */
71: intr_set_handler(INTR_NUM_TIMER, (unsigned int)&timer_handler);
72: intr_set_handler(INTR_NUM_KB, (unsigned int)&keyboard_handler);
73: intr_set_handler(INTR_NUM_USER128, (unsigned int)&syscall_handler);
74: intr_init();
75: mask = intr_get_mask_master();
76: mask &= ~(INTR_MASK_BIT_TIMER | INTR_MASK_BIT_KB);
77: intr_set_mask_master(mask);
78: sti();
79:
80: /* End of kernel initialization process */
81: while (1) {
82: x86_halt();
83: }
84:
85: return 0;
86: }
ブートローダー・カーネルと来て、ここがC言語のスタート地点です。ここではカーネルの初期化を行うkern_init関数を定義しています。なお、kern_init関数は69行もあり、OS5の中で3番目に長い関数です。
初期化の流れは以下の通りです。
2.と7.を除き、各処理は関数化されており、初期化処理の本体は各関数内で行っています。初期化処理の内容については各ソースコードで説明します。なお、2.と7.の処理について、例外・割り込み共にintr_set_handler関数でハンドラを設定しています。第1引数が割り込み/例外のベクタ番号で、第2引数がハンドラの先頭アドレスです。kernel/sys.S で定義していたハンドラはここで設定されます。そして、7.のintr_init関数で割り込み初期化後、割り込みマスクの設定でタイマーとキーボードのみ割り込みを有効化しています。sti関数でアセンブラのsti命令を呼び出すことにより、CPUの割り込み機能が有効化されます。
カーネル自体はイベントドリブンで動作するように作っています。そのため、カーネルの各機能の設定を終えると8.でx86_halt関数(割り込み等が発生するまで命令実行を停止させるx86のhlt命令を呼び出す関数)を呼び出し、でCPUを寝かせます。
カーネル起動時に最初に起動されるタスクであるシェルはどのように起動されるのかというと、4.でシェルの実行バイナリを見つけ、5.でカーネルのタスクの枠組みへシェルを設定し、6.でメモリ周りの設定を行い、7.の割り込み有効化でタイマー割り込みが開始することでスケジューラが動作を開始する、という流れです。スケジューラが動作を開始すると、10ms周期のタイマー割り込み契機でランキューのタスクを切り替えてタスクを実行します。
定数化できていないマジックナンバーについて、fs_init関数に渡している"0x00011000"は、ユーザーランドの先頭アドレスです。
リスト3.6: kernel/include/kernel.h
1: #ifndef _KERNEL_H_
2: #define _KERNEL_H_
3:
4: enum {
5: SYSCALL_TIMER_GET_GLOBAL_COUNTER = 1,
6: SYSCALL_SCHED_WAKEUP_MSEC,
7: SYSCALL_SCHED_WAKEUP_EVENT,
8: SYSCALL_CON_GET_CURSOR_POS_Y,
9: SYSCALL_CON_PUT_STR,
10: SYSCALL_CON_PUT_STR_POS,
11: SYSCALL_CON_DUMP_HEX,
12: SYSCALL_CON_DUMP_HEX_POS,
13: SYSCALL_CON_GET_LINE,
14: SYSCALL_OPEN,
15: SYSCALL_EXEC,
16: SYSCALL_EXIT
17: };
18:
19: enum {
20: EVENT_TYPE_KBD = 1,
21: EVENT_TYPE_EXIT
22: };
23:
24: #endif /* _KERNEL_H_ */
カーネルがユーザーランド(アプリケーション郡)へ公開するシステムコールを定義しています。
現状、カーネルとアプリケーションの間のAPIはシステムコールのみです。
また、UNIXのようにデバイスなどをファイルにしていない事もあり、ひたすらシステムコールが増えていく枠組みです。ただし、システムコールのインタフェースは単純なので、これはこれでシンプルで良いかなとも思います。
リスト3.7: kernel/include/intr.h
1: #ifndef _INTR_H_ 2: #define _INTR_H_ 3: 4: #define IOADR_MPIC_OCW2 0x0020 5: #define IOADR_MPIC_OCW2_BIT_MANUAL_EOI 0x60 6: #define IOADR_MPIC_ICW1 0x0020 7: #define IOADR_MPIC_ICW2 0x0021 8: #define IOADR_MPIC_ICW3 0x0021 9: #define IOADR_MPIC_ICW4 0x0021 10: #define IOADR_MPIC_OCW1 0x0021 11: #define IOADR_SPIC_ICW1 0x00a0 12: #define IOADR_SPIC_ICW2 0x00a1 13: #define IOADR_SPIC_ICW3 0x00a1 14: #define IOADR_SPIC_ICW4 0x00a1 15: #define IOADR_SPIC_OCW1 0x00a1 16: 17: #define INTR_NUM_USER128 0x80 18: 19: void intr_init(void); 20: void intr_set_mask_master(unsigned char mask); 21: unsigned char intr_get_mask_master(void); 22: void intr_set_mask_slave(unsigned char mask); 23: unsigned char intr_get_mask_slave(void); 24: void intr_set_handler(unsigned char intr_num, unsigned int handler_addr); 25: 26: #endif /* _INTR_H_ */
割り込みコントローラ(PIC:Programmable Interrupt Controller)のレジスタのIOアドレスのdefineと、割り込み設定関数のプロトタイプ宣言です。
リスト3.8: kernel/intr.c
1: #include <intr.h>
2: #include <io_port.h>
3:
4: void intr_init(void)
5: {
6: /* マスタPICの初期化 */
7: outb_p(0x11, IOADR_MPIC_ICW1);
8: outb_p(0x20, IOADR_MPIC_ICW2);
9: outb_p(0x04, IOADR_MPIC_ICW3);
10: outb_p(0x01, IOADR_MPIC_ICW4);
11: outb_p(0xff, IOADR_MPIC_OCW1);
12:
13: /* スレーブPICの初期化 */
14: outb_p(0x11, IOADR_SPIC_ICW1);
15: outb_p(0x28, IOADR_SPIC_ICW2);
16: outb_p(0x02, IOADR_SPIC_ICW3);
17: outb_p(0x01, IOADR_SPIC_ICW4);
18: outb_p(0xff, IOADR_SPIC_OCW1);
19: }
20:
21: void intr_set_mask_master(unsigned char mask)
22: {
23: outb_p(mask, IOADR_MPIC_OCW1);
24: }
25:
26: unsigned char intr_get_mask_master(void)
27: {
28: return inb_p(IOADR_MPIC_OCW1);
29: }
30:
31: void intr_set_mask_slave(unsigned char mask)
32: {
33: outb_p(mask, IOADR_SPIC_OCW1);
34: }
35:
36: unsigned char intr_get_mask_slave(void)
37: {
38: return inb_p(IOADR_SPIC_OCW1);
39: }
40:
41: void intr_set_handler(unsigned char intr_num, unsigned int handler_addr)
42: {
43: extern unsigned char idt;
44: unsigned int intr_dscr_top_half, intr_dscr_bottom_half;
45: unsigned int *idt_ptr;
46:
47: idt_ptr = (unsigned int *)&idt;
48: intr_dscr_bottom_half = handler_addr;
49: intr_dscr_top_half = 0x00080000;
50: intr_dscr_top_half = (intr_dscr_top_half & 0xffff0000)
51: | (intr_dscr_bottom_half & 0x0000ffff);
52: intr_dscr_bottom_half = (intr_dscr_bottom_half & 0xffff0000) | 0x00008e00;
53: if (intr_num == INTR_NUM_USER128)
54: intr_dscr_bottom_half |= 3 << 13;
55: idt_ptr += intr_num * 2;
56: *idt_ptr = intr_dscr_top_half;
57: *(idt_ptr + 1) = intr_dscr_bottom_half;
58: }
割り込み/例外の初期化や設定を行う関数群を定義しています。ハードウェアとしてはプログラマブルインタラプトコントローラ(PIC)を扱う関数群です。
intr_init関数では割り込み番号の開始を「0x20(32)番以降」に設定しています(outb_p(0x20, IOADR_MPIC_ICW2)でマスタPICを0x20〜に設定し、outb_p(0x28, IOADR_SPIC_ICW2)でスレーブPICを0x28〜に設定)。割り込み番号も例外番号も同じ番号の空間なので、割り込み番号の開始を「0番以降」としてしまうと、割り込みの0番と例外の0番でバッティングします。例外は0〜20(0x14)番まであって、こちらは変更できないので、割り込み番号を0x20〜にしています。
割り込み番号の開始番号(0x20)や初期化の値は書籍「パソコンのレガシィI/O 活用大全」を参考にしています。なお、割り込み番号の開始が0x20なのはLinuxカーネルでもそうだったと思います(違っていたらゴメンナサイ)。
リスト3.9: kernel/include/excp.h
1: #ifndef _EXCP_H_ 2: #define _EXCP_H_ 3: 4: #define EXCEPTION_MAX 21 5: #define EXCP_NUM_DE 0 6: #define EXCP_NUM_DB 1 7: #define EXCP_NUM_NMI 2 8: #define EXCP_NUM_BP 3 9: #define EXCP_NUM_OF 4 10: #define EXCP_NUM_BR 5 11: #define EXCP_NUM_UD 6 12: #define EXCP_NUM_NM 7 13: #define EXCP_NUM_DF 8 14: #define EXCP_NUM_CSO 9 15: #define EXCP_NUM_TS 10 16: #define EXCP_NUM_NP 11 17: #define EXCP_NUM_SS 12 18: #define EXCP_NUM_GP 13 19: #define EXCP_NUM_PF 14 20: #define EXCP_NUM_MF 16 21: #define EXCP_NUM_AC 17 22: #define EXCP_NUM_MC 18 23: #define EXCP_NUM_XM 19 24: #define EXCP_NUM_VE 20 25: 26: extern unsigned char exception_handler; 27: extern unsigned char divide_error_handler; 28: extern unsigned char debug_handler; 29: extern unsigned char nmi_handler; 30: extern unsigned char breakpoint_handler; 31: extern unsigned char overflow_handler; 32: extern unsigned char bound_range_exceeded_handler; 33: extern unsigned char invalid_opcode_handler; 34: extern unsigned char device_not_available_handler; 35: extern unsigned char double_fault_handler; 36: extern unsigned char coprocessor_segment_overrun_handler; 37: extern unsigned char invalid_tss_handler; 38: extern unsigned char segment_not_present_handler; 39: extern unsigned char stack_fault_handler; 40: extern unsigned char general_protection_handler; 41: extern unsigned char page_fault_handler; 42: extern unsigned char x87_fpu_floating_point_error_handler; 43: extern unsigned char alignment_check_handler; 44: extern unsigned char machine_check_handler; 45: extern unsigned char simd_floating_point_handler; 46: extern unsigned char virtualization_handler; 47: 48: void do_exception(void); 49: void do_page_fault(unsigned int error_code, unsigned int address); 50: 51: #endif /* _EXCP_H_ */
例外の番号とアセンブラ・C言語側のハンドラの定義です。
# なぜenumを使わないのか。。。
リスト3.10: kernel/excp.c
1: #include <excp.h>
2: #include <console_io.h>
3:
4: void do_exception(void)
5: {
6: put_str("exception\r\n");
7: while (1);
8: }
9:
10: void do_page_fault(unsigned int error_code, unsigned int address)
11: {
12: put_str("page fault\r\n");
13: put_str("error code: 0x");
14: dump_hex(error_code, 8);
15: put_str("\r\n");
16: put_str("address : 0x");
17: dump_hex(address, 8);
18: put_str("\r\n");
19: while (1);
20: }
kernel/sys.Sの例外のハンドラから呼び出される本体の処理を記述しています。
例外は起きてしまった場合、デバッグしなければならないので、例外に陥った時点でハンドラ内でwhile (1)でブロックするようにしています。
リスト3.11: kernel/include/memory.h
1: #ifndef __MEMORY_H__
2: #define __MEMORY_H__
3:
4: #define PAGE_SIZE 0x1000
5: #define PAGE_ADDR_MASK 0xfffff000
6:
7: struct page_directory_entry {
8: union {
9: struct {
10: unsigned int all;
11: };
12: struct {
13: unsigned int p: 1, r_w: 1, u_s: 1, pwt: 1, pcd: 1, a: 1,
14: reserved: 1, ps: 1, g: 1, usable: 3,
15: pt_base: 20;
16: };
17: };
18: };
19: struct page_table_entry {
20: union {
21: struct {
22: unsigned int all;
23: };
24: struct {
25: unsigned int p: 1, r_w: 1, u_s: 1, pwt: 1, pcd: 1, a: 1,
26: d: 1, pat: 1, g: 1, usable: 3, page_base: 20;
27: };
28: };
29: };
30:
31: void mem_init(void);
32: void mem_page_start(void);
33: void *mem_alloc(void);
34: void mem_free(void *page);
35:
36: #endif /* __MEMORY_H__ */
MMU(メモリ管理ユニット)周りの定数、構造体、関数の定義です。
リスト3.12: kernel/memory.c
1: #include <stddef.h>
2: #include <memory.h>
3:
4: #define CR4_BIT_PGE (1U << 7)
5: #define MAX_HEAP_PAGES 64
6: #define HEAP_START_ADDR 0x00040000
7:
8: static char heap_alloc_table[MAX_HEAP_PAGES] = {0};
9:
10: void mem_init(void)
11: {
12: struct page_directory_entry *pde;
13: struct page_table_entry *pte;
14: unsigned int paging_base_addr;
15: unsigned int i;
16: unsigned int cr4;
17:
18: /* Enable PGE(Page Global Enable) flag of CR4*/
19: __asm__("movl %%cr4, %0":"=r"(cr4):);
20: cr4 |= CR4_BIT_PGE;
21: __asm__("movl %0, %%cr4"::"r"(cr4));
22:
23: /* Initialize kernel page directory */
24: pde = (struct page_directory_entry *)0x0008f000;
25: pde->all = 0;
26: pde->p = 1;
27: pde->r_w = 1;
28: pde->pt_base = 0x00090;
29: pde++;
30: for (i = 1; i < 0x400; i++) {
31: pde->all = 0;
32: pde++;
33: }
34:
35: /* Initialize kernel page table */
36: pte = (struct page_table_entry *)0x00090000;
37: for (i = 0x000; i < 0x007; i++) {
38: pte->all = 0;
39: pte++;
40: }
41: paging_base_addr = 0x00007;
42: for (; i <= 0x085; i++) {
43: pte->all = 0;
44: pte->p = 1;
45: pte->r_w = 1;
46: pte->g = 1;
47: pte->page_base = paging_base_addr;
48: paging_base_addr += 0x00001;
49: pte++;
50: }
51: for (; i < 0x095; i++) {
52: pte->all = 0;
53: pte++;
54: }
55: paging_base_addr = 0x00095;
56: for (; i <= 0x09f; i++) {
57: pte->all = 0;
58: pte->p = 1;
59: pte->r_w = 1;
60: pte->g = 1;
61: pte->page_base = paging_base_addr;
62: paging_base_addr += 0x00001;
63: pte++;
64: }
65: for (; i < 0x0b8; i++) {
66: pte->all = 0;
67: pte++;
68: }
69: paging_base_addr = 0x000b8;
70: for (; i <= 0x0bf; i++) {
71: pte->all = 0;
72: pte->p = 1;
73: pte->r_w = 1;
74: pte->pwt = 1;
75: pte->pcd = 1;
76: pte->g = 1;
77: pte->page_base = paging_base_addr;
78: paging_base_addr += 0x00001;
79: pte++;
80: }
81: for (; i < 0x400; i++) {
82: pte->all = 0;
83: pte++;
84: }
85: }
86:
87: void mem_page_start(void)
88: {
89: unsigned int cr0;
90:
91: __asm__("movl %%cr0, %0":"=r"(cr0):);
92: cr0 |= 0x80000000;
93: __asm__("movl %0, %%cr0"::"r"(cr0));
94: }
95:
96: void *mem_alloc(void)
97: {
98: unsigned int i;
99:
100: for (i = 0; heap_alloc_table[i] && (i < MAX_HEAP_PAGES); i++);
101:
102: if (i >= MAX_HEAP_PAGES)
103: return (void *)NULL;
104:
105: heap_alloc_table[i] = 1;
106: return (void *)(HEAP_START_ADDR + i * PAGE_SIZE);
107: }
108:
109: void mem_free(void *page)
110: {
111: unsigned int i = ((unsigned int)page - HEAP_START_ADDR) / PAGE_SIZE;
112: heap_alloc_table[i] = 0;
113: }
メモリ関係の関数群で、主にCPUのメモリ管理ユニット(MMU)の設定を行います。MMUはページングという機能を提供するものです。ページングは、メモリを「ページ」という単位で分割し、「仮想アドレス」という実際のアドレス(物理アドレス)とは別のアドレスを割り当てて管理します。そして、仮想アドレスから物理アドレスへの変換表を「ページテーブル」と呼びます。変換の流れを図3.1に示します。なお、複数のページテーブルをまとめたものを「ページディレクトリ」と呼びます。
図3.1: ページテーブルとMMU
CPUの設定でページングを有効化すると、カーネルやアプリケーションは仮想アドレスで動作するようになります。これにより、「アプリケーションはカーネルの領域へアクセスさせない」、「アプリケーションはすべて同じアドレスから実行を開始する」といったことを実現しています。なお、ページサイズは4KBです*2。また、仮想アドレスは"Virtual Address"で"VA"、物理アドレスは"Physical Address"で"PA"などと記載されていたりもします。
[*2] CPUの設定で変更できます。
ページディレクトリとページテーブルへ設定を追加し、あるVAをPAに対応付けることを「マッピング」、「マップする」の様に呼びます。OS5でのマッピングに関して、まず、OS5ではVAの0x0000 0000~0x1FFF FFFFをカーネル空間、0x2000 0000~0xFFFF FFFFをユーザ空間としています(図3.2)。カーネル空間はVA=PAとなるようマップしています(図3.3)。0x0000 0000~0x1FFF FFFFのアドレス空間内には、カーネルやユーザーランドの実行バイナリ等を配置している「コンベンショナルメモリ」が全て含まれます。そのため、カーネルはRAMに配置したすべての資源にアクセスできることになります。ユーザ空間は実行するタスク*7ごとにマップを変えます。例えば、shellの実行時はユーザ空間をshellが配置されているPAへマップします(図3.4)。
[*7] タスクとアプリケーションは同じものを指します。カーネルではCPUのデータシートの表現に合わせて「タスク」と呼び、ユーザーランドでは直観的な分かりやすさから「アプリケーション」と呼んでいます。
図3.2: VAのマッピングについて(1)
図3.3: VAのマッピングについて(2)
図3.4: VAのマッピングについて(3)
kernel/init.cのkern_init関数からは、mem_init関数とmem_page_start関数を呼び出しています。共にMMUのページング機能の関数で、mem_initで設定し、mem_page_startで有効化します。mem_initではグローバルページ機能を有効化した後、カーネルのページディレクトリ/テーブルを設定しています。なお、mem_init関数はOS5で2番目に長い関数です(76行)。
残る2つの関数はメモリの動的確保(mem_alloc関数)と解放(mem_free関数)です。動的確保/解放はページサイズに合わせて4KB単位で、heap_alloc_tableという配列で管理しています。
仮想アドレス空間は0x0000 0000〜0x1fff ffffがカーネル空間で、0x2000 0000〜0xffff ffffがユーザ空間です。カーネル空間へは物理アドレスの同じアドレス(0x0000 0000〜0x1fff ffff)が対応付けられています(マップされています)。
「仮想アドレス」という言い回しは、実はIntel CPUの言い回しではないです。ページングの仕組みをARM CPUで先に勉強していて、Intel CPUの「リニアアドレス」よりわかりやすい気がして、ページングに関しては「仮想アドレス」という言い回しを使っています。ちなみに、x86はページングの他にセグメンテーションという仕組みもあるため、物理アドレスへ至る流れは「論理アドレス(セグメンテーション)」→「リニアアドレス(ページング)」→「物理アドレス」となります。
セグメンテーションもページングと同じく物理アドレスを分割し、「論理アドレス」というアドレスを割り当てて管理する機能です。「論理アドレス」という形でページングと同様に「仮想アドレス」を提供できます。(ちなみに、セグメンテーションにもページフォルト例外同様に「セグメント不在例外」があります。)
ハードウェアが持つ機能は積極的に使うようにしていきたいのですが、ページングで事足りているため、セグメンテーションは使用していません。ただし、ページングと違いセグメンテーションは機能として無効化することができないので、1つのセグメントがメモリ空間全て(0x0000 0000〜0xffff ffff)を指すように設定しています(kernel/sys.Sの211〜217行目)。
リスト3.13: kernel/include/sched.h
1: #ifndef _SCHED_H_ 2: #define _SCHED_H_ 3: 4: #include <cpu.h> 5: #include <task.h> 6: 7: #define TASK_NUM 3 8: 9: extern struct task task_instance_table[TASK_NUM]; 10: extern struct task *current_task; 11: 12: unsigned short sched_get_current(void); 13: int sched_runq_enq(struct task *t); 14: int sched_runq_del(struct task *t); 15: void schedule(void); 16: int sched_update_wakeupq(void); 17: void wakeup_after_msec(unsigned int msec); 18: int sched_update_wakeupevq(unsigned char event_type); 19: void wakeup_after_event(unsigned char event_type); 20: 21: #endif /* _SCHED_H_ */
スケジューラに関するヘッダファイルです。
リスト3.14: kernel/sched.c
1: #include <stddef.h>
2: #include <sched.h>
3: #include <cpu.h>
4: #include <io_port.h>
5: #include <intr.h>
6: #include <timer.h>
7: #include <lock.h>
8: #include <kern_task.h>
9:
10: static struct {
11: struct task *head;
12: unsigned int len;
13: } run_queue = {NULL, 0};
14: static struct {
15: struct task *head;
16: unsigned int len;
17: } wakeup_queue = {NULL, 0};
18: static struct {
19: struct task *head;
20: unsigned int len;
21: } wakeup_event_queue = {NULL, 0};
22: static struct task dummy_task;
23: static unsigned char is_task_switched_in_time_slice = 0;
24:
25: struct task task_instance_table[TASK_NUM];
26: struct task *current_task = NULL;
27:
28: unsigned short sched_get_current(void)
29: {
30: return x86_get_tr() / 8;
31: }
32:
33: int sched_runq_enq(struct task *t)
34: {
35: unsigned char if_bit;
36:
37: kern_lock(&if_bit);
38:
39: if (run_queue.head) {
40: t->prev = run_queue.head->prev;
41: t->next = run_queue.head;
42: run_queue.head->prev->next = t;
43: run_queue.head->prev = t;
44: } else {
45: t->prev = t;
46: t->next = t;
47: run_queue.head = t;
48: }
49: run_queue.len++;
50:
51: kern_unlock(&if_bit);
52:
53: return 0;
54: }
55:
56: int sched_runq_del(struct task *t)
57: {
58: unsigned char if_bit;
59:
60: if (!run_queue.head)
61: return -1;
62:
63: kern_lock(&if_bit);
64:
65: if (run_queue.head->next != run_queue.head) {
66: if (run_queue.head == t)
67: run_queue.head = run_queue.head->next;
68: t->prev->next = t->next;
69: t->next->prev = t->prev;
70: } else
71: run_queue.head = NULL;
72: run_queue.len--;
73:
74: kern_unlock(&if_bit);
75:
76: return 0;
77: }
78:
79: void schedule(void)
80: {
81: if (!run_queue.head) {
82: if (current_task) {
83: current_task = NULL;
84: outb_p(IOADR_MPIC_OCW2_BIT_MANUAL_EOI | INTR_IR_TIMER,
85: IOADR_MPIC_OCW2);
86: task_instance_table[KERN_TASK_ID].context_switch();
87: }
88: } else if (current_task) {
89: if (current_task != current_task->next) {
90: current_task = current_task->next;
91: if (is_task_switched_in_time_slice) {
92: current_task->task_switched_in_time_slice = 1;
93: is_task_switched_in_time_slice = 0;
94: }
95: outb_p(IOADR_MPIC_OCW2_BIT_MANUAL_EOI | INTR_IR_TIMER,
96: IOADR_MPIC_OCW2);
97: current_task->context_switch();
98: }
99: } else {
100: current_task = run_queue.head;
101: if (is_task_switched_in_time_slice) {
102: current_task->task_switched_in_time_slice = 1;
103: is_task_switched_in_time_slice = 0;
104: }
105: outb_p(IOADR_MPIC_OCW2_BIT_MANUAL_EOI | INTR_IR_TIMER,
106: IOADR_MPIC_OCW2);
107: current_task->context_switch();
108: }
109: }
110:
111: int sched_wakeupq_enq(struct task *t)
112: {
113: unsigned char if_bit;
114:
115: kern_lock(&if_bit);
116:
117: if (wakeup_queue.head) {
118: t->prev = wakeup_queue.head->prev;
119: t->next = wakeup_queue.head;
120: wakeup_queue.head->prev->next = t;
121: wakeup_queue.head->prev = t;
122: } else {
123: t->prev = t;
124: t->next = t;
125: wakeup_queue.head = t;
126: }
127: wakeup_queue.len++;
128:
129: kern_unlock(&if_bit);
130:
131: return 0;
132: }
133:
134: int sched_wakeupq_del(struct task *t)
135: {
136: unsigned char if_bit;
137:
138: if (!wakeup_queue.head)
139: return -1;
140:
141: kern_lock(&if_bit);
142:
143: if (wakeup_queue.head->next != wakeup_queue.head) {
144: if (wakeup_queue.head == t)
145: wakeup_queue.head = wakeup_queue.head->next;
146: t->prev->next = t->next;
147: t->next->prev = t->prev;
148: } else
149: wakeup_queue.head = NULL;
150: wakeup_queue.len--;
151:
152: kern_unlock(&if_bit);
153:
154: return 0;
155: }
156:
157: int sched_update_wakeupq(void)
158: {
159: struct task *t, *next;
160: unsigned char if_bit;
161:
162: if (!wakeup_queue.head)
163: return -1;
164:
165: kern_lock(&if_bit);
166:
167: t = wakeup_queue.head;
168: do {
169: next = t->next;
170: if (t->wakeup_after_msec > TIMER_TICK_MS) {
171: t->wakeup_after_msec -= TIMER_TICK_MS;
172: } else {
173: t->wakeup_after_msec = 0;
174: sched_wakeupq_del(t);
175: sched_runq_enq(t);
176: }
177: t = next;
178: } while (wakeup_queue.head && t != wakeup_queue.head);
179:
180: kern_unlock(&if_bit);
181:
182: return 0;
183: }
184:
185: void wakeup_after_msec(unsigned int msec)
186: {
187: unsigned char if_bit;
188:
189: kern_lock(&if_bit);
190:
191: if (current_task->next != current_task)
192: dummy_task.next = current_task->next;
193: current_task->wakeup_after_msec = msec;
194: sched_runq_del(current_task);
195: sched_wakeupq_enq(current_task);
196: current_task = &dummy_task;
197: is_task_switched_in_time_slice = 1;
198: schedule();
199:
200: kern_unlock(&if_bit);
201: }
202:
203: int sched_wakeupevq_enq(struct task *t)
204: {
205: unsigned char if_bit;
206:
207: kern_lock(&if_bit);
208:
209: if (wakeup_event_queue.head) {
210: t->prev = wakeup_event_queue.head->prev;
211: t->next = wakeup_event_queue.head;
212: wakeup_event_queue.head->prev->next = t;
213: wakeup_event_queue.head->prev = t;
214: } else {
215: t->prev = t;
216: t->next = t;
217: wakeup_event_queue.head = t;
218: }
219: wakeup_event_queue.len++;
220:
221: kern_unlock(&if_bit);
222:
223: return 0;
224: }
225:
226: int sched_wakeupevq_del(struct task *t)
227: {
228: unsigned char if_bit;
229:
230: if (!wakeup_event_queue.head)
231: return -1;
232:
233: kern_lock(&if_bit);
234:
235: if (wakeup_event_queue.head->next != wakeup_event_queue.head) {
236: if (wakeup_event_queue.head == t)
237: wakeup_event_queue.head = wakeup_event_queue.head->next;
238: t->prev->next = t->next;
239: t->next->prev = t->prev;
240: } else
241: wakeup_event_queue.head = NULL;
242: wakeup_event_queue.len--;
243:
244: kern_unlock(&if_bit);
245:
246: return 0;
247: }
248:
249: int sched_update_wakeupevq(unsigned char event_type)
250: {
251: struct task *t, *next;
252: unsigned char if_bit;
253:
254: if (!wakeup_event_queue.head)
255: return -1;
256:
257: kern_lock(&if_bit);
258:
259: t = wakeup_event_queue.head;
260: do {
261: next = t->next;
262: if (t->wakeup_after_event == event_type) {
263: t->wakeup_after_event = 0;
264: sched_wakeupevq_del(t);
265: sched_runq_enq(t);
266: }
267: t = next;
268: } while (wakeup_event_queue.head && t != wakeup_event_queue.head);
269:
270: kern_unlock(&if_bit);
271:
272: return 0;
273: }
274:
275: void wakeup_after_event(unsigned char event_type)
276: {
277: unsigned char if_bit;
278:
279: kern_lock(&if_bit);
280:
281: if (current_task->next != current_task)
282: dummy_task.next = current_task->next;
283: current_task->wakeup_after_event = event_type;
284: sched_runq_del(current_task);
285: sched_wakeupevq_enq(current_task);
286: current_task = &dummy_task;
287: is_task_switched_in_time_slice = 1;
288: schedule();
289:
290: kern_unlock(&if_bit);
291: }
スケジューラの関数を定義しています。カーネルの中ではkernel/console_io.cに次いで長いソースファイルです(291行)。
一番重要な関数はschedule関数です。主にタイマー割り込み(10ms)で呼び出され、実行するタスクを切り替える(コンテキストスイッチ)役割を担います(図3.5)。実行可能なタスクは「ランキュー」というキューへ設定します。そのため、コンテキストスイッチの際は、ランキューの中から次のタスクを選択します(図3.6)。
図3.5: タイムスライスについて
図3.6: コンテキストスイッチまでの流れ
その他には、ランキューとウェイクアップキューの操作の関数を定義しています。OS5のスケジューラでは、タスクは時間経過やイベント(キーボード入力、タスク終了)を待つことができます。待っている間はランキューから外し、ウェイクアップキュー、あるいはウェイクアップイベントキューへ追加します。ウェイクアップキューが時間経過待ちのキューで、ウェイクアップイベントキューがイベント発生待ちのキューです。
これらのキューの使い方の例としてuptimeというアプリケーションがウェイクアップキューを使用してスリープする流れを説明します。まず、uptimeが「33ms後に起こしてほしい」とカーネルへ通知します(図3.7)。アプリケーションとカーネルのインタフェースはシステムコールで、この場合、"SYSCALL_SCHED_WAKEUP_MSEC"というシステムコールを引数に「33ms」を設定して発行します。ソースコードの対応する箇所はapps/uptime/uptime.cのsyscall(SYSCALL_SCHED_WAKEUP_MSEC, 33, 0, 0);です(23行目)。
図3.7: スリープの流れ(1)
すると、カーネルはuptimeをランキューから外し、ウェイクアップキューへ移します(図3.8)。ランキューにはshellのみになるので、以降はshellのみ実行されます。ソースコードとしては、システムコールの入り口処理を実装しているkernel/syscall.cから、"SYSCALL_SCHED_WAKEUP_MSEC"の場合、kernel/sched.cのwakeup_after_msec()(185~201行目)を呼び出しています。
図3.8: スリープの流れ(2)
そして、タイマー割り込み発生時に所定の時間(今回の場合「33ms」)経過していたことを確認すると、uptimeをランキューへ戻します(図3.9)。ソースコードとしては、kernel/timer.cでsched_update_wakeupq()を呼び出しています。sched_update_wakeupq()は、kernel/sched.cの157~183行目で定義しています。
図3.9: スリープの流れ(3)
なお、全てのタスクがスリープ等でランキューから抜けた場合、「カーネルタスク」が動作します(図3.10)。カーネルタスクの実体は初期化完了後のkern_init関数(kernel/init.c)で、80〜83行目でx86のhlt命令を無限ループで何度も実行しているため、カーネルタスクがスケジュールされると、割り込みが入るまでCPUはhlt命令で休むことになります。なお、schedule関数内において、81〜87行目の条件分岐がカーネルタスクへコンテキストスイッチしている箇所です。
図3.10: カーネルタスク
タスクがスリープした後のコンテキストスイッチにはちょっとした問題があります。例えば、shellがタイムスライス(10ms)の途中でキー入力待ちでスリープしたとします(図3.11)。shellが"SYSCALL_SCHED_WAKEUP_EVENT"のシステムコールを発行することになり、kernel/sched.cのwakeup_after_event関数が呼ばれます。
図3.11: スリープ後のコンテキストスイッチの問題(1)
shellがランキューから外され、uptimeが次に実行するタスクとして選択されたとします。すると、shellのタイムスライスの残り時間が経過するとコンテキストスイッチされてしまいます。タイムスライスを10msとしている以上、タスクには10msは実行させてあげるべきなので、これではちょっと不平等です(図3.12)。
図3.12: スリープ後のコンテキストスイッチの問題(2)
そのため、OS5カーネルのスケジューラでは、タスクがスリープした場合はタイムスライスの残りを次のタスクへプレゼントしたものと考え、スリープ後のタイマー割り込みではコンテキストスイッチしないようにしています(図3.13)。
図3.13: スリープ後のコンテキストスイッチの問題(3)
実装としては、スリープでのコンテキストスイッチ時にフラグ変数is_task_switched_in_time_sliceをセットします(wakeup_after_msecとwakeup_after_event関数内でセットしています)。そして、schedule関数内で、is_task_switched_in_time_sliceをチェックし、セットされていた場合はcurrent_task->task_switched_in_time_sliceをセットしています(92行目、102行目)。この時のcurrent_taskはコンテキストスイッチ後のタスクを指しています。コンテキストスイッチの後、タイマー割り込みが発生しても、タイマー割り込みハンドラ(kernel/timer.cのdo_ir_timer関数)でcurrent_task->task_switched_in_time_sliceをチェックし、セットされている場合はschedule関数を呼び出さないようにしています(kernel/timer.cの12~18行目)。
task_instance_tableはカーネルタスクにしか使っていないです。元々はこのテーブルにすべてのタスクが並んでいたのですが、メモリの動的確保を実装し、タスクの生成時に動的に確保するようにしたため、今ではカーネルタスクだけtask_instance_tableに残っている状態です。
リスト3.15: kernel/include/kern_task.h
1: #ifndef _KERN_TASK_H_ 2: #define _KERN_TASK_H_ 3: 4: #define KERN_TASK_ID 0 5: 6: void kern_task_init(void); 7: 8: #endif /* _KERN_TASK_H_ */
カーネルタスクのtask_instance_table内のインデックス(KERN_TASK_ID)の定義と、初期化関数(kern_task_init)のプロトタイプ宣言を行っています。
リスト3.16: kernel/kern_task_init.c
1: #include <kern_task.h>
2: #include <cpu.h>
3: #include <sched.h>
4:
5: #define KERN_TASK_GDT_IDX 5
6:
7: static void kern_task_context_switch(void)
8: {
9: __asm__("ljmp $0x28, $0");
10: }
11:
12: void kern_task_init(void)
13: {
14: static struct tss kern_task_tss;
15: unsigned int old_cr3, cr3 = 0x0008f018;
16: unsigned short segment_selector = 8 * KERN_TASK_GDT_IDX;
17:
18: kern_task_tss.esp0 = 0x0007f800;
19: kern_task_tss.ss0 = GDT_KERN_DS_OFS;
20: kern_task_tss.__cr3 = 0x0008f018;
21: init_gdt(KERN_TASK_GDT_IDX, (unsigned int)&kern_task_tss,
22: sizeof(kern_task_tss), 0);
23: __asm__("movl %%cr3, %0":"=r"(old_cr3):);
24: cr3 |= old_cr3 & 0x00000fe7;
25: __asm__("movl %0, %%cr3"::"r"(cr3));
26: __asm__("ltr %0"::"r"(segment_selector));
27:
28: /* Setup context switch function */
29: task_instance_table[KERN_TASK_ID].context_switch =
30: kern_task_context_switch;
31: }
カーネルタスクの初期化を行うkern_task_init関数を定義しています。今実行中のコンテキストをタスクとして登録する点が、その他のタスク登録とは異なります。(そのため、今だにkern_task_init関数が残っています。)
やっていることは以下の3つです。
1.について、タスクステートセグメント(TSS)の設定を行っています。x86 CPUはタスク管理の機能を持っており、x86 CPUの枠組みでタスクを管理する際の構造がTSSです。TSSもセグメントなので、GDTへ登録します(21〜22行目)。
2.について、CR3はページディレクトリのベースアドレスとキャッシュの設定を行うレジスタです*4。ページディレクトリのベースアドレスは4KB(0x1000)の倍数でなければならないので、下位12ビットは必ず0です。そこで、CR3の下位12ビットにページディレクトリの設定を行うビットがあります。設定ビットはビット4(PCD*5)とビット3(PWT*6)で、それ以外のビット(ビット11〜5とビット2〜0)は予約ビットです。CR3へ設定したい内容は、15行目で変数cr3へ設定しています。ページディレクトリの開始アドレスが0x0008 f000で(図1.2)、PCDとPWTを共にセットするため、CR3へ設定する値は"0x0008 f018"です。なお、CR3の予約ビットへは、CR3を読み出して得られた値を書き込まなければならないとデータシートに記載されています。そのため、23〜25行目では、CR3レジスタを読み出し(23行目)、読みだした値(old_cr3変数)の予約ビットのみcr3変数へ反映し(24行目)、その後cr3変数の値をCR3レジスタへ格納(25行目)ということを行っています。
[*4] そのため、CR3は、PDBR(ページディレクトリベースレジスタ)とも呼ばれます。
[*5] ページキャッシュディスエーブルです。セットされているとページディレクトリのキャッシュが抑制されます。
[*6] ページレベル書き込み透過です。セットされるとライトスルーキャッシングが有効になり、クリアされるとライトバックキャッシングが有効になります。
3.はltr命令を使用して1.で登録したTSSをタスクレジスタ(TR)へ登録しています。
4.はコンテキストスイッチ用の関数をstruct task構造体のエントリへ登録しているところです。struct taskはOS5のカーネルでタスクを管理する構造体です。kernel/sched.cでも使用していますが、task_instance_table配列はstruct task構造体の配列です。kernel_task_context_switch関数が登録される関数で、やっていることはljmp命令のインラインアセンブラ1行です。ljmp命令はオペランドにGDT内のTSSのオフセットを与えるとそのタスクへコンテキストスイッチできます。カーネルタスクのTSSのGDT内でのオフセットは0x28なので、ljmp命令で0x28を指定することでカーネルタスクへコンテキストスイッチできます。なお、第2オペランドはコンテキストスイッチの場合、無視されます。
4.のtask_instalce_tableは、実は古い実装で、今は使用しているのはカーネルタスクのみです。その他のユーザーランドのタスクではstruct taskはタスク生成時に動的に確保します(kernel/task.cで説明します)。
リスト3.17: kernel/include/task.h
1: #ifndef _TASK_H_
2: #define _TASK_H_
3:
4: #include <cpu.h>
5: #include <fs.h>
6:
7: #define CONTEXT_SWITCH_FN_SIZE 12
8: #define CONTEXT_SWITCH_FN_TSKNO_FIELD 8
9:
10: struct task {
11: /* ランキュー・ウェイクアップキュー(時間経過待ち・イベント待ち)の
12: * いずれかに繋がれる(同時に複数のキューに存在することが無いよう
13: * 運用する) */
14: struct task *prev;
15: struct task *next;
16:
17: unsigned short task_id;
18: struct tss tss;
19: void (*context_switch)(void);
20: unsigned char context_switch_func[CONTEXT_SWITCH_FN_SIZE];
21: char task_switched_in_time_slice;
22: unsigned int wakeup_after_msec;
23: unsigned char wakeup_after_event;
24: };
25:
26: extern unsigned char context_switch_template[CONTEXT_SWITCH_FN_SIZE];
27:
28: void task_init(struct file *f, int argc, char *argv[]);
29: void task_exit(struct task *t);
30:
31: #endif /* _TASK_H_ */
タスクの構造体(struct task)に関する定義と、タスク生成時の初期化関数(task_init)とタスク終了関数(task_exit)のプロトタイプ宣言です。
リスト3.18: kernel/task.c
1: #include <task.h>
2: #include <memory.h>
3: #include <fs.h>
4: #include <sched.h>
5: #include <common.h>
6: #include <lock.h>
7: #include <kernel.h>
8: #include <cpu.h>
9:
10: #define GDT_IDX_OFS 5
11: #define APP_ENTRY_POINT 0x20000030
12: #define APP_STACK_BASE_USER 0xffffe800
13: #define APP_STACK_BASE_KERN 0xfffff000
14: #define APP_STACK_SIZE 4096
15: #define GDT_USER_CS_OFS 0x0018
16: #define GDT_USER_DS_OFS 0x0020
17:
18: /*
19: 00000000 <context_switch>:
20: 0: 55 push %ebp
21: 1: 89 e5 mov %esp,%ebp
22: 3: ea 00 00 00 00 00 00 ljmp $0x00,$0x0
23: a: 5d pop %ebp
24: b: c3 ret
25: */
26: unsigned char context_switch_template[CONTEXT_SWITCH_FN_SIZE] = {
27: 0x55,
28: 0x89, 0xe5,
29: 0xea, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30: 0x5d,
31: 0xc3
32: };
33:
34: static unsigned short task_id_counter = 1;
35:
36: static int str_get_len(const char *src)
37: {
38: int len;
39: for (len = 0; src[len] != '\0'; len++);
40: return len + 1;
41: }
42:
43: void task_init(struct file *f, int argc, char *argv[])
44: {
45: struct page_directory_entry *pd_base_addr, *pde;
46: struct page_table_entry *pt_base_addr, *pt_stack_base_addr, *pte;
47: struct task *new_task;
48: unsigned int paging_base_addr, phys_stack_base, phys_stack_base2;
49: unsigned int i;
50: unsigned int len = 0;
51: unsigned int argv_space_num, vsp, arg_size;
52: unsigned char *sp, *sp2;
53: char *t;
54:
55: /* Allocate task resources */
56: pd_base_addr = (struct page_directory_entry *)mem_alloc();
57: pt_base_addr = (struct page_table_entry *)mem_alloc();
58: pt_stack_base_addr = (struct page_table_entry *)mem_alloc();
59: new_task = (struct task *)mem_alloc();
60: phys_stack_base = (unsigned int)mem_alloc();
61: phys_stack_base2 = (unsigned int)mem_alloc();
62:
63: /* Initialize task page directory */
64: pde = pd_base_addr;
65: pde->all = 0;
66: pde->p = 1;
67: pde->r_w = 1;
68: pde->pt_base = 0x00090;
69: pde++;
70: for (i = 1; i < 0x080; i++) {
71: pde->all = 0;
72: pde++;
73: }
74: pde->all = 0;
75: pde->p = 1;
76: pde->r_w = 1;
77: pde->u_s = 1;
78: pde->pt_base = (unsigned int)pt_base_addr >> 12;
79: pde++;
80: for (i++; i < 0x3ff; i++) {
81: pde->all = 0;
82: pde++;
83: }
84: pde->all = 0;
85: pde->p = 1;
86: pde->r_w = 1;
87: pde->u_s = 1;
88: pde->pt_base = (unsigned int)pt_stack_base_addr >> 12;
89: pde++;
90:
91: /* Initialize task page table */
92: pte = pt_base_addr;
93: paging_base_addr = (unsigned int)f->data_base_addr >> 12;
94: for (i = 0; i < f->head->block_num; i++) {
95: pte->all = 0;
96: pte->p = 1;
97: pte->r_w = 1;
98: pte->u_s = 1;
99: pte->page_base = paging_base_addr++;
100: pte++;
101: }
102: for (; i < 0x400; i++) {
103: pte->all = 0;
104: pte++;
105: }
106:
107: /* Initialize stack page table */
108: pte = pt_stack_base_addr;
109: for (i = 0; i < 0x3fd; i++) {
110: pte->all = 0;
111: pte++;
112: }
113: paging_base_addr = phys_stack_base >> 12;
114: pte->all = 0;
115: pte->p = 1;
116: pte->r_w = 1;
117: pte->u_s = 1;
118: pte->page_base = paging_base_addr;
119: pte++;
120: paging_base_addr = phys_stack_base2 >> 12;
121: pte->all = 0;
122: pte->p = 1;
123: pte->r_w = 1;
124: pte->u_s = 1;
125: pte->page_base = paging_base_addr;
126: pte++;
127: pte->all = 0;
128: pte++;
129:
130: /* Setup task_id */
131: new_task->task_id = task_id_counter++;
132:
133: /* Setup context switch function */
134: copy_mem(context_switch_template, new_task->context_switch_func,
135: CONTEXT_SWITCH_FN_SIZE);
136: new_task->context_switch_func[CONTEXT_SWITCH_FN_TSKNO_FIELD] =
137: 8 * (new_task->task_id + GDT_IDX_OFS);
138: new_task->context_switch = (void (*)(void))new_task->context_switch_func;
139:
140: /* Setup GDT for task_tss */
141: init_gdt(new_task->task_id + GDT_IDX_OFS, (unsigned int)&new_task->tss,
142: sizeof(struct tss), 3);
143:
144: /* Setup task stack */
145: /* スタックにint argcとchar *argv[]を積み、
146: * call命令でジャンプした直後を再現する。
147: *
148: * 例) argc=3, argv={"HOGE", "P", "FUGAA"}
149: * | VA | 内容 | 備考 |
150: * |-------------+-------------------+--------------------------------|
151: * | 0x2000 17d0 | | |
152: * | 0x2000 17d4 | (Don't Care) | ESPはここを指した状態にする(*) |
153: * | 0x2000 17d8 | 3 | argc |
154: * | 0x2000 17dc | 0x2000 17e4 | argv |
155: * | 0x2000 17e0 | (Don't Care) | |
156: * | 0x2000 17e4 | 0x2000 17f0 | argv[0] |
157: * | 0x2000 17e8 | 0x2000 17f5 | argv[1] |
158: * | 0x2000 17ec | 0x2000 17f7 | argv[2] |
159: * | 0x2000 17f0 | 'H' 'O' 'G' 'E' | |
160: * | 0x2000 17f4 | '\0' 'P' '\0' 'F' | |
161: * | 0x2000 17f8 | 'U' 'G' 'A' 'A' | |
162: * | 0x2000 17fc | '\0' | |
163: * |-------------+-------------------+--------------------------------|
164: * | 0x2000 1800 | | |
165: * (*) call命令はnearジャンプ時、call命令の次の命令のアドレスを
166: * 復帰時のEIPとしてスタックに積むため。
167: */
168: for (i = 0; i < (unsigned int)argc; i++) {
169: len += str_get_len(argv[i]);
170: }
171: argv_space_num = (len / 4) + 1;
172: arg_size = 4 * (4 + argc + argv_space_num);
173:
174: sp = (unsigned char *)(phys_stack_base2 + (APP_STACK_SIZE / 2));
175: sp -= arg_size;
176:
177: sp += 4;
178:
179: *(int *)sp = argc;
180: sp += 4;
181:
182: *(unsigned int *)sp = APP_STACK_BASE_USER - (4 * (argc + argv_space_num));
183: sp += 4;
184:
185: sp += 4;
186:
187: vsp = APP_STACK_BASE_USER - (4 * argv_space_num);
188: sp2 = sp + (4 * argc);
189: for (i = 0; i < (unsigned int)argc; i++) {
190: *(unsigned int *)sp = vsp;
191: sp += 4;
192: t = argv[i];
193: for (; *t != '\0'; t++) {
194: vsp++;
195: *sp2++ = *t;
196: }
197: *sp2++ = '\0';
198: vsp++;
199: }
200:
201: /* Setup task_tss */
202: new_task->tss.eip = APP_ENTRY_POINT;
203: new_task->tss.esp = APP_STACK_BASE_USER - arg_size;
204: new_task->tss.eflags = 0x00000200;
205: new_task->tss.esp0 = APP_STACK_BASE_KERN;
206: new_task->tss.ss0 = GDT_KERN_DS_OFS;
207: new_task->tss.es = GDT_USER_DS_OFS | 0x0003;
208: new_task->tss.cs = GDT_USER_CS_OFS | 0x0003;
209: new_task->tss.ss = GDT_USER_DS_OFS | 0x0003;
210: new_task->tss.ds = GDT_USER_DS_OFS | 0x0003;
211: new_task->tss.fs = GDT_USER_DS_OFS | 0x0003;
212: new_task->tss.gs = GDT_USER_DS_OFS | 0x0003;
213: new_task->tss.__cr3 = (unsigned int)pd_base_addr | 0x18;
214:
215: /* Add task to run_queue */
216: sched_runq_enq(new_task);
217: }
218:
219: void task_exit(struct task *t)
220: {
221: unsigned char if_bit;
222: struct page_directory_entry *pd_base_addr, *pde;
223: struct page_table_entry *pt_base_addr, *pt_stack_base_addr, *pte;
224: unsigned int phys_stack_base, phys_stack_base2;
225:
226: kern_lock(&if_bit);
227:
228: sched_update_wakeupevq(EVENT_TYPE_EXIT);
229: sched_runq_del(t);
230:
231: pd_base_addr =
232: (struct page_directory_entry *)(t->tss.__cr3 & PAGE_ADDR_MASK);
233: pde = pd_base_addr + 0x080;
234: pt_base_addr = (struct page_table_entry *)(pde->pt_base << 12);
235: pde = pd_base_addr + 0x3ff;
236: pt_stack_base_addr = (struct page_table_entry *)(pde->pt_base << 12);
237: pte = pt_stack_base_addr + 0x3fd;
238: phys_stack_base = pte->page_base << 12;
239: pte = pt_stack_base_addr + 0x3fe;
240: phys_stack_base2 = pte->page_base << 12;
241:
242: mem_free((void *)phys_stack_base2);
243: mem_free((void *)phys_stack_base);
244: mem_free(t);
245: mem_free(pt_stack_base_addr);
246: mem_free(pt_base_addr);
247: mem_free(pd_base_addr);
248:
249: schedule();
250:
251: kern_unlock(&if_bit);
252: }
タスク*7の実行開始時の初期化を行うtask_init関数と、タスク終了時の終了処理を行うtask_exit関数を定義しています。また、カーネル内ではここでしか使わないためにstr_get_len関数もここで定義しています。なお、task_initはOS5の中で最も長い関数です(175行)。
[*7] OS5ではx86 CPUの言い回しに合わせて「タスク」と呼んでいます。なお、カーネルより上位のユーザーランドで話すときは分かり易さから「アプリケーション」と呼んでいます。実体は共に同じものです。
task_initはタスクの生成から、スケジューラへの登録までを行います。以下の流れです。
1.ではmem_allocを、「ページディレクトリ」、「ページテーブル(コード・データ領域)」、「ページテーブル(スタック領域)」、「struct task」、「スタック領域(x2)」の合計6回呼び出しています。4KB毎のアロケーションなので、合計すると24KBがタスク一つ当たりに必要なメモリです。
2.について、ページディレクトリ・ページテーブルの構成を図3.14に示します。例としてshellとuptimeの2つのタスクについて描いています。共にカーネルのページテーブルを指しているのは、システムコール呼び出しでユーザーモードからカーネルモードへ権限昇格した際に、カーネル空間の関数を呼び出せるようにするためです。
[*8] ページという単位で物理アドレスを仮想アドレスに対応付けます。この対応表をページディレクトリ、ページテーブルと呼びます。2つあるのは階層構造になっているためです。ページディレクトリ→ページテーブルという構造で、ページディレクトリ1つに1024個のページディレクトリを持ちます。
図3.14: OS5のページディレクトリ・ページテーブル構成
4.では、コンテキストスイッチの関数のバイナリを動的に生成しています。kernel/kern_task_init.cでも説明しましたが、ljmp命令はオペランドにGDT内のTSSのオフセットを指定することで、コンテキストスイッチできます。データシートを読む限り、このオペランドはレジスタを指定することもできるようなのですが、少なくともQEMUで正常動作を確認できていません。そこで、苦肉の策として、ljmp命令を含むコンテキストスイッチ用の関数のコンパイル後のバイナリを予め用意しておき(18〜32行目のcontext_switch_template配列)、task_initで新しいタスクを生成する際に、context_switch_templateからコピーしてオペランドの部分のバイナリのみ書き換える事を行っています。
6.について、タスクを実行開始する際に、あたかもランキューに以前から居たかのようにコンテキストスイッチできるよう、確保したばかりのスタック領域へ値を積んでいます。
リスト3.19: kernel/include/fs.h
1: #ifndef _FS_H_
2: #define _FS_H_
3:
4: #include <list.h>
5:
6: #define MAX_FILE_NAME 32
7: #define RESERVED_FILE_HEADER_SIZE 15
8:
9: struct file_head {
10: struct list lst;
11: unsigned char num_files;
12: };
13:
14: struct file_header {
15: char name[MAX_FILE_NAME];
16: unsigned char block_num;
17: unsigned char reserve[RESERVED_FILE_HEADER_SIZE];
18: };
19:
20: struct file {
21: struct list lst;
22: struct file_header *head;
23: void *data_base_addr;
24: };
25:
26: extern struct file *fshell;
27:
28: void fs_init(void *fs_base_addr);
29: struct file *fs_open(const char *name);
30: int fs_close(struct file *f);
31:
32: #endif /* _FS_H_ */
ファイルシステム周りの構造体等を定義しているソースコードです。
リスト3.20: kernel/fs.c
1: #include <fs.h>
2: #include <stddef.h>
3: #include <memory.h>
4: #include <list.h>
5: #include <queue.h>
6: #include <common.h>
7:
8: struct file_head fhead;
9: struct file *fshell;
10:
11: void fs_init(void *fs_base_addr)
12: {
13: struct file *f;
14: unsigned char i;
15: unsigned char *file_start_addr = fs_base_addr;
16:
17: queue_init((struct list *)&fhead);
18: fhead.num_files = *(unsigned char *)fs_base_addr;
19:
20: file_start_addr += PAGE_SIZE;
21: for (i = 1; i <= fhead.num_files; i++) {
22: f = (struct file *)mem_alloc();
23: f->head = (struct file_header *)file_start_addr;
24: f->data_base_addr =
25: (char *)file_start_addr + sizeof(struct file_header);
26: file_start_addr += PAGE_SIZE * f->head->block_num;
27: queue_enq((struct list *)f, (struct list *)&fhead);
28: }
29: fshell = (struct file *)fhead.lst.next;
30: }
31:
32: struct file *fs_open(const char *name)
33: {
34: struct file *f;
35:
36: /* 将来的には、struct fileのtask_idメンバにopenしたタスクの
37: * TASK_IDを入れるようにする。そして、openしようとしているファ
38: * イルのtask_idが既に設定されていれば、fs_openはエラーを返す
39: * ようにする */
40:
41: for (f = (struct file *)fhead.lst.next; f != (struct file *)&fhead;
42: f = (struct file *)f->lst.next) {
43: if (!str_compare(name, f->head->name))
44: return f;
45: }
46:
47: return NULL;
48: }
49:
50: int fs_close(struct file *f __attribute__ ((unused)))
51: {
52: /* 将来的には、fidに対応するstruct fileのtask_idメンバーを設定
53: * なし(0)にする。 */
54: return 0;
55: }
ファイルシステムの初期化と操作を行う関数群です。カーネル初期化(kern_init関数)でfs_initを呼び、openシステムコールからfs_openが呼ばれます。
まず、OS5のファイルシステムは「特定のメモリ領域のバイナリ列を『ファイル』として認識するためのルール集」に過ぎません。OS5のファイルシステムにおけるルールを図3.15、図3.16、図3.17、図3.18、図3.19、図3.20で説明します。
図3.15: OS5のファイルシステム(1)
図3.16: OS5のファイルシステム(2)
図3.17: OS5のファイルシステム(3)
図3.18: OS5のファイルシステム(4)
図3.19: OS5のファイルシステム(5)
図3.20: OS5のファイルシステム(6)
kernel/fs.cについて、fs_initは引数でファイルシステムが配置されている領域の先頭アドレスを受け取り、ファイルシステムの内容をスキャンしてstruct fileという構造体のリンクリストを作成します(図3.21、図3.22、図3.23)。
図3.21: ファイルシステム初期化(nextの関係)
図3.22: ファイルシステム初期化(prevの関係)
図3.23: ファイルシステム初期化(head・data_base_addrの関係)
OS5では「ファイルシステムの1番最初のエントリをカーネル起動後最初に起動させるアプリケーションバイナリとする」ルールにしています。そこで、1番最初のエントリをstruct file *fshellへ設定しています。なお、ファイルシステムの1番目のエントリは、実行ファイルであればシェルで無くとも良いです。特に意味もなくfshellという変数名のままになっています。
fs_openは引数で与えられたファイル名と一致するstruct fileエントリをリンクリストから検索してstruct fileのポインタを返します。
リスト3.21: kernel/include/syscall.h
1: #ifndef _SYSCALL_H_ 2: #define _SYSCALL_H_ 3: 4: unsigned int do_syscall(unsigned int syscall_id, unsigned int arg1, 5: unsigned int arg2, unsigned int arg3); 6: 7: #endif /* _SYSCALL_H_ */
システムコール割り込みから呼び出されてシステムコールを実行するdo_syscall関数のプロトタイプ宣言のみです。
リスト3.22: kernel/syscall.c
1: #include <syscall.h>
2: #include <kernel.h>
3: #include <timer.h>
4: #include <sched.h>
5: #include <fs.h>
6: #include <task.h>
7: #include <console_io.h>
8: #include <cpu.h>
9:
10: unsigned int do_syscall(unsigned int syscall_id, unsigned int arg1,
11: unsigned int arg2, unsigned int arg3)
12: {
13: unsigned int result = -1;
14: unsigned int gdt_idx;
15: unsigned int tss_base_addr;
16:
17: switch (syscall_id) {
18: case SYSCALL_TIMER_GET_GLOBAL_COUNTER:
19: result = timer_get_global_counter();
20: break;
21: case SYSCALL_SCHED_WAKEUP_MSEC:
22: wakeup_after_msec(arg1);
23: result = 0;
24: break;
25: case SYSCALL_SCHED_WAKEUP_EVENT:
26: wakeup_after_event(arg1);
27: result = 0;
28: break;
29: case SYSCALL_CON_GET_CURSOR_POS_Y:
30: result = (unsigned int)cursor_pos.y;
31: break;
32: case SYSCALL_CON_PUT_STR:
33: put_str((char *)arg1);
34: result = 0;
35: break;
36: case SYSCALL_CON_PUT_STR_POS:
37: put_str_pos((char *)arg1, (unsigned char)arg2,
38: (unsigned char)arg3);
39: result = 0;
40: break;
41: case SYSCALL_CON_DUMP_HEX:
42: dump_hex(arg1, arg2);
43: result = 0;
44: break;
45: case SYSCALL_CON_DUMP_HEX_POS:
46: dump_hex_pos(arg1, arg2, (unsigned char)(arg3 >> 16),
47: (unsigned char)(arg3 & 0x0000ffff));
48: result= 0;
49: break;
50: case SYSCALL_CON_GET_LINE:
51: result = get_line((char *)arg1, arg2);
52: break;
53: case SYSCALL_OPEN:
54: result = (unsigned int)fs_open((char *)arg1);
55: break;
56: case SYSCALL_EXEC:
57: task_init((struct file *)arg1, (int)arg2, (char **)arg3);
58: result = 0;
59: break;
60: case SYSCALL_EXIT:
61: gdt_idx = x86_get_tr() / 8;
62: tss_base_addr = (gdt[gdt_idx].base2 << 24) |
63: (gdt[gdt_idx].base1 << 16) | (gdt[gdt_idx].base0);
64: task_exit((struct task *)(tss_base_addr - 0x0000000c));
65: result = 0;
66: break;
67: }
68:
69: return result;
70: }
システムコールのソースファイルです。このソースファイルではシステムコールの入り口処理(do_syscall関数)を定義しています。
システムコール呼び出しの流れを説明します。「shellが"Hello"とコンソール画面へ表示したい」とします(図3.24)。
図3.24: システムコール実行の流れ(1)
コンソール画面へ文字列を表示するためのシステムコールは"CON_PUT_STR"です。システムコール呼び出しにおいて、アプリケーションとカーネルでパラメータの受け渡しには汎用レジスタ(EAX、EBX、ECX、EDX)を使用します。CON_PUT_STRを実行するために、汎用レジスタへ必要なパラメータを設定します(図3.25)。
図3.25: システムコール実行の流れ(2)
システムコールを発行するトリガーはソフトウェア割り込みです。OS5では割り込み番号128番をシステムコールとしています。そのため、shellは128番のソフトウェア割り込みを実行します(図3.26)。
図3.26: システムコール実行の流れ(3)
すると、カーネル側で128番の割り込みハンドラが呼び出され、同時に特権レベルが昇格するので、カーネル空間の関数を呼び出せるようになります(図3.27)。なお、割り込みハンドラの入り口はkernel/sys.Sのsyscall_handlerラベルの箇所です(174〜193行目)。syscall_handlerからkernel/syscall.cのdo_syscall関数を呼び出しています。
図3.27: システムコール実行の流れ(4)
割り込みハンドラ内では、汎用レジスタに設定された値に従って、カーネル空間内の関数を呼び出します(図3.28)。
図3.28: システムコール実行の流れ(5)
割り込みハンドラからreturnすると、元の特権レベルに戻ります(図3.29)。そして、元のアプリケーションの処理を再開します。
図3.29: システムコール実行の流れ(6)
現状、用意しているシステムコールは表3.1の通りです。
表3.1: システムコール一覧
| 定数名(番号) | 機能 |
|---|---|
| SYSCALL_TIMER_GET_GLOBAL_COUNTER(1) | タイマカウンタ取得 |
| SYSCALL_SCHED_WAKEUP_MSEC(2) | ウェイクアップ時間(ms)設定 |
| SYSCALL_SCHED_WAKEUP_EVENT(3) | ウェイクアップイベント設定 |
| SYSCALL_CON_GET_CURSOR_POS_Y(4) | カーソルY座標取得 |
| SYSCALL_CON_PUT_STR(5) | コンソールへ文字列出力(座標指定なし) |
| SYSCALL_CON_PUT_STR_POS(6) | コンソールへ文字列出力(座標指定あり) |
| SYSCALL_CON_DUMP_HEX(7) | コンソールへ16進で数値出力(座標指定なし) |
| SYSCALL_CON_DUMP_HEX_POS(8) | コンソールへ16進で数値出力(座標指定あり) |
| SYSCALL_CON_GET_LINE(9) | コンソール入力を1行取得 |
| SYSCALL_OPEN(10) | ファイルオープン |
| SYSCALL_EXEC(11) | ファイル実行 |
| SYSCALL_EXIT(12) | タスク終了 |
リスト3.23: kernel/include/cpu.h
1: #ifndef _CPU_H_
2: #define _CPU_H_
3:
4: #include <asm/cpu.h>
5:
6: #define X86_EFLAGS_IF 0x00000200
7: #define GDT_KERN_DS_OFS 0x0010
8:
9: #define sti() __asm__ ("sti"::)
10: #define cli() __asm__ ("cli"::)
11: #define x86_get_eflags() ({ \
12: unsigned int _v; \
13: __asm__ volatile ("\tpushf\n" \
14: "\tpopl %0\n":"=r"(_v):); \
15: _v; \
16: })
17: #define x86_get_tr() ({ \
18: unsigned short _v; \
19: __asm__ volatile ("\tstr %0\n":"=r"(_v):); \
20: _v; \
21: })
22: #define x86_halt() __asm__ ("hlt"::)
23:
24: struct segment_descriptor {
25: union {
26: struct {
27: unsigned int a;
28: unsigned int b;
29: };
30: struct {
31: unsigned short limit0;
32: unsigned short base0;
33: unsigned short base1: 8, type: 4, s: 1, dpl: 2, p: 1;
34: unsigned short limit1: 4, avl: 1, l: 1, d: 1, g: 1,
35: base2: 8;
36: };
37: };
38: };
39:
40: struct tss {
41: unsigned short back_link, __blh;
42: unsigned int esp0;
43: unsigned short ss0, __ss0h;
44: unsigned int esp1;
45: unsigned short ss1, __ss1h;
46: unsigned int esp2;
47: unsigned short ss2, __ss2h;
48: unsigned int __cr3;
49: unsigned int eip;
50: unsigned int eflags;
51: unsigned int eax;
52: unsigned int ecx;
53: unsigned int edx;
54: unsigned int ebx;
55: unsigned int esp;
56: unsigned int ebp;
57: unsigned int esi;
58: unsigned int edi;
59: unsigned short es, __esh;
60: unsigned short cs, __csh;
61: unsigned short ss, __ssh;
62: unsigned short ds, __dsh;
63: unsigned short fs, __fsh;
64: unsigned short gs, __gsh;
65: unsigned short ldt, __ldth;
66: unsigned short trace;
67: unsigned short io_bitmap_base;
68: };
69:
70: extern struct segment_descriptor gdt[GDT_SIZE];
71:
72: void init_gdt(unsigned int idx, unsigned int base, unsigned int limit,
73: unsigned char dpl);
74:
75: #endif /* _CPU_H_ */
x86 CPUに依存するインラインアセンブラのマクロや、構造体等を定義しています。
リスト3.24: kernel/cpu.c
1: #include <cpu.h>
2:
3: void init_gdt(unsigned int idx, unsigned int base, unsigned int limit,
4: unsigned char dpl)
5: {
6: gdt[idx].limit0 = limit & 0x0000ffff;
7: gdt[idx].limit1 = (limit & 0x000f0000) >> 16;
8:
9: gdt[idx].base0 = base & 0x0000ffff;
10: gdt[idx].base1 = (base & 0x00ff0000) >> 16;
11: gdt[idx].base2 = (base & 0xff000000) >> 24;
12:
13: gdt[idx].dpl = dpl;
14:
15: gdt[idx].type = 9;
16: gdt[idx].p = 1;
17: }
x86 CPUに依存する処理を記述しているソースファイルです。今のところ、GDTへのエントリ追加を行うinit_gdt関数のみです。
リスト3.25: kernel/include/io_port.h
1: #ifndef _IO_PORT_H_
2: #define _IO_PORT_H_
3:
4: #define outb(value, port) \
5: __asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))
6:
7: #define inb(port) ({ \
8: unsigned char _v; \
9: __asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \
10: _v; \
11: })
12:
13: #define outb_p(value, port) \
14: __asm__ ("outb %%al,%%dx\n" \
15: "\tjmp 1f\n" \
16: "1:\tjmp 1f\n" \
17: "1:"::"a" (value),"d" (port))
18:
19: #define inb_p(port) ({ \
20: unsigned char _v; \
21: __asm__ volatile ("inb %%dx,%%al\n" \
22: "\tjmp 1f\n" \
23: "1:\tjmp 1f\n" \
24: "1:":"=a" (_v):"d" (port)); \
25: _v; \
26: })
27:
28: #endif /* _IO_PORT_H_ */
x86 CPUはI/Oのアドレス空間は分かれており、I/Oへのアクセスにはin、outという命令があります。このソースファイルではこれらの命令をインラインアセンブラでマクロ化しています。
リスト3.26: kernel/include/console_io.h
1: #ifndef _CONSOLE_IO_H_
2: #define _CONSOLE_IO_H_
3:
4: #define IOADR_KBC_DATA 0x0060
5: #define IOADR_KBC_DATA_BIT_BRAKE 0x80
6: #define IOADR_KBC_STATUS 0x0064
7: #define IOADR_KBC_STATUS_BIT_OBF 0x01
8:
9: #define COLUMNS 80
10: #define ROWS 25
11:
12: #define ASCII_ESC 0x1b
13: #define ASCII_BS 0x08
14: #define ASCII_HT 0x09
15:
16: #ifndef COMPILE_APP
17:
18: #define INTR_IR_KB 1
19: #define INTR_NUM_KB 33
20: #define INTR_MASK_BIT_KB 0x02
21: #define SCREEN_START 0xb8000
22: #define ATTR 0x07
23: #define CHATT_CNT 1
24:
25: struct cursor_position {
26: unsigned int x, y;
27: };
28:
29: extern unsigned char keyboard_handler;
30: extern struct cursor_position cursor_pos;
31:
32: void con_init(void);
33: void update_cursor(void);
34: void put_char_pos(char c, unsigned char x, unsigned char y);
35: void put_char(char c);
36: void put_str(char *str);
37: void put_str_pos(char *str, unsigned char x, unsigned char y);
38: void dump_hex(unsigned int val, unsigned int num_digits);
39: void dump_hex_pos(unsigned int val, unsigned int num_digits,
40: unsigned char x, unsigned char y);
41: unsigned char get_keydata_noir(void);
42: unsigned char get_keydata(void);
43: unsigned char get_keycode(void);
44: unsigned char get_keycode_pressed(void);
45: unsigned char get_keycode_released(void);
46: char get_char(void);
47: unsigned int get_line(char *buf, unsigned int buf_size);
48:
49: #endif /* COMPILE_APP */
50:
51: #endif /* _CONSOLE_IO_H_ */
コンソールドライバのヘッダファイルです。
#ifndef COMPILE_APPでは、ユーザーランド側でincludeされた場合に関数定義等を参照させないようにしています。コンソールドライバは、CON_PUT_STRシステムコール等でユーザーランドから呼び出します。システムコールにパラメータを与える際に、コンソール1画面の行数・列数等が必要になるため、それらの情報はkernel/以下のヘッダファイルをincludeさせるようにしています。
ただし関数などは、アプリケーションレベルの特権レベルで動作しているユーザーランドからはアクセスできないので、ifndefで無効化しています。
リスト3.27: kernel/console_io.c
1: #include <cpu.h>
2: #include <intr.h>
3: #include <io_port.h>
4: #include <console_io.h>
5: #include <sched.h>
6: #include <lock.h>
7: #include <kernel.h>
8:
9: #define QUEUE_BUF_SIZE 256
10:
11: const char keymap[] = {
12: 0x00, ASCII_ESC, '1', '2', '3', '4', '5', '6',
13: '7', '8', '9', '0', '-', '^', ASCII_BS, ASCII_HT,
14: 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i',
15: 'o', 'p', '@', '[', '\n', 0x00, 'a', 's',
16: 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';',
17: ':', 0x00, 0x00, ']', 'z', 'x', 'c', 'v',
18: 'b', 'n', 'm', ',', '.', '/', 0x00, '*',
19: 0x00, ' ', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
20: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '7',
21: '8', '9', '-', '4', '5', '6', '+', '1',
22: '2', '3', '0', '.', 0x00, 0x00, 0x00, 0x00,
23: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
24: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
25: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
26: 0x00, 0x00, 0x00, '_', 0x00, 0x00, 0x00, 0x00,
27: 0x00, 0x00, 0x00, 0x00, 0x00, '\\', 0x00, 0x00
28: };
29:
30: struct queue {
31: unsigned char buf[QUEUE_BUF_SIZE];
32: unsigned char start, end;
33: unsigned char is_full;
34: } keycode_queue;
35:
36: struct cursor_position cursor_pos;
37:
38: static unsigned char error_status;
39:
40: static void enqueue(struct queue *q, unsigned char data)
41: {
42: unsigned char if_bit;
43:
44: if (q->is_full) {
45: error_status = 1;
46: } else {
47: error_status = 0;
48: kern_lock(&if_bit);
49: q->buf[q->end] = data;
50: q->end++;
51: if (q->start == q->end) q->is_full = 1;
52: kern_unlock(&if_bit);
53: }
54: }
55:
56: static unsigned char dequeue(struct queue *q)
57: {
58: unsigned char data = 0;
59: unsigned char if_bit;
60:
61: kern_lock(&if_bit);
62: if (!q->is_full && (q->start == q->end)) {
63: error_status = 1;
64: } else {
65: error_status = 0;
66: data = q->buf[q->start];
67: q->start++;
68: q->is_full = 0;
69: }
70: kern_unlock(&if_bit);
71:
72: return data;
73: }
74:
75: void do_ir_keyboard(void)
76: {
77: unsigned char status, data;
78:
79: status = inb_p(IOADR_KBC_STATUS);
80: if (status & IOADR_KBC_STATUS_BIT_OBF) {
81: data = inb_p(IOADR_KBC_DATA);
82: enqueue(&keycode_queue, data);
83: }
84: sched_update_wakeupevq(EVENT_TYPE_KBD);
85: outb_p(IOADR_MPIC_OCW2_BIT_MANUAL_EOI | INTR_IR_KB,
86: IOADR_MPIC_OCW2);
87: }
88:
89: void con_init(void)
90: {
91: keycode_queue.start = 0;
92: keycode_queue.end = 0;
93: keycode_queue.is_full = 0;
94: error_status = 0;
95: }
96:
97: void update_cursor(void)
98: {
99: unsigned int cursor_address = (cursor_pos.y * 80) + cursor_pos.x;
100: unsigned char cursor_address_msb = (unsigned char)(cursor_address >> 8);
101: unsigned char cursor_address_lsb = (unsigned char)cursor_address;
102: unsigned char if_bit;
103:
104: kern_lock(&if_bit);
105: outb_p(0x0e, 0x3d4);
106: outb_p(cursor_address_msb, 0x3d5);
107: outb_p(0x0f, 0x3d4);
108: outb_p(cursor_address_lsb, 0x3d5);
109: kern_unlock(&if_bit);
110:
111: if (cursor_pos.y >= ROWS) {
112: unsigned int start_address = (cursor_pos.y - ROWS + 1) * 80;
113: unsigned char start_address_msb =
114: (unsigned char)(start_address >> 8);
115: unsigned char start_address_lsb = (unsigned char)start_address;
116:
117: kern_lock(&if_bit);
118: outb_p(0x0c, 0x3d4);
119: outb_p(start_address_msb, 0x3d5);
120: outb_p(0x0d, 0x3d4);
121: outb_p(start_address_lsb, 0x3d5);
122: kern_unlock(&if_bit);
123: }
124: }
125:
126: void put_char_pos(char c, unsigned char x, unsigned char y)
127: {
128: unsigned char *pos;
129:
130: pos = (unsigned char *)(SCREEN_START + (((y * COLUMNS) + x) * 2));
131: *(unsigned short *)pos = (unsigned short)((ATTR << 8) | c);
132: }
133:
134: void put_char(char c)
135: {
136: switch (c) {
137: case '\r':
138: cursor_pos.x = 0;
139: break;
140:
141: case '\n':
142: cursor_pos.y++;
143: break;
144:
145: default:
146: put_char_pos(c, cursor_pos.x, cursor_pos.y);
147: if (cursor_pos.x < COLUMNS - 1) {
148: cursor_pos.x++;
149: } else {
150: cursor_pos.x = 0;
151: cursor_pos.y++;
152: }
153: break;
154: }
155:
156: update_cursor();
157: }
158:
159: void put_str(char *str)
160: {
161: while (*str != '\0') {
162: put_char(*str);
163: str++;
164: }
165: }
166:
167: void put_str_pos(char *str, unsigned char x, unsigned char y)
168: {
169: while (*str != '\0') {
170: switch (*str) {
171: case '\r':
172: x = 0;
173: break;
174:
175: case '\n':
176: y++;
177: break;
178:
179: default:
180: put_char_pos(*str, x, y);
181: if (x < COLUMNS - 1) {
182: x++;
183: } else {
184: x = 0;
185: y++;
186: }
187: break;
188: }
189: str++;
190: }
191: }
192:
193: void dump_hex(unsigned int val, unsigned int num_digits)
194: {
195: unsigned int new_x = cursor_pos.x + num_digits;
196: unsigned int dump_digit = new_x - 1;
197:
198: while (num_digits) {
199: unsigned char tmp_val = val & 0x0000000f;
200: if (tmp_val < 10) {
201: put_char_pos('0' + tmp_val, dump_digit, cursor_pos.y);
202: } else {
203: put_char_pos('A' + tmp_val - 10, dump_digit, cursor_pos.y);
204: }
205: val >>= 4;
206: dump_digit--;
207: num_digits--;
208: }
209:
210: cursor_pos.x = new_x;
211:
212: update_cursor();
213: }
214:
215: void dump_hex_pos(unsigned int val, unsigned int num_digits,
216: unsigned char x, unsigned char y)
217: {
218: unsigned int new_x = x + num_digits;
219: unsigned int dump_digit = new_x - 1;
220:
221: while (num_digits) {
222: unsigned char tmp_val = val & 0x0000000f;
223: if (tmp_val < 10) {
224: put_char_pos('0' + tmp_val, dump_digit, y);
225: } else {
226: put_char_pos('A' + tmp_val - 10, dump_digit, y);
227: }
228: val >>= 4;
229: dump_digit--;
230: num_digits--;
231: }
232: }
233:
234: unsigned char get_keydata_noir(void)
235: {
236: while (!(inb_p(IOADR_KBC_STATUS) & IOADR_KBC_STATUS_BIT_OBF));
237: return inb_p(IOADR_KBC_DATA);
238: }
239:
240: unsigned char get_keydata(void)
241: {
242: unsigned char data;
243: unsigned char dequeuing = 1;
244: unsigned char if_bit;
245:
246: while (dequeuing) {
247: kern_lock(&if_bit);
248: data = dequeue(&keycode_queue);
249: if (!error_status)
250: dequeuing = 0;
251: kern_unlock(&if_bit);
252: if (dequeuing)
253: wakeup_after_event(EVENT_TYPE_KBD);
254: }
255:
256: return data;
257: }
258:
259: unsigned char get_keycode(void)
260: {
261: return get_keydata() & ~IOADR_KBC_DATA_BIT_BRAKE;
262: }
263:
264: unsigned char get_keycode_pressed(void)
265: {
266: unsigned char keycode;
267: while ((keycode = get_keydata()) & IOADR_KBC_DATA_BIT_BRAKE);
268: return keycode & ~IOADR_KBC_DATA_BIT_BRAKE;
269: }
270:
271: unsigned char get_keycode_released(void)
272: {
273: unsigned char keycode;
274: while (!((keycode = get_keydata()) & IOADR_KBC_DATA_BIT_BRAKE));
275: return keycode & ~IOADR_KBC_DATA_BIT_BRAKE;
276: }
277:
278: char get_char(void)
279: {
280: return keymap[get_keycode_pressed()];
281: }
282:
283: unsigned int get_line(char *buf, unsigned int buf_size)
284: {
285: unsigned int i;
286:
287: for (i = 0; i < buf_size - 1;) {
288: buf[i] = get_char();
289: if (buf[i] == ASCII_BS) {
290: if (i == 0) continue;
291: cursor_pos.x--;
292: update_cursor();
293: put_char_pos(' ', cursor_pos.x, cursor_pos.y);
294: i--;
295: } else {
296: put_char(buf[i]);
297: if (buf[i] == '\n') {
298: put_char('\r');
299: break;
300: }
301: i++;
302: }
303: }
304: buf[i] = '\0';
305:
306: return i;
307: }
コンソールのデバイスドライバです。OS5ではキーボード入力とテキストモードでの画面出力をコンソールとして抽象化しています。そのため、このソースファイルでキー入力と画面出力を共に扱っています。なお、単体のソースファイルの行数としてはboot/boot.sの328行に次いで2番目に長いソースファイルです(307行)。
このソースファイル内で重要なのはget_char関数と、put_char関数です。1行分の入力を取得するget_line関数や文字列を画面表示するput_str関数はget_char関数とput_char関数を内部で呼び出しているため、ここではget_char関数とput_char関数の動作の流れを説明します。
get_char関数の呼び出しによって何が起こるのかというと、キューからキーコードを含むデータ(キーデータ)を取り出し、ASCIIコードへ変換し戻り値として返します。get_keycode_pressed関数が押下時のキーコードを返す関数で、keymapはキーコードをASCIIコードへ変換する配列です。キーボードコントローラ(KBC)が返すキーデータにはBRAKEというビットが有り、このビットが立っている場合、該当のキーから指が離された事を示します。get_keydata_pressedではget_keydata関数でキューから取得したキーデータにBRAKEのビットが立っている間、ブロックします(押下中を示すキーデータが取得できるまで呼び出し元へreturnしない)。なお、キューにキーデータを積む関数はdo_ir_keyboardです。kernel/sys.S のkeyboard_handlerハンドラから呼び出されます。
put_char関数の呼び出しでは何が起こるのかというと、引数で渡されたASCIIコード値をカーソル位置に対応したVRAMのアドレスへ書き込みます。グローバル変数のstruct cursor_position cursor_posがカーソル位置を保持している変数で、put_char_pos関数が指定された座標のVRAMアドレスへASCIIコードを書き込む関数です。定数SCREEN_STARTがVRAMの先頭アドレスです。
kernel/init.cのkern_init関数からはカーソルの設定(cursor_posの初期化とupdate_cursor関数によるカーソル位置と表示開始位置の更新)とコンソールドライバの初期化(con_init関数によるキーコードのキューの初期化とエラーステータスの初期化)を行っています。
キー入力していると、コンソール画面にゴミが出力されることがありました。
当初、全く理由が分からず、少なくとも2週間程、悩んでいた覚えがあります。KBCの割り込み契機のエンキューでゴミが入っているのか、デキュー処理に問題があるのかと、問題を切り分けていきました。
結論としては、get_keydata関数内にて、dequeue関数呼び出しと、その後のerror_status変数のチェックの間にKBC割り込みが入ることがあり、その際に割り込みハンドラから呼び出されるエンキュー処理でerror_status変数を上書きしてしまう事が原因でした。そのため、正しいキーデータを取得できていないのに、get_keydata関数内のwhileループを抜けてしまい、get_keydata関数は正常ではないキーデータをreturnしていました。
そのため、get_keydata関数内のdequeue関数呼び出しからerror_statusチェックの間は割り込み禁止(ロック)するようにしています*9。割り込みハンドラ内とそれ以外で同じリソース(変数等)へアクセスする場合は、正しくロックする必要がある、ということでした。なお、今見ていると、変数error_statusをエンキュー用とデキュー用で分けても良かったと思います。
[*9] https://github.com/cupnes/os5/commit/8f0ffffdf1811a150ec8e95aa6f706940c356850
noirは"NO InteRrupt"の略で、割り込み禁止区間内(主に割り込みハンドラ)で呼び出される事を想定した関数です。これは当初、カーネルのロック処理がネストに対応して居なかったため(kernel/lock.cで説明します)で、割り込み禁止区間内から呼び出されるか、そうでないかで関数を分けていました。今はロック機能がネストに対応しているので、_noirの関数は不要なのですが、割り込みハンドラからの呼び出しではまだ修正されずに残っています。
OS5ではBIOSで画面モードをテキストモードに設定しています。SCREEN_STARTはテキストモードでのVRAMの先頭アドレスで、MC6845というCRTコントローラ(CRTC)のVRAMです。
このCRTCはテキストモードでの使用が前提であるため、コントローラ内にフォントを内蔵しており、VRAMのアドレス空間へASCIIで値を書き込むだけで画面表示ができます。カーソル位置と表示開始位置の設定も可能です。カーソル位置の設定により任意の場所にカーソルを設置できます。また、表示開始位置の設定に関しては、そもそもMC6845は表示領域(80x25文字)以上のVRAM領域を持っており、VRAM内のどこからを表示するかを「何文字目からスタート」という形で指定できます。これにより、表示開始位置の設定を行うだけで、画面スクロールが実現できます。
そのため、OS5はフォントも持っていないし、画面スクロールの処理もCRTCへ設定しているだけで、ソフトウェアで処理しているわけではありません。
このように、ハードウェアがどんな機能を持っているかを知っているとソフトウェアでの実装を減らせて便利です(「ハードウェア」を「API」と読み替えても同じことですね)。
リスト3.28: kernel/include/timer.h
1: #ifndef _TIMER_H_ 2: #define _TIMER_H_ 3: 4: #define IOADR_PIT_COUNTER0 0x0040 5: #define IOADR_PIT_CONTROL_WORD 0x0043 6: #define IOADR_PIT_CONTROL_WORD_BIT_COUNTER0 0x00 7: #define IOADR_PIT_CONTROL_WORD_BIT_16BIT_READ_LOAD 0x30 8: #define IOADR_PIT_CONTROL_WORD_BIT_MODE2 0x04 9: /* Rate Generator */ 10: 11: #define INTR_IR_TIMER 0 12: #define INTR_NUM_TIMER 32 13: #define INTR_MASK_BIT_TIMER 0x01 14: 15: #define TIMER_TICK_MS 10 16: 17: extern unsigned char timer_handler; 18: extern unsigned int global_counter; 19: 20: void timer_init(void); 21: unsigned int timer_get_global_counter(void); 22: 23: #endif /* _TIMER_H_ */
PIC(Programmable Interval Timer)のレジスタのIOアドレスと割り込み番号などの定数の定義と、関数のプロトタイプ宣言です。
リスト3.29: kernel/timer.c
1: #include <timer.h>
2: #include <io_port.h>
3: #include <intr.h>
4: #include <sched.h>
5:
6: unsigned int global_counter = 0;
7:
8: void do_ir_timer(void)
9: {
10: global_counter += TIMER_TICK_MS;
11: sched_update_wakeupq();
12: if (!current_task || !current_task->task_switched_in_time_slice) {
13: /* タイムスライス中のコンテキストスイッチではない */
14: schedule();
15: } else {
16: /* タイムスライス中のコンテキストスイッチである */
17: current_task->task_switched_in_time_slice = 0;
18: }
19: outb_p(IOADR_MPIC_OCW2_BIT_MANUAL_EOI | INTR_IR_TIMER, IOADR_MPIC_OCW2);
20: }
21:
22: void timer_init(void)
23: {
24: /* Setup PIT */
25: outb_p(IOADR_PIT_CONTROL_WORD_BIT_COUNTER0
26: | IOADR_PIT_CONTROL_WORD_BIT_16BIT_READ_LOAD
27: | IOADR_PIT_CONTROL_WORD_BIT_MODE2, IOADR_PIT_CONTROL_WORD);
28: /* 割り込み周期11932(0x2e9c)サイクル(=100Hz、10ms毎)に設定 */
29: outb_p(0x9c, IOADR_PIT_COUNTER0);
30: outb_p(0x2e, IOADR_PIT_COUNTER0);
31: }
32:
33: unsigned int timer_get_global_counter(void)
34: {
35: return global_counter;
36: }
タイマーの初期化と設定を行う関数群を定義しています。
timer_init関数がkernel/init.cのkern_init関数から呼ばれるタイマー初期化の関数です。OS5では10ms周期の割り込みに設定しています。タイマーの挙動は図3.30の通りです。
図3.30: タイマー割り込みの振る舞い
また、do_ir_timer関数が kern/sys.S のtimer_handlerハンドラから呼ばれる関数です。
リスト3.30: kernel/include/stddef.h
1: #ifndef _STDDEF_H_ 2: #define _STDDEF_H_ 3: 4: #define NULL ((void *)0) 5: 6: #endif /* _STDDEF_H_ */
汎用的に使用する定数の定義です。今のところ、NULLのみです。
リスト3.31: kernel/include/common.h
1: #ifndef _COMMON_H_ 2: #define _COMMON_H_ 3: 4: int str_compare(const char *src, const char *dst); 5: void copy_mem(const void *src, void *dst, unsigned int size); 6: 7: #endif /* _COMMON_H_ */
共通で使用される関数をkernel/common.cにまとめています。このヘッダファイルではプロトタイプ宣言を行っています。
リスト3.32: kernel/common.c
1: #include <common.h>
2:
3: int str_compare(const char *src, const char *dst)
4: {
5: char is_equal = 1;
6:
7: for (; (*src != '\0') && (*dst != '\0'); src++, dst++) {
8: if (*src != *dst) {
9: is_equal = 0;
10: break;
11: }
12: }
13:
14: if (is_equal) {
15: if (*src != '\0') {
16: return 1;
17: } else if (*dst != '\0') {
18: return -1;
19: } else {
20: return 0;
21: }
22: } else {
23: return (int)(*src - *dst);
24: }
25: }
26:
27: void copy_mem(const void *src, void *dst, unsigned int size)
28: {
29: unsigned char *d = (unsigned char *)dst;
30: unsigned char *s = (unsigned char *)src;
31:
32: for (; size > 0; size--) {
33: *d = *s;
34: d++;
35: s++;
36: }
37: }
common.cには、汎用的に使用されるような関数を集めています。
リスト3.33: kernel/include/lock.h
1: #ifndef _LOCK_H_ 2: #define _LOCK_H_ 3: 4: void kern_lock(unsigned char *if_bit); 5: void kern_unlock(unsigned char *if_bit); 6: 7: #endif /* _LOCK_H_ */
カーネルのロック機能についての関数のプロトタイプ宣言です。
リスト3.34: kernel/lock.c
1: #include <lock.h>
2: #include <cpu.h>
3:
4: void kern_lock(unsigned char *if_bit)
5: {
6: /* Save EFlags.IF */
7: *if_bit = (x86_get_eflags() & X86_EFLAGS_IF) ? 1 : 0;
8:
9: /* if saved IF == true, then cli */
10: if (*if_bit)
11: cli();
12: }
13:
14: void kern_unlock(unsigned char *if_bit)
15: {
16: /* if saved IF == true, then sti */
17: if (*if_bit)
18: sti();
19: }
ロック機能に関するソースコードです。ロックとはある処理を実行中に、割り込みなどでCPUが別の処理を行わないようにする機能です。kern_lockでは、cli命令で割り込みを無効化し、kern_unlockでは、sti命令で割り込みを有効化します。
引数のunsigned char *if_bitは、kern_lock実行時の割り込み有効/無効状態を保持するために使用します。例えば、親の関数でkern_lockを実行していて既に割り込み無効区間(ロック区間)内であるにも関わらず、子側のkern_lock〜kern_unlockで割り込みを有効化してしまうと、親側のロックに影響します。このようなネストしたkern_lock/kern_unlockの呼び出しのためにif_bitを使用します。
リスト3.35: kernel/include/list.h
1: #ifndef _LIST_H_
2: #define _LIST_H_
3:
4: struct list {
5: struct list *next;
6: struct list *prev;
7: };
8:
9: #endif /* _LIST_H_ */
リンクリストはカーネル内で頻繁に使用するため専用のヘッダファイルを用意しています。struct listを構造体の一つ目のメンバとすることで、構造体にリンクリストの機能を持たせることができます。例としては、kernel/include/fs.hのstruct fileの定義を見てみてください。(このヘッダファイルは、kernel/include/stddef.hへまとめても良さそうな気がします。)
リスト3.36: kernel/include/queue.h
1: #ifndef _QUEUE_H_ 2: #define _QUEUE_H_ 3: 4: #include <list.h> 5: 6: void queue_init(struct list *head); 7: void queue_enq(struct list *entry, struct list *head); 8: void queue_del(struct list *entry); 9: void queue_dump(struct list *head); 10: 11: #endif /* _QUEUE_H_ */
キュー構造の関数のプロトタイプ宣言です。キュー構造はカーネル内で頻繁に登場するため、専用のヘッダファイルを用意しています。
リスト3.37: kernel/queue.c
1: #include <queue.h>
2: #include <list.h>
3: #include <console_io.h>
4:
5: void queue_init(struct list *head)
6: {
7: head->next = head;
8: head->prev = head;
9: }
10:
11: void queue_enq(struct list *entry, struct list *head)
12: {
13: entry->prev = head->prev;
14: entry->next = head;
15: head->prev->next = entry;
16: head->prev = entry;
17: }
18:
19: void queue_del(struct list *entry)
20: {
21: entry->prev->next = entry->next;
22: entry->next->prev = entry->prev;
23: }
24:
25: void queue_dump(struct list *head)
26: {
27: unsigned int n;
28: struct list *entry;
29:
30: put_str("h =");
31: dump_hex((unsigned int)head, 8);
32: put_str(": p=");
33: dump_hex((unsigned int)head->prev, 8);
34: put_str(", n=");
35: dump_hex((unsigned int)head->next, 8);
36: put_str("\r\n");
37:
38: for (entry = head->next, n = 0; entry != head; entry = entry->next, n++) {
39: dump_hex(n, 2);
40: put_str("=");
41: dump_hex((unsigned int)entry, 8);
42: put_str(": p=");
43: dump_hex((unsigned int)entry->prev, 8);
44: put_str(", n=");
45: dump_hex((unsigned int)entry->next, 8);
46: put_str("\r\n");
47: }
48: }
カーネル内で汎用的に使えるよう、ここでキュー構造を定義しています。
リスト3.38: kernel/include/debug.h
1: #ifndef __DEBUG_H__ 2: #define __DEBUG_H__ 3: 4: extern volatile unsigned char _flag; 5: 6: void debug_init(void); 7: void test_excp_de(void); 8: void test_excp_pf(void); 9: 10: #endif /* __DEBUG_H__ */
デバッグ機能に関するフラグ変数のexternとプロトタイプ宣言があります。
リスト3.39: kernel/debug.c
1: #include <debug.h>
2:
3: volatile unsigned char _flag;
4:
5: void debug_init(void)
6: {
7: _flag = 0;
8: }
9:
10: /* Test divide by zero exception */
11: void test_excp_de(void)
12: {
13: __asm__("\tmovw $8, %%ax\n" \
14: "\tmovb $0, %%bl\n" \
15: "\tdivb %%bl"::);
16: }
17:
18: /* Test page fault exception */
19: void test_excp_pf(void)
20: {
21: volatile unsigned char tmp;
22: __asm__("movb 0x000b8000, %0":"=r"(tmp):);
23: }
カーネルデバッグのための機能です。デバッグフラグ_flagは、debug.hをincludeして1をセットすると、カーネルのデバッグログをコンソールへ出力するように用意しています。(ただし、_flagは誰も使っていないです。。)