4 điểm bởi GN⁺ 2023-08-31 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 qua Context mà không cần thư viện chuẩn
  • Context là 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àm start sẽ đượ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 trong Context
  • 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ần rust nightly, gcc, và qemu với các cờ Virgl và SDL

1 bình luận

 
GN⁺ 2023-08-31
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ại
    Nhì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

    • Nói đúng, và tôi thấy phản biện kia gần như là đánh tráo trọng tâm
      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
    • Không nên nhìn theo kiểu tất cả hoặc không gì cả
      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ạn
      Ví 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
    • Tôi là tác giả
      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
    • Tôi không phải nhà phát triển hệ điều hành, nhưng tôi nghĩ số lượng lõi tạo ra khác biệt
      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ậy
      Cá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
    • Có lý do khiến thế giới chuyển sang đa nhiệm tiền nhiệm sau thử nghiệm đa nhiệm hợp tác của Windows 3.1
  • 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

    • Trong Smalltalk, nó không hẳn là hàm độc lập mà là phương thức của lớp, nhưng đúng ở chỗ mọi đối tượng trong image đều gửi thông điệp cho nhau, tức là gọi phương thức của nhau
      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âu “ứng dụng chỉ là một hàm” nghe như lời nguyền của phát triển greenfield
      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
    • Thực ra tôi tò mò nó khác gì với các hệ điều hành khác, nơi ứng dụng là hàm int main() { … }
    • Điều này trông khớp với định nghĩa của unikernel
      Một ví dụ khác viết bằng Rust là https://github.com/hermit-os/hermit-rs
    • Cứ chạy thử Classic MacOS kiểu cũ là được
  • Ý 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ôi là tác giả
      Ý 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_uring hẳ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ợp
    Việ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ự

    • Tôi đoán I/O bất đồng bộ sẽ được triển khai kiểu cập nhật yêu cầu I/O trong Context rồi trả về, sau đó khi sẵn sàng thì hàm được gọi lại
      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

    • Trong thế giới trình duyệt, mọi trang web đang mở đều chia sẻ bộ nhớ heap của trình duyệt nhưng không can thiệp lẫn nhau
      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
    • Ví dụ đó còn gượng ép hơn, vì giả định mọi hệ thống đều có sandboxing tệ như Windows và Linux
      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ìnhtệ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 execve
    Có thể tạo tiến trình bằng clone3 hoặc fork mà 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ả hai
    Mộ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

    • Nghĩ thêm một chút, có vẻ một trong những sai lầm lớn là sự đồng nhất hóa định dạng tệp thực thi
      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
    • Tôi từng nghĩ thế mạnh của Linux là driver hơn là ABI ổn định
      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
    • Tôi tò mò không biết bạn đã xem Zircon (Fuchsia) xử lý chuyện này thế nào chưa
      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

    • Tôi nhớ vào giữa những năm 2000, Microsoft Research có một hệ điều hành nguyên mẫu được viết hoàn toàn bằng .NET
      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 yield và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
    • Trong các hệ thống Smalltalk như Squeak hay Pharo, khi một luồng bị treo, người dùng dừng thực thi bằng phím tắt
      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?

    • Theseus là một ví dụ của cách thứ hai được triển khai bằng Rust mà không cần bytecode
      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ư bytecode
      Nhì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/
    • Tôi đoán là mọi “chương trình” chia sẻ một không gian địa chỉ duy nhất, nhưng có thể dùng bộ nhớ ảo để giới hạn khả năng nhìn thấy các page có thể truy cập tại một thời điểm nhất định
      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
    • https://en.wikipedia.org/wiki/Capability-based_addressing ?
  • 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

    • Nói thêm, câu “đa nhiệm hợp tác không nhất thiết đồng nghĩa với hiệu năng kém” là muốn nói hiệu năng tương tác kém
      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/