유저레벨에서 커널에 있는 API 를 동작시키는 인터페이스는 시스템 콜로써 항상 사용되는 것이다.

하지만 커널 레벨에서 사용자 공간의 어플리케이션을 실행하는 것은 그다지 알려져 있지 않다.


하지만 가만히 생각해 보면 핫플러그인과 같이 USB 가 꼽히면 자동으로 마운트되는 것을 우리는 알고 있다.

과연 어플리케이션에서 주기적으로 USB 꼽힌 것을 체크해서 마운트 하는 것일까?

그렇지 않다.

커널 레벨에서 USB가 인식되면 종류에 따라 마운트를 시켜주는 함수가 실행되게 된다.

혹은 모듈이 필요하면 모듈까지도 로딩을 시켜준다.

이런 인터페이스가 가능한 커널 함수를 소개한다.


그 인터페이시의 이름은 usermodehelper API 이다.

그 API를 한번 살펴보면 

usermod 헬퍼 API 핵심함수

call_usermodehelper_setup 사용자 지역 호출을 위한 핸들러 준비

call_usermodehelper_setkeys 헬퍼의 세션 키 설정

call_usermodehelper_setcleanup 헬퍼의 정리 함수 설정

call_usermodehelper_stdinpipe 헬퍼의 stdin 파이프 작성

call_usermodehelper_exec 사용자 지역 호출 호출


이정도로 구성되어 있다.


자료구조를 살펴보면 

kernel/kmod.c#L117 

 117struct subprocess_info {

 118        struct work_struct work;

 119        struct completion *complete;

 120        char *path;

 121        char **argv;

 122        char **envp;

 123        struct key *ring;

 124        enum umh_wait wait;

 125        int retval;

 126        struct file *stdin;

 127        void (*cleanup)(char **argv, char **envp);

 128};

의외로 아주 간단한 구조를 갖고 있다.


이제 사용자 모드 헬퍼 API 의 내부구조를 살펴보자.



kernel_execve 를 사용하여 커널 공간 커널모듈 로더로 사용하는 구현을 한다.

kernel_execve 는 부팅시 init 프로세스를 시작하는데 사용되는 함수이며 사용자 모드 헬퍼 API는 사용하지 않는다.

사용자 모드 헬퍼의 작업은 사전 구성된 subprocess_info 구조에서 사용자 공간 애플리케이션을 시작하는 데 사용되는 

call_usermodehelper_exec에 대한 호출로 시작한다. 이 함수는 subprocess_info 구조 참조와 열거 유형

(기다리지 않을 것인지, 프로세스가 시작되기를 기다릴 것인지 아니면 전체 프로세스가 완료되길 기다릴 것인지)이라는 

두 가지 인수를 승인한다. 

그러면 호출을 비동기로 수행하는 작업 구조(khelper_wq)로 subprocess_info(또는 이 구조의 work_struct 요소)가 큐에 삽입된다.

그림....

figure2[1].gif

요소가 khelper_wq에 배치되면 작업 큐에 대한 핸들러 함수(이 경우에는 __call_usermodehelper)가 호출되어 

khelper 스레드를 통해 실행된다. 

이 함수는 사용자 공간 호출에 필요한 모든 정보가 포함된 subprocess_info 구조를 큐에서 제거하여 시작된다. 

다음 경로는 wait 변수 열거에 따라 다르다. 요청자가 전체 프로세스가 완료될 때까지 기다리길 원하는 경우

(사용자 공간 호출(UMH_WAIT_PROC) 또는 전혀 기다리지 않음(UMH_NO_WAIT) 포함) 커널 스레드가 

wait_for_helper 함수에서 작성된다. 

그렇지 않으면 요청자는 단순히 사용자 공간 애플리케이션이 호출될 때까지 기다리고(UMH_WAIT_EXEC) 완료될 때까지는 

기다리지 않으려고 한다. 

이 경우 커널 스레드는 ____call_usermodehelper()를 위해 작성된다.


call_usermodehelper_exec ->  khelper_wq 등록 -> run subprocess_info->work ( __call_usermodehelper )  ->

___call_usermodehelper()   ->call_usermodehelper_freeinfo()


와 같은 일련의 과정을 따르고 중간에 wait 냐 no wait 냐에 따라 중간 처리가 달라진다.



샘플  driver 하나 소개한다. 

#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 );


usermode helper  API 테스트
static int umh_test( void )
{
 struct subprocess_info *sub_info;
 char *argv[] = { "/usr/bin/logger", "help!", NULL };ㅏ // path, arg,,,, termination
 static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };

 sub_info = call_usermodehelper_setup( argv[0], argv, envp );  

 if (sub_info == NULL) return -ENOMEM;

 return call_usermodehelper_exec( sub_info, UMH_WAIT_PROC );
}



