存档

作者存档

读书笔记-《细说清朝》

四月 20th, {2013 1 条评论 9,461 人阅读过  

读历史首先是要看写这段历史的人的口碑和人品,再去看读者对这本书的评价,历史被歪曲的太多,没有经过沉淀的历史书籍从不敢读,黎东方教授的细说系列长久以来被认为是历史读物中的上品,而清朝又是和我们衔接最紧密的一段历史,对于清朝发生的那些事情多多少少都有些了解,却从来没有完整客观地知道那个时代都经历过什么,也从来没有对那个时代的人有一个客观的评价,于是鼓起勇气读完了《细说清朝》。

读这本书感觉作者仿佛一位说书人,将那个年代的历史对你娓娓道来,清朝瞬间的崛起,短暂的辉煌,上百年的内忧外患都在本书中尽然展现,这本书更好地帮我们了解了那个时代的历史,更好地了解了那个时代的人。

中国的历史是帝王的历史,相比之下美国的历史则是平民的历史,这一点也不假,那么厚的一本《光荣与梦想》也不过讲了区区四十年的美国历史,刨除几个大的历史事件,绝大多数篇幅都在描述民众生活常态,以至于读起来会感觉枯燥很多,甚至一部《阿甘正传》都能概括美国大半部历史,相比之下四十年在中国的历史长河中不过是弹指一瞬,中国有太多的四十年,有太多的朝代更迭,帝王更替,而在这漫长的历史中最不值钱的也最不值得一提的便是平民百姓,思想被禁锢被奴化的中国人最容易被历史淡忘,于是历史上只留下了帝王将相。

从这本书中重新认识了李鸿章,历史课本中总是会给他戴上误国卖国的帽子,也正是他在那些不平等条约上签字,可现实的李鸿章虽比不过曾国潘清廉,但也不是个卖国的小人,正直与否我不敢说,但他确实是个好的外交家,虽然关于条约的周旋其实本无意义,弱国无外交的道理我们都懂,但李也没有轻易地做最后的让步,从书中我看到的不是李鸿章卑躬屈膝的奴才嘴脸,而是一个为了国家民族利益奔波的古稀老人,至于李的人品到底如何我的认知水平不敢枉下定论,俄国人说给了李巨额贿赂,黎东方在书中也对此提出质疑,一方面没有确凿的证据,另一方面一个七十多岁的老人,行将就木,而且处在那样一个乱世,要那么多钱有何用?我也并非在替李辩护,只是之前在认识能力有限的时候被灌输了太多关于他不客观的评价,想改变一下认识而已。

历史是人写的,总会有不经意的扭曲或者臆断,司马迁的史记也未必完全客观,三皇五帝的历史应该大多数靠传说吧,更不用说市井中的刺客列传,但这都可以理解,而对历史的刻意扭曲就只能持抨击的态度了,中国每一个专制的政权上台以前做的很重要的事情就是修订前朝历史,而更重要的事情是把对本朝不利的历史给抹掉或曲解,所以我不喜欢读抗战的历史,专制执政党出版的历史往往都不是真正的历史,现实中都有那么多隐藏的真相,更不用说那段曲折的历史了,我对于历史的兴趣不在于研究,只是仅仅想知道过去都发生过什么事,想对印象中的那些人有个客观公正的评价,仅此而已,经过篡改的历史说成思想的毒药应该不算过份。

清朝写的明史据说前半部还算客观,后半部明清交替的时候自然就倾向于美化清朝,有人说袁崇焕在清朝撰写的明史中被从误国改成了民族英雄,至于袁崇焕有没有误国我不敢说,但他却实用了不少昏招,印象最深刻的是他绝不该杀毛文龙,他在关外一系列举措加速了明朝的灭亡,虽然当时的明朝确实“气数已尽”,但如果当时将袁崇焕换成孙承宗,相信明朝不至于亡地那么快。

回想一下清朝似乎没有多少平静的年月,从努尔哈赤起兵反清到多尔衮带兵入关,这个过程一直都是战乱,终于到了康熙这一朝平定了三藩,收复了台湾,又平了西北,算得上安定,黎东方对康熙的评价很高,事实上也只是说康熙是个好皇帝而已,但翻遍本书也只是对康熙一人有这样的评价,而且对于一个的皇帝的评价说他好,那就说明他确实做过一些了不起的事情,至少不会是个昏君,而传说中的康乾盛世,也不过是乾隆占了一点祖宗的光而已,在乾隆那一朝就慢慢地开始有反清的势力抬头了,于是到了嘉庆各路反清势力大规模活动起来,白莲教天理教天地会,到了道光年间发生了著名的鸦片战争,再到了咸丰年间,内忧外患一起来,太平天国兴起,各咱联军一起涌了进来,想想当一个乱世的帝王真不如当一个治世的平民。

年幼的光绪皇帝,四岁本是在父母呵护之下无忧无虑玩耍的年纪,却被无情地推上了政治的风口浪尖,认识到中国残酷的现状后他励志图改,只可惜维新只持续了百日,日本的明治维新让日本由一个原本靠抢劫中国沿海的蛮夷国家,跻身于世界强国之列,徜若光绪皇帝的变法行动能顺利执行,那中国未来一百年的历史或许会明朗许多,但时局已定,清朝贯彻了两百年太监后宫不能干政,确实未曾出现过像王振魏忠贤那样的太监,可惜最终却亡国于后宫之手,也真是讽刺。

在清朝所有这些王公大臣中最感兴趣的不是曾国藩,不是李鸿章,更不是和坤,是第二次鸦片战争时期的广东巡抚叶名琛,记不清有没有在历史课本上读过此人,但这个人的作风确实让人不可思议,对他的评价也往往趋向于两个极端,有人把他称做“六不”总督,“不战、不和、不守,不死、不降、不走;相臣度量,疆臣抱负;古之所无,今之罕有”,是居于天朝的傲慢还是一种绝望中的坦然,这些都说不清楚了,总督衙门被炮轰他仍能处之坦然,虽不作为但也不贫生不卖国,最后被俘绝食而死,也有封疆大吏应有的气度,在当时的环境下出战其实也是徒增损伤,而他偏偏又不议和,这是出于民族气节还是有其它原因我想我们后世人很难说清楚了,对于我只是知道历史上曾经有过这样一个人,至少在现在我还做不出对他客观的评价。

读中国的历史已经不需要再去思考“这个朝代为何灭亡”这样的问题了,用公知最喜欢说的一句话就可以概括“体制有问题”,时下的体制都有问题,更不用说那些高度集权的封建王朝了,一个王朝的兴起可能由一个人主导,王朝的长治久安却绝不是一个人能主导的了的,倘若说康熙是个好皇帝,但却不能保证每个皇帝都是康熙,也不能保证在今天的政治背景下康熙是个好的皇帝,在明天的政治背景下康熙还是个好皇帝,历史有太多的偶然有太多的变数,权力没有制衡历史就很容易被引导到错误的方向,从而一发而不可收,而这过程中损失最大的是平民百姓,时至今日的中国打着宪政的幌子,骨子里还是前朝的模式,这样的社会不敢期望长治久安。

分类: 读书笔记 标签: ,

读书笔记-《观念的水位》

三月 17th, {2013 3 条评论 11,490 人阅读过  

网络上对于刘瑜的评论似乎趋向于两个极端,说她好的自然大有人在,说她不好的也不在少数,还有人调侃地把她和安妮宝贝和孤郭敬明相提并论,不同的人有不同的政治立场,刘瑜作为一名时评作家,她的观点自然有人赞同有人反对,这都无可厚非,不管是《民主的细节》还是刚读完的这本《观念的水位》,对我而言都是非常有营养的。

根据百度百科里面的介绍,刘瑜在《南方周末》写时评专栏,在《新周刊》写书评和影评专栏,也正像作者在序言中提到的,《观念的水位》这本书就是这些专栏文章的合集,所以前半部分大多是时政评论,后半部分就是书评和影评,我对书评和影评不太感冒,没有读过的书没看过的电影被别人一评价,这书这电影再看起来就没有原来的味道了,观点就会不自觉得被这些评论牵着鼻子走,所以对我而言前半部分的吸引力要远大于后半部分。这本书把一些离散的文章集合在一起,作者也没有刻意地去让他们之前有什么衔接,所以读起来思维可能需要有一点跳跃,比如前一篇在讲伯林墙,后一篇就已经在说曾国潘了。

书中有一篇文章名字和书名相同,以这篇文章的标题作为书名,我相信作者有她的意图,此书的出版意在提高读者的政治观念,文化观念,历史观念,因为只有民众观念的提高才能促成社会变革,这篇文章中有一段话我很赞同,在这里引用一下:

我心中理想的社会变革是一个“水张船高”的地程:政治制度的变革源于公众政治观念的变化,而政治观念的变化又根植于人们生活观念的变化。水涨了,般自然浮起来。我观察社会变革的动力,不那么关注船上有没有技艺高超的船夫,而更关注水位的变化。

网络兴起确实促进了公众观念的提高,虽然我们的信息仍然受到层层封锁,但信息流速之快总让我们有意无意间捕捉到那些残酷的社会现实,当満大街的人都在谈论民主,都在渴望民主的时候,那民主的到来就真的不远了,倘若民众仍然蒙昧地相信”光荣伟大正确“这些空洞的字眼,那我们期待的民主恐怕永远不会到来,哲学家告诉我们没有什么东西是绝对的,辩证法简单而生硬,所谓的”光荣伟大正确“经不起任何一个角度的推敲,岂图依靠洗脑来维系的政权和封建的伦理道德有什么区别,言论都谈不上自由哪里来的什么民主呢,又哪里谈得上什么伟大光荣正确呢。

刘瑜《在民主的细节》一书中给我们讲述了美国的民主制度,我不敢肯定书中她的观点绝对正确,比如提到伊战我们脑海中就不自觉地浮现出”霸权主义“这样的词眼,甚至都没有去思考,不禁有点可怕,刘瑜站在了美国政府的一边,她认为伊战是民主国家的民主输出,她给出的是一个全新的观点,我虽不敢轻易赞同,但也绝没有理由否定,这些国际上的政治决策,我觉得也不是我读一两本跟政治有关的书就可以轻易理解的。不管观点正确与否,刘瑜的文字都很诚肯,有一种让人信服的力量,再配合上她给出的相关数据,就让人更加无言反对了。

扯开伊战不说,西方国家的民主才真的可以称地上是民主,而且把西方国家的政治制度拿来和我们国家做一个简单的对比,就会发现其实政治也是一种科学,读我国历代帝王的历史,经常会尝试去想如果我坐在帝王的位置上该如何去管理这样一个庞大的国家,才可以对人民不失公允,思前想后才发现这绝不是一个人能完成的事情,政治制度本身就是一种科学,靠自觉或者靠道德约束的制度都不靠谱,孔子说仁,孟子说向善,无疑都是在凸显人性中的善,那人性中的恶呢,人是社会的动物,如果你说人性本善,那怎么解释文革时期疯狂的红卫兵,又怎么解释二战时泯灭人性的纳粹,体制内不可能人人都是海瑞,没有三权分立的相互制约,空谈民主空谈自由都是徒劳。

回想起我们国家的历史,那真是不折不扣的血泪史,封建王朝的历史自然不必言说,那些在当家做主站起来的先辈,在大饥荒中饿死一批,在文革中整死一批,最后活下来的又是什么遭遇,我从来不敢去读关于文革的书,太过于压抑,你不敢想象那些残酷的事情就发生在我们这样一个人民民主专政的国家,对了,什么叫人民民主专政呢,专政这样的词眼又如何能跟民主捆绑在一起呢,还是说我们的民主其实又是中国特色社会主义的民主,还是说所谓的人民当家做主其实就是王朝更迭中帝王口中的”为民请愿“,不过是一个王朝取代另一个王朝冠冕堂皇的说词,换作现代就是一个政党取代另一政党的说词。

更多的时候即使我们这些从学校走出来的人也被历史课本蒙蔽了双眼,考研时反复背诵地群众路线,说白了就是打地主分田地,简单暴力而又直接,刘瑜从另一个角度给我们揭开了历史的面纱,难道国民党就不懂得团结群众了吗,这也是我一直迷惑的地方,蒋公眼里就真的只有”剿匪“?对于他们这些熟读历史的政治家来说,不会不懂得群众的力量这种事情,国民党也曾实行过土改,只不过作为当时国内唯一的合法政党,国民党实行的是温和的和平土改,政府使用土地债券从地主手中买地,再让农民通过分期付款的方式低价从政府手中购买土地,对于农民和地主都是一种双赢的方式,关于和平土改缘何失败,有各种各样的说法,但刘瑜给的结论最能让人接受,也最一针见血,打土豪分田地这种办法简单粗暴,农民本来就对地主没有好感,突然有人告诉你说又可以打地主又可以免费分田地,多么现实的问题,在那种历史背景下,在那种文化程度下,农民自然会选择这种原始暴力的方式。

我们生活的社会不像书本中描绘的那样美好,有太多的残酷的历史被从书本中抹掉,有太多残酷的现实被从网络中抽离,我们吃不上安全食品就去骂不良商家,买不起房就去骂地产商,试问一个有良性制度的政府会允许这些事情出现吗,民生这种字眼都已经显得空洞无力了。

时代不一样了,民众的观念也在提高,相信历史总有一天会播开云雾。

分类: 读书笔记 标签:

读书笔记-《万历十五年》

三月 9th, {2013 没有评论 9,167 人阅读过  

过去的一年里读了好多书,科幻玄幻政治历史,有些书是用来娱乐打发时间,有些书是为了开阔眼界,丰富内涵,每读完一本总会有些许的思考,但却一直没有把读完这本书带来的收获记录下来,今天决定开始对读过的每本书写读书笔记,不能让这些书的内容只是作为过眼云烟,记录下来也算对自己有所沉淀。

黄仁宇的这本《万历十五年》是从杭州回北京的飞机上读完的,读这本书花了不少时间,这期间也主要因为工作太忙,工作之余还要抽出时间来补专业知识,所以就冲淡了读这些文学作品的时间。出差唯一的好处是可以在旅途中读书,这个过程是很享受的。

《万历十五年》这本书写的是万年皇帝第十五年也就是公元1587年前前后后的历史,作者黄仁宇博士履历丰富,学识也很渊博,《万历十五年》在我看来没有博人眼球的华美文字,字里行间也是朴实无华,作者站在一个后来人的角度上客观公正地去审视那个年代的历史,为我们重现了一个典型的中国封建王朝的政治和民生。

时处明朝的中国在世界上还是一个辉煌的国度,但其实是在走向没落的边缘,当时的政治体制就已经决定了明朝的没落是不可避免的。

不得不说,从读过关于明朝的书籍来看,明朝的政治制度相比其它的朝代还是有一定的优越性的,文官集团的斗争虽然是王朝落没的关键因素,它也同样带有一点民主的意思,低级别的文官敢于上书弹劾比自己官位高得多的官员,甚至弹劾自己的领导,更有甚者有人公开上书指点皇上的是非,很多文官也有着他们自己的诤诤铁骨,虽然这远远称不上民主,但说它有一点民主的意思我觉得也不过份。

作者在本书中多次明确地阐述,封建王朝最终落没的原因,在于统治者总是试图通过道德来约束人民,而没有建立完善的法律框架,于是四书五经三从四德等等这些就在那个年代发挥了巨大的作用,普通百姓想要入朝为官就只有科举这一条出路,科举的内容也无非是四书五经圣人之言,朱熹的批注也被指定成唯一的官方正确版,如果说现在的中国社会没有言论自由,那封建社会的学者连思想自由都没有,由于缺乏完善的法律框架,统治者就必须将这些圣人之言作为人民日常生活的准绳,以此来教化人民,而那时的中国是一个典型的农业社会,人民知识水平和智力水平低下,依靠这些很容易愚化百姓,但仅仅凭道憄上的约束是远远不够的,法治不健全就会导致各项政策难以推动,就更不用说官员钻各种空子了,这些问题即便在今天的中国,有了一定的法治基础都还远远没有解决,更不用说在那个落后的年代了。

《万历十五年》通过几个历史事件和几个历史人物刻画了那个年代的历史,作者挑选的那个时代的几个人物也各有他们的特点。

张居正是一个改革派,作为皇帝的老师同时也受到太后的青睐,手上有着至高无上的权力,由他来推行变革可以说是顺水推舟,张居正虽然生活上远不能算是个清官,作风上也受人非议,但通过这些历史作品可以看出他也确实心怀天下,结交党羽也是为了能在朝中取得更多的支持,以此来作为政治基础来推行他的改革,虽然张居正生活不够清廉,但在他的努力之下民生确实有所改善。需要明白,但凡变革就必然会触动很多人的利益,张居正虽然到死也没有卸任,但在他死后弹劾他的言论纷至沓来,最终在死后还得到清算,因为有太多人的利益受到了触动,弹劾地人如此之多以至于和张居正有过密切联系的人也大都没有什么好下场,一代名将戚继光之所以在死后三年仍不能见容于万历皇帝,也是因为他之前与张居正交往过于密切。

海瑞印象中应该出现过在历史课本上,各个地方给他们标题都是“清官”,为官清廉地有些极端,记得在《明朝那些事儿》里面作者写到有一次海瑞买了两斤肉,卖肉的屠夫激动万分,感叹居然在有生之年还能做成海县令的生意,海瑞清廉地有些洁癖,在那样的政治背景之下,如此正直的官是不容易于他人的,正直是必要的,但变通仍不可或缺,海瑞岂图靠一人之力构建和谐社会这样的打算很显然是不会成功的,还是绕回来,没有统一的制度,单靠一已之力是不可能对社会现状有颠覆性的改变的。

相比于海瑞,戚继光则有他的过人之处,他的军事才能无可挑剔,即使没有读过明史的人提到戚继光也会不自觉地在他名字前面加上个“抗倭英雄”,戚继光练兵有他自己的手段,用兵也有自己的方法,以至于在和倭寇的战争戚家军几乎未尝过败绩。看看明朝其它的一些悲剧人物就可以知道,单打独斗是成不了气侯的,海瑞一生正直如此,最终其实也是壮志未酬,而戚继光可以说成是识实务者,他懂得适应当前的形势来做出决断,又懂得结交朝中大臣,张居正就对他赏识有加,可以说有张居正在朝中的大力支持,戚继光才能在外更好地发挥他的军事才能。

书中还写到了内阁首辅申时行,哲学家李贽,但总起来讲没有哪个历史人物有能力靠一己之力改变历史的,历史的车轮滚滚向前,也只不过是汇聚了众多历史人物的力量,由底层的人民大众推动向前的,《万历十五年》是本好书,让我们更清晰地了解那个时代的历史。

分类: 读书笔记 标签:

Common Lisp使用iolib进行网络编程

一月 4th, {2012 2 条评论 14,630 人阅读过  

Common Lisp进行网络编程可用的库还是挺多的,比较常用的库有usocketiolib,usocket我简单了解了一下没有真正拿来用,它的API比较简单,文档写得比较全面,相比之下,iolib要比usocket强大的多,但缺点是文档太少,官方的文档可用的内容非常少,但如果能阅读一下iolib的相关源码,就会发现其实iolib是一个很强大的网络编程库,其中包含了DNS解析,socket基本操作(bind,listen等等),IO多路复用以及通常用来做IPC的socketpair,而且iolib的multiplex用起来有种libevent的感觉,用iolib可以实现一般的应用层网络编程,至于是否支持raw socket,我还没仔细研究,不过感觉应该问题不大。

1.iolib的安装
使用asdf-install可以在线安装iolib,但貌似asdf-install不会自动解决包的依赖问题,最近才发现原来asdf-install其实已经是一个废弃的项目,官方已经不推荐使用了,在cliki的asdf-install首页最开头就有一句醒目的提示语:

ASDF-install is OBSOLETE. DO NOT USE ASDF-INSTALL, EVER. DO NOT ASK AROUND ABOUT HOW TO GET IT RUNNING. IT IS O-B-S-O-L-E-T-E. Not working. Not maintained. Please use quicklisp instead.

取而代之的是quicklisp,之前就有人跟我推荐过quicklisp我还没来得及尝试,这几天试了下确实非常方便,可以自动地下载程序包及其依赖的相关程序包,无需手工解决依赖问题,让我想到debian的apt-get,关于quicklisp的安装和使用都非常简单,在它首页上都有使用说明。而且quicklisp几乎每个月都会在官方blog上放出过去的几个月程序包的下载排行(如:Project download stats for November),可以在选择程序包的时候有个参考。

2.创建passive socket

当创建一个用于充当server角色的程序时通常需要创建passive socket,用来监听客户端的连接,关于socket编程的基本步骤已经是大家所熟知的了,create socket,bind,listen,accept等等,iolib是使用cffi(The Common Foreign Function Interface)通过调用linux系统调用来实现的,因此和用C语言编程几乎是一个套路,方法如下:

(setq socket
      (make-socket
       :connect :passive
       :address-family :internet
       :type :stream
       :external-format '(:utf-8 :eol-style :crlf)))
 
(bind-address socket
              (ensure-address "127.0.0.1")
              :port 1086
              :reuse-addr t)
 
(listen-on socket :backlog 10)
 
(setq client (accept-connection socket))
 
(multiple-value-bind (who port) (remote-name socket)
      (format t "Client ~A:~D connected.~%" who port))
 
(close socket)

几个操作一目了解,更细节的操作(比如如何创建UDP套接字)就去翻下源码好了,bind-address这个这个函数第二个参数用ensure-address把字符串转换成所需要的address类型,iolib也定义了一系列形如+ipv4-unspecified+的静态变量,类似于C语言里面的INADDR_ANY,最后一个参数reuse-addr相当于用setsockopt对套接字设置SO_REUSEADDR选项。

对于iolib的passive我一直有一个问题未能解决,当程序作为server在监听客户端连接时,在C语言中可以使用CTRL+C给程序发送SIGINT信号让程序终止,排除TIME_WAIT等这些情况,程序再次启动时仍可以bind到同一个指定端口(即使没有显示地调用close关闭套接字),但在slime中使用C-c C-c终止程序,并确保在slime-selecter中已经结束掉所有的用户线程之后,再次启动程序绑定同一个端口便会提示端口已被占用,除非结束掉lisp进程(sbcl/ccl等),我也就直接选择重启emacs,这个问题一直未能解决,困扰我很长时间,所以我只能在程序运行中不断地插入close来在不该关闭的地方临时关闭套接字。

3. 创建active socket

通常使用active socket的程序是作为客户端的角色,下面是我写的一段简单的示例代码,用于发送一个http请求:

(let (socket ip http)
  (setf socket (make-socket
              :connect :active
              :address-family :internet
              :type :stream
              :external-format '(:utf-8)
              :ipv6 nil))
 
  (setf ip (lookup-hostname "basiccoder.com"))
  (format t "IP of ~a is: ~a~%" host ip)
 
  (connect socket ip :port 80 :wait t)
  (format t "Connected to ~a via ~a:~a to ~a:~a~%"
          host (local-host socket) (local-port socket)
          (remote-host socket) (remote-host socket))
 
  (setf http (make-http-request "GET" "/" host
               (("Connection" "Closed")
                ("User-Agent" "Mozilla"))))
 
  (format t "send: ~A~%" http)
 
  (format socket http)
  (finish-output socket))

使用lookup-hostname来解决IP地址,通过connect来向远程服务器进行连接,make-http-request是我写的一个生成http请求头的一个宏,返回http请求字符串;创建的socket对象其实是一个流对象,因此可以使用format向流中写入数据,写入的数据会保存在缓冲区中,当调用(finish-output socket)函数时开始执行数据的发送操作。iolib也提供了send-to函数,不详细讨论了。

4. 从流中读取数据
由于socket对象是一个流对象,因此可以用任何从流中读出数据的方法来从socket中接收数据,如read-line,read-byte等等,但read-line存在一个问题,当使用read-line读取数据时,当数据中存在非ASCII字符中便会抛出异常,它在读取的过程中会对数据进行ASCII解码,而read-byte则不会存在这个问题,因为它读出的是二进制的字节,它不关心编码方式,但read-byte我感觉很多情况下是不太适用的,因为它一次只能读取一个字节,一般情况下多次执行这样的操作效率不会太高,当然,iolib也提供了receive-from函数,间接地调用了系统调用recvfrom(),是一种带缓冲区的接收方式,也比较符合C语言的编程习惯。

receive-from的使用方法如下:

(multiple-value-bind (buf-vector rbytes)
          (receive-from socket :buffer buf-vector
                               :start 0 :end 4096
                               :size 4096)

receive-from返回的是values,主返回值是包含接收到的数据的vector,另一个返回值是读取到的字节数,函数调用的参数里面:buffer不是必须的,当:buffer未指定时,则需要指定:size参数,这时receive-from会自动创建一个指定大小的vector并将数据填充后返回。receive-from返回的vector中保存的是字节码值,并不是字符串,可以使用octets-to-string函数将其转换成string,具体可以参考下我的这篇日志:Common Lisp为Babel添加GBK支持

5. IO多路复用

iolib提供了multiplex机制,原理也是对epoll/poll/kqueue进行了封装,我在linux下测试默认是用的epoll,使用方法和libevent非常相似,首先要创建一个全局的event base:

(setf *http-event-base*
        (make-instance 'iomux:event-base))
</lisp>
 
将要进行利用的socket对象添加到该event base中,使用set-io-handler函数:
<pre lang="lisp">
(set-io-handler *http-event-base*
                (socket-os-fd socket)
                :read
                (make-http-event-loop conn client)
                :one-shot t)

第三个选项:read表示监听套接字是否有数据可读,同类的选项还有:write和:error,第四个参数是事件发生是要执行的回调函数,由于lisp中没有类似于C语言中的void*这种方式,不会像C语言一样给回调函数通过一个指针来传递相关的参数,但lisp的高阶函数使用传递额外参数更加方便了,上述代码中的make-http-event-loop函数的返回值是一个lambda函数,用来作为set-io-handler的回调函数,而 conn和clinet两个参数可以通过make-http-event-loop传递给lambda函数:

(defun make-http-event-loop (conn client)
  (lambda (fd event exception)
     (format t "event ~A on fd(~D) with connection
:~A client :~A" event fd conn client)))

最后,调用event-dispatch函数来进入事件循环:

(event-dispatch *http-event-base*)
(when *http-event-base*
  (close *http-event-base*))

6. iolib的其它参考文档

分类: Lisp 标签: , ,

Common Lisp为Babel添加GBK支持

十二月 22nd, {2011 1 条评论 11,338 人阅读过  

前段时间在学Common Lisp,接触新语言我干的第一件事一般是通过HTTP抓取某个web页面,因为对网络编程比较感兴趣,而且平时写的程序也多是网络相关的,所以比较关心这方面的用法,于是用IOLib写了一个简单的小程序尝试着抓取了几个大门户网站的页面代码,关于IOLib的基本用法改天我也写篇日志记录一下,也算是和大家分享一下,毕竟能找到的中文资料比较少,而且文档也不是特别全,就像这篇文章里面说的:”Such is the nature of open source documentation. “,于是大多数的用法都得通过hack源代码来弄明白,言归正传,在写这个小程序的时候我遇到了一些问题,关于字符编码的问题,下面慢慢道来吧。

IOLib的receive-from方法是通过调用recvfrom来进行的,这种带缓存的接收方式很符合其它语言进行编程的套路,但它所接收到的buffer数据是需要存储在一个vector ‘(unsigned-byte 8)中的,虽然字符串在本质上也是向量,但对于字符串的很多操作不能直接应用于vector,而且vector中的元素都是每个字符的unicode编码,而不是确定的字符,于是便需要进行转换,最初我使用的办法:

(map 'string #'code-char buffer)

这个方法简单粗暴,直接在对一个vector上的每一个元素应用code-char函数,然后将输出映射为string,首先需要了解code-char/char-code这对函数的用法,大多数的Common Lisp实现都使用Unicode字符编码,当然,Unicode向下兼容ASCII和ISO-8859-1,所以这一对函数的作用就是在字符和它的Unicode码之间相互转换,如:

CL-USER> (char-code #\中)
20013
CL-USER> (code-char 20013)
#\U4E2D
CL-USER> (format t "~d" #x4E2D)
20013
CL-USER> (format t "~a" #\U4E2D)

字符“中”的Unicode码是20013,用code-char将其转换成字符之后,REPL并不是将可视的字符直接显示出来,而是显示#\U4E2D,表示这是一个Unicode字符,后面的4E2D是该字符的16进程Unicode码,换成10进制也便是20013,将该这了符直接打印出的话便可以得到字符“中”了。

当抓取的页面编码不是Unicode(一般情况下都不是的),直接使用这种办法转换成字符串就会导致除ASCII字符以外的其它字符乱码。

Google搜索下在stackoverflow上发现这个问题的解决办法,babelflexi-streams的octets-to-string函数可以实现由vector向string的转换,而vector必须是(make-array element-length :element-type ‘(unsigned-byte 8))类型,在使用该函数在抓取新浪/网易等门户网站的数据的时候会抛出异常:

代码:

(babel:octets-to-string buffer :encoding :utf-8)

异常:

Illegal :UTF-8 character starting at position 437.
 [Condition of type BABEL-ENCODINGS:INVALID-UTF8-CONTINUATION-BYTE]

起初我对这个问题百思不得其解,后来在水木还有StackOverflow上提出这个问题之后,我才发现原来我的问题是那么弱,也非常感谢大家的热心解答,确实如字面意思,有非法的UTF-8字符,也就是说字符串并非使用UTF-8编码的,而我习惯性地以为这些大网站理应都是UTF-8编码才对,谁知跟我想的正好想反,他们大多都不是UTF-8,有GBK甚至有GB2312,关于GBK和UTF-8我在推特上提起过这个问题,有推友说大网站仍在延用GB2312是为了节省流量,这个我可以接受,也有推友说使用GBK是因为它比UTF-8可能多几个字符,这个我也可以接受,但有的推友却跑过来说GB*是国标,用UTF-8什么的那是崇洋媚外,这个我表示无论如何也接受不了,互联网是没有国界的存在,在这样一个大的开放平台上你搞个国标有个毛用,当然我们的互联网也不开放是真的。

GB2312是GBK的子集,GBK向下兼容GB2312,使用GB2312就意味着能使用的字符数要远小于GBK,这两者相对于UTF-8对于中文的优点就是它们对于汉字的编码是两个字节的,而UTF-8是三个字节的,当然,我说的仅是指汉字,UTF-8是变长编码的,它也支持2字节和4字节甚至更多。

babel是不支持GBK的,sbcl的sb-ext:octets-to-string和Closure的ccl:octets-to-string都支持GBK,我个人比较较真,想给babel写个GBK的patch,以便可以实现平台无关的转码。

可能是我搜索技术不行,搜到的关于GBK的中文正式文档不多,只在维基百科上找到了关于GBK的介绍,虽然没有涉及到具体的GBK到Unicode转码规则,但它提供的关于GBK的介绍也是很有帮助的,发现原来GBK与Unicode的转换并不像UTF-8与Unicode之间的转换那样有固定的规则,而是需要通过查表实现,于是乎我在网上下载到了一个包含所有GBK符号的码表,排列顺序也是按GBK的相关规则来的,解码的过程我参考了这篇文章:从GBK到Unicode的中文字符映射,编码的话就是反其道而行之,原理很简单,参考维基百科上的两个图就可以推算出来。

对于lisp而言,宏是这门语言一个很大的亮点,通过宏甚至可以定制语言的特性,babel写义了几个宏作为编解码的接口,最重要的有下面四个:

define-octet-counter
define-code-point-counter
define-encoder
define-decoder

前两个宏首先对要进行编/解码的数据进行预处理,分别计算出编/解码后的数据所占用的长度,从而预先分配存储空间。后两个宏分别定义对应的编码和解码算法,这两个宏具体的使用方法都是参考了enc-unicode.lisp这个文件里面关于UTF-8的编解码代码。

添加了GBK支持的babel我推送到github上去了(https://github.com/levin108/babel/),需要的同学可以下载,有什么写得不对的或者不好的欢迎提出来,我是新手需要学习。

分类: Lisp 标签: , ,

使用ASDF构建Common Lisp程序包

十二月 13th, {2011 7 条评论 14,050 人阅读过  

在切入正题之前先写点不相关的,工作确定之后便开始忙论文的事,忙里偷闲总想搞点什么以做娱乐,不得不说,腾讯面试官说过的要精通两到三门不同的语言我印象很深刻,自己也想尝试一下新东西,VIM让我审美疲劳了,也想尝试一下Emacs,机缘巧合由田春老师翻译的《实用Common Lisp编程》刚上市不久,Emacs和Lisp也有不少渊源,再加上Lisp作为一门生命持久的元老级别的语言,至今仍然能倍受广大黑客的推崇,我相信它一定有学习的价值,而且Hadoop的MapReduce据说也是受Lisp的map和reduce函数的启发而来,相信对于Lisp的学习肯定不会是浪费时间,尽管将来工作中应用Lisp的机会可能很少,但深入学习的话肯定会对自己有一定的启发和帮助。

于是几乎同一时间我开始尝试使用Emacs并在卓越上订购了中文版的《实用Common Lisp编程》,抽空阅读尝试。总起来说这本书是非常不错的,几乎是面面俱到,但有些我认为也很有用的宏如defstruct,deftype,check-type等书中没有给出相关介绍,另外关于cl的package书中有一章节专门讲了定义的规则,但对于package的管理及安装并没有提及,我个人觉得如果是practical编程的话提一下cl中重要的ASDF包管理工具还是很有必要的,既然书中没有提到就得自己通过其它的渠道去了解学习,这方面中文的资料相对较少,大多数的资料都是在外文网站上查到的,当然也包括到stackoverflow上的提问。

ASDF全称是Another System Definition Facility,asdf这个组合很有意思,正好和左手的键盘基本按键重合,当初还以为是作者很有个性地起了这个随意的名字呢。我个人的理解是ASDF是类似于automake或者cmake的工具,提供一种程序包的管理方法和工具,因此也没什么复杂的地方,只不过有一些基本的规则需要遵守,了解了就没什么了。

程序包中一般会有一个.asd文件,该文件定义了程序包中源码文件的组织方式及依赖关系,类似于cmake中的CMakelists.txt,当然该文的编码方式是使用lisp风格的。

1. asdf工具的安装配置。
目前存在的common lisp实现有很多,在这篇文章中有介绍:Common Lisp Implementations: A Survey,免费的common lisp实现中性能比较好使用也比较简单的应该属sbcl了,《practical common lisp》也推荐了sbcl,作为一个新手我第一选择当然也选了sbcl,在sbcl中已经集成了asdf工具,无需再手动安装,但安装方法也很简单,可以参考ASDF Manual,该手册对asdf有详细的介绍。

2. asdf包的编译加载。
asdf是一个工具集,可以对包进行各种操作,其中包括编译,加载等。asdf对于包的编译和加载等操作都需要基于.asd文件,也就是说编译某个包asdf需要先找到该包对应的.asd文件,该文件一般是存放在源码根目录中的,asdf对于该文件的寻址有两种方式,new style和old style,下面分别简单介绍下这两种方式:

old style是为了兼容旧版本的程序的,目前已经不推荐新程序使用,其方法是将程序包的.asd文件所存在的路径添加到asdf的*central-registry*变量中,可使用如下语句完成该操作:

(push "/home/levin/lisp/spider/" asdf:*central-registry*)

上述操作便可将/home/levin/lisp/spider/(注意结尾的’/')这个路径添加到*central-registry*变量中,这样asdf便可对该包进行寻址,但在REPL中执行的该操作只对当前的会话有效的,REPL重启后需要重新添加路径到该变量中,未免有些繁琐,可以将该语句添加至common lisp实现的启动文件中,如sbcl即为.sbclrc这个文件,这样在REPL启动之后该路径便会自动添加到*central-registry*变量中。

new style是ASDF2所提倡使用的方法,其方法是在~/.config/中创建common-lisp目录用于存放相关的配置文件,和该主题相关的配置文件需要创建一个名为source-registry.conf.d的目录,在该目录下可以创建文件名任意的文件,将下面的语句添加至该文件中即可,文件如:

~/.config/common-lisp/source-registry.conf.d/01-spider-source.conf

(:directory "/home/lisp/lisp/spider/")

如上表示在/home/lisp/lisp/spider/这个目录中查找.asd文件,也可以将:directory替换成:tree,使asdf在目录中递归寻找.asd文件:

(:tree "/home/lisp/lisp/spider/")

完成对asdf包的寻址操作之后,便可以使用asdf对程序包进行编译加载,编译和加载包分别可使用:

CL-USER> (asdf:compile-system :spider)
CL-USER> (asdf:load-system :spider)

也可以使用asdf的operate对包进行编译和加载:

CL-USER> (asdf:oos 'asdf:compile-op :spider)
CL-USER> (asdf:oos 'asdf:load-op :spider)

对于一个未经编译完成的包在编译完成会自动加载,同样,未编译完成的包在加载时也会自动先进行编译。

3. asdf系统的构建:

之前提到的.asd文件是asdf的很重要的文件,该文件最简单的形式如下:

(defpackage :spider-system
  (:use :cl :asdf))
(in-package :spider-system)
 
(defsystem spider
  :name "spider"
  :author "levin li"
  :version "0.0.1"
  :license "MIT"
  :description "A spider program."
  :depends-on (:iolib)
  :components ((:file "package")
               (:file "spider" :depends-on ("package"))))

最开头的包定义是为了防止system名与其它包冲突,定义的包中包含一些基本信息,:depends-on声明了该程序将要依赖的包的,:components定义了包中的组件,即源代码,上述的源码包中只有两个文件,package.lisp和spider.lisp,package.lisp是定义一个cl package,可以理解为其它语言中的namespace,package.lisp定义了一个package,之后的程序如spider.lisp可能都会包含在该包中,因此:depends-on(“package”)这句话就非常重要,它会在编译spider.lisp之前先加载已经编译的好的package.lisp,若未声明spider依赖package,则在一次编译完成后REPL重启并对spider.lisp进行修改,而package.lisp未加改动,则asdf只会编译修改过的spider.lisp,这时候系统会提示spider包未找到,因为asdf发现package.lisp的目标文件是最新的,默认不会对其进行编译和加载,这也是我刚开始遇到的问题之一,在认真读了ASDF Manual后才解决了这个问题,关于该文件的一些详细的定义可以参考ASDF Manual。

package.lisp的内容如下:

(in-package :cl-user)
 
(defpackage :spider
  (:use :cl :iolib)
  (:export :send-request
           :test))

其中定义了包的名称,包的依赖的其它包,及要导出的符号等,关于package的更多介绍还是参考《practical common lisp》等资料。而spider.lisp是代码文件,开头需要有一行:

(in-package :spider)

表明当前程序包在spider包中定义,继而可以在spider.lisp中编写其它的逻辑代码,定义函数,宏或变量,所有的这些定义都被包含在包spider中,其中只有在package.lisp中声明为:export的符号才可以被其它包所引用,如上定义,该包中只有send-request和test两个函数可以被其它外部包引用。

4. asdf包的安装

绝大多数的common lisp包都是使用asdf组织的,可以使用asdf-install工具安装软件包,asdf-install使用之前需要先加载:

CL-USER> (asdf:oos 'asdf:load-op :asdf-install)

之后便可以使用asdf-install安装软件包,一般而言有三种方式:

1. 通过包的名字进行安装:

CL-USER> (asdf-install:install "iolib")

这时asdf-install会自动从cliki.net上下载可用的包并安装,http://www.cliki.net/asdf-install这个页面列出了在线可用的软件包的列表。

2. 通过包的url进行安装:

CL-USER> (asdf-install:install "http://weitz.de/files/cl-ppcre.tar.gz")

3. 通过包的本地路径进行安装:
CL-USER> (asdf-install:install “/home/levin/lisp/iolib.tar.gz”)

更加详细的安装方法可以参考asdf-install turorial.

分类: Lisp 标签: , ,

毕业求职经历

十一月 2nd, {2011 27 条评论 16,014 人阅读过  

前天去淘宝把三方交了,找工作就到此为止了,找工作的经历虽然不算复杂漫长,但也觉得很累很辛苦。

正式开始投简历找工作应该是从九月份开始的,九月中旬注册了几家招聘网站,陆陆续续地开始往外投简历,目标也比较明确,虽然专业一直是通信,但却想去互联网公司做后端开发,服务器或者分布式系统相关的开发,所以通信类的公司没有怎么投简历,只投了华为的云计算方向和爱立信的软件研发方向。国企没有投,片面地觉得国企是最没有战斗力的公司,还是想到互联网行业的私企做些有挑战性的工作。

第一家面试的公司是IBM研究院,在上地的钻石大厦,离北邮超级远,先坐车到清华西门,然后转车到西北旺,下车后发现右手边是农田,左手边是树林,没有路人,没有出租车,于是完全迷路了,后来沿着树林走了一段时间后偶然发现树林对面有座大楼冒出个头来,于是穿过树林过去问了下那楼的保安,说那就是钻石大厦,OMG,让我好找。IBM CRL没有笔试,师兄推荐的部门,因为是第一次面试没什么经验,也没有准备英文自我介绍,还有些紧张,做了一个简单的slide,没有被问到复杂的算法问题,因为时间比较短,所以挑了一两个重点的项目讲了讲,结束后感觉表现真的很烂,也可能是师兄推荐的原因最后让我拿到了二面的机会,二面安排在十一前,仍然是讲项目,在师兄的指点下改了slide,而且也提前准备了英文自我介绍,这中间也参加过几次面试,也有了点经验,所以感觉还不错,结束后几天也拿到了互联网公司的offer,IBM CRL感觉不太适合我,那边偏向于研究一些,像我这种做工程的在那边确实不是太好发展,于是联系师兄把情况说清楚了,已经浪费了大家两次面试的时间了,如果有第三次面试机会我还是不去了,这样IBM的面试也就到此为止了。

第二家面试的单位应该是华为了,之前参加过一个华为的机试,出奇的简单,就只有一道题补全一个函数,功能是求数组里面小于平均数的数的个数,两分钟就写完了然后在机房里面刷推想等下同学,于是就被赶出去了。过了不久通知面试,在知春路某酒店,记得通知的是12点,十一点半赶过去,等了一会儿面试官让先去吃饭,吃完饭回来继续等,直到等到下午将近一点半实在有点不耐烦了,去找hr简单表达了一下不满,于是过了十分钟大约一点半左右被安排了面试。虽然面试的是云计算,但我相信给我面试的那个姐姐应该对云计算也没有什么深入的了解,而且对我的开源项目表现出不屑,只对简历上写的实验室的863项目感点兴趣,说是因为是国家项目,其实学校里面做的国家项目大家都懂的,当时自己心情也确实没那么有耐心了,毕竟已经在那里浪费了两个小时的时间了,一个多小时后结束,第二天居然收到了二面通知,没有去参加二面,华为就这样结束了。

考虑到华为的这种情况,通信公司我已经完全不想考虑了,爱立信的机试我也没有参加。

网易有道经过实验室师兄内推搞到了一个提前参加笔试的机会,清华科技园创业大厦,笔试是在晚上,题量不大但做得很烂,但很幸运地收到了面试通知,一面的时候和面试官比较投缘,问的问题我也大都了解,tcp/ip,socket,epoll等等的,都比较熟,算法问题也都答的差不多,很顺利进了二面,二面表现比较差,问了几个系统设计的题,微博的推送机制,搜索引擎的搜索提示,这些我之前还真没去了解过,想了想把就重点说了下cache,结合着kv系统简单设计了下,面试官也没说好坏,开始问的两个算法题记不清是什么了,但印象中是没有什么好的思路的,那一次面试感觉非常差,但因为没有收到拒信也纠结了好久,终于在十一过后的某一天收到了三面的通知,有点意外,三面的大哥没有问算法,问我的软件的架构,分布式缓存系统设计等等,感觉答的还不错,感觉至少要比二面好,等了好久,却收到了有道的拒信,有道成了第一家拒掉我的公司,找工作需要实力也需要机缘,有道的面试我应该是实力和机缘都不够吧。

淘宝的笔试在华为二面的那天晚上,没有参加华为下午的二面也是怕他们太拖拉耽误了淘宝的笔试,笔试现场很混乱,因为没有记名,霸面的同学把屋子挤得满满的,记不清当时是怎么协调的了,出着汗答到时间结束,最后一个算法大题居然没来得及想,随便写了个简单算法,后来面试的时候瞅了一眼笔试的试卷,那个题只得了3分,后面的附加题到是抽时间做了,线程进程,锁,网络编程都是比较熟悉的东西。在北理参加360笔试的时候收到了淘宝的面试通知,东三环嘉泰金融大厦,离北邮也确实有点远,淘宝当天面试的人很多,但还算比较准时的,一面问的基础都比较熟悉,每次一被问到STL我就怂了,这东西只会简单的用用,不过跟面试官表达过长时间使用C之后,他问的关于STL的都是怎么用的方面,这些我还都是搞得定的,后来让写了个递归的小程序,也很简单,结束后让我去休息去等一会儿,过了一会儿又把我叫到一间小会议室里面,我问这是要干嘛,说是面试啊,原来是连着面的,于是自我介绍讲项目,二面的面试官感觉很犀利,问C10K,prefork/worker-master在C10K问题上的差别,我之前理解的也不太对,面试官也都给我解释清楚了,后来问了很多,关于读的书,平时的爱好等等,还出了两个智力题,只想出来了一个,本来有不少问题要问的,关于淘宝的开源产品等等的问题,后来HR在外面示意时间应该是超了,我就没仔细问,于是又去休息区等着了。中午淘宝请吃了个午饭,下午一点多三面,见到了大牛行癫,被问到了Memcached和Redis的哈希表的处理方式的问题,异步的expand,但Memcached其实我表达错了,Memcached是异步来rehash的,而Redis因为是单线程,它的rehash是嵌在各种操作(lookup,insert)中进行的,都不是一下全部完成的,因为大数据量时rehash会相当耗时,当然,我理解不对的地方行癫也都给我解释清楚了,最后还把二面关于淘宝基础产品的问题都问了,之后就是回来等消息了,几天后收到offer,没有传说中的几十w年薪,选择淘宝是因为在淘宝能做想做的方向,@colyli帮忙推荐到淘宝的虚拟化团队做分布式存储,这个方向我很感兴趣,所以拿到淘宝offer之后找工作就基本接近尾声了。

360的面试是在淘宝后的几天,北大博雅大酒店,打车杀了过去,360也是三面连着的,前两面是技术面,最后一面是hr面,一面的技术面问了很多基础知识,甚至还问了vim的操作,这些都比较熟悉,另外也问了C10K的问题,其它的都是在聊天了,面试官人很好,给我讲hadoop,分布式系统等等,二面也差不多的流程,感觉面试官主要是想看我能不能把心思都放到工作上,可能是因为简历上我在工作之外做的自己的项目比较多,当然,这个肯定不是问题,三面过后也同样拿到了360的offer,和淘宝几乎是同一时间,感觉非常对不起360,没有决定去却接受了人家的offer,这是非常损rp的,所以在把三方交给淘宝之后赶紧写邮件给360的hr拒掉了这个offer。

腾讯的笔试应该是在清华,比较晚了,因为当时拿到了淘宝的offer,其实已经不太想再折腾了,因为感觉选择越多最后纠结就越多,淘宝的工作我已经很满意了,但后来觉得腾讯是中国互联网老大,不去试试也不甘心,笔试还不错,题基本上都会,一面是在知春路的京仪大酒店,两点钟面试官非常准时,问的东西都很常细,线程池,锁,网络编程的细节,算法,都是些很细节的问题,那次面试我才知道有pthread_cond_timewait()这个函数,惭愧,二面安排在腾讯北京分公司,苏州街银科大厦,面试过程比较随意,面试人很好,感觉就是聊天,问了一些关于项目的问题,最后问了一个智力题,当时感冒还发着烧,脑子实在是转不动,于是就没想出来,于是就这样结束了,三面是hr面,hr叔叔人也很好,问了一下工作中遇到的问题,offer的期待等等,二十多分钟,最后送我走的时候还给我介绍了腾讯北京这边的情况,给我介绍了腾讯演播室,带我看了很多名星的签名,照片,走的时候还看到了超级正典腾讯18层的前台MM,突然觉得去腾讯工作也不错哈。hr说要11月1号到10号之间才会发offer,30号我就需要把三方交给淘宝,跟hr说过这个情况,说自己没有太多选择,希望能尽快得到腾讯的消息好做决定,但30号也实在太仓促只有两天的时间,于是最后只能选择了淘宝,今天是2号仍然没有收到腾讯的消息,有可能是挂了,即便是有幸拿到offer现在也去不了了。

参加过的笔试还有微软,EMC这两家笔试完都没消息了,微软笔试答得很烂,可以理解,EMC笔试感觉还不错的,结果也一样挂了,当时都没怎么重视笔试,以为笔试一般不会挂掉,但事实上我大多数都是挂在了笔试。

之前也参加过谷歌的笔试,安排在了清华的技科楼,当时顶着巨大的堵车压力勉强按时赶到清华,但诺大一个清华居然没有人知道技科楼怎么走,问了好久都没有找到路,最后好不容易找到个地图才发现了那位置,找到考场的时候已经开始考了将近20分钟了,最近我还很嚣张地提前交了试卷,其实感觉答得还算可以,不过肯定是过不了的,后来果然收到了谷歌的拒信,不过能有机会参加一次谷歌的笔试也很满足啦,嗯。

参加过笔试面试的差不多就这几家,没有广撒网式的投简历,之前也投过新浪搜狐亚马逊等互联网公司的简历,最后都放弃了笔试,当然,绝不是看不上这些公司,只是觉得有个差不多的工作就可以了,选择越多纠结越多,而且最后拿到别人的offer最后又不去给人家公司也会带来麻烦,影响人家招聘。

找工作的经历基本上就这些了,抓紧时间把毕业论文搞出来,也希望能有时间可以去淘宝实习几天,提前了解下要做的东西。

分类: My Life 标签:

Redis的事件循环与定时器模型

十月 7th, {2011 1 条评论 15,355 人阅读过  

假期的最后一天,简单翻阅了下Redis的源码,读一款server软件的源码我一般是从进程/线程模型开始的,Redis让我有些诧异,它采用了单进程单线程的模型,一般的server软件都会采用多进程或者多线程再或者多线程多进程混合的模型来设计,从而充分利用多核处理器的并行计算能力来提高软件的性能,Redis这种模型我只能推断程序的可并行化程度不高,顺序计算反而能省去多线程同步和维护线程池/进程池的开销,我对于数据库server端的设计没有什么经验也没有太多的理解,如有谬误欢迎大家指正。

当然,这里要写的不是关于Redis的进程模型,而是Redis的事件模型和定时器模型。

Redis没有依赖libevent,而是自己通过IO多路复用的方式来实现了事件循环和定时器,不像nginx或者apache有多种多路复用方式可供选择,Redis只采用了三种:epoll/kqueue/select,默认采用epoll,在linux环境下最优的方式当然是epoll,当在FreeBSD平台下epoll不存在时则使用kqueue,当然若两种方式都未定义则使用性能最差的select,我只阅读了跟epoll相关的代码。

main()函数的最后调用了aeMain()这个函数进入Redis的事件循环,这个函数的很简单,循环调用aeProcessEvents()来对事件进行处理:

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在此之前Redis做了很多初始化的工作,这些工作大多是在initServer()这个函数中执行的,初始化一些相关的list,dict等,调用aeCreateEventLoop()初始化eventloop,这个函数初始化eventloop相关的数据结构,并最终调用了epoll_create()函数,对epoll上下文进行初始化。紧接着Redis创建了用于listen的socket对象,并调用aeCreateFileEvent()把该socket描述符的读事件加入到事件池中去,另外,还调用了aeCreateTimeEvent()函数来初始化一下定时器,定期地执行serverCron()这个函数,接下来看一下aeCreateFileEvent()aeCreateTimeEvent()这两个函数。

aeCreateFileEvent()这个函数初始化aeFileEvent结构(该结构保存事件的一些状态,以及事件的文件描述符等),并调用aeApiAddEvent()函数将描述符相关的事件添加到事件池中,对于epoll它的实现如下:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;
 
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

非常简洁,这个函数只不过是把epoll_ctl()相关的操作做了一下封装,至此描述符已经加入到事件池中进行监听了,接着看aeCreateTimeEvent()这个函数。

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    long long id = eventLoop->timeEventNextId++;
    aeTimeEvent *te;
 
    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    te->id = id;
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    te->clientData = clientData;
    te->next = eventLoop->timeEventHead;
    eventLoop->timeEventHead = te;
    return id;
}

同样是初始化数据结构,但没有调用aeApiAddEvent()这个函数,当然,定时器又不需要文件描述符,当然不需要添加相关事件,定时器的实现只是使用了epoll_wait()的定时功能,aeAddMillisecondsToNow()这个函数顾名思义是把当前时间加上一个给定的毫秒数,然后算出一个when_sec和when_ms,eventloop对象的timeEventHead实际上是一个单向链表,它用于保存所有的定时器事件,当添加一个定时器事件时其实只是向该链表头中插入了一个元素,其会后由aeProcessEvents()这个函数遍历该链表取出超时的事件进行处理,接着我们看下这个事件处理里面最核心的函数。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;
 
    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
 
    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;
 
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            long now_sec, now_ms;
 
            /* Calculate the time missing for the nearest
             * timer to fire. */
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to se the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                tvp = NULL; /* wait forever */
            }
        }
 
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;
 
	    /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            if (fe->mask & mask & AE_READABLE) {
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
 
    return processed; /* return the number of processed file/time events */
}

这其中的aeApiPoll()这个函数其实就是对epoll_wait()操作的一个封装,epoll_wait()的最后一个参数是一个毫秒级的超时时间,Redis充分利用了这个时间在对IO事件进行监听的同时来实现了定时。这个函数的前面一大部分代码是在计算这个超时时间的,它调用aeSearchNearestTimer()这个函数来获取最近要超时的一个定时器对象,如何获取的呢?就是遍历刚才的提到的那个timeEventHead链表,来找出时间值最小的一个,注意是遍历,因为链表中的定时器也是无序的,不过我相信作者有一天会把它换成红黑树或者其它的数据结构吧。如果找到一个将要超时的定时器,则将它与当前时间进行比较,如果当前时间大于定时器时间则表示定时器已超时,将超时时间设为0,若当前时间小于定时器时间,则将超时时间设为两者之差。如果定时器队列为空,或者说没有任何定时器事件,则可以根据AE_DONT_WAIT这个标志来决定epoll_wait()是non-blocking立即返回,还是一直阻塞在那里。

aeApiPoll()函数返回时,有两种情况,一种是IO事件被触发,另一种是定时器超时,当IO事件被触发时,遍历所有活跃描述符并调用相关的回调函数对其进行处理。当没有IO事件被触发,而是超时时,则返回值numevents为0,函数会转向processTimeEvents()来遍历定时器列表,调用定时器回调函数处理定时器事件,当IO事件被触发而并没有定时器超时时,如果设置了AE_TIME_EVENTS标志则也会对定时器列表进行遍历,主循环便是如此,我认为这会多少对效率有一定的影响,当然可能现在的Redis定时器列表并不太大,所以效率问题也可以忽略。

以上是简单地对今天的工作做的总结,欢迎大家批评指教。

分类: NoSql 标签: , ,

Memcached内存管理机制浅析

九月 8th, {2011 5 条评论 16,899 人阅读过  

Memcached的内存管理在网上也可以搜集到不少不错的文章,新浪的这篇《Memcached深度分析》讲得不错,读别人的文章还是不如自己直接去读源码分析源码来得直接,这里写一下我阅读Memcached源码时对于Memcached内存管理机制的理解。

Memcached的代码结构很简单,从main()函数入口进去之后便是几个模块的初始化函数,和内存管理相关的主要有两个函数,一个是assoc_init(),这个是用来初始化哈希表的,关于这个哈希表的作用留在外面讨论,另一个是slabs_init(),该函数用来初始化slab,下面先来讨论一下slab机制。

1. 内存slab的管理
1.1 slabs的初始化

Memcached把内存分为一个个的slab,每个slab又分成一个个的chunk,系统会定义一个slab_class数组,其中每个元素是都是一个对该slab的描述,包括这个slab里面的每个chunk的大小,这个slab里面包含多少个chunk等信息,先把slab分布情况打印出来看看,对Memcached的内存分配有个大体的认识,然后再去读代码可能会好一些。

$ memcached -vv
slab class 1: chunk size 80 perslab 13107
slab class 2: chunk size 104 perslab 10082
slab class 3: chunk size 136 perslab 7710
slab class 4: chunk size 176 perslab 5957
slab class 5: chunk size 224 perslab 4681
slab class 6: chunk size 280 perslab 3744
slab class 7: chunk size 352 perslab 2978
slab class 8: chunk size 440 perslab 2383
slab class 9: chunk size 552 perslab 1899
slab class 10: chunk size 696 perslab 1506
slab class 11: chunk size 872 perslab 1202
slab class 12: chunk size 1096 perslab 956
slab class 13: chunk size 1376 perslab 762
slab class 14: chunk size 1720 perslab 609
slab class 15: chunk size 2152 perslab 487
slab class 16: chunk size 2696 perslab 388
slab class 17: chunk size 3376 perslab 310
slab class 18: chunk size 4224 perslab 248
slab class 19: chunk size 5280 perslab 198
slab class 20: chunk size 6600 perslab 158
slab class 21: chunk size 8256 perslab 127
slab class 22: chunk size 10320 perslab 101
slab class 23: chunk size 12904 perslab 81
slab class 24: chunk size 16136 perslab 64
slab class 25: chunk size 20176 perslab 51
slab class 26: chunk size 25224 perslab 41
slab class 27: chunk size 31536 perslab 33
slab class 28: chunk size 39424 perslab 26
slab class 29: chunk size 49280 perslab 21
slab class 30: chunk size 61600 perslab 17
slab class 31: chunk size 77000 perslab 13
slab class 32: chunk size 96256 perslab 10
slab class 33: chunk size 120320 perslab 8
slab class 34: chunk size 150400 perslab 6
slab class 35: chunk size 188000 perslab 5
slab class 36: chunk size 235000 perslab 4
slab class 37: chunk size 293752 perslab 3
slab class 38: chunk size 367192 perslab 2
slab class 39: chunk size 458992 perslab 2
slab class 40: chunk size 573744 perslab 1
slab class 41: chunk size 717184 perslab 1
slab class 42: chunk size 1048576 perslab 1

这是Memcached的默认配置,chunk size是按照CHUNK_ALIGN_BYTES对齐的,chunk size相比于前一个slab中的chunk size有一个上升因子factor,1.4.7里面factor的默认值是1.25,我们可以看到按默认配置slab总共分成了42类。
先给出一个我用Dia画的Memcached的内存分配图,Dia不如Visio好用,凑合着画了一个,如果有理解不对的地方欢迎大家指出。

接下来看一下slabs_init()的代码,还是只保留关键代码,节省版面。

void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
    unsigned int size = sizeof(item) + settings.chunk_size;
 
    mem_limit = limit;
    if (prealloc) {
        /* Allocate everything in a big chunk with malloc */
        mem_base = malloc(mem_limit);
        if (mem_base != NULL) {
            mem_current = mem_base;
            mem_avail = mem_limit;
        } 
    }
    memset(slabclass, 0, sizeof(slabclass));
    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES)
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
 
        slabclass[i].size = size;
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
        size *= factor;
    }
    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
#ifndef DONT_PREALLOC_SLABS
    {
        char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");
 
        if (pre_alloc == NULL || atoi(pre_alloc) != 0) {
			printf("prealloc memory.\n");
            slabs_preallocate(power_largest);
        }
    }
#endif
}

prealloc这个参数其实是跟后面的DONT_PREALLOC_SLABS这个宏是相关的,表示是否要在初始化的时候为slabs分配内存,如果需要预先为slabs分配内存,则先跟系统申请mem_limit字节的内存,之后的slab都是从这块内存上分配的,这块内存大小默认是64M,说起来当时犯了个很低级的错误,当时看到这里的时候发现mem_limit的默认值是1024 * 1024 * 64,于是断点在这里,发现malloc()没有返回NULL,当时想我一个2G的机器申请64G的内存到底是怎么分配成功的,纠结了好久才发现不是64G,是64M,所以读代码/写代码的时候还真得保持头脑清醒才行。。。

然后再说prealloc,如果没有定义DONT_PREALLOC_SLABS这个宏的话,初始化的时候会先申请64M的内存,接着调用preallocate这个函数,看下这个函数的注释,然后我要吐槽一下我的英语,前两天被它的那句注释搞晕了。

#ifndef DONT_PREALLOC_SLABS
/* Preallocate as many slab pages as possible (called from slabs_init)
on start-up, so users don't get confused out-of-memory errors when
they do have free (in-slab) space, but no space to make new slabs.
if maxslabs is 18 (POWER_LARGEST - POWER_SMALLEST + 1), then all
slab types can be made. if max memory is less than 18 MB, only the
smaller ones will be made. */
static void slabs_preallocate (const unsigned int maxslabs);
#endif

这句话的大体意思应该是:在启动的时候尽可能多地分配slabs页,所以用户不要因为内存足够却获得OOM而感到郁闷。。。

我当时的理解时:在启动的时候尽可能多地分配slabs页,这样用户就不会因为内存足够却被提示OOM而感到郁闷了。。。

我仔细地查阅源代码,发现如果prealloc,那么64M的内存用光之后并不会再去malloc新内存,跟我当时理解的这句注释的意思正好相反,仔细研究代码发现代码没有什么问题之后我反过来看了一眼这句注释,我觉得是我把这句英文理解错了吧。。。

OK,也就是说如果开启了prealloc功能的话,那么很有可能在有空闲内存的情况下分配内存失败,另外提前为slabs分配内存也有可能会造成内存的浪费,有可能所有的item都不会使用某个slab class,这样这个slab class里面分配的内存就浪费掉了,DONT_PREALLOC_SLABS在1.4.7里面是默认定义的,也就是说prealloc功能是默认关闭的,于是就不考虑先prealloc了。

slabs_init()接下来的代码就很简单了,对每个slab的chunk size进行对齐然后设置该slab class的相关成员变量的值。

1.2 slabclass_t的结构介绍
typedef struct {
    unsigned int size;      /* sizes of items */
    unsigned int perslab;   /* how many items per slab */
    void **slots;           /* list of item ptrs */
    unsigned int sl_total;  /* size of previous array */
    unsigned int sl_curr;   /* first free slot */
    void *end_page_ptr;         /* pointer to next free item at end of page, or 0 */
    unsigned int end_page_free; /* number of items remaining at end of last alloced page */
    unsigned int slabs;     /* how many slabs were allocated for this class */
    void **slab_list;       /* array of slab pointers */
    unsigned int list_size; /* size of prev array */
    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    size_t requested; /* The number of requested bytes */
} slabclass_t;
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

size和perslab这两个字段已经说过了,slots这里存放的是空闲的slab列表,当调用do_slabs_free()这个函数之后,要释放的chunk就被放到这个数组的尾部,sl_curr数组尾部开始的第一个空闲的chunk,sl_total表示数组的总大小,当sl_curr大小等于sl_total的时候数组会通过realloc()进行扩容,容易是旧容量的2倍。

end_page_ptr这个字段表示该slab里面的当前空闲的chunk地址,end_page_free,表示该slab中剩余的空闲chunk的数目,其它的几个字段按注释都很容易理解了。

1.3 创建新的slab

前面提到的slabs_preallocate()函数只不过是对每一个已初始化的slab_class调用do_slabs_newslab()函数为其分配一块slab内存空间,看下这个函数的代码。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = p->size * p->perslab;
    char *ptr;
 
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
        ((ptr = memory_allocate((size_t)len)) == 0)) {
        return 0;
    }
 
    memset(ptr, 0, (size_t)len);
    p->end_page_ptr = ptr;
    p->end_page_free = p->perslab;
 
    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
 
    return 1;
}

slab_class的slab_list字段保存的是已分配的slabs列表,该列表实际上是个数组,当数组中没有空闲位置时则会调用grop_slab_list()对数组进行扩容,接下来便会调用memory_allocate()给slab分配内存,这个函数会检测是否已经开始了prealloc功能,如果开启了便会在预分配的内存块上申请一块内存,当这块预分配的内存用完时并不会对其进行扩容,于是便返回分配内存失败,这也就造成了系统明明有剩余内存,Memcached却提示SERVER_ERROR out of memory,当然,如果没有开启prealloc功能,这个函数便会直接调用malloc()分配内存,接下来对各个指针进行初始化。刚分配的空闲slab,它的end_page_str指针是指向slab内存首部的,end_page_free字段代表slab内存中包含中的chunk数。

1.4 在slab上分配内存

在某个slab_class上分配size大小的内存的函数是do_slabs_alloc(),这个函数有两个参数,要分配的内存字节数size,和该内存应该存在于哪个slab class上的slab class id. 这两个参数在是有相关性的,在调用该函数的时候class id一般是通过size来计算得出来的,先看一下这个函数:

static void *do_slabs_alloc(const size_t size, unsigned int id) {
    slabclass_t *p;
    void *ret = NULL;
 
    if (id < POWER_SMALLEST || id > power_largest) {
        return NULL;
    }
 
    p = &slabclass[id];
 
    /* fail unless we have space at the end of a recently allocated page,
       we have something on our freelist, or we could allocate a new page */
    if (! (p->end_page_ptr != 0 || p->sl_curr != 0 ||
           do_slabs_newslab(id) != 0)) {
        /* We don't have more memory available */
        ret = NULL;
    } else if (p->sl_curr != 0) {
        /* return off our freelist */
        ret = p->slots[--p->sl_curr];
    } else {
        /* if we recently allocated a whole page, return from that */
        ret = p->end_page_ptr;
        if (--p->end_page_free != 0) {
            p->end_page_ptr = ((caddr_t)p->end_page_ptr) + p->size;
        } else {
            p->end_page_ptr = 0;
        }
    }
 
    if (ret) {
        p->requested += size;
    }
 
    return ret;
}

1.如果end_page_ptr等于0,并且sl_curr等于0,则表示slab中已经没有空闲内存,并且回收的chunk free list里面也没有可用内存了,于是这时候需要调用do_slabs_newslab()创建新的slab。

2.如果sl_curr不等于0,则表示chunk free list中还有可用的内存,直接返回一个可用的chunk即可。

3.如果chunk free list里面没有可用内存,而slab中还有空闲内存,则直接从slab中申请一个chunk的内存,然后将end_page_ptr后移。

2. 对象item的管理
2.1 item对象的分配

存入系统的每个key-value对都会被转换成一个item,这个item中保存了相关的状态标志信息,当服务器收到一个set请求时便需要在内存中创建一个item,item的内存理所当然是在上面讨论过的slab分配器上分配的。item的存储使用了LRU的方法,把item链入一个链表中,其中全局变量heads[LARGEST_ID]tails[LARGEST_ID]这两个数组保存各个slab class所对应的item链表的表头和表尾,item创建的函数do_item_alloc()太长,就不把代码贴出来了,描述一下它的过程。

在这里先提一下前面提到的哈希表,哈希表是用来把item通过key散列到哈希表上的,这样就可以通过key来快速地定位item,在do_item_unlink()这个函数中,首先要把该item从哈希表中删除,然后再从list中移除,最后检测该item的refcount,如果refcount是0,则调用item_free()释放内存,item_free()再调用底层的slab_free()去释放内存,slab_free()只是do_slab_free()的线程安全版本,它在内部先加锁随后调用do_slab_free(),再之后解锁。

OK,接着看item的分配过程,首先会从链表的尾开始往前找,如果某节点的item设置了过期时间并且该item已过期,则回收该item,调用do_item_unlink()把它从链表中取出来,刚才说过do_item_unlink()这个函数在refcount为0的时候会释放掉这个item,所以为了防止这个item内存被释放,先将它的refcount设置为1,若向前查找50次都没有找到符合要求的item,则循环断开。

如果没有找到可以回收的item,然后就调用slabs_alloc()分配内存,如果内存也分配失败,就尝试着从链表尾开始向前找出一些没有人用的item(refcount=0),把它do_item_unlink()掉,这时候因为refcount=0,所以它相关的内存也会被释放还给slab分配器,这个尝试又从尾向前尝试50次,OK,slab分配器中可能又有可用内存了,再用slabs_alloc()分配内存,如果还失败。。。好吧,这次只能从链表中删除一些正在引用但过期时间小于current_time – CURRENT_REPAIR_TIME的节点,这个尝试又从尾向前尝试50次,OK,再做最后一次尝试再去slabs_alloc()分配内存,如果这次还是失败,那就彻底放弃了,内存分配失败。。。

3. Memcached的哈希表
3.1 Memcached用到的哈希算法

Memcached用到的哈希算法比较复杂,算法地址在http://burtleburtle.net/bob/hash/doobs.html,Memcached维护了两个哈希表,primary_hashtable和old_hashtable,primary_hashtable是当前正在使用的哈希表,当表没有进行扩张时从这张表中插入或者查找,old_hashtable用于哈希表扩张的时候使用,它指向旧的哈希表,当哈希表中的item数大于表的大小的3/2时,则哈希表进行扩张,此时插入和查找等操作都是在old_hashtable中进行的。

3.2 数据项插入哈希表

数据项插入哈希表时用了assoc_insert()这个函数,下面看下它的代码

int assoc_insert(item *it) {
    uint32_t hv;
    unsigned int oldbucket;
 
    assert(assoc_find(ITEM_key(it), it->nkey) == 0);  /* shouldn't have duplicately named things defined */
 
    hv = hash(ITEM_key(it), it->nkey, 0);
    if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        it->h_next = old_hashtable[oldbucket];
        old_hashtable[oldbucket] = it;
    } else {
        it->h_next = primary_hashtable[hv & hashmask(hashpower)];
        primary_hashtable[hv & hashmask(hashpower)] = it;
    }
 
    hash_items++;
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
        assoc_expand();
    }
 
    MEMCACHED_ASSOC_INSERT(ITEM_key(it), it->nkey, hash_items);
    return 1;
}

如果expanding是true,哈希表正在扩张,则把item插入到old_hashtable中,否则则插入到primary_hashtable中,然后检测item数是否大于hashsize * 3 / 2,如果是,则进行扩张,哈希表的查找删除等操作也大致类似,不拿出来说了。

3.3 哈希表的扩张

哈希表的扩张其实是异步进行的,Memcached在初始化时在main()函数中会调用start_assoc_maintenance_thread()函数来开启一个线程对哈希表进行定期维护,线程函数通过对条件变量的wait进行睡眠,当被激活时发现expanding为true,则对哈希表进行扩张,把旧表的元素复制到新表中,然后释放旧表的内存空间,搞定后再睡去。。。触发哈希表扩张事件的函数是assoc_expand()

static void assoc_expand(void) {
    old_hashtable = primary_hashtable;
 
    primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
    if (primary_hashtable) {
        if (settings.verbose > 1)
            fprintf(stderr, "Hash table expansion starting\n");
        hashpower++;
        expanding = true;
        expand_bucket = 0;
        pthread_cond_signal(&maintenance_cond);
    } else {
        primary_hashtable = old_hashtable;
        /* Bad news, but we can keep running. */
    }
}

这个函数让把old_hashtable指向primary_hashtable,之后给primary_hashtable重新分配内存空间,然后把expanding标志设为true,接着激活maintenace_cond信号,maintenace线程被唤醒开始异步地把old_hashtable中的元素拷贝到primary_hashtable中来。

OK,这是我对Memcached内存管理机制的一个简单的探索和了解,如有谬误的地方,欢迎大家批评指正。

分类: NoSql 标签: ,

VIM复制粘贴的那些事

九月 7th, {2011 11 条评论 16,121 人阅读过  

说起vim的复制粘贴一直是个比较困扰我的问题,之前一直用gvim,跟系统剪贴板之间的复制粘贴都没有问题,gvim毕竟还需要再开个窗口,麻烦,而且不如vim那样快捷,但vim里面最让我头痛的是复制粘贴问题,想把vim里面的内容复制到其它的地方貌似怎么也不行,粘贴进来的话免强可以,但格式可能会很乱,折腾来折腾去,今天先是发现了个往外复制比较蹩脚的办法:

:set mouse=v

这样鼠标就可以变成文本选择指针的样子,可以选择选择字体,然后点右键选复制,或者CTRL+SHIFT+C,但如果mouse=a这种模式下的话选择之后,右键的复制是灰色的,当然这种情况复制如果有行号的话行号也会被复制进去,所以在复制前先把行号关了,复制完再打开,这办法貌似也能凑合,但着实太不专业,而且太麻烦,远不如用gvim的“+y这种来得方便,“+y这个很多推友表示是可以的,但在我这里不可以,刚经@multiple1902指点,有可能是vim编译的问题。

查看了一下vim版本相关信息:

vim --version

发现clipboardxterm_clipboard这些选项都是不可使用的功能,这样看来果然是编译问题了,slackware讲求KISS,编译的时候没有加入该选项也可以理解,于是从vim官网svn上check下来源码(话说我为啥check下来的是7.2呢,难道7.3还没有stable…),重新编译。

编译的时候必须要选上的选项一个是–enable-multibyte,如果没加这个选项的话,中文应该就会乱码了。
另外关于剪贴板相关的选项,具体我也不清楚是哪一个,看了下几个相关的选项觉得最有可能是的是–enable-xim,只加了这两个选项之后编译完测试发现vim已经可以和系统剪贴板之间共享数据了,“+y可以使用了,世界一下子就清爽了很多,于是重新加完整选项编译:

./configure --prefix=/usr \
            --sysconfdir=/etc \
            --enable-tclinterp \
            --enable-pythoninterp \
            --enable-perlinterp \
            --enable-rubyinterp \
            --enable-cscope \
            --enable-multibyte \
            --enable-xim \
            --enable-gtk2-check \
            --enable-fontset \
            --with-x

OK,编译完成之后removepkg vim删掉之前slackware安装包中提供的vim,然后用makepkg打包安装,哎呀,说起包管理方式我还是最喜欢slackware的tgz包,虽然没有debian的apt那么华丽,但简洁就是美啊。

vim安装完之后已经可以用了,但存在一个问题,vim退出之后终端便会乱码,到网上搜了半天各种关于ubuntu下的解法,拿到slackware上来完全不适用,命令啦路径啦什么的根本找不到,最后搜到一个非常简单的解决办法,说是这个问题是因为gnome-terminal标题名称的不标准,我矁了一眼标题写着“终端”两个字,给出的解决办法是在前后各加一个空格,让系统让为它是英文,我直接把它改成了Terminal,然后再启动vim然后退出就没有问题了。

总算解决了我对于vim的怨念,一直以为是vim的问题,没想到竟然是slackware打包的问题。

分类: Linux 标签: ,