有一种错叫持有锁

a432345546f9548da01309e86e8e475d.png

有一种错叫

持有锁

记得有一次在M国出差时,偶遇N君,周末一起到O州之国家公园爬山。山中漫步时,大家天南海北地闲聊,聊的内容大多都不记得了。今天任然记得的是,N君偶发感慨:“M国是很讲规则的地方,在这里没有钱不会被人瞧不起,但是不懂规则就会被人瞧不起……”其实,不仅M国如此,软件世界也是很讲规则的地方。

oops和panic

以Linux为例,当内核空间的代码违反规则时,轻则有oops警告,如果严重了,则有系统Panic。而一旦进入Panic流程,则只能玉石俱焚,系统重启。

内核有一个名为panic_on_oops的变量,当这个变量为1时,所有oops都会升级为Panic。对于可靠性要求高的系统,这个变量一般是设为1的。

panic_on_oops
=============


Controls the kernel's behaviour when an oops or BUG is encountered.


= ===================================================================
0 Try to continue operation.
1 Panic immediately.  If the `panic` sysctl is also non-zero then the
  machine will be rebooted.
= ===================================================================

名字与panic_on_oops类似的内核变量还有很多,比如:

panic_on_stackoverflow
panic_on_unrecovered_nmi
panic_on_warn
panic_on_rcu_stall
panic_on_io_nmi
panic_on_taint

这么多panic_on变量也从侧面说明了内核的规则很多。在内核代码里搜索oops,则可以搜到更多的内核规则。

5db74afbc72705073571db5827498128.png

9724392444534a90907418dab26f8909.png

oops连续剧

因为代码里有这么多的oops逻辑埋伏着,所以在实践中,遇到oops也是常有的事。对于一台Linux机器,我很喜欢浏览它的内核消息,在观察内核消息时,经常可以看到各种oops。最近在开发幽兰的新镜像时,也遇到一串oops。说一串oops,是因为这个oops是像连续剧一样,有很多“集”。每一集主题相同,但是“剧情”有所不同。

比如,下面是第一集:

[    8.089900] 1 lock held by kworker/4:1/54:
[    8.089902]  #0: ffffff8105c121a0 (&rport->mutex){....}-{3:3}, at: rockchip_chg_detect_work+0x2c4/0x540
[    8.089920] CPU: 4 PID: 54 Comm: kworker/4:1 Not tainted 5.10.110 #6
[    8.089922] Hardware name: Rockchip RK3588 code book YourLand (DT)
[    8.089926] Workqueue: events rockchip_chg_detect_work
[    8.089930] Call trace:
[    8.089933]  dump_backtrace+0x0/0x210
[    8.089936]  show_stack+0x2c/0x38
[    8.089940]  dump_stack_lvl+0xd4/0xf8
[    8.089942]  dump_stack+0x14/0x30
[    8.089945]  process_one_work+0x404/0x5a0
[    8.089947]  worker_thread+0x48/0x460
[    8.089950]  kthread+0x128/0x130
[    8.089953]  ret_from_fork+0x10/0x1c

就像写文章有套路一样,Oops信息也有相对固定的格式,一般包含如下几个部分:

  • 所犯错误,或者说“罪名”

  • 发生地,包括CPU,当前进程,系统信息等

  • 调用栈

  • 寄存器信息

  • 其它现场信息

对于本例,内核给出的罪名如下:

1 lock held by kworker/4:1/54

直接翻译便是“1个锁被kworker/4:1/54所持有”。

罪出何名

在一些地方,持枪是犯法的,但是这里说的是持有锁,持有锁也犯法么?

根据上面的信息搜索内核代码,可以找到打印这个信息的地方,即:

printk("%d lock%s held by %s/%d:\n", depth,
        depth > 1 ? "s" : "", p->comm, task_pid_nr(p));

根据这个printk的写法,可以知道罪名信息中的54是线程ID,kworker/4:1是系统线程的线程名。

0affb7b05b9f185b7f50dcc3d6b3cbe6.png

