디바이스 드라이버에 Argument 전달

디바이스 드라이버가 등록될때나 실행되고 있을때 인자를 전달할 수 있을까요? 예를 들면 우리가 ls 명령어를 실행할때 ls -l /etc/와 같이 -l /etc/와 같은 인자들을 전달하는 것처럼 말이죠. 응용 프로그램에서는 간단합니다. 우리는 알고 있죠. C언어를 사용한다면 main에서 argc와 args를 사용해서 목적을 달성 할 수 있다는 것을 말이죠. 

int main(int argc, char* argv[])

 

리눅스의 디바이스 드라이버에서도 가능합니다. 바로 아래의 매크로들을 이용하면 됩니다. 포스팅 아래에서는 아래의 매크로를 통해서 예제 코드를 구현합니다.

  • module_param(name, type, perm) : 변수 name을 설정합니다. name의 자료형은 type이고, perm은 권한을 나타냅니다.
  • module_param_array(name, type, num, perm) : 배열 버전입니다. num이 자료형의 배열 크기를 나타냅니다.
  • module_param_cb(name, &ops, &name, perm) : 만약 paremeter의 값이 바뀐 경우 알아차려야한다면, 이 매크로를 사용하면 됩니다. cb는 callback의 약자입니다.

 

그리고 이러한 파라미터를 읽거나 수정할때 권한(permission)을 지정할 수 있습니다. S_I를 Prefix로 하고, R(Read), W(Write), X(eXecute)를 의미하며 USR은 user, GRP는 group 권한을 의미합니다. |(OR)을 통해서 여러 권한을 줄 수 있습니다.

  • S_IRUSR
  • S_IWUSR
  • S_IXUSR
  • S_IRGRP
  • S_IWGRP
  • S_IXGRP

 

아래의 코드를 보면서 어떻게 위의 매크로들이 사용되는지 알아보도록 하겠습니다. 


소스코드 

//passing_params.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>


int value, arr_value[4];
char *name;
int cb_value = 0;

module_param(value, int, S_IWUSR|S_IRUSR);
module_param(name, charp, S_IRUSR|S_IWUSR);
module_param_array(arr_value,int, NULL, S_IRUSR|S_IWUSR);

// module_param_cb를 위한 setter
int notify_param(const char *val, const struct kernel_param *kp){
        int res = param_set_int(val, kp);
        if(res == 0){
                printk(KERN_INFO "Call back function called...\n");
                printk(KERN_INFO "New value of cb_value = %d\n",cb_value);
                return 0;
        }
        return -1;
}

const struct kernel_param_ops my_param_ops = {
        .set = &notify_param, //위에 정의한 setter
        .get = &param_get_int, // standard getter
};

module_param_cb(cb_value, &my_param_ops, &cb_value, S_IRUGO|S_IWUSR);

static int __init my_module_init(void){
        int i;
        printk(KERN_INFO "===== Print Params =====\n");
        printk(KERN_INFO "value = %d \n", value);
        printk(KERN_INFO "cb_value = %d \n", cb_value);
        printk(KERN_INFO "name = %s\n", name);
        for(i = 0; i < sizeof(arr_value)/sizeof(int); i++){
                printk(KERN_INFO "arr_value[%d] = %d \n", i, arr_value[i]);
        }
        return 0;
}

