去年十一月份对在对国产机的测试中,由于国产机的热插拔功能不是很好,并且其开机设备的扫描差距也比较大,例如第一次可能检测出的设备名字为/dev/sda /dev/sdb /dev/sdc 当可能启动出现的设备成了/dev/sda /dev/sde /dev/sdf这样的,并且发现这时候如果在上一次利用/dev/sd[abc]创建的阵列会在这样的一次重启中坏掉,因为此次他认不得了/dev/sdb /dev/sdc 因为当时对内核源码 ,对阵列的管理等等这一块还刚开始,所以只能从直觉上去认为是因为阵列成员的识别是通过设备文件名,所以那时候就萌生了是否可以利用设备固有的一些信息,例如序列号来识别阵列成员,后来跟老师交流在测试过程的这个问题后,他也比较对这个挺有兴趣,于是从那时候起就开始研究起了内核源码md模块及软阵列的用户管理程序mdadm源码。现在已经算是停下来一些时候,后续会将这一块以几个文章写一写。
本来已经修改了阵列创建后将序列号写到了磁盘上,但是在研究开机阵列的自动检测功能中,却意外的发现,这问题只是存在于国产阵列中,,在标准的服务器中并没有这个问题。
首先,我们还是从系统启动过程说起,但是这里省略了前边执行BIOS,加载MBR,检查活动分区,已经怎样bootloader,(这部分可以参见另一个文章 linux引导过程),这里直接从内核镜像加载到内存后开始后的decompress_kernel->start_kernel开始说起。我们这里先知道,内核镜像加载到内存后需要解压缩内核(decompress_kernel) 然后开始做内核的一些初始化工作(从strart_kernel开始)。
首先我们观察下在内核编译的时候对drivers下的MD的配置,默认的情况是上边,也就是会将MD模块直接编译进入内核,而如果我们将MD选择编译成内核模块,那么AUTODETECT功能会被隐藏,也就是AUTODETECT功能需要MD的支持,这是必然的。(这里不清楚的请查看关于设备驱动Kconfig文件)
而默认情况下是第一种被编译进内核,那也就是说阵列的开机自动检测应该在系统初始化的源码中有所体现。于是,我便开始从MD中追溯自动检测的起源。
首先当然最好的入口是MD的Kconfig,下面截取前边一部分我们想要的
#
# Block device driver configuration
#
menuconfig MD
bool "Multiple devices driver support (RAID and LVM)"
depends on BLOCK
help
Support multiple physical spindles through a single logical device.
Required for RAID and logical volume management.
if MD
config BLK_DEV_MD
tristate "RAID support"
---help---
This driver lets you combine several hard disk partitions into one
logical block device. This can be used to simply append one
partition to another one or to combine several redundant hard disks
into a RAID1/4/5 device so as to provide protection against hard
disk failures. This is called "Software RAID" since the combining of
the partitions is done by the kernel. "Hardware RAID" means that the
combining is done by a dedicated controller; if you have such a
controller, you do not need to say Y here.
More information about Software RAID on Linux is contained in the
Software RAID mini-HOWTO, available from
<http://www.tldp.org/docs.html#howto>. There you will also learn
where to get the supporting user space utilities raidtools.
If unsure, say N.
config MD_AUTODETECT
bool "Autodetect RAID arrays during kernel boot"
depends on BLK_DEV_MD=y
default y
---help---
If you say Y here, then the kernel will try to autodetect raid
arrays as part of its boot process.
If you don't use raid and say Y, this autodetection can cause
a several-second delay in the boot time due to various
synchronisation steps that are part of this step.
If unsure, say Y.
可以看到config MD_AUTODETECT就是我们苦苦寻觅的东西,然后我们从Makefile上去找关于这个,我们多希望从makefile中找到类似obj-$(CONFIG_MD_LINEAR) += linear.o
obj-$(CONFIG_MD_RAID0) += raid0.o
的obj-$(CONFIG_MD_AUTODETECT) +=XXXX.o 这样我们便可以知道这个功能关联的是哪个.c文件,但是很不幸,我们找不到,那我们就只能采用寻找reference(说明一下,我阅读内核源码都是使用sourceInsight,应该一般看内核源码都是使用他把,除非自己在linux下配置自己的VIM),
很开心,我们找到了他,看看是怎么说的:
#ifdef CONFIG_MD_AUTODETECT
static int __initdata raid_noautodetect;
#else
static int __initdata raid_noautodetect=1;
#endif
static int __initdata raid_autopart;
这里就是说,如果我们内核比哪一的时候将MD_AUTODETECT选上了,我们就初始化 raid_noautodetect为0,否则就初始化为1,然后我们就继续按上面的方法查找这边便令是在哪里使用到,一查,原来在内核源码下面的init的do_mounts_md.c中的md_run_setup函数
void __init md_run_setup(void)
{
create_dev("/dev/md0", MKDEV(MD_MAJOR, 0));
if (raid_noautodetect)
printk(KERN_INFO "md: Skipping autodetection of RAID arrays. (raid=autodetect will force)\n");
else
autodetect_raid();
md_setup_drive();
}
而我们知道,我们默认的是将AUTODETECT编译进内核的,那么他应该是跑了else分支,稍微进去看一下就知道他主要是发送一个RAID_AUTORUN的sys_ioctl给MD,我们再继续找md_run_setup看看再哪里使用了它。
好的,有两处,一是在init下的do_mounts.h下
#ifdef CONFIG_BLK_DEV_MD
void md_run_setup(void);
#else
static inline void md_run_setup(void) {}
也就是说,如果我们在编译内核的时候将BLK_DEV_MD选上的话才有md-run_setup函数,不然他就是个空函数,结合上面内核编译的图,我们知道autodetect功能依赖于blk_dev_md 所以,看起来是对的,就是我们先在编译内核时候先是支持了MD,这样就会有md_run_setup函数,而后才有了autodetect使用了这个函数。
另一个有md_run_setup的地方就是do_mounts.c的 prepare_namespace函数
void __init prepare_namespace(void)
{
int is_floppy;
if (root_delay) {
printk(KERN_INFO "Waiting %dsec before mounting root device...\n",
root_delay);
ssleep(root_delay);
}
/*
* wait for the known devices to complete their probing
*
* Note: this is a potential source of long boot delays.
* For example, it is not atypical to wait 5 seconds here
* for the touchpad of a laptop to initialize.
*/
wait_for_device_probe();
md_run_setup();
if (saved_root_name[0]) {
root_device_name = saved_root_name;
if (!strncmp(root_device_name, "mtd", 3) ||
!strncmp(root_device_name, "ubi", 3)) {
mount_block_root(root_device_name, root_mountflags);
goto out;
}
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
if (initrd_load())
goto out;
/* wait for any asynchronous scanning to complete */
if ((ROOT_DEV == 0) && root_wait) {
printk(KERN_INFO "Waiting for root device %s...\n",
saved_root_name);
while (driver_probe_done() != 0 ||
(ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)
msleep(100);
async_synchronize_full();
}
is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;
if (is_floppy && rd_doload && rd_load_disk(0))
ROOT_DEV = Root_RAM0;
mount_root();
out:
devtmpfs_mount("dev");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
再继续查找prepare_namespace。可以看到他是在init的main.c下的kernel_init,如果对于内核启动熟悉的同学看到这个函数应该特别激动,实际他是个线程,看看这个东西在哪里。好了,在init/main.c下的rest_init注册的一个内核线程:
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
rcu_scheduler_starting();
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
unlock_kernel();
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
/** 进入idle循环以消耗空闲的CPU时间片, 该函数从不返回。然而,当有实际工作要处理时,该函数就会被抢占。
*/
cpu_idle();
}
继续往上找找rest_init.,终于在init/main.c的 start_kernel中找到了他,这样就跟我们开始说的内核镜像加载进入内核后执行解压缩后会开始的一个函数了。
所以阵列的自动开机检测功能应该是这样一个流程:
1 内核编译autodetect选上
2 decompress_kernel() -> ......->start_kernel()->rest_init->kernel_thread(kernel_init..)
kernel_init->prepare_namespace()->md_run_setup()->autodetect_raid();
autodetect_raid()->sys_ioctl(fd, RAID_AUTORUN, raid_autopart);
此过程可以参见我使用屏幕录像专家录下的过程:http://pan.baidu.com/s/1bnkZyX5(因为老师让把整个过程在例会上做分享,所以就把他们都用录像专家录下来,在讲的时候不用翻来翻去,讲的思路也顺畅一些)
于是到这里,我们下一步工作就是看下发送过来的RAID_AUTORUN在MD中怎么处理。
现在md.c中的md_ioctl中找到RAID_AUTORUN这个cmd,其对应的函数为autostart_arrays,于是我们看下这个函数
static void autostart_arrays(int part)
{
mdk_rdev_t *rdev;
struct detected_devices_node *node_detected_dev;
dev_t dev;
int i_scanned, i_passed;
i_scanned = 0;
i_passed = 0;
printk(KERN_INFO "md: Autodetecting RAID arrays.\n");
while (!list_empty(&all_detected_devices) && i_scanned < INT_MAX) {
i_scanned++;
node_detected_dev = list_entry(all_detected_devices.next,
struct detected_devices_node, list);
list_del(&node_detected_dev->list);
dev = node_detected_dev->dev;
kfree(node_detected_dev);
rdev = md_import_device(dev,0, 90);
if (IS_ERR(rdev))
continue;
if (test_bit(Faulty, &rdev->flags)) {
MD_BUG();
continue;
}
set_bit(AutoDetected, &rdev->flags);
list_add(&rdev->same_set, &pending_raid_disks);//像阵列的盘放在一起
i_passed++;
}
printk(KERN_INFO "md: Scanned %d and added %d devices.\n",
i_scanned, i_passed);
autorun_devices(part);
}
为了防止提出的源码太多,这里主要以流水方式解释这过程重要函数做的事情,,如下:
autostart_arrays
list_empty(&all_detected_devices) ->
md_import_device
—— alloc_disk_sb指向保存该盘sb页面
——load_super(rdev1,null)这里第二个是null返回1
——read_disk_sb通过发送bio读入sb页
所以,md_import_device创建磁盘struct md_rdev结构,通过list_empty(&all_detected_devices)所有像阵列成员的都在连在一个链表
解释下这个流程:就是autostart_arrays中主要是一个list_empty循环,循环体中调用md_import_device,而md_import_device主要调用alloc_disk_sb分配指向保存该设备盘sb的页面结构,然后调用load_super,但是这里注意每次第二个参数都是null,于是load_super会返回1, 因为load_super很重要后面还用到,所以在这里以超级块0.9的贴出其代码,1.0的版本跟这个类似,这一次的load_super 跑的是
if (!refdev) {
ret = 1;
}
而load_super中主要是发送一个bio读入sb页,所以整个md_import_device就是创建磁盘struct md_rdev结构,通过list_empty(&all_detected_devices)所有像阵列成员的都在连在一个链表。
static int super_90_load(mdk_rdev_t *rdev, mdk_rdev_t *refdev, int minor_version)
{
char b[BDEVNAME_SIZE], b2[BDEVNAME_SIZE];
mdp_super_t *sb;
int ret;
/*
* Calculate the position of the superblock (512byte sectors),
* it's at the end of the disk.
*
* It also happens to be a multiple of 4Kb.
*/
rdev->sb_start = calc_dev_sboffset(rdev->bdev);
ret = read_disk_sb(rdev, MD_SB_BYTES);
if (ret) return ret;
ret = -EINVAL;
bdevname(rdev->bdev, b);
sb = (mdp_super_t*)page_address(rdev->sb_page);
if (sb->md_magic != MD_SB_MAGIC) {
printk(KERN_ERR "md: invalid raid superblock magic on %s\n",
b);
goto abort;
}
if (sb->major_version != 0 ||
sb->minor_version < 90 ||
sb->minor_version > 91) {
printk(KERN_WARNING "Bad version number %d.%d on %s\n",
sb->major_version, sb->minor_version,
b);
goto abort;
}
if (sb->raid_disks <= 0)
goto abort;
if (md_csum_fold(calc_sb_csum(sb)) != md_csum_fold(sb->sb_csum)) {
printk(KERN_WARNING "md: invalid superblock checksum on %s\n",
b);
goto abort;
}
rdev->preferred_minor = sb->md_minor;
rdev->data_offset = 0;
rdev->sb_size = MD_SB_BYTES;
if (sb->level == LEVEL_MULTIPATH)
rdev->desc_nr = -1;
else
rdev->desc_nr = sb->this_disk.number;
if (!refdev) {
ret = 1;
} else {
__u64 ev1, ev2;
mdp_super_t *refsb = (mdp_super_t*)page_address(refdev->sb_page);
if (!uuid_equal(refsb, sb)) {//比较两个块之间的uuid
printk(KERN_WARNING "md: %s has different UUID to %s\n",
b, bdevname(refdev->bdev,b2));
goto abort;
}
if (!sb_equal(refsb, sb)) {
printk(KERN_WARNING "md: %s has same UUID"
" but different superblock to %s\n",
b, bdevname(refdev->bdev, b2));
goto abort;
}
ev1 = md_event(sb);
ev2 = md_event(refsb);
if (ev1 > ev2)
ret = 1;
else
ret = 0;
}
rdev->sectors = rdev->sb_start;
if (rdev->sectors < sb->size * 2 && sb->level > 1)
/* "this cannot possibly happen" ... */
ret = -EINVAL;
abort:
return ret;
}
然后看上面auto_start_arrays最后调用的是 autorun_devices(part),
static void autorun_devices(int part)
{
mdk_rdev_t *rdev0, *rdev, *tmp;
mddev_t *mddev;
char b[BDEVNAME_SIZE];
printk(KERN_INFO "md: autorun ...\n");
while (!list_empty(&pending_raid_disks)) {
int unit;
dev_t dev;
LIST_HEAD(candidates);
rdev0 = list_entry(pending_raid_disks.next,
mdk_rdev_t, same_set);
printk(KERN_INFO "md: considering %s ...\n",
bdevname(rdev0->bdev,b));
INIT_LIST_HEAD(&candidates);
rdev_for_each_list(rdev, tmp, &pending_raid_disks)
if (super_90_load(rdev, rdev0, 0) >= 0) {//uuid不同的话返回-22,uuid相同的话则比较事件,rdev新则成为新表头
printk(KERN_INFO "md: adding %s ...\n",
bdevname(rdev->bdev,b));
list_move(&rdev->same_set, &candidates);
}
/*
* now we have a set of devices, with all of them having
* mostly sane superblocks. It's time to allocate the
* mddev.
*/
if (part) {
dev = MKDEV(mdp_major,
rdev0->preferred_minor << MdpMinorShift);
unit = MINOR(dev) >> MdpMinorShift;
} else {
dev = MKDEV(MD_MAJOR, rdev0->preferred_minor);
unit = MINOR(dev);
}
if (rdev0->preferred_minor != unit) {
printk(KERN_INFO "md: unit number in %s is bad: %d\n",
bdevname(rdev0->bdev, b), rdev0->preferred_minor);
break;
}
md_probe(dev, NULL, NULL);
mddev = mddev_find(dev);
if (!mddev || !mddev->gendisk) {
if (mddev)
mddev_put(mddev);
printk(KERN_ERR
"md: cannot allocate memory for md drive.\n");
break;
}
if (mddev_lock(mddev))
printk(KERN_WARNING "md: %s locked, cannot run\n",
mdname(mddev));
else if (mddev->raid_disks || mddev->major_version
|| !list_empty(&mddev->disks)) {
printk(KERN_WARNING
"md: %s already running, cannot run %s\n",
mdname(mddev), bdevname(rdev0->bdev,b));
mddev_unlock(mddev);
} else {
printk(KERN_INFO "md: created %s\n", mdname(mddev));
mddev->persistent = 1;
rdev_for_each_list(rdev, tmp, &candidates) {
list_del_init(&rdev->same_set);
if (bind_rdev_to_array(rdev, mddev))
export_rdev(rdev);
}
autorun_array(mddev);
mddev_unlock(mddev);
}
/* on success, candidates will be empty, on error
* it won't...
*/
rdev_for_each_list(rdev, tmp, &candidates) {
list_del_init(&rdev->same_set);
export_rdev(rdev);
}
mddev_put(mddev);
}
printk(KERN_INFO "md: ... autorun DONE.\n");
}
#endif /* !MODULE */
static int get_version(void __user * arg)
{
mdu_version_t ver;
ver.major = MD_MAJOR_VERSION;
ver.minor = MD_MINOR_VERSION;
ver.patchlevel = MD_PATCHLEVEL_VERSION;
if (copy_to_user(arg, &ver, sizeof(ver)))
return -EFAULT;
return 0;
}
autorun_devices
list_empty(&pending_raid_disks) 大循环
——rdev_for_each_list()内循环
——super_90_load(rdev, rdev0) 第二个就是参照了
——read_disk_sb
——equal_uuid及比较事件
load函数 uuid不同的话返回-22继续内循环,uuid相同的话则比较事件,rdev从总链表移除并成为新表头。经过内循环将得到uuid相同,表头事件最新的链表,然后利用这个链表创建mddev(利用md_probe,mddev_find),然后bind_rdev_to_array
经过外面的大循环则整理出各个阵列
所以从这个过程上看,阵列的自动启动应该是通过比较每个设备上记录的阵列的uuid来识别的,那么便不存在国产机中如果设备名字变了阵列就坏了的情况才对啊。
于是,便对标准的服务器进行测试,结果,才发现,真的是如代码的样子,即使是设备名字变了,他依旧可以可以组建起阵列,甚至在一台机子上创建的阵列,直接关机后将几个成员盘装进另一个服务器,他启动还可以组建。
过程如下:http://pan.baidu.com/s/1gdgh7EV
于是,便开始寻找为什么国产机上与标准的服务器在处理阵列的自动启动有何区别,结果发现。
在国产机中有两个不一样的地方
1 将autodetect编译成模块
2 将系统启动脚本rc.sysinit修改了
他将autodetect功能编译成了模块,那么就相当于
#ifdef CONFIG_MD_AUTODETECT
static int __initdata raid_noautodetect;
#else
static int __initdata raid_noautodetect=1;
#endif
#ifdef CONFIG_BLK_DEV_MD
void md_run_setup(void);
这两部分都不会跑了,那么在开机系统启动时候就不会去执行自动检测功能了。
而对于2 ,标准的rc.sysinit如左边,而国产机中如右边
国产机的rc.sysinit中
国产机中他居然是利用mdadm的mdadm -As功能来执行阵列的自动检测的,而在mdadm的assemble中,他的load_super是
static int load_super0(struct supertype *st, int fd, char *devname)这样的形式,他相当是通过设备文件句柄去访问设备,那么就会存在如果当设备名变化了,访问到的设备就不再是之前的设备了。所以就出现测试所说的问题,而自己对这个国产机的脚本进行一些修改,注释掉if.....fi之间,恢复与标准的一样的做法, 只是这里modprobe md还是存在,结果确实开机后不能检测到阵列,演示过程如下:http://pan.baidu.com/s/1jGfv10E