-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 267 KB
/
index.json
1
[{"categories":["thinker"],"content":"堂吉诃德夙愿就是成为一个骑士,坚守骑士精神,惩恶扬善,行侠仗义。因为世道的黑暗却依旧坚守骑士精神闹了很多笑话。那么真正可笑的是堂吉诃德,还是堕落的世界呢?他成了一个庸俗世界骑士理想的殉道者。 我如果因为良善扶起躺在地上的老人,亲朋好友觉得我脑子进水了。那么错的是我呢?还是这个世界呢? 堂吉诃德用荒诞反讽现实,尤其当下社会这样的精神如此稀少珍贵了。 ","date":"2024-10-04","objectID":"/posts/2024/20241004/:0:0","tags":["SS"],"title":"堂吉诃德","uri":"/posts/2024/20241004/"},{"categories":["thinker"],"content":" 故事背景是资产阶级反封建的新文化运动1800年左右,拿破仑流放,路易十八上位。 ","date":"2023-09-11","objectID":"/posts/read/2023911/:0:0","tags":["SS"],"title":"基督山伯爵","uri":"/posts/read/2023911/"},{"categories":["thinker"],"content":"复仇起因 爱德蒙·邓蒂斯一个19岁年轻有为的青年,最可贵的是又有一颗纯洁善良的心。马上就要当摩莱尔老板公司的船长了,还要迎娶挚爱美丽善良的美蒂西斯。纯洁的他看不见暴风雨来临,一个嫉妒他要当船长的邓格拉斯,一个憎恨他的情敌弗楠,另一个则是见不得人好的邻居卡德罗斯。开始了他们的诬陷阴谋,诬陷邓蒂斯是拿破仑党派,这三人分别是幕后黑手始作俑者、执行寄信的人、良心可能受到一丝谴责目睹一切的旁观者。最后再加上一个伪君子维尔福检察长,他的父亲受到了这次阴谋的牵连,于是即便在善良的老板摩莱尔走访求助中,他表现得铁面无私。而邓蒂斯在最幸福的时刻,在要和挚爱结婚的时刻,关入了大牢,纯真的他都不知道自己为何会落到这样的境地。 除了上面的四个罪人,就是这三个唯一爱他的人:邓蒂斯的老父亲在痛苦贫穷中饥饿而死,美蒂西斯为这老父亲忙里忙外,天天苦等邓蒂斯的归来,但一个无依无靠的女子等了那么多年,还能干什么呢?在不知情的情况下应允了嫡堂哥哥弗楠的求爱。还有那摩莱尔为了救自己的小船长使劲了办法。 邓蒂斯在黑牢中从希望到绝望再到想要自杀离开人世,绝望更容易看到希望,从黑牢中遇到了挖洞想逃跑的法利亚长老,因为计算失误,挖到了他的邻居邓蒂斯房间。他给了他希望,和他聊天,法利亚是个有大智慧,大经历的人,从聊天中帮他分析出了谋害他的四个罪人,完全正确的四个罪人。本想一起逃离,但法利亚因为疾病死在狱中,托付给邓蒂斯的是一笔基督山的隐蔽巨大财富,和自己的全部知识与智慧,邓蒂斯又陷入了失去挚友的痛苦中,没过多久复仇和希望超越了痛苦,邓蒂斯趁着狱卒运送包裹长老的尸袋间隙,掉包成功,离开了关了他14年的监狱,一个19岁的邓蒂斯死了,而现在活着的是富可敌国,足智多谋等待复仇33岁的基督山伯爵。已经没人能认出他的模样,得知了唯一亲人父亲是因饥饿穷苦而死,确认了长老的分析,报答了摩莱尔先生的恩惠,开始了复仇计划。 上面大概是复仇开始前的起因,复仇过程精彩绝伦,简单概括难以形容,不写也罢。 ","date":"2023-09-11","objectID":"/posts/read/2023911/:1:0","tags":["SS"],"title":"基督山伯爵","uri":"/posts/read/2023911/"},{"categories":["thinker"],"content":"复仇过程思想的挣扎 他在监狱中经历了最绝望的时刻,已经放弃了上帝。却没想到上帝给了他第二次生命,法利亚长老就是他的上帝,他不再迷茫,明白这第二次生命的意义,那就是复仇。没有任何事情能让他再有所动容,只有摩莱尔的孩子,每次见到恩人的孩子他面无表情的脸上才能看到温暖。现在他的仇人都已经身处高位。 对于邻居基督山伯爵可怜了卡德罗斯这个邻居,虽然没良心,但却算不上对邓蒂斯有大罪的人,但最后却因自己无限增长的贪婪毁掉了这份怜悯。 对于弗楠基督山伯爵还保留着邓蒂斯对美蒂西斯的爱,在抹掉弗楠名誉的计划中,弗楠儿子阿尔培决定为父亲的荣誉和这个做了很久朋友的伯爵进行一场生死决斗,美蒂西斯祈求基督山伯爵的宽容,他带着邓蒂斯的记忆竟想输掉决斗用死放弃整个复仇计划。在我想开始厌恶这女人的时候,作者又给了一阵安定剂,美蒂西斯将一切告诉了阿尔培,她依旧那样的纯洁善良,而他的儿子继承了他母亲的善良,主动放弃了这次决斗,和伯爵握手言和,带着母亲离开了家,离开了曾引以为傲的父亲。弗楠自杀了,失去了不属于他的荣誉,失去了不属于他的妻儿。而伯爵重新拾起了复仇。 对于维尔福伯爵利用他的家庭矛盾,让维尔福的第二任妻子学会了制毒,她为了让儿子继承财产,一次次毒害了四人,再加上维尔福的私生子曝光,让他承受不住这一次次打击成了疯子。而伯爵又一次动摇了,因为复仇的范围太大了,没有控制住伤害了太多无辜人。第四个人本来会死,但她却是摩莱尔儿子马西米兰的爱人,伯爵保护了这位恩人之子的爱人凡尔蒂,也算是一些慰藉。好人的好报让伯爵复仇的心恢复了一些。 最后对于邓格拉斯,他已成为银行家,伯爵令其破产,最后本应饿死于强盗洞里,但伯爵只让他留了些钱,苟且活着,有时候活着比死了还难受。也许是伯爵在前面一次次复仇中已经得到了宽慰,曾经的邓蒂斯占领了上风。 伯爵在复仇过程中有过挣扎动摇,纯洁的邓蒂斯可能仍在占领良善高地,一次次的复仇让伯爵也宽容了一些。完成一切使命的他,准备谢幕的他,失去了初恋不再相信爱情的他,最后因为海蒂一次次内心表露重新找回了爱情。恩人之子的幸福让他恢复了邓蒂斯本人。基督山伯爵走了,邓蒂斯又回来了。 ","date":"2023-09-11","objectID":"/posts/read/2023911/:2:0","tags":["SS"],"title":"基督山伯爵","uri":"/posts/read/2023911/"},{"categories":["thinker"],"content":"最后 世上没有快乐和痛苦,只有一种状况和另一种状况的比较,这句话是基督山伯爵说的,他用这句话来激励马西米兰,让他忍受痛苦后找回了挚爱。好人的好报,恶人的恶报,伯爵用这宏大的一生相信:人类的一切智慧是包含在这四个字里面的:“等待”和“希望”。 ","date":"2023-09-11","objectID":"/posts/read/2023911/:3:0","tags":["SS"],"title":"基督山伯爵","uri":"/posts/read/2023911/"},{"categories":["thinker"],"content":"生产资料私有制的出现个人主义随之而来,并随着私有制的发展而发展,当下是个人主义横行的时代。但它不能与利己主义画等号,个人主义有其积极性,然而一个人没有精神基础,最终只会沦为物欲的个人成为利己主义者,为了自己的物质欲望需要人与人之间的帮助,成了熟人社会(《乡土中国》),这就是当下的中国。 精神的空虚导致了富贵不能乐业,贫贱难耐凄凉的情形,人生失去了信念。人生的目标是伟大的,但现在是把功利的目标偷换成了人生目标。因此我们需要寻求安心立命的方法。 如果说西方人以对上帝信仰来立心,那么中国人用什么呢?我们用的就是我们自己的本心,中国哲学是人生的哲学,阳明心学不是纯粹的理论学问,研究它不是为了提高对现实世界的认知,而是来解决安心立命的大问题。 东周末年天下大乱,当时的人们面临巨大痛苦,王室架空,诸侯混战,这种局面让我们的民族面临两种可能性,一种是民族解体,另一种是出现伟大的论道先哲们。我们的民族是幸运的,出现了一批哲人(百家争鸣)。到了汉武帝时期,董仲舒和汉武帝联起手来罢黜百家,独尊儒术,从此以儒家为尊,中国的道统成立了,也就是以儒家为根本的文化精神传统,迎来了两汉经学。直至东汉末年礼坏乐崩,再次大乱,走投无路之际道家再被拾起进入了魏晋玄学。 但这并不能阻止中国文化生命的衰落,民族仍没有精神家园,只有少数文人得到解脱,拥有风流潇洒的人生态度。在濒死之际佛学于两汉之间进入中国,经文人们的努力到了唐朝终于让佛学中国化,从此成为中国思想的一部分。中国问题还没解决,但有了希望,受佛陀思想启发,迎来了属于中国的禅宗(六祖慧能),禅宗的法门是修心,禅宗就是佛教的心学。于是中国人开始反本开新,从儒家道统开出新的道统迎来了宋明心学。宋明心学仍然是儒家,但是是儒家的新道统,其主题是以儒家为本的三家合流,从而树立中国人的独立人格。但心学没有大行天下,满清入关,取消了宰相制度中断了知识分子天下关怀的实践路径。 破山中贼易,破心中贼难,我们不可能以宗教信仰立心,因为我们还是中国人。但仍要解决在个人主义社会时代是否有安心立命的地方,而不仅仅是物欲的个人。中国哲学一直以出世入世为核心主题,因为中国哲学是人生哲学。为此需要从哲学中,历史中寻求答案。 ","date":"2023-06-03","objectID":"/posts/2023/%E5%AE%89%E5%BF%83%E7%AB%8B%E5%91%BD/:0:0","tags":["SS"],"title":"安心立命","uri":"/posts/2023/%E5%AE%89%E5%BF%83%E7%AB%8B%E5%91%BD/"},{"categories":["thinker"],"content":"刻意练习是什么 和重复练习不同,刻意练习是”不重复“的练习。在我看来重复练习和刻意练习不是互斥的,而是相辅相成的。刻意练习是为了进步,重复练习是为了巩固。 以练习投篮为例,在各种参数相同的情况下(姿势、身体情况等)投了10次进不去,那么再投100次也进不去,因为你在重复错误。我们需要的是不断的微调,第一次进不去,调整手腕,第二次进不去再调整力度,第三次,第四次… 直到进球,掌握了一个正确的投球感觉。这是迷你版的刻意练习;掌握了投球感觉,进行重复练习巩固达到更稳定的水准。 因此刻意练习是一种有目的,有反馈的练习方式。 ","date":"2023-02-21","objectID":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/:1:0","tags":["SS"],"title":"刻意练习","uri":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/"},{"categories":["thinker"],"content":"刻意练习的过程 准备:设定好学习的目标、规划每一个阶段、最后学以致用才能有效反馈 反馈:对正确的与错误的做分析,减少后续的问题 集中精力:有效的集中1分钟,也比散漫的10分钟有用 保持动力:给自己设定阶段奖励、自勉等可以增强信息和动力 走出舒适区:和臭棋篓子下棋越下越臭,赢了也不会获得进步。因此要逐步和更强的人下。 瓶颈的攻破:遇到瓶颈,可以换个思路方法,调整节奏等,每个人的学习方法是不同的 ","date":"2023-02-21","objectID":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/:2:0","tags":["SS"],"title":"刻意练习","uri":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/"},{"categories":["thinker"],"content":"练习时的重要点 ","date":"2023-02-21","objectID":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/:3:0","tags":["SS"],"title":"刻意练习","uri":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/"},{"categories":["thinker"],"content":"走出舒适区 单个的细胞和组织在尽最大的努力使一切保持相同,人类的身体有一种偏爱稳定性的倾向。 人慵懒的躺在床上是符合生理的,当运动的时候会打破这种稳定,所以会带来痛苦,但当进行了长期的运动,使得身体超出了体内平衡机制能够补偿的界限时就打破了这种痛苦。届时来到了新的区域。 如果你能做100个俯卧撑,那么每天做10个很轻松,但做120会感受到痛苦。在舒适区内(永远做10个俯卧撑)不会有任何成长,只有循序渐进坚持脱离舒适区才能带来进步,当然也要注意不能离舒适区过远(比如每天200个就不切实际)。 ","date":"2023-02-21","objectID":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/:3:1","tags":["SS"],"title":"刻意练习","uri":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/"},{"categories":["thinker"],"content":"创建心里表征 一种与我们大脑正在思考的某个物体、某个观点、某些信息或者其他任何事物相对应的心理结构,或具体或抽象 练习过程中创建属于自己的心里表征,可以更高效的进行后续的学习。其实就是尽可能的了解事物的本质。比如很多词语可以来说明什么是心里表征:举一反三、既见树木,又见森林等 理解一件事的背后底层规律,上层无数的变化不再重要,做到以不变应万变。 以数学举例:记住难题的解题方法和规律,可以举一反三更多的难题;而如果只记住每道题的答案,那就无法面对新的问题,其实就是总结知识点的底层核心,其上的建筑都是水到渠成。 再比如关于记忆:一堆无关联的单词难以记住,但如果是一句有意义的话就可以瞬间记住。知识也一样,多个知识由点及面构建起一个网状图,就能有1+1\u003e2的效果。 当对某个领域达到了精通的地步,就能达到\"预测未来“的效果,只看到一个点,就能分析出整个关系网。守门员根据对方踢球瞬间做出合理决策(扑球方向、方式);军事领域可以根据当前战争,分析国家之间的政治情况。 ","date":"2023-02-21","objectID":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/:3:2","tags":["SS"],"title":"刻意练习","uri":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/"},{"categories":["thinker"],"content":"理论与实践 《实践论》里说:“实践、认识、再实践、再认识,这种形式,循环往复以致无穷,而实践和认识之每一循环的内容,都比较地进行到了高一级的程度。” 刻意练习也是一样的,要对练习的东西进行实践与反馈。才能确保刻意练习的正确性,优化调整之前的错误点,这也是学精一样东西的最优解。 ","date":"2023-02-21","objectID":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/:4:0","tags":["SS"],"title":"刻意练习","uri":"/posts/read/%E5%88%BB%E6%84%8F%E7%BB%83%E4%B9%A0/"},{"categories":["coder"],"content":"目录 ├─cmd │ └─main.go # 程序主入口 └─pkg ├─config # 一些配置相关 ├─engine # chrome相关程序 ├─filter # 去重相关 ├─js # 一些注入的js ├─logger # 日志 ├─model # url和请求相关的库 └─tools # 一些通用类库 └─requests ","date":"2023-01-04","objectID":"/posts/2023/20230104/:1:0","tags":["go"],"title":"crawlergo源码","uri":"/posts/2023/20230104/"},{"categories":["coder"],"content":"爬虫生命周期 上述标记了每个大步骤标记了a、b、c… 便于后面分析功能时说明在哪个步骤实现的 初始化全局参数(所有的url请求都依赖这些参数): 设置关键字过滤IgnoreKeywords; 设置表单参数,若没有则使用默认值: default= config.go-\u003eDefaultInputText 初始化爬虫任务 初始化参数(一些chrome的配置): 最大并发数、最大深度、url请求超时时间等 初始化浏览器 InitBrowser():忽略证书错误、不请求图片url、不展示gui等 初始化过滤器 smartFilter.Init():所有url在爬取前都要经过smartFilter.DoFilter(*model.Request)进行过滤 创建协成池(根据参数max-tab-count):同时开启最多max-tab-count个tab页,也就是并发爬取数量限制 运行爬虫任务 若配置了--robots-path true(默认false)则爬取robot.txt中的url 若配置了--fuzz-path-dict path 则开启目录爆破,爬取文件中所有关键字组成的url;也可以使用--fuzz-path true 开启后会爬取path_expansion.go-\u003epathStr变量中的内容 开始爬取: 每个url请求都会调用addTask2Pool(req *model.Request) 每个url的爬取流程 url会调用addTask2Pool,并且每个url都会是一个独立的goroutine 每个url执行会创建一个tab页来运行: 每个tab页都会监听多种事件,便于在不同时刻处理不同的请求逻辑: 可以参考 https://chromedevtools.github.io/devtools-protocol/tot/Network/ 重要的事件有: 请求前的拦截、解析js中的url、重定向、40x请求、表单填充等 开始爬取(tab *Tab) Start(): 到这一步才真正的开始爬取 每个tab请求都遵循InitBrowser()时的参数,并且每个tab页也可以在chromedp.Run中添加参数 爬取ing:爬取过程中事件会经过刚才注册过的 爬取后:收集所有url结果添加到func (tab *Tab) AddResultUrl 爬取的url结果会再次过滤后添加到addTask2Pool,进行下一阶段爬取 从1开始重复流程,直至爬取不到任何结果或符合终止条件(最大深度、最大时间等) 输出结果,终止浏览器 ","date":"2023-01-04","objectID":"/posts/2023/20230104/:2:0","tags":["go"],"title":"crawlergo源码","uri":"/posts/2023/20230104/"},{"categories":["coder"],"content":"过滤去重 使用方式是: spiderman --filter-mode=smart http://127.0.0.1 // taskconfig.go: type TaskConfig struct { // ... FilterMode string // simple、smart、strict // ... } // config.go: const ( // 默认-简单过滤: 其他过滤条件都包含简单过滤 // 对req做md5操作,比较md5值,并且过滤掉一些无用url:config.go:StaticSuffix 参数中的比如 png、jpg等 SimpleFilterMode = \"simple\" // 智能过滤 SmartFilterMode = \"smart\" // 严格智能过滤: 在智能过滤基础上增加了一点逻辑,对大小写、下划线等更敏感 StrictFilterMode = \"strict\" ) 此功能流程: 在步骤c进行初始化设置 在步骤c创建初始url时 进行一次过滤 在步骤g产生新的url后 进行一次过滤 在步骤h将最终产出的url req 做最简单的md5 过滤 因此如果需要增加新的过滤方式,或调整过滤流程,需要注意上述流程,另外可以以接口的形式来解耦filter模块,这是我提的pr:https://github.com/Qianlitp/crawlergo/pull/136 ","date":"2023-01-04","objectID":"/posts/2023/20230104/:3:0","tags":["go"],"title":"crawlergo源码","uri":"/posts/2023/20230104/"},{"categories":["coder"],"content":"智能过滤 根据正则将匹配结果进行标记 比如: www.baidu.com/page/1 --\u003e www.baidu.com/page/{int} 因此www.baidu.com/page/1 和www.baidu.com/page/2 最终处理后都是www.baidu.com/page/{int} 表示相同url ","date":"2023-01-04","objectID":"/posts/2023/20230104/:3:1","tags":["go"],"title":"crawlergo源码","uri":"/posts/2023/20230104/"},{"categories":["coder"],"content":"关于进程超时 目前的超时只能根据并发数量-m、tab-run-timeout、--max-crawled-count 大致的进行时间上的控制 参考。我这里增加了 最大的执行时间控制。但无法达到精准的控制,只能在秒级别内,比如设置20秒,会在20-23秒左右停止 根据上图,可以知道d和f两个步骤,一个是加入新的url,一个是tab创建设置tab的超时时间。因此可以在这两个地方进行限制。 增加参数,并记录Crawlergo启动时间 type TaskConfig struct{ //.. MaxRunTime int64 Start time.Time } func (t *CrawlerTask) Run() { t.Config.Start = time.Now() //... } 产生新url 阶段 - task_main.go:addTask2Pool(): 若检测超时则无法再添加新的url 创建新的tab func (t *CrawlerTask) addTask2Pool(req *model.Request) { t.taskCountLock.Lock() if t.crawledCount \u003e= t.Config.MaxCrawlCount { t.taskCountLock.Unlock() return } else { t.crawledCount += 1 } if t.Start.Add(time.Second * time.Duration(t.Config.MaxRunTime)).Before(time.Now()) { t.taskCountLock.Unlock() return } t.taskCountLock.Unlock() //..... } 创建tab准备爬取 阶段 - task_main.go:Task():进程剩余时间和tab最大超时时间 取最小,作为tab的超时时间,如果没时间了,则取消创建 func (t *tabTask) Task() { // 设置tab超时时间,若设置了程序最大运行时间, tab超时时间和程序剩余时间取小 timeremaining := t.crawlerTask.Start.Add(time.Duration(t.crawlerTask.Config.MaxRunTime) * time.Second).Sub(time.Now()) tabTime := t.crawlerTask.Config.TabRunTimeout if t.crawlerTask.Config.TabRunTimeout \u003e timeremaining { tabTime = timeremaining } if tabTime \u003c= 0 { return } //..... } ","date":"2023-01-04","objectID":"/posts/2023/20230104/:4:0","tags":["go"],"title":"crawlergo源码","uri":"/posts/2023/20230104/"},{"categories":["coder"],"content":"output // task_main.go type Result struct { // 存储的url,是请求完成并且是过滤后的;也就是最终产出的全部内容 ReqList []*model.Request // 存储的是搜集到的所有url(没有过滤,也不一定会进行请求的url) // 它的存储逻辑是直接将每个tab产出的req存入,如果多个tab并发执行,是会重复的,因此在任务完成后需要去重操作 // 主要作用是 展示爬取到的全部域名 AllReqList []*model.Request //.. } 举例: 假设捕获到了两个url要进行爬取,并且分别会产出3个url,如下关系: 1. www.baidu.com/a - www.baidu.com/c - www.baidu.com/d - www.baidu.com/e 2. www.baidu.com/b - www.baidu.com/c - www.baidu.com/d - www.baidu.com/f 根据参数可以设置最大并发爬取--max-tab-count,比如是 2,整个爬取流程: 假设tab1优先完成,产出了b/c/d三个url,那么tab2经过过滤最终只上报f一个url。 ","date":"2023-01-04","objectID":"/posts/2023/20230104/:5:0","tags":["go"],"title":"crawlergo源码","uri":"/posts/2023/20230104/"},{"categories":["thinker"],"content":"任何决策之前都需要调查,没有调查就没有发言权。做生意就要研究调查一个市场的情况,分析接下来要做什么,怎么做。 很明显,我们当下目标无论怎样都不能脱离天津,因此首先要对天津市场做分析才能做出合理规划。然而一个行业会受到各种变量的影响,大到国家的政策、龙头企业的影响力(龙头企业可以对行业标准的制定有影响力),小到区域的影响力、同行的竞争;还有气候、疫情等自然因素。因此在讨论天津形式之前不得不讨论当下龙头企业的市场行情,在讨论龙头市场行情之前不得不讨论整个烘焙市场情况。 无论是长久未来的发展还是从当前市场竞争或可见未来要遇到的困难,都急迫要合理规划好一个战略目标,指导发展路线。下面我将从宏观到微观的进行分析行业市场情况,再做出个人决策看法。 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:0:0","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"中国烘焙行业的当下格局 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:1:0","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"市场规模 行业规模决定了当下的市场上限,100份的蛋糕如果都已经被瓜分,这个行业也就没有介入的机会。 目前来看面包市场规模近400亿,景气度向上、集中度不高。这两个词什么意思? 景气度向上说明了这个行业在可见的未来是繁荣发展的,相反的例子就是油车在未来发展是不景气的;集中度不高表示这个行业的垄断能力弱,虽然桃李目前是国内龙头,但只是在整体市场方面,细化到区域甚至地方都有中小企业竞争。 与全球市场相比,中国烘焙行业整体看集中度不高。相较于日本前五大公司集中度43%,中国烘焙行业前五大公司仅占市场总量的10.6%,市场尚有连锁化和整合的空间 另外每个行业都有生命周期,比如缝纫机产业就已经消亡,但食品行业一般不会因科技、社会发展等因素而轻易被取代(不过食品的制作方式会有变化)。 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:1:1","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"为什么竞争是激烈的 为什么当前的行业规模是这样的格局?小作坊为什么能够存在?核心原因是烘焙行业的自身性质导致。行业的特殊性质,导致了集中度具有天然的不同 从行业的规模成长来看,面包市场的成长是较为缓慢的,且具有一定成本,在一个区域内想要一家独大,需要口碑、价格、店面选择等多种考虑。即使站住了脚,又因面包这种食品的特点同质化严重,你做的好吃其他厂家会竞相模仿,来侵蚀市场。 从品牌效益来看,你买了个包或者一双鞋别人会问你是什么牌子的。而你吃的面包或者面条,没人会问你,甚至你自己也不在乎。因此烘焙行业的品牌效益在初期阶段是次要的,尤其是食品行业口碑比品牌重要的多,桃李每年的广告支出只占营收入的0.8%。品牌效益低也是其他同行有切入点的原因。而真正的广告其实是陈列费,并不是靠网络、视频宣传等。 从产品特点来看,面包的保质期短,无法全国性质的铺货销售,导致了企业数量多,规模都不大,市场集中度就较低。龙头桃李因为资金雄厚可以在个地方建立工厂来弥补这方面的问题,其他中小企业只能在本地销售短保面包,或者以长保面包为主,这也导致了每个地方都有地头蛇。另外运输问题,难以实现跨区域经营,基本上以本区域市场为主。还有个较为次要但未来可能要关注的点,就是不同地方人群口味也有不同。 从投入来讲,这个行业的从业者都可以自己发展,扩张规模难,但初期成本低,只要手上有多家超市的关系,就能让自己多一些渠道来售卖自己的货物。这也导致了此行业的竞争是如此的激烈,门槛低。 因此我国的烘焙行业未来是乐观的(市场大),眼下是惨烈的(行业特性导致门槛低竞争大) ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:1:2","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"参考 https://www.askci.com/news/chanye/20220701/1711421910498.shtml https://www.chinabaogao.com/detail/608800.html ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:1:3","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"龙头桃李企业的市场形式 了解了整个行业的大致情况,接下来需要了解本行业的龙头是如何发展的,我们可以学习他的发展路线。但因为时代背景不同,当前的规模不同,因此我们不能照搬全收,要深思熟虑的学习总结出自己的路线。 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:2:0","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"上下游供应 为减少“中间商赚差价”,提高企业经营利润,并保持产品的新鲜度,吴志刚创立了桃李面包 “中央工厂+批发”的模式进行生产和销售,大规模生产减低生产成本和配送成本,让一块面包的毛利超35%。 桃李主要布局全国工厂,物流分为一级物流和二级物流,一级物流直接从工厂送往KA客户;二级物流从工厂送往分销站,然后在配送周边中小超市、便利店。而物流公司主要采取第三方物流模式,公司可控,但不归属于自己 通过规模化生产大幅度降低对上游食品采购价格(同时由于烘焙类产品原料面粉、鸡蛋都是国家重点民生保障产品,价格浮动不会太大)议价能力,而下游又依靠强大的渠道能力实现大规模分销 第一种大中型城市采用自己直营模式,直接供货到终端。这也是桃李面包的主要销售来源模式。主要是直接与KA以及直营中小客户签订供货协议,约定商品交付、货款的支付方式、商品折扣、促销政策、退换货模式以后进行销售。一般再接到订单后第二天组织进行生产。这种模式非常容易,相当于桃李面包直接把面包所有权批发给超市业态,基本相当于对终端直供模式。这个过程中,一般大概会有5%-8%的退换货 第二种模式是经销商模式。由于有些中小非桃李面包的中心城市采用经销商模式,也就是依靠经销商去辅助面向终端更小的店铺。这种模式主要是适应小门店,小商店客户。从逻辑上而言,如果面对小商店也采用直销模式,就会面对很多问题 聚焦于少而精,不追求品种多,追求单品生产销售规模,大规模的生产可降低公司的单位生产成本,直接销售给商超等终端,可大幅降低公司的销售费用,所以继续提升产能降低价格就是应有的选择了。 https://xueqiu.com/8919416229/87614369 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:2:1","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"天津市场 目前天津市场竞争激烈,无论线上线下,都有同类竞品,且大部分对手影响力和销售量都比我们强。我们要在夹缝中生存甚至超越是困难的。我们甚至连同厂竞品都无法消灭。大商超被桃李、宾堡占据。中小超市五花八门竞争激烈。大商超难以进入,还有部分老店和天津大供货商合作(三家利、星耀市场)。我们的大客户主要是连锁超市,其次为中小超市。 天津市场的特点是中老年居多,吃的好回头率高。 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:3:0","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"我们当下的策略与目标 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:4:0","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"线下战略 TODO ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:4:1","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"线上战略 互联网时代,线上销售是重要的销量来源,是推广品牌的重要据点。因此在合适的时机做线上售卖是应该的也是必然的。 从开店角度线下成本高但成效快,线上成本低成效慢。从稳定后的销售角度,线下稳定但收益少,线上推广成本高昂,竞争激烈但收益大。 下面分析线上销售的时机,和线上战略方案。 天时:只要产品不错,即便竞品略强只要占据天时(10年的老店永远比刚开的新店强)也难以撼动。比如10年的游戏代练老店只要价格合理,新店无法与之对抗。 地利:一个品牌可能刚成立线上渠道,但也能有很好的销量,那就是线下已经有足够的影响力,足以影响客户群体,线上购买。比如某化妆品名牌从未有线上渠道,突然开启后加上一波广告,就可以摧枯拉朽,势不可挡。 人和:线上产品五花八门,总会有漏掉的客户群体,抓住这个空挡也能有不错成效。比如某类食品都是美味新鲜,这时以零糖、低脂作为突破口,就有大收获。 我们的产品不具备上述所有情况,各种网络渠道都不算新奇产品都有竞品。线下知名度几乎为零(只局限于天津部分区域)。同质化严重难以有突破口,即使有也会立刻被冲击。这种情况还要做吗?我想还是可以做,但要有条件的做,成本低可以做,投入大就搁浅;可以不挣钱,但不能赔钱。 方案规划 既然能做,那要怎么做呢?三步走 1. 借鸡下蛋 开线上店铺,无论是投入还是推广都需要极大成本,且不一定有成效。所以继续以供货商的身份入驻商家,与商家合作,互惠互利。销量高的大店铺,不可能与我们合作,太小的店铺意义不大,可以优先找中小型商家入手。商家选择待议(产品不冲突,地理位置,商家不是独立品牌等),做业务合作沟通。 前几个月以养为主,厂家和供货商减少利润甚至不要利润。当销量提高,再做互惠互利。每个平台合作1-2个商家。 另外要注意超市平台的线上服务,美团超市、饿了么超市、叮咚买菜等,我们的合作超市如果有线上平台,可以在供货的时候和他们沟通,如果线上没有我们的产品,可以想办法挂到他们的线上店铺。 2. 偷梁换柱 当线下线上销售量巨大,直接让厂家禁止与其他供货商合作,灭掉同厂竞品。 3. 一锤定音 当品牌知名度提高,资金充足,再以商家的身份入驻各平台,但不售卖与之合作的产品,和谐共存。最后独立开厂,提高自身利益。 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:4:2","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"其他 给超市、合作商查看了解:企业门户小程序(产品介绍+公司简介+合作联系+留言) 销售系统:管理产品、出单、数据分析等 ","date":"2023-01-02","objectID":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/:5:0","tags":["SS"],"title":"烘焙市场分析","uri":"/posts/2023/%E7%83%98%E7%84%99%E5%B8%82%E5%9C%BA%E5%88%86%E6%9E%90/"},{"categories":["thinker"],"content":"单个公司–\u003e整个行业–\u003e目前形式–\u003e行业周期 实地调查:主要是发放调查问卷,或者询问熟悉行业市场的人,或者做一些实际的、小规模的活动进行调查预测 文案调查:主要是网上资料搜索和图书馆找相关书籍,多为二手资料。 案例调查:主要研究行业龙头公司的市场和产品 ","date":"2022-11-19","objectID":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/:0:0","tags":["SS"],"title":"调查方法","uri":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/"},{"categories":["thinker"],"content":"调查方法 如何有效的分析一个行业 盈利模式是什么 行业龙头老大是谁 行业的上游供应链、下游消费者是谁 行业的产能如何?其在整体经济结构中的地位 行业的法律监管如何 内部的发展,外在的影响, 收入和利润变化搞清楚了,就能做预测模型 ","date":"2022-11-19","objectID":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/:1:0","tags":["SS"],"title":"调查方法","uri":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/"},{"categories":["thinker"],"content":"微观层面 销售、利润率、是否随着市场变化而变化: 比如月初进了个新品,看看整体销售量是否有变化;面包属于季度性强的行业,所以要同比来看,较去年增加了多少; 超市里多了个其他公司的品类,是否受影响 某个超市的增加减少,对面包的影响; 人力成本的涨幅,对公司造成的影响: 司机与店之间的关系 最小的运作单位的集合,造成了宏观层面的发展 ","date":"2022-11-19","objectID":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/:1:1","tags":["SS"],"title":"调查方法","uri":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/"},{"categories":["thinker"],"content":"集群层面 深度挖掘了细节,需要进行抽象总结;寻找规律。 多个运作单位之间是有共同点和不同点的,可以根据情况进行分类;比如最简单的可以分为大商超和小门面。不同的类型他们的目标客户和销售方式可能有所不同。因此在复制模式的时候,要根据类型选择。 还可以根据销售量来分类,比如a类型的超市都拥有每月10w的销售量,b类拥有5w销售量 是否受到其他产品的分流效应;此效应影响哪些商超,要把握大方向,忽略小细节 比如建厂后的人口、区域因素是 单个经销商无法看到的和提炼的,只有站在更高才能看到不一样的东西。每个层面有不同的角度 ","date":"2022-11-19","objectID":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/:1:2","tags":["SS"],"title":"调查方法","uri":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/"},{"categories":["thinker"],"content":"市场和经济层 整体性思考的多了,就会发现已经站在了不在属于单个行业的高度了。而是整个市场和经济层面的高度。 这时应该更关注品牌之间的竞争策略,互动情况,宏观经济结构影响,政治敏锐。 比如在市场层面,经过长期观察,能看到肯德基和麦当劳的竞争关系,通过关系和变化,能否看出消费者喜欢吃什么样的快餐、 在外卖行业的引入后,肯德基受到了怎样的影响。 总而言之就是市场变量如何影响行业,影响范围,影响时间等;从最初的面包品类变量变成了大的市场宏观变量;不变的是影响关系,变化的是 微观-\u003e宏观的变量 发展空间、所处阶段、竞争格局 ","date":"2022-11-19","objectID":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/:1:3","tags":["SS"],"title":"调查方法","uri":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/"},{"categories":["thinker"],"content":"参考来源 公众号学术论坛:知网、小木虫 数据资讯网站:199it、搜数网、卡思数据 世界银行公开数据:ceic、knoema全球数据 研究报告:证监会官网、行行查、36kr http://www.stats.gov.cn/ http://www.csrc.gov.cn/ 行行查网站 https://www.fxbaogao.com/ https://www.36kr.com/ https://www.eastmoney.com/ 亿欧 钛媒体 QYResearch http://www.soshoo.com/index.do ","date":"2022-11-19","objectID":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/:1:4","tags":["SS"],"title":"调查方法","uri":"/posts/2023/%E8%B0%83%E6%9F%A5%E6%96%B9%E6%B3%95/"},{"categories":["thinker"],"content":"人生的意义是什么? 人生的意义是找到一种比死更重要的东西 人活着总要走向死亡,面对死亡更容易获得超越死亡的东西,苦难常常是人与死亡之间的桥梁,因此苦难是最容易帮助人获得意义的。 苦难不值得称颂,只是苦难不是自找的,是外来的,无法躲避的。我们没有选择苦难的自由,但我们却有面对苦难时选择面对方式的自由。若能克服苦难直面它,说明找到了比自身更具价值的东西,肖申克救赎中安迪被单独关禁闭1个月,出来后却不觉得难受,因为他借助音乐找到了铁笼无法束缚自己的东西,心灵上的自由,面对苦难的态度上的自由。 孔子说 杀身成仁, 孟子说 舍生取义。他们早就明白有一种东西是比生死更重要的,朝闻道夕死可矣,只要找到了这样的意义,人生短暂又何妨呢? 我们也许无法成为大哲学家,构建出属于自己的思想系统,来诠释人生的意义。但那无法选择的苦难来临时直面它的勇气,可以帮我们找寻人生的意义,又因为是外来的无法选择的,因此不同的人,在不同的阶段,不同的思想就会有不同的意义。唯一相同的就是这些意义是超越生死的。 ","date":"2022-04-08","objectID":"/posts/read/20220408/:0:0","tags":["SS"],"title":"人生的意义","uri":"/posts/read/20220408/"},{"categories":["thinker"],"content":"事业 职业发展的驱动力一定是来自个体本身。记住:工作是属于公司的,而职业生涯却是属于你自己的。 ","date":"2022-01-02","objectID":"/posts/read/20220102/:1:0","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["thinker"],"content":"职业生涯 首先要化被动为主动转换心态:我入职一家公司是因为符合对方的要求,转变为公司是客户,它选择我,是因为我有它需要的专业技能,把自己当成一个企业,我这个公司所能提供的服务就是创建软件。既如此推销自己就是推销产品,吸引更多的客户(找到更好的工作)。 集中精力成为一位专家,专门为某一特定类型的客户提供专业的整体服务。心态转换后,职业就成了商业活动,开始思考目标(职业的大规划) ","date":"2022-01-02","objectID":"/posts/read/20220102/:1:1","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["thinker"],"content":"晋升 要承担更多责任,有很多可以承担的事情:辅助新人、文档规范、负责项目、优化项目(功能、流程)、曝光自己(分享、帮助他人、发表意见等)、持续学习与输出迎接机会。 我之前在一家公司任职,没事的时候看技术leader负责的系统实现进行学习。有一天他辞职了,这个系统没人负责了,而我因为之前了解过,帮忙修改了bug,之后就开始维护此系统。半年后涨了薪水,这是我第一次体会到机会是留给有准备的人。 ","date":"2022-01-02","objectID":"/posts/read/20220102/:1:2","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["thinker"],"content":"成为专业人士 既然是专业人士,那么只是完成是不够的,要正确的完成,做该做的,合理的时间管理,应对不确定因素要未雨绸缪。 ","date":"2022-01-02","objectID":"/posts/read/20220102/:1:3","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["thinker"],"content":"维持兴趣 学习一个东西。一开始有激情,但总会半途而废。因为刚学会皮毛尝到了点甜头,这是有动力的。不过需要再深入学习很久才能尝到下一个甜头,到最后,甜头越来越难得到。虽然最终的结果是甜蜜的,但过程是枯燥的。因此为了保持开始的激情,要多人为制造一些甜头,比如学钢琴,每个阶段以一个喜欢的曲子为目的,学下去。比每天无目的弹练习曲有意思的多。 另外要对自己负责,有责任感。制定计划在目标达到之前,把这个当成和刷牙一样的习惯,列入计划 ","date":"2022-01-02","objectID":"/posts/read/20220102/:2:0","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["thinker"],"content":"如何学习 对于程序员,在学习一个技术的过程中,知行合一尤为重要。只看不做容易忘记。《软技能》里有十个步骤,我结合个人学习方式,整合了一下。 孔子讲学而时习之,不亦说乎,后人翻译都是学习了再温习很快乐。但应该很少有人觉得温习是快乐的吧,我从一个儒学大师傅佩荣教授的书里看到了新的解释,觉得非常对,他认为: 时论语中只有时机和节气的意思,没有时常的含义,另外温习怎么会快乐?因此应该是学一样东西,在恰当的时候加以练习实践,不也觉得快乐吗。读书学习都应该有目的性,就算是消遣也是一种目的。 程序员学习技术更是如此,毫无目的的学一个新技术,不如优先学习工作需要的(或者个人要用到),比较容易获得这种快乐。 ","date":"2022-01-02","objectID":"/posts/read/20220102/:3:0","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["thinker"],"content":"步骤 学习目的(如果有的话): 有实践的目的,最容易学进去;当然,只是单纯的兴趣也可以 了解此技术的优缺点(或者说适用范围): 如果要实践,在什么情况下用到这种技术是自然要了解 寻找/筛选资源: 利用网络,书籍找到资源,再结合其他人的评价和个人的了解筛选出好的资源 浅尝辄止: 先简单了解,直接实操,了解整个流程后再深入核心 计划: 了解皮毛应该用不了多长时间因此我不把它放入计划中,之后的深入学习才需要计划,分解学习难点,指定一个合理的计划,更容易在每个阶段后做出调整或者奖励自己,看到进度比盲目的向前进要容易成功。 边学边实践: 对所学知识有了初步了解,又制定好了计划,就可以学习与实战互相结合了,利用新学会的技术一步步做更复杂的事情,遇到无法解决的再回头学习不会的东西,学会后解决问题,循环反复,最后实现目标。 总结归纳: 整理实践与学习的知识点 输出: 分享他人或写成文章,在这个过程中会因为要公开而更加细致的完善自己之前的不足之处 ","date":"2022-01-02","objectID":"/posts/read/20220102/:3:1","tags":["SS"],"title":"《软技能》","uri":"/posts/read/20220102/"},{"categories":["coder"],"content":"在我看来软件设计主要做两件事一个是划分边界另一个是做权衡。 划分边界往小了说就是一个变量应该放到哪个模块(类或包),往大了说一个功能应属于哪个服务,有了边界还要考虑它们之间的依赖关系。权衡也有多种情况:比如功能实现上的优先级、扩展性的度、更细节点就是算法的时间与空间的抉择;还有限制上的平衡,比如一个函数的参数是个更宽泛的接口(基类)还是针对性的具体类型。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:0:0","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"什么是软件架构 从开发者的角度来看,尽管他们英勇,加班和奉献,但他们根本没有得到任何东西了。 他们所有的努力都被从开发功能特性中转移出来,现在被用来管理混乱。 他们的工作已经改变了,从一个地方转移到另一个地方,下一个和另一个地方,这样下去他们就只能增加小小的功能特征了。— 《架构整洁之道》 创建一个能运行的程序是相对简单的,但让程序持续的运行,并且对不断变化的需求做出反应就复杂的多。如果算法+数据结构=程序的话,那么底层设计+高层架构=软件系统。 而一个软件系统是由多个功能组合而成,那么组合的方式就是所谓的架构,功能的设计与组合方式是互相影响的, 所以设计与架构无需刻意区分。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:1:0","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"软件架构的目标 最小成本构建业务需求 –《架构整洁之道》 架构的设计就是为了实现业务需求,这是最最基本的,否则软件毫无用处。而业务是会随着用户或者市场需求不断变化的,所以我们的架构要适应变化也是非常重要的。适应也就意味着用更小代价来构建新需求。所以这是软件架构的核心目标。 我们在架构设计中还要考虑需求的实现与适应变化(可扩展性)之间的权衡,比如从开发角度改动影响大的应该优先开发,不影响其他逻辑的可以延后;从产品角度有急需上线的也有不太着急的功能。设计者还需要考虑一个度的问题,当前产品100个用户,就不应该考虑1w个用户的场景,但可以稍微考虑1000个用户的情况,那么这个度可能受开发进度的影响,或者市场变化的影响。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:2:0","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"编程范式 知道了架构目标,那么实现目标的方法是怎样的呢?那就是编程范式,常见的编程范式主要是三种: 范式 作用 限制 结构化编程 将模块递归降解拆分为可推导的单元,更方便进行测试进行证伪,限制了goto 对程序控制权直接转移进行了限制和规范 面向对象编程 利用核心的多态性对依赖关系进行反转(策略与实现的分离) 对程序控制权的间接转义进行了限制和规范 函数式编程 对可变性进行了隔离 对程序中的赋值进行了限制和规范 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:3:0","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"结构化编程 代码实现后还需要验证它的准确性,防止bug产生,而软件测试是复杂的。Dijkstra 提出的解决方案是采用数学推导方法:程序员可以用代码将一些己证明可用的结构串联起来,只要自行证明这些额外代码是正确的,就可以推导出整个程序的正确性。 在整个证伪过程中: goto 语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。另外goto完全可以由其他语句替代(while、if/else): Bohm 和 Jocopini 证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。这个发现非常重要: 因为它证明了我们构建可推导模块所需要的控制结构集与构建所有程序所需的控制结构集的最小集是等同的。 这样一来,结构化编程就诞生了 Dijkstra: 测试只能展示bug存在,不能证明不存在bug 计算机程序的准确性是无法证明的,只能证伪。因此我们只能在尽可能多的情况下确保程序是没有bug的,但无法证明程序在任何条件任何情况下都是完美的。 总结 结构化编程限制了goto,将模块拆解为可推导的单元,更容易进行测试 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:3:1","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"面向对象编程 首先明确一点就是这些编程范式都是设计并非技术,所以任何语言都可以达到范式的效果。面向对象所谓的封装继承多态,即便非面向对象语言也可以支持。 比如多态性: /* file: int getchar(){ return FILE-\u003eread(); } 网络io: int getchar(){ return SOCKET-\u003eread(); } */ void copy() { int c; while((c = getchar()) != EOF){ putchar(c); } } 这是c语言一个copy功能,只要具体设备实现了getchar接口,无论我们写文件或者网络io都可以用此功能。这就是多态性,调用者不用改动任何写入代码,即可支持多种设备。 这是函数指针的一种应用,而函数指针具有危险性(只有在具体调用的时候才能确定是否实现了接口),只能依靠人为遵守约定。 面向对象的作用就是对封装继承的显示支持,并且让多态可以更安全的使用(语言层面定义函数指针的规范)。依靠多态性可以更简单安全的实现依赖反转。 什么是依赖反转?高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象来互相依赖,举个例子: class Animal { public void say() { System.out.println(\"do nothing\") } } class Cat extends Animal{ public void say() { System.out.println(\"meow\"); } } class Dog extends Animal{ public void say() { System.out.println(\"woof\"); } } class Main { public static void main(String[] args) { List\u003cMammal\u003e mammals = new ArrayList\u003c\u003e(); mammals.add(new Dog()); mammals.add(new Cat()); for (Mammal mammal : mammals) { mammal.say(); } } } 主函数的流程(高层模块)与具体实现的Dog、Cat类(底层模块)不再依赖。中间通过Animal(抽象层 可以是接口、基类、抽象类等)解耦。高层模块不再依赖具体的实现,而是反过来了,底层实现依赖上层提供的接口,在面向对象编程中,得到了更加简单安全的实现。 非依赖反转 符合依赖反转 总结 面向对象让高层策略性组件与底层实现性组件分离,让插件式的架构流行起来,还让高层组件可以独立与底层实现部署。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:3:2","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"函数式编程 函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错,这使得单元测试和调试都更容易。 另外不修改外部状态在多线程下更加简单,不用考虑锁、脏数据问题。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:3:3","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"总结 每个范式提出了新的限制,约束了编写方式,并没有增加新的能力。告诉我们不该做什么,而不是告诉我们该做什么。结构化编程限制了流程,拆解了模块,方便测试证伪;面向对象限制了依赖关系,高层不再依赖具体实现,这种解耦带来了众多好处;函数式编程限制了赋值,解决线程带来的问题。我们一般的应用开发,这几种范式都可能会用到。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:3:4","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"关于设计原则 设计原则的目的: 让设计更容易改动和复用,既然容易改动和复用也就意味着其他开发人员更容易理解。 如果说编程范式约束规范了我们的整套代码,那设计原则是对模块与模块之间关系的规范,我们常说的设计原则主要指的是SOLID原则,也就是: 单一职责 开闭原则 里氏替换原则 接口隔离原则 依赖反转原则 这几种原则其实是殊途同归,都是为了划清边界,理清依赖关系。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:4:0","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"再说面向对象 上面说过,编程范式只是限制了某些事情,而不是增加了某些能力。封装继承多态,并不是面向对象语言独有的,只是面向对象语言对其进行了更加规范的语言层面的限制。 我最开始说划分边界,那封装的作用就是明确了成员的内外关系,划分了边界 继承则是有两个作用一个是代码复用、另一个是抽离策略与行为(基类设定行为,子类具体实现细节)。然而代码复用不需要继承也能轻易实现,如果为了代码复用利用继承关系,反而让两个类产生了关系增加了耦合,强面向对象语言比如c#或java,任何成员(变量或者函数)都需要宿主(类或结构体),那么代码复用要么利用继承关系,要么new一个对象再复用。不过还好有个静态类可以更简单的应对复用。 继承的策略与行为的抽离才是最关键的,因为两者的分离才有了多态性让代码变得更软。然而策略与行为的抽离并不是继承创造出来的,而是函数指针的功劳。那么看来面向对象的继承实际上并没有什么优点,可能唯一的优点就是让多态的实现变得更安全和便捷。 多态性使得调用者无需关注具体实现者,多个不同实现者也可以用同样方式调用。 封装使得成员明确了职责,划分了边界,多态让依赖关系更加容易,这两点让代码模块化更简单灵活。而面向对象语言从语法层面规范了它们。 ","date":"2021-11-07","objectID":"/posts/2021/20211107-design/:5:0","tags":["设计"],"title":"软件设计随想","uri":"/posts/2021/20211107-design/"},{"categories":["coder"],"content":"我把单机和集群分开总结,这篇主要是单机下的基础,优化方案等 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:0:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"概要 有些细节直接看书就行,就不再重复了,一些常用的命令也不说了。重点说使用场景,注意事项,性能分析调优等。 先简单介绍下最基本的数据结构: typedef struct redisObject { // 数据类型 string list set zset hset unsigned type:4; // 编码类型 unsigned encoding:4; // 开启策略后 内存不足进行指定回收 unsigned lru:LRU_BITS; // 内存回收相关,redis使用引用计数回收内存,当refcount为0后,回收内存 int refcount; //指向具体的用户数据 void *ptr; } robj; redis存储的数据都是以上述redisObject对象方式存储,主要是为了几个功能: 节省内存、增加内存回收机制、对外的api掩盖了编码不同的复杂 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:1:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"基础命令 编码方式在什么情况下转换细节不说了,具体可以看《redis设计与实现》 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:2:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"string 字符串类型数据会有3种编码方式: int、embstr、raw; 使用场景: 最常用的缓存功能: key是关键字,value是缓存信息 限速: 比如验证码每隔5秒才能重新请求一次,设定一个key超时时间,获取不到key时才能重新请求验证码 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:2:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"hset 字典类型有2种编码方式: ziplist、hashtable 使用场景: 整合string: 如果同类数据都单独kv存储,键过多浪费内存,在业务上也不直观,这时候用hset内聚 字符串序列化: 直接将序列化数据一键保存,坏处是数据量大的情况下要全部取出,修改后再更新,并且序列化有一定的开销。如果用字典取代则第一次存储会麻烦一点,但之后可以指定具体key进行修改,不过hset的整体内存消耗也会大于一个简单的k/v。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:2:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"list 列表结构对插入,查询是有顺序的 列表类型有2种编码方式: ziplist、quicklist、linkedlist 使用场景: 队列: lpush + rpop 栈: lpush + lpop 消息队列: 命令增加b则可以阻塞,用lpush+brpop 就可以当成一个简单的消息队列使用 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:2:3","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"set set不允许数据重复,set之间可以进行交并差集操作 列表类型有2种编码方式: intset(所有元素都是整数)、hashtable 使用场景: 标签: 每个用户有一个爱好集合,多个用户直接查看共同爱好只需要进行交集操作 sinter user1 user2 抽奖: 生成n个随机数写入set,然后每个用户都能进行 spop key 获得一个数字 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:2:4","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"zset 有序集合根据分值进行排序,增加一个元素都会设置一个分值,之所以用skiplist好处是范围查询。 列表类型有2种编码方式: ziplist、skiplist 使用场景: 排行榜: 根据游戏玩家战斗积分进行排名 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:2:5","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"有用的小功能 这里介绍一些不太常用,但比较有用的redis操作 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"Lua \u0026 事务 一组动作要么全部执行成功,要么全部执行失败,这就是事务。 redis中使用事务很简单: 127.0.0.1:6379\u003e multi OK 127.0.0.1:6379(TX)\u003e set a 11 QUEUED 127.0.0.1:6379(TX)\u003e set b aa QUEUED 127.0.0.1:6379(TX)\u003e exec 1) OK 2) OK 127.0.0.1:6379\u003e redis不支持事务回滚,如果写错了某个命令, 最终执行成功后,是无法回滚的。lua脚本可以提供更强大的功能。 命令: EVAL(命令的关键字) \"luascript(Lua脚本)\" numkeys(指定的Lua脚本需要处理键的数量) key1 key2.. arg1 arg2... 举例: 127.0.0.1:6379\u003e set a hello OK 127.0.0.1:6379\u003e eval \"return redis.call('GET',KEYS[1])..ARGV[1]\" 1 a ee \"helloee\" 也可以将脚本加载到redis中,达到复用 127.0.0.1:6379\u003e script load \"return redis.call('GET',KEYS[1])..ARGV[1]\" \"2395484f3580116c01aecdda33849d4b42b2d5c2\" 127.0.0.1:6379\u003e evalsha 2395484f3580116c01aecdda33849d4b42b2d5c2 1 a ee \"helloee\" 那么事务能做的,用lua脚本也可以实现,并且lua还能更简单的实现复杂的带业务逻辑的事务比如: 需要对某个有序集合范围内的数据进行分值统一修改, ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"Bitmap 比如有这样一个场景: 游戏服务搞活动,连续7天登录可以获得奖励。 如果用redis实现,那我们要分别记录7条用户id的集合,最后做交集,计算出符合要求的用户,进行奖励发放。 如果用户量很大,每天上线的用户很多,每个集合会很大,这时候用bitmap可以很方便解决。 一个位只能表示0或1,一个byte则能表示8个元素的状态,如果有8个id为1,2,3,4,5,6,7,8的用户,每个用户登录则修改所占位为1,假设第一天前四个用户登录了,那么一个byte则为11110000,这就是bitmap的原理。随着用户的数量增大,就比其他数据结构节约更大内存。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"Hyperloglog hyperloglog使用基数估算算法,可以大幅度降低内存占用,用来估算一批数据中不重复的元素数量,比hashmap、bitmap内存占用还要小。 但不精准,redis的失误率是0.81%,这种使用场景主要是数据量大或对精确度不敏感的场景,比如查看google.com每天的访问总量,这种上亿访问量的站点,即便有几十万,几百万的不精准也不重要。 常用命令: PFADD key element [element …] : 添加指定元素到 HyperLogLog 中。 PFCOUNT key [key …] : 返回给定 HyperLogLog 的基数估算值。 PFMERGE destkey sourcekey [sourcekey …] : 将多个 HyperLogLog 合并为一个 HyperLogLog ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:3","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"GEO 录入地址的经纬度,就可以获取两个地址的距离,也可以根据经纬度判断是否在某个地址范围 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:4","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"Pipeline 减少网络rtt 多个命令一起发送给redis服务器,redis将结果集统一返回给客户端,减少了多个单一命令的往返时间,多次往返时间变成了一次。 所以在网络延迟大的情况下,pipeline效果更明显。需要注意的是pipeline不是原子操作。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:5","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"其他命令 append 127.0.0.1:6379\u003e set a hello OK 127.0.0.1:6379\u003e append a world (integer) 10 127.0.0.1:6379\u003e get a \"helloworld\" 127.0.0.1:6379\u003e exists b (integer) 0 127.0.0.1:6379\u003e append b hello (integer) 5 127.0.0.1:6379\u003e get b \"hello\" 若有内容则追加,否则和set相同,这个命令的好处是可以追加,也就是无需get后再重新set。 需要注意的是无论原本的编码格式是什么 embstr或 int,无论追加什么内容都会改变为 raw编码 127.0.0.1:6379\u003e set a 1 OK 127.0.0.1:6379\u003e object encoding a \"int\" 127.0.0.1:6379\u003e append a 2 (integer) 2 127.0.0.1:6379\u003e get a \"12\" 127.0.0.1:6379\u003e object encoding a \"raw\" getset 返回老值并赋予一个新值,当没有此值的时候返回空 127.0.0.1:6379\u003e getset a hello (nil) 127.0.0.1:6379\u003e getset a world \"hello\" 127.0.0.1:6379\u003e get a \"world\" 可以理解为 get + set 命令集,且是原子性的 stream 主要用于消息队列,缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:3:6","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"持久化 redis是内存数据库,内存在程序退出或异常退出后都会丢失,redis提供了两种持久化的方法 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:4:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"rdb rdb触发条件有两种 手动: 手动触发比较简单:使用save命令,会阻塞主线程,在完成之前无法使用其他命令, bgsave会fork子进程异步执行save。 自动: 一般配置文件里会有 save m n相关配置,表示m秒内数据进行n次修改时,自动触发bgsave。 rdb是全量复制,好处是备份方便,备份文件拷贝到其他机器做灾难恢复,也可以对备份后的rdb做分析检查redis内存性能问题。坏处就是需要fork子进程,频繁操作成本高。 无法实时的持久化。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:4:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"aof aof提供了命令级别的持久化,会把写入相关命令同步到aof文件中,因为一条写入命令就立刻落地到文件中,会影响redis的高性能。所以官方提供了三种持久化策略进行选择: AOF_FSYNC_NO : redis不做任何同步操作,保存时机由系统决定;因为调用的系统命令sync AOF_FSYNC_EVERYSEC :每一秒钟保存一次 write会写入命令,但fsync命令才会最终执行同步到文件中,所以redis有一个单独的线程来每秒进行fsync操作,理论上只有在系统突然宕机的情况下丢失1秒的数据。(严格 来说最多丢失1秒数据是不准确的) AOF_FSYNC_ALWAYS :每执行一个命令保存一次 每次有写入命令,都会进行同步到aof文件操作,虽然保证了实时性(最多丢失一个命令),但如果同步过程阻塞,则会影响整个redis的命令读写效率。 当然,也提供了手动执行的方式: 命令为 bgrewriteaof aof是追加式的写入,这样的缺点就是会有重复,比如: set a hello set a world 两条命令,其实可以合并为一条set a world,这以点redis提供了重写机制,用来将已有的aof文件进行整合缩减。 重写条件在配置文件中,比如当aof文件增长比达到 n% 后就会进行重写,会后台启动一个子线程进行重写操作。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:4:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"内存回收 redis主要有两种回收策略,设置了过期的key,过期后回收。内存达到上限后有限制的回收。这些都有配置进行策略性的回收 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:5:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"过期策略 redis使用了两种方式进行过期键的回收 惰性过期 只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存 定期过期 每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 两者结合来提高内存的释放效率,节省cpu资源 lazy free 若删除一个大的key,因为慢而阻塞redis,所以4.0开始加入了惰性删除。使用命令unlink key 可以主动的使用惰性删除某个key。或根据下面配置,进行被动的删除。 ## 在内存到达最大内存需要逐出数据时使用 ## 建议关闭,避免内存未及时释放 lazyfree-lazy-eviction no ## 在KEY过期时使用 ## 建议开启 lazyfree-lazy-expire no ## 隐式删除服务器数据时,如RENAME操作 ## 建议开启 lazyfree-lazy-server-del no 无论是主动还是被动他们的流程都是一样的: 删除的时候计算Lazy Free方式释放对象的成本,只有超过特定阈值,才会采用Lazy Free方式 Lazy Free方式会调用bioCreateBackgroundJob函数来使用BIO线程后台异步释放对象。 当Redis对象执行UNLINK操作后,对应的KEY会被立即删除,不会被后续命令访问到,对应的VALUE采用异步方式来清理。 若对过期不敏感,可以考虑多个key分散过期时间,防止key都在一个时间内过期造成性能影响。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:5:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"数据删除 redis作为缓存服务时可以利用下面策略来降低内存, 具体策略受maxmemory-policy参数控制,Redis支持6种策略 noeviction:不删除任何key,,新写入操作会报错。 allkeys-lru:在键空间中,移除最近最少使用的key。 allkeys-random:在键空间中,随机移除某个key。 volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。 volatile-random:在设置了过期时间的键空间中,随机移除某个key。 volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:5:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"内存优化 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:6:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"基础对象的编码调整 不同的数据有不同的编码方式(encoding)主要是用来节省内存空间,比如使用hset数据类型时,在元素数量小于配置值的时候,同时所有值都小于hash-max-ziplist-value配置时,使用ziplist结构当字典使用,更加节省内存。当超出条件后检索效率会降低,所以会改为hashtable。 每种redis数据结构都有2种或以上的编码方式来实现效率和空间的平衡,元素数量极少的时候即便是0(n²)也可以满足性能需求 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:6:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"对象共享 相同数据多个key会指向同一个对象,这时refcount 会增加引用数量,不过这种节省内存的方式redis只提供了数字上的对象复用,因为判断数字是否一致时间复杂度为0(1),而字符串需要0(n),其他类型可能更复杂,如果复用就导致了时间换空间,对于高性能的redis并不合适。一个数字类型占用空间很小,比一个redisObject对象小的多,再加上判断快,所以对于数字对象的内存共享是很有意义的。 redis 只支持10000以内的数字对象复用,并且不可配置写死在代码中的。 server.h #define OBJ_SHARED_INTEGERS 10000 server.c void createSharedObjects(void) { //... for (j = 0; j \u003c OBJ_SHARED_INTEGERS; j++) { shared.integers[j] = makeObjectShared(createObject(OBJ_STRING,(void*)(long)j)); shared.integers[j]-\u003eencoding = OBJ_ENCODING_INT; } //... } 需要注意的是如果开启 maxmemory和LRU淘汰策略后对象池就无效了。因为共享一个redisObject后也会共享lru字段: typedef struct redisObject { // 数据类型 string list set zset hset unsigned type:4; // 编码类型 unsigned encoding:4; // 开启策略后 内存不足进行指定回收 unsigned lru:LRU_BITS; // 内存回收相关,redis使用引用计数回收内存,当refcount为0后,回收内存 int refcount; //指向具体的用户数据 void *ptr; } robj; 导致无法对每个对象的最后访问时间进行分别记录。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:6:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"内存碎片 什么是内存碎片?网上看到一个例子非常贴切: 坐高铁,假设一个车厢60个位置,目前空位有3个,但这三个都是独立的位置,不是连续的。这时候如果有3个朋友想坐在一起,就无法满足,只能选其他车厢,也就是整体内存是足够的,但无法提供服务 造成内存碎片主要原因是: 为了方便的做内存管理,内存分配器不会完全按照申请的大小做分配,比如jemalloc分配器,我们申请15字节,jemalloc会给我们20字节内存,这样好处是如果继续写入5字节内容,就减少了一次分配次数,但多出来的5字节就是碎片。 正常的业务都会对kv内容进行修改删除造成内存扩大或释放 如何判断内存碎片情况: 使用命令:info memory # Memory used_memory:1073741736 used_memory_human:1024.00M used_memory_rss:1997159792 used_memory_rss_human:1.86G # mem_fragmentation_ratio 大于1但小于1.5。这种情况是合理的。 # mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了 mem_fragmentation_ratio:1.86 解决方法: 重启redis: 重启后数据重新加载,之前非连续的内存就能连续了 配置设置: 此配置可以控制redis自动清理 activedefrag true # 是否开启自动内存清理 active-defrag-ignore-bytes 100mb # 表示内存碎片的字节数达到 100MB 时,开始 清理; active-defrag-threshold-lower 10 # 表示内存碎片空间占操作系统分配给 Redis 的 总空间比例达到 10% 时,开始清理。 active-defrag-cycle-min 25 # 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展; active-defrag-cycle-max 75 # 表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致 响应延迟升高。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:6:3","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"一些性能问题 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:7:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"慢查询日志 slowlog-log-slower-than 10000 记录超过10000微妙的命令 slowlog-max-len 128 最多存储128条慢查询日志 使用命令 slowlog get 就能查询出所有符合上述条件的命令 1) 1) (integer) 10466 日志id 2) (integer) 1650529643 命令执行时间 3) (integer) 377462 执行耗时(微妙) 命令和命令参数 4) 1) \"LRANGE\" 2) \"1611a5de-c05f-11ec-9c1a-0050569ae574_20220421_160652_421424\" 3) \"0\" 4) \"-1\" 5) \"127.0.0.1:45984\" 执行命令的客户端 6) \"\" ..... ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:7:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"bigkey 利用命令redis-cli -h ip -p port -a pwd --bigkeys 可以查看bigkeys信息。 redis的命令读写是单线程的,操作大的key会直接影响整个阻塞整个服务。redis4.0 前 删除bigkey会阻塞住,4.0之后支持了异步删除。建议不用redis存,或者将bigkey进行拆分 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:7:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"redis与系统 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:8:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"redis与cpu 先简单说说cpu cpu: 中央处理器,一个cpu不等于物理核,也不等于逻辑核。 物理核: cpu真正的运行单元,有独立的运作能力(能独自运行指令、有独立缓存) 逻辑核: 物理核中逻辑层面的核,一个物理核可以有多个逻辑核,物理核通过高速运算,让应用层误以为有多个cpu在运算 奔腾处理器时代,计算机想要提高运算性能,可以使用多个cpu,插入到主板上。但主板上的多个cpu之间进行通信效率非常低,因为通过系统总线完成,所以无法做到1+1=2的效果。既然多个cpu之间通信效率低,于是又在单cpu上进行了研究,之后英特尔开发了超线程(Hyper-threading)技术,它可以复制cpu内部组件,便于线程之间共享信息,这样的好处是加快了多个计算过程,更高效的利用cpu。假设只有一个物理核的cpu,利用超线程,操作系统误以为有2个物理核,需要注意的是这是提高了cpu的利用率,但并没有真正达到2倍的cpu处理能力。超线程提高了性能, 但并没有达到真正意义上的并行处理,之后多核架构的出现,一个cpu内有多个物理核心,达到了真正意义上的并行处理,多个物理核直接不在靠系统总线传输,而是通过共享芯片的内部总线。 最后多个物理核+超线程,就有了现在的 双核4线程/八核16线程的cpu。 对于cpu调用程序 软亲和性:进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他CPU。Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性,因此linux通过这种软的亲和性试图使某进程尽可能在同一个CPU上运行 硬亲和性:将进程或者线程绑定到某一个指定的cpu核运行,虽然Linux尽力通过一种软的亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户强制指定进程无论如何都必须在指定的处理器上运行。 目前我们的cpu架构是numa架构(非统一内存访问架构(Non-uniform Memory Access,简称NUMA架构),这意味着物理核之间如果处于不同的numa节点,那么内存是分离的,a核心(socket 1)访问b核心(socket 2)内存数据是需要经过总线的,会增加延迟。 linux使用lscpu看cpu情况: Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 1 Core(s) per socket: 1 座: 8 NUMA 节点: 1 厂商 ID: GenuineIntel CPU 系列: 6 型号: 79 型号名称: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz 步进: 1 CPU MHz: 2199.998 BogoMIPS: 4399.99 超管理器厂商: VMware 虚拟化类型: 完全 L1d 缓存: 32K L1i 缓存: 32K L2 缓存: 256K L3 缓存: 25600K NUMA 节点0 CPU: 0-7 3种方式 指定某个进程绑定到cpu: taskset -pc cpuid 进程id 启动进程的时候进行绑定: taskset -c cpuid 程序启动项 使用系统调用: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include \u003csched.h\u003e /* 设置进程号为pid的进程运行在mask所设定的CPU上 * 第二个参数cpusetsize是mask所指定的数的长度 * 通常设定为sizeof(cpu_set_t) * 如果pid的值为0,则表示指定的是当前进程 */ int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);/* 获得pid所指示的进程的CPU位掩码,并将该掩码返回到mask所指向的结构中 */ redis的持久化,还有个别命令都是在子进程或子线程执行的,也就是说对于redis绑定一个物理核还是有可能阻塞的。另外redis网络用的是io多路复用,监听的是io事件,在使用numa架构的时候我们应该防止redis在绑定cpu时跨节点。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:8:1","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"redis 与linux hugepage 写时复制 fork是系统命令,主进程执行后,会将内存数据完全的拷贝在子进程中,相当于创建了一个快照。redis用fork的好处是方便,且不影响主进程工作,因为是完全拷贝了主进程的内存,但当redis内存数据非常大的时候,fork会非常慢,若使用了10g内存,fork之后总体就占了20G内存 linux提供了fork的写时复制(copy-on-write),主要作用就是将拷贝推迟到写操作真正发生时,这也就避免了大量无意义的拷贝操作。 也就是在redis进行 rdb save的时候,fork是很快的,因为fork只是子进程指向了与主进程同样的物理内存中,并没有发生内存复制操作。类似应用代码中创建了个指针,两个指针指向的是同一个数据。只有当主进程的数据做了修改,才会开始复制,并且只会复制修改所在内存页的数据,也就是复制的效率取决于redis在save过程中的写命令是否频繁,内存页的大小。但因为redis总体是读多写少。也就是说假设在fork后进行备份过程中,redis并没有任何写入行为,那么fork子进程进行持久化操作是不会产生额外的使用内存。 刚才说内存页也影响了rdb效率,是因为linux hugepage(大内存页),hugepage可以增加命中率减少页数量的,这对数据库来讲是个好处。同样的内存需求情况下内存页大了意味着页表项的减少,这样就可以提高快表的命中率了,linux系统是支持内存大页机制的 默认是2mb: grep Huge /proc/meminfo,但对于redis进行rdb时利用写时复制,内存页大导致主进程写入操作会复制更大的内存空间和数据,所以如果开启了hugepage redis会有下面log: WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise \u003e /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never'). 如果有rdb需求则可以考虑关闭linux hugepage。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:8:2","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"redis 与 vm redis3.0 之前自己开发了vm,之后就去掉了,主要原因可能是代码复杂,重启慢等。3.0开始使用了/proc/sys/vm/overcommit_memory系统相关的vm,若没设置,会有下面的log WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. linux系统配置/proc/sys/vm/overcommit_memory 有三种策略: # 设置为2,禁用overcommit,会降低内存的使用效率,浪费内存资源。但是不会发生OOM。 # 设置为1,内核假装总是有足够的内存,直到它实际耗尽 # 设置为0,默认值,适度超发内存,但也有OOM风险。(这也是数据库经常发生OOM的原因) vm.overcommit_memory=1 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:8:3","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"配置 # -------- 持久化相关 ------------- # rdb相关持久化 # save \u003cseconds\u003e \u003cchanges\u003e # Redis 默认配置文件中提供了三个条件: # 90 0 秒(15 分钟)内有 1 个更改 save 900 1 # 300 秒(5 分钟)内有 10 个更改 save 300 10 # 60 秒内有 10000 个更改 save 60 10000 # 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大 rdbcompression yes # 存储路径 dir /var/lib/redis/ # aof持久化 # 是否启动aof appendonly no # 三种aof策略 # no:表示等操作系统进行数据缓存同步到磁盘(快) # always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全) # everysec:表示每秒同步一次(折衷,默认值) appendfsync everysec # -------- 内存管理策略 ------------- # volatile-lru: 对设置了过期时间的keys适用LRU淘汰策略 # allkeys-lru: 对所有keys适用LRU淘汰策略 # volatile-lfu: 对设置了过期时间的keys适用LFU淘汰策略 # allkeys-lfu: 对所有keys适用LFU淘汰策略 # volatile-random: 对设置了过期时间的keys适用随机淘汰策略 # allkeys-random: 对所有keys适用随机淘汰策略 # volatile-ttl: 淘汰离过期时间最近的keys # noeviction: 不淘汰任何key,仅对写入操作返回一个错误 maxmemory-policy noeviction # 默认是noeviction # -------- 惰性删除 ------------- ## 在内存到达最大内存需要逐出数据时使用 ## 建议关闭,避免内存未及时释放 lazyfree-lazy-eviction no ## 在KEY过期时使用 lazyfree-lazy-expire no ## 隐式删除服务器数据时,如RENAME操作 lazyfree-lazy-server-del no # -------- 数据结构高级配置 ------------- # ziplist最大条目数 hash-max-ziplist-entries 512 # ziplist单个条目value的最大字节数 hash-max-ziplist-value 64 # ziplist列表最大值,默认存在五项: # -5:最大大小:64 Kb \u003c——不建议用于正常工作负载 # -4:最大大小:32 Kb \u003c——不推荐 # -3:最大大小:16 Kb \u003c——可能不推荐 # -2:最大大小:8 Kb\u003c——很好 # -1:最大大小:4 Kb \u003c——好 list-max-ziplist-size -2 # 一个quicklist两端不被压缩的节点个数 # 0: 表示都不压缩。这是Redis的默认值 # 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩 # 3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。 list-compress-depth 0 # 当集合中的元素全是整数,且长度不超过set-max-intset-entries(默认为512个)时, # redis会选用intset作为内部编码,大于512用set。 set-max-intset-entries 512 # 当有序集合的元素小于zset-max-ziplist-entries配置(默认是128个),同时每个元素 # 的值都小于zset-max-ziplist-value(默认是64字节)时,Redis会用ziplist来作为有 # 序集合的内部编码实现,ziplist可以有效的减少内存的使用。 zset-max-ziplist-entries 128 zset-max-ziplist-value 64 # value大小 小于等于hll-sparse-max-bytes使用稀疏数据结构(sparse),大于hll-sparse-max-bytes使用稠密的数据结构(dense) hll-sparse-max-bytes 3000 # Streams单个节点的字节数,以及切换到新节点之前可能包含的最大项目数。 stream-node-max-bytes 4096 stream-node-max-entries 100 # 主动重新散列每100毫秒CPU时间使用1毫秒,以帮助重新散列主Redis散列表(将顶级键映射到值) activerehashing yes # -------- 内存碎片 ------------- # 是否启用碎片整理,默认是no activedefrag no # 最小的碎片空间浪费量 active-defrag-ignore-bytes 100mb # 最小的碎片百分比阈值 active-defrag-threshold-lower 10 # 最大的碎片百分比阈值 active-defrag-threshold-upper 100 # 碎片整理周期CPU消耗最小百分比 active-defrag-cycle-min 1 # 碎片整理周期CPU消耗最大百分比 active-defrag-cycle-max 25 # redis5.0之后的配置 从set / hash / zset / list 扫描的最大字段数 active-defrag-max-scan-fields 1000 # redis6.0之后的配置 默认情况下,用于清除的Jemalloc后台线程是启用的。 jemalloc-bg-thread yes # -------- cpu绑定(redis6.0) ------------- # 设置redis服务器的IO线程组的CPU绑定:0,2,4,6 server_cpulist 0-7:2 # 设置BIO线程的CPU绑定为:1,3: bio_cpulist 1,3 # 设置AOF子进程的CPU绑定为:8,9,10,11 aof_rewrite_cpulist 8-11 # 设置bgsave的CPU绑定为:1,10-11 bgsave_cpulist 1,10-11 # -------- 其他 ------------- # 执行大于多少微妙,才存入慢查询队列 slowlog-log-slower-than 10000 # 慢查询最多保存多少日志 slowlog-max-len 128 # 一个Lua脚本最长的执行时间,单位为毫秒,如果为0或负数表示无限执行时间,默认为5000 lua-time-limit 5000 6379来源: 是手机按键的 MERZ,原因是redis作者Antirez在看一个广告,意大利广告女郎「Alessia Merz」在电视节目上说了一堆愚蠢的话。 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:9:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"参考 《redis设计与实现》 《redis开发与运维》 http://cenalulu.github.io/linux/numa/ https://cloud.tencent.com/developer/article/1465603 ","date":"2021-09-06","objectID":"/posts/2021/20210906-redis/:10:0","tags":["redis"],"title":"redis-单机场景","uri":"/posts/2021/20210906-redis/"},{"categories":["coder"],"content":"dlv在服务器排查程序问题,没有可视化界面比较繁琐,利用vscode+dlv 远程调试,就和在本地调试一样简单 ","date":"2021-08-13","objectID":"/posts/2021/20210813-dlv/:0:0","tags":["go"],"title":"vscode+dlv 远程调试","uri":"/posts/2021/20210813-dlv/"},{"categories":["coder"],"content":"安装 在服务器和本地开发环境都要安装dlv: go install github.com/go-delve/delve/cmd/dlv@latest; 两个机器版本一定要一模一样 vscode配置launch.json { \"version\": \"0.2.0\", \"configurations\": [ { \"name\": \"remote debug\", \"type\": \"go\", \"request\": \"attach\", // 这里设置remote 远程 \"mode\":\"remote\", // 目标机器 端口 \"port\": 2345, // 目标机器 ip \"host\": \"10.10.10.123\", // 本地与目标一模一样的工程目录位置 \"substitutePath\": [ { \"from\": \"本地项目目录/hello\", \"to\": \"/服务器项目目录/hello\" }, ] } ], } ","date":"2021-08-13","objectID":"/posts/2021/20210813-dlv/:1:0","tags":["go"],"title":"vscode+dlv 远程调试","uri":"/posts/2021/20210813-dlv/"},{"categories":["coder"],"content":"运行 代码编译增加 -gcflags='all=-N -l' 在服务器端的程序目录下(main.go所在位置)执行命令: dlv --listen=:2345 --headless=true --api-version=2 --log attach 进程id 本地vscode启动调试 这样就能远程调试了 ","date":"2021-08-13","objectID":"/posts/2021/20210813-dlv/:2:0","tags":["go"],"title":"vscode+dlv 远程调试","uri":"/posts/2021/20210813-dlv/"},{"categories":["coder"],"content":"gin是go开发的一个开源高性能http框架,其主要是把go官方的net/http进行了扩展,前缀树实现了动态路由、支持了中间件、对请求信息进行封装方便用户层使用等。本文基于 gin v1.7.2版本 ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:0:0","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"创建流程 一个Engine实例可以使用New 或者 Default进行创建,唯一区别就是Default默认增加了两个中间件:日志Logger(), panic捕获 Recovery() 初始化会初始化以下内容: //gin.go engine := \u0026Engine{ //默认的分组 RouterGroup: RouterGroup{ Handlers: nil, basePath: \"/\", root: true, }, FuncMap: template.FuncMap{}, RedirectTrailingSlash: true, RedirectFixedPath: false, HandleMethodNotAllowed: false, ForwardedByClientIP: true, RemoteIPHeaders: []string{\"X-Forwarded-For\", \"X-Real-IP\"}, TrustedProxies: []string{\"0.0.0.0/0\"}, TrustedPlatform: defaultPlatform, UseRawPath: false, RemoveExtraSlash: false, UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, trees: make(methodTrees, 0, 9), delims: render.Delims{Left: \"{{\", Right: \"}}\"}, secureJSONPrefix: \"while(1);\", } engine.RouterGroup.engine = engine //Context上下文Pool engine.pool.New = func() interface{} { return engine.allocateContext() } 初始化好后,就可以注册业务的相关api,比如GET、POST等。默认情况下所有的api都是在根分组下,举个例子: r := gin.Default() //此处的api所在的分组是默认分组,所以请求api的时候直接 /ping即可 r.GET(\"/ping\", func(c *gin.Context) { c.String(http.StatusOK, \"pong\") }) 下面说一下注册api的流程: //routergroup.go func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { //2. 将相对路径转为绝对路径 //主要就是 分组的路径+请求的路径,比如:分组为 v1/,请求路径是 hello ,这个请求的全路径就是 v1/hello absolutePath := group.calculateAbsolutePath(relativePath) //3. 因为gin支持中间件,这里是把组携带的handler和传递过来的请求函数进行组合 handlers = group.combineHandlers(handlers) //4. 将中间件和请求函数的组合放入路由中 //这样的话,一次api请求,会执行一系列的函数集,达到中间件的效果 //因为中间件是属于组的,所以一个组下的所有api都支持 group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } //1. 对外提供的http方法 //其他POST DELETE PUT 等注册流程都一样 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodGet, relativePath, handlers) } 初始化好一个Engine,并且注册了api后,就可以运行服务,对外使用了。 // main.go func main() { r := gin.Default() r.GET(\"/ping\", func(c *gin.Context) { c.String(http.StatusOK, \"pong\") }) r.Run(\":9000\") } // gin.go func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() err = engine.parseTrustedProxies() if err != nil { return err } address := resolveAddress(addr) debugPrint(\"Listening and serving HTTP on %s\\n\", address) //gin实现了ServeHTTP(w http.ResponseWriter, req *http.Request) //所以注册到http服务 err = http.ListenAndServe(address, engine) return } 因为官方 net/http 提供了接口:ServeHTTP(ResponseWriter, *Request),gin实现了接口: //gin.go func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) } 所以在底层收到http消息后,会回调gin实现的ServeHTTP,这样http消息就可以走gin提供的路由、中间件等逻辑了 ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:1:0","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"请求流程 发起http请求 底层回调gin注册函数ServeHTTP 从sync.pool中获取一个可用Context 因为是结构体并且sync.pool机制不会主动重置Context,所以手动重置Context 从前缀树中寻找对应路由 执行请求对应的函数 将结果写入响应Response Context放回sync.pool中 //gin.go //底层回调gin注册函数`ServeHTTP` func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { //从sync.pool中获取一个可用`Context` c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req //因为是结构体并且sync.pool机制不会主动重置`Context`,所以手动重置`Context` c.reset() //执行请求 engine.handleHTTPRequest(c) engine.pool.Put(c) } 从pool中获取,如果没有会进行创建,创建函数是在初始化的时候注册的。 //gin.go func New() *Engine { //... engine.pool.New = func() interface{} { return engine.allocateContext() } } func (engine *Engine) allocateContext() *Context { v := make(Params, 0, engine.maxParams) return \u0026Context{engine: engine, params: \u0026v} } ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:2:0","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"分组与中间件 ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:3:0","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"分组的作用 分组的好处是将其下的所有api进行统一管理,如果没有分组,增加一个通用功能,就需要对每一个api分别添加。比如:对/admin开头的路由进行鉴权,gin中只需要这样做: gAdmin:=r.Group(\"/admin\").Use(func(c *gin.Context) { //鉴权 }) gAdmin.GET(\"/delUser\", func(c *gin.Context) {}) gAdmin.GET(\"/addUser\", func(c *gin.Context) {}) 当用户请求 /admin/delUser和/admin/addUser时,会先执行鉴权函数。Use也就是增加中间件的方法。 ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:3:1","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"分组的路由 另外一个路由的添加是由分组地址+api的地址组合而成,初始化Engine的时候会默认有个根组它的basePath为 /: //gin.go func New() *Engine { engine := \u0026Engine{ RouterGroup: RouterGroup{ Handlers: nil, basePath: \"/\", root: true, }, //... } //... return engine } 如果不创建其他组,使用默认组的话: func main() { r := gin.Default() //请求路由为 group.basePath+ `/ping` = http://127.0.0.1/ping r.GET(\"/ping\", func(c *gin.Context) { c.String(http.StatusOK, \"pong\") }) } 分组有父子关系,下面这个a分组派生于根分组,所以a分组下的api路由是 /a/xxx,b分组下的api路由是/a/b/xxx,c分组由根分组派生,所以c分组下的api路由是 /c func main() { r := gin.Default() aGroup := r.Group(\"/a\") bGroup := aGroup.Group(\"/b\") cGroup := r.Group(\"/c\") } 路由如此,中间件也会如此,组b下的api包含所有父组的中间件: //routergroup.go func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return \u0026RouterGroup{ //新的组包含父辈的所有中间件 Handlers: group.combineHandlers(handlers), basePath: group.calculateAbsolutePath(relativePath), engine: group.engine, } } ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:3:2","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"中间件的执行 现在知道了分组和路由的关系,看看中间件是如何执行的。gin在注册一个api的时候,会把组中的中间件函数和api函数放到数组里,增加到路由里: //routergroup.go func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) //将中间件和api函数组合,中间件在数组前面 api函数在其后 handlers = group.combineHandlers(handlers) group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize \u003c int(abortIndex), \"too many handlers\") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers } 来了一个请求后,找到对应路由下的函数放到Context上下文中,调用Next执行,并且要注意的是所有的中间件所有的函数都用的同一个Context func (engine *Engine) handleHTTPRequest(c *Context) { //.. value := root.getValue(rPath, c.params, unescape) if value.params != nil { c.Params = *value.params } if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } //.. } 这里说一下Next执行细节,用个例子来说明:对分组a下所有api请求计时 func Logger(c *gin.Context) { //开始计时 t := time.Now() //调用下一个函数 c.Next() //计算用时 latency := time.Since(t) log.Print(latency) } func Hello(c *gin.Context){ fmt.Println(\"hello\") } func main() { r := gin.Default() aGroup := r.Group(\"/a\").Use(Logger) aGroup.GET(\"b\", Hello) r.Run(\":9000\") } 初始化Context func (c *Context) reset() { c.Writer = \u0026c.writermem c.Params = c.Params[:0] c.handlers = nil //index字段是-1 c.index = -1 c.fullPath = \"\" c.Keys = nil c.Errors = c.Errors[:0] c.Accepted = nil c.queryCache = nil c.formCache = nil *c.params = (*c.params)[:0] } 调用函数 Next(index=0) func (c *Context) Next() { c.index++ for c.index \u003c int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } } 执行了Logger函数: 开始计时 Logger内部执行c.Next,再次调用Next函数 因为index是c中的变量,所以会变成index=1 所以此时执行了Hello Hello执行后因为index已经变成2了,所以Next完结 Logger因c.Next()完成,继续执行后续操作:计算时间 打印时间 完成了整个api调用 ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:3:3","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"路由 上面说的各种流程都没讲一个请求过来,是如何找到具体的执行函数的,这里就是路由的作用了。用map存路由表,索引效率高效,但只支持静态路由。类似/hello/:name 可以匹配到 /hello/wang /hello/zhang的动态路由不支持。gin里使用了前缀树来实现。前缀树就不在这里介绍了 ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:4:0","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"创建路由 下面注册了5个api,从源码看看是如何执行的 func main() { r := gin.New() r.GET(\"/index\", func(c *gin.Context) { c.JSON(200, \"index\") }) r.GET(\"/inter\", func(c *gin.Context) { c.JSON(200, \"inter\") }) r.GET(\"/user/get\", func(c *gin.Context) { c.JSON(200, \"/user/get\") }) r.GET(\"/user/del\", func(c *gin.Context) { c.JSON(200, \"/user/del\") }) r.GET(\"/user/:name\", func(c *gin.Context) { c.JSON(200, \"/user/:name\") }) r.Run(\":9000\") } func (n *node) addRoute(path string, handlers HandlersChain) { fullPath := path n.priority++ // 第一个api注册因为根节点path和children是空的所以直接成为根节点的子节点 if len(n.path) == 0 \u0026\u0026 len(n.children) == 0 { n.insertChild(path, fullPath, handlers) n.nType = root return } parentFullPathIndex := 0 walk: for { // 获得第一次字符不同的位置 比如 // \"/index\" 和 \"/inter\" 第一次字符不同的位置 也就是 i=3 i := longestCommonPrefix(path, n.path) if i \u003c len(n.path) { child := node{ path: n.path[i:], wildChild: n.wildChild, indices: n.indices, children: n.children, handlers: n.handlers, priority: n.priority - 1, fullPath: n.fullPath, } /*当前node增加一个子节点child 以 inter为例子,api inter插入之前已经有了index,并且发现他们有相同字符in所以将index节点改为 in节点,dex变成子in的子节点,后面的代码会再将 ter放到in子节点中。 child := node{ path: n.path[i:], // dex wildChild: n.wildChild, // false indices: n.indices, // \"\" children: n.children, //null handlers: n.handlers, // index func priority: n.priority - 1, fullPath: n.fullPath, // /index } n.indices = \"d\" n.path = \"/in\" n.fullPath = \"\" */ n.children = []*node{\u0026child} n.indices = bytesconv.BytesToString([]byte{n.path[i]}) n.path = path[:i] n.handlers = nil n.wildChild = false n.fullPath = fullPath[:parentFullPathIndex+i] } if i \u003c len(path) { path = path[i:] c := path[0] if n.nType == param \u0026\u0026 c == '/' \u0026\u0026 len(n.children) == 1 { parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ continue walk } // 以user/del为例; // 此时根节点的indices= \"iu\" 匹配到了相同字符 `u` 于是进行跳转,并将n指向 path=\"user/\" 的节点 for i, max := 0, len(n.indices); i \u003c max; i++ { if c == n.indices[i] { parentFullPathIndex += len(n.path) i = n.incrementChildPrio(i) n = n.children[i] continue walk } } if c != ':' \u0026\u0026 c != '*' \u0026\u0026 n.nType != catchAll { //以插入inter api为例: // 此时n.indices = \"dt\" dex和ter的首字母 n.indices += bytesconv.BytesToString([]byte{c}) child := \u0026node{ fullPath: fullPath, } n.addChild(child) n.incrementChildPrio(len(n.indices) - 1) //这里这么写是方便后面流程通用 看 FF: n = child } else if n.wildChild { n = n.children[len(n.children)-1] n.priority++ if len(path) \u003e= len(n.path) \u0026\u0026 n.path == path[:len(n.path)] \u0026\u0026 n.nType != catchAll \u0026\u0026 (len(n.path) \u003e= len(path) || path[len(n.path)] == '/') { continue walk } pathSeg := path if n.nType != catchAll { pathSeg = strings.SplitN(pathSeg, \"/\", 2)[0] } prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path panic(\"'\" + pathSeg + \"' in new path '\" + fullPath + \"' conflicts with existing wildcard '\" + n.path + \"' in existing prefix '\" + prefix + \"'\") } n.insertChild(path, fullPath, handlers) return } //FF: if n.handlers != nil { panic(\"handlers are already registered for path '\" + fullPath + \"'\") } n.handlers = handlers n.fullPath = fullPath return } } 最后这个前缀树的结构应该是这样的: ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:4:1","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["coder"],"content":"api请求 engine.trees这是一个数组,每个请求类型(POST GET PUT…)独立一个树 type methodTree struct { method string root *node } type methodTrees []methodTree 当接收到底层传来的http请求后,先找到指定请求类型的树结构,然后再查询路由,查询方式比较简单,主要就是遍历树。为了提高查询效率,indices的作用就是在查询子节点之前,先找indices里有没有请求的path首字符,没有的话直接查询失败。 //gin.go func (engine *Engine) handleHTTPRequest(c *Context) { //... httpMethod := c.Request.Method t := engine.trees for i, tl := 0, len(t); i \u003c tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root //查询路由 value := root.getValue(rPath, c.params, unescape) if value.params != nil { c.Params = *value.params } if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } //... } } ","date":"2021-07-21","objectID":"/posts/2021/20210721-gin/:4:2","tags":["go","源码"],"title":"gin源码","uri":"/posts/2021/20210721-gin/"},{"categories":["thinker"],"content":" 辩证法研究对象的本质自身中的矛盾 – 列宁 从这句话得知矛盾是辩证法的核心。因为是核心,所以涉及许多方面和哲学问题:两种宇宙观、矛盾的普遍性、矛盾的特殊性、主要矛盾和主要矛盾的方面、矛盾诸多方面的同一性和斗争性、对抗在矛盾中的地位。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:0:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"两种宇宙观 主要有两种宇宙观,一种是形而上学的见解,一种是辩证法的见解。 由于欧洲许多国家的社会经济情况进到了资本主义高度发展的阶段,生产力、阶级斗争和科学均发展到了历史上未有过的水平,工业无产阶级成为历史发展的最伟大的动力,因而产生了马克思主义的唯物辩证法的宇宙观。于是,在资产阶级那里,除了公开的极端露骨的反动的唯心论之外,还出现了庸俗的进化论,出来对抗唯物辩证法 上面这句话怎么理解?我认为是资本主义因为社会的发展达到了绝对的高度,冲破了封建社会的枷锁,而工人阶级是资本主义的产物,帮助资本主义打破封建奴隶社会,这样才能方便资本扩张,于是创造了世界通史。证明了过去几千年的封建社会不会是永远存在的,总会因为内部外部的条件激化发生改变,因而给唯物辩证法带来了充分的证明。根据唯物辩证法,事物的发展是对立统一的,不会一成不变,这样的话资产阶级获得了利益,当然要防止这种思想吧。 形而上学的宇宙观就是用孤立的、静止的、和片面的观点去看世界,这种宇宙观把世界一切事物,一切事物形态和种类,都看成永远彼此孤立和永远不变话。如果有变化只是数量和场所的变更。而且是由外因而不是事物内部导致的。按照这种思想,资本主义应当是自古至今一直存在,且未来也会存在的。 唯物辩证法的宇宙观是从事物内部和与其他事物的关系去研究事物的发展。事物发展的第一根本原因是内部矛盾性,第二原因是和其他事物互相联系互相影响。和形而上学相比,单纯的外部因素只能引起事物的机械运动:范围大小、数量变化等。无法说明事物何以有性质上的千差万别及其互相变化。另外即便是外部因素的变化也依靠的是事物内部矛盾性。 外因是变化的条件,内因是变化的根据,外因通过内因起作用 举个例子:鸡蛋因合适的温度(外因)变成鸡,但温度不能让石头变成鸡(内因是变化的根据),温度能影响鸡蛋不能影响石头因为二者的根据(内部本质)是不同的。 另外一个例子:俄国十月革命开创了俄国新纪元,也影响了其他国家,中国也因此发生了内部的革命。这也说明事物之间是会互相关联影响的。 以上对比了形而上学和唯物辩证法的区别。形而上学看事物是:孤立、静止的,因此事物只有数量和场景上的变化,无质变;辩证法看事物是:互相关联影响的,事物变化主要依靠内部因素和外部条件,所以会产生质变。 辩证法的宇宙观,不论在中国,在欧洲,在古代就产生了。但是古代的辩证法带著自发的朴素的性质,根据当时的社会历史条件,还不可能有完备的理论,因而不能完全解释宇宙,后来就被形而上学所代替 为什么唯物辩证法到了近代才有影响力?随着科学、社会的进步资本的扩张对世界有了更深层更全面的认识,再加上马克思、恩格斯、列宁斯大林等对唯物辩证法的发展才能有所成就。 主要地就是教导人们要善于去观察和分析各种事物的矛盾的运动,并根据这种分析,指出解决矛盾的方法 辩证法的宇宙观有什么作用呢?我们可以根据事物的发展,找出其中的规律,找到核心的矛盾点,找到问题,才能解决问题 ","date":"2021-06-13","objectID":"/posts/read/20210613/:1:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾的普遍性 矛盾的普遍性或者说绝对性有两方面意义: 矛盾存在于一切事物的发展过程中 每一事物的发展过程中存在着自始至终的矛盾运动 ","date":"2021-06-13","objectID":"/posts/read/20210613/:2:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾存在于一切事物的发展过程中 矛盾是简单的运动形式的基础,更是复杂的运动形式的基础 根据上面的话,无论是自然、精神、社会都有互相对立矛盾的趋势。在力学中,作用和反作用。在物理学中,阳电和阴电。在化学中,原子的化合和分解。在社会科学中,阶级斗争。因为有了矛盾,才能解决矛盾,解决了矛盾才能使得事物发展甚至发生质变,才有了向上的朝气,一旦矛盾消失,事物的生命周期也就终结了。由此看来,不论是简单的运动形式,或复杂的运动形式,不论是客观现象,或思想现象,矛盾是普遍地存在著,矛盾存在于一切过程中 ","date":"2021-06-13","objectID":"/posts/read/20210613/:2:1","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"每一事物的发展过程中存在著自始至终的矛盾运动 既然矛盾存在于一切事物的发展过程中,那么每一过程的开始阶段,是否也有矛盾存在呢?是否每一事物的发展过程具有自始至终的矛盾运动呢?答案是有的。 如果说事物发展的开始不是内部矛盾,而是由外部矛盾导致,那就回到了形而上学的宇宙观去了。世界上的每一差异中就已经包含矛盾,差异就是矛盾,比如劳资之间,从一开始就有了矛盾,只是还没有被激化,他们的差异性就是矛盾。 新过程的发生是什么呢?这是旧的统一和组成此统一的对立成分让位于新的统一和组成此统一的对立成分,于是新过程就代替旧过程而发生。旧过程完结了,新过程发生了。新过程又包含著新矛盾,开始它自己的矛盾发展史 “马克思在《资本论》中,首先分析的是资产阶级社会(商品社会)里最简单的、最普通的、最基本的、最常见的,最平常的,碰到亿万次的关系—-商品交换。这一分析在这个最简单的现象中(资产阶级社会的这个‘细胞’之中)暴露了现代社会的一切矛盾(以及一切矛盾的胚芽)。往后的叙述又向我们表明了这些矛盾和这个社会各个部分总和的自始至终的发展(增长与运动两者)。” 这应该是一般辩证法的……叙述(以及研究)方法。 那一般辩证法的研究和叙述方法是什么?找到事物发展的最核心现象,从现象出发,找到其中的矛盾,然后分析这些矛盾和现象各个部分总和和自始至终的发展,这样就能正确分析历史和现状,并推断将来。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:2:2","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾的特殊性 各种物质运动形式中的矛盾,都带特殊性。人的认识物质,就是认识物质的运动形式,因为除了运动的物质以外,世界上什么也没有,而物质的运动则必取一定的形式 从研究方向来说: 因此对于每一种运动形式我们要注意它和其它各种运动形式的共同点。但造成运动形式不同点的是我们尤其要重视的,因为它是矛盾的特殊性,造成事物运动本质不同的核心结果,才能区分事物。这也是世界万物千差万别的根本原因。如果说矛盾的普遍性和绝对性说明了事物内在的变化本质,那么矛盾的特殊性与相对性则说明了事物之间的根本差异。无论是自然界的、或者是社会形式和思想形式都有特殊的矛盾和特殊的本质。也因此科学的研究区分就是根据其中的特殊性展开的: 例如,数学中的正数和负数,机械学中的作用和反作用,物理学中的阴电和阳电,化学中的化分和化合,社会科学中的生产力和生产关系、阶级和阶级的互相斗争,军事学中的攻击和防御,哲学中的唯心论和唯物论、形而上学观和辩证法观等等,都是因为具有特殊的矛盾和特殊的本质,才构成了不同的科学研究的对象 从认识过程来说: 人们总是先认识不同事物之间的区别,然后进一步进行概括,逐渐认识诸多事物共同的本质。当认识了共同本质之后,就可以以这种认识为知道,继续深入研究特殊性,然后又根据深入认识后的特殊性,再补充发展共同性。整个过程就是 先从特殊性到一般性,再由一般性到特殊性,循环往复,每次循环又能得到新的认识,使人类的认识不断提高深化。 从研究过程来说: 不但要研究每一个大系统的物质运动形式的特殊的矛盾性及其所规定的本质,而且要研究每一个物质运动形式在其发展长途中的每一个过程的特殊的矛盾及其本质。一切运动形式的每一个实在的非臆造的发展过程内,都是不同质的。我们的研究工作必须着重这一点,而且必须从这一点开始。 为什么这么说? 因为不同的矛盾要用不同的方法来解决 这是十分重要的马克思主义原则,否则千篇一律的套用公式只会发展的更坏,具体问题具体分析。 例如,无产阶级和资产阶级的矛盾,用社会主义革命的方法去解决;人民大众和封建制度的矛盾,用民主革命的方法去解决;殖民地和帝国主义的矛盾,用民族革命战争的方法去解决;在社会主义社会中工人阶级和农民阶级的矛盾,用农业集体化和农业机械化的方法去解决;共产党内的矛盾,用批评和自我批评的方法去解决;社会和自然的矛盾,用发展生产力的方法去解决 研究问题,忌带主观性、片面性和表面性。所谓主观性,就是不知道客观地看问题,也就是不知道用唯物的观点去看问题。 主观性导致只缘身在此山中,无法正确的认识问题,片面性导致只见树木不见森林,看不清问题全貌。表面形导致只能看见表面问题,无法深入到核心问题。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:3:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"主要矛盾和主要矛盾的方面 ","date":"2021-06-13","objectID":"/posts/read/20210613/:4:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾的主次之分 在复杂的事物的发展过程中,有许多的矛盾存在,其中必有一种是主要的矛盾,由于它的存在和发展规定或影响着其它矛盾的存在和发展 例如在资本主义社会中,无产阶级和资产阶级这两个矛盾着的力量是主要的矛盾;其它的矛盾力量,例如,残存的封建阶级和资产阶级的矛盾,农民小资产者和资产阶级的矛盾,无产阶级和农民小资产者的矛盾,自由资产阶级和垄断资产阶级的矛盾,资产阶级的民主主义和资产阶级的法西斯主义的矛盾,资本主义国家相互间的矛盾,帝国主义和殖民地的矛盾,以及其它的矛盾,都为这个主要的矛盾力量所规定、所影响 ","date":"2021-06-13","objectID":"/posts/read/20210613/:4:1","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾地位的变化 随着发展,主要矛盾也可能变成次要矛盾,矛盾的地位会发生变化,然而不管怎样,过程发展的各个阶段中,只有一种主要的矛盾起着领导的作用,是完全没有疑义的 半殖民地的国家如中国,其主要矛盾和非主要矛盾的关系呈现着复杂的情况。当着帝国主义向这种国家举行侵略战争的时候,这种国家的内部各阶级,除开一些叛国分子以外,能够暂时地团结起来举行民族战争去反对帝国主义。这时,帝国主义和这种国家之间的矛盾成为主要的矛盾,而这种国家内部各阶级的一切矛盾(包括封建制度和人民大众之间这个主要矛盾在内),便都暂时地降到次要和服从的地位。中国一八四零年的鸦片战争,一八九四年的中日战争,一九零零年的义和团战争和目前的中日战争,都有这种情形。 当着国内革命战争发展到从根本上威胁帝国主义及其走狗国内反动派的存在的时候,帝国主义就往往采取上述方法以外的方法,企图维持其统治:或者分化革命阵线的内部,或者直接出兵援助国内反动派。这时,外国帝国主义和国内反动派完全公开地站在一个极端,人民大众则站在另一极端,成为一个主要矛盾,而规定或影响其它矛盾的发展状态。十月革命后各资本主义国家援助俄国反动派,是武装干涉的例子。一九二七年的蒋介石的叛变,是分化革命阵线的例子。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:4:2","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾的主要和非主要的方面互相转化 事物的性质也就随着起变化,发展到某一阶段,就互易其位置,当新的事物取代了旧的事物,就会发生质的变化,由此可见事物的性质是由主要矛盾所规定的 在资本主义社会中,资本主义已从旧的封建主义社会时代的附庸地位,转化成了取得支配地位的力量,社会的性质也就由封建主义的变为资本主义的。在新的资本主义社会时代,封建势力则由原来处在支配地位的力量转化为附庸的力量,随着也就逐步地归于消灭了,例如英法诸国就是如此。随着生产力的发展,资产阶级由新的起进步作用的阶级,转化为旧的起反动作用的阶级,以至于最后被无产阶级所推翻,而转化为私有的生产资料被剥夺和失去权力的阶级,这个阶级也就要逐步归于消灭了。人数比资产阶级多得多、并和资产阶级同时生长、但被资产阶级统治着的无产阶级,是一个新的力量,它由初期的附属于资产阶级的地位,逐步地壮大起来,成为独立的和在历史上起主导作用的阶级,以至最后夺取政权成为统治阶级。这时,社会的性质,就由旧的资本主义的社会转化成了新的社会主义的社会。这就是苏联已经走过和一切其它国家必然要走的道路。 在做编码设计的时候,也是从无知到清晰的矛盾过程,一上来无知处于主要矛盾,所以随意乱用设计,为了设计而设计,影响后续工作。当熟悉设计,有了清晰的概念,清晰占据了主导地位,于是在设计编码的时候不在为了设计而设计,而是有充足原因的设计。 从这一点也能看出,塞翁失马焉知非福,物极必反,新陈代谢这些词语都是符合这样的性质的。另外我们不能把过程中所有的矛盾平均看待,即便两个矛盾看似势均力敌,但只是暂时的,基本形态择是不平衡的。所以世界上没有绝对地平衡发展的东西,从根本上反驳了平衡论或者均衡论 ","date":"2021-06-13","objectID":"/posts/read/20210613/:4:3","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"总结 在解决问题的时候要优先找到主要矛盾,任何过程如果有多数矛盾存在的话,其中必定有一种是主要的,起着领导的、决定的作用,其它则处于次要和服从的地位。因此,研究任何过程,如果是存在着两个以上矛盾的复杂过程的话,就要用全力找出它的主要矛盾。捉住了这个主要矛盾,一切问题就迎刃而解了。因为矛盾的转化性质,也给人一种向上的精神,因为失败不会一直存在下去,这也是主席的一种乐观精神的来源吧,反者道之动 ","date":"2021-06-13","objectID":"/posts/read/20210613/:4:4","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"矛盾诸方面的同一性和斗争性 ","date":"2021-06-13","objectID":"/posts/read/20210613/:5:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"同一性 同一性、统一性、一致性、互相渗透、互相贯通、互相依赖(或依存)、互相联结或互相合作,这些不同的名词都是一个意思,说的是如下两种情形:第一、事物发展过程中的每一种矛盾的两个方面,各以和它对立着的方面为自己存在的前提,双方共处于一个统一体中;第二、矛盾着的双方,依据一定的条件,各向着其相反的方面转化。这些就是所谓同一性 没有生就没有死,没有上就没有下,没有资产阶级就没有无产阶级,这些之间是有矛盾的,但每个方面都不能单独存在,因为失去了对方就失去了自己存在的意义。比如没了无产阶级,资产阶级没人可以压迫那他就不再是资产阶级了,这种依赖就是同一性。所以矛盾的双方虽然是互相对立斗争并且排斥的,但也因为彼此成为存在的意义,互为因果。资产阶级与无产阶级是互相渗透互相斗争的,因为压迫与被压迫是矛盾的。 对立怎样能够是同一的?上面说的互相依存便是其中一个意义 相互依存有同一性所以在一个统一体中,这只构成了机械辩证法,还需要矛盾的事物互相转化,一个事物的发展,达到了一定的条件,就会产生质的变化,无产阶级压迫至极,产生革命最终成为了统治者,原来的地主变成了被统治者。如果他们之间没有关联也就不会互相斗争发生质变。这就是第二种意义 巩固无产阶级的专政或人民的专政,正是准备着取消这种专政,走到消灭任何国家制度的更高阶段去的条件。建立和发展共产党,正是准备着消灭共产党和一切政党制度的条件。建立共产党领导的革命军,进行革命战争,正是准备着永远消灭战争的条件。这许多相反的东西,同时却是相成的东西。 当初国家制造原子弹,就是为了反对原子弹,为了反对而创造出反对的东西,这和制造原子弹的国家有了同一性那就是都在制造原子弹。这就是矛盾双方之间的同一性 为什么鸡蛋能够转化为鸡子,而石头不能够转化为鸡子呢?为什么战争与和平有同一性,而战争与石头却没有同一性呢?为什么人能生人不能生出其它的东西呢?没有别的,就是因为矛盾的同一性要在一定的必要的条件之下。缺乏一定的必要的条件,就没有任何的同一性。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:5:1","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"斗争性 同一性决定了矛盾相互依存与一个统一体,而斗争性择决定了矛盾的质变的原因,我觉得这里写的很好懂: 无论什么事物的运动都采取两种状态,相对地静止的状态和显着地变动的状态。两种状态的运动都是由事物内部包含的两个矛盾着的因素互相斗争所引起的。 第一种只会产生量变,后一种才会产生质变。事物总是不断地由第一种状态转化为第二种状态,而矛盾的斗争则存在于两种状态中,并经过第二种状态而达到矛盾的解决。所以说,对立的统一是有条件的、暂时的、相对的,而对立的互相排除的斗争则是绝对的。 前面我们曾经说,两个相反的东西中间有同一性,所以二者能够共处于一个统一体中,又能够互相转化,这是说的条件性,即是说在一定条件之下,矛盾的东西能够统一起来,又能够互相转化;无此一定条件,就不能成为矛盾,不能共居,也不能转化。由于一定的条件才构成了矛盾的同一性,所以说同一性是有条件的、相对的。这里我们又说,矛盾的斗争贯串于过程的始终,并使一过程向着他过程转化,矛盾的斗争无所不在,所以说矛盾的斗争性是无条件的、绝对的。 有条件的相对的同一性和无条件的绝对的斗争性相结合,构成了一切事物的矛盾运动。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:5:2","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"对抗在矛盾中的地位 在矛盾的斗争性的问题中,包含着对抗是什么的问题。我们回答道:对抗是矛盾斗争的一种形式,而不是矛盾斗争的一切形式。 列宁说:“对抗和矛盾断然不同。在社会主义下,对抗消灭了,矛盾存在着。”[37]这就是说,对抗只是矛盾斗争的一种形式,而不是它的一切形式,不能到处套用这个公式。 不能遇到矛盾就进行对抗,对抗在事物发展中是必然的,但在矛盾的不同阶段,不同时刻是不能套用公式随意进行对抗的,我们应该给矛盾的双方留有余地,犯了错误的能及时改正,这时候过火的斗争就不合适了。 ","date":"2021-06-13","objectID":"/posts/read/20210613/:6:0","tags":["SS"],"title":"关于矛盾论","uri":"/posts/read/20210613/"},{"categories":["thinker"],"content":"小团队的话,管理者个人开发能力过关,再加上分配工作合理就能有不错的效果。再进一步就是把项目规划好,在项目进展中及时纠正错误问题(计划、设计、工作分配等),及时反馈问题(技术不可达),做好与其他部门的协作。这应该会是一个不错稳定的团队。 一个团队想要发展除了保持上面的内部优点,还需要多做一些对外的输出。将自己内部孵化的技术、文档、规范等推广到公司其他部门,这样的话团队不只是一个盈利点,还是能为公司整体带来更大收益面。那么如何才能对外输出呢?我现在能想到的只有技术相关,所以只说一下技术方面,比如良好的代码管理,良好的架构设计,业务的解耦,可以让很多技术点独立化,达到拿来就用,这样可以使全部的开发团队受益,而不必重复造轮子;良好的文档管理,文档规范,再加上刚才说的良好的代码,可以让其他团队迅速了解并进入我们的团队,达到团队之间的互通,使得技术松耦合,团队高内聚;如此我认为可以成为一个比较优秀的团队。 一个优秀的团队,可以为公司带来不只一方面的好处(只是完成一个项目的好处),还能带来上面说的为整体提升。也因此可以让公司发展更加健康(良好的人员结构,良好的技术基础)可以让产品迭代更新更迅速。这时候团队想做大,我认为主要依靠一个良性循环,公司因有好的团队变得健壮,团队因为公司的迅速成长,水涨船高变得越来越壮大。这时候需要把重点多分配到人员管理上,对员工有更好的福利,招聘更优秀的人,因为程序员做的不是体力活,人多不一定力量大。并且管理者也应该从开发前线,多拿出一些时间放到整体技术把控和人员管理上。这么说来,我认为一个大而强的团队除了主观因素(内部健康发展)还需要客观因素(公司整体素质)才可以吧。 ","date":"2021-06-10","objectID":"/posts/read/20210610/:0:0","tags":["SS"],"title":"管理什么的","uri":"/posts/read/20210610/"},{"categories":["thinker"],"content":"以下的读书方法针对的是以收集信息、获得判断为目的 为什有的人努力学习了却没有结果呢?因为我们大多数时候只是以过程为导向学习,而没有以目标为导向 ","date":"2021-03-03","objectID":"/posts/read/20210303/:0:0","tags":["SS"],"title":"提高读书效果","uri":"/posts/read/20210303/"},{"categories":["thinker"],"content":"1. 效率 - 明确目标 有目的性阅读,而非过程导向 比如商务类书籍不需要从头读到尾,根据目录、序言等摘取对自己有用的信息即可 时间意识 规范时间阅读,提高专注力 读书一定要有时间意识,才能提高效率。可以先确定读完需要多少时间,可以分成几次完成,每次需要花多少时间。比随机性的读到哪算哪有效率 选择性阅读 略读不重要的,细读重要的: 二八定律 ","date":"2021-03-03","objectID":"/posts/read/20210303/:0:1","tags":["SS"],"title":"提高读书效果","uri":"/posts/read/20210303/"},{"categories":["thinker"],"content":"2. 记忆 - 温故知新 笔记、圈重点、思维导图等 既然一本书的重要内容只有20%,那么通过做笔记可以帮助我们找到和巩固这20%的内容。 ","date":"2021-03-03","objectID":"/posts/read/20210303/:0:2","tags":["SS"],"title":"提高读书效果","uri":"/posts/read/20210303/"},{"categories":["thinker"],"content":"3. 行动 - 学以致用 量化行动: 大目标分阶段实现 分享知识 奖励: 阶段目标给自己一点奖励,增加行动力 ","date":"2021-03-03","objectID":"/posts/read/20210303/:0:3","tags":["SS"],"title":"提高读书效果","uri":"/posts/read/20210303/"},{"categories":["thinker"],"content":"原文 https://www.zhihu.com/question/27603479 ","date":"2021-03-03","objectID":"/posts/read/20210303/:0:4","tags":["SS"],"title":"提高读书效果","uri":"/posts/read/20210303/"},{"categories":["coder"],"content":"grpc Server 本文简单阅读源代码,了解grpc server的执行流程,从建立连接,到处理一条请求的过程。 ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:0:0","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"使用方式 使用方式很简单,生成pb,注册建立服务,就可以等待请求了 type Hello struct{ } func (h *Hello) Say(ctx context.Context, request pb.HelloRequest)(*pb.HelloResponse, error){ fmt.Println(request.Msg) return \u0026pb.HelloResponse{Msg: \"wwww\"}, nil } func main(){ lis, _ := net.Listen(\"tcp\", \"127.0.0.1:8888\") //1. 创建一个grpc服务器对象 gRpcServer := grpc.NewServer() //2. 注册pb函数 pb.RegisterHelloServiceServer(gRpcServer, \u0026Hello{}) //3. 开启服务端 //阻塞 gRpcServer.Serve(lis) } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:1:0","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"建立grpc server流程 ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:2:0","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"NewServer NewServer进行创建一个grpc服务,初始化一些参数。还可以进行函数选项模式,来传递初始化的配置。 默认情况下会建立一个以下参数的grpc服务: 接受数据最大4M 发送数据最大2g 连接超时120秒 读和写缓存1mb 默认一个请求一个goroutine numServerWorkers numServerWorkers设定了开启多少个工作协程,如果没设置,则来了一条消息就会处理创建一个goroutine。 如果设置了,会将请求消息进行分发给这多个worker func (s *Server) serveStreams(st transport.ServerTransport) { st.HandleStreams(func(stream *transport.Stream) { if s.opts.numServerWorkers \u003e 0 { data := \u0026serverWorkerData{st: st, wg: \u0026wg, stream: stream} select { case s.serverWorkerChannels[atomic.AddUint32(\u0026roundRobinCounter, 1)%s.opts.numServerWorkers] \u003c- data: default: go func() { s.handleStream(st, stream, s.traceInfo(st, stream)) wg.Done() }() } } else { go func() { defer wg.Done() s.handleStream(st, stream, s.traceInfo(st, stream)) }() } }) } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:2:1","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"注册函数 利用反射,将具体实现的结构体和与之对应的函数存储到 grpcServer的services变量中 key: 结构体名称 (一般在pb文件里会根据proto生成) value: 函数信息(调用函数的指针,函数名称,Metadata) 将函数信息存储后,来了一个请求根据请求信息,找到指定的函数,进行调用 type ServiceDesc struct { ServiceName string //服务名称 HandlerType interface{} //结构体类型 Methods []MethodDesc//一元函数 Streams []StreamDesc//流函数 Metadata interface{}// 元数据 } //注册服务函数 //args: sd 文件描述, srv: 具体实现的结构体 func (s *Server) RegisterService(sd *ServiceDesc, ss interface{}) { if ss != nil { ht := reflect.TypeOf(sd.HandlerType).Elem() st := reflect.TypeOf(ss) if !st.Implements(ht) { logger.Fatalf(\"grpc: Server.RegisterService found the handler of type %v that does not satisfy %v\", st, ht) } } s.register(sd, ss) } //利用反射将服务注册到字典 func (s *Server) register(sd *ServiceDesc, ss interface{}) { s.mu.Lock() defer s.mu.Unlock() s.printf(\"RegisterService(%q)\", sd.ServiceName) if s.serve { logger.Fatalf(\"grpc: Server.RegisterService after Server.Serve for %q\", sd.ServiceName) } if _, ok := s.services[sd.ServiceName]; ok { logger.Fatalf(\"grpc: Server.RegisterService found duplicate service registration for %q\", sd.ServiceName) } info := \u0026serviceInfo{ serviceImpl: ss, methods: make(map[string]*MethodDesc), streams: make(map[string]*StreamDesc), mdata: sd.Metadata, } for i := range sd.Methods { d := \u0026sd.Methods[i] info.methods[d.MethodName] = d } for i := range sd.Streams { d := \u0026sd.Streams[i] info.streams[d.StreamName] = d } s.services[sd.ServiceName] = info } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:2:2","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"监听 当客户端建立连接,会为其单独创建一个goroutine进行后续的数据传输 func Serve(lis net.Listener) error{ //... for { rawConn, err := lis.Accept() go func() { s.handleRawConn(rawConn) }() } //... } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:2:3","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"处理一个grpc请求的流程 建立连接 创建goroutine处理连接 grpc基于http2,根据tcp连接信息 创建http2 传输结构 newHTTP2Transport 创建新的goroutine,将http2传输信息 进行处理 经过http2_server.go:455中的处理,从tcp层读取数据进行解析,最后执行此处传递过去的函数指针 根据请求信息,找到指定函数。最后进行调用注册的应用层业务 找到字典里对应的执行函数 如果没找到,则判断unknownStreamDesc 执行,这个一般用来自定义路由 判断一元,还是流 执行函数 将结果写回给对方 移除连接 ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:3:0","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"code func Serve(lis net.Listener) error{ for { //1. 建立连接 rawConn, err := lis.Accept() go func() { //2. 创建goroutine处理连接 s.handleRawConn(rawConn) }() } } func (s *Server) handleRawConn(rawConn net.Conn) { conn, authInfo, err := s.useTransportAuthenticator(rawConn) // 3. grpc基于http2,根据tcp连接信息 创建http2 传输结构 st := s.newHTTP2Transport(conn, authInfo) go func() { //4. 创建新的goroutine,将http2传输信息 进行处理 s.serveStreams(st) //7. 移除连接 s.removeConn(st) }() } func (s *Server) serveStreams(st transport.ServerTransport) { //5. 经过http2_server.go:455中的处理,从tcp层读取数据进行解析,最后执行此处传递过去的函数指针 st.HandleStreams(func(stream *transport.Stream) { if s.opts.numServerWorkers \u003e 0 { data := \u0026serverWorkerData{st: st, wg: \u0026wg, stream: stream} select { case s.serverWorkerChannels[atomic.AddUint32(\u0026roundRobinCounter, 1)%s.opts.numServerWorkers] \u003c- data: default: go func() { s.handleStream(st, stream, s.traceInfo(st, stream)) wg.Done() }() } } else { go func() { defer wg.Done() s.handleStream(st, stream, s.traceInfo(st, stream)) }() } }) } func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Stream, trInfo *traceInfo) { //6. 根据请求信息,找到指定函数。最后进行调用注册的应用层业务 srv, knownService := s.services[service] if knownService { if md, ok := srv.methods[method]; ok { s.processUnaryRPC(t, stream, srv, md, trInfo) return } if sd, ok := srv.streams[method]; ok { s.processStreamingRPC(t, stream, srv, sd, trInfo) return } } // 此处可以进行自定义的路由 if unknownDesc := s.opts.unknownStreamDesc; unknownDesc != nil { s.processStreamingRPC(t, stream, nil, unknownDesc, trInfo) return } ... } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:3:1","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"解析请求头数据的细节 读取底层tcp数据,最后进行解析 解析头数据 根据解析后的头数据,进行一系列的设置 设置:超时的ctx 设置:metadata 存入context中 执行函数指针 http2从tcp将数据报转换成http2认识的具体数据。之后grpc将http2的数据封装成grpc用到的stream结构中,还有一些参数timeout、content-type等封装到stream中的ctx中,到这里为止还没有对具体的请求数据做任何操作。 type decodeState struct { serverSide bool //用了http2的解析,就一定是true data parsedHeaderData//请求过来的关键参数 } func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) { for { switch frame := frame.(type) { case *http2.MetaHeadersFrame: //1. 读取底层tcp数据,最后进行解析 if t.operateHeaders(frame, handle, traceCtx) { t.Close() break } case *http2.DataFrame: t.handleData(frame) case *http2.RSTStreamFrame: t.handleRSTStream(frame) case *http2.SettingsFrame: t.handleSettings(frame) case *http2.PingFrame: t.handlePing(frame) case *http2.WindowUpdateFrame: t.handleWindowUpdate(frame) case *http2.GoAwayFrame: // TODO: Handle GoAway from the client appropriately. default: if logger.V(logLevel) { logger.Errorf(\"transport: http2Server.HandleStreams found unhandled frame type %v.\", frame) } } } } //对解码后的报头进行操作 func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream), traceCtx func(context.Context, string) context.Context) (fatal bool) { //2. 解析头数据 if h2code, err := state.decodeHeader(frame); err != nil { return false } //grpc层的数据 s := \u0026Stream{ recvCompress: state.data.encoding, method: state.data.method, contentSubtype: state.data.contentSubtype, } //一个关键的ctx包含很多机制 s.ctx //3. 根据解析后的头数据,进行一系列的设置 //4. 设置:超时的ctx if state.data.timeoutSet { s.ctx, s.cancel = context.WithTimeout(t.ctx, state.data.timeout) } else { s.ctx, s.cancel = context.WithCancel(t.ctx) } //address pr := \u0026peer.Peer{ Addr: t.remoteAddr, } s.ctx = peer.NewContext(s.ctx, pr) //5. 设置:metadata 存入context中 if len(state.data.mdata) \u003e 0 { s.ctx = metadata.NewIncomingContext(s.ctx, state.data.mdata) } //6. 执行函数指针 handle(s) return false } func (d *decodeState) decodeHeader(frame *http2.MetaHeadersFrame) (http2.ErrCode, error) { ... for _, hf := range frame.Fields { d.processHeaderField(hf) } ... } func (d *decodeState) processHeaderField(f hpack.HeaderField) { switch f.Name { case \"content-type\": //如果类型 不包含 `application/grpc` 则抛异常,可以是`application/grpc;xxxx`等 contentSubtype, validContentType := grpcutil.ContentSubtype(f.Value) if !validContentType { d.data.contentTypeErr = fmt.Sprintf(\"transport: received the unexpected content-type %q\", f.Value) return } d.data.contentSubtype = contentSubtype d.addMetadata(f.Name, f.Value) d.data.isGRPC = true case \":path\": //依靠它找到需要调用的函数 比如/pb.TspService/Hello d.data.method = f.Value case \"grpc-timeout\": //如果有超时设置,会创建ctx context.WithTimeout(t.ctx, state.data.timeout) d.data.timeoutSet = true var err error if d.data.timeout, err = decodeTimeout(f.Value); err != nil { d.data.grpcErr = status.Errorf(codes.Internal, \"transport: malformed time-out: %v\", err) } default: //自定义的metadata在这里处理 d.addMetadata(f.Name, v) } } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:4:0","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"解析请求数据的细节 利用反射注册的函数,进行调用,参数传递,传递解析方式,但不会调用 调用到pb里注册的函数,在_TspService_Hello_Handler中进行具体处理 解析请求信息 调用拦截器 拦截器过滤后,进行最终的函数调用 源码 //最终解析在这里 func _TspService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(HelloRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TspServiceServer).Hello(ctx, in) } info := \u0026grpc.UnaryServerInfo{ Server: srv, FullMethod: \"/pb.TspService/Hello\", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TspServiceServer).Hello(ctx, req.(*HelloRequest)) } return interceptor(ctx, in, info, handler) } func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.Stream, info *serviceInfo, md *MethodDesc, trInfo *traceInfo) (err error) { df := func(v interface{}) error { //根据content-type 获取解析器,进行解析 if err := s.getCodec(stream.ContentSubtype()).Unmarshal(d, v); err != nil { return status.Errorf(codes.Internal, \"grpc: error unmarshalling request: %v\", err) } if sh != nil { sh.HandleRPC(stream.Context(), \u0026stats.InPayload{ RecvTime: time.Now(), Payload: v, WireLength: payInfo.wireLength + headerLen, Data: d, Length: len(d), }) } if binlog != nil { binlog.Log(\u0026binarylog.ClientMessage{ Message: d, }) } if trInfo != nil { trInfo.tr.LazyLog(\u0026payload{sent: false, msg: v}, true) } return nil } ctx := NewContextWithServerTransportStream(stream.Context(), stream) //利用反射进行函数调用 //info.serviceImpl函数, ctx函数的第一个参数(metadata等信息),df请求数据(protobuf的解析) //s.opts.unaryInt 拦截器 reply, appErr := md.Handler(info.serviceImpl, ctx, df, s.opts.unaryInt) opts := \u0026transport.Options{Last: true} //发送结果给请求方 if err := s.sendResponse(t, stream, reply, cp, opts, comp); err != nil { } return err } Metadata Metadata是在一次 RPC 调用过程中关于这次调用的信息。 是 key-value的形式。其中 key 是 string 类型, value是[]string。 Metadata 对于 gRPC 本身来说透明, 它使得 client 和 server 能为对方提供本次调用的信息。就像一次 http 请求的 RequestHeader 和 ResponseHeader,http header 的生命周期是一次 http 请求, Metadata 的生命周期则是一次 RPC 调用 //grpc的结构 type MD map[string][]string ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:5:0","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"使用 发送方 dd := metadata.Pairs(\"hello\", \"world\") ctx = metadata.NewOutgoingContext(ctx, dd) r, err := c.Hello(ctx, \u0026pb.HelloRequest{Msg: \"fff\"}) 接收方 func (s *server) Hello(c context.Context, p *pb.HelloRequest) (*pb.HelloResponse, error) { md, _ := metadata.FromIncomingContext(c) fmt.Println(md.Get(\"hello\")) } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:5:1","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"注意事项 metadata本意是用来描述调用的信息的:协议的格式、调用方的请求方式、参数、非业务相关信息等。数据相关的不要用metadata进行存储。这样可以在不进行解析传输数据的情况下,依靠metadata进行一些逻辑处理,比如根据metatdata判断数据解析的方式、一些中间服务根据metadata信息,进行面向服务的操作。 拦截器 拦截器(Interceptor) 类似于 HTTP 应用的中间件(Middleware),能够让你在真正调用 RPC 方法前,进行身份认证、日志、限流、异常捕获、参数校验等通用操作,和 Python 的装饰器(Decorator) 的作用基本相同。 客户端发起请求前做一些验证,服务端处理消息前做过滤 grpc服务端和客户端都提供了interceptor功能 client:发起请求前做统一处理 server:收到请求,进入具体执行函数之前,对请求做统一处理 调用方式类似链表、调用一个后再调用下一个节点 ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:5:2","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"grpc源码 func NewServer(opt ...ServerOption) *Server { //.. chainUnaryServerInterceptors(s) chainStreamServerInterceptors(s) //.. return s } func chainUnaryServerInterceptors(s *Server) { if s.opts.unaryInt != nil { interceptors = append([]UnaryServerInterceptor{s.opts.unaryInt}, s.opts.chainUnaryInts...) } var chainedInt UnaryServerInterceptor if len(interceptors) == 0 { chainedInt = nil } else if len(interceptors) == 1 { chainedInt = interceptors[0] } else { chainedInt = func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) { return interceptors[0](ctx, req, info, getChainUnaryHandler(interceptors, 0, info, handler)) } } s.opts.unaryInt = chainedInt } ","date":"2021-01-19","objectID":"/posts/2021/20210119-grpc/:5:3","tags":["grpc","源码"],"title":"grpc-go","uri":"/posts/2021/20210119-grpc/"},{"categories":["coder"],"content":"TCP/IP 模型 应用层将数据传递给传输层,传输层将数据分段,每段加入自己的首部数据,然后传递给下一层,之后的每层都会封装上自己层需要的首部,最后经过物理链路传递到指定主机,然后每层又向剥洋葱一样,一层层处理自己的首部数据,到达传输层,传输层最终交付给应用程序。 数据传输流程: ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:1:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"传输层 与应用层的关系:应用层将数据发送给传输层,传输层将其进行分段处理,每段数据加入头协议,然后将每组发出去 与网络层的关系:分组的数据发给网络层,网络层将数据真正的发给链路层,然后从物理链路发到指定服务的进程中去 举例: 北京一个家庭: 小明家庭 天津一个家庭: 小红家庭 小明写了封信,交给管家,管家将信给邮政局,邮政局收到后,寄到天津小红家,管家将信给小红; 小明家庭的任意成员写信-\u003e小明家的管家-\u003e邮局-\u003e小红家的管家-\u003e小红 应用层数据:小明信的内容 进程:小明,小明家庭成员都是这个主机内所有进程。 主机:小明的家庭 和 小红的家庭 传输层:管家,每个主机有一个管家 网络层:邮局 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:2:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"简单介绍udp tcp tcp将应用层数据,分段处理 称为 报文段(seg-ment);upd将应用层数据,分段处理 称为 数据报。 IP层 网际协议,为主机之间提供逻辑通信,尽力而为的交付服务(best-effort delivery service) 所以不保证数据不会丢失,报文段的顺序,并且附带了一个唯一表示的地址也就是ip地址来进行主机之间的确认。 udp和tcp 的基本责任是对两个端系统间的进程进行交付服务。主机间交付扩展到进程间交付被称之为 传输层的多路复用和多路分解。 并且都提供了差错检查字段,来对完整性进行校验。而udp也是不可靠服务,所以它仅提供了这两种服务:差错校验和进程到进程之间的交付。 tcp提供了可靠传输,通过流量控制,序号,确认,定时器等确保数据正确的,有序的到达接收进程;还提供了拥塞控制,调节网络流量速率,为整个互联网代理通用的好处,提供平等的带宽,这也是udp的传输速率高于tcp原因之一。 对于拥塞控制可以举个例子:拥塞控制如同交通规则,当车辆很多的情况下,大家都遵守交通虽然会降低一点开车速度。但对整个城市的交通提供了高效的运转,一辆辆tcp汽车,会公平的遵守交通达到目的地。而一辆udp汽车运行其中不遵守交规,在车辆少的情况下,是绝对高速的,但高峰期的时候绝对会出现各种事故(丢包率提高等) ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:3:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"多路复用,多路分解 多路复用:一个主机有多个进程,每个进程为了通信会建立一个套接字(socket),而一个主机只有一个传输层,所以多个socket将数据传输给传输层,传输层将这些数据进行封装上首部信息(为了以后的分解)从而生成报文段。然后将报文段传递给网络层,这个过程就是多路复用。 多路分解:当主机收到其他主机的数据,传输层根据报文段的首部信息找到指定的socket。这个过程就是多路分解 举例:依旧是上面的例子,当管家收到信件后,需要依靠信件上的名字,给指定的成员(小明,爸爸,妈妈)。 这个管家的操作就是多路分解。当成员(小明,爸爸,妈妈)写了信给管家,管家进行整理然后发送给邮政局,这个管家的操作就是多路复用 通过上述描述,传输层的多路复用的要求是: 每一个套接字要有唯一的标识,否则传输层无法分辨该将数据给谁 每个报文段要有特殊的字段来标识,要交付给哪个套接字(也就是端口 port) |-----------------| | 源端口 | 目的端口 | |-----------------| | 其他首部字段 | |-----------------| | 应用数据 | |-----------------| ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:4:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"udp的多路复用的多路复用/分解 一个udp套接字使用一个二元组来全面标识,该二元组包含:一个目的IP地址和一个目的端口号。这也是为什么多个客户端连会接到同一个服务进程的同一个套接字 //udp server func main() { listen, err := net.ListenUDP(\"udp\", \u0026net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 8080, }) if err != nil { fmt.Printf(\"listen failed, err:%v\\n\", err) return } fmt.Println(\"listen udp Start...:\") for { var data [1024]byte //读取UDP数据 count, addr, err := listen.ReadFromUDP(data[:]) if err != nil { fmt.Printf(\"read udp failed, err:%v\\n\", err) continue } fmt.Printf(\"data:%s addr:%v count:%d\\n\", string(data[0:count]), addr, count) //返回数据 _, err = listen.WriteToUDP([]byte(\"hello client\"), addr) if err != nil { fmt.Printf(\"write udp failed, err:%v\\n\", err) continue } } } ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:4:1","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"tcp的多路复用的多路复用/分解 一个tcp套接字使用一个四元组来全面标识,该四元组包含:一个目的IP地址和一个目的端口,一个源地址和一个源端口。所以每一个客户端连接都会维护一个套接字,一个tcp服务会维护多个套接字管理多个客户端连接 一个tcp server func main() { listen, err := net.Listen(\"tcp\", \"0.0.0.0:8888\") if err != nil { fmt.Println(\"listen failed, err:\", err) return } fmt.Println(\"listen tcp Start...:\") for { conn, err := listen.Accept() if err != nil { fmt.Printf(\"accept failed, err:%v\\n\", err) continue } go process(conn) } } func process(conn net.Conn) { defer conn.Close() for { var buf [128]byte n, err := conn.Read(buf[:]) if err != nil { fmt.Printf(\"read from conn failed, err:%v\", err) break } fmt.Printf(\"recv from client, content:%v\\n\", string(buf[:n])) } } ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:4:2","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"udp udp其实就是做了多路复用/多路分解,差错检查。除此之外没任何额外功能,和网络层唯一区别就是这点功能。由RFC 768定义的UDP只做传输层能够做的最少工作。 udp将数据附加上多路复用/分解服务的源和目的端口字段,以及其他两个小字段后,将报文段交给网络层。网络层将报文段封装到一个IP数据报中,然后尽力而为交给目的主机,到达后udp进行多路分解交付给指定的进程,在发送数据前并没有与接收方进行握手确认。所以udp被称为 无连接的 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:5:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"udp优点 关于何时,发送什么数据的应用层控制更为精细:tcp为了可靠传输不管交付用多长时间,udp不提供不必要的额外功能所以更快 无需建立连接:意味着不会有建立连接的延迟 无连接状态:tcp为了实现可靠传输会维护连接状态,udp没有,因此一般情况udp的能支持更多的用户量 分组首部开销小: tcp报文段20个字节的首部开销,udp8字节 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:5:1","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"udp报文段 首部四个字段,都是2字节,一共8字节 +--------+--------+--------+--------+ | Source | Destination | | Port | Port | +--------+--------+--------+--------+ | | | | Length | Checksum | +--------+--------+--------+--------+ | | | data | +----------------------------------- 源端口:源端口号。在需要对方回信时选用。不需要时可用全0 目的端口:目的端口号。这在终点交付报文时必须要使用到 长度: UDP用户数据报的长度,其最小值是8(仅有首部) 校验和:检测UDP用户数据报在传输中是否有错。有错就丢弃 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:5:2","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"udp校验和计算 校验和提供了差错检测功能。也就是说,报文段从源到达目的地的过程中,判断比特是否发生了改变(链路中的噪声干扰或者路由器的问题)。发送方的udp对报文段中的所有16比特字的和进行反码运算,求和时遇到溢出都会被回卷。得到的结果放到校验和字段checksum 举例: 字段 十进制 二进制 源端口 63549 1111100000111101 目的端口 12345 11000000111001 数据长度 17 10001 发送前计算校验和: 源端口+目的端口+数据长度 = 1111100000111101+0011000000111001+10001 = 10010100010000111 16位溢出,回卷,抛弃首位:10010100010000111 ==\u003e 0010100010000111 反码:0010100010000111==\u003e1101011101111000 校验和 = 1101011101111000 接受后检验校验和: 源端口+目的端口+数据长度 = 1111100000111101+0011000000111001+10001 = 10010100010000111 16位溢出,回卷,抛弃首位:10010100010000111 ==\u003e 0010100010000111 结果与校验和相加:0010100010000111(计算结果) + 1101011101111000(校验和) 如果结果不为 1111111111111111(16个1) 则一定有问题;如果结果为16个1,则表示可能没错(单比特翻转) 结论:校验和功能,不只是在传输层,在网络层,链路层也会做校验,每一层都会做自己首部的校验处理。虽然如此,依旧不能保证最终的无误。也会有几率检查不出来(单比特翻转) 概率虽然低,但依旧有可能。这时需要依靠应用层做最终的数据校验 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:5:3","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"可靠传输 可靠传输协议(reliable data transfer protocol)是使用一个个的技术点组合达到最终的可靠传输。 所以我们一个个了解,最终迭代出一个可靠传输的最终版本,当了解了这些可靠传输的每一个原理,就能在应用层用udp实现可靠传输了。 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:6:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"经完全可靠信道的可靠数据传输 rdt1.0 假设传输保证可靠,不丢包,不出差错,发送与接收端的收发效率也完全一致的时候模型 伪代码 发送端: loop: data = rcv_app() pkg=make_pkt(data) //添加首部 udt_send(pkg) //发送数据 接收端: loop: pkg = deliver_data(data) //从网络层获取报文段 data = extract(pkg) //解析首部 to_app(data) 完全不必加入额外的任何操作 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:6:1","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"能处理比特差错信道的可靠传输 rdt2.0 假设数据不会丢失,在发送数据的整条链路中,比特可能受损,在和别人打电话的时候,如果听到了对方完整的话,会说一句’ok’,来证明自己听到了全部内容,且听懂了。 这个‘ok’ 使用了 肯定确认(positive acknowledgment) 与否定确认(negative acknowledgment) (没听到说一句’请你再说一遍‘)。 当对方收到 肯定确认就开始说下一句了, 但当收到否定确认,就要重新说一遍刚才说的话。 基于上面的重传机制的可靠传输协议称之为 自动重传请求(AutomaticRepeat reQuest, ARQ)协议,此协议需要另外三种协议来处理存在的比特差错情况: 差错检测:和上面的udp一样 接收方反馈:收到的消息回答 肯定确认ACK 或者NAK。接收方需要向发送方发送一个报文段,其中一个字段只需要1bit,0(nak)或者1(ack) 重传:接收方收到有差错的分组时,重传刚才的报文段 伪代码 发送端: loop: data = rcv_app() *pkg = make_pkt(data, checksum) //添加首部 udt_send(pkg) //发送数据 *if isNAK(rcvpkg) *udt_send(oldpkg) //重传刚才的数据 接收端: loop: pkg = deliver_data(data) //从网络层获取报文段 data = extract(pkg) //解析首部 *if is_err(data, checksum) //收到消息并且是错误的 *udt_send(NAK) *else *udt_send(ACK) to_app(data) 由于发送方在没有收到回复的时候要一直等着,不能发送下一个报文段,因此这样的协议 被称为 停等(stop and wait)协议。 目前看起来是可行了,但ack和nak 出现了比特差错怎么办? 当接收方收到有差错的ack或者nak的时候,重传当前数据即可,这种方式 叫冗余分组 duplicate packet。但重传了冗余的数据,接收方因为无法确认,对方是否正确收到ack或者nak,所以不确定这次数据是新的,还是冗余的。这个解决方案有个简单方法(当前所有传输协议几乎都用这个方法)就是在数据分组里添加个新字段,对数据进行编号,将发送数据分组的序号(sequence number)放到这个字段。 在rdt2.0 的基础上添加个新字段 做 rdt2.1 来处理上面的问题。 当前rdt2.0是停等协议,所以无需让序号递增,只需要1个bit,区分本次数据和上次数据即可。 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:6:2","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"能处理比特差错信道的可靠传输 rdt2.1 在2.0的基础上,添加一个序号字段,用于区分发送方的数据是新的还是重发的。 伪代码 发送端: *seq = 0 loop: data = rcv_app() *seq = seq % 2 *pkg = make_pkt(data, checksum) //添加首部 udt_send(pkg) //发送数据 // 接收方回应nak,或者响应的结果有差错 *if isNAK(rcvpkg) || is_err(ack, checksum) *udt_send(oldpkg) //重传刚才的数据 *else *seq ++ 接收端: seq = 0 loop: *seq = seq % 2 pkg = deliver_data(data) //从网络层获取报文段 data = extract(pkg) //解析首部 *if is_err(data, checksum) //收到消息并且是错误的 *udt_send(NAK) *else if is_ok(data, checksum)\u0026\u0026 seq == rcvseq udt_send(ACK) *seq ++ to_app(data) \u0026else //如果接收方成功收取,但发送方ack有差错会一直重试,这里只需要一直响应ack即可 udt_send(ACK) ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:6:3","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"能处理丢包信道的可靠传输:rdt3.0 上面的例子都是在数据不丢失的情况下进行的。但现实中发送方发送一个数据段, 发送过程中可能会丢失,或者接收方收到后发送ask,这个ask丢失了,都会造成发送方无法及时响应。如果发送方能等足够长的时间确定丢失,则它只需要重传即可。 但发送方要等多久合适呢?最好的时间范围应该是这样:发送方–\u003e接收方,接收方—\u003e发送方,这两个时间,也就是往返延迟(RTT),但确定这个时间是难以估算的。等的太久造成延迟因此实践中采取的方法是发送方明智的选择一个时间值,判断可能发生了丢包(TCP会对RTT做实现) 发送方不确定丢失的原因,但结果一样,就是重传。为了实现在指定时间判断重传。需要一个 倒计数定时器(countdown timer),每次发送数据段: 启动一个定时器 处理定时器中断(正常的中断,或者超时的中断) 终止定时器 伪代码 发送端: seq = 0 loop: data = rcv_app() seq = seq % 2 pkg = make_pkt(data, checksum) //添加首部 *udt_send(pkg) \u0026\u0026 start_timer() //发送数据 // 接收方回应nak,或者响应的结果有差错 if isNAK(rcvpkg) || is_err(ack, checksum) udt_send(oldpkg) //重传刚才的数据 else seq ++ stop_timer() if timer reach udt_send(oldpkg)\u0026\u0026 start_timer() ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:6:4","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"流水线可靠传输协议 rdt4.0 现在我们不怕丢失数据,不怕数据有差错了,但效率很低,因为是停等协议,一个个数据处理,所以这次将是在上面的基础上,改进为流水线的处理方式。 要做到这个需要: 每个分组必须有一个唯一序号 做区分,之前的停等一次只可能一个分组,如果有差错重发,重发上一次的分组,所以只要能区分本次和上次即可,所以序号只需要一个bit,标识此分组是重发的还是新的 最低限度发送方需要缓存发送成功但没收到确认的分组序号,以便进行重新发送 为了实现以上两点,达到流水线操作,引入了回退N步 回退n步 回退N步协议中,允许发送多个分组,而无需等待。但也受限于最大的允许数N。 base:第一个未确认的分组 nextseqnum:最小的未使用的序号(也就是待发送的序号) N:窗口长度,所以gbn也称为 滑动窗口协议(sliding-window protocol) 伪代码 发送: //当小于窗口总长度才进行发送 if(nextseqnum\u003c base+N){ //压缩 sndpkt[nextseqnum] = make_pkt(nextseqnum, data, checksum) //发送 udt_send(sndpkt[nextseqnum]) //如果是窗口的第一个分组,怎启动定时器 if(base == nextseqnum) start_timer() nextseqnum++ }else{ //阻塞不发 } 定时器的作用: if timeout{ start_timer() //重开一个定时器 udt_send(sndpkt[base]) udt_send(sndpkt[base+1]) ... udt_send(sndpkt[nextseqnum-1]) } 收到接收方的回应: //当消息无差错 if(rdt_rcv(pkt) \u0026\u0026 notcorrupt(pkt)){ base = getacknum(pkt) + 1 // 滑动窗口起点向前移动 if (base == nextseqnum) //如果 stop_timer() else start_timer() }else{//有错误 重新发送所有窗口内未确认的分段 } 这个发送方缓存了窗口大小N的序号,接收方没做任何处理,只有在有序和无差错的情况下才回复ask,否则直接丢弃。 好处是接收方非常简单,缺点是 第一个分段的异常,会导致后面全部分段重新发送 选择重传 在回退N步的基础上,选择重传做了优化,也就是只重发有差错或者超时的分组。 为了实现这个机制需要具备以下条件 每个分组有自己的独立定时器(回退N步窗口里的分组公用一个) 接收方也需要有个窗口缓存已收到的分组序号 回退N步会将无序的分组也丢弃, 而现在需要将无序的暂时缓存起了,等待中间未到达的分组收到后,再传给上层调用方 发送方和接收方对窗口的移动需要同步 接收方收到的分组序号有以下几种可能: 和窗口起始位置相同,则将窗口进行向前移动,如果前移后又是一个已确认的(无序的缓存),则继续前移,直到找到第一个未收到的分组 收到一个中间序号,缓存起来 收到一个小于窗口起点的序号,返回ack 发送方有以下可能: 定时器超时 重发此分组 收到发送方的ack序号,对此序号标记为已完成,如果是base则窗口向前移动,如果前移后又是一个已确认的(无序的缓存),则继续前移,直到找到第一个未收到的分组 如果接收方发送了ack,但发送方丢失或者差错,则重发或者超时发 因为接收方的ack响应也可能丢包或者差错,所以发送方会重发,又因为接收方的第三点,导致总能重新返回ack,这一点是为了防止发送方的无限制重新发送. 总结: 利用校验和检查比特错误,定时器用于 超时/重传,序号保证按序发送数据,ack,nak响应是否接受成功 实现可靠传输,窗口流水线可以让多个分组同时发送,提高效率。 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:6:5","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"tcp连接 tcp是面向连接(connection-orientend)的可靠传输。因为一个应用进程开始向另一个应用进程发数据前,两个进程必须先互相握手,需要预备某些报文段,确立传输的参数,和一些与连接相关的状态变量。 这些状态是存在于传输层的,中间的网络层,链路层等不会知道这些细节,它们只知道数据报文,不知道什么是连接。 tcp连接提供的是全双工服务(full duplex service) 这表示,数据从a服务流向b服务的同时,b服务的数据也可以流向a。 tcp连接也是点对点(point to point),在一个连接下,发送方只能给相对应的接收方,不能给多个接收方。 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:7:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"tcp报文段结构 源端口与目标端口:分别写入源端口号和目标端口号. 32位序列号:也就是我们tcp三次握手中的seq,表示的是我们tcp数据段发送的第一个字节的序号,范围[0,2^32 - 1],例如,我们的seq = 201,携带的数据有100,那么最后一个字节的序号就为300,那么下一个报文段就应该从301开始. 32位确认序列号:也就是ack(假设为y),它的值是seq+1,表示的意义是y之前的数据我都收到了,下一个我期望收到的数据是y.也就是我回过去的seq = y. 首部长度:占4位.也叫数据偏移,因为tcp中的首部中有长度不确定的字段. URG:紧急指针标志位,当URG=1时,表明紧急指针字段有效.它告诉系统中有紧急数据,应当尽快传送,这时不会按照原来的排队序列来传送.而会将紧急数据插入到本报文段数据的最前面. ACK:当ACK=1时,我们的确认序列号ack才有效,当ACK=0时,确认序号ack无效,TCP规定:所有建立连接的ACK必须全部置为1. PSH:推送操作,很少用,没有了解. RST:当RST=1时,表明TCP连接出现严重错误,此时必须释放连接,之后重新连接,又叫重置位. SYN:同步序列号标志位,tcp三次握手中,第一次会将SYN=1,ACK=0,此时表示这是一个连接请求报文段,对方会将SYN=1,ACK=1,表示同意连接,连接完成之后将SYN=0 FIN:在tcp四次挥手时第一次将FIN=1,表示此报文段的发送方数据已经发送完毕,这是一个释放链接的标志. 16位窗口的大小:win的值是作为接收方让发送方设置其发送窗口大小的依据. 紧急指针:只有当URG=1时的时候,紧急指针才有效,它指出紧急数据的字节数 序号和确认号 因为是全双工服务,并且tcp把数据看做一个无结构的,有序的字节流,所以序列号seq是字节流的编号。比如5000byte的数据,tcp根据MSS为1000byte,那么tcp会划分为5个报文段,0~999,1000~1999…; 那么主机A请求发出去,seq表示自己发送的数据,而ack表示期待从B获得79 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:7:1","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"RTO的计算(Retransmission-TimeOut)即重传超时时间 可靠传输中需要计算一个RTT,而这个RTT不能太长,也不能太短。为此官方给出了以下计算: 并不是每次都重新算一次RTT,而是每隔一段时间 重传的数据不做统计 将多次的RTT取平均 根据上面的条件,计算出一个 SampleRTT,简单的往返时间。但不能直接使用,需要再利用这个公式做最后的计算: EstimatedRTT = (1 -a)* EstimatedRTT * SampleRTT 也就是新的EstimatedRTT,是由老的EstimatedRTT,计算得来。其中a=0.875。 这样就估算出了最终的EstimatedRTT时间,但如果知道rtt变化的一个范围也是有价值的。 DevRTT= (1 -b)DevRTT+b abs(EstimatedRTT - SampleRTT) ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:7:2","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"TCP的状态机 网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。 握手 第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。 第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。 从客户端的视角来看,我接到了服务端发送过来的响应数据包,说明服务端接收到了我在第一次握手时发送的网络包,并且成功发送了响应数据包,这就说明,服务端的接收、发送能力正常。而另一方面,我收到了服务端的响应数据包,说明我第一次发送的网络包成功到达服务端,这样,我自己的发送和接收能力也是正常的。 第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。 第一、二次握手后,服务端并不知道客户端的接收能力以及自己的发送能力是否正常。而在第三次握手时,服务端收到了客户端对第二次握手作的回应。从服务端的角度,我在第二次握手时的响应数据发送出去了,客户端接收到了。所以,我的发送能力是正常的。而客户端的接收能力也是正常的 具体操作: 客户端发送一个SYN段,并指明客户端的初始序列号,即ISN(c). 服务端发送自己的SYN段作为应答,同样指明自己的ISN(s)。为了确认客户端的SYN,将ISN(c)+1作为ACK数值。这样,每发送一个SYN,序列号就会加1. 如果有丢失的情况,则会重传。 为了确认服务器端的SYN,客户端将ISN(s)+1作为返回的ACK数值 对于建链接的3次握手: 三次握手才可以阻止重复历史连接的初始化(主要原因) 三次握手才可以同步双方的初始序列号 三次握手才可以避免资源浪费 挥手 因为TCP是全双工通信的 第一次挥手 :因此当主动方发送断开连接的请求(即FIN报文)给被动方时,仅仅代表主动方不会再发送数据报文了,但主动方仍可以接收数据报文。 第二次挥手:被动方此时有可能还有相应的数据报文需要发送,因此需要先发送ACK报文,告知主动方“我知道你想断开连接的请求了”。这样主动方便不会因为没有收到应答而继续发送断开连接的请求(即FIN报文) 第三次挥手 :被动方在处理完数据报文后,便发送给主动方FIN报文;这样可以保证数据通信正常可靠地完成。发送完FIN报文后,被动方进入LAST_ACK阶段(超时等待)。 4. 第四挥手: 如果主动方及时发送ACK报文进行连接中断的确认,这时被动方就直接释放连接,进入可用状态。 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:7:3","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"流量控制与拥塞控制 TCP为它的应用程序提供了流量控制服务(flow control service),以消除发送方使接收方数据溢出的可能性。 流量控制因此是一种速度匹配模块,发送方的发送速率与接收方应用程序的读取速率相匹配,另一种控制发送方速度的方式是拥塞控制(congestion control),但是这两者是不同的: 流量控制基于对端的窗口大小来调整发送方的发送速度 拥塞控制基于IP网络的速度来调整发送方的发送策略 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:8:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"流量控制 由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。发送放的窗口 swnd 和接收方窗口 rwnd client端的可用窗口大小为360字节。client发送140字节数据到server,其中seq=1,length=140;发送之后,client的可用窗口向右移动140字节,窗口总大小还是360字节。 server端的可用窗口大小为360字节。收到client发来的140字节数据之后,server端接收窗口向右移动140字节,但是由于应用程序繁忙,只取出了其中的100字节,因此server在ACK的时候,可用窗口还剩360-100=260字节,ACK=141。 client在接收到server的ACK=141报文之后,发送窗口左边缘向右移动140字节,表示前面发送的140字节server已经接收到了。剩下的260字节,由于server端告知窗口大小为260字节,client调整自己的发送窗口为260字节,表示此时不能发送大于260字节的数据。 client发送180字节,可用窗口变成80(260-189)字节。 5 server收到client发送的180字节,放入buffer中,这时应用程序还是很繁忙一个字节都没有处理,因此这一次应答回客户端ACK=321(140+180+1),窗口大小为80(260-180)。 client收到server的确认应答,确认了第二次发送的180字节已经被server端收到,于是发送窗口左边缘向 前移动了180字节。 client发送80字节,可用窗口变成0(80-80)。 server收到了80字节,但是这一次应用程序还是一个字节都没有从buffer中取出处理,因此server应答client端ACK=401(140+180+80+1),窗口大小为0(80-80)。 client收到确认包,确认之前发送的80字节已经到达server端。另外server端告知窗口大小为0,因此client无论是否有数据需要发送,都不能发送了 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:8:1","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"拥塞控制 拥塞流量 拥塞窗口 cwnd 变化的规则: 只要网络中没有出现拥塞,cwnd 就会增大; 但网络中出现了拥塞,cwnd 就减少 其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。 防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:慢开始、拥塞避免、快重传、快恢复。 过程: cwnd \u003c ssthresh 用慢开始 cwnd \u003e= ssthresh 停止慢开始,改为拥塞避免 cwnd = ssthresh 慢开始和拥塞避免都可以 在tcp双方建立关系后, 拥塞窗口cwnd的值被设置为1,还需设置慢开始门限ssthresh(65535字节大小),在执行慢开始算法时,发送方每收到一个对新报文段的确认时,就把拥塞窗口cwnd的值比上一次*2,每次都是指数倍增长。然后开始下一轮的传输,当拥塞窗口cwnd增长到慢开始门限值时,就使用拥塞避免算法 慢启动 TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗? 当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1 假设当前发送方拥塞窗口cwnd的值为1,而发送窗口swnd等于拥塞窗口cwnd,因此发送方当前只能发送一个数据报文段(拥塞窗口cwnd的值是几,就能发送几个数据报文段),接收方收到该数据报文段后,给发送方回复一个确认报文段,发送方收到该确认报文后,将拥塞窗口的值变为2, 发送方此时可以连续发送两个数据报文段,接收方收到该数据报文段后,给发送方一次发回2个确认报文段,发送方收到这两个确认报文后,将拥塞窗口的值加2变为4,发送方此时可连续发送4个报文段,接收方收到4个报文段后,给发送方依次回复4个确认报文,发送方收到确认报文后,将拥塞窗口加4,置为8,发送方此时可以连续发送8个数据报文段,接收方收到该8个数据报文段后,给发送方一次发回8个确认报文段,发送方收到这8个确认报文后,将拥塞窗口的值加8变为16 当前的拥塞窗口cwnd的值已经等于慢开始门限值,之后改用拥塞避免算法。 拥塞避免 也就是每个传输轮次,拥塞窗口cwnd只能线性加一,而不是像慢开始算法时,每个传输轮次,拥塞窗口cwnd按指数增长。同理,16+1……直至到达24,假设24个报文段在传输过程中丢失4个,接收方只收到20个报文段,给发送方依次回复20个确认报文段,一段时间后,丢失的4个报文段的重传计时器超时了,发送发判断可能出现拥塞,更改cwnd和ssthresh.并重新开始慢开始算法 就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。 这时候触发重传机制 快速重传 还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。 TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下: cwnd = cwnd/2 ,也就是设置为原来的一半; ssthresh = cwnd; 进入快速恢复算法 快速恢 cwnd = cwnd + 3 MSS,加3 MSS的原因是因为收到3个重复的ACK。 重传重复ACK(duplicate ACK)指定的数据包。 如果再收到重复ACK,cwnd递增1。 如果收到新的ACK,表明重传的报文已经收到。此时将cwnd设置为ssthresh值,进入拥塞避免状态 ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:8:2","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"关于数据包的最大值确定 UDP和TCP协议利用端口号实现多项应用同时发送和接收数据。数据通过源端口发送出去,通过目标端口接收。有的网络应用只能使用预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为UDP和TCP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。动态端口的范围是从1024到65535。 MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64Bytes最大不能超过1518Bytes,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。 由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。 链路层帧的大小 1500(不包括帧头、帧尾): UDP 包的大小就应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes) TCP 包的大小就应该是 1500 - IP头(20) - TCP头(20) = 1460 (Bytes) ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:9:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"参考 《计算机网络自顶向下方法》 https://andrewpqc.github.io/2018/07/21/tcp-flow-control-and-congestion-control/ ","date":"2021-01-05","objectID":"/posts/2021/20210105-tcpudp/:10:0","tags":["网络"],"title":"计算机网络-传输层","uri":"/posts/2021/20210105-tcpudp/"},{"categories":["coder"],"content":"Context有什么用 当处理一个请求A,而这个请求需要在3秒内完成相应,A请求分别创建了B和C goroutine来处理逻辑,如果B或者C处理时间过长超过了3秒,那么继续执行显然是没必要且浪费资源。这时候就需要一个能终止他们的操作,而go没有提供类似 goroutineID这样的变量来记录goroutine状态。官方认为这样非常容易被滥用。所以Context就为此而来。 利用 channel/select ,以信号的方式来通知需要停止的goroutine 可以为Context记录一个key/value 来包含一些请求相关的信息 func B(ctx context.Context) error { for { select { case \u003c-time.After(1 * time.Second): fmt.Println(\"hello B\") case \u003c-ctx.Done(): fmt.Println(\"b is end\") return ctx.Err() } } } func C(ctx context.Context) error { for { select { case \u003c-time.After(1 * time.Second): fmt.Println(\"hello C\") case \u003c-ctx.Done(): fmt.Println(\"b is end\") return ctx.Err() } } } func main() { //创建一个有取消机制的context ctx, cancle := context.WithCancel(context.Background()) //创建两个goroutine每秒打印一句话 go B(ctx) go C(ctx) //5秒后发出取消信号,停止B,C time.Sleep(5 * time.Second) cancle() fmt.Println(\"end\") } ","date":"2020-04-01","objectID":"/posts/2020/20200401-go-context/:1:0","tags":["go","源码"],"title":"源码阅读 - go Context","uri":"/posts/2020/20200401-go-context/"},{"categories":["coder"],"content":"源码分析 //context.go type Context interface { Deadline() (deadline time.Time, ok bool) Done() \u003c-chan struct{} Err() error Value(key interface{}) interface{} } context包对外提供了5个api: //返回一个有取消 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func Background() Context func TODO() Context type emptyCtx int 官方实现了一个默认结构,其实现的每一个api都不做任何逻辑,都返回空值。 还实现了String函数来打印实例名称。这个结构体虽然不做任何操作,但却非常重要, emptyCtx实例出background和todo 对外提供Background() 和TODO() 这两个实例除了名称不同,其他都一模一样。对于此代码里有官方注释 // TODO returns a non-nil, empty Context. Code should use context.TODO when // it's unclear which Context to use or it is not yet available (because the // surrounding function has not yet been extended to accept a Context // parameter). func TODO() Context { return todo } 每一个Context都可以根据这三个api派生出n个子context。 对于派生子context,是一个树状结构,最初由根节点(比如backgroud),不断用提供的with api创建出一个个子context,每一个子contetxt又能创建出n个子context。 当对一个context进行打印: 他的打印顺序是对String()做递归操作从根节点开始到自身将所有String()返回的字符串拼接 Value() 也是一个递归操作,从当前节点开始判断key是否相同,是则返回结果,否就查询父节点,直到找到结果,或查询到根节点返回nil WithCancel/Timeout/deadline本质上都是一样的操作,都返回了一个cancelFunc,执行这个函数指针,可以发起一个停止信号,停止所有child context timeout达到指定时间间隔执行停止信号,deadline到达某一时间点执行停止信号 timeout也是调用了deadline函数 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } 关键说说WithCancel 他的执行顺序是 先找到父节点中最近的那个为cancel或者timer的ctx,如果没找到则表示当前节点就是第一个cancel类型ctx,创建个goroutine来等待父节点的cancel func parentCancelCtx(parent Context) (*cancelCtx, bool) { for { switch c := parent.(type) { case *cancelCtx: return c, true case *timerCtx: return \u0026c.cancelCtx, true case *valueCtx: parent = c.Context default: return nil, false } } } 如果找到了父节点,就把当前节点加入到父节点的字典里,以便父节点控制全部子节点,key是ctx地址,value是struct{},struct{}不占字节所以这样写,也相当于set结构体。 这里加了锁,是为了保障在多个goroutine中对同一个cxt做with操作,防止race 最后无论是定时器的达到时间,还是主动取消,都是相同的操作。 func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic(\"context: internal error: missing cancel error\") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err if c.done == nil { c.done = closedchan } else { close(c.done) } for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } } 会给done写入,这样就让所有在select Done()的地方都触发, 之后会对当前ctx全部child做同样操作,这里做了加锁操作,也是为了防止多个goroutine里对同一个ctx执行cancel ","date":"2020-04-01","objectID":"/posts/2020/20200401-go-context/:2:0","tags":["go","源码"],"title":"源码阅读 - go Context","uri":"/posts/2020/20200401-go-context/"},{"categories":["coder"],"content":"总结 只用cancelCtx,emptyCtx,timerCtx三个结构,简洁的代码实现了一个 goroutine之间的上下文。 对于打印和value() 操作的是当前-根节点的ctx context利用channel当做信号对多goroutine之间发起cancel操作 type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time } ","date":"2020-04-01","objectID":"/posts/2020/20200401-go-context/:3:0","tags":["go","源码"],"title":"源码阅读 - go Context","uri":"/posts/2020/20200401-go-context/"},{"categories":["thinker"],"content":"第一章 简要 一个叫 何塞·阿尔卡蒂奥·布恩迪亚 的人,从吉卜赛人 梅尔吉亚德斯,一个诚实善良的人手中换来各种新奇未见过的道具:磁铁,放大镜等。 他天马行空,爱钻研,专注,却又有些愚蠢,这些道具在他手中没有按他所想如意,结果让自己变得抑郁寡欢,曾经的他带领人们寻找安家之所,他和妻子是全村的楷模,如今痴迷于科学,变得慵懒不修边幅。但他却根据吉普赛人的知识,自己研究出了 地球是圆的,在当时难以置信的超前想法。没多久他的好友梅尔吉亚德斯病逝,之后来了 另一批吉普赛人,他第一次见到,抚摸到冰。 人物关系 何塞·阿尔卡蒂奥·布恩迪亚 妻子 乌尔苏拉·伊瓜兰 大儿子 小何塞·阿尔卡蒂奥 小儿子 何塞·奥雷里亚诺·布恩迪亚 吉普赛人 好友 买卖道具 梅尔吉亚德斯 第二章 简要 何塞与乌尔苏拉是表亲,不敢生子,怕出畸形,却被村中的普鲁邓希奥·阿基拉尔耻笑,一怒之下何塞用长矛杀死了他。也因此不再惧怕生子,但良心的谴责,被迫他们远离村子,去其他地方。何塞的很多朋友也因探险欢呼加入他们。就这样奔波几个月,建立了马孔多。 大儿子 何塞·阿尔卡迪奥 鸡鸡很大,他年纪不大的时候和 庇拉尔·特尔内拉 发生关系。这女人是和她母亲年龄相仿,曾参加建立马孔多远征的人。 与她在一起,他感到担忧又充满欲望,并将此事告知弟弟。不久特尔内拉告诉他:“你要当父亲了”,他才从幻梦中惊醒,不再想情爱,而是担忧,想脱离现实。 在吉普赛人来到村子,他又与一位年龄很小,很美的吉普赛人一见钟情,发生关系,并随吉普赛人偷偷离去,远离家乡。 儿子失踪,母亲连忙追寻,父亲依旧炼金,小儿子 奥蕾里亚诺知道缘由,反感特尔内拉。 母亲在失踪几月后,归来。儿子没找到,却扎到了丈夫在失败的远征中没能发现的通向伟大发明的道路。和他们一样的村民,这些村民比他们生活的更好,见惯了各样改善生活的机器。 言语 何塞·阿尔卡掉:“我想和你单独在一起,总有一我要把这一切告诉所有人,用不着躲躲藏藏。” 特尔内拉:“那太好了,要是能单独在一起,我们就可以点亮灯,互相能看见,而且我想怎么叫就怎么叫,不用管别人,你在我耳边想说什么就说什么。” 人物 环境 三女儿 阿玛兰妲 马孔多 何塞·阿尔卡蒂奥·布恩迪亚,带领人安札定居的村落 庇拉尔·特尔内拉 大儿子的情人 第三章 简要 特尔内拉生了儿子,何塞阿尔卡蒂奥布莱迪恩 坚持抚养,名叫 阿尔卡迪奥。 家里收留了 比西塔西翁姐弟,来照料孩子和家务。他们是为了逃避部落的失眠症来到了马孔多。 过了不久,几位皮草商人,带来了一位孤儿交给何塞一家,她是乌尔苏拉的挚友 尼卡诺尔·乌略亚和妻子丽贝卡·蒙铁尔 的女儿。此女一直行为古怪,过了很久才融入家里,不在那么古怪。但不久失眠症袭击了整个村落,全民无法入眠,并且记忆力越来越差,这时梅尔吉亚德斯来了,他用一种药水拯救了村子。他没死,难忍孤独的他又重返人间,和何塞重温昔日友情。 奥利里亚诺想买下妓女,有欲望有怜悯,有冲动,想保护她,当他作出决定去卡塔里诺店时,姑娘已经离开了镇子。他感到挫败,用工作逃避,决心远离女人来遮掩自己无能带来的羞耻。梅尔吉亚德斯语言马孔多会变成一座光明的城市,矗立着玻璃建造的高楼大厦,却不再有布恩迪亚家的丝毫血脉。乌尔苏拉设计并找人建造了马孔多最大的房子,来为家人居住,但镇里的里正下令要求 房子必须刷蓝色漆。何塞和马孔多的创建者们决定驱逐外来者,不该由外人管理马孔多。 何塞看在摩斯科特家人的份上,和平解决。奥蕾莉亚诺看见里正的9岁女儿蕾梅黛丝后就爱上了她。 言语 “我宁愿掂起一个活人,也不愿后半辈子都惦着一个死人” “但她的影子正折磨着他身体的某个部位,那是一种肉体上的感觉,几乎在他行走时构成障碍,就像鞋子里进了一粒小石子” 人物 里正 堂阿波利纳尔·摩斯科特 政府派来监管 马孔多的官 安帕萝 是 堂阿波利纳尔·摩斯科特女儿 16岁 蕾梅黛丝 是 堂阿波利纳尔·摩斯科特女儿 9岁 丽贝卡 一位孤儿 她是乌尔苏拉的挚友尼卡诺尔·乌略亚和妻子丽贝卡·蒙铁尔 的女儿 比西塔西翁姐弟 何塞家的佣人 阿尔卡蒂奥·何塞 长子何塞阿尔卡蒂奥和特尔内拉的孩子 第四章 简要 新家建好,乌尔苏拉订购了很多家具,还有一台自动钢琴,一位意大利技师 皮埃特罗·克雷斯皮调试,安装钢琴,还教贝丽卡和阿玛兰妲跳舞,两女都爱上了他。贝丽卡对他的思念让她再次涌现出童年爱吃土的习惯,奥雷里亚诺此时对蕾梅黛丝的爱意愈加强烈,家中弥漫爱情瘟疫。奥雷里亚诺和马格尼菲科·比斯巴勒和豪里内勒多·马尔克斯去了卡塔里诺的店里寻欢,之后与庇拉尔·特尔内拉发生了关系,特尔内拉叶了解的奥雷里亚诺的痛苦,保证会帮他实现。当家里人知道了孩子们被爱所困扰,做出了妥协,去堂阿波利纳尔·摩斯科特家提亲,贝丽卡成了克雷斯皮的未婚妻,但阿玛兰妲憎恨这件事,发誓阻止他们的婚事。 梅尔吉亚德斯去世了,死前说“我已达到了永生”。全村为他举行了隆重的葬礼。何塞开始抛弃炼金研究起了 克雷斯皮带给他的神奇的机械玩具。而特尔内拉怀孕了,是奥雷里亚诺的孩子。何塞不眠不休的研究机械,在一天失眠的夜里看见了 普鲁邓希奥·阿基拉尔,这个当年被他杀死的男人。他们不再是冤家,而一直聊到天亮。但之后何塞向中了邪一样,语无伦次还捣毁家里,最后被人捆绑在树上。 人物 皮埃特罗·克雷斯皮 贝丽卡未婚夫,意大利技师 马格尼菲科·比斯巴勒和豪里内勒多·马尔克斯 马孔多早起创建者们的孩子 奥雷里亚诺·何塞 奥雷里亚诺和特尔内拉的儿子 第五章 简要 奥蕾里亚诺和雷梅黛丝 结婚,雷梅黛丝很受他们的欢迎,有她在乌尔苏拉家变得欢快起来,本来丽贝卡和皮埃特罗也要一起结婚的。可因收到一封母亲病危的假信,错过了婚礼。尼卡诺尔·雷伊纳神甫主持的婚礼,并且之后他想在这里筹钱造教堂,还想为何塞传福音让他信教,不过失败了。家人决定教堂建好再重新筹划丽贝卡的婚礼。阿玛兰坦百般阻挠婚礼失败,最后决定下毒。雷梅黛丝不慎误食毒药死去。家中所有人都很难过包括阿玛兰坦,受到了良心的谴责。婚礼也就一直延误下去。没过多久,大儿子何塞·阿尔卡蒂奥回来了,经过千锤百炼,受尽风吹雨打,身体强如刚铁,力气比十多个人加起来都大。不顾家人反对阿尔卡蒂奥与丽贝卡成了亲,离开家,在村落租了个小屋。除了奥蕾莉亚诺没人关心他们。阿玛兰妲也与皮埃特罗定下婚约。奥雷里亚诺与堂阿波利纳尔·摩斯科特经常相约玩多米诺,关系甚好。当保守派与自由派准备开战,一位村中的医生,阿利黎奥·诺格拉开始教唆村中的青年追寻自由派。一开始奥利里亚诺是拒绝的,医生死后,他成了村中自由派的领导,奥雷里亚诺上校。 人物 尼卡诺尔·雷伊纳 神甫 从大泽区请来做婚礼主持的。 阿利黎奥·诺格拉 马孔多的医生 ","date":"2020-02-10","objectID":"/posts/2020/20200210-book-%E7%99%BE%E5%B9%B4%E5%AD%A4%E7%8B%AC/:0:0","tags":null,"title":"百年孤独","uri":"/posts/2020/20200210-book-%E7%99%BE%E5%B9%B4%E5%AD%A4%E7%8B%AC/"},{"categories":["thinker"],"content":"第六章 简要 奥雷里亚诺临行出战前,将马孔多托付给阿尔卡蒂奥·何塞 。当阿尔卡蒂奥掌权,开始征兵,武装村落,成为马孔多最残酷的统治者。乌尔苏拉得知后,鞭打他,最后村子由她做主。阿玛兰妲的温柔赢得了皮埃特罗的爱,但当皮埃特罗求婚时,阿玛兰妲冷酷的拒绝了。皮埃特罗瞬间崩溃,在多日的祈求与求爱无果后割腕自杀了。阿尔卡蒂奥显出少有的慷慨,下令全镇为他守丧。阿尔卡蒂奥见到庇拉尔·特尔内拉后回忆起了过往,又想与她肌肤之爱,对方假意接受,到了晚上花钱让桑塔索菲亚·德拉·彼达成了她的替身。之后他们俩便互相纠缠了,并生下了一个女儿,之后又怀孕了。之后阿尔卡蒂奥又开始征用土地,收取租金下葬等费用,没过多久奥雷里亚诺派了格雷戈里奥·史蒂文森上校来镇里报信,自由派危在旦夕,希望村里放弃抵抗投降,换取安全但阿尔卡蒂奥拒绝了。之后保守派来到攻打村落,激烈的战争,双方伤亡惨重,保守党获胜。而最后阿尔卡蒂奥处刑枪决。死之前给女儿起名乌尔苏拉,如果二胎是男孩就叫何塞·阿尔卡蒂奥,用祖父母的名字。 人物 桑塔索菲亚·德拉·彼达 阿尔卡蒂奥·何塞 的情人,父母是开日用品店的 言语 阿玛兰妲的善解人意 ,以及不失分寸又包容一切的温柔,织起一副无形的网罗把男友围在其中,他不得不用自己未戴戒指的苍白手指生生剥开,才能在八点时告辞离去 ","date":"2020-02-10","objectID":"/posts/2020/20200210-book-%E7%99%BE%E5%B9%B4%E5%AD%A4%E7%8B%AC/:0:1","tags":null,"title":"百年孤独","uri":"/posts/2020/20200210-book-%E7%99%BE%E5%B9%B4%E5%AD%A4%E7%8B%AC/"},{"categories":["coder"],"content":"虚拟内存系统解决了物理寻址的缺点。利用内存管理单元(MMU)和页表(Page Table)将虚拟地址转换为物理内存地址。 进程运行过程不再加载全部数据,而是只保留当前运行需要的数据在内存中。为了让MMU更高效加入了TLB,缓存映射关系,还利用多级页表降低页表内存占用有了 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:0:0","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"虚拟内存系统 计算机系统主存被组织成一个连续字节大小的数组,每一个数组成员都有一个唯一的物理地址(Physical Address) 早期计算机使用物理寻址方式,这样的坏处是 系统中多个进程所使用的内存,进程之间容易互相读写数据造成各种问题 每个进程内存分布不同,管理不便 进程中暂无用处的数据也会被加载,进程过多就会导致内存不够用 现代处理器使用虚拟寻址(Virtual Address),利用虚拟地址映射成物理地址再进行访问,解决了上面的主要问题。既然需要地址转换,这就需要内存管理单元(Memory Management Unit,MMU)和页表(Page Table,PT)来处理 因为每个进程都有统一的访问方式,这样进程之间也不会互相影响 内存管理更加简单,每个进程看起来都在独享全部内存 节省内存空间,利用内存分页,物理内存中只保留进程当前活动区域,并根据需要在磁盘和主存之间来回传送数据 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:1:0","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"页表 虚拟内存系统将虚拟内存分割成一个个大小相同的虚拟页(Virtual Page,VP),类似的物理内存也被分割成物理页(Physical Page,PP)大小和VP相同,物理页也被称为叶帧(Page Frame)。 页表其实就是一个数组,每个元素称为页表项(Page Table Entry,PTE),PTE负责把虚拟页映射到磁盘或者物理页上。 任意时刻虚拟页面的集合都分为三个不想交的子集: 未分配的:VM系统还未分配的页,物理内存,磁盘都没有与之关联的数据(图中0,3) 已分配已缓存的:已分配到了物理内存中(图中1,4,6) 已分配未缓存:数据块存在于磁盘中,还未被加载到内存(图中2,5,7) ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:1:1","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"页表项 当MMU从PTE获取物理内存地址,要根据PTE知道: 虚拟页是否被缓存了 缓存命中需要知道具体存在于哪个物理内存页中 缓存未命中需要知道此虚拟页在磁盘的什么地方进行缓存替换操作 整个页表数据结构由操作系统进行负责维护,以及在磁盘与主存之间来回传送页,进行替换的时候需要向系统内核发送一个缺页异常,内核会做一些处理。 PTE负责把虚拟页映射到磁盘或者物理页上,假设需要两个数据: 地址字段:存放映射的地址 有效位:判断此页是否被缓存 有了这两个字段就可以进行判断: 设置了有效位,地址字段不为空:数据缓存在物理内存页中,地址字段为物理页的起始位置 没有设置有效位,地址字段为空:此虚拟页还未被分配 没有设置有效位,地址字段不为空:虚拟页被分配,但还未缓存到物理内存中,只在磁盘上,地址字段指向该虚拟页在磁盘上的起始位置 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:1:2","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"映射流程 虚拟地址有两部分VPN+VPO。MMU利用VPN找到对应的PTE, 例如 VPN 0 对应 PTE 0,找到PTE后,PTE中的有效位决定是否有效,是否需要缺页处理。 如果有效,则得到其中的PPN,使用PPN+VPO 得到最终的物理内存地址 页面命中,cpu硬件流程: 缺页流程,页面命中完全由硬件处理,处理缺页需要硬件和操作系统内核协作完成: ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:2:0","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"虚拟内存系统带来的优势 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:3:0","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"权限控制 每个PTE(页表项)高位部分存储了表示权限的位,MMU通过检查这些位来进行权限控制(sup表示进程是否必须运行在内核(超级管理员)模式下才能运行)。 如果违反了权限cpu会触发一个 一般保护故障,将控制传给内核的异常处理程序,linux shell一般称之为段错误(segmentation fault) ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:3:1","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"节省内存 MMU根据虚拟地址读取页表,发现设置了有效位,表明缓存命中,地址字段存储了物理页地址,就可以找到数据在物理内存中的位置,这是页命中,相对的就会触发缺页异常(缓存不命中 page fault): (见上图) MMU读取页表想获得VP1地址的时候发现未设置有效位,缓存未命中,只存在于磁盘,并触发一个缺页异常 缺页异常会调用内核中的缺页异常处理程序,该程序选择一个牺牲页,将其复制回磁盘(页面调出),取消设置PTE的有效位 异常处理程序再把磁盘上的VP1复制到物理内存中(页面调入),设置有效位 将指令重新发到MMU,再次执行的时候就可以命中了 上述中磁盘与物理内存中传送页的活动叫交换(swapping)或者页面调度(paging),当不命中的时候才进行换页操作这种策略称为按需页面调度(demand paging) 空间局部性和工作集导致效率高,如果不高说明发生了 抖动(thrashing) ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:3:2","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"内存管理更加方便 进程看起来就可以独享整个计算机空间了,因为在进程眼里全部虚拟内存都可以使用,所以每个进程也需要有自己的页表来进行映射,操作系统为每个进程都提供了一个独立的页表。 这样做有很多好处: 简化链接器:每个进程都有独立空间,数据段相同,这样的一致性,简化了链接器的设计与实现 简化加载:把目标文件(可执行文件和共享对象文件)中的.text和.data节加载到一个新创建的进程中,Linux加载器为代码和数据段分配虚拟页,把他们标记为无效的(即未被缓存的),将页表条目指向目标文件中适当的位置。整个行为加载器不会从磁盘复制内容到内存,而是靠虚拟内存系统的按需页面调度来处理 简化共享:每个进程有了独立地址空间,一致性的处理方式,让多个进程之间共享一些内核库更加简单(如上图) 简化内存分配:页表进行了和物理内存的映射,所以在分配内存的时候不需要考虑连续个物理页空间,可以随机分配 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:3:3","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"让虚拟内存系统更健壮 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:4:0","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"缓存方式 在存储器章节说过,梯形存储体系中,本层存储器是为上一层提供缓存的,当高速缓存未命中由物理内存提供缓存服务,物理内存比高速缓存慢10倍。 当物理内存缓存未命中由磁盘提供缓存服务,而磁盘比物理内存慢10w倍。所以物理内存的未命中代价开销要比高速缓存未命中大得多,而且读取磁盘\b中一个扇区的第一个字节时间开销比读这个扇区中连续字节要慢大约10w倍。所以: 物理页有更大的尺寸 4KB ~ 2MB:为了不命中处罚和访问第一个字节的开销 全相连:由于不命中处罚,任何虚拟页都可以放置在任何物理页中 缓存替换策略更复杂:不命中的时候替换进行替换页,替换错了还会出现缓存未命中,所以开销也会很大,需要更强大的替换算法(由操作系统提供) 写回而不是直写:访问磁盘很慢,所以需要一个修改位标记是否被修改,没有被修改就无需在替换的时候写入磁盘。 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:4:1","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"多级页表 32位操作系统就需要管理 $2^{32}$ 字节的虚拟内存地址 4GB 假设一个PTE需要4KB($2 ^ {12}$ 字节),一共需要:$2^{32}$ * $2^{-12}$ = $2^{20}$ 个PTE 假设一条PTE记录有4个字节($2^2$),一共需要 $2^{20}$ * $2^2$ = $2^{22}$字节 = 4MB 不分页情况下用4MB覆盖全部虚拟内存地址,也就是每个进程都有4MB的页表 多级页表主要从两部分降低内存: 如果一级页表中的一个PTE为空,那么对应的二级页表就不会存在 只有一级页表存在于主存中,虚拟内存系统可以按需创建,调入或调出二级页表 下图中,第一级页表每个PTE映射一个片(chunk)大小为$2^{10}$字节(可以理解为一个页表)。 这样计算下来,一级页表总共4KB, 二级页表共1024个chunk,但中只用到了3个也就是 3*4KB = 12KB。 下图展示了多级页表情况下整个映射过程,一级一级的索引到最终物理内存地址 虽然多级页表用空间换时间,但TLB和局部性让他并不比单级慢很多 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:4:2","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"TLB加速翻译 为了加速翻译 Translation Lookaside Buffer(TLB),可以理解为页表在处理芯片上的缓存 第1步 cpu产生一个虚拟地址 第2,3步MMU从TLB取出对应PTE 第4步将这个虚拟地址翻译成物理内存地址 然后发送到高速缓存/主存 返回数据 如果TLB不命中,则需要从高速缓存/主存取出相应PTE,存放到TLB,可能会覆盖已有TLB条目 ","date":"2019-09-28","objectID":"/posts/2019/20190928-csapp4/:4:3","tags":["操作系统"],"title":"读CSAPP(4) - 虚拟内存","uri":"/posts/2019/20190928-csapp4/"},{"categories":["coder"],"content":"了解硬件 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:1:0","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"随机访问存储器(Random-Access Memory,RAM) RAM分两类,静态(SRAM)的和动态的(DRAM),SRAM要比DRAM更快,价格也更高。 SRAM用于高速缓存存储器,可以在cpu芯片上,也可以在片下。DRAM用来作为主存以及 图形系统的帧缓冲区。无论哪种RAM一旦断电,所有信息都会丢失。 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:1:1","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"磁盘存储 磁盘存储数据的数量级更大,比RAM大得多,但读取信息要比DRAM慢10w倍,比SRAM慢100w倍。 磁盘分为机械硬盘和固态硬盘,机械硬盘的读写速度要低于固态硬盘,但价格低廉。 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:1:2","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"总线 IO总线:例如,鼠标键盘,图形卡,磁盘等设备连接的称为io总线 cpu使用内存映射I/O技术(memory-mapped I/O)来向I/O设备发起命令 使用内存映射技术向io设备发起命令 磁盘控制器接收到命令,读取扇区,并执行到主存的DMA传送,磁盘进行直接内存访问的操作叫做DMA(Direct Memory Access) DMA传送完毕,磁盘控制器用中断方式通知cpu cpu接收到中断信号,从内存读取缓存的数据 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:1:3","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"局部性 一个好的程序应该有良好的局部性,这样可以使得效率更快 时间局部性:被引用过一次的内存,很可能在不久的将来再次被引用多次 空间局部性:如果一个内存位置被引用了一次,那么程序再不久的将来可能会引用其附近的内存位置 看一个例子 int sumvec(int v[N]) { int i , sum = 0; for(i = 0; i \u003c N; i ++) sum+=v[i]; return sum; } 这个函数中,sum就有很好的时间局部性,在多次循环中,会一直访问同一个内存位置,因为是标量所以没有空间局部性。 数组v被顺序读取,读取第i个位置,那么附近的位置也会在下次循环中读取,所以有很好的空间局部性,但每个变量只访问一次,所以时间局部性很差。 这个函数中要么有好的时间局部性,要么有好的空间局部性,所以sumvec函数有良好的局部性。 再看一个例子: int sumarray1(int a[M][N]) { int i ,j , sum = 0; for(i = 0;i \u003c M ;i ++) for(j = 0; j \u003c N ;j ++) sum+=a[i][j]; return sum; } int sumarray2(int a[M][N]) { int i ,j , sum = 0; for(i = 0;i \u003c N;i ++) for(j = 0; j \u003c M ;j ++) sum+=a[i][j]; return sum; } 两个程序做的事情一模一样,效率看起来也是一样的。但根据局部性原理: sumarray1有空间局部性,sumarry2则没有。 这个二维数组存储在内存的顺序是一行行的存储,也就是按照行来读就会按序读取,按列读取就会跳着读。 空间局部性的临近读取,导致最终sumarry1更高效。 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:2:0","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"局部性总结 重复引用相同变量的程序有良好的时间局部性 在内存中大跨度跳来跳去的程序,空间局部性会很差 cpu取指执行的时候,循环遍历有好的空间局部性和时间局部性。循环体越小,循环次数越多,局部性越好 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:2:1","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"存储结构 缓存类型 缓存内容 缓存在何处 延迟(时钟周期) 管理 CPU寄存器 4或8 字节 芯片上的cpu寄存器 0 编译器 TLB 地址翻译 芯片上的TLB 0 内存管理单元 L1 高速缓存 64 字节块 芯片上的L1 缓存 4 硬件 L2 高速缓存 64 字节块 芯片上的L2 缓存 10 硬件 虚拟内存 4 KB 页 主存 100 硬件+OS 缓冲区缓存 部分文件 主存 100 OS 磁盘缓存 磁盘扇区 磁盘控制器 100,000 磁盘固件 网络缓冲区缓存 部分文件 本地磁盘 10,000,000 NFS 客户端 浏览器缓存 web页 本地磁盘 10,000,000 Web浏览器 Web 缓存 web页 远程服务器磁盘 1,000,000,000 Web 代理服务器 下一层的存储器都是为上一层做缓存的,如果想要获取某个数据对象d,在k层则是缓存命中,如果没有则需要去k+1层查询,查询到后放入k层。 如果这时候k层数据已满就会可能覆盖现在的存储空间块,有专门的替换策略来将新数据替换原先数据。这种多级存储体系将几种存储技术结合起来,更好的解决存储器大容量、高速度和低成本这三者之间的矛盾 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:3:0","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"缓存不命中 当前存储器中获取不到数据,则需要向下级进行获取,这就是缓存不命中,主要有三种: 强制性不命中(Cold/compulsory Miss):当缓存区域没有任何数据的时候(冷缓存),这时候获取任何数据都是不命中的,这是无法避免的 冲突性不命中(conflict miss):如果k层容纳4个数据块, k+1层容纳12个,放置策略导致k+1层的0,4,8,12位置的数据都会放入k层的0块位置,不同cache由于index相同互相替换 容量失效(Capacity Miss):有限的容量放不下大的缓存内容,被替换出去的下次再被访问,无法命中 ","date":"2019-09-23","objectID":"/posts/2019/20190923-csapp3/:3:1","tags":["操作系统"],"title":"读CSAPP(3) - 存储器层次结构","uri":"/posts/2019/20190923-csapp3/"},{"categories":["coder"],"content":"如何使用 channel在\u003c-左边 表示向channel发送数据 channel在\u003c-右边 表示从channel接收数据 close(channelName) 关闭一个channel channel := make(chan string, 2) //发送数据: 写 channel \u003c- \"struct\" //接收数据: 读 data := \u003c-channel fmt.Println(data) close(channel) ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:1:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"Channel的关闭 关闭一个未初始化(nil) 的 channel 会产生 panic 重复关闭同一个 channel 会产生 panic 向一个已关闭的 channel 中发送消息会产生 panic 从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 状态,可以用它来判断 channel 是否关闭,close操作是对写入的关闭,但仍然可以读取,若消息均已读出,则会读到类型的初始值 func SendMessage(channel chan string) { go func(channel chan string) { channel \u003c- \"hello\" close(channel) fmt.Println(\"channel is closed.\") }(channel) } func channalFunc2() { channel := make(chan string, 5) go SendMessage(channel) for { time.Sleep(time.Second) chStr, ok := \u003c-channel if !ok { fmt.Println(\"channel is close!!!!!!.\") break } else { fmt.Printf(\"receive %s\\n\", chStr) } } } ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:2:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"缓冲区 make创建通道时,指定通道的大小时,称为有缓冲通道,反之无缓冲区 无缓冲区或者缓冲区用完,写入一次,就要等待对方读取一次,否则无法继续写入阻塞住,同理读取不出来也会阻塞住 ch := make(chan int, 2) ch \u003c- 1 ch \u003c- 2 // ch \u003c- 3 //阻塞 a := \u003c- ch fmt.Println(a) 可以用len函数查看channel的已用大小, 用cap查看channel的缓存大小 ch := make(chan int, 2) ch \u003c- 1 fmt.Println(len(ch)) //1 fmt.Println(cap(ch)) //2 ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:3:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"单向通道 为了限制channel滥用,禁止其进行读取或者写入操作,让函数具有更高的单一原则,封装性 func counter(out chan\u003c- int) { for x := 0; x \u003c 10; x++ { out \u003c- x } close(out) } func squarer(out chan\u003c- int, in \u003c-chan int) { for v := range in { out \u003c- v } close(out) } func printer(in \u003c-chan int) { for v := range in { fmt.Println(v) } } func channalFunc3() { naturals := make(chan int) squares := make(chan int) //将读写函数分离 //写 chan\u003c- , 读 \u003c-chan go counter(naturals) //写入 go squarer(squares, naturals) //将刚才写入的变成只读的,传参进去, 中间转换层 printer(squares) //只读 } ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:4:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"作用 同步: 依靠阻塞的特性 做多个goroutine之间的锁 var ( sema = make(chan struct{}, 1) rece2 = 0 ) func raceFunc2() int { sema \u003c- struct{}{} rece2++ v := rece2 \u003c-sema return v } go raceFunc2() go raceFunc2() 定时器 fmt.Println(time.Now()) timer := time.NewTimer(time.Second * 2) \u003c-timer.C fmt.Println(time.Now()) //输出: 差两秒 // 2019-06-24 16:03:34.011947 +0800 CST m=+0.000201381 // 2019-06-24 16:03:36.015244 +0800 CST m=+2.003571991 //延迟执行 time.AfterFunc(time.Second*2, func() { fmt.Println(time.Now()) }) //定时器,每隔1秒执行 ticker := time.NewTicker(time.Second) go func() { for tick := range ticker.C { fmt.Println(\"tick at\", tick) } }() 通信: Channel是goroutine之间通信的通道,用于goroutine之间发消息和接收消息 type Cake struct{ state string } func baker(cooked chan\u003c- *Cake) { for { cake := new(Cake) cake.state = \"cooked\" cooked \u003c- cake // baker never touches this cake again } } func icer(iced chan\u003c- *Cake, cooked \u003c-chan *Cake) { for cake := range cooked { cake.state = \"iced\" iced \u003c- cake // icer never touches this cake again } } Select多路复用(I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接) 对channel的read, write,close, 超时事件等进行监听, 同时触发事件会随机执行一个 阻塞在多个channel上,对多个channel的读/写事件进行监控 func doWork(ch chan int) { for { select { case \u003c-ch: fmt.Println(\"receive A \") case \u003c-ch2: fmt.Println(\"receive B \") case \u003c-time.After(2 * time.Second): fmt.Println(\"ss\") default: fmt.Println(\"11111\") } } } func channalFunc5() { var ch chan int = make(chan int) go doWork(ch) for i := 0; i \u003c 5; i++ { ch \u003c- 1 time.Sleep(time.Second * 1) ch2 \u003c- 2 } for { } } ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:5:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"内部细节 ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:6:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"数据结构 type hchan struct { qcount uint // 当前队列中剩余元素个数 dataqsiz uint // 环形队列长度,即可以存放的元素个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 // 每个元素的大小 closed uint32 // 标识关闭状态 elemtype *_type // 元素类型 sendx uint // 队列下标,指示元素写入时存放到队列中的位置 recvx uint // 队列下标,指示元素从队列的该位置读出 recvq waitq // 等待读消息的goroutine队列 sendq waitq // 等待写消息的goroutine队列 lock mutex // 互斥锁,chan不允许并发读写 } type waitq sudog{//对G的封装 } channel 的主要组成有: 一个环形数组实现的循环队列, 用于存储消息元素 recvq和sendq两个链表实现的 goroutine 等待队列, 用于存储阻塞在 recv 和 send 操作上的 goroutine 一个互斥锁,用于各个属性变动的同步 ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:6:1","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"主要函数功能 makechan: 开辟一快连续内存区域存储消息元素 //伪代码 func makechan(t *chantype, size int) *hchan { var c *hchan c = new(hchan) c.buf = malloc(元素类型大小*size) c.elemsize = 元素类型大小 c.elemtype = 元素类型 c.dataqsiz = size return c } send chan\u003c- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒 如果缓冲区中有空余位置,将数据写入缓冲区 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒; func chansend(msg){ if close !=0 { panic(\"close\") return } //1.如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时 //直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程 if sg := recvq.dequeue(); sg != nil{ sg.send(msg) //给此goroutine发消息 sg.ready() //唤醒 return } //2. 跳过2说明无接收方, 如果有缓冲区且不满的话则写入到缓冲区 if qcount \u003c dataqsiz { buf.enqueue(msg) qcount++ return } //3. 没空余位置或者没缓冲区, 将待发送数据写入到当前调用的G, 并加入sendq链表,进入休眠,等待被读方唤醒 sg := get_current_g() sg.msg = msg sg.g.sleep = true sendq.enqueue(sg) } recv \u003c-chan sendq不为空 获取链表的头一个first_g 缓存无数据,将first_g消息复制给当前请求的g,并激活first_g 缓存有数据, 缓存队列 出列消息给当前请求的g,并将first_g数据加入缓存队列,first_g激活 缓存队列有数据将数据出队 复制给当前请求的g 缓存队列无数据将调用此chan的当前g加入recvq链表并设置休眠 func chanrecv(){ if sg:= sendq.dequeue(); sg != nil{ if buff \u003c= 0 { msg := sg.msg g := get_current_g() g.send(msg) sg.sleep = false } else { msg := buff.dequeue() g := get_current_g() g.send(msg) buff.enqueue(sg.msg) sg.sleep = false } return true } if qcount \u003e 0 { msg := buff.dequeue() qcount-- g := get_current_g() g.send(msg) return true } sg := get_current_g() sg.msg = msg sg.g.sleep = true recvq.enqueue(sg) } close: 设置关闭符号为1,唤醒recvq和sendq的g func close(){ if chan == nil { panic(\"close of nil channel\") return } if close !=0 { panic(\"close of closed channel\") return } close = 1 for sg:=recvq.dequeue();sg!=nil{ sg.sleep = false } for sg:=sendq.dequeue();sg!=nil{ sg.sleep = false } } ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:6:2","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"参考 go语言圣经 恋恋美食 blog draveness blog ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-channel/:7:0","tags":["go"],"title":"Go Channel","uri":"/posts/2019/20190917-go-channel/"},{"categories":["coder"],"content":"并发与并行 并行(parallel): 指在同一时刻,有多条指令在多个处理器上同时执行(靠机器) 并发(concurrency): 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,宏观看起来是并行的,微观是cpu在不断的快速切换.(操作系统) ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-groutine/:1:0","tags":["go"],"title":"Go Groutine","uri":"/posts/2019/20190917-go-groutine/"},{"categories":["coder"],"content":"线程安全问题 协作式调度: 当线程终止或阻塞是发生调度 =\u003e “主动让出” 抢占式调度: 允许逻辑上将可继续运行的线程在运行过程中暂停的调度方式 =\u003e “被迫让出” 脏数据原因: 抢占式调度被迫让出cpu控制权,一个行为可能有多个指令组合而成 多指令在执行过程中被中断,导致未执行完整出现脏数据. 举例: i= 0, 线程1 执行 i++,线程 2 也执行 i++, 想要的结果是2 当程序 1 将 i 值读取出来并运算后改为写入的时候,系统抢占式把控制权给个程序 2 程序 2 完整的执行完了 i++,随后系统将控制权交回给程序 1,此时的程序 1 并不知道自己被打断了,也不知道 i 已经被修改,还把之前计算好的值写入,最后把之前的2给覆盖了结果变成了1. 被打断是因为抢占式使用时间到了后被迫交还cpu 值篡改是因为读取i和写入i是两个指令不是一个原子操作 ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-groutine/:2:0","tags":["go"],"title":"Go Groutine","uri":"/posts/2019/20190917-go-groutine/"},{"categories":["coder"],"content":"Coroutine(协程)特点 一种用户态的轻量级线程 轻量级线程(由于线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大,内存消耗更低,一般是2kb vs 1mb) 是语言层面的任务,非系统级别的(由语言层面虚拟机或者go runtime等 进行创建),切换更高效 多个协程可能在一个或多个线程上运行.依靠调度器分配 协作式: 非抢占式(协作)在关键时刻(阻塞,任务完成等)将cpu让给其他线程 同一线程上的多个协程的切换是无线程安全问题的 ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-groutine/:3:0","tags":["go"],"title":"Go Groutine","uri":"/posts/2019/20190917-go-groutine/"},{"categories":["coder"],"content":"Goroutine 概念: goroutine是go语言中的协程 如何使用: go test() 在函数前加go关键字 就可以以新协程来启动test()函数 为什么说协程无线程安全问题: 协程是协作式本身无线程安全问题,但go runtime的scheduler会将多个goroutine分配到不同线程,才会出问题 var s int func test() { for i := 0; i \u003c 10000; i++ { s++ } } func main() { runtime.GOMAXPROCS(1) //这里可以暂时先暂时理解为一个限定只用一个线程 var wg sync.WaitGroup //用于等待所有协程都完成 wg.Add(2) go func() { defer wg.Done() //程序退出的时候执行 test() }() go func() { defer wg.Done() //程序退出的时候执行 test() }() wg.Wait() //等待所有协程的完成 fmt.Println(s) } ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-groutine/:4:0","tags":["go"],"title":"Go Groutine","uri":"/posts/2019/20190917-go-groutine/"},{"categories":["coder"],"content":"调度器(Scheduler) 高级语言对内核线程的封装实现通常有三种线程调度模型: N:1模型.N个用户空间线程在1个内核空间线程上运行,优势是上下文切换非常快但无法利用多核系统的优点. 1:1模型.1个内核空间线程运行一个用户空间线程,充分利用了多核系统的优势但上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换. M:N模型.每个用户线程对应多个内核空间线程,同时也可以一个内核空间线程对应多个用户空间线程,使用任意个内核模型管理任意个goroutine,但缺点是调度的复杂性 go 使用的是第三种模型,Scheduler调度器公平高效合理的将goroutine分配到相应的线程上 M: 操作系统的内核空间线程 G: goroutine对象 P: 代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键,一般256个 当执行 go test() 会发生什么? go test() ; go test() 创建2个 goroutine func test(){ fmt.Println(\"a\") time.sleep(time.Seconds * 2)//休眠2秒 fmt.Println(\"b\") } 加入队列 Scheduler检查空闲的P放入它的local queue等待被调用 无空闲 则加入到global queue 等待空闲后的P来拿取 因为goroutine需要依赖线程,也就是M,且M与P 1: 1的关系. M不断的loop执行goroutine,执行完 取下一个 当local queue中的groutine执行完成,没有了,就去global拿取 当发现global也没了,就去找其他P 偷取(work stealing),偷取数量为P localqueue数量的一半(为了平均分配任务) 偷取也没有的话,最终进入sleep状态,等待再次被唤醒 当M执行某一个goroutine时候如果发生了阻塞操作,M会阻塞,如果当前localqueue有一些可运行的G,Scheduler会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P func hello() { defer wg.Done() fmt.Println(\"a\") time.Sleep(time.Second * 4) fmt.Println(\"b\") } func hello2() { defer wg.Done() fmt.Println(\"c\") time.Sleep(time.Second * 4) fmt.Println(\"d\") } func main() { runtime.GOMAXPROCS(1) //限制只有一个P wg.Add(2) go hello() go hello2() wg.Wait() } 刚才摘除P后的M 运行完成阻塞的goroutine后怎么办?他会继续第3步骤里的事情,拿别人的东西 sysmon(monitor):此线程在go程序启动后创建,用来监控goroutine,做抢占式调用,只列出了goroutine相关,其实还有其他很多功能 向长时间运行的G任务发出抢占调度; 收回因syscall长时间阻塞的P 一旦G的抢占标志位被设为true,那么待G下一次调用函数或方法时,runtime便可以将G抢占 Go运行时系统并没有内核调度器的中断能力(只有系统有),它只能通过向运行时间过长的G中设置抢占flag的方法温柔的让运行的G自己主动让出M的执行权,这里也说明了即使是抢占了但也不会出现线程不安全,因为他不是被突然中断的,而是执行完检测到flag再中断的. ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-groutine/:5:0","tags":["go"],"title":"Go Groutine","uri":"/posts/2019/20190917-go-groutine/"},{"categories":["coder"],"content":"参考 惜暮 blog Morsing’s Blog k2huang blog 图片引自 k2huang blog ","date":"2019-09-17","objectID":"/posts/2019/20190917-go-groutine/:6:0","tags":["go"],"title":"Go Groutine","uri":"/posts/2019/20190917-go-groutine/"},{"categories":["coder"],"content":"高效的程序需要做到 合适的数据结构与算法 编写出编译器能够有效优化以转换成高效可执行代码的源码。 将运算量特别大的计算,可以分成多部分,这些部分可以在多核多处理器的某种组合上并行处理 本篇主要以第二点进行讨论,编译器在优化的时候只会做最坏打算,做各种假设。为了保证程序的准确性,舍弃性能优化。 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:1:0","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"编译器的优化限制 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:2:0","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"内存别名的使用 void twiddle1(long *xp, long *yp) { *xp += *yp; *xp += *yp; } void twiddle2(long *xp, long *yp) { *xp += 2* *yp; } 上面两个程序twiddle2(读xp,读yp,写xp)优于twiddl1(读2次xp,读2次yp,写2次xp)。 看起来编译器也许会将twiddle1优化成twiddle2的形式。但假设xp和yp是引用的同一个内存地址:\u0026xp = \u0026yp void twiddle1(long *xp, long *yp){ *xp += *xp; *xp += *xp; } void twiddle2(long *xp, long *yp){ *xp += 2* *xp; } 代码则可以写成上面这样,这时候两个函数的意义就不同了,twiddle1将xp增加了4倍,twiddle2将xp增加了3倍。 两个指针指向同一个内存地址,称之为:内存别名使用,编译器必须假设不同指针可能指向相同地址,限制了优化策略 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:2:1","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"函数调用 long f(); long func1(){ return f() + f() + f() + f() } long func2(){ return 4*f() } 看起来两个函数结果是相同的,但假设函数f内部为: long counter = 0; long f(){ return counter ++; } 那么func1和func2返回结果就不同了,而且修改全局状态也不同。 大多数编译器不会试图判断一个函数是否没有副作用,如果没有,可能被优化成func2的形式。否则假设最糟糕的情况,保持不变。 用内联函数减少开销,也对展开代码做了优化。 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:2:2","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"对一段代码的优化案例 下面程序是将元素累加,之后我们对combine1函数进行一步步优化 int get_vec_element(vec_ptr v, long index, data_t *dest){ if (index \u003c 0 || index \u003e= v-\u003elen) return 0; *dest = v-\u003edata[index] return 1; } long vec_length(vec_ptr v){ return v-\u003elen; } void combine1(vec_ptr v, data_t *dest){ long i; *dest = 0; for(i = 0; i \u003c vec_length(v); i++ ) { data_t val; get_vec_element(v, i, \u0026val); *dest = *dest + val; } } ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:0","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"1. 消除循环的低效率 将幂等函数移动出循环 void combine2(vec_ptr v, data_t *dest){ long i; *dest = 0; long length = vec_length(v); for(i = 0; i \u003c length; i++ ) { data_t val; get_vec_element(v, i, \u0026val); *dest = *dest + val; } } ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:1","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"2. 减少过程调用 get_vec_element中对边界检查,虽然很有用,但在本例中能明显看出所有引用都是合法,可以去除 void combine3(vec_ptr v, data_t *dest){ long i; *dest = 0; long length = vec_length(v); data_t *data = v-\u003edata; //获取首地址 for(i = 0; i \u003c length; i++ ) { *dest = *dest + data[i]; } } ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:2","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"3. 消除不必要的内存引用 combine3将计算值累积在指针dest位置,每次循环都会进行三步操作 读取dest值 读取data[i]值 写入dest所在内存 从dest读取的值,就是上次写入dest的值,加上个临时的变量 读data[i]值,计算结果放到临时内存 内存的值不写入,当循环完毕最后在写入到dest 优化后每次循环只需要读取一次data[i]值即可。 void combine4(vec_ptr v, data_t *dest){ long i; data_t acc = 0; long length = vec_length(v); data_t *data = v-\u003edata; //获取首地址 for(i = 0; i \u003c length; i++ ) { acc = acc + data[i]; } *dest = acc; } 编译器应该也会帮我们优化成combine4的形式,但由于内存别名的使用,可能会有不同行为。 比如 *dest引用的是data的地址,那结果会有区别,所以不会优化。 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:3","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"4. 循环展开 增加每次循环迭代计算的元素数量,减少迭代次数。 void combine5(vec_ptr v, data_t *dest){ long i; data_t acc = 0; long length = vec_length(v) - 1; data_t *data = v-\u003edata; //获取首地址 for(i = 0; i \u003c length; i+=2 ) { acc = (acc + data[i])+ data[i+1]; } *dest = acc; } 编译器一般会帮助我们进行循环展开操作,只要优化等级3或更高。 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:4","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"5. 提高并行性 在代码层面,下面三行是一条条按序执行,但cpu发现a和b的赋值操作是互不影响的,会同时执行a和b,这种现象叫做:指令级并行。 int a = 1; int b = 2; int c = a + b; 所以我们可以将combine5的代码再进行一次优化: void combine6(vec_ptr v, data_t *dest){ long i; data_t acc0 = 0; data_t acc1 = 0; long length = vec_length(v) - 1; data_t *data = v-\u003edata; //获取首地址 for(i = 0; i \u003c length; i+=2 ) { acc0 = acc0 + data[i]; acc1 = acc1 + data[i+1]; } *dest = acc0 + acc1; } combine5的循环展开,将循环次数减少了一半,提高了效率。但是cpu执行还是按序执行,无法进行并行操作,因为新的acc值总要依靠上一个acc,必须按顺序执行,才能获取。 现在将奇数索引的值赋给acc0,偶数索引的值赋给acc1,两个变量互不影响,cpu就可以进行并行操作了,最后将结果相结合 循环展开的数量并不是越多效率越高,循环的变量一旦超过可用寄存器数量,效率反而会更慢。 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:5","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"6. 分支预测 案例 代码引用-婉儿飞飞 下面代码,对数组值大于等于128的进行求和。求和前进行排序要比不进行排序直接求和效率要高3倍左右。 #include \u003calgorithm\u003e #include \u003cctime\u003e #include \u003ciostream\u003e int main() { // 随机产生整数,用分区函数填充,以避免出现分桶不均 const unsigned arraySize = 32768; int data[arraySize]; for (unsigned c = 0; c \u003c arraySize; ++c) data[c] = std::rand() % 256; // !!! 排序后下面的Loop运行将更快 std::sort(data, data + arraySize); // 测试部分 clock_t start = clock(); long long sum = 0; for (unsigned i = 0; i \u003c 100000; ++i) { // 主要计算部分,选一半元素参与计算 for (unsigned c = 0; c \u003c arraySize; ++c) { if (data[c] \u003e= 128) sum += data[c]; } } double elapsedTime = static_cast\u003cdouble\u003e(clock() - start) / CLOCKS_PER_SEC; std::cout \u003c\u003c elapsedTime \u003c\u003c std::endl; std::cout \u003c\u003c \"sum = \" \u003c\u003c sum \u003c\u003c std::endl; } cpu对分支进行预测,猜测下一步走哪个分支。如果猜对,cpu不会暂停一直运行,猜错,就要停止-回滚-热启动。 cpu根据历史进行猜测下一步走向。有序的数组,预测判断条件的结果更加准确,无序数组无法预测,命中率低,导致效率低。 优化方案 如果可以的话移除分支 //用位运算 替换 if判断 int t = (data[c] - 128) \u003e\u003e 31; sum += ~t \u0026 data[c]; 提高分支的可预测性,比如上面先排序 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:6","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"优化总结 消除函数调用 消除不必要的内存引用 循环展开 提高指令并行 提高分支的预测性 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:7","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"参考 stackoverflow-分支预测 ","date":"2019-09-09","objectID":"/posts/2019/20190909-csapp2/:3:8","tags":["操作系统"],"title":"读CSAPP(2) - 程序性能优化","uri":"/posts/2019/20190909-csapp2/"},{"categories":["coder"],"content":"定义 单处理器中低优先级的进程被高优先级的进程抢占,同时他们访问同一块共享资源 多处理器中,CPU1的进程、CPU2的进程同时访问同一块共享资源 ","date":"2019-09-03","objectID":"/posts/2019/20190903-go-race-condition/:1:0","tags":["go"],"title":"Go 竞态问题","uri":"/posts/2019/20190903-go-race-condition/"},{"categories":["coder"],"content":"如何避免竞态条件 变量只读 //下面两种获取map信息的方式 //懒汉获取方式,有则获取无则加载: 会有读写错乱情况 func loadmap(name string) int { return 2 } func getmap2(name string) int { v, ok := maps[name] if !ok { v = loadmap(name) maps[name] = v } return v } //预先加载好, 使getmap只读, 就不会存在竞态问题 var maps = map[string]int{ \"a\": 1, \"b\": 2, \"c\": 3, } func getmap(name string) int { return maps[name] } 私有化变量 //两个goroutine同时访问了变量a 引发竞态问题,导致结果不准确 var a = 0 var wg sync.WaitGroup //用于等待所有协程都完成 func add() { defer func() { wg.Done() }() for i := 0; i \u003c 10000; i++ { a++ } } func raceFunc1() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(\"ok: \", a) } Channel 靠通信同步数据而不是靠共享内存 //使用channel 将a设置为goroutine的局部变量 var ch chan int func add() { a := 0 for i := 0; i \u003c 10000; i++ { a++ } ch \u003c- a } func raceFunc1() { ch = make(chan int) go add() go add() v1, v2 := \u003c-ch, \u003c-ch fmt.Println(\"ok: \", v1+v2) } ","date":"2019-09-03","objectID":"/posts/2019/20190903-go-race-condition/:2:0","tags":["go"],"title":"Go 竞态问题","uri":"/posts/2019/20190903-go-race-condition/"},{"categories":["coder"],"content":"题外话: 空结构体 如何表达: struct{}是个 ‘空结构体类型’, 和结构体不一样; struct{}{}是空结构体的值 //定义方式: var ss struct{} = struct{}{} ss := struct{}{} var ss struct{} //未初始化的空结构体, go会默认初始化成 struct{}{}类型 //并且 空结构体的 值 只有 struct{}{} 其他都不行 fmt.Println(ss) // {} var ss struct{} = nil //error var ss struct{} = 0 //error //比较 a := struct{}{} // 或者写成 var a struct{} if a == struct{}{} { //不能用 nil或者0等 fmt.Print(\"11111\") // print } else { fmt.Print(\"22222\") } fmt.Println(a) // {} a := struct{}{} b := struct{}{} fmt.Println(a == b) // true 空结构体的内存占用是0 a := struct{}{} println(unsafe.Sizeof(a)) // 0 用途 字典: 当我们想处理一些数据是否存在于字典的时候,我们只想关注key是否存在,value是不必要的,这时候可以用: map[string] struct{} 信号: 当我们用channel来阻塞或者当一个信号触发的时候,我们只关注是否阻塞了,是否触发了,不在意传输的是什么信息: make(chan struct{}) 举例 //字典 m := make(map[string]struct{}) if _, ok := m[\"hello\"]; ok { println(\"yes\") } else { println(\"no\") } //信号 ch := make(chan struct{}) go func() { time.Sleep(time.Second) ch \u003c- struct{}{} }() \u003c-ch fmt.Println(\"hello\") ch:=make(chan struct{}, 100) 定义了有缓冲区的channel, 但缓冲区的内存大小依旧是0, cap(ch) 为100 s := [100]struct{}{} 数组大小为100,占内存为0 ","date":"2019-09-03","objectID":"/posts/2019/20190903-go-race-condition/:3:0","tags":["go"],"title":"Go 竞态问题","uri":"/posts/2019/20190903-go-race-condition/"},{"categories":["coder"],"content":"防止竞态条件 临界区(critical section):在Lock和Unlock之间的代码段中的内容可以随便读取或者修改不会有竞态问题 一个只能为1和0的信号量叫做二元信号量(binary semaphore) 生产消费模式 可以设置访问数量 官方扩展包支持:go get golang.org/x/sync/semaphore var ( sema = make(chan struct{}, 1)//同一时刻 只能一个线程访问 balance int ) func Add(amount int) { sema \u003c- struct{}{} //写入成功 或者 失败阻塞住 balance += amount \u003c-sema } func Get() int { seam \u003c- struct{}{} defer func() { \u003c-seam }() return balance } sync.Mutex互斥锁 var ( mu sync.Mutex // guards balance balance int ) func Add(amount int) { mu.Lock() balance += amount mu.Unlock() } func Get() int{ mu.Lock() defer func() {mu.Unlock()}() return balance } sync.RWMutex读写锁 如果只需要读取变量的状态,不修改变量,我们并发运行事实上是安全的,只要在运行的时候没有修改操作即可。在这种场景下我们需要一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥: 多读单写锁(multiple readers, single writer lock) 适用场景:RWMutex只有当获得锁的大部分goroutine都是读操作, RWMutex需要更复杂的内部记录,所以比mutex慢些 与Mutex比较 RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量 读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥,只有在锁释放后才可以继续申请互斥的锁 使用 Lock()和Unlock()用于申请和释放写锁 RLock()和RUnlock()用于申请和释放读锁 var ( rw sync.RWMutex balance int ) func Add(amount int) { rw.Lock() balance += amount rw.Unlock() } func Get() int{ rw.Lock() defer func() {rw.Unlock()}() return balance } sync.Once惰性初始化 判空后进行初始化操作,但多协程情况下容易出现竞态条件导致初始化多次 gogo1 查性能 //线程安全但效率慢 var mu sync.Mutex var icons map[string]image.Image // Concurrency-safe. func Icon(name string) image.Image { mu.Lock() defer mu.Unlock() if icons == nil { loadIcons() } return icons[name] } //线程安全且高效,但代码复杂容易出错 var mu sync.RWMutex var icons map[string]image.Image func Icon(name string) image.Image { mu.RLock() if icons != nil { icon := icons[name] mu.RUnlock() return icon } mu.RUnlock() // acquire an exclusive lock mu.Lock() if icons == nil { // NOTE: must recheck for nil loadIcons() } icon := icons[name] mu.Unlock() return icon } //和读写锁一样,但更简洁 var loadIconsOnce sync.Once var icons map[string]image.Image // Concurrency-safe. func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] } 原子操作 原子操作由底层硬件支持,而锁则由操作系统提供的API实现。若实现相同的功能,通常会更有效率 支持增或减、比较并交换、载入、存储、交换 //注意int关键字应该是 type int int64 //int 8字节, int32 4字节, int64 8字节 但int和int64操作一样但类型是不同的 var counter int32 = 0 //加法 atomic.AddInt32(\u0026counter, 1) //减法 atomic.AddInt32(\u0026counter, -1) //比较并交换, 当counter的值和第二个参数(counter的旧值)不一致 会返回false 交换失败 atomic.CompareAndSwapInt32(\u0026counter, 0, 12) //counter = 12 //载入(读取) v:=atomic.LoadInt32(\u0026counter) //写入 atomic.StoreInt32(\u0026counter, 22) //交换 atomic.SwapInt32(\u0026counter, 11) 自旋锁 线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU type spinLock uint32 func (sl *spinLock) Lock() { for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) { runtime.Gosched() //用于让出CPU时间片 } } func (sl ck()和RUnlock()spinLock) Unlock() { atomic.StoreUint32((*uint32)(sl), 0) } func NewSpinLock() sync.Locker { var lock spinLock return \u0026lock } 互斥锁与自旋锁比较 互斥锁适合用于临界区持锁时间比较长的操作 临界区有IO操作 临界区代码复杂或者循环量大 临界区竞争非常激烈 单核处理器 至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器, 互斥锁开销比自旋锁高,但长时间的锁定自旋锁会占用cpu资源 ","date":"2019-09-03","objectID":"/posts/2019/20190903-go-race-condition/:4:0","tags":["go"],"title":"Go 竞态问题","uri":"/posts/2019/20190903-go-race-condition/"},{"categories":["coder"],"content":"总结 优先防止竞态条件发生 channel通信代替共享内存 原子操作适合简单的操作更简洁高效 读多写少用读写锁 读写频繁用互斥锁 临界区无io操作,执行快,执行频率高,可以使用自旋锁 ","date":"2019-09-03","objectID":"/posts/2019/20190903-go-race-condition/:5:0","tags":["go"],"title":"Go 竞态问题","uri":"/posts/2019/20190903-go-race-condition/"},{"categories":["coder"],"content":"参考 go语言圣经 借真理之力在我有生之年得以征服世界。 – 《v字仇杀队》 ","date":"2019-09-03","objectID":"/posts/2019/20190903-go-race-condition/:6:0","tags":["go"],"title":"Go 竞态问题","uri":"/posts/2019/20190903-go-race-condition/"},{"categories":["coder"],"content":"计算机存的什么 计算机存储的是二进制,每一位存储的是0或1。大多数计算机使用1字节(也就是8位),作为最小可寻址的内存单位。 每个字节都有一个唯一的数字来标识,也就是地址(address)。每个计算机都有一个字长(word size),也就是常说的64位操作系统,32位操作系统。字长决定了虚拟地址空间的大小, 比如32位有4GB的内存空间,64位则是16EB(1TB = 1024GB,1 EB = 1,024 PB = 1,048,576 TB)。 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:1:0","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"整数 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:2:0","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"原码,反码,补码 计算机存储的是二进制,现实中数字有正负之分,二进制如果想表示正负数怎么办呢? 为了解决正负数问题于是有了 原码 ,原码的表示方式是:第一位不再表示有效位,而是符号位,0代表正数,1代表负数 [1001] 这个8位二进制,本该表示 十进制的9,现在他表示为 -1,第一位(1)是符号位,后面的才是真正的值。 正数 二进制 负数 二进制 +0 0000 -0 1000 +1 0001 -1 1001 +2 0010 -2 1010 +3 0011 -3 1011 +4 0100 -4 1100 +5 0101 -5 1101 +6 0110 -6 1110 +7 0111 -7 1111 现在计算机可以存储负数了,我们进行运算操作: 十进制:1+1 = 2 ; 二进制则是:0001+0001 = 0010 十进制:1 + (-1) = 0;二进制则是:0001+1001 = 1010 结果是-2 1+(-1)时结果会出现问题。 虽然原码表示了正负数,运算却有问题。 为了解决正负数相加问题于是有了反码,反码的表示方式是:在原码的基础上,正数的反码是其本身,负数的反码是符号位不变,有效位取反 正数 二进制 负数 二进制 +0 0000 -0 1111 +1 0001 -1 1110 +2 0010 -2 1101 +3 0011 -3 1100 +4 0100 -4 1011 +5 0101 -5 1010 +6 0110 -6 1001 +7 0111 -7 1000 现在我们看看正负数相加: 十进制:1 + (-1) = 0;二进制则是:0001+1110 = 1111 ,正好对应反码表里的 -0 这样就解决了正负数相加的问题,但现在还有一个问题: 0 有两种表示方式:0000和1111,现实中0是不分正负的,计算机也需要解决,否则判断是否为0还需要判断两次(+0和-0)。为了解决0有两种表示类型 于是有了 补码,补码的表示方式是:在反码基础上,正数不变, 负数+1 正数 二进制 负数 二进制 +0 0000 -0 0000 +1 0001 -1 1111 +2 0010 -2 1110 +3 0011 -3 1101 +4 0100 -4 1100 +5 0101 -5 1011 +6 0110 -6 1010 +7 0111 -7 1001 – – -8 1000 原本1111表示-0,+1后: 1111+0001 = 10000,补位后溢出不计入,所以最终结果是0000 1000没有人用,于是就给了-8 现在正负0的二进制形式都为0000,并且还多出了一个表示数字-8 再来计算一下正负数相加: 十进制:1 + (-1) = 0;二进制则是:0001+1111 = 10000 ,溢出不计入,最终结果0000 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:2:1","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"运算溢出 如果存储大小为4位,取值范围就是[1000, 0111] -8 ~ 7。 正溢出: 十进制:5+5 = 10, 二进制0101+0101 = 1010(十进制结果-6),因为溢出导致将原来符号位的0改成了1,结果变成了负数。 负溢出: 十进制-5 + (-5)=-10,二进制1011+1011 = 0110(十进制结果6),溢出位不计入,最终因为溢出导致将原来符号位的1改成了0,结果变成了正数 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:2:2","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"有符号与无符号的转换 在c语言中整型加上unsigned来表示无符号整数。这样的话补码[1000,01111] 的范围是-8 ~ 7,去掉符号位的话就是[0000,1111] 0~15。 无符号转换成有符号 十进制 无符号 有符号 最终十进制结果 1 0001 0001 1 15 1111 1111 -1 有符号转换成无符号 十进制 有符号 无符号 最终十进制结果 1 0001 0001 1 -8 1000 1000 8 -1 1001 1111 15 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:2:3","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"扩展与截断数字 扩展(例如从 4位 到 8位) c语言中short转换为int的操作,小字节转换到大字节,不会对原数据造成丢失 无符号数:加 0 转换前的十进制 4位 8位 转换后的十进制 1 0001 0000,0001 1 15 1111 0000,1111 15 有符号数:加符号位 转换前的十进制 4位 8位 转换后的十进制 -1 1001 1111,0001 -1 -8 1000 1111,1000 -8 截取: 如果将w位转为k位,其中w\u003ek,则取后k位,移除 高位 w ~ k 的位。例如 8位 到 4位:则移除前4位,保留后4位。所以可能会丢失高位的数据,导致结果有问题 无符号数:直接保留后四位 转换前的十进制 8位 4位 转换后的十进制 17 0001,0001 0001 1 255 1111,1111 1111 15 有符号数:保留后四位,第一位表示符号位(结果是补码) 转换前的十进制 8位 4位 转换后的十进制 -9 1000,1001 1001 -7 -15 1000,1111 1111 -1 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:2:4","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"浮点数的表示 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:3:0","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"浮点数在二进制中的存储方式 十进制中 +100.05 可以用科学计数法表示成 +1.0005*$10^2$。这样的话我们就可以根据科学计数法,在二进制中存储了。 将位分成三份存储上面 的值:符号位(0),整数位(10005),阶数位(2)。最后因为是二进制,计算的时候把$10^n$改成$2^n$。 根据国际标准IEEE 754,任意一个二进制浮点数V可以表示成下面的形式: V=$(−1)^s$𝑀$2^E$ $(-1)^s$表示符号位,当s=0,V为正数;当s=1,V为负数。 M表示有效数字,大于等于1,小于2。 E表示指数位 M:二进制中M的第一位总是1,所以IEEE 754规定,这个数字可以忽略,在运算的时候再添上。这样对于32位浮点数,就有24位有效数字,64位则有53位。增加了表示范围 E:E 是无符号的,如何表示负数,也就是 2^-n 这种形式? 实际的E表示为 E = e - Baic。其中e是无符号数,Baic=2^(k-1)-1。以32位浮点数为例,e的范围在0~255,Baic = 127,E的范围就是-126~+127。 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:3:1","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"阶码(E)的值决定了 这个数是规格化,非规格化或者特殊值。 规格化:当阶码域不全为0时,或者不全为1时,得到真实值,再将有效数字M前加上第一位的1 非规格化:阶码域全为0,小数域不再补上1,这样做是为了表示±0,以及接近于0的很小的数字 特殊值:当阶码全为1,小数域全为0的时候,根据s的值,表示为 + \\infty,- \\infty。如果小数域不全为0,则为NaN ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:3:2","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"大小端 数据在内存中的存储顺序方式有大小端之分。 举例:如果int类型的x 存储在0x100的位置,十六进制表示:0x01234567 ,根据大小端有两种不同存储方式: 大端: 按照最高有效位(这里是01)到最低有效位的顺序存储 0x100 0x200 0x300 0x400 01 23 45 67 小端:按照最低有效位(这里是67)到最高有效位的顺序存储 0x100 0x200 0x300 0x400 67 45 23 01 一般在应用层开发无需在意大小端,字节顺序不可见。只有在网络传输的时候,大端机器传输给小端机器,或者反过来时,才会有大小端转换问题。 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:4:0","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"总结 计算机内存有限,溢出是必须考虑的事情。整数用补码形式存放于计算机,便于进行数值计算。两个数进行计算,当超出类型的字节范围,就会有溢出问题,造成程序异常。一个数据的存放在内存的顺序有大小端之分。 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:5:0","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["coder"],"content":"参考 张子秋的博客 插画-李俊达的回答 - 知乎 wdxtub 博客 阮一峰 浮点数 痛苦分两种,一种毫无意义,一种让人更坚强。 – 《纸牌屋》 ","date":"2019-08-27","objectID":"/posts/2019/20190827-csapp1/:6:0","tags":["操作系统"],"title":"读CSAPP(1) - 信息的表示和处理","uri":"/posts/2019/20190827-csapp1/"},{"categories":["thinker"],"content":"学习方面 最近看《暗时间》这本书,感觉很不错。 投入的时间≠实际时间,要用高效的方法学习,充分利用暗时间,学习过程要不断思考推断,而不是一味的死记硬背。 只有当进入沉浸状态学习效率才会提高,并且不被其他因素中断。 最后将学到的知识进行归纳,或者讲给别人听,加深巩固知识。 学习过程或者做一件事情的过程,如果能记录进度,知道自己还差多少完成,心里就会有所期待。 学以致用才是最终目的 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:0","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"实际时间 投入的时间*效率 才是真实的学习时间,不加以思索的学习,容易遗忘,效果低。 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:1","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"暗时间 吃饭,睡觉,公交等 这些碎片时间,进行思考刚学的知识。 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:2","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"沉浸状态 当所有心思都专注在这一件事上,这时候的效率是最高的。 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:3","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"中断 当专注某一件事的时候,突然被外界干扰,或者思绪突然想到了其他事情上。 如同计算机线程之间的切换,多个事情同时处理需要上下文的切换,会造成性能损耗。 专注突然被打断,再次重新专注需要时间 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:4","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"归纳知识 记笔记,或者写博客,按照自己思路将知识描述总结到文本中,加深印象。 建立索引,经常回顾,忘记时也能快速查到。 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:5","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"讲给别人听 这里的讲给别人,也可以是设想讲给别人听, 费曼学习法,大致意思就是将学到的知识,总结成按自己的理解思路进行描述出来,讲述给别人。 这样会更清楚的意识到自己理解了多少,哪里讲述的不清,说明哪里理解的不到位然后重新总结, 反复归纳总结,讲述,直到清晰表达出要点。 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:6","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"},{"categories":["thinker"],"content":"进度 当下载一个软件,能知道下载的进度,这样就能等下去,知道还有多久完成。 学习也一样,如果一直学,没有止境会给人心里感觉很徒劳。 可以将学习某一个知识,进行分段,比如将知识点分成几块,每天完成多少,大约1个月就能完成全部。 明确的目标事半功倍 痛苦的秘密在于有闲工夫担心自己是否幸福。 – 萧伯纳 ","date":"2019-08-22","objectID":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/:1:7","tags":["SS"],"title":"暗时间","uri":"/posts/read/20190821-%E6%9A%97%E6%97%B6%E9%97%B4/"}]