본문 바로가기
IT 기술/리눅스 커널

리눅스 2.6 pdflush VS. 리눅스 2.4 bdflush, kupdate

by 땅뚱 2009. 9. 4.
리눅스 2.4 버전까지 시스템 버퍼에 쌓여있는 내용을 디스크로 기록하는 데몬은 bdflush / kupdate 2가지가 존재하였다. 아래 코드를 보면 kernel_thread 를 사용하여 부팅 초기에 각 데몬을 thread 로 생성하여 실행하는 것을 볼 수 있다.


<linux 2.4.31 fs/buffer.c>
static int __init bdflush_init(void)
{
    static struct completion startup __initdata = COMPLETION_INITIALIZER(startup);

     kernel_thread(bdflush, &startup, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
    wait_for_completion(&startup);
    kernel_thread(kupdate, &startup, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
    wait_for_completion(&startup);
    return 0;
}


bdflush 데몬은 모든 dirty buffer 를 디스크로 기록(write_some_buffers)하는 역할을 수행한다. 모든 dirty buffer 를 디스크에 기록한 이후에는 bdflush_wait 이라는 wait queue 에서 sleep(interruptible_sleep_on) 한다. 이후에 1) 새로운 메모리를 요청하거나, 2) 불필요한 버퍼를 free 하거나 3) write 시점에 특정 조건을 만족하게 되면 다시 bdflush 데몬을 깨워주도록 구현되어 있다.

다음은 bdflush 의 핵심 코드 부분이다.

<linux 2.4.31 fs/buffer.c>
for (;;) {
(생략)
    while (ndirty > 0) {
       spin_lock(&lru_list_lock);
        if (!write_some_buffers(NODEV))
           break;
        ndirty -= NRSYNC;
    }
    if (ndirty > 0 || bdflush_stop())
       interruptible_sleep_on(&bdflush_wait);
 }


interruptible_sleep_on 으로 bdflush_wait queue 에서 sleep 하는 thread 를 깨워주는 함수는 wakup_bdflush 이다. 그럼 어떤 상황에서 bdflush 를 깨우는지 확인해보자 다음은 wakeup_bdflush 함수가 호출되는 부분이다.

<linux 2.4.31 fs/buffer.c>
1. static void free_more_memory(void)
   - 메모리를 더 얻어오는 함수. bdflush 를 깨워준 후에 free 메모리를 얻어오는 함수 호출
2. void balance_dirty(void)
 
- write 가 이루어진 시점에 호출되어 balance_dirty_state() 를 호출하여 bdflush 를 깨울 조건을 판단. 조건을 만족하는 경우에만 bdflush 를 호출
3. int fastcall try_to_free_buffers(struct page * page, unsigned int gfp_mask)
 
- 사용되지 않는 버퍼를 free 시키는 함수.

위 내용을 확인해보면 bdflush 는 메모리를 확보하기 위해서 또는 전체 시스템에서 dirty buffer 가 차지하는 메모리가 일정 이상 넘지 않도록 하기위해서 호출된다는 것을 알 수 있다.

그럼 버퍼를 디스크로 기록하는 또다른 데몬인 kupdate 에 대해서 살펴보자. kupdate 데몬은 bdf_prm.b_un.interval (5초)마다 주기적으로 깨어나서 dirty 된지 30초가 지난 버퍼들만 디스크로 내려주는 기능을 한다.(sync_old_buffers)

각 버퍼가 dirty 된 시간은 어떻게 계산하는 것일까? __mark_dirty / set_buffer_flushtime 함수를 보면 모든 버퍼들은 dirty 가 되는 순간에 현재 jiffies 값에 bdf_prm.b_un.age_buffer(30초)를 더한 값을 버퍼 헤더에 기록해준다. 그러면 kupdate 가 깨어나서 sync_old_buffers 함수를 수행하는데, sync_old_buffers 함수에서 현재 jiffies 값과 버퍼 헤더의 jiffies 값을 비교하여 30초가 지난 버퍼만 디스크에 기록해준다.


