Byeo

[Linux Kernel] Blocking I/O 본문

프로그래밍 (Programming)/네트워크 스택

[Linux Kernel] Blocking I/O

BKlee 2024. 5. 17. 18:04
반응형

개요

Blocking I/O의 동작 방법을 공부해 봅니다. (LDD (https://www.makelinux.net/ldd3/) 3판 section 6.2 요약)

 

sys_accept을 공부하다가 blocking I/O에 대해서는 따로 간단히 정리를 해보면 좋겠다 싶어서 작성하게 되었습니다.

 

 

Blocking I/O

 하드웨어 장비는 언제나 작업을 처리할 수 있는 것은 아니다. 예를 들어, NIC을 통해서 data를 내보내고 싶은데 (send, TX), 이미 수많은 작업들이 쌓여있어서 하드웨어가 이를 수용하지 못할 수 있다. 반대로, application이 무언가 data를 받기를 기대하는데 (recv, RX) 실제로는 NIC에 들어온 data가 없을 수도 있다. 하지만 우리가 작성하는 프로그램은 하드웨어 상태가 어떤지 전혀 신경 쓰지 않는다. 단순히 작업이 잘 수행되고 나서 syscall (write, read)이 return 하기만을 기다릴 뿐! 

 

 즉, application은 syscall이 문제 없이 return 되었을 때 기대하는 상황이 있다. 이를 충족할 수 없는 경우에는 syscall을 호출한 process를 잠시 block 하고, 충족할 수 있는 상황이 될 때까지 쉬어야 한다. 

 

 이 장에서는 process가 어떻게 sleep하고 wake 하는지 다룬다.

 

1) Introduction to Sleeping

 sleep상태란? process가 sleep상태에 빠지게 되면, 해당 프로세스는 특별한 상태로 마크되며 scheduler's run queue에서 제거된다. 다른 누군가가 해당 상태를 바꾸기 전까지 process는 어떠한 CPU에서도 실행되지 않는다. 

 

 Device driver가 process를 sleep시키는 것은 어렵진 않다. 그러나 안전하게 sleep 하기 위해서 지켜야 하는 몇 가지 규칙이 존재한다:

  • 1. atomic context에서 sleep하지 말 것. atomic context는 여러 명령어 라인이 다른 간섭 없이 실행되기로 명시된 상태이다. 즉, spinlock, seqlock, RCU lock 등을 지닌 채(acquire) sleep 하면 안 된다.
  • 2. interrupt가 disable 상태인데 sleep 하지 말 것. 
  • 3. sehmaphore를 지닌 상태에서 sleep 하면, semaphore를 기다리는 다른 모든 thread들도 sleep에 빠지므로 유의. 

4. wake 했다고 어떠한 상태라고 가정하지 말 것.

 sleep에서 주의해야 할 또 하나는, process가 깨어난 뒤 실제로 얼마나 흘렀는지, 상태가 어떻게 변화했는지 알지 못한다는 것이다. 또한 다른 process가 같은 event를 기다리기 위해서 sleep 하고 있었는지도 알지 못한다. 이 말인즉슨, process A와 B가 같은 이벤트를 기다리고 있었고, A가 B보다 먼저 깬 경우, A가 자원을 먼저 사용해 B는 쓸 수 있는 자원이 없게 될 수 있다는 것이다. 즉, event에 의해 깨긴 했어도 어떠한 상황도 가정할 수 없다. 

(작성자 예시: e.g., output buffer가 꽉 찼는데 A와 B가 동시에 Tx를 하고 싶어 했다고 해보자. 이 둘은 Tx가 불가능하므로 sleep에 빠질 것이다. 이후, output buffer가 비어서 A와 B가 event를 받아 일어났는데 A가 먼저 Tx를 해버렸다. 다시 output buffer가 꽉 차면 B는 여전히 Tx가 불가능한 상태가 된다.)

 

5. 당연하지만, process의 sleep을 깨어줄 누군가가 없다면 sleep해선 안된다.

 깨우는 process는 sleep 상태의 process를 찾을 수 있어야 한다. 그리고 무엇에 의해서 wakeup 될 지도 알아야 한다. 이건 작성하는 코드의 몫. sleeping process를 어떻게 찾을지는 wait queue를 통해서 가능하다. wait queue는 특정 event를 기다리는 모든 waiting process들의 list라고 볼 수 있을 것 같다.

 

 Linux에서 wait queue는 wait_queue_head_t 타입의 구조체인 "wait queue head"를 통해 관리된다. 이 wait queue head는 다음 코드를 통해서 정의하고 초기화할 수 있다.

DECLARE_WAIT_QUEUE_HEAD(name);

 

또는

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

방식으로!

 

2) Simple Sleeping

