Tự tay xây dựng kernel hệ điều hành từ con số không
(popovicu.com)- 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
a0v.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
spsang 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ớimaincủa Zig- Entry point của kernel dùng quy ước
exportđể liên kết với C ABI
- Entry point của kernel dùng quy ước
Tệp kernel chính và driver I/O
maintrongkernel.zigtrướ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 MMIOsbi.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
nakedcủ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ằngspđượ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ý
- Trong phần thân, nó gọi
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àoa7, buffer/độ dài vàoa0/a1rồ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áyvirt+nographic+ OpenSBIfw_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
1 bình luận
Ý kiến Hacker News
Có thể trải nghiệm một công việc tương tự dưới dạng “gói” với "Operating System in 1000 Lines of Code"; trước đây tôi đã làm theo bằng Zig (vừa làm vừa chuyển các đoạn mã C sang Zig) và thấy rất thú vị, mã nguồn và VOD của tôi ở đây: https://github.com/kristoff-it/kristos/
Đây là nội dung do chính tác giả bài viết gửi riêng: https://news.ycombinator.com/item?id=45236479, chính tác giả đã tự làm lại Tiny OS Kernel và muốn thử nghiệm cụ thể với RISC-V và môi trường OpenSBI, đồng thời dùng Zig thay vì C truyền thống; thực ra tôi nghĩ cũng có thể dễ dàng làm theo bằng C hoặc Rust, toàn bộ quá trình có phần hơi thô nhưng là một thử nghiệm và phần nhập môn để bắt đầu học phát triển OS Kernel và kiến trúc máy tính, tôi nghĩ đây là một dự án khá vui để nghịch vào cuối tuần, có thể xem toàn bộ walkthrough và link Github ở phía trên
Những dự án như thế này thực sự rất ấn tượng; xét cho cùng Linux cũng chỉ là một kernel, nhưng công việc đó đã mở đường để cài đặt Unix mã nguồn mở lên hàng tỷ thiết bị, tôi nghĩ đó là điều rất tuyệt
Điều thú vị hơn nữa là khi phát hành Linux lần đầu, Torvalds đã viết trong email rằng đây chỉ là “một sở thích thôi, sẽ không to lớn và chuyên nghiệp như GNU” https://groups.google.com/g/comp.os.minix/c/dlNtH7RRrGA/m/SwRavCzVE7gJ
Tôi không nghĩ những dự án như thế này <i>cực kỳ</i> ấn tượng; cách làm một kernel đa nhiệm tối giản đã là lộ trình được biết đến từ vài chục năm trước, việc tạo ra một kernel có thể boot và làm vài tác vụ đơn giản là điều chỉ cần một mức độ thông minh và kiên trì nhất định là làm được, làm trên RISC-V thì có hơi phức tạp hơn x86 một chút nhưng thông tin khởi tạo phần cứng rất dễ tìm (https://wiki.osdev.org/RISC-V_Meaty_Skeleton_with_QEMU_virt_board tham khảo), và chính tác giả cũng nói rõ đây là “bài tập từng làm trong môn hệ điều hành”, theo tôi thì người có bằng kỹ sư phần mềm đều có thể hoàn thành, tất nhiên sẽ có lỗi hoặc phần chưa hoàn thiện, nhưng bản thân việc làm đa tiến trình hoặc cô lập tiến trình bằng MMU giờ không còn là chuyện khó nữa
Cũng như Linux, việc Stallman bắt đầu làm trình biên dịch C và các tiện ích Unix từ năm 1984 cũng đã mở đường cho Unix mã nguồn mở được cài trên hàng tỷ máy
Zig thực sự rất hợp để phát triển hệ điều hành, RISC-V cũng vậy; tôi cũng từng bắt đầu bài tập y hệt với x86 nhưng nhanh chóng nản vì quá nhiều boilerplate legacy, còn phía RISC-V thì gần như không có mấy thứ đó, nên bắt đầu phát triển dễ hơn nhiều https://github.com/Fingel/aeros-v
Tôi nghĩ nếu bắt đầu trên x86 thì chỉ cần viết bootloader ổn là cũng không có quá nhiều boilerplate; multiboot loader thường lo phần real mode, còn đa số mọi người muốn protected mode nên chỉ cần thiết lập vài bảng rồi nhảy sang là được, nếu muốn tắt legacy interrupt controller thì phải đụng thêm một chút, nhưng bù lại có lợi thế là có thể boot thử ngay trên desktop PC (dù cần lưu ý về giao diện console), hobby OS của tôi từng dùng BIOS boot và vài tính năng VGA rồi khổ sở vì tương thích kém; serial console thì dễ hơn nhiều nhưng máy tính ngày nay thường không có cổng serial
Về cơ bản thì đây là việc lôi lại mức độ an toàn của Object Pascal hoặc Modula-2 rồi đóng gói lại bằng cú pháp C; C vốn cũng chẳng có gì đặc biệt ngoài việc được lan rộng nhờ giấy phép của UNIX
Tôi cũng muốn tự thử, nên tò mò kernel RISC-V này đang được chạy trong môi trường nào, chỉ dùng Qemu hay có phần cứng thực nào đáng đề xuất không thì rất mong được chia sẻ
Tôi nghĩ ISA RISC-V thực sự rất dễ tiếp cận; tài liệu rất tốt, ví dụ thì rất nhiều, trình giả lập cũng khá phong phú, và cả machine code chưa nén cũng dễ đọc; tôi đang tự viết một cuốn sách cho con gái mình đồng thời làm một OS nhỏ có Forth và timesharing https://punkx.org/projekt0/book/part1/os.html, nếu là x86 thì tôi nghĩ mình đã chẳng thể bắt tay vào, trong quá trình đó tôi đang học cả Forth lẫn hợp ngữ RISC-V và thực sự rất vui, nếu bạn muốn tự làm một toy OS từ đầu thì đây đúng là thời điểm tuyệt vời (thú vị như thập niên 1980), hãy kiếm một bo mạch RISC-V giá rẻ (như rp2350) rồi tải các phần manual liên quan lên AI như Claude, lúc bí sẽ được giúp rất nhiều
Những thử nghiệm như thế này lúc nào cũng vui và thú vị; tôi cũng muốn khuyến khích mọi người thử cả việc tự làm mật mã của riêng mình hay những thứ khó nhằn khác, lời khuyên “đừng tự triển khai mật mã” có nghĩa là đừng dùng thứ chưa được kiểm chứng trong thực tế, còn cho mục đích thử nghiệm/nghiên cứu thì không có gì nguy hiểm nên cứ thoải mái thử, chúng ta cần nhiều hệ điều hành và nhiều lựa chọn hơn
(Trích phán quyết của Tây Ban Nha) Chặn http thì còn tạm hiểu được, nhưng thế này thì quá đáng...
Tôi có nghe nói ở Tây Ban Nha, Cloudflare (và có lẽ cả những nơi khác) bị chặn nhầm vì vấn đề liên quan đến phát sóng bóng đá; tôi tò mò không biết người ta đang vượt qua chuyện này thế nào, dùng VPN à? Nếu IP quan trọng bị chặn thì công việc chắc cũng bị ảnh hưởng
(Trong phán quyết) đọc đến đoạn nói việc này do Liên đoàn bóng đá chuyên nghiệp Tây Ban Nha và Telefónica Audiovisual Digital khởi xướng, tôi thấy những người như vậy là tội phạm
...cái gì cơ? Ý là một tổ chức bóng đá ở Tây Ban Nha có quyền hạn chế truy cập Internet của cả quốc gia sao?
Tôi tò mò không biết có thể kiếm phần cứng RISC giá rẻ bằng cách nào
Trên AliExpress có bán bo mạch Milk-V Duo S với giá 10 đô, dạo gần đây nó xuất hiện khá thường xuyên trong danh sách gợi ý; thông số chính gồm SG2000 master nâng cấp và 512MB RAM, IO rộng hơn, một số mẫu có WI-FI6/BT5 (trừ mẫu 512M-Basic/eMMC), cổng USB 2.0 host, Ethernet 100Mbps hỗ trợ PoE, dual MIPI CSI, hỗ trợ công tắc chuyển boot giữa RISC-V và ARM, v.v. https://aliexpress.com/w/wholesale-Milk%2525252dV-Duo-S.html
Đã có khá nhiều bo mạch, nhưng cái tôi thấy hứng thú và đã góp vốn là VisionFive 2 Lite https://www.kickstarter.com/projects/starfive/visionfive-2-lite-unlock-risc-v-sbc-at-199/description, tôi không có VisionFive2 thế hệ đầu nhưng nghe đánh giá tốt và hệ sinh thái ngày càng lớn, vẫn còn vài phần chưa hoàn thiện nhưng tôi mong nó sớm được giao; thứ tôi đang dùng cá nhân là PolarFire SoC Discovery Kit, một bo có RISC-V bốn nhân kèm FPGA, hơi đắt (130 đô) và không hợp với tất cả mọi người, nhưng điểm thú vị là bo lại rẻ hơn chính con chip https://www.microchip.com/en-us/development-tool/MPFS-DISCO-KIT, tài liệu và toolchain của Microchip thì cũ kỹ và không thật sự tốt, nhưng khi đã quen rồi thì chạy mã RISC-V bare-metal thực sự rất dễ, ví dụ Linux/bare-metal cũng khá thân thiện
Tôi đề xuất trước mắt nên thử chạy bằng trình giả lập trên máy x86 hoặc máy Apple ngay cả khi chưa có phần cứng thực; tốc độ phát triển sẽ nhanh hơn so với bo thật, và dùng QEMU chẳng hạn là có thể thử ngay https://www.qemu.org/docs/master/system/target-riscv.html
Raspberry Pi Pico 2 cũng hỗ trợ RISC-V nên cũng ổn https://www.raspberrypi.com/products/raspberry-pi-pico-2/
Cảm ơn mọi người vì các câu trả lời, còn những ai downvote câu hỏi của tôi thì tôi không nghĩ có lý do gì để cảm ơn cả