kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

pssh、pscp命令

http://blog.csdn.net/kumu_linux/article/details/8562320

pssh是一个python编写可以在多台服务器上执行命令的工具,同时支持拷贝文件,是同类工具中很出色的,类似pdsh,个人认为相对pdsh更为简便,使用必须在各个服务器上配置好密钥认证访问。

项目地址: https://code.google.com/p/parallel-ssh/

PSSH provides parallel versions of OpenSSH and related tools. Included are pssh, pscp, prsync, pnuke, and pslurp. The project includes psshlib which can be used within custom applications. The source code is written in Python and can be cloned from:

git clone http://code.google.com/p/parallel-ssh/

PSSH is supported on Python 2.4 and greater (including Python 3.1 and greater). It was originally written and maintained by Brent N. Chun. Due to his busy schedule, Brent handed over maintenance to Andrew McNabb in October 2009.

下载安装

下载

wget http://parallel-ssh.googlecode.com/files/pssh-2.3.1.tar.gz

本地下载 pssh-2.3.1.tar.gz

安装
1
2
3
tar xf pssh-2.3.1.tar.gz  
cd pssh-2.3.1/  
python setup.py install  
参数命令介绍

pssh 在多个主机上并行地运行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   -h 执行命令的远程主机列表  或者 -H user@ip:port  文件内容格式[user@]host[:port]

   -l 远程机器的用户名

   -P 执行时输出执行信息
   -p 一次最大允许多少连接
   -o 输出内容重定向到一个文件
   -e 执行错误重定向到一个文件
   -t 设置命令执行的超时时间
   -A 提示输入密码并且把密码传递给ssh
   -O 设置ssh参数的具体配置,参照ssh_config配置文件
   -x 传递多个SSH 命令,多个命令用空格分开,用引号括起来
   -X 同-x 但是一次只能传递一个命令
   -i 显示标准输出和标准错误在每台host执行完毕后

其他命令

1
2
3
4
5
6
7
pscp     传输文件到多个hosts,类似scp

pslurp   从多台远程机器拷贝文件到本地

pnuke    并行在远程主机杀进程

prsync   使用rsync协议从本地计算机同步到远程主机

实例

pssh
1
2
3
4
5
$ pssh -h ip.txt -l root chkconfig --level 2345 snmpd on  
[1] 10:59:29 [SUCCESS] ... ...  
[2] 10:59:29 [SUCCESS] ... ...  
[3] 10:59:29 [SUCCESS] ... ...  
... ...  
pscp
1
2
3
4
5
$ pscp -h ip.txt -l root /etc/snmp/snmpd.conf /etc/snmp/snmpd.conf  
[1] 11:00:42 [SUCCESS] ... ...  
[2] 11:00:42 [SUCCESS] ... ...  
[3] 11:00:42 [SUCCESS] ... ...  
... ...  

linux 中断下半部

http://blog.chinaunix.net/uid-24203478-id-3111803.html

与Linux中断息息相关的一个重要概念是Linux中断分为两个半部:上半部(tophalf)和下半部(bottom half)。上半部的功能是"登记中断",当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。因此,上半部执行的速度就会很快,可以服务更多的中断请求。但是,仅有"登记中断"是远远不够的,因为中断的事件可能很复杂。因此,Linux引入了一个下半部,来完成中断事件的绝大多数使命。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的,下半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。

在Linux2.6的内核中存在三种不同形式的下半部实现机制:软中断,tasklet和工作队列。

Tasklet基于Linux softirq,其使用相当简单,我们只需要定义tasklet及其处理函数并将二者关联:

1
2
void my_tasklet_func(unsigned long); //定义一个处理函数:
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); //定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联

然后,在需要调度tasklet的时候引用一个简单的API就能使系统在适当的时候进行调度运行:

1
tasklet_schedule(&my_tasklet);

此外,Linux还提供了另外一些其它的控制tasklet调度与运行的API:

