Dilluti0n의 블로그

유닉스에서 프로세스는 다른 프로세스가 fork(2) -> exec(3) 해줘야 만들어진다. 그런데 부팅 직후에는 아무 프로세스도 없으니 fork할 주체가 없다. 그래서 커널이 유일하게 직접 실행해주는 유저 프로세스가 init(8)이다. init은 OS에 상주하는 서버 프로그램들(데몬)을 띄우고, /etc/inittab을 기반으로 agetty(8)를 실행시켜 각 터미널에 login(1)을 띄운다. 사용자가 로그인을 완료하면 login이 exec하여 bash 같은 로그인 셸이 호출되고, 그제서야 시스템을 사용할 수 있게 된다.

systemd는 이 init을 대체하면서 서비스 관리뿐 아니라 로깅, 네트워크, 디바이스 관리 등 시스템 전반을 하나로 묶었다. 다른 배포판에서는 데스크탑 유저가 거의 만질 일이 없고 선언적(.service 파일들)으로 데몬들을 관리할 수 있어서 잘 동작한다. 하지만 서비스 스크립트를 직접 수정해야 할 일이 생기면 — Gentoo에서는 꽤 잦은 일이다 — systemd에 통달하지 않는 이상 문제가 발생하기 쉽다. 바이너리 로그(journald)도 journalctl 없이는 읽을 수 없어서 디버깅이 번거롭다.

OpenRC는 init 자체를 대체하지 않는다. init은 sysvinit이 그대로 담당하고, OpenRC는 그 위에서 서비스 간 의존성 관리와 시작/정지를 처리하는 서비스 매니저다. systemd보다 담당하는 일이 훨씬 적고, 모든 서비스가 OpenRC 프레임워크 아래서 동작하는 셸 스크립트로 작성되어 있어 이해하기 쉽고 관리 오버헤드도 적다.

마이그레이션을 해 보니 systemd라는 거대한 레이어 아래서 베일에 싸여 진행되던 일들이 사실 별로 큰 일이 아니라는 걸 알 수 있었다. 배포판이 천차만별의 사용자들에게 일률적인 경험을 제공하기 위해서는 systemd의 기능이 필요할 수도 있지만, 그렇지 않은 사용자에게는 불필요한 레이어가 많고, 그것 때문에 부팅에서 로그인 셸까지의 단순한 과정이 오히려 복잡해진다.

마이그레이션 전 점검

현재 시스템이 systemd에 크게 의존하는 부분은 둘이다.

systemd-boot

Grub2 대신에 쓸 수 있는 UEFI 전용 부트로더다. grub의 난해한 구조와는 달리 꽤 깔끔하다. 설정파일들은 /efi/loader 에 들어가는데, 몇 줄 안된다. bootctl install 을 실행하면 위 경로를 읽어서 /efi/EFI/systemd/systemd-boot.efi 에 부트로더를 작성하고 메인보드의 NVRAM 을 조작해 이 경로를 가리키도록 설정한다. grub과는 달리 구조가 이렇게 단순한 건 Linux 3.1 부터 도입된 CONFIG_EFI_STUB 덕분이다. 이 옵션은 커널 이미지 자체를 EFI 실행파일로 컴파일하게 해준다. 그래서 부트로더가 이미지를 메모리에 한땀 한땀 올리는 대신에 이미지 파일이 있는 경로만 메인보드에게 가르쳐 주면 된다.

어쨋던 이름과는 달리 systemd와는 큰 관계가 없어 마이그레이션엔 별 문제가 없을거 같지만… 원래 리눅스 설치할 때 가장 큰 난관이 부트로더 설정하는 거라 부팅이 안될까봐 떨리는(?) 요인 중에 하나였다.

내가 좋아하는 기능(?) 중에 하나는 installkernel(8) 시에 initramfs 생성 이후 부트로더 업데이트까지 한큐에 되는 건데 일단 그건 마이그레이션 이후 부팅이 된 이후에 생각하기로 했다.

systemd-cryptenroll

tpm에서 initramfs나 커널 파라미터, 부팅 이미지 같은 정보(pcr)를 받아와 그것 자체를 LUKS 키로 등록할 수 있다.

문제는 커널이 업그레이드 될 때 마다 initramfs가 바뀌므로 매번 LUKS key slot에서 등록된 키를 지우고, 다시 재등록 해야 된다. tpm 자체에도 뭔가를 등록해야 해서 수동으로 다 하기는 상당히 복잡한데, 다행히 systemd-cryptenroll(1)이 있어서 아래 스크립트로 등록 과정을 자동화하고 있었다.

