Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이] Process - 대기큐(Wait queue)- (1) [라즈베리파이] 커널 프로세스

대기 큐(wait queue) 소개
대기 큐는 커널에서 여러 용도로 사용합니다. 특히 인터럽트 핸들링과 프로세스 동기화, 타이밍으로 씁니다. 프로세스는 디스크 연산이 끝나기를 기다리거나, 시스템 리소스가 해제되기를 기다리며 시간이 얼마간 흐르길 기다려야 할 때가 있습니다

대기 큐는 여러 이벤트에 대한 조건부로 대기를 구현하고 표현합니다. 특정 이벤트를 기다리는 프로세스는 적절한 대기 큐에 자기 자신을 넣고 CPU 제어를 포기합니다. 그러므로 대기 큐는 잠자고 있는 프로세스들이 모여있는 장소라고 볼 수 있습니다. 이 프로세스들은 특정 조건이 true가 되면 커널이 깨워줍니다. 

대기 큐는 이중 링크드 리스트로 구현되어 있으며, 이 리스트의 각 개체에는 프로세스 디스크립터를 가리키는 포인터가 들어있습니다. 대기 큐 각각은 wait_queue_head_t 구조체 타입의 대기 큐 헤드에 의해 식별할 수 있습니다. 
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;

대기 큐는 커널의 주요 기능 뿐만 아니라 인터럽트 핸들러에 의해서도 씁니다. 따라서 대기 큐 리스트는 프로세스가 동시에 접근해서 레이스 컨디션이 발생해 의도치 않은 결과가 발생하지 않도록 보호를 잘 해야 합니다. 동기화는 대기 큐의 헤드에 있는 lock 이라는 스핀락으로 수행합니다. task_list 필드는 대기 중인 프로세스 리스트의 헤드입니다.. 

wait_queue_entry  타입의 대기 큐 개체는 다음과 같습니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h]
1 struct wait_queue_entry {
2 unsigned int flags;
3 void *private;
4 wait_queue_func_t func;
5 struct list_head entry;
6 };

5번째 줄 코드인 entry 멤버는 잠들어 있는 프로세스를 표현하고, 이 프로세스들은 어떤 이벤트가 발생하길 기다리고 있습니다.
 
동일한 이벤트가 발생되기를 기다리는 프로세스 리스트들을 가리키는 포인터가 들어있다.  프로세스의 디스크립터 주소는 task 필드에 저장돼 있습니다.

그러나 대기 큐에 들어있는 모든 잠든 프로세스를 깨우는 게 항상 편리한 것은 아닙니다. 예를 들어 2개 이상의 프로세스가 배타적인 접근 권한의 리소스가 해제되길 기다리고 있다면, 대기 큐에서 프로세스 하나만 깨우는 게 합리적입니다. 이 프로세스가 리소스를 가져가고, 나머지 프로세스들은 계속 자고 있어야 합니다.

그러므로 잠자는 프로세스에는 두가지 종류가 있습니다. 

배타적 프로세스(대기 큐 개체의 flags 필드가 1로 표기됨)는 커널이 선택해서 깨웁니다. 
그리고 비배타적 프로세스(flags 필드가 0)는 이벤트가 발생했을 때 커널이 항상 깨워줍니다. 한번에 한 프로세스에게만 접근 권한을 주는 리소스를 기다리고 있는 프로세스는 전형적인 배타적 프로세스라 할 수 있습니다. 

디스크 블럭 전송이 완료되기를 기다리는 프로세스 그룹들을 생각해 봅시다.
이들 프로세스는 전송이 완료되자마자 모두 깨어나야 합니다. 나중에 살펴보겠지만, func 필드는 대기 큐에 잠들어 있는 프로세스를 어떻게 깨워야 하는지를 지정하는 데 사용됩니다. 

대기 큐 처리하기
새로운 대기 큐는 DECLARE_WAIT_QUEUE_HEAD(name) 매크로를 사용하여 정의합니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h#L58]
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.head = { &(name).head, &(name).head } }

#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

이 매크로는 name이라는 새로운 대기 큐 헤드 변수를 선언하고, lock과 head 필드를 초기화합니다.
init_waitqueue_head() 함수는 동적으로 할당되었던 대기 큐 헤드 변수를 초기화합니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h]
static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p)
{
wq_entry->flags = 0;
wq_entry->private = p;
wq_entry->func = default_wake_function;
}

비배타적 프로세스인 p는 default_wake_function() 이라는 함수에 의해 깨어날 것이며,  try_to_wake_up() 함수의 랩퍼 함수입니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L3616]
int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags,
  void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}

또 다른 방법으로는 DEFINE_WAIT 매크로를 사용하는 겁니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h#L999]
#define DEFINE_WAIT_FUNC(name, function) \
struct wait_queue_entry name = { \
.private = current, \
.func = function, \
.entry = LIST_HEAD_INIT((name).entry), \
}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

다음 코드를 보면 wait_queue_t 변수를 선언하고, 이 변수를 현재 CPU에서 돌고 있는 프로세스의 디스크립터와 autoremove_wake_function() 함수의 주소로 초기화합니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/drivers/char/nwbutton.c#L31]
static DECLARE_WAIT_QUEUE_HEAD(button_wait_queue); /* Used for blocking read */

autoremove_wake_function() 함수는 잠들어 있는 프로세를 깨우기 위해 default_wake_function()를 호출하고 그런 다음 대기 큐리스트에서 대기 큐 개체를 제거합니다.  
[https://elixir.bootlin.com/linux/v3.19.8/source/kernel/sched/wait.c#L291]
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wait, mode, sync, key);

if (ret)
list_del_init(&wait->task_list);
return ret;
}

잠들어 있는 프로세를 깨우기 위해 default_wake_function()를 호출하고 그런 다음 대기 큐리스트에서 대기 큐 개체를 제거합니다.

마지막으로 커널 개발자는 init_waitqueue_func_entry() 함수를 호출하여 사용자 정의 wake up 함수를 설정할 수 있습니다. 

주요 대기큐 함수들

add_wait_queue() 함수
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/wait.c#L24]
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;

wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
개체가 정의되고나면 대기 큐를 삽입해야 합니다. add_wait_queue() 함수는 비배타적 프로세스를 대기 큐 리스트의 맨 처음에 삽입합니다. 

add_wait_queue_exclusive() 함수 
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/wait.c]
void add_wait_queue_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;

wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue_entry_tail(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}

배타적 프로세스를 대기 큐의 맨 마지막에 삽입합니다.

remove_wait_queue() 함수 
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/wait.c]
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;

spin_lock_irqsave(&wq_head->lock, flags);
__remove_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}

대기 큐 리스트에서 프로세스를 제거합니다.

waitqueue_active() 함수
static inline int waitqueue_active(struct wait_queue_head *wq_head)
{
return !list_empty(&wq_head->head);
}

해당 대기 큐 리스트가 비어있는지 확인합니다.

덧글

댓글 입력 영역