1
2
3
4
5
DECLARE_TASKLET_DISABLED(name,function,data); //与DECLARE_TASKLET类似,但等待tasklet被使能
tasklet_enable(struct tasklet_struct *); //使能tasklet
tasklet_disble(struct tasklet_struct *); //禁用tasklet
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); //类似DECLARE_TASKLET()
tasklet_kill(struct tasklet_struct *); // 清除指定tasklet的可调度位,即不允许调度该tasklet

我们先来看一个tasklet的运行实例,这个实例没有任何实际意义,仅仅为了演示。它的功能是:在globalvar被写入一次后,就调度一个tasklet,函数中输出"tasklet is executing":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//定义与绑定tasklet函数
void test_tasklet_action(unsigned long t);
DECLARE_TASKLET(test_tasklet, test_tasklet_action, 0);

void test_tasklet_action(unsigned long t)
{
	printk("tasklet is executing\n");
}

...

ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
	...
	if (copy_from_user(&global_var, buf, sizeof(int)))
	{
		return - EFAULT;
	}

	//调度tasklet执行
	tasklet_schedule(&test_tasklet);
	return sizeof(int);
}

下半部分的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。

在Linux2.6的内核中存在三种不同形式的下半部实现机制:软中断,tasklet和工作队列。

下面将比较三种机制的差别与联系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
软中断:    1、软中断是在编译期间静态分配的。
           2、最多可以有32个软中断。
           3、软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。
           4、可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),
              因此也需要使用自旋锁来保护其数据结构。
           5、目前只有两个子系直接使用软中断:网络和SCSI。
           6、执行时间有:从硬件中断代码返回时、在ksoftirqd内核线程中和某些显示检查并执行软中断的代码中。

tasklet:   1、tasklet是使用两类软中断实现的:HI_SOFTIRQ和TASKLET_SOFTIRQ。
           2、可以动态增加减少,没有数量限制。
           3、同一类tasklet不能并发执行。
           4、不同类型可以并发执行。
           5、大部分情况使用tasklet。

工作队列:  1、由内核线程去执行,换句话说总在进程上下文执行。
           2、可以睡眠,阻塞。

gdb线程

GDB多线程调试的基本命令。

info threads 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。 前面有*的是当前调试的线程。

thread ID 切换当前调试的线程为指定ID的线程。

break thread_test.c:123 thread all 在所有线程中相应的行上设置断点

thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command。

thread apply all command 让所有被调试线程执行GDB命令command。

set scheduler-locking off|on|step 估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

理解Ksplice执行过程

http://m.blog.chinaunix.net/uid-29280350-id-4717510.html

http://m.blog.chinaunix.net/uid-29280350-id-4906197.html


注:在Linux-3.0.0 到 linux-3.8.0上能够正常运行,超过3.8.13就会导致系统桌面崩溃

1.Ksplice-create

Ksplice-create用于创建补丁文件,根据用户提供的不同的更新文件,ksplice-create有三种不同的途径:
1)Patch文件
2)Diffext指定新文件的后缀
3)使用git指定新的标记

同时,ksplice-create还需要指定orig_config_dir(指定config的目录),在该目录下要有以下几个文件:
1)当前run内核的System.map
2)当前run内核的.config
3)当前run内核的modules库下的build链接
以上三项缺一不可。

1.1配置

根据配置变量,组织make命令:

1
make -rR

如果定义了jobs

1
-jn

如果定义了verbose level

1
V=1 否则 -s

make_ksplice 变量:

1
@make -f $datadir/Makefile.ksplice @kbuild_flags

如果定义了build_modules

1
KSPLICE_BUILD_MODULES=1

1.2 Revert

配置变量完成后,ksplice-create会查找linux内核代码目录下是否会存在*.KSPLICE_presrc文件,存在该类型的文件则表明在该linux内核目录下曾制作过补丁文件,因此需要先将代码恢复为原始代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
my @revert_flags=("KSPLICE_MODE=revert");
	Revert_orig()
		Find出*.KSPLICE_presrc的文件,将之恢复为原始文件
		执行命令:make -rR -f Makefile.ksplice KSPLICE_MODE=revert

