Rust cho hệ thống tệp
(lwn.net)- Tại LSFMM+BPF Summit 2024, đã có thảo luận về cách áp dụng Rust cho hệ thống tệp Linux, và bản vá RFC thứ hai xuất hiện sau RFC tháng 12/2023 trở thành tâm điểm tranh luận
- Phía Rust-for-Linux muốn đưa các yêu cầu của API hệ thống tệp vào hệ thống kiểu của Rust để phát hiện lỗi ở thời điểm biên dịch, tự động hóa việc dọn dẹp tài nguyên và giảm các lỗ hổng liên quan đến bộ nhớ
- Trường hợp
iget_locked()cho thấy hướng đi màget_or_create_inode()trong Rust muốn ép buộc bằng kiểu và cơ chế dọn dẹp tự động đối với các bước như kiểm tra null, phân biệt trạng thái inode và xử lý lỗi vốn trước đây do phía gọi trong C tự thực hiện - Dave Chinner, Christian Brauner, James Bottomley, Ted Ts'o và những người khác lo ngại về sự không khớp tên gọi giữa API C và API Rust, việc đồng bộ API, khác biệt trong vòng đời đối tượng và gánh nặng bảo trì khi phải tính đến hơn 50 hệ thống tệp
- Cốt lõi của xung đột không nằm ở bản thân ưu điểm của lớp trừu tượng Rust, mà ở câu hỏi ai sẽ gánh nỗi đau khi phải giữ cho binding và lớp trừu tượng Rust theo kịp lúc mã C tiếp tục thay đổi
Phiên thảo luận Rust cho hệ thống tệp tại LSFMM+BPF
- Tại Linux Storage, Filesystem, Memory Management, and BPF Summit 2024, Wedson Almeida Filho và Kent Overstreet đã bàn về cách sử dụng Rust cho hệ thống tệp Linux
- Almeida đã gửi bộ bản vá RFC về lớp trừu tượng Rust cho hệ thống tệp vào tháng 12/2023, và đã có khác biệt quan điểm xoay quanh cách tiếp cận này
- Cùng ngày diễn ra phiên họp vào giữa tháng 5, Almeida đã gửi phiên bản RFC thứ hai để thảo luận cùng các chủ đề Rust khác
Mục tiêu của lớp trừu tượng hệ thống tệp trong Rust-for-Linux
- Lớp trừu tượng hệ thống tệp được đề xuất phản ánh định hướng mà Rust-for-Linux project theo đuổi
- Trọng tâm là biểu đạt nhiều hơn các yêu cầu của API hệ thống tệp bằng hệ thống kiểu của Rust để bắt lỗi ngay ở thời điểm biên dịch
- Đồng thời muốn tự động hóa những việc khó cung cấp một cách an toàn trong mã C
- Ví dụ: dọn dẹp tài nguyên
- Mục tiêu là làm cho trải nghiệm phát triển hệ thống tệp hiệu quả hơn, giảm thời gian debug các vấn đề mà compiler có thể phát hiện, và hạ thấp các lỗ hổng liên quan đến bộ nhớ
- Overstreet cho biết ông đã quá nhiều lần phải truy vết các bug kéo dài 2 tuần trong bcachefs và tin rằng Rust mang lại nhiều thứ hơn C
- Rust loại bỏ hành vi không xác định
- Cung cấp khả năng nhìn thấy những gì đang diễn ra bên trong mã
- Nếu có thể chứng minh tính đúng đắn của mã Rust, số bug cản trở phát triển tính năng sẽ giảm đi rất nhiều
Ví dụ về hệ thống kiểu quanh iget_locked()
- Almeida lấy
iget_locked()trong kernel hiện tại làm ví dụ trên slide để minh họa các yêu cầu phức tạp - Phía gọi trong C phải tự xử lý nhiều điều kiện
- Phải kiểm tra xem giá trị trả về có phải null hay không
- Phải xác định
struct inodetrả về là inode mới hay inode đã tồn tại - Nếu là inode mới thì phải khởi tạo trước khi dùng
- Nếu khởi tạo thất bại thì phải gọi
iget_failed()
- Al Viro không đồng ý với một phần các yêu cầu mà Almeida nêu cho phía gọi
iget_locked(), và tranh luận tiếp tục xoay quanh hành vi chi tiết - Overstreet cho rằng nếu đóng gói các quy tắc này vào kiểu và lớp trừu tượng Rust, compiler có thể ép buộc cách xử lý đúng
- Hàm tương ứng phía Rust mà Almeida đưa ra là
get_or_create_inode()- Cũng như trong C, vẫn phải kiểm tra việc thất bại
- Khi thành công, phía gọi sẽ nhận một inode đếm tham chiếu thông thường hoặc một inode mới
- Với inode thông thường, bộ đếm tham chiếu sẽ tự giảm khi đối tượng không còn được tham chiếu nữa
- Với inode mới, nếu không được khởi tạo thì sẽ tự động gọi xử lý tương đương
iget_failed() - Khi inode mới đã được khởi tạo một lần, nó trở thành inode thông thường và sau đó áp dụng cơ chế tự giảm đếm tham chiếu
- Các hành vi này được ép buộc bằng hệ thống kiểu
- Viro đặt câu hỏi các ràng buộc này thực sự sẽ được định nghĩa ở đâu trong mã nguồn
- Almeida trả lời rằng ông muốn xác định các ràng buộc từ Viro và các nhà phát triển hệ thống tệp khác, rồi tạo ra các kiểu và lớp trừu tượng để ép buộc chúng
Sự đứt gãy giữa API C và API Rust
- Dave Chinner cho rằng nếu API C và API Rust có tên khác nhau thì nhà phát triển hiện có sẽ khó nhìn mã C để biết lời gọi tương ứng trong Rust là gì
- Cũng có lo ngại rằng nếu không dùng cùng tên, nó có thể trở thành một API hoàn toàn xa lạ với cộng đồng phát triển hiện tại
- Khi mã C thay đổi thì mã Rust cũng phải chạy theo, nên câu hỏi ai sẽ đảm nhận công việc đó vẫn còn bỏ ngỏ
- Almeida thừa nhận đây là vấn đề cần thảo luận
- Ông không phản đối việc đổi tên
- Nhưng ông cũng không cho rằng
iget_locked()là một cái tên tốt, và xem đây có thể là cơ hội để đặt tên tốt hơn
- Viro cho rằng
iget_locked()không phải là hàm thành viên của đối tượng superblock mà là một hàm thư viện, nên ví dụ này không phù hợp - Almeida trả lời rằng
get_or_create_inode()cũng có thể chuyển thành hàm thư viện, và ví dụ này nhằm cho thấy cách mã hóa các ràng buộc vào kiểu
Lựa chọn giữa lớp trừu tượng tổng quát và cách tiếp cận xoay quanh hệ thống tệp đơn giản
- Christian Brauner cho rằng trước tiên cần xác định liệu lớp trừu tượng Rust có phải là lớp trừu tượng tổng quát cho mọi hệ thống tệp trong kernel hay chỉ tập trung vào những khả năng cần thiết cho các hệ thống tệp đơn giản hơn được viết bằng Rust
- Về lâu dài, nếu một hàm như
get_or_create_inode()chứa nhiều ràng buộc hơn rất nhiều so vớiiget_locked(), vấn đề có thể phát sinh - Mã C, đặc biệt là ở giai đoạn đầu, có thể tiến hóa nhanh hơn mã Rust, nên hai API sẽ phải liên tục được đồng bộ
- Overstreet cho rằng điểm mấu chốt là có thực hiện refactor và dọn dẹp cùng lúc khi thêm lớp trừu tượng Rust hay không, và ông rất tin rằng điều đó là cần thiết
- James Bottomley cho rằng vòng đời đối tượng được mã hóa trong API Rust nhưng phía C lại không có biểu đạt tương ứng
- Nếu vòng đời đối tượng thay đổi ở một bên, bug có thể xuất hiện ở bên kia
- Chinner nói rằng vòng đời của đối tượng inode đôi khi khác nhau tùy từng hệ thống tệp
- Nếu đưa một cách hiểu vòng đời duy nhất vào API, các hàm đó có thể không hoạt động với một số hệ thống tệp
- Almeida trả lời rằng ví dụ này chỉ được dùng cho những hệ thống tệp hiện đang gọi
iget_locked()và có thể hưởng lợi từ nó- Các nhà phát triển Rust không định ép hệ thống tệp phải thay đổi cách chúng đang hoạt động
“Ai sẽ gánh nỗi đau?”
- Ted Ts'o nói rằng có vẻ như đang có nỗ lực chuyển tất cả mọi người sang “tôn giáo” Rust, nhưng Linux có hơn 50 hệ thống tệp và sẽ không thể chuyển đổi ngay lập tức
- Mã C sẽ tiếp tục được cải thiện, và nếu các thay đổi đó làm vỡ binding Rust thì các hệ thống tệp phụ thuộc vào binding đó cũng có thể hỏng theo
- Ts'o cho rằng trong một thời gian, binding Rust sẽ là công dân hạng hai, và binding Rust bị hỏng là vấn đề của các nhà phát triển Rust-for-Linux chứ không phải của toàn bộ cộng đồng hệ thống tệp
- Ông cho rằng trong vòng 1–2 năm tới sẽ lộ rõ việc vừa phát triển binding Rust vừa để mã C tiến hóa, đồng thời nhồi nhiều ngữ nghĩa vào hệ thống kiểu, là cách tiếp cận tốt hay xấu
- Với Ts'o, thay đổi lớn rốt cuộc là vấn đề phân bổ nỗi đau
- Nhà phát triển thay đổi API C có thể nói rằng họ sẽ sửa phần mã C bị ảnh hưởng, nhưng không sửa binding Rust vì họ không biết Rust
- Almeida trả lời rằng ông không cố định API C, mà muốn các nhà phát triển hệ thống tệp giải thích ngữ nghĩa của API để ông mã hóa chúng vào Rust
- Bottomley cho rằng càng mã hóa nhiều ngữ nghĩa vào binding thì nó càng dễ tổn thương hơn từ góc nhìn đồng bộ
- Almeida đáp rằng khi API thay đổi thì vẫn phải cập nhật người dùng API, cũng như với mọi người dùng khác
Nên đưa điều gì vào method, function và type
- Viro lại đặt vấn đề với việc phương án thay thế
iget_locked()phụ thuộc vào method- Ông cho rằng khi dùng method thì tham số không được chỉ định một cách tường minh
- Overstreet cho rằng sự khó chịu với method bắt nguồn từ các ngôn ngữ như C++ vốn phụ thuộc quá nhiều vào kế thừa
- Rust không làm như vậy, và method trong Rust phần lớn là yếu tố cú pháp
- Jan Kara phân biệt giữa hành vi gắn với chính inode và hành vi nội tại của hàm
iget_locked()- inode đi kèm những hành vi như đếm tham chiếu và cách xử lý của nó
- hàm
iget_locked()có những hành vi riêng của nó
- Overstreet và Almeida trả lời rằng cả hai phần đều được mã hóa vào kiểu nhưng tách biệt, và các hàm khác sử dụng kiểu inode có thể có giá trị trả về với thuộc tính khác nhau
- Viro giải thích vì sao inode trong VFS hiện hoạt động theo cách hiện nay, và đồng ý rằng nên bắt đầu nhỏ rồi xem hướng đi tiếp theo
- Overstreet nói ví dụ lần này có thể quá phức tạp nên không phải điểm khởi đầu tốt, còn Viro đáp “Không, không phải vậy”, rồi phiên thảo luận kết thúc
1 bình luận
Ý kiến trên Hacker News
Tôi không hiểu việc nói rằng mỗi hệ thống tệp có vòng đời inode tùy chỉnh, nhưng lại dùng cùng các hàm quản lý vòng đời và chỉ khác về ngữ nghĩa
Nếu cùng một hàm phải được dùng khác nhau tùy theo chi tiết triển khai, nghe có vẻ như trái ngược với một lớp trừu tượng
Nếu vòng đời inode là theo từng hệ thống tệp, thì nên quản lý bằng các hàm riêng cho từng hệ thống tệp
Khi tập hợp các thông tin như vậy, có thể sẽ lộ ra những điểm cần refactor để ngay từ đầu những câu hỏi kiểu này không phát sinh, và đó là điều tốt
Đây là phần tổng quan về lớp VFS, xử lý hành vi theo từng hệ thống tệp nhưng vẫn duy trì một giao diện nhất quán ở phía kernel
Vòng đời inode có thể chỉ là một ví dụ ban đầu để mở đầu cuộc thảo luận
Compiler sẽ hỗ trợ các tham chiếu tạm thời, nhưng hệ thống tệp vẫn phải lưu số lượng liên kết trên đĩa
iget_locked()được tập trung ở đây là một mẫu cụ thểKhông phải hệ thống tệp nào cũng dùng cách đó, và tùy tình huống cũng có thể không dùng
Ví dụ FAT không dùng vì nó tự tạo số inode và duy trì ánh xạ riêng từ vị trí FAT sang inode
Cũng có những hệ thống tệp như
prockhông cache đối tượng inodeBản thân đối tượng inode dường như có cùng luồng trạng thái bất kể nó đến từ đâu, nên từ góc nhìn của bên tiêu thụ, cách dùng
inodekhông thay đổiĐiều khác nhau là cách lớp hệ thống tệp tạo đối tượng inode và xử lý nội bộ nó
Có lẽ đang đặt sai câu hỏi
Rust có cần thay đổi để gọi C dễ hơn không?
Tôi đã thử Rust một chút, nhưng từ góc nhìn của một lập trình viên nghiệp dư, tôi vẫn chưa rõ phải tương tác với C như thế nào
Trong khi đó với C++ hay Objective C thì chỉ cần include đúng header và gọi hàm
Swift có thể include file Objective C, rồi từ đó gọi C
Trong trường hợp này, thay vì kỳ vọng các nhà phát triển kernel phải thích nghi với ngôn ngữ, có lẽ ngôn ngữ Rust nên linh hoạt hơn một chút
Chỉ cần khai báo hàm bên ngoài rồi gọi
Ví dụ như trong sách Rust tại https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#usin..., có thể khai báo bằng
extern "C"Nếu không muốn viết tay toàn bộ khai báo cho một thư viện phức tạp, có thể dùng công cụ như bindgen để tự động tạo các khai báo
externtừ file header C: https://github.com/rust-lang/rust-bindgenCó thể lập luận rằng sẽ tốt hơn nếu thứ như bindgen được tích hợp vào Rust để dùng mà không cần phụ thuộc bên thứ ba hay cấu hình
build.rs, nhưng đó không phải trọng tâm của bài viết nàyVấn đề không phải là binding cấp thấp, mà là wrapper cấp cao theo phong cách Rust; và không thể có một công cụ tổng quát nào tự động tạo ra những wrapper như vậy từ mã C tùy ý
Bài viết nói về việc tìm cách thực sự triển khai driver filesystem trong kernel bằng Rust
Một điểm quan trọng nữa là mã Rust trong kernel tất yếu sẽ tiêu thụ các interface C
Với các use case đang nghĩ tới, bindgen khá phù hợp: https://github.com/rust-lang/rust-bindgen
Muốn gọi từ Rust thì khai báo
extern "C" fn foo() -> T, rồi truyền cờ liên kết bằng thuộc tính#[link]hoặcbuild.rsCó thể tạo sẵn binding bằng crate
bindgen, hoặc tạo trongbuild.rsrồi include bằnginclude!()Thông thường sẽ tạo một crate
-syschỉ chứa các binding được sinh ra, còn mã thực tế thìusebinding từ crate sys đó như bình thườngNgay cả trong C++ và Objective C, không chỉ include đúng header mà còn phải link tới thư viện
Gọi hai chiều là có thể, nhưng nếu C không biểu đạt được những gì Rust có thể biểu đạt, điều đó sẽ ảnh hưởng lớn đến thiết kế API mà cả hai bên phải dùng chung
Chỉ cần khai báo hàm C bằng
extern "C"rồi gọiThường cần
unsafevà phải chuyển tham chiếu sang con trỏ thô hoặc ép kiểu, nhưng bản thân cú pháp thì đơn giảnCũng có công cụ quét file header C để tạo khai báo, trong đó bindgen được dùng nhiều nhất
Điểm tranh luận của bài này gần với việc dùng Rust như thế nào hơn là bản thân ngôn ngữ
Các nhà phát triển Rust-for-Linux muốn dùng tính năng và hệ thống kiểu của Rust để mã hóa ngữ nghĩa của các lời gọi API, khiến chúng an toàn hơn và ít lỗi hơn
Phía những người làm C lo rằng làm vậy sẽ khiến hành vi và ngữ nghĩa của C API khó phát triển hơn
Vì khi C API thay đổi thì Rust API cũng phải được sửa, và họ không muốn gánh phần việc đó
Một phương án dễ chấp nhận hơn là dùng ít tính năng và hệ thống kiểu của Rust hơn khi mã hóa ngữ nghĩa vào Rust API
Như vậy, khi C API thay đổi, việc cập nhật Rust API sẽ mang tính cơ học và đơn giản; nhưng nếu Rust-for-Linux không thể dùng các tính năng của Rust để tạo API tốt hơn và an toàn hơn, thì sẽ nảy sinh câu hỏi ý nghĩa của công việc này là gì
Dù vậy, cũng hơi lạ khi thừa nhận mình chưa hiểu đủ về ngôn ngữ mà lại nói chắc chắn về chủ đề này
Vì không rành filesystem của Linux nên tôi không rõ Rust API này là wrapper quanh C API, hay là một bản triển khai lại
Nếu là triển khai lại hoặc là API riêng, việc giữ nguyên tên như C API có vẻ sẽ càng ngày càng gây nhầm lẫn
Tôi nghĩ vậy dù ban đầu nó có thể giúp các lập trình viên quen thuộc hiểu nhanh hơn
iget_locked()trong Rust, và tên của nó làget_or_create_inode()Câu trả lời có vẻ là triển khai lại, và không dùng cùng tên
Xét cách những cuộc thảo luận kiểu này thường diễn ra và quy mô thay đổi liên quan, cuộc thảo luận lần này lịch sự một cách đáng ngạc nhiên
Tôi không đồng ý với bầu không khí tiêu cực trong thread này
Tôi khá lạc quan ở chỗ những người liên quan đã truyền đạt rõ các điểm đau cốt lõi mà không nói nhảm
Cuộc thảo luận thực tế hẳn đã gay gắt, lan man và nhiều bắt bẻ, đúng kiểu tranh luận ngôn ngữ lập trình giữa những người lập dị có quan điểm mạnh
Có vẻ Jake Edge, người viết bản tóm tắt này, rất giỏi trong việc gạt bỏ những phần đó và chỉ ghi lại phần cốt lõi
Một số bình luận bên dưới trang lwn.net khá thô lỗ
Hãy thử tưởng tượng bạn nhận được bình luận kiểu Khoa học tiến bộ sau mỗi đám tang trong một dự án mã nguồn mở mà bạn đang đóng góp
Việc nhân Linux có thêm nhiều lựa chọn luôn là điều có lợi
Nhưng Rust có thể không phải là câu trả lời cho mọi thứ
Rust cố gắng hết sức để bảo đảm một mô hình lập trình an toàn, nhưng mô hình đó cũng có giới hạn
Có thể trông như cứ vấn đề bộ nhớ thì dùng Rust, vấn đề đồng thời thì chuyển sang Rust, nhưng không thể làm mọi việc mà C làm nếu không có các khối
unsafeRust có thể đem lại góc nhìn mới cho những vấn đề này, nhưng không phải là giải pháp hoàn chỉnh
C, đặc biệt là C dùng trong kernel, đẩy trách nhiệm cho từng người phải nắm hoàn toàn mọi quy tắc ngầm
Điều đó không thể mở rộng
Ngay cả các nhà phát triển kernel dùng cùng một cấu trúc dữ liệu, khi ở chung một phòng, cũng không thể hoàn toàn thống nhất về các quy tắc đó
Rust mạnh ở chỗ làm lộ rõ các quy tắc cần biết, và nếu người khác có thể bảo đảm việc tuân thủ quy tắc thì biến nó thành chuyện không còn là vấn đề của mình nữa
Đôi khi kết quả có thể kém tối ưu hơn, nhưng ngay cả trong kernel Linux, mặc định kém tối ưu hơn thường vẫn là đúng; còn với những ai có dư sức học thêm sáu quy tắc kỳ quặc để đạt hiệu năng tốt hơn thì chỉ cần cung cấp lối thoát
unsafeunsafeChỉ là chỉ nên dùng khi cần thiết
Việc dùng một khối
unsafecó phạm vi ảnh hưởng rất hạn chế không có nghĩa là mọi bảo đảm nhận được ở phần mã còn lại đều biến mấtunsafeNhưng cho rằng vì phải dùng
unsafenên Rust không phù hợp là một hiểu lầmMục đích của phân tách an toàn/không an toàn trong Rust là đánh dấu rõ phần nào của mã là không an toàn, để có thể tập trung kiểm toán vào phần nhỏ đó và nếu phần đó đúng thì có thể tin rằng phần còn lại sẽ hoạt động
Có lý do chính đáng nào khiến mã hệ thống tệp bắt buộc phải là
unsafekhông?Có lẽ chỉ là một tập con rất nhỏ cần ở vài chỗ
Những gì xảy ra bên trong không trực quan
Nhìn vào biên bản họp, Rust trong kernel trông giống như chi phí phức tạp bổ sung
Nếu viết lại hệ điều hành từ đầu thì có thể tận dụng trọn vẹn sức mạnh của ngôn ngữ
Nhưng nếu gắn thêm bên cạnh một codebase khổng lồ đã tồn tại, nó sẽ phát sinh thêm vấn đề như thấy ở đây
Hãy xem blog nói rằng driver GPU bằng Rust của Asahi Linux được làm trong một tháng
Có thể tìm
tales of the m1 gputrên Google, và tác giả có quan điểm rất tiêu cực về Hacker NewsNếu muốn thì có thể đọc qua liên kết này: https://asahilinux.org/2022/11/tales-of-the-m1-gpu/
Có áp dụng phổ quát được hay không thì phải chờ xem trong vài năm tới
Chi phí đó sẽ được xem là cần thiết để đón nhận tương lai và tiến bộ
Tôi tự hỏi vì sao không giới hạn vào một tập con an toàn, thay vì lao vào một làn sóng khổng lồ đầy lỗi chưa biết và các thỏa hiệp
Việc nó đã quá khổng lồ không có nghĩa là phải dừng đổi mới và chuyển sang trạng thái bảo trì vô thời hạn
Như thuế trong đời thực, nếu chi phí ở một phía được dùng để bù cho vấn đề khác thì có thể đó không phải là lỗ ròng
Chỉ nhìn vào một cuộc thảo luận đơn lẻ chưa có kết luận mà nói rằng nó sẽ không bù được vấn đề nào thì lập luận có vẻ hơi ngắn
Phần nói rằng tên của C API và Rust API lệch nhau nên nhìn mã C không biết lệnh gọi Rust tương đương là gì trông giống như cuộc chiến với quy ước đặt tên cũ
Đã từng có những trường hợp giải quyết tốt bằng cách giữ cùng tên, còn khi muốn tên thay thế thì để tên mới bọc tên cũ
Dù vậy, đặt tên vẫn khó
Hai vấn đề còn lại là tính đồng thời và lỗi lệch một đơn vị