
자바스크립트로 리눅스 init 프로세스를? Ultimate-Linux 프로젝트 분석
자바스크립트로 리눅스 PID 1(init)을 구현한 Ultimate-Linux 프로젝트를 분석합니다. QuickJS와 musl libc를 활용한 시스템 프로그래밍의 원리를 살펴봅니다.
김영태
테크리드

안녕하세요, 8년차 개발자 김테크입니다. 오늘은 백엔드 개발자나 인프라 엔지니어라면 한 번쯤 눈이 휘둥그레질 만한 재미있는 프로젝트를 하나 소개하려고 합니다.
보통 리눅스 사용자 공간(Userspace)이라고 하면 C로 작성된 GNU Coreutils나 BusyBox, 혹은 최근에는 Rust로 재작성된 도구들을 떠올리곤 합니다. 그런데 "순수 자바스크립트로 리눅스 마이크로 배포판을 만들었다"는 주장을 하는 프로젝트가 등장했습니다. 바로 Ultimate-Linux라는 프로젝트입니다.
자바스크립트가 웹 브라우저나 Node.js 런타임 위에서 돌아가는 건 익숙하지만, 리눅스 커널 바로 위에서 PID 1(init 프로세스)로 돌아간다니, 기술적으로 어떻게 구현했는지 호기심이 생기더군요. 오늘은 이 프로젝트가 어떻게 작동하는지, 그리고 우리에게 어떤 시스템적 통찰을 주는지 뜯어보겠습니다.

먼저 이 프로젝트의 핵심 아이디어는 간단합니다. 리눅스 커널은 부팅이 끝나면 딱 하나의 프로세스를 실행합니다. 보통은 systemd나 SysVinit 같은 init 시스템이죠. 하지만 커널 입장에서 이 첫 번째 프로세스가 무엇인지는 중요하지 않습니다. 그저 커널과 대화(System Call)할 수만 있으면 됩니다.
이 프로젝트의 개발자는 QuickJS라는 아주 가볍고 빠른 자바스크립트 엔진을 사용했습니다. QuickJS는 임베디드 시스템에서도 사용할 수 있을 정도로 작고, ES2020 스펙을 거의 지원하는 훌륭한 엔진입니다.
개발 과정은 꽤 흥미롭습니다. 단순히 Node.js를 설치하는 게 아닙니다. 그랬다가는 의존성 지옥에 빠지겠죠.
첫째, 자바스크립트로 쉘(Shell) 로직을 짭니다. ls, cd, cat, mkdir 같은 기본적인 명령어들을 JS 함수로 구현하는 것이죠.
둘째, 이 JS 코드를 QuickJS 컴파일러(qjsc)를 이용해 C 코드로 변환(Transpile)합니다.
셋째, 여기서 중요한 포인트가 나옵니다. 바로 정적 링크(Static Linking)입니다. 보통 리눅스 프로그램은 glibc 같은 공유 라이브러리에 의존합니다. 하지만 텅 빈 깡통 같은 리눅스 환경에서 돌아가야 하므로, 외부 의존성이 없어야 합니다. 개발자는 이를 위해 musl libc를 사용했습니다. musl은 가볍고 정적 링크가 용이해서 알파인 리눅스(Alpine Linux) 등 컨테이너 환경에서도 애용되는 라이브러리입니다.
결과적으로 시스템의 라이브러리에 전혀 의존하지 않는, 덩그러니 혼자 실행 가능한 바이너리 파일 ultimate_shell이 탄생하게 됩니다.

물론 개발자도 고백하듯 "100% 순수 자바스크립트"는 아닙니다. 파일 시스템을 마운트(mount)하는 등의 저수준 작업은 자바스크립트만으로는 한계가 있어서 아주 약간의 C 코드가 접착제 역할을 합니다. 하지만 핵심 로직이 JS로 돌아간다는 점은 변함이 없습니다.
이 바이너리를 initramfs(초기 램 파일 시스템)로 압축한 뒤, QEMU 같은 가상머신 관리자를 통해 부팅합니다. 이때 커널 파라미터로 rdinit=/ultimate_shell을 넘겨주면, 커널은 부팅 직후 우리의 자바스크립트 프로그램을 시스템의 주인(PID 1)으로 임명합니다.
QEMU 콘솔에 ULTIMATE LINUX SHELL이라는 문구가 뜨고 JS로 작성된 쉘이 입력을 기다리는 모습을 보면, 인프라 엔지니어로서 묘한 카타르시스가 느껴집니다.
이 프로젝트가 단순히 장난감처럼 보일 수도 있습니다. 하지만 여기서 얻을 수 있는 기술적 교훈은 묵직합니다.
우선 "리눅스는 커널일 뿐이고, OS의 나머지는 사용자가 정의하기 나름"이라는 사실을 명확히 보여줍니다. 우리가 당연하게 생각하는 우분투나 CentOS의 환경은 커널 위에 쌓아 올린 하나의 '취향'일 뿐이라는 거죠.
또한, 시스템 콜(System Call)과 ABI(Application Binary Interface)의 안정성에 대한 증명이기도 합니다. 언어가 무엇이든(Go, Rust, 심지어 JS라도), 리눅스 커널이 약속한 규약만 지킨다면 시스템을 제어할 수 있다는 것을 보여줍니다.
실무에서 도커 이미지를 최적화하거나, 쿠버네티스 노드의 부팅 문제를 트러블슈팅할 때 init 프로세스의 동작 원리를 이해하고 있는 것은 큰 도움이 됩니다. "왜 컨테이너가 시작하자마자 죽지?"라는 의문이 들 때, PID 1이 제대로 돌고 있는지, 라이브러리 링크는 깨지지 않았는지 확인하는 습관은 이런 기초 원리에서 나오니까요.
시간이 되신다면 주말에 한 번쯤 가상머신을 띄워놓고 이 엉뚱하지만 진지한 자바스크립트 리눅스를 체험해 보시는 건 어떨까요? "Hello World"를 콘솔에 찍는 것과는 차원이 다른 재미를 느낄 수 있을 겁니다.


