Fomos: Hệ điều hành thử nghiệm được xây dựng bằng Rust
(github.com/Ruddle)- Fomos là một hệ điều hành thử nghiệm được xây dựng bằng Rust, đồng thời là dự án nhằm tìm hiểu các ý tưởng về OS phi-Unix và những bài toán của mô hình exo-kernel
- Cung cấp đầu ra đồ họa, cấp phát động, nạp và chạy đồng thời nhiều ứng dụng, hỗ trợ chuột·bàn phím Virtio và lập lịch hợp tác
- Ứng dụng được xử lý như một hàm đơn lẻ có dạng
pub extern "C" fn _start(ctx: &mut Context) -> i32, và nhận các chức năng của OS thông quaContextmà không cần thư viện chuẩn Contextlà một cấu trúc chứa trạng thái và các chức năng như log, PID, framebuffer,calloc,cdalloc,store, đầu vào; các tính năng mới được thêm ở phía sau để duy trì khả năng tương thích với ứng dụng cũ- Không có system call; ứng dụng trả lại quyền điều khiển cho OS bằng
return, sau đó hàmstartsẽ được gọi lại theo mô hình thực thi hợp tác - Trạng thái ứng dụng có thể được lưu trong
Context.store, và vòng lặp kernel có cấu trúc đơn giản: duyệt danh sách ứng dụng rồi gọi_start(Context::new(...))cho từng ứng dụng - Vì toàn bộ chức năng và tác dụng phụ đều được truyền qua
Context, nên có thể xây dựng sandboxing, đo đạc và gỡ lỗi bằng cách thay thế hoặc bọc các hàm trongContext - Hiện chưa triển khai bảo mật, và ứng dụng vẫn có thể xem RAM của ứng dụng khác; có kế hoạch triển khai bảo mật dữ liệu mà không cần context switch và stack bộ nhớ ảo riêng cho từng ứng dụng
- Các hạng mục còn thiếu gồm lưu trữ bền vững, hỗ trợ GPU, mạng và lớp trừu tượng để chia sẻ dữ liệu/chức năng giữa các ứng dụng; Virgl hiện đang được phát triển
- Việc build được thực hiện bằng
./build.sh; có thể cầnrust nightly,gcc, vàqemuvới các cờ Virgl và SDL
1 bình luận
Các ý kiến trên Hacker News
Điều tôi không thích là trong hệ thống tiền nhiệm thì
while (true)có thể làm hệ thống chậm đi, nhưng trong hệ thống hợp tác, ngay khoảnh khắc nó không trả lại quyền điều khiển thì về cơ bản máy sẽ đứng lạiNhìn từ góc độ bảo mật, kiểu hệ thống này cũng khiến tấn công từ chối dịch vụ trở nên quá dễ, và một lỗi trong bất kỳ ứng dụng nào cũng có thể lan ra toàn hệ thống
Tôi không phải nhà phát triển hệ điều hành, nên nếu sai thì mong được chỉnh lại
Lý do hệ điều hành từ bỏ đa nhiệm hợp tác không phải vì nó giải quyết vĩnh viễn mọi vấn đề “sử dụng tài nguyên mất kiểm soát”, mà vì những lỗi đơn giản ở mức ứng dụng như chặn luồng UI hay vô tình tạo vòng lặp vô hạn sẽ phá hỏng trạng thái của toàn hệ thống
Với một hệ thống được thiết kế để chạy chương trình tùy ý, điều đó khá chí mạng
Vẫn có thể cho phép ứng dụng được lập lịch theo kiểu hợp tác, đồng thời ngăn
while(true){}chiếm giữ hệ thống vô hạnVí dụ có thể đặt giới hạn thời gian theo từng ứng dụng, hoặc thử nghiệm hơn là phát hiện vòng lặp và đặt một giới hạn thời gian rộng rãi
Với lập trình viên thì sẽ thuận tiện khi mức cô lập giữa các chương trình không liên quan là tối đa và giữa các chương trình liên quan là tối thiểu, nhưng với người dùng thì sẽ thuận tiện khi tài nguyên tính toán được cô lập mạnh, còn những thứ như lưu trữ hay quyền hạn thì ít bị chặn hơn
Trên thực tế cần một cơ chế lập lịch phức tạp gần với tiền nhiệm hơn hợp tác, nhưng vẫn hơi mang tính hợp tác
Ngay cả trên Linux, một chương trình độc hại như fork bomb cũng không khó làm hệ thống đứng, đặc biệt khi bật swap; và dù có bộ lập lịch tiền nhiệm, nếu một chương trình chiếm 99% các luồng hệ thống thì thực tế phần lớn thời gian vẫn là chương trình đó chạy
Lập lịch là một phổ, và hệ điều hành hiện nay tuy là tiền nhiệm nhưng cũng có phần hợp tác ở mức nào đó
Ứng dụng có thể quyết định có nhường quyền điều khiển hay không
Ngược lại, có thể để hệ điều hành ở dạng hợp tác, nhưng khi vượt ngưỡng sử dụng tài nguyên hoặc xảy ra ngắt timer thì hiếm khi chuyển ngữ cảnh sang chế độ lỗi để biến thành tiền nhiệm, kết thúc ứng dụng rồi quay lại hợp tác
Có thể gọi cách này là hợp tác lạc quan, tiền nhiệm bi quan
Vòng lặp
while(true)sẽ làm sập hệ thống đơn lõi, nhưng trên hệ thống đa lõi thì không nhất thiết như vậyCác điểm đánh đổi ngày nay có thể đã khác so với thời cấu trúc cơ bản của các hệ điều hành chúng ta đang dùng được hình thành
Tôi đặc biệt thích đoạn “trong Fomos, ứng dụng chỉ là một hàm”
File thực thi của Unix hay Windows rất phức tạp so với các hàm độc lập, nên thật khó tưởng tượng một kernel được viết theo cách này sẽ tuyệt đến mức nào
Tôi cũng tò mò liệu Smalltalk/Squeak có theo kiểu này không, và mong tác giả tiếp tục với hệ thống tệp, trình quản lý tác vụ, stack bộ nhớ an toàn, rồi cả chia sẻ tài nguyên
Tất nhiên yêu cầu tối thiểu cho bản chứng minh khái niệm là cũng phải chạy được DOOM
Hệ điều hành của Lisp machine gần hơn: ban đầu là các hàm độc lập gọi lẫn nhau mà không có hệ thống đối tượng, về sau trở thành các generic function chuyên biệt theo lớp của đối số
Có vẻ các nhà thiết kế vẫn chưa phát hiện vì sao các hệ điều hành khác rốt cuộc lại cần những thứ mà hiện tại họ đang bỏ sót
int main() { … }Một ví dụ khác viết bằng Rust là https://github.com/hermit-os/hermit-rs
Ý tưởng thì ổn, nhưng cách cứ tiếp tục gắn thêm hàm mới vào struct Context để tương thích với các mục cũ là con đường dẫn tới địa ngục tương thích ngược
Chẳng khác nào tự nhốt mình vào tình thế không thể loại bỏ các mục cũ hoặc đã bị khai tử khỏi struct Context
Có vẻ cách tốt hơn là đưa quản lý phiên bản ngữ nghĩa vào giữa hệ điều hành và ứng dụng
Nếu ứng dụng khai báo nó được build cho hoặc phụ thuộc vào phiên bản hệ điều hành nào, hệ điều hành có thể kiểm tra tương thích và truyền vào phiên bản struct Context phù hợp
Phần lớn vấn đề tương thích ngược vẫn còn, nhưng có thể giữ struct Context gọn gàng bằng cách đặt nhiều struct theo từng phiên bản major/minor trong kernel
Ý tưởng hay, nhưng tôi cũng thích sự đơn giản của việc chỉ có một giao diện runtime
Dù sao nếu hệ điều hành phải xử lý mọi phiên bản, các ứng dụng tương lai có thể dùng padding để tạo cảm giác “gọn gàng”
struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }Tuy nhiên viết ra như vậy rồi thì tôi cũng thấy có gì đó hơi sai
Việc ứng dụng khai báo phiên bản của nó nghe như vấn đề mà các định dạng file thực thi như ELF đã giải quyết rồi, nên tôi đang thử một phương án khác
Đoạn “sleep hay chờ bất đồng bộ thế nào? Chỉ cần return” nghe hơi lạ
I/O bất đồng bộ kiểu
io_uringhẳn sẽ rất tuyệt, nhưng mô hình này có vẻ loại trừ những thứ như vậy, nên có thể khó đạt hiệu năng phù hợpViệc không hỗ trợ bất đồng bộ cũng lạ, vì nó có thể gắn với các điểm tạm dừng tự nhiên
Tuy vậy, để làm thế có lẽ sẽ phải từ bỏ khá nhiều thiết kế yêu cầu lưu và tải trạng thái ứng dụng một cách tường minh trên đĩa, và chi phí có vẻ lớn
Tôi nghĩ networking cũng có thể trở nên khó, ít nhất là khó làm hiệu quả, vì lý do tương tự
Hàm này trông giống điểm cuối của một event loop nhận trạng thái tùy ý làm tham số, nên những việc event loop làm nhìn chung có thể được tổng quát hóa
Tuy nhiên như vậy sẽ phải từ bỏ coroutine ở cấp ngôn ngữ và hỗ trợ async
Ví dụ được đưa ra quá gượng ép
Trong hệ điều hành tiền nhiệm, ứng dụng thường bị treo theo cách không biến toàn bộ hệ thống thành hợp tác, như deadlock giữa các luồng hoặc vòng lặp vô hạn
Ngoài ra, hệ thống tiền nhiệm có thể chấm dứt ứng dụng nếu nó tạo quá nhiều luồng hoặc tệp, hoặc dùng quá nhiều bộ nhớ, từ rất lâu trước khi thực tế trở thành hệ hợp tác
Hệ thống của chúng tôi chỉ đơn giản là khoan dung hơn
Hơn nữa, dù nói “sandboxing là miễn phí nếu chấp nhận các giả định”, lại nói “bất kỳ ứng dụng nào cũng có thể dễ dàng xem RAM của ứng dụng khác, và đây là vấn đề khó giải”, vậy thì sandboxing không miễn phí
Dù vậy đây vẫn là ý tưởng hay, và mong tác giả thành công
Lời giải cho vấn đề này có thể là tạo các hàm, tức các closure bao bọc ứng dụng, hoạt động như Context của chính ứng dụng
Tôi tự hỏi sẽ thế nào nếu một ứng dụng có thể mở ứng dụng khác, hoặc nếu một ứng dụng có thể trở thành hệ điều hành cho ứng dụng khác
Nếu là một hệ thống được thiết kế đúng để sandboxing vững chắc, nó sẽ đặt giới hạn cho mọi tài nguyên và từ chối yêu cầu khi đạt tới giới hạn
Tôi tò mò Fomos phân biệt tiến trình và tệp thực thi như thế nào
Trên Linux, tiến trình là dữ liệu nội bộ của kernel như các con trỏ argv/envp, stack, heap, signal mask, bảng file handle, signal handler, không gian địa chỉ ảo bao gồm bộ nhớ có thể thực thi, cùng uid, gid
Tệp thực thi là tệp chứa các bit đủ để loader lấp đầy không gian địa chỉ đó khi gọi system call
execveCó thể tạo tiến trình bằng
clone3hoặcforkmà không cần tệp thực thi; kernel dùng ELF và phần lớn user space dùng RTLD loader của GLIBC, nhưng để tạo tiến trình theo một định dạng tệp thực thi cụ thể thì không nhất thiết cần cả haiMột tệp thực thi liên kết tĩnh không có mã độc lập vị trí, từ góc nhìn của assembler, gần giống “chỉ là một hàm”, nhưng nếu phân giải symbol lúc runtime mà không có ASLR thì sẽ dễ bị tấn công tràn bộ đệm khi địa chỉ của hàm phụ thuộc đã được biết
Tôi muốn có giải pháp thay thế cho các khiếm khuyết của glibc và mô hình tiến trình Posix, nhưng tôi cho rằng phần lớn độ phức tạp của tệp thực thi Unix là mang tính bản chất
Phân giải symbol lúc runtime thì khó nhưng hữu ích; cho phép interpreter tùy ý tuy phiền phức nhưng là điểm Linux mạnh hơn Windows và MacOS; và việc cung cấp giao diện kernel bằng các system call ổn định là thế mạnh của Linux
Mac dùng Mach-O, Windows dùng PE, Linux dùng ELF, nhưng không có lý do gì để không có một hệ sinh thái đa dạng các định dạng thực thi/liên kết
Một hệ điều hành có mô hình nạp mã rất đơn giản sẽ là nơi tốt cho những thử nghiệm như vậy
ABI ổn định cũng không phải điểm độc đáo chỉ Linux mới có, và tính hữu ích của quyết định đó cũng khá đáng nghi, nhưng hỗ trợ driver thì rất ấn tượng và khó phủ nhận
Khá thú vị
Tôi không rõ làm sao có thể đạt được mức bảo mật và an toàn nào đó với các ứng dụng hợp tác không đáng tin cậy
Bất kỳ ứng dụng nào cũng có thể giữ CPU vô thời hạn và làm kernel cùng các ứng dụng khác ngừng hoạt động
Lý do chúng ta dùng hệ điều hành lập lịch tiền nhiệm là vì có thể dừng một ứng dụng hoạt động sai mà không làm hỏng phần còn lại của hệ thống
Nó dùng đa nhiệm tiền nhiệm nhưng không cưỡng chế bảo vệ bộ nhớ; thay vào đó đặt compiler làm dịch vụ hệ thống để chỉ có các tệp thực thi do compiler hệ thống tạo và ký mới được chạy
Vì compiler đảm bảo bảo vệ bộ nhớ tại thời điểm build, system call và giao tiếp liên tiến trình trở nên rất rẻ
Với một compiler tinh vi hơn một chút, cũng có thể chèn các lời gọi
yieldvào những vị trí cần thiết để cưỡng chế đa nhiệm hợp tác theo cách tương tựKhông thể giải được bài toán dừng tổng quát, nhưng vẫn tồn tại những lớp chương trình mà phân tích tĩnh có thể chứng minh là sẽ kết thúc hoặc nhường quyền
Chỉ cần xử lý riêng các chương trình không thể chứng minh được, và cũng có thể dùng watchdog timer để tự động dừng các chương trình hoạt động sai
Mã không đáng tin cậy không chạy trong image “chính” mà chạy trong một VM có thể vứt bỏ
Ở đây cũng có thể dùng hypervisor để áp dụng cùng mô hình, nhưng không ai chỉ dùng riêng một hệ thống Smalltalk, và vẫn cần một mức hạ tầng nào đó
Tôi tò mò liệu có thể triển khai bảo mật trong hệ điều hành này mà không cần thiết kế lại hoàn toàn, và thực tế là không làm lại những gì các hệ điều hành hiện có đã làm hay không
Tôi biết hai cách để cưỡng chế bảo mật cho các ứng dụng chạy trên cùng phần cứng
Một là cách ly tiến trình bằng bộ nhớ ảo ở runtime, cách còn lại là loader kiểm chứng tại thời điểm tải xem mã có truy cập bộ nhớ tùy ý hay không
Cách sau thường được cưỡng chế bằng máy ảo chỉ cho phép bytecode của một tập lệnh bị giới hạn, không có phép toán con trỏ, như JVM hay Smalltalk
Tác giả Fomos không muốn chuyển ngữ cảnh và cách ly bộ nhớ, còn trình biên dịch Rust thì không tạo bytecode; liệu có cách nào khác không?
Theo tôi hiểu, vì các quy tắc như cấm
unsafeđược cưỡng chế trong một trình biên dịch đã được chứng nhận, nên ở đây mã nguồn thực chất đóng vai trò như bytecodeNhìn thoáng qua thì rất giống Midori, nhưng chi tiết triển khai lại khá khác
Trong Theseus, driver, ứng dụng, v.v. là các đối tượng ELF, tất cả được liên kết động vào một tệp thực thi duy nhất, tức kernel, và còn có những kỹ thuật thú vị như nâng cấp nóng
https://github.com/theseus-os/Theseus
https://www.theseus-os.com/
Kiểu như nếu xảy ra lỗi phân đoạn ở runtime, hệ thống sẽ kiểm tra một dạng token bảo mật nào đó để xem caller có quyền truy cập và gọi page đó hay không
Tôi không biết trong thực tế nó sẽ khả dụng đến mức nào
Dù là đa nhiệm hợp tác, bây giờ có rất nhiều core, nên có lẽ không giống thời Classic MacOS
Một hai tiến trình không chịu nhường chưa chắc sẽ giữ chặt toàn bộ hệ thống
Nếu một hàm chạy sai và không return, hệ thống có thể kết thúc nó khi đã dùng hết core
Đa nhiệm hợp tác không nhất thiết đồng nghĩa với hiệu năng kém
Chia sẻ thời gian vốn là cách chia một CPU đơn khổng lồ cho nhiều người dùng, nhưng giờ CPU đa nhân cho một người dùng đã phổ biến, nên đã đến lúc nghĩ về những cách khác để sử dụng core
Việc dự án này tồn tại thật sự khiến tôi rất kỳ vọng
Trong mô hình này không có chuyển ngữ cảnh, nên thậm chí hiệu năng có khả năng tốt hơn
Vì vậy tôi bắt đầu tò mò chuyện gì sẽ xảy ra nếu tăng timeslice của Linux lên một giá trị phi lý như 10 giây
Tôi muốn nghe chi tiết hơn về kế hoạch bảo mật
Nhìn chung, tôi cho rằng những thử nghiệm như thế này cho thấy hệ điều hành có thể được cải thiện bằng thiết kế greenfield
Nó làm tôi hơi nhớ đến Mirage OS: https://mirage.io/