2P by GN⁺ | ★ favorite | 댓글 1개
  • 2024년 LSFMM+BPF Summit에서 Linux 파일 시스템에 Rust를 적용하는 방안이 논의됐고, 2023년 12월 RFC 이후 나온 두 번째 RFC 패치가 토론의 중심이 됨
  • Rust-for-Linux 쪽은 파일 시스템 API의 요구사항을 Rust 타입 시스템에 담아 컴파일 시점 오류 검출, 자원 정리 자동화, 메모리 관련 취약점 감소를 노림
  • iget_locked() 사례는 C 호출자가 직접 처리하던 null 확인, inode 상태 구분, 실패 처리 등을 Rust의 get_or_create_inode()타입과 자동 정리로 강제하려는 방향을 보여줌
  • Dave Chinner, Christian Brauner, James Bottomley, Ted Ts'o 등은 C API와 Rust API의 이름 불일치, API 동기화, 객체 생명주기 차이, 50개 이상의 파일 시스템을 고려한 유지보수 부담을 우려함
  • 갈등의 핵심은 Rust 추상화의 장점 자체보다, C 코드가 계속 바뀔 때 Rust 바인딩과 추상화를 맞춰 가는 고통을 누가 부담할지에 있음

LSFMM+BPF에서 열린 파일 시스템 Rust 세션

  • 2024년 Linux Storage, Filesystem, Memory Management, and BPF Summit에서 Wedson Almeida Filho와 Kent Overstreet가 Linux 파일 시스템에 Rust를 사용하는 방안을 다룸
  • Almeida는 2023년 12월 파일 시스템용 Rust 추상화 RFC 패치 세트를 올렸고, 이 접근을 둘러싸고 의견 차이가 있었음
  • 세션이 열린 5월 중순 같은 날, Almeida는 두 번째 RFC 패치 버전을 올려 다른 Rust 관련 주제와 함께 논의하려 함

Rust-for-Linux의 파일 시스템 추상화 목표

  • 제안된 파일 시스템 추상화는 Rust-for-Linux project가 추구하는 방향을 반영함
  • 핵심은 파일 시스템 API의 요구사항을 Rust 타입 시스템으로 더 많이 표현해 컴파일 시점에 실수를 잡는 것임
  • C 코드에서 쉽게 제공하기 어려운 작업도 자동화하려 함
    • 예: 자원 정리
  • 목표는 파일 시스템 개발 경험을 더 생산적으로 만들고, 컴파일러가 찾을 수 있는 문제를 디버깅하는 시간을 줄이며, 메모리 관련 취약점을 낮추는 것임
  • Overstreet는 bcachefs에서 2주짜리 버그 추적을 너무 많이 겪었고, Rust가 C보다 더 많은 것을 제공한다고 봄
    • Rust는 정의되지 않은 동작을 제거함
    • 코드 내부에서 무슨 일이 일어나는지 볼 수 있는 기능을 제공함
    • Rust 코드의 올바름을 증명할 수 있게 되면 기능 개발을 방해하는 버그가 훨씬 줄어들 것으로 봄

iget_locked()를 둘러싼 타입 시스템 사례

  • Almeida는 슬라이드에서 현재 커널의 iget_locked()가 복잡한 요구사항을 가진다고 예를 들었음
  • C 호출자는 여러 조건을 직접 처리해야 함
    • 반환값이 null인지 확인해야 함
    • 반환된 struct inode가 새 inode인지 기존 inode인지 확인해야 함
    • 새 inode라면 사용 전에 초기화해야 함
    • 초기화가 실패하면 iget_failed()를 호출해야 함
  • Al Viro는 Almeida 슬라이드의 iget_locked() 호출자 요구사항 일부에 동의하지 않았고, 세부 동작을 두고 논쟁이 이어짐
  • Overstreet는 이런 규칙을 Rust 타입과 추상화에 캡슐화하면 컴파일러가 올바른 처리를 강제할 수 있다고 봄
  • Almeida가 제시한 Rust 쪽 대응 함수는 get_or_create_inode()였음
    • C와 마찬가지로 실패 여부는 확인해야 함
    • 성공 시 호출자는 일반 참조 카운트 inode를 받거나 새 inode를 받음
    • 일반 inode는 객체가 더 이상 참조되지 않을 때 참조 카운트가 자동 감소함
    • 새 inode는 초기화되지 않으면 iget_failed()에 해당하는 처리를 자동 호출함
    • 새 inode가 한 번 초기화되면 일반 inode가 되고, 이후 참조 카운트 자동 감소가 적용됨
    • 이 동작들은 타입 시스템으로 강제됨
  • Viro는 이 제약들이 실제 소스 코드 어디에 정의될지 의문을 제기함
  • Almeida는 Viro와 다른 파일 시스템 개발자들로부터 제약을 파악한 뒤, 이를 강제할 타입과 추상화를 만들려는 것이라고 답함