static void __exit my_module_exit(void){
        printk(KERN_INFO "Kernel Module Removed ...\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Reakwon");
MODULE_DESCRIPTION("A simple parameter test module");
MODULE_VERSION("1:1.0");

 

Makefile

obj-m += passing_params.o
KDIR = /lib/modules/$(shell uname -r)/build

all :
        make -C $(KDIR) M=$(shell pwd) modules

clean :
        make -C $(KDIR) M=$(shell pwd) clean

insmod로 parameter 전달

$ sudo insmod passing_params.ko value=4 name="reakwon" arr_value=1,2,3,4

dmesg를 확인하면 잘 전달되고 읽히는 것을 확인할 수 있습니다.

insmod parameter 전달

 


파라미터 업데이트시 Callback

만약 parameter를 디바이스 드라이버가 실행 중일때 변경하고 이에 따른 동작을 수행하고자 한다면 어떻게 할까요? module_param_cb가 이런 이유때문에 존재합니다. 

모듈의 parameter는 /sys/module 아래의 자신의 모듈 이름의 디렉토리 하위 parameters 디렉토리에서 관리가 됩니다. 확인해볼까요? 

$ ls /sys/module/passing_params/parameters/
arr_value  cb_value  name  value

 

그래서 parameters의 하위의 변수들의 값을 바꿀수 있습니다. 이때 callback 함수를 등록하면 호출이 되게 됩니다.  위의 전체 코드 중 이와 관련한 코드가 여깄습니다.

// module_param_cb를 위한 setter
int notify_param(const char *val, const struct kernel_param *kp){
        //...//
}

const struct kernel_param_ops my_param_ops = {
        .set = &notify_param, //위에 정의한 setter
        .get = &param_get_int, // standard getter
};

module_param_cb(cb_value, &my_param_ops, &cb_value, S_IRUGO|S_IWUSR);

 

kernel_param_ops가 callback을 관리하는 구조체이고 여기의 멤버로 .set, .get, .free가 있습니다. 

struct kernel_param_ops 
{
 int (*set)(const char *val, const struct kernel_param *kp);
 int (*get)(char *buffer, const struct kernel_param *kp);
 void (*free)(void *arg);
};

 

아래와 같이 parameter의 값을 변경해봅시다. 

$ sudo sh -c "echo 1010 > /sys/module/passing_args/parameters/cb_value"

 

그리고 dmesg를 통해서 kernel 메시지를 확인하면 우리가 등록한 notify_param Callback함수가 호출됨을 알 수 있습니다. 

callback 호출

 

 

이 포스팅은 embetronicx의 contents를 참고하여 작성된 포스팅입니다. 여기에 다 담지못하는 부분은 아래의 페이지에서 참고하시기 바랍니다. 앞으로도 embetronicx의 tutorial을 기반으로 작성할 예정입니다.

https://embetronicx.com/tutorials/linux/device-drivers/linux-device-driver-tutorial-part-3-passing-arguments-to-device-driver/

 

반응형
블로그 이미지

REAKWON

와나진짜

,

디바이스 드라이버(Device Driver)

여러분들이 usb포트를 통해서 펜으로 그림을 그리거나, 무선 마우스를 이용해서 마우스를 조정할때 윈도우즈에서 usb드라이버를 설치해달라고 하지 않던가요? 물론 이미 드라이버가 설치되어있으면 상관없겠지만 처음 사용할 경우에는 usb드라이버를 설치해달라고 컴퓨터가 요청을 할겁니다. 윈도우즈는 그 장치가 무선 마우스인지, 키보드인지 구분할 수가 없으며 어떻게 조정을 해야하는지도 모르기 때문입니다. 그래서 컴퓨터가 이 장치를 동작시킬때 어떻게 동작되어야하는지에 대한 프로그램을 따로 설치해야합니다. 여러분들이 알게 모르게 설치했던 것이 바로 디바이스 드라이버입니다. 구경이나 한번 해볼까요? 시작 프로그램에서 장치관리자를 검색해서 들어가보세요.

 

장치 관리자

이후 사운드쪽에서 디바이스 하나의 속성을 보도록 합시다.

사운드 디바이스

드라이버 탭을 보면 정보와 업데이트, 사용안함 이런 버튼이 존재하는지 알 수 있습니다. 여러분들이 간혹가다가 멀쩡한 스피커에서 소리가 안들릴때 있지 않나요? 그때 한 방법은 컴퓨터를 reboot하는 방법이 있겠지만 아래의 디바이스 드라이버를 다시 구동시켜주면 됩니다. 어떻게 하냐구요? 간단합니다. 

'디바이스 사용 안함' 을 눌러서 해제시키고, 다시 '디바이스 사용'을 눌러서 다시 장치를 구동시켜주면 어떨때는 해결되기도 합니다.

디바이스 속성

 

지금까지 간단하게 윈도우즈에서 디바이스를 살펴보았는데 자세히 그 내부를 들여다보면 아래의 도식과 같습니다. 사용자와 가장 접점에 있는 사용자 응용 프로그램 혹은 어플리케이션이 동작을 수행할때 내부적으로 System call을 사용합니다. open(), read(), write() 그런것들 있죠? 여러분들이 사용하는 printf()함수는 시스템 콜이 아니라 write를 잘 포장해서 만든 표준 입출력 라이브러리라고 합니다. 시스템 콜은 사용자 영역(kernel space)에서 호출이 가능합니다. 

 

여기서 사용자 영역이라고 하는 것은 실제 앱을 이용하는 사용자가 아니고 여러분들이 코딩할때 실행 프로그램을 만드는 그 영역을 말하는 겁니다. gcc로 실행 파일을 만들고 실행하죠. 여기서 printf나 메모리 할당(malloc), 그리고 포인터를 이용해서 데이터를 바꾸는데 심한 제약이 있었나요? 정말 알수없는 프로그램을 짜지 않는 이상 그런 경우는 없지요. 간단히 말해서 '내가 마음대로 프로그램을 짜고 실행할 수 있는 공간이구나' 라고 생각하시면 됩니다. 

device driver

시스템 콜이 호출이 되면 이제 커널 영역(kernel space) 내부로 호출이 전달됩니다. 커널 내부에 있는 가상 파일 시스템으로 전달, 그리고 아까 이야기했던 디바이스 드라이버가 인터페이스를 통해서 하드웨어를 제어합니다. 

여기서 또 커널 영역이라함은 무엇일까요? 일반 사용자가 접근할 수 없는 커널만의 영역을 이야기합니다. 여러분들이 커널 영역 메모리를 참조하거나 데이터를 변경할 수 없습니다. 디바이스 드라이버가 커널 쪽에 위치해있으므로 커널 영역의 프로그램이라고 보시면 되는데, 커널의 메모리를 손대는 곳이기 때문에 항상 안전성에 주의해야합니다. 혹시나 여러분들이 블루 스크린을 경험해본적 있죠? 디바이스 드라이버나 커널에 의해 시스템이 사용할 수 없는 상태가 됐기 때문입니다.

리눅스 디바이스는 세가지가 존재합니다. 캐릭터 디바이스, 블록 디바이스, 네트워크 디바이스가 있지요.

모듈(Module)

커널의 일부분인 프로그램으로 커널에 추가 기능이 모듈로 관리가 됩니다. 여러분들이 다바이스 드라이버를 만들고 추가할때 커널의 모듈로 끼워넣으면 됩니다. 모듈은 커널의 일부분입니다. 디바이스 드라이버가 하드웨어를 동작해야하기 때문에 커널의 일부분으로 동작해야합니다. 그래서 커널 모듈로 동작되어지는데, 이때 착각하지 말아야할 점은 모듈은 커널의 일부, 그리고 그 모듈에서 디바이스 드라이버가 동작됩니다. 즉, 모듈은 디바이스 드라이버가 아닙니다. 디바이스 드라이버가 모듈로 커널의 일부분으로 추가되어 동작하는 것이지요.

모듈이라는 개념이 없을때 디바이스 드라이버를 만들었다면  커널이 바뀌었기 때문에 다시 커널 컴파일을 해야하는 과정이 있었죠. 커널 컴파일 과정은 오래 걸리는 작업이라서 시간을 많이 소비하는 작업이기도 합니다. 모듈의 개념이 도입된 이후 위의 과정없이 모듈을 설치하고 해제할 수 있습니다. 바로 이 모듈에서 장치를 등록하거나 해제할 수 있습니다. 예를 들어 문자형 장치를 등록할때는 아래의 함수를 이용하죠. 

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

 

 

아직까지는 이 함수가 어떤 역할을 하는지 몰라도 됩니다. 우선 모듈이 어떻게 등록이 되는지부터 보도록 하겠습니다. 아주 간단한 모듈 하나를 만들어보도록 하겠습니다. 우선 여러분들이 C 프로그래밍을 하듯이 아래의 코드를 작성해줍니다.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>


static int __init my_init(void){
        printk("hello, kernel!\n");
        return 0;
}

static void __exit my_exit(void){
        printk("goodbye, kernel!\n");
}

module_init(my_init);
module_exit(my_exit);

 

맨 위의 3개의 헤더파일(linux/module.h, linux/kernel.h, linux/init.h)는 다 포함시켜주어야합니다.

우리가 맨 처음 모듈을 설치할때 초기화하는 module_init과 제거할때 호출되는 module_exit이 module.h에 정의되어있습니다. 이 매크로 함수의 매개변수를 우리가 정의한 함수로 전달해주면 됩니다. 이 무슨 역할을 하는지는 아래에서 보시면 됩니다.

#define module_init(x)  __initcall(x);
//...//
#define module_exit(exitfn)                                     \
        static inline exitcall_t __maybe_unused __exittest(void)                \
        { return exitfn; }                                      \
        void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));

 