sleep

 Process가 sleep상태에 들어간다는 것은 미래에 어떤 조건 (특정 상황)이 true가 되는 것을 기다린다는 것과 의미가 같다. 위에서 언급했듯, sleep상태에 들어가는 process는 미래에 자신이 원하는 조건이 true가 될 수 있는지 반드시 확인하고 들어가야 한다.

 Linux Kernel에서 sleep 하는 가장 쉬운 방법은 wait_event라는 매크로를 호출하는 것이다. wait_event는 상황에 맞게 쓸 수 있도록 다양한 형태가 존재한다.

 

wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

 

queue는 사용할 wait queue head이다. pointer가 아닌 value로서 전달된다. condition은 임의의 boolean 표현이며, 조건이 true가 되기 전까지 sleep 한다. 이 condition은 수시로 체크되기 때문에 side effect가 존재해서는 안된다. 

 

 wait_event를 사용한다면, process는 uninterruptible sleep에 빠진다. interruptible 상태를 원한다면 wait_event_interruptible을 사용하면 된다. 이 매크로는 integer value 하나를 반환하는데, 0이 아니면 sleep이 어느 signal에 의해 interrupt 되었음을 의미한다. timeout이 붙은 매크로는 지정된 jiffie가 지나면 condition과 상관없이 0을 return 하는 매크로다. 

 

참고

wait_event 매크로

/**
 * wait_event - sleep until a condition gets true
 * @wq_head: the waitqueue to wait on
 * @condition: a C expression for the event to wait for
 *
 * The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
 * @condition evaluates to true. The @condition is checked each time
 * the waitqueue @wq_head is woken up.
 *
 * wake_up() has to be called after changing any variable that could
 * change the result of the wait condition.
 */
#define wait_event(wq_head, condition)						\
do {										\
	might_sleep();								\
	if (condition)								\
		break;								\
	__wait_event(wq_head, condition);					\
} while (0)

 

 

wakeup

 다른 thread (다른 process, interrupt handler, 등등)는 sleep process에 대해 wakeup을 수행할 수 있다. wake_up 매크로가 존재하며, 변형으로 다음과 같이 1개가 추가로 더 있다.

 

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

 

wake_up은 queue에 있는 모든 process들을 깨운다. wake_up_interruptible은 queue에서 interruptible sleep들만 모두 깨운다. 즉, wait_event와 wake_up이 pair이고, wait_event_interruptible과 wake_up_interruptible이 pair라고 보면 될 것 같다.

 

Example

아래 코드는 sleep과 wake_up의 예제이다. sleepy_read를 호출하면 sleep에 빠지고, 다른 누군가가 write를 하면 잠자고 있던 read의 sleep을 깨운다.

static DECLARE_WAIT_QUEUE_HEAD(wq);
static int flag = 0;

ssize_t sleepy_read (struct file *filp, char _ _user *buf, size_t count, loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) going to sleep\n",
            current->pid, current->comm);
    wait_event_interruptible(wq, flag != 0);
    flag = 0;
    printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
    return 0; /* EOF */
}

ssize_t sleepy_write (struct file *filp, const char _ _user *buf, size_t count,
        loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
            current->pid, current->comm);
    flag = 1;
    wake_up_interruptible(&wq);
    return count; /* succeed, to avoid retrial */
}

 

 물론 이 코드도 정답은 아니다! sleepy_read를 두 번 호출해서 두 process가 잠들었다가 동시에 깨면, 두 process가 모두 flag=0을 볼 수 있기 때문 (multicore race condition의 자세한 내용은 스킵). 따라서 atomic manner로 처리해야 함에 유의하자!

 

3) Blocking and Nonblocking Operations

 또 한 가지 중요한 점은, process를 언제 잠들게 할 것이냐에 대한 결정을 하는 것이다.

 

application이 명시적으로 blocking을 하지 않길 원하는 경우도 있다. 이는 O_NONBLOCK flag로서 전달되며, filp->f_flags를 통해 확인할 수 있다. 기본적으로 filp->f_flags에는 O_NONBLOCK이 set 되어 있지 않으며, 따라서 blocking이 기본 동작이다. blocking의 경우 다음 표준 시맨틱을 따라야 한다.

 

