Robust C++: Safety Net
Greg Utas - 13/May=2020
Greg Utas - 13/May=2020
[SHOWTOGROUPS=4,20]
Keeping a program running when it would otherwise abort
This article presents a Thread class that prevents exceptions (such as those caused by bad pointers) from forcing a program to exit. It also describes how to capture information that facilitates debugging when such errors occur in software already released to users.
Some programs need to keep running even after nasty things happen, such as using an invalid pointer. Servers, other multi-user systems, and real-time games are a few examples. This article describes how to write robust C++ software that does not exit when the usual behavior is to abort. It also discusses how to capture information that facilitates debugging when nasty things occur in software that has been released to users.
Background
It is assumed that the reader is familiar with C++ exceptions. However, exceptions are not the only thing that robust software needs to deal with. It must also handle Для просмотра ссылки Войдиили Зарегистрируйся, which the operating system raises when something nasty occurs. The header Для просмотра ссылки Войди или Зарегистрируйся defines the following subset of POSIX signals for C/C++:
Using the Code
The code in this article is taken from the Robust Services Core (RSC), a large repository that, among other things, provides a framework for developing robust C++ applications. It contains over 220K lines of software organized into static libraries, each in its own namespace. All the code excerpted in this article comes from the namespace NodeBase in the Для просмотра ссылки Войдиили Зарегистрируйся directory. NodeBase contains about 55K lines of code that provide base classes for things such as:
An application developed using RSC derives from Thread to implement its threads. Everything described in this article then comes for free—unless the application isn't targeted for Windows, in which case that abstraction layer also has to be implemented.
If you don't want to use RSC, you can copy and modify its source code to meet your needs, subject to the terms of its GPL-3.0 license.
Overview of the Classes
RSC contains many details that are not relevant to this article, so the code that we look at will be excerpted from the relevant classes and functions, but with irrelevant details removed. Many of these details are nonetheless important and need to be considered if your approach is to copy and modify RSC software.
We will start by outlining the classes that appear in this article. In most cases, RSC defines each class in a .h of the same name and implements it in a .cpp of the same name. You should therefore be able to easily find the full version of each class in the Для просмотра ссылки Войдиили Зарегистрируйся directory.
Thread
Software that wants to be continuously available must catch all exceptions. A single-threaded application could do this in main. But RSC supports multi-threading, so it does this in a base Thread class from which all other threads derive. Thread has a loop that invokes the application in a try clause that is followed by a series of catch clauses which handle any exception not caught by the application.
SysThread
This is a wrapper for a native thread and is created by Thread's constructor. Much of the implementation is platform-specific.
Daemon
When a Thread is created, it can register a Daemon to recreate the thread after it is forced to exit, which usually occurs when the thread has caused too many exceptions.
Exception
The direct use of <exception> is inappropriate in a system that needs to debug problems in released software. Consequently, RSC defines a virtual Exception class from which all of its exceptions derive. This class's primary responsibility is to capture the running thread's stack when an exception occurs. In this way, the entire chain of function calls that led to the exception will be available to assist in debugging. This is far more useful than the C string returned by std::exception::what, stating something like "invalid string position", which specifies the problem but not where it arose and maybe not even uniquely where it was detected.
SysThreadStack
SysThreadStack is actually a namespace that wraps a handful of functions. The function of most interest is one that actually captures a thread's stack. Exception's constructor invokes this function, and so does a function (Debug::SwLog) whose purpose is to generate a debug log to record a problem that, although unexpected, did not actually result in an exception. All SysThreadStack functions are platform-specific.
SignalException
When a POSIX signal occurs, RSC throws it in a C++ exception so that it can be handled in the usual way, by unwinding the stack and deleting local objects. SignalException, derived from Exception, is used for this purpose. It simply records the signal that occurred and relies on its base class to capture the stack.
PosixSignal
Each signal supported within RSC must create a PosixSignal instance that includes its name (e.g. "SIGSEGV"), numeric value (11), explanation ("Invalid Memory Reference"), and other attributes. The PosixSignal instances for various signals defined by the POSIX standard, including those in <csignal>, are implemented as private members of the simple class SysSignals. The subset of signals supported on the target platform are then instantiated by SysSignals::CreateNativeSignals.
Throwing a SignalException turns out to be a useful way to recover from serious errors. RSC therefore defines signals for internal use in NbSignals.h. An instance of PosixSignal is also associated with each of these:
Walkthroughs
Creating a Thread
Now for the details. Let's start by creating a Thread. A subclass can add its own thread-specific data, but we're interested in Thread's constructor:
This constructor creates an instance of SysThread, which in turn creates a native thread. The arguments to SysThread's constructor are the thread's attributes:
Here is SysThread's constructor:
This has invoked three platform-specific functions (see SysThread.win.cpp if you're interested in the details):
EnterThread is the entry function for all Thread subclasses.
[/SHOWTOGROUPS]
Keeping a program running when it would otherwise abort
This article presents a Thread class that prevents exceptions (such as those caused by bad pointers) from forcing a program to exit. It also describes how to capture information that facilitates debugging when such errors occur in software already released to users.
- ..../KB/cpp/5165710/master.zip
Some programs need to keep running even after nasty things happen, such as using an invalid pointer. Servers, other multi-user systems, and real-time games are a few examples. This article describes how to write robust C++ software that does not exit when the usual behavior is to abort. It also discusses how to capture information that facilitates debugging when nasty things occur in software that has been released to users.
Background
It is assumed that the reader is familiar with C++ exceptions. However, exceptions are not the only thing that robust software needs to deal with. It must also handle Для просмотра ссылки Войди
- SIGINT: interrupt (usually when Ctrl-C is entered)
- SIGILL: illegal instruction (perhaps a stack corruption that affected the instruction pointer)
- SIGFPE: floating point exception (includes dividing by zero)
- Для просмотра ссылки Войди
или Зарегистрируйся: segment violation (using a bad pointer) - SIGTERM: forced termination (usually when the kill command is entered)
- SIGBREAK: break (usually when Ctrl-Break is entered)Для просмотра ссылки Войди
или Зарегистрируйся - SIGABRT: abnormal termination (when abort is invoked by the C++ run-time environment)
Using the Code
The code in this article is taken from the Robust Services Core (RSC), a large repository that, among other things, provides a framework for developing robust C++ applications. It contains over 220K lines of software organized into static libraries, each in its own namespace. All the code excerpted in this article comes from the namespace NodeBase in the Для просмотра ссылки Войди
- system initialization/reinitialization
- configuration parameters
- multi-threading
- object pooling
- CLI commands
- logging
- debugging tools
An application developed using RSC derives from Thread to implement its threads. Everything described in this article then comes for free—unless the application isn't targeted for Windows, in which case that abstraction layer also has to be implemented.
If you don't want to use RSC, you can copy and modify its source code to meet your needs, subject to the terms of its GPL-3.0 license.
Overview of the Classes
RSC contains many details that are not relevant to this article, so the code that we look at will be excerpted from the relevant classes and functions, but with irrelevant details removed. Many of these details are nonetheless important and need to be considered if your approach is to copy and modify RSC software.
We will start by outlining the classes that appear in this article. In most cases, RSC defines each class in a .h of the same name and implements it in a .cpp of the same name. You should therefore be able to easily find the full version of each class in the Для просмотра ссылки Войди
Thread
Software that wants to be continuously available must catch all exceptions. A single-threaded application could do this in main. But RSC supports multi-threading, so it does this in a base Thread class from which all other threads derive. Thread has a loop that invokes the application in a try clause that is followed by a series of catch clauses which handle any exception not caught by the application.
SysThread
This is a wrapper for a native thread and is created by Thread's constructor. Much of the implementation is platform-specific.
Daemon
When a Thread is created, it can register a Daemon to recreate the thread after it is forced to exit, which usually occurs when the thread has caused too many exceptions.
Exception
The direct use of <exception> is inappropriate in a system that needs to debug problems in released software. Consequently, RSC defines a virtual Exception class from which all of its exceptions derive. This class's primary responsibility is to capture the running thread's stack when an exception occurs. In this way, the entire chain of function calls that led to the exception will be available to assist in debugging. This is far more useful than the C string returned by std::exception::what, stating something like "invalid string position", which specifies the problem but not where it arose and maybe not even uniquely where it was detected.
SysThreadStack
SysThreadStack is actually a namespace that wraps a handful of functions. The function of most interest is one that actually captures a thread's stack. Exception's constructor invokes this function, and so does a function (Debug::SwLog) whose purpose is to generate a debug log to record a problem that, although unexpected, did not actually result in an exception. All SysThreadStack functions are platform-specific.
SignalException
When a POSIX signal occurs, RSC throws it in a C++ exception so that it can be handled in the usual way, by unwinding the stack and deleting local objects. SignalException, derived from Exception, is used for this purpose. It simply records the signal that occurred and relies on its base class to capture the stack.
PosixSignal
Each signal supported within RSC must create a PosixSignal instance that includes its name (e.g. "SIGSEGV"), numeric value (11), explanation ("Invalid Memory Reference"), and other attributes. The PosixSignal instances for various signals defined by the POSIX standard, including those in <csignal>, are implemented as private members of the simple class SysSignals. The subset of signals supported on the target platform are then instantiated by SysSignals::CreateNativeSignals.
Throwing a SignalException turns out to be a useful way to recover from serious errors. RSC therefore defines signals for internal use in NbSignals.h. An instance of PosixSignal is also associated with each of these:
Код:
// The following signals are proprietary and are used to throw a
// SignalException outside the signal handler.
//
constexpr signal_t SIGNIL = 0; // nil signal (non-error)
constexpr signal_t SIGWRITE = 121; // write to protected memory
constexpr signal_t SIGCLOSE = 122; // exit thread (non-error)
constexpr signal_t SIGYIELD = 123; // ran unpreemptably too long
constexpr signal_t SIGSTACK1 = 124; // stack overflow: attempt recovery
constexpr signal_t SIGSTACK2 = 125; // stack overflow: exit thread
constexpr signal_t SIGPURGE = 126; // thread killed or suicided
constexpr signal_t SIGDELETED = 127; // thread unexpectedly deleted
Walkthroughs
Creating a Thread
Now for the details. Let's start by creating a Thread. A subclass can add its own thread-specific data, but we're interested in Thread's constructor:
Код:
Thread::Thread(Faction faction, Daemon* daemon) :
daemon_(daemon),
faction_(faction)
{
// Thread uses the PIMPL idiom, with much of its data in priv_.
//
priv_.reset(new ThreadPriv);
// Create a new thread. StackUsageLimit is in words, so convert
// it to bytes.
//
auto prio = FactionToPriority(faction_);
systhrd_.reset(new SysThread(this, EnterThread, prio,
ThreadAdmin::StackUsageLimit() << BYTES_PER_WORD_LOG2));
Singleton< ThreadRegistry >::Instance()->BindThread(*this);
if(daemon_ != nullptr) daemon_->ThreadCreated(this);
}
This constructor creates an instance of SysThread, which in turn creates a native thread. The arguments to SysThread's constructor are the thread's attributes:
- the Thread object being constructed (this)
- its entry function (EnterThread for all Thread subclasses; it receives this as its argument)
- its priority (RSC bases this on a thread's Faction, which is not relevant to this article)
- its stack size, defined by the configuration parameter ThreadAdmin::StackUsageLimit
Here is SysThread's constructor:
Код:
SysThread::SysThread(const Thread* client,
const ThreadEntry entry, Priority prio, size_t size) :
nthread_(nullptr),
nid_(NIL_ID),
event_(CreateSentry()),
guard_(CreateSentry()),
signal_(SIGNIL)
{
// Create the thread and set its priority.
//
nthread_ = Create(entry, client, size, nid_);
SetPriority(prio);
}
This has invoked three platform-specific functions (see SysThread.win.cpp if you're interested in the details):
- Create creates the native thread. Its platform-specific handle is saved in nthread_, and its thread number is saved in nid_.
- CreateSentry creates event_, which the thread can wait on and which is signaled when the thread should resume execution (e.g., when the thread wants to sleep until a timeout occurs). When the thread is ready to run, it waits on guard_ until signaled to proceed, which allows RSC to control the scheduling of threads.
- SetPriority sets the thread's priority.
EnterThread is the entry function for all Thread subclasses.
Код:
main_t Thread::EnterThread(void* arg)
{
// Our argument (self) is a pointer to a Thread.
//
auto self = static_cast< Thread* >(arg);
// Indicate that we're ready to run. This blocks until we're signaled
// to proceed. At that point, register to catch signals and invoke our
// entry function.
//
self->Ready();
RegisterForSignals();
return self->Start();
}
[/SHOWTOGROUPS]
Последнее редактирование: