커널 기본 요소 (std::kernel)
std/kernel/은 작은 커널이나 하이퍼바이저를 처음부터 작성하기 위한
구성 요소 — 물리 메모리 할당, 페이지 테이블과 MMU 컨텍스트, 프로세스
제어 블록, 라운드 로빈 스케줄러, IPC 메일박스, 시스템 콜 디스패치 —
를 제공합니다. 모두 프리스탠딩 환경에서 안전하며, 단일 주소 공간을 위한
펌웨어를 작성한다고 가정하는 std::hal/
std::interrupt보다 더 저수준으로, 여러 개의
주소 공간을 관리하는 데 쓰입니다.
#include <std/kernel/frame.h>
#include <std/kernel/process.h>
#include <std/kernel/scheduler.h>
static struct FrameAllocator frames;
void kernel_setup() {
frames.init(1024); // 1024개의 4K 프레임 = 4 MiB 추적
long long f = frames.alloc(); // OOM 시 -1
struct PCB p = std::PCB::new(1, 0x1000, 0x8000, 0); // pid, entry, sp, page_table
struct Scheduler sched = std::Scheduler::new();
unsafe { sched.spawn_proc(&p); }
int next = sched.next(); // 우선순위가 가장 높은 READY 프로세스 인덱스
}
물리 프레임 얼로케이터 (kernel/frame.h)
FrameAllocator는 4 KiB 프레임 단위의 비트맵 얼로케이터입니다
(FRAME_BITMAP_SIZE = 4096 워드 = 131072개 프레임 = 인스턴스 하나당
512 MB 추적). init(n), alloc()(프레임 번호, OOM 시 -1),
free(frame), is_used(frame), mark_range(start, count)(범위를 미리
예약 — 예를 들어 커널 이미지 자체), free_count(). init은 n을 비트맵이
추적하는 131072개 프레임으로 제한하며, 이미 해제된 프레임을 해제하면 아무
일도 하지 않습니다. init_at(base, n)은 프레임 0을 물리 주소 0이 아닌
base에서 시작합니다(대부분의 Arm·RISC-V 보드는 RAM이 1~2 GiB에서
시작합니다). frame_addr(f)는 프레임 f의 물리 주소를 줍니다.
페이징 (kernel/paging.h)
PageEntry(64비트 원시 엔트리 하나: 비트 [63:12]에 물리 프레임 번호,
비트 [11:0]에 플래그)와 PageTable(이를 512개 담는 테이블). 플래그:
PAGE_PRESENT, PAGE_WRITABLE, PAGE_USER, PAGE_WRITE_THRU,
PAGE_NO_CACHE, PAGE_ACCESSED, PAGE_DIRTY, PAGE_HUGE.
PageTable 메서드: init(), map(idx, phys_addr, flags),
unmap(idx), is_present(idx), get_phys(idx), get_flags(idx),
set_flags(idx, flags).
PAGE_SIZE는 4096바이트, PAGE_TABLE_SIZE는 테이블당 512개 엔트리입니다.
MMU 컨텍스트 (kernel/mmu.h)
MmuContext<F: MmuFormat>는 CPU가 직접 탐색하는 페이지 테이블을 만듭니다.
F는 컴파일 타임에 고르는 페이지 테이블 형식 타입이며
(BST<K, V, C: Order>의 순서 관계와 같은 방식), 아키텍처마다 하나씩 있습니다:
| 형식 | 테이블 |
|---|---|
MmuX86_64 |
x86-64 4단계 페이징, 48비트 VA, 8바이트 엔트리. 실행 불가 페이지는 NX를 설정하며 EFER.NXE가 필요합니다 |
MmuX86_32 |
PAE 없는 x86 32비트 페이징: 2단계, 4바이트 엔트리(NX 없음) |
MmuSv39 |
RISC-V Sv39(RV64): 3단계, 8바이트 PTE. 리프는 A와 D를 미리 설정합니다 |
MmuSv32 |
RISC-V Sv32(RV32): 2단계, 4바이트 PTE |
MmuAArch64 |
AArch64 stage 1, 4 KiB 그래뉼, 48비트 VA: 4단계. 리프는 일반 메모리에 MAIR 인덱스 0, 디바이스 메모리에 1을 씁니다(MAIR_EL1을 Attr0 = 0xFF, Attr1 = 0x04로 설정). TCR_EL1은 4 KiB 그래뉼과 T0SZ = 16이어야 합니다 |
MmuArmV7 |
ARMv7-A short descriptor: 16 KiB 정렬된 16 KiB L1 테이블과 1 KiB L2 테이블. 일반 메모리는 write-back(TEX=001 C B), 디바이스 메모리는 TEX=000 B |
MmuNative는 컴파일 대상 CPU의 형식입니다(MPU만 있고 MMU가 없는 Cortex-M과
wasm에는 없습니다). 모든 형식은 어느 대상에서든 만들고 탐색할 수 있지만(예:
호스트의 테스트나 이미지 빌더), activate()와 TLB 함수는 형식이 실행 중인
베어메탈 CPU의 것일 때만 동작하고 그 외에는 0을 반환합니다.
#include <std/kernel/mmu.h>
static struct FrameAllocator g_frames;
void vm_setup(unsigned long root) { // 0으로 채운 4 KiB 정렬 테이블
g_frames.init_at(0x80400000UL, 256UL);
struct MmuContext<MmuNative> m = std::MmuContext::new(root, &g_frames, 0UL);
m.map(0x80000000UL, 0x80000000UL, MMU_WRITE | MMU_EXEC); // 항등 매핑: 커널 이미지
m.map(0x10000000UL, 0x10000000UL, MMU_WRITE | MMU_DEVICE); // UART
m.activate(); // satp / CR3 / TTBR0
}
MmuContext::new(root, frames, phys_offset)(정적 메서드이며F는 선언된 타입에서 추론됩니다):root는 0으로 채운 루트 테이블의 물리 주소(4 KiB 정렬,MmuArmV7은 16 KiB),frames는 새 중간 테이블마다 프레임 하나를 공급하는 살아 있는&staticFrameAllocator이며, 테이블은 물리 주소 +phys_offset에서 읽고 씁니다(항등 매핑 커널은 0, higher-half 커널은 direct map의 기준 주소).map(virt, phys, flags)는 4 KiB 페이지 하나를 매핑합니다.flags는MMU_WRITE,MMU_EXEC,MMU_USER,MMU_DEVICE입니다(모든 매핑은 읽을 수 있으며, RISC-V는 캐시 가능성을 PMA에 맡깁니다). 테이블 프레임을 할당할 수 없거나 더 큰 페이지가 이미virt를 덮고 있으면 0을 반환합니다.unmap(virt),walk(virt, &phys_out)(더 큰 블록/슈퍼페이지 리프도 따라갑니다),entry(virt, &entry_out)(리프 엔트리 원본).tlb_flush_all(),tlb_flush_page(virt),activate(): x86은CR3재적재 /invlpg, RISC-V는satp기록(S·U 모드 변환을 켭니다) /sfence.vma, AArch64는TTBR0_EL1기록 /TLBI, ARMv7은TTBR0기록 /TLBIALL입니다. 페이징 켜기(CR0.PG,SCTLR.M)와 MAIR/TCR 설정은 부트 코드의 몫입니다.
tests/stdlib/baremetal/qemu/run.sh는 모든 형식을 QEMU에서 변환을 켠 채로
실행합니다 — S 모드의 Sv39와 Sv32, MMU를 켠 AArch64와 ARMv7, x86 32비트
페이징과 x86-64 롱 모드 — 그리고 별칭 매핑을 통해 페이지를 읽고 씁니다.
페이지 테이블 형식: MmuFormat
MmuContext는 제한된 제네릭 구조체입니다.
형식 매개변수는 MmuFormat 트레이트를 만족해야 하므로, 페이지 테이블 형식이
아닌 타입은 map() 깊숙한 곳이 아니라 MmuContext<...>를 쓴 지점에서 오류가
납니다(type 'X' does not satisfy constraint 'MmuFormat' ... (no method 'levels')).
| 트레이트 | 요구 사항 | 만족하는 타입 |
|---|---|---|
MmuFormat |
levels(); shift(l)(단계 l의 인덱스가 시작하는 VA 비트, 루트가 l = 0); entries(l); esize()(엔트리당 4 또는 8바이트); table_desc(phys)와 leaf_desc(phys, flags)(다음 단계 테이블을 가리키는 엔트리 / MMU_* 플래그를 가진 4 KiB 페이지 엔트리); valid(e), is_leaf(e, l), addr(e, l); load_root(root), flush_all(), flush_page(virt)(실행했으면 1, 실행 중인 CPU의 형식이 아니면 0). 모두 const |
MmuX86_64, MmuX86_32, MmuSv39, MmuSv32, MmuAArch64, MmuArmV7(따라서 MmuNative도) |
map, unmap, walk, entry의 모든 단계는 이 메서드들에 대한 직접적이고
인라인 가능한 호출입니다. 새 아키텍처의 형식은 이 메서드들을 가지기만 하면
준수하며, mmu.h의 다른 부분은 바뀌지 않습니다. 페이지 테이블을 다루는 코드도
같은 제약으로 한 번만 작성합니다. 아래 예제는 호스트 버퍼에 RISC-V Sv39 테이블을
만들고(물리 주소가 곧 호스트 주소이므로 phys_offset은 0) 이를 탐색합니다:
#include <std/kernel/mmu.h>
// 모든 페이지 테이블 형식에 대해 한 번만 작성합니다.
generic<F: MmuFormat>
int map_range(&MmuContext<F> m, unsigned long virt, unsigned long phys,
unsigned long pages, unsigned int flags) {
unsigned long i = 0UL;
while (i < pages) {
if (!m.map(virt + i * 4096UL, phys + i * 4096UL, flags)) return 0;
i = i + 1UL;
}
return 1;
}
static unsigned char g_mem[17 * 4096]; // 프레임 16개 + 정렬 여유분
static struct FrameAllocator g_frames;
int main() {
// 호스트에서 g_mem 안에 RISC-V Sv39 테이블을 만듭니다(물리 주소 == 호스트 주소).
unsigned long base = 0UL;
unsafe { base = ((unsigned long)&g_mem[0] + 4095UL) & ~4095UL; }
g_frames.init_at(base, 16UL);
g_frames.mark_range(0UL, 1UL); // 프레임 0은 루트 테이블
struct MmuContext<MmuSv39> m = std::MmuContext::new(base, &g_frames, 0UL);
if (!map_range(&m, 0x40000000UL, 0x80000000UL, 4UL, MMU_WRITE)) return 1;
unsigned long phys = 0UL;
if (!m.walk(0x40002010UL, &phys) || phys != 0x80002010UL) return 2;
return m.activate(); // 0: Sv39는 이 CPU의 MMU가 아닙니다
}
프로세스 제어 블록 (kernel/process.h)
PCB — pid, state(PROC_READY/PROC_RUNNING/PROC_BLOCKED/
PROC_ZOMBIE), priority, 저장된 stack_ptr/pc, page_table(물리
루트 주소), parent_pid, exit_code. PCB::new(pid, entry, sp, page_table); 메서드 set_state, set_priority, save_context(sp, pc), exit(code)(PROC_ZOMBIE로 전이).
스케줄러 (kernel/scheduler.h)
Scheduler — 최대 SCHED_MAX_PROCS(256)개의 PCB에 대한 우선순위
기반 라운드 로빈. Scheduler::new(); spawn_proc(&PCB)(인덱스 반환,
가득 찼으면 -1), next()(우선순위가 가장 높은 READY 프로세스 선택),
yield()(현재 프로세스를 READY로 표시하고 다음 프로세스 선택),
block_current(), unblock(idx), remove(idx)(좀비 회수),
ready_count(). next()는 READY 프로세스 중 우선순위가 가장 높은 것을
고르고, 우선순위가 같으면 현재 프로세스 다음부터 돌아가며 고릅니다.
remove는 current가 같은 프로세스를 가리키도록 유지합니다(제거된 것이
현재 프로세스였으면 -1).
IPC 메일박스 (kernel/ipc.h)
Mailbox(Mailbox::new(owner_pid)로 생성) — 고정 용량
(MAILBOX_CAPACITY = 64개 메시지, MSG_MAX_SIZE = 256바이트 페이로드)
메시지 큐. send(sender_pid, type, payload)(payload는
[]const unsigned char이며 최대 256바이트를 보관. 신호 전용 메시지는 빈 슬라이스를 전달), recv(&Message out),
peek(&Message out)(비파괴적), has_msg(),
length(), clear().
시스템 콜 테이블 (kernel/syscall.h)
SyscallTable(SyscallTable::new()로 생성) — 타입이
fn long long(long long, long long, long long)인 핸들러를 최대
SYSCALL_MAX(256)개까지 등록. register_(num, handler)/
unregister_(num)(끝의 밑줄: register는 C 키워드이므로),
dispatch(num, arg0, arg1, arg2)(핸들러의 결과를 반환하며, 등록되지
않았으면 -1), is_registered(num).