健壮的 C++:初始化和重启
构建 main() 函数并从内存损坏中快速恢复
引言
在许多 C++ 程序中,main
函数 #include
了整个世界,完全缺乏结构。本文介绍了如何以结构化的方式初始化系统。然后讨论了如何通过快速重新初始化系统的一部分来演变设计,以支持从严重错误(通常是内存损坏)中恢复,而无需重新启动其可执行文件。
Using the Code
本文中的代码取自 Robust Services Core (RSC)。如果您是第一次阅读有关 RSC 某个方面文章,请花几分钟阅读这篇 序言。
初始化系统
我们将从了解 RSC 在系统启动时如何初始化开始。
模块
每个 Module
子类代表一组相互关联的源文件,提供某些逻辑功能。1 这些子类中的每一个都负责
- 实例化它所依赖的模块(在其构造函数中)
- 启用它所依赖的模块(在其
Enable
函数中) - 在可执行文件启动时初始化它所代表的源文件集(在其
Startup
函数中)
每个 Module
子类目前与一个静态库一一对应。这效果很好,因此不太可能改变。静态库之间的依赖关系必须在构建可执行文件之前定义,因此很容易在模块之间应用相同的依赖关系。而且,由于没有哪个静态库非常大,每个模块都可以轻松地初始化其所属的静态库。
下面是一个典型模块的框架
class SomeModule : public Module
{
friend class Singleton<SomeModule>;
private:
SomeModule() : Module("sym") // symbol if a leaf module
{
// Modules 1 to N are the ones that this module requires.
// Creating their singletons ensures that they will exist in
// the module registry when the system initializes. Because
// each module creates the modules on which it depends before
// it adds itself to the registry, the registry will contain
// modules in the (partial) ordering of their dependencies.
//
Singleton<Module1>::Instance();
// ...
Singleton<ModuleN>::Instance();
Singleton<ModuleRegistry>::Instance()->BindModule(*this);
}
~SomeModule() = default;
void Enable() override
{
// Enable the modules that this one requires, followed by this
// module. This must be public if other modules require this
// one. The outline is similar to the constructor.
//
Singleton<Module1>::Instance()->Enable();
// ...
Singleton<ModuleN>::Instance()->Enable();
Module::Enable();
}
void Startup() override; // details are specific to each module
};
如果每个模块的构造函数都实例化了它所依赖的模块,那么叶子模块是如何创建的?答案是 main
创建了它们。main
的代码很快就会出现。
启用模块
模块框架的最新更改意味着实例化一个模块不再会导致调用其 Startup
函数。模块现在还必须先被启用,然后才能调用其 Startup
函数。
与模块的构造函数实例化其所需模块的方式相同,其 Enable
函数会启用这些模块,以及自身。这引出了一个类似的问题:叶子模块是如何启用的?答案是,当叶子模块调用基类 Module
构造函数时,它必须提供一个唯一标识它的符号。然后,可以通过将此符号包含在 配置参数 OptionalModules
中来启用叶子模块,该参数位于 配置文件 中。
为什么要添加一个单独的步骤来启用已经存在于构建中的模块?答案是,一个产品可能有很多可选的子系统,每个子系统由一个或多个模块支持。在某些情况下,产品可能部署为仅需要一组模块的特定角色。在其他情况下,产品可能承担多个角色,因此需要几组模块。为每种可能的角色组合创建唯一的构建是一项管理负担。可以通过交付一个包含所有角色的单一的、超集构建来避免这种负担。然后,可以通过使用 OptionalModules
配置参数启用实际需要的模块,来启用所需角色的子集。如果客户后来决定需要另一组角色,只需更新配置参数并重新启动系统即可实现。
例如,考虑互联网。IETF 定义了许多支持各种角色的应用层协议。在一个大型网络中,支持许多这些协议的产品可能被部署为一个专用的边界网关路由器。但在一个小网络中,该产品也可能充当 DNS 服务器、邮件服务器 (SMTP) 和呼叫服务器 (SIP)。
ModuleRegistry
上面构造函数最后一行出现的单例 ModuleRegistry
。它包含系统中所有模块,并按其依赖关系(偏序)排序。ModuleRegistry
还有一个 Startup
函数,通过调用每个已启用模块的 Startup
来初始化系统。
Thread、RootThread 和 InitThread
在 RSC 中,每个线程都派生自基类 Thread
,它封装了一个原生线程,并提供了与异常处理、调度和线程间通信等相关的功能。
RSC 创建的第一个线程是 RootThread
,它很快被 C++ 运行时系统创建的用于运行 main
的线程创建。RootThread
只是将系统提升到可以创建下一个线程的程度。那个线程 InitThread
负责初始化系统的大部分内容。初始化完成后,InitThread
充当看门狗,以确保线程正在被调度,而 RootThread
则充当看门狗,以确保 InitThread
正在运行。
main()
在回显并保存命令行参数后,main
只是实例化叶子模块。RSC 目前有 15 个静态库,因此有 15 个模块。通过这些模块的构造函数传递实例化的模块不需要由 main
实例化。
main_t main(int argc, char* argv[])
{
// Echo and save the arguments. Create the desired modules
// modules and finish initializing the system. MainArgs is
// a simple class that saves and provides access to the
// arguments.
//
std::cout << "ROBUST SERVICES CORE" << CRLF;
MainArgs::EchoAndSaveArgs(argc, argv);
CreateModules();
return RootThread::Main();
}
static void CreateModules()
{
Singleton<CtModule>::Instance();
Singleton<OnModule>::Instance();
Singleton<CnModule>::Instance();
Singleton<RnModule>::Instance();
Singleton<SnModule>::Instance();
Singleton<AnModule>::Instance();
Singleton<DipModule>::Instance();
}
一旦系统初始化完成,在 CLI 上输入 >modules
命令将显示以下内容,这是调用已启用模块来初始化其静态库的顺序。
如果基于 RSC 构建的应用程序不需要特定的静态库,可以通过注释掉其模块的实例化来排除该库的所有代码,链接器将从可执行文件中排除所有该库的代码。即使实例化了该库的模块,仍然可以通过从 OptionalModules
配置参数中排除其符号来禁用它。
main
是唯一在静态库之外实现的代码。它位于 rsc 目录中,该目录中唯一的源文件是 main.cpp。所有其他软件,无论是框架的一部分还是应用程序的一部分,都位于静态库中。
RootThread::Main
main
所做的最后一件事是调用 RootThread::Main
,这是一个 static
函数,因为 RootThread
还没有被实例化。它的任务是创建实际实例化 RootThread
所需的组件。
main_t RootThread::Main()
{
// Load symbol information.
//
SysStackTrace::Startup(RestartReboot);
// Create the POSIX signals. They are needed now so that
// RootThread can register for signals when it is created.
//
CreatePosixSignals();
// Create the log buffer, which is used to log the progress
// of initialization.
//
Singleton<LogBufferRegistry>::Instance();
// Set up our process.
//
SysThread::ConfigureProcess();
// Create ThreadRegistry. Thread::Start uses its GetState
// function to see when a Thread has been fully constructed
// and can safely proceed.
//
Singleton<ThreadRegistry>::Instance();
// Create the root thread and wait for it to exit.
//
Singleton<RootThread>::Instance();
ExitGate().WaitFor(TIMEOUT_NEVER);
// If we get here, RootThread wants the system to exit and
// possibly get rebooted.
//
Debug::Exiting();
exit(ExitCode);
}
创建 RootThread
单例会导致调用 RootThread::Enter
,它实现了 RootThread
的线程循环。RootThread::Enter
创建 InitThread
,其第一个任务是完成系统的初始化。然后 RootThread
进入睡眠状态,运行一个看门狗计时器,当 InitThread
中断 RootThread
告知其系统已初始化时,该计时器将被取消。如果计时器到期,则表示系统初始化失败:它“开箱即死”,因此 RootThread
退出,这将导致 RootThread::Main
调用 exit
。
ModuleRegistry::Startup
为了完成系统的初始化,InitThread
调用 ModuleRegistry::Startup
。此函数调用每个模块的 Startup
函数。它还记录了每个模块初始化花费的时间,为清晰起见已删除的代码。
void ModuleRegistry::Startup(RestartLevel level) // RestartLevel will be defined later
{
for(auto m = modules_.First(); m != nullptr; modules_.Next(m))
{
if(m->IsEnabled())
{
m->Startup(level);
}
}
}
该函数完成后,控制台上将出现类似如下的内容。
Module::Startup 函数
Module Startup
函数本身并不是特别有趣。RSC 的设计原则之一是,处理用户请求的对象应该在系统初始化期间创建,以便在系统投入服务后提供可预测的延迟。这是 NbModule
的 Startup
代码,它初始化了 NodeBase
命名空间。
void NbModule::Startup(RestartLevel level)
{
// Create/start singletons. Some of these already exist as a
// result of creating RootThread, but their Startup functions
// must be invoked.
//
Singleton<PosixSignalRegistry>::Instance()->Startup(level);
Singleton<LogBufferRegistry>::Instance()->Startup(level);
Singleton<ThreadRegistry>::Instance()->Startup(level);
Singleton<StatisticsRegistry>::Instance()->Startup(level);
Singleton<AlarmRegistry>::Instance()->Startup(level);
Singleton<LogGroupRegistry>::Instance()->Startup(level);
CreateNbLogs(level);
Singleton<CfgParmRegistry>::Instance()->Startup(level);
Singleton<DaemonRegistry>::Instance()->Startup(level);
Singleton<DeferredRegistry>::Instance()->Startup(level);
Singleton<ObjectPoolRegistry>::Instance()->Startup(level);
Singleton<ThreadAdmin>::Instance()->Startup(level);
Singleton<MsgBufferPool>::Instance()->Startup(level);
Singleton<ClassRegistry>::Instance()->Startup(level);
Singleton<Element>::Instance()->Startup(level);
Singleton<CliRegistry>::Instance()->Startup(level);
Singleton<SymbolRegistry>::Instance()->Startup(level);
Singleton<NbIncrement>::Instance()->Startup(level);
// Create/start threads.
//
Singleton<FileThread>::Instance()->Startup(level);
Singleton<CoutThread>::Instance()->Startup(level);
Singleton<CinThread>::Instance()->Startup(level);
Singleton<ObjectPoolAudit>::Instance()->Startup(level);
Singleton<StatisticsThread>::Instance()->Startup(level);
Singleton<LogThread>::Instance()->Startup(level);
Singleton<DeferredThread>::Instance()->Startup(level);
Singleton<CliThread>::Instance()->Startup(level);
// Enable optional modules.
//
Singleton<ModuleRegistry>::Instance()->EnableModules();
}
在返回之前,NbModule::Startup
会启用 OptionalModules
配置参数中的模块。它可以做到这一点,因为 NodeBase
是 RSC 的最低层,所以 NbModule::Startup
总是被调用。
重启系统
到目前为止,我们有了一个具有以下特征的初始化框架:
- 结构化和分层的初始化方法
- 简单的
main
,只需创建叶子模块 - 通过不实例化初始化它的模块,轻松地从构建中排除静态库
- 通过仅启用满足其角色所需的模块,轻松定制超集负载
现在我们将增强此框架,以便我们可以重新初始化系统以从严重错误中恢复。 健壮的 C++:安全网 描述了如何为单个线程执行此操作。但有时系统会进入某种状态,其中该文章中描述的错误类型会再次发生。在这种情况下,需要采取更严厉的行动。数据经常损坏,修复它将使系统恢复健康。系统的部分重新初始化,而不是完全重启,通常可以做到这一点。
如果我们可以分层初始化系统,我们也应该能够分层关闭它。我们可以定义 Shutdown
函数来补充我们已经看到的 Startup
函数。但是,我们只想执行部分关闭,然后进行部分启动以重新创建关闭阶段销毁的东西。如果我们能做到这一点,我们就实现了部分重新初始化。
但是,我们究竟应该销毁什么并重新创建什么?有些东西很容易重新创建。其他东西将花费更长的时间,在此期间系统将不可用。因此,最好采用灵活的策略。如果系统遇到麻烦,首先重新初始化可以快速重新创建的东西。如果这不能解决问题,则扩大重新初始化的范围,依此类推。最终,我们将不得不放弃并重新启动。
因此,我们的重启(重新初始化)策略会升级。RSC 支持三个重启级别,其范围小于完全重启。当系统遇到问题时,它会尝试通过启动具有最窄范围的重启来恢复。但如果很快再次遇到问题,它会增加下一次重启的范围。
- 温重启会销毁临时数据,并退出并尽可能多地重新创建线程。当前正在处理的任何用户请求都会丢失,必须重新提交。
- 冷重启还会销毁动态数据,即在处理用户请求时会发生变化的数据。例如,所有会话都会丢失,必须重新启动。
- 重载重启还会销毁相对静态的数据,例如用户请求很少修改的配置数据。这些数据通常从磁盘或网络加载,例如内存中的用户配置文件数据库,以及包含在服务器到客户端 HTTP 消息中的图像数据库。
因此,Startup
和 Shutdown
函数需要一个参数来指定正在发生哪种类型的重启。
enum RestartLevel
{
RestartNil, // in service (not restarting)
RestartWarm, // deleting MemTemporary and exiting threads
RestartCold, // warm + deleting MemDynamic & MemSlab (user sessions)
RestartReload, // cold + deleting MemPersistent & MemProtected (config data)
RestartReboot, // exiting and restarting executable
RestartExit, // exiting without restarting
RestartLevel_N // number of restart levels
};
发起重启
重启发生如下:
- 决定需要重启的代码调用
Restart::Initiate
。 Restart::Initiate
抛出ElementException
。Thread::Start
捕获ElementException
并调用InitThread::InitiateRestart
。InitThread::InitiateRestart
中断RootThread
告知它将要开始重启,然后中断自身以启动重启。- 当
InitThread
被中断时,它调用ModuleRegistry::Restart
来管理重启。此函数包含一个状态机,通过调用ModuleRegistry::Shutdown
(如下所述)和ModuleRegistry::Startup
(已描述)来逐步完成关闭和启动阶段。 - 当
RootThread
被中断时,它会启动一个看门狗计时器。重启完成后,InitThread
中断RootThread
,后者取消计时器。如果计时器到期,RootThread
会强制InitThread
退出并重新创建它。当InitThread
重新进入时,它会再次调用ModuleRegistry::Restart
,这将把重启升级到下一个级别。
重启期间删除对象
由于重启的目标是尽快重新初始化系统的一部分,因此 RSC 采取了激进的方法。它不是逐个删除对象,而是简单地释放它们分配的堆。例如,在拥有数万个会话的系统中,这大大缩短了冷重启所需的时间。缺点是它增加了一些复杂性,因为每种类型的内存都需要自己的堆。
MemoryType | 基类 | 属性 |
MemTemporary | 临时 | 不会在任何重启中幸存 |
MemDynamic | 动态 | 在温重启后幸存,但在冷重启或重载重启后不幸存 |
MemSlab | 池化 | 在温重启后幸存,但在冷重启或重载重启后不幸存 |
MemPersistent | 持久 | 在温重启和冷重启后幸存,但在重载重启后不幸存 |
MemProtected | Protected | 写保护;在温重启和冷重启后幸存,但在重载重启后不幸存 |
MemPermanent | 永久 | 在所有重启后幸存(这是 C++ 默认堆的包装器) |
MemImmutable | 不可变 | 写保护;在所有重启后幸存(类似于 C++ 全局 const 数据) |
要使用给定的 MemoryType
,一个类派生自 **基类** 列中的相应类。其工作原理稍后将进行描述。
Module::Shutdown 函数
模块的 Shutdown
函数与其 Startup
函数非常相似。它对静态库中的对象调用 Shutdown
,但顺序与调用其 Startup
函数的顺序相反。这是 NbModule
的 Shutdown
函数,它(或多或少)是之前出现的其 Startup
函数的镜像。
void NbModule::Shutdown(RestartLevel level)
{
Singleton<NbIncrement>::Instance()->Shutdown(level);
Singleton<SymbolRegistry>::Instance()->Shutdown(level);
Singleton<CliRegistry>::Instance()->Shutdown(level);
Singleton<Element>::Instance()->Shutdown(level);
Singleton<ClassRegistry>::Instance()->Shutdown(level);
Singleton<ThreadAdmin>::Instance()->Shutdown(level);
Singleton<ObjectPoolRegistry>::Instance()->Shutdown(level);
Singleton<DeferredRegistry>::Instance()->Shutdown(level);
Singleton<DaemonRegistry>::Instance()->Shutdown(level);
Singleton<CfgParmRegistry>::Instance()->Shutdown(level);
Singleton<LogGroupRegistry>::Instance()->Shutdown(level);
Singleton<AlarmRegistry>::Instance()->Shutdown(level);
Singleton<StatisticsRegistry>::Instance()->Shutdown(level);
Singleton<ThreadRegistry>::Instance()->Shutdown(level);
Singleton<LogBufferRegistry>::Instance()->Shutdown(level);
Singleton<PosixSignalRegistry>::Instance()->Shutdown(level);
Singleton<TraceBuffer>::Instance()->Shutdown(level);
SysThreadStack::Shutdown(level);
Memory::Shutdown();
Singletons::Instance()->Shutdown(level);
}
鉴于重启会释放一个或多个堆,而不是期望这些堆上的对象被删除,那么 Shutdown
函数的目的是什么?答案是,在重启中幸存的对象可能持有指向将被销毁或重新创建的对象的指针。它的 Shutdown
需要清除这些指针。
NbModule
的 Startup
函数创建了多个线程,那么为什么它的 Shutdown
函数不关闭它们呢?原因是 ModuleRegistry::Shutdown
在重启的早期处理了这个问题。
ModuleRegistry::Shutdown
此函数首先允许一部分线程运行一段时间,以便它们可以生成任何待处理的日志。然后,它通知所有线程重启,计算有多少线程愿意退出,然后调度它们直到它们退出。最后,它按照与其 Startup
函数调用相反的顺序关闭所有模块。与 ModuleRegistry::Startup
一样,记录重启进度的代码已删除以保持清晰。
void ModuleRegistry::Shutdown(RestartLevel level)
{
if(level >= RestartReload)
{
Memory::Unprotect(MemProtected);
}
msecs_t delay(25);
// Schedule a subset of the factions so that pending logs will be output.
//
Thread::EnableFactions(ShutdownFactions());
for(size_t tries = 120, idle = 0; (tries > 0) && (idle <= 8); --tries)
{
ThisThread::Pause(delay);
if(Thread::SwitchContext() != nullptr)
idle = 0;
else
++idle;
}
Thread::EnableFactions(NoFactions);
// Notify all threads of the restart.
//
auto reg = Singleton<ThreadRegistry>::Instance();
auto exiting = reg->Restarting(level);
auto target = exiting.size();
// Signal the threads that will exit and schedule threads until the planned
// number have exited. If some fail to exit, RootThread will time out and
// escalate the restart.
//
for(auto t = exiting.cbegin(); t != exiting.cend(); ++t)
{
(*t)->Raise(SIGCLOSE);
}
Thread::EnableFactions(AllFactions());
{
for(auto prev = exiting.size(); prev > 0; prev = exiting.size())
{
Thread::SwitchContext();
ThisThread::Pause(delay);
reg->TrimThreads(exiting);
if(prev == exiting.size())
{
// No thread exited while we were paused. Resignal the remaining
// threads. This is similar to code in InitThread.HandleTimeout
// and Thread.SwitchContext, where a thread occasionally misses
// its Proceed() and must be resignalled.
//
for(auto t = exiting.cbegin(); t != exiting.cend(); ++t)
{
(*t)->Raise(SIGCLOSE);
}
}
}
}
Thread::EnableFactions(NoFactions);
// Modules must be shut down in reverse order of their initialization.
//
for(auto m = modules_.Last(); m != nullptr; modules_.Prev(m))
{
if(m->IsEnabled())
{
m->Shutdown(level);
}
}
}
关闭线程
ModuleRegistry::Shutdown
(通过 ThreadRegistry
)调用 Thread::Restarting
来检查线程是否愿意在重启期间退出。该函数反过来调用 virtual
函数 ExitOnRestart
。
bool Thread::Restarting(RestartLevel level)
{
// If the thread is willing to exit, ModuleRegistry.Shutdown will
// momentarily signal it and schedule it so that it can exit.
//
if(ExitOnRestart(level)) return true;
// Unless this is RootThread or InitThread, mark it as a survivor. This
// causes various functions to force it to sleep until the restart ends.
//
if(faction_ < SystemFaction) priv_->action_ = SleepThread;
return false;
}
ExitOnRestart
的默认实现是
bool Thread::ExitOnRestart(RestartLevel level) const
{
// RootThread and InitThread run during a restart. A thread blocked on
// stream input, such as CinThread, cannot be forced to exit because C++
// has no mechanism for interrupting it.
//
if(faction_ >= SystemFaction) return false;
if(priv_->blocked_ == BlockedOnStream) return false;
return true;
}
愿意退出的线程会收到 SIGCLOSE
信号。在发送此信号之前,Thread::Raise
会调用线程上的 virtual
函数 Unblock
,以防它当前被阻塞。例如,每个 UdpIoThread
实例都在 IP 端口上接收 UDP 数据包。由于待处理的用户请求应在温重启后幸存,UdpIoThread
会重写 ExitOnRestart
以在温重启期间返回 false
。在其他类型的重启期间,它返回 true
,并且它对 Unblock
的重写会向其套接字发送一条消息,以便其对 recvfrom
的调用立即返回,从而允许其退出。
支持内存类型
本节讨论支持 MemoryType
所需的内容,每种内存类型都有其自己的持久性和保护特性。
堆
每种 MemoryType
都需要自己的堆,以便在适当类型的重启期间通过简单地释放该堆来批量删除其所有对象。默认堆是平台特定的,因此 RSC 定义了 SysHeap
来包装它。虽然此堆永远不会被释放,但包装它允许跟踪派生自 Permanent
的对象所占用的内存。
为了在 Windows 上支持写保护内存,RSC 必须实现自己的堆,因为 Windows 提供的自定义堆出于某种未公开的原因,如果被写保护,很快就会失败。因此,现在有一个基类 Heap
,以及三个子类。
SysHeap
,已提及,它包装了默认的 C++ 堆并支持MemPermanent
。BuddyHeap
,它的一个实例支持除MemPermanent
和MemSlab
之外的所有内容。每个都是固定大小的堆(尽管大小可配置),它使用伙伴分配实现,并且可以被写保护。SlabHeap
,它支持MemSlab
。这是一个可扩展堆,用于那些很少(如果曾经)在分配内存后释放内存的应用程序。对象池使用此堆,以便它们可以增长以处理高于预期的工作负载。
接口 Memory.h 用于分配和释放各种类型的内存。其主要函数类似于 malloc
和 free
,而各种堆是 Memory.cpp 的私有内容。
// Allocates a memory segment of SIZE of the specified TYPE. The
// first version throws an AllocationException on failure, whereas
// the second version returns nullptr.
//
void* Alloc(size_t size, MemoryType type);
void* Alloc(size_t size, MemoryType type, std::nothrow_t&);
// Deallocates the memory segment returned by Alloc.
//
void Free(void* addr, MemoryType type);
基类
可以动态分配对象的类派生自前面提到的类之一,例如 Dynamic
。如果不是这样,它的对象将从默认堆分配,这等同于派生自 Permanent
。
支持各种内存类型的基类简单地重写 operator
new
和 operator
delete
以使用适当的堆。例如:
void* Dynamic::operator new(size_t size)
{
return Memory::Alloc(size, MemDynamic);
}
void* Dynamic::operator new[](size_t size)
{
return Memory::Alloc(size, MemDynamic);
}
void Dynamic::operator delete(void* addr)
{
Memory::Free(addr, MemDynamic);
}
void Dynamic::operator delete[](void* addr)
{
Memory::Free(addr, MemDynamic);
}
分配器
一个具有 std::string
成员的类希望该字符串从用于该类对象的同一堆中分配内存。如果字符串改为从默认堆分配内存,则在对象的堆被释放时,重启将导致内存泄漏。尽管重启会释放字符串对象本身使用的内存,但其析构函数不会被调用,因此它分配的用于保存其字符的内存将泄漏。
因此,RSC 为每种 MemoryType
提供了一个 C++ 分配器,以便在非默认堆上分配对象的类可以使用标准库中的类。这些分配器定义在 Allocators.h 中,并用于定义从所需堆分配内存的 STL 类。例如:
typedef std::char_traits<char> CharTraits;
typedef std::basic_string<char, CharTraits, DynamicAllocator<char>> DynamicStr;
然后,派生自 Dynamic
的类使用 DynamicStr
来声明本来是 std::string
成员的内容。
写保护数据
内存类型 表 指出 MemProtected
是写保护的。这样做的理由是,只有在重载重启期间才会被删除的数据,因为它们必须从磁盘或网络加载,所以重新创建它们成本很高。这些数据也比其他数据改变得少得多。因此,保护它们免受踩踏是明智的,但并非成本高昂。
在系统初始化期间,MemProtected
是未受保护的。在开始处理用户请求之前,系统会将 MemProtected
写保护。应用程序必须显式地解除保护并重新保护它,以便修改从其堆分配的内存的数据。只有在重载重启期间,它才会再次解除保护,同时重新创建这些数据。
出于同样的原因,还定义了另一种写保护内存 MemImmutable
。它包含不应更改的关键数据,例如 Module
子类和 ModuleRegistry
。一旦系统初始化完成,它就被永久写保护,以防止被踩踏。
当系统运行时,在修改受保护内存之前必须解除对其的保护。忘记这样做会导致一个与坏指针引起的异常几乎相同的异常。由于这些异常的根本原因截然不同,RSC 使用专有的 POSIX 信号 SIGWRITE
来区分它们,而不是通常表示坏指针的 SIGSEGV
,以表示写入受保护内存。
修改受保护内存(例如,插入新的订阅者配置文件)后,必须立即重新保护。堆栈对象 FunctionGuard
用于此目的。它的构造函数会解除内存保护,当它离开作用域时,它的析构函数会自动重新保护它。
FunctionGuard guard(Guard_MemUnprotect);
// change data located in MemProtected
return; // MemProtected is automatically reprotected
还有一个使用频率远低于 FunctionGuard
的 Guard_ImmUnprotect
,用于修改 MemImmutable
。FunctionGuard
构造函数调用一个私有的 Thread
函数,该函数最终会解除所讨论内存的保护。该函数由 Thread
定义,因为每个线程都有一个用于 MemProtected
和 MemImmutable
的取消保护计数器。这使得取消保护事件可以嵌套,并在线程被调度进来时恢复其当前的内存保护属性。
设计一个混合内存类型的类
并非所有类都能满足于使用单一的 MemoryType
。例如,RSC 的配置参数派生自 Protected
,但其统计数据派生自 Dynamic
。有些类希望包含同时支持这些功能的成员。
另一个例子是订阅者配置文件,它通常派生自 Protected
。但它也可能跟踪订阅者的状态,这些状态变化太频繁,无法放入写保护内存,因此会放在配置文件之外,也许放在 Persistent
内存中。
以下是设计具有混合内存类型的类的指南:
- 如果一个类直接嵌入另一个类,而不是通过指针分配它,那么该类将与所有者位于同一
MemoryType
中。但是,如果嵌入的类分配自己的内存,它必须使用与所有者相同的MemoryType
。这之前已经讨论过,与 字符串 相关。 - 如果一个类希望写保护其大部分数据,但也包含一些变化太频繁的数据,它应该使用 PIMPL idiom 将其更动态的数据分配到一个
struct
中,该struct
通常具有相同的持久性。也就是说,派生自Protected
的类将其动态数据放入派生自Persistent
的struct
中,而派生自Immutable
的类将其动态数据放入派生自Permanent
的struct
中。这样,主类及其关联的动态数据要么在重启中幸存,要么一起被销毁。2 - 如果一个类需要包含一个具有不同持久性的类,它应该通过
unique_ptr
来管理它,并重写前面讨论的Shutdown
和Startup
函数。- 如果该类拥有一个持久性较低的对象,其
Shutdown
函数会调用unique_ptr::release
来清除指向该对象的指针(如果重启将销毁它)。当其Startup
函数注意到nullptr
时,它会重新分配该对象。 - 如果该类拥有一个持久性较高的对象,其
Shutdown
函数可能会调用unique_ptr::reset
以防止在销毁所有者的重启期间发生内存泄漏。但如果它能找到该对象,则无需执行任何操作。在重启的启动阶段重新创建它时,其构造函数不得盲目创建持久性较高的对象。相反,它必须首先尝试找到它,通常是在此类对象的注册表中。这是更可能的情况;该对象被设计为在重启中幸存,因此应该允许它这样做。
- 如果该类拥有一个持久性较低的对象,其
编写 Shutdown 和 Startup 函数
许多 Shutdown
和 Startup
函数会使用几个函数。Base::MemType
返回一个类使用的内存类型,而 Restart::ClearsMemory
和 Restart::Release
使用它的结果。
// Types of memory (defined in SysTypes.h).
//
enum MemoryType
{
MemNull, // nil value
MemTemporary, // does not survive restarts
MemDynamic, // survives warm restarts
MemSlab, // survives warm restarts
MemPersistent, // survives warm and cold restarts
MemProtected, // survives warm and cold restarts; write-protected
MemPermanent, // survives all restarts (default process heap)
MemImmutable, // survives all restarts; write-protected
MemoryType_N // number of memory types
};
// Returns the type of memory used by the object (overridden by
// Temporary, Dynamic, Persistent, Protected, Permanent, and Immutable).
//
virtual MemoryType MemType() const;
// Returns true if the heap for memory of TYPE will be freed and
// reallocated during any restart that is currently in progress.
//
static bool ClearsMemory(MemoryType type);
// Invokes obj.release() and returns true if OBJ's heap will be freed
// during any restart that is currently in progress.
//
template<class T> static bool Release(std::unique_ptr< T >& obj)
{
auto type = (obj == nullptr ? MemNull : obj->MemType());
if(!ClearsMemory(type)) return false;
obj.release();
return true;
}
自动重启
如果温、冷、重载重启序列未能使系统恢复正常,则重启将升级到 RestartReboot
。为了支持自动重启,RSC 必须使用简单的 Launcher 应用程序启动,其源代码位于 launcher 目录中。RSC 的构建会生成 rsc.exe 和 launcher.exe。
当 Launcher 启动时,它只是请求包含它将作为子进程创建的 rsc.exe 的目录,以及 rsc.exe 的任何额外命令行参数。然后它启动 rsc.exe 并进入睡眠状态,等待它退出。要启动重启,RSC 以非零退出码退出,这会导致 Launcher 立即重新创建它。
当使用 Launcher 启动 RSC 时,CLI 命令 >restart
exit
必须用于正常关闭 RSC。它会导致 RSC 以退出码 0
退出,这将阻止 Launcher 立即重新创建它。
代码运行跟踪
RSC 的 output 目录包含控制台记录(*.console.txt)、日志文件(*.log.txt)和函数跟踪(*.trace.txt),内容如下:
- 系统初始化,在文件 init.* 中
- 一次温重启,在文件 warm* 中(warm1.* 和 warm2.* 分别是重启前和重启后)
- 一次冷重启,在文件 cold* 中(cold1.* 和 cold2.* 分别是重启前和重启后)
- 一次重载重启,在文件 reload* 中(reload1.* 和 reload2.* 分别是重启前和重启后)
重启是使用 CLI 的 >restart
命令发起的。
注释
1 本文中使用的术语模块与 C++20 中引入的模块无关。这个术语不会因为 C++ 后来也开始使用它而改变。
2 RSC 在多个地方以这种方式使用 PIMPL idiom:只需查找任何名为 dyn_
的成员。
历史
- 2022 年 9 月 20 日:更新以反映对选择性启用模块的支持。
- 2022 年 3 月 29 日:添加了关于自动重启的章节。
- 2020 年 5 月 4 日:更新以反映对重载重启和写保护内存的支持;添加了关于设计混合内存类型类的章节。
- 2019 年 12 月 23 日:初始版本