内核线程中获取接收到的信号

 在测试开发的内核模块时,发现了一个BUG:在模块没有卸载时使用reboot命令重启系统的话,系统重启不了,查看日志发现在创建的内核线程中陷入了死循环,导致系统无法重启。检查了代码,发现产生问题的原因是当系统调用返回-EINTR(也就是被信号中断),内核线程中的循环没有退出,而是继续循环操作,这个逻辑跟业务是相符合的并没有错误。问题就在于没有检查接收到的是什么信号,如果是在系统重启时发送的信号或者执行关机时发送的信号,应该退出循环。剩下的就是找到在内核线程中获取接收的信号的方法。

   在用户态获取阻塞的信号,调用的就是sigpending(),因此首先尝试调用sys_sigpending()来获取。sys_sigpending()作为系统调用是没有导出的,因此不能直接调用,但是可以通过/proc/kallsyms文件来获取sys_sigpending()的地址来调用这个函数。在我的测试机上,sys_sigpending()的地址为0xffffffff810802e0。测试代码如下所示:

/*
 * fcluster.c
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/signal.h>
#include <linux/spinlock.h>
#include <linux/sched.h>
#include <linux/uaccess.h>

static int remove_mod = 0;

static int my_sigpending(sigset_t *set)
{
    int (*sigpending)(sigset_t *set);
    int ret;
    mm_segment_t old_fs;
    
    sigpending = (typeof(sigpending))0xffffffff810802e0;
    
    old_fs = get_fs();
    set_fs(get_ds());
    ret = sigpending(set);
    set_fs(old_fs);
    
    return ret;
}

static int thread_process(void *arg)
{
    sigset_t *sigset, __sigset;
    
    sigset = &__sigset;
    
    allow_signal(SIGURG);
    allow_signal(SIGTERM);
    allow_signal(SIGKILL);
    allow_signal(SIGSTOP);
    allow_signal(SIGCONT);
    
    printk(KERN_ALERT "the pid of thread_process is %d.\n", current->pid);
    
    my_sigpending(sigset);
    printk(KERN_ALERT "Before receive signal, signal map: 0x%lX.\n", sigset->sig[0]);
    
    for ( ; !remove_mod; ) {
        /* Avoid infinite loop */
        msleep(1000);
        if (signal_pending(current)) {
            my_sigpending(sigset);
            printk(KERN_ALERT "Received signal, signal map: 0x%lX.\n", sigset->sig[0]);
            printk(KERN_ALERT "Receive SIGURG signal ? %s.\n", 
                sigismember(sigset, SIGURG) ? "true" : "false");
            printk(KERN_ALERT "Receive SIGTERM signal ? %s.\n", 
                sigismember(sigset, SIGTERM) ? "true" : "false");
            printk(KERN_ALERT "Receive SIGKILL signal ? %s.\n", 
                sigismember(sigset, SIGKILL) ? "true" : "false");
            printk(KERN_ALERT "Receive SIGSTOP signal ? %s.\n", 
                sigismember(sigset, SIGSTOP) ? "true" : "false");
            /* Use halt to stop the system */
            printk(KERN_ALERT "Receive SIGCONT signal ? %s.\n", 
                sigismember(sigset, SIGCONT) ? "true" : "false");
            break;
        }
    }
    return 0;
}

static int __init fcluster_init(void)
{
    kernel_thread(thread_process, NULL, CLONE_FILES);
    return 0;
}

static void __exit fcluster_exit(void)
{
    remove_mod = 1;
    msleep(2000);
}

MODULE_LICENSE("GPL");
module_init(fcluster_init);
module_exit(fcluster_exit);
  内核线程如果想接收用户终端发送的信号,必须在处理函数中调用allow_signal()来指定允许接收哪些信号。my_sigpending()是对sys_sigpending()的简单封装,用来获取当前内核线程阻塞的信号。
将上面的代码编译成内核模块,插入到系统中,打开系统日志,查看创建的内核线程的ID(我的是3278,如下图所示),然后在另一个终端中使用kill命令给创建的内核线程发送SIGTERM命令。测试结果要通过系统日志文件(/var/log/messages)来查看,如下图所示:

内核线程中获取接收到的信号_第1张图片

查看系统日志发现获取到的信号位图竟然是0!不可能啊,因为从上面的代码中可以看出只有在signal_pending()函数返回true的情况下(也就是接收到信号时),才能输出上图中的日志信息。代码很简单,关键的函数就是my_sigpending(),该函数只是对sys_sigpending()进行了简单的封装,

因此还是要从sys_sigpending()的实现中查找原因。

  查看sys_sigpending()的源码,只是对do_sigpending()函数的简单封装,继续从do_sigpending()中找原因。do_sigpending()源码如下:

long do_sigpending(void __user *set, unsigned long sigsetsize)
{
	long error = -EINVAL;
	sigset_t pending;

	if (sigsetsize > sizeof(sigset_t))
		goto out;

	spin_lock_irq(&current->sighand->siglock);
	sigorsets(&pending, &current->pending.signal,
		  &current->signal->shared_pending.signal);
	spin_unlock_irq(&current->sighand->siglock);

	/* Outside the lock because only this thread touches it.  */
	sigandsets(&pending, &current->blocked, &pending);

	error = -EFAULT;
	if (!copy_to_user(set, &pending, sigsetsize))
		error = 0;

out:
	return error;
}	
  do_sigpending()首先调用sigorsets()将当前进程的信号信息(current->pending.signal和current->signal->shared_pending.signal)进行或操作(也就是将两个地方的信号掩码合并起来),存储在临时变量pending中。获取当前进程的信号掩码后,在传递到上层时,还要调用sigandsets()将pending和当前进程的信号掩码进行与操作后的结果就再传递到上层。

  现在的问题就是要确定在调用sigandsets()之前pending的值和current->blocked的值。根据do_sigpending()来修改thread_process()函数,打印输出当前进程的信号位图和信号掩码,修改后的thread_process()函数如下所示:

static int thread_process(void *arg)
{
    sigset_t *sigset, __sigset;
    
    sigset = &__sigset;
    
    allow_signal(SIGURG);
    allow_signal(SIGTERM);
    allow_signal(SIGKILL);
    allow_signal(SIGSTOP);
    allow_signal(SIGCONT);
    
    printk(KERN_ALERT "the pid of thread_process is %d.\n", current->pid);
    
    spin_lock_irq(&current->sighand->siglock);
    sigorsets(sigset, &current->pending.signal,
        &current->signal->shared_pending.signal);
    spin_unlock_irq(&current->sighand->siglock);
	
    printk(KERN_ALERT "Before receive signal, signal map: 0x%lX.\n", sigset->sig[0]);
    printk(KERN_ALERT "Beofore receive signal, blocked map: 0x%lX.\n", current->blocked.sig[0]);
    
    for ( ; !remove_mod; ) {
        /* Avoid infinite loop */
        msleep(1000);
        if (signal_pending(current)) {
            spin_lock_irq(&current->sighand->siglock);
	        sigorsets(sigset, &current->pending.signal,
		        &current->signal->shared_pending.signal);
	        spin_unlock_irq(&current->sighand->siglock);
            printk(KERN_ALERT "Received signal, signal map: 0x%lX.\n", sigset->sig[0]);
            
            printk(KERN_ALERT "Receive SIGURG signal ? %s.\n", 
                sigismember(sigset, SIGURG) ? "true" : "false");
            printk(KERN_ALERT "Receive SIGTERM signal ? %s.\n", 
                sigismember(sigset, SIGTERM) ? "true" : "false");
            printk(KERN_ALERT "Receive SIGKILL signal ? %s.\n", 
                sigismember(sigset, SIGKILL) ? "true" : "false");
            printk(KERN_ALERT "Receive SIGSTOP signal ? %s.\n", 
                sigismember(sigset, SIGSTOP) ? "true" : "false");
            /* Use halt to stop the system */
            printk(KERN_ALERT "Receive SIGCONT signal ? %s.\n", 
                sigismember(sigset, SIGCONT) ? "true" : "false");
            break;
        }
    }
    return 0;
}
测试结果如下所示:

内核线程中获取接收到的信号_第2张图片
从蓝色的部分可以看出,current->blocked为0,也就是说当前内核线程的信号掩码为0,所以在do_sigpending()中调用sigandsets()来将当前进程的的信号位图和掩码执行与操作的结果总是0。

 从测试结果来看调用sys_sigpending()来获取内核线程的方法不行,获取内核线程接收的信号可以通过将current->pending.signal和current->signal->shared_pending.signal中的信号合并得到。至此,我们解决了第一个问题,如何获取内核线程接收到的信号。

  接下来解决另一个问题,就是要获取系统重启时内核线程接收到的信号。reboot之后系统会重启,重启之后模块中输出的信息没有保存在系统日志/var/log/messages中,需要想别的办法来拿到重启时模块输出的日志信息。google了一番没有什么收获,最后想到使用kdump+crash来拿到重启时系统日志信息。kdump可以在内核崩溃时转储生成core文件,crash用来分析生成的core文件,在crash中使用dmesg命令可以看到系统崩溃时的日志信息。所以如果在我们的内核线程接收到信号,打印完日志信息后让内核崩溃就可以看到我们输出的日志信息了。让内核崩溃很简单,使用BUG()宏或直接调用panic()函数。修改测试thread_process()函数,将if分支中的”break;“语句替换为BUG()或panic(),代码就没必要贴了,直接上测试结果:

内核线程中获取接收到的信号_第3张图片

 上图中蓝色圈住的部分,可以可以看到同时接收到了SIGTERM和SIGCONT信号,但是这个测试结果是在家里用虚拟机测试的,在公司中拿真实的服务器(刀片机)测试时只检测到SIGTERM信号。本来想做一个处理,让测试结果看起来和真实的服务器一致,但是想通过这个细节给没有被虚拟机坑过的人一个提醒,真正测试开发的程序时一定要使用和生产相同的环境,尽量不用使用虚拟机来测试。曾经测试一个网络相关的模块时,和同事花了整整一上午,最后确认是虚拟机提供的虚拟网卡驱动的问题,真心坑啊!

  除了测试系统重启时内核线程接收的信号,还测试了关机时内核线程接收的信号,发现关机时内核线程接收到的信号是竟然是SIGCONT,而不是认为的SIGKILL或SIGSTOP!以后遇到不确定的问题,一定要自己动手测试一番,以免出错。

  可能有人会问,为什么不直接看sys_reboot()系统调用来看会给内核线程发送什么信号?而要这么麻烦来测试?在测试之前,看了sys_reboot()的源码,但是找了半天也没找到在什么地方发送的信号,最后放弃了,因为现在感觉没有必要花太多的的时间来研究系统重启或关机时内核的处理。内核现在太庞大了,如果各个方面都研究的话不太现实,也没有什么意义,暂时以现在用到的部分为主,等有时间再细细研究其他感兴趣的地方。


你可能感兴趣的:(内核线程中获取接收到的信号)