记一次bug排除心得
问题背景
要做一个需求,大概是检测到某输入重启,于是写一个demo调试一下
c语言程序,交叉编译后在adb shell下运行
思路
用 am 命令直接重启
我们先手动验证一下,发现这个设备不支持am命令吗,遂排除

用 kill 命令杀掉进程,然后重新运行
- 写个demo,这个程序会循环等待输入,按q退出
void test_option()
{
}
int main(int argc, char **argv)
{
int ret;
char *sptr = NULL;
while(1)
{
sptr = input_fgets("Please input option: \n");
if(sptr[0] == 'q')
return 0;
else if (atoi(sptr) == 1)
test_option();
}
return 0;
}
- 另开一个cmd窗口,打开adb shell,查询进程号并杀掉重开

可以看到,通过另一个adb窗口可以很轻易地kill掉进程并重启
那么 直接用system函数写入命令行执行不就完事了?? ,我也是这么认为的。。。但是出问题了!!!
- 问题1: 我手动执行ps看进程号,但是写程序获得进程号呢?
ps | grep nwy_data_test | awk '{print $1}' | head -n 1
这句命令表示取出ps结果的第一列的第一行并打印!也就是上图中的22122
- 问题2:取到22122了,怎么传给命令行呢?

一种更简单的方法是判断输入为1 直接system(killall -9 rst_test && /data/rst_test)

发现会无限循环执行!!!

3. 经过我的调试!!!发现问题出在input_fgets函数!!!
现在已知:手动kill并重启 无论怎样都没问题;写在system里,重启的进程里带标准读取输入的都会出错!!
kill当前进程 重启一个不带input_fgets函数的情况下,会kill、会正常运行新程序
kill当前进程 重启一个带input_fgets函数的情况下,会kill、会忽略fgets的阻塞!!
也就是说 只要kill当前进程后,重启的进程带fgets这种获取命令行输入的都有错!!!
static inline char *input_fgets(char *msg, ...)
{
static char ptr[130] = { 0 };
va_list ap;
va_start(ap, msg);
vprintf(msg, ap);
va_end(ap);
memset(ptr, 0, sizeof(ptr));
fgets(ptr, sizeof(ptr), stdin);
if(strlen(ptr) > 0) {
if('\n' == ptr[strlen(ptr) - 1]) {
ptr[strlen(ptr) - 1] = '\0';
}
}
return ptr;
}
4. 于是,把上面函数的vprintf注释掉,发现:可以正常kill,正常启动,但是fgets直接跳过了,并没有读取到我输入的1!!!!


5. 到这里其实基本已经真相大白了,也就是手动输入命令和system输入命令是有差异的!!!
-
通过查询资料,发现错误大概率是由于system的底层是通过fork创建一个子进程的,而子进程会关联到父进程的输入输出流,通过kill杀掉了父进程,就影响了正常的输入输出流!!!
-
为了验证我的猜想
7.1 先去掉了system里的kill命令,发现一切正常!

7.2 kill后启动另一个不带stdin的进程,发现一切正常!

7.3 kill后启动另一个带输入的进程,读取标准输入流失败!!

我并没有输入任何内容!!也就是说 标准输入流有无数个空 自动读取了!!!
7.4 几个细节
scanf之前不会发生这个错误,scanf之后才发生这个错误!!
if (ferror(stdin)) {
printf("IO错误");
}
这就可以定位到,是scanf/fgets导致的错误,一般是因为stdin标准输入流中有问题!
用 exit 命令退出adb shell,然后重新开启
system是子进程,他执行exit只会退出他的shell 不会影响父进程!!!

- 一个有效的办法是用kill -9 $$ 结束adb shell , 但是经过测试,无论是用sh脚本配置还是system("kill -9 $$"),都不会正确执行,原因在于$$是获取当前进程id,直接手动输入表示结束adb shell,如果在sh脚本执行表示结束当前脚本,在system执行则表示结束当前子进程!!!!!
根因分析
- system命令类似于sh脚本,实际上也是在/bin/sh中写一个脚本并执行,往往是非交互式的脚本,这个过程是在调用处新开一个子进程完成的,恰好我执行的内容是杀掉父进程并启动一个新进程,父进程有用到标准io(获取输入1并执行) 直接把他干掉就导致stdin乱了!!!!!
直接kill/killall 会提示Terminated,加上-9 会提示Killed,这是因为kill指令是通过向目标进程发送相应信号 默认是15(会等待处理完在终止),9是强制杀死!!

最终方法
- 用killall -2 xxx ,相当于在前台程序时执行一个ctrl+c 但他实际上并不会杀掉父进程,因为kill -2十分温和,他会等到子进程结束在杀掉父进程,而子进程又想杀死父进程,父进程要等待子进程杀死自己再去死,就导致死不掉了。。。(有局限性,比如进程中有其他线程,ctrl+c不会退出线程!!)

- 用exec函数族实现,无论重启几次都是原来的进程号,但是会复位到初始状态!!!


kill -2也是一种可行的办法但实际上是覆盖,没关闭原来的进程,会导致资源浪费,exec函数是最根本的解决方法会取代!!!
总结
-
system命令几乎等于写一个sh脚本并执行,区别是system命令是新开子进程,手动执行sh脚本可以新开一个shell执行!!
-
子进程中杀掉父进程是非常危险的行为,会导致很多未知错误,一般不要这样做!!!!
-
一些优雅的发送信号方式,ctrl+z暂停,fg继续,ctrl+S挂到后台 ctrl+Q恢复到前台!

-
进程间通讯的方式之一 管道通信 实现方式是popen pclose
-
fork函数 exec函数族 还可以自定义io区等...很强
-
排查这个bug用了两天半,居然真的用上了操作系统的知识,爽歪歪!!!


浙公网安备 33010602011771号