Fomos: 러스트로 구축된 실험용 운영체제
(github.com/Ruddle)- Fomos는 Rust로 구축된 실험용 OS이며, Non-Unix OS 아이디어와 exo-kernel 패턴의 과제를 이해하기 위한 프로젝트임
- 그래픽 출력, 동적 할당, 동시 앱 로드와 실행, Virtio 마우스·키보드 지원, 협력적 스케줄링을 제공함
- 앱은
pub extern "C" fn _start(ctx: &mut Context) -> i32형태의 단일 함수로 다뤄지며, 표준 라이브러리 없이 OS 기능을Context를 통해 받는 구조임 Context는 로그, PID, 프레임버퍼,calloc,cdalloc,store, 입력 같은 기능과 상태를 담는 구조체이며, 새 기능을 뒤에 추가해 오래된 앱과의 호환을 유지하는 방식임- 시스템 콜은 없으며, 앱은
return으로 OS에 제어를 돌려주고 이후start함수가 다시 호출되는 협력적 실행 모델임 - 앱 상태는
Context.store에 저장할 수 있고, 커널 루프는 앱 목록을 순회하며 각 앱의_start(Context::new(...))를 호출하는 단순화된 구조임 - 모든 기능과 부작용이
Context를 통해 전달되므로,Context의 함수 교체나 래핑을 통해 샌드박싱, 계측, 디버깅을 구성할 수 있는 전제임 - 현재 보안은 구현되지 않았고, 앱이 다른 앱의 RAM을 확인할 수 있는 상태이며, 컨텍스트 스위치와 앱별 가상 메모리 스택 없이 데이터 보안을 구현하려는 계획이 있음
- 누락된 항목은 영구 저장소, GPU 지원, 네트워킹, 앱 간 데이터와 기능 공유를 위한 추상화이며, Virgl은 작업 중인 상태임
- 빌드는
./build.sh로 실행하며,rust nightly,gcc, Virgl 및 SDL 플래그가 있는qemu가 필요할 수 있음
댓글과 토론
Hacker News 의견들
-
마음에 안 드는 부분은 선점형 시스템에서는
while (true)가 시스템을 느리게 만들 수는 있어도, 협력형 시스템에서는 제어를 돌려주지 않는 순간 기계가 사실상 멈춘다는 점임
보안 관점에서도 이런 시스템은 서비스 거부 공격이 너무 쉬워지고, 어떤 앱의 버그 하나가 전체 시스템으로 번질 수 있음
운영체제 개발자는 아니니 틀렸다면 바로잡아 줬으면 함- 맞는 말이고, 그 반론은 논점 흐리기에 가깝다고 봄
운영체제가 협력형 다중작업을 버린 이유는 모든 “폭주하는 자원 사용” 문제를 영원히 해결해서가 아니라, UI 스레드 블록이나 우발적 무한 루프 같은 단순한 앱 수준 오류가 전체 시스템 상태를 망가뜨리기 때문임
임의의 프로그램을 실행하도록 설계된 시스템에서는 꽤 치명적임 - 전부 아니면 전무로만 보면 안 됨
앱이 협력적으로 스케줄링하도록 허용하면서도while(true){}가 시스템을 무한히 붙잡지 못하게 할 수 있음
예를 들어 앱별 제한 시간을 두거나, 더 실험적으로는 루프를 감지하고 넉넉한 제한 시간을 둘 수도 있음
프로그래머에게는 무관한 프로그램 간 격리가 최대이고 관련 프로그램 간 격리는 최소일 때 편하지만, 사용자에게는 계산 자원은 강하게 격리되고 저장소나 권한 같은 것은 덜 막혀 있을 때 편함
실제로는 협력형보다 선점형에 더 가깝지만 약간 협력형인 복잡한 스케줄링이 필요함
Linux에서도 포크 폭탄 같은 악성 프로그램은 특히 스왑이 켜져 있으면 시스템을 멈추게 만들기 어렵지 않고, 선점형 스케줄러가 있어도 한 프로그램이 시스템 스레드의 99%를 차지하면 사실상 대부분 그 프로그램이 실행됨 - 작성자임
스케줄링은 스펙트럼이고, 현재 운영체제도 선점형이지만 어느 정도 협력형이기도 함
앱은 제어권을 양보할지 결정할 수 있음
반대로 운영체제는 협력형으로 두되, 자원 사용 임계값이나 타이머 인터럽트가 발생하면 드물게 실패 모드로 문맥 전환해 선점형으로 바뀌고 앱을 종료한 뒤 다시 협력형으로 돌아갈 수 있음
이를 낙관적 협력형, 비관적 선점형이라고 부를 수 있겠음 - 운영체제 개발자는 아니지만 코어 수가 차이를 만든다고 봄
while(true)루프는 단일 코어 시스템은 무너뜨리지만 다중 코어 시스템에서는 꼭 그렇지 않음
지금 우리가 쓰는 운영체제의 기본 구조가 만들어졌던 시절과 지금은 절충점이 달라졌을 수 있음 - Windows 3.1의 협력형 다중작업 실험 이후 세상이 선점형 다중작업으로 간 데는 이유가 있음
- 맞는 말이고, 그 반론은 논점 흐리기에 가깝다고 봄
-
특히 “Fomos에서 앱은 그냥 함수일 뿐”이라는 부분이 좋았음
Unix나 Windows 실행 파일은 독립 함수에 비해 매우 복잡하니, 이런 식으로 작성된 커널이 얼마나 멋질지 상상하기도 어려움
Smalltalk/Squeak도 이런 방식인지 궁금하고, 작성자가 파일 시스템, 작업 관리자, 안전한 메모리 스택, 자원 공유까지 계속 이어 갔으면 좋겠음
물론 최소 개념 증명 요구사항으로 DOOM 실행도 필요함- Smalltalk에서는 독립 함수라기보다 클래스의 메서드에 해당하지만, 이미지 안의 모든 객체가 서로 메시지를 보내며 즉 서로의 메서드를 호출한다는 점에서는 맞음
Lisp 머신 운영체제가 더 가까운데, 초기에는 객체 시스템 없이 독립 함수들이 서로 호출했고 나중에는 인자 클래스에 특화된 제네릭 함수가 됨 - “앱은 그냥 함수”라는 말은 그린필드 개발의 저주처럼 들림
설계자들이 다른 운영체제가 왜 지금 빠뜨린 것들을 필요로 하게 됐는지 아직 발견하지 못한 상태로 보임 - 실제로는 앱이
int main() { … }함수인 다른 운영체제와 무슨 차이가 있는지 궁금함 - 이건 유니커널의 정의와 맞아 보임
Rust로 만든 다른 예시는 https://github.com/hermit-os/hermit-rs가 있음 - 구식 Classic MacOS를 실행해 보면 됨
- Smalltalk에서는 독립 함수라기보다 클래스의 메서드에 해당하지만, 이미지 안의 모든 객체가 서로 메시지를 보내며 즉 서로의 메서드를 호출한다는 점에서는 맞음
-
아이디어는 괜찮지만, 오래된 항목과 호환되도록 Context 구조체에 새 함수를 계속 덧붙이는 방식은 하위 호환성 지옥으로 가는 길임
오래되거나 폐기된 항목을 Context 구조체에서 제거하지 못하게 스스로를 가두는 셈임
더 나은 방식은 운영체제와 앱 사이에 의미적 버전 관리를 도입하는 것 같음
앱이 어떤 운영체제 버전에 맞춰 빌드됐거나 의존하는지 선언하면, 운영체제는 호환 여부를 확인하고 그에 맞는 Context 구조체 버전을 넘길 수 있음
하위 호환성 문제 대부분은 남지만, 커널 안에 주요/부 버전별 구조체를 여러 개 두는 방식으로 Context 구조체를 깔끔하게 유지할 수 있음- 작성자임
좋은 아이디어지만 런타임 인터페이스가 하나뿐이라는 단순함도 마음에 듦
어차피 운영체제가 모든 버전을 처리해야 한다면, 미래의 앱은 패딩을 써서 “깔끔한” 것처럼 느끼게 할 수도 있음
struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }
다만 이렇게 적고 보니 좀 잘못된 느낌이 들기도 함
앱이 자기 버전을 선언하는 건 ELF 같은 실행 파일 형식이 이미 해결하는 문제처럼 느껴져서, 대안을 시도해 보고 있음
- 작성자임
-
“어떻게 sleep하거나 비동기 대기하나? 그냥 return하면 된다”는 부분이 좀 이상함
io_uring식 비동기 입출력은 훌륭할 텐데, 이 모델은 그런 것을 배제하는 듯해서 적절한 성능을 내기 어려울 수 있음
비동기를 지원하지 않는 것도 이상한데, 자연스러운 중단 지점으로 연결할 수 있기 때문임
다만 그렇게 하려면 애플리케이션 상태를 디스크에 명시적으로 저장하고 불러와야 하는 설계를 많이 포기해야 할 것 같고, 비용이 커 보임
네트워킹도 비슷한 이유로 적어도 효율적으로 하기는 어려워질 수 있다고 봄- 추측하자면 비동기 입출력은 Context에 입출력 요청을 갱신하고 반환한 뒤, 준비되면 함수가 다시 호출되는 식으로 구현될 것 같음
이 함수는 임의 상태를 매개변수로 받는 이벤트 루프의 끝단처럼 보이므로, 이벤트 루프가 하는 일은 대체로 일반화할 수 있을 듯함
다만 언어 수준의 코루틴과 비동기 지원은 포기하게 됨
- 추측하자면 비동기 입출력은 Context에 입출력 요청을 갱신하고 반환한 뒤, 준비되면 함수가 다시 호출되는 식으로 구현될 것 같음
-
제시된 예시는 너무 작위적임
선점형 운영체제에서 앱은 보통 스레드 교착 상태나 무한 루프처럼 전체를 협력형으로 만들지 않는 방식으로 멈춤
또한 선점형 시스템은 앱이 너무 많은 스레드나 파일을 만들거나 메모리를 너무 많이 쓰면 사실상 협력형이 되기 훨씬 전에 종료할 수 있음
우리 시스템은 그저 더 관대한 편임
게다가 “샌드박싱은 전제만 받아들이면 공짜”라고 하면서도 “어떤 앱이든 다른 앱의 RAM을 쉽게 확인할 수 있고, 이건 풀기 어려운 문제”라고 하니, 샌드박싱은 공짜가 아님
그래도 멋진 아이디어이고 작성자가 잘됐으면 함- 브라우저 세계에서는 열린 모든 사이트가 브라우저 힙 메모리를 공유하지만 서로 간섭하지 않음
이 문제의 해법은 함수, 즉 애플리케이션을 감싸는 클로저를 만들어 앱 자체의 Context처럼 동작하게 하는 것일 수 있음
앱이 앱을 열 수 있다면, 또는 앱이 다른 앱에게 운영체제가 될 수 있다면 어떨까 싶음 - 그 예시는 모든 시스템이 Windows와 Linux처럼 샌드박싱이 형편없다고 가정하므로 더 작위적임
견고한 샌드박싱을 위해 제대로 설계된 시스템이라면 모든 자원에 제한을 두고, 제한에 도달하면 요청을 거부함
- 브라우저 세계에서는 열린 모든 사이트가 브라우저 힙 메모리를 공유하지만 서로 간섭하지 않음
-
Fomos가 프로세스와 실행 파일을 어떻게 구분하는지 궁금함
Linux에서 프로세스는 argv/envp 포인터, 스택, 힙, 시그널 마스크, 파일 핸들 테이블, 시그널 핸들러, 실행 가능한 메모리를 포함한 가상 주소 공간과 uid, gid 같은 커널 내부 데이터임
실행 파일은execve시스템 호출 때 로더가 그 주소 공간을 채울 만큼의 비트를 담은 파일임
실행 파일 없이도clone3나fork로 프로세스를 만들 수 있고, 커널은 ELF를 쓰고 사용자 공간 대부분은 GLIBC의 RTLD 로더를 쓰지만 특정 실행 파일 형식으로 프로세스를 만들기 위해 꼭 둘 다 필요하지는 않음
위치 독립 코드가 없는 정적 링크 실행 파일은 어셈블러 관점에서 “그냥 함수”에 가깝지만, ASLR 없이 런타임 심볼을 해석하면 의존 함수 주소가 알려졌을 때 버퍼 오버플로 공격에 취약해짐
glibc의 결점과 Posix 프로세스 모델의 대안을 원하지만, Unix 실행 파일 복잡성의 상당 부분은 본질적이라고 봄
런타임 심볼 해석은 어렵지만 유용하고, 임의 인터프리터 허용은 성가셔도 Linux가 Windows와 MacOS보다 강한 부분이며, 안정적인 시스템 호출로 커널 인터페이스를 제공하는 것이 Linux의 강점임- 조금 더 생각해 보니 큰 실수 중 하나는 실행 파일 형식의 동질화 같음
Mac은 Mach-O, Windows는 PE, Linux는 ELF인 식인데, 실행/링크 형식의 다양한 생태계를 갖지 못할 이유는 없음
코드를 불러오는 모델이 아주 단순한 운영체제는 그런 실험에 좋은 장소임 - Linux의 강점이 안정적인 ABI라기보다 드라이버라고 생각했음
안정적인 ABI가 Linux만의 독특한 점도 아니고 그런 결정의 유용성도 꽤 의심스럽지만, 드라이버 지원은 대단하고 부정하기 어려움 - Zircon(Fuchsia)이 이걸 어떻게 다루는지 봤는지 궁금함
꽤 흥미로움
- 조금 더 생각해 보니 큰 실수 중 하나는 실행 파일 형식의 동질화 같음
-
신뢰할 수 없는 협력형 앱으로 어느 정도의 보안과 안전성을 어떻게 달성할 수 있는지 모르겠음
어떤 앱이든 CPU를 무기한 붙잡아 커널과 다른 앱을 멈출 수 있음
우리가 선점형 스케줄링 운영체제를 쓰는 이유는, 오작동하는 앱을 나머지 시스템을 망치지 않고 중단할 수 있기 때문임- 2000년대 중반 Microsoft Research에 .NET으로만 작성된 프로토타입 운영체제가 있었던 걸로 기억함
선점형 다중작업은 썼지만 메모리 보호는 강제하지 않았고, 대신 컴파일러를 시스템 서비스로 둬서 시스템 컴파일러가 만들고 서명한 실행 파일만 실행할 수 있게 했던 것 같음
컴파일러가 빌드 시점에 메모리 보호를 보장하므로 시스템 호출과 프로세스 간 통신이 매우 저렴해졌음
조금 더 정교한 컴파일러라면 필요한 위치에yield호출을 삽입해 협력형 다중작업도 비슷하게 강제할 수 있음
일반적인 정지 문제는 풀 수 없지만, 정적 분석으로 종료하거나 양보함을 증명할 수 있는 프로그램 부류는 여전히 존재함
증명할 수 없는 프로그램만 별도로 다루면 되고, 감시 타이머로 오작동 프로그램을 자동 중단할 수도 있음 - Squeak이나 Pharo 같은 Smalltalk 시스템에서는 스레드가 멈추면 사용자가 키보드 단축키로 실행을 중단함
신뢰할 수 없는 코드는 “메인” 이미지에서 실행하지 않고 버릴 수 있는 VM에서 돌림
여기서도 하이퍼바이저를 써서 같은 모델을 적용할 수 있겠지만, Smalltalk 시스템만 단독으로 쓰는 사람은 없고 어느 정도 인프라가 필요함
- 2000년대 중반 Microsoft Research에 .NET으로만 작성된 프로토타입 운영체제가 있었던 걸로 기억함
-
완전한 재설계 없이, 사실상 기존 운영체제들이 이미 한 일을 다시 하지 않고 이 운영체제에서 보안을 구현할 수 있는지 궁금함
같은 하드웨어에서 실행되는 애플리케이션의 보안을 강제하는 방법은 두 가지를 알고 있음
하나는 런타임에서 가상 메모리로 프로세스를 격리하는 방식이고, 다른 하나는 로드 시점에 로더가 코드의 임의 메모리 접근 여부를 검증하는 방식임
후자는 보통 JVM이나 Smalltalk처럼 포인터 연산이 없는 제한된 명령어 집합의 바이트코드만 허용하는 가상 머신으로 강제함
Fomos 작성자는 문맥 전환과 메모리 격리 등을 원하지 않고, Rust 컴파일러는 바이트코드를 만들지 않는데 다른 방법이 있을까?- Theseus는 바이트코드 없이 Rust로 구현한 두 번째 방식의 예임
이해한 바로는 인증된 컴파일러에서unsafe금지 같은 규칙을 강제하므로, 여기서는 소스 코드가 사실상 바이트코드에 해당함
얼핏 보면 Midori와 매우 비슷하지만 세부 구현은 꽤 다름
Theseus에서는 드라이버와 애플리케이션 등이 ELF 객체이고, 모두 하나의 실행 파일, 즉 커널로 동적 링크되며 핫 업그레이드 같은 흥미로운 기법도 있음
https://github.com/theseus-os/Theseus
https://www.theseus-os.com/ - 추측이지만 모든 “프로그램”이 하나의 주소 공간을 공유하되, 가상 메모리로 특정 시점에 접근 가능한 페이지의 가시성을 제한할 수 있을 듯함
런타임에서 세그멘테이션 오류가 나면 호출자가 해당 페이지에 접근해 호출할 권한이 있는지 보안 토큰 같은 것을 확인하는 식임
실제로 얼마나 실용적일지는 모르겠음 - https://en.wikipedia.org/wiki/Capability-based_addressing ?
- Theseus는 바이트코드 없이 Rust로 구현한 두 번째 방식의 예임
-
협력형 다중작업이라도 지금은 코어가 매우 많다는 점에서 Classic MacOS 시절과 같지는 않을 것 같음
양보하지 않는 프로세스 한두 개가 반드시 전체 시스템을 붙잡지는 않을 수 있음
함수가 잘못 동작해 반환하지 않는다면 시스템이 코어를 다 썼을 때 종료할 수도 있을 것임
협력형 다중작업이 꼭 나쁜 성능을 뜻하지는 않음
시분할은 원래 거대한 단일 CPU를 여러 사용자에게 나눠 주기 위한 방법이었지만, 이제 단일 사용자 다중 코어 CPU가 흔하니 코어를 쓰는 다른 방식을 생각할 때가 지났음
이 프로젝트가 존재한다는 점이 정말 기대됨- 덧붙이면, “협력형 다중작업이 꼭 나쁜 성능을 뜻하지는 않는다”는 말은 나쁜 상호작용 성능을 뜻한 것이었음
이 모델에서는 문맥 전환이 없어서 오히려 성능이 좋아질 가능성이 있음
그래서 Linux의 타임슬라이스를 10초 같은 말도 안 되는 값으로 올리면 무슨 일이 생길지 궁금해짐
- 덧붙이면, “협력형 다중작업이 꼭 나쁜 성능을 뜻하지는 않는다”는 말은 나쁜 상호작용 성능을 뜻한 것이었음
-
보안 계획을 더 자세히 듣고 싶음
전반적으로 이런 실험은 운영체제가 그린필드 설계로 개선될 수 있음을 보여 준다고 봄
Mirage OS가 조금 떠오름: https://mirage.io/