一个小BUG,引出对Linux启动机制Systemd的代码分析
Posted beyondma
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个小BUG,引出对Linux启动机制Systemd的代码分析相关的知识,希望对你有一定的参考价值。
最近我在生产上遇到一个非常有意思的问题,在Cent OS7以上的操作系统中,VG卷组一激活其默认对应的文件系统也一并挂载上了,而且这还不是红帽和CentOS的特有问题,如果fstab配置default参数的话,其它Linux发行版也有同样的问题。
一般来说用户只需要在启动的时候自动挂载文件系统,在日常使用中激活卷组后,用户很可能希望把这个卷组挂载到其它位置上,而非默认位置,Systemd和fstab这样的联动操作其实给用户日常的使用带来了不少的困扰。
这其实应该是systemd与fstab配合的一个bug,为了说清这个问题,我们先来看,fstab的使用方法,/etc/fstab 文件包含了如下字段,通过空格或 Tab 分隔:
<file system> <dir> <type> <options> <dump> <pass>
- <file systems>:要挂载的分区或存储设备.
- <dir>:文件系统的挂载位置。
- <type>:要挂载设备或是分区的文件系统类型,
- <options>:挂载时使用的参数,这个是我们重点要说的。
其中auto:在启动时时自动挂载。
defaults:使用文件系统的默认挂载参数,默认参数为:rw, suid, dev, exec, auto, nouser, async,也就是包含了auto这个参数
正如前文所说按照常理理解,auto应该是单指启动时的自动挂载。
初识systemd
在Cent os 7版本之前,红帽系的Linux一直采用init机制来进行系统初始化,现在还有很多经典书籍在介绍Linux启动时还是会详细说明0号init进程的由来,总体来说systemd之前的sysvinit和upstart没有太大区别,upstart只是一个支持USB启动的并行版sysvinit。
systemd的出现颇有后来者居上的气势,目前已经基本统一了linux初始化工具的江湖,它克服 sysvinit串行执行启动步骤的,大幅提高系统的启动速度。凭借着优异的表现目前upstart的拥趸Ubuntu也开始在最新版本中使用systemd了。
systemd提供了和 sysvinit 兼容的特性,原先版本系统中已经存在的服务和进程无需修改。这大幅降低了用户的升级成本,使得 systemd的升级替换相对比较平滑。而且systemd 提供了比 upstart 更优秀的并行启动能力,采用了 socket / D-Bus activation 等技术启动服务。并取得了更快的启动速度,我们明显可以感受到在相同配置的配置下,CentOS7比6启动速度要快。
但是因为systemd过于激进了,这其实也是造成前文那个BUG的直接原因。
systemd如何了解系统启动情况
systemd在进行启动任务编排并控制系统其它服务(service)时,需要详细了解系统当前的状态,我们看到systemd使用的技术基于inotify的钩子机制进行的。
比如在https://github.com/systemd/systemd/basic/fs-util.c下的inotify_add_watch_fd函数就是一个添加监视点的入口。
int inotify_add_watch_fd(int fd, int what, uint32_t mask) {
int wd;
/* This is like inotify_add_watch(), except that the file to watch is not referenced by a path, but by an fd */
wd = inotify_add_watch(fd, FORMAT_PROC_FD_PATH(what), mask);
if (wd < 0)
return -errno;
return wd;
}
而再翻开Linux下inotify的代码,你会发现添加监控点及通知操作的代码当中都是有加锁动作的,尤其是在新建监控点时甚至直接上了自旋锁。
https://github.com/torvalds/linux/blob/f8fbb47c6e86c0b75f8df864db702c3e3f757361/fs/notify/inotify/inotify_user.c
static int inotify_new_watch(struct fsnotify_group *group,
struct inode *inode,
u32 arg)
{
struct inotify_inode_mark *tmp_i_mark;
__u32 mask;
int ret;
struct idr *idr = &group->inotify_data.idr;
spinlock_t *idr_lock = &group->inotify_data.idr_lock;//直接上spinlock
mask = inotify_arg_to_mask(inode, arg);
tmp_i_mark = kmem_cache_alloc(inotify_inode_mark_cachep, GFP_KERNEL);
if (unlikely(!tmp_i_mark))
return -ENOMEM;
fsnotify_init_mark(&tmp_i_mark->fsn_mark, group);
tmp_i_mark->fsn_mark.mask = mask;
tmp_i_mark->wd = -1;
ret = inotify_add_to_idr(idr, idr_lock, tmp_i_mark);
if (ret)
goto out_err;
/* increment the number of watches the user has */
if (!inc_inotify_watches(group->inotify_data.ucounts)) {
inotify_remove_from_idr(group, tmp_i_mark);
ret = -ENOSPC;
goto out_err;
}
/* we are on the idr, now get on the inode */
ret = fsnotify_add_inode_mark_locked(&tmp_i_mark->fsn_mark, inode, 0);
if (ret) {
/* we failed to get on the inode, get off the idr */
inotify_remove_from_idr(group, tmp_i_mark);
goto out_err;
}
/* return the watch descriptor for this new mark */
ret = tmp_i_mark->wd;
out_err:
/* match the ref from fsnotify_init_mark() */
fsnotify_put_mark(&tmp_i_mark->fsn_mark);
return ret;
}
我们知道加锁区域内的代码一定要尽力的简洁,耗时操作是严格禁止的,只有这样才能提升内核的运转效率,尤其是自旋锁会将所有CPU全部阻塞住,更是要加小心,因此systemd在接收到inotify的通知时必须要以最快速度,运行完成全部代码。
VG一激活文件系统就挂载的问题到底能不能改
对于VG到底是在启动时被激活的,还是由用户手工激活的,systemd完全可以判断出来,但是这个判断是有代价的,由于inotify的api当中并没有提供一个标志是否为启动时调用的onboot标志,因此systemd只能自行判断系统是否处在启动态,而加这个判断无论是想取得系统的运行时间,还是要取得用户登陆状态,对于systemd这种优化到极致的软件来说,可能都是不能接受的。
为了验证上述是观点,我们可以在实测中尝试在通知处理函数中加入一些耗时操作来观察对于系统的影响,由于inotify可以改变原系统调用(syscall)行为的,
受试验实验环境所限,我手头机器的硬件水平不足以支持我搭建一个虚拟机平台任意测试的要求,因此模拟当中我们使用了另一个监测机制kprobe来进行,和inotify相比,kprobe属于旁路监测重量级较轻,我自行注册了一个监测文件变化的探针,然后在处理事件时加入延时操作,观察对于系统IO的影响。
结果测试发现在我所在的4核/8G的平台上,每秒钟文件打开、关闭文件操作的次数,由每秒867次的锋值下降到了72次,90%以上的下降。因此这个在systemd项目下开了近三年的ISSUE似乎没有好的解法,无论是sysvinit的0号init进程机制,还是在inotify的处理函数中加入系统运行状态的判断,都不是好的办法。如果各位读者有什么好的建议或者思路可以留言提供一下,咱们共同探讨!
以上是关于一个小BUG,引出对Linux启动机制Systemd的代码分析的主要内容,如果未能解决你的问题,请参考以下文章
linux 中断底半部机制对比(任务队列,工作队列,软中断)--由linux RS485引出的血案转