“关于您最近的帖子,我可能会迟到一点,但以防万一:您是否有任何策略或故事来处理您无法摆脱的违反了部分(或全部)API设计准则的外部库?这是一个模糊的问题,但我实际上只是在问你过去作为API用户的任何经历,这些经验在你的脑海中真的很突出。“。
这提醒了我,我一直想写下使用糟糕API的必要步骤,只是为了强调它对程序员来说是多么糟糕。我认为编写API的人并没有真正意识到让它们正确的重要性,以及他们的错误会给成百上千、有时甚至数百万的其他程序员带来多少不必要的工作。因此,我觉得花一篇文章介绍一个API并展示一个API可以制造多少不必要的工作是很重要的。这可能是一个关于它自己的很好的专栏-每周剖析一个糟糕的API。但是由于我没有时间做这样的事情,如果我只打算剖析一个API,那么最重要的问题是,我应该选择哪一个API呢?
在计算史上写一篇关于糟糕的API的文章是一个很棒的时代(换句话说,这是一个真正为了谋生而不得不编程的可怕时代)。市场上有这么多糟糕的API,我可以随机选择一个,很可能会找到足够的问题来填满一篇3000字的文章。但是,如果我只打算在一个API中选择一个特定的操作,那么尝试选择我实际使用过的最差的API似乎是唯一正确的选择。现在有很多API通常会在“最差API”排行榜上名列前茅。例如,CSS在任何有新版本的年份都可能占据前10名中一半的位置。DirectShow虽然仍是一家持续经营的公司,但肯定在其所处时代的排名中占据主导地位。在现代,像Android SDK这样的新来者正在显示出真正的潜力,开发环境是如此错综复杂,以至于从实际的C++代码调用API时,当您尝试与它们一起发布东西时,您最不会担心的是API的质量。但是,当我仔细思考谁是有史以来最重要的糟糕API冠军时,有一个明显的赢家:Windows的事件跟踪。Windows的事件跟踪是一个做一些非常简单的事情的API:它允许系统的任何组件(包括最终用户软件)宣布“事件”,然后任何其他组件都可以“消费”。它是一个日志记录系统,用于记录从内核到内核的所有内容的性能和调试信息。现在,通常情况下,游戏开发人员没有理由直接使用Windows API的事件跟踪。您可以使用Perfmon之类的工具来查看有关游戏的日志信息,例如它使用了多少工作集或它使用了多少磁盘I/O。但是,直接访问事件跟踪给您提供了一个您在其他任何地方都无法获得的特定功能:上下文切换计时。是的,如果您有任何相对较新的Windows版本(如7或8),内核将记录所有线程上下文切换,并且使用这些事件中包含的CPU时间戳,您实际上可以将它们与您自己的游戏内性能分析相关联。这是非常有用的信息,而且通常只能从控制台硬件获得。这就是为什么像RAD的遥测这样的工具可以向您显示正在运行的线程何时被中断,并且必须等待系统线程进行工作,这对于调试奇怪的性能问题通常是至关重要的。到目前为止,API听起来相当不错。我的意思是,上下文切换时间是非常宝贵的信息,所以即使API有点简陋,它仍然是相当棒的,对吗?对吗?
在我们看一看Windows API的实际事件跟踪之前,我想在这里走一走,做我在上周的讲座中说过的事情:首先编写用法代码。无论何时评估API或创建新的API,您都必须始终、始终、始终从编写一些代码开始,就好像您是一个试图做API应该做的事情的用户一样。如果API没有任何限制,这是获得API如何工作的良好、清晰透视图的唯一途径。如果它是“神奇的”,可以这么说。然后,一旦你有了这些,你就可以继续前进,开始思考实际的问题,以及对你来说最好的方法是什么来实现一些可实施的东西。那么,如果我是一名程序员,对Windows API的事件跟踪一无所知,我将如何获得上下文切换列表呢?嗯,我想到了两种方法。最直接的方法如下所示:
这是做这件事的一种方法。很简单,很容易理解,很难搞砸。使用调试器的人将能够准确地看到正在发生的事情,并且如果您做了什么,您将能够非常容易地辨别出来
我不知道任何人应该如何真正学习如何使用Windows API的事件跟踪。也许有一些很好的例子四处流传,但我从来没有找到过。我不得不在长达数小时的实验过程中,从各种文档片段中提取用法代码,将它们拼凑在一起。每次我想出这个过程的另一个步骤时,我都会想,“等等,真的吗?”而每一次微软都含蓄地回答说,“真的。”让我告诉您如何调用API确实会让您对体验的敬畏有所减弱,所以我会说,如果您想要完整的体验,现在就停止阅读,并尝试自己获取上下文切换时间戳。我可以向你保证,这将是几个小时的乐趣和兴奋。那些宁愿以一天的面部护理时刻为代价来节省时间的人,请继续往下看。好的,我们开始吧。与我建议的ETWBeginTrace()等效的是Microsoft的StartTrace()调用。乍一看,它似乎足够无辜:
但是,当您查看需要为Properties参数传入的内容时,事情开始变得有点麻烦了。Windows定义了EVENT_TRACE_PROPERTIES结构,如下所示:
粗略地看一下这些数据就会发现一点奇怪的地方:为什么会有像“EventsLost”和“BuffersWritten”这样的成员呢?这是因为,Microsoft没有为您可能对跟踪执行的不同操作创建不同的结构,而是将API函数分组到几个组中,并且每个组中的所有函数共享其参数的一个合并结构。因此,用户必须完全依赖于每个API的MSDN文档,而不是通过查看函数的参数来清楚地了解函数的内容,并希望它正确地枚举每个调用使用的巨型参数结构的哪些成员,以及这些成员是进入函数还是退出函数。当然,因为它有很多不同的使用方式,而且考虑到未来API也可能使用它,微软要求您在使用它之前将这个巨大的野兽清除为零:
对于StartTrace(),如果我们只想直接取回数据,而不是试图记录到文件中,则需要填写一些成员。这两点有一定的道理:
EnableFlags说明了我们想要的结果。我们想要上下文切换,所以我们设置了该标志。现在,当你有超过32种类型的事件来自一家供应商时会发生什么,我不知道,但我猜他们并不特别关心这种可能性。是的,这就是为什么我在我的提案中使用枚举和函数调用方法,因为它支持40亿个事件类型,但是,嘿,“32个事件类型对每个人来说都应该足够了”,所以Microsoft使用了32位标志字段。这不是什么大事,但这绝对是一种短期思维,会导致不必要的重复函数,在它们的名称后面附加“Ex”。LogFileMode只说明我们是否希望直接获取事件,或者是否只希望内核将它们写入磁盘。因为它们是如此不同的操作,我会把这两件事分解成不同的函数调用,但是,嘿,我们已经有了一个巨大的结构来处理所有的事情,还不如把它们都扔进去。这个领域的情况变得有点奇怪了:
根据文档,这是唯一允许您拥有的值。所以对你来说,这只是一项忙碌的工作。再说一次,没什么大不了的,因为也许他们正试图计划未来的扩张或其他什么(上帝保佑我们)。但是我们在这里已经得到了令人讨厌的数据耦合,其中函数调用和结构内容实际上是冗余的。当我们来到这一领域时,情况继续恶化:
那是什么意思?嗯,隐晦命名的“ClientContext”实际上指的是您希望事件具有的时间戳类型。“TimestampType”可能会更具描述性,但不管怎样。真正有趣的是位于右侧的赤裸裸的“1”值。实际上,您可以将ClientContext设置为一组枚举值,但是Microsoft从未为它们指定符号名称。因此,您只需阅读文档并记住,1表示时间戳来自QueryPerformanceCounter,2表示“系统时间”,3表示CPU周期计数。如果不明显,公共API永远不应该做这样的事情有很多原因。在内部,我偶尔会做这样的事情,比如在索引方案中,当本地代码只想使用-1和-2来处理某种复杂的特殊情况时。但是对于提供给数百万开发人员的API,您总是希望定义您的常量。首先,它使代码具有可读性。没有人知道“1”的ClientContext是什么,但是USE_QUERY_PERFORMANCE_COUNTER_TIMESTAMPS的ClientContext会非常清楚。其次,它使代码可搜索。没人能把空调弄成灰白色的
因此,现在您必须小心在哪里执行此操作,可能是在GUID所在的项目中创建一个新文件,以便每个人都可以引用它们,或者其他类似的无稽之谈,这样您就不会做两次。不过,不管怎样,我们都快填完结构了。我们所要做的就是处理SessionName参数,我们应该可以将其作为字符串传递,对吗?既然这是会议的名称,我想也许就这么做吧:
因为那会是个很棒的会场名字,你不觉得吗?但可惜的是,事情不是这样运作的。结果是,即使您已经在SessionProperties中传递了指定内核是事件源的GUID,也必须传递预定义的常量KERNEL_LOGER_NAME作为会话名称。为什么?嗯,因为一个小小的秘密惊喜,我留点钱给你,这样你就可以品尝到这一切的悬念了。好的,那么,我们开始吧:
看起来不错,对吧?不对。结果表明,虽然SessionName字符串作为第二个参数传递,但这实际上只是一个“方便”特性。实际上,SessionName需要直接嵌入到SessionProperties中,但是因为Microsoft不想限制名称的最大长度,所以他们决定直接将其打包到EVENT_TRACE_PROPERTIES结构之后。所以说真的,你不能这么做:
是的,没错,Windows API的事件跟踪的每个用户都必须自己进行打包结构格式的算法和布局。我完全不知道为什么名称必须以这种方式捆绑在一起,但是如果您希望每个人都这样做,您肯定应该提供一个实用程序宏或函数来为用户做正确的事情,并使他们不需要理解您奇怪的数据打包需求。不过,嘿,至少你不用自己把名字抄进去!Microsoft对此API采用的约定是StartTrace()函数会为您将名称复制到结构中,因为毕竟它是作为第二个参数传递的。嗯,这是一个很好的姿态,但在实践中并不奏效。事实证明,强制会话名称为KERNEL_LOGGER_NAME对于GUID来说并不是多余的,这就是我提到的秘密惊喜。它必须是KERNEL_LOGER_NAME的真正原因是因为Windows只允许您在系统中有一个会话,即从SystemTraceControlGuid读取事件的总会话。其他GUID可以由多个会话读取,但不能由SystemTraceControlGuid读取。因此,当您传递kernel_logger_name时,实际上是在说您想要一个唯一的会话,该会话可以在任何给定时间存在于系统中,GUID为SystemTraceControlGuid。如果其他人已经启动该会话,则您启动该会话的尝试将失败。它会变得更好。该会话对于操作系统是全局的,并且不会在启动它的进程终止时自动关闭。因此,如果您编写了调用StartTrace()的代码,但是该代码中的某个地方存在错误,并且您的程序崩溃,则kernel_logger_name会话仍在运行!当您重新运行程序时(可能在修复了错误之后),尝试StartTrace()将失败,并显示ERROR_ALIGHY_EXISTS。因此,基本上,StartTrace()(它是帮助您将SessionName复制到结构中的调用)很少是您进行的第一个调用。你更有可能做的是把这个叫做:
这将关闭任何现有会话,以便您后续对StartTrace()的调用将成功。当然,ControlTrace()不像StartTrace()那样复制名称,这意味着在实践中您必须自己做,因为StartTrace()是在ControlTrace()之后调用的!
这很疯狂,但这一切的后果更疯狂。如果您想一想只有一个可能的跟踪连接到内核记录器意味着什么,您很快就会意识到其中存在安全问题。如果其他进程调用了StartTrace(),并且他们正在使用内核记录器,那么系统如何知道我们的进程可以进入并停止该跟踪,以便我们可以使用我们的设置重新启动它呢?可笑的是,答案是它不会!事实上,这是一场完全免费的比赛,愿最好的过程获胜!最后调用StartTrace()的人就是配置跟踪的人。嗯,不完全是。显然,您不希望任何旧进程都能够从其他进程窃取内核记录器。因此,微软决定最好的做法是完全禁止所有进程访问内核记录器,除非它们被特别授予管理员特权。是的,我不是在夸大其词。如果您只是想接收上下文切换列表,即使只针对您的进程,它也必须以完全管理员权限运行