进入Makefile.ksplice文件:
	Makefile.ksplice默认目标是__ksplice,
	__ksplice: $(ksplice-deps) $(ksplice-dirs)
		@:

	目标只是依赖两个dirs,没有具体的执行命令,所有的执行命令都是在依赖中执行的。对于ksplice-dirs的命令:

	$(ksplice-dirs):
		$(Q) $(MAKE) $(build)=$(@:_ksplice_%=%)
		其中
		build := -f $(ksplice-makefile) obj

		所以命令展开就是:
		make -f /usr/local/share/ksplice/Makefile.ksplice obj=arch/x86/crypto

		又再次进入makefile.ksplice,这次传入了obj。

revert的作用就是把.ksplice_pre的文件执行cmd_ksplice-revert。

最后通过$(call cmd, ksplice-revert)调用

1
2
cmd_ksplice-revert = touch -r ksplice-revert-stamp $(@:_ksplice-revert_%=%); \
	mv $(@:_ksplice-revert_%=%) $(@:_ksplice-revert_%.KSPLICE_pre=%)

在然后根据ksplice-clean-files把ksplice生成的文件clean掉。

1.3 SNAP

执行完revert之后,重新回到ksplice-create文件中继续执行

1
2
@snap_flags = (“KSPLICE_MODE=snap”);
runval_raw(@make_ksplice,@snap_flags)

展开即为:

1
执行命令:make -rR -f Makefile.ksplice KSPLICE_MODE=snap

进入Makefile.ksplice文件:

1
2
3
4
5
6
7
8
ifeq ($(KSPLICE_MODE),snap)
$(obj)/%.o.KSPLICE: $(obj)/%.o FORCE
	$(if $(strip $(wildcard $<.KSPLICE_pre) $(filter $<, $?)), \
		$(call cmd, ksplice-snap))
	else
		$(obj)/%. o.KSPLICE:$(obj)/%.o
		$(call cmd, ksplice-diff)
	endif

其中

1
2
3
cmd_ksplice-snap = $(ksplice-script) snap $@
cmd_ksplice-diff = $(ksplice-script) diff $@
ksplice-scrript = $(dir $(ksplice-makefile))ksplice-obj.pl

进入ksplice-obj.pl文件中:

1
2
3
4
5
6
7
sub do_snap {
	my ($out) = @_;
	my ($obj) = $out = ~ /^(.*)\.KSPLICE$/ or die;
	die if (!-e $obj);
	unlink "$obj.KSPLICE_pre" if (-e "$obj.KSPLICE_pre");
	empty_diff($out);
}
1
2
3
4
5
6
7
8
9
sub empty_diff {
	my ($out) = @_;
	my ($obj) = $out =~ /^(.*)\.KSPLICE$/ or die;
	unlink "$obj.KSPLICE_new_code" if (-e "$obj.KSPLICE_new_code");
	unlink "$obj.KSPLICE_old_code" if (-e "$obj.KSPLICE_old_code");
	open OUT, '>', "$out.tmp";
	close OUT;
	rename "$out.tmp", $out;
}

snap的工作就是生成一个.o.KSPLICE空文件,函数empty_diff就是用来生成空文件的。.o.KSPLICE文件用来作为一个标志位,只是为了后续diff阶段,如果有不同的.o就会把.o.KSPLICE中写入1,最后遍历所有的.o.KSPLICE,哪些为1就知道哪些有差异了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sub do_diff {
	my ($out) = @_;
	my ($obj) = $out =~ /^(.*)\.KSPLICE$/ or die;
	my $obj_pre = "$obj.KSPLICE_pre";
	die if (!-e $obj);
	die "Patch creates new object $obj" if (!-e $obj_pre);
	if (system('cmp', '-s', '--', $obj_pre, $obj) == 0) {
		unlink $obj_pre;
		return empty_diff($out);
	}

	runval("$libexecdir/ksplice-objmanip", $obj, "$obj.KSPLICE_new_code", "keep-new-code", "$obj.KSPLICE_pre", $ENV{KSPLICE_KID});
	return empty_diff($out) if (!-e "$obj.KSPLICE_new_code");

	open OUT, '>', "$out.tmp";
	print OUT "1\n";
	close OUT;
	rename "$out.tmp", $out;
	runval("$libexecdir/ksplice-objmanip", $obj_pre, "$obj.KSPLICE_old_code", "keep-old-code");
}

