可读性,健壮性,安全性_健壮的C ++:安全网

可读性,健壮性,安全性

介绍 (Introduction)

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.

有些程序即使在发生讨厌的事情后也需要保持运行,例如使用无效的指针。 服务器,其他多用户系统和实时游戏就是其中的几个示例。 本文介绍如何编写健壮的C ++软件,该软件在通常的行为是中止时不会退出。 它还讨论了当已发布给用户的软件中发生令人讨厌的事情时,如何捕获有助于调试的信息。

背景 (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 POSIX signals, which the operating system raises when something nasty occurs. The header <csignal> defines the following subset of POSIX signals for C/C++:

假定读者熟悉C ++异常。 但是,异常并不是功能强大的软件需要处理的唯一事情。 它还必须处理POSIX信号 ,当发生令人讨厌的事情时,操作系统会发出信号 。 标头<csignal>为C / C ++定义了POSIX信号的以下子集:

  • SIGINT: interrupt (usually when Ctrl-C is entered)

    SIGINT :中断(通常在输入Ctrl-C时)

  • SIGILL: illegal instruction (perhaps a stack corruption that affected the instruction pointer)

    SIGILL :非法指令(可能是堆栈损坏影响了指令指针)

  • SIGFPE: floating point exception (includes dividing by zero)

    SIGFPE :浮点异常(包括除以零)

  • SIGSEGV: segment violation (using a bad pointer)

    SIGSEGV :段冲突(使用错误的指针)

  • SIGTERM: forced termination (usually when the kill command is entered)

    SIGTERM :强制终止(通常在输入kill命令时)

  • SIGBREAK: break (usually when Ctrl-Break is entered)1

    SIGBREAK :中断(通常在输入Ctrl-Break时) 1

  • SIGABRT: abnormal termination (when abort is invoked by the C++ run-time environment)

    SIGABRT :异常终止(当C ++运行时环境调用abort时)

Similar to how exceptions are caught by a catch statement, signals are caught by a signal handler. Each thread can register a signal handler against each signal that it wants to handle. A signal is simply an int that is passed to the signal handler as an argument.

catch语句捕获异常的方式类似, Signal信号处理程序捕获。 每个线程可以针对要处理的每个信号注册一个信号处理程序。 信号只是一个int ,它作为参数传递给信号处理程序。

使用代码 (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 225K 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 nb directory. NodeBase contains about 55K lines of code that provide base classes for things such as:

本文中的代码取材于强大的服务核心 (RSC),后者是一个大型存储库,除其他功能外,它还提供了用于开发强大的C ++应用程序的框架。 它包含超过225K的软件行,这些软件被组织到静态库中,每条线都位于自己的名称空间中。 本文摘录的所有代码均来自nb目录中的命名空间NodeBaseNodeBase包含约55K行代码,这些代码为诸如以下内容提供基类:

RSC is targeted at Windows but has an abstraction layer that should allow it to be ported to other platforms with modest effort. The Windows targets (in *.win.cpp files) currently comprise 3K lines of code.

RSC的目标是Windows,但具有一个抽象层,该层应允许它轻而易举地移植到其他平台。 Windows目标(在* .win.cpp文件中)当前包含3K行代码。

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.

使用RSC开发的应用程序派生自Thread来实现其线程。 然后,本文中描述的所有内容都是免费提供的-除非该应用程序并非针对Windows,在这种情况下,还必须实现抽象层。

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.

如果您不想使用RSC,则可以根据其GPL-3.0许可的条款复制和修改其源代码以满足您的需求。

课程概述 (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.

RSC包含许多与本文无关的细节,因此我们看的代码将从相关的类和函数中摘录,但删除了无关的细节。 但是,其中许多细节都很重要,如果您要复制和修改RSC软件,则需要考虑这些细节

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 nb directory.

我们将首先概述本文中出现的类。 在大多数情况下,RSC在相同名称的.h中定义每个类,并在相同名称的.cpp中实现它。 因此,您应该能够在nb目录中轻松找到每个类的完整版本。

线 (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.

想要持续可用的软件必须捕获所有异常。 单线程应用程序可以在main执行此操作。 但是RSC支持多线程,因此它在所有其他线程都派生自的基本Thread类中进行。 Thread具有一个循环,该循环在try子句中调用应用程序,然后在try子句中包含一系列catch子句,这些子句可处理应用程序未捕获的任何异常。

SysThread (SysThread)

This is a wrapper for a native thread and is created by Thread's constructor. Much of the implementation is platform-specific.

这是本机线程的包装,由Thread的构造函数创建。 许多实现是特定于平台的。

守护进程 (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.

当一个Thread被创建,它可以注册一个Daemon它被强制退出,当线程已经造成太多的例外,通常发生后重新创建线程。

例外 (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.

在需要调试已发布软件中的问题的系统中,直接使用<exception>是不合适的。 因此,RSC定义了一个虚拟的Exception类,其所有异常都从该类派生。 此类的主要职责是在发生异常时捕获正在运行的线程的堆栈。 这样,导致异常的整个函数调用链将可用于协助调试。 这比std::exception::what返回的C string有用,它声明类似“ invalid string position ”之类的东西,它指定了问题,但没有指出问题的出处,甚至可能不是唯一地检测到问题。

SysThreadStack (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.

SysThreadStack实际上是一个包装了一些功能的名称空间。 最受关注的功能是实际上捕获线程堆栈的功能。 Exception的构造函数调用此函数,而函数( Debug::SwLog ) Debug::SwLog调用该函数,该函数的目的是生成调试日志以记录一个问题,尽管该问题出乎意料,但实际上并未导致异常。 所有SysThreadStack函数都是特定于平台的。

SignalException (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.

发生POSIX信号时,RSC会将其抛出C ++异常,以便可以通过平移堆栈并删除本地对象以通常的方式对其进行处理。 从Exception派生的SignalException用于此目的。 它仅记录发生的信号,并依靠其基类来捕获堆栈。

PosixSignal (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.

RSC中支持的每个信号都必须创建一个PosixSignal实例,该实例包括其名称(例如"SIGSEGV" ),数值( 11 ),说明( "Invalid Memory Reference" )和其他属性。 POSIX标准定义的各种信号(包括<csignal>信号)的PosixSignal实例被实现为简单类SysSignals private成员。 然后由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:

抛出SignalException证明是从严重错误中恢复的有用方法。 因此,RSC在NbSignals.h中定义了供内部使用的信号PosixSignal的实例也与以下每个对象相关联:

//  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的构造函数感兴趣:

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()->Created(systhrd_.get(), 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:

此构造函数创建SysThread的实例,该实例又创建一个本机线程。 SysThread的构造函数的参数是线程的属性:

  • the Thread object being constructed (this)

    正在构造的Thread对象( this )

  • its entry function (EnterThread for all Thread subclasses; it receives this as its argument)

    其入口函数( EnterThread所有Thread类;它接收this作为其参数)

  • its priority (RSC bases this on a thread's Faction, which is not relevant to this article)

    它的优先级(RSC基于线程的Faction ,与本文无关)

  • its stack size, defined by the configuration parameter ThreadAdmin::StackUsageLimit

    其堆栈大小,由配置参数ThreadAdmin::StackUsageLimit

The new thread is then added to ThreadRegistry, which tracks all active threads.

然后,将新线程添加到ThreadRegistry ,该线程跟踪所有活动线程。

Here is SysThread's constructor:

这是SysThread的构造函数:

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):

这调用了三个特定于平台的功能(如果您对详细信息感兴趣,请参阅SysThread.win.cpp ):

  • Create creates the native thread. Its platform-specific handle is saved in nthread_, and its thread number is saved in nid_.

    Create创建本机线程。 其特定于平台的句柄保存在nthread_ ,其线程号保存在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.

    CreateSentry创建event_ ,线程可以等待它,并在线程应继续执行时(例如,当线程想进入Hibernate状态直到超时时)发出信号。 当线程准备好运行时,它将在guard_等待,直到发出继续进行信号的通知为止,这使RSC可以控制线程的调度。

  • SetPriority sets the thread's priority.

    SetPriority设置线程的优先级。

输入线程 (Entering a Thread)

EnterThread is the entry function for all Thread subclasses.

EnterThread是所有Thread子类的入口函数。

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();
}

RegisterForSignals simply registers SignalHandler against each signal that is native to the underlying platform. This is done by invoking signal (in <csignal>), which must be done by every thread, for each signal that it wants to handle, when the thread is entered and after each time that it receives a signal.

RegisterForSignals只需针对基础平台本地的每个信号注册SignalHandler 。 这是通过调用signal (在<csignal> )来完成的,信号必须由每个线程针对要处理的每个信号,进入线程时以及每次接收到信号之后执行。

void Thread::RegisterForSignals()
{
   auto& signals = Singleton< PosixSignalRegistry >::Instance()->Signals();

   for(auto s = signals.First(); s != nullptr; signals.Next(s))
   {
      if(s->Attrs().test(PosixSignal::Native))
      {
         signal(s->Value(), SignalHandler);
      }
   }
}

We will look at SignalHandler later. To complete this section, we need to look at Start, which EnterThread invoked.

稍后我们将SignalHandler 。 要完成本节,我们需要查看Start ,它是EnterThread调用的。

main_t Thread::Start()
{
   for(NO_OP; true; stats_->traps_->Incr())
   {
      try
      {
         //  Perform any environment-specific initialization (and recovery,
         //  if reentering the thread). Exit the thread if this fails.
         //
         auto rc = systhrd_->Start();
         if(rc != 0) return Exit(rc);

         switch(priv_->traps_)
         {
         case 0:
            break;

         case 1:
         {
            //  The thread just trapped. Invoke its virtual Recover function
            //  in case it needs to clean up unfinished work before resuming
            //  execution.  (The full version of this code is more complex
            //  because it handles the case where Recover traps.)
            //
            priv_->traps_ = 0;
            Recover();
            break;
         }

         default:
            //
            //  TrapHandler (which appears later) should have prevented us
            //  from getting here.  Exit the thread.
            //
            return Exit(priv_->signal_);
         }

         //  Invoke the thread's entry function. If this returns,
         //  the thread exited voluntarily.
         //
         Enter();
         return Exit(SIGNIL);
      }

      //  Catch all exceptions. TrapHandler returns one of
      //  o Continue, to resume execution at the top of this loop
      //  o Release, to exit the thread after deleting it
      //  o Return, to exit the thread immediately
      //  o Rethrow, to rethrow the exception
      //
      catch(SignalException& sex)
      {
         switch(TrapHandler(&sex, &sex, sex.GetSignal(), sex.Stack()))
         {
         case Continue: continue;
         case Release:  return Exit(sex.GetSignal());
         case Return:   return sex.GetSignal();
         default:       throw;
         }
      }
      catch(Exception& ex)
      {
         switch(TrapHandler(&ex, &ex, SIGNIL, ex.Stack()))
         {
         case Continue: continue;
         case Release:  return Exit(SIGNIL);
         case Return:   return SIGDELETED;
         default:       throw;
         }
      }
      catch(std::exception& e)
      {
         switch(TrapHandler(nullptr, &e, SIGNIL, nullptr))
         {
         case Continue: continue;
         case Release:  return Exit(SIGNIL);
         case Return:   return SIGDELETED;
         default:       throw;
         }
      }
      catch(...)
      {
         switch(TrapHandler(nullptr, nullptr, SIGNIL, nullptr))
         {
         case Continue: continue;
         case Release:  return Exit(SIGNIL);
         case Return:   return SIGDELETED;
         default:       throw;
         }
      }
   }
}

Each time through its loop, Start began by invoking SysThread::Start, which allows the native thread to perform any work that is required before it can safely run. This is platform-specific code which looks like this on Windows:

每次循环时, Start通过调用SysThread::Start ,它允许本机线程执行安全运行之前所需的任何工作。 这是特定于平台的代码,在Windows上看起来像这样:

signal_t SysThread::Start()
{
   //  This is also invoked when recovering from a trap, so see if a stack
   //  overflow occurred. Some of these are irrecoverable, in which case
   //  returning SIGSTACK2 causes the thread to exit.
   //
   if(status_.test(StackOverflowed))
   {
      if(_resetstkoflw() == 0)
      {
         return SIGSTACK2;
      }

      status_.reset(StackOverflowed);
   }

   //  The translator for Windows structured exceptions must be installed
   //  on a per-thread basis.
   //
   _set_se_translator((_se_translator_function) SE_Handler);
   return 0;
}

The first part of this deals with thread stack overflows, which can be particularly nasty. The last part installs a Windows-specific handler. Windows doesn't normally raise POSIX signals, but instead has what it calls "structured exceptions". We therefore provide SE_Handler, which translates a Windows-specific exception into a POSIX signal that can be thrown using our SignalException. The code for this will appear later.

第一部分处理线程堆栈溢出,这可能特别令人讨厌。 最后一部分将安装Windows特定的处理程序。 Windows通常不会引发POSIX信号,而是具有所谓的“结构化异常”。 因此,我们提供SE_Handler ,它将Windows特定的异常转换为可以使用SignalException抛出的POSIX信号。 此代码将稍后显示。

退出线程 (Exiting a Thread)

Exit is normally invoked to exit a thread; this occurs when its Enter function returns or if it is forced to exit after an exception. Exit is only bypassed if a Thread somehow gets deleted while it is still running. In that case, TrapHandler returns Return, which causes the thread to exit immediately, given that it no longer has any objects to delete.

通常调用Exit退出线程。 当其Enter函数返回时,或者在异常之后被迫退出时,会发生这种情况。 仅当Thread仍在运行时被某种方式删除时,才会绕过Exit 。 在这种情况下, TrapHandler返回Return ,因为它不再具有要删除的对象,这将导致线程立即退出。

When a Thread object is deleted, its Daemon (if any) is notified so that it can recreate the thread. RSC also tracks mutex ownership, so it releases any mutex that the thread owns. Most operating systems do this anyway, but RSC generates a log to highlight that this occurred. Tracking mutex ownership also allows deadlocks to be debugged as long as the CLI thread is not involved in the deadlock.

删除Thread对象时,将通知其Daemon (如果有),以便其可以重新创建线程。 RSC还会跟踪互斥锁的所有权,因此它将释放线程拥有的所有互斥锁。 无论如何,大多数操作系统都会这样做,但是RSC会生成日志以突出显示这种情况。 只要CLI线程不涉及死锁,跟踪互斥锁的所有权还可以调试死锁。

main_t Thread::Exit(signal_t sig)
{
   delete this;
   return sig;
}

Thread::~Thread()
{
   //  Other than in very rare situations, the usual path is
   //  to schedule the next thread (via Suspend) and delete
   //  this thread's resources.
   //
   Suspend();
   ReleaseResources();
}

void Thread::ReleaseResources()
{
   //  If the thread has a daemon, tell it that the thread is
   //  exiting.  Remove the thread from the registry and free
   //  its native thread.
   //
   Singleton< ThreadRegistry >::Extant()->Erase(this);
   if(dameon_ != nullptr) daemon_->ThreadDeleted(this);
   systhrd_.reset();
}

接收Windows结构化异常 (Receiving a Windows Structured Exception)

As previously mentioned, we register SE_Handler to map each Windows exception to a POSIX signal:

如前所述,我们注册SE_Handler以将每个Windows异常映射到POSIX信号:

//  Converts a Windows structured exception to a POSIX signal.
//
void SE_Handler(uint32_t errval, const _EXCEPTION_POINTERS* ex)
{
   signal_t sig = 0;

   switch(errval)                         // errval:
   {
   case DBG_CONTROL_C:                    // 0x40010005
      sig = SIGINT;
      break;

   case DBG_CONTROL_BREAK:                // 0x40010008
      sig = SIGBREAK;
      break;

   case STATUS_ACCESS_VIOLATION:          // 0xC0000005
      //
      //  The following returns SIGWRITE instead of SIGSEGV if the exception
      //  occurred when writing to a legal address that was write-protected.
      //
      sig = AccessViolationType(ex);
      break;

   case STATUS_DATATYPE_MISALIGNMENT:     // 0x80000002
   case STATUS_IN_PAGE_ERROR:             // 0xC0000006
   case STATUS_INVALID_HANDLE:            // 0xC0000008
   case STATUS_NO_MEMORY:                 // 0xC0000017
      sig = SIGSEGV;
      break;

   case STATUS_ILLEGAL_INSTRUCTION:       // 0xC000001D
      sig = SIGILL;
      break;

   case STATUS_NONCONTINUABLE_EXCEPTION:  // 0xC0000025
      sig = SIGTERM;
      break;

   case STATUS_INVALID_DISPOSITION:       // 0xC0000026
   case STATUS_ARRAY_BOUNDS_EXCEEDED:     // 0xC000008C
      sig = SIGSEGV;
      break;

   case STATUS_FLOAT_DENORMAL_OPERAND:    // 0xC000008D
   case STATUS_FLOAT_DIVIDE_BY_ZERO:      // 0xC000008E
   case STATUS_FLOAT_INEXACT_RESULT:      // 0xC000008F
   case STATUS_FLOAT_INVALID_OPERATION:   // 0xC0000090
   case STATUS_FLOAT_OVERFLOW:            // 0xC0000091
   case STATUS_FLOAT_STACK_CHECK:         // 0xC0000092
   case STATUS_FLOAT_UNDERFLOW:           // 0xC0000093
   case STATUS_INTEGER_DIVIDE_BY_ZERO:    // 0xC0000094
   case STATUS_INTEGER_OVERFLOW:          // 0xC0000095
      sig = SIGFPE;
      break;

   case STATUS_PRIVILEGED_INSTRUCTION:    // 0xC0000096
      sig = SIGILL;
      break;

   case STATUS_STACK_OVERFLOW:            // 0xC00000FD
      //
      //  A stack overflow in Windows now raises the exception
      //  System.StackOverflowException, which cannot be caught.
      //  Stack checking in Thread should therefore be enabled.
      //
      sig = SIGSTACK1;
      break;

   default:
      sig = SIGTERM;
   }

   //  Handle SIG. This usually throws an exception; in any case, it will
   //  not return here. If it does return, there is no specific provision
   //  for reraising a structured exception, so simply return and assume
   //  that Windows will handle it, probably brutally.
   //
   Thread::HandleSignal(sig, errval);
}

接收POSIX信号 (Receiving a POSIX Signal)

We registered SignalHandler to receive POSIX signals. Even on Windows, with its structured exceptions, this code is reached after invoking raise (in <csignal>):

我们注册了SignalHandler以接收POSIX信号。 即使在Windows中(具有结构化异常),也可以在调用raise之后在<csignal>以下代码:

void Thread::SignalHandler(signal_t sig)
{
   //  Re-register for signals before handling the signal.
   //
   RegisterForSignals();
   if(HandleSignal(sig, 0)) return;

   //  Either trap recovery is off or we received a signal that could not be
   //  associated with a thread. Restore the default handler for the signal
   //  and reraise it (to enter the debugger, for example).
   //
   signal(sig, SIG_DFL);
   raise(sig);
}

将POSIX信号转换为SignalException (Converting a POSIX Signal to a SignalException)

Now that we have a POSIX signal which was either received by SignalHandler or translated from a Windows structured exception by SE_Handler, we can turn it into a SignalException:

现在,我们有要么收到一个POSIX信号SignalHandler或由Windows结构化异常翻译SE_Handler ,我们可以把它变成一个SignalException

bool Thread::HandleSignal(signal_t sig, uint32_t code)
{
   Debug::ft(Thread_HandleSignal);

   auto thr = RunningThread(std::nothrow);

   if(thr != nullptr)
   {
      //  Turn the signal into a standard C++ exception so that it can
      //  be caught and recovery action initiated.
      //
      throw SignalException(sig, code);
   }

   //  The running thread could not be identified. A break signal (e.g.
   //  on ctrl-C) is sometimes delivered on an unregistered thread. If
   //  the RTC timeout is not being enforced and the locked thread has
   //  run too long, trap it; otherwise, assume that the purpose of the
   //  ctrl-C is to trap the CLI thread so that it will abort its work.
   //
   auto reg = Singleton< PosixSignalRegistry >::Instance();

   if(reg->Attrs(sig).test(PosixSignal::Break))
   {
      if(!ThreadAdmin::TrapOnRtcTimeout())
      {
         thr = LockedThread();

         if((thr != nullptr) && (Clock::TicksUntil(thr->priv_->currEnd_) > 0))
         {
            thr = nullptr;
         }
      }

      if(thr == nullptr) thr = Singleton< CliThread >::Extant();
      if(thr == nullptr) return false;
      thr->Raise(sig);
      return true;
   }

   return false;
}

The code after the throw requires some explanation. Break signals (SIGINT, SIGBREAK), which are generated when the user enters Ctrl-C or Ctrl-Break, often arrive on an unknown thread. It is reasonable to assume that the user wants to abort work that is taking too long or, worse, stuck in an infinite loop.

throw之后的代码需要一些解释。 用户输入Ctrl-CCtrl-Break时生成的中断信号( SIGINTSIGBREAK )通常到达未知线程。 可以合理地假设用户想要中止时间太长或陷入无限循环的工作。

But what work should be aborted? Here, it must be pointed out that RSC strongly encourages the use of cooperative scheduling, where a thread runs unpreemptably ("locked") and yields after completing a logical unit of work. RSC only allows one unpreemptable thread to run at a time, and it also enforces a timeout on such a thread's execution. If the thread does not yield before the timeout, it receives the internal signal SIGYIELD, causing a SignalException to be thrown. During development, it is sometimes useful to disable this timeout. So in trying to identify which thread is performing the work that the user wants to abort, the first candidate is the thread that is running unpreemptably. However, this thread will only be interrupted if the use of SIGYIELD has been disabled and the thread has already run for longer than the timeout.

但是应该终止哪些工作? 在这里,必须指出的是,RSC强烈鼓励使用协作调度 ,即线程无可避免地运行(“锁定”)并在完成逻辑工作单元后屈服。 RSC一次只允许一个不可抢占的线程运行,并且还会在此类线程的执行上强制超时。 如果线程在超时之前未屈服,则它将接收内部信号SIGYIELD ,从而引发SignalException 。 在开发期间,禁用此超时有时很有用。 因此,在尝试确定哪个线程正在执行用户要中止的工作时,第一个候选对象是正在抢占地运行的线程。 但是,仅当禁用SIGYIELD的使用并且该线程已运行超过超时时间时,该线程才会被中断。

If interrupting the unpreemptable thread doesn't seem appropriate, the assumption is that CliThread should be interrupted. This thread is the one that parses and executes user commands entered through the console. So unless CliThread doesn't exist for some obscure reason, it will receive the SIGYIELD.

如果中断不可抢占的线程似乎不合适,则认为应该中断CliThread 。 该线程是解析和执行通过控制台输入的用户命令的线程。 因此,除非出于某种晦涩的原因不存在CliThread ,否则它将收到SIGYIELD

If a thread to interrupt has now been identified, Thread::Raise is invoked to deliver the signal to that thread.

如果现在已确定要中断的线程,则将调用Thread::Raise将信号传递到该线程。

发信号通知另一个线程 (Signaling Another Thread)

Sending a signal to another thread is problematic. The raise function in <csignal> only signals the running thread. Nor does Windows appear to expose any function that could be used for the purpose. So what to do?

将信号发送到另一个线程是有问题的。 <csignal>raise函数仅表示正在运行的线程。 Windows似乎也没有公开任何可用于此目的的功能。 那么该怎么办?

In RSC, the first thing that most functions do is call Debug::ft to identify the function that is now executing. These calls were removed from the code in this article, but now it is necessary to mention them. The original (and still extant) purpose of Debug::ft is to support a function trace tool, which is why most non-trivial functions invoke it. What this trace tool produces will be seen later. The pervasiveness of Debug::ft also allows it to be co-opted for other purposes. Because a thread is likely to invoke it frequently, it can check if the thread has a signal waiting. If so, boom! It can also check if the thread is at risk of overrunning its stack, in which case boom! (This is better than allowing an overrun to occur. As noted in SE_Handler, Windows no longer even allows a stack overflow exception to be intercepted.)

在RSC中,大多数功能要做的第一件事是调用Debug::ft来标识正在执行的功能。 这些调用已从本文的代码中删除,但是现在有必要提及它们。 Debug::ft的原始(并且仍然是)目的是支持函数跟踪工具,这就是为什么大多数非平凡的函数都调用它的原因。 该跟踪工具产生的结果将在以后看到。 Debug::ft的普遍性还使其可以被选作其他用途。 因为线程可能会频繁调用它,所以它可以检查线程是否有信号等待。 如果是这样,繁荣! 它还可以检查线程是否有溢出其堆栈的风险,在这种情况下,请注意! (这比允许发生溢出更好。如SE_Handler ,Windows甚至不再允许拦截堆栈溢出异常。)

Here is the code that delivers a signal to another thread:

这是向另一个线程传递信号的代码:

void Thread::Raise(signal_t sig)
{
   Debug::ft(Thread_Raise);

   auto reg = Singleton< PosixSignalRegistry >::Instance();
   auto ps1 = reg->Find(sig);

   //  If this is the running thread, throw the signal immediately. If the
   //  running thread can't be found, don't assert: the signal handler can
   //  invoke this when a signal occurs on an unknown thread.
   //
   auto thr = RunningThread(std::nothrow);

   if(thr == this)
   {
      throw SignalException(sig, 0);
   }

   //  If the signal will force the thread to exit, try to unblock it.
   //  Unblocking usually involves deallocating resources, so force the
   //  thread to sleep if it wakes up during Unblock().
   //
   if(ps1->Attrs().test(PosixSignal::Exit))
   {
      if(priv_->action_ == RunThread)
      {
         priv_->action_ = SleepThread;
         Unblock();
         priv_->action_ = ExitThread;
      }
   }

   SetSignal(sig);
   if(!ps1->Attrs().test(PosixSignal::Delayed)) SetTrap(true);
   if(ps1->Attrs().test(PosixSignal::Interrupt)) Interrupt();
}

Given that the target thread can throw a SignalException for itself, via a check supported by Debug::ft, Raise does the following:

假定目标线程可以通过Debug::ft支持的检查为其自身抛出SignalException ,那么Raise执行以下操作:

  • invokes SetSignal to record the signal against the thread

    调用SetSignal以针对线程记录信号

  • invokes Unblock (a virtual function) to unblock the thread if the signal will force it to exit

    如果信号迫使线程退出,则调用Unblock ( virtual函数)来解除线程的阻塞

  • invokes SetTrap if the signal should be delivered as soon as possible instead of waiting until the next time the thread yields (this sets the flag that is checked via Debug::ft)

    如果应该尽快传递信号,则调用SetTrap ,而不是等到下次线程产生时才调用SetTrap (这将设置通过Debug::ft检查的标志)

  • invokes Interrupt to wake up the thread if the signal should be delivered now instead of waiting until the thread resumes execution

    如果信号应该立即传递,则调用Interrupt来唤醒线程,而不是等待直到线程恢复执行

In the above list, whether to invoke each of the last three functions is determined by various attributes that can be set in the signal's instance of PosixSignal.

在上面的列表中,是否调用后三个功能中的每个功能取决于可以在信号实例PosixSignal设置的各种属性。

发生异常时捕获线程的堆栈 (Capturing a Thread's Stack When an Exception Occurs)

SignalException derives from Exception (which derives from std::exception). Although Exception is a virtual class, all RSC exceptions derive from it because its constructor captures the running thread's stack by invoking SysThreadStack::Display:

SignalExceptionException派生(从std::exception派生)。 尽管Exception是一个虚拟类,但所有RSC异常都派生自该类,因为Exception的构造函数通过调用SysThreadStack::Display捕获运行线程的堆栈:

Exception::Exception(bool stack, fn_depth depth) : stack_(nullptr)
{
   //  When capturing the stack, exclude this constructor and those of
   //  our subclasses.
   //
   if(stack)
   {
      stack_.reset(new std::ostringstream);
      if(stack_ == nullptr) return;
      *stack_ << std::boolalpha << std::nouppercase;
      SysThreadStack::Display(*stack_, depth + 1);
   }
}

SignalException simply records the signal and a debug code after telling Exception to capture the stack:

SignalException告诉Exception捕获堆栈后,仅记录信号和调试代码:

SignalException::SignalException(signal_t sig, debug32_t errval) :
   Exception(true, 1),
   signal_(sig),
   errval_(errval)
{
}

Capturing a thread stack is platform-specific. See SysThreadStack.win.cpp for the Windows targets. Here is an example of its output within an RSC log for a Windows structured exception that got mapped to SIGSEGV. The stack trace is the portion after "Function Traceback":

捕获线程堆栈是特定于平台的。 有关Windows目标,请参见SysThreadStack.win.cpp 。 这是在Windows构造的异常的RSC日志中其输出的示例,该异常已映射到SIGSEGV 。 堆栈跟踪是“功能跟踪”之后的部分:

THR902 2-Aug-2019 09:45:43.183 on Reigi {5}
in NodeTools.RecoveryTestThread (tid=15, nid=0x0000bc60): trap number 2
type=Signal
signal : 11 (SIGSEGV: Invalid Memory Reference)
errval : 0xc0000005
Function Traceback:
  NodeBase.SignalException.SignalException @ signalexception.cpp + 40[12]
  NodeBase.Thread.HandleSignal @ thread.cpp + 1801[16]
  NodeBase.SE_Handler @ systhread.win.cpp + 126[13]
  _unDNameEx @ <unknown file> (err=487)
  is_exception_typeof @ <unknown file> (err=487)
  is_exception_typeof @ <unknown file> (err=487)
  is_exception_typeof @ <unknown file> (err=487)
  _CxxFrameHandler3 @ <unknown file> (err=487)
  RtlInterlockedCompareExchange64 @ <unknown file> (err=487)
  RtlInterlockedCompareExchange64 @ <unknown file> (err=487)
  KiUserExceptionDispatcher @ <unknown file> (err=487)
  NodeTools.RecoveryTestThread.Enter @ ntincrement.cpp + 3584[0]
  NodeBase.Thread.Start @ thread.cpp + 2799[15]
  NodeBase.Thread.EnterThread @ thread.cpp + 1564[0]
  BaseThreadInitThunk @ <unknown file> (err=487)
  RtlGetAppContainerNamedObjectPath @ <unknown file> (err=487)
  RtlGetAppContainerNamedObjectPath @ <unknown file> (err=487)


In released software, users can collect these logs and send them to you. Better still, your software can include code to automatically send them to you over the internet. Each of these logs highlights a bug that needs to be fixed.

在发布的软件中,用户可以收集这些日志并将其发送给您。 更好的是,您的软件可以包含代码,以通过Internet自动将其发送给您。 这些日志中的每一个都突出显示了一个需要修复的错误。

从异常中恢复 (Recovering from an Exception)

The above log was produced by TrapHandler, which was mentioned a long time ago as the function that Thread::Start invokes when it catches an exception:

上面的日志是TrapHandler生成的,它是很久以前提到的,它是Thread::Start在捕获异常时调用的函数:

Thread::TrapAction Thread::TrapHandler(const Exception* ex,
   const std::exception* e, signal_t sig, const std::ostringstream* stack)
{
   try
   {
      //  If this thread object was deleted, exit immediately.
      //
      if(sig == SIGDELETED)
      {
         return Return;
      }

      if(Singleton< Threads >::Instance()->GetState() != Constructed)
      {
         return Return;
      }

      //  The first time in, save the signal.  After that, we're dealing
      //  with a trap during trap recovery:
      //  o On the second trap, log it and force the thread to exit.
      //  o On the third trap, force the thread to exit.
      //  o On the fourth trap, exit without even deleting the thread.
      //    This will leak its memory, which is better than what seems
      //    to be an infinite loop.
      //
      auto retrapped = false;

      switch(++priv_->traps_)
      {
      case 1:
         SetSignal(sig);
         break;
      case 2:
         retrapped = true;
         break;
      case 3:
         return Release;
      default:
         return Return;
      }

      //  Record a stack overflow against the native thread wrapper
      //  for use by SysThread::Start.
      //
      if((sig == SIGSTACK1) && (systhrd_ != nullptr))
      {
         systhrd_->status_.set(SysThread::StackOverflowed);
      }

      auto exceeded = LogTrap(ex, e, sig, stack);

      //  Force the thread to exit if
      //  o it has trapped too many times
      //  o it trapped during trap recovery
      //  o this is a final signal
      // 
      auto sigAttrs = Singleton< PosixSignalRegistry >::Instance()->Attrs(sig);

      if(exceeded | retrapped | sigAttrs.test(PosixSignal::Final))
      {
         return Release;
      }

      //  Resume execution at the top of Start.
      //
      return Continue;
   }

   //  The following catch an exception during trap recovery (a nested
   //  exception) and invoke this function recursively to handle it.
   //
   catch(SignalException& sex)
   {
      switch(TrapHandler(&sex, &sex, sex.GetSignal(), sex.Stack()))
      {
      case Continue:
      case Release:
         return Release;
      case Return:
      default:
         return Return;
      }
   }
   catch(Exception& ex)
   {
      switch(TrapHandler(&ex, &ex, SIGNIL, ex.Stack()))
      {
      case Continue:
      case Release:
         return Release;
      case Return:
      default:
         return Return;
      }
   }
   catch(std::exception& e)
   {
      switch(TrapHandler(nullptr, &e, SIGNIL, nullptr))
      {
      case Continue:
      case Release:
         return Release;
      case Return:
      default:
         return Return;
      }
   }
   catch(...)
   {
      switch(TrapHandler(nullptr, nullptr, SIGNIL, nullptr))
      {
      case Continue:
      case Release:
         return Release;
      case Return:
      default:
         return Return;
      }
   }
}

行动准则的痕迹 (Traces of the Code in Action)

RSC has 27 tests that focus on exercising this software. Each of them does something nasty to see if the software can handle it without exiting. During these tests, the function trace tool is enabled so that Debug::ft will record all function calls. For the SIGSEGV test, which is associated with the log shown above, the output of the trace tool looks like this. When the tool is on, code slows down by a factor of about 4x. When the tool is off, calls to Debug::ft incur very little overhead.

RSC有27个针对该软件的测试。 他们每个人都在做一些令人讨厌的事情,以查看该软件是否可以在不退出的情况下进行处理。 在这些测试过程中,将启用功能跟踪工具,以便Debug::ft将记录所有功能调用。 对于SIGSEGV试验,这是与上述情况,跟踪工具的样子的输出中所示的相关联的日志 。 启用该工具后,代码的速度将降低约4倍。 关闭工具后,对Debug::ft调用几乎不会产生任何开销。

析构函数使用错误指针 (A Destructor Uses a Bad Pointer)

The most recently added test uses a bad pointer in the destructor of a concrete Thread subclass. This test should have been added long ago; it is an especially good one because an exception in a destructor normally causes a program to abort. Although RSC survives, what occurs is interesting, at least under Windows. The structured exception (Windows' SIGSEGV equivalent) gets intercepted and thrown as a C++ exception. But this exception is not caught immediately. The C++ runtime code handling the deletion catches the exception itself and continues its work of invoking the destructor chain. This is admirable because it allows the base Thread class to release its resources. Only afterwards does the C++ runtime rethrow the exception, which is finally caught by the safety net in Thread::Start. We now have the unusual situation of a member function running after its object has been deleted. Because Thread::TrapHandler is not virtual, it gets invoked successfully. When it notices that the thread has been deleted, it returns and exits the thread.

最近添加的测试在具体的Thread子类的析构函数中使用了错误的指针。 该测试应该早就添加了; 这是一个特别好的程序,因为析构函数中的异常通常会导致程序中止。 尽管RSC仍然存在,但至少在Windows下,发生的事情很有趣。 结构化异常(相当于Windows的SIGSEGV )被拦截并作为C ++异常抛出。 但是不会立即捕获此异常。 处理删除的C ++运行时代码将捕获异常本身,并继续其调用析构函数链的工作。 这是令人钦佩的,因为它允许基础Thread类释放其资源。 之后 ,C ++运行时才重新抛出异常,该异常最终被Thread::Start的安全网捕获。 现在,我们遇到了一种异常情况,即成员函数删除其对象后便开始运行。 因为Thread::TrapHandler不是virtual ,所以它被成功调用。 当发现线程已被删除时,它将返回并退出该线程。

兴趣点 (Points of Interest)

It is only forthright to mention that the C++ standard does not support throwing an exception in response to a POSIX signal. In fact, it is undefined behavior for a signal handler to do almost anything in a C++ environment! A list of undefined behaviors appears here; those pertaining to signal handling are numbered 128 through 135. The detailed coding standard available on the same website makes these recommendations about signals:

只能直截了当地提到C ++标准不支持响应POSIX信号而引发异常。 实际上,信号处理程序在C ++环境中几乎可以执行任何操作都是未定义的行为 ! 未定义行为的列表出现在这里; 与信号处理相关的编号从128到135。同一网站上提供的详细编码标准对信号提出了以下建议:

  • SIG31-C. Do not access shared objects in signal handlers

    SIG31-C。 不要访问信号处理程序中的共享对象
  • SIG34-C. Do not call signal() from within interruptible signal handlers

    SIG34-C。 不要从可中断信号处理程序中调用signal()

  • SIG35-C. Do not return from a computational exception signal handler

    SIG35-C。 不要从计算异常信号处理程序返回

Fortunately, much of this is theoretical rather than practical. The main reason that most things related to signal handling are undefined behavior is because different platforms support signals in different ways. Many of the risks that lead to undefined behavior result from race conditions that will rarely occur2. Regardless, what can you do if your software has to be robust? It's far better to risk undefined behavior than to let your program exit.

幸运的是,其中大部分是理论上的,而不是实践上的。 大多数与信号处理相关的事情都是未定义行为的主要原因是因为不同的平台以不同的方式支持信号。 导致行为不确定的许多风险是由很少发生的竞赛条件引起的2 。 无论如何,如果您的软件必须强大,该怎么办? 冒着未定义行为的风险要比让程序退出要好得多。

The same rationale, of not being able to depend on how the underlying platform does something, does not excuse the standard's adoption of noexcept. If it were possible to throw an exception in reponse to a signal, any noexcept function would be unable to do so. Even a non-virtual "getter" that simply returns a member's value is now at risk. If such a function is invoked with a bad this pointer, it will add an offset to that pointer and try to read memory. Boom! An ostensibly trivial noexcept function, through no fault of its own, has now caused the invocation of abort when the signal handler throws an exception to recover from the SIGSEGV.

不能依赖于基础平台如何执行某些操作的相同理由也不能成为标准采用noexcept借口。 如果可能引发异常以响应信号,则任何noexcept函数都将无法执行此操作。 即使是仅返回成员值的非virtual “ getter”现在也处于危险之中。 如果使用错误的this指针调用了这样的函数,它将this指针添加一个偏移量并尝试读取内存。 繁荣! 一个显然是微不足道的noexcept功能,通过无过错的自己,现在已经造成的调用abort ,当信号处理程序抛出一个异常,从恢复SIGSEGV

The invocation of abort isn't the end of the world, let alone your program, because your signal handler can turn the SIGABRT into an exception. But now what are we dealing with, abort or an exception? What if the exception isn't "allowed", either because it occurred in a destructor or noexcept function? (Hands up, those of you who have never seen anything nasty happen in a destructor.)

abort的调用不是世界末日,更不用说程序了,因为信号处理程序可以将SIGABRT变成异常。 但是现在我们要处理什么, abort还是例外? 如果异常是因为它发生在析构函数或noexcept函数中而不被允许,该怎么办? (举起手来,从未见过任何令人讨厌的东西的人发生在析构函数中。)

When abort is invoked, the C++ standard says it is implementation dependent whether the stack is unwound in the same way as when an exception is thrown. That is, local objects may not get deleted. So if a function on the stack owns something in a unique_ptr local, it will leak. And if it has wrapped a mutex in a local object whose destructor releases the mutex whenever the function returns, the outcome could be far worse. This is assuming, of course, that your program will be allowed to survive. If it won't, it doesn't really matter.

调用abort ,C ++标准表示堆栈是否以与引发异常时相同的方式解卷取决于实现 。 也就是说,本地对象可能不会被删除。 因此,如果堆栈上的函数在unique_ptr本地拥有某些东西,它将泄漏。 而且,如果它已将互斥包包装在本地对象中,则只要函数返回,析构函数就会释放该互斥对象,则结果可能会更糟。 当然,这是假设您的程序将被保留。 如果不会,那并不重要。

Unless your software is shockingly infallible, it will occasionally cause an abort, and your C++ compiler better allow this to turn into an exception that unwinds the stack in all circumstances. In the end, both your platform and compiler will make it either possible or virtually impossible to deliver robust C++ software.

除非您的软件是绝对可靠的,否则它有时会导致abort ,而您的C ++编译器最好允许它变成异常,从而在所有情况下都释放堆栈。 最后,您的平台和编译器都将使交付健壮的C ++软件成为可能或几乎不可能

To summarize, here are some things that the C++ standard should mandate to get serious about robustness:

总而言之,C ++标准应该强制执行以下一些事项以认真考虑健壮性:

  • A signal handler must be able to throw an exception when it receives a signal.

    信号处理程序在接收到信号时必须能够引发异常。
  • The stack must be unwound if the signal handler throws an exception in reponse to a SIGABRT.

    如果信号处理程序响应SIGABRT引发异常,则必须解开堆栈。

  • std::exception's constructor must provide a way to capture debug information, such as a thread's stack, before the stack is unwound.

    std::exception的构造函数必须提供一种在展开堆栈之前捕获调试信息的方法,例如线程的堆栈。

The good news is that platform and compiler vendors often make it possible to deliver robust software, despite what the standard fails to mandate.

好消息是,尽管标准没有规定,平台和编译器供应商通常仍可以提供强大的软件。

笔记 (Notes)

1 Oops. Although part of POSIX, SIGBREAK is defined in Windows but not in <csignal>.

1糟糕。 尽管SIGBREAK是POSIX的一部分,但SIGBREAK是在Windows中定义的,但不是在<csignal>

2 In UNIX-like environments, signals other than those discussed in this article have sometimes been used as a primitive form of inter-thread communication. This greatly increases the risk of these race conditions and is not recommended here.

2在类似UNIX的环境中,本文中讨论的信号以外的其他信号有时被用作线程间通信的原始形式。 这大大增加了这些竞赛条件的风险,因此不建议在此使用。

翻译自: https://www.codeproject.com/Articles/5165710/Robust-Cplusplus-Safety-Net

可读性,健壮性,安全性

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值