-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
569 lines (569 loc) · 246 KB
/
search.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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[函数调用参数入栈顺序]]></title>
<url>%2F2017%2F08%2F23%2Fstack_order_of_function_parameter%2F</url>
<content type="text"><![CDATA[之前一直以为函数的参数入栈顺序是从右向左,但事实上不同架构的系统实现是不同的。具体分析如下。 32位系统32位系统:参数从右到左入栈 具体分析可看汇编程序,先给出测试代码 1234567891011121314class MC {public: int address(int a, int b) { return 0; }}; int main(){ MC mc; mc.address(1, 2); return 0;} gdb列出汇编代码,如下 123456789101112131415161718192021(gdb) disassDump of assembler code for function main(): 0x080484c8 <+0>: lea 0x4(%esp),%ecx 0x080484cc <+4>: and $0xfffffff0,%esp 0x080484cf <+7>: pushl -0x4(%ecx) 0x080484d2 <+10>: push %ebp 0x080484d3 <+11>: mov %esp,%ebp 0x080484d5 <+13>: push %ecx 0x080484d6 <+14>: sub $0x14,%esp=> 0x080484d9 <+17>: sub $0x4,%esp 0x080484dc <+20>: push $0x2 0x080484de <+22>: push $0x1 0x080484e0 <+24>: lea -0x9(%ebp),%eax 0x080484e3 <+27>: push %eax 0x080484e4 <+28>: call 0x80484fa <MC::address(int, int)> 0x080484e9 <+33>: add $0x10,%esp 0x080484ec <+36>: mov $0x0,%eax 0x080484f1 <+41>: mov -0x4(%ebp),%ecx 0x080484f4 <+44>: leave 0x080484f5 <+45>: lea -0x4(%ecx),%esp 0x080484f8 <+48>: ret 可以看到11-14行,汇编代码是先push $0x2然后push $0x1,最后才push %eax,这个是this指针。也就是说参数入栈顺序为后面的参数先入栈。 64位系统64位系统稍微有点不同,前六个参数先放在寄存器中,其他参数从后面开始入栈。 测试代码如下 1234567891011121314class MC {public: void foo(int a1, int a2, int a3, int a4, int a5, int a6, int a7) { return; }};int main(){ MC mc; mc.foo(1, 2, 3, 4, 5, 6, 7); return 0;} gdb汇编代码如下 123456789101112131415161718192021222324252627282930313233(gdb) disassDump of assembler code for function main(): 0x00000000004005b0 <+0>: push %rbp 0x00000000004005b1 <+1>: mov %rsp,%rbp 0x00000000004005b4 <+4>: sub $0x20,%rsp=> 0x00000000004005b8 <+8>: lea -0x1(%rbp),%rax 0x00000000004005bc <+12>: movl $0x7,0x8(%rsp) 0x00000000004005c4 <+20>: movl $0x6,(%rsp) 0x00000000004005cb <+27>: mov $0x5,%r9d 0x00000000004005d1 <+33>: mov $0x4,%r8d 0x00000000004005d7 <+39>: mov $0x3,%ecx 0x00000000004005dc <+44>: mov $0x2,%edx 0x00000000004005e1 <+49>: mov $0x1,%esi 0x00000000004005e6 <+54>: mov %rax,%rdi 0x00000000004005e9 <+57>: callq 0x4005f6 <MC::foo(int, int, int, int, int, int, int)> 0x00000000004005ee <+62>: mov $0x0,%eax 0x00000000004005f3 <+67>: leaveq 0x00000000004005f4 <+68>: retq //进入foo函数后Dump of assembler code for function MC::foo(int, int, int, int, int, int, int): 0x00000000004005f6 <+0>: push %rbp 0x00000000004005f7 <+1>: mov %rsp,%rbp 0x00000000004005fa <+4>: mov %rdi,-0x8(%rbp) 0x00000000004005fe <+8>: mov %esi,-0xc(%rbp) 0x0000000000400601 <+11>: mov %edx,-0x10(%rbp) 0x0000000000400604 <+14>: mov %ecx,-0x14(%rbp) 0x0000000000400607 <+17>: mov %r8d,-0x18(%rbp) 0x000000000040060b <+21>: mov %r9d,-0x1c(%rbp)=> 0x000000000040060f <+25>: nop 0x0000000000400610 <+26>: pop %rbp 0x0000000000400611 <+27>: retq End of assembler dump. 7-14行,后面的参数7和6先入栈,然后前面1-5以及this指针按照从右向左的顺序依次保存在寄存器中。 23-29行,先将栈中的参数7和6压入,然后在按照1-6寄存器的顺序将寄存器中的参数压入。 64位系统16个寄存器含义]]></content>
<categories>
<category>函数参数</category>
</categories>
<tags>
<tag>gdb</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C++ this指针]]></title>
<url>%2F2017%2F08%2F23%2Fcpp_this%2F</url>
<content type="text"><![CDATA[C++在创建对象时,系统会分配给每个对象一个独立的存储空间。而在类的内部,是通过隐式传递this来访问当前对象自己本身,也就是说只能在成员函数中访问到this指针。下面通过示例代码和gdb调试来探究this。 this指针不是类成员12345678910111213141516171819202122#include <iostream>using namespace std;class MC{public: MC() : val(1) {} void set_val(int v) { this->val = v; } long address() { return &this; }private: int val;};int main(){ MC mc; mc.set_val(100); return 0;} 上述代码会输出4,即sizeof(val)的值,这说明this指针并不是类的成员。 this指针是指针常量123class MC { long address() { return &this; }}; 通过上面可以得知,this指针实际上不是类的成员,于是尝试在打印出this指针的地址。然而上面的代码实际上会编译出错的,错误信息为: 单目‘&’的操作数必须是左值 return &this; 原因暂时不明 在gdb查看this指针类型,可以看出,this指针实际上是个Type *const 12(gdb) whatis thistype = MC * const this指针在成员函数通过参数隐式传入上面说了this指针不是类成员,而是隐式传入给成员函数的。实际上,类对象在调用成员函数时,编译器将其作为第一个参数隐式传给成员函数的,也就是说,this指针实际上是调用时传递给成员函数的一个形参。 gdb step进入成员函数,可以看到函数传递的参数列表第一个就是this,同时也可以看到this指针的内容,也就是当前对象的首地址。 12MC::set_val (this=0x7fffffffe400, v=100) at thisgdb.cc:99 this->val = v; 用gdb info frame查看栈帧如下,同样的也可以看到参数列表第一个就是this。 123456789(gdb) info frameStack level 0, frame at 0x7fffffffe400: rip = 0x40077d in MC::set_val (thisgdb.cc:9); saved rip 0x400705 called by frame at 0x7fffffffe420 source language c++. Arglist at 0x7fffffffe3f0, args: this=0x7fffffffe400, v=100 Locals at 0x7fffffffe3f0, Previous frame's sp is 0x7fffffffe400 Saved registers: rbp at 0x7fffffffe3f0, rip at 0x7fffffffe3f8]]></content>
<categories>
<category>c++</category>
</categories>
<tags>
<tag>this</tag>
</tags>
</entry>
<entry>
<title><![CDATA[TCP-Keeplive与应用层心跳包]]></title>
<url>%2F2017%2F08%2F20%2Ftcp_keepalive_and_heartbeat%2F</url>
<content type="text"><![CDATA[1setsockopt(sockfd, SOL_SOCKET, SO_KEEPLIVE, &on, sizeof(on)); SO_KEEPLIVE实现在服务器侧,由服务器来进行侦测,客户端被动响应,默认时间120s。应用层心跳则是客户端主动上报,服务端收集心跳信息,时间自定义。 SO_KEEPLIVE的实现在TCP协议栈(第四层),应用层心跳实现在第七层。SO_KEEPLIVE由TCP协议规定,应用层的心跳则是由自己来定义协议。 SO_KEEPLIVE是TCP协议自带的保活功能, 使用起来简单, 减少了应用层代码的复杂度. 也会更节省流量。 应用层心跳比较灵活, 因为协议层的心跳只能提供最纯粹的检活功能, 但是应用层自己可以随意控制, 包括协议可能提供的是秒级的。同时,应用层心跳比较通用, 应用层的心跳不依赖协议. 如果有一天不用TCP要改为UDP了, 协议层不提供心跳机制了, 但是你应用层的心跳依旧是通用的, 可能只需要做少许改动就可以继续使用。应用层心跳的不好的地方也很显而易见, 增加开发工作量, 由于应用特定的网络框架, 还可能很增加代码结构的复杂度,同时,应用层心跳的流量消耗更大。]]></content>
<categories>
<category>keeplive</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C++操作符重载]]></title>
<url>%2F2017%2F08%2F03%2Fcpp_operator_reload%2F</url>
<content type="text"><![CDATA[TODO [ ] operator++ & operator– [ ] operator& [ ] operator* [ ] operator-> [ ] operator= [ ] operator[] [ ] type operator()(args)重载对象地()操作符,参数为args,一般用于仿函数。例如: 12345678910111213141516template<typename T>class Less{public: bool operator()(const T &lhs, const T &rhs) { return lhs < rhs; }}int main(){ if (Less(1, 2)) { //... } return 0;} operator type()重载隐式转换操作,转换为type类型,例如: 1234567891011121314151617181920class Bool{public: operator bool(){ return m_b; }private: bool m_b;};int main(){ Bool b(true); if (b) { cout << "Bool::true" << endl; } else { cout << "Bool::false" << endl; } return 0;}]]></content>
<categories>
<category>operator</category>
</categories>
<tags>
<tag>C++</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【转】Actor模型和CSP模型的区别]]></title>
<url>%2F2017%2F07%2F15%2F%5BR%5Ddiference_between_actor_and_csp%2F</url>
<content type="text"><![CDATA[1 Akka/Erlang Actor模型Actors模型(Actor model)首先是由Carl Hewitt在1973定义,由Erlang OTP (Open Telecom Platform) 推广,其 消息传递更加符合面向对象的原始意图。 Actors属于并发组件模型 ,通过组件方式定义并发编程范式的高级阶段,避免使用者直接接触多线程并发或线程池等基础概念。 传统多数流行的语言并发是基于多线程之间的共享内存,使用同步方法防止写争夺,Actors使用消息模型,每个Actors在同一时间处理最多一个消息,可以发送消息给其他Actors,保证了单独写原则,从而巧妙避免了多线程写争夺。Actors模型的特点是: 隔离计算实体 “Share nothing” 没有任何地方同步 异步消息传递 不可变的消息 消息模型类似mailbox / queue Actors是一个轻量级的对象,通过发送消息实现交互。每个Actors在同一时间处理最多一个消息,可以发送消息给其他Actors。在同一时间可以于一个Java虚拟机存在数以百万计的参与者,构架是一个分层的父层(管理) - 子层,其中父层监控子层的行为。还可以很容易地扩展Actor运行在集群中各个节点之间 - 无需修改一行代码。每个演员都可以有内部状态(字段/变量) ,但通信只能通过消息传递,不会有共享数据结构(计数器,队列) 。Akka框架支持两种语言Java和Scala, 在Actor模型中,主角是Actor,类似一种worker,Actor彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的: Actor模型描述了一组为了避免并发编程的常见问题的公理: 所有Actor状态是Actor本地的,外部无法访问。 Actor必须只有通过消息传递进行通信。 一个Actor可以响应消息:推出新Actor,改变其内部状态,或将消息发送到一个或多个其他参与者。 Actor可能会堵塞自己,但Actor不应该堵塞它运行的线程。 2 CSP- Channel模型CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。 Channel模型中,worker之间不直接彼此联系,而是通过不同channel进行消息发布和侦听。消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。 Go语言的CSP模型是由协程Goroutine与通道Channel实现: Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。 通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。 3 Actor模型和CSP区别Actor模型和CSP区别图如下: Actor之间直接通讯,而CSP是通过Channel通讯,在耦合度上两者是有区别的,后者更加松耦合。 同时,它们都是描述独立的进程通过消息传递进行通信。主要的区别在于:在CSP消息交换是同步的(即两个进程的执行”接触点”的,在此他们交换消息),而Actor模型是完全解耦的,可以在任意的时间将消息发送给任何未经证实的接受者。由于Actor享有更大的相互独立,因为他可以根据自己的状态选择处理哪个传入消息。自主性更大些。 在Go语言中为了不堵塞进程,程序员必须检查不同的传入消息,以便预见确保正确的顺序。CSP好处是Channel不需要缓冲消息,而Actor理论上需要一个无限大小的邮箱作为消息缓冲。]]></content>
<categories>
<category>并发编程</category>
<category>Go</category>
</categories>
<tags>
<tag>csp</tag>
<tag>channel</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【转】论述Redis和Memcached的差异]]></title>
<url>%2F2017%2F07%2F15%2F%5BR%5Ddifference_between_redis_and_memcached%2F</url>
<content type="text"><![CDATA[摘要: Redis 和 Memcache 都是基于内存的数据存储系统。Memcached是高性能分布式内存缓存服务;Redis是一个开源的key-value存储系统。与Memcached类似,Redis将大部分数据存储在内存中,支持的数据类型包括:字符串、哈希 表、链表、等数据类型的相关操作。 Redis 和 Memcache 都是基于内存的数据存储系统。Memcached是高性能分布式内存缓存服务;Redis是一个开源的key-value存储系统。与Memcached类似,Redis将大部分数据存储在内存中,支持的数据类型包括:字符串、哈希 表、链表、等数据类型的相关操作。下面我们来进行来看一下redis和memcached的区别。 0 权威比较Redis的作者Salvatore Sanfilippo曾经对这两种基于内存的数据存储系统进行过比较: Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。 内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。 性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。 具体为什么会出现上面的结论,以下为收集到的资料: 1 数据类型支持不同与Memcached仅支持简单的key-value结构的数据记录不同,Redis支持的数据类型要丰富得多。最为常用的数据类型主要由五种:String、Hash、List、Set和Sorted Set。Redis内部使用一个redisObject对象来表示所有的key和value。redisObject最主要的信息如图所示: type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:”123″ “456”这样的字符串。只有打开了Redis的虚拟内存功能,vm字段字段才会真正的分配内存,该功能默认是关闭状态的。 1.1) String常用命令:set/get/decr/incr/mget等; 应用场景:String是最常用的一种数据类型,普通的key/value存储都可以归为此类; 实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。 1.2) Hash常用命令:hget/hset/hgetall等 应用场景:我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日; 实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。 1.3) List常用命令:lpush/rpush/lpop/rpop/lrange等; 应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现; 实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。 1.4) Set常用命令:sadd/spop/smembers/sunion等; 应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的; 实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。 1.5) Sorted Set常用命令:zadd/zrange/zrem/zcard等; 应用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。 实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。 2 内存管理机制不同在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘。Redis只会缓存所有的key的信息,如果Redis发现内存的使用量超过了某一个阀值,将触发swap的操作,Redis根据“swappability = age*log(size_in_memory)”计算出哪些key对应的value需要swap到磁盘。然后再将这些key对应的value持久化到磁盘中,同时在内存中清除。这种特性使得Redis可以保持超过其机器本身内存大小的数据。当然,机器本身的内存必须要能够保持所有的key,毕竟这些数据是不会进行swap操作的。同时由于Redis将内存中的数据swap到磁盘中的时候,提供服务的主线程和进行swap操作的子线程会共享这部分内存,所以如果更新需要swap的数据,Redis将阻塞这个操作,直到子线程完成swap操作后才可以进行修改。当从Redis中读取数据的时候,如果读取的key对应的value不在内存中,那么Redis就需要从swap文件中加载相应数据,然后再返回给请求方。 这里就存在一个I/O线程池的问题。在默认的情况下,Redis会出现阻塞,即完成所有的swap文件加载后才会相应。这种策略在客户端的数量较小,进行批量操作的时候比较合适。但是如果将Redis应用在一个大型的网站应用程序中,这显然是无法满足大并发的情况的。所以Redis运行我们设置I/O线程池的大小,对需要从swap文件中加载相应数据的读取请求进行并发操作,减少阻塞的时间。 对于像Redis和Memcached这种基于内存的数据库系统来说,内存管理的效率高低是影响系统性能的关键因素。传统C语言中的malloc/free函数是最常用的分配和释放内存的方法,但是这种方法存在着很大的缺陷:首先,对于开发人员来说不匹配的malloc和free容易造成内存泄露;其次频繁调用会造成大量内存碎片无法回收重新利用,降低内存利用率;最后作为系统调用,其系统开销远远大于一般函数调用。所以,为了提高内存的管理效率,高效的内存管理方案都不会直接使用malloc/free调用。Redis和Memcached均使用了自身设计的内存管理机制,但是实现方法存在很大的差异,下面将会对两者的内存管理机制分别进行介绍。 Memcached默认使用Slab Allocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。Slab Allocation机制只为存储外部数据而设计,也就是说所有的key-value数据都存储在Slab Allocation系统里,而Memcached的其它内存请求则通过普通的malloc/free来申请,因为这些请求的数量和频率决定了它们不会对整个系统的性能造成影响Slab Allocation的原理相当简单。 如图所示,它首先从操作系统申请一大块内存,并将其分割成各种尺寸的块Chunk,并把尺寸相同的块分成组Slab Class。其中,Chunk就是用来存储key-value数据的最小单位。每个Slab Class的大小,可以在Memcached启动的时候通过制定Growth Factor来控制。假定图中Growth Factor的取值为1.25,如果第一组Chunk的大小为88个字节,第二组Chunk的大小就为112个字节,依此类推。 当Memcached接收到客户端发送过来的数据时首先会根据收到数据的大小选择一个最合适的Slab Class,然后通过查询Memcached保存着的该Slab Class内空闲Chunk的列表就可以找到一个可用于存储数据的Chunk。当一条数据库过期或者丢弃时,该记录所占用的Chunk就可以回收,重新添加到空闲列表中。 从以上过程我们可以看出Memcached的内存管理制效率高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。因为每个Chunk都分配了特定长度的内存空间,所以变长数据无法充分利用这些空间。如图 所示,将100个字节的数据缓存到128个字节的Chunk中,剩余的28个字节就浪费掉了。 Redis的内存管理主要通过源码中zmalloc.h和zmalloc.c两个文件来实现的。Redis为了方便内存的管理,在分配一块内存之后,会将这块内存的大小存入内存块的头部。如图所示,real_ptr是redis调用malloc后返回的指针。redis将内存块的大小size存入头部,size所占据的内存大小是已知的,为size_t类型的长度,然后返回ret_ptr。当需要释放内存的时候,ret_ptr被传给内存管理程序。通过ret_ptr,程序可以很容易的算出real_ptr的值,然后将real_ptr传给free释放内存。 Redis通过定义一个数组来记录所有的内存分配情况,这个数组的长度为ZMALLOC_MAX_ALLOC_STAT。数组的每一个元素代表当前程序所分配的内存块的个数,且内存块的大小为该元素的下标。在源码中,这个数组为zmalloc_allocations。zmalloc_allocations[16]代表已经分配的长度为16bytes的内存块的个数。zmalloc.c中有一个静态变量used_memory用来记录当前分配的内存总大小。所以,总的来看,Redis采用的是包装的mallc/free,相较于Memcached的内存管理方法来说,要简单很多。 3 数据持久化支持Redis虽然是基于内存的存储系统,但是它本身是支持内存数据的持久化的,而且提供两种主要的持久化策略:RDB快照和AOF日志。而memcached是不支持数据持久化操作的。 3.1) RDB快照Redis支持将当前数据的快照存成一个数据文件的持久化机制,即RDB快照。但是一个持续写入的数据库如何生成快照呢?Redis借助了fork命令的copy on write机制。在生成快照时,将当前进程fork出一个子进程,然后在子进程中循环所有的数据,将数据写成为RDB文件。我们可以通过Redis的save指令来配置RDB快照生成的时机,比如配置10分钟就生成快照,也可以配置有1000次写入就生成快照,也可以多个规则一起实施。这些规则的定义就在Redis的配置文件中,你也可以通过Redis的CONFIG SET命令在Redis运行时设置规则,不需要重启Redis。 Redis的RDB文件不会坏掉,因为其写操作是在一个新进程中进行的,当生成一个新的RDB文件时,Redis生成的子进程会先将数据写到一个临时文件中,然后通过原子性rename系统调用将临时文件重命名为RDB文件,这样在任何时候出现故障,Redis的RDB文件都总是可用的。同时,Redis的RDB文件也是Redis主从同步内部实现中的一环。RDB有他的不足,就是一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的,从上次RDB文件生成到Redis停机这段时间的数据全部丢掉了。在某些业务下,这是可以忍受的。 3.2) AOF日志AOF日志的全称是append only file,它是一个追加写入的日志文件。与一般数据库的binlog不同的是,AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令。只有那些会导致数据发生修改的命令才会追加到AOF文件。每一条修改数据的命令都生成一条日志,AOF文件会越来越大,所以Redis又提供了一个功能,叫做AOF rewrite。其功能就是重新生成一份AOF文件,新的AOF文件中一条记录的操作只会有一次,而不像一份老文件那样,可能记录了对同一个值的多次操作。其生成过程和RDB类似,也是fork一个进程,直接遍历数据,写入新的AOF临时文件。在写入新文件的过程中,所有的写操作日志还是会写到原来老的AOF文件中,同时还会记录在内存缓冲区中。当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。然后调用原子性的rename命令用新的AOF文件取代老的AOF文件。 AOF是一个写文件操作,其目的是将操作日志写到磁盘上,所以它也同样会遇到我们上面说的写操作的流程。在Redis中对AOF调用write写入后,通过appendfsync选项来控制调用fsync将其写到磁盘上的时间,下面appendfsync的三个设置项,安全强度逐渐变强。 appendfsync no 当设置appendfsync为no的时候,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。 appendfsync everysec 当设置appendfsync为everysec的时候,Redis会默认每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。但是当这一次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。所以结论就是,在绝大多数情况下,Redis会每隔一秒进行一次fsync。在最坏的情况下,两秒钟会进行一次fsync操作。这一操作在大多数数据库系统中被称为group commit,就是组合多次写操作的数据,一次性将日志写到磁盘。 appednfsync always 当设置appendfsync为always时,每一次写操作都会调用一次fsync,这时数据是最安全的,当然,由于每次都会执行fsync,所以其性能也会受到影响。 对于一般性的业务需求,建议使用RDB的方式进行持久化,原因是RDB的开销并相比AOF日志要低很多,对于那些无法忍数据丢失的应用,建议使用AOF日志。 4 集群管理的不同Memcached是全内存的数据缓冲系统,Redis虽然支持数据的持久化,但是全内存毕竟才是其高性能的本质。作为基于内存的存储系统来说,机器物理内存的大小就是系统能够容纳的最大数据量。如果需要处理的数据量超过了单台机器的物理内存大小,就需要构建分布式集群来扩展存储能力。 Memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。下图给出了Memcached的分布式存储实现架构。当客户端向Memcached集群发送数据之前,首先会通过内置的分布式算法计算出该条数据的目标节点,然后数据会直接发送到该节点上存储。但客户端查询数据时,同样要计算出查询数据所在的节点,然后直接向该节点发送查询请求以获取数据。 相较于Memcached只能采用客户端实现分布式存储,Redis更偏向于在服务器端构建分布式存储。最新版本的Redis已经支持了分布式存储功能。Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。下图给出Redis Cluster的分布式存储架构,其中节点与节点之间通过二进制协议进行通信,节点与客户端之间通过ascii协议进行通信。在数据的放置策略上,Redis Cluster将整个key的数值域分成4096个哈希槽,每个节点上可以存储一个或多个哈希槽,也就是说当前Redis Cluster支持的最大节点数就是4096。Redis Cluster使用的分布式算法也很简单:crc16( key ) % HASH_SLOTS_NUMBER。 为了保证单点故障下的数据可用性,Redis Cluster引入了Master节点和Slave节点。在Redis Cluster中,每个Master节点都会有对应的两个用于冗余的Slave节点。这样在整个集群中,任意两个节点的宕机都不会导致数据的不可用。当Master节点退出后,集群会自动选择一个Slave节点成为新的Master节点。]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>redis</tag>
<tag>cache</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【转】Golang CSP并发模型]]></title>
<url>%2F2017%2F07%2F14%2F%5BR%5Dgolang_csp_concurrent_model%2F</url>
<content type="text"><![CDATA[CSP并发模型CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。 Golang CSPGolang 就是借用CSP模型的一些概念为之实现并发进行理论支持,其实从实际上出发,go语言并没有,完全实现了CSP模型的所有理论,仅仅是借用了 process和channel这两个概念。process是在go语言上的表现就是 goroutine 是实际并发执行的实体,每个实体之间是通过channel通讯来实现数据共享。 ChannelGolang中使用 CSP中 channel 这个概念。channel 是被单独创建并且可以在进程之间传递,它的通信模式类似于 boss-worker 模式的,一个实体通过将消息发送到channel 中,然后又监听这个 channel 的实体处理,两个实体之间是匿名的,这个就实现实体中间的解耦,其中 channel 是同步的一个消息被发送到 channel 中,最终是一定要被另外的实体消费掉的,在实现原理上其实是一个阻塞的消息队列。 Goroutinegoroutine 是实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,类似于 greenthread,go底层选择使用coroutine的出发点是因为,它具有以下特点: 用户空间 避免了内核态和用户态的切换导致的成本 可以由语言和框架层进行调度 更小的栈空间允许创建大量的实例 可以看到第二条 用户空间线程的调度不是由操作系统来完成的,像在java 1.3中使用的greenthread的是由JVM统一调度的(后java已经改为内核线程),还有在ruby中的fiber(半协程) 是需要在重新中自己进行调度的,而goroutine是在golang层面提供了调度器,并且对网络IO库进行了封装,屏蔽了复杂的细节,对外提供统一的语法关键字支持,简化了并发程序编写的成本。 Goroutine 调度器上节已经说了,golang使用goroutine做为最小的执行单位,但是这个执行单位还是在用户空间,实际上最后被处理器执行的还是内核中的线程,用户线程和内核线程的调度方法有: N:1 多个用户线程对应一个内核线程 1:1 一个用户线程对应一个内核线程 M:N 用户线程和内核线程是多对多的对应关系 golang 通过为goroutine提供语言层面的调度器,来实现了高效率的M:N线程对应关系。 图中 M:是内核线程 P : 是调度协调,用于协调M和G的执行,内核线程只有拿到了 P才能对goroutine继续调度执行,一般都是通过限定P的个数来控制golang的并发度 G : 是待执行的goroutine,包含这个goroutine的栈空间 Gn : 灰色背景的Gn 是已经挂起的goroutine,它们被添加到了执行队列中,然后需要等待网络IO的goroutine,当P通过 epoll查询到特定的fd的时候,会重新调度起对应的,正在挂起的goroutine。 Golang为了调度的公平性,在调度器加入了steal working 算法 ,在一个P自己的执行队列,处理完之后,它会先到全局的执行队列中偷G进行处理,如果没有的话,再会到其他P的执行队列中抢G来进行处理。 总结Golang实现了 CSP 并发模型做为并发基础,底层使用goroutine做为并发实体,goroutine非常轻量级可以创建几十万个实体。实体间通过 channel 继续匿名消息传递使之解耦,在语言层面实现了自动调度,这样屏蔽了很多内部细节,对外提供简单的语法关键字,大大简化了并发编程的思维转换和管理线程的复杂性。]]></content>
<categories>
<category>Go</category>
<category>并发编程</category>
<category>转载</category>
</categories>
<tags>
<tag>csp</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Go Channel原理--阻塞式消息队列]]></title>
<url>%2F2017%2F07%2F14%2Fgolang_channel_the_blocked_mq%2F</url>
<content type="text"><![CDATA[channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或 多个goroutine之间传递消息。 channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声 明channel时指定。可以以将其认为是一种类 型安全的管道(pipe)。 阻塞与非阻塞channel内部实现的其实是一个阻塞式消息队列,每当有一个goroutine从channel中读取数据时,如果channel buffer中没有数据,那么读取操作会一直阻塞。同理,往channel中发送数据时,如果channel buffer是满的,那么发送操作也会阻塞。 1234567c1 := make(chan int)val := <-c1 //blockc2 := make(chan int, 2)c2 <- 0c2 <- 1c2 <- 3 //block 线程安全在多线程并发编程中,多个goroutine同时读写同一个channel,channel采用加锁来避免race,从而实现了线程安全。 1234567run := func(c <-chan int) { c <- 0}c := make(chan int)go run(c) //thread safego run(c) //thread safe channel在Go内部其实是下面的hchan类型,该结构体中,通过一个lock的锁对象来保护读写其他字段。 1234567891011121314151617181920type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters // lock protects all fields in hchan, as well as several // fields in sudogs blocked on this channel. // // Do not change another G's status while holding this lock // (in particular, do not ready a G), as this can deadlock // with stack shrinking. lock mutex} 异常读写给一个nil-channel发送数据,将永远阻塞1234func writeNilChannel() { var c chan int c <- 1 //fatal error: all goroutines are asleep - deadlock!} 从一个nil-channel读取数据,将永远阻塞12345func readNilChannel() { var c chan int val := <-c //fatal error: all goroutines are asleep - deadlock! fmt.Println(val)} 出现上面两种情况的原因是: channel 的 buffer 的大小不是类型声明的一部分,因此它必须是 channel 的值的一部分 如果 channel 未被初始化,它的 buffer 的大小将是0,也就是没有buffer 如果 channel 没有 buffer,一个发送将会被阻塞,直到另外一个 goroutine 为接收做好了准备 如果 channel 没有 buffer,一个接收将会被阻塞,直到另外一个 goroutine 发送了数据 给一个closed-channel发送数据,引起panic12345func writeClosedChannel() { c := make(chan int) close(c) c <- 1 //panic: send on closed channel} 从一个closed-channel读取数据,立即返回数据类型的默认值12345678910111213func readClosedChannel() { c4Int := make(chan int) close(c4Int) vi := <-c4Int //0 c4Str := make(chan string) close(c4Str) vs := <-c4Str //"" c4Ptr := make(chan *int) close(c4Ptr) vp := <-c4Ptr //nil} 有效判断channel是否closed123456789//for rangefor v := range c { //do something with v}//or ifif v, ok := <-c; ok { //do something with v}]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>channel</tag>
</tags>
</entry>
<entry>
<title><![CDATA[lock-free]]></title>
<url>%2F2017%2F07%2F13%2Flock-free%2F</url>
<content type="text"><![CDATA[所谓lock-free,也即锁无关,通俗的解释如下 一个“锁无关”的程序能够确保执行它的所有线程中至少有一个能够继续往下执行。” 多线程并发编程中,如果多个线程之间存在共享内存,为了保证线程安全,每个线程存取共享内存的时候都要显式加锁。当有一个线程取得了锁,它就拿到了共享内存的存取权限,其他没有取到锁的线程需要原地阻塞,直到占有锁的线程主动释放锁,同时自己拿到相应的锁。 考虑一种情况,假设拿到锁的线程突然挂了,而且没有释放锁,这种情况下整个程序都会阻塞在这里。而lock-free正是可以避免这种情况,在lock-free下,即使有线程挂掉,也不影响整个程序的继续往下运行,也就是说,系统整体是一直在前进的。 上面说了,显式加锁保证线程安全的同时会出现非lock-free的情况,但是不加锁也会导致着中情况,例如下面的代码: 123while (x == 0) { x = 1 - x;} 如果两个线程同时执行进入while,然后x两次改变值,还是为0,这样的话两个线程都会一直阻塞在while,系统整体无法前进,依然不是lock-free。 关于lock-free的讨论,Andrei Alexandrescu曾在他的专栏Generic写过一片文章,《Lock-Free Data Structures》,详细讲解了lock-free技术,以下是中文译文。 在Generic沉默了一期之后(研究生的学业总是使人不得不投入百分之百的精力),这一期文章的可写内容突然多得令人似乎有点无所适从.例如,其中之一就是关于构造函数的讨论,特别是转发构造函数(forwarding constructor),(构造函数中的)异常处理,以及两段式(two-stage)对象构造.另一个可选主题是创建不完全类型的容器(例如list、vector或map)(顺便再回顾一下Yanslander技术[2]),这一任务可以借助于一些有趣的技巧来完成(当下的标准库容器并不能保证可存放不完全类型的对象)。 虽说以上两个候选主题都挺诱人,不过跟所谓的“锁无关(Lock-Free)”数据结构一比就只能靠边站了,后者是多线程编程的极重要技术之一。在今年的“Programming Language Design and Implementation”大会(http://www.cs.umd.edu/~pugh/pldi04/)上,Michael Maged展示了世界上第一个锁无关的(lock-free)内存分配器[7],跟那些小心翼翼地设计的、基于锁(lock-based)的,同时也更为复杂的内存分配器相比,Michael的锁无关的内存分配器在诸多测试下都表现得更为优越。 Michael的锁无关内存分配器代表着近来出现的许多锁无关数据结构和算法的最新进展。 什么是“锁无关(lock-free)”?不久前这个问题正是我想要问的。作为一个真正的主流多线程程序员,我对基于锁的多线程算法并不感到陌生。在经典的基于锁的多线程程序设计中,只要你需要共享某些数据,你就应当将对它的访问串行化。修改(共享)数据的操作必须以原子操作的形式出现,这样才能保证没有其它线程能在中途插一脚来破坏相应数据的不变式(invariant)。即便像++count(count是整型变量)这样的简单操作也得加锁,因为增量操作实际上是分三步进行的:读、改、写(回),而这显然不是原子的。 简而言之,在基于锁的多线程编程中,你得确保任何针对共享数据的、且有可能导致竞争条件(race conditions)的操作都被做成了原子操作(通过对一个互斥体(mutex)进行加锁解锁)。从好的一面来说,只要互斥体是在锁状态,你就可以放心地进行任何操作,不用担心其它线程会闯进来搞坏你的共享数据。 然而,正是这种在互斥体的锁状态下可以为所欲为的事实同样也带来了问题。例如,你可以在锁期间读键盘或进行某些耗时较长的I/O操作,这便意味着其它想要占用你正占用着的互斥体的线程只能被晾在一旁等着。更糟的是,此时你可能会试图对另一个互斥体进行加锁,后者彼时或许已经被另一个线程占用了,而这个线程倒过来又试图去获取已然被你的线程所占用的互斥体,如此一来两个线程全都陷在那里动弹不得,死锁! 而在锁无关多线程编程的世界里,几乎任何操作都是无法原子地完成的。只有很小一集操作可以被原子地进行,这一限制使得锁无关编程的难度大大地增加了。(实际上,世界上肯定有不少锁无关编程专家,而我则不在此列。不过幸运的是,这篇文章中所提供的基本工具、内容以及热情可能会激发你成为一个这方面的专家。)锁无关编程带来的好处是在线程进展和线程交互方面,借助于锁无关编程,你能够对线程的进展和交互提供更好的保证。但话说回来,刚刚我们所谓的“很小一集可以被原子地进行的操作”又是指哪些操作呢?实际上,这个问题相当于,最少需要一集什么样的原语才能够允许我们实现出任何锁无关的算法(如果存在这么一个“最小集”的话)? 如果你认为这个问题的基础性使得它的解决者理当获得奖赏的话,你并不是唯一一个这么认为的。2003年,Maurice Herlihy因他在1991年发表的开创性论文“Wait-Free Synchronization”( http://www.podc.org/dijkstra/2003.html)而获得了分布式编程的Edsger W. Dijkstra奖。在论文中,Herlihy证明了哪些原语对于构造锁无关数据结构来说是好的,哪些则是不好的。他的论述使得一些看似漂亮的硬件架构突然变得一钱不值,同时他的论文还指明了在将来的硬件中应当实现哪些同步原语。 例如,Herlihy的论文指出,像test-and-set、swap、fetch-and-add、甚至原子队列这样的看似强大的工具都不足以良好地同步两个以上的线程(这一结果很是令人惊讶,因为带有原子push和pop操作的队列看上去提供了一个相当强大的抽象)。从好的一面来说,Herlihy同样给出了一般性结论,他证明了一些简单的结构就足以实现出任何针对任意数目的线程的锁无关算法。 最简单、也是最普遍的一个通用原语就是CAS操作(Compare-And-Swap),这也是我一直使用的原语: 123456789template <class T>bool CAS(T* addr, T expected, T value) { if (*addr == expected) { *addr = value; return true; } return false;} CAS原语负责比较某个内存地址处的内容与一个期望值,如果比较成功则将该内存地址处的内容替换为一个新值。这整个操作是原子的。许多现代处理器都实现了不同位长度的CAS或其等价原语(这里我用模板来表达它正是由于这个原因,我们可以假定一个具体的实现使用元编程来限制可能的T)。作为一个原则,CAS能够操作的位数越多,使用它来实现锁无关数据结构就越容易。如今的32位处理器实现了64位的CAS,例如Intel汇编指令中称它为CMPXCHG8(你得跟这些汇编助记符多亲近亲近)。 一点告诫通常一篇C++文章中会伴随着C++代码片断和示例。理想情况下这些代码会是符合标准C++规范的,而且Generic专栏的确尽量做到了这一点。 然而,当文章涉及的内容是多线程编程时,给出符合标准C++的示例代码就是不可能的了,因为标准C++中根本就没有线程的概念,而你无法编码不存在的东西。因此,这篇文章中的代码某种意义上只不过是“伪码”,而并非可移植的标准C++代码。例如内存屏障(memory barriers),对于本文中描述的算法来说,真正的实现代码要么是汇编写的,要么得在C++代码中到处插入所谓的“内存屏障”(“内存屏障”是一种处理器相关的机制,它能够强制内存读写的顺序性)。为了不失讨论重点,这里我并不打算对内存屏障多作介绍,有兴趣的读者可以参考Butenhof的著作[3],或者在下面的注中提到的一篇关于它的简单介绍[6]。为了方便讨论,这里我假定我们的编译器和硬件都没有进行过分的优化(例如消除某些“冗余”的变量读取,这在单线程环境下的确是个有效的优化)。从技术上来讲,这即是说一个“顺序一致”的模型,其中读和写操作的执行顺序跟它们在源代码中的顺序是完全一样的[8]。 等待无关(Wait-Free)/锁无关(Lock-Free)与基于锁(Lock-Based)的比较一个“等待无关”的程序可以在有限步之内结束,而不管其它线程的相对速度如何。 一个“锁无关”的程序能够确保执行它的所有线程中至少有一个能够继续往下执行。这便意味着有些线程可能会被任意地延迟,然而在每一步都至少有一个线程能够往下执行。因此这个系统作为一个整体总是在“前进”的,尽管有些线程的进度可能不如其它线程来得快。而基于锁的程序则无法提供上述的任何保证。一旦任何线程持有了某个互斥体并处于等待状态,那么其它任何想要获取同一互斥体的线程就只好站着干瞪眼;一般来说,基于锁的算法无法摆脱“死锁”或“活锁”的阴影,前者如两个线程互相等待另一个所占有的互斥体,后者如两个线程都试图去避开另一个线程的锁行为,就像两个在狭窄走廊里面撞了个照面的家伙,都试图去给对方让路,结果像跳交谊舞似的摆来摆去最终还是面对面走不过去。当然,对于我们人来说,遇到这种情况打个哈哈就行了,但处理器却不这么认为,它会不知疲倦地就这样干下去直到你强制重启。 等待无关和锁无关算法的定义意味着它们有更多的优点: 线程中止免疫:杀掉系统中的任何线程都不会导致其它线程被延迟。 信号免疫:C和C++标准禁止在信号或异步中断中调用某些系统例程(如malloc)。如果中断与某个被中断线程同时调用malloc的话,结果就会导致死锁。而锁无关例程则没有这一问题:线程可以自由地互相穿插执行。 优先级倒置免疫:所谓“优先级倒置”就是指一个低优先级线程持有了一个高优先级线程所需要的互斥体。这种微妙的冲突必须由OS内核来解决。而等待无关和锁无关算法则对此免疫。 一个锁无关的WRRM Map写专栏文章的好处之一就是你可以定义自己的缩写,所以就让我们来定义WRRM(Write Rarely Read Many)这一缩写,一个WRRM Map是一个被读比被写的次数多得多的map。例如,对象工厂[1],观察者模式的许多具体实例[5],以及一个将币种跟汇率联系在一起的map(许多时候你只是对它进行读取,而只有很少的时候才会更新),当然还有许许多多其它的查找表。 WRRM Map可以通过std::map或“准标准”的unordered_map(http://www.open-std.org/jtcl/sc22/wg21/docs/papers/2004/n1647.pdf)来予以实现,不过正如我在Modern C++ Design中所说的,assoc_vector(一个已序的vector)也是一个不错的候选,因为它用更新速度换取了查找速度。总而言之,锁无关性质是与具体的数据结构选择无关(正交)的;我们姑且就称后者为Map。同样,出于这篇文章的意图,当我说map时就是指那些提供了根据key来查找并能够更新key-value对的表,下文将不再重申这一点。 让我们来回顾一下一个基于锁的WRRMMap是怎样实现的:我们将一个Map对象与一个Mutex对象结合起来,像这样: 12345678910111213141516// WRRMMap的一个基于锁的实现template <class K, class V>class WRRMMap { Mutex mtx_; Map<K, V> map_;public: V Lookup(const K& k) { Lock lock(mtx_); return map_[k]; } void Update(const K& k, const V& v) { Lock lock(mtx_); map_[k] = v; }}; 为了避免所有权问题以及无效引用问题(这两个问题后面会给我们带来大麻烦),Lookup按值返回其结果。这一做法虽说稳妥但有代价。每次查找都会导致对Mutex加锁/解锁,而实际上一是平行的查找并不需要相互加锁,二是Update比Lookup发生的频率要小得多。那么,现在就让我们来实现一个更好的WRRMMap吧。 垃圾收集,你在何方?对实现一个锁无关的WRRMMap的第一番尝试停在下面这几个问题上: 读取操作(Read)没有任何锁。 更新操作(Update)对整个map作一份拷贝,然后更新这份拷贝,最后再用这份拷贝来替换掉原来的旧map(通过CAS原语来进行)。如果最后一步的CAS操作没有成功的话,就循环尝试“拷贝/更新/CAS回去”这一步骤直到成功。 由于CAS原语所能操作的字节数是有限制的,所以WRRMMap存放指向实际Map的指针,而不是一个Map。 12345678910111213141516171819202122// WRRMMap的首个锁无关的实现// 只有当你有GC的时候才行template <class K, class V>class WRRMMap { Map<K, V>* pMap_;public: V Lookup (const K& k) { // 瞧,没有锁! return (*pMap_) [k]; } void Update(const K& k, const V& v) { Map<K, V>* pNew = 0; do { Map<K, V>* pOld = pMap_; delete pNew; pNew = new Map<K, V>(*pOld); (*pNew) [k] = v; } while (!CAS(&pMap_, pOld, pNew)); // 别 delete pMap_; }}; 这行得通!在一个循环当中,Update()例程对整个map作了一份拷贝,并往该副本中加入了一个新项,最后再尝试交换新旧两个map的指针。这里,最后一步,一定要使用CAS原语而不是简单的赋值,这很关键,否则像下面这样的一个执行序列将会破坏该map: 线程A对该map作了一份副本 线程B对该map作了一份副本并更新了副本 线程A往其副本中加入新项 线程A用其改写后的副本替换原有map,这里,线程A的改写后的副本中没有线程B所作的任何改动。 而通过CAS,一切都能够优雅地完成,因为每个线程都相当于作了如下的陈述:“假设该map自从我上次观察它以来并没有任何更动,那么就进行CAS(写回改动后的新map)。否则一切从头来过。” 根据定义,这就使得Update成了一个锁无关的算法,但它并不是等待无关的。例如,如果若干线程同时调用Update,那么它们每一个都可能会循环尝试不确定的次数,然而总会有某个线程能够确保成功更新该map,因而整体上的进度总是在继续的。另外,幸运的是,Lookup是等待无关的。 在一个拥有垃圾收集的环境中,我们的工作就算完成了,而这篇文章也将在一片欢欣鼓舞中结束。然而,事实是我们并没有垃圾收集,因而只得面对麻烦了。麻烦就是,我们不能简单地就将旧的pMap扔掉(意即delete——译注),如果正当你试图delete它的时候一帮其它线程冲上来想要访问它里面的数据(Lookup)该怎么办呢?你是知道的,垃圾收集器可以访问所有线程的数据及私有栈,所以它对于“哪些pMap所指的map不再被使用了”这个问题有更清晰的认识,而且它也能够优雅地将这些不再被使用的map清理掉。而如果没有垃圾收集器的帮助,事情可就没那么简单了。实际上,是难得多!而且看起来确定性的内存归还在锁无关数据结构方面是个基础问题。 写锁定(Write-Locked)的WRRM Map为了弄清我们遇到的问题有多棘手,让我们先来试试用经典的引用计数技术来实现WRRMMap,看看这有何不可。那么,考虑将一个引用计数器跟map的指针绑定在一起,并让WRRMMap保存一个指向如此构造出的数据结构的指针。 123456template <class K, class V>class WRRMMap { typedef std::pair<Map<K, V>*, unsigned> Data; Data* pData_; ...}; 嗯,看上去不赖。现在,Lookup会先递增pData->second,然后在map中进行查找,最后再递减pData->second。当pData->second计数器的值下降到0的时候,pData->first就可以被delete掉了,接着pData自己也就可以被delete掉了。看起来简单稳妥是不是,只不过…只不过它是幼稚的。试想下面这种情况,正当某个线程注意到引用计数器的值下降到0并着手delete pData时,另一个线程或另一堆线程插进来拽住了垂死的pData_并准备从它进行读取!不管你怎么聪明地安排你的策略,你都逃不开一个基本的问题:即要想读取数据的指针,你得先递增引用计数,但引用计数又必须得是你要读取的数据的一部分,因此倘不先读取那个指针你就无法访问到该引用计数。这就好比一个将开关安放在其顶端的带电铁丝网:要想安全的爬上去你就得先将开关关掉,但要想将开关关掉你就得先安全地爬上去啊! 那么,就让我们来看看有没有其它方法能够正确地delete旧的map。一个方案是等待然后delete。考虑到随着处理器时间(毫秒计)的推移,旧的pMap对象会被越来越少的线程所访问(这是因为新的访问是对新的map进行的),于是一旦那些在CAS操作之前开始的Lookup操作结束了,对应的(旧)pMap也就可以去见阎王了。因此,我们的一个解决方案就是将旧的pMap推入一个队列,并由一个专门的清理线程,每隔,比如说200毫秒,醒来一次,delete掉队列中待得最久的一个pMap。 但这并非一个理论上安全的方案(尽管从实际上来说它可能绝大多数情况下都不会出什么意外)。比如说,由于某种原因一个进行Lookup的线程被延迟了,那么清理线程就会在该线程的眼皮子底下delete掉它正在访问的map。这可以通过总是给清理线程赋予一个比任何线程低的优先级来予以避免,然而总的来说,这个方案骨子里的劣根性是难以清除的。如果你也同意对于这样一个技术我们没法为其作任何正儿八经的辩护的话,我们就继续往下。 其它的方案[4]则依赖于一个扩展的DCAS原子操作,该操作能够compare-and-swap内存中的两个不必连续的字: 1234567891011template <class T1, class T2>bool DCAS(T1* p1, T2* p2, T1 e1, T2 e2, T1 v1, T2 v2) { if (*p1 == e1 && *p2 == e2) { *p1 = v1; *p2 = v2; return true; } return false;} 这里所说的两个字当然是指map的指针以及引用计数器。DCAS已经由摩托罗拉68040处理器(非常低效地)实现了,然而其它处理器并没有实现它。因此,基于DCAS的解决方案主要是被当成理想方案来考虑的。 那么,在确定性析构的大背景下,我们对于该问题的尝试首先是求助于不像DCAS那么要求苛刻的CAS2操作。再次说明,许多32位的机器都实现了64位的CAS操作,被称为CAS2(CAS2仅操作连续的字,所以它的能力显然不及DCAS强大)。作为开始,让我们将引用计数跟它所“保卫”的map指针紧挨着存放在内存中: 123456template <class K, class V>class WRRMMap { typedef std::pair<Map<K, V>*, unsigned> Data; Data data_; ...}; (注意,这一次,引用计数与它所保护的指针紧挨在一起了,这就消除了我们前面提到的“带电铁丝网-开关”的尴尬问题。待会你就会看到这样做的代价。) 那么,让我们修改Lookup从而使其能够在访问map之前先递增引用计数,并在访问结束之后将其递减回去。在下面的代码片断中,为了简洁起见,我并没有考虑异常安全问题(这个问题可以用标准技术来解决)。 12345678910111213141516171819V Lookup(const K& k) { Data old; Data fresh; do { old = data_; fresh = old; ++fresh.second; } while (CAS(&data_, old, fresh)); V temp = (*fresh.first)[k]; do { old = data_; fresh = old; --fresh.second; } while (CAS(&data_, old, fresh)); return temp;} 最后,Update负责将该map替换为一个新的——仅当其引用计数为1的时候。 12345678910111213141516171819202122void Update(const K& k, const V& v) { Data old; Data fresh; old.second = 1; fresh.first = 0; fresh.second = 1; Map<K, V>* last = 0; do { old.first = data_.first; if (last != old.first) { delete fresh.first; fresh.first = new Map<K, V>(old.first); fresh.first->insert(make_pair(k, v)); last = old.first; } } while (!CAS(&data_, old, fresh)); delete old.first; // whew} Update是这样工作的:首先它定义old和fresh两个变量(我们现在应该非常熟悉这两个变量了)。只不过这次old.second并非由data.second赋值而来,而是始终为1。这意味着,Update会一直循环直到它等到data.first指针的引用计数变成1(此时没有任何线程在读),并将其替换为另一个引用计数为1的新map的指针。说白了这相当于如下的陈述:“我将会把旧的map替换成一个更新过的,而且我会留神其它对于该map的更新,不过,仅当现有map的引用计数为1的时候我才会进行替换。”变量last及其相关代码只不过是个优化技巧:当旧map并没有被改动,而只是其引用技术被改动的时候,last可以帮助避免我们重建同一个map。 挺漂亮的手法,对不对?只可惜事实并非如此。Update现在实际上是基于锁的:它得等待所有Lookup结束之后才能去更新该map。而锁无关数据结构的好处就在于一切都能够如行云流水般进行。特别地,在这种实现下,Update很容易就可以被饿死:你只需足够频繁地进行Lookup就行了,让它的引用计数永不降为1。总而言之,到目前为止我们得到的并非一个WRRM Map,而是一个WRRMBNTM(Write-Rarely-Read-Many-But-Not-Too-Many) Map。 结论锁无关数据结构是大有前途的技术。它在线程中止、优先级倒置以及信号安全等方面都有着良好的表现。它们永远不会死锁或活锁。在测试当中,最近的锁无关数据结构远远超越了它们的基于锁的版本[9]。然而,锁无关编程的技巧性要求较高,特别是当涉及到内存释放的时候。一个垃圾收集环境对此是大有裨益的,因为它能够暂停并检视所有线程。不过,如果你想要确定性析构的话,你就需要来自硬件或内存分配器的特殊支持了。在下期的Generic专栏中,我们将会考察优化WRRMMap的途径,使其在确定性析构的基础上实现锁无关。 如果本期的垃圾收集map和WRRMBNTM Map并没能满足你的胃口的话,下面是个省钱小诀窍:Don’t Go watch the movie Alien vs. Predator, unless you like “so bad it’s funny” movies.(译注:one of the crap jokes I do not get)。 Acknowlegments David B. Held, Michael Maged, Larry Evans, and Scott Meyers provided very helpful feedback. Also, Michael graciously agreed to coauthor the next “Generic“ installment, which will greatly improve on our WRRMap implementation. References [1] Alexandrescu, Andrei. Modern C++ Design, Addison-Wesley Longman, 2001. [2] Alexandrescu, Andrei. “Generic:yasli::vector Is On the Move,” C/C++ Users Journal, June 2004. [3] Butenhof, D.R. Programming with POSIX Threads, Addison-Wesley, 1997. [4] Detlefs, David L., Paul A. Martin, Mark Moir, and Guy L. Steele, Jr. “Lock-free Reference Counting,” Proceedings of the Twentieth Annual ACM Symposium on Principles of Distributed Computing, pages 190-199, ACM Press, 2001. ISBN 1-58113-383-9. [5] Gamma, Erich, Richard Helm, Ralph E. Johnson, and John Vlissides. Design Patterns: Elements of Resusable Object-Oriented Software, Addison-Wesley, 1995. [6] Meyers, Scott and Andrei Alexandrescu. “The Perils of Double-Checked Locking.” Dr. Dobb’s Journal, July 2004. [7] Maged, Michael M. “Scalable Lock-free Dynamic Memory Allocation,” Proceedings of the ACM SIGPLAN 2004 Conference on Programming Language Design and Implementation, pages 35-46. ACM Press, 2004. ISBN 1-58113-807-5. [8] Robison, Arch. “Memory Consistency & .NET,” Dr. Dobb’s Journal, April 2003. [9] Maged, Michael M. “CAS-Based Lock-Free Algorithm for Shared Deques,” The Ninth Euro-Par Conference on Parallel Processing, LNCS volume 2790, pages 651-660, August 2003.]]></content>
<categories>
<category>并发编程</category>
</categories>
<tags>
<tag>lock-free</tag>
</tags>
</entry>
<entry>
<title><![CDATA[OAuth2.0协议详解]]></title>
<url>%2F2017%2F07%2F11%2Foauth2dot0_proto%2F</url>
<content type="text"><![CDATA[1 OAuth2.0 工作流程 (A)用户打开客户端以后,客户端要求用户给予授权。 (B)用户同意给予客户端授权。 (C)客户端使用上一步获得的授权,向认证服务器申请令牌。 (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。 (E)客户端使用令牌,向资源服务器申请获取资源。 (F)资源服务器确认令牌无误,同意向客户端开放资源。 2 客户端的授权模式客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。 授权码模式(authorization code) 简化模式(implicit) 密码模式(resource owner password credentials) 客户端模式(client credentials) 2.1 授权码模式 授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。 (A)用户访问客户端,后者将前者导向认证服务器。 (B)用户选择是否给予客户端授权。 (C)假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。 (D)客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。 (E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。 2.2 简化模式 简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了”授权码”这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。 (A)客户端将用户导向认证服务器。 (B)用户决定是否给于客户端授权。 (C)假设用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。 (D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。 (E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。 (F)浏览器执行上一步获得的脚本,提取出令牌。 (G)浏览器将令牌发给客户端。 2.3 密码模式 密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向”服务商提供商”索要授权。 在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。 (A)用户向客户端提供用户名和密码。 (B)客户端将用户名和密码发给认证服务器,向后者请求令牌。 (C)认证服务器确认无误后,向客户端提供访问令牌。 2.4 客户端模式 客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向”服务提供商”进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求”服务提供商”提供服务,其实不存在授权问题。 (A)客户端向认证服务器进行身份认证,并要求一个访问令牌。 (B)认证服务器确认无误后,向客户端提供访问令牌。]]></content>
<categories>
<category>协议</category>
</categories>
<tags>
<tag>oauth</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Go类型转换]]></title>
<url>%2F2017%2F07%2F11%2Fgolang_type_conversion%2F</url>
<content type="text"><![CDATA[golang中string与其他类型的转换 1. 转换方式123456789101112131415161718192021222324252627func main() { var i32Val int = 1 var i64Val int64 = 1 var f32Val float32 = 3.14 var f64Val float64 = 3.14 var strI32Val string = "1" var strI64Val string = "1" var strF32Val string = "3.14" var strF64Val string = "3.14" //int转string fmt.Println(strconv.Itoa(i32Val)) //int64转string fmt.Println(strconv.FormatInt(i64Val, 10)) //第二个参数为进制 //float32转string fmt.Println(strconv.FormatFloat(float64(f32Val), 'f', 6, 32)) //详见strconv包 //float64转string fmt.Println(strconv.FormatFloat(f64Val, 'f', 6, 64)) //string转int fmt.Println(strconv.Atoi(strI32Val)) //string转int64 fmt.Println(strconv.ParseInt(strI64Val, 10, 64)) //string转float32 fmt.Println(strconv.ParseFloat(strF32Val, 32)) fmt.Println(strconv.ParseFloat(strF64Val, 64))} 2. strconv包详解strconv包主要有两组重要函数:strconv.ParseXXX 和strconv.FormatXXX 2.1 strconv.FormatXXX函数(XXX转string) strconv.FormatInt函数 函数原型:func FormatInt(i int64, base int) string i:需要转换的数字 base:进制[2, 36] strconv.FormatInt 函数原型:func FormatFloat(f float64, fmt byte, prec, bitSize int) string f:需要转换的数字 byte:格式标志 ‘b’:-ddddp±ddd,二进制指数 ‘e’:-d.dddde±dd,十进制指数 ‘E’:-d.ddddE±dd,十进制指数 ‘f’:-ddd.dddd,没有指数 prec:精度 bitSize:32或者64 2.2 strconv.ParseXXX函数(string转XXX) strconv.FormatInt 函数原型:func ParseInt(s string, base int, bitSize int) (i int64, err error) s:需要转换的string base:进制 bitSize:32或者64 err:err.Err = ErrSyntax或者err.Err = ErrRange strconv.FormatFloat 函数原型:func ParseFloat(s string, bitSize int) (float64, error) s:需要转换的string bitSize:32或者64 err:err.Err = ErrSyntax或者err.Err = ErrRange ]]></content>
<categories>
<category>golang</category>
</categories>
<tags>
<tag>类型转换</tag>
</tags>
</entry>
<entry>
<title><![CDATA[gRPC安全认证]]></title>
<url>%2F2017%2F07%2F11%2Fgrpc_security_authentication%2F</url>
<content type="text"><![CDATA[1 认证gRPC支持以下认证方式 SSL/TLS认证 OAuth 2.0认证 自定义拓展认证 2 SSL/TLS认证2.1 准备证书 制作私钥 123456# Key considerations for algorithm "RSA" ≥ 2048-bitopenssl genrsa -out server.key 2048 # Key considerations for algorithm "ECDSA" ≥ secp384r1# List ECDSA the supported curves (openssl ecparam -list_curves)openssl ecparam -genkey -name secp384r1 -out server.key 制作公钥 123456789openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650Country Name (2 letter code) [AU]:CNState or Province Name (full name) [Some-State]:BeijingLocality Name (eg, city) []:BeijingOrganization Name (eg, company) [Internet Widgits Pty Ltd]:nilOrganizational Unit Name (eg, section) []:nilCommon Name (e.g. server FQDN or YOUR name) []:HanEmail Address []:nil 2.2 服务端TLS认证123456lis, err := net.Listen("tcp", "127.0.0.1:12345")//TLS认证creds, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key")grpcServer := grpc.NewServer(grpc.Creds(creds)) 2.3 客户端TLS认证123creds, err := credentials.NewClientTLSFromFile("../keys/server.pem", "Han") //Common Nameconn, err := grpc.Dial("127.0.0.1:12345", grpc.WithTransportCredentials(creds)) 3 自定义拓展认证除了TLS认证之外,gRPC还提供了自定义的认证方式,即Token认证。 3.1 Token认证步骤①客户端 定义一个customCredential结构,实现下面两个接口 12func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)func (c customCredential) RequireTransportSecurity() bool client设置rpc连接选项 12var opts []grpc.DialOption := grpc.WithPerRPCCredentials(new(customCredential))conn, err := grpc.Dial("127.0.0.1:12345", opts...) 通过上述两个步骤,客户端在调用服务端接口的时候,就会将Token信息通过请求的metadata发送到服务端(GetRequestMetadata中设置),服务端在请求的metadata中取出Token数据就可以进行校验了。 ②服务端 服务端调用在处理逻辑之前先从请求的metadata中取出Token数据进行校验。其中md是map[string][]string类型,带有Token信息。 1md, ok := metadata.FromContext(ctx) 3.2 客户端Token认证123456789101112131415161718192021222324252627//实现gRPC自定义认证接口type customCredential struct{}func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "appid": "12345", "appkey": "owqp3Zrxtbdt5kVe", }, nil}func (c customCredential) RequireTransportSecurity() bool { return IsOpenTLS}//设置gRPC选项if IsOpenTLS { creds, err := credentials.NewClientTLSFromFile("../keys/server.pem", "Han") //Common Name if err != nil { log.Fatalf("Failed to generate credentials: %v", err) } opts = append(opts, grpc.WithTransportCredentials(creds))} else { opts = append(opts, grpc.WithInsecure())}opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))conn, err := grpc.Dial("127.0.0.1:12345", opts...) 3.3 服务端校验Token12345678910111213141516func (s helloServer) SayHello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloReply, error) { //校验token md, ok := metadata.FromContext(ctx) if !ok { return nil, grpc.Errorf(codes.Unauthenticated, "no token") } if md["appid"][0] != "12345" || md["appkey"][0] != "Xr2nveGW7RL0DTCl" { return nil, grpc.Errorf(codes.Unauthenticated, "Failed to check token!, appid = %q, appkey = %q", md["appid"][0], md["appkey"][0]) } reply := &pb.HelloReply{ Message: "Hello " + request.GetName(), } return reply, nil} 4 gRPC拦截器gRPC中,服务端接收到请求,可以通过使用拦截器interceptor优先对请求数据进行一些处理,再转交给指定的服务,比较适合处理验证,日志等流程。 使用拦截器时,服务端先声明一个interceptor结构来实现具体的处理逻辑: 123456789var interceptor grpc.UnaryServerInterceptorinterceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { err = auth(ctx) //具体处理逻辑 if err != nil { return nil, err } return handler(ctx, req)} 然后调用grpc.UnaryInterceptor来注册该拦截器 123opts = append(opts, grpc.UnaryInterceptor(interceptor))grpcServer := grpc.NewServer(opts...)]]></content>
<categories>
<category>gRPC</category>
</categories>
<tags>
<tag>gRPC</tag>
<tag>安全认证</tag>
</tags>
</entry>
<entry>
<title><![CDATA[gRPC框架基础]]></title>
<url>%2F2017%2F07%2F11%2Fgrpc_basis%2F</url>
<content type="text"><![CDATA[1 概述GRPC是google开源的一个高性能、跨语言的RPC框架,基于HTTP2协议和protobuf 3。开发步骤分为四步: 定义proto接口文件 使用protoc工具生成指定语言代码 启动Server端 启动多个Client端 gRPC资源: 官方文档 GO-gRPC 2 gRPC protogRPC使用protobuf来声明数据模型和RPC接口服务,按照的不同,可以分为四种类型的RPC: 简单RPC:这是最简单的RPC类型,服务端从客户端拿到一个数据对象,然后返回另一个数据对象。 服务端流式RPC:服务端从一个stream来写多个响应,客户端通过这个stream来读取数据。 客户端流式:服务端从客户端获取一个stream,然后通过它来读多个数据,并可以通过它来写单个响应。 双向流式RPC:服务端从客户端获取一个stream,然后通过它来读多个数据,并可以通过它来写多个响应。 具体形式如下: 1234567891011121314//声明服务service Service { //简单RPC rpc NormalRPC(RequestArgs) returns (ReturnArgs) {} //服务端流式RPC rpc ServerStreamRPC(RequestAgrs) returns (stream ReturnStream) {} //客户端流式RPC rpc ClientStreamRPC(stream RequestStream) returns (ReturnArgs) {} //双向流式RPC rpc TwoWayStreamRPC(stream TWStream) returns (TWStream) {}} 3 四种gRPC类型3.1 简单RPC12345func (s *ServiceServer) NormalRPC(ctx context.Context, reqAgs *RequestArgs) (*ReturnAgrs, error) { retAgrs := &ReturnArgs{} //... return retAgrs, nil} 3.2 服务端流式RPC12345678910111213141516171819202122232425//proto 生成type ServiceServer_ServerStreamRPCServer interface { Send(*ReturnStream) error grpc.ServerStream}type serviceServerServerStreamRPCServer struct { grpc.ServerStream}//实现interfacefunc (x *serviceServerServerStreamRPCServer) Send(m *ReturnStream) error { return x.ServerStream.SendMsg(m)}//实现业务逻辑func (s *ServiceServer) ServerStreamRPC(reqArgs *RequestArgs, stream pb.serviceServerServerStreamRPCServer) error { for { retStream := &ReturnStream{} //... if err := stream.Send(retStream); err != nil { return err } } return nil} 3.3 客户端流式RPC1234567891011121314151617181920212223242526272829303132333435//proto生成type ServiceServer_ClientStreamRPCServer interface { SendAndClose(*ReturnArgs) error Recv() (*RequestStream, error) grpc.ServerStream}type serviceServerClientStreamRPCServer struct { grpc.ServerStream}//实现interfacefunc (x *serviceServerClientStreamRPCServer) SendAndClose(m *ReturnArgs) error { return x.ServerStream.SendMsg(m)}func (x *serviceServerClientStreamRPCServer) Recv() (*RequestStream, error) { m := new(RequestStream) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil}//实现业务逻辑func (s *ServiceServer) ClientStreamRPC(stream pb.serviceServerClientStreamRPCServer) error { for { reqStream, err := stream.Recv() if err == io.EOF { retArgs := &ReturnArgs{} //... return stream.SendAndClose(retArgs) } //... } return nil} 3.4 双向流式RPC12345678910111213141516171819202122232425262728293031323334353637383940414243//proto生成type ServiceServer_TwoWayStreamRPCServer interface { Send(*TWStream) error Recv() (*TWStream, error) grpc.ServerStream}type serviceServerTwoWayStreamRPCServer struct { grpc.ServerStream}//实现interfacefunc (x *serviceServerTwoWayStreamRPCServer) Send(m *TWStream) error { return x.ServerStream.SendMsg(m)}func (x *serviceServerTwoWayStreamRPCServer) Recv() (*TWStream, error) { m := new(TWStream) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil}//实现业务逻辑func (s *ServiceServer) TwoWayStreamRPC(stream pb.serviceServerTwoWayStreamRPCServer) error { for { in, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } for { twStream := &TWStream{} //... if err := stream.Send(twStream); err != nil { return err } } } return nil} 4 启动服务4.1 启动服务端 指定监听端口 grpc.NewServer()创建一个gPRC Server实例 注册自定义服务 Server()阻塞等待连接 1234567891011121314func main() { flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { grpclog.Fatalf("failed to listen: %v", err) } var opts []grpc.ServerOption // set options grpcServer := grpc.NewServer(opts...) pb.RegisterServiceServer(grpcServer, newServer()) grpcServer.Serve(lis)} 4. 2 启动客户端12345678910111213func main() { flag.Parse() //set options conn, err := grpc.Dial("serverAddr", opts...) if err != nil { //handle error } defer conn.Close() client := pb.NewServiceClient(conn)}//调用四种服务]]></content>
<categories>
<category>gRPC</category>
</categories>
<tags>
<tag>gRPC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis基础]]></title>
<url>%2F2017%2F07%2F11%2Fredis_basis%2F</url>
<content type="text"><![CDATA[1. key1.1基础操作 12345678910111213141516171819202122# 删除一个keydel key# 判断是否存在(0/1)exists normal:key1# 查找keys pattern# 判断类型type normal:key1# 设置存活时间expire normal:key1 120 #秒pexpire normal:key1 120 #毫秒# 获取存活时间(不存在-2,未设置-1)ttl normal:key1 # 秒pttl normal:key1 #毫秒# 删除存活时间设定persist normal:key1 1.2 序列化与反序列化123456#返回一个序列化值"\x00\x0bhello redis\a\x00(O\t?\x9c\x8b\xba\xcd"dump test:key#序列化一个值到指定key# ttl设置为0表示不设置,不加replace则反序列化到同一个键该值出错restore test:key2 ttl "\x00\x0bhello redis\a\x00(O\t?\x9c\x8b\xba\xcd" [replace] 2. 数据结构2.1 字符串(String)Redis中,最常见数据结构的就是字符串 1234567891011121314151617set test:strings:key1 "Hello Redis"set test:strings:key2 "Hello World"get test:strings:key1get test:strings:key2# 上述等价于mset test:strings:key1 "Hello Redis" test:strings:key2 "Hello World"mget test:strings:key1 test:string:key2# 获取字符串长度strlen test:strings:key1# 获取sub stringgetrange test:strings:key1 6 10# 追加append test:strings:key1 "!" 2.2 哈希表(Hash)散列数据结构提供了一个额外的间接层:一个域(Field) 123456789101112131415hset test:hash:key1 name "junhan"hset test:hash:key1 age 21hset test:hash:key1 phone "188*******6"# 上述等价于hmset test:hash:key1 name "junhan" age 21 phone "188*******6"# 根据key field获取数据hget test:hash:key1 name# 获取多个hmget test:hash:key1 name age phone# 获取所有field valuehgetall test:hash:key1 2.3 链表(List)链表数据结构可以存储一组值,主要操作是添加一个值到第一个或者最后一个位置,以及通过索引来访问数据。 123456789101112131415161718# 从左端逐个插入数据lpush program:language c c++ java go# 从右端逐个插入数据rpush program:language python php# 获取长度llen program:language# 根据索引获取lindex program:language 0# 弹出数据lpop program:languagerpop program:language# 数据截取(留下[s, e)范围,其他删除)ltrim 0 4 2.4 集合(Set)集合用来存储唯一的值,添加相同的值则本次操作将会被忽略 1234567891011121314151617181920sadd set:program:language1 c c++ java gosadd set:program:language2 python php java# 获取所有元素smembers set:program:language1# 判断是否存在sismember set:program:language1 java# 取交集sinter set:program:language1 set:program:language2# 取差集sdiff set:program:language1 set:program:language2# 取并集sunion set:program:language1 set:program:language2# 删除元素srem set:program:language2 java 2.5 有序集合(SortedSet)sortedset是一种集合类数据结构,采用score来标记一个元素,并通过score来进行排序,这里的score相当于rank,它的取值可以是整数值或双精度浮点数。 12345678910111213141516171819zadd sortset:test:key1 1 google.com 2 microsoft.com 3 apple.com 4 amazon.com 10 baidu.com# 递增列出数据zrange sortset:test:key1 0 3# 递减列出数据zrevrange sortset:test:key1 0 3# 通过score计数zcount sortset:test:key1 2 4# 获取scorezscore sortset:test:key1 google.com# 获取rank(根据score排的结果)zrank sortset:test:key1 baidu.com # 4# 删除zrem sortset:test:key1 baidu.com 3. 事务(Transaction)3.1 Redis事务原理Redis是单线程运行的,所以能够保证每个命令的原子性。但在实际开发中可能需要原子地执行一组命令,这个时候可以用redis的事务,它保证: 事务中的命令顺序执行 事务中的命令如单个原子操作般被执行 事务中的命令要么都被执行,要么都不执行 123456multiset test:tran:key1 "test"get test:tran:key1# ...exec # 执行事务discard # 放弃 3.2 事务并发问题虽然Redis是单线程的,但是在起多个redis-cli的时候,还是会出现并发中的一些问题,例如上面的代码,在get之后,set之前,test:tran:key1可能会被其他Redis进程修改,进而引起错误。对于这种情况,可以采用watch的监视。 12345678watch test:tran:key1get test:tran:key1multiset test:tran:key1 "new value"exce# 取消监视unwatch test:tran:key1 如果在set之前有其他客户端修改了test:tran:key1,那么事务将会执行失败。]]></content>
<categories>
<category>Redis</category>
</categories>
<tags>
<tag>redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git基础命令]]></title>
<url>%2F2017%2F07%2F11%2Fgit_basic_commands%2F</url>
<content type="text"><![CDATA[git提交代码的基本步骤 1. Git环境部署123456789101112git config --global alias.ll "log --graph --all --pretty=format:'%Cred%h %Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"git config --global color.status autogit config --global color.branch autogit config --global color.diff autogit config --global color.interactive autogit config --global alias.co checkoutgit config --global alias.br branchgit config --global alias.ci commitgit config --global alias.st statusgit config --global alias.last "log -1 HEAD"git config --global alias.df diffgit config --global color.ui true 2. Git提交规范 git st 查看修改内容 git add 添加修改文件 git commit -am “” 提交到本地仓库 git ll 查看head指针位置,判断是否需要fetch 如果head指针没有指向当前 git fetch git rebase origin/branch 如果有冲突(REBASE 1/2) 解决冲突 git add <冲突文件> git rebase –continue git push origin HEAD:master]]></content>
<categories>
<category>工具使用</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【0x00】Nginx环境源码搭建]]></title>
<url>%2F2017%2F05%2F23%2Fbuild_nginx_environment%2F</url>
<content type="text"><![CDATA[1 基础环境:Ubuntu14.04 64Bit系统2 Nginx源码下载Nginx官网提供了三种类型版本,分别是: Mainline version:开发版 Stable version:最新稳定版,生产环境上建议使用的版本 Legacy versions:老版本的稳定版 这里下载目前最新的Stable Version版本的Nginx,执行命令:12wget http://nginx.org/download/nginx-1.12.0.tar.gztar -zxvf nginx-1.12.0.tar.gz 3 安装依赖直接configure可以发现还需要安装一些依赖,执行命令:12sudo yum install pcre-develsudo yum install zlib-devel 4 编译安装Nginx1234cd nginx-1.12.0./congiguremakesudo make install 完成之后nginx被安装在/usr/local/nginx 5 修改nginx配置nginx的配置文件存放在/usr/local/nginx/conf目录,核心配置文件是nginx.conf,其内容如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331###### Nginx配置文件nginx.conf中文详解 #######定义Nginx运行的用户和用户组user www www;#nginx进程数,建议设置为等于CPU总核心数。worker_processes 8; #全局错误日志定义类型,[ debug | info | notice | warn | error | crit ]error_log /usr/local/nginx/logs/error.log info;#进程pid文件pid /usr/local/nginx/logs/nginx.pid;#指定进程可以打开的最大描述符:数目#工作模式与连接数上限#这个指令是指当一个nginx进程打开的最多文件描述符数目,理论值应该是最多打开文件数(ulimit -n)与nginx进程数相除,但是nginx分配请求并不是那么均匀,所以最好与ulimit -n 的值保持一致。#现在在linux 2.6内核下开启文件打开数为65535,worker_rlimit_nofile就相应应该填写65535。#这是因为nginx调度时分配请求到进程并不是那么的均衡,所以假如填写10240,总并发量达到3-4万时就有进程可能超过10240了,这时会返回502错误。worker_rlimit_nofile 65535;events{ #参考事件模型,use [ kqueue | rtsig | epoll | /dev/poll | select | poll ]; epoll模型 #是Linux 2.6以上版本内核中的高性能网络I/O模型,linux建议epoll,如果跑在FreeBSD上面,就用kqueue模型。 #补充说明: #与apache相类,nginx针对不同的操作系统,有不同的事件模型 #A)标准事件模型 #Select、poll属于标准事件模型,如果当前系统不存在更有效的方法,nginx会选择select或poll #B)高效事件模型 #Kqueue:使用于FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS X.使用双处理器的MacOS X系统使用kqueue可能会造成内核崩溃。 #Epoll:使用于Linux内核2.6版本及以后的系统。 #/dev/poll:使用于Solaris 7 11/99+,HP/UX 11.22+ (eventport),IRIX 6.5.15+ 和 Tru64 UNIX 5.1A+。 #Eventport:使用于Solaris 10。 为了防止出现内核崩溃的问题, 有必要安装安全补丁。 use epoll; #单个进程最大连接数(最大连接数=连接数*进程数) #根据硬件调整,和前面工作进程配合起来用,尽量大,但是别把cpu跑到100%就行。每个进程允许的最多连接数,理论上每台nginx服务器的最大连接数为。 worker_connections 65535; #keepalive超时时间。 keepalive_timeout 60; #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求头的大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。 #分页大小可以用命令getconf PAGESIZE 取得。 #[root@web001 ~]# getconf PAGESIZE #4096 #但也有client_header_buffer_size超过4k的情况,但是client_header_buffer_size该值必须设置为“系统分页大小”的整倍数。 client_header_buffer_size 4k; #这个将为打开文件指定缓存,默认是没有启用的,max指定缓存数量,建议和打开文件数一致,inactive是指经过多长时间文件没被请求后删除缓存。 open_file_cache max=65535 inactive=60s; #这个是指多长时间检查一次缓存的有效信息。 #语法:open_file_cache_valid time 默认值:open_file_cache_valid 60 使用字段:http, server, location 这个指令指定了何时需要检查open_file_cache中缓存项目的有效信息. open_file_cache_valid 80s; #open_file_cache指令中的inactive参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive时间内一次没被使用,它将被移除。 #语法:open_file_cache_min_uses number 默认值:open_file_cache_min_uses 1 使用字段:http, server, location 这个指令指定了在open_file_cache指令无效的参数中一定的时间范围内可以使用的最小文件数,如果使用更大的值,文件描述符在cache中总是打开状态. open_file_cache_min_uses 1; #语法:open_file_cache_errors on | off 默认值:open_file_cache_errors off 使用字段:http, server, location 这个指令指定是否在搜索一个文件是记录cache错误. open_file_cache_errors on;} #设定http服务器,利用它的反向代理功能提供负载均衡支持http{ #文件扩展名与文件类型映射表 include mime.types; #默认文件类型 default_type application/octet-stream; #默认编码 #charset utf-8; #服务器名字的hash表大小 #保存服务器名字的hash表是由指令server_names_hash_max_size 和server_names_hash_bucket_size所控制的。参数hash bucket size总是等于hash表的大小,并且是一路处理器缓存大小的倍数。在减少了在内存中的存取次数后,使在处理器中加速查找hash表键值成为可能。如果hash bucket size等于一路处理器缓存的大小,那么在查找键的时候,最坏的情况下在内存中查找的次数为2。第一次是确定存储单元的地址,第二次是在存储单元中查找键 值。因此,如果Nginx给出需要增大hash max size 或 hash bucket size的提示,那么首要的是增大前一个参数的大小. server_names_hash_bucket_size 128; #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求的头部大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。分页大小可以用命令getconf PAGESIZE取得。 client_header_buffer_size 32k; #客户请求头缓冲大小。nginx默认会用client_header_buffer_size这个buffer来读取header值,如果header过大,它会使用large_client_header_buffers来读取。 large_client_header_buffers 4 64k; #设定通过nginx上传文件的大小 client_max_body_size 8m; #开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。注意:如果图片显示不正常把这个改成off。 #sendfile指令指定 nginx 是否调用sendfile 函数(zero copy 方式)来输出文件,对于普通应用,必须设为on。如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络IO处理速度,降低系统uptime。 sendfile on; #开启目录列表访问,合适下载服务器,默认关闭。 autoindex on; #此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用 tcp_nopush on; tcp_nodelay on; #长连接超时时间,单位是秒 keepalive_timeout 120; #FastCGI相关参数是为了改善网站的性能:减少资源占用,提高访问速度。下面参数看字面意思都能理解。 fastcgi_connect_timeout 300; fastcgi_send_timeout 300; fastcgi_read_timeout 300; fastcgi_buffer_size 64k; fastcgi_buffers 4 64k; fastcgi_busy_buffers_size 128k; fastcgi_temp_file_write_size 128k; #gzip模块设置 gzip on; #开启gzip压缩输出 gzip_min_length 1k; #最小压缩文件大小 gzip_buffers 4 16k; #压缩缓冲区 gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0) gzip_comp_level 2; #压缩等级 gzip_types text/plain application/x-javascript text/css application/xml; #压缩类型,默认就已经包含textml,所以下面就不用再写了,写上去也不会有问题,但是会有一个warn。 gzip_vary on; #开启限制IP连接数的时候需要使用 #limit_zone crawler $binary_remote_addr 10m; #负载均衡配置 upstream piao.jd.com { #upstream的负载均衡,weight是权重,可以根据机器配置定义权重。weigth参数表示权值,权值越高被分配到的几率越大。 server 192.168.80.121:80 weight=3; server 192.168.80.122:80 weight=2; server 192.168.80.123:80 weight=3; #nginx的upstream目前支持4种方式的分配 #1、轮询(默认) #每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。 #2、weight #指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。 #例如: #upstream bakend { # server 192.168.0.14 weight=10; # server 192.168.0.15 weight=10; #} #2、ip_hash #每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。 #例如: #upstream bakend { # ip_hash; # server 192.168.0.14:88; # server 192.168.0.15:80; #} #3、fair(第三方) #按后端服务器的响应时间来分配请求,响应时间短的优先分配。 #upstream backend { # server server1; # server server2; # fair; #} #4、url_hash(第三方) #按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。 #例:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法 #upstream backend { # server squid1:3128; # server squid2:3128; # hash $request_uri; # hash_method crc32; #} #tips: #upstream bakend{#定义负载均衡设备的Ip及设备状态}{ # ip_hash; # server 127.0.0.1:9090 down; # server 127.0.0.1:8080 weight=2; # server 127.0.0.1:6060; # server 127.0.0.1:7070 backup; #} #在需要使用负载均衡的server中增加 proxy_pass http://bakend/; #每个设备的状态设置为: #1.down表示单前的server暂时不参与负载 #2.weight为weight越大,负载的权重就越大。 #3.max_fails:允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream模块定义的错误 #4.fail_timeout:max_fails次失败后,暂停的时间。 #5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。 #nginx支持同时设置多组的负载均衡,用来给不用的server来使用。 #client_body_in_file_only设置为On 可以讲client post过来的数据记录到文件中用来做debug #client_body_temp_path设置记录文件的目录 可以设置最多3层目录 #location对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡 } #虚拟主机的配置 server { #监听端口 listen 80; #域名可以有多个,用空格隔开 server_name www.jd.com jd.com; index index.html index.htm index.php; root /data/www/jd; #对******进行负载均衡 location ~ .*.(php|php5)?$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi.conf; } #图片缓存时间设置 location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$ { expires 10d; } #JS和CSS缓存时间设置 location ~ .*.(js|css)?$ { expires 1h; } #日志格式设定 #$remote_addr与$http_x_forwarded_for用以记录客户端的ip地址; #$remote_user:用来记录客户端用户名称; #$time_local: 用来记录访问时间与时区; #$request: 用来记录请求的url与http协议; #$status: 用来记录请求状态;成功是200, #$body_bytes_sent :记录发送给客户端文件主体内容大小; #$http_referer:用来记录从那个页面链接访问过来的; #$http_user_agent:记录客户浏览器的相关信息; #通常web服务器放在反向代理的后面,这样就不能获取到客户的IP地址了,通过$remote_add拿到的IP地址是反向代理服务器的iP地址。反向代理服务器在转发请求的http头信息中,可以增加x_forwarded_for信息,用以记录原有客户端的IP地址和原来客户端的请求的服务器地址。 log_format access '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" $http_x_forwarded_for'; #定义本虚拟主机的访问日志 access_log /usr/local/nginx/logs/host.access.log main; access_log /usr/local/nginx/logs/host.access.404.log log404; #对 "/" 启用反向代理 location / { proxy_pass http://127.0.0.1:88; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #以下是一些反向代理的配置,可选。 proxy_set_header Host $host; #允许客户端请求的最大单文件字节数 client_max_body_size 10m; #缓冲区代理缓冲用户端请求的最大字节数, #如果把它设置为比较大的数值,例如256k,那么,无论使用firefox还是IE浏览器,来提交任意小于256k的图片,都很正常。如果注释该指令,使用默认的client_body_buffer_size设置,也就是操作系统页面大小的两倍,8k或者16k,问题就出现了。 #无论使用firefox4.0还是IE8.0,提交一个比较大,200k左右的图片,都返回500 Internal Server Error错误 client_body_buffer_size 128k; #表示使nginx阻止HTTP应答代码为400或者更高的应答。 proxy_intercept_errors on; #后端服务器连接的超时时间_发起握手等候响应超时时间 #nginx跟后端服务器连接超时时间(代理连接超时) proxy_connect_timeout 90; #后端服务器数据回传时间(代理发送超时) #后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据 proxy_send_timeout 90; #连接成功后,后端服务器响应时间(代理接收超时) #连接成功后_等候后端服务器响应时间_其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间) proxy_read_timeout 90; #设置代理服务器(nginx)保存用户头信息的缓冲区大小 #设置从被代理服务器读取的第一部分应答的缓冲区大小,通常情况下这部分应答中包含一个小的应答头,默认情况下这个值的大小为指令proxy_buffers中指定的一个缓冲区的大小,不过可以将其设置为更小 proxy_buffer_size 4k; #proxy_buffers缓冲区,网页平均在32k以下的设置 #设置用于读取应答(来自被代理服务器)的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k proxy_buffers 4 32k; #高负荷下缓冲大小(proxy_buffers*2) proxy_busy_buffers_size 64k; #设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长 #设定缓存文件夹大小,大于这个值,将从upstream服务器传 proxy_temp_file_write_size 64k; } #设定查看Nginx状态的地址 location /NginxStatus { stub_status on; access_log on; auth_basic "NginxStatus"; auth_basic_user_file confpasswd; #htpasswd文件的内容可以用apache提供的htpasswd工具来产生。 } #本地动静分离反向代理配置 #所有jsp的页面均交由tomcat或resin处理 location ~ .(jsp|jspx|do)?$ { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8080; } #所有静态文件由nginx直接读取不经过tomcat或resin location ~ .*.(htm|html|gif|jpg|jpeg|png|bmp|swf|ioc|rar|zip|txt|flv|mid|doc|ppt| pdf|xls|mp3|wma)$ { expires 15d; } location ~ .*.(js|css)?$ { expires 1h; } }} 6 启动与测试Nginx执行下列命令启动Nginx1/usr/local/nginx/sbin/nginx 如果在Nginx启动的情况下修改了配置,不需要重启启动Nginx,只需要reload一下即可:12nginx -t #检查配置文件是否有错误nginx -s reload #重新加载配置 启动Nginx之后,访问localhost:80/index.html可以看到如下界面:]]></content>
<categories>
<category>Nginx学习</category>
</categories>
<tags>
<tag>nginx</tag>
</tags>
</entry>
<entry>
<title><![CDATA[两种高性能I/O模式:Reactor和Proactor]]></title>
<url>%2F2017%2F05%2F05%2Fhigh_performance_reactor_and_proactor%2F</url>
<content type="text"><![CDATA[系统I/O 可分为阻塞型,非阻塞同步型以及非阻塞异步型。 阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里。 结果调用者被阻塞了, 这段时间了做不了任何其它事情。在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,非常浪费资源了。例如read()系统调用,调用他的函数会一直阻塞在read()上直到socket可读。 相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等待,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用。例如read()系统调用, 如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉read()的调用者”数据还没准备好,你稍后再试”。 在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。 在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。 一般情况下,I/O 复用机制需要事件分发器(event demultiplexor)。事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者。开发人员在开始的时候需要在事件分发器那里注册感兴趣的事件(如可读或者可写),并提供相应的处理者(event handlers),或者是回调函数(event callback); 事件分发器在适当的时候会将请求的事件分发给这些handler或者callback。 事件分发器主要有两种模式:Reactor和Proactor。 Reactor模式他是基于同步I/O的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。 Proactor模式他是基于异步I/O的。在Proactor模式中,handler直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的handler或者callback。 Reactor模式简单例子: 某个handler宣称对某个socket的某个事件感兴趣 事件分发器等待这个事件的发生 这个事件发生,事件分发器被唤醒并通知对应的handler handler进行相关操作 Proactor模式简单例子: handler投递一个异步写(读)操作,事件分发器只关心这个请求,不关心读(写)事件。 事件分发器等待这个事件的完成 这个事件完成了(操作系统异步完成),操作系统通知事件分发器,然后事件分发器通知对应的handler]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>reactor</tag>
<tag>proactor</tag>
</tags>
</entry>
<entry>
<title><![CDATA[IO阻塞/非阻塞与同步/异步]]></title>
<url>%2F2017%2F05%2F05%2Fio_blocked_and_nonblock_async_and_sync%2F</url>
<content type="text"><![CDATA[1.阻塞(blocking)与非阻塞(nonblocking)阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞指系统调用返回之前,当前进程会被挂起(进入非可执行状态,CPU不会分配时间片)。函数直到有了结果才返回。非阻塞指系统调用没有得到结果,不会阻塞当前进程,而是直接返回,同时伴随返回相应的错误提示,如EWOULDBLOCK,EAGIN。 2.同步(synchronous)与异步(asynchronous)同步和异步关注的是消息通信机制(synchronous communication/ asynchronous communication)。 同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。例如read()调用等待读取完成返回(阻塞),或者read()直接返回不断轮询直到返回结果指示读取完成(非阻塞)。 异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。 3. 陈硕的解释 在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步 IO。]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>nonblocking</tag>
<tag>blocking</tag>
</tags>
</entry>
<entry>
<title><![CDATA[nonblocking网络编程使用应用层Buffer]]></title>
<url>%2F2017%2F05%2F05%2Fnonblocking_network_programing_with_applayer_buffer%2F</url>
<content type="text"><![CDATA[non-blocking IO的核心思想是避免阻塞在read()或者write()或者其他IO系统调用上,这样可以最大限度地复用thread-of-control,让线程只能阻塞在IO multiplexing函数上(select、poll、epoll_wait),这样一来,每个TCP socket都要有stateful的input buffer和output buffer。 Ⅰ. 必须有output buffer 假设程序想通过TCP发送100KB数据,但是write()调用受限于TCP窗口大小,系统只能接受80KB,这个时候肯定不能原地等待(要等对方确认接受数据才能滑动TCP窗口,这个时间是不确定的),此时程序应该尽快交出控制权,返回eventloop,但是剩下的20KB不能不管。 所以,需要一个应用层的output buffer来保存数据,程序只管将数据写入output buffer,剩下的发送交给output buffer。也就是注册一个POLLOUT事件,一旦socket可读就将output buffer中的剩据发送出去,如果没有将output buffer中的数据全部发送出去,就应该继续关注POLLOUT事件,当output buffer中的数据全部写完之后再取消关注POLLOUT事件。 Ⅱ. 必须有input bufferTCP是无边界的字节流协议,接收端必须自己处理“收到一条不完整消息”和“一次收到多条消息的数据“等情况。假设发送方程序发送了两条1KB的消息。接收端可能收到的情况有: 一次性2KB 两次收到1KB 一次400B,一次1400B 等等情况 对于接收端,每次read()调用都必须把socket一次性读取出来(搬到应用层input buffer),否则将反复触发POLLIN事件,造成busy-loop。至于“收到一条不完整消息”和“一次收到多条消息的数据“等情况就由input buffer来处理。这种逻辑一般是codec的职责。]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>nonblocking</tag>
<tag>socket</tag>
</tags>
</entry>
<entry>
<title><![CDATA[EAGAIN、EINTR、EWOULDBLOCK与非阻塞]]></title>
<url>%2F2017%2F05%2F03%2Feagin_eintr_ewouldblock_and_nonblocked%2F</url>
<content type="text"><![CDATA[在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN、EINTR、EWOULDBLOCK算是比较常见的错误,特别是在非阻塞中。 EAGIN和EWOULDBLOCK例如对一个设置了O_NONBLOCKING的文件描述符read,此时没有数据可以读,read调用不会阻塞而是返回0并且设置errno为EAGIN,提示稍后再试。又或者当一个系统调用(如fork())没有足够的资源而执行失败也会设置EAGIN错误,提示稍后再试。 EWOULDBLOCK和EAGIN一样,不同平台上的。 如果对EWOULDBLOCK和EAGIN的情况进行判断,那么系统默认会输出错误描述Resource temporarily unavailable错误并终止进程。 EINTR当一个系统调用执行被中断时,errno被设置为EINTR,错误描述为Interrupted system call。]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>nonblocking</tag>
<tag>errno</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用eventfd进行进程/线程通信]]></title>
<url>%2F2017%2F05%2F03%2Fprocess_communication_eventfd%2F</url>
<content type="text"><![CDATA[eventfd在Linux中类似于管道,但是比管道简单。他的收发缓存只有8字节。使用eventfd通信时,通过evenfd(2)创建一个文件描述符,主要用于事件通知(wait/notify),传输数据只能是8个字节大小,一般用uint64_t,是一个双方协商好的数字。 eventfd(2)eventfd(2)用于创建一个描述符用于事件通知,其原型如下:123#include <sys/eventfd.h>int eventfd(unsigned int initval, int flags); initval参数用于初始化eventfd内部计数器(见下) flags参数用于设定文件描述符标志 EFD_NONBLOCK EFD_CLOEXEC EFD_SEMAPHORE eventfd原理eventfd(2)返回一个文件描述符,并在内核中关联一个对应的struct file结构。这个struct file结构如下:1234567891011121314struct eventfd_ctx { struct kref kref; //linux kernel实现file计数用的 wait_queue_head_t wqh; //Linux kernel等待队列 /* * Every time that a write(2) is performed on an eventfd, the * value of the __u64 being written is added to "count" and a * wakeup is performed on "wqh". A read(2) will return the "count" * value to userspace, and will reset "count" to zero. The kernel * side eventfd_signal() also, adds to the "count" counter and * issue a wakeup. */ __u64 count; //eventfd计数器 unsigned int flags; //文件描述符标志}; read一个eventfd时,从count读出数据,然后将count清零 write一个eventfd时,将数据写入count eventfd应用场景eventfd传输数据只有八个字节,注定了不是用来进行数据传输的。由于eventfd不用自己管理缓存区,因此可以非常搞笑的实现唤醒,即:event wait/notify。例如主线程阻塞在epoll_wait等待返回就绪描述符列表,此时在另外一个线程中修改某个对象,但不是描述符,然而希望epoll_wait能够返回并让程序继续往下执行,这个时候就需要一种机制来唤醒该线程。具体实现如下:其他线程修改对象之后向eventfd写入数据,触发eventfd的可读事件,让epoll_wait返回,这个时候阻塞线程就成功返回了。 eventfd进程通信示例1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253#include <sys/eventfd.h>#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <stdint.h> /* Definition of uint64_t */#define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0)int main(int argc, char *argv[]){ int efd, j; uint64_t u; ssize_t s; if (argc < 2) { fprintf(stderr, "Usage: %s <num>...\n", argv[0]); exit(EXIT_FAILURE); } efd = eventfd(0, 0); if (efd == -1) handle_error("eventfd"); switch (fork()) { case 0: for (j = 1; j < argc; j++) { printf("Child writing %s to efd\n", argv[j]); u = strtoull(argv[j], NULL, 0); strtoull() allows various bases */ s = write(efd, &u, sizeof(uint64_t)); if (s != sizeof(uint64_t)) handle_error("write"); } printf("Child completed write loop\n"); exit(EXIT_SUCCESS); default: sleep(2); printf("Parent about to read\n"); s = read(efd, &u, sizeof(uint64_t)); if (s != sizeof(uint64_t)) handle_error("read"); printf("Parent read %llu (0x%llx) from efd\n", (unsigned long long) u, (unsigned long long) u); exit(EXIT_SUCCESS); case -1: handle_error("fork"); }}]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>eventfd</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用timerfd]]></title>
<url>%2F2017%2F04%2F30%2Flinux_timer_timerfd%2F</url>
<content type="text"><![CDATA[timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,所以能够被用于select/poll的应用场景。 timerfd接口 timerfd_create(2)timerfd_create(2)函数用于创建timerfd文件描述符,其函数原型如下:1int timerfd_create(int clockid, int flags); clockid参数,设置时间获取方式CLOCK_REALTIME: 系统范围内的实时时钟(以1970.1.1为基准)CLOCK_MONOTONIC: 以固定的速率运行,从不进行调整和复位 ,它不受任何系统time-of-day时钟修改的影响(以开机时间为基准) flags参数,设置描述符属性TFD_NONBLOCK: 非阻塞TFD_CLOEXEC: CLOEXEC timerfd_settime(4)timerfd_settime(4)用于启动和停止计数器,原型如下:123456789101112struct timespec{ time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds */};struct itimerspec{ struct timespec it_interval; /* 计时周期 */ struct timespec it_value; /* 开始时间 */};int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); fd参数即timerfd_create(2)创建的timerfd描述符 flags参数0 : 相对计时器,此时itimerspec参数填的是相对时间TFD_TIMER_ABSTIME : 绝对定时器,此时itimerspec填的是使用clock_gettime(2)获取的时间,clock_gettime第一个参数与timerfd_create的clockid参数相同。 new_value参数用于指定定时器的动作,为0表示停止计时器 old_value timerfd_gettime(2)1int timerfd_gettime(int fd, struct itimerspec *curr_value); 此函数用于获得定时器距离下次超时还剩下的时间。如果调用时定时器已经到期,并且该定时器处于循环模式(设置超时时间时struct itimerspec::it_interval不为0),那么调用此函数之后定时器重新开始计时。 read读取timerfd超时事件当定时器超时,read读事件发生即可读,返回超时次数(从上次调用timerfd_settime()启动开始或上次read成功读取开始),它是一个8字节的unit64_t类型整数,如果定时器没有发生超时事件,则read将阻塞若timerfd为阻塞模式,否则返回EAGAIN 错误(O_NONBLOCK模式),如果read时提供的缓冲区小于8字节将以EINVAL错误返回。 timerfd实例1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253#include <sys/timerfd.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <time.h>void startRealtimer(unsigned int beginSec, unsigned int periodSec, int count){ int tfd = timerfd_create(CLOCK_REALTIME, TFD_CLOEXEC); timespec now; clock_gettime(CLOCK_REALTIME, &now); struct itimerspec new_value; new_value.it_value.tv_sec = now.tv_sec + beginSec; new_value.it_value.tv_nsec = now.tv_nsec; new_value.it_interval.tv_sec = periodSec; new_value.it_interval.tv_nsec = 0; timerfd_settime(tfd, TFD_TIMER_ABSTIME, &new_value, NULL); for (int i = 0;i < count; /* no ++i */) { unsigned long long data = 0; read(tfd, &data, sizeof(data)); i += data; printf("Realtimer Count %d\n", i); } close(tfd);}void startMonotonictimer(unsigned int beginSec, unsigned int periodSec, int count){ int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); struct itimerspec new_value; new_value.it_value.tv_sec = beginSec; new_value.it_interval.tv_sec = periodSec; timerfd_settime(tfd, 0, &new_value, NULL); for (int i = 0;i < count; /* no ++i */) { unsigned long data; read(tfd, &data, sizeof(data)); i += data; printf("Monotonictimer Count %d\n", i); } close(tfd);}int main(){ startRealtimer(5, 1, 5); startMonotonictimer(5, 1, 5); return 0;}]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>timerfd</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C++四种类型转换]]></title>
<url>%2F2017%2F04%2F19%2Fcpp_four_type_conversions%2F</url>
<content type="text"><![CDATA[C++具有以下四种类型转换符: static_cast const_cast dynamic_cast reinterpret_cast 具体用法一样,都是如下形式:1xxx_cast<type>(expression) 1. static_cast该运算符将expression转换成type类型,没有动态类型检查,不是安全的。而且无法移除原对象的const、volital等属性。它主要用于一下几种情况 用于内置数据类型(int,double等)的转换,不保证安全性 用于基类和派生类之间引用或者指针之间的转换 向上转换(派生类转基类)是安全的 向下转换(基类转派生类)没有动态类型检查,不安全 空指针转换成目标类型空指针 任何类型的expression转成void类型 1234567891011int f2i = static_cast<int>(3.14f); //float->intBase *pb = new Base();Derive *pd = new Derive();Base *pd2b = static_cast<Base*>(pd); //安全Derive *pb2d = static_cast<Derive*>(pb); //不安全Base b;Derive d;Base &rd2b = static_cast<Base&>(d); //安全Derive &rb2d = static_cast<Derive&>(b); //不安全 2. const_castconst_cast用来去掉const属性,主要有3种情况: 常量指针转换为非常量指针,仍然指向原有对象 常量引用转换为非常量引用,仍然绑定原有对象 常量对象转换为非常量对象 3. dynamic_castdynamic_cast的转换类型必须是引用或者指针。一般用于派生关系的上下转换,相比static_cast多了类型检查,即: 向下转换(派生类转基类),同static_cast 向上转换(基类转派生类),转换失败,返回nullptr 1234567891011void func(Base *pb){ Derive *d = dynamic_cast<Derive*>(pb); if (d == nullptr) cout << "A Base Pointer" << endl; else cout << "A Derive Pointer" << endl;}Base *pb = new Base();Base *pd = new Derive();func(pb); //A Base Pointerfunc(pd); //A Derive Pointer 4. reinterpret_castreinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。但是: 错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式。 例如一个辅助hash函数:12345// Returns a hash code based on an addressunsigned short Hash( void *p ) { unsigned int val = reinterpret_cast<unsigned int>( p ); return ( unsigned short )( val ^ (val >> 16));}]]></content>
<categories>
<category>C++</category>
</categories>
<tags>
<tag>类型转换</tag>
</tags>
</entry>
<entry>
<title><![CDATA[右值引用与move语义]]></title>
<url>%2F2017%2F04%2F19%2Frvalue_reference_and_move%2F</url>
<content type="text"><![CDATA[右值C语言中,左值(left value, lvalue)只出现在赋值符左边的量,右值(right value, rvalue)是出现在赋值符右边的量。在C++中,右值的定义稍微不同,每一个表达式都会产生一个左值或者右值,所以表达式也称左值表达式或右值表达式。 对于基础类型,右值不可修改,也不可被const,volatile修饰 对于自定义类型,右值可以被成员函数修改 123456789101112131415161718192021class Foo{public: Foo(int i) : m_i(i) {} void setI(int i) { m_i = i; }private: int m_i;};Foo getFoo() //返回右值,具有临时性{ return Foo(0);}int main(){ getFoo().setI(2); //调用右值的成员函数,可以修改 Foo *f = &getFoo(); //error: taking address of temporary return 0;} 非常量右值引用只能绑定到非常量右值,不能绑定到非常量左值、常量左值和常量右值。常量右值引用可以绑定到非常量右值和常量右值,不能绑定到非常量左值和常量左值 move语义考虑下面的一小段代码123456vector<int> getVector(){ vector<int> vec; return vec;}vector<int> v = getVector(); 上述代码在没有优化的情况下,要调用三次构造函数,分别时构造vec,vec拷贝构造到返回值,返回值赋值构造到v。这样子性能特别差。如果确定某个值是一个非常量右值(或者是一个以后不会再使用的左值),则我们在进行临时对象的拷贝时,可以不用拷贝实际的数据,而只是“窃取”指向实际数据的指针。例如上面的vec,在拷贝构造返回值时,它是一个以后绝对不会使用的左值,所以可以直接把实际数据指针赋给返回值,同理返回值赋值构造v时,返回值是一个非常量右值,也可以直接把实际数据指针赋给v。这样子实际数据拷贝就只有一次,大大提高了效率。C++11提供了std::move操作用于上述情形,考虑下面的代码:12vector<int> v{1, 2};v = getVector(); 第2句代码要做的工作有:①销毁v原先的内存②将getVector返回值内容拷贝一份给v③销毁返回值的内存如果vector定义了move赋值运算符:12template<class T>vector<T>& vector<T>::operator=(vector<T> &&rhs); 那么上述代码就会调用move赋值操作符,将返回值的内存指针赋给v。move不仅适用于右值,可以以用于左值,std::move提供了将左值转成右值的功能。例如使用std::move实现swap:1234567template <class T>void swap(T &lhs, T &rhs){ T tmp(std::move(lhs)); lhs = std::move(rhs); rhs = std::move(tmp);}]]></content>
<categories>
<category>C++</category>
</categories>
<tags>
<tag>rvalue</tag>
<tag>move</tag>
</tags>
</entry>
<entry>
<title><![CDATA[可变参数包与完美转发]]></title>
<url>%2F2017%2F04%2F19%2Fvariable_parameter_and_perfect_forwarding%2F</url>
<content type="text"><![CDATA[可变参数模板可变参数模板(variadic template)是C++11新增的一项特性,使得模板参数可以任意化。一个基本的可变参数模板声明如下:1234template<typename ...Element> class Tuple;//使用Tuple<int, double, string> tup; 其中Element被称为模板参数包(template type parameter pack),参数包的解包采用的是模板特化的方法:12345template<typename Head, typename ...Tail>class tuple<Head, Tail...> : private tuple<Tail...> {};//特化版本template<> class tuple<> {}; 完美转发假设有下面这样的函数,用来转发参数12345template<typename Arg>void forwardArg(TYPE arg){ foo(arg);} arg是Arg类型,即值传递,那么arg可能在forwardArg里面被修改,导致传给foo的参数不一致 arg是Arg&类型,即引用传递,那么forward又没办法接收右值类型 arg是Arg&类型,并重载了const T&的方法,这个时候可以接收右值类型,但是对于多个参数,必须考虑多种情况,这个时候重载多个版本显然不能解决问题。 右值引用的引入解决了这个问题,在这种上下文时,它成为forwarding reference。例如下面情况:12template <class T>void func(T&& param); 其中,T&&不一定表示右值,如果它绑定的类型未知,那么结果也是未知的,可能是左值,也可能是右值。称其为未定引用类型(universal reference),这种类型必须被初始化,并在初始化中决定其类型。 1.引用折叠规则 由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或右值引用的参数初始化,这是经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化就称为引用折叠。——《深入应用C++11-代码优化与工程级应用》,祁宇,P68 对于C++语言,不可以在源程序中直接对引用类型再施加引用。T& &将编译报错。C++11标准中仍然禁止上述显式对引用类型再施加引用,但如果在上下文环境中(包括模板实例化、typedef、auto类型推断等)如出现了对引用类型再施加引用,则施行引用塌缩规则(reference collapsing rule) T& &变为T& T& &&变为T& T&& &变为T& T&& &&变为T&& 2.模板参数类型推导对于函数模板template<class T> void foo(T&& param);,应用引用折叠规则分析如下: 实参是类型A的左值,则模板参数T的类型为A&,形参类型为A& 实参时类型A的右值,则模板参数T的类型为A,形参类型为A&& 3.完美转发及其实现完美转发(perfect forwarding)问题是指函数模板在向其他函数传递参数时该如何保留该参数的左右值属性的问题。也就是说函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;同样如果相应实参是右值,它就应该被转发为右值。C++11提供了std::forward (include <unility>)来实现完美转发,具体操作如下:12345template <typename ...Args>void forward_ref(Args&& ...args){ foo(std::forward<Args>(args)...);} 利用折叠规则,可以写出forward大致实现,如下:12345template<typename T>T&& forward(typename std::remove_reference<T>::type& t){ return static_cast<T&&>(t);} 下面分析下完美转发的工作原理: 如果forward_ref传入的是类型A的左值,根据折叠规则,Args被决议为A&类型,forward<A&>实例化之后返回值为static_cast<A& &&>也即static_cast<A&>,符合预期。 如果forward_ref传入的是类型A的右值,Args被决议为A类型,forward<A>实例化后返回static_cast<A&&>也符合预期。 参考资料如何理解c++中的引用折叠?C++11:深入理解右值引用,move语义和完美转发C++11 图说VS2013下的引用叠加规则和模板参数类型推导规则]]></content>
<categories>
<category>C++</category>
</categories>
<tags>
<tag>template</tag>
<tag>variadic template</tag>
</tags>
</entry>
<entry>
<title><![CDATA[lambda函数]]></title>
<url>%2F2017%2F04%2F19%2Fcpp11_lambda%2F</url>
<content type="text"><![CDATA[Lambda表达书是C++11新增的一个特性,Lambda又称Lambda函数,是C++中的匿名函数。 Lambda函数形式Lambda函数的基本形式是:[ capture ] ( params ) mutable exception attribute -> ret { body }[ capture ] ( params ) -> ret { body }[ capture] ( params ) { body }[ capture] { body } 第一个是Lambda表达式的完成形式, 第二个是const类型的Lambda表达式,不能修改[]列表捕获的变量 第三个是忽略返回类型的Lambda表达式,返回类型通过以下两种规则自动推导 如果body含有return语句,则返回类型为return的值的类型 如果body没有return语句,则返回类型为void func(...)的函数类型 mutable修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。exception说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f() throw(X, Y)。attribute用来声明属性。 capture 用来声明捕获列表,指定了可见域范围内Lambda表达式可见的外部变量列表,具体如下: [] :不捕获外部变量 [&] :以引用类型捕获所有外部变量 [=] :以值捕获所有外部变量 [a, &b] :a以值方式捕获,相当于捕获副本,b以引用捕获 [this] :以值方式捕获this指针 一个简单的例子123456789101112131415161718192021#include <iostream>using namespace std;int main(){ auto func = [](){ cout << "Hello World!" << endl; }; func(); //func自动推导类型为void foo()函数类型 int a = 2; auto divA = [=] (int val) -> int{ return val / a; } //返回类型int func(int)函数类型 cout << "10 div a = " << divA(10) << endl; return 0;}/*输出:Hello World10 div a = 5*/ Lambda函数的用处Lambda函数一般用于以下情形:如果代码里面存在大量的小函数,而这些函数一般只被调用一次。又或者是提供一个接口,具体实现由用户自定义。例如:123456789101112131415161718192021class Something{public: template<typename CheckFunc> bool find(CheckFunc func) { for (auto beg = m_info.begin(); beg != m_info.end(); ++beg) { if (func(*beg)) return true; } return false; }private: std::vector<std::string> m_info;};//用户调用find接口时,check函数一般只作为参数传入时被调用,其他时候不需要使用,这个时候就可以使用Lambda表达式:Something some;//initializedsome.find( [](std::string str) { return str == "Hello"; } );]]></content>
<categories>
<category>C++</category>
</categories>
<tags>
<tag>lambda</tag>
</tags>
</entry>
<entry>
<title><![CDATA[以boost::function和Boost::bind取代虚函数]]></title>
<url>%2F2017%2F04%2F18%2Fuse_functor_and_bind_instead_vfunc%2F</url>
<content type="text"><![CDATA[这是一篇比较情绪化的blog,中心思想是“继承就像一条贼船,上去就下不来了”,而借助boost::function和boost::bind,大多数情况下,你都不用上贼船。 boost::function和boost::bind已经纳入了std::tr1,这或许是C++0x最值得期待的功能,它将彻底改变C++库的设计方式,以及应用程序的编写方式。 Scott Meyers的Effective C++ 3rd ed.第35条款提到了以boost::function和boost:bind取代虚函数的做法,这里谈谈我自己使用的感受。 基本用途boost::function就像C#里的delegate,可以指向任何函数,包括成员函数。当用bind把某个成员函数绑到某个对象上时,我们得到了一个closure(闭包)。例如:123456789101112131415161718192021222324252627class Foo{ public: void methodA(); void methodInt(int a);};class Bar{ public: void methodB();};boost::function<void()> f1; // 无参数,无返回值Foo foo;f1 = boost::bind(&Foo::methodA, &foo);f1(); // 调用 foo.methodA();Bar bar;f1 = boost::bind(&Bar::methodB, &bar);f1(); // 调用 bar.methodB();f1 = boost::bind(&Foo::methodInt, &foo, 42);f1(); // 调用 foo.methodInt(42);boost::function<void(int)> f2; // int 参数,无返回值f2 = boost::bind(&Foo::methodInt, &foo, _1);f2(53); // 调用 foo.methodInt(53); 如果没有boost::bind,那么boost::function就什么都不是,而有了bind(),“同一个类的不同对象可以delegate给不同的实现,从而实现不同的行为”(myan语),简直就无敌了。 对程序库的影响程序库的设计不应该给使用者带来不必要的限制(耦合),而继承是仅次于最强的一种耦合(最强耦合的是友元)。如果一个程序库限制其使用者必须从某个class派生,那么我觉得这是一个糟糕的设计。不巧的是,目前有些程序库就是这么做的。 例1:线程库常规OO设计:写一个Thread base class,含有(纯)虚函数 Thread#run(),然后应用程序派生一个继承class,覆写run()。程序里的每一种线程对应一个Thread的派生类。例如Java的Thread可以这么用。 缺点:如果一个class的三个method需要在三个不同的线程中执行,就得写helper class(es)并玩一些OO把戏。 基于closure的设计:令Thread是一个具体类,其构造函数接受Callable对象。应用程序只需提供一个Callable对象,创建一份Thread实体,调用Thread#start()即可。Java的Thread也可以这么用,传入一个Runnable对象。C#的Thread只支持这一种用法,构造函数的参数是delegate ThreadStart。boost::thread也只支持这种用法。12345678910111213141516171819// 一个基于 closure 的 Thread class 基本结构class Thread { public: typedef boost::function<void()> ThreadCallback; Thread(ThreadCallback cb) : cb_(cb) { } void start() { /* some magic to call run() in new created thread */ } private: void run() { cb_(); } ThreadCallback cb_; // ... }; 使用:123456789class Foo{ public: void runInThread();};Foo foo;Thread thread(boost::bind(&Foo::runInThread, &foo));thread.start(); 例2:网络库以boost::function作为桥梁,NetServer class对其使用者没有任何类型上的限制,只对成员函数的参数和返回类型有限制。使用者EchoService也完全不知道NetServer的存在,只要在main()里把两者装配到一起,程序就跑起来了。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051// libraryclass Connection;class NetServer : boost::noncopyable{ public: typedef boost::function<void (Connection*)> ConnectionCallback; typedef boost::function<void (Connection*, const void*, int len)> MessageCallback; NetServer(uint16_t port); ~NetServer(); void registerConnectionCallback(const ConnectionCallback&); void registerMessageCallback(const MessageCallback&); void sendMessage(Connection*, const void* buf, int len); private: // ...};// userclass EchoService{ public: typedef boost::function<void(Connection*, const void*, int)> SendMessageCallback; // 符合NetServer::sendMessage的原型 EchoService(const SendMessageCallback& sendMsgCb) : sendMessageCb_(sendMsgCb) { } void onMessage(Connection* conn, const void* buf, int size) // 符合NetServer::NetServer::MessageCallback的原型 { printf("Received Msg from Connection %d: %.*s/n", conn->id(), size, (const char*)buf); sendMessageCb_(conn, buf, size); // echo back } void onConnection(Connection* conn) // 符合NetServer::NetServer::ConnectionCallback的原型 { printf("Connection from %s:%d is %s/n", conn->ipAddr(), conn->port(), conn->connected() ? "UP" : "DOWN"); } private: SendMessageCallback sendMessageCb_;}; // 扮演上帝的角色,把各部件拼起来int main(){ NetServer server(7); EchoService echo(bind(&NetServer::sendMessage, &server, _1, _2, _3)); server.registerMessageCallback(bind(&EchoService::onMessage, &echo, _1, _2, _3)); server.registerConnectionCallback(bind(&EchoService::onConnection, &echo, _1)); server.run();} 对面向对象程序设计的影响一直以来,我对面向对象有一种厌恶感,叠床架屋,绕来绕去的,一拳拳打在棉花上,不解决实际问题。面向对象三要素是封装、继承和多态。我认为封装是根本的,继承和多态则是可有可无。用class来表示concept,这是根本的;至于继承和多态,其耦合性太强,往往不划算。 继承和多态不仅规定了函数的名称、参数、返回类型,还规定了类的继承关系。在现代的OO编程语言里,借助反射和attribute/annotation,已经大大放宽了限制。举例来说,JUnit 3.x 是用反射,找出派生类里的名字符合 void test*() 的函数来执行,这里就没继承什么事,只是对函数的名称有部分限制(继承是全面限制,一字不差)。至于JUnit 4.x 和 NUnit 2.x 则更进一步,以annoatation/attribute来标明test case,更没继承什么事了。 我的猜测是,当初提出面向对象的时候,closure还没有一个通用的实现,所以它没能算作基本的抽象工具之一。现在既然closure已经这么方便了,或许我们应该重新审视面向对象设计,至少不要那么滥用继承。 自从找到了boost::function+boost::bind这对神兵利器,不用再考虑类直接的继承关系,只需要基于对象的设计(object-based),拳拳到肉,程序写起来顿时顺手了很多。 对面向对象设计模式的影响既然虚函数能用closure代替,那么很多OO设计模式,尤其是行为模式,失去了存在的必要。另外,既然没有继承体系,那么创建型模式似乎也没啥用了。 最明显的是Strategy,不用累赘的Strategy基类和ConcreteStrategyA、ConcreteStrategyB等派生类,一个boost::function<>成员就解决问题。在《设计模式》这本书提到了23个模式,我认为iterator有用(或许再加个State),其他都在摆谱,拉虚架子,没啥用。或许它们解决了面向对象中的常见问题,不过要是我的程序里连面向对象(指继承和多态)都不用,那似乎也不用叨扰面向对象设计模式了。 或许closure-based programming将作为一种新的programming paradiam而流行起来。 依赖注入与单元测试前面的EchoService可算是依赖注入的例子,EchoService需要一个什么东西来发送消息,它对这个“东西”的要求只是函数原型满足SendMessageCallback,而并不关系数据到底发到网络上还是发到控制台。在正常使用的时候,数据应该发给网络,而在做单元测试的时候,数据应该发给某个DataSink。 安照面向对象的思路,先写一个AbstractDataSink interface,包含sendMessage()这个虚函数,然后派生出两个classes:NetDataSink和MockDataSink,前面那个干活用,后面那个单元测试用。EchoService的构造函数应该以AbstractDataSink*为参数,这样就实现了所谓的接口与实现分离。 我认为这么做纯粹是脱了裤子放屁,直接传入一个SendMessageCallback对象就能解决问题。在单元测试的时候,可以boost::bind()到MockServer上,或某个全局函数上,完全不用继承和虚函数,也不会影响现有的设计。 什么时候使用继承?如果是指OO中的public继承,即为了接口与实现分离,那么我只会在派生类的数目和功能完全确定的情况下使用。换句话说,不为将来的扩展考虑,这时候面向对象或许是一种不错的描述方法。一旦要考虑扩展,什么办法都没用,还不如把程序写简单点,将来好大改或重写。 如果是功能继承,那么我会考虑继承boost::noncopyable或boost::enable_shared_from_this,下一篇blog会讲到enable_shared_from_this在实现多线程安全的Signal/Slot时的妙用。 例如,IO-Multiplex在不同的操作系统下有不同的推荐实现,最通用的select(),POSIX的poll(),Linux的epoll(),FreeBSD的kqueue等等,数目固定,功能也完全确定,不用考虑扩展。那么设计一个NetLoop base class加若干具体classes就是不错的解决办法。 基于接口的设计这个问题来自那个经典的讨论:不会飞的企鹅(Penguin)究竟应不应该继承自鸟(Bird),如果Bird定义了virtual function fly()的话。讨论的结果是,把具体的行为提出来,作为interface,比如Flyable(能飞的),Runnable(能跑的),然后让企鹅实现Runnable,麻雀实现Flyable和Runnable。(其实麻雀只能双脚跳,不能跑,这里不作深究。) 进一步的讨论表明,interface的粒度应足够小,或许包含一个method就够了,那么interface实际上退化成了给类型打的标签(tag)。在这种情况下,完全可以使用boost::function来代替,比如:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253// 企鹅能游泳,也能跑class Penguin{ public: void run(); void swim();};// 麻雀能飞,也能跑class Sparrow{ public: void fly(); void run();};// 以 closure 作为接口typedef boost::function<void()> FlyCallback;typedef boost::function<void()> RunCallback;typedef boost::function<void()> SwimCallback;// 一个既用到run,也用到fly的客户classclass Foo{ public: Foo(FlyCallback flyCb, RunCallback runCb) : flyCb_(flyCb), runCb_(runCb) { } private: FlyCallback flyCb_; RunCallback runCb_;}; // 一个既用到run,也用到swim的客户classclass Bar{ public: Bar(SwimCallback swimCb, RunCallback runCb) : swimCb_(swimCb), runCb_(runCb) { } private: SwimCallback swimCb_; RunCallback runCb_;};int main(){ Sparrow s; Penguin p; // 装配起来,Foo要麻雀,Bar要企鹅。 Foo foo(bind(&Sparrow::fly, &s), bind(&Sparrow::run, &s)); Bar bar(bind(&Penguin::swim, &p), bind(&Penguin::run, &p));} 实现Signal/Slotboost::function + boost::bind 描述了一对一的回调,在项目中,我们借助boost::shared_ptr + boost::weak_ptr简洁地实现了多播(multi-cast),即一对多的回调,并且考虑了对象的生命期管理与多线程安全;并且,自然地,对使用者的类型不作任何限制,篇幅略长,留作下一篇blog吧。(boost::signals也实现了Signal/Slot,但可惜不是线程安全的。) 最后,向伟大的C语言致敬!]]></content>
<categories>
<category>转载</category>
</categories>
<tags>
<tag>function</tag>
<tag>bind</tag>
</tags>
</entry>
<entry>
<title><![CDATA[哈希表及其基本操作]]></title>
<url>%2F2017%2F04%2F18%2Fhash_basis%2F</url>
<content type="text"><![CDATA[0 哈希表概述 哈希表散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。 哈希函数把关键字集合K映像到一个有限的连续地址集D的映射关系H表示为:H(key):K->D,key∈K映射关系H称为哈希函数。 1 哈希函数构造法1.1 直接定址法1234int hash(int key){ return a * key + b; //a为缩放系数,b为平移系数} 优点:简单无冲突缺点: 空间浪费 1.2 保留余数法1234int hash(int key){ return key % p; //p取不大于地址长度m的最大素数} 1.3数字分析法、折叠法、平方取中法2 避免冲突2.1开放定址法 线性探测法1Hi = (H(key) + i) % m //1 ≤ i ≤ m-1 优点:算法简单缺点:容易产生堆聚现象 二次探测法12di = 1, -1, 4, -4, 9, -9 ...Hi = (H(key) + di) % m ////1 ≤ i ≤ m-1 2.2链地址法将关键西为同义词的记录链接在同一单链表中。1234567---------------------------------| | | | | | | | |--------------------------------- | |----- -----| | | |----- ----- 简单实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849typedef struct Node { RcdType r; struct Node *next;} Node;class hash_tabletypedef struct{ Node *rcd; int size; int count; int (*hash)(KeyType key, int hashSize);} hash_table;//初始化void create_hash_table(hash_table &table, int size, int (*hash)(KeyType, int){ table.rcd = (Node*)malloc(sizeof(Node) * size); table.size = size; table.hash = hash;}//查找Node *search_hash_table(hash_table &tab, int key){ int index = tab.hash(key, tab.size); for (Node *p = &tab.rcd[index], p != NULL; p = p->next) { if (p->r->key == key) return p; } return NULL;}//插入数据bool insert_hash_table(hash_table &tab, RcdType r){ if (search_hash_table(tab, r->key) == NULL) { int index = tab.hash(r.key, table.size); Node *p = (Node)malloc(sizeof(Node)); p->r = r; p->next = tab.rcd[p]; //插入表头 tab.rcd[p] = p; ++tab.count; return true; } return false;} 3.C++11哈希表——std::unordered_mapstd::unordered_map是C++新增的一个哈希表类型,和std::map类似,采用key-value结构,通过key快速索引到value。 3.1实现机制unordered_map内部实现了一个链式哈希表,即一个大bucket vecto挂着链表来解决冲突。 hash_map其插入过程是: 得到key 通过hash函数得到hash值 得到桶号(一般都为hash值对桶数求模) 存放key和value在桶内。 其取值过程是:得到key 通过hash函数得到hash值 得到桶号(一般都为hash值对桶数求模) 比较桶的内部元素是否与key相等,若都不相等,则没有找到。 取出相等的记录的value。 3.2 部分源码 unordered_map在bits/unordered_map.h头文件中定义,对哈希表的所有操作都是对成员_M_h进行操作。 12345template<...>class unordered_map { //... _Hashtable _M_h;}; _M_h是_Hashtable类型,定义在bits/hashtable.h头文件中。 Each _Hashtable data structure has: _Bucket[] _M_buckets _Hash_node_base _M_bbegin size_type _M_bucket_count size_type _M_element_count 源码如下:12345678910template<...>class _Hashtable { //...private: __bucket_type* _M_buckets; size_type _M_bucket_count; __before_begin _M_bbegin; size_type _M_element_count; _RehashPolicy _M_rehash_policy;}; 3.3 unordered_map用法 一个简单的使用——key类型为内置类型 123456789101112131415161718#include <iostream>#include <unordered_map>#include <string>int main(){ std::unordered_map<int, std::string> map; map.insert(std::make_pair(1, "one")); map.insert(std::make_pair(2, "tow")); map.insert(std::make_pair(3, "three")); auto iter = map.find(3); if (iter != map.end()) std::cout << "Found:" + iter->second; else std::cout << "Not found" << std::endl; return 0;} 高级用法——key类型为自定义类型使用自定义类型时,需要定义hash_value函数并且重载operator==。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152struct Person {public: //hash_value friend std::size_t hash_value(const Person &p) { std::size_t seed = 0; std::hash_combine(seed, std::hash_value(p.name)); //combine std::hash_combine(seed, std::hash_value(p.age)); return seed; } friend std::ostream& (std::ostream &os, const Person &p) { os << p.m_name << " " << p.m_age; return os; } Person(std::string name, int age) : m_name(name), m_age(age) {} //重载operator== bool operator==(const Person& p) const { return m_name == p.m_name && m_age == p.m_age; }private: std::string m_name; int m_age; };int main() { typedef std::unordered_map<person,int> umap; umap m; Person p1("Tom1",20); Person p2("Tom2",22); Person p3("Tom3",22); Person p4("Tom4",23); Person p5("Tom5",24); m.insert(umap::value_type(p3, 100)); m.insert(umap::value_type(p4, 100)); m.insert(umap::value_type(p5, 100)); m.insert(umap::value_type(p1, 100)); m.insert(umap::value_type(p2, 100)); for(umap::iterator iter = m.begin(); iter != m.end(); iter++) { std::cout << iter->first; } return 0; } 3.4 优缺点与map的比较 map内部使用红黑树来维护,该结构具有自动排序的功能,因此map内部的所有元素都是有序的。红黑树的每一个节点都代表着map的一个元素,因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了map的效率。插入、查找复杂度一般为O(logn)。 unordered_map,顾名思义,是一个无序map,内部实现了哈希表,因此插入、查找效率一般为O(1) 内存占用方面,map内存占用略低,unordered_map内存占用略高,而且是线性成比例的。 4 哈希表经典面试题 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url? 答案:可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。 遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,…,a999)中。这样每个小文件的大约为300M。 遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,…,b999)。这样处理后,所有可能相同的url都在对应的小文件(a0vsb0,a1vsb1,…,a999vsb999)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。 求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>hash</tag>
</tags>
</entry>
<entry>
<title><![CDATA[栈和队列及其基本操作]]></title>
<url>%2F2017%2F04%2F18%2Fstack_and_queue_basis%2F</url>
<content type="text"><![CDATA[1.栈(stack)如果一个线性结构只允许在序列末端进行操作,则称为栈,栈具有后进先出(Last in first out, LIFO)特点。按实现方式,栈分为顺序栈和链式栈两种。前者用一维数组实现,后者用链表实现。 1.1 顺序栈每当新元素入栈,栈顶索引向后移动,出栈栈顶索引前移。这种实现比较方便,不用移动元素,但是数组必须预先确定大小,若数组空间满了,就必须分配更大的空间进行扩容。12345678910111213141516171819#define MAX_SIZE 1024class stack{public: stack() : m_top(-1), mp_elem(new int[MAX_SIZE];) {} ~stack() { delete[] mp_elem; } void pop() { --m_top; } int top() { return mp_elem[m_top]; } bool push(int val) { if (m_top == MAX_SIZE - 1) return false; mp_elem[++m_top] = val; } bool empty() { return m_top == 0; } void clear() { m_top = 0; }private: int *mp_elem; int m_top;}; 1.2链式栈使用链表维护一个栈,链表头结点表示栈顶结点,每次入栈都新分配一个结点,插到头结点前面即可。123456789101112131415161718192021222324252627282930313233343536struct stack_node { stack_node(int val) : value(val), next(nullptr) {} int value; struct stack_node *next;};class stack { stack() : head(nullptr) {} ~stack() { clear(); } void push(int val) { struct stack_node *pv = new struct stack_node(val); pv->next = head; head = pv; } int top() { return head->val; } void pop() { struct stack_node *p = head; head = head->next; delete p; } bool empty() { return !head; } void clear() { struct struct stack_node *p = head; while (head) { p = head->next; delete head; head = p; } }private: struct stack_node *head;}; 1.3应用举例——括号匹配问题 Problem Description括号匹配序列允许()和[],嵌套顺序任意。例如’([])()’符合, ‘([)]’不符合。 1234567891011121314bool judge(const string &str){ stack s; for (int i = 0;i < str.length(); ++i) { if (str[i] == '(' || str[i] == ')') s.push(str[i]); else if (s.empty()) return false; else if (str[i] == '(' && s.top() == ')' || str[i] == '[' && s.top() == ']') return true; else return false; } return false;} 2.队列队列和栈类似,不同的是,队列是先入先出(First in first out, FIFO)队列一般使用循环结构实现,避免空间不足。例如,对于一个使用一维数组实现的队列。 2.1顺序循环队列1234//队列 {1, 2, 3, 4}----------------------------------| f 1 | 2 | 3 | 4 | rear |---------------------------------- 使用两个指针,front和rear分别指向队头和队尾后一个元素。每次插入元素,,元素出队,front指针向前移动。 插入入队时,rear指针向后移动,rear = (rear + 1) % MAX_SIZE,插入位置为rear 元素出队时,front指针向前移动,front = (front - 1 + MAX_SIZE) % MAX_SIZE。 判空条件:front = (rear + 1) % MAX_SIZE 判满条件:front = rear1234567891011121314151617181920212223242526#define MAX_SIZE 1024class queue {public: queue() : m_front(0), m_rear(1) : pm_elem(new int[MAX_SIZE];) {} ~queue() { delete pm_elem; } bool empty() { return m_front = (m_rear + 1) % MAX_SIZE; } bool push(int val) { if (m_front == m_rear) return false; pm_elem[rear] = val; m_rear = (m_rear + 1) % MAX_SIZE; } int front() { return pm_elem[front]; } int back() { return pm_elem[rear]; } bool pop() { if (empty()) return false; m_front = (m_front + 1) % MAX_SIZE; } private: int *pm_elem; unsigned int mfront; unsigned int m_rear;}; 2.2 链式队列采用链表实现循环队列,和上面差不多,使用两个指针front和rear分别指向链表头结点和尾结点。 判空条件:front == nullptr 3.常见问题3.1 实现一个栈,要求实现出栈,入栈,Min返回最小值的操作的时间复杂度为o(1) 思路:要使这些操作的时间复杂度为o(1),则必须保证栈的每个元素只被遍历一次。求解时需要借助两个栈,一个入数据,一个入所遍历过数据的最小值,遍历结束后,放最小值的栈的栈顶元素即为所求的最小值。 1234567891011121314151617181920212223242526272829303132#include <stack>#include <assert.h>template<typename T>class stackmin{ stackmin() {} void push(T val) { datastack.push(val); if (minstack.empty() || val < minstack.top()) minstack.push(val); else minstack.push(minstack.top()); } void pop() { assert_not_empty(); datastack.pop(); minstack.pop(); } int min() const { assert_not_empty(); return minstack.top(); }private: stack<T> datastack; stack<T> minstack; void assert_not_empty() { assert(datastack.size() > 0 && minstack.size() > 0); }}; 3.2 两个栈实现队列 思路:两个栈,一个入数据,一个出数据。对于入队栈,不论空不空,直接push;对于出队栈,若空则先把出队栈倒入入队栈,再出队。123456789101112131415161718192021template<typename T>class queue { queue() {}; ~queue() {}; void push(const T &t) { in_stack.push(t); } void pop() { if (out_stack.empty()) { while (!in_stack.empty()) { out_stack.push(in_stack.top()); in_stack.pop(); } } }private: stack<T> in_stack; stack<T> out_stack;}; 3.3两个队列实现栈 思路:一个队列作为数据队列,有数据入栈就入队,另一个作为辅助队列,每当出栈,将数据队列入到辅助队列,最后一个弹出。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152template <typename T>class CStack{public: CStack(void){}; ~CStack(void){}; void push(const T& node); T pop();private: queue<T> queue1; queue<T> queue2; };//插入元素template <typename T> void CStack<T>::push(const T& element){ if(queue1.size()>0)//如果queue1不为空则往queue1中插入元素 queue1.push(element); else if(queue2.size()>0)//如果queue2不为空则往queue2中插入元素 queue2.push(element); else//如果两个队列都为空,则往queue1中插入元素 queue1.push(element); }//删除元素template <typename T> T CStack<T>::pop(){ if(queue1.size()==0)//如果queue1为空 { while(queue2.size()>1)//保证queue2中有一个元素,将其余元素保存到queue1中 { queue1.push(queue2.front()); queue2.pop(); } T& data=queue2.front(); queue2.pop(); return data; } else//如果queue2为空 { while(queue1.size()>1)//保证queue2中有一个元素,将其余元素保存到queue1中 { queue2.push(queue1.front()); queue1.pop(); } T& data=queue1.front(); queue1.pop(); return data; }}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>stack</tag>
<tag>queue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[二叉树及其基本操作]]></title>
<url>%2F2017%2F04%2F18%2Fbinary_tree_basis%2F</url>
<content type="text"><![CDATA[0 概念1.二叉树(Binary Tree)是含有n个结点的有限集合,具有以下几个特点: 有且只有一个称(root)的节点 其余结点划分为两个互不相交的子集L和R,称为左子树和右子树 2.二叉树的一些基本概念有: 度:结点的孩子个数称为度,对于二叉树,度可为0,1,2 层次:从根结点开始定义,根结点为1,根的孩子为2,以此类推 深度:二叉树中最大层次称为二叉树的深 3.二叉树性质 度0结点 = 度2结点 + 1 … 4.完全二叉树与满二叉树 满二叉树:深度为k且结点数为2^k-1的二叉树 完全二叉树:深度为k,前k-1层为满二叉树,最后一层结点排列在左边。 1 实现123456789101112131415161718192021222324252627282930typedef struct btnode{ int val; struct btnode *left; struct btnode *right;} btnode, *btree;btree create(int val, btree lt, btree rt){ btree tree; tree = (btree)malloc(sizeof(btnode)); if (tree == NULL) return NULL; tree->val = val; tree->left = lt; tree->right = rt; return tree;}void destroy(btree t){ if (t == NULL) return; destroy(t->left); destroy(t->right); free(t); t = NULL;} 2 遍历1.1 中序遍历中序遍历即LDR,先遍历左子树,再遍历根,最后遍历右子树。 递归形式的LDR 12345678/*中序遍历递归版*/void ldr_rec(btree t, void (*visit)(int)){ if (t == NULL) return; ldr_rec(t->left, visit); visit(t->val); ldr_rec(t->right, visit);} 非递归形式的LDR 1234567891011121314151617181920212223/*中序遍历非递归版*/void ldr_nor(btree t, void (*visit)(int)) //先把所有左结点入栈,然后访问结点,最后转右子树{ if (t == NULL) return; btree p = t; std::stack<btnode*> s; while(!s.empty() || p) { if (p) { s.push(p); p = p->left; } else { p = s.top(); s.pop(); visit(p->val); p = p->right; } }} 1.2 先序遍历先序遍历即DLR,先访问根结点,然后访问左子树,最后访问右子树 递归形式的DLR 12345678/*先序遍历递归版*/void dlr_rec(btree t, void (*visit)(int)){ if(t == NULL) return; visit(t->val); dlr_rec(t->left, visit); dlr_rec(t->right, visit);} 非递归形式的DLR 1234567891011121314151617181920212223/*先序遍历非递归版*/void dlr_nor(btree t, void (*visit)(int)) //先访问结点,然后往左走,直到没有左子树,再转右子树{ if (t == NULL) return; btree p = t; std::stack<btnode*> s; while (!s.empty() || p) { if (p) { visit(p->val); s.push(p); p = p->left; } else { p = s.top(); s.pop(); p = p->right; } }} 1.3 后序遍历后续遍历即LRD,先访问左子树,再访问右子树,最后访问根结点。非递归实现难度在于,要判断此次遍历是从左子树返回还是右子树返回,如果是左子树返回,则应往右走,如果是右子树返回,则可以访问根结点。 递归形式的LRD 12345678/*后序遍历递归版*/void lrd_rec(btree t, void (*visit)(int)){ if (t == NULL) return; lrd_rec(t->left, visit); lrd_rec(t->right, visit); visit(t->val);} 非递归形式的LRD 123456789101112131415161718192021222324252627282930313233343536/*后序遍历非递归版*/void lrd_nor(btree t, void (*visit)(int)) //使用多一个指针记录上次访问位置,每次先访问最左,然后通过stack返回上层,与所记录的上次访问比较{ if (t == NULL) return; std::stack<btnode*> s; btree curr = t, last = NULL; //先把curr移动到最左 while(curr) { s.push(curr); curr = curr->left; } while(!s.empty()) { //curr == NULL curr = s.top(); s.pop(); if (curr->right == NULL || curr->right == last) { visit(curr->val); last = curr; } else { s.push(curr); //进入右子树,并将curr移动到最左 curr = curr->right; while(curr) { s.push(curr); curr = curr->left; } } }} 1.4 层次遍历层次遍历即每次遍历按从左往右的顺序遍历,使用队列即可解决。12345678910111213141516171819/*层次遍历*/void level_traverse(btree t, void (*visit)(int)){ if (t == NULL) return; btree p = NULL; std::queue<btnode*> que; que.push(t); while(!que.empty()) { p = que.front(); que.pop(); visit(p->val); if (p->left) que.push(p->left); if (p->right) que.push(p->right); }} 2 杂项问题2.1 二叉树最大深度 递归法 12345678910/*求树深度,递归实现*/int depth(btree t){ if (t == NULL) return 0; int ldepth = depth(t->left); int rdepth = depth(t->right); return 1 + std::max(ldepth, rdepth);} 非递归法受后序遍历启发,求树的深度实际上就是在LRD过程中,将访问结点改成修改深度值即可。对于左子树返回,则进入右子树,若为右子树返回,则记录下最大深度,然后深度-1,表示返回上一层。实际上,并不需要在后序遍历中记录深度值,LRD每次入栈都是入一层,进入左子树入栈,出来左子树出栈,进入右子树入栈,出来右子树出栈。也就是说,LDR过程中,栈的大小实际上就记录着二叉树的当前深度,只需要每次操作都记录最大深度即可。 1234567891011121314151617181920212223242526272829303132333435363738394041424344/*求树深度,非递归*/int depth_nor(btree t){ if (t == NULL) return 0; int d = 0, maxd = -1, maxstacksize = 0; btree curr = t, last = NULL; std::stack<btnode*> s; while(curr) { s.push(curr); curr = curr->left; //++d; 思路1 } while(!s.empty()) { curr = s.top(); s.pop(); if (curr->right == NULL || curr->right == last) { //返回上层 last = curr; //--d; 思路1 } else { //进入右子树 s.push(curr); curr = curr->right; while(curr) { s.push(curr); curr = curr->left; //++d; 思路1 } } //maxd = std::max(maxd, d); 思路1 maxstacksize = std::max(maxstacksize, (int)s.size()); } return maxstacksize;} 2.2 二叉树最小深度与二叉树的最大深度不同,不能单纯地使用递归,因为二叉树的深度必须是根结点到叶子结点的距离,不能单纯的比较左右子树的递归结果返回较小值,因为对于有单个孩子为空的节点,为空的孩子会返回0,但这个节点并非叶子节点,故返回的结果是错误的。因此,当发现当前处理的节点有单个孩子是空时,返回一个极大值INT_MAX,防止其干扰结果。123456789101112131415161718/*二叉树深度*/#define INT_MAX ((int)(~0U>>1))int minDepth(btree t){ if (t == NULL) return 0; if (t->left == NULL && t->right == NULL) return 1; int ld = minDepth(t->left) + 1; int rd = minDepth(t->right) + 1; //only one if (ld == 1) ld = INT_MAX; if (rd == 1) rd == INT_MAX; return std::min(ld, rd);} 2.3 二叉树宽度层次遍历中,每次都是逐行访问二叉树,这个过程其实就包含着二叉树的宽度。层次遍历中求二叉树宽度的思路是,每次循环都把上一行全部出队,把下一整行全部入队,入队前队列长度即为上一行宽度。12345678910111213141516171819202122232425/*二叉树宽度*/int width(btree t){ if (t == NULL) return 0; int maxwidth = 0; btree p = t; std::queue<btnode*> que; que.push(t); while (!que.empty()) { int n = que.size(); maxwidth = std::max(maxwidth, n); while (n--) { p = que.front(); que.pop(); if (p->left) que.push(p->left); if (p->right) que.push(p->right); } } return maxwidth;} 2.4 重建二叉树输入某二叉树的前序遍历和中序遍历的结果,输出原二叉树。思路:前序遍历的特点是最左边为父结点,右边分别是左子树和右子树。中序遍历的特点是中间为父结点,两边分别是左子树和右子树。如下图:123 前序遍历序列 中序遍历序列 { 1, 2, 4, 7, 3, 5, 6, 8 } { 4, 7, 2, 1, 5, 3, 8, 6 } fa----left----right---- ----left---fa---right---- 由此,可由前序遍历结果得到的父结点找到其在中序遍历的位置index,并构建相应的父结点,然后递归构建index左边的左子树和右边的右子树。1234567891011121314151617181920212223242526272829303132TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) { int i = 0; return getRoot(pre, vin, 0, vin.size(), i);} TreeNode* getRoot(vector<int> pre,vector<int> vin, int beg, int end, int &pi){ //find mi struct TreeNode* root = new TreeNode(0); int mid; if (beg == end - 1) { root->val = vin[beg]; return root; } else if (beg == end) { --pi; return NULL; } for (mid = beg; mid < end; ++mid) if (vin[mid] == pre[pi]) break; root->val = vin[mid]; root->left = getRoot(pre, vin, beg, mid, ++pi); root->right = getRoot(pre, vin, mid + 1, end, ++pi); return root; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>bitree</tag>
</tags>
</entry>
<entry>
<title><![CDATA[八大排序算法]]></title>
<url>%2F2017%2F04%2F18%2Feight_sort_algorithms%2F</url>
<content type="text"><![CDATA[0 排序算法概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。 1 插入排序——直接插入排序(Straight Insertion Sort)1.1算法思想将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。 1.2算法复杂度a. 时间复杂度:最好O(n),最坏O(n2) 1.3算法稳定性如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。 1.4算法实现123456789101112131415161718void straightInsertionSort(vector<int> &vec){ int sentry = vec[0], j;//哨兵 for (int i = 1;i < vec.size(); ++i) { if (vec[i] < vec[i-1]) { sentry = vec[i]; j = i; do { --j; vec[j+1] = vec[j];//后移 } while(vec[j] > sentry); vec[j+1] = sentry; } }} 2 插入排序——希尔排序(Shell’s Sort)2.1算法思想 希尔排序是将待排序列(R1, R2, R3, …, Rn)按照增量d划分为d个子序列,其中第i个子序列为(Ri, Ri+d, … Ri+kd),分别对子序列进行直接插入排序。直接插入排序每次最多移动一个位置,希尔排序则是每次对相隔较远的距离进行比较,使得能够移动时跨越多个记录,实现宏观调整。希尔排序最后一步增量为1,此时序列基本有序,最后一趟对整个序列进行一次直接插入排序,效率比较高。 2.2算法复杂度a.时间复杂度:希尔排序的时间复杂度和增量序列有关,最差O(n),最好O(nlogn)b.空间复杂度:O(1) 2.3算法稳定性希尔排序是一种不稳定的排序算法 2.4算法实现12345678910111213141516171819202122232425void shellinsert(vector<int> &vec, int dk){ int sentry = 0, j; for (int i = dk;i < vec.size(); ++i) { if (vec[i] < vec[i-dk]) { sentry = vec[i]; j = i - dk; vec[i] = vec[j]; while (j >= 0 && sentry < vec[j]) { vec[j+dk] = vec[j]; j -= dk; } vec[j+dk] = sentry; } }}void shellsort(vector<int> &vec, int d[], int n){ for (int i = 0;i < n; ++i) shellinsert(vec, d[i]);} 3 选择排序——简单选择排序(Simple Selection Sort)3.1算法思想 对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面“后一个元素”现变成了“前一个元素”,继续跟他的“后一个元素”进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。 3.2算法复杂度a.时间复杂度:最好O(n2),最坏O(n2),平均O(n2)b.空间复杂度:O(1) 3.3算法稳定性不稳定 3.4算法实现 123456789101112131415void selectionsort(vector<int> &vec){ int k = 0; for (int i = 0;i < vec.size() - 1; ++i) { k = i; for (int j = i + 1; j < vec.size(); ++j) { if (vec[k] > vec[j]) k = j; } if (i != k) swap(vec[i], vec[k]); }} 4 选择排序——堆排序(Heap Sort)5 交换排序——冒泡排序(Bubble Sort)5.1算法思想 冒泡排序算法的运作如下:(从后往前)1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。2.对每一对相邻元素作同样的工作,从开始第一对到结尾的3.最后一对。在这一点,最后的元素应该会是最大的数.针对所有的元素重复以上的步骤,除了最后一个。4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 5.2算法复杂度a.时间复杂度:最好O(n),最坏O(n2),平均O(n2)b.空间复杂度:O(1) 5.3算法稳定性稳定 5.4算法实现1234567891011void bubblesort(vector<int> &vec){ for (int i = 0;i < vec.size() - 1; ++i) { for (int j = i + 1; j < vec.size(); ++j) { if (vec[i] > vec[j]) swap(vec[i], vec[j]); } }} 6 交换排序——快速排序(Quick Sort)6.1 算法原理快速排序是一种采用分治策略的排序算法,其基本思想时: 从数组中选出一个数 把比这个数大的放在右边,小的放在左边 对两边区间进行同样的处理 6.2 算法实现对于一个区间[l, r],令i = l, j = r。每次先从第一个数即a[l]开始,将其作为轴,用X记录值,先从后面j开始,找到第一个比X小的数,填充到a[l],l++,然后从前面i开始找,找到第一个比X大的数,填充到a[j],j–。这样子就得到了新的[i, j]区间,区间外左边的数都比X小,区间外右边的数都比X大。然后重复以上过程,直到i==j也就是区间[i, j]只剩下一个数,这个数满足数组左边比他小,数组右边比他大。这样子就找到了第一个分割点。对分割点左右两边区间进行同样的操作,直到左右两边的区间都是一个数,算法结束。 基准数的选择一般是选择第一个作为基准,但是这样子其实是很糟糕的,对于有序序列反而没有帮助。因此一般基准都是按照三数中值分割法来选择:取左右中三个数的中值作为基准数。1234567891011121314151617181920212223242526272829//返回调整后基准数位置int adjust(int s[], int l, int r){ int i = l, j = r; int x = s[l]; while (i < j) { while (i < j && s[j] >= x) --j; if (i < j) s[i++] = s[j]; while (i < j && s[i] <= x) ++i; if (i < j) s[j--] = s[i]; } s[i] = x; return i;}void quickSort(int s[], int l, int r){ if (l < r) { int index = adjust(s, l, r); quickSort(s, l, index - 1); quickSort(s, index + 1, r); }} 6.3 算法复杂度 时间复杂度:最好O(nlogn),最坏O(n2),当数组是有序是,快速排序退化成冒泡排序 空间复杂度:O(1) 6.4算法稳定性快速排序时不稳定的排序算法 7 归并排序(Merge Sort)7.1算法原理归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 7.2算法实现首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。12345678910111213void merge(const vector<int> &v1, const vector<int> &v2, vector<int> &v){ int i = 0,j = 0, k = 0; while (i < v1.size() && j < v2.size()) { if (v1[i] < v2[j]) v[k++] = v1[i++]; else v[k++] = v2[j++]; } while (i < v1.size()) v[k++] = v1[i++]; while (j < v2.size()) v[k++] = v2[j++];} 可以看出合并有序数列的效率是比较高的,可以达到O(n)。 归并排序的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。12345678910111213141516171819202122232425262728293031323334//[beg, mid] (mid, end]归并void merge(vector<int> &v, int beg, int mid, int end, vector<int> &temp){ int i = beg, j = mid + 1, k = 0; while (i <= mid && j <= end) { if (v[i] <= v[j]) temp[k++] = v[i++]; else temp[k++] = v[j++]; } while (i <= mid) temp[k++] = v[i++]; while (j <= end) temp[k++] = v[j++]; for (int l = 0;l < k; ++l) v[beg + l] = temp[l];}//[beg, end]排序void msort(vector<int> &v, int beg, int end, vector<int> &temp){ if (beg < end) { int mid = (beg + end) / 2; msort(v, beg, mid, temp); msort(v, mid+1, end, temp); merge(v, beg, mid, end, temp); }}void mergesort(vector<int> &v){ vector<int> temp(v.size()); msort(v, 0, v.size() - 1, temp);} 7.3算法稳定性归并时,如果是判断if (v[i] <= v[j])则是稳定的,若为if (v[i] < v[j]),则不稳定。 7.4算法复杂度 时间复杂度:O(nlogn) 空间复杂度:O(n) 8 桶排序/基数排序(Radix Sort)8.1算法思想桶排序是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。 例如要对大小为[1..1000]范围内的n个整数A[1..n]排序 首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储(10..20]的整数,……集合B[i]存储( (i-1)10, i10]的整数,i = 1,2,..100。总共有 100个桶。 然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。 最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。 8.2算法复杂度假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是O(n + m * n/m*log(n/m)) = O(n + nlogn - nlogm)从上式看出,当m接近n的时候,桶排序复杂度接近O(n).当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。 8.3算法缺点前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是: 首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。 其次待排序的元素都要在一定的范围内等等。 9 总结 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>algorithms</tag>
</tags>
</entry>
<entry>
<title><![CDATA[全排列算法]]></title>
<url>%2F2017%2F04%2F18%2Fpermutation_algorithm%2F</url>
<content type="text"><![CDATA[0 描述给定一个字符串,求出所有的全排列。例如”abc”的全排列为”abc”,”acb”,”bac”,”bca”,”cab”,”cba”。 1 递归解法首先考虑bac和cba的由来,这两个都是abc的a和后面的两个交换而来;同理bca和cab分别由bac和cba的b和后面一个交换而来;acb由abc的b和后面一个交换而来。因此,对于递归的话,就是不断与后面的数交换的过程。不过这里还要注意的一点是避免交换重复的字符,也就是遇到重读直接跳过。12345678910111213141516171819202122232425262728vector<string> Permutation(string str) { vector<string> ret; permu(ret, str, 0); std::sort(str.beg(), str.end()); return ret;}void permu(vector<string> &vec, string &str, int beg){ if(beg == str.size()-1) { vec.push_back(str); //到底,保存此次结果 return; } for (int i = beg; i < str.size(); ++i) { if (i == beg || str[i] != str[beg]) //重复不交换 { std::swap(str[beg], str[i]); //交换 permu(vec, str, beg+1); //递归 std::swap(str[beg], str[i]); //复位 } }} 2 非递归解法非递归算法参考了stl的next_permutation算法实现,原理看的不太懂。先看看求出一个排列的下一个最小排列的算法:例如1342,从后往前找,找到第一个递增对,如这里是34,称第一个数为替换数,其坐标称为替换点。然后往后找比替换数大的最小数(一定存在),交换两者。例如这里1342变成1432。然后将替换点后的字符串前后颠倒。即得到1423,这个数就是1324的下一个最小全排列(原理不太懂)。所以整个算法的流程大致如下: 先给出str的最小排列,直接sort即可。 每次求出当前排列的下一最小排列,直到最大排列12345678910111213141516171819202122232425262728293031323334353637void swap(char *a, char *b){ char t = *a; *a = *b; *b = t;}void reverse(char *a, char *b){ while (a < b) swap(a++, b--);}bool next_permutation(char a[]){ char *pend = a + strlen(a); if (a == pend) return false; char *p, *q, *pfind; --pend; p = pend; while (p != a) { q = p--; if (*p < *q) //升序对 { pfind = pend; while (*pfind <= *p) --pfind; swap(pfind, p); reverse(q, pend); return true; } } reverse(p, pend); //最大排列数 return false; //表示到达最大} 3 标准库的std::next_permutation和std::prev_permutation12345678910111213#include <algorithm>template<class BidirectionalIterator>bool next_permutation( BidirectionalIterator _First, BidirectionalIterator _Last);template<class BidirectionalIterator, class BinaryPredicate>bool next_permutation( BidirectionalIterator _First, BidirectionalIterator _Last, BinaryPredicate _Comp ); 使用例子如下:1234567891011121314#include <algorithm>#include <cstring>int main(){ char a[] = "abc"; int len = strlen(a); do { printf("%s\n", a); } while (std::next_permutation(a, a+len)); return 0;}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>algorithms</tag>
</tags>
</entry>
<entry>
<title><![CDATA[堆及其基本操作]]></title>
<url>%2F2017%2F04%2F18%2Fheap_basis%2F</url>
<content type="text"><![CDATA[0 描述堆是具有以下特性的完全二叉树,其所有非叶子结点均不大于(或不小于)其孩子结点。 大顶堆所有非叶子结点均不小于其孩子结点 小顶堆所有非叶子结点均不大于其孩子结点 堆一般使用线性结构存储。12345678910class Heap{public://...private: int *m_data; int m_size; int m_length; bool (*cmp)(int, int);}; 1 堆的筛选操作堆的筛选作用是指定对根结点的子树进行堆特性的维护过程,并且假设子树满足堆特性。12345678910111213141516void shiftDown(int pos){ int c, rc; while(pos < m_length / 2) { c = pos * 2 + 1;//lchild rc = pos * 2 + 2;//rchild if (rc < m_length && cmp(m_data[rc], m_data[c])) c = rc; if (cmp(m_data[pos], m_data[c])) return; std::swap(m_data[pos], m_data[c]); pos = c;//向下调整 }} 分析:对于深度为k的完全二叉树,做一个筛选最多需要比较2(k-1)次。n结点完全二叉树最大深度logn + 1,所以筛选复杂度为O(logn)。 2 堆的插入操作插入时,先将元素插入到堆尾,这时,堆尾结点的双亲结点可能不满足堆特性,则堆尾与双亲交换,然后查看双亲结点是否符合堆特性,重复上述过程,直到符合堆特性或者到达根结点。1234567891011121314151617181920212223242526272829303132333435bool insert(int val){ if (m_length == m_size) return false; m_data[m_length] = val; int curr = m_length++; while (curr != 0) { int parent = (curr-1) / 2; if (cmp(m_data[parent], m_data[curr])) break; std::swap(m_data[parent], m_data[curr]); curr = parent; //向上调整 } return true;}``` 分析: 插入最多判断logn次,时间复杂度为O(logn)。## 3 堆的删除操作 堆只能删除根结点,删除过程如下:- 将尾结点复制到根结点,堆长度-1- 对根结点进行筛选 ```cppbool pop(){ if (m_length == 0) return false; std::swap(m_data[0], m_data[--m_length]); if (m_length > 0) shiftDown(0); return true;} 4 建堆操作 单个结点的完全二叉树满足堆特性 叶子结点满足堆特性由上,只需要按结点编号{n/2, n/2-1, … , 0}的次序对结点进行筛选即可。1234567891011121314151617Heap *create(int *a, int n, bool (*cmp)(int, int) = less_than){ if (a == nullptr || n < 0 || cmp == nullptr) return nullptr; int size = std::max(DEFAULT_SIZE, n); Heap *pheap = new Heap(size, cmp); pheap->m_length = size; for (int i = 0;i < n; ++i) pheap->m_data[i] = a[i]; for (int i = n/2 - 1; i >= 0; --i) shiftDown(i); return pheap;} 分析:建堆操作比较次数不超过4n,时间复杂度O(n) 5 全部代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596#ifndef HEAP_H__#define HEAP_H__#include <algorithm>#include <iostream>static bool less_equal(int lhs, int rhs) { return lhs <= rhs; }static bool greater_equal(int lhs, int rhs) { return lhs >= rhs; }#define DEFAULT_SIZE 128class Heap{public: Heap(int size = DEFAULT_SIZE, bool (*compare)(int, int) = less_equal) : m_data(new int[size]), m_size(size), m_length(0), cmp(compare) {} ~Heap() { delete[] m_data; } void shiftDown(int pos) { int c, rc; while(pos < m_length / 2) { c = pos * 2 + 1;//lchild rc = pos * 2 + 2;//rchild if (rc < m_length && cmp(m_data[rc], m_data[c])) c = rc; if (cmp(m_data[pos], m_data[c])) return; std::swap(m_data[pos], m_data[c]); pos = c;//向下调整 } } bool insert(int val) { if (m_length == m_size) return false; m_data[m_length] = val; int curr = m_length++; while (curr != 0) { int parent = (curr-1) / 2; if (cmp(m_data[parent], m_data[curr])) break; std::swap(m_data[parent], m_data[curr]); curr = parent; //向上调整 } return true; } bool pop() { if (m_length == 0) return false; std::swap(m_data[0], m_data[--m_length]); if (m_length > 0) shiftDown(0); return true; } Heap *create(int *a, int n, bool (*cmp)(int, int) = less_than) { if (a == nullptr || n < 0 || cmp == nullptr) return nullptr; int size = std::max(DEFAULT_SIZE, n); Heap *pheap = new Heap(size, cmp); pheap->m_length = size; for (int i = 0;i < n; ++i) pheap->m_data[i] = a[i]; for (int i = n/2 - 1; i >= 0; --i) shiftDown(i); return pheap; } //debug friend std::ostream& operator<<(std::ostream &os, const Heap &heap) { for (int i = 0;i < heap.m_length; ++i) os << heap.m_data[i] << " "; return os; }private: int *m_data; int m_size; int m_length; bool (*cmp)(int, int);};#endif]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>bitree</tag>
<tag>heap</tag>
</tags>
</entry>
<entry>
<title><![CDATA[GDB调试工具入门]]></title>
<url>%2F2017%2F04%2F17%2Fgdb_basis%2F</url>
<content type="text"><![CDATA[0 gdb介绍调试器GDB允许查看在执行一个程序时其内部时发生了什么,或者是程序奔溃(crashed)时它正在做什么。gdb通过以下四种事情来捕获某个行为的异常错误(bug): 运行程序,指定可能影响其动作的内容。 让程序在指定的情况下停止。 检查当程序停止时发生了什么。 改变程序中的内容,以便于更正一个错误,然后继续寻找下一个错误。 gdb可用于调试C,C++,Fortran,Modula-2。gdb通过在终端执行gdb命令激活。一旦启动,他从命令行读取命令,直到使用quit命令让它退出。也可以通过使用help command命令获取在线帮助。gdb可以使用无参或者带参无选项执行,不过一般都使用带参的命令,例如:指定一个程序或者指定core文件:1234gdb program #指定一个可执行程序gdb program core #指定一个可执行程序以及core文件gdb program pid #指定一个正在运行的程序gdb -p pid #同上 1 gdb常用命令1.1 listlist命令用于查看代码,可简写为l。1234567list #查看上一次list中心附近的10行代码,-5~+5list n #查看第n行附近10行代码,n-5~n+5list b,e #查看b,e行范围的代码list function #查看函数function附近的代码list file:line #查看文件file第line行附件的代码list file:function #查看文件file的函数function附近的代码list *address #地址为address的行附近的代码,使用info add name获取地址 1.2 breakbreak命令用于设置断点,可用b简化;delete用于删除断点,可用d简化。123456break n #在第n行设置断点break function #在函数function设置断点,可以是库函数break file:line #在文件file第line行设置断点break file:function #在文件file的function函数设置断点break n if condition #根据条件在第n行设置断点,例如b 16 if i==10break *address #在地址为address的行设置断点 1.3 delete和clear每次使用break设置断点都会分配一个断点号,例如:1234(gdb) b 16Breakpoint 1 at 0x400512: file test.cc, line 16.(gdb) b 17Breakpoint 2 at 0x40051b: file test.cc, line 17. 要删除断点使用可以使用delete命令:123delete [breakpoints num] [range...]delete n #删除n号断点delete m-n #删除m-n号断点 也可以使用clear命令,clear是基于行的,不是删除所有断点:1234clear n #删除n行的所有断点clear function #删除函数function的断点clear file:line #删除文件file第n行的所有断点clear file:function #删除文件:函数的所有断点 1.4 查看变量 print命令print命令用来在调试程序时查看变量值,可简化为p。 12345678910111213print var #打印var的值print *array@len #以{a, b, ...}格式打印动态数组print array #以{a, b, ...}格式打印静态数组print file::var or print function::var #打印全局变量#指定输出格式:print/u[d|o|x...] ... #作为无符号数输出print/t ... #二进制输出print/d ... #十进制输出print/o ... #八进制输出print/x ... #十六进制输出print/a ... #十六进制输出print/c ... #字符输出print/f ... #浮点数输出 display命令 1display var #每次执行到断点都打印var的值 1.5 程序执行时命令 run命令run命令用来开始执行程序,直到第一个断点停止程序。可简写为r。例如: 1234(gdb) break 20Breakpoint 1 at 0x40056d: file test.cc, line 20.(gdb) runStarting program: /home/chenjunhan/Learning/gdb/test Breakpoint 1, main () at test.cc:20 continue命令continue命令用来从上次断点停止之后开始继续执行程序,直到下一个断点。可简写为c。例如: 12345678910(gdb) b 15Breakpoint 1 at 0x40050b: file test.cc, line 15.(gdb) b 16Breakpoint 2 at 0x400512: file test.cc, line 16.(gdb) rStarting program: /home/chenjunhan/Learning/gdb/testBreakpoint 1, main () at test.cc:15(gdb) continueContinuing.Breakpoint 2, main () at test.cc:16 next命令next命令用于执行一行源程序代码,此行代码中的函数调用也一并执行;即“step over”,可简写为n。 step命令step命令用于执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;即“step into”,可简写为s。 2 gdb调试函数 列出可执行函数所有函数名称 1info|i functions 执行函数内容 123next|n #不进入函数,直接执行函数并返回step|s #进入函数,执行函数体call|print function #任意位置直接执行函数 退出函数 12finish #退出正在执行的函数,自动执行剩下的代码return [expression] #退出正在执行的函数,后面的代码不执行,可以通过expression修改函数返回值 函数堆栈帧 12345info|i frame #显示当前函数堆栈帧信息,包括指令寄存器的值,局部变量地址及值等信息。backtrace|bt #显示堆栈帧层次结构frame n|address #切换函数堆栈帧层次up|down n #向上/下选择n层函数堆栈帧up-silently|down-silently n #同上,不过不打印信息 3 gdb设置watchpointgdb可以使用watch命令设置观察点,也就是当一个变量值发生变化时,程序会停下来。例如:12345678910111213141516int a = 0;void *pthread_func(void *args){ while (1) { ++a; sleep(1); }}int main(){ pthread_t tid; pthread_create(&tid, NULL, pthread_func, NULL); sleep(1000); return 0;} 使用watch|wa观察全局变量a:12345678910111213141516171819202122232425262728(gdb) file catchReading symbols from catch...done.(gdb) break pthread_funcBreakpoint 1 at 0x400659: file catch.c, line 11.(gdb) watch aHardware watchpoint 2: a(gdb) rStarting program: /home/chenjunhan/Learning/gdb/catch [Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".[New Thread 0x7ffff77f6700 (LWP 6578)][Switching to Thread 0x7ffff77f6700 (LWP 6578)]Breakpoint 1, pthread_func (args=0x0) at catch.c:1111 ++a;(gdb) continueContinuing.Hardware watchpoint 2: aOld value = 0New value = 1pthread_func (args=0x0) at catch.c:1212 sleep(1);(gdb) continueContinuing.Breakpoint 1, pthread_func (args=0x0) at catch.c:1111 ++a; 设置观察点 12watch var #为var设置观察点,在run之前watch *(type*)(address) #为地址address指向的变量设置观察点 列出所有观察点 1info watchpoints 断点控制命令 1disable|enable|delete n 为watch指定生效线程 12info threads #列出所有threads编号等信息,thread_create之后watch var thread t_num #只有编号为t_num的thread修改了var值才会停下来 只读/读写观察点,只对硬件观察点才生效 12rwatch|rw var #设置只读观察点每次访问var都会停下来wwatch|aw var #设置读写观察点,当发生读取或改变变量值的行为时,程序就会暂停住 4 gdb设置catchpoint使用gdb调试程序时,可以用tcatch命令设置catchpoint只触发一次。例如:12345678910111213141516int main(){ pid_t pid; int i = 0; for (i = 0;i < 4; ++i) { if ((pid = fork()) < 0) //fork error exit(1); else if (pid == 0) //child exit(0); } printf("Hello World\n");//parent return 0;} 使用tcatch fork为fork设置catchpoint123456789101112(gdb) tcatch forkCatchpoint 1 (fork)(gdb) rStarting program: /home/chenjunhan/Learning/gdb/catch Temporary catchpoint 1 (forked process 6838), 0x00007ffff7ad5ee4 in __libc_fork () at ../nptl/sysdeps/unix/sysv/linux/x86_64/../fork.c:130130 ../nptl/sysdeps/unix/sysv/linux/x86_64/../fork.c: 没有那个文件或目录.(gdb) cContinuing.Hello World[Inferior 1 (process 6834) exited normally] 为fork/vfork/exec设置catchpoint 123catch forkcatch vforkcatch exec 为系统调用设置catchpoint 12catch syscall [syscall_name|syscall_num] #例如catch syscall mmapcatch syscall #为所有系统调用设置catchpoint 5 gdb高级打印技巧5.1 打印局部变量 使用backtrace full命令 1234567891011(gdb) bt full#0 fun_a () at a.c:6 a = 0#1 0x000109b0 in fun_b () at a.c:12 b = 1#2 0x000109e4 in fun_c () at a.c:19 c = 2#3 0x00010a18 in fun_d () at a.c:26 d = 3#4 0x00010a4c in main () at a.c:33 var = -1 使用info locals命令,打印当前函数局部变量 12(gdb) info localsa = 0 5.2 打印打印进程内存信息123info proc mappings #查看内存映像信息info files #更详细地输出进程的内存信息,包括引用的动态链接库等info target #功能同上 5.3 打印变量类型123456789101112typedef struct student_t{ char *name; int age;} student;int main(){ student s; student *ps; return 0;} 使用wathis和ptype命令调试,ptype可以列出详细的类型。123456789(gdb) whatis stype = student(gdb) whatis pstype = student *(gdb) ptype stype = struct student_t { char *name; int age;} 5.4 打印变量所在文件1info variables var 6 gdb应用于多进程/线程6.1 多进程调试a.调试正在运行的进程12345678910#没启动gdbps -a | grep program #查看进程idgdb program pid #调试gdb --pid pid #同上#启动gdbattach pid#断开detach b.调试子进程gdb调试默认追踪父进程,要追踪子进程,则执行下列命令:1set follow-fork-mode child c.同时调试父进程和子进程gdb调试时,默认追踪一个进程,使用下列命令可以同时调试多个进程:12set detach-on-fork off #默认运行父进程,挂起其他进程set schedule-multiple on #父进程,子进程一起运行 之后gdb默认追踪父进程,其他进程被挂起,使用下列命令可以切换进程:1234inferior infno #切换到子进程info inferior #打印所有进程信息inferior n #选择切换调试进程 6.2 多线程调试a.显示线程信息 thread|_thread 查看当前线程id 1234(gdb) thread[Current thread is 1 (Thread 0x7ffff7fcc740 (LWP 9494))](gdb) printf "current thread:%d\n",$_threadcurrent thread:1 info threads 查看所有线程信息 12345(gdb) info threadsId Target Id Frame3 Thread 0x7ffff6ff5700 (LWP 9627) "thread" 0x00007ffff78b7dfd in nanosleep () at ../sysdeps/unix/syscall-template.S:812 Thread 0x7ffff77f6700 (LWP 9626) "thread" 0x00007ffff78b7dfd in nanosleep () at ../sysdeps/unix/syscall-template.S:811 Thread 0x7ffff7fcc740 (LWP 9622) "thread" main () at thread.cc:21 b.切换线程1thread id c.只允许一个线程执行gdb调试程序时,一旦程序断住,所有线程都会停止。当调试其中一个线程时,所有线程都会开始执行。如果想调试一个线程同时其他线程停止,可用以下命令:1set scheduler-locking on 6.3 调试多个程序12345678910#添加一个新的调试程序。-copies指定执行多少份,默认1add-inferior [-copies n] [-exec program]#复制添加一个新的调试程序,infno表示调试进程编号,默认为当前inferiorclone-inferior [-copies -n] [infno]info inferior #列出所有调试的进程maint info program-spaces #列出所有调试的进程名称,inferior编号,进程idinferior n #切换进程 7 gdb分析core dump7.1 core文件程序由于各种异常或者bug导致在运行过程中异常退出或者中止,并且在满足一定条件下(这里为什么说需要满足一定的条件呢?下面会分析)会产生一个叫做core的文件。通常情况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成第一个文件,许多的程序出错的时候都会产生一个core文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。ubuntu默认不生成core文件,可用一下命令解决:1ulimit -c unlimited 7.2 gdb显式生成core文件12generate-core-file #先rungcore #上面的简写 7.3 使用core文件进行调试示例程序:1234567int main(){ int *p = NULL; *p = 0; return 0;} 上面程序当执行*p = 0时程序会crashed,产生core dump file,下面使用gdb进行调试:1234567891011121314151617#shell环境加载core文件gdb test corefile#同上,gdb环境中file testcore corefile#gdb加载core file之后打印的信息[New LWP 9672]Core was generated by `./test'.Program terminated with signal SIGSEGV, Segmentation fault.#0 0x000000000040054b in main () at test.c:88 *p = 0;#也可以使用where命令定位错误位置(gdb) where#0 0x000000000040054b in main () at test.c:8 参考文献]]></content>
<categories>
<category>Linux编程</category>
</categories>
<tags>
<tag>gdb</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C++虚函数表原理]]></title>
<url>%2F2017%2F04%2F16%2Fcpp_virtual_functions%2F</url>
<content type="text"><![CDATA[前言C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。 虚函数表C++中,虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。 1 虚函数表示例C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。例如有个如下的类:123456789101112class Base{public: virtual void foo() { cout << "Base::foo" << endl; } virtual void bar() { cout << "Base::bar" << endl; }}; 按照上面的说法,可以通过如下方式获取Base实例化对象的虚函数表:123Base b;cout << "虚函数表地址: " << (long*)(&b) << endl;cout << "第一个虚函数地址: " << (long*)*(long*)(&b) << endl; 输出结果为:虚函数表地址: 0x7ffcc0c04c80第一个虚函数地址: 0x400c20通过上面分析,其实就可以通过访问虚函数表直接访问虚函数:123456789typedef void(*pFunc)();long *vptr = (long*)(&b);long *pVTable = (long*)(*vptr);pFunc baseFoo = (pFunc)*(pVTable);pFunc baseBar = (pFunc)*(pVTable+1);baseFoo(); //Base::foobaseBar(); //Base::bar 2 虚函数表结构下图展示了C++虚函数表的具体结构:如图所示,可知: 对象实例保存了一个虚函数表地址指针vptr,指向单元存放的是虚函数表的真实地址,可以用如下代码表示: long *vptr = (long*)(&b); long *pVTable = (long*)(*vptr); 虚函数表是一个连续的地址空间,每一项存储一个虚函数的地址,按照函数声明顺序排列,可以用如下代码表示: pFunc base_foo = (pFunc)*(pVTable); pFunc base_bar = (pFunc)*(pVTable+1); 若对象有n个虚函数,实际上虚函数表有n+1项,其中vptr指出的是第一项的地址,也就是第一个虚函数。第0项一般用作保存对象的*type_info信息,用于保存该对象的实际类型。具体可用于dynamic_cast等,可用如下代码表示: const type_info *pti = (const type_info *)*(pVTable - 1) 虚函数表实现多态1 单继承的情况考虑如下代码:12345678910111213141516class Derive : public Base{public: void foo() { cout << "Derive::foo" << endl; } virtual void foo1() { cout << "Derive::foo1" << endl; } virtual void bar1() { cout << "Derieve::bar1" << endl; }}; 对于一个Base类型的对象b和一个Derive类型的对象d,他们的虚函数表分别是:1234b-vtable: | type_info | Base::foo | Base::bar |d-vtable: | type_info | Derive::foo | Base::bar | Deriev:foo1 | Derive::bar1 | 可以看到Derive重写了foo虚函数之后,虚函数表的相应位置被替换为Derive::foo,这个时候访问foo函数实际就访问了Derive::foo而不是Base::foo。例如:12Base *pb = new Derive();pb->foo(); //Derive::foo 2 多继承的情况12345678910111213141516171819class Base1{public: virtual void base1_foo() { cout << "Base1::base1_foo() << endl; } virtual void base1_bar() { cout << "Base1::base1_bar() << endl; }}class Base2{public: virtual void base2_foo() { cout << "Base2::base1_foo() << endl; } virtual void base2_bar() { cout << "Base2::base2_bar() << endl; }}class Derive : public Base1, public Base2{public: void base1_foo() { cout << "Derive::base1_foo() << endl; } void base2_foo() { cout << "Derive::base2_foo() << endl; }} 则Derive对象实际含有的是2个vptr指针,也即含有两个虚函数表,顺序按照继承顺序:1234vptr1 --> vtable1: | type_info | Derive::base1_foo | Base::base1_bar |vptr2 --> vtable2: | type_info | Derive::base2_foo | Base::base2_bar |]]></content>
<categories>
<category>C++</category>
</categories>
<tags>
<tag>虚函数</tag>
</tags>
</entry>
<entry>
<title><![CDATA[[NP1]网络编程常用函数]]></title>
<url>%2F2017%2F03%2F07%2Fnp1_common_functions%2F</url>
<content type="text"><![CDATA[POSIX标准的套接字结构 1.套接字地址结构1.1 IPv4套接字地址结构123456789101112#include <netinet/in.h>struct in_addr {in_addr_t s_addr; //32-bit IPv4 address};struct sockaddr_in {uint8_t sin_len; //length of strcut(16)sa_family_t sin_family; //AF_INETin_port_t sin_port;struct in_addr sin_addr;char sin_zero[8]; //unused} 1.2 通用套接字地址结构123456#include <sys/socket.h>struct sockaddr {uint8_t sa_len;sa_family_t sa_family;char sa_data[14]; //protocol-specific address}; 1.3 IPv6套接字地址结构123456789101112131415#include <netinet/in.h>struct in6_addr {uint8_t s6_addr[16]; //128-bit IPv6 address};#define SIN6_LENstruct sockaddr_in6 {uint8_t sin6_len; //length of strcut(28)sa_family_t sin6_family; //AF_INET6in_port_t sin6_port;uint32_t sin6_flowinfo;strcut in6_addr sin6_addr;uint32_t sin6_scope_id;}; 1.4 新的通用套接字地址结构12345#include <netinet/in.h>struct sockaddr_storage {uint8_t ss_len;sa_family_t family;}; 2. 字节排序函数1234567#include <netinet/in.h>//返回网络字节序uint16_t htons(uint16_t host16bitvalue);uint32_t htonl(uint32_t host32bitvalue);//返回主机字节序uint16_t ntohs(uint16_t net16bitvalue);uint32_t ntohl(uint32_t net32bitvalue); 以上h指host,n指net,s指short,l指long 3. 字节操纵函数1234#include <string.h>void bzero(void *dest, size_t nbytes);void copy(const void *src, const void *ptr2, size_t nbytes);int bcmp(const void *ptr1, const void *ptr2, size_t nbytes); //相等返回0 4.地址转换函数4.1 IPv4地址转换12345#include <arpa/inet.h>int inet_aton(const char *strptr, struct in_addr *addrptr); //字符有效返回1,否则0//有效32-bit二进制网络字节序IPv4地址,否则为INADDR_NONEin_addr_t inet_addr(const chat *strptr);char *inet_ntoa(struct in_addr inaddr); //返回点分十进制字符串 以上a指address,n指net,上述只适用于IPv4地址转换 4.2 通用地址转换123456789#include <atpa/inet.h>//有效返回1,无效返回0,出错返回-1int inet_pton(int family, const char strptr, void *addrptr);//出错返回NULLchar *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //其中len也可指定为以下值#include <netinet/in.h>#define INET_ADDRSTRLEN 16#define INET6_ADDRSTRLEN 46 4.3 I/O操作函数12345#include <unistd.h>//返回:读到的字节数,若已到文件尾为 0,若出错为- 1ssize_t read(int filedes, void *buff, size_t nbytes); //返回:若成功为已写的字节数,若出错为- 1ssize_t write(int filedes, void *buff, size_t nbytes);]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>unp</tag>
</tags>
</entry>
<entry>
<title><![CDATA[sleep函数的几种实现]]></title>
<url>%2F2016%2F10%2F14%2Fimplements_of_linux_sleep%2F</url>
<content type="text"><![CDATA[使用select、polsignal三种方式实现sleep函数 1.使用select实现12345678910111213141516171819202122232425262728/* 使用select实现sleep函数 int select (int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *tvptr); struct timeval{ long tv_sec; // seconds long tv_usec; // and microseconds };*/#include <sys/types.h> /* fd_set data type */#include <sys/time.h> /* struct timval */#include <stddef.h> /**/#include <ourhdr.h>#define MAGNIFICATION 1000000void sleep(unsigned int nusecs){ struct timeval tval; tval.tv_sec = nusecs / MAGNIFICATION; tval.tv_sec = nusecs % MAGNIFICATION; /*接受到信号或者时间到期*/ select(0, NULL, NULL, &tval);} 2.使用poll实现1234567891011121314151617181920212223242526272829/* 使用poll实现sleep函数(time 为毫秒) int poll(struct pollfd fdarry[], unsigned lonng fds, int timeout); struct pollfd { int fd; // file descriptor to check, or < 0 to ignore short events; //events of interest on fd short revents; // events that occurred on fd } ;*/#include <stropts.h>#include <poll.h>#define MAGNIFICATION 1000void sleep(unsigned int nusecs){ struct pollfd dummy; int timeout; if ( (timeout = nusecs / MAGNIFICATION) <= 0) timeout = 1; /*接受到信号或者时间到期*/ poll(&dummy, 0, timeout);} 3.使用signal实现123456789101112131415161718192021222324252627282930313233343536373839404142434445/* 使用sigation实现sleep函数*/#include <signal.h>#include <stddef.h>#include <ourhdr.h>static void sig_alrm(void){ return; //nothing to do}unsigned int sleep(unsigned int nsecs){ struct sigaction newact, oldact; sigset_t newmask, oldmask, suspmask; unsigned int unslept; newact.sa_handler = sig_alrm; sigemptyset(&newact.sa_mask); newact.sa_falgs = 0; sigaction(SIGALRM, &newact, &oldact); sigemptyset(&newmask); sigaddset(&newmask, SIGALRM); /*block SIGALRM and save current signal mask*/ sigprocmask(SIG_BLOCK, &newmask, &oldmask); alarm(nsecs); suspmask = oldmask; sigadelset(&suspmask, SIGALRM); //make sure SIGALRM isn't blocked sigsuspend(&suspmask); //wait for ang signal to be caught unslept = alarm(0); sigaction(SIGALRM, &oldact, NULL); //reset previous action sigprocmask(SIG_SETMASK, &oldmask, NULL); return unslept;}]]></content>
<categories>
<category>Linux编程</category>
</categories>
<tags>
<tag>apue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[getpasss的实现]]></title>
<url>%2F2016%2F10%2F09%2Fimplement_of_getpasss%2F</url>
<content type="text"><![CDATA[APUE 11.10 getpass函数的实现 注意点 调用函数ctermid打开控制终端,而不是直接将/dev/tty写在程序中。 只是读、写控制终端,如果不能以读、写方式打开此设备则出错返回。 阻塞两个信号SIGINT和SIGTSTP。如果不这样做,则在输入INTR字符时就会使程序终止,并使终端仍处于禁止回送状态。与此相类似,输入SUSP字符时将使程序暂停,并且在禁止回送状态下返回到shell。 最多只可取8个字符作为口令。输入的多余字符则被忽略。 源码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#include <signal.h>#include <stdio.h>#include <termios.h>#include <stdlib.h>#define MAX_PASS_LEN 8char *getpass(const char *prompt){ static char buf[MAX_PASS_LEN + 1]; char *ptr; sigset_t sig, sigsave; struct termios term, termsave; FILE *fp; int c; if ( (fp = fopen(ctermid(NULL), "r+")) == NULL ) return NULL; setbuf(fp, NULL); //set not buffer /*block sigint & sigtstp*/ sigemptyset(&sig); sigaddset(&sig, SIGINT); sigaddset(&sig, SIGTSTP); sigprocmask(SIG_BLOCK, &sig, &sigsave); tcgetattr(fileno(fp), &termsave); term = termsave; term.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); tcsetattr(fileno(fp), TCSAFLUSH, &term); fputs(prompt, fp);//output prompt ptr = buf; while ( (c = getc(fp)) != EOF && c != '\n' ) { if (ptr < &buf[MAX_PASS_LEN]) //abandon others character { *ptr = c; ptr++; } } *ptr = '\0'; putc('\n', fp); tcsetattr(fileno(fp), TCSAFLUSH, &termsave); sigprocmask(SIG_SETMASK, &sigsave, NULL); fclose(fp); return buf;}int main(){ char *pass; pass = getpass("Please input the password: "); printf("PASS: %s\n", pass); exit(0);}]]></content>
<categories>
<category>Linux编程</category>
</categories>
<tags>
<tag>apue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu 系统美化]]></title>
<url>%2F2016%2F09%2F10%2Fubuntu_beautify%2F</url>
<content type="text"><![CDATA[主题美化这里使用的是Flatabulous主题。 1. 安装 Ubuntu tweak tool123sudo add-apt-repository ppa:tualatrix/ppasudo apt-get updatesudo apt-get install ubuntu-tweak 2. 安装flatabulous-theme123sudo add-apt-repository ppa:noobslab/themessudo apt-get updatesudo apt-get install flatabulous-theme 3. 安装ultra-flat-icons123sudo add-apt-repository ppa:noobslab/iconssudo apt-get updatesudo apt-get install ultra-flat-icons Alternatively, you could also run sudo apt-get install ultra-flat-icons-orange OR sudo apt-get install ultra-flat-icons-green. 4. 加载主题按Super键,搜索Ubuntu Tweak,主题选择Flatabulous,图标选择ultra-flat-icons,重启即可。 Shell 美化Shell是Linux/Unix的一个外壳,它负责外界与Linux内核的交互,接收用户或其他应用程序的命令,然后把这些命令转化成内核能理解的语言,传给内核。Linux/Unix提供了很多种Shell,常用的Shell有这么几种,sh、bash、csh等,想知道你的系统有几种shell,可以通过以下命令查看:1cat /etc/shells 显示如下:123456/bin/bash/bin/csh/bin/ksh/bin/sh/bin/tcsh/bin/zsh 1. 安装zsh1sudo apt-get install zsh 安装完成后设置当前用户使用 zsh:1chsh -s /bin/zsh 根据提示输入当前用户的密码就可以了,如果执行上面命令没效果的话,再执行:vim ~/.bashrc,在末尾添加bash -c zsh,重启终端即可。 2.安装oh-my-zsh需要先安装Git,然后执行以下指令自动安装oh-my-zsh:1wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh 2. 修改zsh主题oh my zsh 提供了数十种主题,相关文件在~/.oh-my-zsh/themes目录下。在~/.zshrc里找到ZSH_THEME,就可以设置主题了。如设置ZSH_THEME="ys"。]]></content>
<categories>
<category>系统优化</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[chapter5 标准IO库]]></title>
<url>%2F2016%2F09%2F03%2Fapue_c5_standard_io%2F</url>
<content type="text"><![CDATA[《Advanced Programming in the UNIX Environment》第五章——文标准IO库课后习题答案 Exercise 5.1用setvbuf完成setbuf。Ans1234567891011121314151617181920212223242526272829303132333435363738394041//int setvbuf(FILE *fp, char *buf, int mode, size_t size)int SetBuf(FILE *fp, char *buf){ if (NULL == fp) return -1; if (NULL == buf) if ( setvbuf(fp, buf, _IONBF, 0) != 0 ) { printf("setvbuf error(buf == NULL)"); return -1; } else { if (fp == stderr) { if ( setvbuf(fp, buf, _IONBF,BUFSIZ) !=0 ) { printf("setvbuf error(fp == stderr)"); return -1; } } else if (fp == stdin || fp == stdout) { if ( setvbuf(fp, buf, _IOLBF, BUFSIZ) != 0 ) { printf("setvbuf error(fp == stdin || fp == stdout)"); return -1; } } else { if (setvbuf(fp, buf, _IOFBF, BUFSIZ) != 0 ) { printf("setvbuf error(others side)"); return -1; } } } return 0;} Exercise 5.4下面的代码在一些机器上运行正确,而在另外一些机器运行时出错,解释问题所在。123456789#include <stdio.h>int main(void){ char c; while ( (c = getchar()) != EOF ) putchar(c); exit(0);} Ans这是一个比较常见的错误。getc以及getchar的返回值是整型,而不是字符型。由于EOF经常定义为-1,那么如果系统使用的是有符号的字符类型,程序还可以正常工作。但如果使用的是无符号字符类型,那么返回的EOF被保存到字符c后将不再是-1,所以,程序会进入死循环。 int getchar(void);int getc(FILE *fp);若成功则为下一个字符,若已处文件尾端或出错则为EOF]]></content>
<categories>
<category>Linux编程</category>
</categories>
<tags>
<tag>apue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[apue chapter4 文件和目录]]></title>
<url>%2F2016%2F09%2F03%2Fapue_c4_files_and_directories%2F</url>
<content type="text"><![CDATA[《Advanced Programming in the UNIX Environment》第四章——文件和目录课后习题答案 Exercise4.2表4-1指出SVR4没有提供宏S_ISLNK,但是SVR4支持符号连接并且在中定义了SIFLNK,如何修改ourhdr.h使得需要SISLNK宏的程序可以使用它?Ans将下面的几行语句加入123#if defined (S_IFLNK) && !defined(S_ISLNK ) #define S_ISLNK(mode) (((mode) & S_IFMT ) == S_IFLNK)#endif 这是一个我们编写的头文件如何屏蔽某些系统差别的实例。 Exercise 4.7编写一个类似cp(1)的程序,它复制包含空洞的文件,但不将字节0写到输出文件中去。Ans1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677#include <sys/stat.h>#include <sys/types.h>#include <ourhdr.h>#include <fcntl.h>typedef int bool;#define true 1#define false 0#define BUFFER_SIZE 8192#define BLOCK_SIZE 512int copy(const char *readFile, const char *writeFile){ int fd1,fd2; char buf[BUFFER_SIZE] = {0}; struct stat st; bool hasHoles = false; int byte_count = 0,redNum = 0; if ( (fd1 = open(readFile, O_RDWR)) == -1 ) { err_sys("Open file error"); return -1; } if (fstat(fd1, &st) == -1) { err_sys("fstat error"); return -1; } else if (S_ISREG(st.st_mode) && st.st_size > BLOCK_SIZE * st.st_blocks) hasHoles = true; if ( (fd2 = open(writeFile, O_RDWR | O_APPEND | O_CREAT | O_TRUNC, 0664)) == -1) { err_sys("open write file error"); return -1; } if ( (redNum = read(fd1, buf, BUFFER_SIZE)) == -1 ) { err_sys("read error"); return -1; } if (hasHoles) { int i = 0; for (i = 0;i < redNum; ++i) { buf[byte_count] = buf[i]; if (buf[i] != '\0') ++byte_count; } } else byte_count = redNum; if (write(fd2, buf, BUFFER_SIZE) == -1) { err_sys("write file error"); return -1; } close(fd1); close(fd2); return 0;}int main(int argc, char *argv[]){ if (argc != 3) err_quit("error args"); if (copy(argv[1], argv[2]) == -1) err_sys("copy error"); exit(0);}]]></content>
<categories>
<category>Linux编程</category>
</categories>
<tags>
<tag>apue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[apue chapter3 Files IO]]></title>
<url>%2F2016%2F08%2F29%2Fapue_c3_file_io%2F</url>
<content type="text"><![CDATA[《Advanced Programming in the UNIX Environment》第三章——文件IO课后习题答案 Exercise3.2编写一个同3.12节中的dup2功能相同的函数,要求不调用fcntl函数并且要有正确的出错处理。思路:递归调用dup123456789101112131415161718192021222324252627282930313233#include <ourhdr.h>#include <fcntl.h>int dup_2(int, int);int main(void){ //dup_2(1,10);}/* if success * return the filedes * else * return -1 */int dup_2(int filedes, int filedes2){ if (filedes2 < 0) return -1; if (filedes == filedes2) return filedes2; if (close(filedes2) == -1) return -1; int newfd; if ((newfd = dup(filedes)) == filedes2) return filedes; else { dup_2(filedes, filedes2); close(newfd); } return filedes2;} Exercise3.3假设一个进程执行下面的3个函数调用:123fd1 = open(pathname, oflags);fd2 = dup(fd1);fd3 = open(pathname, oflags); 画出结果图(见图3-3)。对fcntl作用于fd1来说,F_SETFD命令会影响哪一个文件描述符?FSETFL呢?Ans每次调用open函数就分配一个文件表项,如果两次打开的是相同的文件,则两个文件表项指向相同的v节点。调用dup引用已存在的文件表项(此处指fd1的文件表项),见图C-1。当F_SETFD作用于fd1时,只影响fd1的文件描述符标志;F_SETFL作用于fd1时,则影响fd1及fd2的文件描述符标志。 F_SETFL 将文件状态标志设置为第三个参数的值。(取为整型值)F_SETFD 对于filedes 设置文件描述符标志。 Exercise3.4在许多程序中都包含下面一段代码:12345dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd > 2) close(fd); 为了说明if语句的必要性,假设fd是1,画出每次调用dup2时3个描述符项及相应的文件表项的变化情况。然后再画出fd为3的情况。Ans如果fd是1,执行dup2(fd,1)后返回1,但是没有关闭描述符1(见3 .12节)。调用3次dup2后,3个描述符指向相同的文件表项,所以不需要关闭描述符。如果fd是3,调用3次dup2后,有4个描述符指向相同的文件表项,所以需要关闭描述符3。事实上,0、1、2分别表示标准输入,标准输出,标准错误输出,一般是不关闭的。而当fd > 2时,表示其余的文件描述符,一般而言需要关闭。 Exercise3.5在Bourne shell和KornShell中,digit1>&digit2表示要将描述符digit1重定向至描述符digit2的同一文件。请说明下面两条命令的区别。12a.out > outfile 2>&1a.out 2>&1 > outfile (提示:shell从左到右处理命令行。)Ansshell从左到右处理命令行,所以a.out > outfile 2>&1首先设置标准输出到outfile,然后执行dups将标准输出复制到描述符2(标准错误)上,其结果是将标准输出和标准错误设置为相同的文件,即描述符1和2指向相同的文件表项。而对于命令行a.out 2 >&1 >outfile由于首先执行dups,所以描述符2成为终端(假设命令是交互执行的),标准输出重定向到outfile。结果是描述符1指向outfile的文件表项,描述符2指向终端的文件表项。]]></content>
<categories>
<category>网络编程</category>
</categories>
<tags>
<tag>apue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Windows1 0 删除内置app]]></title>
<url>%2F2016%2F08%2F06%2Fwin10_delete_buildin_app%2F</url>
<content type="text"><![CDATA[Windows10自带了一大堆乱七八糟的app,而且在控制面板的卸载程序找不到这些= =,其实只要两条命令就可以卸载。 1.使用Windows PowerShell卸载 命令Get-AppxPackage -AllUsers获取app列表先打开PowerShell,在Cortana直接搜就可以,然后右键管理员运行。 输入命令Get-AppxPackage -AllUsers,然后就会输出所有app列表,大致上长的和下面一样: 1234567Name : Microsoft.BingWeatherPublisher : CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=USArchitecture : X86ResourceId :Version : 4.7.118.0PackageFullName : Microsoft.BingWeather_4.7.118.0_x86__8wekyb3d8bbwe... 命令Remove-AppxPackage PackageFullName卸载程序例如卸载上面说的BingWeather,可以执行: 1Remove-AppxPackage Microsoft.BingWeather_4.7.118.0_x86__8wekyb3d8bbwe 2.删除安装包使用PowerShell卸载程序之后,这些内置app的安装包还残留在C盘中,作为一个强迫症患者,不能忍!这些安装包的路径如下1C:\Program Files\WindowsApps 选择对应的安装包删除即可。 每博一首歌]]></content>
<categories>
<category>系统优化</category>
</categories>
<tags>
<tag>windows</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo 绑定自定义域名]]></title>
<url>%2F2016%2F08%2F03%2Fhexo_bind_customize_domain%2F</url>
<content type="text"><![CDATA[搭建好属于自己的博客并托管到之后GitHub会分配一个二级域名name.github.io,当然也可以绑定自定义的域名。 1.申请域名国内申请域名可以在万网或者是腾讯云上等购买,域名并不是想象中的那么贵,我买的域名也就5软妹币/年,挺划算的。 2.添加CNAME在站点根目录的的source目录下新建一个名为CNAME的文件,里面写上自定义域名,比如我的CNAME是这样的:1junhan.win 之后hexo d提交到github即可。 3.添加域名解析万网上可以解析域名,dnspod也可以。既然我的域名是在万网上买的,所以就直接在万网上进行解析。 a.添加两个指向GitHub服务器的A记录,如下图 b.添加指向github二级域名的CNAME记录,如下图 4.使用自定义域名直接访问HEXO博客http://junhan.win: Demo]]></content>
<categories>
<category>个人博客</category>
</categories>
<tags>
<tag>hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[闲着没事乱折腾]]></title>
<url>%2F2016%2F07%2F30%2Fthe_toss%2F</url>
<content type="text"><![CDATA[本来今晚是要干活的,有点懒散,索性继续瞎搞Hexo博客配置。今天把我的博客主题换成Next了,看来看去还是这个比较简约。之后又胡乱配置了一些东西,例如标签云,背景图片,背景音乐。这里记下Next主题官方教程位置开始使用 添加标签云 先在Hexo站目录下执行 1hexo new page tags 之后生成/source/tags/tags.md,打开修改为如下格式,即添加type: “tags”: 12345---title: tagsdate: 2016-07-30 21:23:21type: "tags"--- 添加背景图片打开\themes\next\source\css\_schemes\Pisces\index.sty,可以看到第一句是这样的:1body { background: #f5f7f9; } 把他改成如下形式:1body { background:url(/images/background.jpg); } 其中/images/background.jpg是事先放好的背景图片 添加背景音乐这里使用网易云音乐,很简单。如上图,点生成外链播放器,然后直接把HTML代码贴到markdown博文下就可以了,效果如下:]]></content>
<categories>
<category>个人博客</category>
</categories>
<tags>
<tag>hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Windows10+CentOS7双系统(UEFI+GPT)]]></title>
<url>%2F2016%2F07%2F26%2Fwin10_and_centos7_dualsystem_with_eufi_and_gpt%2F</url>
<content type="text"><![CDATA[前言眼馋双系统有一段时间了,然而之前安装失败格盘的惨痛教训历历在目。这几天闲着没事查阅了好多资料,怂了一个星期,终于决定再来一次尝试。总的来说安装过程还是挺顺利的,但是后期Windows引导的问题折腾了三天。。。技术不好,最终搞出个另类的双系统:默认启动Windows10,同时支持Windows Quick Boot;BIOS下切换到CentOS。 设备信息 PC: Thinkpad E431,Microsoft Windows10 Pro 64Bit (10240)CentOS版本:CentOS-7-x86_64-DVD-1511(这个版本的CentOS支持UEFI) 前期准备1. 分配CentOS安装盘符直接使用Windows的磁盘管理,用磁盘压缩切一个空间出来就好了。我是切了50G出来。 2. 关闭Windows Quick执行Win+R输入gpedit.msc,计算机配置->管理模块->关机,双击右边,选择已禁用。 3. 关闭Secure Boot这个要在BIOS下执行。 安装CentOS1. UltraISO制作CentOS启动盘。2. 设置CentOS镜像位置BISO选择U盘启动,接下来应该会看到黑色界面,如下:将光标移到第一行,然后这里不是直接点Install CentOS7,要按Tab键先配置CentOS镜像位置。按下Tab之后可以看到一下三行英文:123setparams "Install CentOS 7" Install limuze /image/vmlinuz inst.stage2=hd:LABEL=CentOS\x207\x20x86_64 quiet initrdefi /image/pxeboot/initrd.img 这个是用来选择镜像位置的,因为CentOS它不会自动定位到正确的位置。。。所以接下来要先修改上面的内容。① 把第二句改成:1limuze /image/vmlinuz initrd=initrd.img linux dd quiet ② 接下来按Ctrr+x执行,就可以看到所有盘符和编号了。类似于下面这样:可以看到,CentOS镜像的位置(也就是我的U盘)是sdb4。记住这个sdb4,然后关掉这个界面重新再来一次。③ 重新来一次又来到了步骤2的那个图,还是按Tab键,这一次将第二行改成如下形式:1limuze /image/vmlinuz inst.stage2=hd:/dev/sdb4 quiet 接下来按Ctrr+x执行,CentOS就开始安装了。记得要勾选一个桌面(如gnome桌面)。 4. 设置CentOS磁盘分区进来安装界面之后,选择前面切出来的那个盘。CentOS安装过程中要设置磁盘分区,这个就涉及到Linux的磁盘分区。下面是我的设置情况:12345/ :大小30G,设备类型`LAM`,文件系统`ext4`/boot :大小200M,设备类型`标准分区`,文件系统`ext4`/boot/efi:大小128M,设备类型、文件系统默认值(这个efi分区是放CentOS的uefi文件的,貌似最后也就占10M左右的空间)/swap :大小8G,设备类型`LAM`,文件系统`ext4`(据说swap分区要为物理内存的两倍,不过觉得我8G内存给它8G已经算多了)/home: :剩下的空间都给它,设备类型`LAM`,文件系统`ext4` 之后就开始漫长的安装了。。。。 修复引导CentOS安装完毕时候会重启电脑,这个时候你会看到系统选项有一个Windows10和 CentOS,选择Windows10,“卧槽!我的Win10居然没事,网上那群骗子,害我虚惊一场,重启看看CentOS先”。选择CentOS,然后就看到下面的东东:“特么我这个是Linux啊,你提示Windows未启动是什么意思?”然后就开始了我的折腾之旅。。。。。(这里省略上万字的心酸历程)下面是解决方案 1. 网友建议网上说的在Windows下使用easybcd添加CentOS的引导,反正我试了很多遍就是没成功。事实上easybcd只能添加CentOS的mbr引导,这个可以在easybcd看出,然而我是通过UEFI来装的,应该就不行。 2. 几番折腾,新办法① 前面CentOS分区的时候实际上/boot/efi是一个ESP分区(UEFI 系统分区)。里面放的是CentOS的EFI引导文件。12345678910111213$ ls -R EFI/EFI/:BOOT/ centos/EFI/BOOT:BOOTX64.EFI* fallback.efi*EFI/centos:BOOT.CSV gcdx64.efi* grub.cfg.bak grubx64.efi* shim.efi*fonts/ grub.cfg grubenv MokManager.efi* shim-centos.efi*EFI/centos/fonts:unicode.pf2 其中最重要的文件是grubx64.efi,开机时,BIOS先通过ESP分区找到相应的efi程序,然后加载启动系统,这里的grubx64.efi就是用来加载CentOS的。② 以此类推,Windows下肯定也有类似的文件。的确,在装Windows10的时候,会自动分配一个隐藏的ESP分区,盘符别名为SYSTEM_DRV:12BOOT/EFI/ 其中EFI/Microsoft/Boot目录里面放的就是加载Windows系统的efi文件。即EFI/Microsoft/Boot/bootmgr.efi。由上可知,整个硬盘共有两个ESP分区,常理上讲好像有点不科学,具体我也不知道可不可以。我觉得可能是不可以的,测试了下,发现BIOS每次都是从SYSTEM_DRV里面搜索efi程序,而CentOS的efi又不在SYSTEM_DRV目录下,这应该就是CentOS无法启动的原因。③ 所以接下来我就把CentOS的ESP分区里面的EFI/centos整个文件夹都拷贝到SYSTEM_DRV盘下的/EFI目录下。重启电脑发现还是不行,原因很简单a.如果想要出现两个系统的选择项,那就要使用Win10引导CentOS或者有个程序来专门引导两个系统,前者我查了很多资料还是没弄出来,好像是要修改Windows的BCD文件,有点麻烦。至于后者,有个叫rEFind的程序(rEFind下载)可以达到目的,不过弄出来界面太丑了,我放弃了。b.如果想要使用BIOS引导,就要把EFI/centos里面的路径写到一些特殊的文件,这个要用到一个叫BOOTICE的工具BOOTICE下载。 3. BOOTICE使用教程① 打开BOOTICE,选择UEFI,点修改启动序列② 选择左边的添加,先随便选一个本地磁盘的efi文件,然后把左边的启动文件改为1\EFI\centos\grubx64.efi 启动分区选择和Windows系统一样的项。最后把它移动到第二个,保存。③ 使用PE把\EFI\centos从CentOS的ESP目录移动到Windows的ESP目录下。这一步是为了让上面设置启动文件:\EFI\centos\grubx64.efi生效。不得不说,PE真是个好工具。 成功通过上述步骤之后,重启电脑,电脑应该还是自动进入Win10,因为BOOTICE工具是把CentOS添加到BIOS的启动序列中= =重启,进入BIOS(Thinkpad是F12),可以看到BIOS启动列表有Windows10、CentOS、USB HDD等等,这个USB HDD就是U盘,点CentOS,就可以进入CentOS的引导了,然后启动CentOS。到这里就成功了。 心得装这个双系统,修复引导花了我好长时间,不过也学到了很多东西,比如UEFI和传统Legacy的区别、UEFI的工作原理、PE的作用等等,最终文件没有发生丢失,也算是值了。下面是总结。 UEFI+GPT装双系统真麻烦 微软垄断心态真可怕 Google搜索东西靠谱多了 PE真是个好工具(进入磁盘修改EFI文件) 我装的双系统怎么和大家的不一样= =(又要继续干活了。。。) 参考资料CentOS7安装教程 U盘安装CentOS7的最终解决方案UEFI引导修复 UEFI主板GPT方式安装CentOS6.4 UEFI+GPT安装Windows8和CentOS双系统 Windows10与CentOS的完美结合 UEFI的两种启动模式 支持 efi 的主板 双系统安装 ubuntu - 学习 EFI 和 gpt 如何在UEFI模式下Win8与Ubuntu多系统的安装? 如何在UEFI+GPT下使用rEFind实现Win10 + Kali2.0 双引导!]]></content>
<categories>
<category>系统优化</category>
</categories>
<tags>
<tag>linux</tag>
<tag>windows</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux文件与目录管理]]></title>
<url>%2F2016%2F07%2F14%2Flinux_files_and_directories%2F</url>
<content type="text"><![CDATA[一、目录与路径12345. 代表此目录.. 代表上层目录- 代表前一个工作目录~ 代表当前用户的主文件夹~accout 代表用户account的主文件夹 与目录处理相关的命令1234cd: 切换目录pwd: 显示当前目录mkdir: 新建目录rmdir: 删除空目录 cd命令 cd [绝对路径 or 相对路径] pwd命令 pwd [-P]参数:-P 显示出当前路径,而非使用连接(link)路径 例如:1234567[junhan@localhost test_dir]$ cd /var/mail[junhan@localhost mail]$ pwd/var/mail[junhan@localhost mail]$ pwd -P/var/spool/mail[junhan@localhost mail]$ ls -ld /var/maillrwxrwxrwx. 1 root root 10 7月 7 18:39 /var/mail -> spool/mail mkdir命令 mkdir [-mp] 目录名称-m: 直接配置权限(默认使用umask)-p: 递归创建 示例:123456[junhan@localhost test_dir]$ mkdir t1/t2/t3/t4mkdir: 无法创建目录"t1/t2/t3/t4": 没有那个文件或目录[junhan@localhost test_dir]$ mkdir -p t1/t2/t3/t4[junhan@localhost test_dir]$ mkdir -m 711 test2[junhan@localhost test_dir]$ ls -ld test2drwx--x--x. 2 junhan junhan 6 7月 15 01:52 test2 rmdir命令 rmdir [-p] 目录名称-P: 连同上层空目录一起删除 二、文件目录管理12345ls: 查看文件与目录cp: 复制rm: 删除mv: 移动basename/dirname: 文件名/目录名 ls命令 ls [-aAdfFhilnrRSt] 目录名称参数:-a : 全部文件,包括.和..(常用来显示所有文件)-A : 全部文件,不包括.和..-d : 只列出目录本身(常用来显示目录本身信息)-F : 给予附加数据结构,如:*:代表可执行文件;/:表示目录;=:表示socket文件;|:表示管道文件.-i : 列出i-node-l : 列出长数据串,包含文件属性和权限等信息(常用于查看权限)-R : 连同子目录一同显示ls [--color={never,auto,always}] 目录名称–color={never,auto,always} : 设置显示颜色ls [--full-time] 目录名称–full-time : 显示完整时间–time={atime,ctime} : 显示访问时间(atime),权限改变时间(ctime) cp命令 cp [-adfilprus] source destinationcp option source1 source2 source3 ... directory参数-a : 相当于-pdr-d : 如果是连接文件,则复制连接文件(默认复制的是源文件)-f : 即force,强制复制-i : 若已经存在,则询问(常用)-l : 用来硬连接的连接文件创建-P : 连同属性一同复制(默认复制不会复制属性,此命令常用于备份文件)-r : 递归复制(常用于复制整个目录)-s : 复制成符号链接文件(symbolice link)-u : 即update,源文件相对较旧才更新 rm命令 rm [-fir]参数:-f : 即force-i : 询问-r : 递归删除 mv命令 mv [-fiu] source destinationmv [options] source1 source2 ... directory参数:-f : force-i : 询问-u : 比较旧才更新 basename/dirname命令示例:123456[junhan@localhost junhan]$ dirname test_dir/file t1/ test2/ [junhan@localhost junhan]$ dirname test_dir/file test_dir[junhan@localhost junhan]$ basename test_dir/file file 三、文件内容查阅12345678cat : 第一行开始显示tac : 最后一行开始显示nl : 显示行号more : 一页页显示less : 一页页显示,课往前翻页head : 看头几行tail : 看末几行od : 指定进制查看 cat命令(concatenate) cat [-AbEnTv]-A : 等价于-vET,显示特殊字符-b : 显示行号,不包括空白行-n : 显示行号,包括空白行 nl命令 nl [-bnw] 文件-b a : 同cat -n-b t : 同cat -b (默认值) more和less more:空格:向下翻页回车:向下一行/seq:向下搜索seq:f:显示文件名和当前行号q :退出less:[PgDn]:向下翻页[PgUp]:向上翻页/seq:向下查?seq:向上查n :重复查 head和tail 文件 head/tail [-n number]:number+表示前number行,number-表示除后number行。tail -f 文件: 动态监测 od命令 od [it Type] 文件a : 默认字符c : ASCIId[size] : 十进制,占用size位o[size] : 八进制x[size] : 十六进制f[size] : 浮点数 四、修改文件时间或创建新文件 时间 mtime : 文件修改时间(modification time)ctime : 权限或属性修改时间(status time)atime : 被读取时间,如cat,cp(access time)使用ls -l --time={ctime,atime} 文件或目录即可显示,默认显示mtime touch命令 touch [-acdmt]-a : 修改atime-c : 修改ctime-m : 修改mtime-d : 修改日期,后接日期-t : 修改日期,后接日期 [YYMMDDhhmm] 五、文件权限 umask命令 umaskumask -S 12345# 示例[junhan@localhost junhan]$ umask0022[junhan@localhost junhan]$ umask -Su=rwx,g=rx,o=rx 由umask可知,新建一个目录或者文件的默认权限:文件:-rw-rw-rw- - 022 = -rw-r--r--<<<<<<< HEAD 目录:drwxrwxrwx - 022 = drwxr-wr-x 目录:drwxrwxrwx - 022 = drwxr-wr-x ADD:commit]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[搭建腾讯云服务器]]></title>
<url>%2F2016%2F07%2F11%2Fbuild_tencent_cloud_server%2F</url>
<content type="text"><![CDATA[前言本文讲的是搭建腾讯云服务器的经过,由于对这些也不是很懂,所以就一点点来咯,本文会不定时更新。 腾讯云服务器介绍腾讯云服务器对在校大学生有优惠活动,每月可以提供64元的代金券直到毕业,最低配置的云服务器65/月,相当于每月1元就可以拥有一台云服务器,算是挺实惠的,所以就来玩玩看。详情可以戳腾讯 云+校园计划按照官方指引,执行下面的四个步骤之后就可以购买云服务器和域名了。 注册 腾讯云帐号完成 实名认证(财付通认证)完成 学生认证领用代金券 整个验证过程应该要两天,之后代金券就会到账了。 购买域名//等待更新。。。]]></content>
<categories>
<category>云服务器</category>
</categories>
<tags>
<tag>云服务器</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo搭建博客(二) 自定义博客主题]]></title>
<url>%2F2016%2F07%2F10%2Fhexo_config_customize_theme%2F</url>
<content type="text"><![CDATA[说实话,Hexo默认的landspace主题实在是太low了,于是上网查了下修改主题的教材。没有前端基础,所以搞得乱七八糟的,终于搞定!Hexo主题直接上Github搜就有了,很多。我用的是yilia主题,下面来个预览图。 Hexo根目录要配置主题,就要了解下Hexo博客根目录下的基本信息1234567├── _config.yml├── package.json├── scaffolds├── source| ├── _drafts| └── _posts└── themes _config.yml:网站的 配置 信息,您可以在此配置大部分的参数。package.json:应用程序的信息。scaffolds:模版文件夹。当您新建文章时,Hexo会根据scaffold 来建立文件。source:资源文件夹是存放用户资源的地方。除posts文件夹之外,开头命名为(下划线)的文件/文件夹和隐藏的文件将会被忽略。Markdown和HTML文件会被解析并放到public文件夹,而其他文件会被拷贝过去。themes:主题 文件夹。Hexo会根据主题来生成静态页面。 下载主题包下载主题有两个方法 直接从Github上下载,然后放在E:\HEXO\themes里面(这里的E:\HEXO是上篇讲的本地博客根目录)例如我使用的yilia主题,它的链接:https://github.com/litten/hexo-theme-yilia。点击下载Zip压缩包到本地,然后放在E:\HEXO\themes里面。 直接执行命令下载1git clone https://https://github.com/litten/hexo-theme-yilian.git themes/hexo-theme-yilia 之后会自动下载yilia,并保存在E:\HEXO\themes\yilia。 启用主题打开E:\HEXO\_config.yml,找到theme字段,将landspace改为yilia。 更新12cd themes/yiliagit pull 配置主题在themes\yilia目录下有个_config.yml,这是来配置主题用的。具体语法我也不是很懂,不过看起来理解应该不难。主要用到的有下面这些:1.菜单和友链1234menu: 主页: / 所有文章: /archives 随笔: /tags/随笔 menu是用来创建菜单的,我这里创建了三个菜单。1234# SubNavsubnav: github: "https://github.com/ChenJunhan" weibo: "http://weibo.com/2633996855" 这个是来编辑友情链接的。其他的应该也没什么了,我了解的就这么多。2.头像和图标_config.yml中,有两个字段用来配置头像和网站图标的。123456# Miscellaneousgoogle_analytics: ''favicon: /img/favicon.png#你的头像urlavatar: /img/avatar.jpg url可以使用外链,也可以使用本地链接,本地链接是E:\HEXO\themes的相对链接。例如头像我放在E:\HEXO\themes\img\avatar.png。3.其他配置其他配置例如css,这些很前端。是的,我一点也不会。因为代码块的显示出了点问题,所以我捣鼓了下E:\HEXO\themes\yilia\source\css\_partial下的highlight.styl,这个是用来配置代码高亮的,其他的看名字应该也可以猜到它的功能。 为Hexo博文添加标签写博文的时候,在最上面的tags字段声明即可。之后public下会生成一个tags文件夹,和一个tags.html文件,这个不用理它。单个tag12345---title: xxxdate: xxxtags: tag--- 多个tags1234567---title: xxxdate: xxxtags: - tag1 - tag2---]]></content>
<categories>
<category>个人博客</category>
</categories>
<tags>
<tag>hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo搭建博客(一) 使用Hexo搭建博客并托管到Github]]></title>
<url>%2F2016%2F07%2F10%2Fhexo_build_and_host_to_gitpage%2F</url>
<content type="text"><![CDATA[前言听光哥讲完怎么用Hexo搭建博客之后就立马捣鼓了起来。一开始还蛮顺利的,一下子就搭建好。到了后面修改主题时,没有前端基础的弱势彻底显现出来。于是今天早上起来就把昨天弄的全删了,查了很多教程,现在重新来一遍! 准备工作 安装Node.js,我下载的是Node.js的4.4.7版本。Node.js下载链接 安装Git,我下载的是Git的2.9.0版本。Git下载链接 安装和配置Hexo 安装Hexo随便找个地方,右键Git Bush Here,输入下面的指令:1npm install hexo -g ps. 翻墙的话速度会快很多。 初始化博客新建一个文件夹,用来保存网站,例如我新建的文件夹是E:\HEXO,然后在该目录下右键Git Bush Here,输入下面的指令:1hexo init 执行完毕之后会在该目录下生成一些配置文件和模板文件。 生成博客继续执行:1hexo g 然后会生成一个public文件夹,里面存放的就是博客静态文件。 本地部署在E:\HEXO目录下执行:1hexo server 如果看到以下输出,说明本地部署成功。1INFO Hexo is running at http://localhost:4000/. Press Ctrl+C to stop. 然后打开浏览器访问http://localhost:4000,就可以预览到我们的博客。 部署到Github只有部署到Github(或者其他代码托管网站),我们的博客才能被其他人访问。 注册Github账号,Github注册 新建一个repository,这里要注意repository命名格式必须是下面这样的:1username.github.io 例如我的就是ChenJunhan.github.io 配置_config.yml文件在E:\HEXO下找到_config.yml文件,打开编辑,在最后面找到下面这段代码:1234# Deployment## Docs: http://hexo.io/docs/deployment.htmldeploy: type: 替换成下面的代码:123456# Deployment## Docs: http://hexo.io/docs/deployment.htmldeploy: type: git repository: https://github.com/用户名/用户名.github.io.git branch: master 这里要注意repository、branch字段前加两个空格,冒号后面一个空格 在开始部署之前,要先将相应的依赖文件先装好,所以先执行: 1npm install hexo-deployer-git --save 正式部署这个步骤很简单,只需要两个命令 12hexo ghexo d 执行完毕之后本地文件就被upload到Github上了,接下来使用http://用户名.github.io就可以访问我们的博客。 写新博文执行以下命令会在E:\source_post下生成一个new_blog_name.md文件。1hexo new "new_blog_name" 这个文件就是我们的博文,可以使用MarkDown进行编辑,直接txt编辑也可以。其中开头几段是这篇博文的具体信息,例如本文:12345---title: Hexo搭建博客(一) 使用Hexo搭建博客并托管到Githubdate: 2016-07-10 15:27:07tags:--- 写完博文记得要执行以下命令:12hexo ghexo d 其他在E:\HEXO目录下的_config.yml文件可以配置博客的一些基础属性,例如博客标题、作者、语言、博客主题等。123456# Sitetitle: subtitle: description:author: language: Hexo配置博客前期还是挺简单的,通过上面的几个步骤,博客就搭建好了,不过界面有点low。网上有很多教程教怎么修改主题的。有兴趣可以点击这里。]]></content>
<categories>
<category>个人博客</category>
</categories>
<tags>
<tag>hexo</tag>
</tags>
</entry>
</search>