luks-tpm-update() {
    local LUKS_BLK_DEV=/dev/nvme0n1p2

    sudo systemd-cryptenroll "$LUKS_BLK_DEV" --wipe-slot=tpm2 &&
        sudo systemd-cryptenroll "$LUKS_BLK_DEV" \
             --tpm2-device=auto \
             --tpm2-pcrs=4+9+12 \
             --tpm2-with-pin=no
}

systemd-cryptenroll 로 등록된 키는 LUKS 헤더에 systemd만 알 수 있는 표시를 하는데, initramfs의 systemd-cryptsetup(8) 이 부팅 시에 LUKS에 등록된 tpm 키가 존재하면 tpm에 요청을 보내 LUKS open 을 시도한다.

즉 initramfs에서 systemd를 사용하지 않을꺼면 그것과도 작별해야 하는데… 우선 LUKS 마스터 키가 있으니 systemd-cryptsetup 대신에 dracut의 crypt 모듈이 cryptsetup open 을 시킬 수 있을거라 당장 급한 건 아니라서 일단 마이그레이션 후, 부팅을 시키고 생각해 보기로 했다.

백업

뭔가가 잘못돼서 시스템을 사용하지 못하게 될 수도 있으니 백업을 먼저 해야 한다.

먼저 현재 서브볼륨 리스트를 보면…

$ sudo btrfs subvolume list /
ID 256 gen 371246 top level 5 path etc

ID 257 gen 371255 top level 5 path home
ID 258 gen 371255 top level 5 path var

etc는 왜 별개 서브볼륨으로 되어있는지 모르겠다. /bin 에 새 바이너리들이 들어오면 /etc 도 바뀌니 함께 백업하는 편이 낫다. 그래서 백업을 만들고 삭제했다.

sudo cp -a /etc /etc.bak
sudo btrfs subvolume delete /etc

이제 mv /etc.bak /etc 만 하면 되는데….

$ sudo btrfs subvolume delete /etc
Delete subvolume 256 (no-commit): '//etc'
$ sudo mv /etc.bak /etc
sudo: you do not exist in the passwd database

아… 루트 셸이 열려 있었으면 그냥 mv 하면 될 텐데 나는 루트 비밀번호를 지워서 로그인 불가능하게 만들어서, sudo(8)를 통하지 않고는 루트 셸에 진입할 수가 없다. 이런 경우에는 복구 usb 로 부팅해서 파일시스템을 따로 마운트한 뒤에 mv 해주면 된다. 여하튼, 마이그레이션을 할 때 시스템의 많은 부분이 변하므로 chroot 환경에서 작업하는 편이 안전할 거 같긴 하다.

Install media 로 부팅 후 ssh 연결

Minimal Installation CD를 다운로드 받고, usb를 굽고 부팅한다. 이건 원래 Gentoo를 설치할 때 쓰라고 만든 이미지인데, 이런 작업을 할 때나 시스템의 뭔가가 잘못되서 부팅이 안되는 상황이 오면 유용하게 사용한다.

Figure 1: 라이브 부팅

Figure 1: 라이브 부팅

여기서는 GUI의 가장 큰 축복인 “복사 붙혀넣기"를 사용하지 못한다. 그래서 다른 컴퓨터에서 ssh로 연결해 작업한다.

rc-service sshd start
ip a # 로컬 아이피 확인

확인한 아이피로 다른 컴퓨터에서 로그인하면 된다.

ssh root@<ip>

Install media에 기본으로 들어있는 tmux(1)를 활용하면…

Figure 2: ssh 연결

Figure 2: ssh 연결

이렇게 된다.

mount

mount(8) 하기 앞서 LUKS 볼륨을 열어준다.

cryptsetup open /dev/nvme0n1p2 luks

입력한 후 패스워드를 입력하면 커널의 dm-crypt 모듈이 /dev/mapper/luks 에 평문으로 사용할 수 있는 가상 블록디바이스를 제공해 준다. 해당 디바이스에는 파일시스템이 들어있으므로, mount할 수 있다.

mount /dev/mapper/luks /mnt/gentoo

드디어 우리 /etc 님을 되살릴 수 있다! 그리고 하던 백업도 마저 하자.

mv /etc.bak /etc
btrfs subvolume snapshot -r / /root-$(date --iso)
btrfs subvolume snapshot -r /var /var-$(date --iso)

이렇게 하면 snapshot을 찍은 시점의 서브볼륨 상태가 또다른 서브볼륨에 담긴다. 물론 실제 데이터는 복사되지 않고, btrfs 가 COW (copy-on-write) 이후 정리할 때 스냅샷에 포함되어 있는 청크는 건드리지 않는 방식이다.