<linux 2.4.31 fs/buffers>
       for (;;) {
(생략)          ...
            interval = bdf_prm.b_un.interval;
            if (interval) {
                tsk->state = TASK_INTERRUPTIBLE;
                schedule_timeout(interval);
            } else {
            tsk->state = TASK_STOPPED;
            schedule(); /* wait for SIGCONT */
            }
(생략)          ...
           sync_old_buffers();
           if (laptop_mode)
               fsync_dev(NODEV);
           run_task_queue(&tq_disk);
       }


이러한 구조는 리눅스 2.5 이상에서 구조가 완전히 바뀐다. 리눅스 2.5 이상 버전에서 아무리 찾아봐도 bdflush 라는 이름의 함수가 존재하지 않는다. 대신 pdflush 라는 함수가 존재하는데, 구조를 잘 살펴보면, 2.4 에 비해서 조금은 구조화되고 깔끔해 진 것을 알 수 있다.

우선 pdflush 는 주어진 함수 포인터를 수행하는 커널 thread 로 구성되어 있다. 함수 포인터는 특정 함수에 의해서 등록되며, 함수 포인터를 등록해주는 함수는 자고 있는 pdflush thread 를 깨워준다.

기본적으로 커널이 부팅되면 2개의 thread 가 미리 생성된다. 생성된 pdflush 는 기본적으로 sleep 상태로 들어간다. 그리고 필요에 따라 깨워지며 최대 8개까지 thread 가 생성되어 동시에 수행될 수 있다.

아래 함수를 보면 부팅 초기에 pdflush thread 를 2개 생성하는 것을 확인 할 수 있다.

<linux 2.6.30-5 mm/pdflush.c>
static int __init pdflush_init(void)
{
(생략)  ...
      for (i = 0; i < MIN_PDFLUSH_THREADS; i++)
           start_one_pdflush_thread();
      return 0;
}

앞서 말한 바와 같이 pdflush 는 등록된 함수 포인터를 수행한다. 즉, 특정 작업을 수행하도록 설계되어 있지 않다. 사용자(커널 프로그래머)가 데몬으로 수행하기위해 필요한 함수를 pdflush_operation 을 사용하여 등록하는 구조로 되어 있다.

start_one_pdflush_thread 함수는 kernel_thread 함수를 사용하여 pdflush 함수를 thread 로 생성하여 수행한다. pdflush 함수는 궁극적으로 __pdflush 를 호출하는데, pdflush 의 핵심 부분이 __pdflush 함수이다. 그럼 __pdflush 가 어떤 방식으로 동작하는 지를 알아보자. 아래 코드에서 빨간색으로 되어있는 부분이 pdflush 함수의 중요한 부분이다.

