- Chia sẻ trải nghiệm hiện thực một kernel nguyên mẫu hệ điều hành chia sẻ thời gian trên kiến trúc RISC-V
- Giải thích khái niệm và cách vận hành của kernel chia sẻ thời gian (time-sharing) theo hướng thực hành, đồng thời triển khai bằng Zig thay vì C để tăng khả năng tái lập
- Áp dụng cách tiếp cận unikernel là đóng gói kernel và mã người dùng vào một binary duy nhất, đồng thời sử dụng OpenSBI như một lớp trung gian cho xuất console và điều khiển timer
- Các thread chạy ở user mode (U-mode), còn kernel ở supervisor mode (S-mode) thực hiện context switch thông qua timer interrupt, đồng thời vượt qua ranh giới bằng system call
- Điểm cốt lõi là kỹ thuật chuyển đổi luồng bằng cách thay thế stack frame do interrupt prologue/epilogue dựng lên để khôi phục tập thanh ghi và CSR của thread khác
- Dựa trên máy ảo QEMU và OpenSBI mới nhất, bài viết cung cấp một môi trường học tập có thể tái hiện cho mọi người, đồng thời kết nối về mặt khái niệm phổ ảo hóa như thread · process · container nên có giá trị như tài liệu nền tảng cho mục đích giáo dục và thực hành
Tổng quan
- Giới thiệu quá trình tự hiện thực một kernel hệ điều hành chia sẻ thời gian trên kiến trúc RISC-V
- Đối tượng chính là người mới học phần mềm hệ thống và kiến trúc máy tính, sinh viên, cùng các kỹ sư quan tâm đến nguyên lý vận hành mức thấp
- Thử nghiệm này sử dụng ngôn ngữ Zig thay cho C để tăng khả năng tái lập khi thực hành, đồng thời cài đặt đơn giản hơn
- Mã nguồn cuối cùng được công khai trong kho popovicu/zig-time-sharing-kernel; có thể có đôi chút chênh lệch so với nội dung bài viết
- Khuyến nghị xem phiên bản trong kho là nguồn tham chiếu duy nhất thay vì các đoạn mã trích trong bài
- Khi thực hành, nên căn chỉnh môi trường dựa trên linker script và build option của kho để thuận tiện hơn
Recommended reading
- Bài viết giả định người đọc đã có kiến thức nền tảng về kiến trúc máy tính như thanh ghi, định địa chỉ bộ nhớ, interrupt
- Khuyến nghị các tài liệu đọc trước như Bare metal on RISC-V, quy trình khởi động SBI, ví dụ timer interrupt
- Bài về bản phân phối Linux siêu nhỏ cũng hữu ích ở mức tùy chọn để hiểu triết lý tách biệt giữa kernel và user space
Unikernel
- Áp dụng cấu hình unikernel liên kết ứng dụng và OS kernel thành một tệp thực thi duy nhất
- Tránh được độ phức tạp của loader và linker trong runtime, đồng thời đơn giản hóa việc nạp mã người dùng vào bộ nhớ cùng với kernel
- Với mục tiêu giáo dục và tái lập, cách này có ưu điểm là triển khai đơn giản và môi trường nhất quán
Lớp SBI
- RISC-V sử dụng mô hình đặc quyền M/S/U mode; trong thử nghiệm này, OpenSBI được đặt ở M-mode còn kernel chạy ở S-mode
- Việc ủy quyền xuất console và điều khiển thiết bị timer cho SBI giúp đảm bảo tính di động
- Khi SBI không khả dụng có thể dùng UART MMIO làm đường dự phòng, nhưng bài thực hành khuyến nghị dùng OpenSBI mới nhất
Mục tiêu của kernel
- Để đơn giản hóa, chỉ hỗ trợ thread tĩnh, và thread được cấu thành từ các hàm không kết thúc
- Thread chạy ở U-mode và gửi system call tới kernel S-mode
- Hiện thực lập lịch chia sẻ thời gian trên một lõi duy nhất để có thể chuyển sang thread khác ở mỗi timer tick
Ảo hóa và thread thực chất là gì
- Threading chia sẻ thời gian là một hình thức ảo hóa cho phép nhiều công việc chạy song song trên một lõi đơn mà không thay đổi mô hình lập trình
- Khác với lập lịch hợp tác, việc chuyển đổi diễn ra bởi timer interrupt mà không cần yield tường minh
- Mỗi thread có tập thanh ghi và stack không thể bị thread khác đụng tới, còn phần bộ nhớ còn lại có thể chia sẻ
Stack và ảo hóa bộ nhớ
- Mỗi thread phải có stack riêng; theo calling convention, đây là thành phần thiết yếu để duy trì ngữ cảnh thực thi như biến cục bộ và bảo toàn
ra
- Phổ ảo hóa có thể hình dung là thread < process < container < VM, với mức độ cô lập và góc nhìn (view) khác nhau
- Trên Linux, container được hiện thực bằng sự kết hợp các cơ chế kernel như chroot, cgroups
Ảo hóa một thread
- Mục tiêu ảo hóa tối thiểu trong thử nghiệm này là giữ nguyên mô hình lập trình, bảo vệ thanh ghi và một phần CSR, cấp phát stack riêng
- Nhấn mạnh rằng nếu không bảo vệ được góc nhìn thanh ghi thì việc tính toán có ý nghĩa sẽ không còn khả thi
- Có thể seed sẵn
a0 v.v. lên stack để việc truyền tham số khi thread khởi động trở nên gọn gàng
Ngữ cảnh interrupt
- Có thể hiểu interrupt theo mô hình tương tự lời gọi hàm, trong đó prologue/epilogue sẽ lưu/khôi phục thanh ghi lên stack
- Để timer interrupt bất đồng bộ không làm hỏng thanh ghi, việc tuân thủ quy ước bảo toàn là bắt buộc
- Ví dụ assembly không chỉ lưu x0–x31 mà còn lưu/khôi phục cả CSR như sstatus, sepc, scause, stval
Hiện thực (mức cao)
Tận dụng quy ước stack của interrupt
- Phần thân của routine interrupt nằm giữa prologue và epilogue; nếu thay
sp sang một vùng nhớ khác thì hệ thống sẽ khôi phục tập thanh ghi của ngữ cảnh khác
- Điều này tương ứng với context switch và là ý tưởng cốt lõi của cách hiện thực chia sẻ thời gian trong thử nghiệm
- Timer interrupt can thiệp định kỳ để luân phiên thực thi luồng chính và luồng interrupt
Tách biệt kernel/user space
- Giữ nguyên ranh giới kernel S-mode / người dùng U-mode, còn việc xử lý interrupt và system call được thực hiện trong S-mode trap handler
- Trình tự khởi động là OpenSBI ở M-mode → khởi tạo kernel ở S-mode → bắt đầu thread ở U-mode
- Timer interrupt tuần hoàn giúp thực hiện lập lịch và chuyển ngữ cảnh
Hiện thực (mã nguồn)
Khởi động bằng assembly
- Trong
startup.S, xây dựng chuỗi tối thiểu gồm khởi tạo BSS và thiết lập stack pointer ban đầu, sau đó nhảy tới main của Zig
- Entry point của kernel dùng quy ước
export để liên kết với C ABI
Tệp kernel chính và driver I/O
main trong kernel.zig trước tiên kiểm tra chức năng console của OpenSBI, và nếu thất bại sẽ fallback sang UART MMIO
sbi.debug_print được gọi bằng cách thiết lập các thanh ghi a0/a1/a6/a7 đúng theo giao thức ECALL
- Sau khi thiết lập timer, hệ thống đăng ký interrupt handler cho S-mode và kích hoạt tick
S-mode handler và context switch
- Handler được viết bằng quy ước
naked của Zig để tự tay cấu thành prologue/epilogue đầy đủ, bao gồm cả bảo toàn CSR
- Trong phần thân, nó gọi
handle_kernel(sp) rồi thay bằng sp được trả về để quyết định có chuyển ngữ cảnh hay không
- Dùng
scause để phân biệt ECALL từ U-mode hay timer interrupt rồi rẽ nhánh xử lý
Các thread ở user space
- Mã người dùng được đưa vào cùng kernel trong một binary duy nhất; thread ví dụ sẽ lặp lại việc in chuỗi → vòng lặp delay
syscall.debug_print đặt số hiệu system call 64 vào a7, buffer/độ dài vào a0/a1 rồi thực hiện ECALL
- Khi khởi tạo thread, stack được seed bằng địa chỉ quay về và các giá trị thanh ghi ban đầu để ngay lần quay về đầu tiên có thể dùng tham số luôn
Chạy kernel
- Build bằng
zig build, chạy trên QEMU với máy virt + nographic + OpenSBI fw_dynamic
- Khi khởi động, sau banner OpenSBI sẽ xuất hiện đan xen các dòng in định kỳ theo từng thread ID
- Nếu build với
-Ddebug-logs=true, hệ thống sẽ hiển thị chi tiết nguồn interrupt, stack hiện tại, log enqueue/dequeue
Kết luận
- Thử nghiệm này hiện đại hóa một kernel phục vụ giáo dục bằng tổ hợp RISC-V + OpenSBI + Zig, qua đó nâng cao khả năng tái lập và tính dễ đọc
- Dù có một số đơn giản hóa như xử lý lỗi tối thiểu và stack cấp phát dư, trọng tâm vẫn là học bản chất của context switch và sự tách biệt đặc quyền
- Khả năng chuyển sang máy thật là có thể, với điều kiện điều chỉnh các hằng số linker/driver và đảm bảo SBI khả dụng
Ghi chú bổ sung: tóm tắt phổ ảo hóa
- Threads: chủ yếu ảo hóa thanh ghi và stack, khả năng chia sẻ bộ nhớ cao
- Process: ảo hóa không gian địa chỉ để cô lập bộ nhớ, có thể chứa nhiều thread bên trong
- Container: đơn vị cô lập được tạo thành bằng cách kết hợp các góc nhìn môi trường như filesystem namespace và network namespace
- VM: hướng tới ảo hóa hoàn toàn trên toàn bộ phần cứng
Tóm tắt các điểm hiện thực cốt lõi
- Hiện thực context switch bằng cách thay stack của interrupt
- Trong S-mode trap handler, lưu/khôi phục toàn bộ trạng thái bao gồm CSR
- Đường xuất kép: ưu tiên SBI, fallback sang UART MMIO
- Lập lịch đơn giản xoay quanh thread tĩnh · một lõi · time slice
- Làm rõ ranh giới U/S bằng system call dựa trên ECALL
Chưa có bình luận nào.