특정 커널 함수 호출(시스템 호출)은 GNU/Linux에서 애플리케이션 개발의 자연스러운 부분이다. 하지만 사용자 공간을 호출하는 커널 공간을 사용하는 것은 어떨까? 매일 사용할 수 있는 이 기능에 적합한 애플리케이션이 다수 있다는 사실이 밝혀졌다. 예를 들어, 커널에서 모듈을 로드해야 하는 장치를 찾으면 이 프로세스가 어떻게 발생하는가? 사용자 모드 헬퍼 프로세스를 통해 커널에서 동적 모듈 로딩이 발생한다.
사용자 모드 헬퍼, API(Application Programming Interface) 및 커널에서 이 기능이 사용되는 사례에 대한 일부 예제를 살펴보는 것에서부터 시작해 보자. 그런 다음 API를 사용하여 이 기능의 작동 방식 및 제한사항에 대해 더 잘 알 수 있도록 하는 샘플 애플리케이션을 빌드한다.
사용자 모드 헬퍼 API는 잘 알려진 옵션 세트가 포함된 단순 API이다.
예를 들어, 사용자 공간에서 프로세스를 작성하려면 일반적으로
실행 파일의 이름, 실행 파일에 대한 옵션 및 환경 변수 세트를
제공한다(메인 페이지에서 execve 참조).
커널에서 프로세스를 작성하는 경우에도 동일하게 적용된다.
하지만 커널 공간에서 프로세스를 시작하기 때문에 몇 가지
추가적인 옵션을 사용할 수 있다.
표 1에서는 사용자 모드 헬퍼 API에서 사용할 수 있는 핵심 커널 함수 세트를 보여준다.
표 1. 사용자 모드 헬퍼 API의 핵심 함수
| API 함수 | 설명 |
|---|---|
call_usermodehelper_setup | 사용자 지역 호출을 위한 핸들러 준비 |
call_usermodehelper_setkeys | 헬퍼의 세션 키 설정 |
call_usermodehelper_setcleanup | 헬퍼의 정리 함수 설정 |
call_usermodehelper_stdinpipe | 헬퍼의 stdin 파이프 작성 |
call_usermodehelper_exec | 사용자 지역 호출 호출 |
또한 이 표에는 표 2에서 커널 함수를 캡슐화하는 몇 가지 단순화 함수가 있다(복수의 호출 대신 단일 호출이 필요함). 이러한 단순화 함수는 대부분의 경우에 유용하므로 가능하면 이러한 함수를 사용하는 것이 좋다.
표 2. 사용자 모드 헬퍼 API의 단순화
| API 함수 | 설명 |
|---|---|
call_usermodehelper | 사용자 지역 호출 작성 |
call_usermodehelper_pipe | stdin 파이프를 사용하여
사용자 지역 호출 작성 |
call_usermodehelper_keys | 세션 키를 사용하여 사용자 지역 호출 작성 |
먼저 핵심 함수에 대해 살펴본 후 단순화 함수에서 제공하는 기능에 대해
알아본다. 핵심 API는 subprocess_info 구조라는
핸들러 참조를 사용하여 작동한다. ./kernel/kmod.c에서 찾을 수 있는
이 구조는 지정된 사용자 모드 헬퍼 인스턴스에 필요한 모든 요소를
집계한다. 이 구조 참조는 call_usermodehelper_setup에
대한 호출에서 리턴된다. 구조(및 후속 호출)는 call_usermodehelper_setkeys(신임
저장용), call_usermodehelper_setcleanup 및
call_usermodehelper_stdinpipe에 대한 호출을
통해 추가로 구성된다. 마지막으로 구성이 완료되면 call_usermodehelper_exec에
대한 호출을 통해 구성된 사용자 모드 애플리케이션을 호출할 수 있다.
핵심 함수는 제어의 상당 부분을 제공하며 여기서는 헬퍼 함수가 단일
호출로 더 많은 작업을 수행한다. 파이프 관련 호출(call_usermodehelper_stdinpipe 및
헬퍼 함수 call_usermodehelper_pipe)은 헬퍼가
사용할 연관된 파이프를 작성한다. 구체적으로 파이프가 작성된다(커널에
있는 파일 구조). 파이프는 사용자 공간 애플리케이션이 읽을 수 있고
커널측에서 쓸 수 있다. 이러한 쓰기를 수행할 때 사용자 모드 헬퍼와
함께 파이프를 사용할 수 있는 유일한 애플리케이션은 코어 덤프이다.
이 애플리케이션(./fs/exec.c do_coredump())에서
코어 덤프는 파이프를 통해 커널 공간으로부터 사용자 공간으로 작성된다.
이러한 함수와 sub_processinfo 사이의 관계는
subprocess_info 구조에 대한 세부 사항과 함께
그림 1에 설명되어 있다.
그림 1. 사용자 모드 헬퍼 API 관계
표 2에 있는 단순화 함수는
call_usermodehelper_setup 함수 및
call_usermodehelper_exec 함수를 내부적으로 수행한다.
표 2에서 마지막 두 호출은
call_usermodehelper_setkeys 및
call_usermodehelper_stdinpipe를 각각 호출한다.
call_usermodehelper_pipe에 대한
소스는 ./kernel/kmod.c에서 찾을 수 있고 call_usermodehelper 및
call_usermodhelper_keys에 대한 소스는
./include/linux/kmod.h에서 찾을 수 있다.
커널에서 사용자 공간 애플리케이션을 호출하는 이유는 무엇일까?
이제 커널에서 사용자 모드 헬퍼 API를 사용할 일부 위치에 대해 살펴보자. 표 3은 유일한 애플리케이션 목록을 제공하지는 않지만 흥미로운 사용의 단면을 보여준다.
표 3. 커널에서 사용자 모드 헬퍼 API 애플리케이션
| 애플리케이션 | 소스 위치 |
|---|---|
| 커널 모듈 로딩 | ./kernel/kmod.c |
| 전원 관리 | ./kernel/sys.c |
| 제어 그룹 | ./kernel/cgroup.c |
| 보안 키 생성 | ./security/keys/request_key.c |
| 커널 이벤트 전달 | ./lib/kobject_uevent.c |
사용자 모드 헬퍼 API의 가장 간단한 애플리케이션 중 하나는 커널 공간에서
커널 모듈을 로드하는 것이다. request_module
함수는 사용자 모드 헬퍼 API의 기능을 캡슐화하고 단순한 인터페이스를
제공한다. 일반적인 사용 모델에서는 커널이 장치 또는 필요한 서비스를
식별하고 request_module에 대한 호출을 작성하여
모듈을 로드한다. 사용자 모드 헬퍼 API를 통해 모듈이 modprobe를
통해 커널에 로드된다(request_module을 통해
사용자 공간에서 호출된 애플리케이션).
모듈 로딩과 비슷한 애플리케이션은 장치 핫 플러깅을 사용하여 런타임 시 장치를 추가 또는 제거하는 것이다. 이 기능은 사용자 모드 헬퍼 API를 통해 사용자 공간에서 /sbin/hotplug 유틸리티를 호출하여 구현된다.
request_module을 통한 사용자 모드 헬퍼 API의
흥미로운 애플리케이션은 텍스트 검색 API(./lib/textsearch.c)이다.
이 애플리케이션에서는 커널에서 구성 가능한 텍스트 검색 인프라를
제공한다. 이 애플리케이션에서는 검색 알고리즘을 로드 가능한 모듈로
동적으로 로드하여 사용자 모드 헬퍼 API를 사용한다. 2.6.30 커널 릴리스에서는
Boyer-Moore(./lib/ts_bm.c), 순수 유한 상태 기계 접근 방식(./lib/ts_fsm.c) 및
Knuth-Morris-Pratt 알고리즘(./lib/ts_kmp.c)이라는 세 가지 알고리즘이
지원된다.
사용자 모드 헬퍼 API도 순차적인 시스템 종료 시 Linux를 지원한다. 시스템 종료가 필요한 경우 커널은 사용자 공간에서 /sbin/poweroff 명령을 호출하여 시스템을 종료한다. 기타 애플리케이션은 소스 위치와 함께 표 3에 나열되어 있다.
kernel/kmod.c에서 사용자 모드 헬퍼 API에 대한 API 및 소스를 찾는다(기본적으로
커널 공간 커널 모듈 로더로 사용됨을 보여줌). 구현에서는 kernel_execve를
사용하여 잡다한 작업을 처리한다. kernel_execve는
부팅 시 init 프로세스를 시작하는 데 사용되는
함수이며 사용자 모드 헬퍼 API는 사용하지 않는다.
사용자 모드 헬퍼 API의 구현은 매우 단순하고 간단하다(그림 2 참조).
사용자 모드 헬퍼의 작업은 사전 구성된 subprocess_info
구조에서 사용자 공간 애플리케이션을 시작하는 데 사용되는
call_usermodehelper_exec에 대한 호출로
시작한다. 이 함수는 subprocess_info 구조
참조와 열거 유형(기다리지 않을 것인지, 프로세스가 시작되기를
기다릴 것인지 아니면 전체 프로세스가 완료되길 기다릴 것인지)이라는
두 가지 인수를 승인한다. 그러면 호출을 비동기로 수행하는
작업 구조(khelper_wq)로
subprocess_info(또는 이 구조의
work_struct 요소)가 큐에 삽입된다.
그림 2. 사용자 모드 헬퍼 API의 내부 구현
요소가 khelper_wq에 배치되면 작업 큐에 대한
핸들러 함수(이 경우에는 __call_usermodehelper)가
호출되어 khelper 스레드를 통해 실행된다.
이 함수는 사용자 공간 호출에 필요한 모든 정보가 포함된 subprocess_info
구조를 큐에서 제거하여 시작된다. 다음 경로는 wait
변수 열거에 따라 다르다. 요청자가 전체 프로세스가 완료될 때까지 기다리길
원하는 경우(사용자 공간 호출(UMH_WAIT_PROC) 또는
전혀 기다리지 않음(UMH_NO_WAIT) 포함) 커널
스레드가 wait_for_helper 함수에서 작성된다.
그렇지 않으면 요청자는 단순히 사용자 공간 애플리케이션이 호출될 때까지
기다리고(UMH_WAIT_EXEC) 완료될 때까지는
기다리지 않으려고 한다. 이 경우 커널 스레드는 ____call_usermodehelper()를
위해 작성된다.
wait_for_helper 스레드에서는 SIGCHLD 신호
핸들러가 설치되고 다른 커널 스레드가 ____call_usermodehelper를
위해 작성된다. wait_for_helper 스레드에서는
____call_usermodehelper 커널 스레드(SIGCHLD
신호에 의해 표시됨)의 종료를 대기하는 데 필요한 sys_wait4에
대한 호출이 작성된다. 그런 다음 스레드는 필요한 정리 작업을 수행한다(UMH_NO_WAIT에
대한 구조를 비우거나 단순히 완료 알림을 다시 call_usermodehelper_exec()에
전송함).
____call_usermodehelper 함수는 사용자 공간에서
애플리케이션을 시작하는 실제 작업이 발생하는 위치이다. 이 함수는 모든
신호를 차단 해제하고 세션 키 링을 설정하여 시작된다. 또한 이 함수는
stdin 파이프를 설치한다(요청된 경우). 초기화를
좀 더 수행하면 사용자 공간 애플리케이션이 kernel/syscall.c의 kernel_execve에
대한 호출을 통해 호출되며 여기에는 이전에 정의된 path,
argv 목록(사용자 공간 애플리케이션 이름) 및
환경이 포함된다. 이 프로세스가 완료되면 스레드가 do_exit()에
대한 호출을 통해 존재한다.
또한 이 프로세스에서는 세마포어와 비슷한 조작인 Linux 완료를 사용한다.
call_usermodehelper_exec 함수가 호출되면
완료가 선언된다. subprocess_info 구조가
khelper_wq에 배치되면 완료 변수를 유일한
인수로 사용하여 wait_for_completion에 대한
호출이 작성된다. 이 변수는 subprocess_info
구조에 complete 필드로도 저장되어 있다.
하위 스레드에서 call_usermodehelper_exec 함수를
호출하려는 경우에는 subprocess_info 구조의 완료
변수를 나타내는 커널 메소드 complete를 호출한다.
이 호출은 함수가 계속 수행될 수 있도록 함수를 잠금 해제한다. 이 API의
구현은 include/linux/completion.h에서 찾을 수 있다.
참고자료 섹션에 있는 링크를 따라 사용자 모드 헬퍼 API에 대한 자세한 정보를 찾을 수 있다.
이제 사용자 모드 헬퍼 API의 단순한 사용에 대해 살펴보자. 먼저 표준 API에 대해 살펴본 후 헬퍼 함수를 사용하여 항목을 단순화하는 방법에 대해 알아본다.
이 데모에서는 API를 호출하는 로드 가능한 단순 커널 모듈을 개발한다.
Listing 1에는 모듈 입력 및 종료 함수를 정의하는 상용구 모듈 함수가
제공된다. 이러한 두 함수는 모듈의 modprobe 또는
insmod(모듈 입력 함수)와 모듈의
rmmod(모듈 종료)에서 호출된다.
Listing 1. 모듈 상용구 함수
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kmod.h>
MODULE_LICENSE( "GPL" );
static int __init mod_entry_func( void )
{
return umh_test();
}
static void __exit mod_exit_func( void )
{
return;
}
module_init( mod_entry_func );
module_exit( mod_exit_func );
|
사용자 모드 헬퍼 API의 사용이 Listing 2에 표시되며
이 Listing을 자세하게 살펴본다. 함수는 다양한 필요한 변수 및 구조의
선언으로 시작된다. 사용자 공간 호출을 수행하는 데 필요한 모든 정보가
포함된 subprocess_info 구조로 시작한다.
call_usermodehelper_setup을 호출하면
이 호출이 초기화된다. 그런 다음 argv라는
인수 목록을 정의한다. 이 목록은 일반적인 C
프로그램에 사용된 argv 목록과 비슷하며
애플리케이션(배열의 첫 번째 인수) 및 인수 목록을 정의한다. 목록의
끝을 표시하려면 NULL 터미네이터가 필요하다. 여기서 argv
목록의 길이는 알려져 있기 때문에 argc
변수(인수 수)는 내재적이라는 것에 주의한다. 이 예제에서 애플리케이션
이름은 /usr/bin/logger이며 인수는 help!이고
NULL로 종료된다. 다음으로 필요한 변수는 환경 배열(envp)이다.
이 배열은 사용자 공간 애플리케이션의 실행 환경을 정의하는 매개변수의
목록이다. 이 예제에서는 쉘에 대해 정의된 몇 가지 일반적인 매개변수를
정의하고 NULL 항목으로 종료한다.
Listing 2. 단순 usermode_helper API 테스트
static int umh_test( void )
{
struct subprocess_info *sub_info;
char *argv[] = { "/usr/bin/logger", "help!", NULL };
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };
sub_info = call_usermodehelper_setup( argv[0], argv, envp, GFP_ATOMIC );
if (sub_info == NULL) return -ENOMEM;
return call_usermodehelper_exec( sub_info, UMH_WAIT_PROC );
}
|
다음으로 call_usermodehelper_setup에 대한
호출을 작성하여 초기화된 subprocess_info
구조를 작성한다. 이전에 초기화된 변수를 메모리 초기화에 필요한
GFP 마스크를 표시하는 네 번째 매개변수와 함께 사용한다. 설정 함수에
대해 내부적으로는 커널 메모리를 할당하고 0으로 설정하는 kzalloc에
대한 호출이 있다. 이 함수에는 GFP_ATOMIC 또는
GFP_KERNEL 플래그(전자는 호출 대기 상태가
안 되도록 정의하고 후자는 대기 상태가 가능하도록 정의함)가 필요하다.
새 구조의 빠른 테스트 수행 후(즉, NULL이 아님) call_usermodehelper_exec
함수를 사용하여 호출을 계속 작성한다. 이 함수에서는 subprocess_info
구조 및 열거를 사용하여 대기 여부를 정의한다(내부 구조 섹션에
설명되어 있음). 이렇게만 하면 된다. 모듈이 로드되면 /var/log/messages
파일에 메시지가 표시된다.
call_usermodehelper_setup 및 call_usermodehelper_exec
함수를 함께 수행하는 call_usermodehelper API
함수를 사용하여 이 프로세스를 더 단순화할 수 있다. Listing 3에
표시된 것처럼 함수만 제거되는 것이 아니라 호출자가 subprocess_info
구조를 관리해야 할 필요성도 없어진다.
Listing 3. 더 단순한 사용자 모드 헬퍼 API 테스트
static int umh_test( void )
{
char *argv[] = { "/usr/bin/logger", "help!", NULL };
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };
return call_usermodehelper( argv[0], argv, envp, UMH_WAIT_PROC );
}
|
Listing 3에서는 호출 설정과 작성에 대해 동일한 요구사항이 존재한다(예:
argv 및 envp 배열
초기화). 여기서 유일한 차이점은 헬퍼 함수는 setup 및
exec 함수를 수행한다는 점이다.
사용자 모드 헬퍼 API는 커널 모듈 로딩, 장치 핫 플러깅 및 udev에 대한 이벤트 분배에 이르기까지 광범위하고 다양한 용도로 사용된다는 점에서 커널에 중요한 의미를 가진다. API의 순수 애플리케이션을 유효성 검증하는 것이 중요하긴 해도 이는 알고 있어야 할 커널의 중요한 특징이기 때문에 Linux 커널 툴킷에 추가하면 유용하다.
교육
- 사용자 모드 헬퍼 API에 대한 정보는
거의 존재하지 않지만 구현은 매우 깔끔하고 단순하게 수행된다.
모든 소스 개정에 대한 소스 브라우저인 LXR(Linux Cross Referencer)을
통해 구현을 검토할 수 있다. 유용한 두 가지 기본 파일은
kmod.c와
kmod.h이다.
- /proc 파일 시스템은 커널과 사용자
공간 사이의 통신에 필요한 메소드를 제공한다(즉, 가상 파일 시스템을
통함). "/proc
파일 시스템을 활용한 리눅스 커널 접근"(developerworks, 2006년 3월)에서
/proc 파일 시스템에 대해 자세히 알아볼 수 있다.
- Linux 시스템 호출 인터페이스에서는
사용자 공간 애플리케이션이 커널 기능을 호출하는 방법을 제공한다.
새 시스템 호출을 추가하는 방법을 포함한 Linux 시스템 호출에 대한
자세한 정보는 "리눅스
시스템 호출을 활용한 커널 명령"(developerworks, 2007년 3월)을 참조한다.
- 사용자 모드 헬퍼 API에 대해 설명하기 위해
이 기사에서는 로드 가능한 커널 모듈을 사용하여 테스트 애플리케이션을
커널에 설치한다. 로드 가능한 커널 모듈과 그 구현에 대한 자세한 정보는
"리눅스
커널 동적 적재 모듈 분석"(developerworks, 2008년 7월)을 참조한다.
- 2.6 커널 작업 큐 인터페이스에 대한
자세한 정보는 커널 작업 큐의 조작 및 API에 대해 적절하게 소개하는
2003년부터의 Linux Journal 기사를
참조한다.
- developerWorks 리눅스 영역에서는
리눅스 개발자에게 도움이 되는 여러 가지 리소스를 제공하고 있다.
- developerWorks
기술 행사 및 웹 캐스트를 통해 최신 정보를 얻을 수 있다.
- Twitter의 developerWorks 페이지를 살펴보자.
제품 및 기술 얻기
- developerWorks에서 직접 다운로드할 수 있는 IBM
시험판 소프트웨어를 사용하여 Linux와 관련된 후속 개발 프로젝트를 구현해 볼 수 있다.
토론
- developerWorks community에 참여하자.
개발자 중심 블로그, 포럼, 그룹 및 Wiki 검색 중에 다른 developerWorks
사용자와 의견을 교환해 보자.
M. Tim Jones는 임베디드 펌웨어 아키텍트이자 Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming(현재 2판), AI Application Programming(현재 2판) 및 BSD Sockets Programming from a Multilanguage Perspective의 저자이다. 정지 위성을 위한 커널 개발에서 시작해 임베디드 시스템 아키텍처와 네트워크 프로토콜 개발에 이르기까지 다양한 분야에 대한 공학 지식을 가지고 있다. 콜로라도주 롱몬트 소재의 Emulex Corp.에서 컨설턴트 엔지니어로 활약하고 있다.