背景:我们的系统做了虚拟化处理,系统存在多个init,每个init负责一个业务分支
问题的产生:在某次操作的时候,上层应用中的一个进程会调用KillAllProcesses()接口,向节点/proc/sysrq-trigger写i,目的是kill掉当前系统中除了init进程外的其他子进程。
static void KillAllProcesses() { android::base::WriteStringToFile("i", "/proc/sysrq-trigger"); }
本来这是一次正常的操作,但是发现每次操作之后,子init负责的整个用户态就挂了,这和我的预期很不一致,我的目标是kill掉除了init进程外的其他子进程,当时百思不得其解,因为就是一个向/proc/sysrq-trigger 写i的动作,为什么会杀掉我的"init"呢,我翻了好几遍有关/proc/sysrq-trigger的用法,用法没有问题,这个问题当时僵持了有一段时间,最后和组内一个同事交流(多交流,一个人看问题有时候很容易陷入死结,别人可以看到你看不到的地方)发现这样的现象是对的,只是我忽略了一点,我将该进程的创造始祖"init"当做了一个真正的init
因为虚拟化的原因,其实这个"init"在内核视角并不是一个真正的init,所以向/proc/sysrq-trigger写i后,/proc/sysrq-trigger做了它该做的,杀死了除了init之外的其他进程,而我的"init"就属于这个其他进程。
问题找到了了,那么思路就很清晰了,我需要让系统认为我的"init"也是一个init,当识别到我的init后,不要去kill掉,将他当做一个正常的init,只去kill掉它的所有子孙进程即可,所以我们需要在内核态对sysrq-trigger做虚拟化操作,让他认识我的"init".
/proc/sysrq-trigger节点在内核drivers/tty/sysrq.c文件中生成,顺藤摸瓜,在sysrq_key_table数组中找到了关于写"i"操作的结构体:
static struct sysrq_key_op *sysrq_key_table[36] = {
&sysrq_loglevel_op, /* 0 */
&sysrq_loglevel_op, /* 1 */
&sysrq_loglevel_op, /* 2 */
&sysrq_loglevel_op, /* 3 */
&sysrq_loglevel_op, /* 4 */
&sysrq_loglevel_op, /* 5 */
&sysrq_loglevel_op, /* 6 */
&sysrq_loglevel_op, /* 7 */
&sysrq_loglevel_op, /* 8 */
&sysrq_loglevel_op, /* 9 */
/*
* a: Don't use for system provided sysrqs, it is handled specially on
* sparc and will never arrive.
*/
NULL, /* a */
&sysrq_reboot_op, /* b */
&sysrq_crash_op, /* c & ibm_emac driver debug */
&sysrq_showlocks_op, /* d */
&sysrq_term_op, /* e */
&sysrq_moom_op, /* f */
/* g: May be registered for the kernel debugger */
NULL, /* g */
NULL, /* h - reserved for help */
&sysrq_kill_op, /* i */
进而找到sysrq_kill_op
static struct sysrq_key_op sysrq_kill_op = {
.handler = sysrq_handle_kill,
.help_msg = "kill-all-tasks(i)",
.action_msg = "Kill All Tasks",
.enable_mask = SYSRQ_ENABLE_SIGNAL,
};
看到hander这个名字就八九不离十了,看看,一直往下跟:
static void sysrq_handle_kill(int key)
{
send_sig_all(SIGKILL);
console_loglevel = CONSOLE_LOGLEVEL_DEBUG;
}
/*
* Signal sysrq helper function. Sends a signal to all user processes.
*/
static void send_sig_all(int sig)
{
struct task_struct *p;
read_lock(&tasklist_lock);
for_each_process(p) {
if (p->flags & PF_KTHREAD)
continue;
if (is_global_init(p))
continue;
do_send_sig_info(sig, SEND_SIG_FORCED, p, true);
}
read_unlock(&tasklist_lock);
}
上述代码可以看到,for_each_process会遍历所有的用户态进程,但是他有一个动作:
if (is_global_init(p))这里就是判断该进程是否为init进程的地方,写i操作为什么不会杀死init的原因就在这里。
所以我只需要在这里做处理就可以了
下面是改进后的代码:
static void send_sig_all(int sig)
{
struct task_struct *p;
struct task_struct *child_reaper;
struct pid_namespace *ns;
struct nsproxy *nsproxy;
struct list_head *pos;
read_lock(&tasklist_lock);
nsproxy = task_nsproxy(current);
if(nsproxy) {
ns = nsproxy->pid_ns_for_children;
if(!ns) {
pr_err("get the ns fail in sysrq.c\n");
return;
}
} else {
pr_err("get the nsproxy fail in sysrq.c\n");
return;
}
if(ns == &init_pid_ns) { //这里是真正的init
for_each_process(p) {
if (p->flags & PF_KTHREAD)
continue;
if (is_global_init(p))
continue;
do_send_sig_info(sig, SEND_SIG_FORCED, p, true);
}
} else { //child:All processes belonging to namespce except init in the
namespace should be killed
child_reaper = ns->child_reaper;
if(!child_reaper) {
pr_err("get the child_reaper fail in sysrq.c\n");
return;
}
list_for_each(pos,&child_reaper->children) {
p = list_entry(pos,struct task_struct,sibling);
if (p->flags & PF_KTHREAD)
continue;
if (child_reaper->tgid == p->tgid)
continue;
do_send_sig_info(sig, SEND_SIG_FORCED, p, true);
}
}
read_unlock(&tasklist_lock);
}
上述代码基本的思路和关键是要获得当前进程所属的pid namespace,还有当前命名空间的init进程
nsproxy = task_nsproxy(current);
ns = nsproxy->pid_ns_for_children;
上面两句用于获取当前进程的命名空间
child_reaper = ns->child_reaper;
child_reaper 的定义为:
struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
struct rcu_head rcu;
int last_pid;
unsigned int nr_hashed;
struct task_struct *child_reaper;
child_reaper指向的是当前命名空间的init进程,每个命名空间都有一个作用相当于全局init进程的进程。
能够找到当前进程的命名空间和当前命名空间的init进程,这个事就好办了,然后遍历该进程,杀掉除了该进程外的其他子孙进程
ok,问题解决。
题外话:
该问题解决后,因为兄弟组有部署docker,docker也使用了namespace,所有我们想docker会不会也有这样的问题,如果有的话,岂不是很危险,我们做了实验,在docker上向sysrq-trigger写i,发现这个节点我们写不了,?这么神奇,最后发现docker做了规避,docker将这个sysrq-trigger节点做了处理,将 该节点重新挂载成为一个只读的节点,所以你是无法对其写的,只能读读读,果然优秀的软件考虑的真是到位啊。。。。。