참고로 나는 파일시스템 루트를 별개 서브볼륨으로 만들지 않고, 그냥 루트 서브볼륨을 실제 파일시스템 루트로 사용한다. 파일시스템 루트를 @ 같은 이름으로 루트 서브볼륨 아래 서브볼륨으로 만들고 var 같은 것들도 똑같이 @var 같은 이름으로 만들면 루트 서브볼륨 마운트시 @ 랑 @var 만 나와서 관리하기는 더 편하다. 하지만 스냅샷을 찍을 때에 어차피 서브볼륨 경계에서 멈춰서 그것 이외의 이점은 없다. 실제로 쓰고 싶으면 mount -o subvol=@ /, mount -o subvol=@var /var 식으로 해줘야 하기도 하고..

chroot

이제 profile, USE 플래그를 변경하고 업데이트 하기 위해 아까 마운트된 /mnt/gentoo 로 chroot 해 줄 거다. chroot는 하위 파일시스템을 루트로 사용하는 건데, 예를 들어 /mnt/gentoo 로 chroot 하면 /mnt/gentoo/bin 의 바이너리들을 사용하게 되는 거다. 여기서 emerge 하면 /mnt/gentoo prefix 내에 반영된다.

부트 엔트리도 변경할 수 있으니, 마운트하는걸 잊지 말고

mount /dev/nvme0n1p1 /mnt/gentoo/efi

Gentoo Wiki 의 설명을 따라서 chroot 해 주자. 경험상 이건 스크립트로 만들어 놓는 편이 편하다.

chroot=/mnt/gentoo

mount --rbind /dev $chroot/dev
mount --make-rslave $chroot/dev
mount -t proc /proc $chroot/proc
mount --rbind /sys $chroot/sys
mount --make-rslave $chroot/sys
mount --rbind /tmp $chroot/tmp
mount --bind /run $chroot/run

chroot $chroot /bin/bash

이걸 chrt.sh 같은 이름으로 저장해 두고 실행(sh chrt.sh)하면 된다. 실행하면 기존 부트 usb 의 bash가 아니라, /mnt/gentoo/bin/bash 가 실행되고 chroot셸에 들어가게 된다.

. /etc/profile
export PS1="(chroot) $PS1"

환경 변수 설정이고, 아래껀 그냥 chroot 셸이랑 부트 usb 셸이랑 햇갈리지 마라고 해주는 거다. 나가고 싶으면 그냥 Ctrl+D 로 chroot 셸을 종료시키면 된다.

profile, USE 플래그 설정 및 emerge

Figure 3: 프로파일 목록

Figure 3: 프로파일 목록

eselect profile list 를 하면 프로파일 목록이 보인다. 현재 ../desktop/systemd (stable) 로 설정되어 있는데, eselect profile set 3 으로 그냥 ../desktop (stable) 프로파일로 바꿔 준다.

혹시 systemd 관련 설정이 더 있나 살펴봤다.

Figure 4: grep -r systemd /etc/portage

Figure 4: grep -r systemd /etc/portage

package.use/systemd 파일은 그냥 삭제하면 되는데, 역시 systemd-boot 가 문제다. Gentoo에는 systemd에서 몇가지 유틸리티만 떼와서 제공하는 sys-apps/systemd-utils 패키지가 있는데 이걸 사용하면 된다. package.use/systemd-boot 파일의 systemd를 systemd-utils로 바꿔주고, 한번 emerge -pvDN @world 를 돌려봤다. (p는 pretend, D는 deep, N 은 newuse 의 줄임말이다.)

Figure 5: grep -r systemd /etc/portage

Figure 5: grep -r systemd /etc/portage

이런 에러가 뜬다. installkernel 에서 systemd-boot USE 플래그를 사용하려면 systemd USE 플래그도 함께 사용해야 하나 보다. 딱히 enable 해도 문제는 없을 거 같아서 enable 해 주고, 추가로 sys-apps/systemd-utils 에 kernel-install 이라는 USE 플래그를 발견해서 이것도 함께 enable 해 줬다.

sys-apps/systemd-utils boot kernel-install
sys-kernel/installkernel systemd systemd-boot

그리고 다시 emerge -pvDN @world 를 돌려봤는데…