Blocking

  •  read를 요청했으나 data가 아직 없는 경우, process는 반드시 block 되어야 한다. 이 sleep은 data가 도착하자마자 awakened 되어야 하며 user에게 data를 전달해야 한다. 이는 application이 요청(기대)하고 있던 byte보다 적더라도 return 해야 한다. (그래서 read함수는 반드시 while문으로 감싸야한다!)
  • write를 요청했으나 buffer에 공간이 없는 경우, process는 block 해야 한다. 그리고 read wait queue와 다른 queue를 사용해야 한다. buffer에 공간이 생긴 경우, awakend 되어야 하며 write call은 return 한다. 이때, buffer에 공간이 충분치 않아서 사용자가 요청한 write data 길이보다 적게 쓰이더라도 return 해야 한다.

이 시맨틱은 input buffer와 output buffer가 존재할 때를 가정한다. 그리고 대부분의 device driver는 실제로 두 buffer를 갖고 있다. input buffer는 누구도 읽기 전에 data가 도착할 때를 대비해서 반드시 필요하다. 그렇지 않으면 잃을 테니.. 하지만 write 과정에서는 사실 lose가 발생할 일이 없어서 output buffer를 반드시 요구하는 것은 아니다. 왜냐면 실패하더라도 userspace buffer에 남기 때문.

 

 그렇더라도 성능을 위해서 대부분 output buffer를 갖는다. (1) output buffer가 없으면 write system call이 실패했을 때 userspace는 추후에 write를 다시 호출해야 하는데, 이 비용이 만만치 않다. blocking, context switch, kernel/user transition이 반복되기 때문. (2) output buffer가 충분히 있으면 단 한 번의 write system call로 처리해 버릴 수 있다.

 

NonBlocking

O_NONBLOCK이 명시된 경우, 위의 시맨틱에서 blocking이 되어야 할 상황에 block 되지 않고 -EAGAIN을 return 한다. Application이 만약 O_NONBLOCK을 사용하는 경우, 반드시 errno를 통해 이 -EAGAIN을 확인해야 한다. device driver에서는 read, write, open file operation만 nonblocking flag의 영향을 받는다.

 

 

4) A Blocking I/O Example

다음은 scullpipe driver에서 가져온 예제이다. driver 코드에서는, read call에 의해 block 된 process는 data가 도착할 때 awakened 된다. 일반적으로 하드웨어는 interrupt를 issue 하고, driver는 이 interrupt를 handling 하기 위해 sleep상태의 process를 깨우는 구조로 이뤄진다. 예제로 다루는 scullpipe driver는 특정 하드웨어나 interrupt handler 없이 동작할 수 있는 (toy?) 드라이버라 조금 다름에 유의하자. 

 

 Device driver는 두 개의 wait queue와 하나의 buffer를 포함한 구조체를 사용한다. buffer size는 configurable 하다.

struct scull_pipe {
        wait_queue_head_t inq, outq;       /* read and write queues */
        char *buffer, *end;                /* begin of buf, end of buf */
        int buffersize;                    /* used in pointer arithmetic */
        char *rp, *wp;                     /* where to read, where to write */
        int nreaders, nwriters;            /* number of openings for r/w */
        struct fasync_struct *async_queue; /* asynchronous readers */
        struct semaphore sem;              /* mutual exclusion semaphore */
        struct cdev cdev;                  /* Char device structure */
};

 

read의 구현은 다음과 같다. 이 구현의 read는 blocking과 nonblocking을 모두 커버한다.

static ssize_t scull_p_read (struct file *filp, char _ _user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_pipe *dev = filp->private_data;

    if (down_interruptible(&dev->sem))
        return -ERESTARTSYS;

    while (dev->rp =  = dev->wp) { /* nothing to read */
        up(&dev->sem); /* release the lock */
        if (filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
        if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
            return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
        /* otherwise loop, but first reacquire the lock */
        if (down_interruptible(&dev->sem))
            return -ERESTARTSYS;
    }
    /* ok, data is there, return something */
    if (dev->wp > dev->rp)
        count = min(count, (size_t)(dev->wp - dev->rp));
    else /* the write pointer has wrapped, return data up to dev->end */
        count = min(count, (size_t)(dev->end - dev->rp));
    if (copy_to_user(buf, dev->rp, count)) {
        up (&dev->sem);
        return -EFAULT;
    }
    dev->rp += count;
    if (dev->rp =  = dev->end)
        dev->rp = dev->buffer; /* wrapped */
    up (&dev->sem);

    /* finally, awake any writers and return */
    wake_up_interruptible(&dev->outq);
    PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);
    return count;
}

 

