在測試開發的內核模塊時,發現了一個BUG:在模塊沒有卸載時使用reboot命令重啟系統的話,系統重啟不了,查看日志發現在創建的內核線程中陷入了死循環,導致系統無法重啟。檢查了代碼,發現產生問題的原因是當系統調用返回-EINTR(也就是被信號中斷),內核線程中的循環沒有退出,而是繼續循環操作,這個邏輯跟業務是相符合的並沒有錯誤。問題就在於沒有檢查接收到的是什麼信號,如果是在系統重啟時發送的信號或者執行關機時發送的信號,應該退出循環。剩下的就是找到在內核線程中獲取接收的信號的方法。
在用戶態獲取阻塞的信號,調用的就是sigpending(),因此首先嘗試調用sys_sigpending()來獲取。sys_sigpending()作為系統調用是沒有導出的,因此不能直接調用,但是可以通過/proc/kallsyms文件來獲取sys_sigpending()的地址來調用這個函數。在我的測試機上,sys_sigpending()的地址為0xffffffff810802e0。測試代碼如下所示:
[cpp] view plaincopy
/*
* 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)來查看,如下圖所示:
查看系統日志發現獲取到的信號位圖竟然是0!不可能啊,因為從上面的代碼中可以看出只有在signal_pending()函數返回true的情況下(也就是接收到信號時),才能輸出上圖中的日志信息。代碼很簡單,關鍵的函數就是my_sigpending(),該函數只是對sys_sigpending()進行了簡單的封裝,
因此還是要從sys_sigpending()的實現中查找原因。
查看sys_sigpending()的源碼,只是對do_sigpending()函數的簡單封裝,繼續從do_sigpending()中找原因。do_sigpending()源碼如下:
[cpp] view plaincopy
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(¤t->sighand->siglock);
sigorsets(&pending, ¤t->pending.signal,
¤t->signal->shared_pending.signal);
spin_unlock_irq(¤t->sighand->siglock);
/* Outside the lock because only this thread touches it. */
sigandsets(&pending, ¤t->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()函數如下所示:
[cpp] view plaincopy
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(¤t->sighand->siglock);
sigorsets(sigset, ¤t->pending.signal,
¤t->signal->shared_pending.signal);
spin_unlock_irq(¤t->sighand->siglock<span style="font-family: Arial, Helvetica, sans-serif;">);</span>
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(¤t->sighand->siglock);
sigorsets(sigset, ¤t->pending.signal,
¤t->signal->shared_pending.signal);
spin_unlock_irq(¤t->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;
}
測試結果如下所示:
從藍色的部分可以看出,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(),代碼就沒必要貼了,直接上測試結果:
上圖中藍色圈住的部分,可以可以看到同時接收到了SIGTERM和SIGCONT信號,但是這個測試結果是在家裡用虛擬機測試的,在公司中拿真實的服務器(刀片機)測試時只檢測到SIGTERM信號。本來想做一個處理,讓測試結果看起來和真實的服務器一致,但是想通過這個細節給沒有被虛擬機坑過的人一個提醒,真正測試開發的程序時一定要使用和生產相同的環境,盡量不用使用虛擬機來測試。曾經測試一個網絡相關的模塊時,和同事花了整整一上午,最後確認是虛擬機提供的虛擬網卡驅動的問題,真心坑啊!
除了測試系統重啟時內核線程接收的信號,還測試了關機時內核線程接收的信號,發現關機時內核線程接收到的信號是竟然是SIGCONT,而不是認為的SIGKILL或SIGSTOP!以後遇到不確定的問題,一定要自己動手測試一番,以免出錯。
可能有人會問,為什麼不直接看sys_reboot()系統調用來看會給內核線程發送什麼信號?而要這麼麻煩來測試?在測試之前,看了sys_reboot()的源碼,但是找了半天也沒找到在什麼地方發送的信號,最後放棄了,因為現在感覺沒有必要花太多的的時間來研究系統重啟或關機時內核的處理。內核現在太龐大了,如果各個方面都研究的話不太現實,也沒有什麼意義,暫時以現在用到的部分為主,等有時間再細細研究其他感興趣的地方。