리눅스 커널의 이해(5): 디바이스에 쓰기 동작에 대한 구체적인 작성 예  
등록: 한빛미디어(주) (2005-06-22 15:57:51)

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
리눅스 커널의 이해(2): 리눅스 커널의 동작
리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제

이 번 기사에서는 [디바이스에 쓰기 동작]에 대한 구체적인 작성 예를 살펴보고, 동기화 문제에 대한 처리를 적절히 해 주지 않을 경우 어떤 문제가 발생하는지 보기로 하자. 또한 지난 기사에서 살펴 보았던 동기화 문제에 대한 해결책을 이용하여 발생하는 문제점을 해결해 보기로 하자.

다음은 [디바이스에 쓰기 동작]을 중심으로 작성한 리눅스 디바이스 드라이버의 한 예다. 여기서는 독자가 모듈 형태의 리눅스 디바이스 드라이버를 작성할 줄 알고, 동적으로 리눅스 커널에 모듈을 삽입할 줄 안다고 가정한다.


# vi devwrite.c
#include
#include

#include
#include
#include

ssize_t dev_write(struct file * filp, const char * buffer,
size_t length, loff_t * offset);

struct file_operations dev_fops = {
write:   dev_write,
};

static int major = 0;
int init_module()
{
        printk("Loading devwrite module\n");
        major = register_chrdev(0, "devwrite", &dev_fops);
        if(major < 0) return major;
        return 0;
}

void cleanup_module()
{
        unregister_chrdev(major, "devwrite");
        printk("Unloading devwrite module\n");
}

#define SLOT_NUM    8

char dev_buffer;
int dev_key = 1;

char data_slot[SLOT_NUM];
int full_slot_num = 0;
int empty_slot_num = SLOT_NUM;
int full_slot_pos = 0;
int empty_slot_pos = 0;

void dev_working();

ssize_t dev_write(struct file * filp, const char * buffer,
size_t length, loff_t * offset)
{
        char user_buffer;

        if(length != 1) return -1;

        copy_from_user(&user_buffer, buffer, 1);

        if(dev_key == 0) {                                                   // ①
                if(empty_slot_num <= 0) return -1;                    // ④ start
                empty_slot_num --;

                data_slot[empty_slot_pos] = user_buffer;
                empty_slot_pos ++;
                if(empty_slot_pos == SLOT_NUM)
empty_slot_pos = 0;

                full_slot_num ++;                                             // ④ end
                return 1;

        }
        dev_key = 0;                                                           // ②
        dev_buffer = user_buffer;                                          // ③

        dev_working();                                                       // ⑤

        return 1;
}

static struct timer_list dev_interrupt;
void dev_interrupt_handler(unsigned long dataptr);

void dev_working()
{
        init_timer(&dev_interrupt);                                        // ⑨

        dev_interrupt.function = dev_interrupt_handler;            // ⑦
        dev_interrupt.data = (unsigned long)NULL;
        dev_interrupt.expires = jiffies + 1;                              // ⑥

        add_timer(&dev_interrupt);                                       // ⑧
}

void dev_interrupt_handler(unsigned long dataptr)
{
        printk("%c\n", dev_buffer);
       
        if(full_slot_num <= 0) {dev_key = 1; return;}                // ⑩
        full_slot_num --;                                                      // ⑪ start  

        dev_buffer = data_slot[full_slot_pos];
        full_slot_pos ++;
        if(full_slot_pos == SLOT_NUM) full_slot_pos = 0;

        empty_slot_num ++;                                                  // ⑪ end

        dev_working();

        return;
}


그러면 동기화 문제와 관련한 부분을 중심으로 소스를 살펴 보자.

dev_write 함수는 write 시스템 콜 함수에 의해 시스템 콜 루틴 내부에서 수행된다. dev_write 함수에서 ①, ②, ③ 부분은 논리적으로 다음과 같다.


디바이스를 사용하고 있지 않으면
    디바이스를 사용한다고 표시하고
    데이터를 디바이스 버퍼에 쓰고 나간다


③ 부분에서 dev_buffer 변수는 가상 디바이스의 버퍼를 나타낸다. 그리고 ①과 ② 부분에서 사용한 dev_key 변수는 가상 디바이스의 버퍼를 하나 이상의 프로세스가 동시에 접근하지 못하게 하는 역할을 한다.