无论snap还是diff都是要创建目标.o.KSPLICE, 但是动作不一样,并且snap是FORCE,diff不是强制的,最关键的就是打了patch之后,就会重新生成patch对应的.o,此时依赖条件更新了,就会执行diff命令。

1.4 创建ksplice模块

将kmodsrc目录拷贝到tmp目录下,执行命令:

1
@make_kmodsrc = (@make, "-C", $kernel_headers_dir, "M=$kmodsrc", "KSPLICE_KID=$kid", "KSPLICE_VERSION=1.0", "map_printk=$map_printk");

编译内核模块,然后make modules_install,

1
@make_kmodsrc_install = (@make_kmodsrc, qw(modules_install --old-file=_modinst_post --old-file=_emodinst_post), "MAKE=make --old-file=_modinst_post --old-file=_emodinst_post", "INSTALL_MOD_STRIP=1", "MODLIB=$tmpdir/ksplice-modules");

1.5 PATCH

将准备的patch文件更新到内核中:

1
runval_infile($patchfile, "patch", @patch_opt, "-bz", ".KSPLICE_presrc");

-bz的意思:
-b 备份原始文件
-z 是用.KSPLICE_presrc为后缀备份原始文件。

要注意patch文件中各个文件的行号等内容要对齐,不然patch文件无法更新到内核源码中(要每个文件都要检查,并确认patch文件可用)。

打上补丁后,执行:

1
make_ksplice KSPLICE_MODE=diff

1.6 DIFF

1
2
my @diff_flags = ("KSPLICE_MODE=diff")
runval_raw(@make_ksplice, @diff_flags);

即执行命令:

1
make -rR -f Makefile.ksplice KSPLICE_MODE=diff

进入Makefile.ksplice文件:

1
2
3
4
5
6
7
8
9
10
11
ifeq ($(KSPLICE_MODE),diff)
	define ksplice-cow-check
		$(if $(strip $(1)),$(if $(filter-out %.KSPLICE,$@),$(if $(wildcard $@),$(if $(wildcard $@.KSPLICE_pre),,$(call cmd,ksplice-cow)))))$(1)
	endef

	define ksplice-add-cow-check
		$(v) = $$(call ksplice-cow-check,$(value $(v)))
	endef

	ksplice-cow-eval += $(foreach v,if_changed if_changed_dep if_changed_rule,$(ksplice-add-cow-check))
endif   # KSPLICE_MODE

其中

1
cmd_ksplice-cow = cp -a $@ $@.KSPLICE_pre

diff比较的是.o.KSPLICE_pre 和 新编译的.o,从do_diff的实现来看,在diff之前,KSPLICE_pre就已经生成了,生成KSPLICE_pre的命令只有cmd-ksplice-cow, 即diff操作的结果。

1
2
3
4
5
6
7
8
9
$KSPLICE_MODE ?= diff
ifeq ($(KSPLICE_MODE),snap)
	$(obj)/%.o.KSPLICE: $(obj)/%.o FORCE
		$(if $(strip $(wildcard $<.KSPLICE_pre) $(filter $<, $?)), \
			$(call cmd, ksplice-snap))
else
	$(obj)/%. o.KSPLICE:$(obj)/%.o
	$(call cmd, ksplice-diff)
endif

在此处调用

1
cmd_ksplice-diff=$(ksplice-script) diff $@

进入ksplice-obj.pl中调用函数do_diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sub do_diff {
	my ($out) = @_;
	my ($obj) = $out =~ /^(.*)\.KSPLICE$/ or die;
	my $obj_pre = "$obj.KSPLICE_pre";
	die if (!-e $obj);
	die "Patch creates new object $obj" if (!-e $obj_pre);
	if (system('cmp', '-s', '--', $obj_pre, $obj) == 0) {
		unlink $obj_pre;
		return empty_diff($out);
	}
	runval("$libexecdir/ksplice-objmanip", $obj, "$obj.KSPLICE_new_code", "keep-new-code", "$obj.KSPLICE_pre", $ENV{KSPLICE_KID});
	return empty_diff($out) if (!-e "$obj.KSPLICE_new_code");

	open OUT, '>', "$out.tmp";
	print OUT "1\n";
	close OUT;
	rename "$out.tmp", $out;

	runval("$libexecdir/ksplice-objmanip", $obj_pre, "$obj.KSPLICE_old_code", "keep-old-code");
}

