TimeCop - 检测时间限制违规的工具
本文演示了一个工具,用于检测特定代码区域执行时间超出指定超时的情况,并为这些情况提供了运行时反应机制。
引言
在本文中,我将介绍一种方法,用于检测嵌入在进程中的复杂系统中的时间约束违规。
背景
在复杂系统中,线程死锁或陷入无限循环的情况时有发生。当然,这是一种非常不希望出现的行为,但比简单的访问冲突或任何触发异常的情况更难发现,因为系统在操作系统和处理器的视图中是正常工作的。当系统非常庞大时,此类错误极难重现和修复。由于与客户的物理距离、剥离所有调试信息的发布编译以及许多其他原因,在工作环境中修复此类错误几乎是不可能的。
我提出的系统可以通过定义代码区域的时间约束并使用观察者线程检测其违规来检测此类情况。它不引入任何可由任何形式的调试器访问的符号信息,也不会显着降低性能。
基本概念
此代码背后的基本概念是代码区域的想法。与函数不同,它们可以定义在任何作用域,从一行代码到多个函数调用。这些区域连接形成一个堆栈,这在检索有关违规点的信息时非常有用。
示例
void function()
{
REGION("My region", 1000)
{ // code here should not execute for longer
// than 1000 milliseconds
}
ENDREGION();
}
实现此系统的主要工作是使区域进入/离开代码尽可能轻量级,将负载转移到观察者线程,该线程相对较少地执行其工作。这意味着在区域守护代码中不允许动态分配或长条件序列。
另一个想法是构建一个观察者,它将监视线程并检测终止事件和时间约束违规。此线程必须拥有一组线程句柄,以便在其正常操作期间不创建或关闭任何句柄。当约束被违反时,观察者调用用户定义的回调,提供有关当前区域、违规区域的信息,并提供线程信息。
保持堆栈帧链接
为了保持跨函数调用的类堆栈结构链接,需要一些全局数据。当涉及线程时,全局数据不是一个好主意,这就是线程局部存储(TLS)发挥作用的地方。TLS 是一组数据,位于指定的内存位置或某个数组中的索引下,但每个线程都看到自己的数据副本。此外,当分配 TLS 时,它会出现在所有现有线程和所有将要创建的线程中。
由于我们有一个完美的位置来存储我们的堆栈帧指针,我们可以定义结构。
结构体
由于解决方案是相当低级的,它在某种程度上是结构化的。此概念需要的功能结构包括:
- 线程信息
- 区域描述
- 区域堆栈帧
线程信息块包含线程名称、当前最顶层堆栈帧指针、线程 ID 和线程句柄。
class ThreadInfo
{
public:
inline ThreadInfo();
inline ~ThreadInfo();
class Lock
{
public:
inline Lock(ThreadInfo *ti);
inline ~Lock();
volatile LONG *pLockCount;
};
ThreadCodeRegion *volatile Region;
volatile LONG LockCount;
int ThreadId;
HANDLE ThreadHandle;
std::string ThreadName;
};
区域描述包含名称和超时。可以在其中放置一些额外数据。
class CodeRegion
{
public:
inline CodeRegion(const char *name, int timeout);
const char *Name;
int Timeout;
};
区域堆栈帧包含有关区域的信息、线程进入它的时间以及指向较低帧的链接。
class ThreadCodeRegion
{
public:
inline ThreadCodeRegion(CodeRegion *cr);
inline ~ThreadCodeRegion();
CodeRegion *CurrentRegion;
ThreadCodeRegion *PrevFrame;
int EntryTime;
bool Ignore;
};
观察者
系统的主要对象名为 `TimeCop`。它实现了一个线程,用于监视系统中注册的所有线程。它维护一个 `ThreadId` 到 `ThreadInfo` 的映射,用于执行周期性堆栈帧搜索。它还提供注册线程和为调用线程检索描述符块的功能。
算法
所使用的算法非常简单。观察者遍历所有线程信息块,并为每个信息块执行以下操作:
- 获取自旋锁
- 从堆栈顶部到底部遍历,查找时间约束已被违反的帧
- 释放自旋锁
如果找到违规帧,系统会暂停违规线程并调用用户定义的回调,这些回调可以执行特定于程序的动作。回调返回后,线程恢复执行。
被监视类的头文件如下所示:
class TimeCop
{
public:
TimeCop();
~TimeCop();
static TimeCop *Instance;
static inline ThreadInfo *GetCurrentThreadInfo();
ThreadInfo *InitCurrentThreadInfo(const char *ThreadName);
typedef void (*Callback)(
void *context,
ThreadInfo *Thread,
int TimeInRegion,
ThreadCodeRegion *TopFrame,
ThreadCodeRegion *FrameAtFault);
void AddCallback(Callback cb, void *context);
void RemoveCallback(Callback cb, void *context);
private:
static int TlsEntry;
CRITICAL_SECTION m_MapLock;
typedef std::map <dword, /> ThreadMap;
ThreadMap threadmap;
DWORD StartAtId;
CRITICAL_SECTION m_CBLock;
struct Delegate
{
Callback cb;
void *context;
};
std::vector <delegate /> callbacks;
void InvokeCallbacks(
ThreadInfo *Thread,
int TimeInRegion,
ThreadCodeRegion *TopFrameAtFault,
ThreadCodeRegion *FrameAtFault);
void Start();
void Run();
void Analyze();
bool Analyze(
ThreadMap::iterator it,
int &TimeInRegion,
ThreadCodeRegion *&FrameAtFault,
ThreadCodeRegion *&TopFrameAtFault);
static ULONG WINAPI _run(void *This);
HANDLE m_hThread;
HANDLE m_hWait;
bool ExitRequested;
};
进入和离开区域
正如我在背景部分所述,区域进入和离开代码的设计对于保持系统效率至关重要。代码放置在 `ThreadCodeRegion` 类的构造函数和析构函数中。
ThreadCodeRegion::ThreadCodeRegion(CodeRegion *cr)
: CurrentRegion(cr)
{
// this is a single lookup in the TLS
ThreadInfo *ti=TimeCop::GetCurrentThreadInfo();
PrevFrame=ti->Region;
EntryTime=GetTickCount();
Ignore=false;
// this comes last, when all else is initialized
// - we can avoid acquiring any locks here
InterlockedExchangePointer(&(ti->Region), this);
}
上述代码包含极少的 API 调用,并且它们都没有改变处理器模式(没有发出真正的系统调用)。
离开一个区域会有点麻烦,因为该帧可能正在使用中。为了确保它没有被使用,在将对象从堆栈分离并可以安全销毁之前,会在 `ThreadInfo` 上获取一个锁。
ThreadCodeRegion::~ThreadCodeRegion()
{
ThreadInfo *ti=TimeCop::GetCurrentThreadInfo();
ThreadInfo::Lock lock(ti); // acquire a spinlock
InterlockedExchangePointer(&(ti->Region), PrevFrame);
}
这个自旋锁实际上是一个准自旋锁——如果无法获取临界区,线程会放弃执行。如果成功,则不发出系统调用。
class /*ThreadInfo::*/ Lock
{
public:
inline Lock(ThreadInfo *ti) : pLockCount(&ti->LockCount)
{
// spinlock
while (InterlockedCompareExchange(pLockCount, 1, 0))
SwitchToThread(); // yield execution
}
inline ~Lock()
{
InterlockedDecrement(pLockCount);
}
volatile LONG *pLockCount;
};
使用代码
最终开发人员可以通过三个宏使用该系统:
REGION(name, timeout)
ENDREGION()
INIT_THREAD(name)
第一个定义了一个代码区域。它定义了一个 `CodeRegion` 类的静态局部变量,以及一个 `ThreadCodeRegion` 类型的基于堆栈的局部变量,该变量进入了上述 `CodeRegion`。
#define REGION(name, timeout) {\
static CodeRegion __code_region(name, timeout);\
ThreadCodeRegion __region(&__code_region);
ENDREGION() 只是一个 '}'。INIT_THREAD 是 InitCurrentThread 函数的便捷访问。
#define INIT_THREAD(name) TimeCop::Instance->InitCurrentThreadInfo(name)
一个示例线程入口函数可以如下所示:
ULONG WINAPI Thread(LPVOID param)
{
INIT_THREAD("My thread");
REGION(__FUNCTION__, 10000) // ten seconds
// some code
// only one second here
REGION(__FUNCTION__ " internal region", 1000)
// some critical code
ENDREGION();
// some code
ENDREGION();
return 0;
}
回调
要使系统做一些有用的事情,我们需要设置一些回调。这可以通过简单地在 `TimeCop` 对象上调用 `AddCallback` 来完成。回调由一个函数和一个任意指针组成,该指针可用于为回调传递一些额外的上下文。
示例
void Violation(void *context,
ThreadInfo *Thread,
int time,
ThreadCodeRegion *top,
ThreadCodeRegion *fault)
{
cerr << "Thread " << Thread->ThreadName
<< " violated a time constraint" << endl;
// dump all region names to STDERR
for (const ThreadCodeRegion *frame=top; frame; frame=frame->PrevFrame)
{
cerr << " " << frame->CurrentRegion->Name << endl;
}
fault->Ignore=true;
}
void main()
{
TimeCop tc;
tc.AddCallback(Violation, 0);
CreateThread(0, 0, Thread, 0, 0, 0);
...
...
}
应用
该系统可用于各种目的。最简单的是记录事件或显示一些警告消息(不建议用于没有桌面的服务)。它也可以用于终止违规线程,或对阻塞对象进行一些控制工作。在某些情况下,该系统可用于管理性能。例如,我们可以安排一个线程在后台以低优先级执行一些耗时的工作,同时保持系统响应。但是,如果任务未在所需时间内完成,我们可以提高线程的优先级,以强制任务更快完成。
未来工作
此代码使用相当不准确的时间测量方法——`GetTickCount()`——也许可以使用更好的计时器(尽管,如果每秒定期检查违规,谁会在乎呢?)。仅检查线程的执行时间(而不是系统时间)也可能证明有用。
我还将尝试使违规检查机制更具响应性,例如,通过引入一个控制变量来定义下一次超时的时间——这样周期就可以更短(例如,50毫秒),并且只有当系统时间超过存储在变量中的时间时才进行实际检查。
最后说明
我计划在一些服务器应用程序中使用此代码,因为有时会出现极其罕见的死锁情况。如果您需要执行一些基于时间约束的自诊断或性能管理,该系统可能会证明有用。如果您觉得这个主题有趣或代码有用,我将很高兴知道,并且更高兴证明自己有所帮助。