【问题标题】:Programmatically getting system boot up time in c++ (windows)以编程方式在 C++(Windows)中获取系统启动时间
【发布时间】:2023-04-11 00:25:02
【问题描述】:

很简单,问题是如何使用 c/c++ 在 windows 中获得系统启动时间。

搜索这个并没有给我任何答案,我只找到了一个非常老套的方法,它正在读取文件时间戳(不用说,我中途放弃了阅读)。

我发现的另一种方法实际上是读取 Windows 诊断记录的事件?据说这是最后一次启动时间。

有谁知道如何做到这一点(希望不会有太多丑陋的黑客行为)?

【问题讨论】:

    标签: c++ c windows


    【解决方案1】:

    GetTickCount64 "检索自系统启动以来经过的毫秒数。"

    一旦您知道系统运行了多长时间,只需从当前时间中减去该持续时间即可确定系统何时启动。例如,使用 C++11 chrono 库(Visual C++ 2012 支持):

    auto uptime = std::chrono::milliseconds(GetTickCount64());
    auto boot_time = std::chrono::system_clock::now() - uptime;
    

    【讨论】:

    • 您需要将其与 GetSystemTime 结合起来,并进行一些日期数学运算以获得问题的完整答案。
    • 对不起,如果我不清楚,但我指的是实际时间,而不是多少年前。 IE。 : "系统于 2012/06/01 10:00 启动"
    • 我不会认为 减法 是一种 hack。
    • 我称之为 programming,因为这就是它的大部分内容 - 从你拥有的数据中合成你需要的数据......
    • 这是否每次都能可靠地产生相同的结果?如果system_clock::now 的滴答声与GetTickCount64 不完全同步,或者滴答声发生在两个执行的代码行之间怎么办?我需要一些可以用作查找键的东西。
    【解决方案2】:

    您也可以使用WMI 来获取准确的启动时间。 WMI 不适合胆小的人,但它会为您提供所需的内容。

    相关信息位于LastBootUpTime 属性下的Win32_OperatingSystem 对象上。您可以使用WMI Tools 检查其他属性。

    编辑: 如果您愿意,也可以从命令行获取此信息。

    wmic OS Get LastBootUpTime
    

    作为 C# 中的示例,它将如下所示 (Using C++ it is rather verbose):

    static void Main(string[] args)
    {      
        // Create a query for OS objects
        SelectQuery query = new SelectQuery("Win32_OperatingSystem", "Status=\"OK\"");
    
        // Initialize an object searcher with this query
        ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
    
        string dtString;
        // Get the resulting collection and loop through it
        foreach (ManagementObject envVar in searcher.Get())
            dtString = envVar["LastBootUpTime"].ToString();
    }
    

    【讨论】:

    • 是否可以获得最后的“启动类型”(表示从关机或休眠状态启动)?
    • 命令行wmic OS Get LastBootUpTime 似乎返回了PC 首次启动的时间......wmic LOGON Get StartTime 可能更有用
    【解决方案3】:

    “系统”对象上的“系统运行时间”性能计数器是另一个来源。它可以使用PDH Helper methods 以编程方式使用。但是,它对睡眠/休眠周期并不稳健,因此可能并不比 GetTickCount()/GetTickCount64() 好多少。

    读取计数器会返回一个 64 位 FILETIME 值,即自 Windows Epoch (1601-01-01 00:00:00 UTC) 以来的 100-NS 滴答数。您还可以通过读取 WMI 表来查看计数器返回的值,该表公开了用于计算此值的原始值。 (使用 COM 以编程方式读取,或从 wmic 获取命令行:)

    wmic path Win32_PerfRawData_PerfOS_System  get systemuptime
    

    该查询为我生成 132558992761256000,corresponding to Saturday, January 23, 2021 6:14:36 PM UTC

    您可以使用等效的PerfFormattedData 来获取浮点秒数,或者从wmic 中的命令行读取它,或者在PowerShell 中查询计数器:

    Get-Counter -Counter '\system\system up time'
    

    这将返回 427.0152 秒的正常运行时间。

    我还实现了其他 3 个答案中的每一个,并且有一些观察结果可以帮助那些尝试选择方法的人。

    使用GetTickCount64 并从当前时间中减去

    • 最快的方法,时钟为 0.112 毫秒。
    • 在其参数的 100 ns 分辨率下不会产生唯一/一致的值,因为它取决于时钟节拍。返回的值都在 1/64 秒之内。
    • 需要 Vista 或更高版本。如果您的应用程序/库必须支持较旧的 Windows 版本,XP 的 32 位计数器会在大约 49 天后翻转,并且不能用于这种方法

    使用WMI查询Win32_OperatingSystemLastBootUpTime字段

    • 使用 COM 耗时 84 毫秒,使用 wmic 命令行耗时 202 毫秒。
    • 生成一致的值作为CIM_DATETIME 字符串
    • WMI 类需要 Vista 或更高版本。

    读取事件日志

    • 最慢的方法,耗时 229 毫秒
    • 以秒为单位(Unix 时间)生成一致的值
    • 适用于 Windows 2000 或更高版本。
    • 正如 cmets 中的 Jonathan Gilbert 所指出的,不保证会产生结果。

    这些方法也产生了不同的时间戳:

    • 正常运行时间:1558758098843 = 2019-05-25 04:21:38 UTC(有时 :37)
    • WMI:20190524222528.665400-420 = 2019-05-25 05:25:28 UTC
    • 事件日志:1558693023 = 2019-05-24 10:17:03 UTC

    结论:

    事件日志方法与旧 Windows 版本兼容,在 unix 时间生成一致的时间戳,不受睡眠/休眠周期的影响,但也是最慢的。鉴于这不太可能在循环中运行,这可能是可接受的性能影响。但是,使用这种方法仍然需要处理事件日志达到容量并删除旧消息的情况,可能会使用其他选项之一作为备份。

    【讨论】:

    • 我认为Event Log方法可能不可靠,因为如果Event Log被消息淹没,服务启动的事件可能会被删除。
    • @JonathanGilbert 它当然可以在没有结果的情况下搜索整个日志,但是人们会假设“没有结果”的情况将由异常方法处理(在我的应用程序中,我回退到“现在减去正常运行时间”方法)。
    【解决方案4】:

    C++ 提升used to use WMI LastBootUpTime but switched, in version 1.54, to checking the system event log, and apparently for a good reason:

    ABI 中断:更改了 Windows 中的 bootstamp 功能,以使用 EventLog 服务启动时间作为系统启动时间。以前使用的来自 WMI 的 LastBootupTime 在时间同步和休眠时不稳定,在实践中无法使用。如果您确实需要获取 Boost 1.54 之前的行为,请从命令行或 detail/workaround.hpp 中定义 BOOST_INTERPROCESS_BOOTSTAMP_IS_LASTBOOTUPTIME

    查看 boost/interprocess/detail/win32_api.hpp,在第 2201 行附近,以函数 inline bool get_last_bootup_time(std::string &stamp) 的实现为例。 (如果你想匹配行号,我正在查看版本 1.60。)

    以防万一 Boost 以某种方式死亡,而我将您指向 Boost 并没有帮助(是的,正确),您需要的功能主要是 ReadEventLogA 和要查找的事件 ID(“事件日志已启动”根据to Boost cmets) 显然是6005

    【讨论】:

    • 感谢您的指点!为其他尝试实现此功能的其他人提供更多信息——尝试将 DWORD 事件 ID 匹配到 6005 将失败,因为 32 位数字的前 16 位用于严重性和其他功能;并且对于启动,严重性是“信息”= 01,因此结果 eventId 是 2147489653。只需将最右边的 16 位与 6005 进行比较,这样就可以正常工作。另一件需要注意的事情 - 会有多个条目,需要通过向后迭代(找到第一个)或向前迭代并保留最后一个来找到最新的。
    • 深入挖掘,有一个事件 id 12 与“操作系统启动”相关联,它在事件日志前约 20 秒触发,这将是一个更好的标记。但是,它不会记录“快速启动”,因此最好的操作是检查 6005 和 12,如果在最后一个 6005 的一分钟内发生 12,则使用该 12 的数据。
    【解决方案5】:

    我没有玩过这么多,但我个人认为最好的方法可能是查询“系统”进程的开始时间。在 Windows 上,内核在启动时为自己的目的分配一个进程(令人惊讶的是,快速的谷歌搜索并不能轻易地发现它的实际目的是什么,尽管我确信信息就在那里)。此进程在任务管理器中简称为“系统”,并且在当前 Windows 版本上始终具有 PID 4(显然 NT 4 和 Windows 2000 可能已使用 PID 8)。只要系统正在运行,这个进程就永远不会退出,并且在我的测试中,就其元数据而言,它的行为就像一个成熟的进程。根据我的测试,看起来即使是非提升的用户也可以打开 PID 4 的句柄,请求 PROCESS_QUERY_LIMITED_INFORMATION,并且生成的句柄可以与 GetProcessTimes 一起使用,这将使用 UTC 时间戳填充 lpCreationTime进程开始的时间。据我所知,在系统进程运行之前,Windows 没有任何有意义的运行方式,所以这个时间戳几乎就是 Windows 启动的时间。

    #include <iostream>
    #include <iomanip>
    
    #include <windows.h>
    
    using namespace std;
    
    int main()
    {
        unique_ptr<remove_pointer<HANDLE>::type, decltype(&::CloseHandle)> hProcess(
            ::OpenProcess(
                PROCESS_QUERY_LIMITED_INFORMATION,
                FALSE, // bInheritHandle
                4), // dwProcessId
            ::CloseHandle);
    
        FILETIME creationTimeStamp, exitTimeStamp, kernelTimeUsed, userTimeUsed;
        FILETIME creationTimeStampLocal;
        SYSTEMTIME creationTimeStampSystem;
    
        if (::GetProcessTimes(hProcess.get(), &creationTimeStamp, &exitTimeStamp, &kernelTimeUsed, &userTimeUsed)
         && ::FileTimeToLocalFileTime(&creationTimeStamp, &creationTimeStampLocal)
         && ::FileTimeToSystemTime(&creationTimeStampLocal, &creationTimeStampSystem))
        {
            __int64 ticks =
                ((__int64)creationTimeStampLocal.dwHighDateTime) << 32 |
                creationTimeStampLocal.dwLowDateTime;
    
            wios saved(NULL);
    
            saved.copyfmt(wcout);
    
            wcout << setfill(L'0')
                << setw(4)
                << creationTimeStampSystem.wYear << L'-'
                << setw(2)
                << creationTimeStampSystem.wMonth << L'-'
                << creationTimeStampSystem.wDay
                << L' '
                << creationTimeStampSystem.wHour << L':'
                << creationTimeStampSystem.wMinute << L':'
                << creationTimeStampSystem.wSecond << L'.'
                << setw(7)
                << (ticks % 10000000)
                << endl;
    
            wcout.copyfmt(saved);
        }
    }
    

    我当前启动的比较:

    • system_clock::now() - milliseconds(GetTickCount64()):

    2020-07-18 17:36:41.3284297
    2020-07-18 17:36:41.3209437
    2020-07-18 17:36:41.3134106
    2020-07-18 17:36:41.3225148
    2020-07-18 17:36:41.3145312

    (结果因调用而异,因为 system_clock::now()::GetTickCount64() 不会在完全相同的时间运行并且精度也不相同)

    • wmic OS Get LastBootUpTime

    2020-07-18 17:36:41.512344

    • 事件日志

    没有结果,因为此时我的系统上不存在事件日志条目(最早的事件是从 7 月 23 日开始)

    • GetProcessTimes 在 PID 4 上:

    2020-07-18 17:36:48.0424863

    这与其他方法有几秒钟的不同,但我想不出它本身有任何错误,因为如果系统进程尚未运行,系统真的启动了吗

    【讨论】:

    • 您是否在 PID 0(空闲进程)上尝试过 GetProcessTimes?我发现它在 PID 4 之前几百纳秒开始。
    • 哈,说实话,我什至没有想到尝试打开 PID 0,我想我只是在错误的假设下继续进行,认为这还不够“真实过程”。如果它们的开始时间仅相差几百纳秒,那么在进程表中创建两个条目的代码行可能在同一个函数中是相邻的,我猜 :-)
    • 或者可能在某处具有可变运行时间/可用处理器的多线程进程。我刚刚注意到这两个都比我的 PID 72(安全系统)和 PID 128(注册表)晚了大约 4 秒
    • 值得注意的是,this link 表示 PID 4 可能并不总是系统。我会冒险猜测 0 是一个常数。
    • 嗯,很有趣,所以也许更完整的策略可能是查询 所有 正在运行的进程并找出最早的时间:-P
    猜你喜欢
    • 2016-08-24
    • 2016-10-08
    • 2011-07-30
    • 2013-12-30
    • 2018-05-06
    • 1970-01-01
    • 1970-01-01
    • 2011-03-27
    • 1970-01-01
    相关资源
    最近更新 更多