우리는 전월 호에서 이와 같은 루틴에서 발생하는 동기화 문제를 다음과 같이 처리할 수 있음을 보았다.


cli
디바이스를 사용하고 있지 않으면
    디바이스를 사용한다고 표시하고
    데이터를 디바이스 버퍼에 쓰고 나간다
    sti


리 눅스 커널에는 cli와 sti에 해당하는 local_irq_save와 local_irq_restore라는 매크로가 있다. 이 두 매크로를 이용하여 dev_write 함수의 ①, ②, ③ 부분에서 발생할 수 있는 동기화 문제를 다음과 같이 처리할 수 있다.


unsigned long flags;
local_irq_save(flags);
if(dev_key == 0) {

}
dev_key = 0;
dev_buffer = user_buffer;
local_irq_restore(flags);


여 기서 local_irq_save(flags) 매크로는 CPU 내에 있는 flag 레지스터를 flags 지역 변수에 저장한 다음에 인터럽트를 끄는 역할을 한다. local_irq_restore(flags) 매크로는 flags 지역 변수의 값을 CPU 내에 있는 flag 레지스터로 복구함으로써 인터럽트를 켜는 역할을 한다.

또 dev_write 함수에서 ①과 ④ 부분은 논리적으로 다음과 같다.


디바이스를 사용하고 있으면
    데이터를 데이터 큐에 넣고 나간다


④ 부분에서 data_slot 배열 변수는 원형 데이터 큐를 나타낸다. empty_slot_pos 변수는 데이터를 채워 넣어야 할 큐의 위치를 나타낸다. empty_slot_num 변수는 큐의 비어 있는 데이터 공간의 개수를 나타낸다. 그래서 큐에 데이터를 채워 넣기 전에 empty_slot_num 변수의 값을 하나 감소시킨다. full_slot_num 변수는 큐에 채워진 데이터 공간의 개수를 나타낸다. 그래서 큐에 데이터를 채워 넣은 후에 full_slot_num 변수의 값을 하나 증가시킨다.

우리는 전월 호에서 이와 같은 루틴에서 발생하는 동기화 문제를 다음과 같이 처리할 수 있음을 보았다.


cli
디바이스를 사용하고 있으면
    데이터를 데이터 큐에 넣고 나간다
    sti


따라서 dev_write 함수의 ①과 ④ 부분에서 발생할 수 있는 동기화 문제를 다음과 같이 처리할 수 있다.


unsigned long flags;
local_irq_save(flags);
if(dev_key == 0) {
           if(empty_slot_num <= 0) {
                      local_irq_restore(flags);
                      return -1;
           }
           empty_slot_num --;

           data_slot[empty_slot_pos] = user_buffer;
           empty_slot_pos ++;
           if(empty_slot_pos == SLOT_NUM)
                       empty_slot_pos = 0;

           full_slot_num ++;
           local_irq_restore(flags);
           return 1;
}


⑤ 부분은 ③ 부분에서 디바이스 버퍼에 데이터를 쓰고 나면, 디바이스가 동작하기 시작함을 논리적으로 나타낸다.




[그림 1] 디바이스에 쓰기 예


앞 에서 우리는 ③ 부분에서 가상 디바이스의 버퍼를 사용한다고 했다. 따라서 이 디바이스에 의한 hardware interrupt는 발생할 수 없다. 그래서 여기서는 주기적으로 발생하는 timer interrupt를 가상 디바이스에서 발생하는 hardware interrupt라고 가정한다. 그럴 경우 timer interrupt는 [그림 1]과 같이 발생할 수 있으며, 이 그림은 전월호의 [그림 2]와 논리적으로 크게 다르지 않음을 볼 수 있다.

[그림 1]에서 ⓐ 부분은 dev_working 함수의 ⑥ 부분을 나타낸다. 여기서는 dev_interrupt 구조체 변수의 멤버 변수인 expires 변수 값을 커널 변수인 jiffies 변수 값에 1을 더해서 설정한다. jiffies 변수는 커널 변수로 주기적으로 발생하는 timer interrupt를 처리하는 루틴의 top_half 부분에서 그 값을 하나씩 증가시킨다. 리눅스 커널 버전 2.6에서는 초당 1000 번 timer interrupt가 발생하도록 설정되어 있다.

