yoncho`s blog

6. Systemd | 초기화 관리자 추가 및 로그 파일 디버깅 본문

기술, 나의 공부를 공유합니다./[Vehicle] Yocto

6. Systemd | 초기화 관리자 추가 및 로그 파일 디버깅

욘초 2024. 6. 24. 20:55

 

이번 장에서는 리눅스 시스템 부팅 시 제일 먼저 생성되고 다른 프로세스를 실행하는 init 역할을 하는

초기화 관리자라는 데몬을 설명하겠다.

 

원래 Yocto에서는 System V init이라는 초기화 관리자를 사용하지만 요즘 리눅스 (2015년 기점)들은 Systemd로 초기화 관리자를 바꾸고 있다. 그래서 우린 Systemd만 공부해볼것이다! 후후! 하하!!

 

추가로 어떤 S/W든 개발 시 디버깅이 매우 중요하다. 이건 개발일을 하는 사람들은 모두 알 것이다.

이번 장에서는 초기화 관리자와 같이 로그 파일로 디버깅하는걸 배워볼 것이다.

 

#목차

1. Systemd 초기화 관리자 추가

2. 로그파일을 이용한 디버깅

3. 최종

 

[1] Systemd 초기화 관리자 추가

앞서 우리는 hello 실행파일을 타깃의 rootfs에 추가하고 실행시켜봤다. 

하지만 실행 시키는 동작은 테스트용이지 정상적인 방법은 아니다. 즉, 사람의 개입없이 S/W적으로 동작해야한다.

리눅스에서 Application들은 초기화 관리자(System V init / Systemd)에 의해 실행된다.

 

Yocto는 기본적으로 System V init이지만 요즘 리눅스 시장을 봤을 땐 Systemd로 변경해서 하겠다. 

 

Systemd란?

System V init과 다르게 Component들을 Unit 단위로 나눈다. Unit은 System V init에서의 Service보다 더 큰 개념이다.

System V init과 Systemd의 큰 차이는 System V init은 직렬 처리만 가능하나 Systemd는 병렬로 처리된다. 

그리고 Systemd는 리눅스 시스템 부팅 시 최소한의 서비스만 실행하고 필요한 서비스는 필요 시 마다 실행한다.

=> Systemd가 System V init보다 빠를 수 밖에 없다.

 

Systemd는 복잡하고 많은 기능을 가지고 있다. 우리는 여기서 전부다 공부하지 않고 필요한 Service만 공부할 것이고

아래 그림은 Systemd의 구조이다.

 

 

Systemd에서 'd'는 데몬을 의미하며 데몬은 백그라운드에서 실행되는 프로세스다.

즉, 백그라운드에서 항상 켜져있고 필요할 때마다 즉각 대응할 수 있는 것이다.

데몬은 부모 프로세스를 가지지 않으며 init 바로 아래 대부분 위치한다. 

=> 대부분의 프로세스가 시스템의 첫 프로세스인  Systemd의 하위 프로세스가 되는 것이다.

=> 데몬들을 실행시키고 관리해주는 데몬이 Systemd이다.

 

기존 리눅스에서 init데몬을 Systemd가 대체했기 때문에 init과 동일하게 PID '1'을 갖고 부모 프로세스가 없기 때문에 

PPID 또한 '1' 이다.

 

Systemd는 리소스를 Unit 단위로 관리하는데 아래 구성과 같다.

[ .service / .socket / .device / .mount / .automount / .swap / .target / .path / .timer / .snapshot / .slice / .scope ]

우리는 여기서 .service Unit만 다루겠다.

I. .service Unit File

.service Unit File은 Service나 Application을 서버상에서 어떻게 관리할지 기술한다.

=> 서비스 시작, 중지, 트리거, 종속성 파악 등에 대한 정보를 가진다.

=> 일반적으로 /lib/systemd/system/ 디렉터리 아래 복사해 동작하도록 한다.

 

Unit File 내에서는 'Unit', 'Service', 'Install'과 같은 지시어를 Section이라 한다. 

기본적인 Section들에 대해 설명하겠다.

 

#Unit File Section 종류

I. Unit Section

최초로 동작하는 Section, 다른 Unit간의 관계를 설정

1.Description : unit 설명

2.Documentation : 서비스 문서 uri, main page 제공, $ systemctl status <service_name> 으로 볼 수 있다.

3.After : 현재 Unit보다 먼저 실행되어야하는 Unit들

4.Before : 현재 Unit보다 나중에 실행되어야하는 Unit들

5.Requires : 현재 Unit이 의존하는 모든 Unit들 (단, 연관된 모든 Unit이 정상 동작해야함)

6.Wants : 현재 Unit이 의존하는 모든 Unit들 (단, 연관된 모든 Unit이 정상 동작 안해도됨)

7.BindsTo : 여기에 나열된 Unit들이 종료되면 현재 Unit도 종료된다.

 

II. Service Section

.service Unit File에만 적용되는 Section이다.

1. Type : Service가 어떤 형태로 동작하는지 설정, 아래 값 中 하나 선택

- (a) simple : Type을 별도로 기술 하지 않았다면 기본값으로 동작한다. ExecStart가 같이 존재해야하며 해당 Unit이 시작되면 systemd는  유닛의 시작이 완료되었다고 판단

- (b) forking : 서비스가 자식 프로세스를 생성할 때 사용, PIDFile값에 PID파일을 지정해야한다, 부모 프로세스 추적에 필요하다.

- (c) oneshot : 서비스 시작 시 상태를 activating으로 변경, 끝날때 까지 기다린다. 프로세스가 오래 실행될 경우 다음 동작을 못 하기 때문에 주의해야한다.

- (d) dbus : DBUS에 지정된 BusName이 준비될 때까지 기다리며, DBUS가 준비되면 서비스 실행된다.

- (e) notify : 서비스가 startup이 끝날 때 systemd에 시그널을 보내고, 이 시그널을 받은 systemd는 다음 Unit을 실행시킨다.

- (f) idle : 모든 서비스가 실행된 이후 실행된다.

- (g) ExecStart : 서비스 시작하기 위한 전체 경로, 서비스 시작전 이 명령어를 실행해야한다.

- (h) ExecStartPre : 서비스 시작되기 전 실행할 명령어 설정

- (i) ExecStartPost : 서비스 시작 후 실행할 명령어 

- (j) Restart : 서비스 종료 시 자동으로 재시작, 값으로 [ always, on-success, on-failure, on-abnormal, on-abort, on-watchdotg ] 中 사용 가능

2. TimeoutStartSec : 서비스 시작 시 대기하는 시간

이외 다양한 필드값 존재한다.

 

III. Install Section

Unit이 활성화/비활성화 될 때 Unit의 행동을 정의한다.

- (a) WantedBy : '$ systemctl enable' 명령어로 Unit 등록할 때 등록에 필요한 Unit 지정

- (b) Also : '$ systemctl enable', '$ systemctl disable' 명령어로 Unit을 등록 및 해제 시 함께 enable, disable할 Unit 지정

- (c) Alias : Unit 별칭 지정

 

 

#Systemd를 통한 Hello Application 실행

I. poky/meta-hello/recipes-hello/ 디렉터리 아래 hello.bb 파일을 아래와 같이 수정

DESCRIPTION = "Simple Hello World"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://COPYING;md5=fc535da43eabb329160c3db56b2d9e7e"

SRC_URI = "file://hello.c"
SRC_URI_append = " file://COPYING"
SRC_URI_append = " file://hello.service"			-> (1)
inherit systemd										-> (2)

S = "${WORKDIR}"
SYSTEMD_SERVICE_${PN} = "hello.service"				-> (3)
SYSTEMD_AUTO_ENABLE = "enable"						-> (4)

do_compile(){
        ${CC} hello.c ${LDFLAGS} -o hello
}

do_install(){
        install -d ${D}${bindir}
        install -m 0755 hello ${D}${bindir}
        install -d ${D}${systemd_unitdir}/system		-> (5)
        install -m 0644 hello.service ${D}${systemd_unitdir}/system			-> (6)
}

FILESEXTRAPATHS_prepend := "${THISDIR}/source:"
FILE_${PN} += "${bindir}/hello"
FILE_${PN} += "${systemd_unitdir}/system/hello.service"	  -> (7)

 

(1)은 syustemd가 실행할 hello.service 파일을 추가하는 코드

(2) systemd.bbclass 클래스 파일을 상속 받는 코드

(3),(4) systemd.bbclass 클래스 파일에 정의된 변수

여기서 SYSTEMD_SERVICE 변수는 우리가 만든 '.service' 파일 등록 시 필요하다.

SYSTEMD_AUTO_ENABLE 변수는 리눅스 시스템 부팅 시 Systemd에 등록한 서비스 파일을 자동으로 실행시키는 옵션이다.

(5),(6)은 '.service' 파일을 /lib/systemd/system/ 디렉터리 아래에 위치 시키기 위한 코드

(7)은 패키지 관련 내용을 나중에 패키지 관련 포스트에서 다룰 것이다.

 

#Yocto 사전 정의된 Systemd 관련 디렉터리 지정 변수

variable name to_directory
systemd_unitdir /lib/systemd
systemd_system_unitdir /lib/systemd/system
systemd_user_unitdir /usr/lib/systemd/system

 

 

II. poky/meta-hello/recipes-hello/source/ 디렉터리 아래 hello.service 파일 생성 및 아래와 같이 작업

[Unit]
Description=HELLO WORLD startup script

[Service]
ExecStart=/usr/bin/hello

[Install]
WantedBy=multi-user.target

 

III. poky/meta-hello/conf/ 디렉터리 아래 layer.con 파일을 아래와 같이 수정

Yocto의 기본 초기화 관리자는 System V init이기 때문에 layer.conf 파일을 수정해 Systemd로 변경해줘야한다.

BBPATH                          =. "${LAYERDIR}:"
BBFILES                         += "${LAYERDIR}/recipes*/*.bb \
                                    ${LAYERDIR}/recipes*/*/*.bbappend"
BBFILE_COLLECTIONS              += "hello"
BBFILE_PATTERN_hello            = "^${LAYERDIR}/"
BBFILE_PRIORITY_hello           = "10"
LAYERSERIES_COMPAT_hello        = "${LAYERSERIES_COMPAT_core}"
GLOBAL_VAR                      = "global var"

#추가
DISTRO_FEATURES_append = " systemd"
DISTRO_FEATURES_remove = "sysvinit"
VIRTUAL-RUNTIME_init_manager = "systemd"
VIRTUAL-RUNTIME_initscripts = "systemd-compat-units"
DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"
VIRTUAL-RUNTIME_initscript = "systemd-compat-units"

위에서 VIRTUAL-RUNTIME을 접두어로 가지는 변수는 실행 시간 의존성을 나타내는 변수라고도 한다.

하지만 VIRTUAL-RUNTIME_<name>은 단순 변수로 취급하며 여기서 <name>은 metadata file내 정의되고

VIRTUAL-RUNTIME_<name> 변수는 패키지를 값으로 가진다.

VIRTUAL-RUNTIME_init_manager 변수를 추적 (bitbake-getvar 명령어)해보면 내용 中 RDEPENDS 지시어를 찾을 수 있는데. 이는 실행 시간 의존성을 나타내는 지시어이다. 자세히는 추후 패키지 그룹과 의존성 포스트에서 설명하겠다.

 

IV. poky/meta-hello/recipes-hello/source/ 디렉터리 아래 hello.c에서 sleep(1) 코드 제거 - qemu 실행에 방해된다.

#include <stdio.h>
#include <unistd.h>

int main(){

        int i = 0;
        while (i < 10){
                printf("hello world!\n");
                //sleep(1);
                i++;
        }
        return 0;
}

 

V. bitbake 실행 파일 fetch 와 rootfs 재빌드 그리고 qemu 실행

#fetch
bitbake hello -C fetch

#rebuilt for rootfs
bitbake core-image-minimal -C rootfs

#qemu run
runqemu core-image-minimal nographic

 

VI. qemu에서 hello 실행 로그 확인 

journalctl -u hello -f &
systemctl start hello
systemctl stop hello

'journalctl -u <name> -f &' 명령어는 <name> 실행 파일의 실시간 로그를 백그라운드에서 돌면서 출력하라는 것이다.

 

결과는 아래와 같다.

root@qemux86-64:~# journalctl -u hello -f &
root@qemux86-64:~# -- Logs begin at Mon 2024-06-24 12:44:32 UTC. --
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 hello[136]: hello world!
Jun 24 12:44:42 qemux86-64 systemd[1]: hello.service: Succeeded.

 

 

앞 단계에서 hello.bb에 아래와 같이 작성했다. 

SYSTEMD_SERVICE_${PN} = "hello.service"

SYSTEMD_AUTO_ENABLE = "enable" 

 

이 변수들을 systemd.bbclass 클래스 파일에 정의되어있다. 

systemd.bbclass 클래스 파일은 poky/meta/classes/ 디렉터리 아래 위치하고 있으며 

파일 내용 中 'systemd_postinst()' 함수가 있는데.

이 함수는 pkg_postinst라는 스크립트다. pkg_postinst는 rootfs 생성 시 실행되거나 타깃이 처음 부팅될 때 실행되는 스크립트를 넣을 때 사용된다. 추후 포스트에서 다루겠지만  단순히 systemd_postinst는 타깃이 처음 부팅될 때 실행되는 스크립트라고 알고 있자.

 

systemd_postinst() 함수 내용 中 'systemctl preset xxxx' 명령어를 확인할 수 있는데. 사전 설정 파일 (preset)은 Unit을 활성/ 비활성 할지 설정하는데 사용된다.

=> systemctl enable/ disable과 동일한 것이다.

 

실제로 systemd.bbclass 파일 내용 中 SYSTEMD_SERVICE 변수 값 기반에 xx-hello.preset이라는 사전 설정 파일이 

do_install Task 수행 완료 후 생성된걸 확인할 수 있다. 그리고 생성된 사전 설정 파일은 systemd_postinst() 함수에서 실행되는 구조로 되어있고 타깃이 처음 부팅됐을때 실행된다.

tmp/work/qemux86_64-poky-linux/core-image-minimal/1.0-r0/rootfs/lib/systemd/system-preset/ 디렉터리 아래 가면

98-hello.preset이 생성되어있는걸 확인할 수 있다.

해당 파일의 내용은 'enable hello.service' 이다. 

 

 

 

[2] 로그파일을 이용한 디버깅

bitbake는 빌드를 진행하면서 metadata에 추가한 디버깅 정보나 모든 실행 명령어의 결과와 오류 메시지를 기록한다.

각 Task 별, 전체 Task에 대한 로그 또한 기록해둔다.

로그 파일 생성 위치와 어떻게 이용해야하는지 공부해보자.

 

I. 쿠커의 로그파일 생성

bitbake는 client-server Application이다. 

bitbake 실행 시 백그라운드 서버와 쿠커라고 불리는 백엔드 프로세스가 돌아가고

클라이언트 사용자를 위한 프론트엔드 프로세스를 시작한다. 

 

**쿠커 백엔드 프로세스는 실제 빌드와 모든 metadata file 처리 수행 및 multi thread 실행을 한다. 

백엔드와 프론트엔드는 IPC(Inter Process Communication)을 이용해 정보 교환이 이루어진다.

클라이언트 사용자 인터페이스는 빌드 출력, 상태 및 진행 상황 로깅

bitbake 이벤트 모듈을 통해 빌드 작업의 이벤트 수신하기 위한 메커니즘 제공

기본적인 사용자 인터페이스는 bitbake console command line 인터페이스, Knotty이다.

 

아래 그림은 bitbake 백엔드/프론트엔드 구조다

여기서 Yocto Cooker는 무엇일까??

bitbake 빌드가 시작되면 BBCooker 클래스가 시작된다. (poky/bitbake/lib/bb/cooker.py)

bitbake를 실행할 때 대부분 사용자가 사용하는 기본 서버는 bitbake의 프로스세 서버다.

서버를 불러온 후 bitbake 실행 파일이 Cooker를 실행한다. Cooker는 bitbake의 핵심이며 

Poky빌드 중 발생하는 일 대부분이 처리되는 곳이다.

 

**Cooker는 metadata 구분 분석(parsing) 관리와 종속성 및 Task Chain 생성을 시작하며

빌드를 관리한다. 

bitbake 실행파일이 Cooker를 실행하면 우선 Cooker는 bitbake data repository를 초기화 한 뒤 

Poky의 모든 metadata를 parsing한다 그 다음 runqueue 객체를 만들고 빌드를 시작한다.

 

Cooker에 의해 생성된 Log 파일을 보면 아래와 같다.

NOTE: Resolving any missing task queue dependencies

Build Configuration:
BB_VERSION           = "1.46.0"
BUILD_SYS            = "x86_64-linux"
NATIVELSBSTRING      = "universal"
TARGET_SYS           = "x86_64-poky-linux"
MACHINE              = "qemux86-64"
DISTRO               = "poky"
DISTRO_VERSION       = "3.1.33"
TUNE_FEATURES        = "m64 core2"
TARGET_FPU           = ""
meta
meta-poky
meta-yocto-bsp
meta-hello           = "dunfell:63d05fc061006bf1a88630d6d91cdc76ea33fbf2"
....
...
..

BB_VERSION : bitbake 버전

BUILD_SYS : 호스트 빌드 시스템

NATIVELSBSTRING : 호스트 배포자를 식별하는 문자열 

=> ubuntu-18.04가 아닌 universal인 이유는 네이티브 레시피가 호스트 pc glibc가 아닌 Yocto 공통 glibc를 사용해 빌드되었음을 나타낸다.

MACHINE : bitbake가 빌드하기 위한 타깃 머신 이름

DISTRO : 타깃 시스템의 배포 이름

 

 

II. Task Log File, Task Script File ..and Debugging Tip !

bitbake는 빌드 Task 수행하면서 결과에 대해 로그 파일, 스크립트 파일을 생성하는데

생성 경로는 T 변수가 가지고 있고 기본적으로 ${WORKDIR}/temp/ 디렉터리 아래 생성된다.

 

Task Log File은 'log.do_<task_name>.<pid>' 형식을 가지고 있으며 <pid>는 bitbake가 실행한 task process id 이다.

동일한 Task에 대해 여러번 실행 할 경우 할 때마다 해당 id값은 변한다.

로그파일 中 'log.do_<task_name> 은 현재 수행되는 Task 혹은 최근에 수행된 Task의 Log File을 Link한 것이다.

 

bitbake는 수행 시 실행하는 명령어를 Task Script File로 만든다.

**Task Script File은 'run.do_<task_name>.<pid>' 형식을 가지며 기타 내용은 Log File과 동일하다.

Task Script File이 중요한 이유는 디버깅 시 해당 Script 파일을 이요한다.

 

특히, 로그 파일 중에서 'log.task_order' 파일이 제일 중요하다. 이 파일에는 최근 실행된 프로세스 id와 함께 실행된 Task가 순차적으로 어떤 로그파일로 구성되는지 알 수 있다. 이를 참조해 Task 별 로그 파일을 파악할 수 있다.

 

Task 별 로그 파일을 열어보면 Task 수행 시 발생된 로그들이 모두 기록되어있다. 특히, 에러가 발생했을 때 어떤 이유로 에러가 발생했는지 파악하기 편하다.

 

다음으로 중요한 파일은 run.do_install 파일이다.

**여기서 젤 중요한 디버깅 팁이 있다. 

run.do_<task_name> 파일을 Task Run Script File이라고도 부른다. 

이 파일은 스크립트로 구성되어있어 바로 실행이 가능하다. 

=> 이말인 즉, 우리가 스크립트 파일 안에 디버깅용 코드를 넣고 돌려볼 수 있다는 것!!!!  ㅇ0ㅇ!!

=> 디버깅 코드를 넣고 ' ./run.do_<task_nam>' 명령어를 실행하면 디버깅 코드가 함께 동작하는걸 확인할 수 있다.

 

 

[3] 최종

본 글에서는 Yocto 기본 초기화 관리자는 System V init이라는 것과 Linux 초기화 관리자는 2015년을 기점으로 Systemd로 변하니 우리도 Yocto에서 초기화 관리자를 Systemd로 설정하는 방법을 배웠다. 

그리고 Bitbake의 구조 (백엔드/ 프론트엔드)에서 Cooker의 역할과 Cooker에 의해 생성되는 로그 파일을 알 수 있었다.

그리고 최종적으로 해당 로그 파일에서 어떻게 디버깅해야하는지도 알았다.

현업에서도 특히, 스크립트로 구성된 태스크 실행 스크립트 파일에 디버깅용 코드를 넣고 에러 파악을 한다는 것! 

이것이 유익했다.

Comments