此处有三个关键点,第一system系统调用cmp,比较$(obj)和$obj_pre之间的不同,第二通过调用ksplice-objmanip(即objmanip)生成new_code,并且在.o.KSPLICE_pre中写入标志位1,第三步调用ksplice-objmanip(即objmanip)将未打patch之前的代码生成old_code。第二步和第三步进入到C文件objmanip.c的main()函数中,根据传入的参数的不同,调用不同的函数,最后生成new和old。

1.7 模块编译

命令:

1
runstr(qw(find -name *.KSPLCE* !  ( - name *.KSPLICE -empty ) ! -name .*.KSPLICE.cmd -print0))

找出所有.KSPLICE非空的文件,将读入的内容保存到@modules中。对MOD的处理是在KSPLICE_KMODSRC中生成的。

命令:

1
runval(@make_ksplice, "KSPLICE_MODE=modinst", "MODLIB=$tmpdir/modules", "INSTALL_MOD_STRIP=1", "modules=@modulepaths");

在Makefile.ksplice中,对modinst的处理是:

1
2
3
4
5
6
ifeq ($(KSPLICE_MODE),modinst)
ksplice-deps += ksplice_modinst
PHONY += ksplice_modinst
ksplice_modinst:
	$(Q) $(MAKE) –f $(srctree)/scripts/Makefile.modinst
endif

这里的Makefile.modinst和Makefile.modpost都是内核script中的Makefile。

在ksplice-create中分别调用了两次make_kmodsrc, 第一次编译出ksplice.ko模块,第二次传入参数KSPLICE_MODULES=@modules 生成new.ko 和 old.ko文件。在kmodsrc目录中的Makefile中,第一次编译的是KSPLICE_CORE:

1
2
KSPLICE_CORE = ksplice-$(KSPLICE_KID)
obj-m += $(KSPLICE_CORE).o

实际上最终编译生成ksplice-kid.ko 还是依靠的obj-m的方法编译的。

第二次编译的时候传入的modules,同时KSPLICE_SKIP_CORE=1,表示不编译ksplice.ko

在ksplice-create中,执行命令:

1
2
runval(@make_kmodsrc, "KSPLICE_MODULES=@modules", "KSPLICE_SKIP_CORE=1");
runval(@make_kmodsrc_install, "KSPLICE_MODULES=@modules", "KSPLICE_SKIP_CORE=1");

在kmodsrc/Makefile中:

1
2
3
4
5
6
ifneq ($(KSPLICE_MODULES),)
	$(foreach mod,$(KSPLICE_MODULES),$(obj)/new-code-$(target).o): $(obj)/%.o: $ (src)/new_code_loader.c FORCE
	$(call if_changed_rule,cc_o_c)
	$(foreach mod,$(KSPLICE_MODULES),$(obj)/old-code-$(target).o): $(obj)/%.o: $ (src)/old_code_loader.c FORCE
	$(call if_changed_rule,cc_o_c)
endif

以new为例:

1
2
$(KSPLICE)-n-objs = $(ksplice-new-code-objs)
ksplice-new-code-objs = new-code-$(target).o collect-new-code-$(mod).o

new.ko由new-code-mod.o 和 collect-new-code-$(mod).o 组成。

new-code-mod.o的命令:

1
2
3
$(foreach mod,$(KSPLICE_MODULES),$(obj)/new-code-$(target).o): $(obj)/%.o: \
	$ (src)/new_code_loader.c FORCE
$(call if_changed_rule,cc_o_c)

collect-new-code-$(mod).o的命令:

1
2
3
4
5
$(obj)/collect-new-code-%.o: $(obj)/%.o.KSPLICE_new_code $(obj)/ksplice.lds     FORCE
$(call if_changed,ksplice-collect)
cmd_ksplice-collect = \
	$(ksplice-script) finalize $< $<.final $* && \
	$(LD) --script=$(obj)/ksplice.lds -r -o $@ $<.final

collect的命令最后调用do_finalize生成mod.final,再结合ksplice.lds 生成collect-new-code-mod.o

2.ksplice-apply

2.1 校验补丁文件

第一,执行命令:chdir(unpack_update($file))

其中 unpack_update()在文件Ksplice.pm中,首先检测使用的补丁文件是否是目录,如果是则返回到ksplice-apply文件中;如果是压缩文件则将其解压到/tmp/临时目录下,然后将路径返回到ksplice-apply文件中。

第二,检测目标路径中是否存在contents文件,不存在就退出ksplice-apply程序。

第三,检测当前系统/sys/moudle下面是否已经加载了该补丁文件。

在上述操作中,如果有不满足要求的,通过设置apply_errors来输出错误信息。

2.2 加载补丁文件

执行命令load_module($change->{new_code_file})

1
2
3
4
5
6
7
8
9
10
sub load_module {
	my ($module, @params) = @_;
	push @modules_loaded, ($module =~ m/^(.)\.ko$/);
	if (runval_raw("insmod", $module, @params) != 0){
		pop @modules_loaded;
		child_error();
		return 0;
	}
	return 1;
}

在函数load_module()中调用系统函数insmod来加载ko文件。如果在加载过程中出现错误,由insmod返回错误信息。

内核热补丁技术揭秘

https://ruby-china.org/topics/20680

下述为UCloud资深工程师邱模炯在InfoQ架构师峰会上的演讲——《UCloud云平台的内核实践》中非常受关注的内核热补丁技术的一部分。给大家揭开了UCloud云平台内核技术的神秘面纱。

如何零代价修复海量服务器的Linux内核缺陷?

对于一个拥有成千上万台服务器的公司,Linux内核缺陷导致的死机屡见不鲜。让工程师们纠结的是,到底要不要通过给服务器升级内核来修复缺陷?升级意味者服务器重启、业务中断以及繁重的准备工作;不升级则担心服务器死机,同样造成业务中断和繁重的善后工作。

而在今天的云计算时代,一台宿主机往往运行多个云主机,每一次重启不管是主动升级还是被动死机,都意味着中断其上运行的所有云主机。因此,宿主机内核缺陷的修复更加棘手。

而作为一个支撑着上万家企业用户IT基础架构的云服务商,UCloud云平台上的海量宿主机又是如何修复内核缺陷的呢?

邱模炯透露,如果按照传统的重启方式来修复,那么无论是对于UCloud或是用户,都意味着繁重的运维和业务中断。但是,UCloud通过“内核热补丁技术”——即给运行中的内核打上二进制补丁,UCloud已经做到了零代价免重启修复海量服务器的内核缺陷!目前为止,UCloud对所发现的上游内核10+个缺陷全以热补丁方式修复,累计数万台次,无一例失败且无任何副作用;理论上避免了相应次数的宿主机重启及所隐含的云主机业务中断。这项技术在UCloud已经成熟。

UCloud内核热补丁技术揭秘

UCloud的热补丁技术基于多年前的开源ksplice加以定制优化而来,通过加载一个特殊准备的热补丁模块来修复内核。其过程如下图所示:

热补丁模块由ksplice程序编译生成,包含有缺陷的二进制指令和修复后的二进制指令(这些二进制按函数级别组织);模块加载后,自动定位到内核的缺陷处并以修复指令动态替换缺陷指令。

ksplice热补丁模块的创建原理见下图:

首先获取一份运行中内核对应的源码并编译出二进制,称为pre对象;打上源码补丁再次编译,称为post对象。而运行中的内核二进制称为run对象。post和pre逐条指令比较并找出存在差异的函数,之后把这些差异合并为内核模块形式的热补丁。