C API와 Rust API 사이의 단절

  • Dave Chinner는 C API와 Rust API의 이름이 다르면 기존 개발자가 C 코드를 보고 Rust의 대응 호출을 알기 어렵다고 봄
  • 같은 이름을 쓰지 않으면 기존 개발 커뮤니티에 완전히 낯선 API가 될 수 있다는 우려도 나옴
  • C 코드가 바뀔 때 Rust 코드도 따라가야 하므로, 그 작업을 누가 맡을지가 문제로 남음
  • Almeida는 논의가 필요한 사안이라고 인정함
    • 이름 변경에 반대하지는 않음
    • 다만 iget_locked()가 좋은 이름이라고 보지는 않으며, 더 나은 이름을 만들 기회로 볼 수도 있다고 함
  • Viro는 iget_locked()가 superblock 객체의 멤버 함수가 아니라 라이브러리 함수이므로 예시 선택이 좋지 않다고 봄
  • Almeida는 get_or_create_inode()도 라이브러리 함수로 바꿀 수 있으며, 예시는 제약을 타입에 인코딩하는 방법을 보여주려는 것이었다고 답함

범용 추상화와 단순 파일 시스템 중심 접근 사이의 선택

  • Christian Brauner는 Rust 추상화가 모든 커널 파일 시스템을 위한 범용 추상화인지, Rust로 작성된 더 단순한 파일 시스템에 필요한 기능 중심인지 먼저 정해야 한다고 봄
  • 장기적으로 get_or_create_inode() 같은 함수가 iget_locked()보다 훨씬 많은 제약을 담으면 문제가 생길 수 있음
  • C 코드는 특히 초기에는 Rust 코드보다 더 빠르게 진화할 수 있어, 두 API를 계속 동기화해야 함
  • Overstreet는 Rust 추상화를 추가하면서 리팩터링과 정리를 함께 할 것인지가 핵심이며, 그것이 필요하다고 강하게 봄
  • James Bottomley는 객체 생명주기가 Rust API에는 인코딩되지만 C에는 그에 해당하는 표현이 없다고 봄
    • 한쪽에서 객체 생명주기가 바뀌면 다른 쪽에 버그가 생길 수 있음
  • Chinner는 inode 객체의 생명주기가 때로 파일 시스템별로 다르다고 말함
    • 단일한 생명주기 이해를 API에 넣으면 일부 파일 시스템에서 그 함수들이 동작하지 않을 수 있음
  • Almeida는 예시가 현재 iget_locked()를 호출하고 이득을 볼 수 있는 파일 시스템에만 쓰일 것이라고 답함
    • Rust 개발자들은 파일 시스템이 현재 하는 방식을 바꾸도록 강제하려는 것이 아님