<linux 2.6.28-15 mm/pdflush.c>
static int __pdflush(struct pdflush_work *my_work)
{
        current->flags |= PF_FLUSHER | PF_SWAPWRITE;
        set_freezable();
        my_work->fn = NULL;
        my_work->who = current;
        INIT_LIST_HEAD(&my_work->list);

        spin_lock_irq(&pdflush_lock);
        nr_pdflush_threads++;
        for ( ; ; ) {
                struct pdflush_work *pdf;

                set_current_state(TASK_INTERRUPTIBLE);
                list_move(&my_work->list, &pdflush_list);   --> 1) pdflsuh_list 에 current  work 을 넣고 sleep 한다.
                my_work->when_i_went_to_sleep = jiffies;
                spin_unlock_irq(&pdflush_lock);
                schedule();   --> 1) TASK_INTERRUPTIBLE 이기 때문에 다른 thread 가 깨워주워야 다시 실행된다.
                try_to_freeze();
                spin_lock_irq(&pdflush_lock);
                if (!list_empty(&my_work->list)) {
                        /*
                         * Someone woke us up, but without removing our control
                         * structure from the global list.  swsusp will do this
                         * in try_to_freeze()->refrigerator().  Handle it.
                         */
                        my_work->fn = NULL;
                        continue;
                }
                if (my_work->fn == NULL) {
                        printk("pdflush: bogus wakeup\n");
                        continue;
                }
                spin_unlock_irq(&pdflush_lock);

                (*my_work->fn)(my_work->arg0); --> 2) 사용자가 등록한 furnction 을 수행한다. \
                                                                        어디서 함수를 등록하는 것인가?


                /* 3) pdflush_list 가 empty 인지 1초가 지난 후에도 list 가 비어있는 경우, 즉 모든 pdflush thread 가
                   계속 작업
을 수행하고 있다는 것을 의미하는 경우에 pdflush thread 가 8개 이하인 경우에 새로운
                   pdflush
thread 를 생성한다. */
                if (time_after(jiffies, last_empty_jifs + 1 * HZ)) {
                        /* unlocked list_empty() test is OK here */
                        if (list_empty(&pdflush_list)) {
                                /* unlocked test is OK here */
                                if (nr_pdflush_threads < MAX_PDFLUSH_THREADS)
                                        start_one_pdflush_thread();
                        }
                }

                spin_lock_irq(&pdflush_lock);
                my_work->fn = NULL;

                /* 4) pdflush_list 가 empty 인 경우에는 pdflush 개수와 무관하게 pdflush_list 에 넣고 다시 sleep 하고,
                    그렇지 않은 경우라면, 현재 thread 개수가 3개 이상이고, sleep 후 1초가 지난 pdflush thread 를
                    종료한다. */

                if (list_empty(&pdflush_list))
                        continue;
                if (nr_pdflush_threads <= MIN_PDFLUSH_THREADS)
                        continue;
                pdf = list_entry(pdflush_list.prev, struct pdflush_work, list);
                if (time_after(jiffies, pdf->when_i_went_to_sleep + 1 * HZ)) {
                        /* Limit exit rate */
                        pdf->when_i_went_to_sleep = jiffies;
                        break;                                  /* exeunt */
                }
        }
        nr_pdflush_threads--;
        spin_unlock_irq(&pdflush_lock);
        return 0;
}



최소 생성된 pdflush thread 는 1) 에서 설명한 것과 같이 pdflush_list 에 자신의 구조체를 넣고 sleep 한다. (sleep 하고 있는 pdflush 는 곧 설명할 pdflush_operation 함수를 호출하여 깨워준다.) 깨워진 thread 는 pdflush_operation 함수에서 등록된 함수 포인터를 사용하여 2) 부분에서 해당 함수를 수행한다. 수행이 끝나면 현재 3), 4) 와 같이 pdflush thread 상태를 확인하여 thread 를 더 만들 것인지 존재하는 thread 를 종료할 것인지를 결정한다.

즉 pdflush 데몬은 필요에 따라서 thread 개수를 동적으로 조절하면서 데몬이 할일을 수행한다. 즉 필요하면 증가시키고, 일이없어서 쉬고 있는 경우에는 해당 thread 를 종료시켜서 resource 를 확보한다.

그럼 리눅스 2.4 에서 사용되었던 bdflush / kupdate 즉 dirty buffer 를 디스크로 기록하는 함수가 리눅스 2.5 이상에서는 어떻게 동작하는지 확인해보자. __pdflush 코드 1)에서 보면 pdflush_list 에 넣고 누군가 깨워주기전까지 계속 sleep 하는 것으로 구현되어 있다. 따라서 pdflush_list 에 접근하여 자고 있는 thread 를 깨워주는 코드를 찾아보자.

커널을 찾아보면 pdflush_operation 함수에서 pdflush_list 를 사용하고 있다. 즉 pdflush_operation 이라는 함수에 함수 포인터와 해당 함수의 파라미터를 pdflush_list 에 달려있던 pdflush_work 구조체를 통하여 argument 로 넘기고 원하는 thread 를 pdflush thread 구조를 통해서 수행한다는 것을 알 수 있다.