[blocks B      ] sys-apps/sysvinit ("sys-apps/sysvinit" is soft blocking sys-apps/systemd-258.3
)
[blocks B      ] sys-apps/systemd[resolvconf] ("sys-apps/systemd[resolvconf]" is soft blocking
net-dns/openresolv-3.16.5)
[blocks B      ] sys-apps/gentoo-systemd-integration ("sys-apps/gentoo-systemd-integration" is
soft blocking sys-apps/systemd-utils-255.18)
[blocks B      ] sys-apps/systemd ("sys-apps/systemd" is soft blocking sys-auth/elogind-255.17-
r1, sys-apps/systemd-utils-255.18)
[blocks B      ] net-dns/openresolv ("net-dns/openresolv" is soft blocking sys-apps/systemd-258.3

이런 머지 충돌이 떴다. USE 플래그를 바꿨어도 portage는 systemd와 gentoo-systemd-integration이 시스템에 존재한다고 인식하는데, 이게 새 패키지들이 설치되는 걸 막고 있는 거다. 문제는 새 패키지들이 설치되고 의존성이 해결되어야지 둘을 --depclean 으로 제거 가능하다는 건데.. 이럴 때 쓰라고 만들어 놓은 게 --unmerge 다. 이 옵션은 의존성을 무시하고 패키지를 제거한다.

emerge --unmerge gentoo-systemd-integration systemd

하고 나니깐 emerge -pvDN @world 가 성공해서, -p 를 빼고 실제로 컴파일 했는데..

Figure 6: emerge 실패

Figure 6: emerge 실패

더 안좋은 상황이 발생했다. 로그를 보면 dbus-1 이 요구하는 libsystemd 가 없다고 (방금 삭제했으니) 빌드가 실패한 모양이다. sys-apps/dbus 가 +systemd USE 플래그로 컴파일 되어 있어서 libsystemd 를 요구한다고 생각해 먼저 컴파일 하려고 했는데…

Figure 7: emerge -1 sys-apps/dbus 결과

Figure 7: emerge -1 sys-apps/dbus 결과

그냥 +systemd 를 요구하는 모습이다. 여기서 좀 해멨는데, emerge -pvt sys-apps/dbus 로부터 나온 의존성 트리 결과에서 나온 virtual/libudev 를 재컴파일 emerge -1 virtual/libudev 하니 이게 sys-apps/systemd-utils 를 끌고 왔고, 이후에 차례로

  1. emerge -1 sys-apps/dbus
  2. emerge -DN @world

하니깐 컴파일이 성공했다.

지금 생각해 보면 virtual/libudev 가 문제였다기 보다는, 그 과정에서 sys-apps/systemd-utils 가 의존성으로 설치돼서 기존에 sys-apps/systemd 가 제공하던 libsystemd 과 libudev 를 다시 시스템에 공급하게 된 거다. 처음 일어났던 머지 충돌은 systemd-utils랑 systemd둘이 제공하는 파일이 같아서 systemd가 설치되어 있는 상태에서 portage가 systemd-utils 설치를 거부한 거였고…

애초에 더 나은 순서는:

  1. profile 및 USE 플래그 변경
  2. emerge --unmerge systemd gentoo-systemd-integration 으로 기존 패키지 제거
  3. emerge -1 sys-apps/systemd-utils 로 libsystemd 및 libudev 재설치
  4. emerge -DN @world 로 전체 업데이트

이렇게 했으면 머지 충돌과 라이브러리 공백 없이 전환이 가능했을 거 같다.

아직도 왜 7이 발생했는지는 잘 모르겠다…

initramfs 재생성, 재부팅

아직 initramfs 는 systemd를 사용하고 있으므로, 재생성 해야 한다. 마침 emerge 하면서 sys-auth/rtkit 이 CONFIG_RT_GROUP_SHRED 가 disable 되어 있어야 한다는 경고를 줘서 (rtkit은 pipewire가 실시간 스케줄링을 받기 위한 권한을 부여하는 데몬인데, 이 옵션이 켜져있으면 루트가 아닌 사용자가 실시간 스케쥴을 받기 어려워진다.) 바꾸는 김에 재컴파일 하고 make install 로 installkernel 했는데 dracut이 실패했다.

Figure 8: dracut 실패

Figure 8: dracut 실패

그냥 /etc/dracut.conf 에

add_dracutmodules+=" systemd systemd-initrd "

가 되어 있어서 실패했던 거라, 이렇게 설정했다:

add_dracutmodules+=" udev-rules crypt dm rootfs-block "
omit_dracutmodules+=" systemd systemd-initrd systemd-udevd nbd nfs nvmf network "

다시 make install 하니 성공한다… installkernel 로그는 /var/log/installkernel.log 에서 볼 수 있다.

Tue Feb 17 16:46:11 KST 2026	systemd	6.19.0-gentoo	/usr/lib/kernel	bls	dracut	none	/efi	gentoo/6.19.0-gentoo/linux	gentoo/6.19.0-gentoo/initrd	notset

Ctrl+D 를 눌러 chroot 셸에서 나온 후, umount -r /mnt/gentoo 를 통해 라이브 부팅에 마운트했던 파일시스템을 umount 시켜준 후 재부팅한다.

다행히 부팅에 성공했다! systemd-boot 와 systemd-cryptenroll 대체 과정은 다음 포스팅에서 OpenRC 안정화 과정과 같이 올릴 예정이다.