혹시 객체지향 프로그래밍에서 생성자와 소멸자를 배우셨나요? 그때 객체의 초기설정은 생성자에서, 메모리해제 작업등 자원을 되돌려주는 마무리작업을 소멸자에서 하지 않나요? 그런 역할과 비슷하다고 보시면 됩니다. 

printk는 커널 모듈에서 메시지 내용을 출력할때 사용합니다. 커널 메시지는 dmesg 명령으로 볼 수 있습니다. 여기서 주의할 것은 printf가 아니라 printk라는 점!

어쨌든 코드를 짰다면 이제 컴파일을 해야하는데 Makefile을 이용해야합니다. 방법은 이렇습니다. 

KERNDIR=/lib/modules/$(shell uname -r)/build
obj-m+=mymodule.o
objs+=mymodule.o
PWD=$(shell pwd)

default:
        make -C $(KERNDIR) M=$(PWD) modules

clean:
        make -C $(KERNDIR) M=$(PWD) clean
        rm -rf *.ko
        rm -rf *.o

 

커널 모듈 빌드 디렉토리를 이용해야 하기 때문에 그 경로를 지정해줘야합니다. 이 디렉토리는 /lib/modules/커널 릴리즈 버전/build인데요. 여기서 커널 릴리즈 버전은 uname -r을 보면 알 수 있습니다. KERNDIR이 바로 그것이죠. 저의 경우는 아래와 같네요. 여기의 Makefile을 이용해야하기 때문에 기재해줘야하는 겁니다. 