<linux 2.6.28-15 mm/pdflush.c>
int pdflush_operation(void (*fn)(unsigned long), unsigned long arg0)
{
        unsigned long flags;
        int ret = 0;

        BUG_ON(fn == NULL);     /* Hard to diagnose if it's deferred */

        spin_lock_irqsave(&pdflush_lock, flags);
        if (list_empty(&pdflush_list)) {
                ret = -1;
        } else {
                struct pdflush_work *pdf;

                pdf = list_entry(pdflush_list.next, struct pdflush_work, list);
                list_del_init(&pdf->list);
                if (list_empty(&pdflush_list))
                        last_empty_jifs = jiffies;
                pdf->fn = fn;                --> 실행하고 싶은 함수 포인터 등록
                pdf->arg0 = arg0;         --> 위 함수 포인터에서 사용될 argument 등록
                wake_up_process(pdf->who);  --> 자고있는 pdflush thread 를 깨워준다.
        }
        spin_unlock_irqrestore(&pdflush_lock, flags);

        return ret;
}


여기까지 pdflush thread 를 사용하기 위해서 pdflush_operation 함수를 사용하여 원하는 함수를 등록하고 실행할 수 있다는 것을 확인하였다.

그럼 pdflush_operation 을 호출하는 상황은 어떤 경우가 있는지 확인해보자. 아래 내용은 pdflush_operation 을 호출해주는 부분을 찾아 리스팅 해놓은 것이다.

아래 내용을 리눅스 2.4 버전에서 wakeup_bdflush 를 호출한 부분과 비교해보면 유사하다는 것을 알 수 있을 것이다. 즉 2.4 에서는 bdflush / kupdate 와 같이 직접 thread 를 만들어서 수행하던 것을 2.6 버전에서는 pdflush 라는 하나의 구조를 사용하여 2.4 에서 수행해주던 bdflush / kupdate 기능을 모두 사용할 수 있도록 한 것이다.

이러한 공통 구조는 커널 프로그래밍에서 필요한 thread 가 있을 경우에 따로 thread 를 생성하지 않고 제공된 구조를 사용할 수 있게 함으로써 커널 코드를 좀더 효율적이고 분석하기 쉽게 해준다.

리눅스 2.5 이상 버전에서 사용된 background_writeout 함수는 리눅스 2.4 이하 버전에서 사용되던 bdflush 기능을 수행하고, 리눅스 2.5 이상에서 사용된 wb_kupdate 는 리눅스 2.4 이하 버전에서 kupdate 데몬을 대체한다.

background_writeout 함수로 호출하는 부분
<linux 2.6.28-15 mm/page-writeback.c>
1. static void balance_dirty_pages(struct address_space *mapping)
2. int wakeup_pdflush(long nr_pages)
   a. <fs/buffer.c>static void free_more_memory(void)
   b. <fs/sync.c> static void do_sync(unsigned logn wait)
   c. <mm/vmscan.c>
       static unsigned long do_try_to_free_pages(struct zonelist *zonelist, struct scan_control *sc)

wb_kupdate 함수로 호출하는 부분
<linux 2.6.28-15 mm/page-writeback.c>
1. static void wb_timer_fn(unsigned long unused)

마지막으로 background_writeout 함수를 살펴보면 write_inodes 함수를 호출하여 superblock 에 달려있는 모든 dirty inodes 에 대해서 dirty buffer 를 디스크로 기록하는 기능을 수행한다.


static void background_writeout(unsigned long _min_pages)
{
(생략)
        for ( ; ; ) {
(생략)
                writeback_inodes(&wbc);
                min_pages -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
                if (wbc.nr_to_write > 0 || wbc.pages_skipped > 0) {
                        /* Wrote less than expected */
                        if (wbc.encountered_congestion || wbc.more_io)
                                congestion_wait(WRITE, HZ/10);
(생략)
        }              
}


이상 dirty buffer 를 디스크로 기록해주던 thread 가 2.6 커널에서 어떤 식으로 구현되어있는지 살펴보았다.