-
RISC-V 아키텍처에서 타임셰어링 운영체제 프로토타입 커널을 구현한 경험을 공유
-
시분할(time-sharing) 커널의 개념과 동작 방식을 실습 중심으로 설명하며, C 대신 Zig로 구현해 재현성을 높임
- 커널과 사용자 코드를 하나의 바이너리로 묶는 unikernel 접근을 취하고, 콘솔 출력과 타이머 제어를 위해 OpenSBI를 활용하는 레이어링을 채택
- 스레드는 사용자 모드(U-mode) 에서 실행되고, 커널은 감독자 모드(S-mode) 에서 타이머 인터럽트를 통해 컨텍스트 스위치를 수행하며, 시스템 호출로 경계를 넘나듦
- 핵심은 인터럽트 프로로그/에필로그가 쌓아둔 스택 프레임을 교체해 다른 스레드의 레지스터 집합과 CSR을 복원함으로써 흐름을 전환하는 기법
- QEMU 가상 머신과 최신 OpenSBI를 기반으로 누구나 재현 가능한 학습 환경을 제공하며, 스레드·프로세스·컨테이너 등 가상화 스펙트럼을 개념적으로 연결해 교육 및 실습 목적에 적합한 기초 자료로 가치가 있음
개요
- RISC-V 아키텍처에서 타임셰어링 운영체제 커널을 직접 구현한 과정을 소개함
- 주요 독자는 시스템 소프트웨어 및 컴퓨터 구조 입문자, 학생, 그리고 저수준 동작 원리 이해에 관심 있는 엔지니어
- 이 실험은 기존 C 언어 대신 Zig 언어를 사용하여 실습 재현성을 높였으며 설치가 간단함
- 최종 코드는 popovicu/zig-time-sharing-kernel 저장소에 공개되어 있으며, 본문과 약간의 싱크 차이가 있을 수 있음
- 글의 코드 발췌보다 저장소 버전을 단일 신뢰원으로 간주할 것을 권장함
- 실습 시 저장소의 링커 스크립트·빌드 옵션을 기준으로 환경을 맞추는 편의를 제공함
Recommended reading
- 글은 레지스터, 메모리 주소 지정, 인터럽트 등 컴퓨터 구조 기초를 전제함
- 선행 자료로 Bare metal on RISC-V, SBI 부팅 과정, 타이머 인터럽트 예제를 추천
-
마이크로 리눅스 배포 글은 커널·유저 공간 분리 철학 이해에 선택적으로 유용
Unikernel
- 애플리케이션과 OS 커널을 단일 실행 파일로 링크하는 unikernel 구성을 채택함
- 런타임의 로더·링커 복잡도를 회피하고, 사용자 코드를 커널과 함께 메모리에 적재하는 단순화 달성
- 교육 및 재현 목적에서 배포 단순성과 환경 일관성을 확보하는 장점 제공
SBI layer
- RISC-V는 M/S/U 모드의 권한 모델을 사용하며, 본 실험은 OpenSBI를 M-모드에 두고 커널을 S-모드에서 동작시킴
- 콘솔 출력과 타이머 장치 제어를 SBI로 위임해 이식성을 확보
- SBI 미가용 시 UART MMIO를 폴백으로 사용하되, 실습에선 최신 OpenSBI 사용을 권장
Goal for the kernel
- 단순화를 위해 정적 스레드만 지원하고, 스레드는 종료하지 않는 함수로 구성
- 스레드는 U-모드에서 실행되며 S-모드 커널로 시스템 호출을 보냄
-
타이머 틱마다 다른 스레드로 전환될 수 있도록 단일 코어 기준의 시분할 스케줄링을 구현
Virtualization and what exactly is a thread
- 시분할 스레딩은 프로그래밍 모델을 변화시키지 않고 단일 코어에서 여러 작업을 병행하는 가상화 방식
- 협력형 스케줄링과 달리 명시적 yield 없이 타이머 인터럽트로 전환이 발생
- 스레드는 각자 건드릴 수 없는 레지스터 집합과 스택을 가지며, 나머지 메모리는 공유 가능
The stack and memory virtualization
- 스레드는 별도 스택을 가져야 하며, 호출 규약상 지역 변수·ra 보존 등 실행 컨텍스트 유지에 필수 요소
- 가상화 스펙트럼은 스레드 < 프로세스 < 컨테이너 < VM로 이어지며 격리 수준과 시야(view) 가 달라짐
- 리눅스에서 컨테이너는 chroot, cgroups 등 커널 메커니즘 조합으로 구현됨
Virtualizing a thread
- 본 실험의 최소 가상화 목표는 프로그래밍 모델 불변, 레지스터·일부 CSR 보호, 개별 스택 할당
- 레지스터 뷰가 보호되지 않으면 의미 있는 계산이 불가능해지는 이유를 강조
- 초기 a0 등을 스택에 시딩해 스레드 시작 시 인자 전달을 간결히 처리
Interrupt context
- 인터럽트는 프로로그/에필로그로 레지스터를 스택에 보존/복원하는 함수 호출 유사 모델로 이해할 수 있음
- 비동기 타이머 인터럽트가 레지스터를 망가뜨리지 않도록 보존 규약 준수가 필수
- 예제 어셈블리는 x0–x31 보존에 더해 sstatus, sepc, scause, stval 등 CSR까지 저장/복원
Implementation (high-level)
Leveraging the interrupt stack convention
- 인터럽트 루틴의 본체는 프로로그와 에필로그 사이에 위치하며, sp를 다른 메모리 영역으로 교체하면 다른 컨텍스트의 레지스터 집합을 복원하게 됨
- 이는 곧 컨텍스트 스위치에 해당하며, 본 실험의 시분할 구현 핵심 아이디어
- 타이머 인터럽트가 주기적으로 개입해 메인 흐름과 인터럽트 흐름을 교차 실행함
Kernel/user space separation
-
S-모드 커널 / U-모드 사용자의 경계를 유지하며, 인터럽트와 시스템 호출 처리는 S-모드 트랩 핸들러에서 수행
- 부팅은 M-모드의 OpenSBI→ S-모드 커널 초기화→ U-모드 스레드 시작 순서로 진행
- 주기적 타이머 인터럽트가 스케줄링·문맥 전환을 가능케 함
Implementation (code)
Assembly startup
-
startup.S에서 BSS 초기화와 초기 스택 포인터 설정 후 Zig의 main으로 점프하는 최소 시퀀스 구성
- 커널 진입점은 C ABI와의 연계를 위해 export 규약을 사용
Main kernel file and I/O drivers
-
kernel.zig의 main은 우선 OpenSBI 콘솔 기능을 점검하고, 실패 시 UART MMIO로 폴백함
-
sbi.debug_print는 ECALL 프로토콜에 맞춰 a0/a1/a6/a7 레지스터를 설정해 호출
- 타이머 설정 후 S-모드 인터럽트 핸들러를 등록하고 틱을 활성화
S-mode handler and the context switch
- 핸들러는 Zig의 naked 규약으로 작성해 CSR 보존을 포함한 완전한 프로로그/에필로그를 수동 구성
- 본체에서 handle_kernel(sp)를 호출하고, 반환된 sp로 교체해 스위치 여부를 결정
-
scause로 U-모드 ECALL인지 타이머 인터럽트인지 판별해 분기 처리
The user space threads
- 사용자 코드는 커널과 함께 단일 바이너리에 포함되며, 예시 스레드는 문자열 출력 → 지연 루프를 반복
-
syscall.debug_print는 a7에 64번 시스템 호출 번호, a0/a1에 버퍼/길이를 넣고 ECALL을 수행
- 스레드 초기화 시 스택에 복귀 주소·초기 레지스터 값을 시딩해 첫 복귀 때 바로 인자 사용이 가능
Running the kernel
- 빌드는 zig build, 실행은 QEMU에서 virt 머신 + nographic + OpenSBI fw_dynamic을 지정해 수행
- 부팅 시 OpenSBI 배너 이후 스레드 ID별 주기적 출력이 교차 등장
-
-Ddebug-logs=true로 빌드하면 인터럽트 원천, 현재 스택, 큐잉·디큐잉 로그가 상세히 표시
Conclusion
- 본 실험은 RISC-V + OpenSBI + Zig 조합으로 교육용 커널을 현대화해 재현성과 가독성을 높임
- 최소한의 오류 처리와 과할당 스택 등 단순화가 있으나, 컨텍스트 스위치의 본질과 권한 분리 학습에 초점이 맞춰짐
- 리얼 머신 이식성은 링커·드라이버 상수 조정과 SBI 가용성 확보를 전제로 가능
추가 메모: 가상화 스펙트럼 정리
-
Threads: 레지스터·스택 가상화 위주, 메모리 공유 가능성 높음
-
Process: 주소 공간 가상화로 메모리 격리, 내부에 다수 스레드 포함 가능성
-
Container: 파일시스템·네트워크 네임스페이스 등 환경 시야를 조합해 구성되는 격리 단위임
-
VM: 하드웨어 전반의 완전 가상화를 지향함
핵심 구현 포인트 요약
-
인터럽트 스택 교체로 컨텍스트 스위치 실현
-
S-모드 트랩 핸들러에서 CSR 포함 전체 상태 보존/복원
-
SBI 우선, UART MMIO 폴백의 출력 경로 이중화
-
정적 스레드·단일 코어·타임 슬라이스 중심의 단순 스케줄링
-
ECALL 기반 시스템 호출로 U/S 경계 명확화