[그림 1]의 ⓑ 부분에서는 jiffies 변수 값을 증가시키고 있다. jiffies 변수 값을 증가시키는 함수는 do_timer 함수이며, timer interrupt handler 내에서 이 함수를 호출한다. do_timer 함수는 리눅스 커널 소스의 linux/kernel/timer.c 파일에서 찾을 수 있다.

[그림 1]의 ⓒ 부분에서는 dev_interrupt 구조체 변수의 expires 변수 값과 현재의 jiffies 변수 값을 비교하여 작거나 같으면 dev_interrupt 구조체 변수의 function 함수 포인터 변수가 가리키는 함수를 수행한다. 이 부분은 timer interrupt를 처리하는 루틴의 bottom_half 부분이며 timer_bh 함수 내에서 run_timer_list 함수를 호출하여 수행한다. timer_bh 함수는 리눅스 커널 소스의 linux/kernel/timer.c 파일에서 찾을 수 있다. [그림 1]의 ⓒ 부분에서는 dev_working 함수의 ⑦ 부분에 의해 실제로는 dev_interrupt_handler 함수가 수행된다.

dev_interrupt_handler 함수를 살펴보기 전에 timer_bh 함수 내의 run_timer_list 함수의 역할을 좀 더 보기로 하자. run_timer_list 함수는 timer_list 구조체 변수로 이루어진 linked list에서 timer_list 구조체 변수를 소비하는 역할을 한다. 구체적으로 timer_list 구조체 변수의 expires 변수 값이 현재 jiffies 변수 값보다 작거나 같을 경우 해당하는 timer_list 구조체 변수를 linked list에서 떼내어, timer_list 구조체 변수의 function 포인터 변수가 가리키는 함수를 수행한다. dev_working 함수의 ⑧ 부분에서 사용한 add_timer 함수는 커널 함수이며 run_timer_list 함수가 소비하는 linked list에 timer_list 구조체 변수를 하나 더해 주는 생산자 역할을 한다. dev_working 함수의 ⑨ 부분에서 사용한 init_timer 함수는 timer_list 구조체 변수를 초기화해주는 커널 함수이다.

그러면 dev_interrupt_handler 함수를 보기로 하자. dev_interrupt_handler 함수는 가상 디바이스의 top_half 루틴과 bottom_half 루틴을 나타낸다. dev_interrupt_handler 함수에서 ⑩ 부분은 논리적으로 다음과 같다.


데이터 큐가 비어 있으면
    디바이스를 다 사용했다고 표시하고 나간다


또 dev_interrupt_handler 함수에서 ⑩과 ⑪ 부분은 논리적으로 다음과 같다.


데이터 큐가 비어 있지 않으면
    데이터를 하나 꺼내서
    디바이스 버퍼에 쓰고 나간다


⑪ 부분에서 full_slot_pos 변수는 데이터를 비울 큐의 위치를 나타낸다.

이상에서 dev_write 함수에서 동기화 문제가 발생할 수 있으며 다음과 같이 해결할 수 있다.


ssize_t dev_write(struct file * filp, const char * buffer,
                       size_t length, loff_t * offset)
{
            char user_buffer;
            unsigned long flags;

            if(length != 1) return -1;

            copy_from_user(&user_buffer, buffer, 1);

            local_irq_save(flags);
            if(dev_key == 0) {
                       if(empty_slot_num <= 0) {
                                   local_irq_restore(flags);
                                   return -1;
                       }
                       empty_slot_num --;

                       data_slot[empty_slot_pos] = user_buffer;
                       empty_slot_pos ++;
                       if(empty_slot_pos == SLOT_NUM)
                                    empty_slot_pos = 0;

                       full_slot_num ++;
                       local_irq_restore(flags);
                       return 1;
            }
            dev_key = 0;
            dev_buffer = user_buffer;
            local_irq_restore(flags);
       
            dev_working();

            return 1;
}


