[原写于2012年12月]

​把flashcache改为request-based后,虽然IO数量可以按比例控制了,但是作为“珍稀”资源的cache(通常是昂贵的固态硬盘或更昂贵的fusionio卡),也需要按例分配不同进程以不同的cache空间。近一个月我一直在忙着加这个功能。
新代码写完了,压力测试二十多个小时,出现了一个死锁:

<4>[11957.888102] [] ? nmi+0x20/0x30
<4>[11957.897016] [] ? _spin_lock_irqsave+0x2f/0x40
<4>[11957.906089] <> [] ? flashcache_clean_set+0x9f/0x600 [flashcache]
<4>[11957.915610] [] ? dm_io_async_bvec+0xbc/0xf0 [flashcache]
<4>[11957.925377] [] ? flashcache_io_callback+0x0/0x4a0 [flashcache]
<4>[11957.935318] [] ? flashcache_read_miss+0x9e/0x150 [flashcache]
<4>[11957.945378] [] ? flashcache_read+0x138/0x330 [flashcache]
<4>[11957.955511] [] ? flashcache_mk_rq+0x165/0x1d0 [flashcache]
<4>[11957.965677] [] ? dm_request+0x5d/0x210 [dm_mod]
<4>[11957.975911] [] ? generic_make_request+0x261/0x530

应该是有进程拿到了request_queue的锁 queue_lock 却再没有释放。
由于出现了“flashcache”的字样,有很大嫌疑是我新加的代码有问题,但是不管怎样,二十多个小时的重现步骤太慢了,得找到一个快点的,于是花了几天尝试不同的压测脚本(一边尝试,一边把我的cubieboard刷成了android,我的天,android比linaro好用多了,而且是刷在cubieboard自带的NAND里,可以不用SD卡,刷机所需image见这里),终于把重现步骤缩短为2个小时左右,接下来把我加的代码全回退,依然有死锁——看来是redhat-2.6.32内核的问题了。

我在内核报死锁的地方watchdog_overflow_callback()函数里调用panic()之前加了一句show_state(),即把死锁时所有进程的stack信息统统输出到dmesg里去(再依靠netoops把消息传到另外一台机器上,因为出现死锁的本机已经登不上去了)。
跑了不到两个小时,死锁出现了,我在netoops吐出来的一堆stack信息里一个个找,看谁像是那个拿到锁不还的人,发现一个可疑的:cfq_get_cfqg()里调用blk_init_rl()函数。blk_init_rl函数里面要做一个内存分配,但是传给kmalloc_node的gfp_mask却是GFP_KERNEL,带上GFP_KERNEL后在分配不到内存时进程会去睡眠直到别的进程释放了一些可用内存,blk_init_rl()已经拿到了queue_lock,现在却一睡不醒(系统内存紧张,迟迟没有可用内存),当然别的进程来取queue_lock就等疯了。
看来不是我加的代码造成的,也与flashcache没有关系,是block层的问题。

这种情况一般有两种修复办法:一是在分配内存之前把queue_lock释放掉,分配后再重新拿锁;或者,把分配内存的gfp_mask设为GFP_ATOMIC,就是没内存就直接返回NULL,不睡。
不过我看了一下upstream代码,这个问题已经修复了。参见Tejun Heo的两个patch,12,第二个patch的头几个改动才是把GFP_KERNEL改成GFP_ATOMIC。
没想到netoops加show_state()调试死锁这么方便,当然,难的是找到快速重现bug的办法。