scull_p_read 함수를 살펴보면, while loop에서 device semaphore를 holding 한 채 buffer를 검사한다. 만약 data가 존재하면 while 문 자체를 skip 한다. 만약 buffer가 비어있다면 sleep 해야만 한다. 이를 하기 전에, 1)에서 다뤘던 대로 semaphore를 놓아야 한다. lock을 release 하고 filp->f_flags를 확인한 뒤, O_NONBLOCK이면 바로 return 한다. (왜 nonblocking check는 semaphore를 release 한 뒤에 하는 걸까?) blocking이면 wait_event_interruptible을 호출한다.

 

 누군가가 이제 깨웠다고 가정하자. 그래도 우리는 아무것도 모른다.

 

 1. 한 가지 가능성은 process가 signal을 받은 것. wait_event_interruptible 함수를 감싸고 있는 if문은 signal이 수신된 것인지 확인한다. 0이 아니면 interrupt에 의해 wakeup 된 것이므로 더 이상 진행하지 않는다. 또한, signal이 도착했는데 blocking상태가 아니었다면, upper layer에게 -ERESTARTSYS를 전달한다. 이 값은 virtual filesystem (VFS) layer에서 사용되며, 이 값을 받으면 system call을 재실행하거나 user에게 -EINTR를 반환한다.

 

 2. 하지만, signal이 없었더라도 여전히 읽을 수 있는 data가 잘 존재하는지는 보장하지 못한다. 다른 thread가 race에서 이겨서 data를 먼저 가져갔을 수도 있기 때문. 이 가능성을 배제하기 위해서 semaphore를 획득한 뒤 while loop에서 다시 검증한다. 즉, loop를 빠져나가는 경우에는 우리는 semaphore lock을 acquire 했고, data는 존재한다는 것을 보장할 수 있다.

 

추가로, scull_p_read가 또 다른 지점에서 sleep 할 수 있다. 예를 들어 copy_to_user를 수행한다고 하자. scull은 kernel에서 user로 data를 copy 하는 동안 semaphore를 hodling 한 채 sleep 한다면, 이는 문제없다. kernel이 copy_to_user 작업이 끝난 뒤 sleep process를 깨워줄 텐데, 이때 semaphore를 사용하지 않기 때문. 따라서 deadlock이 발생하지 않는다. 또한, semaphore를 지니고 있었기 때문에 device memory는 driver가 sleep 하는 동안 그대로 유지된다.

 

5) Advanced Sleeping

지금까지 다룬 내용만으로도 device driver에 필요한 sleep을 구현할 수는 있으나, 복잡한 locking이나 성능이 필요한 경우 더 깊은 sleep에 대한 이해를 필요로 한다.

 

How a process sleeps

linux/wait.h를 살펴보면, wait_queue_head_t라는 단순한 구조체를 볼 수 있다. 이는 spinlock과 linked list로 구성 되어 있다. 이 list는 wait_queue_t로서 wait_queue의 entry이다. 이 구조체가 sleeping process에 대한 정보와 어떻게 깰 지에 대해서 정보를 갖고 있다.

// linux-5.15/include/linux/wait.h:27

/*
 * A single wait-queue entry structure:
 */
struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};

struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};

 

첫 번째 단계는 wait_queue_t 구조체를 할당받고 초기화하는 것이다. 이 것이 제대로 queue에 추가된다면, 이를 깨워야 하는 process가 sleep process를 정확히 잘 찾을 수 있을 것이다.

 

 다음 단계는 sleep에 들어갈 process의 state를 mark 하는 것이다. linux/sched.h에 process가 가질 수 있는 몇몇 상태들이 정의되어 있는데, TASK_RUNNING은 언제든지 실행가능한 상태를 의미한다. process가 sleep상태의 경우, TASK_INTERRUPTIBLE 또는 TASK_UNINTERRUPTIBLE 두 개의 상태를 가질 수 있다. 

 linux 2.6부터는 직접 process state를 조작하는 것이 아니라 set_current_state를 통해서 바꿀 수 있다. 

 

 마지막 단계는 processor를 포기 (yield)하는 것이다. 단, 포기하기 전에 마지막으로 condition을 한번 더 체크해야 한다. 상황이 그 사이에 변화해서 event가 processor 포기보다 먼저 도착했을 수 있기 때문. 따라서 조건을 체크하지 않는다면 예상보다 더 오래 sleep에 빠질 수 있다.

