- Tôi cho rằng mọi người chưa nhận thức đầy đủ về mức độ thiếu sót của tài liệu Linux kernel API, và cách Rust giải quyết một phần vấn đề đó
- Tôi đã viết các lớp trừu tượng Rust cho nhiều subsystem, và trong gần như mọi trường hợp đều phải đọc mã nguồn C để hiểu đầy đủ cách sử dụng API một cách an toàn
- Chỉ với chữ ký hàm cùng các chú thích tài liệu liên quan hoặc tài liệu tường minh thì rất khó nắm bắt trọn vẹn cách dùng API an toàn
- Có cần phải giữ khóa hay không
- Đối số reference count có chuyển giao tham chiếu hay tự lấy tham chiếu riêng của nó
- Khi callback được gọi thì khóa vẫn đang được giữ hay phải tự lấy
- Có điều gì đặc biệt ở callback
freehay không - Thứ tự khóa được dự kiến là gì
- Có những tình huống đặc biệt nào mà một số thao tác có thể dùng khóa tùy trường hợp hay không
- Có chấp nhận đối số NULL hay không và cách dùng hợp lệ là gì
- Trong tình huống lỗi thì reference count sẽ xảy ra chuyện gì
- Con trỏ reference count được trả về đã được tăng sẵn hay chỉ là một lần mượn ngầm từ tham chiếu của đối số được truyền vào
- Giá trị trả về luôn là con trỏ hợp lệ, có thể là NULL, hay thậm chí có thể là
ERR_PTR - Con trỏ được trả về qua đối số gián tiếp khi lỗi sẽ được dọn về NULL hay giữ nguyên
- Khi không cần con trỏ trả về thì có hợp lệ để truyền
NULL **hay không
- Đôi khi các yêu cầu là hợp lý nhưng không được tài liệu hóa
- Đôi khi yêu cầu quá linh hoạt hoặc quá phức tạp, nên khi viết lớp trừu tượng Rust phải đưa ra quyết định mang tính chủ quan để thu hẹp về một cách dùng an toàn
- Đôi khi phải đưa thêm cơ chế khóa vào bên trong lớp trừu tượng để đảm bảo an toàn
- Đôi khi phải chỉnh sửa nhẹ mã C để nó trực giao hơn, logic hơn và dễ dùng hơn (ví dụ: lộ ra hàm mở khóa để dùng trong trạng thái đã khóa)
- Đôi khi cơ chế khóa quá tinh vi, nên vẫn có thể viết lớp trừu tượng Rust an toàn, nhưng cần một chú thích tài liệu rất lớn để cảnh báo phải cẩn thận với cách dùng và thứ tự giải phóng nhằm tránh deadlock (Rust về bản chất không ngăn được deadlock)
- Đôi khi không thể giải quyết nếu không làm mã C hợp lý hơn (trường hợp
drm_sched)
- Tuy nhiên trong phần lớn trường hợp, những điểm thỏa hiệp khi viết lớp trừu tượng Rust lại gợi ra các vấn đề thiết kế của mã C và hướng cải thiện
- Cách tiếp cận chung là "trước tiên viết mã Rust với thay đổi tối thiểu lên mã C để tránh xung đột, sau đó dựa trên các bài học rút ra để đề xuất thay đổi cho mã C" (hiện vẫn chưa đi đến phần thứ hai)
- Kết quả là trong đa số trường hợp, chỉ cần nhìn vào Rust API cũng có thể biết cách dùng đúng
- Không cần lo về reference count, con trỏ NULL, bỏ sót kiểm tra kết quả, hay giải phóng tham chiếu khi lỗi
- Không cần lo về dùng khóa đúng cách, bỏ sót lấy tham chiếu hay giải phóng hai lần
- Không cần băn khoăn về cách mã hóa giá trị trả về lỗi
- Vì nếu làm sai những điều đó thì mã sẽ không biên dịch
- Tất nhiên vẫn có thể dùng API sai, nhưng tệ nhất cũng chỉ dẫn đến trả về lỗi hoặc deadlock (deadlock có thể debug dễ bằng
lockdep, và việc tích hợpArc<>có thể bắt lỗi khóa liên quan đến giải phóng/bỏ tham chiếu)
- Ngay cả OpenFirmware/DeviceTree API vốn được tài liệu hóa tương đối nghiêm ngặt, trong C việc tuân thủ mọi quy tắc vẫn rất nhàm chán và dễ phát sinh lỗi
- Nhìn vào mã OF của driver thì khả năng rò rỉ tham chiếu là rất cao
- Phần lớn hệ thống không biên dịch kernel với
OF_DYNAMIC, nên reference count bị bỏ qua và vì vậy lỗi này không bị phát hiện hoặc sửa chữa - Nhưng lớp trừu tượng OF bằng Rust mà tôi viết xử lý reference counting tự động, nên không cần bận tâm về việc đó
- Ưu điểm của việc viết mã kernel bằng Rust so với C
- Khi viết mã kernel bằng C thì chỉ có hai lựa chọn
- Cứ làm rồi hy vọng reviewer sẽ bắt ra, hoặc khổ sở debug
- Hoặc dành hàng giờ để hiểu mọi thứ trước khi dám dùng mã và hy vọng bắt được hết mọi vấn đề
- Điều này cũng làm tăng khối lượng công việc của reviewer và maintainer
- Họ phải xem xét bản gửi lên để kiểm tra xem mọi quy tắc ẩn không được tài liệu hóa có được tuân thủ đầy đủ không
- Đôi khi vẫn bỏ sót vấn đề, và đôi khi vấn đề lớn đến mức phải refactor mạnh tay
- Trong Rust, tất cả những điều đó biến mất. Nếu biên dịch được thì an toàn, không có lỗi vận hành sai hay rò rỉ tham chiếu (mã
unsafelà ngoại lệ, nhưng chỉ cần review phần đó, và có quy tắc là phần này phải được tài liệu hóa tốt)- Tất nhiên vẫn cần code review và sự hỗ trợ của chuyên gia từng subsystem. Rust không phải phép màu khiến mã trở nên hoàn hảo
- Nhưng nó loại bỏ mọi vấn đề và sai sót thấp tầng ngớ ngẩn, để có thể tập trung vào các vấn đề cấp cao hơn
- Quan điểm về các nhà phát triển Linux
- Tôi không trách các nhà phát triển Linux vì tài liệu chưa hoàn chỉnh
- Linux kernel cực kỳ phức tạp và phải xử lý nhiều vấn đề tinh vi
- Phần lớn user-space API có các quy tắc đơn giản hơn nhiều để dùng an toàn
- Kernel thì rất khó
- Ngay cả các nhà phát triển kernel giàu kinh nghiệm cũng vẫn thường làm sai những thứ này
- Đây không phải vấn đề kỹ năng, mà là con người không thể nhét hết mọi quy tắc phức tạp đó vào đầu và thực hiện chính xác mỗi lần
- Giải pháp
- Chúng ta cần công cụ
- Giải pháp là Rust. Chỉ cần mã hóa toàn bộ quy tắc một lần vào code và hệ thống kiểu, sau đó không phải lo nữa
- Cũng giống như cách giải quyết tranh cãi về coding style là mã hóa toàn bộ quy tắc vào trình format tự động
- Khi đó chúng ta có thể ngừng lo về mọi vấn đề an toàn thấp tầng, ownership và đồng bộ hóa, để tập trung vào những thứ quan trọng hơn như thiết kế driver và subsystem ở tầng cao
- Định dạng mã trong dự án Rust for Linux
- Dự án Rust for Linux thực sự áp dụng
rustfmtcho các bản gửi lên - Khi viết kernel Rust, không cần lo về định dạng mã hay bị phàn nàn trong code review
- Chỉ cần chạy
make rustfmtlà được
- Dự án Rust for Linux thực sự áp dụng
Ý kiến của GN⁺
- Bài viết này chỉ ra khá rõ các vấn đề về tài liệu hóa API và an toàn trong phát triển Linux kernel. Nó cho thấy rõ hạn chế của ngôn ngữ C và ưu điểm của Rust
- Tuy nhiên, cách nói rằng "Rust là giải pháp duy nhất" có phần hơi cường điệu. Một số cải thiện vẫn có thể đạt được bằng các phương pháp khác như công cụ phân tích tĩnh
- Rust giải quyết được nhiều vấn đề như an toàn bộ nhớ, nhưng vẫn cần code review và kiểm thử kỹ lưỡng. Đây không phải viên đạn bạc
- Việc chuyển sang Rust có thể đi kèm nhiều khó khăn như khả năng tương thích với mã C hiện có, đường cong học tập của lập trình viên, v.v. Có vẻ cách tiếp cận triển khai dần dần sẽ phù hợp hơn
- Để cải thiện các tập quán và văn hóa lâu đời của Linux kernel, ngoài Rust còn cần nỗ lực trên nhiều mặt như tài liệu hóa, cố vấn và giao tiếp
- Nhìn chung, bài viết này vừa cho thấy rõ tiềm năng và lợi thế của Rust trong phát triển Linux kernel, vừa tránh kỳ vọng thái quá hay sự tin tưởng mù quáng, nên mang lại một góc nhìn cân bằng. Việc đưa Rust vào sẽ khó cả về mặt kỹ thuật lẫn văn hóa, nhưng về dài hạn có thể góp phần cải thiện độ an toàn và khả năng bảo trì của mã kernel.
2 bình luận
Rust... cá nhân tôi cũng đã thử học, nhưng công ty chúng tôi vẫn chưa áp dụng. Vì đã có cả một núi mã viết bằng C++, lại còn vấn đề nhân sự hiện tại phải học lại Rust nữa... Tôi có nghe nói ở Hàn Quốc cũng đã có công ty đang áp dụng Rust vào production, nên sẽ rất hay nếu có thể chia sẻ những kinh nghiệm liên quan.
Ý kiến Hacker News
Các ngôn ngữ như Rust và Swift có tính biểu đạt cao, nên trình biên dịch có thể cho biết tính an toàn luồng của kiểu dữ liệu hoặc phương thức
Nhiều thư viện Rust vẫn thiếu tài liệu
Đã gặp khó khăn với borrow checker khi cố dùng Rust như C
&selfhoặc&mut self&mut self, thì cần dùng mutex để chia sẻ instance giữa các luồngKhi nhìn vào API Rust, trong đa số trường hợp có thể biết cách sử dụng cho đúng
Một ví dụ cụ thể trong Rust là cách dùng khóa để bảo vệ dữ liệu
Ngay cả ở các ngôn ngữ khác, việc triển khai trùng lặp API cũng có thể làm tăng độ rõ ràng của code và tài liệu
Khi dùng C trong Python extension, có vấn đề là phải biết calling convention
Những người này là các anh hùng và đang làm một công việc tuyệt vời
Tiến thêm một bước tới việc hiện thực hóa code tự tài liệu hóa hoàn toàn