uname -r

또 우리가 만든 모듈은 현재 디렉토리에 있죠? pwd의 결과를 넣어주면 됩니다. 

make를 하여 빌드합니다. 혹시 make 실행할 수 없다면 apt-get install make 해서 설치해주세요.

kernel object

make하고 빌드를 하면 산출물 중에서 .ko 파일이 보이실건데요. Kernel Object의 약자를 확장자로 갖고 있는 이것이 우리들이 설치할 모듈입니다. 설치해볼까요?

insmod

모듈을 설치하는 명령어는 insmod입니다. install module이죠. 자 이것을 이용해서 설치해봅시다. 사용법은 insmod [모듈명]으로 insmod mymodule.ko명령을 쳐보세요.

insmod

무소식이 희소식인 리눅스 세상에서 아무런 응답이 없으니 잘 설치된것 같은데요. 우리가 코드에서 init_module에서 printk로 문자열을 출력해주었죠? 모듈이 등록되었으니 분명 메시지가 나올테니 확인해보세요. dmesg | tail -1로 가장 마지막 줄을 확인해보도록 합시다. 

hello, kernel!

lsmod

우리가 설치한 모듈뿐만 아니라 설치된 다른 모듈도 보고싶다면 lsmod 명령을 사용하여 확인할 수 있습니다. 저의 머신에서는 아래와 같은 모듈들이 있습니다. 저의 모듈도 보이는군요.

lsmod

 

rmmod

마지막으로 모듈을 제거할때는 rmmod명령을 치면 됩니다. 사용법은 rmmod [제거할 모듈명] 으로 끝 확장자 .ko는 기재하지 않아도 상관없습니다.

rmmod

제거되었을까요? 우리가 코드에서 exit_module에서 전달한 함수가 있었죠? 그때도 우리는 printk로 메시지를 출력하게 만들었습니다. dmesg로 가장 마지막 메시지를 확인해보도록 합시다.

goodbye

 

커널 영역에서의 프로그래밍은 어려운 작업이고, 그것에 따른 세세한 조작을 할 수 있게 만들어줍니다. 예를 들어 커널 모듈을 통해서 우리는 시스템 콜을 후킹할 수도 있습니다. 아까 말씀드렸다시피 모듈은 커널의 일부분이며 반드시 디바이스 드라이버로 동작하지 않을 수 있지요.

이상으로 가장 간단한 리눅스 모듈을 만들어보았습니다. 최대한 쉽게 쓰려고 했는데 이해가 가셨나요? 

반응형
블로그 이미지

REAKWON

와나진짜

,