SegmentFault daryl的技术天地最新的文章
2019-02-26T19:00:00+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Redis 的 string
https://segmentfault.com/a/1190000018309248
2019-02-26T19:00:00+08:00
2019-02-26T19:00:00+08:00
daryl
https://segmentfault.com/u/daryl
5
<h2>Redis 的 string</h2>
<p>Redis 的字符串就是 SET 和 GET 操作对应的类型,算是 Redis 里最常用的类型了。</p>
<h3>0x00 动态字符串 sds</h3>
<p>Redis 内部的字符串表示,没有直接使用 C 语言字符串,而是对其进行了一定的改造,改造后的字符串在内存管理和长度计算方面的性能都有所提升。</p>
<p>举个例子,假设要存储的是字符串”redis“。</p>
<pre><code>+--------+--------+-------------+
| len | alloc | |r|e|d|i|s| |
+--------+--------++-----------++
| |
v v
flag '\0'
</code></pre>
<p>这个图就是 sds 的内存结构。sdshdr 分四个部分,从左往右一次是字符串长度、开辟的内存空间、sdshdr 的类型以及字符串本身。在字符串初始化好之后,会返回一个指针,指向字符串本身的首地址,也就是 <code>r</code> 的内存地址。这样,既能方便地享受 C 语言字符串带来的兼容性,又可以对内存管理了如指掌。</p>
<h3>0x01 redisObj</h3>
<pre><code class="c">typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;</code></pre>
<p>这是 Redis 的对象的结构,每一个 key 或者 value 都会存储成这样一个结构体。可以看一下第二个成员变量 <code>encoding</code>。这是 Redis 对这个对象做的编码操作。,Redis string 在存储的时候,会努力进行三种方式的编码,分别是 <code>OBJ_ENCODING_RAW</code>、<code>OBJ_ENCODING_INT</code> 和 <code>OBJ_ENCODING_EMBSTR</code>,针对这三种编码,尽可能地进行去优化内存空间。</p>
<p>最后一个成员变量则是指向具体的内容,如字符串类型的对象就直接指向对应的 sds 字符串的地址。</p>
<h3>0x02 OBJ_ENCODING_INT</h3>
<p>Redis 在拿到了用户 SET 命令的字符串后,如果用户 SET 了一个纯数字字符串,就会做这种优化。这种优化有一个好处,可以看到 <code>redisObject</code> 中的 <code>ptr</code> 变量,这是一个指针,也就意味着,这个变量占据 8 个字节的内存空间。8 个字节的内存空间,刚好可以存储一个 64 位的整数。Redis 会判断字符串的长度是不是小于 20,如果小于 20 则会尝试将字符串整数化。这里的 20 是因为 64 位整型可以表示的范围是 [-9223372036854775808,9223372036854775807]。</p>
<p>这样做了之后,就不需要额外的空间为这个“字符串”生成一个动态字符串,直接存在 <code>ptr</code> 就可以了。</p>
<h3>0x03 OBJ_ENCODING_EMBSTR</h3>
<p>Redis 在转换成整型失败后,就会尝试这种编码。这种编码其实是把 robj 和 sds 字符串放在了同一块内存空间中。这种编码对内存空间优化不大,但是它们的空间是连续的。这样做,我认为有几个好处。</p>
<ol>
<li>开辟和回收空间的时候,只需要进行一次操作就可以了,这样做减少了 malloc 和 free 的次数。</li>
<li>连续的空间,对系统缓存更加友好。</li>
</ol>
<p>目前,是对长度小于 44 的小字符串进行这种编码。</p>
<h3>0x04 OBJ_ENCODING_RAW</h3>
<p>在前面的几种尝试都失败之后,就只能存储成最原始的动态字符串了。但是 Redis 在这里依然还是会做一点事情,就是会把动态字符串的多余的空间给释放掉,目前(5.0)是会释放掉 10% 长度的冗余空间,如果不足 10% 就不会释放。</p>
<h3>0x05 后记</h3>
<p>Redis 对内存的优化真是做到了极致,这里还只是冰山一角,我了解到的还只是冰山一角。</p>
<blockquote>本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。</blockquote>
《深入理解计算机系统》读书笔记:5.5 vs 5.6
https://segmentfault.com/a/1190000018204652
2019-02-19T19:00:00+08:00
2019-02-19T19:00:00+08:00
daryl
https://segmentfault.com/u/daryl
4
<h2>0x00 前言</h2>
<p>没有看过或者没有看到这里的小伙伴们,看到这个标题一定觉得摸不着头脑。那这里就先来解释一下背景。</p>
<pre><code class="c">double poly(double a[], double x, long degree)
{
long i;
double result = a[0];
double xpwr = x;
for (i = 1; i <= degree; i++) {
result += a[i] * xpwr;
xpwr = x * xpwr;
}
return result;
}</code></pre>
<pre><code class="c">double polyh(double a[], double x, long degree)
{
long i;
double result = a[degree];
for (i = degree; i >= 0; i--) {
result = a[i] + x * result;
}
return result;
}</code></pre>
<p>这是 CSAPP 的两道题,每一题是一段代码,这两段代码实现了同一个功能。这两道题有一个共同的问题,比较这两段代码的性能。</p>
<h2>0x01 答案</h2>
<p>这里的答案是,poly 的性能比 polyh 的性能要高。poly 的 CPE 是 5,而 polyh 的 CPE 是 8。</p>
<p>这就显得很尴尬了,我原以为两个函数的 CPE 都是 8。</p>
<h2>0x02 我的猜想</h2>
<p>polyh 的 CPE 是 8 我没有疑问,因为这个循环里的操作是无法并行的,也就是下一次迭代会依赖上一次迭代产生的结果。所以,CPE = 5 + 3,5 是浮点数乘法的延迟下届,3 是浮点数加法的延迟下界。</p>
<p>poly 的 CPE 我原本认为也是 8,两个乘法是可以并行的,但是这个加法的是依赖于第一个乘法的值,无法并行,所以 CPE = 5 + 3 = 8。</p>
<h2>0x03 指令集并行和流水线</h2>
<p>上面的是我的猜想,所以我认为这里的答案是它们的 CPE 是相同的,性能也是相同的。但是如前面所写,答案并不是这样的。于是,我把之前看的东西都翻出来想了一下,真的不是这样的。</p>
<p>现代 CPU 是有一个流水线的概念的。什么是流水线呢,想象一下汽车车间,我们造一辆汽车,是分成了很多道工序的,比如装配发动机、装车门、轮子等等。现代 CPU 也是类似的,我们看到的一条指令,在执行的时候,经历了一长串的流水线,导致了指令真正的执行顺序和我们看到的可能是不一样的,但是由于现代出来的这种机制,可以确保最后的结果是和我们看到的是一样的。</p>
<h2>0x04 解释</h2>
<p>poly 函数,在执行的时候,由于有两个浮点数乘法单元,所以 <code>a[i] * xpwr</code> 和 <code>xpwr = x * xpwr</code> 可以并行执行。而 <code>a[i] * xpwr</code> 可以通过流水线的数据转移,让这个加法 <code>result + a[i] * xpwr</code> 可以在下一次迭代的时候执行,因为每次迭代的时候,两个乘法都不会依赖 <code>result</code> 这个结果。这样,加法和乘法可以并行执行。浮点乘法的延迟下界是 5,浮点加法的延迟下界是 3,所以浮点乘法是关键路径,CPE 也自然就是 5 了。</p>
<p>再来看看 polyh 函数。这个函数的循环里只有一个浮点乘法运算和一个浮点加法运算。先来看看浮点乘法运算,<code>x * result</code>,很显然,每一次乘法都需要依赖上一次迭代的结果,导致了加法无法和乘法并行执行。于是,CPE 就成了 5 + 3 = 8 了。</p>
<h2>0x05 最后</h2>
<p>这个例子,我觉得很有趣,因为它涉及到了一个流水线的细节。同时,也说明了,并不是操作少的代码,效率就高。</p>
<blockquote>本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。</blockquote>
<h2>0x06 参考文献</h2>
<p>《深入理解计算机系统(第 3 版)》第 4、5 章</p>
菜鸟做 bomb lab 之第一关
https://segmentfault.com/a/1190000016679875
2018-10-15T08:30:00+08:00
2018-10-15T08:30:00+08:00
daryl
https://segmentfault.com/u/daryl
1
<p>第一题比较简单,但本菜鸡也做了两个小时(╯‵□′)╯︵┻━┻。。。</p>
<p>首先打开事先已经反汇编的 bomb.s 文件,通过 <code>bomb.c</code> 已经知道每一关都是一个函数,它们的命名都是 <code>phase_x</code>,x 代表该关卡的数字,如果某个关卡输入的不正确,就会引爆炸弹 <code>explode_bomb</code>。首先看 main 函数的这几行</p>
<pre><code class="asm">400e1e: bf 38 23 40 00 mov $0x402338,%edi
400e23: e8 e8 fc ff ff callq 400b10 <puts@plt>
400e28: bf 78 23 40 00 mov $0x402378,%edi
400e2d: e8 de fc ff ff callq 400b10 <puts@plt>
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
400e44: bf a8 23 40 00 mov $0x4023a8,%edi</code></pre>
<p>打开 gdb,先给这一行打上断点 <code>break *0x400e23</code>,然后 <code>run</code> 起来。这里可以看到调用了 <code>puts</code> 这个函数,寄存器 <code>%edi</code> 存储的是函数的第一个参数,我们把它的结果打印出来 <code>x/s 0x402338</code>、<code>x/s 0x402378</code>,发现得到了运行 bomb 后输出的字符串。说明第一关就是从这里开始的。</p>
<p>由于返回值是存在 <code>%rax</code> 中的,这里 <code>mov %rax %rdi</code>,说明输入的内容传参给了 <code>phase_1</code>。在 gdb 里给 phase_1 打断点 <code>break phase_1</code>。</p>
<pre><code class="asm">0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq</code></pre>
<p>通过这里的代码,就可以分析出来,通过调用 <code>string_not_equal</code> 比较输入的字符串与 <code>0x402400</code> 存储的字符串是否相等,来决定是不是 <code>explode_bomb</code>。通过这个函数名也可以知道一定要输入与 <code>0x402400</code> 相同的字符串就可以通过第一关了。所以在这里打个断点 <code>break *0x400ee9</code>,然后 <code>x/s 0x402400</code> 打印出来这里的字符串,我这里是 <code>Border relations with Canada have never been better.</code>,然后输入这个字符串,第一关就过了~</p>
<h2>string_not_equal</h2>
<p>虽然这样就过关了,但是我还是对这里的代码好奇,毕竟是学习嘛,看看这里的代码熟悉熟悉汇编。</p>
<pre><code class="asm">0000000000401338 <strings_not_equal>:
401338: 41 54 push %r12
40133a: 55 push %rbp
40133b: 53 push %rbx
40133c: 48 89 fb mov %rdi,%rbx
40133f: 48 89 f5 mov %rsi,%rbp
401342: e8 d4 ff ff ff callq 40131b <string_length>
401347: 41 89 c4 mov %eax,%r12d
40134a: 48 89 ef mov %rbp,%rdi
40134d: e8 c9 ff ff ff callq 40131b <string_length>
401352: ba 01 00 00 00 mov $0x1,%edx
401357: 41 39 c4 cmp %eax,%r12d
40135a: 75 3f jne 40139b <strings_not_equal+0x63>
40135c: 0f b6 03 movzbl (%rbx),%eax
40135f: 84 c0 test %al,%al
401361: 74 25 je 401388 <strings_not_equal+0x50>
401363: 3a 45 00 cmp 0x0(%rbp),%al
401366: 74 0a je 401372 <strings_not_equal+0x3a>
401368: eb 25 jmp 40138f <strings_not_equal+0x57>
40136a: 3a 45 00 cmp 0x0(%rbp),%al
40136d: 0f 1f 00 nopl (%rax)
401370: 75 24 jne 401396 <strings_not_equal+0x5e>
401372: 48 83 c3 01 add $0x1,%rbx
401376: 48 83 c5 01 add $0x1,%rbp
40137a: 0f b6 03 movzbl (%rbx),%eax
40137d: 84 c0 test %al,%al
40137f: 75 e9 jne 40136a <strings_not_equal+0x32>
401381: ba 00 00 00 00 mov $0x0,%edx
401386: eb 13 jmp 40139b <strings_not_equal+0x63>
401388: ba 00 00 00 00 mov $0x0,%edx
40138d: eb 0c jmp 40139b <strings_not_equal+0x63>
40138f: ba 01 00 00 00 mov $0x1,%edx
401394: eb 05 jmp 40139b <strings_not_equal+0x63>
401396: ba 01 00 00 00 mov $0x1,%edx
40139b: 89 d0 mov %edx,%eax
40139d: 5b pop %rbx
40139e: 5d pop %rbp
40139f: 41 5c pop %r12
4013a1: c3 retq</code></pre>
<p>看代码,发现很符合书上讲的,<code>%r12</code>、<code>%rbp</code>、<code>%rbx</code> 都是被调用者保存的寄存器。</p>
<p>首先 0x401342 ~ 0x40135a,判断了它们的长度是不是相同,如果长度不相同,那么它们必然不是同一个字符串。<code>mov $0x1,%edx</code> 和 <code>mov %edx,%eax</code> 返回了 1。</p>
<p>0x40135c ~ 0x401361 这几行,判断了所输入的字符串的第一个字符是不是 <code>\0</code>。因为走到这条命令,已经判断过长度是相同的了,如果其中的一个字符串的首字符是 <code>\0</code>,那么另外一个必然是一样的(所有的字符串一定都包含一个 <code>\0</code>),所以这里直接就返回 0。</p>
<p>0x401361 ~ 0x40137f 是一个循环,它遍历了两个字符串,每一个字符是不是相同的,直到遇到 <code>\0</code>。</p>
<h2>string_length</h2>
<pre><code class="asm">000000000040131b <string_length>:
40131b: 80 3f 00 cmpb $0x0,(%rdi)
40131e: 74 12 je 401332 <string_length+0x17>
401320: 48 89 fa mov %rdi,%rdx
401323: 48 83 c2 01 add $0x1,%rdx
401327: 89 d0 mov %edx,%eax
401329: 29 f8 sub %edi,%eax
40132b: 80 3a 00 cmpb $0x0,(%rdx)
40132e: 75 f3 jne 401323 <string_length+0x8>
401330: f3 c3 repz retq
401332: b8 00 00 00 00 mov $0x0,%eax
401337: c3 retq</code></pre>
<p>这个函数就比较简单了,其实就是找到 <code>\0</code> 的位置,然后返回其余首地址的差,即长度。这个翻译成 C 语言可以这么写。</p>
<pre><code class="c">int string_length(char *s)
{
char *b = a;
while (*b != 0)
b = b + 1;
return (int) (b - a);
}</code></pre>
<blockquote>本文是作者在看《深入理解计算机系统》以及完成 bomb lab 时的理解与总结,谨此记录下来已被日后翻阅。同时,也分享给各位希望了解这些知识的同道者们。由于作者水平有限,如有错误之处,望不吝赐教,深表感谢。</blockquote>
我的 2017 —— 一个 PHPer 的自白
https://segmentfault.com/a/1190000012676646
2018-01-02T23:58:59+08:00
2018-01-02T23:58:59+08:00
daryl
https://segmentfault.com/u/daryl
17
<p>转眼间 2017 年过去了。我已经不能说自己是去年的毕业生了,时光匆匆,感觉自己越来越老了。</p>
<p>这一年,我所经历的,让我收获很多,让我懂得很多,让我明白了很多。也许是明确了某一个目标,也许是其它的什么,我觉得,2017 年也许真的是我的一个开端。</p>
<h2>事</h2>
<p>说实话,这一段的标题,真的不好想。所以我写的时候,空了出来。这里我要写的是,关于我对编程的一些感悟。经历了一些事,也参加了一些事,觉得,啊,原来我想要的,是这样的。</p>
<h3>大会</h3>
<p>2017 年,我去了 PHPCON。说实话,这是我第二次花钱去参加大会,也是花钱最多的一次。不过真的觉得很值,因为这次大会,让我明白了很多。首先,是鸟哥关于 PHP7 的介绍。他通过底层实现来介绍 PHP7 的一些优化的地方,但是由于水平有限,我不能听懂。包括韩天峰的演讲,我也一样不能完全理解。究其根本,还是由于我的基础知识太弱导致的。于是,我在会后,努力去学习这些基础知识,才发现,啊,原来他们讲的是这样的!</p>
<p>去年,我给我自己的定位是“Re: 从零开始的编程生活”。今年,也是这么觉得,而且感觉更加强烈了。</p>
<h3>小会</h3>
<p>值得一提的是,今年 7 月份,我参加了一个 Swoole 的分享会。分享会是在一家咖啡馆里开的,很有氛围(旁边就是 bilibili link(雾。会后,韩天峰 dalao 请了我们外地去的盆友吃了饭(然后被抢买单了(雾。这都不是重点,重点是,通过这次,我明白了,我接下去的路该怎么走。我更加明白了,我要学习哪些东西。</p>
<h3>其它</h3>
<p>其它嘛,也不好说,说了也不好。懂的自然懂。</p>
<h2>书</h2>
<p>上面都说了,我也知道自己该看些什么了。于是,我花了半年的时间看完了《深入理解计算机系统》,受益匪浅。同时,我看完了《深入 PHP:面向对象、模式与实践》,了解了很多常用的设计模式,完善了面向对象的很多知识。还有《Go 语言实战》。Go 是我一直想学习的一门语言,现在终于有机会完整地去学习了。我又开始看了《现代操作系统》和《数据结构与算法分析(C 语言描述)》。对于数据库,我现在正在看《高性能 MySQL》,收益颇多。</p>
<h2>出游</h2>
<h3>沙巴</h3>
<p>这是部门组织的出游,获得了最佳团队,拿到了一笔经费,于是有了这次出游。这次旅行,我感受到了异国风情,还和海水近距离地接触了。浮潜这个一直不敢做的项目,也尝试了。总之,这是一个不错的地方</p>
<h3>四川</h3>
<p>是的,今年我又去了四川,和群里的某基友一起去的。不过这次没有在成都逗留,而是直接出发去了稻城亚丁。从四川去稻城亚丁,花费了 3 天时间。从几百米的海拔,到近 5000 米的海拔。在汶川,我看到了璇口中学遗址,感触颇多。在前往稻城亚丁的路上,我看到了任何其它地方都看不到的景色。美!在亚丁,也花了 4 个小时爬山,看到了美丽的风景,真的值了。于是,我的下个目的地是——西藏。</p>
<p><img src="/img/remote/1460000012676651?w=1920&h=1080" alt="" title=""></p>
<h2>2018</h2>
<p>对于 2018,我给自己定了很多目标,不管能不能达成,我都要尽力去做。</p>
<p>2017 年,我虽然看了一些书,但是我觉得还是远远不够的。我希望在 2018 年,我能够阅读完《现代操作系统》、《数据结构与算法分析(C 语言描述)》。这些都是基础中的基础,我现在明白了,当了解了这些后,其它的也都能理解了。还有《高性能 MySQL》也要读完。再有就是《UNIX 环境高级编程》和《UNIX 网络编程》。这也是做服务端编程的基础,这两本书也许读不完,但是我会把它们当做一个持续的目标,这个目标也包括《TCP/IP 详解》。2017 年学习了 Go 语言,但是发现似乎《Go 语言实战》这本书讲的过于宽泛了。我希望能够阅读一遍《Go 程序设计语言》,辅之以实践。</p>
<p>2017 年,我坚持阅读 Laravel 的源码,让我学到了很多。今年,我依然会坚持阅读 Laravel 的源码。同时,我还尝试阅读了 PHP 的源码。发现,把它和上面的三本书配合起来,非常的合适。所以,2018 年,我依然要坚持阅读 PHP 的源码,同时学习扩展的编写。在有余力的情况下,我还要阅读 Nginx 的源码。</p>
<p>2018,坚持!</p>
《现代操作系统》读书笔记——进程
https://segmentfault.com/a/1190000012463746
2017-12-18T09:23:00+08:00
2017-12-18T09:23:00+08:00
daryl
https://segmentfault.com/u/daryl
4
<blockquote>本文原始地址:<a href="https://link.segmentfault.com/?enc=fJyBtwdVnLx7%2F%2BPSVJtlSQ%3D%3D.D0gSF%2FwbsF0vW53p1TpdeJdbvQ69jLlRmvZabxQUlG8bwZQHqmVCdRWUjSkl0w6HS2U8ofyNLVvl0bnnmMxNFmrZ2fhrEaJto1OrB1wl540%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=3RXkvZDqhtpaNLXyLargnw%3D%3D.OMWKNlqn6aEEkwTsArEM9ngA0yqshZjg35ECEXB1O9gUM378YyXLNOuZYBPwz8tXeRnge6pcNewmQmsqOGEXHjKKqv%2FgChiXb54gUF50vGU%3D" rel="nofollow">https://is.daryl.moe/2017/12/...</a>
</blockquote>
<p>一个进程就是一个正在执行的程序实例,它包括程序计数器、寄存器以及变量的当前值。一个程序运行,它的逻辑计数器装入 CPU 的程序计数器中;一个程序暂停,CPU 的程序计数器被保存在内存的逻辑程序计数器中,在下次运行前不占用 CPU。</p>
<p>要特别注意的是,进程并不是在 CPU 中一直运行的,CPU 会在各进程之间来回切换,所以每个进程执行的速度是不确定的。所以,大多数进程并不受 CPU 多道程序设计或其它进程相对速度的影响。</p>
<h2>进程的创建和终止</h2>
<p>有四种情况会导致进程的创建,它们分别是系统初始化、正在运行的程序执行了创建进程的系统调用、用户请求创建一个新进程、一个批处理作业的初始化。拿 Linux 为例,Linux 启动时的第一个进程是 0 号进程,它是所有进程的祖先。其次是 1 号进程,它是所有用户进程的祖先。</p>
<p>我们都知道 Nginx。当我们启动 Nginx 后,它一直在默默地监听端口执行 WEB 服务,而我们没有感知。这一类进程,便是守护进程。</p>
<p>任何进程,都可以创建一个新的进程,这个进程便是子进程,执行的是 <code>fork</code> 系统调用。进程可以 fork 子进程,子进程共享父进程的数据,但是,子进程对数据做的任何修改,对于父进程,都是不可见的。子进程的内存地址空间,是父进程的副本,是不同的地址空间。</p>
<p>进程只能有一个父进程,但是一个进程可以有多个子进程。在 UNIX 中,进程不能剥夺其子进程的继承权。</p>
<p>可写的空间是不共享的,共享的空间是不可写的。子进程可以共享父进程的内存空间,但是要通过写时复制共享。即在修改部分内存,先要明确地复制,以确保发生在私有的内存区域。</p>
<p>进程终止通常由下面的情况引起,正常退出、出错退出、严重错误、被其他进程杀死。其中后面两种情况是非自愿的。Linux 中自愿终止进程可以通过调用 <code>exit</code> 系统调用完成。</p>
<h2>进程的状态</h2>
<p>进程由三种状态,运行、阻塞和就绪。</p>
<p>处在运行态的进程,在这个时刻实际占用 CPU。</p>
<p>处在就绪态的进程具备可以运行条件,但是因为其它进程正在运行,而被操作系统暂停运行。</p>
<p>处在阻塞态的进程,因该进程调用了本地阻塞的系统调用,导致暂停运行。处在阻塞态的进程,不具备可以运行的条件。除非外部某实践发生,例如本地阻塞的调用完成,方能够转换为就绪态,等待操作系统调度。</p>
<h2>进程的实现</h2>
<p>操作系统维护这一个进程表,每一个进程的详细信息都保存在这张进程表中。包括程序计数器、堆栈指针、内存分配情况、文件打开情况、中断向量等。</p>
<p>如前面所说,每个进程都不是一直在 CPU 中运行地。其中就绪态和阻塞态是非运行状态。当操作系统将正在运行的进程切换为就绪态,或者进程因为调用了本地阻塞的系统调用而进入阻塞态时,其运行信息被保存在进程表中。当操作系统重新切换进程为运行状态时,将进程表中的信息恢复,就像进程没有中断一样。</p>
<p>以 IO 为例。当一个进程 A 调用了网络 IO 的系统调用时,由于该系统调用时阻塞的,于是操作系统将其切换为阻塞态,并且将它的现场都保存在进程表中,并将此地址与中断向量映射保存。然后,切换 B 进程。一段时间过去,此时可能是 C 进程在 CPU 中运行,A 进程调用的 IO 完成了,则该硬件发生了一个中断。此时,操作系统将中断正在运行的 C 进程,同时通过中断向量找到进程表中的 A 进程的现场并恢复到 A 进程没有中断时的状态,继续运行。</p>
<p>一个进程,在其生命周期中,会被中断数千次。每次中断后恢复运行,都会恢复到未中断时的状态。</p>
<h2>关于进程变慢的误区</h2>
<p>这里只说说自己的理解。在工作的时候,偶然听到同事这样说,意思大致是这样的:一个进程在网络请求的时候,会影响其它进程的速度,导致其它进程变慢。这是不对的。一个进程,在遇到本地阻塞调用时,会中断。这时,它的现场被保存在了进程表中,CPU 运行其它的进程。不存在一个进程会导致其它进程运行变慢的可能,只会存在调度因为进程数量变多而变慢的情况,它们是不一样的。</p>
<blockquote>本文是作者阅读《现代操作系统》的一些总结与理解,谨此记录下来已被日后翻阅。同时,也分享给各位希望了解这些知识的同道者们。由于作者水平有限,如有错误之处,望不吝赐教,谨表感谢。</blockquote>
《现代操作系统》读书笔记——线程
https://segmentfault.com/a/1190000012382588
2017-12-12T09:30:00+08:00
2017-12-12T09:30:00+08:00
daryl
https://segmentfault.com/u/daryl
5
<p>线程是最小的任务调度单位,是依赖于进程而存在的迷你进程。和进程一样,线程也有三种状态——运行、就绪、阻塞。我认为,线程是进程中任务真正的执行者,而进程提供了内存空间、CPU、程序计数器以及寄存器让线程使用。</p>
<h2>为什么要存在</h2>
<p>对于进程来说,多个进程之间无法分享内存空间,对于一些应用而言,共享内存空间的能力是必须的,而同一个进程下的多个进程,是共享进程的内存空间的。同时,由于这一特性,线程的创建相较于进程,要快得多。</p>
<p>还有一点,可能也是很多人误解的,那就是线程可以加快程序的运行速度。这是一种错误的观点。多线程并不能加快程序执行的速度,而是能充分利用 CPU,从而给我们造成了程序速度变快的假象。例如,在 I/O 处理时,线程阻塞另一个线程开始,而不需要等待 I/O 完成。</p>
<p>这里,我们应该知道,在单 CPU 的情况下,某一时刻只能有一个进程运行,只能有一个线程运行。</p>
<h2>用户态与内核态</h2>
<p>线程有两种,一个是内核态线程,一个是用户态线程。</p>
<p>当在一个进程中创建一个内核态线程时,这个线程便陷入了内核中,同时占据 CPU、寄存器和程序计数器,并且和进程一样,将线程表存在内核中。内核对这个线程是有感知的,并且内核直接参与线程的调度。</p>
<p>而用户态线程的状态是存储在进程中的。进程中有一个专门的运行时系统,用于调度用户线程。所以,每个进程可以自己实现自己的调度算法,从而可以增加程序的灵活度。例如,在一个线程要做可能会造成阻塞的事情时,会通知运行时系统,由运行时系统来通过进程中的线程表的线程的状态,来决定调度线程。</p>
<p>但是,由于内核不直接参与调度用户线程,那么就有一个问题,进程调度线程需要线程主动让出,这样用户线程的权利是非常大的。如果一个线程做了造成本地阻塞的事情而不通知进程,那么就会使整个进程一直处于阻塞状态,即使进程中其它线程是就绪状态。</p>
<p>这里插一句,好像上面的事情和现在说的比较多的协程很相似。没错,我认为,协程就是用户态线程,它有相当高的主动权,来通过进程的堆栈来调度,可以有效解决并发的问题。</p>
<h2>混合实现</h2>
<p>可以看到,内核线程和用户线程都有各自的优点和缺点。</p>
<p>内核线程由内核直接调度,在多处理机系统中可以实现同时运行。而且由于是内核调度,线程没有主动权,可以避免线程占据 CPU 太久而导致其他线程无法运行。但是由于内核线程是内核直接调度的,在调度时会陷入内核,这个代价是非常大的。</p>
<p>用户线程由进程中的运行时系统来调度,所有的线程的数据都保存在进程栈中,而不需要陷入内核,不需要上下文切换,也不需要对缓存进行刷新,所以这样的调度时非常快的。同时用户线程由进程实现调度算法,有很强的扩展性。但是由于用户线程的主动权非常大,可能会导致线程阻塞但其它就绪的线程没有机会运行。而且由于线程的数据均保存在进程的内存中,如果线程很多,可能会占用相当多的内存,从而发生一些问题。</p>
<p>既然内核线程和用户线程由各自的优点,那么可以将它们的有点集合起来。例如,可以使用内核线程,并且内核线程和用户线程多路复用,通过内核线程来控制和使用用户线程。</p>
<h2>调度程序激活机制</h2>
<p>调度程序激活的目标是模拟内核线程的功能,但是为线程提供在用户空间中才能实现的更好的性能以及更强大的灵活性。</p>
<p>进程的运行时系统,将线程分配在处理器上。当内核了解到一个线程阻塞时,它启动运行时系统,以此当做通知,让运行时系统决定如何调度自己的拥有的线程。</p>
<p>在某个用户线程运行的过程中,发生了一个硬件中断,此时 CPU 进入内核态。如果进程中的线程需要这个中断,进程对中断感兴趣,那么进程将被中断的线程挂起并保存在堆栈中,然后选择线程进行调度。如果不感兴趣,则恢复被中断的线程。</p>
<blockquote><p>本文是作者阅读《现代操作系统》的一些总结与理解,仅此记录下来已被日后翻阅。同时,也分享给各位希望了解这些知识的同道者们。由于作者水平有限,如有错误之处,望不吝赐教,谨表感谢。</p></blockquote>
源码安装 NSQ
https://segmentfault.com/a/1190000012102734
2017-11-22T10:00:00+08:00
2017-11-22T10:00:00+08:00
daryl
https://segmentfault.com/u/daryl
0
<p>因为业务需要,要用到 NSQ。所以学习了下 NSQ。首先是安装,我在自己电脑上,倾向于源码安装。一是源码安装可以安装最新的代码,二是整个安装过程可以自己掌控。</p>
<p>但是,安装过程中遇到了一些坑。主要还是我对 Go 以及一些衍生工具用的不是特别熟悉,并且在网上搜索到的文章,都是抄来抄去的很多并不能解决我的问题。所以我把整个安装过程记录下来,给自己一个备忘,给别人一个方便。</p>
<h2>安装 Go</h2>
<p>NSQ 是用 Go 写的,所以安装 NSQ 之前,要先安装 Go。</p>
<p>我这里给出安装具体过程的命令。具体可以参考我写的另外一篇文章 <a href="https://segmentfault.com/a/1190000009594143">从零开始学习 Go ——安装</a>。</p>
<pre><code class="shell">echo "export GOROOT=$HOME/.golang/go" >> ~/.bash_profile
echo "export GOPATH=$HOME/.golang/path" >> ~/.bash_profile
echo "export PATH=$PATH:$HOME/.golang/go/bin" >> ~/.bash_profile
echo "export GOROOT_BOOTSTRAP=$HOME/.golang/go1.4" >> ~/.bash_profile
source ~/.bash_profile
cd ~
mkdir .golang
git clone https://github.com/golang/go.git go
cp -r go go1.4
cd go1.4
git checkout -b release-branch.go1.4 origin/release-branch.go1.4
cd src
./make.bash
cd ../../go
git checkout -b release-branch.go1.8 origin/release-branch.go1.8 #可以根据需要选择合适的版本
cd src
./make.bash
go version</code></pre>
<h2>安装 dep</h2>
<p>由于 NSQ 采用了 dep 作为包管理工具,所以我们还要安装这个工具。</p>
<pre><code class="shell">go get -u github.com/golang/dep/cmd/dep</code></pre>
<p><code>go get</code> 是 Go 官方的包管理工具。执行完这个命令之后,我们会发现在 <code>$HOME/.golang/go/bin</code> 中能够找到可执行文件 <code>dep</code>。</p>
<h2>安装 NSQ</h2>
<p>准备工作都做完了,我们正式开始安装 NSQ 了。这里有官方文档,可以看一下。<a href="https://link.segmentfault.com/?enc=uOUr4WENk%2BjudoRnw29jpw%3D%3D.AgjXo3ditAWDk7cZumrn6acO3kW4DT85Bp0P51DRv5P27OUD4LO%2Fj%2FdqzGn3B19p" rel="nofollow">http://nsq.io/deployment/installing.html</a>。</p>
<pre><code class="shell">git clone https://github.com/nsqio/nsq $GOPATH/src/github.com/nsqio/nsq
cd $GOPATH/src/github.com/nsqio/nsq
dep ensure</code></pre>
<p>官网写到了这一步。然后,我发现,我到处都找不到 NSQ 的执行文件。然后我各种姿势上网搜,结果搜出来的都没什么软用。于是我仔仔细细地研究了下目录里面的文件,发现了一个 <code>Makefile</code> 文件。打开一看,发现,需要 make 一下才可以。所以我们需要多做下面这一步。</p>
<pre><code class="shell">make</code></pre>
<p>好了,到此 NSQ 就安装完了。</p>
从零开始写 PHP 扩展(一)
https://segmentfault.com/a/1190000012025326
2017-11-16T10:00:00+08:00
2017-11-16T10:00:00+08:00
daryl
https://segmentfault.com/u/daryl
24
<p>PHP 是用 C 语言写的。对于每个 PHPer 来说,都有着内心的一种希望写扩展的冲动了吧。然而,缺乏一个很好的切入点。Google 上搜 PHP 扩展开发,大部分都是复制品文章,甚至有些人连操作都没有操作过就搬运在了自己的博客。不过也有几篇好教程,但是都是 PHP 5 时代的产物,隐藏着非常多的坑。我会将我自己慢慢踩坑的过程记录下来,也许这就成了其它人的“教程”了吧。</p>
<h2>生成一个扩展</h2>
<p>想必很多人已经看到很多网上的教程了。大多都是教我们执行这个命令:<code>$ ./ext_skel --extname=extname</code>。但是,当你 clone 了 PHP 源码后会发现,master 分支下并没有 <code>ext/ext_skel</code> 这个文件。所以,我总结了一下:</p>
<p>如果你是直接下载 PHP 的源码,或者在已经 release 的版本分之下,你可以执行这个命令</p>
<pre><code class="bash">$ cd ext
$ ./ext_skel --extname=extname</code></pre>
<p>如果你是直接在 master 分支下,只有 <code>ext_skel.php</code> 文件,这个时候你就直接可以执行这个 PHP 文件</p>
<pre><code class="bash">$ cd ext
$ php ext_skel.php --ext extname</code></pre>
<p>由于我是直接在 master 分支下开发的,所以后面的都是默认在 master 分之下的操作。</p>
<p>生成了扩展之后,我们会看到四个文件和一个文件夹。现在这个阶段,我们只需要用到两个文件,<code>.c</code> 文件和 <code>.h</code> 文件。</p>
<h2>一个小坑</h2>
<p>在我们生成好扩展之后,我们可以试着编译一下</p>
<pre><code class="bash">$ phpize
$ ./configure
$ make && make test</code></pre>
<p>我们会惊讶地发现,编译的时候会有一个 warning。</p>
<pre><code>warning: implicit declaration of function
'ZEND_PARSE_PARAMETERS_NONE' is invalid in C99 [-Wimplicit-function-declaration]
ZEND_PARSE_PARAMETERS_NONE();
^
1 warning generated.</code></pre>
<p>然后你再执行 <code>make test</code> 发现有一个测试没有通过。没错,脚本为我们生成好的文件,居然通不过自己的测试。有没有觉得很诡异。我们看看 warning 的具体信息。找不到函数 <code>ZEND_PARSE_PARAMETERS_NONE</code>。看了一下文件,发现在第 15 行。看看这个函数名大概也能猜出来是什么意思了。于是我去 PHP 源码里搜了一下。可是我们发现了这样一个宏定义。</p>
<pre><code class="c">#ifndef zend_parse_parameters_none
#define zend_parse_parameters_none() \
zend_parse_parameters(ZEND_NUM_ARGS(), "")
#endif</code></pre>
<p>替换掉原来的大写之后,就没有 warning 了。这也算是官方给我们挖了一个小坑吧。虽然大写的有宏定义,但是为什么会报错,我也不太清楚了。</p>
<h2>定义一个函数</h2>
<p>我想,大多数人写扩展,肯定至少希望实现一个函数,不会是要几个全局变量就去写个扩展的吧(雾</p>
<p>这里 PHP 给我们提供了一个有用的宏 PHP_FUNCTION。生成好的代码里也有定义好的两个函数,可以参照它的用法。这个宏最终会被翻译成一个函数。例如 <code>PHP_FUNCTION(name)</code> 最终会被翻译成 <code>void zif_name(zend_execute_data *execute_data, zval *return_value)</code></p>
<p>同时我们看到有定义了这么一个数组</p>
<pre><code class="c">const zend_function_entry cesium_functions[] = {
PHP_FE(cesium_test1, arginfo_cesium_test1)
PHP_FE(cesium_test2, arginfo_cesium_test2)
PHP_FE_END
};</code></pre>
<p>我们需要将新添加的函数添加到这个数组里。像这样</p>
<pre><code class="c">const zend_function_entry cesium_functions[] = {
PHP_FE(cesium_test1, arginfo_cesium_test1)
PHP_FE(cesium_test2, arginfo_cesium_test2)
PHP_FE(name, NULL)
PHP_FE_END
};</code></pre>
<p>记住,结尾不要加分号或者逗号。最后,我们可以个这个函数一个输出</p>
<pre><code class="c">PHP_FUNCTION(name)
{
php_printf("Hello\n");
}</code></pre>
<p>编译安装完了之后我们就可以使用这个函数了</p>
<h2>总结</h2>
<p>本文仅仅是展示了从创建扩展开始到运行的全过程,本着能运行的心态来走完这些流程。</p>
<blockquote><p>由于作者水平有限,如有错误,敬请不吝赐教。</p></blockquote>
浅入理解单例模式
https://segmentfault.com/a/1190000011637047
2017-10-20T09:00:00+08:00
2017-10-20T09:00:00+08:00
daryl
https://segmentfault.com/u/daryl
3
<h2>问题</h2>
<h3>恼人的全局变量</h3>
<p>在 PHP 中,甚至不只 PHP 中,我们都会用到全局变量,以保存全局状态。可是,往往全局变量是全局共享的,任何地方任何代码都有可能将其覆盖。例如,我们定义一个全局变量叫做 PHONE。我们在某一行代码中,将其定义成了 iPhone,但是我们不小心在另一行代码中将其覆写成了 Nokia。这就非常的尴尬了,因为本来我们并不想它被覆写。</p>
<h3>繁琐的参数传递</h3>
<p>在一个系统中,我们会定义许多的方法,生成很多的对象。有时候,我们会使用很多的方法,对同一个对象做操作。在不使用全局变量的情况下,我们需要将对象作为参数传入方法中。但是这样传递同一个对象,可能会造成混乱,还可能造成不必要的依赖。</p>
<p>其实我们只需要一个全局可访问的对象就可以解决这个,但是全局变量又会出现我们上面的说的问题。</p>
<h2>解决</h2>
<h3>目标</h3>
<p>我们要解决这些问题,我们对这样的对象有下面的几个目标。</p>
<ul>
<li>这个对象,无论在哪里都能访问,就想全局变量一样。</li>
<li>这个对象,和全局变量不同,不能被覆写。</li>
<li>这个对象,整个系统中只存在一个,对它的修改在整个系统中都能被感知到。</li>
</ul>
<p>以上的几个目标,就是我们所需要的,也就是单例模式的特征。</p>
<h2>UML</h2>
<p><img src="/img/remote/1460000011637052?w=680&h=578" alt="" title=""></p>
<h3>实现</h3>
<pre><code class="php">class Preference
{
private static $instance;
private $props = [];
private __construct() {}
public static function getInstance()
{
if (empty(self::$instance)) {
self::$instance = new Preference();
}
return self::$instance;
}
public function setProperty($key, $value)
{
$this->props[$key] = $value;
}
public function getProperty($key)
{
return $this->props[$key];
}
private function __clone() {}
private function __sleep() {}
private function __wakeup() {}
}</code></pre>
<p>我们在这里引入了一个私有的构造函数,这样,外部就无法实例化这个对象了。同时,我们使用 <code>getInstance</code> 方法来获取具体的实例,而无法去覆写它,这就达成了第二个目标。</p>
<p>由于 <code>$instance</code> 和 <code>getInstance</code> 都是静态的,所以我们可以通过 <code>Preference::getInstance()</code> 访问,具体的实例。这样就使得全局都可以访问到它了,它就像全局变量一样了,这就达成了第一个目标了。</p>
<p>对于这个类,我们无法生成第二个对象,因为它的构造函数是私有的,并且 <code>__clone</code> 方法是私有的,而且,<code>getInstance</code> 在判断已经有了一个实例的情况下默认返回该实例。这就达成了第三个目标了。</p>
<p>同时,我们也尽量避免序列化这个实例,所以我们给 <code>__wakeup</code> 和 <code>__sleep</code> 这两个魔术方法私有。</p>
<p>这就是单例模式。</p>
<h2>后记</h2>
<p>对于单例模式,其实没有那么高大上。只不过是更改的对象的访问范围,以及对象始终存在,仅此而已。</p>
<blockquote><p>最后,本文章是作者在学习设计模式时的感想。部分参考自《深入 PHP 面向对象、模式与实践(第 3 版)》。如有错误,感谢大神不吝赐教。</p></blockquote>
浮点数那些事儿
https://segmentfault.com/a/1190000011328199
2017-09-24T09:31:43+08:00
2017-09-24T09:31:43+08:00
daryl
https://segmentfault.com/u/daryl
3
<blockquote><p>本文为作者自己的总结的,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。</p></blockquote>
<p>说起浮点数,大家都是又恨又爱的。爱呢,是因为,只有它可以方便地使用小数;恨呢,是因为它并不能精确地表示小数。</p>
<p>以 PHP 为例:<code>floor((0.1 + 0.7) * 10)</code> 这样一个函数调用,根据数学老师死得晚原理,大家都能得出 8 这个结果。可是事实上呢?它会返回 7。数学老师的棺材板。。。(╯‵□′)╯︵┻━┻</p>
<p>可是为什么会出现这种情况呢?这就要从浮点数的特性说起了。</p>
<h2>万物皆二进制</h2>
<p>我们都知道,在计算机中,一切的一切都是二进制表示的。假设一个 4 字节整型的十进制数 8,在大端表示的机器中,表示成 <code>00000000 00000000 00000000 00001000</code>(<code>0x0000008</code>)。将十进制整数转换成二进制数,是非常容易的。可是,小数呢?比如,我们要表示 1.75,该怎么存储在计算机中呢?显然,不能像整数一样存储了。</p>
<h2>小数的二进制</h2>
<p>让我们回忆一下,在十进制中,小数是怎么计算的。上面的 1.75 我们是这么算的:1 × 10^0 + 7 × 10^-1 + 5 × 10^-2 。那么我们按照相同的规则,来用二进制计算一下小数部分:0.75 = 1/2 + 1/4,也就是 1 × 2^-1 + 1 × 2^-2 ,再加上前面的整数部分,那么整个式子就变成了 1 × 2^0 + 1 × 2^-1 + 1 × 2^-2 ,写成二进制形式就是 1.11。所以,1.75 的二进制表示是 1.11。</p>
<p>对于将小数转换为二进制,和整数部分除二取余相反的,是乘二取整。</p>
<p>0.75 * 2 = 1.5 -> 1<br>0.5 * 2 = 1 -> 1</p>
<p>所以我们同样可以得出 1.11。</p>
<h2>科学计数法</h2>
<p>好了,我们已经知道如何表示一个小数的二进制了。辣么,问题来了。学过 C 语言的同学都知道,一个 <code>float</code> 只有 4 字节,一个 <code>double</code> 也只有 8 字节。那么,这么表示一个小数,好像范围很有限。</p>
<p>在数学老师哭晕在厕所之前,我们应该还记得十进制数中有这么一个东西——科学计数法,我们可以很方便地用它来表示很大的十进制数。那么,同理,我们也可以用在浮点数的表示上。</p>
<p>让我们先来回忆一下,科学计数法的表示。假设我们有一个数 17500,我们可以用科学计数法表示成 1.75 × 10^4 。我们照葫芦画瓢,在二进制数中,假设有一个数是 11010。我们来和十进制对应一下。十进制是乘 10,那么二进制就是乘 2,我们对应的就可以写成 1.101 × 2^100 。对,其实就是这么简单。那也许有的人会问了,为什么不写成 0.1101 × 2^101 呢?我们再来回忆一下,在十进制科学计数法中,是不是有一个规定,整数部分的范围是 [1,10)。那对应到我们的二进制数上,这个规定就可以变成 [1,2) 了,没错,对应关系就是这么简单。</p>
<h2>浮点数</h2>
<p>好了,我们现在也知道怎么使用二进制来表示小数,以及使用科学计数法来表示二进制小数了。那么,我们距离把数字存入计算机内存仅剩一步之遥了,我们要把所有的东西存到内存里去,那么我们就需要合理地分配内存空间。浮点数有两种,一种是单精度浮点数(<code>float</code>),占用 4 字节的内存。其中,1 位是符号位,8 位是阶码(幂),23 位是尾数(小数部分)。</p>
<p>细心的各位可能会发现,好像没有整数部分?别急,这就是上面那个规定的有用之处。当整数部分在 [1,2) 之间时,也就只可能取到一个值 1,那么,对于这个值,我们是不是就可以当做默认值而不记录在浮点数的表示中了?而这样,我们的浮点数的精度又多了一位(小数部分的位数决定了精度)。这种表示叫做隐含 1 开头的表示。</p>
<h3>规格化与非规格化</h3>
<h4>偏置值</h4>
<p>到了这里,我们发现,第一位是浮点数的正负符号,那么,对于一个科学计数法来说,阶码同样需要有正负。而在单精度中,阶码只有 8 位;双精度中,阶码只有 11 位。如果我们给阶码表示成补码,那么,我们能够表示的数的范围就会缩小,这样显然是不划算的。于是,偏置值就由此诞生了。</p>
<h4>规格化的值(阶码不全为 0 或 1)</h4>
<p>在内存中的规格化的浮点数表示中,阶码并非是 2 的幂,而是经过计算的结果,这个计算公式就是 <em>e - Bias</em>,这里的 <em>Bias</em> 就是<code>偏置值</code>,而 <em>e</em> 就是阶码在浮点数中的二进制表示。<em>Bias</em> 的值是 2^<em>k</em>-1 - 1(单精度是 127,双精度是 1023),所以,<em>e - Bias</em> 的取值范围就是 [-126, 127](单精度)和 [-1022, 1023](双精度)。其实如果对补码了解的比较好的同学,应该就能看出来,这其实就是省略了符号位的补码表示)。</p>
<p>通过上面的隐含 1 开头的表示的尾数,我们可以计算出基数 <em>M = 1 + f</em>。那么我们整个的浮点数可以写成这样一个表达式:<em>M</em> × (<em>e - Bias</em>)。</p>
<h4>非规格化的值(阶码全为 0)</h4>
<p>对于规格化和非规格化的值来说,我们都可以用同一个式子来表示。不过,为了某些更加方便的原因(这里就不展开讲了),对它们做了区分。如果按照规格化的计算来看,阶码的值是 0 - <em>Bias</em>,不过在这里,我们让阶码的值等于 1 - <em>Bias</em>。同样的,由于我们给阶码加了 1,那么整个浮点数就会向左移动一位,那么,我们需要让浮点数的值不变,<em>M</em> 就不在需要上面整数部分的 1 了,所以 <em>M = f</em>。</p>
<p>同时,我们会发现一个问题,那就是 +0.0 和 -0.0 在浮点数的二进制表示上是不同的。</p>
<h4>特殊值(阶码全为 1)</h4>
<p>最后,还剩下这样一种数字,那就是阶码全为 1 的情况。当小数为 0 的时候,浮点数的值为 ∞。当小数不为 0 时,浮点数的值为 NaN,即不是一个数(Not a Number)。</p>
<h2>计算浮点数</h2>
<p>好了,扯了这么多,我们现在回到最开始的问题上,<code>floor((0.1 + 0.7) * 10) = 7</code>。我们先看 0.1 的二进制表示。</p>
<p>首先,我们将十进制小数转换成二进制小数,可以得到 0.000[1100]···。让我们转换成浮点数的二进制表示。按照上面的规则,它可以被表示成科学计数法 1.10011001100110011001100 × 2^-4 ,这样,阶码就是 -4 + 127 = 123,二进制表示为 01111011。所以,整个浮点数的二进制表示就是 <code>00111101110011001100110011001100</code>(<code>0x3dcccccc</code>)。同样的,0.7 会表示为<code>00111101001100110011001100110011</code>(<code>0x3d333333</code>)。</p>
<p>首先我们要对阶码小的数进行对阶,然后再进行尾数的加法,这样,我们得到的值就是 <code>00111101111001100110011001100101</code>。我们将其转换成十进制,发现,它是小于 0.8 的。因此,当我们再进行乘法运算向下取整时,会等于 7。</p>
<h2>最后</h2>
<p>其实,浮点数有很多坑。因此,我们在使用浮点数的时候,一定要小心。还有,涉及到金额计算的时候,一定不能使用浮点数。</p>
<blockquote><p>本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。</p></blockquote>
<h2>参考文献</h2>
<p>《深入理解计算机系统(第 3 版)》第 2.4.2 节</p>
Generator 的异常处理
https://segmentfault.com/a/1190000010743480
2017-08-19T11:14:05+08:00
2017-08-19T11:14:05+08:00
daryl
https://segmentfault.com/u/daryl
2
<blockquote><p>本文是我在研究 PHP 异步编程时的总结。对于相当多的 PHPer 来说,可能都不知道 Generator,或者对 Generaotr 的流程不是很熟悉。因为 Generator 使得程序不再是顺序的。鉴于本人的水平有限,如果有不同意见,还望指点一二,不胜感激!</p></blockquote>
<h2>PHP 中的异常处理</h2>
<p>从 PHP 5 开始,PHP 为我们提供了 try catch 来进行异常处理。当我们使用 catch 将异常捕获,那么一场后续的代码就会执行。我们看看下面的例子。</p>
<pre><code class="php">try {
throw new Exception('e');
} catch (Exception $e) {
echo $e->getMessage(); // output: e
}
echo 2; // output: 2</code></pre>
<p>如果我们没有将异常捕获,那么后面的代码就不会执行了。</p>
<pre><code class="php">throw new Exception('e'); // throw an exception
echo 2; // not execute</code></pre>
<h2>Generator 的 throw 方法</h2>
<p>在 PHP 中,Generator 提供了 <code>throw</code> 方法来抛出异常。用法和普通的异常一样,只不过把 <code>throw</code> 关键字改成了方法调用。</p>
<pre><code class="php">function gen()
{
yield 0;
yield 1;
yield 2;
yield 3;
}
$gen = gen();
$gen->throw(new Exception('e')); // throw an exception
var_dump($gen->valid()); // output: false
echo 2; // not execute</code></pre>
<p>同样的,我们可以这个异常捕获,通过 try catch 来进行。</p>
<pre><code class="php">try {
$gen->throw(new Exception('e'));
} catch (Exception $e) {
echo $e->getMessage(); // output: e
}
var_dump($gen->valid()); // output: false
echo 2; // output: 2</code></pre>
<p>我们可以看到,当我们使用 throw 抛出异常后,当前的生成器的 valid 变成了 false。但是考虑下面一种情况,当我们在外面调用 throw 方法后,在生成器函数中捕获异常,会发生什么呢?我们来看下面的例子。</p>
<pre><code class="php">function gen()
{
yield 0;
try {
yield;
} catch (Exception $e) {
echo $e->getMessage(); // output: e
}
yield 2;
yield 3;
}
$gen = gen();
$gen->next(); // reach the point of catching exception
$gen->throw(new Exception('e'));
var_dump($gen->valid()); // output: true
echo 2; // output: 2</code></pre>
<p>当我们在生成器函数捕获来自 throw 方法抛出的异常后,生成器依然是 valid 的。但是如果像刚才一样只是在调用 throw 方法,那么生成器就结束了。</p>
<h3>在生成器函数中抛出异常</h3>
<pre><code class="php">function gen()
{
yield 0;
throw new Exception('e');
yield 2;
yield 3;
}
$gen = gen();
$gen->next();
$gen->current(); // throw an exception
var_dump($gen->valid()); // output: false
echo 2; // not execute</code></pre>
<p>之前我们看到的是调用 throw 方法来抛出异常。那么在生成器函数中,抛出一个异常而没有在生成器函数中捕获,结果也都是一样的。同样的,如果在生成器函数中捕获了异常,那么就和之前的例子一样了。</p>
<p>在理解了上面的例子之后,我们就要考虑一下,如果有嵌套的生成器,会发生什么了。</p>
<h2>嵌套生成器</h2>
<p>当我们在一个生成器函数中,<code>yield</code> 了另外一个生成器函数之后,就会变成嵌套生成器。我们来看下面的例子。</p>
<pre><code class="php">function subGen()
{
yield 1;
throw new Exception('e');
yield 4;
}
function gen()
{
yield 0;
yield subGen();
yield 2;
yield 3;
}
$gen = gen();
$gen->next();
$gen->current()->next(); // throw an exception
echo 2; // not execute</code></pre>
<p>对于嵌套的生成器来说,如果子生成器中抛出了异常,那么在没有捕获这个异常的情况下,会一级一级向上抛出,直到结束。</p>
<p>刚才我们尝试了,在抛出异常之后,valid 的返回值变成了 false。那么在嵌套生成器中,是不是也是这样呢?我们把异常捕获,使程序能够继续执行下去,来看下面这个例子。</p>
<pre><code class="php">function subGen()
{
yield 1;
throw new Exception('e');
yield 4;
}
function gen()
{
yield 0;
yield subGen();
yield 2;
yield 3;
}
$gen = gen();
$gen->next();
try {
$gen->current()->next();
} catch (Exceprion $e) {
echo $e->getMessage(); //output: e
}
var_dump($gen->valid()); // output: true
echo 2; // output: 2</code></pre>
<p>所以,当子生成器抛出异常后在迭代的过程中被正常地捕获,那么,父生成器便不会受到影响,valid 的返回值依然是 <code>true</code>。</p>
<h2>总结</h2>
<p>关于生成器的异常处理,这里来进行一下总结。</p>
<ul>
<li>在生成器中抛出一个异常,或者使用 throw 方法抛出一个异常,那么,生成器的迭代便会结束,valid 变成 <code>false</code>;</li>
<li>在生成器中抛出一个异常,迭代过程中对异常进行捕获,生成器的迭代依然会结束,valid 依然会变成 <code>false</code>;</li>
<li>在生成器中抛出一个异常,在生成器中将其捕获处理,生成器的迭代不会结束,valid 会返回 <code>true</code>;</li>
<li>在嵌套的生成器中,如果子生成器抛出了异常,只会对子生成器产生影响,不会对父生成器产生影响。</li>
</ul>
<h2>后记</h2>
<p>yield 为我们提供了使用 PHP 实现半协程的工具。最近在研究使用 yield 实现半协程,而这个过程中,对异常的处理,是非常重要的。但是 yield 的运行方式决定了异常处理比较难以理解。于是我花了几天的时间,尝试了各种可能,得出来的这些结论。当然由于本人水平有限,如有错误,还望指点一二,不胜感激。</p>
使用桩件 (Stub) 解决 Laravel 单元测试中的依赖
https://segmentfault.com/a/1190000010605518
2017-08-11T09:05:00+08:00
2017-08-11T09:05:00+08:00
daryl
https://segmentfault.com/u/daryl
17
<blockquote><p>本文是我在实践后的一点总结,难免有不妥之处。如有幸得大神路过,还望不吝赐教,小弟在此谢过了!</p></blockquote>
<p>很早就知道有单元测试的概念,也曾尝试过,但是一直对单元测试的概念和方法,比较模糊。在听了 <a href="/u/vimac">@vimac</a> 大神的讲堂 <a href="https://segmentfault.com/l/1500000009799342">PHP单元测试与测试驱动开发</a> 后,慢慢地对单元测试和 PHPUnit 的认识清晰了起来,也开始慢慢地去实践单元测试。</p>
<h2>Laravel 中的依赖</h2>
<p>我们都知道,Laravel 使用了 IoC,各个模块之间也因此解耦了。而正是因为这一点,我们在 Laravel 中编写单元测试的时候,变得更加轻松了。</p>
<h3>举个栗子</h3>
<p>考虑以下场景。我们在开发中,可能会在控制器和模型之间加一个 <code>Repository</code> 来处理数据。那么我们的 <code>Controller</code> 就会依赖 <code>Respository</code>。利用 Laravel 的 IoC,我们可以定义一个 <code>Service Provider</code> 来集中将 <code>Respository</code> 注入到容器中。</p>
<p>假设我们现在有这样一个 Repository,里面记录了商品的信息,我们想要在 Controller 中获取某件商品信息,然后执行一些业务逻辑。</p>
<pre><code class="php">Class GoodRepository
{
public function getGoodById($goodId)
{
// TODO: Get good by its id.
}
}
class GoodController extends Controller
{
public function show($id, GoodRepository $goodRepository)
{
// TODO: Do something with good info from that repository.
}
}
// In route/api.php
Route::get('/api/good/{id}', 'GoodController@show');
// Create a RepositoriesServiceProvider in Provider/RepositoriesServiceProvider.php。
// And inject the GoodRepository into Container.
class RepositoriesServiceProvider extends ServiceProvider
{
public function boot()
{
}
public function register()
{
$this->app->singleton(GoodRepository::class);
}
}</code></pre>
<p>好了,我们可以发现,<code>GoodController</code> 是依赖 <code>GoodRepository</code> 的,而 <code>GoodRepository</code> 是依赖数据库中的数据的。可是我们在做单元测试的时候,希望尽可能少的产生依赖。所以,我们应该希望能够掌控 <code>GoodRepository</code> 所返回的数据。</p>
<p>在 Laravel,提供了 <code>$this->get('/path/to/route');</code> 的方法来对 HTTP 请求进行测试。这个测试必然会涉及到刚才所提到的那些依赖,如何解决这个依赖的问题,我们可以请出我们的主角————桩件。</p>
<h3>桩件</h3>
<blockquote><p>将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。</p></blockquote>
<p>这是 PHPUnit <a href="https://link.segmentfault.com/?enc=PHQdWYvZ%2F6XjTNSdmYP6HA%3D%3D.o9uGxrm7TJjyGsMQetobUPp7JhjYZLpVx0a8YQ4NKafwjp%2FfZnx5JqWcnrvYbd%2FCB8iGhyT0ZXRwc0nNhM0espT3XJo7FXMRxGJZiFtjK%2F3OKrt0rdh%2FpKkC8We5T7S0" rel="nofollow">文档上</a> 的解释。那我的理解呢,所谓的桩件,就是模拟一个依赖的类的行为,使得这个行为所做的事情在我们自己的掌控之中。比如上面的这种情况,我们希望模拟 <code>GoodRepository</code> 的 <code>getGoodById</code> 方法返回与真正的返回结构相同的值,而不需要依赖外部数据源。</p>
<h3>在 Laravel 中使用桩件</h3>
<p>我们通过 <code>Service Provider</code> 注册了 <code>GoodRepository</code> 单例,那么按照这个思路,我们在写单元测试的时候,就可以将我们定义的桩件,注册为 <code>GoodRepository</code> 单例。</p>
<pre><code class="php">class GoodControllerTest extends TestCase
{
public function testShow()
{
$data = []; // The data returns from GoodRepository::getGoodById.
$stub = $this->createMock(GoodRepository::class);
$stub->method('getGoodById')->will($this->returnValue($data));
$this->app->singleton(GoodRepository::class, function () use ($stub) {
return $stub;
});
$response = $this->get('/api/good/1');
// Some assertions.
}
}</code></pre>
<p>我们通过在这里将桩件 <code>$stub</code> 用单例模式注册给了 <code>Container</code>,在调用 <code>$this->get('/api/good/1');</code> 时原本在 <code>Controller</code> 中的 <code>GoodRepository</code> 依赖就变成了我们自定义的桩件 <code>$stub</code>。我们将 <code>$data</code> 定义为和返回值相同的结构,注册到桩件中。这样,所有的数据都在我们可控的范围了。</p>
<p>如果我们在这里不使用桩件,而是直接依赖外部(数据库)中的数据,那么如果 id 为 1 的数据被删除了,我们是不是就要改成 2 了呢?我们是不是就要重新计算数据了匹配断言了呢?这样的测试,可靠性便大大降低。</p>
<h2>后记</h2>
<p>任何一个可靠的系统,单元测试都是必不可少的。庆幸的是,PHPUnit 帮我们提供了好用的单元测试。本文所讲的,也只是 PHPUnit 的九牛一毛。而我自己也在慢慢摸索慢慢实践中。与君共勉。</p>
<p>最后还是推荐去听一下 <a href="/u/vimac">@vimac</a> 的讲堂 <a href="https://segmentfault.com/l/1500000009799342">PHP单元测试与测试驱动开发</a>,受益匪浅。</p>
php 内核探秘之 PHP_FUNCTION 宏
https://segmentfault.com/a/1190000010529733
2017-08-07T08:35:00+08:00
2017-08-07T08:35:00+08:00
daryl
https://segmentfault.com/u/daryl
18
<blockquote><p>本人也只是个初入门的菜鸟,因对技术有着向往,故在“无趣”的工作之余,尽自己所能提升自己。由于我的 C 语言功底也有限,故本文的深度也有限,如有幸得大牛阅读,还望指导一二,小弟感激不尽。</p></blockquote>
<h2>PHP 的函数</h2>
<p>作为 PHPer,我们几乎每天都在写函数,我们一定会好奇,那些 PHP 内置的函数,是长什么样子的。如果写过 PHP 扩展的话,一定知道这个宏:<code>PHP_FUNCTION</code>。在定义一个函数的时候,这样来使用这个宏。例如 <code>array_change_key_case</code>,它的定义是这样的:<code>PHP_FUNCTION(array_change_key_case)</code>。没错,就是这么简单。但是,在这个简单的背后,却没有这么简单。</p>
<h2>PHP_FUNCTION 追根溯源</h2>
<h3>宏</h3>
<p>相信对这篇文章感兴趣的同学,一定多少对 C 语言以及它的宏定义有一定的了解。如果没有,也不要紧,我这里来简单解释一下,什么是宏。</p>
<p>C 语言中的宏,我认为,可以理解为一种简单的封装。通过宏定义,可以对开发者隐去一些细节,让开发者在使用简单的语法来完成重复的复杂的编码。当然,宏定义还有其它的用途,但是,我们在 <code>PHP_FUNCTION</code> 涉及到的就是这个作用。有下面的代码。</p>
<pre><code class="c">#define TEST(test) void test(int a)
TEST(haha)</code></pre>
<p>宏,就是完全的替换,即使用后面的语句替换前面的。那么对于下面的 <code>TEST(haha)</code> 就相当于下面的样子。</p>
<pre><code class="c">void haha(int a)</code></pre>
<h3>PHP_FUNCTION 的定义</h3>
<p>首先,我们要定义函数,这样使用这个宏。</p>
<pre><code class="c">PHP_FUNCTION(array_change_key_case)
{
// TODO
}</code></pre>
<p>我们在 <code>php-src/main/php.h</code> 中找到了下面的定义。</p>
<pre><code class="c">#define PHP_FUNCTION ZEND_FUNCTION</code></pre>
<p>也就是说,这里用 <code>ZEND_FUNCTION</code> 替换了 <code>PHP_FUNCTION</code> 这个宏。所以,我们的定义就相当于变成了这样。</p>
<pre><code class="c">ZEND_FUNCTION(array_change_key_case)
{
// TODO
}</code></pre>
<p>我们继续往下找,因为,这里还是宏,我们并没有看到我们希望看到的代码。我们可以在 <code>php-src/Zend/zend_API.h</code> 中找到下面的定义。</p>
<pre><code class="c">#define ZEND_FN(name) zif_##name
#define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)</code></pre>
<p>我们看到,在宏定义中,使用了另外的宏。不要怕,还是一个词,替换。我们按照这样的步骤来。(<code>##</code> 是一个连接符,它的作用是,是将它前面的与后面的,按照字符串的方式连接起来。</p>
<ol><li><p>替换 <code>ZEND_FUNCTION</code></p></li></ol>
<pre><code class="c">ZEND_NAMED_FUNCTION(ZEND_FN(name))
{
// TODO
}</code></pre>
<ol><li><p>替换 <code>ZEND_FN</code></p></li></ol>
<pre><code class="c">ZEND_NAMED_FUNCTION(zif_array_change_key_case)
{
// TODO
}</code></pre>
<ol><li><p>替换 <code>ZEND_NAMED_FUNCTION</code></p></li></ol>
<pre><code class="c">void zif_array_change_key_case(INTERNAL_FUNCTION_PARAMETERS)
{
// TODO
}</code></pre>
<p>到这里,我们可以看到,这里已经基本和我们熟悉的函数定义差不多了,不过,这还没完,以为,这里还有宏,那就是 <code>INTERNAL_FUNCTION_PARAMETERS</code>。我们找到 <code>php-src/Zend/zend.h</code>,可以找到 <code>INTERNAL_FUNCTION_PARAMETERS</code> 的宏定义。</p>
<pre><code class="c">#define INTERNAL_FUNCTION_PARAMETERS zend_execute_data *execute_data, zval *return_value</code></pre>
<p>好了,依然按照替换的原则,我们就可以将函数定义变成这样了。</p>
<pre><code class="c">void zif_array_change_key_case(zend_execute_data *execute_data, zval *return_value)
{
// TODO
}</code></pre>
<p>看,整个函数的定义,已经完全没有宏了,这已经是我们在熟悉不过的 C 语言函数的定义了。这就是<br><code>PHP_FUNCTION</code> 的整个定义的过程。</p>
<h3>execute_data 和 return_value</h3>
<p><code>return_value</code>,顾名思义,就是定义的 PHP 函数的返回值。而 <code>execute_data</code>,按照我的理解,就是 Zend 内部的一个调用栈,而在执行这个函数的时候,指向的是这个函数的栈帧。具体的细节,暂时在这里先不考虑,有兴趣的同学可以来这里看一下。<a href="https://link.segmentfault.com/?enc=15wG7R4WHA3aFGU9i%2FsGGw%3D%3D.EbW%2FzqiAiPsZAudZk7EOuKZTVrOl%2BH5fW1EUKfPHpwpu%2FVrohTfuCX07guukPf%2FXMwGJaXWqKBdIOQbubSP08Q%3D%3D" rel="nofollow">深入理解 PHP 内核</a></p>
<h2>后记</h2>
<p>我始终认为,对于一个 PHPer 来说,C 语言是一项必不可少的技能。理解 PHP 的内核,对于我们编写出高质量的代码,起到了关键的作用。所以,我现在开始研究 PHP 的源码实现了。我希望我能通过这些文章,记录下我理解源码的瞬间,也希望我的文章能让更多的 PHPer,进入到 PHP 内核的世界。</p>
每日一个 php 函数——array_change_key_case
https://segmentfault.com/a/1190000010476969
2017-08-03T00:17:47+08:00
2017-08-03T00:17:47+08:00
daryl
https://segmentfault.com/u/daryl
1
<blockquote><p>因为已经有文档了,可能有些人觉得我写这个有些多余了。可是并不是每一个 PHPer 都会好好地去阅读文档,自然有一些函数可能都没有听说过(很不幸我也是这其中的一员)。我也希望能通过写这些文章,能够促使我完整地读完文档,同时,能够给其它的 PHPer 一个参考,“啊,原来还有这个函数” 的感觉。同时,我也希望我能通过写这些文章,去阅读各个函数的 C 语言实现。也实现自我驱动地学习。</p></blockquote>
<h2>函数原型</h2>
<pre><code class="php">array array_change_key_case ( array $array [, int $case = CASE_LOWER ] )</code></pre>
<p>该函数的具体作用是,将一个数组中的所有的英文字母转换为大写或小写。</p>
<p>我们可以看到,这个函数接收两个参数,返回一个数组。第一个参数数组没有使用引用的方式,那么说明该函数并不会改变原数组,它会生成新的数组作为返回值。而第二个参数是可选的,它控制着该函数是转换成大写还是小写。默认是转化为小写。</p>
<h2>函数使用</h2>
<h3>第二个参数</h3>
<p>函数的第二个参数传入的是一个预定义常量,分别是 CASE_LOWER 和 CASE_UPPER,前者是将 key 转换成小写,也是函数的默认值;后者是将 key 转换成大写。</p>
<h3>使用</h3>
<pre><code class="php">$arr = [
'loWer' => 1,
];
$toLower = array_change_key_case($arr, CASE_LOWER);
// 我认为,不管它的默认值是什么,我们都要写上这第二个参数。我们的代码写出来,是给人看的,不是给机器看的。
// 所以我们的代码应当尽量多的包含语义。
$toUpper = array_change_key_case($arr, CASE_UPPER);
var_dump($toLower);
/**
[
'lower' => 1
]
*/
var_dump($toUpper);
/**
[
'LOWER' => 1
]
*/</code></pre>
<p>不过,这个函数不是递归的。我们看一下下面这个例子。</p>
<pre><code class="php">$arr = [
'loWer' => [
'Lower' => 1,
],
];
$toLower = array_change_key_case($arr, CASE_LOWER);
var_dump($toLower);
/**
[
'lower' => [
'Lower' => 1,
],
]
*/</code></pre>
<h3>坑</h3>
<p>这个函数的使用,是有个坑的,这个坑就是,当转换之后,如果结果中有两个相同的 key,那么就会保留最后的那个。举个例子。</p>
<pre><code class="php">$arr = [
'key' => 1,
'kEy' => 2,
'keY' => 3,
];
$toLower = array_change_key_case($arr, CASE_UPPER);
var_dump($toLower); // ['key' => 3]</code></pre>
<p>在这个例子中,我们发现,当执行转换之后,三个 key 变成相同的了,那么在这种情况下,只会保留最后一个元素作为 key。这里得到的数组是 <code>['key' => 3]</code>。</p>
<h2>内核实现</h2>
<p>该函数的源代码在 <code>php-src/ext/standard/array.c</code> 中。</p>
<h3>源码</h3>
<p>我们先来看一下源代码。</p>
<pre><code class="c">PHP_FUNCTION(array_change_key_case)
{
zval *array, *entry;
zend_string *string_key;
zend_string *new_key;
zend_ulong num_key;
zend_long change_to_upper=0;
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_ARRAY(array)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(change_to_upper)
ZEND_PARSE_PARAMETERS_END();
array_init_size(return_value, zend_hash_num_elements(Z_ARRVAL_P(array)));
ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_key, string_key, entry) {
if (!string_key) {
entry = zend_hash_index_update(Z_ARRVAL_P(return_value), num_key, entry);
} else {
if (change_to_upper) {
new_key = php_string_toupper(string_key);
} else {
new_key = php_string_tolower(string_key);
}
entry = zend_hash_update(Z_ARRVAL_P(return_value), new_key, entry);
zend_string_release(new_key);
}
zval_add_ref(entry);
} ZEND_HASH_FOREACH_END();
}</code></pre>
<h3>关于 PHP_FUNCTION 宏</h3>
<p>熟悉 PHP 扩展开发的同学应该都知道,<code>PHP_FUNCTION</code> 这个宏,是定义一个 PHP 函数用的,参数就是 PHP 函数的函数名。关于这个宏,有兴趣的可以去看看源码,它其实是将 <code>PHP_FUNCTION(array_change_key_case)</code> 替换成了 <code>void zif_array_change_key_case(zend_execute_data *execute_data, zval *return_value)</code>,这样的一个函数定义。注意里面的 <code>return_value</code> 变量,后面会用到这个变量。</p>
<h3>逻辑代码</h3>
<p>其实,真正的逻辑代码,是在 <code>ZEND_HASH_FOREACH_KEY_VAL</code> 宏和 <code>ZEND_HASH_FOREACH_END</code> 之间的。上面的几个宏,是为了检查并获取传参进 PHP 函数的变量。我们可以看到 <code>zend_long change_to_upper=0;</code> 这个是用来判断,是大写还是小写的。这里定义的默认值是 0,所以这个函数的默认是小写。而整个函数的最核心的代码是 <code>php_string_toupper</code> 和 <code>php_string_tolower</code> 这两个函数。</p>
<p>这是其中之一的代码。</p>
<pre><code class="c">PHPAPI zend_string *php_string_toupper(zend_string *s)
{
unsigned char *c, *e;
c = (unsigned char *)ZSTR_VAL(s);
e = c + ZSTR_LEN(s);
while (c < e) {
if (islower(*c)) {
register unsigned char *r;
zend_string *res = zend_string_alloc(ZSTR_LEN(s), 0);
if (c != (unsigned char*)ZSTR_VAL(s)) {
memcpy(ZSTR_VAL(res), ZSTR_VAL(s), c - (unsigned char*)ZSTR_VAL(s));
}
r = c + (ZSTR_VAL(res) - ZSTR_VAL(s));
while (c < e) {
*r = toupper(*c);
r++;
c++;
}
*r = '\0';
return res;
}
c++;
}
return zend_string_copy(s);
}</code></pre>
<p>用 C 写过转换字符串大小写的同学都知道,其实和我们自己实现的思路基本都差不多。只是用了几个宏。<code>c</code> 就是字符串的首地址,<code>e</code> 是字符串 <code>'\0'</code> 的地址。从 <code>c</code> 到 <code>e</code> 循环,然后来对每一个地址的字符转换大小写。</p>
<p>这里用到的 <code>islower</code>、<code>isupper</code>、<code>tolower</code>、<code>toupper</code> 都是 <code>ANSI C</code> 中提供的函数。</p>
<h2>结语</h2>
<p>PHP 的文档其实是很好的学习资料,但是很多 PHPer 都没有真正好好看过文档(包括我)。前面也说了,我写这个的目的,就是希望能够通过这种方式,来对文档进行全面的扫描。同时,也从每一个函数切入,逐步地去看 PHP 内核的实现。对于后面 C 的文字,我只能尽我自己所能来解释了,有不到的地方大家多包涵,毕竟我也是一个学习者。</p>
乌云一周年祭?
https://segmentfault.com/a/1190000010272021
2017-07-20T13:34:48+08:00
2017-07-20T13:34:48+08:00
daryl
https://segmentfault.com/u/daryl
40
<p>宁愿这是谣言,然而你终究没有回来。??????</p>
<p>———— 曾经的路人甲敬上。</p>
<p><img src="/img/bVRgnr?w=700&h=750" alt="notice.png" title="notice.png"></p>
浅入理解 PHP 中的 Generator
https://segmentfault.com/a/1190000009828729
2017-06-19T06:30:00+08:00
2017-06-19T06:30:00+08:00
daryl
https://segmentfault.com/u/daryl
31
<h2>何为 Generator</h2>
<p>从 PHP 5.5 开始,PHP 加入了一个新的特性,那就是 <code>Generator</code>,中文译为<code>生成器</code>。生成器可以简单地用来实现对象的迭代,让我们先从官方的一个小例子说起。</p>
<h3>xrange</h3>
<p>在 PHP 中,我们都知道,有一个函数叫做 <code>range</code>,用来生成一个等差数列的数组,然后我们可以用这个数组进行 <code>foreach</code> 的迭代。具体就想这样。</p>
<pre><code class="php">foreach (range(1, 100, 2) as $num) {
echo $num . PHP_EOL;
}</code></pre>
<p>这一段代码就会输出首项为 1,末项为 100,公差为 2 的等差数列。它的执行顺序是这样的。首先,<code>range(1, 100, 2)</code> 会生成一个数组,里面存了上面那样的一个等差数列,之后在 <code>foreach</code> 中对这个数组进行迭代。</p>
<p>那么,这样就会出现一个问题,如果我要生成 100 万个数字呢?那我们就要占用上百兆内存。虽然现在内存很便宜,但是我们也不能这么浪费内存嘛。那么这时,我们的生成器就可以排上用场了。考虑下面的代码。</p>
<pre><code class="php">function xrange($start, $limit, $step = 1) {
while ($start <= $limit) {
yield $start;
$start += $step;
}
}
foreach (xrange(1, 100, 2) as $num) {
echo $num . PHP_EOL;
}</code></pre>
<p>这段代码所的出来的结果,和前面的那段代码一模一样,但是,它内部的原理是天翻地覆了。</p>
<p>我们刚才说了,前面的代码,<code>range</code> 会生成一个数组,然后 <code>foreach</code> 来迭代这个数组,从而取出某一个值。但是这段代码呢,我们重新定义了一个 <code>xrange</code> 函数,在函数中,我们用了一个关键字 <code>yield</code>。我们都知道定义一个函数,希望它返回一个值得时候,用 <code>return</code> 来返回。那么这个 <code>yield</code> 呢,也可以返回一个值,但是,它和 <code>return</code> 是截然不同的。</p>
<p>使用 <code>yield</code> 关键字,可以让函数在运行的时候,中断,同时会保存整个函数的上下文,返回一个 <code>Generator</code> 类型的对象。在执行对象的 <code>next</code> 方法时,会重新加载中断时的上下文,继续运行,直到出现下一个 <code>yield</code> 为止,如果后面没有再出现 <code>yield</code>,那么就认为整个生成器结束了。</p>
<p>这样,我们上面的函数调用可以等价地写成这样。</p>
<pre><code class="php">$nums = xrange(1, 100, 2);
while ($nums->valid()) {
echo $nums->current() . "\n";
$nums->next();
}</code></pre>
<p>在这里,<code>$num</code> 是一个 <code>Generator</code> 的对象。我们在这里看到三个方法,<code>valid</code>、<code>current</code> 和 <code>next</code>。当我们函数执行完了,后面没有 <code>yield</code> 中断了,那么我们在 <code>xrange</code> 函数就执行完了,那么 <code>valid</code> 方法就会变成 <code>false</code>。而 <code>current</code> 呢,会返回当前 <code>yield</code> 后面的值,这是,生成器的函数会中断。那么在调用 <code>next</code> 方法之后,函数会继续执行,直到下一个 <code>yield</code> 出现,或者函数结束。</p>
<p>好了,到这里,我们看到了通过 <code>yield</code> 来“生成”一个值并返回。其实,<code>yield</code> 其实也可以这么写 <code>$ret = yield;</code>。同返回值一样,这里是将一个值在继续执行函数的时候,传值进函数,可以通过 <code>Generator::send($value)</code> 来使用。例如。</p>
<pre><code class="php">function sum()
{
$ret = yield;
echo $ret . PHP_EOL;
}
$sum = sum();
$sum->send('I am from outside.');</code></pre>
<p>这样,程序就会打印出 <code>send</code> 方法传进去的字符串了。在 <code>yield</code> 的两边可以同时有调用。</p>
<pre><code class="php">function xrange($start, $limit, $step = 1) {
while ($start <= $limit) {
$ret = yield $start;
$start += $step;
echo $ret . PHP_EOL;
}
}
$nums = xrange(1, 100, 2);
while ($nums->valid()) {
echo $nums->current() . "\n";
$nums->send($nums->current() + 1);
}</code></pre>
<p>而像这样的使用,<code>send()</code> 可以返回下一个 <code>yield</code> 的返回。</p>
<h2>其它的 Generator 方法</h2>
<h3>Generator::key()</h3>
<p>对于 <code>yield</code>,我们可以这样使用 <code>yield $id => $value</code>,这是,我们可以通过 <code>key</code> 方法来获取 <code>$id</code>,而 <code>current</code> 方法返回的是 <code>$value</code>。</p>
<h3>Generator::rewind()</h3>
<p>这个方法,可以帮我们让生成器重新开始执行并保存上下文,同时呢,会返回第一个 <code>yield</code> 返回的内容。在第一次执行 <code>send</code> 方法的时候,<code>rewind</code> 会被隐式调用。</p>
<h3>Generator::throw()</h3>
<p>这个方法,向生成器中,抛送一个异常。</p>
<h2>后记</h2>
<p><code>yield</code> 作为 PHP 5.5 的新特性,让我们用了新的方法来高效地迭代数据。同时,我们还可以使用 <code>yield</code> 来实现协程。</p>
一个 PHPer 第一次用 Koa2 写 Node.js 的心路历程
https://segmentfault.com/a/1190000009733610
2017-06-11T09:45:00+08:00
2017-06-11T09:45:00+08:00
daryl
https://segmentfault.com/u/daryl
9
<p>学了一段时间的 js 了,突然想实践一下。正好公司有个小的项目要做,就顺手拿 Koa2 来做了。真是不做不知道,做了想不到。踩了一堆新手坑。</p>
<h2>初次接触 Koa2</h2>
<p>在知道 Koa2 之前,我也了解过 Express,可惜并没有实战用过。后来大家都说 Koa 是一个比 Express 更牛X的东西,于是在好(作)奇(死)心作祟下,直接去用 Koa2 了。后来证明的确是作死,原本用 PHP 一天就能写完东西,愣是让我搞了三天。</p>
<h3>安装</h3>
<p>最近 Node.js V8 发布了,原生支持 <code>async</code> 和 <code>await</code> 调用了,所以直接把 Node.js 升级了一下。</p>
<p>根据 Koa2 的教程,安装很简单,我是使用的 yarn 的(还真是比 npm 快)。</p>
<pre><code class="bash">yarn add koa</code></pre>
<p>默认就装了 Koa 2.2。然后装完了,其实我是一脸懵逼的,文档上说这样用。</p>
<pre><code class="javascript">const Koa = require('koa')
const app = new Koa()
// response
app.use(ctx => {
ctx.body = 'Hello Koa'
})
app.listen(3000)</code></pre>
<p>我照着代码写了下来,的确成功了。可是,难不成我要把所有的逻辑写在 <code>app.use</code> 里?</p>
<p><img src="/img/remote/1460000009733613?w=521&h=534" alt="a" title="a"></p>
<h3>中间件</h3>
<p>我感觉我受到了惊吓,吓得我赶紧往下看文档。原来 Koa2 是一个中间件模型。<code>app.use</code> 可以有很多,每一个 <code>app.use</code> 会注册一个中间件,这个中间件是具体做事情的。每个中间件是依次执行的。一个经典的洋葱图可以解释这一切。</p>
<p><img src="/img/remote/1460000009733614?w=478&h=435" alt="" title=""></p>
<p>那么,上面的实例就可以改造成这样。</p>
<pre><code class="javascript">app.use(async (ctx, next) => {
await next()
ctx.body = 'Hello Koa'
})</code></pre>
<p>按照上面的洋葱头,以心为单位,next的两侧的语句分别在洋葱的左侧和右侧进行执行,颇像 <code>Laravel</code> 的中间件。</p>
<p>就这样,我知道了,所有的操作不必写在同一个 <code>app.use</code> 里。可是,下一个问题来了,我要把所有的逻辑都写再一个文件里?说好的 MVC 呢?没有 MVC 也叫做框架?Are you kidding me?(好吧后来发现原来 Koa2 并不是一个装置做网站的框架)</p>
<p>既然没有 MVC,那就自己动手丰衣足食吧。</p>
<h3>路由</h3>
<p>首先要处理的就是路由的问题。不过,由于是第一次用这货写项目,时间紧,(伪)任务重,看了文档后发现,原来还有一个中间件列表的链接,里面有各种开源的中间件。我想你们一定隔着屏幕都能听到我发出杠铃般的笑声了。有一个中间件非常棒,叫做 <a href="https://link.segmentfault.com/?enc=SA5QfjUhiupoAWn3ppw%2Brw%3D%3D.1DYpmX8U7%2F2smkv%2BHL7vp6PL1vD%2BtoCbpRHe0SVe9m6%2BMOpuy4L2DSsgKpMWLG%2B3" rel="nofollow">koa-router</a>。这货是这么用的。</p>
<pre><code class="javascript">var Koa = require('koa')
var Router = require('koa-router')
var app = new Koa()
var router = new Router()
router.get('/', function (ctx, next) {
// ctx.router available
});
app.use(router.routes())</code></pre>
<p>虽然是把逻辑和 <code>app.use</code> 分开了,但是,好像还是没有解决刚才的问题。说好的 MVC 也没有出现。于是我再去找了找,居然没有 <code>Controller</code> 的中间件。我一下就懵逼了,玩脱了?还有一天啊我的宝贝儿。经过我半秒钟的慎重思考,我还是用 <code>koa-router</code> 自己实现一个控制器吧。</p>
<h3>Controller</h3>
<pre><code class="javascript">const fs = require('fs')
function addRoutes(router, routes) {
for (let route in routes) {
switch (route.method) {
case: 'post':
router.post(route.uri, route.fn)
console.log(`Register post url: ${route.uri}`)
break
case: 'get':
router.get(route.uri, route.fn)
console.log(`Register get url: ${route.uri}`)
break
default:
console.log(`Invalid url: ${route}`)
}
}
}
function addControllers(router) {
let files = fs.readdirSync(__dirname + '/controllers')
let controllerFiles = files.filter(f => {
return f.endsWith('.js')
})
for (let controllerFile in controllerFiles) {
console.log(`process controller: ${controllerFile}...`)
let routes = require(__dirname + '/controllers')
addRoutes(router, routes)
}
}
module.exports = () => {
let router = require('koa-router')()
addControllers(router)
return router.routes()
}</code></pre>
<p>我通过在 <code>controllers</code> 文件夹中,创建若干 <code>js</code> 文件来作为 <code>Controller</code> 来使用。这里稍微参考了下 <a href="https://link.segmentfault.com/?enc=oydD57%2FDBFOcQrO%2FacAx6g%3D%3D.TE1DS2U%2Fml2JS1Cinw2XfZWpTmofUEiQ4I8ZKwX%2Fk4ttvYYuVlvFQezDJJSvLHloJtqHjx23hmYQkMLBRQ9RIrWGOm%2F1V8HpGQlWBg8aFyeHM0oi6LVW98JvlLqlDjFE6Pyfpj96H2rfQ4EZJFJh5JvyVyeuboDCz6haV00rQWvvquoLozMKelSfT0Eb5M8R" rel="nofollow">廖雪峰的文章</a>。</p>
<p>然后,我们只需要在 <code>controllers</code> 文件夹中添加合适的文件就可以了。例如我们添加一个文件叫做 <code>chart.js</code> ,然后这样写代码。</p>
<pre><code class="javascript">let hello = async (ctx, next) => {
ctx.body = 'Hello the fucking world!'
}
module.exports = [
{
method: 'get',
uri: 'hello',
fn: hello,
}
]</code></pre>
<p>最后再在 <code>app.js</code> 注册中间件即可。</p>
<p>除此之外,我们还需要能够处理 <code>ctx</code> 里的内容,因为它里面存储的是原始的内容。还是由于时间紧,任(填)务(坑)重(急),我用了 <a href="https://link.segmentfault.com/?enc=DmNNpkVGtQB%2BeT5RNRaCpw%3D%3D.ZWgg%2BheTopZOFqFdr9Z%2FNowjqAbpvvHEOU3V3E5Vc%2B2vDUkKheKhzeRvS3DXqdMm" rel="nofollow">koa-bodyparser</a>。</p>
<pre><code class="javascript">const bodyParser = require('koa-bodyparser')
app.use(bodyParser())</code></pre>
<p>这里要提醒的是,这货一定要放在处理路由中间件的前面。</p>
<h3>Model</h3>
<p><code>MVC</code> 的 <code>C</code> 已经解决了,接下来就要解决 <code>M</code> 的问题了。这里我用的是 <a href="https://link.segmentfault.com/?enc=ZPszvWOEkEsEmA64Ut0KGw%3D%3D.OoQ6vSIrHKxEZdQybT2rSr%2FnRN2PafHp2%2Fco8NyhS0edYaFGeOzE9RhSEQ4X5H0M" rel="nofollow">Sequelize</a>。这个 <code>ORM</code> 和大多数的 <code>ORM</code> 都差不多,所以在这里这次没有踩到什么坑。我在根目录下新建了一个 <code>config.js</code> 的配置文件,然后新建了 <code>model.js</code> 用来定义模型。</p>
<pre><code class="javascript">const Sequelize = require('sequelize')
const config = require('./config').databases
...
module.exports = {
//models
}</code></pre>
<h3>View</h3>
<p>视图,我是使用了一个中间件叫做 <a href="https://link.segmentfault.com/?enc=6z4l6a%2BLseDut2Ay4X7CXA%3D%3D.uRzl97gKtSPAFcq1ZeOt2LPFb1cbfSzSr6gau6I1oiirYtZlOdptXQzQgxy7MUzK" rel="nofollow">koa-view</a>。由于它使用的是 <a href="https://link.segmentfault.com/?enc=ZmINh4dRa7EwG%2BuULAEoqg%3D%3D.fv9RDfVHen7gSf5tfTprtGhOyajF9EZXptGirtpnJT46e0vdmpGlAji925Xy69a%2B" rel="nofollow">Nunjucks</a> 模板引擎,对于写 <code>PHP</code> 的我相对熟悉一点。</p>
<pre><code class="javascript">const view = require('koa-view')
const app = Koa()
app.use(view(__dirname + '/views'))</code></pre>
<pre><code class="javascript">//controller
let Hello = (ctx, next) => {
ctx.render('hello', datas)
}</code></pre>
<p>只要在 'views' 文件夹中定义相对应的 html 文件即可。</p>
<h2>后记</h2>
<p>这次的尝试,终于在我的修修补补中,搞出了一个简陋的 <code>MVC</code> 模型。赶在了 deadline 前完成,真是一波三折啊。学习新技术,就是这样,要实践嘛= =下面给出我的项目目录作参考</p>
<pre><code>koa2/
|
+- controllers/
| |
| +- chart.js
| ...
|
+- static/
| |
| +- js/
| ...
| |
| +- style/
| |
| +- img
| ...
|
+- views/
| |
| +- game.html
| ...
|
+- app.js
|
+- config.js
|
+- controller.js
|
+- model.js
|
+- package.json
|
+- yarn.lock
|
+- node_modules/</code></pre>
<p>菜鸟作品,如有错误请指正,不胜感激。</p>
<p>如果你喜欢我的文章,那就请我喝杯奶茶吧~</p>
<p><img src="/img/remote/1460000009733699?w=266&h=377" alt="" title=""></p>
理解 Go 语言中的方法和接收者
https://segmentfault.com/a/1190000009643429
2017-06-03T14:05:00+08:00
2017-06-03T14:05:00+08:00
daryl
https://segmentfault.com/u/daryl
4
<h2>0x01 前言</h2>
<p>Go 语言的语法实在有些不一样,与其它面向对象语言相比,Go 的方法似乎有些晦涩。</p>
<h2>0x02 方法的定义</h2>
<p>在 Go 语言里,方法和函数只差了一个,那就是方法在 <code>func</code> 和标识符之间多了一个参数。</p>
<pre><code class="golang">type user struct {
name string,
email string,
}
//这是函数的定义
func notify(email string) {
fmt.Println("Email is %s", email)
}
//这是方法的定义
func (u user) notify(email string) {
fmt.Println("Email is %d", email)
}</code></pre>
<p>我们可以看到,方法是在 <code>func</code> 和 <code>notify</code> 之间多了一个 <code>user</code> 类型的参数 <code>u</code>,这个 <code>u</code> 就称作接收者。</p>
<h2>0x03 接收者</h2>
<p>接收者有两种,一种是值接收者,一种是指针接收者。顾名思义,值接收者,是接收者的类型是一个值,是一个副本,方法内部无法对其真正的接收者做更改;指针接收者,接收者的类型是一个指针,是接收者的引用,对这个引用的修改之间影响真正的接收者。像上面一样定义方法,将 <code>user</code> 改成 <code>*user</code> 就是指针接收者。</p>
<h3>接收者与对象</h3>
<p>相信有很多人看到这个接收者之后都很苦恼,到底这个接收者是什么,是干什么用的。我们在学习一门新的语言的时候,都讲究触类旁通,和我们已经了解的语言作对比。那么我们就通过拿 Go 和其它带有类的面向对象的语言做对比来搞清楚接收者是什么。这里我们用 <code>php</code> 来举例子。</p>
<p>在 <code>php</code> 中,我们要定义一个方法,首先是要定义一个类。</p>
<pre><code class="php">class User
{
protected $email;
protected $name;
poublic function __construct($name, $email)
{
$this->email = $email;
$this->name = $name;
}
public function notify()
{
echo "Email is {$email}.\n";
}
public function changeEmail($email)
{
$this->email = $email;
}
}</code></pre>
<p>然后再实例化一个对象,进行操作,像这样。</p>
<pre><code class="php">$user = new User('daryl', 'daryl@example');
$user->changeEmail('daryl@example.com');
$user->notify();</code></pre>
<p>接下来,我们参照着来写一下 Go 的方法定义。</p>
<p>首先,我们是先要定义一个类型,比如就是 <code>user</code> 好了,然后我们再定义方法。</p>
<pre><code class="golang">type user struct {
name string
email string
}
func (u user) notify() {
fmt.Println("Email is %d", u.email)
}
func (u *user) changeEmail(email string) {
u.email = email
}</code></pre>
<p>我们定义了两个方法,一个是 <code>notify</code>,它是值接收者方法;还有一个是 <code>changeEmail</code>,它是指针接收者方法。可以看到,值接收者方法,接收者是一个副本,无法修改;指针接收者是引用,可以修改。</p>
<p>我们再来看一下调用。</p>
<pre><code class="golang">daryl := {"daryl", "daryl@oldexample.com"}
daryl.changeEmail("daryl@example.com")
daryl.notify()</code></pre>
<p>看看,是不是很熟悉!对,就像我们刚刚写过的 <code>php</code> 代码一样,有没有!<code>daryl</code> 就是对象,<code>name</code> 和 <code>email</code> 就是属性,<code>notify</code> 和 <code>changeEmail</code> 就是它的方法。只是,不同的是,我们没有将它放到 <code>class</code> 中,而是用另外一种方式让它们结合了,有了关系!</p>
<p>关于值接收者和指针接收者,其实 Go 在编译的时候有一个隐式转换,将其转换为正确的接收者类型。就像下面这样。</p>
<pre><code class="golang">//daryl.changeEmail("daryl@example.com")
(&daryl).changeEmail("daryl@example.com")
wife := &daryl
//wife.notify()
(*wife).notify()</code></pre>
<h2>0x04 后记</h2>
<p>最近在学习 Go 语言,看到有很多人评价 Go 的语法很丑陋,这一点确实不可否认。但是,它的语法有很简单,对于熟悉 C 的人、熟悉含有类的面向对象的语言的人,稍加对比,就能发现其很多相似之处。</p>
<p>上面的都是我自己的拙见,如有错误或者不对的地方,非常欢迎指出!人生总是要不断地去学习嘛~</p>
从零开始学习 Go ——安装
https://segmentfault.com/a/1190000009594143
2017-05-30T09:35:00+08:00
2017-05-30T09:35:00+08:00
daryl
https://segmentfault.com/u/daryl
4
<h2>0x01 设置 Go 环境</h2>
<p>要安装并顺利使用 Go,第一步就是要设置 Go 的环境。</p>
<p>需要设置的 Go 的环境变量,一共有三个。</p>
<ul>
<li><p><code>GOROOT</code> Go 语言的源码以及安装目录。</p></li>
<li><p><code>GOPATH</code> Go 语言的开发目录,目录可以有多个,但是,当我们执行 <code>go get</code> 命令的时候,如未指定目录,会默认保存在第一个目录下。</p></li>
<li><p><code>GOROOT_BOOTSTRAP</code> 这个目录在安装 Go 1.5 版本及之后的版本时需要设置。由于在 1.4 版本后,Go 编译器实现了自举,即通过 1.4 版本来编译安装之后版本的编译器。如果不设置该环境变量的话,会产生这样一个错误 <code>Set $GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4.</code>。</p></li>
</ul>
<p>除此之外,还需要配置 <code>PATH</code> 环境变量到 Go 的二进制程序目录。</p>
<p>我们需要在 <code>~/.bash_profile</code> 中添加下面的代码(我把所有的 Go 语言相关的东西都放在了 <code>~/.golang</code> 下面了):</p>
<pre><code class="shell">export GOROOT=$HOME/.golang/go
export GOPATH=$HOME/.golang/path
export PATH=$PATH:$HOME/.golang/go/bin
export GOROOT_BOOTSTRAP=$HOME/.golang/go1.4</code></pre>
<h2>0x02 安装 Go</h2>
<p>我们有两种方式下载 Go,一个是直接<a href="https://link.segmentfault.com/?enc=QT%2FcRqXnDxZKwufwuPEXAg%3D%3D.JERfNjCAyL6xD%2By3VLX2pPq%2Fipi8laDqpReLBVUxcoGZI7gHgIWhbaGR8WIJfLuszS4MyT%2FRrmZVdkQfdqAgmg%3D%3D" rel="nofollow">下载源码</a>,另一个是通过 GitHub 克隆项目,个人推荐选择第二种,地址:<a href="https://link.segmentfault.com/?enc=NYcM67L0ahEIsXgNgLoJNw%3D%3D.4sqjNtdf%2BHv6WhEoPhhnUEkWbL6AUmyur3xIxkUOD4A%3D" rel="nofollow">GayHub</a>。</p>
<p>首先将项目克隆到本地。</p>
<pre><code class="shell">$ git clone https://github.com/golang/go.git ~/.golang/go</code></pre>
<p>然后再复制一份作为 1.4 版本的目录。</p>
<pre><code class="shell">$ cp -r go go1.4</code></pre>
<p>进入 1.4 的文件夹后,将切换分支开始安装。</p>
<pre><code class="shell">$ git checkout -b release-branch.go1.4 origin/release-branch.go1.4
$ cd go1.4/src
$ ./make.bash</code></pre>
<p>编译安装好之后,进入之前的 go 文件夹,真正开始编译安装 Go。</p>
<pre><code class="shell">$ cd go/src
$ ./make.bash</code></pre>
<p>最后,我们试试 <code>go version</code> 来查看版本,可能会发现很奇怪的东西。</p>
<pre><code class="shell">$ go version
go version devel +d64c49098c Sun May 28 10:23:38 2017 +0000 darwin/amd64</code></pre>
<p>这是我们编译了 HEAD 的版本,也就是最新提交的版本,这个版本并不稳定。我们可以将分之切换到稳定版本来进行安装。截止到这篇文章,Go 的最新稳定版本时 1.8.3。所以我们要讲分支切换到 <code>release-branch.go1.8</code>。</p>
<h2>0x03 完整命令</h2>
<pre><code class="shell">$ echo "export GOROOT=$HOME/.golang/go" >> ~/.bash_profile
$ echo "export GOPATH=$HOME/.golang/path" >> ~/.bash_profile
$ echo "export PATH=$PATH:$HOME/.golang/go/bin" >> ~/.bash_profile
$ echo "export GOROOT_BOOTSTRAP=$HOME/.golang/go1.4" >> ~/.bash_profile
$ source ~/.bash_profile
$ cd ~
$ mkdir .golang
$ git clone https://github.com/golang/go.git go
$ cp -r go go1.4
$ cd go1.4
$ git checkout -b release-branch.go1.4 origin/release-branch.go1.4
$ cd src
$ ./make.bash
$ cd ../../go
$ git checkout -b release-branch.go1.8 origin/release-branch.go1.8
$ cd src
$ ./make.bash
$ go version</code></pre>
为什么Swoole可以加速php
https://segmentfault.com/a/1190000009486485
2017-05-20T09:25:00+08:00
2017-05-20T09:25:00+08:00
daryl
https://segmentfault.com/u/daryl
24
<h2>前言</h2>
<p>最近在研究<code>Swoole</code>,原来一直听别人在说<code>Swoole</code>可以加速,一直都是懵逼的。在研究了<code>Swoole</code>之后,我有了一些自己的理解。</p>
<h2>PHP-CGI 的黑历史</h2>
<p>对于 <code>PHP</code> 处理网络请求,大家基本上也都是再用 <code>CGI</code> 的方式来做的。那么,什么是 <code>CGI</code> 呢。</p>
<h3>CGI</h3>
<p><code>CGI</code>,全称 <code>Common Gateway Interface</code>,中文称作“公共网关接口”。也许有很多人认为 <code>CGI</code> 是一个程序,没错,曾经的我也是这么认为的。直到我从《图解HTTP》开始细细地研究<code>HTTP</code>协议之后,我才知道,原来 <code>CGI</code> 是一种协议。任何编程语言,都可以实现 <code>CGI</code>,所以任何语言都可以作为网站的后台语言(扯远了)。</p>
<h3>PHP-CGI</h3>
<p>上面说了,<code>CGI</code> 是一个协议,所以,<code>PHP</code> 有自己对 <code>CGI</code> 的实现,那就是 <code>PHP-CGI</code>。可是呢,随着技术的发展,人们开始意识到,<code>PHP-CGI</code> 的性能不是那么尽如人意。我们知道,<code>PHP</code> 在运行的时候,是依赖配置文件 <code>php.ini</code>的。所以,每当 <code>PHP-CGI</code> 开始工作的时候,它是完完全全的一个新进程,它需要重新加载配置文件并初始化,这就造成了很大的资源和时间的浪费。</p>
<h3>FastCGI</h3>
<p>那么,怎么才能避免这种浪费呢,聪明的程序员们想出了另外一种方法:我们为什么不预先加载好配置,然后,每一个执行的任务只需要复制当前的进程,不就能避免上面的浪费了么。于是,<code> FastCGI</code> 便横空出世。</p>
<p><code>FastCGI</code>,全称 <code>Fast Common Gateway Interface</code>,中文译作“快速公共网管接口”。没错,这又是个协议。当然,这个协议并不是因为 <code>PHP</code> 才有的。</p>
<h3>Apache (httpd)</h3>
<p>几乎所有的 <code>Web</code> 容器都实现了 <code>FastCGI</code> 的功能。首先是 <code>httpd</code>。对于 <code>PHP</code> 来说,<code>httpd</code> 是通过自身来实现一个 <code>FastCGI</code> 的模块的。它会预先加载好 <code>php.ini</code> 文件中的配置。待到有请求进入需要 <code>PHP</code> 处理时,<code>PHP</code> 就不需要再对 <code>php.ini</code> 重新加载了。这也就是每改动过 <code>php.ini</code> 后都要重启 <code>httpd</code> 服务的原因。</p>
<h3>Nginx 与 php-fpm</h3>
<p><code>php-fpm</code> 也是 <code>FastCGI</code> 的一种实现。通常我们是将 <code>Nginx</code> 的 <code>PHP</code> 处理部分代理到 <code>php-fpm</code> 的端口上,交给 <code>php-fpm</code> 来处理。而 <code>php-fpm</code> 同样是通过预先加载配置,然后给到子进程的方式的,它会对进程做一些管理。</p>
<h2>Swoole</h2>
<p>辣么问题来了,<code>php-fpm</code> 虽然实现了 <code>FastCGI</code>,但是,它在处理请求的时候,依然要重新运行一个脚本,像 <code>Laravel</code> 一样的框架,一开始就要加载辣么多依赖和文件,依然是一个不小的开销。我们看一下 <code>Laravel</code> 的 <code>public/index.php</code> 的源码。</p>
<pre><code class="php">require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);</code></pre>
<p>看看前面两条语句,这需要加载多少个依赖啊,这都是大把大把的时间和资源啊,每一次请求都需要加载一边,真是心疼啊。</p>
<p>那么,我们为什么不能像之前一样,能够不重新加载配置文件的 <code>FastCGI</code> ,来一个不用加载这么多的依赖的方式呢?</p>
<p>当然可以啦,这时候 <code>Swoole</code> 就派上用场了。既然是通过 <code>$app->make</code> 的方式来生成一个新的 <code>Kernel</code> 对象,那么 <code>Application</code> 的对象 <code>$app</code> 自然是不会有什么改变的了。所以,我们可以在收到请求之前,就把 <code>$app</code> 给生成好,这样就会快了,不是么?我们可以对它进行一个简单的改造。</p>
<pre><code class="php">require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$serv = new \Swoole\Server\Http('127.0.0.1', 9501);
$serv->on('request', function ($req, $res) use ($app) {
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$res->end($response);
$kernel->terminate($request, $response);
});
$serv->start();</code></pre>
<p>好了,我们现在就可以通过执行这个脚本来监听9501端口了。然后就像 <code>Nginx</code> 配置 <code>php-fpm</code> 一样来配置它就可以了。这样我们可以看到,在收到请求之前,就已经把依赖加载干净了,剩下的就是处理请求了。</p>
<p>当然我的这个改动很简陋,根本无法用于生产环境的,只是提供一个例子。</p>
<h2>后记</h2>
<p>以上只是我自己的理解和对我自己的理解进行的总结。对于 <code>Swoole</code> 我还在探索当中,因为它需要的只是实在是太多了,需要一点一点积累。本文可能有不对的地方,欢迎各位大神来拍砖!</p>
TCP 状态转移
https://segmentfault.com/a/1190000009196565
2017-04-25T23:57:23+08:00
2017-04-25T23:57:23+08:00
daryl
https://segmentfault.com/u/daryl
1
<h2>TCP 状态转移</h2>
<p>在《Linux 高性能服务器编程》中,有下面这样的状态转移图。</p>
<p><img src="/img/remote/1460000009196568?w=729&h=651" alt="" title=""></p>
<h3>TCP 的各个状态</h3>
<h4>客户端</h4>
<h6>建立连接(三次握手)</h6>
<ul>
<li><p><code>SYN_SENT</code> 在客户端发送第一个同步报文段(<strong>第一次握手</strong>)之后,就会进入这个状态。</p></li>
<li><p><code>ESTABLISHED</code> 在收到服务端发送的确认和同步报文段(<strong>第二次握手</strong>)后,客户端只需要发送出一个确认报文段(<strong>第三次握手</strong>),就算完成了三次握手了,即客户端认为连接已经建立。</p></li>
</ul>
<h6>断开连接(四次挥手)</h6>
<ul>
<li><p><code>FIN_WAIT_1</code> TCP 断开连接时,需要进行四次挥手。当客户端主动关闭连接,发出第一个 <code>FIN</code> 报文段(<strong>第一次挥手</strong>)后,即进入此状态。</p></li>
<li><p><code>FIN_WAIT_2</code> 服务端在收到第一个 <code>FIN</code> 报文段后,回复给客户端确认报文段(<strong>第二次挥手</strong>)后的状态。其实 <code>FINE_WAIT</code> 的含义,就是等待服务端发来的 <code>FIN</code> 报文段。</p></li>
<li><p><code>TIME_WAIT</code> 在客户端收到服务端的 <code>FIN</code> 报文(<strong>第三次挥手</strong>)后,返回给服务端确认报文段(<strong>第四次挥手</strong>)后的状态。</p></li>
</ul>
<h4>服务端</h4>
<h6>建立连接(三次握手)</h6>
<ul>
<li><p><code>CLOSED</code> 一个假想的起点和终点,不是一个实际的状态。</p></li>
<li><p><code>LISTEN</code> 在程序启动后,等待客户端连接的状态。</p></li>
<li><p><code>SYN_RCVD</code> 在每一个 TCP 连接建立时,都要进行三次握手,这个状态表示服务端接收到客户端发来的同步报文段(<strong>第一次握手</strong>),并且向客户端发送了确认同步报文段(<strong>第二次握手</strong>)之后的状态,在这个状态时,其实连接已经经历了两次握手。</p></li>
<li><p><code>ESTABLISHED</code> 收到客户端发来的确认报文段(<strong>第三次握手</strong>),即三次握手成功后,双方正式建立连接的状态。</p></li>
</ul>
<h6>断开连接(四次挥手)</h6>
<ul>
<li><p><code>CLOSE_WAIT</code> 在收到客户端主动断开连接的 <code>FIN</code> 报文段(<strong>第一次挥手</strong>)后,返回给客户端确认报文段(<strong>第二次挥手</strong>)后的状态。</p></li>
<li><p><code>LAST_ACK</code> 服务端发送 <code>FIN</code> 报文段(<strong>第三次挥手</strong>)后的状态。在该状态之后,收到确认报文段(<strong>第四次握手</strong>),连接就关闭了。</p></li>
</ul>
<h3>时序图</h3>
<p>对于书中的状态转移图,有一些复杂和抽象。我根据自己的分析,画出了一份时序图。</p>
<h4>建立连接(三次握手)</h4>
<p><img src="/img/remote/1460000009196569?w=405&h=562" alt="" title=""></p>
<h4>断开连接(四次挥手)</h4>
<p><img src="/img/remote/1460000009196570?w=456&h=817" alt="" title=""></p>
<h3>TIME_WAIT 状态</h3>
<p>第一次看这个转移图的时候,我很疑惑,为什么要有一个 <code>TIME_WAIT</code> 状态?为什么不能直接到达 <code>CLOSED</code> 状态?相信很多人看到这个状态转移图,也会有这样的疑惑。其实,原因大致有下面两点。</p>
<h5>可靠地终止连接</h5>
<p>假设没有 <code>TIME_WAIT</code> 这种状态。现实中,网络环境不是理想的。在数据包传输的过程中,难免会有一些延时、丢包的情况发生。如果,在客户端最后一个确认报文段发出去之后,由于某种原因,没有到达服务端,这样,服务端在超时后,就会向客户端重新发一个 <code>FIN</code> 报文段,请求重传。由于在客户端,连接实际上已经断开,端口已经关闭。那么在客户端收到这个报文段后,会向服务端发送一个 <code>RST</code> 报文段。而服务器收到该报文段后,会认为是错误的,它所期望收到的是确认报文段。</p>
<h5>保证让迟来的报文段有足够的时间被处理</h5>
<p>同样在不理想的网络环境中,有些包会在网络中有所延迟。假如没有 <code>TIME_WAIT</code> 这种状态,在关闭连接后并且建立新的连接后,可能会收到该数据包。由于序号不同,客户端会要求服务端重传数据包,这样,连接就会混乱出错。而 <code>TIME_WAIT</code> 状态的时间,一般是 <code>2MAL</code> 时间,并且端口没有释放。这样,前一个连接的报文段有足够的时间被识别或者丢弃,也就不会出现这个问题。</p>
IP路由与转发
https://segmentfault.com/a/1190000008666802
2017-03-12T23:32:57+08:00
2017-03-12T23:32:57+08:00
daryl
https://segmentfault.com/u/daryl
0
<h2>Internet Protocol</h2>
<p>IP 全称为 Internet Protocol,顾名思义,是工作在网络层的一种协议。既然已经有了 MAC 地址,IP 地址就作为一种给网络赋予层次、方便管理的协议而存在。而网络层次的精髓,在于 IP 的路由与转发。</p>
<h2>IP 模块</h2>
<p>所有支持 IP 的主机或者路由器,都含有处理 IP 的 IP 模块。</p>
<p>IP 模块的工作有两个,一个是处理来自数据链路层的数据,根据自身是否允许路由以及 IP 数据报是否发给自己来进行路由。另一个是封装上层协议并且发送给下层。而路由过程呢,就是前者,判断外来的 IP 数据报是否应该转发。如果转发模块不允许转发 IP 数据报,并且该 IP 数据报不是发给本机的,则转发模块将会丢弃该 IP 数据报。</p>
<p>另外,当该 IP 数据报设置了源站路由选项,则转发模块也会根据路由选项中的 IP 来判断是丢弃还是转发。</p>
<p>一般来说,路由器多用于转发 IP 数据报,而主机多用于接受 IP 数据报,即转发模块不允许转发。</p>
<p>要实现 IP 的路由,则必须要在每一个主机、路由器中存储一张路由表,这样才会更加快捷、方便地知道该 IP 数据报的下一跳是哪一个主机(路由器)。</p>
<h2>路由表</h2>
<p>下面是我的电脑的路由表:</p>
<p><img src="/img/remote/1460000008666805?w=1134&h=594" alt="" title=""></p>
<p>看一下这张路由表,第一个是 <code>default</code>,说明这一条记录是默认路由,即该主机所在的网络的网关。如果一个 IP 数据报的目的 IP 地址没有存在这张路由表中,则默认发给 <code>default</code>,由它来转发该 IP 数据报。</p>
<p>下面的几项中,有一些 <code>Gateway</code> 中是 <code>link#4</code> 的。这里不同的主机显示的可能不一样,这个就相当于 <code>*</code> ,这个标志,说明其对应的 IP 与本机在同一个网络内,在数据转发模块接收到来自上层协议且目的 IP 为其中一个时,默认直接发送,而不需要进行路由。</p>
<p>路由表相当于一个路由的缓存,所以需要时长地更新。更新的方式有两种,一种是手动更新,即运行 <code>route</code> 命令;另一种就是自动更新,一般使用的协议是 BGP(Border Gateway Protocol)、RIP(Routing Information Protocol)。</p>
<h2>路由与转发</h2>
<p>路由就是通过路由表来进行 IP 模块的转发的。</p>
<p>数据包在以太网上传输,有多种可选的结构的,可是不管是什么样的结构,传输都是没有方向性的。那么在同一个网络内,以太网上传输的数据包会经过每一台主机或路由器。当它们确认,接收到的数据包不是发给自己的,就会将其通过 IP 转发子模块转发出去。当源 IP 与目的 IP 在同一个网络内时,上图中<code>Gateway</code>字段为<code>*</code>或者<code>link#4</code>的选项就会告诉主机,可以直接发送给目的地址,而不需要网关。</p>
<p>那么路由器是做什么的呢。路由器也可以被看做成一台主机。在同一网络内,它同样也会转发不属于它的数据包。但是当目的 IP 不在同一网络内时,路由器便会充当连接外部网络和该网络的桥梁。所以当路由为默认路由时,路由器会直接转发数据包而不会做处理。</p>
<h2>转发时的目的 IP</h2>
<p>其实一直有一个问题,我很困惑,那就是在 IP 报文传输、转发的过程中,源 IP 和目的 IP 是不会因为转发而改变的。那么就会有一个问题:当我们的数据报通过网关在公网中传输时,源 IP 不变,那么目的 IP 在返回的数据报目的 IP 该是什么呢。这么多的局域网,又如何区分呢。</p>
<p>这里,就有一个重要的转换,网络地址转换(Network Address Translation),简称 NAT 。NAT 有两种方式,一种是基本网络地址转换(Basic NAT),另一种是网络地址端口转换(NAPT)。前者要求每一个连接都要对应一个公网 IP 地址,后者则是在路由器转发到公网是,将 IP 地址改写,在路由器内部,使用端口来对应内网源主机。</p>
<p>有了这个,就理解前面的 IP 不变了。使用 NAT 后,我们的数据包经过了路由器的处理之后重新封装了,已经不是原来的 IP 数据报了,而目标 IP 可能也已经变成对方路由器的公网 IP 了。</p>
<p>而在主机和路由器之间,以及路由器和目的主机的路由器之间,无论经过多少跳,多少次转发,都不会改变源 IP 和目的 IP,这样就能理解源 IP 和目的 IP 不会改变了。</p>
<h2>IP 重定向</h2>
<p>当主机的路由表过期了,发送的数据包发错了怎么办?这就有了 ICMP 包来做重定向,是主机重新发送数据包到正确的 IP ,同时来更新路由表。</p>
<p>当我们的主机在发送数据包后,某一跳的路由发现,其实可以有更好的路线发送数据包,便会在收到数据包后回复一个 ICMP 的包,来告诉上一跳的主机或路由器,重新发送给新的路由,同时丢弃该数据包。这就是 IP 重定向了。</p>
<p>注意:一般来说,主机只能接受 ICMP 重定向报文,而路由器只能发送 ICMP 重定向报文。</p>
数据段、数据报、数据包、帧的区别与联系
https://segmentfault.com/a/1190000008449308
2017-02-23T00:51:24+08:00
2017-02-23T00:51:24+08:00
daryl
https://segmentfault.com/u/daryl
4
<p>在看《Linux高性能服务器编程》的过程中,TCP/IP 的章节里发现了几个比较有意思的名词:报文段、数据报、数据包和帧。这几个名词都是在讲协议部分出现的,一开始没明白有哪些区别、哪些联系,后来仔细想了下,又查了一下,似乎了解了大概。</p>
<h2>帧与数据包</h2>
<p>首先不容易理解的是<code>数据包</code>和<code>帧</code>。</p>
<p>数据包,就是从最上层,一层一层封装,直到网络层的,最后借由数据链路层发送出去的数据单元。</p>
<p>帧,是数据链路层的传输单元。</p>
<p>这么一看,数据包和帧好像没什么不一样,好像数据传递的都是一样的。可是为什么会把它们区分开呢?学习 TCP/IP 协议的同学应该都知道,数据链路层中有 MTU 这样一个东西,它是帧最大传输单元。</p>
<p>数据包是一个完整的数据单元,但是如果数据包的大小超过了 MTU 呢?所以,可能许多帧组合在一起,才能形成一个完整的数据包,这就是帧和数据包的关系。</p>
<h2>数据包与数据报</h2>
<p>说完了数据包与帧,那么数据包与数据报又是什么关系呢?</p>
<p>数据包是整个的数据单元,那么数据报就是组成这一个数据单元的分组。每一层封装后的数据都可以称作数据报也就是说,一个完整的数据包是有若干个数据报组成的。</p>
<p>而和帧不同的是,帧是作为数据链路层的传输单元,而数据报数据包的分组。</p>
<h2>数据段</h2>
<p>在看协议的时候,有了一个疑问,那就是同是在传输层的 TCP 和 UDP 两个协议,一个称为 TCP 数据段,一个称为 UDP 数据报。这是为啥嘞?</p>
<p>原来上面的数据报还有一条,数据报是面向无连接传输的协议,而 TCP 是面向连接的协议。</p>
<p>UDP 是无连接的协议,就是随着 IP 一起传输就可以了,不必去管连接和分组问题。可是 TCP 不一样了,面向连接,在数据部分很大的时候,要分组。这样每个分组就称为 TCP 数据段。这些 TCP 段组成了一个完整的 TCP 数据报。</p>
<h2>后记</h2>
<p>这几个概念算是勉勉强强自圆其说了。菜鸟文章,大神路过如果有错误欢迎指正,非常非常感谢!</p>