
드라이버의 개요 두 번째 편이다.
모듈은 드라이버를 담아 주는 개념이라고 생각하면 된다.
이번 글에서는 커널과 모듈의 빌드부터 시작해서
모듈의 틀을 만들어 보는 내용까지가 들어 있다.
Application / Kernel 빌드 과정의 비교
| 단계 | Application Build | Kernel Build |
|---|---|---|
| Fetch | FTP, GitHub 등에서 소스 다운로드 | linux-6.1.21.tar.gz 다운로드 (kernel.org) |
| Unpack | tar -xvf ... 압축 해제 | tar -xvf ... 압축 해제 |
| Patch | 필요 시 패치 파일 적용 | 벤더(Vendor) 패치 적용 등 |
| Configure | ./configure (환경 자동 감지) | make menuconfig (수동/상세 설정 필수), 타겟 보드에 맞는 드라이버/옵션 선택 |
| Compile | make (오브젝트 생성) | make (커널 이미지 및 모듈 생성) |
| Link | 라이브러리 링크 | vmlinux 생성 (Static Linking) |
| Install | make install (/usr/bin 등 복사) | modules_install, headers_install, 이미지 복사 |
커널 설정의 개요
커널 빌드 전 어떤 기능을 포함할지 결정하는 단계
필수 패키지 설치
root@host:~/linux-6.1.21# apt install libncurses-dev flex bison
설정 도구 실행
# make menuconfig 명령어를 사용
메뉴 파일(Kconfig)를 불러들여 메뉴를 표시하고, 터미널 기반의 GUI 메뉴로 옵션 선택
root@host:~/linux-6.1.21# export KERNEL=kernel8
root@host:~/linux-6.1.21# export ARCH=arm64
root@host:~/linux-6.1.21# make menuconfig
참고 이미지


옵션 선택
<*> (Built-in) : 커널 이미지(zImage)에 영구적으로 포함
<M> (Module) : 별도의 모듈 파일(.ko)로 빌드되어 필요 시 로딩
< > (Excluded) : 빌드에서 제외
설정 파일 관리 (.config)
menuconfig 에서 설정한 내용은 .config 파일에 저장됨
설정 파일 재활용을 위해 미리 만들어둔 설정 파일인 _defconfig 를 이용 (위치 : arch/arm64/configs/ )
.config를 저장하거나 불러와 이전에 지정했던 상태로 세팅 가능
root@host:~/linux-6.1.21# export ARCH=arm
root@host:~/linux-6.1.21# make bcm2835_defconfig
HOSTCC scripts/kconfig/conf.o
HOSTLD scripts/kconfig/conf
#
# configuration written to .config
#
# 이 명령은 구형 설정이므로 RPi 4 64bit 환경에는 부적합
KConfig (Kernel Configuration)
커널의 기능과 드라이버 옵션을 정의하는 설정 명세 파일
# make menuconfig 를 실행했을 때 보여지는 메뉴 구조와 옵션의 타입, 의존성 등을 결정
사용자가 선택한 값은 .config 파일에 CONFIG_XXX=y 또는 m 형태로 저장
Kbuild (Kernel Build System)
리눅스 커널을 빌드하기 위한 규칙과 스크립트 체계
.config 에 저장된 설정값(CONFIG_XXX)을 참조하여 Makefile 을 제어함
핵심 규칙
obj-y : 커널 이미지(zImage)에 포함, Built-in
obj-m : 모듈 파일(.ko)로 생성
obj- (공백) : 빌드에서 제외
동작 예시 : DHT11 드라이버
.config 파일 확인
CONFIG_DHT11=m (모듈로 설정됨)
Makefile 확인:
makefileobj-$(CONFIG_DHT11) += dht11.o
# 위 변수가 'm'으로 치환되어 -> obj-m += dht11.o 가 됨
결과
obj-m 으로 지정되어 있으므로 dht11.ko 파일 생성
# make 또는 # make -jN 를 실행하면 여러 파일들이 생성됨커널 이미지
vmlinux , zImage 또는 uImage 또는 bzImage 를 생성
커널 모듈
.ko : 각 드라이버의 컴파일된 모듈 파일들
디바이스 트리 (Device Tree)
보드(MCU, RAM, ROM, Devices를 포함한 SoC)의 하드웨어 스펙을 커널에 알려주는 데이터 구조
소스 파일
.dts : Source, 사람이 읽을 수 있는 소스 파일로 C언어의 .c 와 유사
.dtsi : Include, 공통 내용을 담은 헤더로 C언의 .h 와 유사
컴파일러
dtc : Device Tree Compiler 사용
실행 파일
.dtb : Blob, 컴파일된 바이너리로 커널이 읽는 파일
.dtbo : Overlay, 런타임에 동적으로 적용하는 오버레이 바이너리
# make -jN
멀티코어 상에서 빠른 빌드에 사용되는 명령어
(예 : 코어가 4개일 때 # make -j4 )
# make zImage / # make uImage
압축된 커널 이미지 혹은 U-Boot용 이미지를 생성할 때 사용하는 명령어
모듈 빌드 과정
커널 소스/헤더 준비 → 빌드 환경 설정 → 빌드 → 설치
커널 소스/헤더 준비 (Build Environment)
Host 개발 (Cross Compile)
PC에서 빌드하여 타겟 보드로 옮기는 방식으로, 전체 커널 소스 필요 (예 : /work/linux )
Native 개발 (On Board)
라즈베리파이 등 보드에서 직접 빌드하는 방식
/lib/modules/$(uname -r)/build : 모듈 빌드에 필요한 최소한의 헤더가 링크된 경로
모듈 빌드 환경 설정 (modules_prepare)
호스트 개발 시 커널 소스를 다운로드 후 환경 설정이 필요함
모듈을 빌드하기 전에 기본적인 구성과 버전 체크 등을 수행
명령어 : # make modules_prepare
모듈 빌드하기 (# make modules)
KDIR 에 있는 커널의 Makefile 규칙을 빌려와서 M 디렉토리에 있는 소스 컴파일
# -C: 커널 소스 경로로 이동
# M=: 현재 모듈 소스 경로를 알려줌 (구식 SUBDIRS 대신 사용)
root@host:~/linux-6.1.21# make -C $(KDIR) M=$(PWD) modules
모듈 설치하기 (# make modules_install)
설치된 모듈이 필요한 시점에 커널이 모듈을 찾아 로드할 수 있음
생성한 .ko 모듈 파일을 시스템 표준 경로(/lib/modules/커널버전/ )에 복사
modules.dep 파일을 갱신하여 modprobe 가 의존성을 파악할 수 있게 함
INSTALL_MOD_PATH : Cross Compile 시에는 모듈 설치 위치를 지정해 주어야 함
modprobe : 스마트한 로더
/lib/modules/ 경로를 참고하여 모듈과 의존성까지 자동으로 로드 및 제거
전통적인 명령어 : 수동 제어
# insmod file.ko : 현재 디렉토리의 파일을 커널에 로드, 의존성 해결은 하지 않음
# lsmod : 현재 커널에 로드된 모듈 목록 확인
# rmmod <name> : 모듈 언로드(제거)
커널 로깅(Logging)
printk() 함수를 사용
User Space의 printf 에 대응되는 커널 함수
커널 메시지 버퍼에 로그를 기록함
로그 보는 방법
/var/log/messages : 로그가 저장되어 있는 파일
실시간 로그 확인
dmesg : 커널 부팅부터 현재까지의 로그 출력
dmesg -c : 로그를 출력한 뒤 버퍼를 비워 정리, 테스트 반복 시 유용
커널 모듈(Kernel Module)
리눅스 커널에 동적으로 적재되거나 제거할 수 있는 코드 덩어리
.ko 파일로 만들어져 있으며, 디바이스 드라이버뿐만 아니라 커널의 기능을 확장하는 모든 코드
디바이스 드라이버(Device Driver)
특정 하드웨어를 제어하기 위해 만들어진 소프트웨어
하드웨어의 복잡한 동작을 감추고 커널이나 유저에게 표준화된 인터페이스를 제공
모듈과 드라이버의 관계
모듈로 만든 드라이버
필요할 때 insmod 로 커널에 적재할 수 있는 드라이버
개발과 배포가 유연한 편으로, 대부분의 드라이버가 이 방식을 사용함
모듈이 아닌 드라이버(Built-in)
make zImage 할 때 커널에 포함되어 나오는 드라이버
시스템 타이머 등 부팅에 필수적인 장치는 모듈로 만들 수 없음
드라이버가 아닌 모듈
방화벽, 파일시스템 등 하드웨어 제어와 상관없는 순수 소프트웨어 기능
Makefile
# Makefile
ifdef ARCH # Cross 빌드
KDIR=/work/linux
else # Native 빌드
KDIR=/lib/modules/$(shell uname -r)/build
endif
all: dev
# 커널 소스 디렉토리로 이동해서 Makefile을 실행, 빌드할 소스는 PWD에 있음
dev:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
@$(MAKE) -C $(KDIR) M=$(PWD) clean
모듈 빌드 예제 : work_drivers/exercise/03/01
모듈 담당 헤더
<linux/kernel.h> : printk 등 커널의 기본 함수 포함
<linux/module.h> : 모듈 관련 매크로와 struct module 이 정의된 파일
struct module
커널 내에서 모듈을 관리하기 위한 핵심 구조체
모듈의 상태, 모듈 리스트, 중복되지 않는 모듈명, 외부 참조 가능한 심볼 변수 테이블, 모듈 초기화/해제 함수 포인터, 모듈 코드와 데이터가 할당된 메모리 주소 등을 포함
모듈의 기본 형태
초기화 함수/해제 함수
모듈은 항상 초기화와 종료 함수 쌍으로 구성되며, 커널 매크로를 통해 등록됨
각각 모듈 로드 시와 모듈 제거 시에 실행됨
초기화 함수 예시 : static int my_init(void);
초기화 해제 함수 예시 : static int my_exit(void);
등록 매크로 <linux/init.h>
module_init(my_init) : 커널에게 초기화 함수를 알림
module_exit(my_exit) : 커널에게 종료 함수를 알림
내부 호출 흐름
module_init -> __initcall(fn) -> device_initcall(fn)
module_exit -> __exitcall(fn)
Helper 매크로 (커널 3.x 이후)
module_platform_driver(my_driver) : 플랫폼 드라이버용
module_i2c_driver(...), module_spi_driver(...)
모듈 라이선스(Module License)
MODULE_LICENSE() : 모듈에 적용되는 라이선스 정책을 커널에 명시하는 매크로
라이선스는 무조건 필요하고, 없으면 경고 메시지 발생 및 기능에 제약
예 : “GPL” 라이센스 명시하려면 MODULE_LICENSE(”GPL”)
인트리 빌드 (In-Tree Build)
커널 소스 트리 내부(drivers/ 등)에 모듈 소스를 두고 커널과 함께 빌드하는 방식
커널 소스 준비
빌드할 전체 커널 소스 필요
소스 위치
drivers/char/ 등 적절한 디렉토리에 드라이버 소스 파일(.c) 넣기
Makefile 수정
해당 디렉토리의 Makefile에 빌드 객체 추가하기
obj-m += xxx.o : 무조건 모듈로 빌드
obj-$(CONFIG_XXX_POLE) += xxx.o : Kconfig 설정 옵션에 맞춰서 빌드
Make
커널 최상위에서 # make 를 실행해 빌드하기
CONFIG_ 가 y 면 zImage에 포함되고, m이면 xxx.ko 파일이 생성됨
아웃오브트리 빌드 (Out-of-Tree)
커널 소스 트리가 아닌 외부(사용자 홈 디렉토리 등)에서 독립적으로 모듈을 빌드하는 방식
커널 소스 준비
빌드된 커널 헤더나 소스 필요
소스 위치
임의의 작업 디렉토리에 드라이버 소스 파일 넣기
Makefile 작성
obj-m := xxx.o # 단일 소스일 때
xxx-objs := xxx.o yyy.o # 여러 소스 파일로 하나의 모듈(xxx.ko)을 만들 때
Make
# make -C <KernelLocation> M=$(PWD) modules
해당 디렉토리에서 make 를 통해 .ko 파일 생성
-C <KernelLocation> : 커널 소스 트리가 있는 위치 지정
M=$(PWD) : 드라이버 소스가 있는 곳을 현재 작업 디렉토리로 지정, SUBDIRS=$(PWD) 와 같음
모듈 설치
# make modules_install : 빌드 후 명령을 통해 모듈 설치
생성된 .ko 파일들을 `/lib/modules/$(uname -r)/ 아래의 적절한 경로로 복사
모듈 정의 파일인 modules.dep 을 생성/갱신하여 모듈 리스트에 등록
모듈의 메시지 출력 : printk
커널 모듈은 터미널(stdout)에 직접 출력이 불가능하므로 커널 메시지 버퍼에 기록
<linux/kernel.h>에 정의되어 있으며, 정확한 위치는 /kernel/printk/printk.c
printf()와 사용법이 유사하지만 실수 출력을 위한 부동 소수점 포맷 %f, %e 를 지원하지 않음
/var/log/messages 등의 로그 파일에 내용 저장
출력된 로그 확인하기
# dmesg : Display Message, 실시간/전체 로그 확인 명령어
# cat /var/log/messages : 파일을 통해 확인, root 권한 필요
로그 레벨(Log Level)
로그 레벨은 메시지의 중요도를 나타내는 레벨
숫자가 작을수록 우선 순위 높
이 레벨에 따라 printk 는 콘솔에 즉시 내용을 출력할지, 버퍼에만 기록할지 결정
printk의 로그 레벨보다 콘솔의 로그 레벨이 높아야 메시지 확인 가능
로그 레벨 지정 방법
문자열 앞에 매크로 상수를 붙여 사용
예 : printk(KERN_WARNING "Warning message\n");
로그 레벨 우선순위
| 매크로 상수 | 숫자 | 의미 |
|---|---|---|
KERN_EMERG | <0> | 시스템이 동작 불가능한 상태 |
KERN_ALERT | <1> | 즉각적인 조치가 필요한 상태 |
KERN_CRIT | <2> | 치명적인(Critical) 오류 |
KERN_ERR | <3> | 일반적인 오류 상태 |
KERN_WARNING | <4> | 경고 메시지 |
KERN_NOTICE | <5> | 정상이지만 중요한 알림 |
KERN_INFO | <6> | 일반 정보 메시지 |
KERN_DEBUG | <7> | 디버깅용 메시지 |
콘솔 출력 제어 (/proc/sys/kernel/printk)
커널은 현재 설정된 콘솔 로그 레벨보다 중요한 메시지만 콘솔 화면에 출력
cat /proc/sys/kernel/printk
# 출력 예: 7 4 1 7
# (1) (2) (3) (4)
Console Log level
현재 콘솔 출력 커트라인, 이 값보다 높은 우선순위 메시지(값이 작은 것)만 출력
Default Message Log Level
printk 에 별도의 로그 레벨을 설정하지 않을 경우 적용되는 기본값
Minimum Console Log Level
Console Log Level을 이 값 이하로 내릴 수 없음
Default Console Log Level
Console Log Level의 부팅 시 기본값
# 레벨을 8로 높여서 모든(0~7) 로그가 보이게 함
# ssh 터미널(pts)는 콘솔이 아니므로 이 설정과 무관하게 dmesg로 확인해야 함
# 직접 보려면 라즈베리파이에 모니터 연결 필요
echo 8 > /proc/sys/kernel/printk모듈 출력 예제 /work_drivers/exercise/03/02
pr_ 매크로pr_ 매크로
커널 출력을 할 때 printk 에 로그 레벨을 매번 입력하는 번거로움이 존재함
이런 불편함을 줄이기 위해 Wrapper로 만든 pr_ 매크로 시리즈를 제공
pr_info("Module loaded successfully\n"); 형태로 사용
pr_ 매크로의 장점
로그 레벨이 매크로 이름에 포함되어 있어 KERN_INFO 등의 상수를 쓰는 것보다 직관적, 가독성 향상
로그 레벨을 수동으로 입력할 필요가 없어 오류 감소
현재 리눅스 커널 개발에서 권장되는 표준 로깅 방식
pr_ 매크로 종류
| 매크로 함수 | 대응되는 printk 레벨 | 용도 및 의미 |
|---|---|---|
pr_emerg() | KERN_EMERG | 시스템이 붕괴 직전인 긴급 상황 |
pr_alert() | KERN_ALERT | 즉각적인 조치 필요 |
pr_crit() | KERN_CRIT | 치명적인(Critical) 하드웨어/소프트웨어 오류 |
pr_err() | KERN_ERR | 드라이버 동작 실패 등 복구 불가능한 에러 |
pr_warn() | KERN_WARNING | 잠재적 문제 경고 (동작은 가능) |
pr_notice() | KERN_NOTICE | 정상적이지만 중요한 알림 |
pr_info() | KERN_INFO | 일반적인 정보 및 상태 메시지 |
pr_debug() | KERN_DEBUG | 개발용 디버깅 로그 (기본적으로 출력 안 됨) |
모듈 등록/해제 매크로
module_init(func)
모듈 로드(insmod) 시 실행될 초기화 함수 등록
module_exit(func)
모듈 제거(rmmod) 시 실행될 종료 함수 등록
module_platform_driver(driver)
플랫폼 드라이버를 개발할 때, init / exit 함수 등록 과정을 한 줄로 축약해 주는 편리한 매크로
모듈 라이선스 매크로와 모듈 정보
modinfo <filename.ko> : 모듈의 정보를 볼 수 있는 명령어
MODULE_LICENSE("License") : 모듈에 적용되는 라이센스 명기
MODULE_AUTHOR("Name") : 모듈 제작자의 이름(정보) 명기
MODULT_DESCRIPTION("Desc") : 모듈 설명을 명기
모듈 라이선스 작성 예제 03/03
모듈에 매개변수 전달하기
모듈을 로드할 때 상황에 맞는 매개변수를 전달받아 드라이버의 동작을 동적으로 변경하는 기능
하드웨어 설정값(NIC의 I/O 주소, IRQ값)을 하드코딩하지 않고 유연하게 설정할 수 있음
매개변수 전달 방법
모듈 로드 시 명령어 뒤에 변수명=값 형태로 전달하고, 배열은 쉼표로 구분하여 지정
# 단일 값 전달
insmod my_module.ko myint=100 mystring="Hello"
# 배열 값 전달
insmod my_module.ko myarray=1,2,3
매개변수 전달 코드 구현
매개변수 전달을 위해서는 <linux/moduleparam.h>가 필요함
먼저 전역 변수를 선언하고 module_param 매크로로 연결
module_param(name, type, perm) : 기본 매크로
module_param_array(name, type, &count_var, perm) : 배열 매크로
module_param 전달 항목
name : 매개변수로 사용할 변수 이름
type : 변수의 데이터 타입
&count_var : 실제로 입력된 개수를 저장할 변수의 주소 (없으면 NULL)
perm : sysfs 접근 권한, 0644등의 일반적인 8진수 형식 또는 S_ifoo 정의를 OR 연산
(예 : S_IRUGO | S_IWUSR (0444 | 0644) : 모든 사용자는 읽기, 사용자는 쓰기 가능)
지원 데이터 타입
| 타입 이름 | C 언어 타입 | 설명 |
|---|---|---|
| byte, short, ushort | char, short, unsigned short | 작은 정수형 |
| int, uint | int, unsigned int | 일반 정수형 |
| long, ulong | long, unsigned long | 큰 정수형 |
| charp | char * | 문자열 포인터 (String) |
| bool, invbool | bool | 불리언 (invbool은 값 반전) |
| intarray | int * | 정수 배열 |
매개변수 전달 예제 03/04
커널 심볼 (Kernel Symbols)
커널 내부에서 다른 모듈이 접근할 수 있도록 공개된 함수나 변수
여러 드라이버에서 공통된 기능을 라이브러리처럼 재사용하거나 다른 커널 모듈이 특정 모듈의 함수나 변수를 써야할 때 유용
전역 심볼 테이블(/proc/kallsyms) 파일에서 현재 커널에 등록된 모든 심볼 확인 가능
심볼 네임스페이스 (Symbol Namespace)
커널 심볼이 많아지면서 발생하는 이름 충돌 문제를 해결
모듈 간 종속성을 명확하게 구분하여 관리하기 위한 메커니즘
커널 심볼 내보내기
EXPORT_SYMBOL(symbol)
심볼을 리눅스 커널의 전역 심볼 테이블에 등록하여 모든 모듈에서 사용 가능하게 함
라이선스 제약 없이 접근 가능
EXPORT_SYMBOL_GPL(symbol)
GPL 라이선스 모듈만 접근을 허용하는 매크로
커널 내부 핵심 함수들은 대부분 이 매크로로 보호 처리됨
EXPORT_SYMBOL_NS(symbol, namespace)
심볼을 특정 네임스페이스로 내보냄
사용하는 쪽에서 MODULE_IMPORT_NS(namespace) 를 명시해야 접근 가능
지쳤나요? 네.
드라이버의 세계는 정말 넓고 깊고 어렵구나
아직까지는 모듈의 틀만 만드는 수준이지만
다음 글부터는 좀 더 본격적인 내용이 나오게 될 것...!