-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
330 lines (174 loc) · 197 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Jacksing's Blog</title>
<subtitle>Jacksing</subtitle>
<link href="https://wzzzx.github.io/atom.xml" rel="self"/>
<link href="https://wzzzx.github.io/"/>
<updated>2024-04-28T06:02:30.000Z</updated>
<id>https://wzzzx.github.io/</id>
<author>
<name>Jacksing</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>Message-only windows</title>
<link href="https://wzzzx.github.io/Windows/message-only-windows/"/>
<id>https://wzzzx.github.io/Windows/message-only-windows/</id>
<published>2024-04-26T05:48:27.000Z</published>
<updated>2024-04-28T06:02:30.000Z</updated>
<content type="html"><![CDATA[<p>Message-Only Windows 是一个非常特殊的窗口类型,而且名字也具有很强的误导性。但是在了解了之后就会知道,怪不得这个窗口类型怎么好像没见到有人在用。</p><span id="more"></span><p>在 <a href="/Windows/about-message-loop/" title="消息循环的一点思考">消息循环的一点思考</a>中,笔者提到了一个通用的消息循环模块,这个模块的设计初衷是为了解决项目中存在的多个消息循环的问题。刚开始做的时候意识到,标准的 Windows 窗口会额外处理很多不同的事件,所以希望能有一个替代的方案,尽可能地无视无关的消息。翻阅了文档之后,找到了这个 Message-Only Windows。但是,深入了解一下后发现,这个窗口类型非常误导人,它完全不适用我们的场景。</p><p>微软在<a class="link" href="https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows" >文档<i class="fas fa-external-link-alt"></i></a>中对这个窗口做了个介绍。简单地说,Message-Only Windows 是一个不可见的窗口,它不会显示在屏幕上,也不会接收用户输入,也不会被其他窗口捕获焦点。它存在的唯一一个目的就是接收和处理消息。初步看来,这貌似是一个很符合我们要求的窗口类型。不过,其存在着一大堆的限制,以至于我们没法使用。</p><p>Message-Only Windows 是 <code>HWND_MESSAGE</code> 的子窗口,挂载在它的下面会导致窗口无法接收到任何以 <code>HWND_BROADCAST</code> 发送的系统广播消息,或者其他发送到 top-level windows 上的窗口消息。这就意味着,很多系统的状态变更事件,我们都没法知道了。</p><p>这个窗口存在的意义就是为了收发私有消息。所以对于一个普通的应用而言,它没有什么太大的意义。</p><blockquote><p><a class="link" href="https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows" >Message-Only Windows<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://devblogs.microsoft.com/oldnewthing/20171218-00/?p=97595" >What kind of messages can a message-only window receive?<i class="fas fa-external-link-alt"></i></a></p></blockquote>]]></content>
<summary type="html">Windows,message-only windows</summary>
<category term="Windows" scheme="https://wzzzx.github.io/categories/Windows/"/>
</entry>
<entry>
<title>关于消息循环的一点思考</title>
<link href="https://wzzzx.github.io/Windows/about-message-loop/"/>
<id>https://wzzzx.github.io/Windows/about-message-loop/</id>
<published>2024-04-24T05:17:16.000Z</published>
<updated>2024-04-28T06:01:38.000Z</updated>
<content type="html"><![CDATA[<p>最近仔细阅读了一下 Windows 的消息循环机制,发现可以基于这个机制实现一些有趣的功能。这篇文章就是对消息循环的一点思考。</p><span id="more"></span><h2 id="项目情况"><a href="#项目情况" class="headerlink" title="项目情况"></a>项目情况</h2><p>最近的这个项目,是一个后台的普通进程,该进程在开机启动之后,没有什么特殊原因的话,是不会退出的。所以过去的操作就是,在 <code>main</code> 函数里直接使用 Windows 消息循环的套路代码,一个死循环放在那里,其他的模块自己开自己的线程去搞事情。</p><p>这样明显是一个非常不合理的操作,随便想想都能罗列出好些问题:</p><ol><li>平白无故浪费了一个线程,该线程开着却什么事情都不干。</li><li>在项目里还有好些地方也需要使用窗口消息,之前就是需要的地方自己整个窗口整一套事件循环,这样就会有好多个消息循环,不好管理。</li><li>虽然笔者的项目是不会退出的,但是现在这个做法真的也就一点退路都不留,完全没有任何办法去进行退出操作。</li></ol><h2 id="前期改进措施"><a href="#前期改进措施" class="headerlink" title="前期改进措施"></a>前期改进措施</h2><p>针对于第二点,笔者在项目的中期,额外迭代了一个通用的消息循环模块,这个模块内部就有着一个消息循环,其他模块只需要注册自己的消息处理函数就可以了。这样某种程度上就解决了第二点的问题。</p><p>并且在后续项目开发的时候,产生了这样的一个需求。笔者在 A 线程里无法调用某个接口,否则会产生死锁,这时候需要丢给其他的线程去执行。这时候笔者想到可以用上这个通用的消息循环模块,所以笔者给这个消息循环模块加上了一个发送消息的接口,这样就可以在 A 线程里发送消息给消息循环线程,然后消息循环线程再去执行这个接口。</p><h2 id="重构思路"><a href="#重构思路" class="headerlink" title="重构思路"></a>重构思路</h2><p>虽然有了一个通用消息循环模块,但项目主线程的操作始终还是一个心结,这看着就很丑。所以笔者决定重构一波。</p><p>整体的一个思路就是,以 <code>Qt</code> 的消息循环为模板,重构一个消息循环模块出来。主线程可以是一个消息循环,但是需要让其他的线程也能使用到它,将它的能力开放出来。针对于此,这样一个消息循环模块肯定是需要有这么几个部分的:</p><ol><li>消息循环触发函数,通过这个函数可以让线程正式进入消息循环中</li><li>广播发送和单播发送消息的接口,可以让其他线程发送消息给消息循环线程</li><li>公共消息与私有消息的区分。公共消息可以用于广播,每个业务模块都可以根据广播消息进行相应的业务操作。私有消息是提供给不同的业务模块使用的,这样可以避免不同的业务模块之间的消息冲突</li><li>注册和注销消息处理函数的接口,可以让其他线程注册自己的消息处理函数</li><li>窗口句柄获取接口,通过这个接口可以获取到当前消息循环的窗口句柄</li><li>消息循环退出接口,这样可以让消息循环线程退出消息循环</li></ol><h2 id="开始设计"><a href="#开始设计" class="headerlink" title="开始设计"></a>开始设计</h2><h3 id="回调函数设计"><a href="#回调函数设计" class="headerlink" title="回调函数设计"></a>回调函数设计</h3><p>Windows 的消息循环回调中,会通过一个 procedure 函数来处理消息。这个函数有四个参数,其中第一个是窗口句柄。</p><p>Windows 的消息循环中,事件的处理是通过一个 Procedures 来实现的,也就是回调函数。这个回调函数有四个参数,其中第一个参数是窗口句柄,其他的具体可见<a class="link" href="https://learn.microsoft.com/en-us/windows/win32/winmsg/about-window-procedures#structure-of-a-window-procedure" >文档<i class="fas fa-external-link-alt"></i></a>。在我们的项目里,对于窗口句柄的需求看起来是没有的,所以设计消息回调函数的时候,只保留了后面的三个参数,函数返回值也是没有存在必要的。具体签名如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> MessageCallback = std::function<<span class="built_in"><span class="keyword">void</span></span>(UINT msgId, UINT_PTR first, LONG_PTR second)>;</span><br></pre></td></tr></table></figure><h3 id="消息定义"><a href="#消息定义" class="headerlink" title="消息定义"></a>消息定义</h3><p>通过这样的划分有几个好处。首先公共消息的定义肯定是放在一个全局可见的区域的,而在业务里,大部分的消息其实都应该是私有的,这样就不需要每申请一个消息就需要在全局定义一次,这样会显得很乱。其次,通过这样的划分,可以避免不同的业务模块之间的消息冲突。</p><p>Windows 规定了用户自定义消息从 <code>WM_USER</code> 开始,到 <code>0x7FFF</code> 为止,所以我们可以在这个范围内定义我们的消息。这里我们的制定策略是,公共消息在头文件从 <code>WM_USER</code> 开始定义,私有消息则通过函数接口获取。</p><p>这种设计可以极大的避免消息冲突问题,多人协作开发的时候,也不需要沟通之后再进行消息添加。并且对于具体的消息值,是一个完全没有必要去关心的东西。</p><h3 id="窗口句柄"><a href="#窗口句柄" class="headerlink" title="窗口句柄"></a>窗口句柄</h3><p>Windows 下很多的接口都需要一个窗口的句柄来进行操作,所以我们的消息循环模块也需要提供一个接口来获取窗口句柄。这个接口的设计也是很简单的,只需要返回一个窗口句柄就可以了。</p><h3 id="消息处理"><a href="#消息处理" class="headerlink" title="消息处理"></a>消息处理</h3><p>在对外暴露的整个消息循环模块里,<code>MsgLoop</code> 是唯一的一个类,这个类提供了几个 <code>static</code> 方法用来进行一些消息循环相关的操作,这些方法都保证是线程安全的。如下枚举:</p><ol><li><code>exec</code>:这个方法用于陷入消息循环,直到消息循环退出</li><li><code>boardcast</code>:该方法用于广播消息,所有注册的消息处理函数都会收到这个消息</li><li><code>handle</code>:该方法用于获取当前消息循环的窗口句柄</li><li><code>quit</code>:该方法用于退出消息循环</li></ol><p>消息的处理则额外创建了一个类 <code>MsgOperator</code>,这个类放置于 <code>MsgLoop</code> 内。这个类用于注册和注销消息处理函数,发送事件,同时还需要提供一个接口用来生成私有消息。</p><h2 id="实现细节"><a href="#实现细节" class="headerlink" title="实现细节"></a>实现细节</h2><h3 id="窗口创建"><a href="#窗口创建" class="headerlink" title="窗口创建"></a>窗口创建</h3><h4 id="创建时机"><a href="#创建时机" class="headerlink" title="创建时机"></a>创建时机</h4><p>在我们的模块设计中,进程调用了 <code>exec</code> 之后就陷入了消息循环当中了。照理来说,窗口以及相关资源的窗口应该在调用这个函数的时候才创建。但是这存在一个问题,如果在 <code>exec</code> 之前,就需要使用窗口句柄来初始化一些操作,又或者某个业务需要发生一些私有消息来进行初始化操作,那么这个时候窗口还没有创建出来,这就会导致一些问题。</p><p>一般情况下,可以额外提供一个初始化函数,这个函数用于初始化窗口,然后在 <code>exec</code> 之前调用这个函数。但是这样的话,就会导致使用者需要额外的操作,又不太友好了。</p><p>所以我们需要在 <code>exec</code> 之前就创建窗口,并且这个窗口的创建还应该足够的快。在 C++ 里,比较通用的解决方案就是全局 <code>static</code> 变量了。这种变量会在 <code>main</code> 函数之前初始化。所以窗口的创建我们可以选在这个变量中进行。</p><p>不过这种方式带来的弊端就是,出现错误的时候,无法记录相关的日志。除非我们在窗口创建时就进行一些日志的初始化工作。但是这个时候并不想去介入过多额外的东西,所以笔者选择了无视错误信息。</p><h4 id="消息队列创立"><a href="#消息队列创立" class="headerlink" title="消息队列创立"></a>消息队列创立</h4><p>在<strong>创建时机</strong>所提及的弊端除了需要快速创建窗口之外,还需要尽可能快的创建消息事件循环。这里笔者采用了一个比较简单且通用的方式,就是在窗口成功创建之后,直接调用 <code>PeekMessage</code> 函数来让系统迅速创建一个消息事件循环。注意参数应该设置为 <code>PM_NOREMOVE</code>,避免移除掉了需要被处理的消息。</p><h4 id="类名"><a href="#类名" class="headerlink" title="类名"></a>类名</h4><p>Windows 要求创建窗口的时候,提供一个类名,为了避免跟其他的类名发生冲突。笔者项目里的类名额外增加了一个固定的 <code>uuid</code> 进去。使用固定的类名主要是考虑以后可能会涉及到一些类名的查询工作。</p><h3 id="多线程安全"><a href="#多线程安全" class="headerlink" title="多线程安全"></a>多线程安全</h3><p>对于 <code>MsgLoop</code> 的几个静态方法,都保证是线程安全的。在这里方法中,唯一会导致线程不安全的变量就是窗口句柄了。为了提高性能,这里采用了读写锁的方式来进行保护。每次进入这些函数时,都会加一个读锁,判断窗口句柄是否存在。这里还有一个注意点,现阶段不存在的话,笔者就直接让函数返回了。后续会让进程直接崩溃。毕竟整个进程都依赖着这个窗口循环,窗口句柄不存在的话,这个进程就没有存在的必要了。</p><p>在 <code>exec</code> 函数中,从消息循环中退出后,需要将窗口句柄设为 <code>nullptr</code>,这样可以避免其他业务还在使用这个窗口句柄。</p><h3 id="控制消息"><a href="#控制消息" class="headerlink" title="控制消息"></a>控制消息</h3><p>对于这个消息循环组件,现在所暴露的公共属性只有两个,一个是消息循环创建消息,一个是退出消息。之所以提供这个,也是从 <code>QCoreApplication::aboutToQuit</code> 得到的启示。这两个消息可以让业务进行一些初始化和退出操作。并且保证这些操作都在消息循环内。</p><h4 id="发送时机"><a href="#发送时机" class="headerlink" title="发送时机"></a>发送时机</h4><p>从上文提到的,调用 <code>exec</code> 的时候,可能已经晚了,很多操作都需要在之前就搞定好。所以得利用好 Windows 消息循环的特性,在创建完窗口的时候,我们会调用 <code>PeekMessage</code> 函数来让系统迅速创建消息事件循环,紧接着我们就可以发送一个消息循环创建消息。这时候发送的消息是不会被处理的,因为还没有地方去获取和处理它。等调用者调用 <code>exec</code> 的时候,就会进入消息循环,这个时候这条创建消息就会被立马处理,并且因为是创建完立马发送进来的,所以基本可以确保会在消息队列的最前面。</p><p>退出的消息则是在 <code>quit</code> 函数中去调用,我们会在退出之前,发送一个退出消息,而后立马发送 <code>WM_QUIT</code> 消息,这样就基本上就保证了退出消息会是最后一个需要被处理的消息,并且还给了业务一个机会去处理这个退出消息。</p><h2 id="非及时性"><a href="#非及时性" class="headerlink" title="非及时性"></a>非及时性</h2><p>既然决定使用了消息循环机制来处理事件,就必须要接收事件的发送和处理是分离且不及时的这一情况。主线程会依次收到不同的消息,处理结束后再进行下一个消息的处理。这里必然存在着消息处理延迟。不过可以考虑在 <code>exec</code> 中,强行提高一下线程的优先级,确保消息能够尽可能地及时处理。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>自从整了这么一个组件出来之后,笔者整个项目都围绕着这么一个类展开,跨线程通信 / 消息处理 / winapi 参数设置等操作都顺手多了,整个代码的架构也优雅多了。</p>]]></content>
<summary type="html"><p>最近仔细阅读了一下 Windows 的消息循环机制,发现可以基于这个机制实现一些有趣的功能。这篇文章就是对消息循环的一点思考。</p></summary>
<category term="Windows" scheme="https://wzzzx.github.io/categories/Windows/"/>
</entry>
<entry>
<title>系统息屏亮屏相关操作</title>
<link href="https://wzzzx.github.io/Windows/system_screen_operation/"/>
<id>https://wzzzx.github.io/Windows/system_screen_operation/</id>
<published>2024-04-15T04:39:09.000Z</published>
<updated>2024-04-23T16:08:16.000Z</updated>
<content type="html"><![CDATA[<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>我们当前的设备有个比较奇葩的地方,就是不会无法响应 Windows 的息屏亮屏操作。所以软件上的解决方案是,需要去监听 Windows 的相关息屏亮屏消息,然后通过硬件暴露出来的接口告知硬件,硬件再进行相应的变更。</p><h3 id="设置息屏"><a href="#设置息屏" class="headerlink" title="设置息屏"></a>设置息屏</h3><p>设置息屏相对来说比较好处理,我们只需要广播一条息屏的消息出来,系统就会去响应。Windows 将这条消息放置在 <code>WM_SYSCOMMAND</code> 下,指定操作对象为 <code>SC_MONITORPOWER</code>,参数设置为 2 即可。具体代码如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, <span class="number">2</span>);</span><br></pre></td></tr></table></figure><p>在一般情况下,发生上面这条窗口消息都可以让屏幕息屏。但是,这是一个非常<strong>错误</strong>的做法!The Old New Thing 通过了一系列的博客阐述了一个概念,给桌面窗口发消息属于一种 hack 行为。这种行为可能会导致一些不可预知的问题,这条消息会被所有的 Top most 窗口处理,这意味着消息会被处理 N 次,虽然在大部分情况下这个广播消息不会有什么问题。但是类似的操作难免会在其他的地方给自己挖坑。需要记住一点的是,<strong>在 Windows 下,不要依赖不受控制的窗口去处理自己的事</strong>。</p><p>正确的做法应该是创建一个自己的窗口,然后给这个窗口发送消息,由它来处理相关的事。</p><h3 id="设置亮屏"><a href="#设置亮屏" class="headerlink" title="设置亮屏"></a>设置亮屏</h3><p>在设置息屏的消息参数中,1 表示设置为亮屏。但是实际验证后发现,调用这个参数会让屏幕亮起来,然后不到两秒钟的时间就又息屏了。并不清楚这个问题的原因是啥,可能是因为系统?笔者使用的系统版本为 Windows 10 家庭中文版, 版本号为 22H2。但是 Google 了一波后发现,大家貌似都存在这样的问题。</p><p>针对这个问题,笔者考虑了一个比较取巧的方式来实现。既然移动鼠标能够让屏幕点亮,所以笔者的做法就是通过模拟鼠标移动来实现亮屏。并且还只能轻微移动,避免干扰用户的使用,具体代码如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">TurnOnDisplay</span><span class="params">()</span> </span>{</span><br><span class="line"> INPUT inputs[<span class="number">2</span>] = {};</span><br><span class="line"> ZeroMemory(inputs, <span class="keyword">sizeof</span>(inputs));</span><br><span class="line"></span><br><span class="line"> inputs[<span class="number">0</span>].type = INPUT_MOUSE;</span><br><span class="line"> inputs[<span class="number">0</span>].mi.dwFlags = MOUSEEVENTF_MOVE;</span><br><span class="line"> inputs[<span class="number">0</span>].mi.dx = <span class="number">0</span>;</span><br><span class="line"> inputs[<span class="number">0</span>].mi.dy = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> inputs[<span class="number">1</span>].type = INPUT_MOUSE;</span><br><span class="line"> inputs[<span class="number">1</span>].mi.dwFlags = MOUSEEVENTF_MOVE;</span><br><span class="line"> inputs[<span class="number">1</span>].mi.dx = <span class="number">0</span>;</span><br><span class="line"> inputs[<span class="number">1</span>].mi.dy = <span class="number">-1</span>;</span><br><span class="line"></span><br><span class="line"> SendInput(<span class="number">2</span>, inputs, <span class="keyword">sizeof</span>(INPUT));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>mouse_event</code> 这个接口会简单一些,但是 Windows 已经标记为废弃了。所以更换成了 <code>SendInput</code> 这个接口。</p><h3 id="获取当前屏幕状态"><a href="#获取当前屏幕状态" class="headerlink" title="获取当前屏幕状态"></a>获取当前屏幕状态</h3><p>设置相对来说还是一个比较简单的存在。但是获取当前屏幕状态就比较麻烦了。</p><p>Windows 貌似对显示器有点偏见,以前在实现显示器相关的需求的时候,就发现这一块操作的接口要么基本没有,要么就是只提供了一些很简单的能力。如果要获取非显示器设备的电源设置,文档上可以找到 <a class="link" href="https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getdevicepowerstate" >GetDevicePowerState<i class="fas fa-external-link-alt"></i></a> 这个接口。但是这个接口只能获取设备的电源状态,偏偏又不能获取显示器的电源状态。</p><p>所以在这一时找不到方案去获取息屏状态的情况下,笔者实现的方案是去枚举显示设备,然后根据枚举出来的设备一个个查询他们的电源数据。根据微软的<a class="link" href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/device-power-states" >定义<i class="fas fa-external-link-alt"></i></a>,如果设备电源状态为 D0,表示设备是完全开启的。否则都是一些低功耗或休眠的状态。具体到显示器上,屏幕点亮的时候,设备必然处于 D0 状态。所以我们可以通过这个状态来判断显示器是否亮屏。</p><p>枚举设备,查询设备信息需要用到 <a class="link" href="https://learn.microsoft.com/en-us/windows/win32/api/setupapi/" >SetupAPI<i class="fas fa-external-link-alt"></i></a> 这一套东西。而设备主要是通过微软预定义的 <a class="link" href="https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors" >GUID<i class="fas fa-external-link-alt"></i></a> 来标识的,对于显示器,其 GUID 为 <code>{4d36e96e-e325-11ce-bfc1-08002be10318}</code>。</p><p>有思路之后,处理起来其实就很简单了。通过 <code>SetupDiGetClassDevsW</code> 这一接口可以获取当前的设备集,而后利用 <code>SetupDiEnumDeviceInfo</code> 来枚举设备,对于每个设备,使用 <code>SetupDiGetDeviceRegistryPropertyW</code> 来读取设备电源数据。代码可以简单参考如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">DEFINE_GUID</span>(GUID_DEVCLASS_MONITOR, <span class="number">0x4d36e96e</span>L, <span class="number">0xe325</span>, <span class="number">0x11ce</span>, <span class="number">0xbf</span>, <span class="number">0xc1</span>, <span class="number">0x08</span>, <span class="number">0x00</span>, <span class="number">0x2b</span>, <span class="number">0xe1</span>,</span><br><span class="line"> <span class="number">0x03</span>, <span class="number">0x18</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">GetMonitorState</span><span class="params">()</span> </span>{</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="通知"><a href="#通知" class="headerlink" title="通知"></a>通知</h3><p>通知也是一个大麻烦,在 <code>SetupAPI</code> 中,笔者暂时没有找到合适的接口来注入通知回调。这个确实有点奇怪,这种数据的变更通知看起来是一个强需求,不过可能以窗口消息的形式对外暴露?后续可以研究一下。</p><p>在网上一通搜索之后,发现我们可以通过电源设置相关的接口来实现屏幕状态的变更通知。电源管理也是 Windows 整个体系内很大的课题,在我们的需求场景下,我们可以使用 <a class="link" href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerpowersettingnotification" >RegisterPowerSettingNotification<i class="fas fa-external-link-alt"></i></a> 这个接口来注册通知。该接口可以提供很多不同类型的电源状态变更通知,这些类型通过预设 GUID 来区分。对于屏幕的亮屏息屏状态,我们可以使用 <code>GUID_CONSOLE_DISPLAY_STATE</code> 这个 GUID 即可。而后,屏幕的状态变更会通过窗口消息 <a class="link" href="https://learn.microsoft.com/en-us/windows/win32/power/wm-powerbroadcast" >WM_POWERBROADCAST<i class="fas fa-external-link-alt"></i></a> 来通知。</p><p>这个接口有两个有意思的地方。首先,屏幕除了息屏和灭屏两种状态外,还额外提供了一个灰显的状态,有这个状态的加持,整个接口就更加完备了。其次,在注册通知成功的时候,窗口消息会立马收到一个事件,告知当前的状态。在这一能力加持下,我们甚至可以不需要通过枚举显示设备的方式来获取当前的显示器状态。</p><blockquote><p>参考链接</p><p><a class="link" href="https://www.codeproject.com/Articles/1193099/Determining-the-Monitors-On-Off-sleep-Status" >Determining the Monitor’s On/Off (sleep) Status<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://learn.microsoft.com/en-us/windows/win32/power/power-setting-guids" >Power Setting GUIDs<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://stackoverflow.com/a/613300/20831244" >how can i detect the Monitor state<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://devblogs.microsoft.com/oldnewthing/20060613-05/?p=30893" >Fumbling around in the dark and stumbling across the wrong solution<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/device-power-states" >Device Power States<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://learn.microsoft.com/en-us/windows/win32/power/wm-powerbroadcast" >WM_POWERBROADCAST message<i class="fas fa-external-link-alt"></i></a></p></blockquote>]]></content>
<summary type="html"><h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>我们当前的设备有个比较奇葩的地方,就是不会无法响应 Windows 的息屏亮屏操作。所以软件上的解决方案是,需要去监听 Windows 的相</summary>
<category term="Windows" scheme="https://wzzzx.github.io/categories/Windows/"/>
</entry>
<entry>
<title>注册表监听避免消息丢失</title>
<link href="https://wzzzx.github.io/Windows/handle_registry_monitor_message_lose/"/>
<id>https://wzzzx.github.io/Windows/handle_registry_monitor_message_lose/</id>
<published>2024-04-08T05:08:27.000Z</published>
<updated>2024-04-16T06:12:46.000Z</updated>
<content type="html"><![CDATA[<p>对于注册表的变化监听,<code>Windows</code> 通过 <code>RegNotifyChangeKeyValue</code> 函数来实现。这个函数的原型如下所示:</p><span id="more"></span><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function">LONG <span class="title">RegNotifyChangeKeyValue</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function"> HKEY hKey,</span></span></span><br><span class="line"><span class="params"><span class="function"> BOOL bWatchSubtree,</span></span></span><br><span class="line"><span class="params"><span class="function"> DWORD dwNotifyFilter,</span></span></span><br><span class="line"><span class="params"><span class="function"> HANDLE hEvent,</span></span></span><br><span class="line"><span class="params"><span class="function"> BOOL fAsynchronous</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在该函数中,由 <code>hEvent</code> 和 <code>fAsynchronous</code> 来控制同步或者异步。如果 <code>fAsynchronous</code> 为 <code>TRUE</code>,那么函数就会立即返回,否则会一直等待注册表的变化。总的来说,这个函数比较简单,但是有一个特点,就是 <strong>只会通知一次</strong>。使用其来完成注册表监听这个事的操作流程一般都是:开始监听 -> 收到通知 -> 处理通知 -> 重新建立监听。</p><p>但是这里就存在一个让笔者感到非常困惑的地方,为什么要设计为只通知一次呢?如果在收到通知后,重新建立监听前,这段时间内发生了变化,理论上是没办法知道的。在笔者看来,这是一个比较大的问题。</p><p>仔细啃了下文档之后,发现微软其实是提供了解决方案的。在这种需要重复监听的场景下,可以重复使用 <code>hKey</code> 参数,无需每次监听都去创建新的。而后利用这个参数,在监听建立的时,如果已经发生了注册表的变更,不论是同步还是异步,都会立即返回结果。这样就可以避免消息丢失的问题。</p><p>这种设计某种程度而言,就是做了一个消息的压缩处理。当注册表被频繁更改的时候,使用这个接口能够确保获取到最终结果,但是中途的变更可能会丢失。这个问题在实际使用中需要注意。</p><p>此外,还有一个地方需要额外注意。一旦 <code>hKey</code> 所对应的项被删了,那么还想重新建立监听的话,就需要<strong>重新创建</strong> <code>hKey</code>。对于注册表项来说,其生命周期在被删除的那一刻就彻底终结了,所有指向这个项的 <code>handle</code> 都会全部失效。此时即便重新建立一个同名的项,那也是别的项了。必须等到重新创建一个新的项,才能重新建立监听。而监听新建项这个时,只能通过其父节点来完成。不存在的项都无法使用 <code>RegOpenKeyEx</code> 来打开并监听。</p><h3 id="实现方案"><a href="#实现方案" class="headerlink" title="实现方案"></a>实现方案</h3><p>开发一个注册表监听的服务,通常的做法是监听到变化之后,再触发回到函数通知给调用者。但是,一旦这个处理逻辑耗时比较长,在一些频繁变更的注册表项上,就会出现消息被压缩的问题。为了尽可能减少这类现象,笔者的实现方案是拆分监听和处理两个事到不同的线程中去。监听线程只负责监听,而处理线程则负责处理。这样可以避免监听线程因为处理消息而阻塞,导致消息丢失的问题。</p><p>但是这个方案还是存在着一个比较麻烦的问题。两个线程之间的操作必然涉及到同步问题,一旦处理线程占用着锁并且操作耗时太久,一样会导致原先的问题。所以在这个地方,我们必须摈弃锁的使用。</p><p>为了实现这个需求,笔者在项目里借用了 Windows 的窗口消息队列。监听线程通过 <code>PostMessage</code> 函数将消息和变化的项发送到处理线程的消息队列中,而处理线程则通过常规的操作方式取出这一消息,再针对性地进行处理即可。这样就避免了锁的使用,也避免了消息丢失的问题。这样的方式下,能最大限度避免消息压缩,确保每个变更事件都能通知到调用方。</p><blockquote><p>参考链接:</p><p><a class="link" href="https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue" >RegNotifyChangeKeyValue function<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://devblogs.microsoft.com/oldnewthing/20200507-00/?p=103733" >Why does RegNotifyChangeKeyValue stop notifying once the key is deleted<i class="fas fa-external-link-alt"></i></a></p></blockquote>]]></content>
<summary type="html"><p>对于注册表的变化监听,<code>Windows</code> 通过 <code>RegNotifyChangeKeyValue</code> 函数来实现。这个函数的原型如下所示:</p></summary>
<category term="Windows" scheme="https://wzzzx.github.io/categories/Windows/"/>
</entry>
<entry>
<title>对象如何正确地转化为布尔值</title>
<link href="https://wzzzx.github.io/C/the-safe-bool-idiom/"/>
<id>https://wzzzx.github.io/C/the-safe-bool-idiom/</id>
<published>2022-12-08T15:20:56.000Z</published>
<updated>2023-01-08T07:40:20.000Z</updated>
<content type="html"><![CDATA[<p>将对象转化为布尔值是一个非常常规的需求,在诸如 <code>if (expr) {}</code> 或 <code>while (expr) {}</code> 这样的语句中,都会要求对象能够隐式转化为布尔值。C++ 也提供了一系列的方式用于对象的隐式或显式类型转换。但是诸如转化为 int,都是一些比较简单的操作,唯独 bool 比较特殊。在 C++ 中,有太多的类型可以跟 bool 类型相互转换,比如 int, 指针,对象等等。这也带来了一个问题,哪种转换方式才是最好的,所带来的副作用的是最小的。这个问题也叫做安全布尔问题(Safe bool)。</p><span id="more"></span><h2 id="隐式类型转换"><a href="#隐式类型转换" class="headerlink" title="隐式类型转换"></a>隐式类型转换</h2><p>讨论这一切之前,先简单了解一下 C++ 的隐式转换工作规则。针对于隐式转换,C++ 定义了一套隐式转换序列:</p><ul><li>零个或一个标准转换序列</li><li>零个或一个用户自定义转换序列</li><li>零个或者一个标准转换序列</li></ul><p>标准转化序列顾名思义,就是内置的一些隐式转换规则,比如 int 转为 bool,数组类型转化为指针。而用户自定义转换序列就是用户自己所定义的一系列隐式转换规则,对于第二标准转换序列,只有在实施了用户自定义转换序列之后才能运用。在官方文档中并没有给两个标准转换序列命名,所以分别按其使用的节点来称呼。</p><p>每次进行隐式转换的时候,编译器会先使用第一标准转换序列进行转换。如果无法正确进行转换,就会使用用户自定义转换序列进行转换,如果此时能够正确转换的话,就会使用第二标准转换序列进行最后的收尾工作。不过,如果是转换构造函数的话,就不会触发第二标准转换序列了。</p><p>举个例子:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span> {</span></span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="title">int</span><span class="params">()</span> <span class="keyword">const</span></span>;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">A a;</span><br><span class="line"><span class="keyword">bool</span> b = a;</span><br></pre></td></tr></table></figure><p>在这个例子中,第一标准转换序列把 <code>A*</code> 转换为 <code>const A*</code>,用户自定义转换序列把 <code>const A*</code> 转换为 <code>int</code>,最后再经由第二转换序列,把 <code>int</code> 转换为 <code>bool</code>。</p><p>弄清楚隐式转换的规则后,对后续所涉及到的隐式转换会有更好的理解。更为具体的例子,可以参考《<a class="link" href="https://www.cnblogs.com/apocelipes/p/14415033.html" >彻底理解 c++ 的隐式类型转换<i class="fas fa-external-link-alt"></i></a>》</p><h2 id="安全布尔问题"><a href="#安全布尔问题" class="headerlink" title="安全布尔问题"></a>安全布尔问题</h2><p>在引入安全布尔问题之前,得先回答一个问题,自定义类型为什么需要布尔值转换。一个最为常见的场景就是,我们需要去判断资源是否存在。在传统的 C 语言的做法中,都是采用指针来引用资源。所以在任意一个地方,我们只需要去判断指针是否为空,即可判断资源是否存在。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (some_resource *p = get_resource()) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 C++ 中,我们会把资源放置于对象内部,使用包括 RAII 的方法来管理资源,对于资源是否可用的判断就会复杂一些。针对于这一问题,一个最为简单的方案提供一个方法,通过该方法来判断内部资源是否可用。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ResourcePtr ptr = <span class="built_in">get_resource</span>();</span><br><span class="line"><span class="keyword">if</span> (ptr.<span class="built_in">is_valid</span>()) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这一方法最大的问题在于,不可能会存在一个约定俗成的命名用于确定资源是否可用,除了 <code>is_valid</code> 之外,还可以是 <code>isValid, Valid, is_empty, empty</code> 等等,调用者需要去记住这些方法名,才能正确的使用。这样的话,就会增加调用者的负担,而且还会增加代码的复杂度。此外,在<strong>模板编程</strong>的时候,这一方法会带来更为严峻的问题,它没法通用于所有的类型。</p><p>所以最为完美的方案就是,对象能够顺利的转换为布尔类型,调用者可以直接使用对象来进行资源的判断。这就带来我们所提及的安全布尔问题,所幸这一问题在 C++11 之后在语言层面上得到了完美的解决,但是可以了解一下,过去是如何解决这一问题的。</p><h3 id="使用-operator-bool"><a href="#使用-operator-bool" class="headerlink" title="使用 operator bool"></a>使用 <code>operator bool</code></h3><p>C++ 提供自定义类型转换的方法,所以最显而易见的方案就是利用 <code>operator bool</code> 来实现对象的布尔转换。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Testable</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">Testable</span>(<span class="keyword">bool</span> b = <span class="literal">true</span>) : <span class="built_in">ok_</span>(b) {}</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="title">bool</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ok_;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">bool</span> ok_;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>在实现好 <code>operator bool</code> 之后,我们就可以直接使用对象来进行判断了。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Testable t;</span><br><span class="line"><span class="keyword">if</span> (t) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从某种程度上来说,作为语言本身所提供的方案,这本应该是完美无缺的。但是,这一方案也存在着一些问题。在上一节提到的隐式转换规则中说到,在调用了用户自定义类型转换之后,还会有一个标准转换序列用于进行一些收尾转换工作。正是因为这一规则,会带来几个可能违背我们想法的问题。</p><p>其一就是,布尔值可以很轻易的转换为 int 类型,所以就可能会写出下面这样的代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IntPtr</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">IntPtr</span>(<span class="keyword">int</span>* p = <span class="literal">nullptr</span>) : <span class="built_in">ptr_</span>(p) {}</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="title">bool</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ptr_ != <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">operator</span> <span class="keyword">int</span>* () <span class="keyword">const</span> {</span><br><span class="line"> <span class="keyword">return</span> ptr_;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">int</span>* ptr_;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> a = <span class="number">10</span>;</span><br><span class="line"> <span class="function">IntPtr <span class="title">ptr</span><span class="params">(&a)</span></span>;</span><br><span class="line"> <span class="keyword">if</span> (ptr) {</span><br><span class="line"> <span class="keyword">int</span> val = ptr; <span class="comment">// 此处会出问题</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们原本是想在判断了 <code>ptr</code> 是否为空之后,再通过 <code>*</code> 操作符获取到其中的值。但是由于忽略了 <code>*</code> 操作符,导致这段代码虽然可以通过编译,但是 <code>val</code> 的值只会是 0 和 1。显然这是不符合预期的。而且问题也很隐晦,很难发现。这里便是第二转换序列进行了一些收尾工作,将 <code>bool</code> 类型转换为 <code>int</code> 类型。</p><p>其次就是,像文件句柄或网络连接这样的资源之间是不可以互相比较的。但是由于定义了 <code>operator bool</code>,使得这些资源在语言层面上能够进行比较,这显然也是不合理的。比如下面这样的代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IntPtr</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">IntPtr</span>(<span class="keyword">int</span>* p = <span class="literal">nullptr</span>) : <span class="built_in">ptr_</span>(p) {}</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="title">bool</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ptr_ != <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">int</span>* ptr_;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> IntPtr ptr1, ptr2;</span><br><span class="line"> <span class="keyword">if</span> (ptr1 == ptr2) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>ptr1</code> 和 <code>ptr2</code> 作为一个指向 <code>int</code> 值的对象,它们之间的比较是没有任何意义的。但编译器并不会报错,而且继续跑下来,直到运行时才会出现问题。</p><h3 id="使用-operator"><a href="#使用-operator" class="headerlink" title="使用 operator!"></a>使用 <code>operator!</code></h3><p>要在实现布尔值转换的基础上,解决上述两个问题,<code>operator!</code> 应该是一个比较不错的解决方案。具体可以看下面的代码实现:</p><p>对于布尔值而言,置否是一个很常见的操作。所以也可以利用其来实现布尔值的转换。这个方案可以规避</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IntPtr</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">IntPtr</span>(<span class="keyword">int</span>* p = <span class="literal">nullptr</span>) : <span class="built_in">ptr_</span>(p) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> <span class="keyword">operator</span>!() {</span><br><span class="line"> <span class="keyword">return</span> !ptr_;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">int</span>* ptr_;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> IntPtr ptr1, ptr2;</span><br><span class="line"> <span class="keyword">if</span> (!!ptr1) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!ptr1 == !ptr2) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这一方法最大的问题在于不够直观,每次判断时至少都需要使用一个感叹号。而且并没有解决可比较的问题。</p><h3 id="使用-operator-void"><a href="#使用-operator-void" class="headerlink" title="使用 operator void*"></a>使用 <code>operator void*</code></h3><p>在上文提到过,C 语言使用指针来进行资源的可用性判断是一个很不错的方式。所以一个转换思路就是借助于这一现成方案,在各式指针中,<code>void *</code> 指针除了用于进行类型转换,基本就没有其他的操作了,所以可以将对象隐式转换为 <code>void *</code> 指针。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IntPtr</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">IntPtr</span>(<span class="keyword">int</span>* p = <span class="literal">nullptr</span>) : <span class="built_in">ptr_</span>(p) {}</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="keyword">void</span>* <span class="title">const</span> <span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (ptr_) <span class="keyword">return</span> ptr_;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">int</span>* ptr_;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>这个方案看起来是比较完美的,上文所提到的各类问题基本都可以完美的避过。所以标准库中,<code>std::cout</code> 就采用了这一种方案用于隐式转换。但是它同时也暗含着一个比较严重的问题,像下面语句是完全合法的。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">IntPtr ptr;</span><br><span class="line">detele ptr;</span><br></pre></td></tr></table></figure><p>根据墨菲定律,可能出问题的话,就一定会出问题。所以还是需要探讨一个更完美的方案。</p><h3 id="使用内部-class-指针进行转换"><a href="#使用内部-class-指针进行转换" class="headerlink" title="使用内部 class 指针进行转换"></a>使用内部 <code>class</code> 指针进行转换</h3><p>使用内部 <code>class</code> 来进行布尔值的隐式转换也是一个不错的思路,具体的做法如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IntPtr</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">IntPtr</span>(<span class="keyword">int</span>* p = <span class="literal">nullptr</span>) : <span class="built_in">ptr_</span>(p) {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">test_class</span>;</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">operator</span> <span class="keyword">const</span> test_class* () <span class="keyword">const</span> {</span><br><span class="line"> <span class="keyword">return</span> ptr_ ? <span class="keyword">reinterpret_cast</span><<span class="keyword">const</span> test_class*>(<span class="keyword">this</span>) : <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">int</span>* ptr_;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>在类的内部创建一个不需要实现的内部类,而后提供一个方法,通过该方法进行类型转换。因为类没有实现,所以类型转换的目标是指向该内部类的指针。虽然这一方案能够实现布尔值转换并进行判断,但是这种方案一样存在着不容忽视的问题。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">IntPtr p1, p2;</span><br><span class="line"><span class="keyword">if</span> (p1 == p2) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (p1 < p2) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>指向数据的指针是具备比较大小的能力的,这个能力在这一方案下反倒成了一个问题。所以该方案的可用性倒不如上面提到的一些方案。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h3 id="explicit-转换函数"><a href="#explicit-转换函数" class="headerlink" title="explicit 转换函数"></a><code>explicit</code> 转换函数</h3><p>从 C++11 开始,这一关键词被引入到转换函数上,用于在语言层面上禁止隐式转换和拷贝初始化。这一关键词的引入,可以在相关<a class="link" href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2333.html" >草案<i class="fas fa-external-link-alt"></i></a>上看到比较完整的提议论述。</p><p>对于开发者而言,这个关键词所带来的好处不言而喻,与本文最相关的一点就是,布尔值的隐式转换问题从此有了一个完美的官方标准方案。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">IntPtr</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">IntPtr</span>(<span class="keyword">int</span>* p = <span class="literal">nullptr</span>) : <span class="built_in">ptr_</span>(p) {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">test_class</span>;</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">explicit</span> <span class="keyword">operator</span> <span class="title">bool</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ptr_;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="keyword">int</span>* ptr_;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>只需要添加一个关键词,即可将类的布尔值隐式转换牢牢限制在判断语句内。</p><h3 id="旧版本的解决方案"><a href="#旧版本的解决方案" class="headerlink" title="旧版本的解决方案"></a>旧版本的解决方案</h3><p>如果是一个 C++11 之前的代码,如何实现一个比较完美的布尔值隐式转换方式呢?上文提到了四种解决方案,从直接使用语言提供的方案,到利用空指针和指向数据的指针。除了这些之外,还缺乏了一个<strong>指向函数的指针</strong>。</p><p>跟平时所使用的指针不同,指向函数的指针式不一样完全不一样的存在。在 C++ FAQ 中,作者有提及,指向成员函数的指针并不单纯的只是一个指针,其更像是一个特殊的数据结构。而且,在语言的标准中,函数和数据的存放位置并没有做定义,因此,指向函数的指针和指向数据的指针不能够互相转换。(<a class="link" href="https://isocpp.org/wiki/faq/pointers-to-members#cant-cvt-memfnptr-to-voidptr" >1<i class="fas fa-external-link-alt"></i></a> <a class="link" href="https://isocpp.org/wiki/faq/pointers-to-members#cant-cvt-fnptr-to-voidptr" >2<i class="fas fa-external-link-alt"></i></a>)。也正因如此,针对于指向数据的指针的那些操作,对于指向函数的指针来说,都是不可行的。</p><p>所以,可以利用指向成员函数的指针来完成这个布尔值的隐式转换操作。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Testable</span> {</span></span><br><span class="line"> <span class="keyword">bool</span> ok_;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">typedef</span> <span class="title">void</span> <span class="params">(Testable::* bool_type)</span><span class="params">()</span> <span class="keyword">const</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">this_type_does_not_support_comparisons</span><span class="params">()</span> <span class="keyword">const</span> </span>{}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="function"><span class="keyword">explicit</span> <span class="title">Testable</span><span class="params">(<span class="keyword">bool</span> b = <span class="literal">true</span>)</span> : ok_(b) {</span>}</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="title">bool_type</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ok_ == <span class="literal">true</span> ?</span><br><span class="line"> &Testable::this_type_does_not_support_comparisons : <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>我们需要定义一个成员函数指针 <code>bool_type</code>,<code>bool_type</code> 的细节并不需要对外暴露,同时,还需要定义一个实现该成员函数指针类型的成员函数 <code>this_type_does_not_support_comparisons</code>。之后就就需要再实现一个类型转换函数,将对象转换为 <code>bool_type</code>。因为 <code>bool_type</code> 是一个指针,所以编译器能够自动对它进行布尔值的转换。</p><p>这一方案基本继承了转换为 <code>void*</code> 的方案,只不过,这里的 <code>void*</code> 被替换为了 <code>bool_type</code>。同时,因为 <code>bool_type</code> 是一个成员函数指针,所以还能规避 <code>void*</code> 会被 <code>delete</code> 的问题。</p><p>不过该方案现在还并非完美,因为隐式转换的目标是一个指针,所以不同的 <code>Testable</code> 对象依旧是可以互相比较的。这个问题其实是一个非常严重的语义问题,所以还是需要再进一步完善。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Testable test;</span><br><span class="line">Testable test2;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (test1 == test2) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (test != test2) {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于这个问题的最优解法就是运算符重载,只需要让使用者在调用了 <code>==</code> 后无法通过编译,就可以一劳永逸的解决这个问题。具体的代码如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> T></span><br><span class="line"><span class="keyword">bool</span> <span class="keyword">operator</span>!=(<span class="keyword">const</span> Testable& lhs, <span class="keyword">const</span> T& rhs) {</span><br><span class="line"> lhs.<span class="built_in">this_type_does_not_support_comparisons</span>();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> T></span><br><span class="line"><span class="keyword">bool</span> <span class="keyword">operator</span>==(<span class="keyword">const</span> Testable& lhs, <span class="keyword">const</span> T& rhs) {</span><br><span class="line"> lhs.<span class="built_in">this_type_does_not_support_comparisons</span>();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们在声明 <code>bool_type</code> 的实现函数时,将其访问权限设置为了 <code>private</code>,这样就无法通过外界进行访问。因此在重载 <code>==</code> 的时候,可以通过调用一个无法访问的函数的方式来阻止编译。</p><p>因此,通过上述的操作,就可以在并不带来副作用的情况下,完美解决 <code>Testable</code> 对象的比较问题。</p><h3 id="旧方案的推广方案"><a href="#旧方案的推广方案" class="headerlink" title="旧方案的推广方案"></a>旧方案的推广方案</h3><p>既然实现方案有了,接下来就是如何将其变为一个库,供各路调用者直接调用。一个简单的实现如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">safe_bool_base</span> {</span></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="built_in">safe_bool_base</span>() {}</span><br><span class="line"> <span class="built_in">safe_bool_base</span>(<span class="keyword">const</span> safe_bool_base&) {}</span><br><span class="line"> safe_bool_base& <span class="keyword">operator</span>=(<span class="keyword">const</span> safe_bool_base&) { <span class="keyword">return</span> *<span class="keyword">this</span>; }</span><br><span class="line"> ~<span class="built_in">safe_bool_base</span>() {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span>:</span><br><span class="line"> <span class="function"><span class="keyword">typedef</span> <span class="title">void</span> <span class="params">(safe_bool_base::* bool_type)</span><span class="params">()</span> <span class="keyword">const</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">this_type_does_not_support_comparisons</span><span class="params">()</span> <span class="keyword">const</span> </span>{}</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> T = <span class="keyword">void</span>> class safe_bool : <span class="keyword">public</span> safe_bool_base {</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="keyword">operator</span> <span class="built_in">bool_type</span>() <span class="keyword">const</span> {</span><br><span class="line"> <span class="built_in"><span class="keyword">return</span></span> (<span class="keyword">static_cast</span><<span class="keyword">const</span> T*>(<span class="keyword">this</span>))-><span class="built_in">boolean_test</span>()</span><br><span class="line"> ? &safe_bool_base::this_type_does_not_support_comparisons : <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"><span class="keyword">protected</span>:</span><br><span class="line"> ~<span class="built_in">safe_bool</span>() {}</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span><> <span class="class"><span class="keyword">class</span> <span class="title">safe_bool</span><</span><span class="keyword">void</span>> : <span class="keyword">public</span> safe_bool_base {</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="function"><span class="keyword">operator</span> <span class="title">bool_type</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">boolean_test</span>() == <span class="literal">true</span> ?</span><br><span class="line"> &safe_bool_base::this_type_does_not_support_comparisons : <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"><span class="keyword">protected</span>:</span><br><span class="line"> <span class="function"><span class="keyword">virtual</span> <span class="keyword">bool</span> <span class="title">boolean_test</span><span class="params">()</span> <span class="keyword">const</span> </span>= <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">virtual</span> ~<span class="built_in">safe_bool</span>() {}</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> T, <span class="keyword">typename</span> U></span><br><span class="line"><span class="keyword">void</span> <span class="keyword">operator</span>==(<span class="keyword">const</span> safe_bool<T>& lhs, <span class="keyword">const</span> safe_bool<U>& rhs) {</span><br><span class="line"> lhs.<span class="built_in">this_type_does_not_support_comparisons</span>();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> T, <span class="keyword">typename</span> U></span><br><span class="line"><span class="keyword">void</span> <span class="keyword">operator</span>!=(<span class="keyword">const</span> safe_bool<T>& lhs, <span class="keyword">const</span> safe_bool<U>& rhs) {</span><br><span class="line"> lhs.<span class="built_in">this_type_does_not_support_comparisons</span>();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Testable_with_virtual</span> :</span> <span class="keyword">public</span> safe_bool<> {</span><br><span class="line"><span class="keyword">protected</span>:</span><br><span class="line"> <span class="function"><span class="keyword">bool</span> <span class="title">boolean_test</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="comment">// Perform Boolean logic here</span></span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Testable_without_virtual</span> :</span></span><br><span class="line"> <span class="keyword">public</span> safe_bool <Testable_without_virtual> {</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="function"><span class="keyword">bool</span> <span class="title">boolean_test</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="comment">// Perform Boolean logic here</span></span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>这里比较有意思的是,原文作者提供了两种调用方案。一种是通过继承 <code>safe_bool</code> 类,另一种是通过继承 <code>safe_bool</code> 模板类,这两者的区别就在于虚函数的调用开销。</p><h2 id="汇总"><a href="#汇总" class="headerlink" title="汇总"></a>汇总</h2><p>这个问题有意思的地方在于,它是一个非常简单的问题,但是却有着非常多的解决方案。这些解决方案的优劣性也各不相同,但是都能够解决这个问题。这也是 C++ 的一个特点,它的灵活性非常强,但是也因此导致了很多的问题。这也是为什么 C++ 有很多的编码规范,以及很多的编码规范的原因。原文详见:<a class="link" href="http://www.artima.com/cppsource/safebool.html" >Safe Bool Idiom<i class="fas fa-external-link-alt"></i></a>。</p>]]></content>
<summary type="html">对象如何正确地转化为布尔值</summary>
<category term="C++" scheme="https://wzzzx.github.io/categories/C/"/>
</entry>
<entry>
<title>os-comp-记录</title>
<link href="https://wzzzx.github.io/OS/os_comp_daily_schedule/"/>
<id>https://wzzzx.github.io/OS/os_comp_daily_schedule/</id>
<published>2022-10-19T16:50:16.000Z</published>
<updated>2022-11-13T10:14:44.000Z</updated>
<content type="html"><![CDATA[<p>这一博文用于记录每日所学</p><span id="more"></span><h3 id="2022-10-20"><a href="#2022-10-20" class="headerlink" title="2022/10/20"></a>2022/10/20</h3><p>以 <a class="link" href="https://course.rs/about-book.html" >Rust 语言圣经<i class="fas fa-external-link-alt"></i></a> 作为学习材料来学习 Rust 语言。目前进度看到<a class="link" href="https://course.rs/basic/compound-type/string-slice.html" >字符串与切片<i class="fas fa-external-link-alt"></i></a>。不得不说,Rust 还是有点难度的。</p><p>今日感悟:</p><ol><li>表达式与语句。这东西确实是 C++ 所不在意的,目前还是以 C++ 的理解来认识这一表达式,但是语句没想到啥很好的理解方式。暂且忽略</li><li>学习所有权的时候,突然能理解到 C++ 的移动语义具体是在说什么了。所以这里目前也不是什么难点</li><li>借用和解引用让人感觉特别繁琐且麻烦</li></ol><h3 id="2022-10-21"><a href="#2022-10-21" class="headerlink" title="2022/10/21"></a>2022/10/21</h3><p>今日进度:<a class="link" href="https://course.rs/basic/compound-type/tuple.html" >元组<i class="fas fa-external-link-alt"></i></a> -> <a class="link" href="https://course.rs/basic/flow-control.html" >流程控制<i class="fas fa-external-link-alt"></i></a></p><ol><li>复合类型有点麻烦!</li><li>枚举的特性在 rust 里是一个挺不错的存在,能够包罗万千,我很喜欢这种东西</li><li>结构体内部变量的所有权也能够转移出去,感觉这个特性很可怕,还是得悠着点用。其他的地方倒和 C++ 没啥太大区别</li><li>流程控制这里,for 循环可能导致的所有权转移需要额外注意下,不过用久了估计就是习惯性加上引用</li></ol><h3 id="2022-11-02"><a href="#2022-11-02" class="headerlink" title="2022/11/02"></a>2022/11/02</h3><p>今日感悟:本来打算暂时放弃学习了,想着以现在工作的语言为主,先好好学学 C++ 的模板,不过突然仔细想了想,也不见得我就真乖乖去学丫,还是捡起来吧。这回以测试为主,边测试边啃,快速过完 rust 的基本概念,之后学习用到了再来啃。</p><ol><li>学到了个 as 关键词,看起来是用来转换类型的</li><li>原来序列可以划分为两类,… 和 …=,这是之前忽视的地方</li><li>单元类型占用空内存,这倒跟 C++中空类不一样,这样怎么知道这个单元类型的具体位置呢?</li><li>rust 的字符原来采用的是 Unicode 编码,这个和 C++ 的 char 类型不一样,C++ 的 char 类型是 ASCII 编码,这个需要注意下,之前一直以为是 utf-8。</li><li>表达式和语句究竟是怎样的一个区别到现在都还没搞懂,暂时也只能记住 += 这东西不是表达式</li></ol><h3 id="2022-11-05"><a href="#2022-11-05" class="headerlink" title="2022/11/05"></a>2022/11/05</h3><p>今日感悟:太忙了太忙了,工作后要学习下真的好难抽出时间。</p><ol><li>单个字符采用 Unicode 编码,占用四个字节,但是字符串却是采用 UTF-8 编码。这个地方确实有点诡异</li><li>借用和所有权那里倒感觉还好,这套机制挺不错的,但是学习起来难度并不大,所有权最为简单的理解就是 <code>std::unique_ptr</code></li><li>枚举看来看去,感觉就是一个带名字的元组,同时提供了一个父类的概念出来</li><li>始终 <code>if let</code> 语句有点难以理解,从 <a class="link" href="https://stackoverflow.com/questions/65684540/how-to-read-if-let-expressions" >StackOverflow<i class="fas fa-external-link-alt"></i></a> 上找到一个回答,虽然也没有很好的解释,但是从评论找到了 <a class="link" href="https://doc.rust-lang.org/book/ch18-02-refutability.html" >Refutability: Whether a Pattern Might Fail to Match<i class="fas fa-external-link-alt"></i></a> 这个章节,倒是解决了一点关于模式匹配的困惑。<code>let</code> 的要求是不可驳模式匹配,所以 <code>if let PATTERN = SOME_VALUE {}</code> 这里可以这么理解,如果 pattern 能够匹配到 SOME_VALUE,那么执行后续的表达式。这样即便 PATTERN 会是 <code>Some(x)</code> 需要赋值的东西,也可以通过匹配来理解。</li><li>大抵是 C++ 写多了,对于诸如 <strong>匹配守卫</strong>,**@绑定** 这类语法糖,唯一的感觉就是好繁琐</li></ol><p>番外:<br>我们可以写出这样的代码:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> o: <span class="built_in">Option</span><<span class="built_in">i32</span>> = <span class="literal">Some</span>(<span class="number">5</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> a = <span class="keyword">if</span> <span class="keyword">let</span> <span class="literal">Some</span>(x) = <span class="literal">Some</span>(o) {</span><br><span class="line"> o</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>编译的时候一定会报错:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">= note: expected unit type `()`</span><br><span class="line"> found enum `Option<i32>`</span><br><span class="line">= note: `if` expressions without `else` evaluate to `()`</span><br><span class="line">= help: consider adding an `else` block that evaluates to the expected type</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>根据 <a class="link" href="https://doc.rust-lang.org/book/ch18-02-refutability.html" >Refutability: Whether a Pattern Might Fail to Match<i class="fas fa-external-link-alt"></i></a> 所了解到的,一个简单的解释角度就是,<code>let</code> 这东西所接受的值是 irrefutable 的,如果 o 是 None 的话,将不会产生任何的匹配,<code>let</code> 就接受不到任何的东西。在 <a class="link" href="https://stackoverflow.com/questions/43335193/why-does-an-if-without-an-else-always-result-in-as-the-value" >StackOverflow<i class="fas fa-external-link-alt"></i></a> 也有一个回答,但是不知道他想表达什么。</p><h3 id="2022-11-06"><a href="#2022-11-06" class="headerlink" title="2022/11/06"></a>2022/11/06</h3><ol><li>在刷泛型的题目的时候,发现第一题所声明的结构体就看不懂,仔细翻了翻回去,才发现还有元组结构体这么个玩意儿</li><li>模板,泛型这东西,确实有点难,很不好理解。关掉 GitHub Copilot 后按着示例一点点写一遍代码,感觉还是有点繁琐,但是清晰了很多</li></ol><h3 id="2022-11-12"><a href="#2022-11-12" class="headerlink" title="2022/11/12"></a>2022/11/12</h3><ol><li>几天没看书了,确实忙啊</li><li>特征这东西确实不简单,不过暂时也只是了解了而已。不理解为什么不整个继承,却要使用特征对象这样的东西</li></ol><h3 id="2022-11-13"><a href="#2022-11-13" class="headerlink" title="2022/11/13"></a>2022/11/13</h3><p>今日感悟:基础部分这就算是学完了,接下来该刷题目了</p><ol><li>类型转换也是个麻烦事,不过一时半会暂时可以不管</li><li>后续的基础内容看起来还好,没啥特别要紧的。</li></ol>]]></content>
<summary type="html">用于记录 os comp 的学习过程</summary>
<category term="OS" scheme="https://wzzzx.github.io/categories/OS/"/>
</entry>
<entry>
<title>如何学好英语</title>
<link href="https://wzzzx.github.io/english/how-to-learn-english/"/>
<id>https://wzzzx.github.io/english/how-to-learn-english/</id>
<published>2022-07-30T15:11:25.000Z</published>
<updated>2022-07-30T15:41:31.000Z</updated>
<content type="html"><![CDATA[<p>阅读完李笑来老师的<a class="link" href="https://github.com/xiaolai/everyone-can-use-english" >人人都能用英语<i class="fas fa-external-link-alt"></i></a>后,感觉颇有收获,简单进行了一点摘要。其次,以前曾在 V 站看到了一篇学习英语的帖子,其中介绍的单词列表挺不错的,所以也一起放了进来。</p><span id="more"></span><h2 id="口语"><a href="#口语" class="headerlink" title="口语"></a>口语</h2><ol><li>最大的问题不是不会说,而是不知道说什么</li><li>日常整理所用中文表达的内容,尝试翻译为英语</li><li>着重收集一些不直观的表达,这些就是地道的关键</li><li>长篇大论一定要先写下来</li><li>多批改往日的作文和别人的作文</li><li>复述是一个有效简单的手段</li></ol><h2 id="语音"><a href="#语音" class="headerlink" title="语音"></a>语音</h2><ol><li>提高听力输入量</li><li>直接上正常语速的材料</li><li>每日听力保持四小时,听不懂无所谓</li><li>语速放慢,注意停顿,可以刻意练习停顿</li><li>恶补音标,一定要对读音上心,多查音标</li><li>学音标前一定要有一定的朗读经验,认真学习每个音标和例词的读法</li><li>不要忽略单词的拼读规则</li><li>英式发音及其音标:牛津大学出版社香港中文站的 Flash 教程《Guide to English Phonetc Symbols》</li><li>美式发音及其音标:爱荷华大学的美国音标在线学习程序《Phonetics: The Sound of American English》</li><li>跟读训练步骤:<ol><li>精读文本,查好所有不认识的单词。标记好音标,特别是重音和元音</li><li>反复听录音,标记好连读,浊化等地方</li><li>熟悉后反复跟读。先听一句读一句,再影子跟读,最后同步跟读</li><li>一段时间后录音矫正,录完后隔段时间再去听和纠正自己</li><li>完成上述步骤后,背诵</li></ol></li></ol><h2 id="朗读"><a href="#朗读" class="headerlink" title="朗读"></a>朗读</h2><ol><li>朗读是语文教育的最古老、最普及、成本最低、效果最好的训练方式</li></ol><h2 id="词典"><a href="#词典" class="headerlink" title="词典"></a>词典</h2><ol><li>不必迷信英英词典,要互相对照使用</li><li>对于英英词典,可以从下面的单词入手,先熟悉起来</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">a, about, above, across, act, after, against, all, along, and, any, appear, arrange, as, at, be, back, before, behind, below, between, beyond, break, bring, but, by, can, carry, catch, charge, come, cover, cut, die, do, down, drive, drop, due, enter, ever, fall, feel, figure, fill, find, follow, foot, for, from, get, give, go, good, hand, have, hear, high, hold, home, if, in, it, keep, lead, let, life, like, line, little, live, look, lose, low, make, man, matter, may, move, must, nice, no, not, now, of, off, on, one, only, often, or, order, out, over, pass, play, point, poor, put, raise, rather, reach, read, return, right, rise, round, run, save, say, see, send, serve, set, settle, shall, sit, so, some, spare, speak, stand, strike, such, take, tell, that, the, then, there, this, through, time, to, treat, turn, under, up, use, walk, want, way, wall, what, when, where, why, will, with, without, word, work, world, write, yet.</span><br></pre></td></tr></table></figure><h2 id="语法"><a href="#语法" class="headerlink" title="语法"></a>语法</h2><ol><li>任何自然语言的语法总是由许多的充满例外的规则构成</li><li>做替换练习是掌握语法知识的最有效方法</li><li>《朗文英语语法》和《朗文高级英语语法参考及练习》</li><li>《剑桥中级英语语法》花半年时间弄三遍以上。例句非常的好,甚至可以当口语教材使用</li><li>学习外语的真正难点在于母语和外语这两种语言之间的非一一对应之处,两种语言重合、一一对应的部分,是容易学会的。</li><li>学习步骤。通读,认真阅读例句,做替换练习</li></ol><h2 id="精读"><a href="#精读" class="headerlink" title="精读"></a>精读</h2><ol><li>搞清楚每句话的含义</li><li>整理每句话之间的关系,概括段落</li><li>整理词汇</li><li>反复阅读,可以发现之前所没有发现的东西</li><li>通过口头或书面复述文章</li><li>若干天后要复习</li></ol>]]></content>
<summary type="html">这是一篇读后感或者总结</summary>
<category term="english" scheme="https://wzzzx.github.io/categories/english/"/>
</entry>
<entry>
<title>中文编码变化史</title>
<link href="https://wzzzx.github.io/encoding/chinese-encoding-history/"/>
<id>https://wzzzx.github.io/encoding/chinese-encoding-history/</id>
<published>2022-06-08T16:12:29.000Z</published>
<updated>2022-08-15T17:16:03.000Z</updated>
<content type="html"><![CDATA[<p>因为匮乏一个系统性的介绍,并且现在大多数情况下都是使用 UTF-8 这样的 Unicode 编码,导致一直以来都是懵懵懂懂的应对中文编码,所以这篇博客会简要的介绍一下中文编码的发展历史,以便对中文编码的发展历史有基本的了解。</p><span id="more"></span><h2 id="中文编码发展史"><a href="#中文编码发展史" class="headerlink" title="中文编码发展史"></a>中文编码发展史</h2><p>按发布时间介绍我们所使用过的几个比较重要的标准。</p><h3 id="GB2312"><a href="#GB2312" class="headerlink" title="GB2312"></a>GB2312</h3><p><code>GB2312</code> 是 1981 年发布的汉字编码国家标准,该标准采用双字节编码,但是只收录了 6763 个汉字和 682 个其它符号。文字的缺失问题相对来说还是比较严重的,只能勉强使用。而且也没有收录相关少数民族的语言。</p><h3 id="GB13000"><a href="#GB13000" class="headerlink" title="GB13000"></a>GB13000</h3><p>1993 年,国际标准制定了 Unicode 1.1 的标准,在这一背景,中国推出了 <code>GB13000</code> 标准。<br>该标准收录了 <code>GB2312</code>,Unicode 1.1 等标准所定义的字符,总字符数达到 20902 个。该标准也是一个特立独行的标准,除了 <code>ASCII</code>,它不支持过往的标准。</p><h3 id="GBK"><a href="#GBK" class="headerlink" title="GBK"></a>GBK</h3><p>因为 <code>GB2312</code> 的字符缺失问题比较严重,微软在此基础上利用 <code>GB2312</code> 没有利用到的编码空间,扩展了 <code>GBK</code> 编码,后国家将其作为一个规范。该标准收录了 <code>GB13000</code> 所有字符,但是能兼容 <code>GB2312</code>。该编码目前也在广泛的使用。</p><h3 id="GB18030"><a href="#GB18030" class="headerlink" title="GB18030"></a>GB18030</h3><p><code>GBK</code> 编码仅仅是一个规范,在此基础上,国家制定了 <code>GB18030</code> 编码标准,该标准收录了 27484 个汉字,不对 <code>GB13000</code> 做兼容处理。但是目前这一标准在 Windows 系统里并没有真正的启用。详细的展开可以看这篇<a class="link" href="https://blog.csdn.net/jaminwm/article/details/8033805" >博客<i class="fas fa-external-link-alt"></i></a></p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>可以通过简单的几个维度来总结一下这些编码。</p><ol><li>使用范围,目前使用比较广泛的就是 <code>GBK</code> 了,Windows 简体中文默认就是使用该编码,但是国家标准里要求,所有中文平台都应该支持 <code>GB18030</code></li><li>从字节数的角度来看,这些字符集都是<strong>双字节字符集</strong> DBCS(Double Byte Character Set)</li><li>从兼容性的角度来看,这些字符编码的发布时间,兼容性可以通过下图快速记住。</li></ol><p><img lazyload src="/images/loading.svg" data-src="https://git.poker/Wzzzx/image/blob/main/chinese-encoding-history/1.5ts3uolxg7g0.webp?raw=true" alt="编码集关系" ></p>]]></content>
<summary type="html">简要介绍中文编码的发展历史</summary>
<category term="encoding" scheme="https://wzzzx.github.io/categories/encoding/"/>
</entry>
<entry>
<title>「翻译」Windows 的极限:物理内存</title>
<link href="https://wzzzx.github.io/OS/Pushing-the-Limits-of-Windows-Physical-Memory/"/>
<id>https://wzzzx.github.io/OS/Pushing-the-Limits-of-Windows-Physical-Memory/</id>
<published>2022-02-14T14:10:38.000Z</published>
<updated>2022-02-19T12:42:29.000Z</updated>
<content type="html"><![CDATA[<blockquote><p>翻译来源 <a class="link" href="https://techcommunity.microsoft.com/t5/windows-blog-archive/pushing-the-limits-of-windows-physical-memory/ba-p/723674" >Pushing the Limits of Windows: Physical Memory<i class="fas fa-external-link-alt"></i></a></p></blockquote><h2 id="Physical-Memory(物理内存)"><a href="#Physical-Memory(物理内存)" class="headerlink" title="Physical Memory(物理内存)"></a>Physical Memory(物理内存)</h2><p>One of the most fundamental resources on a computer is physical memory. Windows’ memory manager is responsible with populating memory with the code and data of active processes, device drivers, and the operating system itself. Because most systems access more code and data than can fit in physical memory as they run, physical memory is in essence a window into the code and data used over time. The amount of memory can therefore affect performance, because when data or code a process or the operating system needs is not present, the memory manager must bring it in from disk.</p><p>物理内存是计算机最为基础的资源之一。Windows 的内存管理器会给活跃进程,硬件驱动和操作系统自身的代码和数据分配相应的内存空间。因为大多数系统会访问比物理内存大得多的代码和数据,所以物理内存实际上更像是一个窗口。当进程或操作系统所需的数据或代码无法访问时,内存管理器就会从硬盘进行加载,所以物理内存的大小会对性能产生影响。</p><p>Besides affecting performance, the amount of physical memory impacts other resource limits. For example, the amount of non-paged pool, operating system buffers backed by physical memory, is obviously constrained by physical memory. Physical memory also contributes to the system virtual memory limit, which is the sum of roughly the size of physical memory plus the maximum configured size of any paging files. Physical memory also can indirectly limit the maximum number of processes, which I’ll talk about in a future post on process and thread limits.</p><p>除了影响性能,物理内存的大小也会限制其他资源。例如,由物理内存大小所支持的操作系统缓冲池,非分页缓冲池 (<a class="link" href="https://developer.aliyun.com/article/464466" >non-paged pool<i class="fas fa-external-link-alt"></i></a>) 的大小。物理内存同样对虚拟内存的大小有影响,虚拟内存的大小大体上由物理内存大小加上 <a class="link" href="https://docs.microsoft.com/en-us/windows/client-management/introduction-page-file" >paging files<i class="fas fa-external-link-alt"></i></a> 可配置的最大大小。物理内存还会间接地影响到最大进程数,这个会将后面谈到进程和线程数量极限的文章中提到。</p><h2 id="Windows-Server-Memory-Limits(Windows-Server-内存限制)"><a href="#Windows-Server-Memory-Limits(Windows-Server-内存限制)" class="headerlink" title="Windows Server Memory Limits(Windows Server 内存限制)"></a>Windows Server Memory Limits(Windows Server 内存限制)</h2><p>Windows support for physical memory is dictated by hardware limitations, licensing, operating system data structures, and driver compatibility. The Memory Limits for Windows Releases page in MSDN documents the limits for different Windows versions, and within a version, by SKU.</p><p>Windows 对内存的限制主要取决于硬件限制,许可证,操作系统数据结构和驱动兼容性。MSDN 中的 Windows 版本<a class="link" href="https://docs.microsoft.com/en-us/windows/win32/memory/memory-limits-for-windows-releases?redirectedfrom=MSDN" >内存限制页面<i class="fas fa-external-link-alt"></i></a>记录了不同 Windows 版本的限制,以及在一个版本中,按 SKU 的限制。</p><p>You can see physical memory support licensing differentiation across the server SKUs for all versions of Windows. For example, the 32-bit version of Windows Server 2008 Standard supports only 4GB, while the 32-bit Windows Server 2008 Datacenter supports 64GB. Likewise, the 64-bit Windows Server 2008 Standard supports 32GB and the 64-bit Windows Server 2008 Datacenter can handle a whopping 2TB. There aren’t many 2TB systems out there, but the Windows Server Performance Team knows of a couple, including one they had in their lab at one point. Here’s a screenshot of Task Manager running on that system:</p><p>你可以看到在不同 Windows 版本的 SKU 上有着不同的物理内存支持许可。例如,32 位的 Windows Server 2008 Standard 只支持 4 GB,而 Windows Server 2008 Datacenter 能够支持 64 GB。与此同时,64 位的 Windows Server 2008 Standard 支持 32 GB,而 64 位的 Windows Server 2008 Datacenter 能够支持 2 TB。外界没有很多 2 TB 大小的系统,但是 Windows Server Performance Team 知道一些,其中一台就在它们的实验室里。下图是那个系统的任务管理器截图:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121037i8A1A9892115C2119.5yfsh7f3w5c0.webp" alt="121037i8A1A9892115C2119" ></p><p>The maximum 32-bit limit of 128GB, supported by Windows Server 2003 Datacenter Edition, comes from the fact that structures the Memory Manager uses to track physical memory would consume too much of the system’s virtual address space on larger systems. The Memory Manager keeps track of each page of memory in an array called the PFN database and, for performance, it maps the entire PFN database into virtual memory. Because it represents each page of memory with a 28-byte data structure, the PFN database on a 128GB system requires about 980MB. 32-bit Windows has a 4GB virtual address space defined by hardware that it splits by default between the currently executing user-mode process (e.g. Notepad) and the system. 980MB therefore consumes almost half the available 2GB of system virtual address space, leaving only 1GB for mapping the kernel, device drivers, system cache and other system data structures, making that a reasonable cut off:</p><p>Windows Server 2003 Datacenter 32 位版本最大 128 GB 物理内存的限制,是因为内存管理器用来管理内存的数据结构在大内存的系统上会占用过多的虚拟内存空间。内存管理器在一个被称为 <a class="link" href="https://rayanfam.com/topics/inside-windows-page-frame-number-part1/" >PFN<i class="fas fa-external-link-alt"></i></a> 数据库的数组中追踪内存中所有的页,并且整个 PFN 数据库会被映射于虚拟内存中用于提升性能。因为它用一个 28 字节的数据结构来标记每一页,所以在 128 GB 的系统上会占用 980 MB 的空间。32 位 Windows 由硬件决定的 4 GB 虚拟内存空间会被默认划分为正在运行的用户态程序(例如记事本)和操作系统。2 GB 的系统虚拟地址空间被 980 MB 消耗了接近一半,仅留了 1 GB 的空间用于映射内核,设备驱动器,系统缓存和其他系统数据结构,所以理所当然做了限制。</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121038i10B7969ABC196455.2xj5g4oev9u0.webp" alt="121038i10B7969ABC196455" ></p><p>That’s also why the memory limits table lists lower limits for the same SKU’s when booted with 4GB tuning (called 4GT and enabled with the Boot.ini’s /3GB or /USERVA, and Bcdedit’s /Set IncreaseUserVa boot options), because 4GT moves the split to give 3GB to user mode and leave only 1GB for the system. For improved performance, Windows Server 2008 reserves more for system address space by lowering its maximum 32-bit physical memory support to 64GB.</p><p><a class="link" href="https://docs.microsoft.com/en-us/windows/win32/memory/4-gigabyte-tuning" >4GT<i class="fas fa-external-link-alt"></i></a> 会分配 3 GB 空间给用户态,只留下 1 GB 给操作系统,这就是为什么内存限制表会对相同 SKU 但是以 4GB tuning(被称为 4GT,通过 Boot.ini 的 /3GB 和 /USERVA 命令和 Bcdedit’s /Set IncreaseUserVa boot 选项开启)启动的系统列出更低的限制。</p><p>The Memory Manager could accommodate more memory by mapping pieces of the PFN database into the system address as needed, but that would add complexity and possibly reduce performance with the added overhead of map and unmap operations. It’s only recently that systems have become large enough for that to be considered, but because the system address space is not a constraint for mapping the entire PFN database on 64-bit Windows, support for more memory is left to 64-bit Windows.</p><p>内存管理器可以通过映射部分 PFN 数据库 到系统地址空间的方式来容纳更多的内存,但是这些额外的映射和取消映射的开销会增加系统复杂度和降低性能。直到现在系统变得足够得大才开始去考虑这些问题,但是因为在 64 位 Windows 上映射整个 PFN 数据库并不会限制系统地址空间,对更大内存的支持就留给了 64 位 Windows。</p><p>The maximum 2TB limit of 64-bit Windows Server 2008 Datacenter doesn’t come from any implementation or hardware limitation, but Microsoft will only support configurations they can test. As of the release of Windows Server 2008, the largest system available anywhere was 2TB and so Windows caps its use of physical memory there.</p><p>64 位 Windows Server 2008 Datacenter 最大支持 2 TB 的限制并不是因为任何实现或硬件限制,仅仅是因为 Microsoft 只会支持它们所能测试的配置。截止到 Windows Server 2008 的发行,世界上最大的系统内存是 2 TB,所以 Windows 在物理内存上是有限制的。</p><h2 id="Windows-Client-Memory-Limits(Windows-用户版内存限制)"><a href="#Windows-Client-Memory-Limits(Windows-用户版内存限制)" class="headerlink" title="Windows Client Memory Limits(Windows 用户版内存限制)"></a>Windows Client Memory Limits(Windows 用户版内存限制)</h2><p>64-bit Windows client SKUs support different amounts of memory as a SKU-differentiating feature, with the low end being 512MB for Windows XP Starter to 128GB for Vista Ultimate and 192GB for Windows 7 Ultimate. All 32-bit Windows client SKUs, however, including Windows Vista, Windows XP and Windows 2000 Professional, support a maximum of 4GB of physical memory. 4GB is the highest physical address accessible with the standard x86 memory management mode. Originally, there was no need to even consider support for more than 4GB on clients because that amount of memory was rare, even on servers.</p><p>从低端的 512 MB 的 Windows XP 的内存到 Vista Ultimate 的 128 GB 的内存再到 Windows 7 Ultimate 的 192 GB,64 位的 Windows 用户版以不同的数量的内存支持作为 SKU 的特性区分。然而,所有的 32 位 Windows 用户版 SKU,包括 Windows Vista, Windows XP 和 Windows 2000 Professional 都只支持最大 4 GB 的物理内存。标准 x86 内存管理器模式最大只能支持 4 GB 的物理地址寻址空间。最开始的时候,在用户版并不需要额外考虑支持超过 4 GB 的内存,因为这么大内存的机器即便是在服务端都很罕见。</p><p>However, by the time Windows XP SP2 was under development, client systems with more than 4GB were foreseeable, so the Windows team started broadly testing Windows XP on systems with more than 4GB of memory. Windows XP SP2 also enabled Physical Address Extensions (PAE) support by default on hardware that implements no-execute memory because its required for Data Execution Prevention (DEP), but that also enables support for more than 4GB of memory.</p><p>然而,当时正在进行着 Windows XP SP2 的开发,可以预见客户端的系统内存将会超过 4 GB,所以 Windows 团队开始在内存超过 4GB 的系统上广泛测试 Windows XP。因为数据执行保护 (<a class="link" href="https://docs.microsoft.com/en-us/windows/win32/memory/data-execution-prevention" >DEP<i class="fas fa-external-link-alt"></i></a>) 的原因,Windows XP SP2 默认支持在实现了 <a class="link" href="https://en.wikipedia.org/wiki/Executable_space_protection" >no-execute memory<i class="fas fa-external-link-alt"></i></a> 的硬件上开启物理地址扩展 (<a class="link" href="https://docs.microsoft.com/en-us/windows/win32/memory/physical-address-extension" >PAE<i class="fas fa-external-link-alt"></i></a>) 支持,这也让其能够支持超过 4 GB 大小的内存。</p><p>What they found was that many of the systems would crash, hang, or become unbootable because some device drivers, commonly those for video and audio devices that are found typically on clients but not servers, were not programmed to expect physical addresses larger than 4GB. As a result, the drivers truncated such addresses, resulting in memory corruptions and corruption side effects. Server systems commonly have more generic devices and with simpler and more stable drivers, and therefore hadn’t generally surfaced these problems. The problematic client driver ecosystem led to the decision for client SKUs to ignore physical memory that resides above 4GB, even though they can theoretically address it.</p><p>他们发现许多系统会因为一些硬件驱动而崩溃,挂起或无法启动,这通常是一些客户端上特有的,但服务端不会有的音视频驱动,这些驱动编写时并没有考虑物理内存超过 4 GB 的情况。因此,程序截断了这些地址,导致了一些内存损坏和副作用。服务端系统一半会有更通用的驱动,这些驱动更为简单和健壮,因此一般不会面对这个问题。这些有问题的客户端驱动生态导致客户端 SKUs 即便理论上能够访问超过 4 GB 的物理内存地址,也决定去忽略它,只使用 4 GB 以内的地址空间。</p><h2 id="32-bit-Client-Effective-Memory-Limits(32-位客户端有效内存的限制)"><a href="#32-bit-Client-Effective-Memory-Limits(32-位客户端有效内存的限制)" class="headerlink" title="32-bit Client Effective Memory Limits(32 位客户端有效内存的限制)"></a>32-bit Client Effective Memory Limits(32 位客户端有效内存的限制)</h2><p>While 4GB is the licensed limit for 32-bit client SKUs, the effective limit is actually lower and dependent on the system’s chipset and connected devices. The reason is that the physical address map includes not only RAM, but device memory as well, and x86 and x64 systems map all device memory below the 4GB address boundary to remain compatible with 32-bit operating systems that don’t know how to handle addresses larger than 4GB. If a system has 4GB RAM and devices, like video, audio and network adapters, that implement windows into their device memory that sum to 500MB, 500MB of the 4GB of RAM will reside above the 4GB address boundary, as seen below:</p><p>即便许可证限制了 32 位客户端 SKUs 只能访问 4 GB,但有效内存的限制其实会更低,它取决于系统芯片和所连接的设备。原因是物理内存不只映射 RAM 地址,x86 和 x64 系统为了兼容 32 位的操作系统不知道如何处理超过 4 GB 的地址空间的问题,将所有的硬件地址都映射到了 4 GB 以内的地址空间中。如果一个系统有 4 GB 的 RAM,硬件内存大小为 500 MB 的硬件设备,例如音视频设备和网络驱动器,那么在这 4 GB 的 RAM 中,有 500 MB 会被它们所占用,所下图所示:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121039i7E8269B9271C660F.3e6llup17920.webp" alt="121039i7E8269B9271C660F" ></p><p>The result is that, if you have a system with 3GB or more of memory and you are running a 32-bit Windows client, you may not be getting the benefit of all of the RAM. On Windows 2000, Windows XP and Windows Vista RTM, you can see how much RAM Windows has accessible to it in the System Properties dialog, Task Manager’s Performance page, and, on Windows XP and Windows Vista (including SP1), in the Msinfo32 and Winver utilities. On Window Vista SP1, some of these locations changed to show installed RAM, rather than available RAM, as documented in this Knowledge Base article.</p><p>这样会导致你无法在一个装有 3 GB 或更大内存的 32 位 Windows 客户端上享受所有内存的好处。在 Windows 2000, Windows XP 和 Windows Vista RTM 上,你可以在系统属性弹窗,任务管理器的性能页查看到 Windows 能够访问到多少内存,在<br>Windows XP 和 Windows Vista(包括 SP1)还可以使用 Msinfo32 和 Winver 工具。正如这篇<a class="link" href="https://support.microsoft.com/zh-cn/topic/windows-vista-sp1-%E5%8C%85%E6%8B%AC%E6%8A%A5%E5%91%8A%E7%9A%84%E5%AE%89%E8%A3%85%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98-ram-646be31d-1a07-bed9-4929-e1ab2ca468e9" >文档<i class="fas fa-external-link-alt"></i></a>所说,在 Window Vista SP1 上,一些地方不显示可用 RAM,改为显示已安装的 RAM。</p><p>On my 4GB laptop, when booted with 32-bit Vista, the amount of physical memory available is 3.5GB, as seen in the Msinfo32 utility:</p><p>我 4 GB 的笔记上一旦使用 32 位的 Vista,Msinfo32 所显示的可用物理内存就会是 3.5 GB:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121040i7A1417E484652CAA.1jl026z22pfk.webp" alt="121040i7A1417E484652CAA" ></p><p>You can see physical memory layout with the Meminfo tool by Alex Ionescu (who’s contributing to the 5th Edition of the Windows Internals that I’m coauthoring with David Solomon ). Here’s the output of Meminfo when I run it on that system with the -r switch to dump physical memory ranges:</p><p>你可以通过 <a class="link" href="http://www.alex-ionescu.com/" >Alex Ionescu<i class="fas fa-external-link-alt"></i></a> 的 <a class="link" href="http://www.winsiderss.com/tools/meminfo/meminfo.htm" >Meminfo<i class="fas fa-external-link-alt"></i></a> 工具查看物理内存结构,(他正在为我和 <a class="link" href="https://www.hugedomains.com/domain_profile.cfm?d=solsem.com" >David Solomon<i class="fas fa-external-link-alt"></i></a> 共同编写的第五版 Windows 内部资料提供帮助)。下图是我在系统上使用 -r 这个命令去打印物理内存排列时,Meminfo 的输出:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121041i4AA3C3702DCF35A4.5ml8ti9vs6s0.webp" alt="121041i4AA3C3702DCF35A4" ></p><p>Note the gap in the memory address range from page 9F0000 to page 100000, and another gap from DFE6D000 to FFFFFFFF (4GB). However, when I boot that system with 64-bit Vista, all 4GB show up as available and you can see how Windows uses the remaining 500MB of RAM that are above the 4GB boundary:</p><p>可以注意到内存地址排布在 9F0000 到 100000 和 DFE6D000 到 FFFFFFFF(4 GB) 都有一个空缺。然而当我改用 64 位的 Vista 时,所有的 4 GB 都显示为可用,你可以看到 Windows 在 4 GB 的内存边界内是怎么使用这保留的 500 MB 空间的:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121042i40E3EAD02D59A9AF.2xkmm9uglww0.webp" alt="121042i40E3EAD02D59A9AF" ></p><p>What’s occupying the holes below 4GB? The Device Manager can answer that question. To check, launch “devmgmt.msc”, select Resources by Connection in the View Menu, and expand the Memory node. On my laptop, the primary consumer of mapped device memory is, unsurprisingly, the video card, which consumes 256MB in the range E0000000-EFFFFFFF:</p><p>那么是什么占据了 4 GB 内的内存空间呢?设备管理器可以回答这个问题。通过运行 “devmgmt.msc”,在菜单选择通过连接列出资源,然后站靠内存节点即可。不出意外的,在我的笔记本上,这些映射的硬件内存中消耗最大的是显卡,它的地址范围是 E0000000-EFFFFFFF,占据了 256 MB 的空间。</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121043i50E52B9758F5B8B3.49fy13va57o0.webp" alt="121043i50E52B9758F5B8B3" ></p><p>Other miscellaneous devices account for most of the rest, and the PCI bus reserves additional ranges for devices as part of the conservative estimation the firmware uses during boot.</p><p>其他各式的设备则占据了剩余的空间,PCI 总线还为设备额外保留了一部分空间,固件启动期间会使用到这些保守估计的空间。</p><p>The consumption of memory addresses below 4GB can be drastic on high-end gaming systems with large video cards. For example, I purchased one from a boutique gaming rig company that came with 4GB of RAM and two 1GB video cards. I hadn’t specified the OS version and assumed that they’d put 64-bit Vista on it, but it came with the 32-bit version and as a result only 2.2GB of the memory was accessible by Windows. You can see a giant memory hole from 8FEF0000 to FFFFFFFF in this Meminfo output from the system after I installed 64-bit Windows:</p><p>4 GB 内的地址会用在高端游戏系统上会被剧烈消耗掉。例如,我在一个精品游戏电脑组装公司购买了一个 4 GB 的 RAM 和两个 1 GB 的显卡。我以为它们会安装一个 64 位的 Vista,所以没有去指定系统版本,但它们给了一个 32 位的系统,这导致 Windows 只能读取到 2.2 GB 的地址。在我安装了 64 位的 Windows 后,你可以在 Meminfo 的输出中看到一个从 8FEF0000 到 FFFFFFFF 的内存漏洞:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121044i698749E2C1A1F3D2.3ncnge41rp00.webp" alt="121044i698749E2C1A1F3D2" ></p><p>Device Manager reveals that 512MB of the over 2GB hole is for the video cards (256MB each), and it looks like the firmware has reserved more for either dynamic mappings or because it was conservative in its estimate:</p><p>从设备管理器可以看出,这个 2 GB 的内存漏洞中,有 512 MB 是保留给显卡的(每张 256 MB),看起来固件会为动态映射保留更多的空间,或者是因为它的分配预测比较保守:</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121045i26EFB039D1FD4324.5ogzkqn3jbs0.webp" alt="121045i26EFB039D1FD4324" ></p><p>Even systems with as little as 2GB can be prevented from having all their memory usable under 32-bit Windows because of chipsets that aggressively reserve memory regions for devices. Our shared family computer, which we purchased only a few months ago from a major OEM, reports that only 1.97GB of the 2GB installed is available:</p><p>即使系统只有 2 GB 的内存也会被阻止在 32 位的 Windows 下去使用所有的内存,因为芯片积极地为硬件保留内存区域。我们一个月前从一个主要的 OEM 厂商那购买的家庭共享电脑报告了这 2 GB 的内存只有 1.97 GB 是可用的。</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121046iA45FC4F0A8121F2F.ct0vyv2l2co.webp" alt="121046iA45FC4F0A8121F2F" ></p><p>The physical address range from 7E700000 to FFFFFFFF is reserved by the PCI bus and devices, which leaves a theoretical maximum of 7E700000 bytes (1.976GB) of physical address space, but even some of that is reserved for device memory, which explains why Windows reports 1.97GB.</p><p>物理地址范围 7E700000 到 FFFFFFFF 会被 PCI 总线和硬件所持有,理论上最多留下 7E700000 字节 (1.976 GB) 的物理地址空间,但其中的一些甚至会被硬件内存所保留,这就解释了为什么 Windows 只报告了 1.97 GB 的大小。</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121047iD4B5FA50BB9AD4CE.3w7kr2p0t2w0.webp" alt="121047iD4B5FA50BB9AD4CE" ></p><p>Because device vendors now have to submit both 32-bit and 64-bit drivers to Microsoft’s Windows Hardware Quality Laboratories (WHQL) to obtain a driver signing certificate, the majority of device drivers today can probably handle physical addresses above the 4GB line. However, 32-bit Windows will continue to ignore memory above it because there is still some difficult to measure risk, and OEMs are (or at least should be) moving to 64-bit Windows where it’s not an issue.</p><p>因为硬件厂商现在需要提交 32 位和 64 位的驱动到微软的 Windows Hardware Quality Laboratories (WHQL) 来获取驱动签名认证,所以现在主要的硬件驱动都能够处理超过 4 GB 的物理地址。然而因为一些难以衡量的风险,32 位的 Windows 还是会继续忽略超过 4 GB 的内存,OEMs 正在转移到 64 位的 Windows 上,在哪里就不会是个问题。</p><p>The bottom line is that you can fully utilize your system’s memory (up the SKU’s limit) with 64-bit Windows, regardless of the amount, and if you are purchasing a high end gaming system you should definitely ask the OEM to put 64-bit Windows on it at the factory.</p><p>不管物理内存的总数是多少,底线就是你能够在 64 位的 Windows 上充分使用你的系统内存(达到 SKU 的限制)。如果你购买一台高端的游戏系统,你应该明确要求 OEM 在制造厂里就给它安装一个 64 位的 Windows。</p><h2 id="Do-You-Have-Enough-Memory-(你有足够的内存吗?)"><a href="#Do-You-Have-Enough-Memory-(你有足够的内存吗?)" class="headerlink" title="Do You Have Enough Memory?(你有足够的内存吗?)"></a>Do You Have Enough Memory?(你有足够的内存吗?)</h2><p>Regardless of how much memory your system has, the question is, is it enough? Unfortunately, there’s no hard and fast rule that allows you to know with certainty. There is a general guideline you can use that’s based on monitoring the system’s “available” memory over time, especially when you’re running memory-intensive workloads. Windows defines available memory as physical memory that’s not assigned to a process, the kernel, or device drivers. As its name implies, available memory is available for assignment to a process or the system if required. The Memory Manager of course tries to make the most of that memory by using it as a file cache (the standby list), as well as for zeroed memory (the zero page list), and Vista’s Superfetch feature prefetches data and code into the standby list and prioritizes it to favor data and code likely to be used in the near future.</p><p>不管你的系统有多大的内存,问题是,它真的足够吗?不幸的是,并没有任何准确快速的规则让你明确判断。不过你可以使用一个比较通用的方法,你需要长时间监视系统<strong>可用内存</strong>的大小,特别是你打开一些内存集中型的工作时。Windows 上<strong>可用内存</strong>的定义是没有分配给进程,系统和硬件驱动的物理内存。正如其名,可用内存在进程或系统需要的时候能够分配出去。当然,内存管理器会尽可能地通过将其作为文件缓存 (<a class="link" href="https://docs.microsoft.com/en-us/windows-server/administration/performance-tuning/subsystem/cache-memory-management/" >the standby list<i class="fas fa-external-link-alt"></i></a>) 或者 <a class="link" href="https://en.wikipedia.org/wiki/Zero_page" >zeroed memory<i class="fas fa-external-link-alt"></i></a> (the zero page list) 来充分利用这些内存。Vista 的 Superfetch 特性还会提前将数据和指令放置到 standby list 中,优先考虑使用那些不久的将来就会被使用的数据和指令。</p><p>If available memory becomes scarce, that means that processes or the system are actively using physical memory, and if it remains close to zero over extended periods of time, you can probably benefit by adding more memory. There are a number of ways to track available memory. On Windows Vista, you can indirectly track available memory by watching the Physical Memory Usage History in Task Manager, looking for it to remain close to 100% over time. Here’s a screenshot of Task Manager on my 8GB desktop system (hmm, I think I might have too much memory!):</p><p>如果可用内存不足的话,意味着进程和系统非常活跃地使用物理内存。如果在过去一段时间都接近于 0 ,那么你很可能可以从增加内存获取到收益。有好几种方法去追踪可用内存。在 Windows Vista 上,你可以在任务管理器的物理内存使用记录中,通过寻找它过去接近 100% 的那段时间来间接地追踪到可用内存的变化。</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121048i87B78C52C2AA4D6E.23g58wcra8u8.webp" alt="121048i87B78C52C2AA4D6E" ></p><p>On all versions of Windows you can graph available memory using the Performance Monitor by adding the Available Bytes counter in the Memory performance counter group:</p><p>在所有版本的 Windows 上,只需要将可用字节总数添加到内存性能计数组。就可以通过 Performance Monitor 来绘制可用内存曲线。</p><p><img lazyload src="/images/loading.svg" data-src="https://cdn.jsdelivr.net/gh/Wzzzx/image@main/Pushing-the-Limits-of-Windows-Physical-Memory/121049iA3715C835EBE4A7C.4mjhma5f4ne0.webp" alt="121049iA3715C835EBE4A7C" ></p><p>You can see the instantaneous value in Process Explorer’s System Information dialog, or, on versions of Windows prior to Vista, on Task Manager’s Performance page.</p><p>你可以在进程管理器的系统信息页看到瞬间数值。而在 Vista 之前版本的 Windows 上,可以在任务管理器的性能页上看到。</p><h2 id="Pushing-the-Limits-of-Windows(触碰-Windows-的极限)"><a href="#Pushing-the-Limits-of-Windows(触碰-Windows-的极限)" class="headerlink" title="Pushing the Limits of Windows(触碰 Windows 的极限)"></a>Pushing the Limits of Windows(触碰 Windows 的极限)</h2><p>Out of CPU, memory and disk, memory is typically the most important for overall system performance. The more, the better. 64-bit Windows is the way to go to be sure that you’re taking advantage of all of it, and 64-bit Windows can have other performance benefits that I’ll talk about in a future Pushing the Limits blog post when I talk about virtual memory limits.</p><p>在 CPU、内存和磁盘中,内存通常对整个系统性能最为重要。内存越多越好,使用 64 位的 Windows 是保证你能够利用到所有物理内存的方法。64 位的 Windows 也能够获得其他的性能收益,我会在之后博文虚拟内存限制中谈到这点。</p>]]></content>
<summary type="html"><blockquote>
<p>翻译来源 <a class="link" href="https://techcommunity.microsoft.com/t5/windows-blog-archive/pushing-the-limits-of-windows-physi</summary>
<category term="OS" scheme="https://wzzzx.github.io/categories/OS/"/>
</entry>
<entry>
<title>实验2:系统调用</title>
<link href="https://wzzzx.github.io/6-S081/lab2-system-call/"/>
<id>https://wzzzx.github.io/6-S081/lab2-system-call/</id>
<published>2022-02-12T17:45:59.000Z</published>
<updated>2022-02-13T05:06:18.000Z</updated>
<content type="html"><![CDATA[<p>上一节介绍了<a href="/6-S081/analyze-system-call/" title="系统调用">系统调用</a>的相关细节之后,实验二的代码实现起来其实并不难。实验二的代码可以在 <a class="link" href="https://github.com/Wzzzx/6.S081-lab/tree/master/lab2" >lab2<i class="fas fa-external-link-alt"></i></a> 中查看到。</p><span id="more"></span><h2 id="trace"><a href="#trace" class="headerlink" title="trace"></a>trace</h2><p>对于这个系统调用,每次系统调用都会产生一次额外的判断,看起来有点浪费资源了。而且每个进程都会携带一个 <code>mask</code>,大部分情况下空间也浪费了。不知道 Linux 是怎么实现的。</p><p>为了实现父子进程都会被 <code>trace</code> 追踪到,就需要修改一下 <code>fork</code>,从这也可以了解到 <code>fork</code> 的一些细节。感觉有意思的主要是两点。</p><ul><li>创建子进程的时候,会将子进程的 <code>a0</code> 寄存器直接设置为 0,这也是为什么父子进程的返回值会不同的原因。</li><li>子进程的创建就是一个简单的拷贝,所以父子进程的一切都会保持一致,这里可以做一个 <code>Copy-on-write</code> 的优化,好像到课程后面会涉及到。</li></ul><h2 id="sysinfo"><a href="#sysinfo" class="headerlink" title="sysinfo"></a>sysinfo</h2><p>这个系统调用就比较好玩,从这也了解了一些好玩的东西。</p><h3 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h3><p>可以发现在 <code>kernel/proc.c</code> 设置了一个 64 个元素的 <code>proc</code> 数组,这个数组就是 Xv6 的 process table。也就是说,Xv6 最多能同时运行 64 个进程。process table 也是一个很大的主题,这个后续的课应该会讲到,暂时不用管。</p><p>从这可以联系到 Linux 和 Windows。Linux 的最大进程数可以在 <code>/proc/sys/kernel/pid_max</code> 这个文件中查看到,32 位系统最大进程数为 32768,64 位为 4194304。Windows 好像并没有官方文档说明最大进程数,但是可以在 <a class="link" href="https://techcommunity.microsoft.com/t5/windows-blog-archive/pushing-the-limits-of-windows-processes-and-threads/ba-p/723824" >Pushing the Limits of Windows: Processes and Threads<i class="fas fa-external-link-alt"></i></a> 一文中了解到相关的信息。</p><h3 id="内存"><a href="#内存" class="headerlink" title="内存"></a>内存</h3><p>Xv6 对内存的管理看起来比较简单粗暴,直接使用一个链表将所有的可用内存串在一起。从代码中可以看到,Xv6 每页的大小为 4096 个字节,其能接受的最大内存大小为 128M。并且 Xv6 只使用 0x80000000L 之后的内存,在<a href="/6-S081/kernel-link-file/" title="内核链接脚本解析">内核链接脚本解析</a>中有提到过,之前的地址空间将会用于放置 IO 设备。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总体难度不大,但是从这个实验却可以了解到系统内部的很多细节知识,不得不说这实验设计的真不错。</p>]]></content>
<summary type="html">实验二相关代码</summary>
<category term="6.S081" scheme="https://wzzzx.github.io/categories/6-S081/"/>
</entry>
<entry>
<title>解析系统调用</title>
<link href="https://wzzzx.github.io/6-S081/analyze-system-call/"/>
<id>https://wzzzx.github.io/6-S081/analyze-system-call/</id>
<published>2022-02-12T09:20:44.000Z</published>
<updated>2022-02-12T16:17:25.000Z</updated>
<content type="html"><![CDATA[<p>Xv6 的系统调用相对来说还是比较简单的,只需要修改几个文件就可以实现自定义系统调用了。</p><span id="more"></span><h2 id="创建一个系统调用"><a href="#创建一个系统调用" class="headerlink" title="创建一个系统调用"></a>创建一个系统调用</h2><ul><li><strong>确定函数签名</strong></li></ul><p>首先需要确定的是函数签名。按 Xv6 的规范,如果没有特殊情况,其返回值应该是 <code>int</code> 类型,一切顺利返回 0,否则返回一个用于标记错误类型的负数。</p><ul><li><strong>创建系统调用接口</strong></li></ul><p>确定好函数签名之后,在 <code>user/user.h</code> 进行声明。对于函数,我们需要声明并实现才可以正确编译和使用。而系统调用和函数调用最为重要的一个区别就是,执行系统调用会陷入内核态,这就导致系统调用接口的实现方式跟普通函数存在区别。</p><p>对于声明完的系统调用,其实现还不需要自己去实现。现在只需要在 <code>user/usys.pl</code> 中,仿照预设好的系统调用,添加一个 Perl 函数调用即可。这段脚本的作用就是在构建的时候去生成系统调用接口在用户态的实现。</p><ul><li><strong>创建系统调用号</strong></li></ul><p>用户态的系统调用接口的作用就是使进程陷入内核态,触发真正的函数。对于每个系统调用,都需要在 <code>kernel/syscall.h</code> 中分配一个独一无二的系统调用号。这里还需要仿照预设好的系统调用进行名称的设定。</p><ul><li><strong>完成实现逻辑</strong></li></ul><p>具体的系统调用逻辑按需要在不同的文件中去实现,此时的命名也可以按需要去确定,不需要跟系统调用保持一致。要注意的是,所有的实现函数的返回值都是 <code>uint64</code>,参数都是 <code>void</code>。</p><ul><li><strong>建立系统调用号和实现的联系</strong></li></ul><p>实现完系统调用逻辑后,需要在 <code>kernel/syscall.c</code> 将系统调用号和实现函数建立绑定。首先需要使用 <code>extern</code> 将实现函数引入该文件,而后按系统调用号大小顺序在 <code>syscalls</code> 填入实现函数即可。</p><h2 id="细节详解"><a href="#细节详解" class="headerlink" title="细节详解"></a>细节详解</h2><p>通过上述的操作,就可以自定义出任意的系统调用用于满足应用需求了。但还有很多细节需要进一步探讨。</p><h3 id="系统调用用户态实现"><a href="#系统调用用户态实现" class="headerlink" title="系统调用用户态实现"></a>系统调用用户态实现</h3><p>用户态的实现细节全都在 <code>user/usys.pl</code> 所定义的 <code>entry</code> 中。</p><p>函数第一行会输出 <code>.global $name</code>,这条指令在全局符号表定义了一个符号,符号的名字就是系统调用名。这就是编译的时候不会报 <code>unresolved externals</code> 错误的关键了。函数名本质上就是一个符号,所以一些语言会要求函数名保持唯一性,不然会报 <code>redefinition</code> 的错误。</p><p>函数第二行的输出是创建一个 <code>Labels</code>。这个就是汇编的函数接口了。</p><p>函数第三行的输出比较关键。内核依靠系统调用号来得知进程调用了哪个系统调用,这也是在上一节说到的,系统调用号必须保持唯一性的原因。当进程通过系统调用陷入内核态的时候,内核通过 <code>a7</code> 这个寄存器来获取具体的系统调用号。所以这里会把系统调用号加载到 <code>a7</code> 这个寄存器中。这里也可以看到系统调用号的名称的设定方式。</p><p>函数的第四行输出只有一个简单的指令,这个指令是进程陷入内核态的关键。该指令会触发一个中断,提升系统的<strong>特权级</strong>,同时将程序计数器(program counter)调整到预先定义好的入口处,也就是 <code>kernel/syscall.c</code> 的 <code>syscall</code>。这时候系统就会进入内核态,执行入口处的代码。</p><p>最后一句输出则是一个简单的返回。</p><h3 id="寄存器作用和参数传递"><a href="#寄存器作用和参数传递" class="headerlink" title="寄存器作用和参数传递"></a>寄存器作用和参数传递</h3><p>系统调用号会被直接存放于 <code>a7</code> 这个寄存器中,所有的参数会被依次放置于 <code>a0</code> 开始的寄存器上。当参数的数量过多的时候,会有一套不一样的方式来传递,暂时可以不考虑。此外,在放置参数到寄存器上时,采用的是从右到左的方式,最右边的参数放置的寄存器编号越大。系统调用执行完毕后,返回值则会被放置于 <code>a0</code>。</p><p>参数通过寄存器来传递可以节约大量的访存操作,这也是为什么在实现系统调函的函数时候,参数可以设置为 <code>void</code> 的原因。</p><h3 id="系统调用参数获取"><a href="#系统调用参数获取" class="headerlink" title="系统调用参数获取"></a>系统调用参数获取</h3><p>当一切顺利,程序正确运行到内核的时候,还需要考虑的一个问题就是参数的获取。参数涉及到了内核态和用户态的数据交换,处理起来会比较复杂。特别是像 <code>exec</code> 这种带有指针的参数,会额外带来两个问题。其一是<strong>安全问题</strong>,如何防范用户程序刻意传入的非法指针。其二是<strong>地址转变问题</strong>,在操作系统中,所有的程序使用的都是虚拟地址,有各自的页表。用户进程和内核进程是两套不一样的地址,如何在其中进行数据的交换。</p><p>好在内核代码已经实现了全部的参数获取接口。我们可以通过 <code>argint</code>,<code>argaddr</code>,<code>argstr</code> 这三个函数来获取到某一个编码的寄存器所存储的数据,并将其转化为整型,指针和字符串。这三个函数都是封装了 <code>argraw</code> 来实现功能。对于结构体,则可以采用 <code>copyout</code> 和 <code>copyin</code> 来进行数据的传递。</p><p>具体的实现现在暂时不必考虑,之后可以再做了解。</p>]]></content>
<summary type="html">剖析系统调用的调用过程</summary>
<category term="6.S081" scheme="https://wzzzx.github.io/categories/6-S081/"/>
</entry>
<entry>
<title>调试运行第一个Xv6程序</title>
<link href="https://wzzzx.github.io/6-S081/run-first-xv6-program/"/>
<id>https://wzzzx.github.io/6-S081/run-first-xv6-program/</id>
<published>2022-02-01T15:10:20.000Z</published>
<updated>2022-02-03T10:04:47.000Z</updated>
<content type="html"><![CDATA[<p>Xv6 刚启动的时候处于 <strong>Machine Mode</strong>,完成基本配置工作后就会尽快跳转到 <strong>Supervisor Mode</strong>,在这个模式下,<code>main</code> 会尽快完成系统的配置工作并创建出第一个用户程序。</p><span id="more"></span><h3 id="前置知识"><a href="#前置知识" class="headerlink" title="前置知识"></a>前置知识</h3><p>Xv6 是一个支持多进程的操作系统,在 Makefile 中可以看到 <code>CPUS</code> 会被默认设置为 3,为了调试方便,在启动的时候,可以将其设置为 1。这样就只会在一个 CPU 上运行程序,单个断点就可以阻止整个操作系统的运行。</p><h3 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h3><p>系统可以采用 Ubuntu Server 20.04,调试用的 GDB 是 gdb-multiarch 9.2.0,qemu 则采用 SiFive 所提供的 qemu-system-riscv64 5.1.0。</p><h2 id="开始运行"><a href="#开始运行" class="headerlink" title="开始运行"></a>开始运行</h2><p>按<a href="/6-S081/debug-xv6/" title="调试xv6">调试xv6</a> 一文来操作的话,开发调试环境是可以顺利跑起来的。此时在一个终端执行 <code>make CPUS=1 qemu-gdb</code> 将系统跑起来,然后在另一个终端执行 <code>gdb-multiarch</code> 将 gdb 运行起来,此时 gdb 会自动连接到 qemu 的调试环境内,一切顺利的话,可以看到下面这样的输出:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2</span><br><span class="line">Copyright (C) 2020 Free Software Foundation, Inc.</span><br><span class="line">License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html></span><br><span class="line">This is free software: you are free to change and redistribute it.</span><br><span class="line">There is NO WARRANTY, to the extent permitted by law.</span><br><span class="line">Type "show copying" and "show warranty" for details.</span><br><span class="line">This GDB was configured as "x86_64-linux-gnu".</span><br><span class="line">Type "show configuration" for configuration details.</span><br><span class="line">For bug reporting instructions, please see:</span><br><span class="line"><http://www.gnu.org/software/gdb/bugs/>.</span><br><span class="line">Find the GDB manual and other documentation resources online at:</span><br><span class="line"> <http://www.gnu.org/software/gdb/documentation/>.</span><br><span class="line"></span><br><span class="line">For help, type "help".</span><br><span class="line">Type "apropos word" to search for commands related to "word".</span><br><span class="line">The target architecture is assumed to be riscv:rv64</span><br><span class="line">warning: No executable has been specified and target does not support</span><br><span class="line">determining executable automatically. Try using the "file" command.</span><br><span class="line">0x0000000000001000 in ?? ()</span><br></pre></td></tr></table></figure><h3 id="起步操作"><a href="#起步操作" class="headerlink" title="起步操作"></a>起步操作</h3><p>开始的时候,可以通过 <code>b _entry</code> 在系统入口处设置一个断点,在<a href="/6-S081/kernel-link-file/" title="链接控制脚本">链接控制脚本</a>中提到过,系统的入口点会被设置为 <code>0x80000000</code>。但是不知道什么原因,在我的机器上,这个断点的地址会被设置为 <code>0x80000004</code>,而在老师课上所展示的却是 <code>0x8000000a</code>。但是这时候如果通过 <code>info address _entry</code> 查看 <code>_entry</code> 的地址,又可以看到其位于 <code>0x80000000</code> 上。具体是什么原因还需要进行后续的探究。</p><p>断点设置好了之后,可以输入 <code>c</code> 让系统跑起来,这时候会直接卡在断点处。为了方便调试,这时候可以使用 <code>layout split</code> 将 gdb 窗口划分为指令和代码两个窗口,方便查看每个代码所对应的指令。</p><p>这时候系统处于 <strong>Machine Mode</strong>,此时会配置好环境,创建好程序栈,以便转入 <strong>Supervisor Mode</strong> 运行 C 代码。这些代码暂时可以不必理会。</p><h3 id="进入-main-函数"><a href="#进入-main-函数" class="headerlink" title="进入 main 函数"></a>进入 <code>main</code> 函数</h3><p>越过 <strong>Machine Mode</strong> 相关的代码后,就可以直接来到内核起始处,这里跟普通的 C 程序一样,都是以 <code>main</code> 作为入口函数。通过命令 <code>b main</code> 将断点设置在 <code>main</code> 函数处。然后直接输入 <code>c</code> 让程序跑起来,直到 <code>main</code> 函数再停下来。</p><p>在 <code>main</code> 函数内会完成系统的各种配置,包括设置好虚拟内存,页表,文件系统等等,具体可以参看<a class="link" href="https://github.com/mit-pdos/xv6-riscv/blob/f2ab0eb644a60f946f35fcb5578fba53720edfa7/kernel/main.c" >代码<i class="fas fa-external-link-alt"></i></a>中的注释。老师在课上有提到,这些初始化的操作是有顺序的,不能随意调换。</p><p>在这些初始化的函数中,现在需要关注的是 <code>userinit()</code>。在这个函数内,会完成第一个 <code>User Mode</code> 程序的创建。</p><h3 id="进入-userinit-函数"><a href="#进入-userinit-函数" class="headerlink" title="进入 userinit() 函数"></a>进入 <code>userinit()</code> 函数</h3><p>在 <code>main</code> 函数内使用 <code>n</code> 命令让程序逐行执行,直到 <code>userinit()</code> 的时候,改用 <code>s</code> 让程序进入函数体内。</p><p>这个函数的操作就是手动创建了一个用户进程,将这个进程设置为 <code>RUNNABLE</code> 状态,以便 cpu 进行调度执行。这里有个有意思的地方,<code>userinit()</code> 创建用户进程的方式,是直接使用程序的二进制形式来创建。</p><p>这个用户程序的二进制代码定义直接硬编码在 <code>initcode</code> 这个数组里,相应的可执行程序是 <code>user/initcode</code>。</p><h3 id="initcode-程序"><a href="#initcode-程序" class="headerlink" title="initcode 程序"></a><code>initcode</code> 程序</h3><p><code>initcode</code> 的定义在 <a class="link" href="https://github.com/mit-pdos/xv6-riscv/blob/f2ab0eb644a60f946f35fcb5578fba53720edfa7/user/initcode.S" >user/initcode.S<i class="fas fa-external-link-alt"></i></a>。这个程序就是 Xv6 所创建的第一个用户态程序,从 <code>userinit()</code> 中可以看出,第一个用户态程序只能以靠手动的形式来创建,所以会要求它足够的<strong>简单</strong>。因此,<code>initcode</code> 更像一个<strong>楔子</strong>,用来引出真正的带有逻辑的用户程序。</p><p>从代码中可以看出,<code>initcode</code> 就是利用 <code>exec</code> 这个系统调用来创建出一个更为复杂的可执行程序。它会将 <code>init</code> 这个程序加载到 <code>a0</code>,这个便是需要创建的程序的程序名,而后将参数 <code>argv</code> 加载到 <code>a1</code>。<code>exec</code> 这个系统调用的调用号是 7,所以需要将 7 加载到 <code>a7</code> 上。相关的操作完成后,就会调用 <code>ecall</code> 将控制权交回给操作系统。</p><h3 id="触发-syscall"><a href="#触发-syscall" class="headerlink" title="触发 syscall"></a>触发 <code>syscall</code></h3><p>从上面可以看出,<code>userinit</code> 会创建一个简单的用户态程序,该程序会使用系统调用 <code>exec</code> 来创建一个真正有逻辑,较为复杂的用户态程序来替代自己。</p><p>系统如何创建的第一个程序具体细节此时倒不必过多关注,需要关注的就是系统调用的具体逻辑,所以可以用 <code>b syscall</code> 将断点设置到系统调用的触发函数上。在 <code>userinit()</code> 内直接使用 <code>c</code> 让程序直接跑到断点的位置。</p><p><code>syscall</code> 这个函数位于 <code>kernel/syscall.c</code> 内,内核可以通过 <code>p->trapframe->a7</code> 获取到系统调用号,这时候使用 <code>p num</code> 可以查看到这个系统调用号就是当时加载到 <code>a7</code> 的 7。程序运行到 <code>p->trapframe->a0 = syscalls[num]();</code> 时,直接使用 <code>s</code> 陷入到 <code>exec</code> 这个系统调用内。</p><h3 id="进入-exec"><a href="#进入-exec" class="headerlink" title="进入 exec"></a>进入 <code>exec</code></h3><p><code>exec</code> 位于 <code>kernel/sysfile.c</code> 里,进入这个函数后,会把需要调用的程序路径放置到 <code>path</code>,启动参数放置到 <code>argv</code> 中。而后直接使用 <code>exec</code> 将 <code>path</code> 中的程序创建出来</p><h3 id="创建完成"><a href="#创建完成" class="headerlink" title="创建完成"></a>创建完成</h3><p>通过上述的流程,可以正确地创建出真正意义上的第一个用户态程序,<code>init</code>。这个程序的源代码在 <code>user/init.c</code>。从代码里可以看出,这个程序唯一目的就是修改文件描述符,将 0 和 1 强制设定为标准输入输出。并且维持 <code>sh</code> 的运行。</p><p>至此,整个系统就顺利地运行起来了。</p>]]></content>
<summary type="html">搭配GDB了解清楚从启动到运行第一个程序的过程</summary>
<category term="6.S081" scheme="https://wzzzx.github.io/categories/6-S081/"/>
</entry>
<entry>
<title>内核链接脚本解析</title>
<link href="https://wzzzx.github.io/6-S081/kernel-link-file/"/>
<id>https://wzzzx.github.io/6-S081/kernel-link-file/</id>
<published>2022-01-23T03:49:00.000Z</published>
<updated>2022-02-03T10:04:06.000Z</updated>
<content type="html"><![CDATA[<p>具体的代码可以见<a class="link" href="https://github.com/mit-pdos/xv6-riscv/blob/f2ab0eb644a60f946f35fcb5578fba53720edfa7/kernel/kernel.ld" >仓库<i class="fas fa-external-link-alt"></i></a>,这里挑选几个说一下。</p><span id="more"></span><h3 id="输出架构"><a href="#输出架构" class="headerlink" title="输出架构"></a>输出架构</h3><p><code>OUTPUT_ARCH</code> 这个命令用于指定输出文件的系统架构,这里采用的是 riscv。</p><h3 id="入口点"><a href="#入口点" class="headerlink" title="入口点"></a>入口点</h3><p><code>ENTRY</code> 则定义了程序的入口点,xv6 的默认入口点是 <code>_entry</code>。入口点代码在 <a class="link" href="https://github.com/mit-pdos/xv6-riscv/blob/f2ab0eb644a60f946f35fcb5578fba53720edfa7/kernel/entry.S" >entry.S<i class="fas fa-external-link-alt"></i></a> 内。在链接脚本内,这个入口地址会被设置为 0x80000000。</p><h3 id="位置计数器"><a href="#位置计数器" class="headerlink" title="位置计数器"></a>位置计数器</h3><p>进入 <code>SECTIONS</code> 后,当前程序的初始地址默认是 0x0。这里跟 Linux 的文件系统一样,采用 “.” 来表示当前地址。这个符号的名字叫<strong>位置计数器</strong> (location counter),表示随后的段 / 变量对应的内存地址。这就意味着如果直接给 “.” 赋值,相当于切换了工作目录,后续段的相对位置都会发生改变。</p><p>这里可以看一个简单的例子</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">SECTIONS</span><br><span class="line">{</span><br><span class="line"> . = 0x10000;</span><br><span class="line"> .text : { *(.text) }</span><br><span class="line"> . = 0x8000000;</span><br><span class="line"> .data : { *(.data) }</span><br><span class="line"> .bss : { *(.bss) }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>进入 <code>SECTIONS</code> 后,位置计数器会被设置为 0x10000,所以链接器会将 <code>.text</code> 的地址设置为 0x10000。完成 <code>.text</code> 的设置后,位置计数器会被修改为 0x8000000,<code>.data</code> 的地址会被设置为 0x8000000。轮到 <code>.bss</code> 的时候,它会紧跟着 <code>.data</code> 段,此时位置计数器的值就是 0x8000000 加上 <code>.data</code> 段的大小。</p><h3 id="xv6-初始地址设置"><a href="#xv6-初始地址设置" class="headerlink" title="xv6 初始地址设置"></a><code>xv6</code> 初始地址设置</h3><p>一进入 <code>SECTIONS</code> 脚本就会将当前的初始路径设置为 0x80000000。老师课上说这个地址是 qemu 认可的地址,第一条指令必须放置是它。<a class="link" href="https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf" >书本2.6节<i class="fas fa-external-link-alt"></i></a>的说法是 0x0 到 0x80000000 这段地址用于放置 IO 设备,所以第一条指令所在的地址就必须是 0x80000000。</p><h3 id="段内容解析"><a href="#段内容解析" class="headerlink" title="段内容解析"></a>段内容解析</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">.text : {</span><br><span class="line"> *(.text .text.*)</span><br><span class="line"> . = ALIGN(0x1000);</span><br><span class="line"> _trampoline = .;</span><br><span class="line"> *(trampsec)</span><br><span class="line"> . = ALIGN(0x1000);</span><br><span class="line"> ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");</span><br><span class="line"> PROVIDE(etext = .);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>内核的段设置都大同小异,所以看一个 <code>.text</code> 就可以了。</p><p><code>.text</code> 段的第一个表达式 <code>*(.text .text.*)</code>,其中的 <code>*</code> 是通配符。其表示所有输入文件的 <code>.text</code> 和 <code>.text.*</code> 段都放置于输出文件的 <code>.text</code> 段中。</p><p>接着是命令 <code>ALIGN(exp)</code>,这个命令会返回位置计数器对齐到下一个 <code>exp</code> 边界的地址,其并不会修改位置计数器的值。</p><p><code>PROVIDE</code> 这个命令类似于 GCC 中的 <code>attribute((weak))</code>。现在只需要简单的理解为定义了一个符号。</p><p>剩余的 <code>_trampoline</code> 和 <code>trampsec</code> 在页表的章节会涉及到,到时候再去了解即可。</p><blockquote><p><strong>参考</strong></p><p><a class="link" href="https://zhuanlan.zhihu.com/p/383729996" >链接脚本<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://zhuanlan.zhihu.com/p/363308789" >GNU 链接脚本0 - 链接脚本基本介绍<i class="fas fa-external-link-alt"></i></a></p></blockquote>]]></content>
<summary type="html">介绍内核链接脚本的构成</summary>
<category term="6.S081" scheme="https://wzzzx.github.io/categories/6-S081/"/>
</entry>
<entry>
<title>调试xv6</title>
<link href="https://wzzzx.github.io/6-S081/debug-xv6/"/>
<id>https://wzzzx.github.io/6-S081/debug-xv6/</id>
<published>2022-01-22T13:22:08.000Z</published>
<updated>2022-02-03T10:04:06.000Z</updated>
<content type="html"><![CDATA[<p>调试是一个比较麻烦的事情,需要倒腾的东西有点多,而且有些问题我现在也没搞清楚</p><span id="more"></span><h2 id="调试工具"><a href="#调试工具" class="headerlink" title="调试工具"></a>调试工具</h2><p>现在已知的调试工具有三种,暂时也还没搞清楚它们之间具体有什么区别。</p><h3 id="riscv64-unknown-elf-gdb"><a href="#riscv64-unknown-elf-gdb" class="headerlink" title="riscv64-unknown-elf-gdb"></a><code>riscv64-unknown-elf-gdb</code></h3><p>在<a href="/6-S081/xv6-running/" title="启动">启动</a> <code>xv6</code> 的一文介绍了 <code>SiFive</code> 有提供一套工具链用来启动系统,在这套工具链里可以找到 <code>riscv64-unknown-elf-gdb</code> 用于调试</p><h3 id="gdb-multiarch"><a href="#gdb-multiarch" class="headerlink" title="gdb-multiarch"></a><code>gdb-multiarch</code></h3><p><a class="link" href="https://pdos.csail.mit.edu/6.828/2019/tools.html" >官方工具页<i class="fas fa-external-link-alt"></i></a>所提供的工具是 <code>gdb-multiarch</code>。这个可以通过 <code>apt-get</code> 直接安装。此外,在工具页所提供的 <a class="link" href="https://github.com/riscv/riscv-gnu-toolchain" >GitHub<i class="fas fa-external-link-alt"></i></a> 中可以发现,该仓库所提供的包也是 <code>riscv64-unknown-elf-gdb</code></p><h3 id="riscv64-linux-gnu-gdb"><a href="#riscv64-linux-gnu-gdb" class="headerlink" title="riscv64-linux-gnu-gdb"></a><code>riscv64-linux-gnu-gdb</code></h3><p>在<a class="link" href="https://www.youtube.com/watch?v=o44d---Dk4o&ab_channel=DavidMorejon" >视频<i class="fas fa-external-link-alt"></i></a>中可以看到,老师所使用的调试工具是 <code>riscv64-linux-gnu-gdb</code>,因为前面的工具已经解决了调试问题,所以没有去找这个包的安装方式</p><h2 id="gdbinit-文件"><a href="#gdbinit-文件" class="headerlink" title=".gdbinit 文件"></a><code>.gdbinit</code> 文件</h2><p>每次开启调试的时候,<code>Makefile</code> 会使用 <code>.gdbinit.tmpl-riscv</code> 生成<code>.gdbinit</code> 文件。从 <code>Makefile</code> 可以看到,会变的地方就是调试端口号。这个文件的用途是用于初始化调试环境,<code>gdb</code> 启动的时候会读取其中的命令并执行。具体可以见<a class="link" href="https://man7.org/linux/man-pages/man5/gdbinit.5.html" >帮助文档<i class="fas fa-external-link-alt"></i></a></p><h2 id="疑难杂症"><a href="#疑难杂症" class="headerlink" title="疑难杂症"></a>疑难杂症</h2><h3 id="Undefined-item-quot-riscv-rv64-quot"><a href="#Undefined-item-quot-riscv-rv64-quot" class="headerlink" title="Undefined item: "riscv:rv64""></a><code>Undefined item: "riscv:rv64"</code></h3><p>使用 <code>gdb-multiarch</code> 时会在开头看到这个错误提示,此时通过命令 <code>set architecture</code> 查看当前所支持的所有系统架构,发现并没有 <code>riscv64</code> 的支持,怪不得会有这个错误提示。在 <a class="link" href="https://stackoverflow.com/a/60577077" >Stack Overflow<i class="fas fa-external-link-alt"></i></a> 上找到的答案说需要将版本升级到 8.3 以上才可以。但是我的系统是 <code>Ubuntu 18.04</code>,<a class="link" href="https://launchpad.net/ubuntu/bionic/amd64/gdb-multiarch" >系统源<i class="fas fa-external-link-alt"></i></a>所支持的最高版本就只有 8.1.1。所以没办法使用这个工具进行调试了。</p><p><del>不知道课程官方为什么推荐这个作为调试工具,可能我哪些地方没有弄对吧</del> Ubuntu 18 的源所带的版本太旧了,所以不支持 riscv 架构。需要把系统升级到 20 以上的版本才可以。</p><h3 id="no-symbol-table-is-loaded-use-the-file-command"><a href="#no-symbol-table-is-loaded-use-the-file-command" class="headerlink" title="no symbol table is loaded. use the file command"></a><code>no symbol table is loaded. use the file command</code></h3><p>出现这个问题是因为符号没有正确的加载,一开始有怀疑是编译的时候没有使用 <code>-g</code> 这个选项,导致没有把符合表打进去。但是查看 <code>Makefile</code> 后发现,其使用了 <code>-ggdb</code> 这个命令,一样能够将符号表打到可执行文件里。要解决这个问题也很简单,只需要在 <code>gdb</code> 的命令行里输入 <code>file kernel/kernel</code> 就可以把内核的符号加载进来</p><h3 id="auto-loading-has-been-declined-by-your-auto-load-safe-path’-set-to-“-debugdir-datadir-auto-load”"><a href="#auto-loading-has-been-declined-by-your-auto-load-safe-path’-set-to-“-debugdir-datadir-auto-load”" class="headerlink" title="auto-loading has been declined by your `auto-load safe-path’ set to “$debugdir:$datadir/auto-load”"></a>auto-loading has been declined by your `auto-load safe-path’ set to “$debugdir:$datadir/auto-load”</h3><p>这个也很奇怪,每次启动的时候都会输出这个警告。后来才意识到,为了出现避免出现一些<a class="link" href="https://blog.deniffer.com/post/%E9%9A%8F%E7%AC%94/qemu-gdb-can-not-access-memory/" >安全问题<i class="fas fa-external-link-alt"></i></a>,<code>gdb</code> 不会再主动的执行任何文件。所以需要在用户目录下声明,<code>xv6</code> 这个仓库的 <code>.gdbinit</code> 文件是安全的</p><p>通过 <code>echo add-auto-load-safe-path {PATH}/.gdbinit > ~/.gdbinit</code> 命令,其中 <code>{PATH}</code> 换成自己仓库路径即可</p><p>这个问题顺带影响了第二个问题。如果没有添加为可信任文件,会导致 <code>gdb</code> 不主动加载内核文件,影响符号解析。</p><blockquote><p><strong>参考</strong></p><p><a href="https://www.cnblogs.com/jiu0821/p/6244324.html"><code>GDB</code> 配置与 <code>.gdbinit</code> 的编写</a></p><p><a class="link" href="https://sourceware.org/gdb/onlinedocs/gdb/Auto_002dloading-safe-path.html" >Security restriction for auto-loading<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://blog.deniffer.com/post/%E9%9A%8F%E7%AC%94/qemu-gdb-can-not-access-memory/" >GDB调试指南1<i class="fas fa-external-link-alt"></i></a></p><p><a class="link" href="https://www.cnblogs.com/KatyuMarisaBlog/p/13727565.html" >MIT 6.S081 xv6调试不完全指北<i class="fas fa-external-link-alt"></i></a></p></blockquote>]]></content>
<summary type="html">介绍如何调试运行xv6操作系统</summary>
<category term="6.S081" scheme="https://wzzzx.github.io/categories/6-S081/"/>
</entry>
<entry>
<title>运行xv6系统</title>
<link href="https://wzzzx.github.io/6-S081/xv6-running/"/>
<id>https://wzzzx.github.io/6-S081/xv6-running/</id>
<published>2022-01-22T13:16:10.000Z</published>
<updated>2022-01-23T07:01:05.000Z</updated>
<content type="html"><![CDATA[<p>第二次倒腾这个操作系统了。整个过程其实很简单。我使用的操作系统是 <code>Ubuntu Server 18.04.1 LTS 64bit</code>,但是按照学院官网的<a class="link" href="https://pdos.csail.mit.edu/6.S081/2019/tools.html" >指导<i class="fas fa-external-link-alt"></i></a>去操作似乎有点问题?总之就是没成功。</p><span id="more"></span><h3 id="系统下载"><a href="#系统下载" class="headerlink" title="系统下载"></a>系统下载</h3><p>首先就是源码的下载,通过 <code>git</code> 可以直接下载到</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> git://github.com/mit-pdos/xv6-riscv.git</span><br></pre></td></tr></table></figure><h3 id="组件下载"><a href="#组件下载" class="headerlink" title="组件下载"></a>组件下载</h3><p>但是通过最底下的操作可以看出,这个操作系统的启动其实就是需要两个组件。<code>riscv64-unknown-elf-gcc</code> 用来编译操作系统,<code>qemu-system-riscv64</code> 用来运行操作系统。所以接下来的工作就是找到这两个组件。</p><p><a href="https://www.sifive.com/"><code>SiFive</code></a> 这家做 <code>RISC-V</code> 的厂商,在其官网的<a class="link" href="https://www.sifive.com/software" >下载页面<i class="fas fa-external-link-alt"></i></a>或者 <a class="link" href="https://github.com/sifive/freedom-tools/releases" >GitHub release<i class="fas fa-external-link-alt"></i></a> 可以看到相关工具的下载。将 <code>GNU Embedded Toolchain</code> 和 <code>QEMU</code> 这两个组件下载下来即可。</p><p>下载完后可以在 <code>bin</code> 目录找到所需要的可执行文件,可以将该目录设置到环境变量中,方便直接使用。其他的文件切记不要删除了。编译器和虚拟机在运行的时候,会使用到其中的一些动态库。</p><h3 id="运行系统"><a href="#运行系统" class="headerlink" title="运行系统"></a>运行系统</h3><p>上述工作完成后,到源码的目录执行 <code>make</code> 即可编译系统,通过 <code>make qemu</code> 命令可以调用虚拟机来运行 <code>xv6</code>。</p>]]></content>
<summary type="html">介绍如何编译运行6.S081所需要的xv6</summary>
<category term="6.S081" scheme="https://wzzzx.github.io/categories/6-S081/"/>
</entry>
<entry>
<title>二分搜索全攻略</title>
<link href="https://wzzzx.github.io/dataStructure/binary-search-raiders/"/>
<id>https://wzzzx.github.io/dataStructure/binary-search-raiders/</id>
<published>2021-12-12T16:07:24.000Z</published>
<updated>2022-12-07T15:13:57.000Z</updated>
<content type="html"><![CDATA[<p>在一个有序数组中查找特定值的时候,二分搜索法是一个很常见且高效的思路,该方法也称为折半查找(Binary Search),它是一种效率较高的查找方法,可以在数据规模的对数时间复杂度内完成查找。</p><span id="more"></span><p>但是在实现二分搜索法的时候,边界条件判断,下标变更计算等地方都很容易出现问题,这些错误会直接导致代码进入死循环或者崩溃。所以,除了在算法层面了解该算法,在代码层面也需要为什么,才能完整掌握该算法。</p><p>对于二分搜索法的问题,大体上可以分为两类,第一类是找到一个与给定值相同的值,第二类是找到第一个或者最后一个跟给定值相同的值。其实本质上都是在一个<strong>特定区间</strong>内反复折半查找。</p><h3 id="查找特定值"><a href="#查找特定值" class="headerlink" title="查找特定值"></a>查找特定值</h3><p>查找特定值是一个最为常见的操作。先看一个从逻辑推理上是正确的算法实现,代码里的注释标记了最容易出问题的五个地方</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">binarySearch</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>> &vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = vec.<span class="built_in">size</span>() - <span class="number">1</span>; <span class="comment">// 1</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (left <= right) { <span class="comment">// 2</span></span><br><span class="line"> <span class="keyword">int</span> mid = left + (right - left) / <span class="number">2</span>; <span class="comment">// 3</span></span><br><span class="line"> <span class="keyword">int</span> val = vec[mid];</span><br><span class="line"> <span class="keyword">if</span> (val == target) {</span><br><span class="line"> <span class="keyword">return</span> mid;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>; <span class="comment">// 4</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val > target) {</span><br><span class="line"> right = mid - <span class="number">1</span>; <span class="comment">// 5</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="第三点"><a href="#第三点" class="headerlink" title="第三点"></a>第三点</h4><p>这是最简单理解的一点,所以将其放置在最前面来讲。</p><p>两个数相加的时候,一定要注意<strong>溢出</strong>错误。一般情况下可以先强制转换为 <code>unsigned long long</code> 进行计算,但此处通过俩下标的差值就可以避免这种额外的转换。</p><h4 id="第一点"><a href="#第一点" class="headerlink" title="第一点"></a>第一点</h4><p>第一点是最为重要的地方,这个初始值<strong>决定</strong>了后续易错点的代码应该如何写。</p><p>除了示例代码的 <code>vec.size() - 1</code>,此处的取值还可以是 <code>vec.size()</code>。这两个值改变的是算法的<strong>搜索区间</strong>。当取值是 <code>vec.size() - 1</code> 时,算法在<strong>闭区间</strong>内搜索,值都位于 <code>[left, right]</code> 中。当取值为 <code>vec.size()</code> 时,算法会在<strong>左闭右开区间</strong>内搜索,值位于 <code>[left, right)</code> 中。后者也意味着 <code>right</code> 永远指向一个不存在的值。</p><p>当<strong>搜索区间</strong>能够确定下来之后,后续的代码其实就很容易确定了。</p><h4 id="第二点"><a href="#第二点" class="headerlink" title="第二点"></a>第二点</h4><p>当<strong>搜索区间</strong>概念能够理解后,此处应该怎么写其实就很容易理解了。</p><p>当搜索区间是一个<strong>闭区间</strong>的时候,<code>left</code> 和 <code>right</code> 所指向的值都是<strong>可访问的且未访问</strong>的。随着搜索地进行,这两个下标会逐渐接近。经过一轮搜索下标变更后,最极端的情况是 <code>left</code> 等于 <code>right</code> 了。但是在这个时候,它们所指向的值还没有被搜索到。这里可以构造一个大小为 1 的数组帮助理解。初始化的时候,<code>left</code> 和 <code>right</code> 都是 0,此时需要进行一次搜索才能判断结果。所以在这个循环判断条件里,<code>left == right</code> 时不能跳出循环。所以在这种情况下,循环的判断条件是 <code>left <= right</code>。</p><p>当搜索区间是一个<strong>左闭右开区间</strong>时,<code>left</code> 指向的是<strong>可访问的且未访问</strong>的,但 <code>right</code> 所指向的值是不可访问的。在标准库的实现中,左闭右开区间的 <code>right</code> 通常用于标记结束节点。同样的,随着搜索地进行,这两个值会不断地接近。此处也可以借助于一个大小为 1 的数组来理解。初始化的时候,<code>left</code> 是 0,<code>right</code> 是 1。在这种极端情况下,<code>left</code> 严格小于 <code>right</code>。如果此时进行搜索,一旦发现 <code>left</code> 指向的值不是目标值,下标进行变更之后,二者的值就相同了,搜索宣告结束。</p><p>简而言之,搜索持续进行的条件是 <code>left < right</code>,这点毋庸置疑。但是 <code>left</code> 能不能等于 <code>right</code> 就取决于搜索区间,不同的区间下,<code>right</code> 所代表的含义是不同的。</p><h4 id="第四点和第五点"><a href="#第四点和第五点" class="headerlink" title="第四点和第五点"></a>第四点和第五点</h4><p>同样的,第四点和第五点也是跟搜索区间相关联。</p><p>当搜索区间是<strong>闭区间</strong>的时候,两个子区间应该被划分为 <code>[left, mid - 1]</code> 和 <code>[mid + 1, right]</code>。</p><p>当搜索区间是<strong>左闭右开区间</strong>的时候,两个子区间应该被划分为 <code>[left, mid)</code> 和 <code>[mid + 1, right)</code>。</p><p>不管在哪种搜索区间下,任意一个数都应该只被搜索<strong>一次</strong>。在此基础上,划分出来的两个子区间都应该跟原先的区间保持一致,这样才能保证一次只排除一个数且后续的搜索区间是正确的。</p><h3 id="查找特定值的左边界"><a href="#查找特定值的左边界" class="headerlink" title="查找特定值的左边界"></a>查找特定值的左边界</h3><p>对于数组 <code>[1,2,2,2,3]</code>,<code>target = 2</code> 的时候,上文的算法只能返回 2 的索引值。如果希望查找 2 这个目标值的左边界,需要对算法做一定的修改。</p><p>先来看下示例代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">leftBound</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = vec.<span class="built_in">size</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (left < right) {</span><br><span class="line"> <span class="keyword">int</span> mid = left + (right - left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">int</span> val = vec[mid];</span><br><span class="line"> <span class="keyword">if</span> (val == target) {</span><br><span class="line"> right = mid; <span class="comment">// 重点</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val > target) {</span><br><span class="line"> right = mid;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> left;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h4><p>这个算法之所以能够找到左边界,重点在于注释所标记的那一句。每次查找到目标值的时候,都会将搜索区间修改为 <code>[left, mid)</code>,不断向左边收缩,直到找出所有的目标值为止。</p><p>因为搜索区间是一个左闭右开区间,所以搜索的退出条件是 <code>left < right</code>,原因跟查找目标值的算法一样。从这也可以判断出,退出条件是 <code>left == right</code>,所以最后在返回的时候,任意返回一个变量即可。</p><h4 id="返回值"><a href="#返回值" class="headerlink" title="返回值"></a>返回值</h4><p>寻找左边界的接口所返回的值其实有一个特殊的含义,即<strong>小于</strong>目标值的数有多少个。所以对于数组 <code>[1, 2, 2, 2, 3]</code> 的返回值可以有如下解释:</p><ol><li>当目标值为 0 时,返回 0,表明该数组有 0 个小于 0 的数</li><li>当目标值为 1 时,返回 0,表明该数组有 0 个小于 1 的数</li><li>当目标值为 2 时,返回 1,表明该数组有 1 个小于 2 的数</li><li>当目标值为 4 时,返回 5,表明该数组有 5 个小于 5 的数</li></ol><p>但是这样也带来了一个问题。当返回值为 0 的时候,数组中究竟存不存在目标值呢?所以我们需要破坏左边界返回值的特殊含义,让接口返回值仅仅用于确定目标值左边界的具体下标。一旦不存在该目标值,则返回 -1。</p><p>修改后的代码如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">leftBound</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (left >= vec.<span class="built_in">size</span>()) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">if</span> (vec[left] != target) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">return</span> left;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>简单解释一下这两句代码。</p><ul><li>第一个 <code>if</code> 判断用于排除数组中的值都小于目标值的情况,此时 <code>left</code> 的值会等于数组大小</li><li>第二个 <code>if</code> 判断用于区分当 <code>left == 0</code> 时,无法区分是目标值小于数组所有值还是目标值的左边界恰好位于 0 的情况</li></ul><h3 id="查找特定值的右边界"><a href="#查找特定值的右边界" class="headerlink" title="查找特定值的右边界"></a>查找特定值的右边界</h3><p>还是一样先看示例代码。注意!该代码并不完整:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">rightBound</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = vec.<span class="built_in">size</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (left < right)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> mid = left + (right - left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">int</span> val = vec[mid];</span><br><span class="line"> <span class="keyword">if</span> (val == target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val > target) {</span><br><span class="line"> right = mid;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> right;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="原理-1"><a href="#原理-1" class="headerlink" title="原理"></a>原理</h4><p>这个算法找有边界的原理跟找左边界是一样的。每次找到目标值的时候,都将搜索区间修改为 <code>[mid + 1, right)</code>,不断收缩左边界,直接符合退出条件。</p><h4 id="返回值-1"><a href="#返回值-1" class="headerlink" title="返回值"></a>返回值</h4><p>由算法的原理可以看出,每次查找到目标值都会收缩左边界。所以如果不加以操作,最终的返回值其实第一个大于目标值的数值所在下标,该返回值表明数组中<strong>小于等于</strong>目标值的元素个数。所以对于数组 <code>[1, 2, 2, 2, 3]</code> 的返回值可以有如下解释:</p><ol><li>当目标值为 0 时,返回 0,表明该数组有 0 个小于等于 0 的数</li><li>当目标值为 2 时,返回 4,表明该数组有 4 个小于等于 2 的数</li><li>当目标值为 3 时,返回 5,表明该数组有 5 个小于等于 3 的数</li><li>当目标值为 4 时,返回 5,表明该数组有 5 个小于等于 4 的数</li></ol><p>如果希望找到右边界的下标值,这些返回值明显是不正确的。所以需要略作修改,如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">rightBound</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (right == <span class="number">0</span>) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">if</span> (vec[right - <span class="number">1</span>] != target) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">return</span> right - <span class="number">1</span>; <span class="comment">// 重点</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>简单解释一下这段代码:</p><ol><li>第一个 <code>if</code> 用于排除目标值比数组中的值都要小的情况;</li><li>第二个 <code>if</code> 用于排除目标值比数组中的值都要大的情况</li></ol><p>需要重点关注的是注释所标记的那句返回值语句,这里提供三个角度的理解方法:</p><ol><li>由 <code>right</code> 作为返回值所表明的含义可知,正确的右边界下标应该是 <code>right - 1</code>;</li><li>每次找到目标值的时候,左边界都会进行一次收缩操作,这个操作的代码是 <code>left = mid + 1</code>。所以这里简单变换一下就可以算出,<code>mid = left - 1</code>;</li><li>在这个算法里,搜索区间是左闭右开区间,这意味着 <code>right</code> 所指向的位置都是不可访问的,需要向左前进一位才可以访问</li></ol><h3 id="其他搜索区间的代码"><a href="#其他搜索区间的代码" class="headerlink" title="其他搜索区间的代码"></a>其他搜索区间的代码</h3><p>二分搜索法的搜索区间有两种,上文讲解的时候只采用了其中一种,现在将三种算法不同搜索区间下的代码展示于下面:</p><h4 id="查找特定值-1"><a href="#查找特定值-1" class="headerlink" title="查找特定值"></a>查找特定值</h4><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">binarySearch</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = vec.<span class="built_in">size</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (left < right)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> mid = left + (right - left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">int</span> val = vec[mid];</span><br><span class="line"> <span class="keyword">if</span> (val == target) {</span><br><span class="line"> <span class="keyword">return</span> mid;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val > target) {</span><br><span class="line"> right = mid;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="查找特定值的左边界-1"><a href="#查找特定值的左边界-1" class="headerlink" title="查找特定值的左边界"></a>查找特定值的左边界</h4><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">leftBound</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = vec.<span class="built_in">size</span>() - <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (left <= right) {</span><br><span class="line"> <span class="keyword">int</span> mid = left + (right - left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">int</span> val = vec[mid];</span><br><span class="line"> <span class="keyword">if</span> (val == target) {</span><br><span class="line"> right = mid - <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val > target) {</span><br><span class="line"> right = mid - <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (left >= vec.<span class="built_in">size</span>()) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">if</span> (vec[left] != target) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">return</span> left;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="查找特定值的右边界-1"><a href="#查找特定值的右边界-1" class="headerlink" title="查找特定值的右边界"></a>查找特定值的右边界</h4><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">rightBound</span><span class="params">(<span class="keyword">const</span> vector<<span class="keyword">int</span>>& vec, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = vec.<span class="built_in">size</span>() - <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (left <= right)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> mid = left + (right - left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">int</span> val = vec[mid];</span><br><span class="line"> <span class="keyword">if</span> (val == target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (val > target) {</span><br><span class="line"> right = mid - <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (right < <span class="number">0</span>) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">if</span> (vec[right] != target) <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> <span class="keyword">return</span> right;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里注意一点,当目标值小于数组中的所有值时,<code>right</code> 的值会小于 0,所以此处需要做规避处理。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>由上文的讲解可以看出,正确的代码关键之处就在于<strong>搜索区间</strong>。只有在理解了搜索区间的基础上,才能够正确的写出相应的代码。</p><p>查找特定值和查找左右边界的逻辑其实是一样的,都是查找某一个特定的值,不过查找边界的时候需要额外注意边界的返回值问题,以及容错处理。</p>]]></content>
<summary type="html">介绍二分搜索法的正确操作姿势</summary>
<category term="算法与数据结构" scheme="https://wzzzx.github.io/categories/dataStructure/"/>
</entry>
</feed>