“고통을 누가 부담할 것인가”

  • Ted Ts'o는 Rust라는 “종교”로 모두를 전환시키려는 시도가 있는 것처럼 보이지만, Linux에는 50개 이상의 파일 시스템이 있고 즉시 전환되지는 않을 것이라고 말함
  • C 코드는 계속 개선될 것이고, 그 변화가 Rust 바인딩을 깨면 해당 바인딩에 의존하는 파일 시스템도 깨질 수 있음
  • Ts'o는 당분간 Rust 바인딩이 2급 시민이며, 깨진 Rust 바인딩은 전체 파일 시스템 커뮤니티가 아니라 Rust-for-Linux 개발자들의 문제라고 봄
  • 그는 Rust 바인딩 개발과 C 코드 진화를 병행하면서, 대량의 의미를 타입 시스템에 담는 접근이 좋은지 나쁜지 1~2년 안에 드러날 것이라고 봄
  • Ts'o에게 큰 변화는 결국 고통 배분 문제임
    • C API를 바꾸는 개발자가 영향을 받은 C 코드는 고치더라도, Rust를 모르기 때문에 Rust 바인딩은 고치지 않겠다고 말할 수 있음
  • Almeida는 C API를 고정하려는 것이 아니라, 파일 시스템 개발자들이 API의 의미를 설명해 주면 그것을 Rust에 인코딩하려는 것이라고 답함
  • Bottomley는 의미가 바인딩에 더 많이 인코딩될수록 동기화 관점에서 더 취약해질 수 있다고 봄
  • Almeida는 API가 바뀌면 API 사용자를 업데이트해야 하는 것은 다른 사용자와 마찬가지라고 답함

메서드, 함수, 타입에 무엇을 담을지

  • Viro는 iget_locked() 대체안이 메서드에 의존하는 점을 다시 문제 삼음
    • 메서드를 쓰면 인자가 명시적으로 지정되지 않는다고 봄
  • Overstreet는 메서드에 대한 불만이 상속에 지나치게 의존하는 C++ 같은 언어에서 비롯된다고 봄
    • Rust는 그렇게 하지 않으며, Rust의 메서드는 대체로 문법적 요소라고 말함
  • Jan Kara는 inode 자체에 따르는 동작과 iget_locked() 함수에 내재한 동작을 구분함
    • inode에는 참조 카운트와 그 처리 같은 동작이 따름
    • iget_locked() 함수에는 별도의 동작이 내재함
  • Overstreet와 Almeida는 두 부분이 모두 타입에 인코딩되지만 분리되어 있으며, inode 타입을 사용하는 다른 함수는 서로 다른 속성의 반환값을 가질 수 있다고 답함
  • Viro는 VFS에서 inode가 현재 방식으로 동작하는 이유를 설명했고, 작게 시작해서 진행 방향을 보자는 데에는 동의함
  • Overstreet는 이번 예시가 복잡해 출발점으로 좋지 않았을 수도 있다고 했고, Viro는 “아니, 그렇지 않다”고 답하며 세션이 마무리됨

댓글과 토론

