
先介绍下这个案例的背景:在一台机器上,运行了产品A,而后需要增加一些功能,但直接根据A来修改比较麻烦,单独再做一个产品B会更加快捷,也就是说,需要让A和B运行于同一平台。产品A和产品B都包含各自的内核驱动模块,这里分别以M(A)和M(B)指代。
在开发产品B的过程中,发现有时加载M(B)之后,通过"lsmod"命令看到的M(B)的引用计数为0,同时各种奇怪的现象开始出现,比如ssh登录不了啊,查看"/proc/devices"会卡住啊。经过多次试验,确定是在卸载M(A)之后,就会必然出现这个问题,但是单独多次加载、卸载M(A)并不会有问题。
比较有用的信息是syslog里打印的一段back trace:

从call trace可以看到,这是一次open()的系统调用,现在M(B)对应的cdev已经注册成功,在"/dev"目录下也生成了设备节点,可以推断这个操作可能就是在open这个设备,然后在调用try_module_get()进行引用计数加1的时候,由于PTE的内容为0,即访问的虚拟地址没有对应的物理地址,就会触发page fault(异常地址保存在CR2中)。
由于page fault发生在内核态,且属于不可修复的错误,因此将形成Oops。内核oops之后可能还是可以继续运行,但是会出现一些不可预知的结果(比如之前提到的那些现象)。
那为什么会访问到这个地址去呢?比较关键的是这2个函数:
static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *p = inode->i_cdev;
if (!p) {
struct kobject *kobj;
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
...
}
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
struct kobject *kobj;
struct probe *p;
retry:
for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next)
...
}
似乎是在通过主设备号(major)来查找module所对应的kobject的相关信息。出问题的M(B)对应的主设备号是向内核动态申请的,获得的值是243(第一个参数为0代表动态分配,以伪代码示意)。
M(B)->major = register_chrdev(0, M(B)->name, &M(B)->dev_fops);
再来看看M(A),它卸载之前使用的主设备号也是243。分析到这里,感觉离真相开始接近了。那为什么都是243呢?
static int find_dynamic_major(void)
{
struct char_device_struct *cd;
for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) {
if (chrdevs[i] == NULL)
return i;
...
}
因为对于动态申请,内核是从254往下找的(直到"CHRDEV_MAJOR_DYN_END"代表的234),从"/proc/devices"的输出信息中可以看到,从244到254的主设备后已经被其他驱动对应的设备占据了,当M(A)被卸载后,243的主设备号被释放,因此当M(B)加载完成并申请设备号时,243就被内核给了M(B),这无可非议。
难道是因为M(A)卸载的不干净,而M(B)访问到的这个错误地址就是M(A)之前使用的地址?为了印证这一点,再重启复现一次,当M(A)被加载后,查看其section的地址信息:

然后对比一下M(B)试图访问的地址,两者一致,证据确凿,M(A)这回是脱不了干系了。但所谓“卸载的不干净”是个通俗但不太专业的说法,什么叫不干净,明明"dev"下的设备节点删除了,"lsmod"看不到这个module了,在/proc/modules"里也消失地无影无踪了。
那是不是M(B)不用243这个设备号就不会出现这个问题呢?于是,我查看"/proc/devices"文件,选了一个没有其他设备使用的major,尝试通过静态注册设备号的方式来验证。
前面用的这个"register_chrdev"函数存在一个问题,当申请一个主设备号时,会将这个major下面的所有子设备号(minor)都给你,实在太浪费了,所以后来出现了一个更精确的"register_chrdev_region","region"的意思就是你可以指定子设备号的范围。
除此以外,这两个函数的使用也是存在区别的,带"region"的函数需要自行进行"cdev"相关的操作。
int __register_chrdev(unsigned int major, ...)
{
struct cdev *cd = __register_chrdev_region(major, baseminor, count, name);
cdev = cdev_alloc();
cdev_add(cdev, MKDEV(cd->major, baseminor), count);
...
}
这样改动之后,问题没有再出现了,但是静态注册的方式存在隐患,可能和别的模块使用的设备号冲突(就像静态IP一样),还是推荐尽量使用动态申请的方式。好在产品A的团队很快介入了,负责分析这个问题的同事是Windows方向的,对Linux内核相对不是那么熟悉,他在网上查了一下,截了张图片,问我是不是由于没有使用cdev_del()导致。

我并没有看过产品A的代码,不知道M(A)卸载用的什么函数,但最开始看到这张图片的时候,我的第一想法是:会用"unregister_chrdev_region"的,大概率是应该知道配合cdev操作来用的吧。
然后我看了下"cdev_del"的实现,进行的主要是kobject的unmap操作:
void cdev_del(struct cdev *p)
{
kobj_unmap(cdev_map, dev, count);
kobject_put(&p->kobj);
}
而在产品A的代码里,确实没有调用这个函数,卸载时没有unmap,所以M(B)在进行map时,找到的就是之前主设备号243的owner的地址。看来应该就是这个原因引起的了,修改后重新出包,问题解决。
当年这位改代码的同学还是蛮有技术追求的,知道用带"region"的函数更好,而且看到在写注册函数的时候,也是用了"cdev_add"的,姑且认为他并不是不知道,只是在写卸载函数的时候一不小了遗漏了吧(以下使用伪代码的形式示意)。通过review能减少这种问题吗?也许吧。
dev_t dev-A = MKDEV(major, minor);
unregister_chrdev_region(dev-A, nr);
kfree(dev-A);
//unregister_chrdev(dev-A-major, "dev-A-name");
我们的产品需要适配多种版本的Linux内核,在内核升级的过程中,常常会涉及到内核API的变更,在替换调用的内核API的时候,一定要知道两者实现上的差异。
这个bug其实存在相当长一段时间了,只是用户一般不会在安装了我们的产品A后把它卸载掉,然后安装另一个驱动,所以一直没有暴露出来,直到开发产品B,才触发了这个bug的显现。
这个问题让我有点后怕的是,好在M(A)只是没有做unmap,地址还是释放了的,否则当M(B)试图去用这个地址的时候,会以为是个有效的地址,那么接下来将访问到一些错误的内容,最后的现象可能是自己的功能异常。
这个时候,你多半以为是M(B)自己的问题,怎么也想不到问题的根源是出在M(A)的卸载上。发生page fault,算是第一时间暴露了问题,否则问题的出现与根源隔的远,就很难排查了。这同时也提醒我们,当客户的机器出现了故障,带着一个包含我们产品代码打印的call trace来兴师问罪时,也并非就一定是我们产品导致的问题。
原创文章,转载请注明出处。