ブートローダーからジャンプしてくると、カーネルの初期化処理が実行されます。カーネルの各機能の初期化を行った後は、カーネルは割り込み駆動で動作し、何もしない時はhlt命令でCPUを寝させるようにしています。
カーネルの各機能の実装については、2016年4月に発表させていただいた勉強会のスライドにまとめています。各機能の実装について、図を使って説明していますので、興味があれば見てみてください。
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
のように指定せねばならず、冗長ですね。
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
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
されるヘッダーファイルです。#define
1つだけなので先に紹介してしまいます。
ここでは、GDTのサイズを定義しています。GDTはC言語からも参照することがあるので、ヘッダーファイルに分離しています。
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をシステムコールの戻り値に使っているからです。
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"は、ユーザーランドの先頭アドレスです。
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のようにデバイスなどをファイルにしていない事もあり、ひたすらシステムコールが増えていく枠組みです。ただし、システムコールのインタフェースは単純なので、これはこれでシンプルで良いかなとも思います。
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と、割り込み設定関数のプロトタイプ宣言です。
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カーネルでもそうだったと思います(違っていたらゴメンナサイ)。
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を使わないのか。。。
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)
でブロックするようにしています。
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(メモリ管理ユニット)周りの定数、構造体、関数の定義です。
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に示します。なお、複数のページテーブルをまとめたものを「ページディレクトリ」と呼びます。
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のデータシートの表現に合わせて「タスク」と呼び、ユーザーランドでは直観的な分かりやすさから「アプリケーション」と呼んでいます。
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行目)。
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_ */
スケジューラに関するヘッダファイルです。
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)。
その他には、ランキューとウェイクアップキューの操作の関数を定義しています。OS5のスケジューラでは、タスクは時間経過やイベント(キーボード入力、タスク終了)を待つことができます。待っている間はランキューから外し、ウェイクアップキュー、あるいはウェイクアップイベントキューへ追加します。ウェイクアップキューが時間経過待ちのキューで、ウェイクアップイベントキューがイベント発生待ちのキューです。
これらのキューの使い方の例としてuptimeというアプリケーションがウェイクアップキューを使用してスリープする流れを説明します。まず、uptimeが「33ms後に起こしてほしい」とカーネルへ通知します(図3.7)。アプリケーションとカーネルのインタフェースはシステムコールで、この場合、"SYSCALL_SCHED_WAKEUP_MSEC"というシステムコールを引数に「33ms」を設定して発行します。ソースコードの対応する箇所はapps/uptime/uptime.cのsyscall(SYSCALL_SCHED_WAKEUP_MSEC, 33, 0, 0);
です(23行目)。
すると、カーネルはuptimeをランキューから外し、ウェイクアップキューへ移します(図3.8)。ランキューにはshellのみになるので、以降はshellのみ実行されます。ソースコードとしては、システムコールの入り口処理を実装しているkernel/syscall.cから、"SYSCALL_SCHED_WAKEUP_MSEC"の場合、kernel/sched.cのwakeup_after_msec()
(185~201行目)を呼び出しています。
そして、タイマー割り込み発生時に所定の時間(今回の場合「33ms」)経過していたことを確認すると、uptimeをランキューへ戻します(図3.9)。ソースコードとしては、kernel/timer.cでsched_update_wakeupq()
を呼び出しています。sched_update_wakeupq()
は、kernel/sched.cの157~183行目で定義しています。
なお、全てのタスクがスリープ等でランキューから抜けた場合、「カーネルタスク」が動作します(図3.10)。カーネルタスクの実体は初期化完了後のkern_init
関数(kernel/init.c)で、80〜83行目でx86のhlt
命令を無限ループで何度も実行しているため、カーネルタスクがスケジュールされると、割り込みが入るまでCPUはhlt
命令で休むことになります。なお、schedule
関数内において、81〜87行目の条件分岐がカーネルタスクへコンテキストスイッチしている箇所です。
タスクがスリープした後のコンテキストスイッチにはちょっとした問題があります。例えば、shellがタイムスライス(10ms)の途中でキー入力待ちでスリープしたとします(図3.11)。shellが"SYSCALL_SCHED_WAKEUP_EVENT"のシステムコールを発行することになり、kernel/sched.cのwakeup_after_event
関数が呼ばれます。
shellがランキューから外され、uptimeが次に実行するタスクとして選択されたとします。すると、shellのタイムスライスの残り時間が経過するとコンテキストスイッチされてしまいます。タイムスライスを10msとしている以上、タスクには10msは実行させてあげるべきなので、これではちょっと不平等です(図3.12)。
そのため、OS5カーネルのスケジューラでは、タスクがスリープした場合はタイムスライスの残りを次のタスクへプレゼントしたものと考え、スリープ後のタイマー割り込みではコンテキストスイッチしないようにしています(図3.13)。
実装としては、スリープでのコンテキストスイッチ時にフラグ変数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
に残っている状態です。
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
)のプロトタイプ宣言を行っています。
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で説明します)。
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
)のプロトタイプ宣言です。
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個のページディレクトリを持ちます。
4.では、コンテキストスイッチの関数のバイナリを動的に生成しています。kernel/kern_task_init.cでも説明しましたが、ljmp
命令はオペランドにGDT内のTSSのオフセットを指定することで、コンテキストスイッチできます。データシートを読む限り、このオペランドはレジスタを指定することもできるようなのですが、少なくともQEMUで正常動作を確認できていません。そこで、苦肉の策として、ljmp
命令を含むコンテキストスイッチ用の関数のコンパイル後のバイナリを予め用意しておき(18〜32行目のcontext_switch_template
配列)、task_init
で新しいタスクを生成する際に、context_switch_template
からコピーしてオペランドの部分のバイナリのみ書き換える事を行っています。
6.について、タスクを実行開始する際に、あたかもランキューに以前から居たかのようにコンテキストスイッチできるよう、確保したばかりのスタック領域へ値を積んでいます。
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_ */
ファイルシステム周りの構造体等を定義しているソースコードです。
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で説明します。
kernel/fs.cについて、fs_init
は引数でファイルシステムが配置されている領域の先頭アドレスを受け取り、ファイルシステムの内容をスキャンしてstruct file
という構造体のリンクリストを作成します(図3.21、図3.22、図3.23)。
OS5では「ファイルシステムの1番最初のエントリをカーネル起動後最初に起動させるアプリケーションバイナリとする」ルールにしています。そこで、1番最初のエントリをstruct file *fshell
へ設定しています。なお、ファイルシステムの1番目のエントリは、実行ファイルであればシェルで無くとも良いです。特に意味もなくfshell
という変数名のままになっています。
fs_open
は引数で与えられたファイル名と一致するstruct file
エントリをリンクリストから検索してstruct file
のポインタを返します。
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
関数のプロトタイプ宣言のみです。
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)。
コンソール画面へ文字列を表示するためのシステムコールは"CON_PUT_STR"です。システムコール呼び出しにおいて、アプリケーションとカーネルでパラメータの受け渡しには汎用レジスタ(EAX、EBX、ECX、EDX)を使用します。CON_PUT_STRを実行するために、汎用レジスタへ必要なパラメータを設定します(図3.25)。
システムコールを発行するトリガーはソフトウェア割り込みです。OS5では割り込み番号128番をシステムコールとしています。そのため、shellは128番のソフトウェア割り込みを実行します(図3.26)。
すると、カーネル側で128番の割り込みハンドラが呼び出され、同時に特権レベルが昇格するので、カーネル空間の関数を呼び出せるようになります(図3.27)。なお、割り込みハンドラの入り口はkernel/sys.Sのsyscall_handler
ラベルの箇所です(174〜193行目)。syscall_handler
からkernel/syscall.cのdo_syscall
関数を呼び出しています。
割り込みハンドラ内では、汎用レジスタに設定された値に従って、カーネル空間内の関数を呼び出します(図3.28)。
割り込みハンドラからreturnすると、元の特権レベルに戻ります(図3.29)。そして、元のアプリケーションの処理を再開します。
現状、用意しているシステムコールは表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) | タスク終了 |
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に依存するインラインアセンブラのマクロや、構造体等を定義しています。
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
関数のみです。
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
という命令があります。このソースファイルではこれらの命令をインラインアセンブラでマクロ化しています。
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
で無効化しています。
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」と読み替えても同じことですね)。
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アドレスと割り込み番号などの定数の定義と、関数のプロトタイプ宣言です。
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の通りです。
また、do_ir_timer
関数が kern/sys.S のtimer_handler
ハンドラから呼ばれる関数です。
1: #ifndef _STDDEF_H_ 2: #define _STDDEF_H_ 3: 4: #define NULL ((void *)0) 5: 6: #endif /* _STDDEF_H_ */
汎用的に使用する定数の定義です。今のところ、NULL
のみです。
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にまとめています。このヘッダファイルではプロトタイプ宣言を行っています。
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には、汎用的に使用されるような関数を集めています。
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_ */
カーネルのロック機能についての関数のプロトタイプ宣言です。
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
を使用します。
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へまとめても良さそうな気がします。)
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_ */
キュー構造の関数のプロトタイプ宣言です。キュー構造はカーネル内で頻繁に登場するため、専用のヘッダファイルを用意しています。
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: }
カーネル内で汎用的に使えるよう、ここでキュー構造を定義しています。
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とプロトタイプ宣言があります。
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
は誰も使っていないです。。)