创建好的热补丁模块在加载到内核时还会做些检验工作:对比pre和run对象。只有通过检验才能成功加载进内核。pre-run比较的目的是为了辨别编译过程差异是否过大以致于不能打入post对象的热补丁;更重要的是,从pre-run差异中提取的关键信息才能把post对象的热补丁顺利打入运行中内核。

热补丁模块加载到内核后,便自动进行内核修复。也就是使用热补丁中的二进制指令替换有缺陷的二进制指令。这里ksplice利用了Linux内核的stop_machine机制:停止所有CPU的执行,只留下主CPU进行二进制指令替换。值得注意的是,stop_machine后如果发现任何一个线程栈的内容与热补丁存在冲突,就需要退出指令替换以避免系统崩溃。所以并非所有热补丁都能打入内核,有些频繁使用的内核函数(如schedule, hrtimer相关)就无法热补丁,重试次数再多也无济于事。

ksplice同时支持对内核和模块进行热补丁,也支持热补丁后叠加热补丁,灵活方便。不过也存在一些缺陷:stop_machine期间整个系统处于中断状态,虽然单次中断小于1ms,但有些时候多次重试的累计中断也不小;另外,有些频繁使用的函数无法打入热补丁。

kpatch和kgraft

kpatch和kgraft均是近期新出现的内核热补丁技术,前者属于Redhat公司,后者SuSE。两者原理和ksplice大致相同,都想合并进Linux内核,内核社区正在争议对比。

kpatch原理和前面讲的ksplice很接近。最大的区别在于二进制指令替换,stop_machine停止所有CPU执行后ksplice直接修改,而kpatch则借助ftrace机制来触发替换。目前的实现上,kpatch有较大局限性,不支持对模块打热补丁,不支持函数静态变量等。

kgraft原理也基本一样。主要的差异有两点:

1)热补丁生成方法不同;

2)热补丁打入内核过程里kgraft用到了RCU渐进方法。得益于RCU方法,kgraft无需像ksplice和kpatch一样调用stop_machine并检查线程栈的冲突。不过它的缺点也缘于RCU,涉及到数据结构改变时,kgraft更难通过编写辅助代码打入热补丁,这限制了kgraft的应用范围。

有关kpatch和kgraft的详细情况请分别参考Redhat和SuSE网站的技术资料。

除了免重启修复,热补丁还用于内核开发过程的性能分析和故障定位。比如,加上性能统计代码生成热补丁,就可以在线分析感兴趣的性能问题;加入额外调试代码捕捉运行中内核的异常。这些非常有用,更是海量服务器里捕捉不可重现内核异常的不二法宝。由于热补丁不需要重启服务器,既可打入也可撤销,所以不会有副作用。

UCloud对开源Ksplice的优化主要在以下三个方面:

支持高版本内核

热补丁技术与内核紧密耦合。不同版本的内核在指令结构体,符合表结构体和一些特性上(比如早期内核没有ftrace)有所不同,直接影响热补丁成败。UCloud研究了各版本内核的区别,使得同一份ksplice支持各个版本的Linux内核。值得一提的是,解决了ftrace与ksplice不兼容的问题。

允许热修复频繁调用的函数

不管什么样的热补丁技术,两种类型的内核函数难以热补丁:频繁使用的内核函数如schedule, hrtimer;经常处于线程栈内核部分顶部的函数,如sys_poll, sys_read。UCloud更改了ksplice相关内核代码和用户态工具,成功解除了这些限制,比如UCloud现网服务器已打入了三个hrtimer热补丁。

减少业务中断时间

ksplice是在stop_machine后替换二进制指令的。虽然单次stop_machine对业务造成的中断在一毫秒左右,但有些频繁使用的内核函数需要大量重试才能碰到合适的热补丁时机,于是会造成最长达上百毫秒的中断。UCloud在此做过一点优化,使得业务中断时间控制在十毫秒级别。

海量服务器环境下热补丁技术可用来零代价且无副作用地修复内核缺陷,而且内核开发也因热补丁能走得更远更好。以前因为缺乏辅助分析手段和惧怕内核BUG,即使适合在内核实现的特性也被告诫移到用户态实现,然而有了热补丁,相关观念也可以适当调整,内核开发也可以更加大胆和跳脱。