if (!condition)
    schedule(  );

 

condition을 process state를 변경하고 나서 한 번 더 확인함에 따라, 모든 발생 가능한 event 순서 문제를 커버할 수 있다. 만약 event가 process state를 변경하기 전에 도착했다면, 이 condition check를 한 번 더 함으로써 sleep을 방지할 수 있다. 만약 그 이후에 도착했다면, process는 다시 runnable상태가 될 것이다.

 

 schedule함수는 scheduler를 호출하고 CPU를 yield 하게 만드는 함수이다. 

 

 이후 정리 작업도 필요하다. if condition이 true여서 schedule()를 호출하지 않았다면, process state를 다시 TASK_RUNNING으로 변경해야 하고 wait queue에서 지워야 한다. 만약 schedule()에서 return 되었다면, 이 단계는 불필요하다. process가 runnable 되기 전까지 return 되지 않을 것이기 때문! 

 

Manual sleeps

 지금까지의 과정을 모두 일일이 하는 것은 성가신 일이다. 이를 조금 더 쉽게 하는 방법을 소개한다.

 

 첫 번째 단계는 wait queue entry를 생성하고 초기화하는 것이다.

DEFINE_WAIT(my_wait);

 

 다음 단계는 wiqt queue entry를 queue에 넣고 process state를 설정하는 것이다. 이 과정을 다음 함수로 처리할 수 있다.

void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state);

 

queue와 wait은 각각 wait queue head와 process entry를 의미한다. state는 변경할 새로운 state 값이다. 값은 TASK_INTERRUPTIBLE이거나 TASK_UNINTERRUPTIBLE이어야 한다.

 

 prepare_to_wait을 호출한 다음, process는 schedule을 호출할 수 있다. 물론 조건을 한 번 더 체크하고 나서. schedule이 return 되면 resource들을 정리하면 된다. 이 역시도 다음 함수를 통해서 쉽게 할 수 있다.

void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);

 

그다음으로 state를 검사하고 wait을 다시 해야 할지 등을 판단하면 된다.

 

다음 예제는 write에서 buffer가 있는지 검사하고 sleep 여부를 결정하는 작업을 수행한다.

/* Wait for space for writing; caller must hold device semaphore.  On
 * error the semaphore will be released before returning. */
