스핀락(spinlock)과 뮤텍스(mutex)를 잘못 쓴 커널 코드는 대부분 조용히 지나간다. 초기화하지 않은 스핀락은 0으로 채워진 메모리라 x86에서는 그냥 동작하고, 스핀락 안에서 호출한 kmalloc(GFP_KERNEL)은 메모리가 넉넉하면 잠들지 않으니 아무 일도 없다. 그러다 메모리 압박이 오거나 다른 태스크와 경합이 붙는 날, 원인과 동떨어진 곳에서 행(hang)이나 soft lockup으로 터진다.
이 글에서는 락 오용 10가지를 커널 모듈로 만들어, 락 디버깅 옵션을 끈 커널·DEBUG_SPINLOCK/DEBUG_MUTEXES/DEBUG_ATOMIC_SLEEP만 켠 커널·PROVE_LOCKING(lockdep)까지 켠 커널에서 각각 어떤 메시지가 나오는지 비교한다. lockdep의 락 순서(ABBA) 검증은 LOCKDEP 사용법, watchdog 설정은 Kernel Lockup Debug 사용법에서 따로 다뤘다.
실험 환경과 커널 옵션
| 옵션 | 잡는 것 | 비고 |
|---|---|---|
DEBUG_SPINLOCK | 초기화 누락(bad magic), 재귀 잠금, 잡지 않은 락 해제, 다른 태스크·CPU의 해제 | spinlock_t에 magic·owner 필드 추가 |
DEBUG_MUTEXES | 다른 태스크의 mutex_unlock, 잘못된 대기 상태 | PREEMPT_RT에서는 사용 불가 |
DEBUG_ATOMIC_SLEEP | 스핀락·RCU·IRQ 등 atomic 문맥에서 잠들 수 있는 함수 호출 | might_sleep()이 있는 함수만 검사 |
PROVE_LOCKING | 위 전부 + 재귀·순서·IRQ 문맥 불일치, 락 잡은 채 복귀 | LOCKDEP, DEBUG_SPINLOCK, DEBUG_MUTEXES를 select |
DETECT_HUNG_TASK, SOFTLOCKUP_DETECTOR | 실제로 멈춘 뒤의 증상 | 세 커널 모두 켬 |
커널 6.12.0 소스에서 같은 x86_64 설정을 바탕으로 세 가지를 빌드했고, busybox initramfs와 함께 QEMU(TCG)로 부팅했다.
SRC=~/kbuild/linux-6.12
BASE=~/kbuild/config.gcc # x86_64 기본 설정
build() { # build <이름> <추가로 켤 옵션...>
v=$1; shift
mkdir -p build-$v && cp $BASE build-$v/.config
$SRC/scripts/config --file build-$v/.config \
--enable DETECT_HUNG_TASK --enable SOFTLOCKUP_DETECTOR --enable DEBUG_FS \
$(printf -- '--enable %s ' "$@")
make -C $SRC O=$PWD/build-$v olddefconfig
make -C $SRC O=$PWD/build-$v -j6 bzImage modules
}
build off
build lite DEBUG_SPINLOCK DEBUG_MUTEXES DEBUG_ATOMIC_SLEEP
build on DEBUG_SPINLOCK DEBUG_MUTEXES DEBUG_ATOMIC_SLEEP PROVE_LOCKING$ grep -E "^CONFIG_(DEBUG_SPINLOCK|DEBUG_MUTEXES|DEBUG_ATOMIC_SLEEP|PROVE_LOCKING|LOCKDEP|DETECT_HUNG_TASK|SOFTLOCKUP_DETECTOR)=" build-{off,lite,on}/.config
build-off/.config:CONFIG_SOFTLOCKUP_DETECTOR=y
build-off/.config:CONFIG_DETECT_HUNG_TASK=y
build-lite/.config:CONFIG_SOFTLOCKUP_DETECTOR=y
build-lite/.config:CONFIG_DETECT_HUNG_TASK=y
build-lite/.config:CONFIG_DEBUG_SPINLOCK=y
build-lite/.config:CONFIG_DEBUG_MUTEXES=y
build-lite/.config:CONFIG_DEBUG_ATOMIC_SLEEP=y
build-on/.config:CONFIG_SOFTLOCKUP_DETECTOR=y
build-on/.config:CONFIG_DETECT_HUNG_TASK=y
build-on/.config:CONFIG_PROVE_LOCKING=y
build-on/.config:CONFIG_DEBUG_SPINLOCK=y
build-on/.config:CONFIG_DEBUG_MUTEXES=y
build-on/.config:CONFIG_LOCKDEP=y
build-on/.config:CONFIG_DEBUG_ATOMIC_SLEEP=y
$ ls -l build-{off,lite,on}/arch/x86/boot/bzImage | awk "{print \$5, \$9}"
13550592 build-lite/arch/x86/boot/bzImage
13468672 build-off/arch/x86/boot/bzImage
14492672 build-on/arch/x86/boot/bzImage
lockdep을 켠 커널은 이미지만 약 1MB 커지고, 락 획득마다 의존성 그래프를 갱신하므로 성능 측정용 커널과는 분리해서 쓴다.
테스트 모듈
시나리오 이름을 모듈 파라미터로 받아 해당 함수 하나만 실행한다.
// SPDX-License-Identifier: GPL-2.0
/* 락 오용 시나리오 모음 — insmod lockbug.ko test=<이름> */
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/hrtimer.h>
#include <linux/kthread.h>
#include <linux/completion.h>
static char *test = "none";
module_param(test, charp, 0444);
static DEFINE_SPINLOCK(slock);
static DEFINE_MUTEX(mlock);
/* 1. 같은 스핀락을 두 번 잡기 */
static void spin_recursion(void)
{
spin_lock(&slock);
spin_lock(&slock);
}
/* 2. 잡지 않은 스핀락 해제 */
static void spin_double_unlock(void)
{
spin_lock(&slock);
spin_unlock(&slock);
spin_unlock(&slock);
}
/* 3. 스핀락을 잡은 채 sleep */
static void spin_sleep(void)
{
spin_lock(&slock);
msleep(10);
spin_unlock(&slock);
}
/* 3-2. 스핀락을 잡은 채 GFP_KERNEL 할당 (메모리가 넉넉하면 실제로 잠들지 않는다) */
static void spin_gfp_kernel(void)
{
void *p;
spin_lock(&slock);
p = kmalloc(64, GFP_KERNEL);
spin_unlock(&slock);
kfree(p);
}
/* 4. 초기화하지 않은 스핀락 사용 */
struct dev_ctx {
spinlock_t lock;
int count;
};
static void spin_uninit(void)
{
struct dev_ctx *ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return;
/* spin_lock_init(&ctx->lock); 누락 */
spin_lock(&ctx->lock);
ctx->count++;
spin_unlock(&ctx->lock);
kfree(ctx);
}
/* 5. 하드IRQ와 프로세스 컨텍스트에서 irqsave 없이 같은 락 사용 */
static struct hrtimer timer;
static enum hrtimer_restart timer_fn(struct hrtimer *t)
{
spin_lock(&slock);
spin_unlock(&slock);
return HRTIMER_NORESTART;
}
static void spin_irq_unsafe(void)
{
spin_lock(&slock); /* 인터럽트가 켜진 상태 */
spin_unlock(&slock);
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
timer.function = timer_fn;
hrtimer_start(&timer, ms_to_ktime(10), HRTIMER_MODE_REL);
msleep(100);
hrtimer_cancel(&timer);
}
/* 6. 다른 태스크가 잡은 뮤텍스 해제 */
static DECLARE_COMPLETION(locked);
static int locker_fn(void *data)
{
mutex_lock(&mlock);
complete(&locked);
return 0; /* 잠근 채로 종료 */
}
static void mutex_other_owner(void)
{
kthread_run(locker_fn, NULL, "locker");
wait_for_completion(&locked);
mutex_unlock(&mlock);
}
/* 7. 스핀락 안에서 mutex_lock (경합이 없으면 실제로 잠들지 않는다) */
static void mutex_in_atomic(void)
{
spin_lock(&slock);
mutex_lock(&mlock);
mutex_unlock(&mlock);
spin_unlock(&slock);
}
/* 8. 같은 뮤텍스를 두 번 잡기 */
static void mutex_recursion(void)
{
mutex_lock(&mlock);
mutex_lock(&mlock);
}
/* 9. 뮤텍스를 잡은 채 사용자 공간으로 복귀 */
static void mutex_return_held(void)
{
mutex_lock(&mlock);
}
/* 10. 버그 두 개를 연달아 — 두 번째도 보고될까 */
static void two_bugs(void)
{
spin_uninit();
spin_gfp_kernel();
mutex_other_owner();
}
static const struct {
const char *name;
void (*fn)(void);
} tests[] = {
{ "spin_recursion", spin_recursion },
{ "spin_double_unlock", spin_double_unlock },
{ "spin_sleep", spin_sleep },
{ "spin_gfp_kernel", spin_gfp_kernel },
{ "spin_uninit", spin_uninit },
{ "spin_irq_unsafe", spin_irq_unsafe },
{ "mutex_other_owner", mutex_other_owner },
{ "mutex_in_atomic", mutex_in_atomic },
{ "mutex_recursion", mutex_recursion },
{ "mutex_return_held", mutex_return_held },
{ "two_bugs", two_bugs },
};
static int __init lockbug_init(void)
{
int i;
for (i = 0; i < ARRAY_SIZE(tests); i++) {
if (!strcmp(test, tests[i].name)) {
pr_info("lockbug: run %s\n", test);
tests[i].fn();
pr_info("lockbug: %s returned\n", test);
return 0;
}
}
return -EINVAL;
}
module_init(lockbug_init);
MODULE_LICENSE("GPL");#!/bin/busybox sh
/bin/busybox --install -s /bin
mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys
T=$(sed -n 's/.*locktest=\([a-z_]*\).*/\1/p' /proc/cmdline)
echo 15 > /proc/sys/kernel/hung_task_timeout_secs
echo "===MARK start $T"
insmod /lockbug.ko test=$T &
sleep ${WAIT:-40}
echo "===MARK end $T"
poweroff -fqemu-system-x86_64 -machine q35 -cpu max -smp 2 -m 1024 -nographic -no-reboot \
-kernel build-$v/arch/x86/boot/bzImage -initrd ir-$v.cpio.gz \
-append "console=ttyS0 no_timer_check tsc=reliable loglevel=7 locktest=$t WAIT=$w"KVM 없이 TCG로 돌리면 lockdep 커널이 tsc-early 클럭소스를 불안정으로 판정한 뒤 hpet로 넘어가면서 부팅 도중 RCU stall로 멈췄다. tsc=reliable을 주자 세 커널 모두 2초 안에 부팅됐다.
결과 한눈에 보기
| 시나리오 | off | lite (DEBUG_*) | on (+lockdep) |
|---|---|---|---|
spin_recursion | 26초 뒤 soft lockup만 | spinlock recursion 즉시 + soft lockup | possible recursive locking 즉시 + soft lockup |
spin_double_unlock | preemption imbalance 경고만 | spinlock already unlocked | bad unlock balance |
spin_sleep (msleep) | scheduling while atomic | scheduling while atomic | scheduling while atomic + 잡고 있는 락 표시 |
spin_gfp_kernel | 아무 메시지 없음 | sleeping function called from invalid context | 같음 + 잡고 있는 락 표시 |
spin_uninit | 아무 메시지 없음 | spinlock bad magic | trying to register non-static key |
spin_irq_unsafe | 아무 메시지 없음 | 아무 메시지 없음 | inconsistent {HARDIRQ-ON-W} -> {IN-HARDIRQ-W} |
mutex_other_owner | 아무 메시지 없음 | DEBUG_LOCKS_WARN_ON(__owner_task(owner) != get_current()) | bad unlock balance |
mutex_in_atomic | 아무 메시지 없음 | sleeping function called from invalid context | 같음 + Invalid wait context |
mutex_recursion | 29초 뒤 hung task만 | 29초 뒤 hung task만 | possible recursive locking 즉시 + hung task |
mutex_return_held | 아무 메시지 없음 | 아무 메시지 없음 | lock held when returning to user space |
디버그 옵션이 없는 커널에서 10개 중 6개는 흔적 없이 지나갔고, 나머지도 한참 뒤 증상만 남았다. 가벼운 DEBUG_* 옵션만으로 스핀락 계열은 대부분 잡히지만, IRQ 문맥 불일치와 락을 잡은 채 복귀하는 경우는 lockdep만 잡는다.
스핀락 오용
같은 락을 두 번 잡기
[ 2.111319] lockbug: run spin_recursion
[ 28.316653] watchdog: BUG: soft lockup - CPU#1 stuck for 26s! [insmod:76]
[ 28.318040] RIP: 0010:queued_spin_lock_slowpath+0x7f/0x290
[ 2.420590] lockbug: run spin_recursion
[ 2.420910] BUG: spinlock recursion on CPU#1, insmod/76
[ 2.425933] lock: slock+0x0/0xffffffffffffef00 [lockbug], .magic: dead4ead, .owner: insmod/76, .owner_cpu: 1
[ 2.432804] CPU: 1 UID: 0 PID: 76 Comm: insmod Tainted: G O 6.12.0 #1
[ 2.433724] Call Trace:
[ 2.434946] dump_stack_lvl+0x53/0x70
[ 2.558995] do_raw_spin_lock+0x80/0xb0
off 커널은 26초가 지나서야 watchdog이 CPU가 멈췄다고 알려줄 뿐 어떤 락인지 모른다. DEBUG_SPINLOCK은 잠그기 직전에 owner == current를 검사해 락 이름과 소유자를 바로 찍는다.
static inline void
debug_spin_lock_before(raw_spinlock_t *lock)
{
SPIN_BUG_ON(READ_ONCE(lock->magic) != SPINLOCK_MAGIC, lock, "bad magic");
SPIN_BUG_ON(READ_ONCE(lock->owner) == current, lock, "recursion");
SPIN_BUG_ON(READ_ONCE(lock->owner_cpu) == raw_smp_processor_id(),
lock, "cpu recursion");
}잡지 않은 락 해제
[ 2.095976] lockbug: run spin_double_unlock
[ 2.096126] lockbug: spin_double_unlock returned
[ 2.096358] initcall lockbug_init+0x0/0xff0 [lockbug] returned with preemption imbalance
[ 2.097683] WARNING: CPU: 0 PID: 76 at init/main.c:1282 do_one_initcall+0x1c7/0x220
[ 1.467538] lockbug: run spin_double_unlock
[ 1.467739] BUG: spinlock already unlocked on CPU#0, insmod/76
[ 1.467845] lock: slock+0x0/0xffffffffffffef00 [lockbug], .magic: dead4ead, .owner: <none>/-1, .owner_cpu: -1
off 커널의 경고는 spin_unlock()이 선점 카운트를 한 번 더 내린 결과를 initcall이 끝난 뒤에 발견한 것이라 원인 위치가 없다. lite 커널은 .owner: <none>/-1로 이미 풀린 락이라는 것을 해제 시점에 보여준다.
초기화하지 않은 스핀락
[ 1.558570] lockbug: run spin_uninit
[ 1.558775] BUG: spinlock bad magic on CPU#1, insmod/76
[ 1.558894] lock: 0xff1fcdec81874b80, .magic: 00000000, .owner: <none>/-1, .owner_cpu: 0
[ 1.559960] CPU: 1 UID: 0 PID: 76 Comm: insmod Tainted: G O 6.12.0 #1
[ 1.560602] Call Trace:
[ 1.561213] dump_stack_lvl+0x53/0x70
[ 1.561361] do_raw_spin_lock+0x63/0xb0
[ 1.567270] lockbug: spin_uninit returned
[ 2.829928] lockbug: run spin_uninit
[ 2.830186] INFO: trying to register non-static key.
[ 2.830267] The code is fine but needs lockdep annotation, or maybe
[ 2.830429] turning off the locking correctness validator.
kzalloc으로 0이 채워진 spinlock_t는 qspinlock의 unlocked 값과 같아서 off 커널에서는 문제없이 동작했다. .magic: 00000000이 spin_lock_init() 누락의 직접 증거다.
스핀락을 잡은 채 잠들 수 있는 함수 호출
[ 1.979703] lockbug: run spin_sleep
[ 1.979819] BUG: scheduling while atomic: insmod/76/0x00000002
[ 1.981964] __schedule_bug+0x4d/0x60
[ 1.982055] __schedule+0x816/0x910
[ 1.982119] schedule+0x22/0xd0
[ 1.982169] schedule_timeout+0x99/0x170
[ 1.991137] lockbug: spin_sleep returned
[ 2.195276] lockbug: run spin_gfp_kernel
[ 2.195657] BUG: sleeping function called from invalid context at include/linux/sched/mm.h:321
[ 2.195861] in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 76, name: insmod
[ 2.196010] preempt_count: 1, expected: 0
msleep()처럼 실제로 스케줄러에 들어가면 옵션과 무관하게 scheduling while atomic이 나온다. 반면 kmalloc(GFP_KERNEL)은 메모리가 충분하면 잠들지 않으므로 off 커널에서는 조용했고, DEBUG_ATOMIC_SLEEP이 might_sleep() 지점에서 "잠들 수도 있었다"를 미리 잡았다.
IRQ 문맥과 프로세스 문맥에서 같은 락
[ 2.813518] lockbug: run spin_irq_unsafe
[ 2.824783]
[ 2.824836] ================================
[ 2.824899] WARNING: inconsistent lock state
[ 2.825103] 6.12.0 #1 Tainted: G O
[ 2.825182] --------------------------------
[ 2.825323] inconsistent {HARDIRQ-ON-W} -> {IN-HARDIRQ-W} usage.
[ 2.825420] swapper/0/0 [HC1[1]:SC0[0]:HE0:SE1] takes:
[ 2.825517] ffffffffc0215198 (slock){?.+.}-{2:2}, at: timer_fn+0x10/0x30 [lockbug]
[ 2.826037] {HARDIRQ-ON-W} state was registered at:
[ 2.826142] lock_acquire+0xc0/0x2d0
[ 2.826398] _raw_spin_lock+0x2b/0x40
[ 2.826478] spin_irq_unsafe+0x10/0x70 [lockbug]
[ 2.828294]
[ 2.828294] other info that might help us debug this:
[ 2.828437] Possible unsafe locking scenario:
[ 2.828437]
[ 2.828533] CPU0
[ 2.828580] ----
[ 2.828633] lock(slock);
[ 2.828691] <Interrupt>
[ 2.828808] lock(slock);
[ 2.828868]
[ 2.828868] *** DEADLOCK ***
[ 2.828868]
[ 2.828957] no locks held by swapper/0/0.
[ 2.829024]
인터럽트를 켠 채 spin_lock으로 잡은 락을 hrtimer 콜백(하드IRQ)에서도 잡았다. 락을 쥔 순간 같은 CPU에 인터럽트가 들어와야 실제로 멈추지만, lockdep은 두 사용 이력만으로 경고하며 프로세스 문맥 쪽을 spin_lock_irqsave()로 바꾸면 사라진다.
뮤텍스 오용
다른 태스크가 잡은 뮤텍스 해제
[ 1.963444] lockbug: run mutex_other_owner
[ 1.966829] DEBUG_LOCKS_WARN_ON(__owner_task(owner) != get_current())
[ 1.969408] WARNING: CPU: 0 PID: 77 at kernel/locking/mutex.c:923 __mutex_unlock_slowpath.isra.0+0x176/0x290
[ 2.500119] lockbug: run mutex_other_owner
[ 2.505280] WARNING: bad unlock balance detected!
[ 2.505653] insmod/76 is trying to release lock (mlock) at:
[ 2.506835] but there are no more locks to release!
[ 2.507836] no locks held by insmod/76.
off 커널에서는 아무 경고 없이 락이 풀려, 커널 스레드가 들고 있다고 믿는 락을 다른 태스크가 해제한 상태가 된다. DEBUG_MUTEXES는 __mutex_unlock_slowpath()의 MUTEX_WARN_ON(__owner_task(owner) != current)로 잡는다.
스핀락 안에서 mutex_lock
[ 1.878787] lockbug: run mutex_in_atomic
[ 1.878933] BUG: sleeping function called from invalid context at kernel/locking/mutex.c:283
[ 1.879099] in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 76, name: insmod
[ 1.879476] preempt_count: 1, expected: 0
[ 1.885501] lockbug: mutex_in_atomic returned
[ 2.259025] [ BUG: Invalid wait context ]
[ 2.259324] insmod/76 is trying to lock:
[ 2.259382] ffffffffc0117148 (mlock){....}-{3:3}, at: mutex_in_atomic+0x1e/0x40 [lockbug]
[ 2.259625] context-{4:4}
[ 2.259666] 1 lock held by insmod/76:
[ 2.259718] #0: ffffffffc0117198 (slock){+.+.}-{2:2}, at: mutex_in_atomic+0x10/0x40 [lockbug]
경합이 없으면 mutex_lock()은 fast path로 바로 끝나 off 커널에서는 드러나지 않는다. lockdep은 {2:2}(스핀락) 안에서 {3:3}(잠드는 락)을 잡으려 한다는 wait-type 규칙 위반으로 한 번 더 경고한다.
같은 뮤텍스를 두 번 잡기
[ 1.871995] lockbug: run mutex_recursion
[ 31.193898] INFO: task insmod:77 blocked for more than 15 seconds.
[ 31.194949] Tainted: G O 6.12.0 #1
[ 31.195288] task:insmod state:D stack:14168 pid:77 tgid:77 ppid:1 flags:0x00004002
[ 31.195945] Call Trace:
[ 31.196493] __schedule+0x3f3/0x910
[ 31.197089] schedule+0x22/0xd0
[ 31.197222] schedule_preempt_disabled+0x10/0x20
[ 31.197363] __mutex_lock.constprop.0+0x3bf/0x6a0
[ 31.197628] lockbug_init+0x69/0xff0 [lockbug]
[ 2.253411] lockbug: run mutex_recursion
[ 2.254041] WARNING: possible recursive locking detected
[ 2.254408] insmod/76 is trying to acquire lock:
[ 2.254493] ffffffffc014e148 (mlock){+.+.}-{3:3}, at: lockbug_init+0x69/0xff0 [lockbug]
[ 2.255196] but task is already holding lock:
[ 2.255298] ffffffffc014e148 (mlock){+.+.}-{3:3}, at: mutex_recursion+0x12/0x20 [lockbug]
[ 2.255922] lock(mlock);
[ 2.255981] lock(mlock);
[ 2.256037] *** DEADLOCK ***
[ 2.256288] #0: ffffffffc014e148 (mlock){+.+.}-{3:3}, at: mutex_recursion+0x12/0x20 [lockbug]
[ 30.864651] INFO: task insmod:76 blocked for more than 15 seconds.
[ 30.870415] INFO: lockdep is turned off.
off·lite 커널에서는 hung_task_timeout_secs를 15초로 줄였는데도 두 번째 검사 주기에서야 D 상태 태스크로 보고됐고, 백트레이스에는 __mutex_lock만 있을 뿐 누가 락을 쥐고 있는지는 없다. lockdep 커널은 잠그는 순간 경고했지만, 그 첫 보고로 lockdep이 꺼져서 hung task 시점의 INFO: lockdep is turned off.처럼 보유 락 목록을 더 이상 보여주지 못했다.
뮤텍스를 잡은 채 사용자 공간으로 복귀
[ 2.923954] lockbug: run mutex_return_held
[ 2.924179] lockbug: mutex_return_held returned
[ 2.925880]
[ 2.925941] ================================================
[ 2.926023] WARNING: lock held when returning to user space!
[ 2.926247] 6.12.0 #1 Tainted: G O
[ 2.926321] ------------------------------------------------
[ 2.926412] insmod/76 is leaving the kernel with locks still held!
[ 2.926755] 1 lock held by insmod/76:
[ 2.926828] #0: ffffffffc0351148 (mlock){+.+.}-{3:3}, at: 0xffffffffc0355079
시스템 콜이나 ioctl 오류 경로에서 mutex_unlock()을 빠뜨린 버그가 이런 모양이다. 다음 호출이 올 때까지 증상이 없어서, 이 경고는 lockdep 커널에서만 볼 수 있었다.
첫 보고 이후에는 조용해진다
한 번의 실행에서 초기화 누락 → 스핀락 안 GFP_KERNEL → 다른 태스크의 뮤텍스 해제를 연달아 일으켰다(two_bugs). lockdep 커널은 init 스크립트에 grep debug_locks /proc/lockdep_stats를 추가해 상태도 확인했다.
[ 2.061601] lockbug: run two_bugs
[ 2.061752] BUG: spinlock bad magic on CPU#1, insmod/76
[ 2.071987] BUG: sleeping function called from invalid context at include/linux/sched/mm.h:321
[ 2.079410] lockbug: two_bugs returned
[ 2.292197] lockbug: run two_bugs
[ 2.292447] INFO: trying to register non-static key.
[ 2.292789] turning off the locking correctness validator.
[ 2.298569] BUG: sleeping function called from invalid context at include/linux/sched/mm.h:321
[ 2.299120] INFO: lockdep is turned off.
[ 2.305685] lockbug: two_bugs returned
debug_locks: 0
세 번째 버그(뮤텍스 소유자 불일치)는 두 커널 모두 보고하지 않았다. 락 디버깅 경고는 출력 전에 debug_locks_off()를 거치므로 이미 꺼져 있으면 침묵하고, 이 플래그와 무관한 might_sleep() 검사만 두 번째 버그를 보고했다.
static void spin_bug(raw_spinlock_t *lock, const char *msg)
{
if (!debug_locks_off())
return;
spin_dump(lock, msg);
}주의사항
| 항목 | 내용 |
|---|---|
| 첫 경고만 믿을 것 | 락 디버깅 경고는 부팅 후 한 번만 나온다. 첫 번째를 고치고 재부팅해야 다음 버그가 보인다. lockdep 커널은 /proc/lockdep_stats의 debug_locks: 0으로 확인 |
| 부팅 중 다른 드라이버의 경고 | 내 모듈을 올리기 전에 이미 debug_locks가 꺼져 있으면 내 버그는 보고되지 않는다. dmesg에 앞선 lockdep 경고가 없는지 먼저 확인 |
msleep vs GFP_KERNEL | DEBUG_ATOMIC_SLEEP은 might_sleep() 지점만 본다. 실제로 잠드는 경로는 옵션 없이도 scheduling while atomic으로 나온다 |
| 경로를 실행해야 잡힌다 | 모든 검사는 런타임 검사다. 오류 경로·IRQ 경로는 실제로 한 번 이상 실행돼야 경고가 뜬다 |
| lockdep 없는 lite 커널의 한계 | IRQ 문맥 불일치, 락 잡은 채 복귀, 락 순서(ABBA)는 못 잡는다 |
| 재귀 잠금은 멈춘다 | 경고를 찍은 뒤에도 실제로 데드락에 빠진다. 스핀락은 soft lockup, 뮤텍스는 hung task로 이어지므로 테스트는 VM에서 |
| 운영 커널 | 배포판 커널은 이 옵션들이 꺼져 있다. 락 경합 통계만 필요하면 lockstat을 쓴다 |
| QEMU TCG에서 lockdep 커널 | 부팅이 RCU stall로 멈추면 tsc=reliable을 커맨드라인에 추가 |
마무리
| 상황 | 권장 설정 |
|---|---|
| 드라이버·모듈 개발용 테스트 VM | PROVE_LOCKING (나머지는 자동 select) + DEBUG_ATOMIC_SLEEP |
| lockdep 오버헤드가 부담되는 장시간 테스트 | DEBUG_SPINLOCK + DEBUG_MUTEXES + DEBUG_ATOMIC_SLEEP |
| 이미 멈춘 시스템 분석 | DETECT_HUNG_TASK, SOFTLOCKUP_DETECTOR + SysRq로 태스크 덤프 |
락 오용은 대부분 "지금은 운 좋게 동작하는" 코드라 테스트 통과만으로는 안심할 수 없다. 새 모듈은 적어도 한 번은 lockdep 커널에서 모든 경로를 돌려보고, 첫 경고가 나오면 그것부터 고친 뒤 다시 돌리는 순서가 가장 확실하다.