실행함수를 위한 몇가지 변수가 선언된다.
call_usermodehelper_setup  함수의 호출을 통해 sub_info 구조체를 리턴받게 되는데
함수 호출시 웤크큐가 초기화되고 __call_usermodehelper 가 함수로 등록이 되며
kmalloc 으로 할당받은 구조체의 내용이 채워진다.
sub_info = kzalloc(sizeof(struct subprocess_info),  GFP_ATOMIC);

INIT_WORK(&sub_info->work, __call_usermodehelper);

sub_info->path = path;
sub_info->argv = argv;
sub_info->envp = envp;
이 과정이 끝나면 usermodehelper API 의 호출을 위한 kthread 인 khelper 에 등록이 완료된다.
마지막으로 call_usermodehelper_exec  함수가 실행이 되는데 
인자로는 struct subprocess 포인터와 옵션은 UMH_WAIT_PROC  이다.
call_usermodehelper_exec  에서는 조건 검사후

완료처리를 위한 complete 설정..
sub_info->complete = &done;

대기 상태 설정
sub_info->wait = wait;
워크큐 실행
queue_work(khelper_wq, &sub_info->work);
대기옵션 검사후
complete 대기
queue_work(khelper_wq, &sub_info->work);
return 
의 과정으로 이루어진다.



==================================================================================================
커널에는 좀더 실플하게 한번의 호출로 실행할수 있는 함수가 있다. 
kmod.h 에는 call_usermodehelper_setup  과 call_usermodehelper_exec 를 모두 호출해주는 단일 API  가 있다.
include/linux/kmod.h 
 67static inline int
 68call_usermodehelper(char *path, char **argv, char **envp, enum umh_wait wait)
 69{
 70        struct subprocess_info *info;
 71
 72        info = call_usermodehelper_setup(path, argv, envp);
 73        if (info == NULL)
 74                return -ENOMEM;
 75        return call_usermodehelper_exec(info, wait);
 76}
 
  위의 함수를 이용해 보면 예제 프로그램은 아래와 같이 만들수 있다.
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 );
}
마치 사용자 프로그램에서 main 프로그램의 변형을 보는 것 같다.
위의 에제를 실행함에 있어 빠진 것들이 있다.
일반적으로 사용자 환경에서 실행될때 shell 에서 해주는 역할들이 없기 때문에
signal 처리, 세션 키 링 설정, stdio 파이프 설치등 모든 설정을 envp 에 일일이 해주어야 한다는 것이다.
위의 예제가 logger 가 아니라 printf("hello world\n"); 를 실행하는 예제라면
"hello world" 라는 문구를 볼수 없다.  왜냐하면 stdio pipe 를 연결하지 않았기 때문이다.
logger 는 /var/log/messages 안에서 내용을 살펴보면 실행 여부를 확인할수 있다.


커널에는 모듈을 로딩시켜주는 modprobe 함수가 있다.
잘 알다시키 usb 디바이스가 인식되어 필요한 모듈이 로딩되지만
usb 디바이스를 뺀다고 해서 모듈이 내려가진 않는다.
이런 경우는 어떻게 해야 할까?
아쉽게도 그런 함수는 없다.
이유는 의존관계가 있기 때문이다.
그럼에도 모듈내리는 함수를 한번 만들어본다.

kernel/kmod.c   파일에는 request_module 이란 함수가 있다.
그럼 모듈을 내려주는 함수를 한번 추가해 보자.