static int scull_getwritespace(struct scull_pipe *dev, struct file *filp)
{
    while (spacefree(dev) == 0) { /* full */
        DEFINE_WAIT(wait);
        
        up(&dev->sem);
        if (filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
        PDEBUG("\"%s\" writing: going to sleep\n",current->comm);
        prepare_to_wait(&dev->outq, &wait, TASK_INTERRUPTIBLE);
        if (spacefree(dev) == 0)
            schedule(  );
        finish_wait(&dev->outq, &wait);
        if (signal_pending(current))
            return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
        if (down_interruptible(&dev->sem))
            return -ERESTARTSYS;
    }
    return 0;
}

 

 언급했던 DEFINE_WAIT을 통해서 wait queue entry를 초기화한다. 그리고 semaphore를 release 하고 NONBLOK을 체크한 뒤, prepare_to_wait을 통해서 wait을 준비한다. 마지막으로 진짜 잠들기 전에 spacefree 함수를 통해서 조건을 한 번 더 체크한다. 이 체크가 없다면, scull_reader process가 해당 buffer를 진짜로 완전히 비워버렸을 경우 wakeup을 놓쳐서 영원히 잠들어버릴 수 있기 때문.

 

 여전히 의문이 남는 케이스가 있다. (글을 읽으면서 계속 의아했다! "lock을 놔버렸는데 if와 schedule 사이에서 도착하는 wakeup은 loss가 아니야?") wakeup이 if의 테스트 이후, 그리고 schedule 이전에 오면 어떻게 될까?  이 때도 문제가 없다. wakeup은 process state를 다시 TASK_RUNNING으로 바꿔놓았을 것이고, schedule은 바로 return 할 것이기 때문. test가 어쨌든 process 자기 자신을 wait queue에 넣고 state를 바꿔놓았다면, 문제는 없다.

 

그림을 그려보면 대충 다음과 같을 것 같다!

 

 마지막으로 finish_wait을 호출한다. signal_pending은 signal에 의해서 깬 것인지 확인하는 것이다. 만약 그렇다면 user에게 이를 알려서 조치를 취하도록 해야 한다. 

 

Exclusive waits

 앞서 설명했듯, wake_up은 wait_queue에 있는 모든 process를 runnable로 만든다. 대부분의 상황에서 정상적인 행동이다. 하지만, 몇몇 상황에서는 어차피 단 하나의 process만 해당 resource를 가져갈 수 있는 경우가 있다 (그리고 이를 코딩할 때 알고있다.). 이 때에는 resource를 획득한 어느 한 process를 제외하고 awakened된 다른 process들은 결국 다시 잠들 수 밖에 없는 구조다. 

 결국, 이러한 상황에서 waiting process N개 있다면 N개의 process를 전부 깨우고 N-1개는 다시 잠드는 자명한 상황이 되는 것이다. 이를 "thundering herd" 라고 칭하며, 성능에 많은 영향을 미친다.

 

  이 "thundering herd"를 해결하기 위해서, 커널 개발자는 "exclusive wait" option을 kernel에 추가했다. 일반적인 sleep과 비슷하지만, 2가지 차이점이 존재한다:

  • 1. wait queue entry가 WQ_FLAG_EXCLUSIVE flag bit를 마크한 상태라면, wait queue의 마지막에 추가된다. 만약 마크되지 않았다면 보통처럼 wait queue list의 처음에 추가된다.
  • 2. wake_up이 불린 상황에서 wait queue의 process들을 깨울 텐데, WQ_FLAG_EXCLUSIVE flag bit가 마크된 wait queue entry를 깨웠다면 더 깨우는 작업을 중단한다.

결국, exclusive wait process는 단 하나만 깨어진다. 그리고 먼저 잠든게 먼저 깬다. 단, 이 상황에서도 nonexclusive wait queue entry는 모두 깨운다는 점은 유의하자.

 

 exclusive wait은 다음 두 조건을 만족하면 사용해볼만 하다.

  • 1. resource간에 상당한 contention이 예상되는 경우
  • 2. 그리고 한 process만 깨워도 resource를 모두 소진하는데 충분한 경우

 Apache Web Server의 경우가 좋은 사례일 것으로 보인다.: 새로운 연결 요청이 하나 들어오면 Apache process들 중에서 오로지 하나만 깨서 처리하면 된다. 

 

사용 방법은 다음과 같다.

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);

 

The details of waking up

  몇몇 wakeup 함수의 variants를 소개한다.

wake_up(wait_queue_head_t *queue);
wake_up_interruptible(wait_queue_head_t *queue);

 

 wake_up은 queue에 있는 모든 non-exclusive wait process들을 깨우고, exclusive wait process를 1개만 깨운다. wake_up_interruptible은 uninterruptible sleep만 제외하고 나머지들에 대해서 동일하게 동작한다. 이들은 return 하기 전에, 하나 혹은 여러개의 깨어진 process들을 schedule할 수 있다.

 

wake_up_nr(wait_queue_head_t *queue, int nr);
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr);

 

 이 함수는 wake_up과 비슷하다. 단, exclusive waiter를 nr개 깨울 수 있다는 점만 제외하고! nr에 0을 주면 모든 exclusive waiter들을 깨운다.

 

wake_up_all(wait_queue_head_t *queue);
wake_up_interruptible_all(wait_queue_head_t *queue);

 

 exclusive waiter들도 포함해서 모두 깨운다. interruptible은 interruptible로 마크된 waiter들을 대상으로 모두 깨운다.

 

wake_up_interruptible_sync(wait_queue_head_t *queue);

 

 보통 일어난 process는 현재 실행중인 process를 중단하고 schedule된다. 다른 말로, wake_up은 atomic이 아니라는 이야기. 따라서 wake_up이 atomic context 내에서 실행중이라면, 일반 적인 경우에는 scheduling이 일어나지는 않는다.

 

 하지만 만약 명백하게 rescheduling이 되지 않도록 해야 한다면, sync가 붙은 이 함수를 사용하면 된다. 

 

끝!

반응형
Comments