查看原文
其他

有一种错叫持有锁

格蠹老雷 格友 2023-06-10


有一种错叫

持有锁



记得有一次在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_stackoverflowpanic_on_unrecovered_nmipanic_on_warnpanic_on_rcu_stallpanic_on_io_nmipanic_on_taint

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




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是系统线程的线程名。

根据调用栈的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)。

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



  代码源头  

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

#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);}

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

在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);}

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

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

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

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

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




回避解法

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

CONFIG_LOCKDEP=yCONFIG_LOCK_STAT=y

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

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

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








天下无锁

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

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

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

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

“我等会儿还用……”

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

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



-END-





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

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

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


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


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


访问方式

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

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

https://nanocode.cn/#/download

格友公众号

盛格塾小程序


往期 · 精彩推荐

RK3588主板探秘

在RK3588上体验UEFI

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

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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存