程序被调试器断住以后的故事

摘要

在Linux和Windows系统上开发应用程序的过程中,经常会使用到调试器来进行软件的调试。大部分时候我们打开调试器就是“打断点”和“单步跟”。其实有很多细节是容易被忽略的,本文关注其中下面几个问题:

1 大家都知道线程是操作系统的调度执行单位,进程是线程的载体。那么当程序运行到调试断点处停下来的时候,是整个进程都停止运行,还是只有当前执行的线程停止,其它线程还在照常运行?

当程序运行到调试断点处停下来了以后,程序创建的定时器和sleep睡眠函数还在计时吗?还会调用到超时回调函数吗?程序恢复运行后只能调用一次还是可以调多次?


如果您可以将这些问题的答案脱口而出,那我要为您点个赞,这篇文章对您来说可能太easy了。如果您觉得需要去实验一下才能给出答案,那么请往下看,相信一定会有收获。楼主会在Linux+GDB和Windows+VS2010两套平台上进行实验,并给出实验结果和分析。


Linux + GDB

先进行的是Linux平台上的实验,测试代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include <signal.h>
#include<sys/time.h>

void sigalrm_fn(int signo)
{
    static int cnt = 0;
    struct  timeval    tv;
    gettimeofday(&tv,NULL);
    printf("Timer is running  %d    %d\n", ++cnt,tv.tv_sec);
    alarm(10);
}

void *Thread1(void *arg)
{
        static int cnt = 0;
	struct  timeval    tv;
        while (1)
        {
            gettimeofday(&tv,NULL);
            printf("Thread1  is running %d  %d\n",++cnt,tv.tv_sec);
            sleep(7);
        }
        return NULL;
}

int main(int agrc,char* argv[])
{

        pthread_t tidp;
        struct  timeval    tv;
        static int cnt = 0;   

        pthread_create(&tidp,NULL,Thread1,NULL);
        signal(SIGALRM, sigalrm_fn);
        alarm(10);
        
        while (1)
        {
	    gettimeofday(&tv,NULL);
            printf("main thread is running %d %d\n",++cnt,tv.tv_sec);
            sleep(5);
        }
        return 0;
}

编译命令如下:(-g选项表示增加调试信息,给gdb使用;-lpthread是在链接pthread库)

gcc test.c -o test -g -lpthread

./test执行

Linux是基于信号来实现定时器的,不管是秒级的alarm还是据称可以精确到微秒级的settimer,都是由linux内核来进行计数,并在到达超时时间后,由内核发送一个信号到应用程序。由于对精度要求不高,楼主这里使用了alarm来实现定时器,每10秒超时一次。运行起来的log输出是这个样子的,每一行的后面那一长串数字是时间,以秒为单位


main_thread每5秒输出一次打印,Thread1每7秒输出一次打印,timer每10秒输出一次打印

所有线程都会被停住吗?

在程序运行的过程中,启动gdb,并attach到该进程上。由于gdb attach上去之后会直接把程序停住,所以和打断点停住的效果是一样的。

这时候所有的log输出都停了,这也说明了,整个进程里所有的线程全都被停止了,下面的这张图也给出了证据,上面两个红色的框里,29032是test启动时的主线程,29033是程序里创建的线程(LWP就是Light Weight Process,Linux对于线程就叫这个称呼),它们都被gdb touch到了。下面的那个红色的框表明当面进程的PC指针位置(运行到了哪里,nanosleep就是sleep函数的系统调用)


timer和sleep的计时还在继续吗?

使用gdb把程序停住之前,给一张程序log输出的截图,


过了约100秒之后,在gdb里面执行continue命令,让程序继续运行,输出的log如下:

图片里第一条红线上面的部分是gdb继续运行前输出的log,第一条红线和第二条红线间的3条log都是continue命令执行下去就会立刻输出的log,它们的时间都是1468563254,几乎是同时输出,这也说明了sleep函数和timer的计时还在继续,而且早已超时,但始终得不到执行。实际上,熟悉Linux的朋友肯定都知道,这些计时操作都是在内核里完成的,而gdb只是停住了一个应用程序,内核当然还是在正常运转的。

PS:gdb里有一条命令叫set scheduler-locking on它的作用如字面意思,禁止调度器,就是只允许当前选择的线程继续运行调试,其它所有的线程都停止运行。


Window +VS2010

测试程序的框架基本上是一样的,为方便有的同学可以直接拷贝去测试,贴一下吧
#include "stdafx.h"
#include "windows.h"
#include <stdio.h>
#include <Mmsystem.h>
#pragma comment(lib, "Winmm.lib")

void WINAPI onTimeFunc(UINT wTimerID, UINT msg,DWORD dwUser,DWORD dwl,DWORD dw2)
{
	static int cnt;
	SYSTEMTIME sys;
	GetLocalTime(&sys);
	printf("Timer is running %d, min: %d sec: %d \n",++cnt,sys.wMinute,sys.wSecond);
}


DWORD WINAPI Thread1(LPVOID pM) 
{
	static int cnt = 0;
	while (1)
	{
		SYSTEMTIME sys;
		GetLocalTime(&sys);
		printf("Thread1  is running %d, min: %d sec: %d \n",++cnt,sys.wMinute,sys.wSecond);
		Sleep(7000);
	}
	return 0;
}


int _tmain(int argc, _TCHAR* argv[])
{
	static int cnt = 0;   

	CreateThread(NULL,0,Thread1,NULL,0,NULL);
        timeSetEvent(10000, 1, (LPTIMECALLBACK)onTimeFunc, DWORD(1), TIME_PERIODIC);
	while (1)
	{
		SYSTEMTIME sys;
		GetLocalTime(&sys);
		printf("main thread is running %d, min: %d sec: %d \n",++cnt,sys.wMinute,sys.wSecond);
		Sleep(5000);
	}
	return 0;
}

在vs2010里编译通过,按F5启动调试模式。程序运行起来的log输出是这样的:(后面两个min和sec是当前系统时间的分和秒)


在Thread1函数的while循环里打一个断点,程序被停住,log输出也完全停止。这里看起来和Linux没有什么区别,调试器把整个进程都停下来了。
在使用调试器断点把程序停止过一段时间再放开让程序继续执行,有意思的事情出现了,log输出入下图所示:


程序是从18分0秒左右开始被调试器停住,到19分15秒左右放开继续运行,中间大概间隔了75秒。main_thread,thread1,和timer都会在第一时间输出log,说明它们的sleep和timer都早已超时,这里的原因和Linux一样,这些计数都是在内核里完成的,程序被停住并无影响。但是不同的是timer连续打印了8次,说明这期间的timer的每10秒一次超时的消息全部都保存了下来。这是为什么?

原来,Windows的进程都是采用消息驱动的机制,每个进程有一个消息队列,进程的每个动作都是由该消息队列里的内容来决定的。定时器消息就是其中的一种消息,每当定时器超时的时候,windows内核会给该进程发送一个消息,这个消息就存储在消息队列里,平常状态下,进程会立刻去该队列取消息并根据其内容做相应的工作,但由于进程被调试器暂时的block住了,所以这些超时消息都会缓存在消息队列里,等到进程得以再次运行的时候,一次性做了处理。

而Linux测试例程里的定时器是基于SIGALRM信号机制实现的,它属于“不可靠信号”,Linux内核没有为进程的不可靠信号做队列缓存,最多只存储一个,这也是Linux里比较有名的“信号丢失”问题。

如果想了解更多内容请参考《Windows核心编程》和《Unix环境高级编程》两本经典著作



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值