根据调用栈的process_one_work可以找到发起这次“兴师问罪”行动的地方:

if (unlikely(in_atomic() || lockdep_depth(current) > 0)) {
        debug_show_held_locks(current);
        dump_stack();
    }

其中的debug_show_held_locks函数实现在kernel\locking\lockdep.c中,这个.c有6000多行C代码,里面有很多函数都是审计纠错的。文件开头的描述也言简意赅地表达了这个目的:

Runtime locking correctness validator

代码的作者是Linux内核圈里的名人:因戈·莫而纳(Ingo Molnar)。

ed182f18c3ef3eeb8258315f1d8d3866.png

2011年内核峰会上的因戈·莫而纳

(照片来自LWN)

/*
 * Careful: only use this function if you are sure that
 * the task cannot run in parallel!
 */
void debug_show_held_locks(struct task_struct *task)
{
    if (unlikely(!debug_locks)) {
        printk("INFO: lockdep is turned off.\n");
        return;
    }
    lockdep_print_held_locks(task);
}
EXPORT_SYMBOL_GPL(debug_show_held_locks);

值得说明的是:内核的这个“锁监督机制”被视为一种高端服务,一旦内核有污点,那么这个服务可能被取消。比如下面这几句来自其它系统的内核消息表示,因为加载了nvidia协议的驱动,污染了内核,因为此锁调试服务被禁止了。

[    0.923328] nvidia: loading out-of-tree module taints kernel.
[    0.923330] nvidia: module license 'NVIDIA' taints kernel.
[    0.923331] Disabling lock debugging due to kernel taint

69f361b4432ca3f957369a8a25a6b493.jpeg

  代码源头  

再回到我们的问题,看来是触发了内核锁监督机制。在罪名信息的下面一行,打印了这起事故涉及的锁:

#0: ffffff8105c121a0 (&rport->mutex){....}-{3:3}, at: rockchip_chg_detect_work+0x2c4/0x540

上面信息分为如下几个部分:

  • 锁的序号,当涉及多个锁时,依次排列

  • 锁对象的地址

  • 锁的名字

  • 执行加锁动作的代码地址

后三部分信息来自如下代码:

printk(KERN_CONT "%px", hlock->instance);
    print_lock_name(lock);
    printk(KERN_CONT ", at: %pS\n", (void *)hlock->acquire_ip);

其中的print_lock_name函数也是出自因戈之手,摘录如下:

static void print_lock_name(struct lock_class *class)
{
    char usage[LOCK_USAGE_CHARS];


    get_usage_chars(class, usage);


    printk(KERN_CONT " (");
    __print_lock_name(class);
    printk(KERN_CONT "){%s}-{%d:%d}", usage,
            class->wait_type_outer ?: class->wait_type_inner,
            class->wait_type_inner);
}

根据加锁函数的信息,可以找到持锁的代码,来自瑞芯微。

efe5667469f4711c1cd72494ea4bb5da.png

在1392行,果然有加锁动作。

虽然这个函数中有解锁调用,但是解锁动作是在case语句里的。也就是说可能只加锁,不解锁。在函数末尾的注释明确描述了这个特征:

/*
     * Hold the mutex lock during the whole charger
     * detection stage, and release it after detect
     * the charger type.
     */
    schedule_delayed_work(&rport->chg_work, delay);
}

意思是:在整个充电器检测阶段都持有锁,直到检测到充电器类型才释放。

糊涂啊,这显然是和内核的锁政策对抗啊。上面的函数是以“作业”的形式提交给内核的,由内核的工作线程来执行。工作线程在调用作业函数后,例行检查是否有锁违规,结果被查到了。

搜索内核消息,可以看到被查到很多次。

a5117a7b64c2ac5ac8b215b6ccf2fb0b.png

阅读持锁代码所在源文件的其它代码,可以看到它管理的是幽兰的USB PHY设备,与充电逻辑有关。源文件名中的inno应该是PHY芯片的厂商名。