완 성된 소스를 다음과 같이 컴파일한 후 insmod 명령어를 이용하여 커널에 devwrite.o 모듈을 끼워 넣는다. 컴파일하는 부분에서 –D__KERNEL__ 옵션은 #define __KERNEL__ 이라는 매크로 문장을 컴파일하고자 하는 파일의 맨 위쪽에 써 넣는 효과와 같으며, __KERNEL__ 매크로는 컴파일하는 소스가 커널의 일부가 될 수 있다는 의미를 가진다. MODULE 매크로는 컴파일하는 소스를 커널에 모듈형태로 동적으로 끼워 넣거나 빼 낼 수 있다는 의미이다. -I/usr/src/linux-2.4/include 옵션은 파일 내에서 참조하는 헤더파일을 찾을 디렉토리를 나타낸다. 일반적으로 PC 상에서 리눅스 커널 소스를 설치할 경우 /usr/src 디렉토리 아래 linux 내지는 linux-2.4 와 같은 디렉토리 아래 놓인다. 모듈 프로그램은 커널의 일부가 되어 동작하며 따라서 그 모듈이 동작할 커널을 컴파일하는 과정에서 참조했던 헤더파일을 참조해야 한다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# lsmod
# insmod devwrite.o -f
# lsmod
Module                         Size  Used by    Tainted: PF
devwrite                        3581   0  (unused)


/proc/devices 파일은 커널내의 디바이스 드라이버에 대한 정보를 동적으로 나타낸다. 이 파일을 들여다보면 방금 끼워 넣은 디바이스 드라이버의 주 번호가 253임을 알 수 있다. 주 번호는 바뀔 수도 있으니 주의하기 바란다.


# cat /proc/devices
Character devices:
              ...
253 devwrite
              ...
Block devices:


우리가 작성한 디바이스 드라이버를 접근하기 위해 문자 디바이스 파일을 다음과 같이 만든다.


# mknod /dev/devwrite c 253 0
# ls -l /dev/devwrite
crw-r--r--    1 root     root     253,   0  7월 12 00:03 /dev/devwrite


그리고 우리가 작성한 디바이스 드라이버를 사용할 응용 프로그램을 다음과 같이 작성한다.


# vi devwrite-app.c
#include
#include
#include

int main()
{
        int fd;

        fd = open("/dev/devwrite", O_RDWR);

        write(fd, "A", 1);

        close(fd);
}


그리고 다음과 같이 응용 프로그램을 컴파일한 후 응용 프로그램을 수행해 본다. 화면에는 아무 내용도 뜨지 않는다.


# gcc devwrite-app.c -o devwrite-app
# ./devwrite-app


이 젠 모듈을 커널에서 빼낸후 /var/log/messages 파일의 맨 뒷부분을 읽어 본다. 각각 insmod 명령어를 수행하는 과정에서 커널내에서 수행한 init_module 함수, 좀 전에 수행한 응용 프로그램을 수행하는 과정에서 커널내에서 수행한 dev_interrupt_handler 함수, rmmod 명령어를 수행하는 과정에서 커널내에서 수행한 cleanup_module 함수에서 찍은 메시지를 볼 수 있다.


# rmmod devwrite
# tail /var/log/messages
...
Jul 12 00:16:44 localhost kernel: Loading devwrite module
Jul 12 00:17:00 localhost kernel: A
Jul 12 00:17:13 localhost kernel: Unloading devwrite module


그 러면 위와 같이 동기화 문제를 처리 하지 않을 경우 어떤 문제가 발생할 수 있는지 예를 하나 보기로 하자. 다음 예는 전월호의 [그림 5]와 [그림 6]의 경우에서 보았던 루틴간 경쟁 상태를 발생시킨다. 먼저 dev_write 함수와 dev_interrupt_handler 함수를 각각 다음과 같이 고친다.


ssize_t dev_write(struct file * filp, const char * buffer,
                       size_t length, loff_t * offset)
{
             char user_buffer;
             int i;

             if(length != 1) return -1;

             copy_from_user(&user_buffer, buffer, 1);

             for(i=0;i<600;i++) {                                             // ⑫-⑴
                        user_buffer = (char)(i%10)+48;

                        if(dev_key == 0) {
                                   printk("<--1\n");                         // ⑬-⑴
                                   if(empty_slot_num <= 0) {
                                               printk("slot is full\n");
                                               while(1) if(empty_slot_num > 0) break;         // ⑫-⑵
                                   }
                                   empty_slot_num --;

                                   data_slot[empty_slot_pos] = user_buffer;
                                   empty_slot_pos ++;
                                   if(empty_slot_pos == SLOT_NUM)
                                               empty_slot_pos = 0;

                                   {
                                               int j;
                                               for(j=0;j<0x1000000;j++);
                                    }
                                    printk("<--2\n");                       // ⑬-⑵
                                    full_slot_num ++;

                                    continue;                                  // ⑫-⑶
                        }
                        dev_key = 0;
                        dev_buffer = user_buffer;

                        dev_working();
                }

                return 1;
}