49 char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
50 
51 /**
52  * request_module - try to load a kernel module
53  * @fmt:     printf style format string for the name of the module
54  * @varargs: arguements as specified in the format string
55  *
56  * Load a module using the user mode module loader. The function returns
57  * zero on success or a negative errno code on failure. Note that a
58  * successful module load does not mean the module did not then unload
59  * and exit on an error of its own. Callers must check that the service
60  * they requested is now available not blindly invoke it.
61  *
62  * If module auto-loading support is disabled then this function
63  * becomes a no-operation.
64  */
65 int request_module(const char *fmt, ...)
66 {
67     va_list args;
68     char module_name[MODULE_NAME_LEN];
69     unsigned int max_modprobes;
70     int ret;
71     char *argv[] = { modprobe_path, "-q", "--", module_name, NULL };
72     static char *envp[] = { "HOME=/",
73                 "TERM=linux",
74                 "PATH=/sbin:/usr/sbin:/bin:/usr/bin",
75                 NULL };
76     static atomic_t kmod_concurrent = ATOMIC_INIT(0);
77 #define MAX_KMOD_CONCURRENT 50  /* Completely arbitrary value - KAO */
78     static int kmod_loop_msg;
79 
80     va_start(args, fmt);
81     ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
82     va_end(args);
83     if (ret >= MODULE_NAME_LEN)
84         return -ENAMETOOLONG;
85 
86     /* If modprobe needs a service that is in a module, we get a recursive
87      * loop.  Limit the number of running kmod threads to max_threads/2 or
88      * MAX_KMOD_CONCURRENT, whichever is the smaller.  A cleaner method
89      * would be to run the parents of this process, counting how many times
90      * kmod was invoked.  That would mean accessing the internals of the
91      * process tables to get the command line, proc_pid_cmdline is static
92      * and it is not worth changing the proc code just to handle this case. 
93      * KAO.
94      *
95      * "trace the ppid" is simple, but will fail if someone's
96      * parent exits.  I think this is as good as it gets. --RR
97      */
98     max_modprobes = min(max_threads/2, MAX_KMOD_CONCURRENT);
99     atomic_inc(&kmod_concurrent);
100     if (atomic_read(&kmod_concurrent) > max_modprobes) {
101         /* We may be blaming an innocent here, but unlikely */
102         if (kmod_loop_msg++ < 5)
103             printk(KERN_ERR
104                    "request_module: runaway loop modprobe %s\n",
105                    module_name);
106         atomic_dec(&kmod_concurrent);
107         return -ENOMEM;
108     }
109 
110     ret = call_usermodehelper(modprobe_path, argv, envp, 1);
111     atomic_dec(&kmod_concurrent);
112     return ret;
113 }
114 EXPORT_SYMBOL(request_module);
이 함수는 대표적인 call_usermodehelper 사용예이다.
일반적인 시스템이 갖고 있는 환경으로 환경변수를 세팅하고 
어플리케이션 영역의 modprobe 를 실행시켜준다.
반대로 파라미터에 -r 을 주면 제거되지 않을까?
int remove_module(const char *fmt, ...)
116 {
117     va_list args;
118     char module_name[MODULE_NAME_LEN];
119     unsigned int max_modprobes;
120     int ret;
121     char *argv[] = { modprobe_path, "-r", "--", module_name, NULL };
122     static char *envp[] = { "HOME=/",
123                 "TERM=linux",
124                 "PATH=/sbin:/usr/sbin:/bin:/usr/bin",
125                 NULL };
126     static atomic_t kmod_concurrent = ATOMIC_INIT(0);
127 #define MAX_KMOD_CONCURRENT 50  /* Completely arbitrary value - KAO */
128     static int kmod_loop_msg;
129     
130     va_start(args, fmt);
131     ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
132     va_end(args);
133     if (ret >= MODULE_NAME_LEN)
134         return -ENAMETOOLONG;
135 
136     /* If modprobe needs a service that is in a module, we get a recursive
137      * loop.  Limit the number of running kmod threads to max_threads/2 or
138      * MAX_KMOD_CONCURRENT, whichever is the smaller.  A cleaner method
139      * would be to run the parents of this process, counting how many times
140      * kmod was invoked.  That would mean accessing the internals of the
141      * process tables to get the command line, proc_pid_cmdline is static
142      * and it is not worth changing the proc code just to handle this case. 
143      * KAO.
144      *
145      * "trace the ppid" is simple, but will fail if someone's
146      * parent exits.  I think this is as good as it gets. --RR
147      */
148     max_modprobes = min(max_threads/2, MAX_KMOD_CONCURRENT);
149     atomic_inc(&kmod_concurrent);
150     if (atomic_read(&kmod_concurrent) > max_modprobes) {
151         /* We may be blaming an innocent here, but unlikely */
152         if (kmod_loop_msg++ < 5)
153             printk(KERN_ERR
154                    "request_module: runaway loop modprobe %s\n",
155                    module_name);
156         atomic_dec(&kmod_concurrent);
157         return -ENOMEM;
158     }
159 
160     ret = call_usermodehelper(modprobe_path, argv, envp, 1);
161     atomic_dec(&kmod_concurrent);
162     return ret;
163 }
164 EXPORT_SYMBOL(remove_module);
와 같이 수정한다.
바뀐것은 -q 옵션대신에 -r 로 remove option 을 준것 뿐이다.
잘된다.
include/linux/kmod.h
28 #ifdef CONFIG_KMOD
29 /* modprobe exit status on success, -ve on error.  Return value
30  * usually useless though. */
31 extern int request_module(const char * name, ...) __attribute__ ((format (printf, 1, 2)));
32 extern int remove_module(const char * name, ...) __attribute__ ((format (printf, 1, 2)));
33 #else
34 static inline int request_module(const char * name, ...) { return -ENOSYS; }
35 #endif
처럼 remove_module 함수도 등록을 해서 사용할수 있게 하면 커널내부에서
모듈의 usermodehelper 기능을 이용한 모듈의 로딩과 언로딩이 가능하다.
==================================================================================================


커널에서 사용자 공간에 있는 어플리케이션을 실행시킬수 있다는 것은 매우 흥미로운 것이다.
여러분도 이제 여러분의 드라이버에 핫플러그인을 어떻게 하면 구현할수 있는지 그려질 것이라 생각한다.