Linux Kernel Hacker

rousalome.egloos.com

포토로그




[Linux][Kernel] panic@___might_sleep Linux Kernel - Panic/Crash

리눅스 커널 synchronization의 꽃 중의 하나인 Mutex Lock에 대해서 조금 짚어 볼께요. Mutex Lock은 보통 스핀락(Spinlock)과 많이 비교하죠. 사실 소스 코드를 보면 Mutex Lock이 스핀락보다 훨씬 소프트웨어적으로 복잡해요. 그 이유는?
1> Mutex Lock을 잠근 프로세스만 해제할 수 있어요
2> 이미 다른 프로세스가 Mutex Lock을 획득한 상태면 struct mutex.wait_list에 대기하고 Wait Queue에 넣고 잠들어야 해요.
음, 이 소리는. Mutex Lock을 잡고 있는 프로세스가 Mutex Lock을 해제하면 누군가가 다시 대기 중이던 프로세스를 WaitQueue에서 끄집어 내서 런큐에 큐잉을 해줘야겠죠. 

마지막으로 Mutex Lock을 잡는 동작에서 Sanity-Check을 해주는 루틴이 있어요. 이 역할을 하는 함수가 ___might_sleep()이거든요. 함수의 구현부는 아래와 같은데요. 아래 if문 3개의 조건을 만족하면 return되는데, return이 안되면 커널 크래시가 기다리고 있어요.
void ___might_sleep(const char *file, int line, int preempt_offset)
{
 static unsigned long prev_jiffy; /* ratelimiting */
 unsigned long preempt_disable_ip;

 rcu_sleep_check(); /* WARN_ON_ONCE() by default, no rate limit reqd. */
 if ((preempt_count_equals(preempt_offset) && !irqs_disabled() &&  //<<--
      !is_idle_task(current)) || oops_in_progress)
  return;
 if (system_state != SYSTEM_RUNNING &&
     (!__might_sleep_init_called || system_state != SYSTEM_BOOTING))
  return;
 if (time_before(jiffies, prev_jiffy + HZ) && prev_jiffy)
  return;
 prev_jiffy = jiffies;

 /* Save this before calling printk(), since that will clobber it */
 preempt_disable_ip = get_preempt_disable_ip(current);

 printk(KERN_ERR
  "BUG: sleeping function called from invalid context at %s:%d\n",
   file, line);
 printk(KERN_ERR
  "in_atomic(): %d, irqs_disabled(): %d, pid: %d, name: %s\n",
   in_atomic(), irqs_disabled(),
   current->pid, current->comm);
//snip
#ifdef CONFIG_PANIC_ON_SCHED_BUG
 BUG();  //<←

그런데 위 코드에서 커널 패닉이 죽는 덤프를 봤거든요. 
콜스택은 아래와 같구요. Binder 드라이버에서 Mutex Lock을 잡는 동작에서 커널 크래시가 발생한 거네요.
-000|do_debug_exception(addr = 0, esr = 0, regs = 0x0)
-001|el1_dbg(asm)
 -->|exception
-002|___might_sleep(file = 0xFFFFFF8BA9C4C2F3, ?, ?)  //<<-- panic
-003|__might_sleep(file = 0xFFFFFF8BA9C4C2F3, line = 98, preempt_offset = 0)
-004|mutex_lock(lock = 0xFFFFFFCCF32FC938)
-005|binder_defer_work(proc = 0xFFFFFFCCE000FC80, ?)
-006|binder_flush(filp = 0xFFFFFFCC998D8300, ?)
-007|filp_close(filp = 0xFFFFFFCC998D8300, id = 0xFFFFFFCD472DBAC0)
-008|__close_fd(files = 0xFFFFFFCD472DBAC0, ?)
-009|task_close_fd.isra.33(?)
-010|binder_transaction_buffer_release(proc = 0xFFFFFFCD46098480, buffer = 0xFFFFFF801F12D270, failed_at
-011|binder_transaction(?, thread = 0xFFFFFFCCE000EC80, tr = 0xFFFFFFCCA944BC30, ?, extra_buffers_size =
-012|binder_thread_write(proc = 0xFFFFFFCCE000FC80, thread = 0xFFFFFFCCE000EC80, ?, ?, consumed = 0xFFFFF
-013|binder_ioctl_write_read(filp = 0xFFFFFFCC998D8300, ?, arg = 549020159624, thread = 0xFFFFFFCCE000EC8
-014|binder_ioctl(filp = 0xFFFFFFCC998D8300, cmd = 3224396289, arg = 549020159624)
-015|vfs_ioctl(inline)
-015|do_vfs_ioctl(filp = 0xFFFFFFCC998D8300, fd = 1, ?, arg = 549020159624)
-016|SYSC_ioctl(inline)
-016|sys_ioctl(fd = 1, cmd = 3224396289, arg = 549020159624)

커널 패닉이 발생한 이유는 해당 프로세스의 struct thread_info의 preempt_count값이 2이기 때문이죠.
(struct thread_info*)(0xffffffcca944b880 & ~0x3fff) = 0xFFFFFFCCA9448000 -> (
    flags = 0x2,
    addr_limit = 0x0000008000000000,
    task = 0xFFFFFFCD03CC1FC0,
    ttbr0 = 0x44510000BB944000,
    preempt_count = 0x2,  //<<--
    cpu = 0x6)

다시 말하자면, preempt_disable() 함수를 두 번 호출했다는 거죠.

preempt_disable()의 구현부를 보면, 간단히 struct thread_info.preempt_count 값만 +1시키고 있어요.
#define preempt_disable() \
do { \
 preempt_count_inc(); \
 barrier(); \
} while (0)

사실 이 값 struct thread_info.preempt_count이 0이어야만 preemption이 되어 스케쥴이 가능하거든요. 그 이유는 다른 섹션에서 다루기로 하구요. (어셈블코드 리뷰가 필요할 것 같네요.)
아무리 눈을 씻고 찾아봐도, 그리 논리적 오류가 될만한 코드가 없어 보여서,
아래와 같은 패치를 반영했더니, 크래시는 사라졌어요.

원리는 잠시 preempt_count를 -1 시켜서 _mutex_sleep에서 크래시를 회피하고 이후,
다시 원래 값으로 돌리는 거죠.
diff --git a/drivers/android/binder.c b/drivers/android/binder.c
index d1490be..eea603b 100644
--- a/drivers/android/binder.c
+++ b/drivers/android/binder.c
@@ -458,7 +458,9 @@ static long task_close_fd(struct binder_proc *proc, unsigned int fd)
        if (proc->files == NULL)
                return -ESRCH;

+       preempt_enable_no_resched();
        retval = __close_fd(proc->files, fd);
+       preempt_disable();
        /* can't restart close syscall because file table entry was cleared */
        if (unlikely(retval == -ERESTARTSYS ||
                     retval == -ERESTARTNOINTR ||

관련 매크로는 아래 코드를 참고하세요.
#define preempt_enable_no_resched() sched_preempt_enable_no_resched()

#define sched_preempt_enable_no_resched() \
do { \
 barrier(); \
 preempt_count_dec(); \
} while (0)

 #define __preempt_count_dec() __preempt_count_sub(1)


덧글

댓글 입력 영역