Hacker News 의견들
  • 각 파일시스템이 inode 생명주기를 커스텀으로 가진다면서도, 같은 생명주기 관리 함수를 쓰고 의미론만 달라진다는 게 이해되지 않음
    같은 함수가 구현 세부사항에 따라 다르게 쓰여야 한다면 추상화 계층의 반대처럼 들림
    inode 생명주기가 파일시스템별이라면 파일시스템별 함수로 관리해야 함

    • 같은 의문이 들었고, Rust 작업을 위해 모든 C API를 이해하거나 문서화하려는 것으로 보임
      그런 정보를 모으다 보면 이런 질문이 애초에 나오지 않도록 리팩터링할 지점이 드러날 수 있고, 그건 좋은 일임
    • 처음 보는 자료라면 https://www.kernel.org/doc/html/latest/filesystems/vfs.html가 도움이 될 수 있음
      파일시스템별 동작을 처리하면서도 커널 쪽에는 일관된 인터페이스를 유지하는 VFS 계층 개요임
    • 보편적으로 가능한 부분은 VFS 계층에서 최대한 추상화하되, 맞지 않는 예외는 파일시스템별 계층에서 처리하려는 것으로 이해함
      inode 생명주기는 논의를 시작하기 위한 초기 사례였을 수도 있음
    • 컴파일러가 inode 수명을 추적하는 방식으로 동작하게 하려는 것 같음
      컴파일러가 임시 참조를 도와주되, 파일시스템은 여전히 링크 수를 디스크에 저장해야 함
    • inode를 만들고 캐시에 넣는 데 쓸 수 있는 함수는 여러 가지가 있으며, 여기서 집중하는 iget_locked()는 그중 특정 패턴임
      모든 파일시스템이 그 방식을 쓰지는 않고, 상황에 따라 안 쓰기도 함
      예를 들어 FAT는 inode 번호를 만들어내고 FAT 위치에서 inode로 가는 자체 매핑을 유지하기 때문에 쓰지 않음
      proc처럼 inode 객체를 캐시하지 않는 파일시스템도 있음
      inode 객체 자체는 어디서 왔든 상태 흐름이 같아 보이므로, 소비자 관점에서 inode 사용법은 바뀌지 않음
      달라지는 건 파일시스템 계층이 inode 객체를 만들고 내부적으로 다루는 방식임
  • 질문을 잘못하고 있는 건 아닐까 싶음
    Rust가 C를 더 쉽게 호출하도록 바뀌어야 하는 걸까?
    Rust를 조금 해봤지만 취미 개발자 입장에서는 C와 어떻게 상호 운용해야 하는지 아직 명확하지 않음
    반면 C++나 Objective C에서는 올바른 헤더를 포함하고 함수를 호출하면 됨
    Swift는 Objective C 파일을 포함할 수 있고, 거기서 C를 호출할 수 있음
    이 경우에는 커널 개발자들이 언어에 맞추기를 기대하기보다, Rust 언어가 조금 유연해져야 하는 것 아닐까 싶음

    • Rust에서 C 호출은 꽤 단순함
      외부 함수를 선언하고 호출하면 됨
      예를 들어 Rust 책의 https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#usin...에 나온 것처럼 extern "C"로 선언할 수 있음
      복잡한 라이브러리에서 선언을 손으로 전부 쓰고 싶지 않다면, C 헤더 파일에서 extern 선언을 자동 생성하는 bindgen 같은 도구를 쓰면 됨: https://github.com/rust-lang/rust-bindgen
      bindgen 같은 것이 Rust에 포함되어 제3자 의존성이나 build.rs 설정 없이 쓸 수 있으면 좋다는 주장은 가능하지만, 이 글의 핵심은 그게 아님
      문제는 저수준 바인딩이 아니라 Rust다운 고수준 래퍼이며, 임의의 C 코드에서 그런 래퍼를 자동으로 만들어낼 일반 도구는 있을 수 없음
    • 이건 Rust에서 중요한 난점도 아니고 글의 주제와도 관련이 없음
      글은 Rust로 커널 파일시스템 드라이버 등을 실제 구현하는 방법을 찾는 내용임
      커널 안의 Rust 코드는 필연적으로 C 인터페이스를 소비한다는 점도 중요함
      떠올리는 용례에는 bindgen이 꽤 잘 맞음: https://github.com/rust-lang/rust-bindgen
    • 실제로는 꽤 쉬움
      Rust에서 호출하려면 extern "C" fn foo() -> T를 선언하고, #[link] 속성이나 build.rs로 링크 플래그를 넘기면 됨
      bindgen 크레이트로 바인딩을 미리 생성하거나 build.rs에서 만들고 include!()로 포함할 수 있음
      보통은 생성된 바인딩만 담은 -sys 크레이트를 만들고, 실제 코드는 그 sys 크레이트의 바인딩을 평소처럼 use
      C++와 Objective C에서도 올바른 헤더를 포함하는 것뿐 아니라 라이브러리에 링크해야 함
    • 핵심은 Rust가 C로는 표현할 수 없는 불변 조건을 모델링할 수 있다는 점임
      양방향 호출은 가능하지만, C가 Rust가 표현할 수 있는 것을 표현하지 못한다면 양쪽이 공통으로 써야 하는 API 설계에 중요한 영향이 생김
    • C 호출은 이미 아주 단순하므로 Rust가 더 쉽게 바뀔 필요는 없음
      C 함수를 extern "C"로 선언하고 호출하면 됨
      보통 unsafe가 필요하고 참조를 원시 포인터로 변환하거나 캐스팅해야 하지만, 문법 자체는 간단함
      C 헤더 파일을 스캔해 선언을 만들어주는 도구도 있으며, bindgen이 가장 많이 쓰임
      이 글의 쟁점은 언어 자체가 아니라 Rust를 어떻게 쓰느냐에 가까움
      Rust-for-Linux 개발자들은 Rust의 기능과 타입 시스템으로 API 호출의 의미론을 인코딩해 더 안전하고 오류가 덜 나게 만들고 싶어 함
      C 쪽 사람들은 그렇게 하면 C API의 동작과 의미론을 발전시키기 어려워질까 걱정함
      C API가 바뀔 때 Rust API도 고쳐야 하고, 그 작업을 떠안고 싶지 않기 때문임
      더 받아들이기 쉬운 대안은 Rust API에 의미론을 인코딩하는 데 Rust 기능과 타입 시스템을 덜 쓰는 것임
      그러면 C API 변경 시 Rust API 갱신은 기계적이고 단순해지지만, Rust-for-Linux가 Rust 기능으로 더 좋고 안전한 API를 만들 수 없다면 이 작업의 의미가 무엇인지 의문이 생김
      다만 언어를 충분히 이해하지 못한다고 인정하면서 이 주제에 대해 확정적으로 말하는 건 좀 이상함
  • 이 Rust API가 C API를 감싸는 것인지, 아니면 재구현하는 것인지 Linux 파일시스템을 잘 몰라서 명확하지 않았음
    재구현이거나 별도 API라면 C API와 이름을 그대로 유지하는 건 시간이 갈수록 더 혼란을 만들 것 같음
    처음에는 익숙한 개발자가 더 빨리 이해하는 데 도움이 되더라도 그렇다 봄

    • Almeida가 Rust에서 iget_locked()에 해당하는 것을 보여줬고 이름은 get_or_create_inode()였음
      답은 재구현이며 같은 이름을 쓰지 않는 쪽으로 보임
  • 이런 논의가 보통 흘러가는 방식과 변화의 규모를 생각하면, 이번 논의는 놀라울 정도로 예의 바른 편
    이 스레드의 부정적인 분위기에는 동의하지 않음
    관련자들이 핵심 고통 지점을 헛소리 없이 명확히 전달했다는 점에서 꽤 낙관적임

    • 내용보다도 훌륭한 기록 정리 때문에 더 읽게 됨
      실제 논의는 강한 의견을 가진 괴짜들끼리 하는 프로그래밍 언어 논쟁답게 격하고, 산만하고, 꼬투리 잡기도 많았을 것 같음
      이 요약을 쓴 Jake Edge가 그런 부분을 걷어내고 핵심만 적는 데 매우 뛰어난 듯함
  • lwn.net 페이지 아래 댓글 일부는 꽤 무례함
    기여 중인 오픈소스 프로젝트에 Science advances one funeral at a time 같은 댓글을 받는다고 상상해보면 됨

  • Linux 커널에 선택지가 더 많아지는 건 항상 이로움
    하지만 Rust가 모든 것의 해답은 아닐 수 있음
    Rust는 안전한 프로그래밍 모델을 보장하려고 최선을 다하지만, 그 모델에도 한계가 있음
    메모리 문제면 Rust를 쓰고, 동시성 문제면 Rust로 바꾸자는 식으로 보일 수 있지만, unsafe 블록 없이 C가 하는 모든 일을 할 수는 없음
    Rust는 이런 문제에 새로운 관점을 줄 수 있지만 완전한 해결책은 아님

    • 이 작업에서 Rust의 큰 장점은 그런 안전성 문제를 타입으로 캡슐화하는 데 적극적이라는 점이고, 이 글도 바로 그 이야기임
      C, 특히 커널에서 쓰이는 C는 암묵적 규칙을 모두가 완전히 알아야 한다는 책임을 각자에게 떠넘김
      그건 확장될 수 없음
      같은 자료구조를 쓰는 커널 개발자들이 한 방에 모여서도 그 규칙에 완전히 합의하지 못했음
      Rust는 알아야 할 규칙을 드러내고, 다른 누군가가 규칙 준수를 보장할 수 있다면 그걸 내 문제가 아니게 만드는 데 강함
      때로 결과가 덜 최적일 수 있지만, Linux 커널에서도 덜 최적인 기본값이 맞는 경우가 많고, 더 나은 성능을 위해 이상한 규칙 여섯 개를 더 배울 여력이 있는 사람에게는 unsafe 탈출구를 제공하면 됨
    • unsafe 블록은 쓸 수 있음
      필요할 때만 써야 할 뿐임
      영향 범위가 아주 제한된 unsafe 블록을 쓴다고 해서 나머지 코드에서 얻는 보장이 전부 사라지는 건 아님
    • 저수준 작업에는 unsafe 코드가 필요하다는 건 맞음
      하지만 unsafe를 써야 하니 Rust가 적합하지 않다는 건 오해임
      Rust의 안전/비안전 구분은 코드의 어느 부분이 비안전한지 명확히 표시해, 작은 부분에 감사를 집중하고 그 부분만 맞으면 나머지는 동작한다고 신뢰할 수 있게 해주는 데 목적이 있음
    • 이 중 실제로 100% 명백히 필요한 부분이 얼마나 되는지 궁금함
      파일시스템 코드에서 반드시 unsafe여야 할 좋은 이유가 있을까?
      아마 몇 군데에 필요한 아주 작은 부분집합일 것 같음
    • Rust가 이해가 잘 맞아떨어질 때가 있어서 좋아하긴 하지만, 비동기 쪽은 아직 거친 부분이 많다고 느낌
      내부에서 무슨 일이 일어나는지 직관적이지 않음
  • 회의록을 보면 커널 안의 Rust는 추가 복잡성 비용처럼 보임
    운영체제를 처음부터 새로 쓴다면 언어의 힘을 온전히 쓸 수 있음
    하지만 이미 거대한 코드베이스 옆에 덧붙이면 여기서 보듯 추가 문제가 생김

    • 맞지만, 그 비용은 더 쉬운 드라이버 개발로 상쇄되어야 함
      Asahi Linux의 Rust GPU 드라이버가 한 달 만에 만들어졌다는 블로그를 보면 됨
      Google에서 tales of the m1 gpu를 검색하면 되고, 작성자는 Hacker News에 매우 부정적인 생각을 갖고 있음
      원하면 링크로 읽을 수 있음: https://asahilinux.org/2022/11/tales-of-the-m1-gpu/
      보편적으로 적용될지는 앞으로 몇 년을 봐야 함
    • 완벽을 추구하다가 좋은 것을 놓치는 느낌이 강함
    • Rust의 장점은 동의하지만, 이성이 과대광고를 이기기는 어렵다고 보는 편임
      그 비용은 미래와 진보를 받아들이기 위해 필요한 것으로 여겨질 것임
      왜 알 수 없는 버그와 절충이 많은 거대한 흐름에 뛰어들기보다, 안전한 부분집합으로 제한하지 않는지 궁금함
    • 추가 코드라면 Rust뿐 아니라 무엇이든 복잡성을 도입한다고도 말할 수 있음
      이미 너무 거대하니 혁신을 멈추고 무기한 유지보수 상태로 들어가야 한다는 뜻은 아님
      현실의 세금처럼 한쪽의 비용이 다른 문제를 상쇄하는 데 쓰인다면 순손실이 아닐 수 있음
      결론도 나지 않은 단일 논의만 보고 아무 문제도 상쇄하지 못할 거라고 말하는 건 논지가 짧아 보임
  • C API와 Rust API의 이름이 어긋나서 C 코드를 보고 동등한 Rust 호출을 알 수 없다는 부분은, 오래된 이름 짓기 관례와의 싸움처럼 보임
    같은 이름을 유지하되 대체 이름을 원할 때는 새 이름이 옛 이름을 감싸도록 해서 잘 풀린 적이 있음
    그래도 이름 짓기는 어렵다

    • 컴퓨터 과학의 두 가지 큰 문제 중 하나임
      나머지 둘은 동시성과 오프바이원 오류임