返回信息流整理简历和过往工作经历,将曾经写过的一个内核调试报告给发出来吧。
这大概是之前比较开心的时候之一吧,理所当然的加班,加班也开心。
具体问题是这样的:当进程间频繁的互发unix signal,signal会丢。
当这个bug发现时,距离芯片的投片的已经没有多少时间了,如果真的是硬件的bug,那么芯片还能不能投?老大找我开会说,接下来每天都加班,然后debug工作就开始了。
这是在ex公司工作时写的有限的几个报告之一,当初不懂的写报告的重要性,现如今这个报告就是我曾经为中国芯而工作的纪念吧。
报告如下:
r4k_switch.S bug
Bug 代码示例:
arch/mips/kernel/r4k_switch.S
/*
* task_struct *resume(task_struct *prev, task_struct *next,
* struct thread_info *next_ti)
*/
/*
* check if we need to save FPU registers
*/
PTR_L t3, TASK_THREAD_INFO(a0)
LONG_L t0, TI_FLAGS(t3)
li t1, _TIF_USEDFPU
and t2, t0, t1
beqz t2, 1f
nor t1, zero, t1
and t0, t0, t1
LONG_S t0, TI_FLAGS(t3)
触发bug的代码示例:
void signal_wake_up(struct task_struct *t, int resume)
{
unsigned int mask;
set_tsk_thread_flag(t, TIF_SIGPENDING);
触发bug的现场说明:
set_tsk_thread_flag(t, TIF_SIGPENDING);将会对进程的flag置SIGPENDING位,而此函数是对用llsc实现set_bit函数的简单包裹,也就是说当置位结束函数返回时,flag的SIGPENDING位理应被置位。但如果发生下示的情况时,置的位将会丢失。
core 0 core 1
LONG_L t0, TI_FLAGS(t3)
li t1, _TIF_USEDFPU
and t2, t0, t1
beqz t2, 1f
nor t1, zero, t1 set_tsk_thread_flag(t, TIF_SIGPENDING);
and t0, t0, t1
LONG_S t0, TI_FLAGS(t3)
如上所示,当CPU1将该位置位后,紧接着会被CPU0清位。从而造成用户空间的进程丢失一个unix signal。另外,必须要说的是,CPU0并不会在每次执行上下文切换都会存储浮点寄存器,LONG_S t0, TI_FLAGS(t3)指令并不是每次都会执行。只有当beqz t2, 1f不跳转时,才会被执行,而不跳转的先决条件是进程使用了FPU。当有一条浮点指令时,即会触发浮点异常并将flag中的TIF_USEDFPU位置位,于是操作系统不能使用lazyFPU模式,而必须保存浮点寄存器,此时隐藏的那个bug就有可能触发。
此项测试也在模拟器上进行过大量的测试,但因为模拟器的bug,beqz t2, 1f每次均会发生跳转,所以该bug没能在模拟器重现,并因为模拟器测试通过而一度怀疑硬件问题。
追踪bug的过程:
首先简单描述发现bug的测试程序,该测试程序的主要内容为父子进程互相发送信号。父进程kill信号给子进程,从而子进程执行信号处理函数,而信号处理函数给父进程回发信号,然后退出,如此交错发送信号。当触发bug时,该程序的表现现象为丢失信号。代码如下:
#include "unpipc.h"
static int counter, nloop;
static pid_t childpid, parentpid;
void
sig_usr1(int signo)
{
Kill(parentpid, SIGUSR2); /* child receives USR1, sends USR2 */
return;
}
void
sig_usr2(int signo)
{
if (++counter < nloop)
Kill(childpid, SIGUSR1); /* parent receives USR2, sends USR1 */
else
Kill(parentpid, SIGTERM); /* parent terminates below */
return;
}
void
sig_term(int signo)
{
printf("latency: %.3f usec\n", Stop_time() / nloop);
Kill(childpid, SIGTERM);
exit(0);
}
int
main(int argc, char **argv)
{
if (argc != 2)
err_quit("usage: lat_signal <#loops>");
nloop = atoi(argv[1]);
counter = 0;
parentpid = getpid();
Signal(SIGUSR1, sig_usr1); /* for child */
Signal(SIGUSR2, sig_usr2); /* for parent */
if ( (childpid = Fork()) == 0) {
for ( ; ; ) { /* child */
pause();
}
exit(0); /* never reached */
}
/* 4parent */
Signal(SIGTERM, sig_term); /* for parent only */
Start_time();
Kill(childpid, SIGUSR1);
for ( ; ; )
pause();
}
在追踪的过程中,对该测试代码进行了各种改写,并生成了多种变体,例如更换程序中使用的不可靠信号为可靠信号,又例如当信号丢失之后,发送sigint(通过ctrl – c发送给前台会话组进程,已为测试程序增写sigint 信号处理程序,故不会简单的终止进程),发现测试又能继续运行,所以猜测信号可能并没有丢失。
为了更好的追踪错误,追踪出信号究竟是如何在内核中丢失的,只能更改内核代码。更改调试内核代码的过程个人很有学习意义,过程如下:
增添打记的函数:
//for fpga debug xzhao
#define DBG_ARR_SIZE 512
extern unsigned int dbg_arr[DBG_ARR_SIZE];
extern unsigned int dbg_pt;
/* tmp bit 16-20 where point */
#include <asm/atomic.h>
static inline void push_mark(struct task_struct *p, int mark)
{
unsigned int pos,tmp;
if (current->pid < 50)
return;
pos = atomic_add_return(1,(atomic_t *)&dbg_pt);
mark = (current->pid << 8) | mark;
tmp = raw_smp_processor_id() << 8;
tmp |= task_thread_info(p)->flags & 0xff;
dbg_arr[pos%DBG_ARR_SIZE] = (mark << 16) | tmp;
}
static inline void dbg_push(unsigned int tmp)
{
int pos = atomic_add_return(1,(atomic_t *)&dbg_pt);
dbg_arr[pos%DBG_ARR_SIZE] = tmp;
}
//end xzhao
通过push_mark函数,我们可以记录kernel运行过程中一些关键的时间点,某个变量的值。例如我们使用该函数监测了进程的flag的sigpending位。我们的编码规则为:31-24位为进程ID,23-16位为标记位置编码,16-8位为CPU ID,7-0位保存监测的flag。记录的数据存放在dbg_arr中。
通过下面的代码可以将dbg_arr中的数据导出来:
/**
* sys_kill - send a signal to a process
* @pid: the PID of the process
* @sig: signal to be sent
*/
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct siginfo info;
//xzhao
// printk("pid:%d sig:%08x\n",pid ,sig);
if( unlikely( (sig == 2) && (pid < 45 && pid > 40)) ){
int i, pos = dbg_pt;
for(i=0; i<DBG_ARR_SIZE; i++){
printk("\t%08x\n",dbg_arr[(pos--)%DBG_ARR_SIZE]);
}
}
//end
Pid为shell的pid,每次启动约为42或者43,故简单的做个判断。Sigint(ctrl – c)信号等于2。故键盘按ctrl –c 即可打印出我们标记的数据。
打标记也是一个渐进的过程,通过不断打标读出测试数据,并不断的猜测,最终贴进了错误的发生点。贴近的数据记录如下:
3f5b0100
3f310100
3f6b0100
3f2f0100
3e620000
3e610000
3e560000
3e640000
3e630000
3f230100
3e620000
3f210100
3e610000
3f200100
3f450100
3e550000
3e6a0000
3f410100
3f400102
62 21
Switch_to() set_bit()
63 23
标记62是切换之前,标记63是切换之后。标记21是置sigpending位之前,23是置pending位之后。
经过多次测试,发现出现错误的模式均如上表所示。也就是说,当CPU0正在进程切换的时候,CPU1给CPU0发送的信号,会“丢失”。贴近到此处,范围已经较小了,通过逻辑分析仪跟踪PC流,抓出出错时的PC流,并根据kernel dump分析,找当CPU1正在置位时,CPU0在干什么,最终找到错误代码:r4k_switch.S 中的resume函数。
这是一条镜像帖。来源:北邮人论坛 / soft-design / #43967同步于 2013/9/15
该镜像源已超过 30 天没有更新,可能在源站已被删除。
SoftDesign机器人发帖
发一个内核丢失unix signal的debug报告吧
zxsword
2013/9/15镜像同步2 回复
订阅后,新回复会通过你的通知中心匿名送达。
2 条回复