芯动科技有限公司(Innosilicon)是世界先进、国内领军的高端混合电路芯片设计公司,中国高速芯片技术市场领导企业,在全球范围内拥有核心竞争优势。

回避解法

对于这个问题,硬件图伙伴认为是格蠹新增的内核选项导致的,因为他们那里看不到这些oops。顺着这个线索追查,的确是与内核的编译选项有关。格蠹的内核编译选项新增了如下两个:

CONFIG_LOCKDEP=y
CONFIG_LOCK_STAT=y

其中的CONFIG_LOCKDEP就是用来开启上面说的锁调试功能的。如果把这个选项设置为n,那么内核消息中的oops就没有了。

但这样做其实是禁止了锁监督机制,而幽兰的内核,是不想关闭这个选项的,因为关闭这个选项意味着放弃了内核的一项高端服务。而这个服务对于发现内核代码的设计不足是有益的。

从表面看,这个服务只是打印一些警告信息,但从深层说,它代表着对规则的重视和守护。而幽兰的内核是看重规则的。如本文开头所言,对规则的重视程度代表了一个系统的文明程度和价值取向。当一个行为与规则矛盾时,应该纠正行为,而不是要禁止规则。

0d7672bcbd070684e6b51b4e26127e59.png

8743bee25788b69a6f79fe9ee7bd4606.png

f8c7d15c1070ac823260ac72de223ee6.png

天下无锁

搜索包含问题代码的源文件名,发现就在前几天,有个内核补丁刚好是关于这个文件的。

ba1385e5b00a0185ed29686f94eff644.png

顺着这个补丁看内核代码树的代码,与本地代码比较,很容易发现,在新的代码中,加锁的那些行已经不见了。如此看来,已经有人发现了这个持锁问题,并且做了修正。根据新代码调整本地代码,编译更新后,oops不见了。

读到这里,有格友可能会心生疑惑,难道内核代码不可以持有锁吗?当然不是。问题的关键是持有锁的时间长短。总的来说,锁是用来保护共享资源的,对这些资源的占用代表着对公共资源的占用。对于这种占有,时间要尽可能短,不应该拿到了就不释放,长时间占着。好比是办公大楼里的卫生间,大楼里的每个人都有使用卫生间的权利,但是把卫生间当作个人休息室,进去了在里面刷屏看剧就不对了。为此,Linux内核设计了监督机制,在某段代码即将“赋闲”时,对其做检查,如果发现它手里还持有锁,则给予警告:

“你都要失去执行权了,为什么还持锁不放?”

“我等会儿还用……”

“等会儿还用,就该等会儿再排队获取……”

从珍惜公共资源的角度看,内核的这个检查是健康且必要的。

-END-

a246785630146160e1baf7e3196eb615.jpeg

db1bd35b1d31f63e6489d0181dc057bc.jpeg

08fed95f985cfb939f200e564f269907.jpeg

b2faca76b30b9feb42e8c27a008cf511.png

购买幽兰代码本即可成为兰舍会员,与众多技术高手一起成长。

购买可前往淘宝格友小店:

https://m.tb.cn/h.Uuv7fit?tk=N1iIdn8t4CI

2bed6c2a13ea0c24a71c3c2ade91811b.png

盛格塾是格蠹科技旗下的知识分享平台,是以“格物致知”为教育理念的现代私塾。

本着为先圣继绝学的思想,盛格塾努力将传统文化中的精华与现代科技密切结合,以传统文化和人文情怀阐释现代科技,用现代科技传播传统文化。

访问方式

手机端:微信小程序搜索“盛格塾”

电脑端:下载Nano Code社区版客户端

https://nanocode.cn/#/download

94a87a55ce8323f9a9dda2f961095cad.png

格友公众号

22e6ee2ad0f9e02f078db1096b7f57f5.png

盛格塾小程序

往期 · 精彩推荐

RK3588主板探秘

在RK3588上体验UEFI

比内存被踩还难调试的问题

在调试器下理解RK3588和LINUX5.10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值