(1)PC端接收不到設備端應用程序採集通過網絡發送的圖像
(2)PC端可以ping通設備端,telnet可以登錄設備,設備ping PC端只能通一個數據包
於是我telnet登錄異常設備,通過tftp http ftp上傳下載文件到PC,發現都正常。
起初懷疑是設備網卡driver出現問題,但是細想網卡driver處於數據鏈路層,
上層(不管應用層是哪種協議)傳下來的數據包對於driver來說是一樣的。tftp ftp http能正常工作,說明網卡driver能正常收發數據, ping應該也能正常工作纔對。
仔細看ping返回的結果,發現有一些不一樣的地方,如下:
# ping 10.0.14.198
PING 10.0.14.198 (10.0.14.198): 56 data bytes
64 bytes from 10.0.14.198: seq=0 ttl=127 time=0.817 ms
^C
--- 10.0.14.198 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.817/0.817/0.817 ms
ping通一個icmp包,之後就沒有反應,ctrl+c程序退出顯示沒有丟包。
從現象上來看,並不是ping出現丟包,而是感覺ping好像在完成一次數據包通訊後阻塞。
想到這裏,就不再懷疑是網卡driver的問題,想再找找看系統其他的異常現象。
ps查看系統進程時發現異常設備pid已經到了2萬多,並且不再增加,而對比正常設備pid在1萬多,pid還在增加。
於是想是不是異常設備的圖像採集程序產生進程過多,系統進程數到達最大值,無法創建進程導致這個bug,
寫一段測試代碼如下:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAXPROCESS 65535
#define SLEEPTIME 1
void main (int argc , char ** argv)
{
pid_t pid;
int count = 0;
int maxprocess = MAXPROCESS;
if (argc == 2) {
maxprocess = atoi(argv[1]);
}
for (count = 0; count < maxprocess; count++)
{
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
printf("child %d start\n", count);
sleep(SLEEPTIME);
printf("child %d end\n", count);
exit(0);
}
printf("parent:create %d child\n", count);
}
for (count = 0; count < MAXPROCESS; count++) {
wait();
}
exit(0);
}
創建指定個數子進程sleep 1s,父進程等待子進程退出後回收資源然後退出。
編譯運行,結果如下:
# ./fork_test 1
parent:create 0 child
child 0 start
^C
#
發現程序不退出,根據打印發現是子進程sleep沒有退出,咦,這是什麼情況。直接在console下執行sleep命令,發現也是阻塞不退出,只能ctrl+c退出。
這是該bug的另一個現象:sleep阻塞。
查看系統的pid限制,如下:
# cat /proc/sys/kernel/pid_max
32768
觀察正常設備pid,發現系統pid在達到32768後會從0-32768中再找已釋放的pid使用。所以也不再懷疑是系統進程數限制,還得再找其他線索。
date查看系統時間與hwclock獲取的RTC時間對比,發現系統時間跟RTC時間差距較大,但是kernel啓動加載RTCdriver後會同步系統時間和RTC時間,系統時間與RTC時間應該一致呀?
觀察找到原因,發現又一個重要bug現象,系統時間走的是準的,晚於RTC時間的原因是,系統時間在某個時間值上往前走180s左右就會回跳回來再往前走,來回循環,導致系統時間晚於RTC時間!
到這裏,關於這個bug已經發現4種現象:
(1)PC端接收不到設備端應用程序採集通過網絡發送的圖像
(2)PC端可以ping通設備端,telnet可以登錄設備,設備ping PC端只能通一個數據包
(3)設備端sleep會阻塞
(4)設備端date系統時間走180s回跳
直覺感覺要從date這個現象入手,首先找date命令實現,嵌入式設備文件系統中使用的是busybox,其中有簡化的date命令,也可以找glibc庫來查看完整版本的date命令。這裏不再詳述date實現,date最終是調用gettimeofday來獲取時間。
void do_gettimeofday(struct timeval *tv)
{
struct timespec now;
getnstimeofday(&now);
tv->tv_sec = now.tv_sec;
tv->tv_usec = now.tv_nsec/1000;
}
void getnstimeofday(struct timespec *ts)
{
unsigned long seq;
s64 nsecs;
WARN_ON(timekeeping_suspended);
do {
seq = read_seqbegin(&timekeeper.lock);
*ts = timekeeper.xtime;
nsecs = timekeeping_get_ns();
/* If arch requires, add in gettimeoffset() */
nsecs += arch_gettimeoffset();
} while (read_seqretry(&timekeeper.lock, seq));
timespec_add_ns(ts, nsecs);
}
static inline s64 timekeeping_get_ns(void)
{
cycle_t cycle_now, cycle_delta;
struct clocksource *clock;
/* read clocksource: */
clock = timekeeper.clock;
cycle_now = clock->read(clock);
/* calculate the delta since the last update_wall_time: */
cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;
/* return delta convert to nanoseconds using ntp adjusted mult. */
return clocksource_cyc2ns(cycle_delta, timekeeper.mult,
timekeeper.shift);
}
do_gettimeofday調用getnstimeofday,最關鍵的是timerkeeper.xtime,這是kernel的牆上時間,xtime的更新是在kernel下clockevent註冊的時鐘中斷,只要kernel時鐘中斷正常,xtime時間就會不斷被更新。但是由於kernel一般是1/100s產生一次時鐘中斷(kernel配置默認爲100HZ),當然對於tickless sysytem,時鐘中斷不固定,但是精度都不夠高。
爲了提高時鐘精度,調用timekeeping_get_ns,使用已註冊clocksource提供的read函數,來獲取距上次update xtime的時間,來作爲xtime的補充時間,提高精度。
kernel下xtime的更新和獲取機制有時間還需要仔細研究下,這裏先說這些。
kernel下xtime的操作流程如下:
gettimeofday <===獲取=== xtime <===更新=== clockevent clocksource
(1)gettimeofday時獲取xtime出錯
(2)xtime存儲出錯
(3)更新xtime出錯
如何排除,想到了二分法,如果能夠直接獲取xtime值,與gettimeofday獲取的值對比,就可以確定到底是哪一步出了問題。
首先要說明下xtime,在kernel源碼的kernel/time/timekeepering.c中定義了struct timekeepering結構體用來表徵kernel下與時間相關內容,其中就有xtime成員,結構體定義如下:
struct timespec {
__kernel_time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
tv_sec和tv_nsec表示了從1970-1-1以來的時間。但是xtime並沒有留接口給系統調用等,無法從用戶空間來直接獲取xtime,並且該bug復現難,需要設備運行很長時間,因此也不能修改kernel後再重新啓動。
那怎麼辦,想到了一個辦法:driver module + application。
在timekeeping.c中也找到了kernel下來獲取xtime的接口,如下:
unsigned long get_seconds(void)
{
return timekeeper.xtime.tv_sec;
}
EXPORT_SYMBOL(get_seconds);
於是編寫如下模塊代碼:
#include <linux/mm.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/mman.h>
#include <linux/random.h>
#include <linux/init.h>
#include <linux/raw.h>
#include <linux/tty.h>
#include <linux/capability.h>
#include <linux/ptrace.h>
#include <linux/device.h>
#include <linux/highmem.h>
#include <linux/crash_dump.h>
#include <linux/backing-dev.h>
#include <linux/bootmem.h>
#include <linux/splice.h>
#include <linux/pfn.h>
#include <linux/export.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define GET_XTIME 0
static int dev_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t dev_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
return 0;
}
static ssize_t dev_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
return 0;
}
static long dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int __user *argp = (int __user *)arg;
unsigned long now = 0;
switch (cmd) {
case GET_XTIME :
now = get_seconds();
if (copy_to_user(argp, &now, 4))
return -EFAULT;
break;
default :
return -EFAULT;
}
return 0;
}
static const struct file_operations dev_fops = {
.read = dev_read,
.write = dev_write,
.open = dev_open,
.unlocked_ioctl = dev_ioctl,
};
static struct cdev char_dev;
static int major;
static int __init char_dev_init(void)
{
int rc;
int err;
dev_t devid;
rc = alloc_chrdev_region(&devid, 0, 1, "char_dev");
if (rc != 0)
{
printk("alloc chardev region failed\n");
return -1;
}
major = MAJOR(devid);
cdev_init(&char_dev, &dev_fops);
cdev_add(&char_dev, devid, 1);
return 0;
}
static void __exit char_dev_exit(void)
{
cdev_del(&char_dev);
unregister_chrdev_region(MKDEV(major,0), 1);
}
module_init(char_dev_init);
module_exit(char_dev_exit);
編譯此模塊,insmod插入kernel中,接着編寫一個應用程序如下:
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#define GET_XTIME 0
void main(void)
{
unsigned long now = 0;
struct timeval tv;
struct timezone tz;
gettimeofday(&tv, &tz);
printf("tv.tv_sec = %d\n", tv.tv_sec);
printf("tv.tv_usec = %d\n", tv.tv_usec);
int fd = open("/dev/char_dev", O_RDWR);
if (fd < 0)
{
printf("open failed\n");
return;
}
ioctl(fd, GET_XTIME, &now);
printf("xtime.tv_sec = %d\n", now);
close(fd);
}
分別將gettimeofday獲取的時間和kernel中xtime的時間打印出來。編譯程序,在kernel下運行3次,如下:
# ./dev_tool
tv.tv_sec = 1427286832
tv.tv_usec = 617831
xtime.tv_sec = 1427286754
#
#
# ./dev_tool
tv.tv_sec = 1427286835
tv.tv_usec = 17649
xtime.tv_sec = 1427286754
#
#
# ./dev_tool
tv.tv_sec = 1427286840
tv.tv_usec = 281584
xtime.tv_sec = 1427286754
很明顯可以看出,xtime的時間是停止的,那爲什麼gettimeofday時間還會走呢?上面分析過gettimeofday實現,爲了提高精度,gettimeofday的時間 = xtime + 根據clocksource->read獲取的cycles換算出來的補充時間
那就來看下我們設備中註冊的clocksource什麼樣,如下:
static cycle_t
timer_get_cycles( struct clocksource *cs )
{
return __raw_readl( IO_ADDRESS( REG_TIMER_TMR2DL ));
}
static struct clocksource timer_clocksource =
{
.name = MTAG_TIMER,
.rating = 300,
.read = timer_get_cycles,
.mask = CLOCKSOURCE_MASK( 32 ),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
static u32 notrace
update_sched_clock( void )
{
return __raw_readl(IO_ADDRESS( REG_TIMER_TMR2DL ));
}
static int __init
timer_clocksource_init( void )
{
u32 val = 0, mode = 0;
timer_stop( 2 );
__raw_writel( 0xffffffff, IO_ADDRESS( REG_TIMER_TMR2TGT )); // free-running timer as clocksource
val = __raw_readl( IO_ADDRESS( REG_TIMER_TMRMODE ));
mode = ( val & ~( 0x0f << TIMER2_MODE_OFFSET )) | TIMER2_CONTINUOUS_MODE;
__raw_writel( mode, IO_ADDRESS( REG_TIMER_TMRMODE ));
timer_start( 2 );
setup_sched_clock( update_sched_clock, 32, 24000000 );
if(clocksource_register_hz( &timer_clocksource, 24000000 ))
{
panic("%s: can't register clocksource\n", timer_clocksource.name);
}
return 0;
}
可以看出根據該clcoksource->read獲取的最大cycles爲0xffffffff,而timer的工作頻率是24MHZ,因此換算成時間就是178.9s。
看到這個數字我一下就興奮了,因爲前面說過的一個bug現象,date時間走3分鐘就跳轉回來。這裏就可以解釋這個現象了:
kernel下的xtime時間停止了,但是由於clocksource的補充精度時間最大可以補充178.9s,
所以gettimeofday獲取時間就在xtime基礎上最多走178.9s,溢出後重新從0開始計數,時間又回到xtime重新開始!
那麼問題來了,爲什麼xtime停止更新了呢?
xtime的更新是基於kernel時鐘中斷,具體函數還是在timekeepering.c中的update_wall_time,產生一次時鐘中斷就會將新增的時間加在xtime上。
難道是沒有時鐘中斷了?
多次查看/proc/interrupts(涉及公司設備內容,這裏就不貼了),發現timer的中斷果然沒有變化啊!
利用kernel下預留的操作寄存器命令查看timer模塊寄存器,發現產生時鐘中斷的timer的狀態寄存器是stop。
爲了驗證是timer中斷沒有了導致該bug,首先在出現該bug的設備直接再次start timer,發現設備恢復正常。而在正常設備上stop timer,就會復現該bug。這就說明無timer intr就是該bug的本質!
從最開始懷疑網卡driver有問題,經過一連串的推測驗證後,終於確定了引起該bug的原因:timer interrupt沒有了!
算是有一個階段性小勝利,哈哈。
但是問題還沒有最終解決,kernel代碼中哪裏導致了timer stop呢?
首先想到要讓timer stop,軟件只可能去置位stop寄存器。那隻需要找出kernel中stop timer的接口,確定哪裏會調用它,就可以縮小問題範圍了。kernel中timer intr的代碼如下:
static void
timer_set_mode( enum clock_event_mode mode, struct clock_event_device *evt )
{
u32 val = 0, timermode = 0;
val = __raw_readl( IO_ADDRESS( REG_TIMER_TMRMODE ));
switch( mode )
{
case CLOCK_EVT_MODE_PERIODIC:
timer_stop( 1 );
timermode = ( val & ~( 0x0f << TIMER1_MODE_OFFSET )) | TIMER1_PERIODICAL_MODE;
__raw_writel( TIMER1_TARGET, IO_ADDRESS( REG_TIMER_TMR1TGT ));
__raw_writel( timermode, IO_ADDRESS( REG_TIMER_TMRMODE ));
timer_start( 1 );
break;
case CLOCK_EVT_MODE_ONESHOT:
timer_stop( 1 );
timermode = ( val & ~( 0x0f << TIMER1_MODE_OFFSET )) | TIMER1_ONE_SHOT_MODE;
__raw_writel( TIMER1_TARGET, IO_ADDRESS( REG_TIMER_TMR1TGT ));
__raw_writel( timermode, IO_ADDRESS( REG_TIMER_TMRMODE ));
timer_start( 1 );
break;
case CLOCK_EVT_MODE_UNUSED:
case CLOCK_EVT_MODE_SHUTDOWN:
default:
VLOGD( MTAG_TIMER, "time stop and clr src pnd. mode = %d", mode );
timer_stop(1);
timer_clr_pnd(1);
VLOGD( MTAG_TIMER, "REG_TIMER_TMREN is %u; REG_TIMER_TMRPND is %u", \
readl(IO_ADDRESS( REG_TIMER_TMREN )), readl(IO_ADDRESS( REG_TIMER_TMRPND )));
break;
}
}
static int
timer_set_next_event( unsigned long cycles, struct clock_event_device *evt )
{
timer_stop( 1 );
__raw_writel( cycles, IO_ADDRESS( REG_TIMER_TMR1TGT ));
timer_start( 1 );
return 0;
}
static struct clock_event_device timer_clockevent =
{
.name = MTAG_TIMER,
.features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
.rating = 200,
.set_mode = timer_set_mode,
.set_next_event = timer_set_next_event,
};
static void __init
timer_clockevent_init( void )
{
clockevents_calc_mult_shift( &timer_clockevent, CLOCK_TICK_RATE, 4 );
timer_clockevent.max_delta_ns = clockevent_delta2ns( 0xffffffff, &timer_clockevent );
timer_clockevent.min_delta_ns = clockevent_delta2ns( CLOCKEVENT_MIN_DELTA, &timer_clockevent );
timer_clockevent.cpumask = cpumask_of( 0 );
clockevents_register_device( &timer_clockevent );
}
....
static void
timer_clock_event_interrupt( void )
{
struct clock_event_device *evt = &timer_clockevent;
timer_clr_pnd( 1 );
evt->event_handler( evt );
}
static irqreturn_t
timer_interrupt( int irq, void *dev_id )
{
u32 srcpnd = 0;
struct clock_event_device *evt = &timer_clockevent;
srcpnd = __raw_readl(IO_ADDRESS( REG_TIMER_TMRPND ));
if( srcpnd & TIMER1_EVENT )
{
timer_clock_event_interrupt();
}
__raw_writel( srcpnd, IO_ADDRESS( REG_TIMER_TMRPND ));
return IRQ_HANDLED;
}
static struct irqaction timer_irq =
{
.name = "timer",
.flags = IRQF_DISABLED | IRQF_TIMER | IRQF_IRQPOLL,
.handler = timer_interrupt,
.dev_id = NULL,
};
這裏只貼出clockevent和timer irq處理相關的部分代碼,可以看出涉及到stop timer只有set_next_event和set_mode中,set_next_event會在timer_interrupt中的evt->event_handler中調用,來設置下次觸發intr的時間點,set_mode來設置timer的工作模式。
直覺感覺,set_mode應該只在timer初始化時使用,而set_next_event會在每次timer intr中使用。因此想在set_mode中加打印來看下哪裏會調用set_mode(猜測set_mode調用少,set_next_event中不能加打印,因爲timer intr太多)。
set_mode加打印後重新編譯kernel,在一臺設備上啓動發現set_mode只會在kernel啓動中調用,進入console後就不會調用了。
這樣其實就排除了set_mode函數的可能性,因爲根據觀察timer intr停止的時間,都是在用戶空間,並且kernel啓動中printk打印的時間戳是正常的。
在kernel啓動後用戶空間發生timer intr停止,軟件上來看,只可能是timer中斷部分出現問題了。
但是看代碼,timer_interrupt中也沒有stop timer的操作啊。
不過還是想到了一種場景會導致stop timer現象:
timer_interrupt中調用timer_clock_event_interrupt,其中又調用clockevent->event_handler,該函數會調用clockevent->set_next_event,
在set_next_event中設置完下一次觸發時間點後就start timer了,回到timer_interrupt中clear下timer intr就會從中斷處理中退出。
如果設置的下次觸發時間點足夠短(kernel爲tickless,每次設置的觸發時間都不一樣),在clear timer intr之前該次intr就產生了,但是接下來就clear掉了。
這樣中斷處理函數退出後就不會再次產生timer intr了。
但是有2個地方我覺得需要驗證下:
(1)timer計數達到目標後,狀態寄存器是否是stop狀態(2)如果是上述場景導致這次bug,那麼延長start timer和clear intr之間時間,應該會讓該bug更快復現
timer經過測試發現計數達到目標後,狀態寄存器就會顯示爲stop狀態。
在time_interrupt中的start timer 和clear intr中間加入一些沒用代碼做延時(不能用delay,因爲現在timer有問題呢),進入console後很快就復現了bug。
所以這個bug的原因就是不應該在set_next_event後再次clear timer intr。於是將上面time_interrupt修改爲:
static irqreturn_t
timer_interrupt( int irq, void *dev_id )
{
u32 srcpnd = 0;
struct clock_event_device *evt = &timer_clockevent;
srcpnd = __raw_readl(IO_ADDRESS( REG_TIMER_TMRPND ));
if( srcpnd & TIMER1_EVENT )
{
timer_clr_pnd( 1 );
evt->event_handler( evt );
}
return IRQ_HANDLED;
}
終於解決了這個bug,對於sleep ping的阻塞問題也就可以理解了:
sleep ping實現中都使用了定時器,定時器是由kernel的時鐘中斷和軟中斷結合實現的,由時鐘中斷來觸發定時器軟中斷,在軟中斷中檢查定時是否到了,所以沒有了時鐘中斷,kernel的定時器機制不能工作。
記錄這次bug調試,並沒有詳細來說明一些知識的細節,而是重在說明思路,如何從最初懷疑網卡driver問題,一步步分析排查,直到最後徹底找到代碼原因。
我想如果要成爲一個系統級程序員,解決類似的bug,開闊的思路比知識更重要!