void dev_interrupt_handler(unsigned long dataptr)
{
                printk("%c\n", dev_buffer);

                if(full_slot_num <= 0) {
                         printk("no data\n");
                         dev_key = 1;
                         return;
                }
                full_slot_num --;

                dev_buffer = data_slot[full_slot_pos];
                full_slot_pos ++;
                if(full_slot_pos == SLOT_NUM) full_slot_pos = 0;

                empty_slot_num ++;

                dev_working();

                return;
}


dev_write 함수의 ⑫-⑴, ⑫-⑵, ⑫-⑶ 부분은 동기화 문제가 발생할 수 있는 영역을 반복적으로 수행함으로써 dev_interrupt_handler 함수와 충돌이 날 가능성을 높이는 역할을 한다. dev_write 함수의 ⑬-⑴, ⑬-⑵ 사이에 dev_interrupt_handler 함수가 끼어 들 경우 문제가 발생한다. ⑭ 부분은 ⑬-⑴, ⑬-⑵ 사이에 dev_interrupt_handler 함수가 끼어 들 가능성을 높이기 위해 끼워 넣었다. 다음과 같이 테스트해 본다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# insmod devwrite.o -f
# ./devwrite-app
# tail /var/log/messages -n 600
...
Jul 12 06:19:46 localhost kernel: <--1                  ⒜
Jul 12 06:19:46 localhost kernel: 6
Jul 12 06:19:46 localhost kernel: no data              ⒞
Jul 12 06:19:46 localhost kernel: <--2                  ⒝
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: <--2
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: 8                       ⒟
Jul 12 06:19:46 localhost kernel: <--2
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: 7                       ⒠
Jul 12 06:19:46 localhost kernel: <--2
...
# rmmod devwrite


테스트를 수행한 결과 ⒜와 ⒝ 사이에 ⒞가 끼어 듦으로써 동기화의 문제가 발생하였다. 그 결과 ⒟와 ⒠에서 8과 7의 데이터 역전 현상이 발생하였으며, 또한 7 데이터에 starvation이 발생하였음을 알 수 있다.

그럼 여기서 발생한 동기화 문제를 해결해 보자. 먼저 dev_write 함수를 다음과 같이 고친다.


ssize_t dev_write(struct file * filp, const char * buffer,
                size_t length, loff_t * offset)
{
        char user_buffer;
        int i;
        unsigned long flags;

        if(length != 1) return -1;

        copy_from_user(&user_buffer, buffer, 1);

        for(i=0;i<600;i++) {
                user_buffer = (char)(i%10)+48;

                local_irq_save(flags);
                if(dev_key == 0) {
                        printk("<--1\n");
                        if(empty_slot_num <= 0) {
                                local_irq_restore(flags);
                                printk("slot is full\n");
                                while(1) if(empty_slot_num > 0) break;
                        }
                        empty_slot_num --;

                        data_slot[empty_slot_pos] = user_buffer;
                        empty_slot_pos ++;
                        if(empty_slot_pos == SLOT_NUM)
                                empty_slot_pos = 0;

                        {
                                int j;
                                for(j=0;j<0x1000000;j++);
                        }
                        printk("<--2\n");
                        full_slot_num ++;
                        local_irq_restore(flags);

                        continue;
                }
                dev_key = 0;
                dev_buffer = user_buffer;
                local_irq_restore(flags);

                dev_working();
        }

        return 1;
}


다음과 같이 테스트를 수행하다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# insmod devwrite.o -f
# ./devwrite-app
# tail /var/log/messages -n 600
...
Nov 19 18:41:07 localhost kernel: 0
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 1
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 2
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 3
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 4
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 5
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 6
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 7
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 8
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 9
...


데이터의 역전 현상이나 starvation 없이 순서대로 data가 가상 디바이스에 전달되는걸 볼 수 있다.

이 상에서 <디바이스에 쓰기 동작>에 대한 구체적인 작성 예를 보았다. 참고로 모듈 프로그래밍은 일반적으로 루트 사용자의 권한으로 해야 한다. 본 기사에서는 리눅스 커널 2.6 버전의 내용을 위주로 동기화의 문제를 다루고 있지만, 실제 동기화에 대한 테스트는 리눅스 커널 2.4 버전의 파란 리눅스 7.3에서 하였으니 이 점 주의 하기 바란다.


+ Recent posts