
暂无个人介绍
快要见家长了,赶紧补一波注意事项~~~ 男生版 1、去女友家拜访,到她家附近时不可以再挽着她的手,因为她的邻居会品头论足一番。 2、进家后,不要对她太随便、太亲热,她的家人在观察着你的一举一动,以免给人留下不好的印象。 3、不可不时看手表,否则她的家人会认为你不愿意呆在她家。 4、离去时不可一踏出门就露出松懈姿态,因为背后她的家人们还在观察你。 5、还应注意一点,去女友家做正式拜访的前一天,一定要洗澡,更衣,要做到干净、整洁,切不可邋里邋遢,既不尊重对方,又给人留下不好的第一印象。最重要的是全身都散发出干净的清洁感,如肩膀上有头屑最让人恶心。男人通常都是以外表取胜,因为父母的眼睛很容易往细处看:干净、整齐的头发、胡子,衬衣领口和袖口洁净,指甲修理整齐、干净,长裤笔挺,干净的袜子的鞋。 初登恋人家门,就像参加一场考试,既要注意服饰仪表,又要为应答交谈作好必要的准备。 这可从四个方面进行: 一是稳定情绪,自信真诚恳切,落落大方,肯定受欢迎;虚伪做作,扭扭捏捏,必然被嫌弃。有了自信心,紧张的心情便会慢慢平静。 二是了解情况。询问恋爱对象,了解其家庭成员,父母的职业,文化,兴趣,经历,性格等,知道越详细越好。情况熟悉,才能先有准备,交谈时也能有的放矢。 三是初拟内容。想想对方会提出些什么问题。在这种场合下,未来的岳父母常会问问你的家庭,工作,爱好等,自己可做些准备。 四是适当准备礼物,初次去对方家里做客,适当送些礼物给对方的父母,很有必要,这不仅表明你对长辈的尊敬,更表明你的诚意,一般来说,对方父母是会接受的,但应注意,送的礼品不宜太贵重,否则起点太高,以后恐怕你难以为继。 初次见面,双方难免感到拘束。寒暄之后,常会冷场。这时就要引出话题。一种方法是就地取材。仔细观察墙壁,窗台,桌子等,墙上的名人字画,窗台上的菊花,桌上的各种小摆设,都可借来入话。因为它们往往体现了主人的情趣爱好,能使主人讲出许多愉快的话来。另一种方法是避生就熟。引出话题要避开对方感到陌生的事,从对方最熟悉的入手,可以谈谈新闻,聊聊天气。 对方父母提问时,要会叙述说明,态度要恭敬,声音略低一些,柔和些。把话一句句,一层层说清楚,谈话时一定彬彬有礼。长辈夸奖年轻人时,总爱说:“这孩子很懂事!”一般长辈衡量青年好坏的标准,除了天生的外貌和本人的才能外,就看他是否“懂事”,也就是是否能通晓情理。在与对方父母交谈时,你一定要注意礼貌问题,要做到谦虚恭敬,善解人意。 初登恋人家门,应忌讳四个方面: 一忌油嘴猾舌。 说话要朴实,说心里话,如一池清水,清澈见底;不做作,不油滑。 二忌自我吹嘘。 要懂得:不表示自己聪明,就是聪明;不夸张自己的美德,就是美德。 三忌胆小怯羞。 要明白:越怕讲错,就越会讲错话;不怕讲错,反倒会使言谈顺畅。胆小,拘谨,害羞,是初登恋人家门交谈时的大忌。 四忌粗言劣语。 语言必须文明,不礼貌的口头禅,习惯语等,一定要戒除。 对方父母请你家宴,说明他们对你有了初步好感,你切不可得意忘形,露出“野性”。由于来做客赴宴是“醉翁之意不在酒”,切莫贪杯,喝个酩酊大醉,美好姻缘可能会因你一醉方休而毁于一旦。 男生拜见女方家长的注意事项 第一次上对方的门,新人心里必定紧张,说什么做什么,多少有点手足无措。希望这篇攻略能为新人们整理出一点头绪,让新人们在准备时有的放矢。 在见女方父母之前,请男孩子们一定好好整理一下自己。如果在女方家里见面,那么整齐的休闲装就可以了。如果是约在酒店或者茶社见面,那最好还是穿正式的西装+皮鞋。 切记,男孩们要把自己收拾的干净利索,这样能讨父母辈的喜欢。指甲缝不要有污垢、皮鞋要擦亮、头发整齐最好不要用发胶、胡子刮干净。要让女方父母觉得,这个男人很得体,把自己的女儿交给他能放心。 拜见家长怎么穿不适宜 不要在未来的岳父岳母面前表现你的小个性了,艺术家那样的披肩长发啦,络腮胡子啦,各种放荡不羁啦,请收敛起来吧,这只能给你减分。夏天的凉拖短裤,冬天挂满泥的球鞋和满是褶皱的裤子,还是早点换了吧。有汗脚的男孩,也要前期做好准备,不要穿不透气的鞋子,换双新袜子,不要让破洞在换鞋后暴露出来。 拜见家长怎么送礼物适宜 送礼物是个大学问,现在已经不再是水果点心的年代了。在挑选礼物的时候,要跟女友商量着来,有没有对方父母一直想买但没有机会去买的东西。送这样的礼物,会让长辈觉得这孩子很贴心。像是茶叶、羊毛披肩这样既实用又有面子的礼物,再好不过了。 拜见家长怎么送礼物不适宜 礼物很重要,价格也很关键。我恰恰不赞同送过于贵重的礼物,毕竟是第一次见面,太贵重的礼物会让对方父母觉得你是不是故意在显示有钱呢,又或者不懂得节俭,这样怎么能过日子呢。 有些爸爸喜欢抽烟,很多男孩子会带上好烟,其实小编不觉得这样的礼物好,因为对健康没有好处。 拜见家长怎么称呼适宜 得体的礼貌,在初次见面中很重要。可以事先问问对方父母的出生年份,决定称呼叔叔阿姨还是伯父伯母,这样比统称叔叔阿姨更得体一些。跟女方父母交流时要带“您”。 拜见家长怎么称呼不适宜 想象不出该是有多没有规矩的孩子,才能称呼对方父母除了“叔叔阿姨伯父伯母”之外的称谓了。 拜见家长怎么聊适宜 谈话时,语速要适中。长辈问什么就答什么,如果稍微有点冷场,男孩子可以主动引起一些话题,可以从对方父母的爱好展开话题,但态度要谦虚谨慎,大方得体,不要滔滔不绝。 拜见家长怎么聊不适宜 在具体聊天的过程中,请新人注意一些禁忌。 1、如果对方父母有身体不好的,请不要聊健康。 2、不要涉及地方家里的私事,比如退休工资啦,以前的单位啦。 3、不要滔滔不绝,那样会让人觉得特别不稳重,爱显示自己。遇上本身低调的父母,会觉得这个小伙子有点夸夸其谈。 4、不要谈及财产之类比较敏感的话题,即使你家里物质条件好,也要低调。 男方第一次去女方家相关事项: 1、时间——最好是一天之中的上午,千万别晚上去(当然这得看你未来的岳父母的意识)这是一般的礼节,以示尊重,并暗示你是一个勤奋之人,因我的父母皆是大家族出来的,特别讲这个。 2、送礼——男方第一次去女方家总不能空手而去,总会想买点礼物,这是很多男孩子好面子才这样想,但是究竟要买什么也拿不定主意。这个礼物不要太多,但是一定要买。要买好看的,拿得出手的,不一定要很实用(因为在他们心理,你毕竟还是“外人”,所以一定要体面)。 3、礼貌——上一辈的人对这个看得是比较重的,第一次去女方家之前,男方可以先问问你女朋友她们那里有没有什么特殊的礼节,如没有特别的,则和长辈的交往中保持谦虚的态度可谓是最大的礼节吧,进女方家门时,男方嘴口一定要甜,比如微笑的问叔叔阿姨你好,这是初次登门给人的第一印象(很关键的)。 4、言谈——在女方家人面前,男孩子要面带微笑,多听少说,认真聆听老人的言谈、并及时应和,说话时大方自然,但不可以得意忘形,保持诚恳、实诚的本色,也许你有些事情还没想好,如大人们问起也许不知怎么回答,那就照实回答,比胡谄强多了,老人往往要的是感觉,也给你的女友一种姿态,你接纳她并会善待她的父母。”多谢””不客气””你们别忙乎,我来”要常挂在嘴边,多说关心的话。 5、话题——避开一些双方敏感的话题,多询问一些女友小时候的事,这是每个父母百说不厌的经典话题,尤其是值得夸耀的事,可问一些细节。当他们的思维跟你转的时候,就减轻了对你的注意力,同时他们会认为你关心他们的女儿。 6、衣着——男方第一见女方家长,衣服不必刻意讲究有多华丽,只要整洁、干净,就行,但不要太随便,还有就是头发一定要干净。 7、神态举止——自然、轻松、愉快。最好用几句简短的话,表明你对将来的打算,并征求他们的意见。对他们的回答哪怕有异议,也点头,微笑,以后再说,不要破坏此次的气氛。做些力所能及的小事吧,也许他们不会让你做,但态度是另外一回事。 8、其他——第一次会面的时间最好不要太长,最好不要住在女方家里,有的家庭很忌讳这个,速战速决,见好就撤,以便为下次会面留下良好的期待。多注意观察,比如看见家里长了花草,就装作很感兴趣可以询问饲养花草的方法,要是看见有什么古董之类的陈设,可以赞美一番。总之就是要抓住岳父母的爱好,他会很有兴趣,觉得你是同道中人。 女生版 其实见家长这事是个说大也大说小也小的事,如果真的因为见家长这事最后两个人掰了,那只能说明两个问题: 第一,你的情商实在让人捉急。 第二,男朋友家里人不同意。 女孩子见家长的话一定会担心他家里人会在怎么想啊,是不是同意啊,喜不喜欢我之类的问题自我烦恼,更有些想事情多的妹纸还会想到婆媳矛盾问题啊巴拉巴拉的。 其实见家长这事,我们要两看,第一呢,让男朋友家里人看一下你,心里有个数,毕竟男朋友的妈妈是我们最大的情敌么。其次,你也可以借机看看男朋友家里的状态,看看他们家是否和谐啊,家里是准婆婆说了算还是公公,家庭气氛怎么样啊等等,如果考察出问题,果断解决也是极好的。闲话不多说,总结下我生活中关于见家长的攻略。 见家长呢,不一定是一定要结婚了才见的,哪怕是在恋爱中也是可以去见家长的。这表明了你的另一半对你的认同以及希望跟你长久发展的愿望。我曾经听过一个男孩子说一直没有勇气带女朋友回家,他觉得很恐怖。所以你的另一半想带你见家长,那么妹纸们,偷笑吧,嘻嘻。笑过之后就要开始准备了。 我把整个过程分为三个阶段:事前、事中和事后三部分。 一、事前:见家长前的准备工作要做好 知己知彼百战百胜 在知道要去后,首先做好信息收集的工作。问问他家爸爸妈妈的一些喜好啊,平时的兴趣啊什么的,这些一定要问清楚。一方面,方便准备给对方父母的礼物。另一方面,可以很靠谱的跟长辈们聊天。不过,这没有那么简单,是个男友九个会跟你说,我也不知道要买什么。放心吧,我爸爸妈妈人很好的。 一般的对话是这样的: 女:去你家买点什么啊?你妈妈喜欢什么啊? 男:我也不知道,你自己看着办吧。 女:化妆品?你妈妈用什么的? 男:不知道…… 反正,就是男朋友一般是不会给你合适的建议的。因为你们的立场是不同,他回去只是回家,爸爸妈妈是自己的亲人,而我们是去见外人,出发点都不一样想问题的角度一定没办法相同。因此,如果你的男朋友能给你靠谱的建议,那我恭喜你呀。如果不能,咱们也要自力更生。 问清楚对方父母的职业、家庭状态(退休了没有啊)、业余爱好、家里都有什么人啊。这次去见面都有哪些人啊等等。 问问对方家里有什么家庭礼仪需要遵守啊。比如有的地方吃饭是必须要端着碗吃,有的地方吃饭绝对不能把碗端起来等等,这些习俗挺琐碎的,也很重要啊,细节决定一切啊。 去对方家一定要提前准备礼物。告诫妹纸们,千万不要空手去,一定不要。虽说空手对方不会说什么,但是真的从礼数上讲真的不好。一般你带着东西去,对方会跟你客气客气:哎呀,来吃个饭还带什么东西呀。下次来就不要带了……哎呀,这多不好意思啊……一般都会这样,看到了“下次”么,如果你不带东西去,那么连听到这句话的机会都被你丢了,多可惜啊。 关于礼物的选择: 水果类。原则是不买那些太普通的,买好吃好看的,又不太重的。水果是第一次见面最稳妥的选择,就是说你拿不准要买什么的时候,或者这次见面是家庭式的,那么买水果不会惊艳,但绝不失礼。 保健品类。这类也是送长辈很不错的选择,一般我会买钙片、维生素或者蛋白粉之类的保健品作为第一次见面的见面礼,不分男女都可以用,还能把对方的爷爷奶奶也送到,简单又实用。 食品类。这类的话最好能跟特定的节日联系起来,比如中秋节,送个月饼啊什么的。 特产类。这是很讨巧的一类礼物,比如我会买点珍珠粉、海边的特产送过去,大方又拿得出手,就说是特产让对方尝鲜。 其他。比如给对方妈妈可以买护肤品、衣服、包、香水什么的。给对方爸爸的话买茶叶、工艺品之类的(我不太会给男生买东西,尤其是长辈,这个我也在学习和总结中呢)。 个人形象塑造。 这个也是需要事前好好想好的。其实很多人都知道要得体大方,简单化妆、淡妆就可以。但是具体的话,我的建议是风格上可以根据个人气质进行选择,但可以在服饰的配色上往偏暖的色调上靠、简单的风格就好。妆容的话淡妆就可以了,标准就是让人看着舒服就好,不用刻意凹造型。 二、事中:眼观六路,耳听八方 事前的成功就等于见家长成功了一半,另一半就要靠自己在现场的掌控了。 都说妈妈是你最大的情敌,这话我特别认同。所有的妈妈在见女朋友的时候心里都会有一个疑问,她能对我儿子好吗? 因此,你做所有事情都要围绕着这句话来,用行动和言语告诉他的妈妈,能。 1、聊天:回答 对方肯定会各种夸自己儿子好。这个时候你就颔首微笑,适时点头,顺着话头也各种明着暗着的夸。一定要笑哦,这是超级加分项。 会说自己老公和家庭。我功力不是很够,很多时候也不能完全分析出来应该怎么应对,但是这种情况总归是要告诉你我们家里很好很和谐。我们也吵架也有不好的时候两人包容就过来了。潜台词就是告诉你一些生活和婚姻的道理。你一定要及时的说,阿姨说的太对了,我也是这么觉得。应该向你们学习之类的,一定要说的及时而真诚啊。 问你和你家庭的情况。这个因人而异,不刻意欺骗,但也挑好的说,别实在的都一咕噜都说了。多留点话题吧,来日方长呢。 2、聊天:发问 问问健康啊,日常生活之类的。 问问男朋友小时候啊什么的,尽量把气氛搞得轻松加愉快。 问问喜好之类的。 问问对方一些生活技能。 3、聊天:夸 夸对方年轻、漂亮、气质、有文化等等 夸手艺啊生活技能高啊之类的 夸男朋友好啊,夸对方教育的好啊等等 聊天的时候,要先少说话,多观察。了解清楚对方父母是话多的还是话少的人之后再确定自己怎么去应对比较稳妥。实在不知道要说什么就多笑多点头多认同,这是真理。如果在吃饭,没话说的时候就给一家人添茶倒水。或者说“阿姨多吃点,这个好吃”之类的。反正别冷场就好。 如果对基本礼仪不熟悉,那么自己去百度下,别失礼就ok了。 4、做事 记得之前看过一个帖子,说不要第一次去男方家太殷勤,不然以后结婚没有地位什么的。我不太赞同那篇文章里的观点。 我还是觉得去对方家,对方款待你,人家家人辛辛苦苦的给你做了饭,你去人家家洗个碗怎么了?不应该吗? 大多数时候你说帮忙做个饭、洗个碗,是个态度问题。对方通常不会让你做,就让你出去客厅做着了,但是这话你一定要说,哪怕真让你打个下手择个菜、端个碗的也没什么啊。告诫姑娘们,去男朋友家提前做好做家务的准备吧,生活永远比想象来的丰富,下一秒会发生什么难说。 三、事后 收尾工作要做好啊,不然就虎头蛇尾了。 通常我会给对方家里发个语音道谢。主要的内容包括,谢谢对方的款待或者邀请,其次呢表达一下今天吃的很开心、聊天很投机之类的、最后可以邀请对方一起吃个饭啊或者下次一起出去玩之类的话。这叫礼尚往来,有来有往。一方面放对方了解了你的态度,还顺便约了下一次的见面,一举两得,又不花费什么成本何乐不为。 ps:还有呢,现在是移动互联网时代了么,对方服务如果微信神马的用的,那么就果断扫一扫,或者回去之后问男友要上对方服父母的微信号,没事去点个赞什么的。这是我最近得出的心得。 ps:其实再详细的攻略都涵盖不了生活。攻略不是教你虚伪,也不是教你敷衍,恰恰是告诉你用真心实意去对待,但是要有智慧的善良和善真诚的对待一些重要的事情。我最后祝每个妹纸都能够获得自己想要的幸福~ 第一次拜见家长补充攻略 【写在前面】 相信今年春节一定有不少人们都借着春节的春风见了双方家长,哈哈,感觉一定“很好”吧。初登恋人家门,就像参加一场考试,既要注意服饰仪表,又要小心言谈举止。虽然现在是婚恋自由的年代,但家长的意见也值得参考,因此,我觉得,做好一些准备,给长辈留下美好的第一印象很有必要。细节决定成败,我下面来奉献自己的第一次拜见家长的攻略,让我们从打扮、语言、神情、动作各个击破吧! 【穿着要得体】 俗话说的好,人靠衣装马靠鞍。见面的第一印象,都是从穿着上体现的。想要TA的家人看你顺眼,你也要迎合TA家人的口味。所以在见家长之前,一定要和TA沟通好,问清TA家人的接受程度在做穿着上的考虑。 如果对方父母思想保守,或有宗教信仰,那你就选择简单、干净的衣服,让那些破洞牛仔裤、金属T恤、超短裙待在衣柜里吧。 如果对方父母时尚前卫,思想开明,那就如自己喜欢的那样(但也不要太随意,运动裤和睡衣通常都不是一个好主意)。 如果对方家境殷实,可选择质地好的高档服装。小贴士:男生一定要打理好自己的小细节。例如,出行前去理发,把胡子刮干净,注意鼻部卫生(嘎嘎),换上一双干净的袜子等。如果穿了衬衫,应不时查看一下领口和袖口有没有脏污,长途旅行很难保证服饰的干净,所以,最好穿深色的衣服或者备一套新装。 【礼物:合意加上心意】 我觉得,送礼物给长辈,合适的才是好的,也最能体现送礼人的心意。拜访对方父母前,可先了解清楚对方家人的兴趣爱好。我有一个女同事讲他第一次到男朋友家时,送了一套钓鱼杂志和一袋外国品种的玫瑰种子。原来,她了解到,男友的父亲是钓鱼爱好者,想在年底续订来年的杂志,而母亲则喜欢园艺,很想种一些稀有花卉。根据这些线索,女同事挑选到了让未来公婆爱不释手的见面礼。小熊觉得说,如果没有好的切入点的话,就选择一些应季的实用品。像现在这个季节送羊绒衫、羊毛围巾、呢帽、保暖内衣、保暖小家电等都是不错的选择。小贴士:值得注意的是,如果要把保健品当作礼物,最好提前了解对方父母的健康状况,以免弄巧成拙。 【说话,要有礼貌】 我第一次见家长的时候,非常的紧张,因为之前一点心理准备都没有,是妈妈把我“骗”过去的。快见到男朋友家长之前,妈妈就对我说,如果你害怕说错话,一言不发,别人会觉得你性格内向,不容易相处。但是,你若不管别人说什么都滔滔不绝地抢话,也不会受欢迎。那我们究竟应该怎么做呢?小熊经过一段时间的总结后就明白了,当对方父母提问时,态度要恭敬,声音略低一些,柔和些。把话一句句,一层层说清楚。当长辈想要插话的时候,你要虚心倾听。当然初次见面,双方难免感到拘束。寒暄之后,常会冷场。找到共同点,这些初步的了解将能派上用场。 父母对运动感兴趣么?如果你们有相同的运动爱好,这将是个很好的话题。 地域渊源。你或你的家人,是否和对方父母来自同一个地方?有无类似的生活经历? 就地取材。仔细观察墙壁、窗台、桌子等,墙上的名人字画,窗台上的花卉,桌上的各种小摆设,都可借来入话。因为它们往往体现了主人的情趣爱好,能使主人讲出许多愉快的话来。当然,也可以从对方最熟悉的入手,可以谈谈新闻,聊聊天气。 【吃饭,注意用餐礼仪】 我在这里特别提醒,对方父母请你家宴,说明他们对你有了初步好感,你切不可得意忘形,这很有可能是一次非常近距离的接触。因为从饭局能看出一个人的许多品性和性格,孰优孰劣一目了然。比如,女生吃饭要细嚼慢咽、夹菜不能满盘乱翻。咀嚼的时候,更不要吧嗒吧嗒。另外切记,当你在席间满口塞满饭菜,还滔滔不绝讲话的时候,你的形象分已经被减去好多。男生,你无论多饿,还是一口口慢慢吃。不要给自己堆好一大碗饭菜,然后埋头猛往嘴巴里面填。这样的行为,会显得你很粗俗。 吃饭过程还要给家长多端茶倒水,男生要给女友夹菜,这些细节,家长都会看在眼里的。 小贴士:由于来赴宴是“醉翁之意不在酒”,切莫贪杯,喝个酩酊大醉,美好姻缘可能会因你一醉方休而毁于一旦。小熊有一个老家的表哥就是因为贪杯,经不住女方家人的轮番劝酒,自己的酒量又有限,知道自己酒量有限还充面子海喝,结果喝得自己都不知道怎么回家的。没过两天,女方那边带话过来就把这门亲事给吹了。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/80738819 本文对今天蚂蚁金服面试中的几个问题进行简单阐述分析,望批评指正。 搜索DAG问题 问题描述:给定一个图,寻找出里面所有的有向无环图(DAG)。 问题分析: 有向无环图 在图论中,如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图(DAG图)。 解题思路 给定一个图,如下: 该图包括多个DAG有向无环图。 于是,对于整个算法,我们使用两个阶段来完成: 第一阶段:计算每个节点到其他节点的有向路径。为了防止成环,我们需要在合并时进行成环检测。 代码如下: public class GraphDAGFinder { private Map<Node, NodeDAGList> nodeDAGListMap = new HashMap<>(); public void findAllDAGinGraph(Set<Node> graphNodes) { for (Node node : graphNodes) { NodeDAGList dagList = this.findAllDAGbyNode(node); System.out.println("NODE [" + node.getId() + "] DAGS = "); for (NodeDAGItem item : dagList.getAllDagItems()) { item.printDAG(); } System.out.println("-------------------------------"); } } private NodeDAGList findAllDAGbyNode(Node n) { NodeDAGList dagList = this.nodeDAGListMap.get(n); if (dagList != null) return dagList; dagList = new NodeDAGList(); NodeDAGItem item = new NodeDAGItem(); item.addNode(n); dagList.addNodeDAGItem(item); if (n.getNextHops().isEmpty()) { return dagList; } for (Node nd : n.getNextHops()) { NodeDAGList tmplist = this.findAllDAGbyNode(nd); for (NodeDAGItem tmpit : tmplist.getAllDagItems()) { if (tmpit.hasNode(n)) continue; NodeDAGItem itm = tmpit.copyDAG(); itm.addNode(n); dagList.addNodeDAGItem(itm); } } this.nodeDAGListMap.put(n, dagList); return dagList; } } 第二阶段:路径合并成图。有了每个节点到其他节点的路径,我们可以对其所有路径进行合并,合并的规则是节点顺序不能交叉,否则会导致成环。在这里,为了合并路径,需要对不同的路径进行组合,然后再合并。 组合的代码如下,主要是寻找出对应路径集合的ID组合: private static Set<List<Integer>> computeCombs(List<Integer> idxs) { Set<List<Integer>> combinations = new HashSet<>(); List<Integer> comb = new ArrayList<>(); for (int i = 1; i < idxs.size(); i++) { getNCombs(nodes, 0, i, comb, combinations); } return combinations; } private static void getNCombs(List<Integer> idxs, int index, int len, List<Integer> comb, Set<List<Integer>> combinations) { if (len == 0) { combinations.add(comb); return; } if (index == idxs.size()) return; comb.add(idxs.get(index)); getNCombs(idxs, index + 1, len - 1, comb, combinations); comb.remove(idxs.get(index)); getNCombs(idxs, index + 1, len, comb, combinations); } 合并流程如下: 从源节点开始,所有路径向后遍历,直到出现交叉,此时,选择某一分支停止向后遍历。以此类推,直到所有分支全部停止或遍历完成。 这里的合并可能有多个结果,统一存储。 最后的结果如下所示: 后续工作 对于整个问题,我们这里采用了两阶段的处理过程,能够得到结果,但是对于大规模节点图,仍然存在时间复杂度较高的问题。因此,后续工作可以围绕如何进一步降低搜索空间的方式来进行进一步的优化。比如,换一个维度,从边入手,而不是从节点入手;又如,使用回溯非递归+全局变量存储的方式来代替现有的递归方案,从而进一步减少栈溢出错误;再如,查阅论文文献,参考别人的算法,理解吃透优化。 脑裂问题 摘自CSDN博客: 在高可用HA系统中,当联系两个节点的心跳线断开时,原本为一个整体的动作协调的HA系统,就会分裂成为两个独立的个体。这时,由于两个节点相互失去了联系,都以为是对方出现了故障,两个节点上的HA软件就会像裂脑人一样,争夺共享资源,争相提供应用服务,这个时候就会出现严重后果: 共享资源被瓜分,两个节点的服务都起不来; 两个节点的服务都起来了,但同时读写共享资源,导致数据损坏。 一般来说,脑裂问题产生的原因主要包括下面几个: 高可用服务器之间的心跳线发生故障,导致无法正常通信; 高可用服务器开启了iptables防火墙,阻挡了心跳消息传输; 高可用服务器上网卡地址等信息配置不正确,导致发送心跳消息失败。 可以认为,在集群或者主备模式下,由于网络原因导致节点之间不可达,此时,每个网络分区就会自主选择出master节点,从而造成原来的集群存在多个master节点。这些master节点会争夺共享资源,从而导致系统出现服务不可用或资源损坏等问题。 解决方案: 添加冗余的心跳线,如双心跳线,尽量减少“脑裂”的发生几率。 设置仲裁机制,当心跳线完全断开时,两个节点相互ping一下参考IP,不通则表示断点就出在本端,此时可自动重启,从而彻底释放可能还占用的共享资源。 增加节点至奇数个,通过raft或paxos协议来进行投票选举主节点。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/80557262 一、epoll简介 epoll是当前在Linux下开发大规模并发网络程序的热门选择,epoll在Linux2.6内核中正式引入,和select相似,都是IO多路复用(IO multiplexing)技术。 按照man手册的说法,epoll是为处理大批量句柄而做了改进的poll。 Linux下有以下几个经典的服务器模型: 1、PPC模型和TPC模型 PPC(Process Per Connection)模型和TPC(Thread Per Connection)模型的设计思想类似,就是给每一个到来的连接都分配一个独立的进程或者线程来服务。对于这两种模型,其需要耗费较大的时间和空间资源。当管理连接数较多时,进程或线程的切换开销较大。因此,这类模型能接受的最大连接数都不会高,一般都在几百个左右。 2、select模型 对于select模型,其主要有以下几个特点: 最大并发数限制:由于一个进程所打开的fd(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此,select模型的最大并发数就被限制了。 效率问题:每次进行select调用都会线性扫描全部的fd集合。这样,效率就会呈现线性下降。 内核/用户空间内存拷贝问题:select在解决将fd消息传递给用户空间时采用了内存拷贝的方式。这样,其处理效率不高。 3、poll模型 对于poll模型,其虽然解决了select最大并发数的限制,但依然没有解决掉select的效率问题和内存拷贝问题。 4、epoll模型 对比于其他模型,epoll做了如下改进: 支持一个进程打开较大数目的文件描述符(fd) select模型对一个进程所打开的文件描述符是有一定限制的,其由FD_SETSIZE设置,默认为1024/2048。这对于那些需要支持上万连接数目的高并发服务器来说显然太少了,这个时候,可以选择两种方案:一是可以选择修改FD_SETSIZE宏然后重新编译内核,不过这样做也会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然Linux中创建线程的代价比较小,但仍然是不可忽视的,加上进程间数据同步远不及线程间同步的高效,所以也不是一种完美的方案。 但是,epoll则没有对描述符数目的限制,它所支持的文件描述符上限是整个系统最大可以打开的文件数目,例如,在1GB内存的机器上,这个限制大概为10万左右。 IO效率不会随文件描述符(fd)的增加而线性下降 传统的select/poll的一个致命弱点就是当你拥有一个很大的socket集合时,不过任一时间只有部分socket是活跃的,select/poll每次调用都会线性扫描整个socket集合,这将导致IO处理效率呈现线性下降。 但是,epoll不存在这个问题,它只会对活跃的socket进行操作,这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的。因此,只有活跃的socket才会主动去调用callback函数,其他idle状态socket则不会。在这一点上,epoll实现了一个伪AIO,其内部推动力在内核。 在一些benchmark中,如果所有的socket基本上都是活跃的,如高速LAN环境,epoll并不比select/poll效率高,相反,过多使用epoll_ctl,其效率反而还有稍微下降。但是,一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。 使用mmap加速内核与用户空间的消息传递 无论是select,poll还是epoll,它们都需要内核把fd消息通知给用户空间。因此,如何避免不必要的内存拷贝就很重要了。对于该问题,epoll通过内核与用户空间mmap同一块内存来实现。 内核微调 这一点其实不算epoll的优点了,而是整个Linux平台的优点,Linux赋予开发者微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么,可以在运行期间动态调整这个内存池大小(skb_head_pool)来提高性能,该参数可以通过使用echo xxxx > /proc/sys/net/core/hot_list_length来完成。再如,可以尝试使用最新的NAPI网卡驱动架构来处理数据包数量巨大但数据包本身很小的特殊场景。 二、epoll API epoll只有epoll_create、epoll_ctl和epoll_wait这三个系统调用。其定义如下: #include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 1、epoll_create #include <sys/epoll.h> int epoll_create(int size); 可以调用epoll_create方法创建一个epoll的句柄。 需要注意的是,当创建好epoll句柄后,它就会占用一个fd值。在使用完epoll后,必须调用close函数进行关闭,否则可能导致fd被耗尽。 2、epoll_ctl #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数,它不同于select是在监听事件时告诉内核要监听什么类型的事件,而是通过epoll_ctl注册要监听的事件类型。 第一个参数epfd:epoll_create函数的返回值。 第二个参数events:表示动作类型。有三个宏来表示: * EPOLL_CTL_ADD:注册新的fd到epfd中; * EPOLL_CTL_MOD:修改已经注册的fd的监听事件; * EPOLL_CTL_DEL:从epfd中删除一个fd。 第三个参数fd:需要监听的fd。 第四个参数event:告诉内核需要监听什么事件。 struct epoll_event结构如下所示: // 保存触发事件的某个文件描述符相关的数据 typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; // 感兴趣的事件和被触发的事件 struct epoll_event { __uint32_t events; // Epoll events epoll_data_t data; // User data variable }; 如上所示,对于Epoll Events,其可以是以下几个宏的集合: EPOLLIN:表示对应的文件描述符可读(包括对端Socket); EPOLLOUT:表示对应的文件描述符可写; EPOLLPRI:表示对应的文件描述符有紧急数据可读(带外数据); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET:将EPOLL设为边缘触发(Edge Triggered),这是相对于水平触发(Level Triggered)而言的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要再次 3、epoll_wait #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 收集在epoll监控的事件中已经发生的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据赋值到这个event数组中,不会去帮助我们在用户态分配内存)。maxevents告诉内核这个events数组有多大,这个maxevents的值不能大于创建epoll_create时的size。参数timeout是超时时间(毫秒)。如果函数调用成功,则返回对应IO上已准备好的文件描述符数目,如果返回0则表示已经超时。 三、epoll工作模式 1. LT模式(Level Triggered,水平触发) 该模式是epoll的缺省工作模式,其同时支持阻塞和非阻塞socket。内核会告诉开发者一个文件描述符是否就绪,如果开发者不采取任何操作,内核仍会一直通知。 2. ET模式(Edge Triggered,边缘触发) 该模式是一种高速处理模式,当且仅当状态发生变化时才会获得通知。在该模式下,其假定开发者在接收到一次通知后,会完整地处理该事件,因此内核将不再通知这一事件。注意,缓冲区中还有未处理的数据不能说是状态变化,因此,在ET模式下,开发者如果只读取了一部分数据,其将再也得不到通知了。正确的做法是,开发者自己确认读完了所有的字节(一直调用read/write直到出错EAGAGIN为止)。 Nginx默认采用的就是ET(边缘触发)。 四、epoll高效性探讨 epoll的高效性主要体现在以下三个方面: (1)select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用,这意味着每次调用select/poll时都要将fd列表从用户空间拷贝到内核,当fd数目很多时,这会造成性能低效。对于epoll_wait,每次调用epoll_wait时,其不需要将fd列表传递给内核,epoll_ctl不需要每次都拷贝所有的fd列表,只需要进行增量式操作。因此,在调用epoll_create函数之后,内核已经在内核开始准备数据结构用于存放需要监控的fd了。其后,每次epoll_ctl只是对这个数据结构进行简单的维护操作即可。 (2)内核使用slab机制,为epoll提供了快速的数据结构。在内核里,一切都是文件。因此,epoll向内核注册了一个文件系统,用于存储所有被监控的fd。当调用epoll_create时,就会在这个虚拟的epoll文件系统中创建一个file节点。epoll在被内核初始化时,同时会分配出epoll自己的内核告诉cache区,用于存放每个我们希望监控的fd。这些fd会以红黑树的形式保存在内核cache里,以支持快速查找、插入和删除。这个内核高速cache,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好想要的size的内存对象,每次使用时都使用空闲的已分配好的对象。 (3)当调用epoll_ctl往epfd注册百万个fd时,epoll_wait仍然能够快速返回,并有效地将发生的事件fd返回给用户。原因在于,当我们调用epoll_create时,内核除了帮我们在epoll文件系统新建file节点,同时在内核cache创建红黑树用于存储以后由epoll_ctl传入的fd外,还会再建立一个list链表,用于存储准备就绪的事件。当调用epoll_wait时,仅仅观察这个list链表中有无数据即可。如果list链表中有数据,则返回这个链表中的所有元素;如果list链表中没有数据,则sleep然后等到timeout超时返回。所以,epoll_wait非常高效,而且,通常情况下,即使我们需要监控百万计的fd,但大多数情况下,一次也只返回少量准备就绪的fd而已。因此,每次调用epoll_wait,其仅需要从内核态复制少量的fd到用户空间而已。那么,这个准备就绪的list链表是怎么维护的呢?过程如下:当我们执行epoll_ctl时,除了把fd放入到epoll文件系统里file对象对应的红黑树之外,还会给内核中断处理程序注册一个回调函数,其告诉内核,如果这个fd的中断到了,就把它放到准备就绪的list链表中。 如此,一棵红黑树、一张准备就绪的fd链表以及少量的内核cache,就帮我们解决了高并发下fd的处理问题。 总结一下: 执行epoll_create时,创建了红黑树和就绪list链表; 执行epoll_ctl时,如果增加fd,则检查在红黑树中是否存在,存在则立即返回,不存在则添加到红黑树中,然后向内核注册回调函数,用于当中断事件到来时向准备就绪的list链表中插入数据。 执行epoll_wait时立即返回准备就绪链表里的数据即可。 五、epoll源码分析 eventpoll_init过程: static int __init eventpoll_init(void) { int error; init_MUTEX(&epsem); /* Initialize the structure used to perform safe poll wait head wake ups */ ep_poll_safewake_init(&psw); /* Allocates slab cache used to allocate "struct epitem" items */ epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); /* Allocates slab cache used to allocate "struct eppoll_entry" */ pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); /* * Register the virtual file system that will be the source of inodes * for the eventpoll files */ error = register_filesystem(&eventpoll_fs_type); if (error) goto epanic; /* Mount the above commented virtual file system */ eventpoll_mnt = kern_mount(&eventpoll_fs_type); error = PTR_ERR(eventpoll_mnt); if (IS_ERR(eventpoll_mnt)) goto epanic; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: successfully initialized.\n", current)); return 0; epanic: panic("eventpoll_init() failed\n"); } 其中,epoll用slab分配器kmem_cache_create分配内存用于存放struct epitem和struct eppoll_entry。 当向系统中添加一个fd时,就会创建一个epitem结构体,这是内核管理epoll的基本数据结构: /* * Each file descriptor added to the eventpoll interface will * have an entry of this type linked to the hash. */ struct epitem { /* RB-Tree node used to link this structure to the eventpoll rb-tree */ struct rb_node rbn; // 用于主结构管理的红黑树 /* List header used to link this structure to the eventpoll ready list */ struct list_head rdllink; // 事件就绪队列 /* The file descriptor information this item refers to */ struct epoll_filefd ffd; // 用于主结构中的链表 /* Number of active wait queue attached to poll operations */ int nwait; // 事件个数 /* List containing poll wait queues */ struct list_head pwqlist; // 双向链表,保存着被监控文件的等待队列 /* The "container" of this item */ struct eventpoll *ep; // 该项属于哪个主结构体 /* The structure that describe the interested events and the source fd */ struct epoll_event event; // 注册的感兴趣的时间 /* * Used to keep track of the usage count of the structure. This avoids * that the structure will desappear from underneath our processing. */ atomic_t usecnt; /* List header used to link this item to the "struct file" items list */ struct list_head fllink; /* List header used to link the item to the transfer list */ struct list_head txlink; /* * This is used during the collection/transfer of events to userspace * to pin items empty events set. */ unsigned int revents; }; 对于每个epfd,其对应的数据结构为: /* * This structure is stored inside the "private_data" member of the file * structure and rapresent the main data sructure for the eventpoll * interface. */ struct eventpoll { /* Protect the this structure access */ rwlock_t lock; /* * This semaphore is used to ensure that files are not removed * while epoll is using them. This is read-held during the event * collection loop and it is write-held during the file cleanup * path, the epoll file exit code and the ctl operations. */ struct rw_semaphore sem; /* Wait queue used by sys_epoll_wait() */ wait_queue_head_t wq; /* Wait queue used by file->poll() */ wait_queue_head_t poll_wait; /* List of ready file descriptors */ struct list_head rdllist; // 准备就绪的事件链表 /* RB-Tree root used to store monitored fd structs */ struct rb_root rbr; // 用于管理所有fd的红黑树(根节点) }; eventpoll在epoll_create时创建: /* * It opens an eventpoll file descriptor by suggesting a storage of "size" * file descriptors. The size parameter is just an hint about how to size * data structures. It won't prevent the user to store more than "size" * file descriptors inside the epoll interface. It is the kernel part of * the userspace epoll_create(2). */ asmlinkage long sys_epoll_create(int size) { int error, fd; struct eventpoll *ep; struct inode *inode; struct file *file; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d)\n", current, size)); /* * Sanity check on the size parameter, and create the internal data * structure ( "struct eventpoll" ). */ error = -EINVAL; if (size <= 0 || (error = ep_alloc(&ep)) != 0) // ep_alloc为eventpoll分配内存并初始化 goto eexit_1; /* * Creates all the items needed to setup an eventpoll file. That is, * a file structure, and inode and a free file descriptor. */ error = ep_getfd(&fd, &inode, &file, ep); // 创建于eventpoll相关的数据结构,包括file、inode和fd等信息 if (error) goto eexit_2; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n", current, size, fd)); return fd; eexit_2: ep_free(ep); kfree(ep); eexit_1: DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n", current, size, error)); return error; } 如上,内核中维护了一棵红黑树,大致结构如下: 下面是epoll_ctl函数过程: /* * The following function implements the controller interface for * the eventpoll file that enables the insertion/removal/change of * file descriptors inside the interest set. It represents * the kernel part of the user space epoll_ctl(2). */ asmlinkage long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event) { int error; struct file *file, *tfile; struct eventpoll *ep; struct epitem *epi; struct epoll_event epds; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p)\n", current, epfd, op, fd, event)); error = -EFAULT; if (ep_op_hash_event(op) && copy_from_user(&epds, event, sizeof(struct epoll_event))) goto eexit_1; /* Get the "struct file *" for the eventpoll file */ error = -EBADF; file = fget(epfd); // 获取epfd对应的文件 if (!file) goto eexit_1; /* Get the "struct file *" for the target file */ tfile = fget(fd); // 获取fd对应的文件 if (!tfile) goto eexit_2; /* The target file descriptor must support poll */ error = -EPERM; if (!tfile->f_op || !tfile->f_op->poll) goto eexit_3; /* * We have to check that the file structure underneath the file descriptor * the user passed to us _is_ an eventpoll file. And also we do not permit * adding an epoll file descriptor inside itself. */ error = -EINVAL; if (file == tfile || !is_file_epoll(file)) goto eexit_3; /* * At this point it is safe to assume that the "private_data" contains * our own data structure. */ ep = file->private_data; down_write(&ep->sem); /* Try to lookup the file inside our hash table */ epi = ep_find(ep, tfile, fd); // 在哈希表中查询,防止重复添加 error = -EINVAL; switch (op) { case EPOLL_CTL_ADD: // 添加节点,调用ep_insert函数 if (!epi) { epds.events |= POLLERR | POLLHUP; error = ep_insert(ep, &epds, tfile, fd); } else error = -EEXIST; break; case EPOLL_CTL_DEL: // 删除节点,调用ep_remove函数 if (epi) error = ep_remove(ep, epi); else error = -ENOENT; break; case EPOLL_CTL_MOD: // 修改节点,调用ep_modify函数 if (epi) { epds.events |= POLLERR | POLLHUP; error = ep_modify(ep, epi, &epds); } else error = -ENOENT; break; } /* * The function ep_find() increments the usage count of the structure * so, if this is not NULL, we need to release it. */ if (epi) ep_release_epitem(epi); up_write(&ep->sem); eexit_3: fput(tfile); eexit_2: fput(file); eexit_1: DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p) = %d\n", current, epfd, op, fd, event, error)); return error; } 对于ep_insert函数,基本代码如下: static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd) { int error, revents, pwake = 0; unsigned long flags; struct epitem *epi; struct ep_pqueue epq; error = -ENOMEM; // 分配一个epitem结构体来保存每个加入的fd if (!(epi = kmem_cache_alloc(epi_cache, SLAB_KERNEL))) goto eexit_1; /* Item initialization follow here ... */ // 初始化结构体 ep_rb_initnode(&epi->rbn); INIT_LIST_HEAD(&epi->rdllink); INIT_LIST_HEAD(&epi->fllink); INIT_LIST_HEAD(&epi->txlink); INIT_LIST_HEAD(&epi->pwqlist); epi->ep = ep; ep_set_ffd(&epi->ffd, tfile, fd); epi->event = *event; atomic_set(&epi->usecnt, 1); epi->nwait = 0; /* Initialize the poll table using the queue callback */ epq.epi = epi; // 安装poll回调函数 init_poll_funcptr(&epq.pt, ep_ptable_queue_proc); /* * Attach the item to the poll hooks and get current event bits. * We can safely use the file* here because its usage count has * been increased by the caller of this function. */ // 将当前item添加至poll hook中,然后获取当前event位 revents = tfile->f_op->poll(tfile, &epq.pt); /* * We have to check if something went wrong during the poll wait queue * install process. Namely an allocation for a wait queue failed due * high memory pressure. */ if (epi->nwait < 0) goto eexit_2; /* Add the current item to the list of active epoll hook for this file */ spin_lock(&tfile->f_ep_lock); list_add_tail(&epi->fllink, &tfile->f_ep_links); spin_unlock(&tfile->f_ep_lock); /* We have to drop the new item inside our item list to keep track of it */ write_lock_irqsave(&ep->lock, flags); /* Add the current item to the rb-tree */ ep_rbtree_insert(ep, epi); /* If the file is already "ready" we drop it inside the ready list */ if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) { list_add_tail(&epi->rdllink, &ep->rdllist); /* Notify waiting tasks that events are available */ if (waitqueue_active(&ep->wq)) wake_up(&ep->wq); if (waitqueue_active(&ep->poll_wait)) pwake++; } write_unlock_irqrestore(&ep->lock, flags); /* We have to call this outside the lock */ if (pwake) ep_poll_safewake(&psw, &ep->poll_wait); DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_insert(%p, %p, %d)\n", current, ep, tfile, fd)); return 0; eexit_2: ep_unregister_pollwait(ep, epi); /* * We need to do this because an event could have been arrived on some * allocated wait queue. */ write_lock_irqsave(&ep->lock, flags); if (ep_is_linked(&epi->rdllink)) ep_list_del(&epi->rdllink); write_unlock_irqrestore(&ep->lock, flags); kmem_cache_free(epi_cache, epi); eexit_1: return error; } 其中,init_poll_funcptr和tfile->f_op->poll将ep_ptable_queue_proc注册到epq.pt中的qproc中。 ep_ptable_queue_proc函数设置了等待队列的ep_poll_callback回调函数。在设备硬件数据到来时,硬件中断函数唤醒该等待队列上等待的进程时,会调用唤醒函数ep_poll_callback。 ep_poll_callback函数主要的功能是将被监视文件的等待事件就绪时,将文件对应的epitem实例添加到就绪队列中,当用户调用epoll_wait时,内核会将就绪队列中的事件报告给用户。 epoll_wait的实现如下: /* * Implement the event wait interface for the eventpoll file. It is the kernel * part of the user space epoll_wait(2). */ asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events, int maxevents, int timeout) { int error; struct file *file; struct eventpoll *ep; DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_wait(%d, %p, %d, %d)\n", current, epfd, events, maxevents, timeout)); /* The maximum number of event must be greater than zero */ if (maxevents <= 0 || maxevents > MAX_EVENTS) // 检查maxevents参数 return -EINVAL; /* Verify that the area passed by the user is writeable */ // 检查用户空间传入的events指向的内存是否可写 if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) { error = -EFAULT; goto eexit_1; } /* Get the "struct file *" for the eventpoll file */ error = -EBADF; file = fget(epfd); // 获取epfd对应的eventpoll文件的file实例,file结构是在epoll_create中创建的 if (!file) goto eexit_1; /* * We have to check that the file structure underneath the fd * the user passed to us _is_ an eventpoll file. */ error = -EINVAL; if (!is_file_epoll(file)) goto eexit_2; /* * At this point it is safe to assume that the "private_data" contains * our own data structure. */ ep = file->private_data; /* Time to fish for events ... */ // 核心处理函数 error = ep_poll(ep, events, maxevents, timeout); eexit_2: fput(file); eexit_1: DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_wait(%d, %p, %d, %d) = %d\n", current, epfd, events, maxevents, timeout, error)); return error; } 其中,调用ep_poll函数,具体流程如下: static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout) { int res, eavail; unsigned long flags; long jtimeout; wait_queue_t wait; /* * Calculate the timeout by checking for the "infinite" value ( -1 ) * and the overflow condition. The passed timeout is in milliseconds, * that why (t * HZ) / 1000. */ jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ? MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000; retry: write_lock_irqsave(&ep->lock, flags); res = 0; if (list_empty(&ep->rdllist)) { /* * We don't have any available event to return to the caller. * We need to sleep here, and we will be wake up by * ep_poll_callback() when events will become available. */ init_waitqueue_entry(&wait, current); add_wait_queue(&ep->wq, &wait); for (;;) { /* * We don't want to sleep if the ep_poll_callback() sends us * a wakeup in between. That's why we set the task state * to TASK_INTERRUPTIBLE before doing the checks. */ set_current_state(TASK_INTERRUPTIBLE); if (!list_empty(&ep->rdllist) || !jtimeout) break; if (signal_pending(current)) { res = -EINTR; break; } write_unlock_irqrestore(&ep->lock, flags); jtimeout = schedule_timeout(jtimeout); write_lock_irqsave(&ep->lock, flags); } remove_wait_queue(&ep->wq, &wait); set_current_state(TASK_RUNNING); } /* Is it worth to try to dig for events ? */ eavail = !list_empty(&ep->rdllist); write_unlock_irqrestore(&ep->lock, flags); /* * Try to transfer events to user space. In case we get 0 events and * there's still timeout left over, we go trying again in search of * more luck. */ if (!res && eavail && !(res = ep_events_transfer(ep, events, maxevents)) && jtimeout) goto retry; return res; } ep_send_events函数用于向用户空间发送就绪事件。ep_send_events函数将用户传入的内存简单封装到ep_send_events_data结构中,然后调用ep_scan_ready_list将就绪队列中的事件传入用户空间的内存。 六、参考 Epoll详解及源码分析——CSDN博客
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/80557158 一、TCP四次挥手过程 TCP在建立连接时需要握手,同理,在关闭连接的时候也需要握手。 具体如下所示: 由于TCP连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭,后发送FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留2MSL时长。 对于MSL,其指的是报文段的最大生存时间。如果报文段在网络中活动了MSL时间,还没有被接收,那么就会被丢弃。关于MSL的大小,RFC 793协议中给出的建议是2分钟,不过Linux中,通常是半分钟。 对于TIME_WAIT状态,有下图: 我们关注几个概念: TIME_WAIT的产生条件:主动关闭方在发送四次挥手的最后一个ACK后会变为TIME_WAIT状态,持续时间为2MSL(Linux中一个MSL是30秒,是不可配置的)。 TIME_WAIT持续两个MSL的作用:首先,可靠安全地关闭TCP连接。比如网络拥塞,如果主动关闭方最后一个ACK没有被被动关闭方接收到,这时被动关闭方会对FIN进行超时重传,在这时尚未关闭的TIME_WAIT就会把这些尾巴问题处理掉,不至于对新连接及其他服务产生影响。其次,防止由于没有持续TIME_WAIT时间导致的新的TCP连接建立起来,延迟的FIN重传包会干扰新的连接。 TIME_WAIT占用的资源:少量内存(大概4K)和一个文件描述符fd。 TIME_WAIT关闭的危害:首先,当网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包到达后会直接影响新的TCP连接;其次,当网络情况不好时,同时没有TIME_WAIT等待时,关闭连接后无新连接,那么当接收到被动方重传或延迟的FIN包后,会给被动方回送一个RST包,可能会影响被动方其他的服务连接。 TCP: time wait bucket table overflow产生原因及影响:原因是当TIME_WAIT数超过了Linux系统的TW数量的阈值。危害是超过阈值后,系统会把多余的TIME_WAIT Socket删除掉,并且显示警告信息。如果是NAT网络且又存在大量访问时,会产生各种连接不稳定断开的情况。 二、相关参数优化调整 1. tcp_tw_recycle 顾名思义就是回收TIME_WAIT连接。可以说这个内核参数已经变成了处理TIME_WAIT的万金油,如果你在网络上搜索TIME_WAIT的解决方案,十有八九会推荐设置它,不过这里隐藏着一个不易察觉的陷阱:当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,由于这些客户端的时间戳可能存在差异,所以从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。参考:tcp_tw_recycle和tcp_timestamps导致connect失败问题。 tcp_tw_recycle = 0 备注:建议不要开启该选项,现在互联网NAT使用很多,可能导致无法进行三次握手。 开启后在3.5*RTO(RTO时间是根据RTT时间计算而来)内回收TIME_WAIT,并60s内同一源ip主机的socket connect请求中的timestamp必须是递增的,对于服务端,同一个源ip可能会是NAT后很多机器,这些机器timestamp递增性无可保证,服务器会拒绝非递增请求连接,直接导致不能三次握手。 2. tcp_tw_reuse 顾名思义就是复用TIME_WAIT连接。当创建新连接的时候,如果可能的话会考虑复用相应的TIME_WAIT连接。通常认为tcp_tw_reuse比tcp_tw_recycle安全一些,这是因为一来TIME_WAIT创建时间必须超过一秒才可能会被复用;二来只有连接的时间戳是递增的时候才会被复用。官方文档里是这样说的:如果从协议视角看它是安全的,那么就可以使用。这简直就是外交辞令啊!按我的看法,如果网络比较稳定,比如都是内网连接,那么就可以尝试使用。 不过需要注意的是在哪里使用,既然我们要复用连接,那么当然应该在连接的发起方使用,而不能在被连接方使用。举例来说:客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端,此类情况使用tcp_tw_reuse是无效的,因为服务端是被连接方,所以不存在复用连接一说。让我们延伸一点来看,比如说服务端是PHP,它查询另一个MySQL服务端,然后主动断开连接,于是TIME_WAIT就落在了PHP一侧,此类情况下使用tcp_tw_reuse是有效的,因为此时PHP相对于MySQL而言是客户端,它是连接的发起方,所以可以复用连接。 说明:如果使用tcp_tw_reuse,请激活tcp_timestamps,否则无效。 tcp_timestamps = 1 tcp_tw_reuse = 1 3. tcp_max_tw_buckets 顾名思义就是控制TIME_WAIT总数。官网文档说这个选项只是为了阻止一些简单的DoS攻击,平常不要人为的降低它。如果缩小了它,那么系统会将多余的TIME_WAIT删除掉,日志里会显示:TCP: time wait bucket table overflow。 需要提醒大家的是物极必反,曾经看到有人把「tcp_max_tw_buckets」设置成0,也就是说完全抛弃TIME_WAIT,这就有些冒险了,用一句围棋谚语来说:入界宜缓。 当出现TCP: time wait bucket table overflow时,尽量调大下面参数: tcp_max_tw_buckets = 256000 三、总结 有时候,如果我们换个角度去看问题,往往能得到四两拨千斤的效果。前面提到的例子:客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端。这里的关键在于主动关闭连接的是服务端!在关闭TCP连接的时候,先出手的一方注定逃不开TIME_WAIT的宿命,套用一句歌词:把我的悲伤留给自己,你的美丽让你带走。如果客户端可控的话,那么在服务端打开KeepAlive,尽可能不让服务端主动关闭连接,而让客户端主动关闭连接,如此一来问题便迎刃而解了。 四、补充 RST简介: 在TCP协议中,RST表示复位,用来关闭异常连接。发送RST包关闭连接时,不必等到缓冲区的数据包都发送出去,直接丢弃缓存区的数据包而发送RST包。接收端在接收到RST包后,不必发送ACK包来确认。 出现RST的几种情况: 端口未打开:客户端连接服务器程序未打开的端口。当客户端向服务器的某个端口发送SYN请求后,但是服务器上并没有打开该端口,因此其会向客户端发送RST。但是,并不是所有的操作系统都会发送RST给客户端,win7就会直接忽略该SYN报文。 在一个已关闭的socket上收到数据:如果某个socket已经关闭,但是依然接收数据,那么也会产生RST。 接收缓冲区还存在数据时,关闭连接:在请求方请求数据,未处理完缓冲区中的所有数据,就请求关闭连接时,请求方不会如预期的发送FIN包,进入4路关闭逻辑,而是可能会直接发送一个RST包强制完成连接的关闭。 参考:https://my.oschina.net/costaxu/blog/127394 参考文献 http://blog.csdn.net/dog250/article/details/13760985 http://blog.sina.com.cn/s/blog_781b0c850100znjd.html https://huoding.com/2013/12/31/316 http://benpaozhe.blog.51cto.com/10239098/1767612
消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。 当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发的Notify、MetaQ、RocketMQ等。 本文不会一一介绍这些消息队列的所有特性,而是探讨一下自主开发设计一个消息队列时,你需要思考和设计的重要方面。过程中我们会参考这些成熟消息队列的很多重要思想。 本文首先会阐述什么时候你需要一个消息队列,然后以Push模型为主,从零开始分析设计一个消息队列时需要考虑到的问题,如RPC、高可用、顺序和重复消息、可靠投递、消费关系解析等。同时,我们也会分析以Kafka为代表的Pull模型所具备的优点。最后是一些高级主题,如用批量/异步提高性能、Pull模型的系统设计理念、存储子系统的设计、流量控制的设计、公平调度的实现等。 一、何时需要消息队列 当你需要使用消息队列时,首先需要考虑它的必要性。可以使用mq的场景有很多,最常用的几种,是做业务解耦/最终一致性/广播/错峰流控等。反之,如果需要强一致性,关注业务逻辑的处理结果,则RPC显得更为合适。 1、解耦 解耦是消息队列要解决的最本质问题。所谓解耦,简单点将就是一个事务,只关心核心流程。而需要依赖其他系统但不那么重要的事情,有通知即可,无需等待结果。换句话说,基于消息的模型,关心的是“通知”,而非“处理”。 比如在美团旅游,我们有一个产品中心,产品中心上游对接的是主站、移动后台、旅游供应链等各个数据源;下游对接的是筛选系统、API系统等展示系统。当上游的数据发生变更的时候,如果不使用消息系统,势必要调用我们的接口来更新数据,就特别依赖产品中心接口的稳定性和处理能力。但其实,作为旅游的产品中心,也许只有对于旅游自建供应链,产品中心更新成功才是他们关心的事情。而对于团购等外部系统,产品中心更新成功也好、失败也罢,并不是他们的职责所在。他们只需要保证在信息变更的时候通知到我们就好了。 而我们的下游,可能有更新索引、刷新缓存等一系列需求。对于产品中心来说,这也不是我们的职责所在。说白了,如果他们定时来拉取数据,也能保证数据的更新,只是实时性没有那么强。但使用接口方式去更新他们的数据,显然对于产品中心来说太过于“重量级”了,只需要发布一个产品ID变更的通知,由下游系统来处理,可能更为合理。 再举一个例子,对于我们的订单系统,订单最终支付成功之后可能需要给用户发送短信积分什么的,但其实这已经不是我们系统的核心流程了。如果外部系统速度偏慢(比如短信网关速度不好),那么主流程的时间会加长很多,用户肯定不希望点击支付过好几分钟才看到结果。那么我们只需要通知短信系统“我们支付成功了”,不一定非要等待它处理完成。 2、最终一致性 最终一致性指的是两个系统的状态保持一致,要么都成功,要么都失败。当然有时间限制,理论上越快越好,但实际上存在各种异常的情况下,可能会有一定的延迟达到最终一致性,但最后两个系统的状态是一样的。 业界有一些为“最终一致性”而生的消息队列,如Notify(阿里)、QMQ(去哪儿)等,其设计初衷,就是为了交易系统中的高可靠通知。 以一个银行的转账过程来理解最终一致性,转账的需求很简单,如果A系统扣钱成功,则B系统加钱一定成功。反之则一起回滚,像什么都没发生一样。 然而,这个过程中存在很多可能的意外: A扣钱成功,调用B加钱接口失败。 A扣钱成功,调用B加钱接口虽然成功,但获取最终结果时网络异常引起超时。 A扣钱成功,B加钱失败,A想回滚扣的钱,但A机器down机。 可见,想把这件看似简单的事真正做成,真的不容易。所有跨VM的一致性问题,从技术的角度通用的解决方案是: 强一致性,分布式事务,但落地太难且成本太高。 最终一致性,主要是用“记录”和“补偿”的方式。在做所有的不确定的事情之前,先把事情记录下来,然后去做不确定的事情,结果可能是:成功、失败或是不确定,“不确定”(例如超时等)可以等价为失败。成功就可以把记录的东西清理掉了,对于失败和不确定,可以依靠定时任务等方式把所有失败的事情重新再做一遍,直到成功为止。 回到刚才的例子,系统在A扣钱成功的情况下,把要给B“通知”这件事记录备案(为了保证最高的可靠性可以把通知B系统加钱和扣钱成功这两件事维护在一个本地事务里),通知成功则删除这条记录,通知失败或不确定则依靠定时任务补偿性地通知我们,直到我们把状态更新成正确的为止。 整个这个模型依然可以基于RPC来做,但可以抽象成一个统一的模型,基于消息队列来做一个“企业总线”。 具体来说,本地事务维护业务变化和通知消息,一起落地(失败则一起回滚),然后RPC到达broker,在broker成功落地后,RPC返回成功,本地消息可以删除。否则本地消息一直靠定时任务轮询不断重发,这样就保证了消息可靠落地broker。 Broker往Consumer发送消息的过程类似,一直发送消息,直到Consumer发送消费成功确认。 我们先不理会重复消息的问题,通过两次消息落地加补偿,下游是一定可以收到消息的。然后依赖状态机版本号等方式做判重,更新自己的业务,就实现了最终一致性。 最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来做最终一致性的事情。另外,所有不保证100%不丢消息的消息队列,理论上无法实现最终一致性。好吧,应该说理论上的100%,排除系统严重故障和bug。 像Kafka一类的设计,在设计层面上就有丢消息的可能(比如定时刷盘,如果掉电就会丢消息)。哪怕只丢千分之一的消息,业务也必须用其他的手段来保证结果正确。 3、广播 消息队列的基本功能之一是进行广播。如果没有消息队列,每当一个新的业务方接入,我们都要联调一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。 比如本文开始提到的产品中心发布产品变更的消息,以及景点库很多去重更新的消息,可能“关心”方有很多个,但产品中心和景点库只需要发布变更消息即可,谁关心谁接入。 4、错峰与流控 试想上下游对于事情的处理能力是不同的。比如,Web前端每秒承受上千万的请求,并不是什么神奇的事情,只需要加多一点机器,再搭建一些LVS负载均衡设备和Nginx等即可。但数据库的处理能力却十分有限,即使使用SSD加分库分表,单机的处理能力仍然在万级。由于成本的考虑,我们不能奢求数据库的机器数量追上前端。 这种问题同样存在于系统和系统之间,如短信系统可能由于短板效应,速度卡在网关上(每秒几百次请求),跟前端的并发量不是一个数量级。但用户晚上个半分钟左右收到短信,一般是不会有太大问题的。如果没有消息队列,两个系统之间通过协商、滑动窗口等复杂的方案也不是说不能实现。但系统复杂性指数级增长,势必在上游或者下游做存储,并且要处理定时、拥塞等一系列问题。而且每当有处理能力有差距的时候,都需要单独开发一套逻辑来维护这套逻辑。所以,利用中间系统转储两个系统的通信内容,并在下游系统有能力处理这些消息的时候,再处理这些消息,是一套相对较通用的方式。 总而言之,消息队列不是万能的。对于需要强事务保证而且延迟敏感的,RPC是优于消息队列的。 对于一些无关痛痒,或者对于别人非常重要但是对于自己不是那么关心的事情,可以利用消息队列去做。 支持最终一致性的消息队列,能够用来处理延迟不那么敏感的“分布式事务”场景,而且相对于笨重的分布式事务,可能是更优的处理方式。 当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。 如果下游有很多系统关心你的系统发出的通知的时候,果断地使用消息队列吧。 二、如何设计一个消息队列 1、综述 我们现在明确了消息队列的使用场景,下一步就是如何设计实现一个消息队列了。 基于消息的系统模型,不一定需要Broker(消息队列服务端)。市面上的的Akka(actor模型)、ZeroMQ等,其实都是基于消息的系统设计范式,但是没有Broker。 我们之所以要设计一个消息队列,并且配备broker,无外乎要做两件事情: 消息的转储,在更合适的时间点投递,或者通过一系列手段辅助消息最终能送达消费者。 规范一种范式和通用的模式,以满足解耦、最终一致性、错峰等需求。 掰开了揉碎了看,最简单的消息队列可以做成一个消息转发器,把一次RPC做成两次RPC。发送者把消息投递到服务端(以下简称Broker),服务端再将消息转发一手到接收端,就是这么简单。 一般来讲,设计消息队列的整体思路是先build一个整体的数据流,例如producer发送给broker,broker发送给consumer,consumer回复消费确认,broker删除/备份消息等。 利用RPC将数据流串起来,然后考虑RPC的高可用性,尽量做到无状态,方便水平扩展。 之后考虑如何承载消息堆积,然后在合适的时机投递消息,而处理堆积的最佳方式,就是存储,存储的选型需要综合考虑性能/可靠性和开发维护成本等诸多因素。 为了实现广播功能,我们必须要维护消费关系,可以利用zookeeper/config server等保存消费关系。 在完成了上述几个功能后,消息队列基本就实现了。然后我们可以考虑一些高级特性,如可靠投递,事务特性,性能优化等。 下面我们会以设计消息队列时重点考虑的模块为主线,穿插灌输一些消息队列的特性实现方法,来具体分析设计实现一个消息队列时的方方面面。 2、实现队列基本功能 RPC通信协议 刚才讲到,所谓消息队列,无外乎两次RPC加一次转储,当然需要消费端最终做消费确认的情况是三次RPC。既然是RPC,就必然牵扯出一系列话题,什么负载均衡啊、服务发现啊、通信协议啊、序列化协议啊,等等。在这一块,我的强烈建议是不要重复造轮子。利用公司现有的RPC框架:Thrift也好,Dubbo也好,或者是其他自定义的框架也好。因为消息队列的RPC,和普通的RPC没有本质区别。当然了,自主利用Memchached或者Redis协议重新写一套RPC框架并非不可(如MetaQ使用了自己封装的Gecko NIO框架,Kafka也用了类似的协议)。但实现成本和难度无疑倍增。排除对效率的极端要求,都可以使用现成的RPC框架。 简单来讲,服务端提供两个RPC服务,一个用来接收消息,一个用来确认消息收到。并且做到不管哪个server收到消息和确认消息,结果一致即可。当然这中间可能还涉及跨IDC的服务的问题。这里和RPC的原则是一致的,尽量优先选择本机房投递。你可能会问,如果producer和consumer本身就在两个机房了,怎么办?首先,broker必须保证感知的到所有consumer的存在。其次,producer尽量选择就近的机房就好了。 高可用 其实所有的高可用,是依赖于RPC和存储的高可用来做的。先来看RPC的高可用,美团的基于MTThrift的RPC框架,阿里的Dubbo等,其本身就具有服务自动发现,负载均衡等功能。而消息队列的高可用,只要保证broker接受消息和确认消息的接口是幂等的,并且consumer的几台机器处理消息是幂等的,这样就把消息队列的可用性,转交给RPC框架来处理了。 那么怎么保证幂等呢?最简单的方式莫过于共享存储。broker多机器共享一个DB或者一个分布式文件/kv系统,则处理消息自然是幂等的。就算有单点故障,其他节点可以立刻顶上。另外failover可以依赖定时任务的补偿,这是消息队列本身天然就可以支持的功能。存储系统本身的可用性我们不需要操太多心,放心大胆的交给DBA们吧! 对于不共享存储的队列,如Kafka使用分区加主备模式,就略微麻烦一些。需要保证每一个分区内的高可用性,也就是每一个分区至少要有一个主备且需要做数据的同步,关于这块HA的细节,可以参考下篇pull模型消息系统设计。 服务端承载消息堆积的能力 消息到达服务端如果不经过任何处理就到接收者了,broker就失去了它的意义。为了满足我们错峰/流控/最终可达等一系列需求,把消息存储下来,然后选择时机投递就显得是顺理成章的了。 只是这个存储可以做成很多方式。比如存储在内存里,存储在分布式kv系统里,存储在磁盘里,存储在数据库里等等。但归结起来,主要有持久化和非持久化两种。 持久化的形式能更大程度地保证消息的可靠性(如断电等不可抗外力),并且理论上能承载更大限度的消息堆积(外存的空间远大于内存)。 但并不是每种消息都需要持久化存储。很多消息对于投递性能的要求大于可靠性的要求,且数量极大(如日志)。这时候,消息不落地直接暂存内存,尝试几次failover,最终投递出去也未尝不可。 市面上的消息队列普遍两种形式都支持。当然具体的场景还要具体结合公司的业务来看。 存储子系统的选择 我们来看看如果需要数据落地的情况下各种存储子系统的选择。理论上,从速度来看,文件系统>分布式KV(持久化)>分布式文件系统>数据库,而可靠性却截然相反。还是要从支持的业务场景出发作出最合理的选择,如果你们的消息队列是用来支持支付/交易等对可靠性要求非常高,但对性能和量的要求没有这么高,而且没有时间精力专门做文件存储系统的研究,DB是最好的选择。 但是DB受制于IOPS,如果要求单broker 5位数以上的QPS性能,基于文件的存储是比较好的解决方案。整体上可以采用数据文件+索引文件的方式处理,具体这块的设计比较复杂。 分布式KV(如MongoDB,HBase)等,或者持久化的Redis,由于其编程接口较友好,性能也比较可观,如果在可靠性要求不是那么高的场景,也不失为一个不错的选择。 消费关系解析 现在我们的消息队列初步具备了转储消息的能力。下面一个重要的事情就是解析发送接收关系,进行正确的消息投递了。 市面上的消息队列定义了一堆让人晕头转向的名词,如JMS 规范中的Topic/Queue,Kafka里面的Topic/Partition/ConsumerGroup,RabbitMQ里面的Exchange等等。抛开现象看本质,无外乎是单播与广播的区别。所谓单播,就是点到点;而广播,是一点对多点。当然,对于互联网的大部分应用来说,组间广播、组内单播是最常见的情形。 消息需要通知到多个业务集群,而一个业务集群内有很多台机器,只要一台机器消费这个消息就可以了。 当然这不是绝对的,很多时候组内的广播也是有适用场景的,如本地缓存的更新等等。另外,消费关系除了组内组间,可能会有多级树状关系。这种情况太过于复杂,一般不列入考虑范围。所以,一般比较通用的设计是支持组间广播,不同的组注册不同的订阅。组内的不同机器,如果注册一个相同的ID,则单播;如果注册不同的ID(如IP地址+端口),则广播。 至于广播关系的维护,一般由于消息队列本身都是集群,所以都维护在公共存储上,如config server、zookeeper等。维护广播关系所要做的事情基本是一致的: 发送关系的维护。 发送关系变更时的通知。 3、队列高级特性设计 上面都是些消息队列基本功能的实现,下面来看一些关于消息队列特性相关的内容,不管可靠投递/消息丢失与重复以及事务乃至于性能,不是每个消息队列都会照顾到,所以要依照业务的需求,来仔细衡量各种特性实现的成本,利弊,最终做出最为合理的设计。 可靠投递(最终一致性) 这是个激动人心的话题,完全不丢消息,究竟可不可能?答案是,完全可能,前提是消息可能会重复,并且,在异常情况下,要接受消息的延迟。 方案说简单也简单,就是每当要发生不可靠的事情(RPC等)之前,先将消息落地,然后发送。当失败或者不知道成功失败(比如超时)时,消息状态是待发送,定时任务不停轮询所有待发送消息,最终一定可以送达。 具体来说: Producer往Broker发送消息之前,需要做一次落地。 请求到server后,server确保数据落地后再告诉客户端发送成功。 支持广播的消息队列需要对每个待发送的endpoint,持久化一个发送状态,直到所有endpoint状态都OK才可删除消息。 对于各种不确定(超时、宕机、消息没有送达、送达后数据没落地、数据落地了回复没收到),其实对于发送方来说,都是一件事情,就是消息没有送达。 重推消息所面临的问题就是消息重复。重复和丢失就像两个噩梦,你必须要面对一个。好在消息重复还有处理的机会,消息丢失再想找回就难了。 Anyway,作为一个成熟的消息队列,应该尽量在各个环节减少重复投递的可能性,不能因为重复有解决方案就放纵的乱投递。 最后说一句,不是所有的系统都要求最终一致性或者可靠投递,比如一个论坛系统、一个招聘系统。一个重复的简历或话题被发布,可能比丢失了一个发布显得更让用户无法接受。不断重复一句话,任何基础组件要服务于业务场景。 消费确认 当broker把消息投递给消费者后,消费者可以立即响应我收到了这个消息。但收到了这个消息只是第一步,我能不能处理这个消息却不一定。或许因为消费能力的问题,系统的负荷已经不能处理这个消息;或者是刚才状态机里面提到的消息不是我想要接收的消息,主动要求重发。 把消息的送达和消息的处理分开,这样才真正的实现了消息队列的本质-解耦。所以,允许消费者主动进行消费确认是必要的。当然,对于没有特殊逻辑的消息,默认Auto Ack也是可以的,但一定要允许消费方主动ack。 对于正确消费ack的,没什么特殊的。但是对于reject和error,需要特别说明。reject这件事情,往往业务方是无法感知到的,系统的流量和健康状况的评估,以及处理能力的评估是一件非常复杂的事情。举个极端的例子,收到一个消息开始build索引,可能这个消息要处理半个小时,但消息量却是非常的小。所以reject这块建议做成滑动窗口/线程池类似的模型来控制,消费能力不匹配的时候,直接拒绝,过一段时间重发,减少业务的负担。 但业务出错这件事情是只有业务方自己知道的,就像上文提到的状态机等等。这时应该允许业务方主动ack error,并可以与broker约定下次投递的时间。 重复消息和顺序消息 上文谈到重复消息是不可能100%避免的,除非可以允许丢失,那么,顺序消息能否100%满足呢? 答案是可以,但条件更为苛刻: 允许消息丢失。 从发送方到服务方到接受者都是单点单线程。 所以绝对的顺序消息基本上是不能实现的,当然在METAQ/Kafka等pull模型的消息队列中,单线程生产/消费,排除消息丢失,也是一种顺序消息的解决方案。 一般来讲,一个主流消息队列的设计范式里,应该是不丢消息的前提下,尽量减少重复消息,不保证消息的投递顺序。 谈到重复消息,主要是两个话题: 如何鉴别消息重复,并幂等的处理重复消息。 一个消息队列如何尽量减少重复消息的投递。 先来看看第一个话题,每一个消息应该有它的唯一身份。不管是业务方自定义的,还是根据IP/PID/时间戳生成的MessageId,如果有地方记录这个MessageId,消息到来是能够进行比对就能完成重复的鉴定。数据库的唯一键/bloom filter/分布式KV中的key,都是不错的选择。由于消息不能被永久存储,所以理论上都存在消息从持久化存储移除的瞬间上游还在投递的可能(上游因种种原因投递失败,不停重试,都到了下游清理消息的时间)。这种事情都是异常情况下才会发生的,毕竟是小众情况。两分钟消息都还没送达,多送一次又能怎样呢?幂等的处理消息是一门艺术,因为种种原因重复消息或者错乱的消息还是来到了,说两种通用的解决方案: 版本号。 状态机。 版本号 举个简单的例子,一个产品的状态有上线/下线状态。如果消息1是下线,消息2是上线。不巧消息1判重失败,被投递了两次,且第二次发生在2之后,如果不做重复性判断,显然最终状态是错误的。但是,如果每个消息自带一个版本号。上游发送的时候,标记消息1版本号是1,消息2版本号是2。如果再发送下线消息,则版本号标记为3。下游对于每次消息的处理,同时维护一个版本号。 每次只接受比当前版本号大的消息。初始版本为0,当消息1到达时,将版本号更新为1。消息2到来时,因为版本号>1.可以接收,同时更新版本号为2.当另一条下线消息到来时,如果版本号是3.则是真实的下线消息。如果是1,则是重复投递的消息。如果业务方只关心消息重复不重复,那么问题就已经解决了。但很多时候另一个头疼的问题来了,就是消息顺序如果和想象的顺序不一致。比如应该的顺序是12,到来的顺序是21。则最后会发生状态错误。 参考TCP/IP协议,如果想让乱序的消息最后能够正确的被组织,那么就应该只接收比当前版本号大一的消息。并且在一个session周期内要一直保存各个消息的版本号。 如果到来的顺序是21,则先把2存起来,待1到来后,先处理1,再处理2,这样重复性和顺序性要求就都达到了。 状态机 基于版本号来处理重复和顺序消息听起来是个不错的主意,但凡事总有瑕疵。使用版本号的最大问题是: 对发送方必须要求消息带业务版本号。 下游必须存储消息的版本号,对于要严格保证顺序的。 还不能只存储最新的版本号的消息,要把乱序到来的消息都存储起来。而且必须要对此做出处理。试想一个永不过期的”session”,比如一个物品的状态,会不停流转于上下线。那么中间环节的所有存储就必须保留,直到在某个版本号之前的版本一个不丢的到来,成本太高。 就刚才的场景看,如果消息没有版本号,该怎么解决呢?业务方只需要自己维护一个状态机,定义各种状态的流转关系。例如,”下线”状态只允许接收”上线”消息,“上线”状态只能接收“下线消息”,如果上线收到上线消息,或者下线收到下线消息,在消息不丢失和上游业务正确的前提下。要么是消息发重了,要么是顺序到达反了。这时消费者只需要把“我不能处理这个消息”告诉投递者,要求投递者过一段时间重发即可。而且重发一定要有次数限制,比如5次,避免死循环,就解决了。 举例子说明,假设产品本身状态是下线,1是上线消息,2是下线消息,3是上线消息,正常情况下,消息应该的到来顺序是123,但实际情况下收到的消息状态变成了3123。 那么下游收到3消息的时候,判断状态机流转是下线->上线,可以接收消息。然后收到消息1,发现是上线->上线,拒绝接收,要求重发。然后收到消息2,状态是上线->下线,于是接收这个消息。此时无论重发的消息1或者3到来,还是可以接收。另外的重发,在一定次数拒绝后停止重发,业务正确。 中间件对于重复消息的处理 回归到消息队列的话题来讲。上述通用的版本号/状态机/ID判重解决方案里,哪些是消息队列该做的、哪些是消息队列不该做业务方处理的呢?其实这里没有一个完全严格的定义,但回到我们的出发点,我们保证不丢失消息的情况下尽量少重复消息,消费顺序不保证。那么重复消息下和乱序消息下业务的正确,应该是由消费方保证的,我们要做的是减少消息发送的重复。 我们无法定义业务方的业务版本号/状态机,如果API里强制需要指定版本号,则显得过于绑架客户了。况且,在消费方维护这么多状态,就涉及到一个消费方的消息落地/多机间的同步消费状态问题,复杂度指数级上升,而且只能解决部分问题。 减少重复消息的关键步骤: broker记录MessageId,直到投递成功后清除,重复的ID到来不做处理,这样只要发送者在清除周期内能够感知到消息投递成功,就基本不会在server端产生重复消息。 对于server投递到consumer的消息,由于不确定对端是在处理过程中还是消息发送丢失的情况下,有必要记录下投递的IP地址。决定重发之前询问这个IP,消息处理成功了吗?如果询问无果,再重发。 4、事务 持久性是事务的一个特性,然而只满足持久性却不一定能满足事务的特性。还是拿扣钱/加钱的例子讲。满足事务的一致性特征,则必须要么都不进行,要么都能成功。 解决方案从大方向上有两种: 两阶段提交,分布式事务。 本地事务,本地落地,补偿发送。 分布式事务存在的最大问题是成本太高,两阶段提交协议,对于仲裁down机或者单点故障,几乎是一个无解的黑洞。对于交易密集型或者I/O密集型的应用,没有办法承受这么高的网络延迟,系统复杂性。并且成熟的分布式事务一定构建与比较靠谱的商用DB和商用中间件上,成本也太高。 那如何使用本地事务解决分布式事务的问题呢?以本地和业务在一个数据库实例中建表为例子,与扣钱的业务操作同一个事务里,将消息插入本地数据库。如果消息入库失败,则业务回滚;如果消息入库成功,事务提交。然后发送消息(注意这里可以实时发送,不需要等定时任务检出,以提高消息实时性)。以后的问题就是前文的最终一致性问题所提到的了,只要消息没有发送成功,就一直靠定时任务重试。这里有一个关键的点,本地事务做的,是业务落地和消息落地的事务,而不是业务落地和RPC成功的事务。这里很多人容易混淆,如果是后者,无疑是事务嵌套RPC,是大忌,会有长事务死锁等各种风险。而消息只要成功落地,很大程度上就没有丢失的风险(磁盘物理损坏除外)。而消息只要投递到服务端确认后本地才做删除,就完成了producer->broker的可靠投递,并且当消息存储异常时,业务也是可以回滚的。 本地事务存在两个最大的使用障碍: 配置较为复杂,“绑架”业务方,必须本地数据库实例提供一个库表。 对于消息延迟高敏感的业务不适用。 话说回来,不是每个业务都需要强事务的。扣钱和加钱需要事务保证,但下单和生成短信却不需要事务,不能因为要求发短信的消息存储投递失败而要求下单业务回滚。所以,一个完整的消息队列应该定义清楚自己可以投递的消息类型,如事务型消息,本地非持久型消息,以及服务端不落地的非可靠消息等。对不同的业务场景做不同的选择。另外事务的使用应该尽量低成本、透明化,可以依托于现有的成熟框架,如Spring的声明式事务做扩展。业务方只需要使用@Transactional标签即可。 5、性能相关 异步/同步 首先澄清一个概念,异步,同步和oneway是三件事。异步,归根结底你还是需要关心结果的,但可能不是当时的时间点关心,可以用轮询或者回调等方式处理结果;同步是需要当时关心的结果的;而oneway是发出去就不管死活的方式,这种对于某些完全对可靠性没有要求的场景还是适用的,但不是我们重点讨论的范畴。 回归来看,任何的RPC都是存在客户端异步与服务端异步的,而且是可以任意组合的:客户端同步对服务端异步,客户端异步对服务端异步,客户端同步对服务端同步,客户端异步对服务端同步。对于客户端来说,同步与异步主要是拿到一个Result,还是Future(Listenable)的区别。实现方式可以是线程池,NIO或者其他事件机制,这里先不展开讲。服务端异步可能稍微难理解一点,这个是需要RPC协议支持的。参考servlet 3.0规范,服务端可以吐一个future给客户端,并且在future done的时候通知客户端。 整个过程可以参考下面的代码: 客户端同步服务端异步。 Future<Result> future = request(server);//server立刻返回future synchronized(future){ while(!future.isDone()){ future.wait();//server处理结束后会notify这个future,并修改isdone标志 } } return future.get(); 客户端同步服务端同步。 Result result = request(server); 客户端异步服务端同步(这里用线程池的方式)。 Future<Result> future = executor.submit(new Callable(){public void call<Result>(){ result = request(server); }}) return future; 客户端异步服务端异步。 Future<Result> future = request(server);//server立刻返回future return future 上面说了这么多,其实是想让大家脱离两个误区: RPC只有客户端能做异步,服务端不能。 异步只能通过线程池。 那么,服务端使用异步最大的好处是什么呢?说到底,是解放了线程和I/O。试想服务端有一堆I/O等待处理,如果每个请求都需要同步响应,每条消息都需要结果立刻返回,那么就几乎没法做I/O合并 (当然接口可以设计成batch的,但可能batch发过来的仍然数量较少)。而如果用异步的方式返回给客户端future,就可以有机会进行I/O的合并,把几个批次发过来的消息一起落地(这种合并对于MySQL等允许batch insert的数据库效果尤其明显),并且彻底释放了线程。不至于说来多少请求开多少线程,能够支持的并发量直线提高。 来看第二个误区,返回future的方式不一定只有线程池。换句话说,可以在线程池里面进行同步操作,也可以进行异步操作,也可以不使用线程池使用异步操作(NIO、事件)。 回到消息队列的议题上,我们当然不希望消息的发送阻塞主流程(前面提到了,server端如果使用异步模型,则可能因消息合并带来一定程度上的消息延迟),所以可以先使用线程池提交一个发送请求,主流程继续往下走。但是线程池中的请求关心结果吗?Of course,必须等待服务端消息成功落地,才算是消息发送成功。所以这里的模型,准确地说事客户端半同步半异步(使用线程池不阻塞主流程,但线程池中的任务需要等待server端的返回),server端是纯异步。客户端的线程池wait在server端吐回的future上,直到server端处理完毕,才解除阻塞继续进行。 总结一句,同步能够保证结果,异步能够保证效率,要合理的结合才能做到最好的效率。 批量 谈到批量就不得不提生产者消费者模型。但生产者消费者模型中最大的痛点是:消费者到底应该何时进行消费。大处着眼来看,消费动作都是事件驱动的。主要事件包括: 攒够了一定数量。 到达了一定时间。 队列里有新的数据到来。 对于及时性要求高的数据,可用采用方式3来完成,比如客户端向服务端投递数据。只要队列有数据,就把队列中的所有数据刷出,否则将自己挂起,等待新数据的到来。 在第一次把队列数据往外刷的过程中,又积攒了一部分数据,第二次又可以形成一个批量。伪代码如下: Executor executor = Executors.newFixedThreadPool(4); final BlockingQueue<Message> queue = new ArrayBlockingQueue<>(); private Runnable task = new Runnable({//这里由于共享队列,Runnable可以复用,故做成全局的 public void run(){ List<Message> messages = new ArrayList<>(20); queue.drainTo(messages,20); doSend(messages);//阻塞,在这个过程中会有新的消息到来,如果4个线程都占满,队列就有机会囤新的消息 } }); public void send(Message message){ queue.offer(message); executor.submit(task) } 这种方式是消息延迟和批量的一个比较好的平衡,但优先响应低延迟。延迟的最高程度由上一次发送的等待时间决定。但可能造成的问题是发送过快的话批量的大小不够满足性能的极致。 Executor executor = Executors.newFixedThreadPool(4); final BlockingQueue<Message> queue = new ArrayBlockingQueue<>(); volatile long last = System.currentMills(); Executors.newSingleThreadScheduledExecutor().submit(new Runnable(){ flush(); },500,500,TimeUnits.MILLS); private Runnable task = new Runnable({//这里由于共享队列,Runnable可以复用,顾做成全局的。 public void run(){ List<Message> messages = new ArrayList<>(20); queue.drainTo(messages,20); doSend(messages);//阻塞,在这个过程中会有新的消息到来,如果4个线程都占满,队列就有机会屯新的消息。 } }); public void send(Message message){ last = System.currentMills(); queue.offer(message); flush(); } private void flush(){ if(queue.size>200||System.currentMills()-last>200){ executor.submit(task) } } 相反对于可以用适量的延迟来换取高性能的场景来说,用定时/定量二选一的方式可能会更为理想,既到达一定数量才发送,但如果数量一直达不到,也不能干等,有一个时间上限。 具体说来,在上文的submit之前,多判断一个时间和数量,并且Runnable内部维护一个定时器,避免没有新任务到来时旧的任务永远没有机会触发发送条件。对于server端的数据落地,使用这种方式就非常方便。 最后啰嗦几句,曾经有人问我,为什么网络请求小包合并成大包会提高性能?主要原因有两个: 减少无谓的请求头,如果你每个请求只有几字节,而头却有几十字节,无疑效率非常低下。 减少回复的ack包个数。把请求合并后,ack包数量必然减少,确认和重发的成本就会降低。 5、push还是pull 上文提到的消息队列,大多是针对push模型的设计。现在市面上有很多经典的也比较成熟的pull模型的消息队列,如Kafka、MetaQ等。这跟JMS中传统的push方式有很大的区别,可谓另辟蹊径。 我们简要分析下push和pull模型各自存在的利弊。 慢消费 慢消费无疑是push模型最大的致命伤,穿成流水线来看,如果消费者的速度比发送者的速度慢很多,势必造成消息在broker的堆积。假设这些消息都是有用的无法丢弃的,消息就要一直在broker端保存。当然这还不是最致命的,最致命的是broker给consumer推送一堆consumer无法处理的消息,consumer不是reject就是error,然后来回踢皮球。 反观pull模式,consumer可以按需消费,不用担心自己处理不了的消息来骚扰自己,而broker堆积消息也会相对简单,无需记录每一个要发送消息的状态,只需要维护所有消息的队列和偏移量就可以了。所以对于建立索引等慢消费,消息量有限且到来的速度不均匀的情况,pull模式比较合适。 消息延迟与忙等 这是pull模式最大的短板。由于主动权在消费方,消费方无法准确地决定何时去拉取最新的消息。如果一次pull取到消息了还可以继续去pull,如果没有pull取到则需要等待一段时间重新pull。 但等待多久就很难判定了。你可能会说,我可以有xx动态pull取时间调整算法,但问题的本质在于,有没有消息到来这件事情决定权不在消费方。也许1分钟内连续来了1000条消息,然后半个小时没有新消息产生,可能你的算法算出下次最有可能到来的时间点是31分钟之后,或者60分钟之后,结果下条消息10分钟后到了,是不是很让人沮丧? 当然也不是说延迟就没有解决方案了,业界较成熟的做法是从短时间开始(不会对broker有太大负担),然后指数级增长等待。比如开始等5ms,然后10ms,然后20ms,然后40ms……直到有消息到来,然后再回到5ms。即使这样,依然存在延迟问题:假设40ms到80ms之间的50ms消息到来,消息就延迟了30ms,而且对于半个小时来一次的消息,这些开销就是白白浪费的。 在阿里的RocketMq里,有一种优化的做法-长轮询,来平衡推拉模型各自的缺点。基本思路是:消费者如果尝试拉取失败,不是直接return,而是把连接挂在那里wait,服务端如果有新的消息到来,把连接notify起来,这也是不错的思路。但海量的长连接block对系统的开销还是不容小觑的,还是要合理的评估时间间隔,给wait加一个时间上限比较好~ 顺序消息 如果push模式的消息队列,支持分区,单分区只支持一个消费者消费,并且消费者只有确认一个消息消费后才能push送另外一个消息,还要发送者保证全局顺序唯一,听起来也能做顺序消息,但成本太高了,尤其是必须每个消息消费确认后才能发下一条消息,这对于本身堆积能力和慢消费就是瓶颈的push模式的消息队列,简直是一场灾难。 反观pull模式,如果想做到全局顺序消息,就相对容易很多: producer对应partition,并且单线程。 consumer对应partition,消费确认(或批量确认),继续消费即可。 所以对于日志push送这种最好全局有序,但允许出现小误差的场景,pull模式非常合适。如果你不想看到通篇乱套的日志~~ Anyway,需要顺序消息的场景还是比较有限的而且成本太高,请慎重考虑。 三、总结 本文从为何使用消息队列开始讲起,然后主要介绍了如何从零开始设计一个消息队列,包括RPC、事务、最终一致性、广播、消息确认等关键问题。并对消息队列的push、pull模型做了简要分析,最后从批量和异步角度,分析了消息队列性能优化的思路。下篇会着重介绍一些高级话题,如存储系统的设计、流控和错峰的设计、公平调度等。希望通过这些,让大家对消息队列有个提纲挈领的整体认识,并给自主开发消息队列提供思路。另外,本文主要是源自自己在开发消息队列中的思考和读源码时的体会,比较不”官方”,也难免会存在一些漏洞,欢迎大家多多交流。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/80539558 在SQL编程中,事务编程已然成为必不可少的一个组成部分。本文将基于MySQL对数据库事务进行简单的介绍和分析。 一、事务概述 事务可以由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。事务能保证数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保其要么对所有修改都已经保存,要么对所有修改操作都不保存。 事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元,狭义上的事务特指数据库事务。一方面,当多个应用程序并发访问数据库时,事务可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。另一方面,事务为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。 下面,我们将聚焦于事务的ACID特性进行讲解与分析。 在MySQL的InnoDB存储引擎中,其默认的事务隔离级别是Read Repeatable(可重复读),完全遵循和满足事务的ACID特性。 下面,我们就具体介绍事务的ACID特性,具体如下: 1、原子性(Atomic) 事务的原子性是指整个数据库事务是不可分割的工作单位。只有是事务中所有的数据库操作都执行成功,整个事务的执行才算成功。事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。 事务的原子性要求事务中包含的各项操作在一次执行过程中,只允许出现以下两种状态之一: 全部成功执行; 全部不执行。 举个例子,一个用户在ATM机前取款,其取款流程为: 登录ATM机平台,验证密码; 从远程银行的数据库中取得账户的信息; 用户在ATM机上输入要提取的金额; 从远程银行的数据库中更新账户信息; ATM机出款; 用户取钱。 整个取款的操作过程应该视为原子操作,即要么都做,要么都不做。不能出现用户钱未从ATM机上取得而银行卡上的钱已经被扣除的情况。通过事物模型,可以保证该操作的原子性。 2、一致性(Consistency) 事务的一致性是指整个数据库事务将数据库从一种一致的状态转变为另一种一致的状态。在事务开始之前和事务结束之后,数据库的完整性约束不会被破坏。因此,事务是一致性的单位,如果事务中的某个动作失败了,系统可以自动地撤销事务使其返回初始化的状态。 举个例子,在表中有一个字段为姓名,它是一个唯一约束,即在表中姓名不能重复。如果一个事务对表进行了修改,但是在事务提交或当事务操作发生回滚后,表中的数据姓名变得非唯一了,那么就破坏了事务的一致性要求,即事务将数据库从一种一致性状态转变为了一种不一致的状态。 3、隔离性(Isolation) 事务的隔离性要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事物提交前对其他事务都不可见。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 通常情况下,实现事务的隔离性需要使用锁机制。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之间的并发性。 4、持久性(Durability) 事务的一致性是指事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。持久性保证的是事务系统的高可靠性(high reliability),而不是高可用性(high availability)。 二、事务分类 从理论上的角度来说,可以把事务分为以下几种类型: 扁平事务(flat transactions) 带有保存点的扁平事务(flat transactions with savepoints) 链事务(chained transactions) 嵌套事务(nested transactions) 分布式事务(distributed transactions) 1、扁平事务 扁平事务是事务类型中最简单的一种,也是实际生产环境中使用最为频繁的事务。 在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束,处于之间的操作是原子的,即要么都执行,要么都回滚。 扁平事务是应用程序成为原子操作的基本组成模块。 扁平事务的三种不同情况: 如上图所示,扁平事务的执行分为三种情况: 成功完成(96%) 应用程序要求停止事务(3%) 由于外界原因强制终止事务(1%) 分析扁平事务的特点,其主要限制就是不能提交或回滚事务的某一部分,或者分几个步骤进行提交。 2、带有保存点的扁平事务 前面我们知道,扁平事务存在不能回滚到同一事务中较早的一个状态的限制,因此带有保存点的扁平事务应运而生。 保存点(savepoint)用于通知系统应该记住事务当前的状态,以便以后发生错误时,事务能够回到该状态。对于扁平事务而言,其隐式地设置了一个保存点,即只能回滚到事务开始时的状态。 保存点用SAVE WORK函数来建立,通知系统记录当前的处理状态。当出现问题时,保存点能用做内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。 带保存点的扁平事务有一个特点,那就是当系统发生崩溃时,所有的保存点都将消失,因为保存点是易失的(volatile),而非持久的(persistent)。 3、链事务 链事务的思想是,在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。此处,提交事务操作和开始下一个事务操作将合并为一个原子操作。链事务中的回滚仅限于当前事务,即只能恢复到最近一个保存点。 其工作方式如下图所示: 4、嵌套事务 嵌套事务是一个层次结构框架。在嵌套事务框架中,有一个顶层事务(top-level transaction),其控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制着每一个局部的变换。 嵌套事务的层次结构如下图所示: 在嵌套事务中,其基本特性如下: 嵌套事务是由若干事务组成的一棵树,子树可以是嵌套事务,也可以是扁平事务。 处在叶子节点的事务是扁平事务,但是每个子事务从根到叶节点的举例可以是不同的。 位于根节点的事务称为顶层事务,其他事务称为子事务。 事务的前驱(predecessor)称为父事务(parent),事务的下一层称为儿子事务(child)。 子事务既可以提交也可以回滚,但是它的提交操作并不马上生效,除非由其父事务提交。由此可以推论,任何子事务都是在顶层事务提交后才真正提交。 树中的任意一个事务的回滚会引起它的所有子事务一同回滚。 在嵌套事务中,实际的工作交由叶子节点来完成,即只有叶节点的事务才能访问数据库、发送消息、获取其他类型的资源。而高层的事务仅负责逻辑控制,决定何时调用相关的子事务。 5、分布式事务 分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常,一个分布式事务中会涉及对多个数据源或业务系统的操作。 对于分布式事务而言,其内容错综复杂,基本理论包括CAP和BASE理论。在后续博客中,我们将详细分析分布式事务。 三、事务的隔离级别 1、事务问题 在数据库中,并发事务之间可能会存在相互影响的问题。在事务进行并发操作时,可能会出现下面一些问题: 脏读 事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的数据就是脏数据。 不可重复读 在同一事务中,对同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录。要避免这种情况,最简单的方式就是对要修改的记录加锁,但这会导致锁竞争加剧,影响性能。 不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。 幻读 在同一事务中,同一个查询多次返回的结果不一致。比如,事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的。对于这种情况,不能通过记录加锁解决,因为对于新增的记录根本无法加锁,解决方式就是将事务串行化,这样就能避免幻读。 幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。 2、隔离级别 下面,我们着重讲解一下事务的隔离级别,分别是: 读未提交(Read Uncommitted) 对于读未提交,该隔离级别允许脏读取,其隔离级别最低。换句换说,如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务,因此还没有进行事务提交;与此同时,允许另一个事务也能够访问该数据。在这种情况下,一个事务可以读到另一个事务未提交的结果,因此,所有的并发事务问题都会发生。该隔离级别可以通过“排他写锁”实现。 举个例子,事务A和事务B同时进行,事务A在整个执行阶段,会将某数据项的值从1开始,做一系列的加1操作直到变成10之后进行事务提交,此时,事务B能够看到这个数据项在事务A操作过程中的所有中间值(如1变成2等),而对这一系列的中间值的读取就是未授权的。 读未提交的实现原理: 事务在读数据时并未对数据加锁; 事务在修改数据时只对数据增加排他写锁。 举例: 事务一共查询了两次,在两次查询的过程中,事务二对数据进行了修改,并未提交(commit)。但是事务一的第二次查询查到了事务二的修改结果。在数据库的读现象浅析中我们介绍过,这种现象我们称之为脏读。 所以,未提交读会导致脏读。 读已提交(Read Committed) 对于读已提交,其仅允许读取已经被提交的数据。也就是说,只有在事务提交后,其更新结果才会被其他事务读取。基于读已提交隔离级别,可以解决脏读问题。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。 同样举上一个例子,事务A和事务B同时进行,事务A进行与上述同样的操作,此时,事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的结果10。 读已提交的实现原理: 事务对当前被读取的数据加行级共享读锁(当读到时才加锁),一旦读完改行,立即释放该行级共享读锁; 事务在更新某个数据时,必须先对其加行级排他写锁,直到事务结束才释放。 举例: 在读已提交隔离级别中,在事务二提交之前,事务一不能读取数据。只有在事务二提交之后,事务一才能读数据。从上面的例子中我们也看到,事务一两次读取的结果并不一致,所以提交读不能解决不可重复读的读现象。 简而言之,读已提交这种隔离级别保证了读到的任何数据都是提交的数据,避免了脏读(dirty reads)。但是不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据。 可重复读(Repeatable Read) 对于可重复读,其保证在事务处理过程中,多次读取同一个数据时,其值和事务开始时刻是一致的。基于可重复读隔离级别,可以解决脏读和不可重复读问题。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。 可重复读的实现原理: 事务在读取某数据时,必须先对其加行级共享读锁,直到事务结束才释放; 事务在更新某数据时,必须先对其加行级排他写锁,直到事务结束才释放。 举例: 在上面的例子中,只有在事务一提交之后,事务二才能更改该行数据。所以,只要在事务一从开始到结束的这段时间内,无论它读取该行数据多少次,结果都是一样的。 从上面的例子中我们可以得到结论:可重复读隔离级别可以解决不可重复读的读现象。但是可重复读这种隔离级别中,还有另外一种读现象解决不了,那就是幻读。看下面的例子: 上面的两个事务执行情况及现象如下: 1、事务一的第一次查询条件是age BETWEEN 10 AND 30;如果这是有十条记录符合条件。这时,它会给符合条件的这十条记录增加行级共享锁。任何其他事务无法更改这十条记录。 2、事务二执行一条sql语句,语句的内容是向表中插入一条数据。因为此时没有任何事务对表增加表级锁,所以,该操作可以顺利执行。 3、事务一再次执行SELECT * FROM users WHERE age BETWEEN 10 AND 30;时,结果返回的记录变成了十一条,比刚刚增加了一条,增加的这条正是事务二刚刚插入的那条。 所以,事务一的两次范围查询结果并不相同。这也就是我们提到的幻读。 串行化(Serializable) 串行化是最严格的事务隔离级别,它要求所有事务都被串行执行,即事务只能一个接一个地处理,不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。 串行化的实现原理: 事务在读取数据时,必须先对其加表级共享读锁,直到事务结束才释放; 事务在更新数据时,必须先对其加表级排他写锁,直到事务结束才释放。 虽然可序列化解决了脏读、不可重复读、幻读等读现象。但是序列化事务会产生以下效果: 无法读取其它事务已修改但未提交的记录。 在当前事务完成之前,其它事务不能修改目前事务已读取的记录。 在当前事务完成之前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。 对比 下图展示了不同隔离级别下事务访问数据的差异。 以上4个隔离级别的隔离性依次增强,分别解决不同的问题。具体对比如下图所示: 事务的隔离级别越高,就越能保证数据的完整性和一致性,但同时对并发性能的影响也越大。 通常,对于绝大多数的应用程序来说,可以优先考虑将数据库的隔离级别设置为读已提交,这能够避免脏读的同时保证较好的并发性能。但是,这种隔离级别会导致不可重复读、幻读等并发问题,不过,较为科学的做法是在可能出现这类问题的个别场合中,由应用程序主动采用悲观锁或乐观锁来进行事务控制。 在MySQL中,InnoDB存储引擎默认的支持隔离级别是Repeatable Read。与标准SQL不同的是,InnoDB存储引擎在Repeatable Read事务隔离级别下,使用Next-Key Lock的锁算法,因此避免了幻读的产生。另外,对于大多数数据库,其默认的事务隔离级别是Read Committed。 四种事务隔离级别从隔离程度上越来越高,但同时在并发性上也就越来越低。之所以有这么几种隔离级别,就是为了方便开发人员在开发过程中根据业务需要选择最合适的隔离级别。 3、MySQL隔离级别配置 在MySQL库启动时设置事务的默认隔离级别,可以修改MySQL的配置文件my.cnf: [mysqld] transaction-isolation = READ-COMMITTED 查看当前会话的事务隔离级别,可以使用: mysql> SELECT @@tx_isolation; 查看全局的事务隔离级别,可以使用: mysql> SELECT @@global.tx_isolation; 四、事务控制语句 1、事务SQL 在MySQL命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句后就会马上执行commit操作。 显式开启一个事务的命令: BEGIN && START TRANSACTION SET AUTOCOMMIT=0 事务控制的基本命令: START TRANSACTION | BEGIN:显式地开启一个事务。 COMMIT:提交事务,并使已对数据库进行的所有修改成为永久性的。 ROLLBACK:回滚事务,并撤销正在进行的所有未提交的修改。 SAVEPOINT identifier:SAVEPOINT允许在事务中创建一个保存节点,一个事务中可以有多个SAVEPOINT。 RELEASE SAVEPOINT identifier:删除一个事务的保存点,当没有一个保存点时,会抛出异常。 ROLLBACK TO [SAVEPOINT]:配合SAVEPOINT一起使用,可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。 SET TRANSACTION:用于设置事务的隔离级别。 2、不好的事务编程 下面,我们关注一些不好的事务编程习惯,主要包括: 在循环中提交 使用自动提交 使用自动回滚 在循环中提交 开发人员非常喜欢在循环中进行事务的提交,如下: CREATE PROCEDURE load1(count INT UNSIGNED) BEGIN DECLARE s INT UNSIGNED DEFAULT 1; DECLARE c CHAR(80) DEFAULT REPEAT('a', 80); WHILE s <= count DO INSERT INTO table1 SELECT NULL,c; COMMIT; SET s = s+1; END WHILE; END; 在上述例子中,是否加上提交命令COMMIT并不是关键,因为InnoDB存储引擎默认为自动提交。 上述方法的问题在于两方面: 当发生错误时,数据库会停留在一个未知的位置。 性能问题。 改进方法: CREATE PROCEDURE load2(count INT UNSIGNED) BEGIN DECLARE s INT UNSIGNED DEFAULT 1; DECLARE c CHAR(80) DEFAULT REPEAT('a', 80); START TRANSACTION; WHILE s <= count DO INSERT INTO table1 SELECT NULL,c; SET s = s+1; END WHILE; COMMIT; END; 对比两种方法的执行时间,有: mysql> CALL load1(10000); ——> 1 min 3.15sec mysql> TRUNCATE TABLE table1; mysql> CALL load2(10000); ——> 0.63 sec 原因何在?每一次提交都会写一次重做日志,但是使用事务只用写一次重做日志。 使用自动提交 使用如下语句来改变当前自动提交的方式: mysql> SET autocommit=0; ——> 关闭自动提交 Query OK, 0 rows affected (0.00sec) 同时,也可使用START TRANSACTION或BEGIN来显式地开启一个事务。显式开启事务后,在默认设置下,MySQL会自动执行SET autocommit=0的命令,并在COMMIT或者ROLLBACK结束一个事务后执行SET autocommit=1命令。 使用自动回滚 对于下面一个存储过程: CREATE PROCEDURE sp_auto_rollback_demo() BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK; START TRANSACTION; INSERT INTO b SELECT 1; INSERT INTO b SELECT 2; INSERT INTO b SELECT 3; COMMIT; END 存储过程sp_auto_rollback_demo定义了一个EXIT类型的HANDLER,当捕获到错误时进行回滚。但是自动回滚会导致一些不明的问题出现。 五、小结 在本文,我们了解了事务如何工作以及如何使用事务,同时讲解了操作事务的SQL语句以及怎样在应用程序中正确地使用事务。切记:MySQL数据库总是自动提交的。在应用程序中,最好的做法是把事务地START TRANSACTION、COMMIT、ROLLBACK操作交给程序端来完成,而不是在存储过程内完成。
关系型数据库简介 关系数据库由由埃德加·科德(IBM)在1969年左右提出。自推出后就成为商业应用的主要数据库模型(与其他数据库模型,如分级、网络或对象模型相比)。如今已有许多商业关系数据库管理系统(RDBMS),如Oracle,IBM DB2和Microsoft SQL Server等;也有许多免费的开源关系数据库,如MySQL,mSQL(mini-SQL)和嵌入式JavaDB(Apache Derby)等。 关系数据库将数据存储在表(table)中,一个表由行和列组成。行称为记录(record)或元组(tuple),列称为字段(field)或属性(attribute)。数据库的表类似于电子表格。不过关系数据库可以在这些表格中产生关联,使得可以有效地存储大量的数据,以及高效地检索数据。 SQL(结构化查询语言)通常用来对关系数据库进行操作。 关系型数据库设计步骤 数据库的设计对经验的要求比理论要高,因为你必须做出许多选择。数据库通常是为了某种应用需求而高度定制的,因此,在数据库设计的指导里,通常都是指出不要做什么而不是要做什么,但最后的决定权还是在设计者的手中。 1. 需求分析 在进行需求分析时,需要尽可能地收集需求,以及定义你的数据库的最终目的。 比如要开发书店查询应用,就要先知道应用有什么需求,包括如何添加书籍,如何查询现有书籍,如何查询订单,如何定义生成报告格式等等。 在这个阶段的分析中,在纸上画出输入表单,以及查询和报告的草图,通常会有不少帮助。 2. 收集数据,组织表并设定主键 在明确数据库设计需求后,接下来就要确定有哪些数据需要存储到数据库中。 通常我们都是将数据基于分类存储到不同的表中。例如,设计一个书店的数据库,就需要对书本、作者、出版社、顾客以及订单等分类进行分表;同时,对于每个表,则要定义好需要哪些列(记录),以书本为例,则需要有标题、作者、出版社、出版日期、ISBN、价格等信息。 另外,对于每一个表,我们需要选择一列(或者多列)作为主键(primary key)。 关于主键 在关系模型中,表不可以含有重复的行,否则会导致检索出现歧义。为保证唯一性,每个表都有某一列(或者多列)作为主键,其目的是可以唯一区分每一行。如果主键只由某列构成,则被称为简单键(simple key),若由多列组成,则称为组合键(composite key)。 大多数商业数据库都基于主键来生成索引以提高查询的速度。另外,主键还被用来被其他表用作关系引用(详见下文)。 主键的选取由库的设计者来决定,通常要遵循以下原则: 主键的值必须是唯一的(即不可重复)。 主键不能为空。 另外,对于主键的选取还有一些Best Practice: 主键的值不可修改。因为主键可能会在其他表中用来引用,如果改了主键的值,就需要把其他表的引用都更新。 主键可以是任何类型,但最好是整数(效率原因)。 主键最好用简单键,如果一定要用组合键,要尽量用最少的列。 目前的数据库都可以不主动指定主键,而是由于数据库自己添加额外的一列类型为自增整数(AutoNumber)并指定为主键。 3. 建立关系 在关系数据库中包含独立且不相关的表格通常没有太大意义,如果真是这种情况你可以考虑使用NoSQL或者电子表格来存储这些内容。 关系型数据库的核心所在就是“关系”二字,甚至可以说设计关系型数据库的关键就是明确各个表之间的关系。 表间关系的类型有如下三种: 一对多(one-to-many) 多对多(many-to-many) 一对一(one-to-one) 一对多 考虑一个族谱关系的例子,一个母亲可能会有0个或多个小孩,但是任意一个小孩都有且只有一个母亲。这样的关系便称为一对多。 一对多的关系不能只用一个表来保存,为什么? 以前面的例子来说,我们一开始可能会考虑建立一个名为Mothers的表,其中保存了母亲的信息如年龄,姓名,血型等,对于其下的小孩,可以创建不同的列,如老大,老二,老三等。但这样我们会面临一个问题,即列的数量是不确定的。 换个方向来说,我们可以建立名为Children的表,其中存储小孩的基本信息,以及其母亲的信息。这样看似能满足要求,但是由于不同的小孩可能会有相同的母亲,因此表中的重复数据是很多的。 因此,考虑支持一对多的数据库关系,我们应该建立两个表,分别为Mothers和Children,只保存各自的属性,并且设置分别的主键为MotherID和ChildrenID。然后我们可以通过在Children新建一列包含MotherID建立一对多的关系,如下图所示: 其中,Children表中的MotherID列又被称为约束或外键(Foreign Key),用SQL描述如下: CREATE TABLE Mothers ( MotherID INTEGER NOT NULL AUTO_INCREMENT, Name VARCHAR(100) NOT NULL, Age SMALLINT NOT NULL, BloodType VARCHAR(2) NOT NULL, PRIMARY KEY (MotherID) ); CREATE TABLE Children ( ChildrenID INTEGER NOT NULL AUTO_INCREMENT, MotherID INTEGER NOT NULL, Name VARCHAR(100) NOT NULL, Age SMALLINT NOT NULL, Sex VARCHAR(50) NOT NULL, BloodType VARCHAR(2) NOT NULL, PRIMARY KEY (ChildrenID), FOREIGN KEY (MotherID) REFERENCES Mothers(MotherID) ); 多对多 考虑一个“产品销售”数据库的例子,某个客户的订单包含一个或者多个产品,而某个产品又可能出现在多个订单之中, 这样的关系便称为是多对多的。 为了构建这样的关系,我们先从两个表订单Orders和产品Products开始看。表Products含有关于产品的信息(如名称、介绍、库存)以及一个主键ProductID;表Orders则包含订单信息(如客户ID、订单日期、订单状态)以及主键OrderID。同样地,我们没法简单地将所有购买的产品保存在订单表里,因为订单所包含的产品记录是不固定的;同理,也没法将所有关联订单保存在产品表里。 因此,为了支持这种多对多的关系,我们需要第三个表。在本例子中,姑且将其命名为OrderDetails,其中每一行都包含了特定的订单信息,对于这个表,主键应为组合键,包含两列信息, 分别为OrderID和ProductID,而这两列也是对应Orders和Products表的Foreign Key,如下图所示: 用SQL描述如下: CREATE TABLE Orders ( OrderID INTEGER NOT NULL AUTO_INCREMENT, OrderDate DATETIME DEFAULT CURRENT_TIMESTAMP, CustomerID INTEGER NOT NULL, PRIMARY KEY (OrderID) ); CREATE TABLE Products ( ProductID INTEGER NOT NULL AUTO_INCREMENT, Name VARCHAR(100) NOT NULL, Stock INTEGER DEFAULT 0, PRIMARY KEY (ProductID) ); CREATE TABLE OrderDetails ( OrderID INTEGER NOT NULL, ProductID INTEGER NOT NULL, FOREIGN KEY (OrderID) REFERENCES Orders(OrderID), FOREIGN KEY (ProductID) REFERENCES Products(ProductID), PRIMARY KEY (OrderID, ProductID) ); 事实上,多对多的关系是以两组一对多的关系来实现的,额外引入的表被称为junction table即连接表。从上面的例子可以看到,每个产品(product)都会在OrderDetails表里出现多次,但OrderDetails里的每一行都只包含一个产品,若每个订单有多个产品则用多行来表示。相应地,对订单(Order)也是类似。 一对一 考虑一个“产品信息”数据库,其中除了产品名称,产品数量等基本信息外,还需要保存产品图片,产品详细等富文本详情信息,一个产品只有0个或者一个详情,一个详情有且只对应一个产品,因此这类关系就可以归类为一对一关系。有些数据库限制了列的数量,或者我们需要将部分敏感信息用另外的表保存,这些情况都可以引进一对一的关系。 回到前面的例子,我们需要分裂出一个称为ProductDetails的表,与Products构成一对一的关系。 用SQL描述如下: CREATE TABLE Products ( ProductID INTEGER NOT NULL AUTO_INCREMENT, Name VARCHAR(50) NOT NULL, PRIMARY KEY (ProductID) ); CREATE TABLE ProductDetails ( ProductID INTEGER NOT NULL AUTO_INCREMENT, DetailInfo VARCHAR(65535), FOREIGN KEY (ProductID) REFERENCES Products(ProductID), PRIMARY KEY (ProductID) ); 可以看到在ProductDetails表中,主键和外键都为同一列, 这保证了一对一的正确性。值得一提的是,这里保证了Products 可以对应0个或1个ProductDetails,但ProductDetails必须对应一个Products,如果后者对前者不是强关联,如“丈夫-妻子” 的关系,那么后者可以不以主键作为外键,而是以另外一列声明为UNIQUE的属性作为外键即可。 精炼及规格化 当设计好一个数据库或者拿到已有的数据库时,我们可能会想要: 增加更多的列 为某个表中的可选数据创建一个新表并建立一对一关系 将一个大表分裂为两个小表 … 在进行这些操作时,下列的规则就可以作为参考。 规范规则(Normalization) 范式(Normal Form),指的是符合某一种级别的关系模式的集合,表示一个关系内部各属性之间的联系的合理化程度,可以在某种程度上认为是一张数据表的表结构所符合的某种设计标准的级别。常见的范式有第一范式、第二范式…第六范式,其严格程度依次上升,一般设计上满足第三范式即可满足日常使用。 第一范式(1NF) 第一范式又称为1NF(First Normal Form),是对关系模式的基本要求,不满足第一范式的数据库就不是关系型数据库。数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。 同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。 如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。 简而言之,第一范式就是没有重复的列。 第二范式(2NF) 第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式必须先满足第一范式(1NF)。第二范式要求数据库表中的每个实例或行必须可以被唯一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。 例如,员工信息表中加上了员工编号(EmployeeID)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个唯一属性列也就是我们之前提到过的主键。 第二范式也要求实体的属性完全依赖于主键。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,例如含有多列的主键,如前文提到的OrderDetails,主键为ProductID和OrderID,若含有一列为产品单价ProductPrice,则不符合2NF,因为ProductPrice只依赖于ProductID而不依赖于OrderID,因此此属性应该保存在Products表中。 简而言之,第二范式就是属性应完全依赖于其主键。 第三范式(3NF) 满足第三范式(3NF)必须先满足第二范式(2NF)。第三范式要求数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖。所谓传递函数依赖,指的是如果存在”A → B → C”的决定关系,则C传递函数依赖于A。 例如,存在一个部门信息表,其中每个部门有部门编号(DepartmentID)、部门名称(DepartmentName)、部门简介等信息。这样一个表就不是3NF的,因为存在传递依赖(EmplyeeID->DepartmentID->DepartmentName),因此在员工信息表中列出部门编号后就不应再将部门名称、部门简介等与部门有关的信息再加入员工信息表中,而是将这部分数据保存在部门信息表中,如果不存在部门信息表,则根据第三范式也应该构建它,否则就会有数据冗余,并且容易产生更新、插入的异常。 简而言之,第三范式就是任一非主键属性不应依赖于其它任何非主键属性。 完整规则(Integrity) 除了设计范式,我们也可以通过完整性规则(Integrity rules)来检查自己的设计。常见的完整性规则如下: 实体完整性(Entity Integrity Rule) 实体完整性指表中行的完整性。主要用于保证操作的数据(记录)非空、唯一且不重复。即实体完整性要求每个关系(表)有且仅有一个主键,每一个主键值必须唯一,而且不允许为“空”(NULL)或重复。 参照完整性(Referential Integrity Rule) 参照完整性属于表间规则。对于永久关系的相关表,在更新、插入或删除记录时,如果只改其一,就会影响数据的完整性。如删除父表的某记录后,子表的相应记录未删除,致使这些记录称为孤立记录。对于更新、插入或删除表间数据的完整性,统称为参照完整性。通常,在客观现实中的实体之间存在一定联系,在关系模型中实体及实体间的联系都是以关系进行描述,因此,操作时就可能存在着关系与关系间的关联和引用。 域完整性(Domain Integrity) 域完整性是指数据库表中的列必须满足某种特定的数据类型或约束。其中约束又包括取值范围、精度等规定。表中的CHECK、FOREIGN KEY 约束和DEFAULT、 NOT NULL定义都属于域完整性的范畴。 用户定义完整性(User-defined Integrity) 又叫业务逻辑完整性(Business logic Integrity),是对数据表中字段属性的约束,用户定义完整性规则(User-defined integrity)也称域完整性规则。包括字段的值域、字段的类型和字段的有效规则(如小数位数)等约束,是由确定关系结构时所定义的字段的属性决定的。如百分制的考试成绩取值范围在0-100之间,订单数量应该小于等于库存量等。 其他 通常我们可以通过对指定的列创建索引来加快数据库的读取和查询速度。在实现上,索引通常是一个结构化文件,可以提高SELECT的速度,却会对INSERT, UPDATE和DELETE的速度有一定负面影响。如果没有索引,进行一次条件查询(比如SELECT * FROM Customers WHERE name=”Sam”),就需要对整个数据库进行一次线性查找和比较。而在带索引的结构中(如B树),查询的时间就能减少到对数级别。当然在这种情况下,插入和删除的时间也从常数上升到对数级别,不过在实践中由于查找的频率远远大于插入和删除,因此索引带来的好处也是很明显的。 对于特定的表来说,索引可以是1列,多列组合(称为组合索引,Concatenated Index)或者是某列的部分内容(称为部分索引,Partial Index)。在一个表里我们也可以建立多个索引,例如需要经常通过电话号码或者名字来查询某个客户,就可以在这两列建立对应的索引。索引最终还是根据实际需要自行选择,值得一提的是大多数RDBMS都会自动基于主键建立索引。 后记 总结一下,在关系数据库设计中,我们首先要明确设计的最终目标,再根据目标决定哪些数据要持久化存储;对于这些数据,要按照功能和逻辑来进行拆分,并且存放在不同的表中,并且明确之间的关系;对于设计好的表,要进行重构,根据设计范式对大表进行拆分和优化;对于每个表要增加对应的完整性检查,关键是实体完整性和参照完整性;最后在实际使用中,对于高频查询的记录构建索引提升效率,以及其他因地制宜的优化。
在常规运维工作中,经常会运用到负载均衡服务。负载均衡分为四层负载和七层负载,那么这两者之间有什么不同? 一、什么是负载均衡 负载均衡(Load Balance)建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。 负载均衡有两方面的含义: 首先,大量的并发访问或数据流量分担到多台节点设备上分别处理,减少用户等待响应的时间; 其次,单个重负载的运算分担到多台节点设备上做并行处理,每个节点设备处理结束后,将结果汇总,返回给用户,系统处理能力得到大幅度提高。 简单来说就是: 其一,是将大量的并发处理转发给后端多个节点处理,减少工作响应时间; 其二,是将单个繁重的工作转发给后端多个节点处理,处理完再返回给负载均衡中心,再返回给用户。 目前负载均衡技术大多数是用于提高诸如在Web服务器、FTP服务器和其它关键任务服务器上的Internet服务器程序的可用性和可伸缩性。 二、负载均衡分类 对于负载均衡,主要分为以下几类: 二层负载均衡(mac) 一般采用虚拟mac地址方式,外部对虚拟MAC地址请求,负载均衡接收后分配后端实际的MAC地址响应) 三层负载均衡(ip) 一般采用虚拟IP地址方式,外部对虚拟的ip地址请求,负载均衡接收后分配后端实际的IP地址响应) 四层负载均衡(tcp) 在三次负载均衡的基础上,用IP+PORT接收请求,再转发到对应的机器。 七层负载均衡(http) 根据虚拟的URL或IP,主机名接收请求,再转向相应的处理服务器。 我们运维中最常见的是四层和七层负载均衡,这里重点说下这两种负载均衡。 四层负载均衡就是基于IP+端口的负载均衡:在三层负载均衡的基础上,通过发布三层的IP地址(VIP),然后加上四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,转发至后台服务器,并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理。 四层负载均衡对应的负载均衡器称为四层交换机(L4 switch),主要分析IP层及TCP/UDP层,实现四层负载均衡。此种负载均衡器不理解应用协议(如HTTP/FTP/MySQL等等)。 实现四层负载均衡的软件有: F5:硬件负载均衡器,功能很好,但是成本很高 LVS:重量级的四层负载软件 Nginx:轻量级的四层负载软件,带缓存功能,正则表达式较灵活 Haproxy:模拟四层转发,较灵活 七层负载均衡就是基于虚拟的URL或主机IP的负载均衡:在四层负载均衡的基础上(没有四层是绝对不可能有七层的),再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。举个例子,如果你的Web服务器分成两组,一组是中文语言的,一组是英文语言的,那么七层负载均衡就可以当用户来访问你的域名时,自动辨别用户语言,然后选择对应的语言服务器组进行负载均衡处理。 七层负载均衡对应的负载均衡器称为七层交换机(L7 switch),除了支持四层负载均衡以外,还有分析应用层的信息,如HTTP协议URI或Cookie信息,实现七层负载均衡。此种负载均衡器能理解应用协议。 实现七层负载均衡的软件有: Haproxy:天生负载均衡技能,全面支持七层代理,会话保持,标记,路径转移 Nginx:只在http协议和mail协议上功能比较好,性能与haproxy差不多 Apache:功能较差 Mysql Proxy:功能尚可 总的来说,一般使用LVS做4层负载,Nginx做7层负载,Haproxy则比较灵活,能同时支持4层和7层负载均衡。 三、四层负载均衡与七层负载均衡对比 1. 从技术原理上分析 所谓四层负载均衡,也就是主要通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 以常见的TCP为例,负载均衡设备在接收到第一个来自客户端的SYN 请求时,即通过上述方式选择一个最佳的服务器,并对报文中目标IP地址进行修改(改为后端服务器IP),直接转发给该服务器。TCP的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下,为保证服务器回包可以正确返回给负载均衡设备,在转发报文的同时可能还会对报文原来的源地址进行修改。 所谓七层负载均衡,也称为“内容交换”,也就是主要通过报文中的真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。 以常见的TCP为例,负载均衡设备如果要根据真正的应用层内容再选择服务器,只能先代理最终的服务器和客户端建立连接(三次握手)后,才可能接受到客户端发送的真正应用层内容的报文,然后再根据该报文中的特定字段,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。负载均衡设备在这种情况下,更类似于一个代理服务器。负载均衡和前端的客户端以及后端的服务器会分别建立TCP连接。所以从这个技术原理上来看,七层负载均衡明显的对负载均衡设备的要求更高,处理七层的能力也必然会低于四层模式的部署方式。 2. 从应用场景的需求上分析 七层应用负载的好处,是使得整个网络更”智能化“。可以参考这篇:http应用优化和加速说明-负载均衡,就可以基本上了解这种方式的优势所在。例如访问一个网站的用户流量,可以通过七层的方式,将对图片类的请求转发到特定的图片服务器并可以使用缓存技术;将对文字类的请求可以转发到特定的文字服务器并可以使用压缩技术。当然这只是七层应用的一个小案例,从技术原理上,这种方式可以对客户端的请求和服务器的响应进行任意意义上的修改,极大地提升了应用系统在网络层的灵活性。很多在后台,例如Nginx或者Apache上部署的功能可以前移到负载均衡设备上,例如客户请求中的Header重写,服务器响应中的关键字过滤或者内容插入等功能。 另外一个常常被提到的功能就是安全性。网络中最常见的SYN Flood攻击,即黑客控制众多源客户端,使用虚假IP地址对同一目标发送SYN攻击,通常这种攻击会大量发送SYN报文,耗尽服务器上的相关资源,以达到拒绝服务(Denial of Service, DoS)的目的。从技术原理上也可以看出,四层模式下这些SYN攻击都会被转发到后端的服务器上;而七层模式下这些SYN攻击自然在负载均衡设备上就截止,不会影响后台服务器的正常运营。另外负载均衡设备可以在七层层面设定多种策略,过滤特定报文,例如SQL Injection等应用层面的特定攻击手段,从应用层面进一步提高系统整体安全。 现在的七层负载均衡,主要还是着重于应用HTTP协议,所以其应用范围主要是众多的网站或者内部信息平台等基于B/S开发的系统。 四层负载均衡则对应其他TCP应用,例如基于C/S开发的ERP等系统。 3. 七层应用需要考虑的问题 是否真的必要。七层应用的确可以提高流量智能化,同时必然不可避免地带来设备配置复杂,负载均衡压力增高以及故障排查上的复杂性等问题。在设计系统时需要考虑四层七层同时应用的混杂情况。 是否真的可以提高安全性。例如SYN Flood攻击,七层模式的确将这些流量从服务器屏蔽,但负载均衡设备本身要有强大的抗DDoS能力,否则即使服务器正常而作为中枢调度的负载均衡设备故障也会导致整个应用的崩溃。 是否有足够的灵活度。七层应用的优势是可以让整个应用的流量智能化,但是负载均衡设备需要提供完善的七层功能,满足客户根据不同情况的基于应用的调度。最简单的一个考核就是能否取代后台Nginx或者Apache等服务器上的调度功能。能够提供一个七层应用开发接口的负载均衡设备,可以让客户根据需求任意设定功能,才真正有可能提供强大的灵活性和智能性。 4. 总体对比 智能性 七层负载均衡由于具备OSI七层的所有功能,所以在处理用户需求上能更加灵活,从理论上讲,七层模型能对用户的所有向服务端的请求进行修改。例如对文件header添加信息,根据不同的文件类型进行分类转发。四层模型仅支持基于网络层的需求转发,不能修改用户请求的内容。 安全性 七层负载均衡由于具有OSI模型的全部功能,能更容易抵御来自网络的攻击;四层模型从原理上讲,会直接将用户的请求转发给后端节点,无法直接抵御网络攻击。 复杂度 四层模型的架构一般比较简单,容易管理,容易定位问题;七层模型架构比较复杂,通常也需要考虑结合四层模型的混用情况,出现问题定位比较复杂。 效率比 四层模型基于更底层的设置,通常效率更高,但应用范围有限;七层模型需要更多的资源损耗,在理论上讲比四层模型有更强的功能,现在的实现更多是基于http应用。 四、负载均衡技术方案说明 目前有许多不同的负载均衡技术用以满足不同的应用需求,下面从负载均衡所采用的设备对象(软/硬件负载均衡),应用的OSI网络层次(网络层次上的负载均衡),及应用的地理结构(本地/全局负载均衡)等来分类。 1. 软/硬件负载均衡 软件负载均衡解决方案是指在一台或多台服务器相应的操作系统上安装一个或多个附加软件来实现负载均衡,如DNS Load Balance,KeepAlive+ipvs等,它的优点是基于特定环境,配置简单,使用灵活,成本低廉,可以满足一般的负载均衡需求。软件解决方案缺点也较多,因为每台服务器上安装额外的软件运行会消耗系统不定量的资源,越是功能强大的模块,消耗得越多,所以当连接请求特别大的时候,软件本身会成为服务器工作成败的一个关键;软件可扩展性并不是很好,受到操作系统的限制;由于操作系统本身的Bug,往往会引起安全问题。 硬件负载均衡解决方案是直接在服务器和外部网络间安装负载均衡设备,这种设备通常是一个独立于系统的硬件,我们称之为负载均衡器。由于专门的设备完成专门的任务,独立于操作系统,整体性能得到大量提高,加上多样化的负载均衡策略,智能化的流量管理,可达到最佳的负载均衡需求。负载均衡器有多种多样的形式,除了作为独立意义上的负载均衡器外,有些负载均衡器集成在交换设备中,置于服务器与Internet链接之间,有些则以两块网络适配器将这一功能集成到PC中,一块连接到Internet上,一块连接到后端服务器群的内部网络上。 软件负载均衡与硬件负载均衡的对比: 软件负载均衡的优点是需求环境明确,配置简单,操作灵活,成本低廉,效率不高,能满足普通的企业需求。缺点是依赖于系统,增加资源开销;软件的优劣决定环境的性能;系统的安全,软件的稳定性均会影响到整个环境的安全。 硬件负载均衡优点是独立于系统,整体性能大量提升,在功能、性能上优于软件方式;智能的流量管理,多种策略可选,能达到最佳的负载均衡效果。缺点是价格昂贵。 2. 本地/全局负载均衡 负载均衡从其应用的地理结构上分为本地负载均衡(Local Load Balance)和全局负载均衡(Global Load Balance,也叫地域负载均衡)。本地负载均衡是指对本地的服务器群做负载均衡,全局负载均衡是指对分别放置在不同的地理位置、有不同网络结构的服务器群间作负载均衡。 本地负载均衡能有效地解决数据流量过大、网络负荷过重的问题,并且不需花费昂贵开支购置性能卓越的服务器,充分利用现有设备,避免服务器单点故障造成数据流量的损失。其有灵活多样的均衡策略把数据流量合理地分配给服务器群内的服务器共同负担。即使是再给现有服务器扩充升级,也只是简单地增加一个新的服务器到服务群中,而不需改变现有网络结构、停止现有的服务。 全局负载均衡主要用于在一个多区域拥有自己服务器的站点,为了使全球用户只以一个IP地址或域名就能访问到离自己最近的服务器,从而获得最快的访问速度,也可用于子公司分散站点分布广的大公司通过Intranet(企业内部互联网)来达到资源统一合理分配的目的。 3. 网络层次上的负载均衡 针对网络上负载过重的不同瓶颈所在,从网络的不同层次入手,我们可以采用相应的负载均衡技术来解决现有问题。 随着带宽增加,数据流量不断增大,网络核心部分的数据接口将面临瓶颈问题,原有的单一线路将很难满足需求,而且线路的升级又过于昂贵甚至难以实现,这时就可以考虑采用链路聚合(Trunking)技术。 链路聚合技术(第二层负载均衡)将多条物理链路当作一条单一的聚合逻辑链路使用,网络数据流量由聚合逻辑链路中所有物理链路共同承担,由此在逻辑上增大了链路的容量,使其能满足带宽增加的需求。 现代负载均衡技术通常操作于网络的第四层或第七层。第四层负载均衡将一个Internet上合法注册的IP地址映射为多个内部服务器的IP地址,对每次 TCP连接请求动态使用其中一个内部IP地址,达到负载均衡的目的。在第四层交换机中,此种均衡技术得到广泛的应用,一个目标地址是服务器群VIP(虚拟 IP,Virtual IP address)连接请求的数据包流经交换机,交换机根据源端和目的IP地址、TCP或UDP端口号和一定的负载均衡策略,在服务器IP和VIP间进行映射,选取服务器群中最好的服务器来处理连接请求。 七层负载均衡控制应用层服务的内容,提供了一种对访问流量的高层控制方式,适合对HTTP服务器群的应用。第七层负载均衡技术通过检查流经的HTTP报头,根据报头内的信息来执行负载均衡任务。 七层负载均衡优点表现在如下几个方面: 通过对HTTP报头的检查,可以检测出HTTP 400、500和600系列的错误信息,因而能透明地将连接请求重新定向到另一台服务器,避免应用层故障。 可根据流经的数据类型(如判断数据包是图像文件、压缩文件或多媒体文件格式等),把数据流量引向相应内容的服务器来处理,增加系统性能。 能根据连接请求的类型,如是普通文本、图象等静态文档请求,还是asp、cgi等的动态文档请求,把相应的请求引向相应的服务器来处理,提高系统的性能及安全性。 七层负载均衡缺点表现在如下几个方面: 七层负载均衡受到其所支持的协议限制(一般只有HTTP),这样就限制了它应用的广泛性。 七层负载均衡检查HTTP报头会占用大量的系统资源,势必会影响到系统的性能,在大量连接请求的情况下,负载均衡设备自身容易成为网络整体性能的瓶颈。 五、负载均衡策略 在实际应用中,我们可能不想仅仅是把客户端的服务请求平均地分配给内部服务器,而不管服务器是否宕机。而是想使Pentium III服务器比Pentium II能接受更多的服务请求,一台处理服务请求较少的服务器能分配到更多的服务请求,出现故障的服务器将不再接受服务请求直至故障恢复等等。选择合适的负载均衡策略,使多个设备能很好的共同完成任务,消除或避免现有网络负载分布不均、数据流量拥挤反应时间长的瓶颈。在各负载均衡方式中,针对不同的应用需求,在OSI参考模型的第二、三、四、七层的负载均衡都有相应的负载均衡策略。 负载均衡策略的优劣及其实现的难易程度有两个关键因素:负载均衡算法以及对网络系统状况的检测方式和能力。 1. 负载均衡算法 (1)轮询均衡(Round Robin):每一次来自网络的请求轮流分配给内部中的服务器,从1至N然后重新开始。此种均衡算法适合于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。 (2)权重轮询均衡(Weighted Round Robin):根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。例如:服务器A的权值被设计成1,B的权值是 3,C的权值是6,则服务器A、B、C将分别接受到10%、30%、60%的服务请求。此种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。 (3)随机均衡(Random):把来自网络的请求随机分配给内部中的多个服务器。 (4)权重随机均衡(Weighted Random):此种均衡算法类似于权重轮循算法,不过在处理请求分担时是个随机选择的过程。 (5)响应速度均衡(Response Time):负载均衡设备对内部各服务器发出一个探测请求(例如Ping),然后根据内部中各服务器对探测请求的最快响应时间来决定哪一台服务器来响应客户端的服务请求。此种均衡算法能较好的反映服务器的当前运行状态,但这最快响应时间仅仅指的是负载均衡设备与服务器间的最快响应时间,而不是客户端与服务器间的最快响应时间。 (6)最少连接数均衡(Least Connection):客户端的每一次请求服务在服务器停留的时间可能会有较大的差异,随着工作时间加长,如果采用简单的轮询或随机均衡算法,每一台服务器上的连接进程可能会产生极大的不同,并没有达到真正的负载均衡。最少连接数均衡算法对内部中需负载的每一台服务器都有一个数据记录,记录当前该服务器正在处理的连接数量,当有新的服务连接请求时,将把当前请求分配给连接数最少的服务器,使均衡更加符合实际情况,负载更加均衡。此种均衡算法适合长时处理的请求服务,如FTP。 (7)处理能力均衡:此种均衡算法将把服务请求分配给内部中处理负荷(根据服务器CPU型号、CPU数量、内存大小及当前连接数等换算而成)最轻的服务器,由于考虑到了内部服务器的处理能力及当前网络运行状况,所以此种均衡算法相对来说更加精确,尤其适合运用到第七层(应用层)负载均衡的情况下。 (8)DNS响应均衡(Flash DNS):在Internet上,无论是HTTP、FTP或是其它的服务请求,客户端一般都是通过域名解析来找到服务器确切的IP地址的。在此均衡算法下,分处在不同地理位置的负载均衡设备收到同一个客户端的域名解析请求,并在同一时间内把此域名解析成各自相对应服务器的IP地址(即与此负载均衡设备在同一位地理位置的服务器的IP地址)并返回给客户端,则客户端将以最先收到的域名解析IP地址来继续请求服务,而忽略其它的IP地址响应。在种均衡策略适合应用在全局负载均衡的情况下,对本地负载均衡是没有意义的。 2. 网络系统状况的检测方式 尽管有多种的负载均衡算法可以较好的把数据流量分配给服务器去负载,但如果负载均衡策略没有对网络系统状况的检测方式和能力,一旦在某台服务器或某段负载均衡设备与服务器网络间出现故障的情况下,负载均衡设备依然把一部分数据流量引向那台服务器,这势必造成大量的服务请求被丢失,达不到不间断可用性的要求。因此,良好的负载均衡策略应有对网络故障、服务器系统故障、应用服务故障的检测方式和能力。通常的检测方式包括: (1)Ping侦测:通过ping的方式检测服务器及网络系统状况,此种方式简单快速,但只能大致检测出网络及服务器上的操作系统是否正常,对服务器上的应用服务检测就无能为力了。 (2)TCP Open侦测:每个服务都会开放某个通过TCP连接,检测服务器上某个TCP端口(如Telnet的23口,HTTP的80口等)是否开放来判断服务是否正常。 (3)HTTP URL侦测:比如向HTTP服务器发出一个对main.html文件的访问请求,如果收到错误信息,则认为服务器出现故障。 3、其他因素 负载均衡策略的优劣除受上面所讲的两个因素影响外,在有些应用情况下,我们需要将来自同一客户端的所有请求都分配给同一台服务器去负担,例如服务器将客户端注册、购物等服务请求信息保存的本地数据库的情况下,把客户端的子请求分配给同一台服务器来处理就显的至关重要了。有几种方式可以解决此问题: (1)一是根据IP地址把来自同一客户端的多次请求分配给同一台服务器处理,客户端IP地址与服务器的对应信息是保存在负载均衡设备上的。 (2)二是在客户端浏览器 cookie内做独一无二的标识来把多次请求分配给同一台服务器处理,适合通过代理服务器上网的客户端。 (3)还有一种路径外返回模式(Out of Path Return),当客户端连接请求发送给负载均衡设备的时候,中心负载均衡设备将请求引向某个服务器,服务器的回应请求不再返回给中心负载均衡设备,即绕过流量分配器,直接返回给客户端,因此中心负载均衡设备只负责接受并转发请求,其网络负担就减少了很多,并且给客户端提供了更快的响应时间。此种模式一般用于HTTP服务器群,在各服务器上要安装一块虚拟网络适配器,并将其IP地址设为服务器群的VIP,这样才能在服务器直接回应客户端请求时顺利的达成三次握手。 六、负载均衡实施要素 1. 性能 性能是我们在引入均衡方案时需要重点考虑的问题,但也是一个最难把握的问题。衡量性能时可将每秒钟通过网络的数据包数目做为一个参数,另一个参数是均衡方案中服务器群所能处理的最大并发连接数目,但是,假设一个均衡系统能处理百万计的并发连接数,可是却只能以每秒2个包的速率转发,这显然是没有任何作用的。 性能的优劣与负载均衡设备的处理能力、采用的均衡策略息息相关,并且有两点需要注意: 均衡方案对服务器群整体的性能,这是响应客户端连接请求速度的关键; 负载均衡设备自身的性能,避免有大量连接请求时自身性能不足而成为服务瓶颈。 有时我们也可以考虑采用混合型负载均衡策略来提升服务器群的总体性能,如DNS负载均衡与NAT负载均衡相结合。另外,针对有大量静态文档请求的站点,也可以考虑采用高速缓存技术,相对来说更节省费用,更能提高响应性能;对有大量 ssl/xml内容传输的站点,更应考虑采用ssl/xml加速技术。 2. 可扩展性 IT技术日新月异,一年以前最新的产品,现在或许已是网络中性能最低的产品;业务量的急速上升,一年前的网络,现在需要新一轮的扩展。合适的均衡解决方案应能满足这些需求,能均衡不同操作系统和硬件平台之间的负载,能均衡HTTP、邮件、新闻、代理、数据库、防火墙和 Cache等不同服务器的负载,并且能以对客户端完全透明的方式动态增加或删除某些资源。 3. 灵活性 均衡解决方案应能灵活地提供不同的应用需求,满足应用需求的不断变化。在不同的服务器群有不同的应用需求时,应有多样的均衡策略提供更广泛的选择。 4. 可靠性 在对服务质量要求较高的站点,负载均衡解决方案应能为服务器群提供完全的容错性和高可用性。但在负载均衡设备自身出现故障时,应该有良好的冗余解决方案,提高可靠性。使用冗余时,处于同一个冗余单元的多个负载均衡设备必须具有有效的方式以便互相进行监控,保护系统尽可能地避免遭受到重大故障的损失。 5. 易管理性 不管是通过软件还是硬件方式的均衡解决方案,我们都希望它有灵活、直观和安全的管理方式,这样便于安装、配置、维护和监控,提高工作效率,避免差错。在硬件负载均衡设备上,目前主要有三种管理方式可供选择: 命令行接口(CLI:Command Line Interface),可通过超级终端连接负载均衡设备串行接口来管理,也能telnet远程登录管理,在初始化配置时,往往要用到前者; 图形用户接口(GUI:Graphical User Interfaces),有基于普通web页的管理,也有通过Java Applet 进行安全管理,一般都需要管理端安装有某个版本的浏览器; SNMP(Simple Network Management Protocol,简单网络管理协议)支持,通过第三方网络管理软件对符合SNMP标准的设备进行管理。
在python开发时,我们经常使用pip来安装相应的python库,但是国外的源下载速度实在太慢,而且经常出现下载后安装出错问题。 要解决该问题,可以把pip安装源替换成国内镜像,可以大幅提升下载速度,还可以提高安装成功率。 国内pip源 注意:新版ubuntu要求使用https源!!! 清华大学:https://pypi.tuna.tsinghua.edu.cn/simple 中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/ 阿里云:https://mirrors.aliyun.com/pypi/simple/ 豆瓣:http://pypi.douban.com/simple/ 临时使用 可以在使用pip的时候加参数-i https://pypi.tuna.tsinghua.edu.cn/simple 例如: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyspider 上述命令可以从清华的python库镜像获取pyspider库并完成安装! 设为默认 修改文件 ~/.pip/pip.conf (Linux) %APPDATA%\pip\pip.ini(Windows 10) $HOME/Library/Application Support/pip/pip.conf(macOS) 修改 index-url值,例如: [global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple Linux中,pip和pip3并存时,只需修改~/.pip/pip.conf文件即可。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79697073 一、Java技术体系 Sun官方所定义的Java技术体系包括以下几个组成部分: Java程序设计语言 各种硬件平台上的Java虚拟机 Class文件格式 Java API类库 来自商业机构和开源社区的第三方类库 JDK(Java Development Kit) —— 包括Java程序设计语言、Java虚拟机、Java API类库。JDK是用于支持Java程序开发的最小环境。 JRE(Jave Runtime Environment) —— 包括Java API类库中的Java SE API子集、Java虚拟机。JRE是支持Java程序运行的标准环境。 下图展示了Java技术体系所包含的内容,以及JDK和JRE所涵盖的范围: 按照技术所服务的领域来分,Java技术体系可以分为四个平台,分别是: Java Card:支持一些Java小程序(Applet)运行在小内存设备(如智能卡)上的平台。 Java ME(Micro Edition):支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API有所精简,并加入了针对移动终端的支持。 Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API。 Java EE(Enterprise Edition):支持使用多层架构的企业应用的Java平台,除了提供Java SE API之外,还对其做了大量的扩充并提供了相关的部署支持。 二、Java技术未来 1. 模块化 模块化是解决应用系统与技术平台越来越复杂、越来越庞大问题的一个重要途径。站在整个软件工业化的高度来看,模块化是建立各种功能的标准件的前提。最近几年的OSGi技术的迅速发展、各个厂商在JCP中对模块化规范的激烈斗争,都能充分说明模块化技术的迫切和重要。 2. 混合语言 当单一的Java开发已经无法满足当前软件的复杂需求时,越来越多基于Java虚拟机的语言开发被应用到软件项目中,Java平台上的多语言混合编程正成为主流,每种语言都可以针对自己擅长的方面更好地解决问题。试想一下,在一个项目之中,并行处理用Clojure语言编写,展示层使用JRuby/Rails,中间层使用Java,每个应用层都将使用不同的编程语言来完成,而且,接口对每一层的开发者都是透明的,各种语言之间的交互不存在任何困难,就像使用自己语言的原生API一样方便,因为它们最终都运行在一个虚拟机之上。因此,整个JVM项目开始推动Java虚拟机从“Java语言的虚拟机”向“多语言虚拟机”的方向发展。 3. 多核并行 如今,CPU硬件的发展方向已经从高频率转变为多核心,随着多核时代的来临,软件开发越来越关注并行编程的领域。Fork/Join模式是处理并行编程的一个经典方法,通过利用Fork/Join模式,我们能够更加顺畅地过渡到多核时代。 在Java8中,将会提供Lambda支持,这将会极大地改善目前Java语言不适合函数式编程的现状。另外,在并行计算中必须提及的还有Sumatra项目,其主要关注为Java提供使用GPU和APU运算能力的工具。在JDK外围,也出现了专为满足并行计算需求的计算框架,如Apache的Hadoop Map/Reduce等。 4. 进一步丰富语法 Java 5曾经对Java语法进行了一次扩充,这次扩充加入了自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环等语法,而后,每一次Java版本的发布,都会进一步丰富Java语言的语法特性,包括Java 8中的Lambda表达式。 5. 64位虚拟机 随着硬件的进一步发展,计算机终究会完全过渡到64位的时代,这是一件毫无疑问的事情,主流的虚拟机应用也终究会从32位发展到64位,而Java虚拟机对64位的支持也将会进一步完善。 二、Java内存管理机制 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。 1. 运行时数据区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖于用户线程的启动和结束而建立和销毁。 Java虚拟机所管理的内存包括以下几个运行时数据区域: 1.1 程序计数器 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。 在虚拟机的概念模型里,字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于程序计数器来完成。 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个核心)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。 如果线程正在执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则程序计数器的值为空(Undefined)。 程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 1.2 Java虚拟机栈 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,栈帧是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 对于C/C++等程序来说,其内存管理常常分为栈、堆等。对于Java,栈即指代虚拟机栈,或者说是虚拟机栈中局部变量表部分。 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用地址,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: java -Xss=512M HackTheJava 该区域可能抛出以下异常: 当线程请求的栈深度超过最大值,会抛出StackOverflowError 异常; 栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError 异常。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。 1.3 本地方法栈 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError异常和OutOfMemoryError异常。 1.4 Java堆 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。在JVM中,几乎所有的对象实例都在这里分配内存。Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也变得不是那么绝对了。 Java堆是垃圾收集器管理的主要区域,因此,Java堆也被称为“GC堆”(Garbage Collected Heap)。 现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把Java堆分成以下三块: 新生代(Young Generation) 老年代(Old Generation) 永久代(Permanent Generation) 当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效的进行垃圾回收,把新生代继续划分为以下三个空间: Eden From Survivor To Survivor 从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置最小值,第二个参数设置最大值。 java -Xms=1M -XmX=2M HackTheJava 1.5 方法区 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该与Java堆区分开来。 Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如同永久代名字一样永久存在。该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。 运行时常量池(Runtime Costant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,如String类的intern()方法。 既然运行时常量区是方法区的一部分,当常量池无法申请到内存时会抛出OutOfMemoryError异常。 1.6 直接内存 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 在Java4中新加入的NIO类,其引入了一种基于通道与缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的应用进行操作。这样能在一些场景中显著提高性能,在一定程度上能避免在Java堆和Native堆中来回复制数据。 2. JVM对象探秘 在本部分,我们将深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。 2.1 对象的创建 在HotSpot虚拟机中,对象的创建过程分为五个步骤: 步骤一 当虚拟机遇到一条new指令时,首先会去检查new指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 步骤二 在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配内存空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有: 指针碰撞(Bump the Pointer):假设Java堆中内存是绝对规整的,即所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,指针碰撞的内存分配方式就是把作为分界点的指针向空闲空间挪动一段与对象大小相等的距离即可。 空闲列表(Free List):如果Java堆中的内存不是规整的,即已使用的内存和空闲的内存相互交错,那么就无法使用指针碰撞了。此时,虚拟机就必须维护一个列表,记录哪些内存块是可用的,于是,空闲列表的内存分配方式就是从该列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。 选择哪种内存分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。 另外,在并发环境下,内存分配方式面临线程安全问题。解决这个问题有两种方案: 对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS结合失败重试的方法来保证更新操作的原子性。 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB)。 步骤三 内存分配完成后,虚拟机需要将分配的内存空间初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在Java代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 步骤四 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。 步骤五 在上面的工作都完成之后,从虚拟机的视角看,一个新的对象已经产生了。但是,从Java程序员的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有字段都还为零。 因此,一般来说,执行new命令之后,会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样,一个真正可用的对象才算完全产生出来。 2.2 对象内存布局 在HotSpot虚拟机中,对象在内存中存储的布局可以划分为3个区域: 对象头(Object Header) 实例数据(Instance Data) 对齐填充(Padding) 对象头信息 HotSpot虚拟机的对象头包括两部分信息: 第一部分:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁等,官方称之为“Mark Word”。该部分数据长度在32位和64位虚拟机中分别为32位和64位。Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。下表是32位HotSpot虚拟机对象头Mark Word。 存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC标记 偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向 第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过该指针来确定这个对象是哪个类的实例。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。 实例数据 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。 该部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序影响。 HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)。从分配策略可以看出,相同宽度的字段总是分配在一起。在满足该前提条件下,在父类中定义的变量会出现在子类之前。 对齐填充 对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。当对象实例数据部分没有对齐时,需要通过对齐填充来补全。 3. OutOfMemoryError异常 3.1 Java堆溢出 Java堆配置参数:-Xmx 和 -Xms Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。 测试代码: public class HeapOOM { static classn OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject); } } } 3.2 虚拟机栈和本地方法栈溢出 Java虚拟机栈配置参数:-Xss 关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常: 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常; 如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。 测试代码: public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try{ oom.stackLeak(); } catch(Throwable e) { System.out.println("stack length = " + oom.stackLength); throw e; } } } 3.3 方法区和运行时常量池溢出 方法区配置参数:-XX:PermSize和-XX:MaxPermSize String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此对象包含的字符串添加到常量池中,并且返回此String对象的引用。 测试代码: public class RuntimeConstantPoolOOM { public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } 3.4 本机直接内存溢出 直接内存配置参数:-XX:MaxDirectMemorySize 三、Java垃圾回收机制 1. 对象存活判断及垃圾回收概述 GC需要完成三件事情: 哪些内存需要回收? 什么时候回收? 如何回收? 为什么需要去了解GC和内存分配呢?答案很简单:当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。 Java堆和方法区的内存的分配和回收是动态的,垃圾收集器主要关注的就是这部分内存。 对象存亡问题:在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。 1.1 引用计数算法 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 客观的说,引用计数算法(Reference Counting)实现简单,判定效率也很高。但Java虚拟机并没有选用引用计数算法来管理内存,最主要的原因是引用计数算法很难解决对象间相互循环引用的问题。 1.2 可达性分析算法 在主流的商用程序语言中(Java、C#)的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。 可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链接相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明该对象是不可用的。 在Java语言中,可作为GC Roots的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象; 方法区中类静态属性引用的对象; 方法区中常量引用的对象; 本地方法栈中JNI(Native方法)引用的对象。 1.3 引用类型 无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。 Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。 强引用 只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。 使用 new 一个新对象的方式来创建强引用。 Object object = new Object(); 软引用 用来描述一些还有用但是并非必需的对象。 在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出溢出异常。 软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。 使用 SoftReference 类来实现软引用。 Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); 弱引用 只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。 使用 WeakReference 类来实现弱引用。 Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); 虚引用 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 使用 PhantomReference 来实现虚引用。 Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj); 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 关于Java中的软引用、弱引用和虚引用,可以参见博客:java中的弱引用、软引用和虚引用 1.4 两次标记清除 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的。 要真正宣告一个对象死亡,至少要经历两次标记过程: 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会。在finalize()函数中,如果对象重新与引用链上的任何一个对象建立关联,那么第二次标记是就将其移除出“即将回收”的集合;否则,第二次标记时,对象将会被宣告真正死亡。 注意,任何一个对象的finalize()方法都只会被系统自动调用一次,不鼓励使用该方法来拯救对象。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好。 1.5 方法区回收 在方法区中进行垃圾收集的性价比一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%-95%的空间,而永久代的垃圾收集效率远低于此。 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。 回收废弃常量与回收堆中的对象非常类似,基于引用的方法可以实现。但是,判定一个类是否是“无用的类”的条件相对苛刻许多。 一个类需要同时满足下面三个条件才能算是“无用的类”: 该类的所有实例都已经被回收; 加载该类的ClassLoader已经被回收; 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。 2. 垃圾回收算法 2.1 标记-清除算法(Mark-Sweep) 标记-清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。 不足之处: 效率问题:标记和清除两个过程的效率都不高; 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 具体工作过程如下: 内存分布图:可见内存碎片化问题严重。 2.2 复制算法(Copying) 复制算法有效地解决了效率问题。 过程为:算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后把已使用的内存空间一次清理掉。 该算法过程实现简单,运行高效。但是,内存缩小一半,代价太大。 具体工作过程如下: 内存分布图:可见很好地解决了内存碎片化问题。 现在的商用虚拟机都采用这种收集算法来回收新生代,由于新生代中的对象98%都是朝生夕死,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor(两块Survivor轮流使用)。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一个Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。 HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。 当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)。对于分配担保,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。 2.3 标记-整理算法(Mark-Compact) 复制收集算法在对象存活率较高时就要进行较多的赋值操作,效率将会变低。 对于标记-整理算法,其过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 具体工作过程如下: 内存分布图为: 2.4 分代收集算法 当前商业虚拟机的垃圾收集都采用“分代收集(Generational Collection)”算法,其根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。 在新生代中,大多采用复制收集算法。在老年代中,由于对象存活率较高并没有额外空间进行分配担保,多是使用“标记-清除”和“标记-整理”算法来进行回收。 3. 垃圾回收实现 3.1 枚举根节点 从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用与执行上下文中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。 在准确式GC中,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当有办法直接得知哪些地方存放着对象引用。 在HotSpot中,其使用一组称为OopMap的数据结构来达到这个目的,在类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。这样,GC在扫描时就可以得知这些信息了。 3.2 安全点(SafePoint) 程序执行时,并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。 安全点的选定既不能太少以致于GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。因此,安全点的选定基本上是以程序是否具有让程序长时间执行的特征为标准进行选定的。 对于安全点,另一个问题就是如何在GC发生时让所有线程都运行到最近的安全点上再停顿下来。两种方案: 抢先式中断(Preemptive Suspension) 主动式中断(Voluntary Suspension) 对于抢先式中断,其不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现线程中断的地方不在安全点上,就恢复线程,让它执行到安全点上。 对于主动式中断,其在当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。 现在基本使用“安全点轮询和触发线程中断”的主动式中断机制。 4. 垃圾收集器 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 下图是HotSpot虚拟机的垃圾收集器。如果两个收集器之间存在连线,就说明可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。 4.1 Serial收集器 它是单线程的收集器。 这不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。 它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 在 Client 应用场景中,分配给虚拟机管理的内存一般来说不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 4.2 ParNew收集器 它是 Serial 收集器的多线程版本。 它是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。 默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。 4.3 Paraller Scavenge收集器 它是并行的多线程收集器。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。 4.4 Serial Old收集器 Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 4.5 Parallel Old收集器 它是 Parallel Scavenge 收集器的老年代版本。 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 4.6 CMS收集器 CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于标记 - 清除算法实现的。 特点:并发收集、低停顿。 分为以下四个流程: 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 并发清除:不需要停顿。 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 具有以下缺点: 对 CPU 资源敏感。CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。 无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。 标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 4.7 G1收集器 G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。 具备如下特点: 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间。 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC 堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。 在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老生代,而 G1 不再是这样,Java 堆的内存布局与其他收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合。 之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。 Region 不可能是孤立的,一个对象分配在某个 Region 中,可以与整个 Java 堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了避免全堆扫描的发生,每个 Region 都维护了一个与之对应的 Remembered Set。虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。 如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: 初始标记 并发标记 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 4.8 七种垃圾收集器的对比 收集器 串行、并行 or 并发 新生代 / 老年代 算法 目标 适用场景 Serial 串行 新生代 复制算法 响应速度优先 单 CPU 环境下的 Client 模式 Serial Old 串行 老年代 标记-整理 响应速度优先 单 CPU 环境下的 Client 模式、CMS 的后备预案 ParNew 并行 新生代 复制算法 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合 Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务 Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务 CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或 B/S 系统服务端上的 Java 应用 G1 并发 both 标记-整理 + 复制算法 响应速度优先 面向服务端应用,将来替换 CMS 四、内存分配与回收策略 对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。 1. 优先在 Eden 分配 大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 关于 Minor GC 和 Full GC: Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。 2. 大对象直接进入老年代 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 3. 长期存活的对象进入老年代 JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增加 1 岁,增加到一定年龄则移动到老年代中(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置)。 4. 动态对象年龄判定 JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无序等待 MaxTenuringThreshold 中要求的年龄。 5. 空间分配担保 在发生 Minor GC 之前,JVM 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话 JVM 会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。 6. Full GC的触发条件 对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: 6.1 调用 System.gc() 此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。 6.2 老年代空间不足 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。 6.3 空间分配担保失败 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。 6.4 JDK 1.7 及以前的永久代空间不足 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 6.5 Concurrent Mode Failure 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 五、JVM监控工具 给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。 这些数据包括: 运行日志 异常堆栈 GC日志 线程快照(threaddump/javacore) 堆转储快照(heapdump/hprof) 1. jps:虚拟机进程状况工具 jps(JVM Process Status Tool)主要用于显示指定系统内所有的HotSpot虚拟机进程。 2. jstat:虚拟机统计信息监视工具 jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中类装载、内存、垃圾收集等运行数据。 3. jinfo:Java配置信息工具 jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。 4. jmap:Java内存映像工具 jmap(Memory Map for Java)命令用于生成堆转储快照。jmap的作用不仅仅是为了获取dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。 5. jhat:虚拟机堆转储快照分析工具 jhat(JVM Heap Analysis Tool)命令主要用于分析jmap生成的堆转储快照。 6. jstack:Java堆栈跟踪工具 jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。 7. JConsole:Java监视与管理控制台 JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视管理工具。 8.VisualVM:多合一故障处理工具 VisualVM 是一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能提供强大的分析能力,对 Java 应用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析,同时它还支持在 MBeans 上进行浏览和操作。 六、Class类文件解析 Java:一次编写,到处运行。Wirte Once, Run Anywhere。 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。 Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。 Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。 1. Class类文件的结构 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。 Class文件是一组以8位字节为基础单元的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。 Class文件中只有两种数据类型:无符号数和表。 无符号数属于基本的数据类型,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。 表是有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。 Class文件中的数据项,无论是顺序还是数量,甚至于数据存储的字节序,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。 Class文件格式如下: 下面,我们就Class文件中各个数据项的具体含义进行分析。 魔数(magic) 每个Class文件的头4个字节称为魔数(magic),它的唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。它的值固定为0xCAFEBABE。 Class文件版本(version) 紧接着magic的4个字节存储的是Class文件的次版本号和主版本号,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件。 常量池(constant_pool) 紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。 常量池中主要存放两大类常量:字面量和符号引用。 字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。 符号引用总结起来则包括了下面三类常量: 类和接口的全限定名(即带有包名的Class名,如:org.lxh.test.TestClass) 字段的名称和描述符(private、static等描述符) 方法的名称和描述符(private、static等描述符) 虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。 这里说明下符号引用和直接引用的区别与关联: 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。 访问标志(access_flags) 在常量池结束之后,紧接着的两个字节表示访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。包括: 这个Class是类还是接口; 是否定义为public类型; 是否定义为abstract类型; 如果是类的话,是否被声明为final等。 访问标志包括public/protected/private/abstract/final等等。 类索引、父类索引与接口索引集合 类索引(this_class):用于确定这个类的全限定名(u2类型)。 父类索引(super_class):用于确定这个类的父类的全限定名(u2类型)(Java不允许多重继承!!!)。 接口索引集合(interfaces):用于描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右在接口索引集合中(u2类型数据集合)。 字段表集合(field_info) 字段表(field_info)用于描述接口或者类中声明的变量。 字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。 可以包括的信息包括: 字段的作用域(public/private/protected修饰符) 是实例变量还是类变量(static修饰符) 可变性(final修饰符) 并发可见性(volatile修饰符) 可否被序列化(transient修饰符) 字段数据类型(基本类型、对象、数组) 字段名称等。 字段表格式如下: 其中的access_flags与类中的access_flags类似,是表示数据类型的修饰符,如public、static、volatile等。 后面的name_index和descriptor_index都是对常量池的引用,分别代表字段的简单名称及字段和方法的描述符。 注意:字段表集合中不会列出从超类或父接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。 方法表集合(method_info) 方法表(method_info)的结构与字段表的结构相同,如下表所示。 方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里。 与字段表集合相对应,如果父类方法在子类中没有被覆写,方法表集合中就不会出现来自父类的方法信息。但同样,有可能会出现由编译器自动添加的方法,最典型的便是类构造器<clinit>方法和实例构造器<init>方法。 在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。 属性表集合(attribute_info) 在前面的Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。 Code属性: Java程序方法体中的代码讲过Javac编译后,生成的字节码指令便会存储在Code属性中,但并非所有的方法表都必须存在这个属性,比如接口或抽象类中的方法就不存在Code属性。 Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码和元数据两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。 Exception属性: 这里的Exception属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。它的结构很简单,只有attribute_name_index、attribute_length、number_of_exceptions、exception_index_table四项,从字面上便很容易理解,这里不再详述。 LineNumberTable属性: 它用于描述Java源码行号与字节码行号之间的对应关系。 LocalVariableTable属性: 它用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的对应关系。 SourceFile属性: 它用于记录生成这个Class文件的源码文件名称。 ConstantValue属性: ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。 在Java中,对非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量(static变量),则有两种方式可以选择: 在类构造其中赋值 使用ConstantValue属性赋值 下面简要说明下final、static、static final修饰的字段赋值的区别: static修饰的字段在类加载过程中的准备阶段被初始化为0或null等默认值,而后在初始化阶段(触发类构造器)才会被赋予代码中设定的值,如果没有设定值,那么它的值就为默认值。 final修饰的字段在运行时被初始化(可以直接赋值,也可以在实例构造器中赋值),一旦赋值便不可更改; static final修饰的字段在Javac时生成ConstantValue属性,在类加载的准备阶段根据ConstantValue的值为该字段赋值,它没有默认值,必须显式地赋值,否则Javac时会报错。可以理解为在编译期即把结果放入了常量池中。 InnerClasses属性: 该属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那么编译器将会为它及它所包含的内部类生成InnerClasses属性。 Deprecated属性和Synthetic属性: 该属性用于表示某个类、字段和方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@Deprecated注释进行设置。 Synthetic属性: 该属性代表此字段或方法并不是Java源代码直接生成的,而是由编译器自行添加的,如this字段和实例构造器、类构造器等。 2. 字节码指令简介 Java虚拟机的指令由一个字节长度的、代表某种特定操作含义的数字(称为操作码,opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,oprands)而构成。 2.1 加载和存储指令 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。 包括:*load/*store/*push/wide/...... 2.2 运算指令 运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。 包括:*add/*sub/*mul/*div/*rem/*neg/*sh*/*or/*and/*xor/*inc/*cmp*/... 2.3 类型转换指令 类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。 Java虚拟机直接支持以下数值类型的宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换): int类型到long/float/double类型; long类型到float/double类型; float类型到double类型。 相对的,处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成。 包括:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f/... 2.4 对象创建与访问指令 对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。 包括: 创建类实例的指令:new 创建数组的指令:newarray/anewarray/multianewarray 访问类字段(static字段)和实例字段(非static字段):getfield/putfield/getstatic/putstatic 把一个数组元素加载到操作数栈的指令:baload/caload/saload/... 将一个操作数栈的值存储到数组元素中的指令:bastore/castore/sastore/... 取数组长度的指令:arraylength 检查类实例类型的指令:instanceof/checkcast 2.5 操作数栈管理指令 Java虚拟机提供了一些用于直接操作操作数栈的指令。 包括:pop/pop2/dup*/swap/... 2.6 控制转移指令 控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。 包括: 条件分支:ifeq/iflt/... 复合条件分支:tableswitch/lookupswitch 无条件分支:goto/goto_w/jsr/jsr_w/ret 2.7 方法调用及返回指令 包括: invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派 invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 invokestatic指令用于调用类方法(static方法) invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法 前面四条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 2.8 异常处理指令 在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现。 2.9 同步指令 同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。 四、虚拟机类加载机制 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 1. 类加载过程 在Java语言里面,类型的加载、连接、初始化过程都是在程序运行期间完成的。 特点:灵活性、动态扩展(运行期动态加载和动态连接) 类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括: 加载(Loading) 验证(Verification) 解析(Resolution) 初始化(Initialization) 使用(Using) 卸载(Unloading) 那么,什么情况下需要开始类加载过程的第一个阶段加载呢?!!有且只有五种情况!! 遇到new/getstatic/putstatic/invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化(分别对应于:使用new实例化对象、读取或设置类的静态字段、调用一个类的静态方法)。 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 当使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 被动引用: 通过子类引用父类的静态字段,不会导致子类初始化; 通过数组定义来引用类,不会触发此类的初始化; 常量在编译阶段会调入类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。(常量传播优化) 对于接口的加载过程,我们需要注意的是:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口时才会初始化。 类加载过程主要包括加载、验证、准备、解析和初始化5个阶段。 1.1 加载 在加载阶段,虚拟机需要完成以下三件事情: 通过一个类的全限定名来获取定义此类的二进制字节流; 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构; 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 值得注意的是,虚拟机设计团队在加载阶段搭建了一个相当开放的、广阔的“舞台”。Java发展历程中,开发人员在这个舞台上玩出了各种花样,例如: 从zip包中读取,最终成为jar/war格式的基础。 从网络中获取,最典型应用就是applet。 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass的代理类的二进制字节流。 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。 非数组类的加载: 对于非数组类的加载,既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成。开发人员可以通过定义自己的类加载器去控制字节流的获取方式。 数组类的加载: 数组类本身不通过类加载器去创建,而是由Java虚拟机直接创建。一个数组类创建过程遵循以下规则: 如果数组的组件类型是引用类型,采用加载过程去加载这个组件类型。 如果数组的组件类型不是引用类型,Java虚拟机将会把数组类标记为与引导类加载器关联。 数组类的可见性与它的组件类型的可见性一致。 1.2 验证 验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。验证是虚拟机对自身保护的一项重要工作。 从整体上看,验证阶段大致上会完成下面4个阶段的检验动作: 文件格式验证 第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合一个Java类型信息的要求。 元数据验证 第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。该验证阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。 字节码验证 第三阶段是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。该验证阶段的主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 符号引用验证 第四阶段是对类自身以外的信息进行匹配性校验。该验证阶段的主要目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类。 对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段。 1.3 准备 准备阶段是为类变量分配内存并设置类变量初始值的阶段。 注意:此时进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量。 考虑下面一个问题: 试比较下面两种情况下在准备阶段后value对应的值是多少。 // 情形一 public static int value = 123; // 情形二 public static final int value = 123; 答案是:对于情形一,准备阶段后value的值为0;对于情形二,准备阶段后value的值为123。 原因在于,情形一下value的赋值操作是在<init>部分完成的,而在情形二下,value对应为ConstantValue属性。 1.4 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 虚拟机规范中并未规定解析阶段发生的具体时间。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。 类或接口的解析 假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机完成整个解析的过程需要以下三个步骤: 如果C不是一个数组类型,那虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C。 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否有访问C的权限。 字段解析 要解析一个未被解析过的字段符号引用,首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析。虚拟机规范要求按照以下步骤进行搜索: 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束; 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束; 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束; 否则,查找失败,抛出java.lang.NoSuchFieldError异常。 类方法解析 对于类方法解析,其首先需要先解析出类方法表class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,接下来虚拟机将会按照如下步骤进行后续的类方法搜索: 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常; 如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束; 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接饮用,查找结束; 否则,在类C实现的接口列表以及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,则说明类C是一个抽象类,这是查找结束,抛出java.lang.AbstractMethodError异常; 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。 接口方法解析 对于接口方法解析,也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,解析成功后,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索: 与类方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,则直接抛出java.lang.IncompatibleClassChangeError异常; 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束; 否则,在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束; 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。 1.5 初始化 类初始化阶段是类加载的最后一步。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。也就是说,初始化阶段是执行类构造器<clinit>方法的过程。 <clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。例如: public class Test { static { i = 0; // 给变量赋值可以正常编译通过 System.out.println(i); // 非法前向引用!!! } static int i = 1; } <clinit>方法与类的构造函数(实例构造器<init>)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>方法执行之前,父类的<clinit>方法已经执行完毕。 由于父类的<clinit>方法先执行,也就有,父类中定义的静态语句块要优先于子类的变量赋值操作。 <clinit>方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>方法。 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作。但是接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法,只有当父接口中定义的变量使用时,父接口才会初始化。 虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>方法完毕。同时,需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>方法的那条线程退出<clinit>方法后,其他线程唤醒之后不会再次进入<clinit>方法。同一个类加载器下,一个类型只会初始化一次。 2. 类加载器 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。 2.1 类与类加载器 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 2.2 类加载器分类 从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器: 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分; 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 从 Java 开发人员的角度看,类加载器可以划分得更细致一些: 启动类加载器(Bootstrap ClassLoader) 此类加载器负责将存放在 <JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 扩展类加载器(Extension ClassLoader) 这个类加载器是由 ExtClassLoader实现的。它负责将<JAVA_HOME>/lib/ext或者被 java.ext.dir系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 应用程序类加载器(Application ClassLoader) 这个类加载器是由AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 2.3 双亲委派模型 应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。 工作过程 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。 好处 使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。 实现 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ //check the class has been loaded or not Class c = findLoadedClass(name); if(c == null) { try{ if(parent != null) { c = parent.loadClass(name, false); } else{ c = findBootstrapClassOrNull(name); } } catch(ClassNotFoundException e) { //if throws the exception , the father can not complete the load } if(c == null) { c = findClass(name); } } if(resolve) { resolveClass(c); } return c; } 五、参考资料 深入理解 Java 虚拟机 Jvm memory Memory Architecture Of JVM(Runtime Data Areas)
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79638755 建议参考官网教程:Setting Up Clustering 一、实验环境 主机节点系统版本: Ubuntu 14.04 (64bit) odl@mpodl:~$ uname -a Linux mpodl 4.2.0-27-generic #32~14.04.1-Ubuntu SMP Fri Jan 22 15:32:26 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux 主机节点硬件配置:单核CPU+4GB内存+50GB硬盘 odl@mpodl:~$ sudo lshw description: Computer product: Standard PC (i440FX + PIIX, 1996) () vendor: QEMU version: pc-i440fx-trusty width: 64 bits capabilities: smbios-2.4 dmi-2.4 vsyscall32 configuration: boot=normal uuid=053D43B6-2E3C-CEA4-4C52-833DDD1749BE *-core description: Motherboard physical id: 0 # CPU信息 *-cpu description: CPU product: QEMU Virtual CPU version 2.0.0 vendor: Intel Corp. physical id: 401 bus info: cpu@0 slot: CPU 1 size: 2GHz capacity: 2GHz width: 64 bits # 内存信息 *-memory description: System Memory physical id: 1000 size: 4GiB # 硬盘信息 *-disk description: ATA Disk product: QEMU HARDDISK physical id: 0.0.0 bus info: scsi@0:0.0.0 logical name: /dev/sda version: 0 serial: QM00001 size: 50GiB (53GB) *-volume:0 description: EXT4 volume vendor: Linux physical id: 1 bus info: scsi@0:0.0.0,1 logical name: /dev/sda1 logical name: / version: 1.0 serial: 65d80188-ddfb-4f08-8018-a0c2e1da8af3 size: 46GiB capacity: 46GiB *-volume:1 description: Extended partition physical id: 2 bus info: scsi@0:0.0.0,2 logical name: /dev/sda2 size: 4093MiB capacity: 4093MiB 集群环境:3台主机节点 Cluster_Node1: Ubuntu 14.04 -- [IP_Addr]=192.168.1.124 Cluster_Node2: Ubuntu 14.04 -- [IP_Addr]=192.168.1.125 Cluster_Node3: Ubuntu 14.04 -- [IP_Addr]=192.168.1.104 所有主机节点都连接到同一台OpenvSwitch交换机上,且能互相PING通!!! 每台主机节点都安装配置有JDK 1.8,安装方法见:Ubuntu下通过PPA方式安装Java 8并自动配置环境变量 二、部署方法 1. 下载Opendaylight Nitrogen 在每台主机节点上执行如下命令,下载tar.gz格式的压缩包到用户目录下的ODL_N子目录: odl@mpodl:~/ODL_N$ wget -P . https://nexus.opendaylight.org/content/repositories/public/org/opendaylight/integration/karaf/0.7.2/karaf-0.7.2.tar.gz 或在官网下载,然后通过xftp软件从本地上传到服务器的Opendaylight运行环境中。 官网地址:https://www.opendaylight.org/technical-community/getting-started-for-developers/downloads-and-documentation 2. 安装Opendaylight Nitrogen 在每台主机节点上执行如下命令,解压压缩包到ODL_N子目录: odl@mpodl:~/ODL_N$ tar -zxvf karaf-0.7.2.tar.gz 解压完成,即认为Opendaylight Nitrogen安装成功。 3. 配置Opendaylight Nitrogen集群 在每台主机节点下执行如下命令,完成集群脚本配置工作: 命令格式:sudo bash ./karaf-0.7.2/bin/configure_cluster.sh [index] [seed_node_list] 其中,[index]为正整数,表示在[seed_node_list]中对应哪个主机节点上配置集群脚本。另外,[seed_node_list]是组成集群的各个主机节点对应的IP地址列表,以空格或逗号隔开。 因此,具体执行命令如下: # Cluster_Node1: IP=192.168.1.124 odl@mpodl:~/ODL_N$ sudo bash ./karaf-0.7.2/bin/configure_cluster.sh 1 192.168.1.124 192.168.1.125 192.168.1.104 # Cluster_Node2: IP=192.168.1.125 odl@mpodl:~/ODL_N$ sudo bash ./karaf-0.7.2/bin/configure_cluster.sh 2 192.168.1.124 192.168.1.125 192.168.1.104 # Cluster_Node3: IP=192.168.1.104 odl@mpodl:~/ODL_N$ sudo bash ./karaf-0.7.2/bin/configure_cluster.sh 3 192.168.1.124 192.168.1.125 192.168.1.104 当执行完之后,结果如下所示: # Cluster_Node1: IP=192.168.1.124 odl@mpodl:~/ODL_N$ sudo bash ./karaf-0.7.2/bin/configure_cluster.sh 1 192.168.1.124 192.168.1.125 192.168.1.104 ################################################ ## Configure Cluster ## ################################################ Configuring unique name in akka.conf Configuring hostname in akka.conf Configuring data and rpc seed nodes in akka.conf modules = [ { name = "inventory" namespace = "urn:opendaylight:inventory" shard-strategy = "module" }, { name = "topology" namespace = "urn:TBD:params:xml:ns:yang:network-topology" shard-strategy = "module" }, { name = "toaster" namespace = "http://netconfcentral.org/ns/toaster" shard-strategy = "module" } ] Configuring replication type in module-shards.conf ################################################ ## NOTE: Manually restart controller to ## ## apply configuration. ## ################################################ 备注: (1)执行如上命令后,会在karaf-0.7.2/configuration目录下生成initial子目录,结果如下所示: odl@mpodl:~/ODL_N$ ls ./karaf-0.7.2/configuration/ context.xml factory initial logback.xml tomcat-logging.properties tomcat-server.xml odl@mpodl:~/ODL_N$ ls ./karaf-0.7.2/configuration/initial/ akka.conf modules.conf module-shards.conf 可见,集群配置脚本在initial子目录下生成了akka.conf、modules.conf和module-shards.conf三个配置文件。 (2)查看第一台Ubuntu主机节点的akka.conf文件内容,具体如下所示: odl@mpodl:~/ODL_N$ cat ./karaf-0.7.2/configuration/initial/akka.conf odl-cluster-data { akka { remote { artery { enabled = off canonical.hostname = "192.168.1.124" # 本机IP地址 canonical.port = 2550 } netty.tcp { hostname = "192.168.1.124" # 本机IP地址 port = 2550 } # when under load we might trip a false positive on the failure detector # transport-failure-detector { # heartbeat-interval = 4 s # acceptable-heartbeat-pause = 16s # } } cluster { # Remove ".tcp" when using artery. # 集群节点列表 seed-nodes = ["akka.tcp://opendaylight-cluster-data@192.168.1.124:2550", "akka.tcp://opendaylight-cluster-data@192.168.1.125:2550", "akka.tcp://opendaylight-cluster-data@192.168.1.104:2550"] roles = ["member-1"] } persistence { # By default the snapshots/journal directories live in KARAF_HOME. You can choose to put it somewhere else by # modifying the following two properties. The directory location specified may be a relative or absolute path. # The relative path is always relative to KARAF_HOME. # snapshot-store.local.dir = "target/snapshots" # journal.leveldb.dir = "target/journal" journal { leveldb { # Set native = off to use a Java-only implementation of leveldb. # Note that the Java-only version is not currently considered by Akka to be production quality. # native = off } } } } } 同样地,第二台和第三台Ubuntu主机节点的akka.conf文件内容相似,只是对应的IP地址不同。 (3)查看第一台Ubuntu主机节点的module-shards.conf文件内容,具体如下所示: odl@mpodl:~/ODL_N$ cat ./karaf-0.7.2/configuration/initial/module-shards.conf module-shards = [ { name = "default" shards = [ { name = "default" replicas = ["member-1", "member-2", "member-3"] } ] }, { name = "inventory" shards = [ { name="inventory" replicas = ["member-1", "member-2", "member-3"] } ] }, { name = "topology" shards = [ { name="topology" replicas = ["member-1", "member-2", "member-3"] } ] }, { name = "toaster" shards = [ { name="toaster" replicas = ["member-1", "member-2", "member-3"] } ] } ] 同样地,第二台和第三台Ubuntu主机节点的module-shards.conf文件内容完全相同。 4. 启动Opendaylight Nitrogen集群 在每台主机节点下执行如下命令,完成Opendaylight节点启动工作: odl@mpodl:~/ODL_N$ ./karaf-0.7.2/bin/karaf karaf: JAVA_HOME not set; results may vary Apache Karaf starting up. Press Enter to open the shell now... 100% [========================================================================] Karaf started in 8s. Bundle stats: 208 active, 209 total ________ ________ .__ .__ .__ __ \_____ \ ______ ____ ____ \______ \ _____ ___.__.| | |__| ____ | |___/ |_ / | \\____ \_/ __ \ / \ | | \\__ \< | || | | |/ ___\| | \ __\ / | \ |_> > ___/| | \| ` \/ __ \\___ || |_| / /_/ > Y \ | \_______ / __/ \___ >___| /_______ (____ / ____||____/__\___ /|___| /__| \/|__| \/ \/ \/ \/\/ /_____/ \/ Hit '<tab>' for a list of available commands and '[cmd] --help' for help on a specific command. Hit '<ctrl-d>' or type 'system:shutdown' or 'logout' to shutdown OpenDaylight. opendaylight-user@root> 然后,执行命令feature:list -i检查odl-mdsal-clustering是否处于已安装状态。如果没有安装,则执行命令feature:install odl-mdsal-clustering完成对应Feature的安装。 opendaylight-user@root>feature:list -i Name | Version | Required | State | Repository | Description ---------------------------------------------------------------------------------------------------------------------------------------------------- odl-mdsal-broker | 1.6.2 | | Started | odl-mdsal-1.6.2 | odl-mdsal-broker odl-mdsal-clustering | 1.6.2 | x | Started | odl-mdsal-clustering | odl-mdsal-clustering 5. 检查Opendaylight Nitrogen集群是否建立 在每台主机节点下执行如下命令,获取主机节点的角色信息(Leader/Follower): opendaylight-user@root> ld | grep clustering 于是,在主机节点一,输出如下: 2018-03-21 13:22:45,688 | INFO | d-dispatcher-125 | Shard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | member-1-shard-prefix-configuration-shard-config (Candidate): Starting new election term 21 2018-03-21 13:22:45,741 | INFO | d-dispatcher-125 | Shard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | member-1-shard-prefix-configuration-shard-config (Candidate) :- Switching from behavior Candidate to Leader, election term: 21 2018-03-21 13:22:45,742 | INFO | ult-dispatcher-5 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-1-shard-prefix-configuration-shard-config , received role change from Candidate to Leader 2018-03-21 13:22:45,821 | INFO | d-dispatcher-121 | Shard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | member-1-shard-prefix-configuration-shard-operational (Candidate): Starting new election term 21 2018-03-21 13:22:45,858 | INFO | d-dispatcher-125 | Shard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | member-1-shard-prefix-configuration-shard-operational (Candidate) :- Switching from behavior Candidate to Leader, election term: 21 2018-03-21 13:22:45,858 | INFO | ult-dispatcher-5 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-1-shard-prefix-configuration-shard-operational , received role change from Candidate to Leader 2018-03-21 13:22:45,872 | INFO | d-dispatcher-125 | EntityOwnershipShard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | member-1-shard-entity-ownership-operational (Candidate): Starting new election term 21 2018-03-21 13:22:45,912 | INFO | d-dispatcher-145 | EntityOwnershipShard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | member-1-shard-entity-ownership-operational (Candidate) :- Switching from behavior Candidate to Leader, election term: 21 2018-03-21 13:22:45,921 | INFO | lt-dispatcher-21 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-1-shard-entity-ownership-operational , received role change from Candidate to Leader 在主机节点二,输出如下: 2018-03-21 13:22:36,286 | INFO | ult-dispatcher-4 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-2-shard-prefix-configuration-shard-config , received role change from null to Follower 2018-03-21 13:22:36,287 | INFO | ult-dispatcher-4 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-2-shard-prefix-configuration-shard-operational , received role change from null to Follower 2018-03-21 13:22:36,287 | INFO | ult-dispatcher-4 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-2-shard-prefix-configuration-shard-config , registered listener akka://opendaylight-cluster-data/user/shardmanager-config 2018-03-21 13:22:36,287 | INFO | ult-dispatcher-4 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-2-shard-prefix-configuration-shard-operational , registered listener akka://opendaylight-cluster-data/user/shardmanager-operational 2018-03-21 13:22:36,305 | INFO | rd-dispatcher-32 | EntityOwnershipShard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | Starting recovery for member-2-shard-entity-ownership-operational with journal batch size 1 2018-03-21 13:22:36,313 | INFO | rd-dispatcher-38 | EntityOwnershipShard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | Recovery completed - Switching actor to Follower - Persistence Id = member-2-shard-entity-ownership-operational Last index in log = -1, snapshotIndex = -1, snapshotTerm = -1, journal-size = 0 2018-03-21 13:22:36,317 | INFO | ult-dispatcher-2 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-2-shard-entity-ownership-operational , received role change from null to Follower 在主机节点三,输出如下: 2018-03-21 13:22:39,418 | INFO | ult-dispatcher-6 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-3-shard-prefix-configuration-shard-operational , received role change from null to Follower 2018-03-21 13:22:39,418 | INFO | ult-dispatcher-6 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-3-shard-prefix-configuration-shard-config , received role change from null to Follower 2018-03-21 13:22:39,418 | INFO | ult-dispatcher-6 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-3-shard-prefix-configuration-shard-operational , registered listener akka://opendaylight-cluster-data/user/shardmanager-operational 2018-03-21 13:22:39,418 | INFO | ult-dispatcher-6 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-3-shard-prefix-configuration-shard-config , registered listener akka://opendaylight-cluster-data/user/shardmanager-config 2018-03-21 13:22:39,466 | INFO | rd-dispatcher-23 | EntityOwnershipShard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | Starting recovery for member-3-shard-entity-ownership-operational with journal batch size 1 2018-03-21 13:22:39,470 | INFO | rd-dispatcher-23 | EntityOwnershipShard | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | Recovery completed - Switching actor to Follower - Persistence Id = member-3-shard-entity-ownership-operational Last index in log = -1, snapshotIndex = -1, snapshotTerm = -1, journal-size = 0 2018-03-21 13:22:39,473 | INFO | lt-dispatcher-31 | RoleChangeNotifier | 120 - org.opendaylight.controller.sal-clustering-commons - 1.6.2 | RoleChangeNotifier for member-3-shard-entity-ownership-operational , received role change from null to Follower 可以看出,第一台Ubuntu主机节点成为Leader,其它两台Ubuntu主机节点成为 Follower ,集群配置成功。但是,从日志也可以看出,对于不同的Shard,存在不同的集群关系。 三、小结 本文详细介绍了Opendaylight Nitrogen集群的搭建指南,后续碰到问题将继续补充。
一、问题的提出 1. 什么是Session? 用户使用网站的服务,需要使用浏览器与Web服务器进行多次交互。HTTP协议本身是无状态的,需要基于HTTP协议支持会话状态(Session State)的机制。具体的实现方式是:在会话开始时,分配一个 唯一的会话标识(SessionID),并通过Cookie将这个标识告诉浏览器,以后每次请求的时候,浏览器都会带上这个会话标识SessionID来告诉Web服务器这个请求是属于哪个会话的。在Web服务器上,各个会话都有独立的存储,保存不同会话的信息。如果遇到禁用Cookie的情况,一般的做法就是把这个会话标识放到URL的参数中。 2. 什么是Session一致性问题? 当Web服务器从一台变为多台时,就会出现Session一致性问题。 如上图所示,当一个带有会话标识的HTTP请求到了Web服务器后,需要在HTTP请求的处理过程中找到对应的会话数据(Session)。但是,现在存在的问题就是:如果我第一次访问网站时请求落到了左边的服务器,那么我的Session就创建在左边的服务器上了,如果我们不做处理,就不能保证接下来的请求每次都落在同一边的服务器上了。这就是Session一致性问题。 二、Session一致性解决方案 1. Session Stiky 在单机的情况下,会话保存在单机上,请求也是由这个机器处理,因此不会有问题。当Web服务器变为多台以后,如果保证同一个会话的请求都在同一个Web服务器上处理,则对该会话来说,与之前单机的情况是一样的。 如果要做到这样,就需要负载均衡器能够根据每次请求的会话标识SessionID来进行请求转发,如下图所示。这种方式称之为Session Stiky方式。 该方案本身非常简单,对于Web服务器来说,该方案和单机的情况是一样的,只是我们在负载均衡器上做了手脚。这个方案可以让同样Session的请求每次都发送到同一个Web服务器来处理,非常利于针对Session进行服务端本地的缓存。 其所存在的问题包括: 如果有一台Web服务器宕机或者重启,则该机器上的会话数据就会丢失。如果会话中有登录状态数据,则用户需要重新登陆。 会话标识是应用层的信息,则负载均衡器要将同一个会话的请求都保存到同一个Web服务器上的话,就需要进行应用层(七层)的解析,这个开销比第四层的交换要大。 负载均衡器变为了一个有状态的节点,要将会话保存到具体Web服务器的映射,因此内存消耗会更大,容灾会更麻烦。 打个比方来说,对于Session Stiky,如果说Web服务器是我们每次吃饭的饭店,会话数据就是我们吃饭用的碗筷。要保证每次吃饭都用自己的碗筷,我就把餐具存在某一家,并且每次都去这家店吃,这是个不错的主意。 2. Session Replication 如果我们继续以去饭店吃饭类比,那么除了前面的方式之外,如果我在每个店都存放一套自己的餐具,就可以更加自由地选择饭店。Session Replication就是这样一种方式,如下图所示。 可以看到,在Session Replication方案中,不再要求负载均衡器来保证同一个会话地多次请求必须到同一个Web服务器上了。而我们的Web服务器之间则增加了会话数据的同步。通过同步就保证了不同Web服务器之间的Session数据的一致。 但是,Session Replication方案也存在一些问题,包括: 同步Session数据造成了网络带宽的开销。只要Session数据有变化,就需要将数据同步到其他所有机器上,机器数越多,同步带来的网络带宽开销就越大。 每台Web服务器都要保存所有的Session数据,如果整个集群的Session数很多的话,每台机器用于保存Session数据的内容占用会很严重。 这就是Session Replication方案。这个方案是靠应用容器来完成Session的复制从而使得应用解决Session问题的,应用本身并不关心这个事情。不过,这个方案并不适合集群机器数多的场景。如果只有几台机器,用该方案是可以的。 3. Session数据集中存储 同样是希望同一个会话的请求可以发到不同的Web服务器上,前面的Session Replication是一种方案,还有一种方案就是把Session数据集中存储起来,然后不同Web服务器从同样的地方来获取Session。其大概的结构如下图所示: 可以看到,与Session Replication方案一样的部分是,会话请求经过负载均衡器后,不会被固定在同样的Web服务器上。不同的地方是,Web服务器之间没有Session数据复制,并且Session数据也不是保存在本机了,而是放在了另一个集中存储的地方。这样,无论是哪台Web服务器,也无论修改的是哪个Session的数据,最终的修改都发生在这个集中存储的地方,而Web服务器使用Session数据时,也是从这个集中存储Session数据的地方来读取。对于Session数据存储的具体方式,可以使用数据库,也可以使用其他分布式存储系统。这个方案解决了Session Replication方案中内存的问题,而对于网络带宽,该方案也比Session Replication要好。 不过,该方案仍存在一些问题,包括: 读写Session数据引入了网络操作,这相对于本机的数据读取来说,问题就在于存在时延和不稳定性,不过由于通信基本发生在内网,问题不大。 如果集中存储Session的机器或者集群存在问题,这就会影响我们的应用。 相对于Session Replication,当Web服务器数量比较大时、Session数比较多的时候,集中存储方案的优势是非常明显的。 4. Cookie Based 对于Cookie Based方案,它对同一个会话的不同请求也是不限制具体处理机器的。与Session Replication和Session数据集中管理的方案不同,这个方案是通过Cookie来传递Session数据的。具体如下图所示。 可以看出,我们的Session数据存放在Cookie中,然后在Web服务器上从Cookie中生成对应的Session数据。这就好比我每次都把自己的碗筷带在身上,这样我去哪家饭店吃饭就可以随意选择了。相对于前面的集中存储,这个方案不会依赖外部的一个存储系统,也就不存在从外部系统获取、写入Session数据的网络时延和不稳定性了。 不过,该方案依然存在不足,包括: Cookie长度限制。由于Cookie是有长度限制的,这也会限制Session数据的长度。 安全性。Session数据本来都是服务器端数据,而这个方案是让这些服务端数据到了外部外部网络及客户端,因此存在安全性的问题。 带宽消耗。这里指的不是内部Web服务器之间的带宽的消耗,而是我们数据中心的整体外部贷款的消耗。 性能消耗。每次HTTP请求和响应都带有Session数据,对Web服务器来说,在同样的处理情况下,响应的结果输出越少,支持的并发请求就会越多。 三、总结 综合而言,上述所有方案都是解决session问题的方案,对于大型网站来说,Session Sticky和Session集中管理是比较好的方案。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79602051 一、Java内存模型基础 1. 两个关键问题 2. Java内存模型的抽象结构 3. 指令序列的重排序 4. 并发编程模型的分类 5. happens-before 二、指令重排序 1. 数据依赖性 2. as-if-serial语义 3. 重排序对多线程的影响 三、顺序一致性内存模型 1、数据竞争与顺序一致性 2、顺序一致性内存模型 3. 同步程序的顺序一致性效果 四、volatile的内存语义 1、volatile的特性 2、volatile读写建立的happens-before关系 3、volatile写读的内存语义 4. volatile内存语义的实现 5. 为什么增强volatile的内存语义 五、锁的内存语义 1、锁的释放获取所建立的happens-before关系 2、锁的释放和获取的内存语义 3、锁内存语义的实现 4、concurrent包的实现 六、final域的内存语义 七、happens-before 1、JMM的设计 2、happens-before的定义 3、happens-before规则 八、双重检查锁定与延迟初始化 1、双重检查锁定 2、问题的根源 3、基于volatile的解决方案 4、基于类初始化的解决方案 九、Java内存模型综述 十、小结 一、Java内存模型基础 1. 两个关键问题 线程之间如何通信; 线程之间如何同步。 线程之间的通信机制:共享内存+消息传递。 在共享内存的并发模型里,线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。 Java并发采用的是共享内存模型,Java线程之间的通信总是隐式进行的。 2. Java内存模型的抽象结构 Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。 JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读写共享变量的副本。 从上图可以看出,如果线程A和线程B之间要通信的话,必须要经历两个步骤: 线程A把本地内存A中更新过的共享变量刷新到主内存中去; 线程B到主内存中去读取线程A之前已经更新过的共享变量。 从整体上看,上述两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。 3. 指令序列的重排序 为了提高性能,编译器和处理器常常会对指令进行重排序。 重排序包括以下三种类型: 编译器优化的重排序 指令级并行的重排序——指令级并行技术。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 内存系统的重排序 对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。 对于处理器重排序,JMM的处理器重排序规则要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。 4. 并发编程模型的分类 处理器使用写缓冲区临时保存向内存写入的数据。 示例: 其内部执行过程如下所示: 这里,处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x=y=0的结果。 为了保证内存可见性,Java编译器在生成指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序。JMM的内存屏障指令分为以下4类: 5. happens-before Java JSR-133使用happens-before概念来阐述操作之间的内存可见性。 与程序员密切相关的happens-before规则如下: 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 volatile变量规则:对一个volatile变量的写,happens-before于任意后续对这个volatile变量的读。 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 happens-before与JMM的关系: 二、指令重排序 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 1. 数据依赖性 如果两个操作访问同一个变量,且这两个操作中有一个写操作,此时这两个操作之间就存在数据依赖性。 对于数据依赖性,主要分为三种类型: 对于上述三种情况,只要重排序两个操作的操作顺序,程序的执行结果就会被改变。 2. as-if-serial语义 as-if-serial语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。 例如: double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C 对于上述操作的数据依赖关系,如下所示: 其存在两种执行顺序,如下所示: as-if-serial语义使单线程程序员无需担心重排序是否会干扰到其正常运行,也无需担心内存可见性问题。 3. 重排序对多线程的影响 示例代码: class RecordExample { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a * a; //4 } } } 可能的程序执行时序图如下: 三、顺序一致性内存模型 顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参考。 1、数据竞争与顺序一致性 Java内存模型规范对数据竞争的定义如下: 在一个线程写一个变量; 在另一个线程读同一个变量; 读和写没有通过同步来排序。 当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。 顺序一致性(Sequentially Consistent)——程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。 2、顺序一致性内存模型 顺序一致性内存模型有两大特性: 一个线程中的所有操作必须按照程序的顺序来执行; 所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。 顺序一致性内存模型为程序员提供的视图: 当多个线程并发执行时,上图的开关装置能把所有线程的所有内存读写操作串行化,即在顺序一致性模型中,所有操作之间具有全序关系。 举例说明:A和B两个线程。 一种执行过程(同步): 另一种执行过程(非同步): 3. 同步程序的顺序一致性效果 示例程序: class SynchronizeExample { int a = 0; boolean flag = false; public synchronized void writer() { // 获取锁 a = 1; flag = true; } // 释放锁 public synchronized void reader() { // 获取锁 if (flag) { int i = a; //... } } // 释放锁 } 两个内存模型中的执行顺序: JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。 未同步程序在两个模型中的执行特性存在如下几个差异: 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(重排序)。 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。 JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读写操作具有原子性。 总线仲裁 –> 总线事务!!! 四、volatile的内存语义 1、volatile的特性 volatile变量具有以下特性: 可见性。 原子性。对任意单个volatile变量的读写具有原子性,但类似volatile++这种复合操作不具有原子性。 2、volatile读写建立的happens-before关系 volatile对于线程的内存可见性的影响比volatile自身的特性更为重要。 从内存语义上来说,volatile的写读与锁的释放获取有相同的内存效果。 请看下面使用volatile变量的示例代码: class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 ... } } } 假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,该过程建立的happens-before关系可以分为3类: 根据程序次序规则:1 happens-before 2; 3 happens-before 4。 根据volatile规则:2 happens-before 3。 根据happens-before的传递性规则:1 happens-before 4。 因此,上述关系的图形化表现形式如下: 在上图中,每一个箭头链接的两个节点,代表了一个happens-before关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens-before保证。 3、volatile写读的内存语义 volatile写的内存语义如下: 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。 volatile读的内存语义如下: 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效。线程接下来将从主内存中读取共享变量。 总结一下: 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了其对共享变量所做修改的消息。 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的在写这个volatile变量之前对共享变量所做修改的消息。 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。 4. volatile内存语义的实现 为了实现volatile内存语义,JMM会限制编译器重排序和处理器重排序。 规则表: 从上表,我们可以看出: 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。其确保了volatile写之前的操作不会被编译器重排序到volatile写之后。 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。其确保了volatile读之后的操作不会被编译器重排序到volatile读之前。 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。 实现方式:内存屏障。 具体内存屏障插入策略如下: 在每个volatile写操作前插入一个StoreStore屏障。 在每个volatile写操作后插入一个StoreLoad屏障。 在每个volatile读操作后插入一个LoadLoad屏障。 在每个volatile读操作后插入一个LoadStore屏障。 保守策略下,volatile写插入内存屏障后生成的指令序列示意图: StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。volatile写之后的StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读写操作重排序(保守策略)。 保守策略下,volatile读插入内存屏障后生成的指令序列示意图: LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。 举一个例子: class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1; // 第一个volatile读 int j = v2; // 第二个volatile读 a = i + j; // 普通写 v1 = i + 1; // 第一个volatile写 v2 = j * 2; // 第二个volatile写 } ... //写 } 对应的指令序列示意图如下: 5. 为什么增强volatile的内存语义 class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 ... } } } 在旧的内存模型中,可以使用指令重排序,因此,时序图如下所示: 结果:读线程B执行4时,不一定能看到线程A在执行1时对共享变量的修改。 volatile内存语义增强: volatile严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写读和锁的释放获取具有相同的内存语义。 不过,要很好地利用volatile来完成锁机制下的并发过程,是十分困难的,一定要谨慎。 五、锁的内存语义 1、锁的释放获取所建立的happens-before关系 锁是Java并发编程中最重要的同步机制。 示例: class MonitorExample { int a = 0; public synchronized void writer() { //1 a++; //2 } //3 public synchronized void reader() { //4 int i = a; //5 ... } //6 } 假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,该过程包含三类关系: 程序次序规则:1 happens-before 2, 2 happens-before 3, 4 happens-before 5, 5 happens-before 6。 监视器锁规则:3 happens-before 4。 happens-before的传递性:2 happens-before 5。 示意图如下: 在上图中,每一个箭头链接的两个节点,代表了一个happens-before关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的happens-before保证。 2、锁的释放和获取的内存语义 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。 当线程获取锁时,JMM会把该线程对应的本地内存设置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取变量。 总结一下: 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了线程A对共享变量所做修改的消息。 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的在释放这个锁之前对共享变量所做修改的消息。 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程b发送消息。 3、锁内存语义的实现 在这里,我们借助ReentrantLock的源码,来分析锁内存语义的具体实现机制。 示例代码: class ReentrantLockExample { int a = 0; ReentrantLock lock = new ReentrantLock(); public void writer() { lock.lock(); //获取锁 try { a++; } finally { lock.unlock(); //释放锁 } } public void reader() { lock.lock(); //获取锁 try { int i = a; ... } finally { lock.unlock(); //释放锁 } } } ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSychronizer(AQS)。 基本类图: ReentrantLock分为公平锁和非公平锁。 公平锁的加锁过程: Step1: ReentrantLock.lock(); Step2: FairSync.lock(); Step3: AbstractQueuedSynchronizer.acquire(int arg); Step4: ReentrantLock.tryAcquire(int acquires); 第四步为核心,如下: protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //获取锁的开始,首先读取volatile变量state if (c == 0) { if (isFirst(current) && compareAndSetState(0, acquires)) { return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded."); } setState(nextc); return true; } return false; } 核心:读取volatile变量state。 公平锁的解锁过程: Step1: ReentrantLock.unlock(); Step2: AbstractQueuedSynchronizer.release(int arg); Step3: Sync.tryRelease(int releases); 第三步为核心,如下: protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(); } boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); //释放锁的最后,写volatile变量state return free; } 公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。 4、concurrent包的实现 Java线程之间的通信方式: A线程写volatile变量,随后B线程读这个volatile变量。 A线程写volatile变量,随后B线程使用CAS更新这个volatile变量。 A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。 A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。 六、final域的内存语义 对于final域,编译器和处理器要遵守两个重排序规则: 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。 七、happens-before happens-before是JMM最核心的概念。 1、JMM的设计 在设计JMM时,需要考虑两个关键因素: 程序员对内存模型的使用。程序员希望基于一个强内存模型来编写代码。 编译器和处理器对内存模型的实现。编译器和处理器希望实现一个弱内存模型。 由于上述两个因素的互相矛盾,因此需要找到一个好的平衡点:一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。 JMM把happens-before要求禁止的重排序分为下面两类: 会改变程序执行结果的重排序; 不会改变程序执行结果的重排序。 JMM对于这两种不同性质的重排序,采取了不同的策略,如下: 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求。 如下所示: 如上所示,可以得出以下两点: JMM向程序员提供的happens-before规则能满足程序员的需求。 JMM对编译器和处理器的束缚已经尽可能少。 基本原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。 2、happens-before的定义 定义如下: 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 —— JMM对程序员的承诺 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。只要结果一致,重排序就不非法。 —— JMM对编译器和处理器重排序的约束原则 3、happens-before规则 程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。 锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。 volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。 传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。 线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。 线程启动规则示例: 线程终结规则示例: 八、双重检查锁定与延迟初始化 1、双重检查锁定 写一个线程安全的单例模式: public class DoubleCheckedLocking { //1 private static Instance instance; //2 public static Instance getInstance() { //3 if (instance == null) { //4:第一次检查 synchronized(DoubleCheckedLocking.class) { //5:加锁 if (instance == null) { //6:第二次检查 instance = new Instance(); //7:问题的根源 } //8 } //9 } //10 return instance; } } 问题: 在线程执行到第四行,代码读取到instance不为null时,instance引用的对象有可能没有完成初始化。 2、问题的根源 示例代码第7行instance = new Instance();创建对象,其可分解为: memory = allocate(); //1. 分配对象的内存空间 ctorInstance(memory); //2. 初始化对象 instance = memory; //3. 设置instance指向刚分配的内存地址 上面2和3之间可能会被重排序。2和3之间重排序之后的执行时序(并不违反JMM规则)如下: memory = allocate(); //1. 分配对象的内存空间 instance = memory; //2. 设置instance指向刚分配的内存地址 // 注意:此时对象还未初始化 ctorInstance(memory); //3. 初始化对象 上述过程多线程下并发执行的情况: 单线程的执行时序图: 多线程的执行时序图: 所以对于上述多线程情况,可以知道,线程B访问目标对象时,目标对象并未进行初始化。此处就会出现问题。 如何解决? 两种方法: 不允许2和3重排序; 允许2和3重排序,但不允许其他线程看到这个重排序。 3、基于volatile的解决方案 public class SafeDoubleCheckedLocking { private volatile static Instance instance; public static Instance getInstance() { if (instance == null) { synchronized (SafeDoubleCheckedLocking.class) { if (instance == null) { instance = new Instance(); //instance为volatile,现在没有问题了 } } } } } 当声明对象的引用为volatile时,2和3之间的重排序在多线程环境中将会被禁止。 4、基于类初始化的解决方案 JVM在类的初始化阶段,会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程多同一个类的初始化。 public class InstanceFactory { private static class InstanceHolder { public static Instance instance = new Instance(); } public static Instance getInstance() { return InstanceHolder.instance; //这里将触发InstancHolder类被初始化 } } 上述过程如下: 该方案的实质是:允许2和3重排序,但不允许非构造线程看到这个重排序。 附加: 一个类或接口被初始化的5种情况: 1. T是一个类,而且一个T类型的实例被创建; 2. T是一个类,且T中声明的一个静态方法被调用; 3. T中声明的一个静态字段被赋值; 4. T中声明的一个静态字段被使用,而且这个字段不是一个常量字段; 5. T是一个顶级类,而且一个断言语句嵌套在T内部被执行。 类初始化的处理过程的五个阶段: 第一阶段:通过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁。 第二阶段:线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待。 第三阶段:线程A设置state=initialized,然后唤醒在condition中等待的所有线程。 第四阶段:线程B结束类的初始化处理。 第五阶段:线程C执行类的初始化的处理。 静态内部类的加载过程:静态内部类的加载不需要依附外部类,在使用时才加载。 九、Java内存模型综述 十、小结 本文对Java内存模型做了比较全面的解读。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79600749 一、volatile的应用 1. volatile的定义与实现原理 2. volatile的使用优化 二、synchronized的应用 1. 锁的实现原理 2. 锁的对比 2.1 偏向锁 2.2 轻量级锁 2.3 锁的对比 三、原子操作的实现原理 1. 术语 2. 处理器如何实现原子操作 3. Java如何实现原子操作 四、小结 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转换为汇编指令在CPU上执行。 Java中所使用的并发机制依赖于JVM的实现和CPU的指令。 一、volatile的应用 如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。 1. volatile的定义与实现原理 CPU术语表如下所示: 那么,volatile是如何保证可见性的呢? 现在,我们看示例代码,如下所示: instance = new Singleton(); //instance是volatile变量 转变为汇编代码,如下所示: 0x01a3de1d: movb $0×0,0×1104800(%esi); 0x01a3de24: lock addl $0×0,(%esp); 有volatile修饰的共享变量进行写操作时会多出第二行汇编代码。 lock前缀的指令在多核处理器下会引发两件事: 将当前处理器缓存行的数据写回到主内存; 写回主内存操作会使其他CPU里缓存了该内存地址的数据失效。 在详细介绍lock指令之前,我们需要对计算机存储层次结构有一个简单的认识: 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。 volatile的两条实现原则: lock指令会引起处理器缓存回写到内存; 一个处理器的缓存回写到内存会导致其他处理器的缓存失效。 2. volatile的使用优化 使用追加字节的方式来优化队列出队和入队的性能! 为什么追加字节能够提高并发编程的效率呢? 因为对于Intel Core、Atom和Pentium M处理器,其L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行。这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。 是不是在使用volatile变量时都应该追加到64字节?不是,两种场景下不能: 缓存行非64字节宽的处理器; 共享变量不会被频繁地读写。 二、synchronized的应用 1. 锁的实现原理 synchronized实现同步的基础:Java中的每一个对象都可以作为锁。 具体表现为: 对于普通同步方法,锁是当前实例对象; 对于静态同步方法,锁是当前类的Class对象; 对于同步方法块,锁是synchronized括号中配置的对象。 JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。 synchronized所用的锁是存在Java对象头里的。 2. 锁的对比 Java中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。 锁的状态会随着竞争情况逐渐升级,锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。 2.1 偏向锁 背景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程得到锁的代价更低,故引入了偏向锁。 方法:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该进程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。 偏向锁使用了一种等到竞争出现才释放锁的机制,所以,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。 2.2 轻量级锁 轻量级锁加锁 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,该过程称之为Displaced MarkWord。然后,线程尝试使用CAS将对象头的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 轻量级锁解锁 轻量级锁解锁时,会使用原子CAS操作将Displaced MarkWord替换回对象头,如果成功,则表示竞争没有发生。如果失败,则表示当前锁存在竞争,锁就会膨胀成为重量级锁。 2.3 锁的对比 三、原子操作的实现原理 原子操作(Atomic Operation):不可被中断的一个或一系列操作。 1. 术语 2. 处理器如何实现原子操作 处理器通过两种方式来实现原子操作: 使用总线锁保证原子性 第一个机制是通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,则该处理器可以独占共享内存。 使用缓存锁保证原子性 第二个机制是通过缓存锁来保证原子性。总线锁开销大,缓存锁开销小。 两种情况下,处理器不会使用缓存锁: 当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行时,处理器会调用总线锁。 有些处理其不支持缓存锁。 3. Java如何实现原子操作 Java通过两种方式实现原子操作: 使用循环CAS实现原子操作 自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。 例如: // 使用CAS实现线程安全计数器 private void safeCount() { for (;;) { int i = atomicI.get(); boolean suc = atomicI.compareAndSet(i, ++i); if (suc) break; } } CAS实现原子操作的三大问题: (1)ABA问题 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。 解决思路:使用版本号! (2)循环时间开销大 (3)只能保证一个共享变量的原子操作 使用锁机制实现原子操作 锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。 除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想要进入同步块的时候,其使用循环CAS的方式来获取锁,当它退出同步块时,其使用循环CAS的方式来释放锁。 四、小结 在本文,我们一起研究了volatile、synchronized和原子操作的实现原理。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79600050 安装环境 操作系统:Ubuntu Xenial 16.04 (LTS) Go语言版本:1.9.4 配置Ubuntu更新源(清华大学) $ sudo vim /etc/apt/sources.list # deb cdrom:[Ubuntu 16.04 LTS _Xenial Xerus_ - Release amd64 (20160420.1)]/ xenial main restricted deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial main restricted deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates main restricted deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial universe deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates universe deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial multiverse deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates multiverse deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security main restricted deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security universe deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security multiverse 卸载旧版本的Docker $ sudo apt-get remove docker docker-engine docker.io 安装新版本的Docker 步骤一:安装必要的系统工具 $ sudo apt-get update $ sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common 步骤二:安装GPG证书 $ curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add - 步骤三:写入软件源信息 $ sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" 步骤四:更新并安装Docker-CE $ sudo apt-get -y update $ sudo apt-get -y install docker-ce 附:安装指定版本的Docker-CE # Step 1: 查找Docker-CE的版本: $ apt-cache madison docker-ce docker-ce | 17.03.1~ce-0~ubuntu-xenial | http://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial/stable amd64 Packages docker-ce | 17.03.0~ce-0~ubuntu-xenial | http://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial/stable amd64 Packages # Step 2: 安装指定版本的Docker-CE: (VERSION 如上 17.03.1~ce-0~ubuntu-xenial) $ sudo apt-get -y install docker-ce=[VERSION] 配置阿里云镜像加速器 需登录阿里云管理控制台,获取专属的加速器地址。 针对Docker客户端版本大于1.10.0的用户,可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器: $ sudo mkdir -p /etc/docker $ sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://irnlfwui.mirror.aliyuncs.com"] } EOF $ sudo systemctl daemon-reload $ sudo systemctl restart docker 测试Docker版本(验证安装是否成功) 命令:docker –version && docker version && docker info $ sudo docker --version Docker version 17.12.1-ce, build 7390fc6 $ sudo docker version Client: Version: 17.12.1-ce API version: 1.35 Go version: go1.9.4 Git commit: 7390fc6 Built: Tue Feb 27 22:17:40 2018 OS/Arch: linux/amd64 Server: Engine: Version: 17.12.1-ce API version: 1.35 (minimum version 1.12) Go version: go1.9.4 Git commit: 7390fc6 Built: Tue Feb 27 22:16:13 2018 OS/Arch: linux/amd64 Experimental: false $ sudo docker info Containers: 3 Running: 0 Paused: 0 Stopped: 3 Images: 6 ... 测试Docker安装 可以通过运行简单的Docker Image来进行安装测试: $ sudo docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world ca4f61b1923c: Pull complete Digest: sha256:083de497cff944f969d8499ab94f07134c50bcf5e6b9559b27182d3fa80ce3f7 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. ... 列出所有的镜像image和容器container: $ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest f2a91732366c 3 months ago 1.85kB $ sudo docker container ls -all CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c621f40a070c hello-world "/hello" 15 minutes ago Exited (0) 15 minutes ago friendly_edison Recap and Cheat Sheet ## List Docker CLI commands docker docker container --help ## Display Docker version and info docker --version docker version docker info ## Excecute Docker image docker run hello-world ## List Docker images docker image ls ## List Docker containers (running, all, all in quiet mode) docker container ls docker container ls --all docker container ls -a -q ## Stop all Docker containers docker stop $(docker ps -a -q) ## Remove all Docker containers docker rm $(docker ps -a -q) ## Remove specific Docker image docker rmi <image_id> ## Remove all untagged Docker images (id is <none>) docker rmi $(docker images | grep "^<none>" | awk "{print $3}") ## Remove all Docker images docker rmi $(docker ps -a -q) 参考资料 其他关于旧版本Docker卸载以及测试开发版本Docker安装的帮助,可以参考官方文档的说明进行安装。 Ubuntu中安装Docker帮助链接
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/79599432 Docker本质上是运行在宿主机上的进程,它通过namespace实现了资源隔离,并通过cgroups实现了资源限制,同时通过写时复制(copy-on-write)实现了高效的文件操作。 一、通过namespace实现资源隔离 Linux内核中提供了6种namespace隔离的系统调用,分别完成对文件系统、网络、进程间通信、主机名、进程号以及用户权限的隔离。 具体如下所示: namespace 系统调用参数 隔离内容 UTS CLONE_NEWUTS 主机名与域名 IPC CLONE_NEWIPC 信号量/消息队列/共享内存 PID CLONE_NEWPID 进程编号 Network CLONE_NEWNET 网络设备/网络栈/端口等 Mount CLONE_NEWNS 挂载点(文件系统) User CLONE_NEWUSER 用户和用户组 Linux内核实现namespace的主要目的之一就是实现轻量级虚拟化容器服务。 在同一个namespace下的进程可以感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛置身于一个独立的系统环境中,从而达到独立和隔离的目的。 1. 进行namespace API操作的4种方式 namespace的API包括clone()、setns()、unshare()以及/proc下的部分文件。 (1)通过clone()在创建新进程的同时创建namespace 对于clone()系统调用,其调用方式如下: int clone(int (*child_func)(void *), void *child_stack, int flags, void *args); clone()实际上是Linux系统调用fork()的一种更通用的实现方式,它可以通过flags来控制使用多少功能。 clone()标志位参数中与namespace相关的四个参数分别是: child_func:用于传入子进程运行的程序主函数; child_stack:用于传入子进程使用的栈空间; flags:表示使用哪些CLONE_*标志位,主要包括如上表所示的系统调用参数; args:表示可用于传入的用户参数。 (2)通过setns()加入一个已经存在的namespace 对于setns()系统调用,其调用方式如下: int setns(int fd, int nstype); 通过setns()系统调用,进程可以从原来的namespace加入某个已存在的namespace中。 setns()系统调用中的参数如下: fd:表示要加入namespace的文件描述符。namespace实际上也是一个文件,有对应的文件描述符。它是一个指向/proc/[pid]/ns目录的文件描述符。 nstype:表示是否检查fd指向的namespace类型是否符合实际要求。该参数为0表示不检查。 通常,为了不影响进程的调用者,也为了使新加入的pid namespace生效,会在setns()函数执行后使用clone()创建子进程继续执行命令,让原先的进程结束运行。 例如,新创建namespace,并在该namespace中调用/bin/bash并接受参数,以运行shell。用法如下所示: fd = open(argv[1], O_RDONLY); // 获取namespace文件描述符 setns(fd, 0); // 加入新的namespace execvp(argv[2], &argv[2]); // 执行程序 假设编译后的程序名称为setnsdemo,于是可以执行: $ ./setnsdemo /proc/27514/ns/uts /bin/bash # uts是对应进程号为27514的进程对应uts的namespace号 至此,就可以在新加入的namespace中执行shell命令了。 (3)通过unshare()在原先进程上进行namespace隔离 对于unshare()系统调用,其调用方式: int unshare(int flags); 与clone()不同的是,unshare()运行在原来的进程上,不需要启动一个新进程。调用unshare()的主要作用是不启动一个新进程就可以起到隔离的效果,相当于跳出原先的namespace进行操作。 (4)查看/proc/[pid]/ns文件 从3.8版本的内核开始,用户就可以在/proc/[pid]/ns文件夹下看到指向不同namespace号的文件,如下图所示: 注意:如果两个进程指向的namespace编号相同,就说明它们在同一个namespace之下,否则便在不同namespace中。 那么为什么要在/proc/[pid]/ns中设置这些link链接呢? 其一:用于记录[pid]所对应的进程的namespace信息,方便查阅; 其二:一旦上述link文件被打开,只要打开的文件描述符存在,就算该namespace下的所有进程都已经结束,这个namespace也会一直存在,后续进程也可以再加入进来。 在Docker中,通过文件描述符定位和加入一个存在的namespace是最基本的方式。 2. UTS namespace UTS(Unix Time-sharing System)namespace提供了主机名和域名的隔离,这样,每个Docker容器就可以拥有独立的主机名和域名了,在网络上可以被当作一个独立的节点,而非宿主机上的一个进程,其标志位为CLONE_NEWUTS。 在Docker中,每个镜像基本都以自身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生任何影响,其原理就是使用了UTS namespace。 使用对比: 没有使用UTS的情况 运行结果如下: 使用UTS的情况 运行结果如下: 可见,当使用UTS隔离之后,整个子进程的主机名和域名发生了改变。 3. IPC namespace 进程间通信(Inter-Process Communication, IPC)涉及的IPC资源包括常见的信号量、消息队列和共享内存,其标志位为CLONE_NEWIPC。 对于IPC资源申请,申请IPC资源就申请了一个全局唯一的32位ID,因此,IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。 在同一个IPC namespace下的进程彼此可见,不同IPC namespace下的进程则互相不可见。 对于IPC namespace隔离的测试,可以参考:http://crosbymichael.com/creating-containers-part-1.html 4. PID namespace PID namespace隔离对进程PID重新标号,即两个不同namespace下的进程可以有相同的PID。每个PID namespace都有自己的计数程序,标志位为CLONE_NEWPID。 内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace。它创建的新的PID namespace被称为child namespace(树的子节点),而原先的PID namespace就是创建的新的PID namespace的parent namespace(树的父节点)。通过这种方式,不同的PID namespace就会形成一个层级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响,但是子节点却不能看到父节点PID namespace中的任何内容。 关于PID namespace的相关结论: 每个PID namespace中的第一个进程“PID 1”,都如同init进程一样具有特殊作用。 namespace中的进程不能通过kill或ptrace来影响父节点或兄弟节点中的进程。 在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。 一种在外部监控Docker中运行程序的方法:监控Docker daemon所在的PID namespace下的所有进程及其子进程,再进行筛选即可。 对于PID namespace中的init进程,其主要用于维护所有后续启动进程的运行状态。当系统中存在树状嵌套结构的PID namespace时,若某个子进程成为孤儿进程,收养该子进程的责任就交给了该子进程所属的PID namespace中的init进程。通观而言,PID namespace维护这样一个树状结构有利于系统的资源监控与回收。因此,如果确实需要在一个Docker容器中运行多个进程,最先启动的命令进程应该是具有资源监控与回收等管理能力的,如/bin/bash。另外,对于PID namespace中的init进程,其同时具有信号屏蔽的特权。也就是说,与init在同一个PID namespace下的进程(即使有超级权限)发送给它的所有信号都会被屏蔽,以防止init进程被误杀。但是,当父节点PID namespace中的进程发送相同的信号给子节点PID namespace中的init进程时,如果该信号是SIGKILL(销毁进程)或SIGSTOP(暂停进程),子节点的init进程会强制执行,其余的信号则会被忽略。同时,一旦init进程被销毁,同一PID namespace中的其他进程也随之接收到SIGKILL信号而被销毁。 对于PID namespace的ps命令,如果只想看到PID namespace本身应该看到的进程,需要重新挂载/proc,命令如下: zjl@ubuntu:~$ mount -t proc proc /proc zjl@ubuntu:~$ ps a 5. mount namespace mount namespace通过隔离文件系统挂载点对文件系统的隔离提供支持,它是第一个Linux namespace,因此标志位比较特殊,为CLONE_NEWNS。 可以通过/proc/[pid]/mount查看到所有挂载在当前namespace中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂在位置等。 具体如下图所示: $ vim /proc/2430/mounts $ vim /proc/2430/mountstats 进程在创建mount namespace时,会把当前的文件结构复制给新的mount namespace。新的mount namespace中的所有mount操作都只影响自身的文件系统,对外界不会产生任何影响。 mount namespace机制:挂载传播(mount propagation)。 挂载传播定义了挂载对象(mount object)之间的关系,这样的关系包括共享关系和从属关系,系统用这些关系决定任何挂载对象中的挂载事件如何传播到其他挂载对象。 共享关系(share relationship):如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然。 从属关系(slave relationship):如果两个挂载对象形成从属关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反之不行。 一个挂载状态可能为以下一种: 共享挂载(shared):传播事件的挂载对象。 从属挂载(slave):接收传播事件的挂载对象。 私有挂载(private):既不传播也不接收传播事件的挂载对象。 不可绑定挂载(unbindable):不允许执行绑定挂载。 示意图如下: 如图,可以得知: mount namespace下的/bin目录与child namespace通过master/slave方式进行挂载传播,当mount namespace中的/bin目录发生变化时,其挂载事件会自动传播到child namespace中; /lib目录使用完全的共享挂载传播,各namespace之间发生的变化都会互相影响; /proc目录使用私有挂载传播的方式,各个mount namespace之间相互隔离; /root目录一般都是管理员所有,不能让其他mount namespace挂载绑定。 在默认情况下,所有挂载状态都是私有挂载。 设置为共享挂载的命令如下: $ mount --make-shared <mount-object> 从共享挂载状态的挂载对象克隆的挂载对象,其状态也是共享的,它们互相传播挂载事件。 设置为从属挂载的命令如下: $ mount --make-slave <shared-mount-object> 来源于从属挂载对象克隆的挂载对象也是从属挂载,它也从属于原来的从属挂载的主挂载对象。 将一个从属挂载对象设置为共享/从属挂载的命令如下: $ mount --make-shared <slave-mount-object> 对于CLONE_NEWNS,当CLONE_NEWNS生效之后,子进程进行的挂载与卸载操作都将只作用于该mount namespace。 6. network namespace network namespace主要提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录以及套接字(socket)等。 对于网络设备,其最多存在于一个network namespace中,可以通过创建veth pair在不同的network namespace间创建通道,以达到通信的目的。对于veth pair,其表示虚拟网络映射对,它有两端,类似管道,如果数据从一端传入,另一端也能接收到。一般情况下,物理网络设备都分配在最初的root namespace中,但是,如果有多块物理网卡,也可以把其中一块或多块分配给新创建的network namespace。 对于network namespace,我们可以认为其是将网络独立出来,模拟一个独立网络实体与外部用户实体进行通信。对于该过程,容器的经典做法就是:创建一个veth pair,一端放置于新的network namespace中,通常命名为eth0,另一端放置在原来的network namespace中连接物理网络设备,然后,再通过把多个设备接入网桥或进行路由转发,以实现网络通信的目的。 另外,在建立起veth pair之前,新的network namespace和旧的network namespace之间通过管道(pipe)来进行通信。具体示意图如下图所示: 与其他namespace类似,对network namespace的使用其实就是在创建的时候添加CLONE_NEWNET标志位。 7. user namespace user namespace主要隔离了安全相关的标识符(identifier)和属性(attributes),包括用户ID、用户组ID、root目录、密钥以及特殊权限。也就是说,一个普通用户的进程通过clone()创建的新进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户。相应的,其在clone()中的标志位为CLONE_NEWUSER。 8. 小结 本节从namespace使用的API开始,结合Docker逐步对6个namespace进行了讲解。 二、cgroups资源限制 对于cgroups,它可以用于限制被namespace隔离起来的资源,还可以为资源设置权重、计算使用量、操控任务启动和停止等。 1. cgroups概念 cgroups是Linux内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。也就是说,cgroups可以限制、记录任务组所使用的物理资源(包括CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。 cgroups具有如下四个特点: cgroups的API以一个伪文件系统的方式实现,用户态的程序可以通过文件操作实现cgroups的组织管理; cgroups的组织管理操作单元可以细粒度到线程级别,另外用户可以创建和销毁cgroup,从而实现资源再分配和管理; 所有资源管理的功能都以子系统的方式实现,接口统一; 子任务创建时与其父任务处于同一个cgroups的控制组。 从本质上,cgroups是内核附加在程序上的一系列钩子(hook),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。 2. cgroups作用 从单个任务的资源控制到操作系统层面的虚拟化,cgroups提供了如下四大功能: 资源限制:cgroups可以对任务使用的资源总额进行限制。 优先级分配:通过分配的CPU时间片数量以及磁盘IO带宽大小,实际上就相当于控制了任务运行的优先级。 资源统计:cgroups可以统计系统的资源使用量,如CPU使用时长等。 任务控制:cgroups可以对任务执行挂起、恢复等操作。 3. cgroups结构 在cgroups中,主要有如下几个术语: task(任务):任务表示系统的一个进程或线程; cgroup(控制组):cgroup是cgroups进行资源控制的单位,它表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个group,也可以从某个cgroup迁移到另外一个cgroup中; subsystem(子系统):cgroups中的子系统就是一个资源调度控制器,如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量; hierachy(层级):层级由一系列cgroup以一个树状结构排列而成,每个层级通过绑定对应的子系统进行资源控制。层级中的cgroup节点可以包含零或多个子节点,子节点继承父节点挂载的子系统。 出于易于管理的目的,在Docker中,每个子系统独自构成一个层级。 对于cgroups的组织结构,主要有以下几个基本规则: 规则1:同一个层级可以附加一个或多个子系统。 规则2:一个子系统可以附加到多个层级,当且仅当目标层级只有唯一一个子系统时。 规则3:系统每次新建一个层级时,该系统上的所有任务默认加入这个新建层级的初始化cgroup,这个cgroup又称root cgroup。对于创建的每个层级,任务只能存在于其中一个cgroup中,即一个任务不能存在于同一个层级的不同cgroup中,但一个任务可以存在于不同层级中的多个cgroup中。如果操作时把一个任务添加到同一个层级的另一个cgroup中,则会将它从第一个cgroup中移除。 规则4:任务在fork/clone自身时创建的子任务默认与原任务在同一个cgroup中,但是子任务允许被移动到不同的cgroup中。 4. cgroups子系统 在cgroups中,子系统实际上就是cgroups的资源控制系统,每种子系统独立地控制一种资源,目前Docker使用如下9种子系统,具体如下: blkio:为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。 cpu:使用调度程序控制任务对CPU的使用。 cpuacct:自动生成cgroup中任务对CPU资源使用情况的报告。 cpuset:可以为cgroup中的任务分配独立的CPU和内存。 devices:可以开启或关闭cgroup中任务对设备的访问。 freezer:可以挂起或恢复cgroup中的任务。 memory:可以设定cgroup中任务对内存使用量的限定,并且自动生成这些任务对内存资源使用情况的报告。 perfevent:使用后使得cgroup中的任务可以进行统一的性能测试。 net_cls:Docker没有直接使用,它通过使用等级识别符(classid)标记网络数据包,从而允许Linux流量控制程序(TC:Traffic Controller)识别从具体cgroup中生成的数据包。 5. cgroups实现方式及工作原理 cgroups的实现本质上是给任务挂上钩子,当任务运行的过程中涉及某种资源时,就会触发钩子上所附带的子系统进行检测,然后根据资源类别的不同使用对应的技术进行资源限制和优先级分配。 (1)cgroups如何判断资源超限及超出限额之后的措施 对于不同的系统资源,cgroups提供了统一的接口对资源进行控制和统计,但限制的具体方式不尽相同。 (2)cgroup的子系统与任务之间的关联关系 实现上,cgroup与任务之间是多对多的关系,因此它们并不直接关联,而是通过一个中间结构把双向的关联信息记录起来。每个任务结构体task_struct都包含了一个指针,可以查询到对应cgroup的情况,同时也可以查询到各个子系统的状态,这些子系统状态中也包含了找到任务的指针,不同类型的子系统按需定义本身的控制信息结构体,最终在自定义的结构体中把子系统状态指针包含进去,然后内核通过container_of等宏定义来获取对应的结构体,关联到任务,以此达到资源限制的目的。 在实际的使用过程中,需要通过挂载cgroup文件系统来新建一个层级结构,挂载时需要指定要绑定的子系统,缺省情况下默认绑定系统所有子系统。在将cgroup文件系统挂载以后,就可以像操作文件一样对cgroups的hierarchy层级进行浏览和操作管理(包括权限管理、子文件管理等等)。除了cgroup文件系统以外,内核没有为cgroups的访问和操作添加任何系统调用。当一个顶层的cgroup文件系统被卸载时,如果其中创建后代cgroup目录,那么就算上层的cgroup被卸载了,层级也是激活状态,其后代cgoup中的配置依旧有效。只有递归式的卸载层级中的所有cgoup,那个层级才会被真正删除。层级激活后,/proc目录下的每个task PID文件夹下都会新添加一个名为cgroup的文件,列出task所在的层级,对其进行控制的子系统及对应cgroup文件系统的路径。同时,一个cgroup创建完成,不管绑定了何种子系统,其目录下都会生成以下几个文件,用来描述cgroup的相应信息。同样,把相应信息写入这些配置文件就可以生效,内容如下: tasks:这个文件中罗列了所有在该cgroup中task的PID。该文件并不保证task的PID有序,把一个task的PID写到这个文件中就意味着把这个task加入这个cgroup中。 cgroup.procs:这个文件罗列所有在该cgroup中的线程组ID。该文件并不保证线程组ID有序和无重复。写一个线程组ID到这个文件就意味着把这个组中所有的线程加到这个cgroup中。 notify_on_release:填0或1,表示是否在cgroup中最后一个task退出时通知运行release agent,默认情况下是0,表示不运行。 release_agent:指定release agent执行脚本的文件路径(该文件在最顶层cgroup目录中存在),在这个脚本通常用于自动化umount无用的cgroup。 可参考:http://www.infoq.com/cn/articles/docker-kernel-knowledge-cgroups-resource-isolation 6. 小结 本节浅入深出地讲解了cgroups,从cgroups是什么,到cgroups该怎么用,最后对大量地cgroup子系统配置参数进行了梳理。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/73609047 本笔记主要记录关于Maven知识体系的一些Pages和Tips! 一、Maven入门手册 阅读内容:Maven Getting Started Guide 和 The Philosophy of Maven 以及 The History of Maven 该文主要涉及的主要知识点包括: 什么是Maven 如何配置Maven项目 如何对Maven应用进行编译/测试/安装 快照(SNAPSHOT)版本简述 如何添加资源文件以及进行过滤 如何使用外部依赖 如何将自己的Jar部署到远端中央仓库 如何构建其他类型的项目 如何一次构建多个项目 二、Maven配置 阅读内容:Configuring Maven 该文提到Maven的配置分为三个层级: Project:项目级,通过pom.xml静态配置。 Installation:安装级,Maven安装时配置。 User:用户级,对于不同的用户有不同的配置。 项目级配置内容定义了对一个项目所需的基本配置信息,该配置与环境和使用者无关,而其他的则是对于当前运行环境的配置。 对于项目级配置内容,一般通过pom.xml进行静态配置。对于用户级配置内容,一般通过${user.home}/.m2/settings.xml进行配置。 三、Maven项目骨架 阅读内容:Introduction to Archetypes 该文对archetype的概念进行了解释,archetype是Maven项目模板工具包。 An archetype is defined as an original pattern or model from which all other things of the same kind are made. 使用方法: mvn archetype:generate Maven中archetype常见类型有: maven-archetype-archetype maven-archetype-j2ee-simple maven-archetype-mojo maven-archetype-plugin maven-archetype-plugin-site maven-archetype-portlet maven-archetype-quickstart maven-archetype-simple maven-archetype-site maven-archetype-site-simple maven-archetype-webapp 四、Maven项目的标准目录布局 阅读内容:Introduction to the Standard Directory Layout 该文介绍了Maven项目的标准目录布局。在进行Maven项目开发时,应该遵循该目录布局进行文件的创建和使用,具体如下: ${basedir} |-- pom.xml |-- src | |-- main | | `-- java | | `-- resources | | `-- filters | `-- test | | `-- java | | `-- resources | | `-- filters | `-- it | `-- assembly | `-- site `-- LICENSE.txt `-- NOTICE.txt `-- README.txt 其中,有: src/main/java 项目的源代码所在的目录 src/main/resources 项目的资源文件所在的目录 src/main/filters 项目的资源过滤文件所在的目录 src/main/webapp 如果是web项目,则该目录是web应用源代码所在目录,如html文件和web.xml等 src/test/java 测试代码所在的目录 src/test/resources 测试相关的资源文件所在的目录 src/test/filters 测试相关的资源过滤文件所在的目录 src/it 集成测试代码所在的目录,主要是供别的插件使用的 src/assembly 组件(Assembly)描述符所在的目录 src/site 站点文件 LICENSE.txt 项目的许可文件 NOTICE.txt 该项目依赖的库的注意事项 README.txt 项目的readme文件 五、POM文件介绍 阅读内容:Introduction to the POM A Project Object Model or POM is the fundamental unit of work in Maven. It is an XML file that contains information about the project and configuration details used by Maven to build the project. 文中对POM文件的基本元素进行了简单的介绍,包括: project modelVersion groupId artifactId packaging version name url description 该文介绍了两个概念:Super POM 和 Minimal POM Super POM Super POM 是Maven的默认POM。所有的POM都会继承该Super POM,也就是说,当你创建项目时,你所使用的POM都是继承了该Super POM的。 其位于$M2_HOME/lib/maven-model-builder-xxx.jar的org/apache/maven/model/pom-4.0.0.xml中。 Minimal POM 一个Minimal POM必须包含下面的指定内容:project/modelVersion/groupId/artifactId/version。另外,可以看到,Minimal POM并未强制要求设置repositories,因为其继承了Super POM,在Super POM中定义了默认的repositories信息。 另外,在该文中,个人觉得很重要的一部分就是关于项目间关系的举例介绍。 项目继承(project inheritance) 项目继承在各自的子模块的POM中指定parent信息。对于如何使用项目继承,文中给出了两个例子Example1和Example2(重点理解)。 项目聚合(project aggregation) 与项目继承不同的是,其放弃在子模块的POM中指定parent信息,而是在parent POM中指定模块信息。如何实现项目聚合?首先,更改parent POM的packaging值为pom;然后,在parent POM中注册modules信息。对于具体案例,文中给出了Example3和Example4两个例子(重点理解)。 同时使用项目继承和项目聚合 Opendaylight中使用该方式进行项目管理。 对于项目关系,总结三条原则: 对每一个child POM指定其parent POM 改变parent POM的packaging属性为pom 在parent注册每个子模块信息 文中给出了Example5的案例进行参考。 同时,该文介绍了如何在POM文件中使用变量。 六、Maven仓库 阅读内容:Introduction to Repositories 该文主要对于Maven的仓库进行了简要的介绍。对于仓库的介绍,也可以参考《Maven实战》的第6章关于仓库的讲解。 七、Maven依赖机制详解 阅读内容:Introduction to Dependency Mechanism Dependency management is one of the features of Maven that is best known to users and is one of the areas where Maven excels. 该文主要介绍了Maven的依赖机制,解释了传递性依赖/依赖范围/依赖管理/系统依赖等内容。着重掌握文中的几个例子。 另外,《Maven实战》的第5章对于坐标和依赖的讲解也很仔细,可以参照阅读。 八、小结 Maven的核心内容就是上面的一些知识点,推荐阅读官网文档和《Maven实战》等书籍,结合实践加深理解。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/73609021 一、系统环境 Ubuntu 14.04 LTS CPU:双核 内存:4GB 二、步骤详解 1. 安装JAVA开发环境 OpenDaylight requires Java 7 JDK for Lithium . For Beryllium, a Java 8 JDK may be required. 过程: Ubuntu 14.04的软件源中暂不支持java 8,因此,首先解决该问题: 对于Oracle JDK: $ sudo add-apt-repository ppa:webupd8team/java $ sudo apt-get update $ sudo apt-get install oracle-java8-installer 对于Open JDK: $ sudo add-apt-repository ppa:openjdk-r/ppa $ sudo apt-get update $ sudo apt-get install openjdk-8-jdk 检测是否安装成功: zjl@zjl-uestc:~$ java -version java version "1.8.0_77" Java(TM) SE Runtime Environment (build 1.8.0_77-b03) Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode) zjl@zjl-kb310:~$ java -version openjdk version "1.8.0_91" OpenJDK Runtime Environment (build 1.8.0_91-8u91-b14-0ubuntu4~14.04-b14) OpenJDK 64-Bit Server VM (build 25.91-b14, mixed mode) 2. 安装maven 3 Ubuntu默认支持的maven版本太低,因此,这里选择安装maven 3.3.9版本。 清除之前安装的maven: $ sudo apt-get purge -y maven 转入下载目录: $ cd ~/Downloads 下载maven-3.3.9(使用清华大学的源): $ sudo wget http://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz 解压安装maven-3.3.9: $ tar -zxvf apache-maven-3.3.9-bin.tar.gz $ sudo cp -r apache-maven-3.3.9 /usr/local $ sudo ln -s /usr/local/apache-maven-3.3.9/bin/mvn /usr/bin/mvn 配置环境变量: $ echo "export M2_HOME=/usr/local/apache-maven-3.3.9" >> ~/.profile $ source ~/.profile 测试是否安装成功: zjl@zjl-kb310:/usr/local$ mvn -v Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47+08:00) Maven home: /usr/local/apache-maven-3.3.9 Java version: 1.8.0_91, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: en_US, platform encoding: UTF-8 OS name: "linux", version: "4.2.0-27-generic", arch: "amd64", family: "unix" 可选:提高Maven可用RAM总量的方法: 一些OpenDaylight项目可能十分大,其耗费资源也会很大,因此,可以增加Maven的可用RAM。 具体方法如下: $ echo " export MAVEN_OPTS='-Xmx1048m -XX:MaxPermSize=512m' " >> ~/.bashrc $ source ~/.bashrc 3. 安装Git 略。。。 4. 修改~/.m2/settings.xml OpenDaylight maintains its own repositories outside of Maven Central, which means maven cannot resolve OpenDaylight artifacts by default. Since OpenDaylight is organized as multiple inter-dependent projects, building a particular project usually means pulling in some artifacts. In order to make this work, your maven installation needs to know the location of OpenDaylight repositories and has to taught to use them. 具体方法: # Shortcut command for grabbing settings.xml $ cp -n ~/.m2/settings.xml{,.orig} ; \ wget -q -O - https://raw.githubusercontent.com/opendaylight/odlparent/master/settings.xml > ~/.m2/settings.xml ~/.m2/settings.xml的内容如下: <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <profiles> <profile> <id>opendaylight-release</id> <repositories> <repository> <id>opendaylight-mirror</id> <name>opendaylight-mirror</name> <url>https://nexus.opendaylight.org/content/repositories/public/</url> <releases> <enabled>true</enabled> <updatePolicy>never</updatePolicy> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>opendaylight-mirror</id> <name>opendaylight-mirror</name> <url>https://nexus.opendaylight.org/content/repositories/public/</url> <releases> <enabled>true</enabled> <updatePolicy>never</updatePolicy> </releases> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> <profile> <id>opendaylight-snapshots</id> <repositories> <repository> <id>opendaylight-snapshot</id> <name>opendaylight-snapshot</name> <url>https://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>opendaylight-snapshot</id> <name>opendaylight-snapshot</name> <url>https://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <activeProfile>opendaylight-release</activeProfile> <activeProfile>opendaylight-snapshots</activeProfile> </activeProfiles> </settings> 如果你使用了代理,那么需要配置代理,具体阅读:Maven proxy configuration。 错误处理: 如果遇到了如下错误: [WARNING] Error initializing: org.codehaus.plexus.velocity.DefaultVelocityComponent java.lang.NoClassDefFoundError: org/apache/commons/lang/StringUtils 添加下面内容到文件~/.m2/repository/org/apache/maven/plugins/maven-archetype-plugin/{version}/maven-archetype-plugin-{version}.pom: <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency>
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/51125246 Install OpenJDK 8 in Ubuntu: For 14.10 and later just run apt-get install openjdk-8-jdk Oracle JAVA 8 Stable release has been released on Mar,18 2014 and available to download and install on official download page. Oracle Java PPA for Ubuntu and LinuxMint is being maintained by Webupd8 Team. JAVA 8 is released with many of new features and security updates, read more about whats new in Oracle Java 8. Installing Java 8 on Ubuntu First you need to add webupd8team Java PPA repository in your system and install Oracle Java 8 using following set of commands. $ sudo add-apt-repository ppa:webupd8team/java $ sudo apt-get update $ sudo apt-get install oracle-java8-installer Verify Installed Java Version After successfully installing oracle Java using above step verify installed version using following command. zjl@zjl-uestc:~$ java -version java version "1.8.0_77" Java(TM) SE Runtime Environment (build 1.8.0_77-b03) Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode) Configuring Java Environment In Webupd8 ppa repository also providing a package to set environment variables, Install this package using following command. $ sudo apt-get install oracle-java8-set-default System Java version switch $ sudo update-java-alternatives -s java-8-oracle References: https://launchpad.net/~webupd8team/+archive/java
问题: 编译C++代码时出现错误提示如下: > g++ *.cpp fileTest.cpp:17:117: error: default argument given for parameter 1 of ‘MyClass::fileTest(const string&, std::string, std::string, std::string)’ [-fpermissive] fileTest.h:15:14: error: after previous specification in ‘MyClass::fileTest(const string&, std::string, std::string, std::string)’ [-fpermissive] 解决办法: 既可以在类的声明中,也可以在函数定义中声明缺省参数,但不能既在类声明中又在函数定义中同时声明缺省参数。 You can declare default arguments in the class declaration or in the function definition, but not both. 因此,将定义或声明中的任一个缺省参数删除即可。 参考网址 http://stackoverflow.com/questions/13295530/this-is-a-new-compile-error-for-me
GDB概述 GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长,尺有所短”就是这个道理。 一般来说,GDB主要帮忙你完成下面四个方面的功能: 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式) 当程序被停住时,可以检查此时你的程序中所发生的事。 动态的改变你程序的执行环境。 从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。 一个调试示例 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int func(int n) { int sum = 0; int i = 0; for(i = 0; i < n; i++) sum += i; return sum; } int main() { int i; long result = 0; for(i = 0; i <= 100; i++) result += i; printf("result[1-100] = %ld\n", result); printf("result[1-250] = %ld\n", func(250)); return 0; } 编译生成执行文件(Linux下): root@iZ2813hasr2Z:~/test/csdnBBS/gdb# gcc test1.c -g -o test1 使用GDB调试: root@iZ2813hasr2Z:~/test/csdnBBS/gdb# gdb test1 GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from test1...done. (gdb) l 15 16 for(i = 0; i < n; i++) 17 sum += i; 18 19 return sum; 20 } 21 22 int main() { 23 int i; 24 long result = 0; (gdb) 25 26 for(i = 0; i <= 100; i++) 27 result += i; 28 29 printf("result[1-100] = %ld\n", result); 30 31 printf("result[1-250] = %ld\n", func(250)); 32 33 return 0; 34 } (gdb) break 23 Breakpoint 1 at 0x40057a: file test1.c, line 23. (gdb) break func Breakpoint 2 at 0x400544: file test1.c, line 13. (gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040057a in main at test1.c:23 2 breakpoint keep y 0x0000000000400544 in func at test1.c:13 (gdb) r Starting program: /root/test/csdnBBS/gdb/test1 Breakpoint 1, main () at test1.c:24 24 long result = 0; (gdb) n 26 for(i = 0; i <= 100; i++) (gdb) 27 result += i; (gdb) 26 for(i = 0; i <= 100; i++) (gdb) 27 result += i; (gdb) c Continuing. result[1-100] = 5050 Breakpoint 2, func (n=250) at test1.c:13 13 int sum = 0; (gdb) n 14 int i = 0; (gdb) 16 for(i = 0; i < n; i++) (gdb) 17 sum += i; (gdb) 16 for(i = 0; i < n; i++) (gdb) 17 sum += i; (gdb) 16 for(i = 0; i < n; i++) (gdb) p sum $1 = 1 (gdb) $2 = 1 (gdb) p i $3 = 1 (gdb) n 17 sum += i; (gdb) 16 for(i = 0; i < n; i++) (gdb) 17 sum += i; (gdb) 16 for(i = 0; i < n; i++) (gdb) 17 sum += i; (gdb) p sum $4 = 6 (gdb) p i $5 = 4 (gdb) bt #0 func (n=250) at test1.c:17 #1 0x00000000004005be in main () at test1.c:31 (gdb) finish Run till exit from #0 func (n=250) at test1.c:17 0x00000000004005be in main () at test1.c:31 31 printf("result[1-250] = %ld\n", func(250)); Value returned is $6 = 31125 (gdb) c Continuing. result[1-250] = 31125 [Inferior 1 (process 8494) exited normally] (gdb) q 使用GDB 一般来说,GDB主要调试的是C/C++程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如: > cc -g hello.c -o hello > g++ -g hello.cpp -o hello 如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。当你用-g把调试信息加入之后,并成功编译目标代码以后,让我们来看看如何用gdb来调试它。 启动GDB的方法有以下几种: gdb <program> program也就是你的执行文件,一般在当前目录下。 gdb <program> core 用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件。 gdb <program> <PID> 如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试它。program应该在PATH环境变量中搜索得到。 GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb -help查看。我在下面只例举一些比较常用的参数: -symbols <file> -s <file> 从指定文件中读取符号表。 -se file 从指定文件中读取符号表信息,并把它用在可执行文件中。 -core <file> -c <file> 调试core dump的core文件。 -directory <directory> -d <directory> 加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。 GDB命令概述 启动gdb后,你就被带入gdb的调试环境中,就可以使用gdb的命令开始调试程序了,gdb的命令可以使用help命令来查看,如下所示: zjl@zjl-virtual-machine:~/projects/GdbStudy$ gdb GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04 Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>. (gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points data -- Examining data files -- Specifying and examining files internals -- Maintenance commands obscure -- Obscure features running -- Running the program stack -- Examining the stack status -- Status inquiries support -- Support facilities tracepoints -- Tracing of program execution without stopping the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help all" for the list of all commands. Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous. gdb的命令很多,gdb把之分成许多个种类。help命令只是例出gdb的命令种类,如果要看种类中的命令,可以使用help 命令,如:help breakpoints,查看设置断点的所有命令。也可以直接help 来查看命令的帮助。 gdb中,输入命令时,可以不用打全命令,只用打命令的前几个字符就可以了。当然,命令的前几个字符应该要标志着一个唯一地命令,在Linux下,你可以敲击两次TAB键来补齐命令的全称,如果有重复的,那么gdb会把其列出来。 示例一:在进入函数func时,设置一个断点:可以敲入break func,或是直接就是b func (gdb) b func Breakpoint 1 at 0x8048458: file hello.c, line 10. 示例二:敲入b按两次TAB键,你会看到所有b打头的命令: (gdb) b backtrace break bt (gdb) 示例三:只记得函数的前缀,可以这样: (gdb) b make_ <按TAB键> 再按下一次TAB键,你会看到: make_a_section_from_file make_environ make_abs_section make_function_type make_blockvector make_pointer_type make_cleanup make_reference_type make_command make_symbol_completion_list (gdb) b make_ GDB会把所有make开头的函数全部例出来给你查看 示例四:调试C++的程序时,有可能函数名一样(重载函数),如: (gdb) b 'bubble( M-? bubble(double,double) bubble(int,int) (gdb) b 'bubble( 你可以查看到C++中的所有的重载函数及参数(注:M-?和“按两次TAB键”是一个意思) 要退出gdb,只需使用quit或命令简称q就行了。 在GDB中运行UNIX的Shell程序 在gdb环境中,你可以执行UNIX的shell的命令,使用gdb的shell命令来完成: (gdb) shell <command string> 调用UNIX的shell来执行,环境变量SHELL中定义的UNIX的shell将会被用来执行,如果SHELL没有定义,那就使用UNIX的标准shell:/bin/sh。(在Windows中使用Command.com或cmd.exe) 还有一个gdb命令是make: (gdb) make <make-args> 可以在gdb中执行make命令来重新build自己的程序。这个命令等价于“shell make ”。 在GDB中运行程序 当以gdb 方式启动gdb后,gdb会在PATH路径和当前目录中搜索的源文件。如要确认gdb是否读到源文件,可使用l或list命令,看看gdb是否能列出源代码。 在gdb中,运行程序使用r或是run命令。 程序的运行,你有可能需要设置下面四方面的事。 程序运行参数 set args 可指定运行时参数(如:set args 10 20 30 40 50)。 show args 命令可以查看设置好的运行参数。 运行环境 path <dir> 可设定程序的运行路径(如:path ./demo/app)。 show paths 查看程序的运行路径。 set environment varname [=value] 设置环境变量。如:set env USER=hchen show environment [varname] 查看环境变量。 工作目录 cd <dir> 相当于shell的cd命令(如:cd ./demo 等价于 shell cd ./demo)。 pwd 显示当前的所在目录。 程序的输入输出 info terminal 显示你程序用到的终端的模式。 使用重定向控制程序输出。如:run > outfile tty命令可以指写输入输出的终端设备。如:tty /dev/ttyb 调试已运行的程序 两种方法: 在Linux下用ps查看正在运行的程序的PID(进程ID),然后用gdb PID格式挂接正在运行的程序; gdb a.out 12963 先用gdb 关联源代码,并进行gdb,在gdb中用attach命令来挂接进程PID,并用detach来取消挂接的进程。 例如:我们去调试运行如下代码的程序。 #include <iostream> #include <unistd.h> using namespace std; int main() { long long i = 0; while(1) { //sleep(1); i++; //cout << "i value = " << i << endl; } return 0; } 编译过程: g++ -g attach_to_running_process_test.cpp GDB调试过程: (gdb) attach 12698 Attaching to program: /root/share/gdbTest/a.out, process 12698 /root/share/gdbTest/a.out has changed; re-reading symbols. Reading symbols from /usr/lib/x86_64-linux-gnu/libstdc++.so.6...(no debugging symbols found)...done. Loaded symbols for /usr/lib/x86_64-linux-gnu/libstdc++.so.6 Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libc-2.19.so...done. done. Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6 Reading symbols from /lib/x86_64-linux-gnu/libm.so.6...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libm-2.19.so...done. done. Loaded symbols for /lib/x86_64-linux-gnu/libm.so.6 Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/ld-2.19.so...done. done. Loaded symbols for /lib64/ld-linux-x86-64.so.2 Reading symbols from /lib/x86_64-linux-gnu/libgcc_s.so.1...(no debugging symbols found)...done. Loaded symbols for /lib/x86_64-linux-gnu/libgcc_s.so.1 main () at attach_to_running_process_test.cpp:19 19 return 0; (gdb) p i $1 = 7065833447 (gdb) p i $2 = 7065833447 (gdb) $3 = 7065833447 (gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. main () at attach_to_running_process_test.cpp:19 19 return 0; (gdb) p i $4 = 7439387814 (gdb) detach Detaching from program: /root/share/gdbTest/a.out, process 12698 (gdb) q 停住/恢复程序运行 调试程序中,停住程序运行是必须的,GDB可以方便地停住程序的运行。你可以设置程序在哪行停住,在什么条件下停住,在收到什么信号时停住等等,以便于查看运行时的变量,以及运行时的流程。 当进程被gdb停住时,你可以使用info program来查看程序是否在运行、进程号、被停住的原因等等。 在gdb中,有以下几种停住方式: 断点(breakpoint) 观察点(watchpoint) 捕捉点(catchpoint) 信号(signal) 线程停止(thread stops) 如果恢复程序运行,可以使用c或continue命令。 设置断点(breakpoint) 我们使用break命令来设置断点。 有这样几种设置断点的方法: (1)break <function> 在进入指定函数时停住。 C++中可以使用class::function或function(type, type)格式来指定函数名(不同类的成员函数可能声明相同,函数可能重载,因此对于重载函数必须指定参数类型)。 (2)break <linenum> 在指定行号停住。 (3)break +offset / break -offset 在当前行号的前面或后面的offset行停住。offset为自然数。 (4)break filename:linenum 在源文件filename的linenum行处停住。 (5)break filename:function 在源文件filename的function函数的入口处停住。 (6)break *address 在程序运行的内存地址处停住。 (7)break break命令没有参数时,表示在下一条指令处停住。 (8)break ... if <condtion> …可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环体中,可以设置break if i=100,表示当i为100时停止程序。 ==>> 查看断点时,可以使用info命令,如下所示:(n表示断点号) info breakpoints [n] info break [n] 设置观察点(watchpoint) 观察点一般用来观察某个表达式(变量也是一种表达式)的值是否发生变化。如果有变化,就立即停止程序。 我们可以使用下面几种方法设置观察点: (1)watch <expr> 为表达式(变量)expr设置一个观察点。一旦表达式值有变化时,就立即停住程序。 (2)rwatch <expr> 当表达式(变量)被读时,停住程序。 (3)awatch <expr> 当表达式(变量)的值被读或被写时,停住程序。 (4)info watchpoints 列出了当前所设置的所有观察点。 示例test1.c的调试过程: zjl@zjl-virtual-machine:~/projects/GdbStudy$ gdb test1 GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04 Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/zjl/projects/GdbStudy/test1...done. (gdb) l 10 sum += i; 11 } 12 13 return sum; 14 } 15 16 int main() 17 { 18 int i; 19 long result = 0; (gdb) break 20 Breakpoint 1 at 0x400532: file test1.c, line 20. (gdb) r Starting program: /home/zjl/projects/GdbStudy/test1 warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000 Breakpoint 1, main () at test1.c:21 21 for (i=1; i<=100; i++) (gdb) watch i Hardware watchpoint 2: i (gdb) c Continuing. Hardware watchpoint 2: i Old value = 0 New value = 2 0x0000000000400548 in main () at test1.c:21 21 for (i=1; i<=100; i++) (gdb) c Continuing. Hardware watchpoint 2: i Old value = 2 New value = 3 0x0000000000400548 in main () at test1.c:21 21 for (i=1; i<=100; i++) 可见,使用watch的步骤如下: 使用break在观察的变量所在处设置断点; 使用run执行程序,直到断点处; 使用watch设置观察点; 使用continue观察设置的观察点是否发生变化。 疑问: 直接设置观察点为什么不行? ==>> 测试对于指针设置观察点,观察值为指针的值,还是指针所在空间。 例如: root@iZ2813hasr2Z:~/share/gdbTest# gdb a.out GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out...done. (gdb) l 33 28 29 int i; 30 long result = 0; 31 32 for (i=1; i<=100; i++) { 33 result += i; 34 } 35 36 printf ("result [1-100] = %ld \n", result); 37 printf ("result [1-250] = %d \n", func(250)); (gdb) b 33 Breakpoint 1 at 0x400764: file general.cpp, line 33. (gdb) watch result No symbol "result" in current context. (gdb) watch i No symbol "i" in current context. (gdb) r Starting program: /root/share/gdbTest/a.out Breakpoint 1, main () at general.cpp:33 33 result += i; (gdb) watch result Hardware watchpoint 2: result No symbol “result” in current context. 也就是说,如果程序没有run,系统就不会对其进行内存分配,因此,result在系统中也没有对应的内存地址,这样,直接进行watch操作会导致认为系统中没有result这个符号。 设置捕捉点(catchpoint) 可以设置捕捉点来捕捉程序运行时的一些事件,如:载入共享库(动态链接库)或是C++的异常。 设置捕捉点的格式为: (1)catch <event> 当event发生时,停住程序。 event可以是下面的内容: throw 一个 C++ 抛出的异常(throw为关键字) catch 一个 C++ 捕捉的异常(catch为关键字) exec 调用系统调用exec时(exec为关键字,目前此功能只在HP-UX下有用) fork 调用系统调用fork时(fork为关键字,目前此功能只在HP-UX下有用) vfork 调用系统调用vfork时(vfork为关键字,目前此功能只在HP-UX下有用) load 或 load 载入共享库(动态链接库)时(load为关键字,目前此功能只在HP-UX下有用) unload 或 unload 卸载共享库(动态链接库)时(unload为关键字,目前此功能只在HP-UX下有用) (2)tcatch <event> 只设置依次捕捉点,当程序停住以后,该捕捉点被自动删除。 维护停止点 前面说了如何设置程序的停止点,gdb中的停止点也是上述的三类。 在gdb中,如果你觉得已定义好的停止点没有用了,可以使用delete、clear、disable、enable命令来进行维护。 (1)clear 清除所有的已定义的停止点。 用法: clear <function> clear <filename:function> 清除所有设置在函数function上的停止点。 clear <linenum> clear <filename:linenum> 清除所有设置在指定行上的停止点。 (2)delete 删除指定的停止点。 用法: delete [breakpoints] [range...] 删除指定的断点,breakpoint为断点号。如果不指定断点号,则表示删除所有的断点。range表示断点号的范围。其简写命令为d。 (3)disable/enable 比删除更好的一种方法是disable停止点,disable了的停止点,gdb不会删除,当你还需要时,enable即可,就好像回收站一样(删除与还原的过程)。 用法: disable [breakpoints] [range...] disable所指定的停止点,breakpoint为停止点号。如果什么都不指定,表示disable所有的停止点。简写命令为dis。 enable [breakpoints] [range...] enable所指定的停止点,breakpoint为停止点号。 enable [breakpoints] once [range...] enable所指定的停止点一次,当程序停住后,该停止点立即被gdb自动disable。 enable [breakpoints] delete [range...] enable所指定的停止点一次,当程序停住后,该停止点立即被gdb删除。 停止条件维护 前面在说到设置断点时,我们提到过可以设置一个条件,当条件成立时,程序自动停止,这是一个非常强大的功能,这里,我想专门说说这个条件的相关维护命令。一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。并且,条件设置好后,我们可以用condition命令来修改断点的条件。(只有break和watch命令支持if,catch目前暂不支持if)。 condition <bnum> <expression> 修改断点号为bnum的停止条件为expression。 condition <bnum> 清除断点号为bnum的停止条件。 还有一个比较特殊的维护命令ignore,你可以指定程序运行时,忽略停止条件几次。 ignore <bum> <count> 表示忽略断点号为bnum的停止条件count次。 为停止点设定运行命令 我们可以使用GDB提供的command命令来设置停止点的运行命令。也就是说,当运行的程序在被停止住时,我们可以让其自动运行一些别的命令,这很有利行自动化调试。对基于GDB的自动化调试是一个强大的支持。 commands [bnum] ...command-list... end 为断点号bnum指写一个命令列表。当程序被该断点停住时,gdb会依次运行命令列表中的命令。 例如: break foo if x>0 commands printf "x is %d/n",x continue end 断点设置在函数foo中,断点条件是x>0,如果程序被停住后,也就是,一旦x的值在foo函数中大于0,GDB会自动打印出x的值,并继续运行程序。 如果你要清除断点上的命令序列,那么只要简单的执行一下commands命令,并直接再打个end就行了。 break foo if x>0 commands end 断点菜单 在C++中,可能会重复出现同一个名字的函数若干次(函数重载),在这种情况下,break 不能告诉gdb要停在哪个函数的入口。当然,你可以使用break (gdb) b String::after [0] cancel [1] all [2] file:String.cc; line number:867 [3] file:String.cc; line number:860 [4] file:String.cc; line number:875 [5] file:String.cc; line number:853 [6] file:String.cc; line number:846 [7] file:String.cc; line number:735 > 2 4 6 Breakpoint 1 at 0xb26c: file String.cc, line 867. Breakpoint 2 at 0xb344: file String.cc, line 875. Breakpoint 3 at 0xafcc: file String.cc, line 846. Multiple breakpoints were set. Use the "delete" command to delete unwanted breakpoints. (gdb) 可见,gdb列出了所有after的重载函数,你选一下列表编号即可。0表示放弃设置断点,1表示所有函数都设置断点。 恢复程序运行和单步调试 当程序被停住,可以使用continue命令恢复程序的运行直至程序结束,或者下一个断点到来。同时,也可以使用step或next命令来实现程序单步跟踪。 (1)continue/c/fg continue [ignore-count] c [ignore-count] fg [ignore-count] 恢复程序运行,直到程序结束,或是下一个断点到来。ignore-count表示忽略其后的断点次数。continue、c、fg三个命令都是一样的意思。 (2)step/s step <count> 单步跟踪,如果有函数调用,step会进入该调用函数。进入函数的前提是,此函被编译有debug信息。此命令类似VC等工具中的step in。后面可以加count也可以不加,不加表示逐条执行,加count表示执行后面的count条指令,然后再停住。 (3)next/n next <count> 同样单步跟踪,如果有函数调用,next不会进入该调用函数。很像VC等工具中的step over。后面可以加count也可以不加,不加表示逐条执行,加表示执行后面的count条指令,然后再停住。 (4)finish 运行程序,直到当前函数完成返回。并打印函数返回时的堆栈地址和返回值及参数值等信息。 (5)until/u 当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。 (6)stepi/si、nexti/ni 单步跟踪一条机器指令!一条程序代码有可能由数条机器指令完成,stepi和nexti可以单步执行机器指令。与之一样有相同功能的命令是“display/i $pc” ,当运行完这个命令后,单步跟踪会在打出程序代码的同时打出机器指令(也就是汇编代码)。 (7)set step-mode set step-mode on 打开step-mode模式,于是,在进行单步跟踪时,程序不会因为没有debug信息而不停住。这个参数有很利于查看机器码。 set step-mod off 关闭step-mode模式。 信号(signal) 信号是一种软中断,是一种处理异步事件的方法。一般来说,操作系统都支持许多信号。尤其是UNIX,比较重要应用程序一般都会处理信号。UNIX定义了许多信号,比如SIGINT表示中断字符信号,也就是Ctrl+C的信号;SIGBUS表示硬件故障的信号;SIGCHLD表示子进程状态改变信号;SIGKILL表示终止程序运行的信号,等等。信号量编程是UNIX下非常重要的一种技术。 gdb有能力在你调试程序的时候处理任何一种信号,你可以告诉gdb需要处理哪一种信号。你可以要求gdb收到你所指定的信号时,马上停住正在运行的程序,以供你进行调试。你可以用gdb的handle命令来完成这一功能。 `handle <signal> <keywords...>` 在gdb中定义一个信号处理。信号可以以SIG开头或不以SIG开头,可以用定义一个要处理信号的范围(如:SIGIO-SIGKILL,表示处理从SIGIO信号到SIGKILL的信号,其中包括SIGIO,SIGIOT,SIGKILL三个信号),也可以使用关键字all来标明要处理所有的信号。一旦被调试的程序接收到信号,运行程序马上会被gdb停住,以供调试。其<keywords>可以是以下几种关键字的一个或多个。 nostop 当被调试的程序收到信号时,GDB不会停住程序的运行,但会打出消息告诉你收到这种信号 stop 当被调试的程序收到信号时,GDB会停住你的程序 print 当被调试的程序收到信号时,GDB会显示出一条信息 noprint 当被调试的程序收到信号时,GDB不会告诉你收到信号的信息 pass noignore 当被调试的程序收到信号时,GDB不处理信号,这表示,GDB会把这个信号交给被调试程序会处理 nopass ignore 当被调试的程序收到信号时,GDB不会让被调试程序来处理这个信号 信息显示命令: info signals info handle 查看有哪些信号正在被gdb检测中。 线程 如果你程序是多线程的话,你可以定义你的断点是否在所有的线程上,或是在某个特定的线程。GDB很容易帮你完成这一工作。 break <linespec> thread <threadno> break <linespec> thread <threadno> if ... linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID,注意,这个ID是GDB分配的,你可以通过“info threads”命令来查看正在运行程序中的线程信息。如果你不指定thread 则表示你的断点设在所有线程上面。你还可以为某线程指定断点条件。如: (gdb) break frik.c:13 thread 28 if bartab > lim 当你的程序被GDB停住时,所有的运行线程都会被停住。这方便你查看运行程序的总体情况。而在你恢复程序运行时,所有的线程也会被恢复运行。那怕是主进程在被单步调试时。 查看栈信息 当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的。 当你的程序调用了一个函数,函数的地址、函数的参数、函数内的局部变量都会被压入“栈”(stack)中。你可以使用gdb命令来查看当前的栈中的信息。 下面是一些查看函数调用栈的gdb命令: (1)backtrace/bt 打印当前的函数调用栈的所有信息。如: (gdb) backtrace #0 Student::setStudentInfo (this=0x602010, score=98.5) at test2.cpp:30 #1 0x00000000004007e3 in main () at test2.cpp:48 从上可以看出函数的调用栈信息:main()—>>>Student::setStudentInfo(double) backtrace <n> bt <n> n是一个正整数,表示只打印栈顶上n层的栈信息。 backtrace <-n> bt <-n> -n表示一个负整数,表示只打印栈底下n层的栈信息。 (gdb) bt 1 #0 Student::setStudentInfo (this=0x602010, score=98.5) at test2.cpp:30 (More stack frames follow...) (gdb) bt -1 #1 0x00000000004007e3 in main () at test2.cpp:48 如果你要查看某一层的信息,你需要切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的就是切换当前栈。 (2)frame/f/up/down frame <n> f <n> n是一个从0开始的整数,是栈中的层编号。比如,frame 0表示栈顶;frame 1表示栈的第二层。 up <n> 表示向栈的上面移动n层,可以不加n,表示向上移动1层 down <n> 表示向栈的下面移动n层,可以不加n,表示向下移动1层 ==>> 上面的命令,都会打印出移动到的栈层的信息。 如果不打印信息,可以使用下面三个命令: select-frame <n>对应于 frame 命令; up-silently <n> 对应于 up 命令; down-silently <n> 对应于 down 命令。 查看当前栈层的信息,你可以使用下面的gdb命令: frame / f 该命令会打印出这些栈信息:栈的层编号,当前的函数名,函数的参数值,函数所在文件及行号,函数执行到的语句。 info frame info f 该命令会打印出更为详细的当前栈层的信息,只不过,大多数都是运行时的内存地址。比如:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等。如: (gdb) f #1 0x00000000004007e3 in main () at test2.cpp:48 48 st->setStudentInfo(s); (gdb) info f Stack level 1, frame at 0x7fffffffe5a0: rip = 0x4007e3 in main (test2.cpp:48); saved rip 0x7ffff773d76d caller of frame at 0x7fffffffe560 source language c++. Arglist at 0x7fffffffe590, args: Locals at 0x7fffffffe590, Previous frame's sp is 0x7fffffffe5a0 Saved registers: rbx at 0x7fffffffe588, rbp at 0x7fffffffe590, rip at 0x7fffffffe598 (gdb) down #0 Student::setStudentInfo (this=0x602010, score=98.5) at test2.cpp:30 30 this->score = score; (gdb) info f Stack level 0, frame at 0x7fffffffe560: rip = 0x400725 in Student::setStudentInfo (test2.cpp:30); saved rip 0x4007e3 called by frame at 0x7fffffffe5a0 source language c++. Arglist at 0x7fffffffe550, args: this=0x602010, score=98.5 Locals at 0x7fffffffe550, Previous frame's sp is 0x7fffffffe560 Saved registers: rbp at 0x7fffffffe550, rip at 0x7fffffffe558 打印信息命令: info args 打印出当前函数的参数名及其值。 info locals 打印出当前函数中所有局部变量及其值。 info catch 打印出当前函数中的异常处理信息。 查看源程序 显示源代码 GDB 可以打印出所调试程序的源代码,当然,在程序编译时一定要加上-g的参数,把源程序信息编译到执行文件中。不然就看不到源程序了。当程序停下来以后,GDB会报告程序停在了那个文件的第几行上。 你可以用list命令来打印程序的源代码。还是来看一看查看源代码的GDB命令吧。 list <linenum> 显示程序第linenum行的周围的源程序。 list <function> 显示函数名为function的函数的源程序。 list 显示当前行后面的源程序。 list - 显示当前行前面的源程序。 一般是打印当前行的上5行和下5行,如果显示函数是是上2行下8行,默认是10行,当然,你也可以定制显示的范围,使用下面命令可以设置一次显示源程序的行数。 set listsize <count> 设置一次显示源代码的行数。 show listsize 查看当前listsize的设置。 list命令还有下面的用法: list <first>, <last> 显示从first行到last行之间的源代码。 list , <last> 显示从当前行到last行之间的源代码。 list + 往后显示源代码。 一般来说在list后面可以跟以下这们的参数: <linenum> 行号。 <+offset> 当前行号的正偏移量。 <-offset> 当前行号的负偏移量。 <filename:linenum> 哪个文件的哪一行。 <function> 函数名。 <filename:function> 哪个文件中的哪个函数。 <*address> 程序运行时的语句在内存中的地址。 搜索源代码 不仅如此,GDB还提供了源代码搜索的命令: forward-search <regexp> search <regexp> 向前面搜索。 reverse-search <regexp> 全部搜索。 其中,就是正则表达式,也主一个字符串的匹配模式,关于正则表达式,我就不在这里讲了,还请各位查看相关资料。 指定源文件的路径 某些时候,用-g编译过后的执行程序中只是包括了源文件的名字,没有路径名。GDB提供了可以让你指定源文件的路径的命令,以便GDB进行搜索。 directory <dirname ... > dir <dirname ... > 加一个源文件路径到当前路径的前面。如果你要指定多个路径,UNIX下你可以使用“:”,Windows下你可以使用“;”。 directory 清除所有的自定义的源文件搜索路径信息。 show directories 显示定义了的源文件搜索路径。 源代码的内存 你可以使用info line命令来查看源代码在内存中的地址。 info line后面可以跟“行号”,“函数名”,“文件名:行号”,“文件名:函数名”,这个命令会打印出所指定的源码在运行时的内存地址,如: (gdb) info line test1.c:func Line 4 of "test1.c" starts at address 0x4004f4 <func> and ends at 0x4004fb <func+7>. 还有一个命令disassemble,你可以查看源程序的当前执行时的机器码,这个命令会把目前内存中的指令dump出来。 如下面的示例表示查看函数func的汇编代码。 (gdb) disassemble func Dump of assembler code for function func: 0x00000000004004f4 <+0>: push %rbp 0x00000000004004f5 <+1>: mov %rsp,%rbp 0x00000000004004f8 <+4>: mov %edi,-0x14(%rbp) 0x00000000004004fb <+7>: movl $0x0,-0x8(%rbp) 0x0000000000400502 <+14>: movl $0x0,-0x4(%rbp) 0x0000000000400509 <+21>: jmp 0x400515 <func+33> 0x000000000040050b <+23>: mov -0x4(%rbp),%eax 0x000000000040050e <+26>: add %eax,-0x8(%rbp) 0x0000000000400511 <+29>: addl $0x1,-0x4(%rbp) 0x0000000000400515 <+33>: mov -0x4(%rbp),%eax 0x0000000000400518 <+36>: cmp -0x14(%rbp),%eax 0x000000000040051b <+39>: jl 0x40050b <func+23> 0x000000000040051d <+41>: mov -0x8(%rbp),%eax 0x0000000000400520 <+44>: pop %rbp 0x0000000000400521 <+45>: retq End of assembler dump. 查看运行时数据 当你调试程序时,当程序被停住时,你可以使用print命令(简写命令为p),或者统一命令inspect来查看当前程序的运行数据。 print命令的格式是: print <expr> print /<f> <expr> <expr>是表达式,是你所调程序的语言的表达式(GDB可以调试多重编程语言),是输出的格式,比如,如果要把表达式按16进制的格式输出,那么就是/x。 表达式 print和许多GDB的命令一样,可以接受一个表达式,GDB会根据当前的程序运行的数据来计算这个表达式,既然是表达式,那么就可以是当前程序运行中的const常量、变量、函数等内容。可惜的是GDB不能使用你在程序中所定义的宏。 表达式的语法应该是当前所调试的语言的语法,由于C/C++是一种大众型的语言,所以,本文中的例子都是关于C/C++的。 在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中。 @ 是一个和数组有关的操作符,在后面会有更详细的说明。 :: 指定一个在文件或是一个函数中的变量。 {<type>} <addr> 表示一个指向内存地址的类型为type的一个对象。 程序变量 在GDB中,你可以随时查看以下三种变量的值: 全局变量(所有文件可见的) 静态全局变量(当前文件可见的) 局部变量(当前Scope可见的) 如果你的局部变量和全局变量发生冲突(也就是重名),一般情况下是局部变量会隐藏全局变量,也就是说,如果一个全局变量和一个函数中的局部变量同名时,如果当前停止点在函数中,用print显示出的变量的值会是函数中的局部变量的值。 如果此时你想查看全局变量的值时,你可以使用“::”操作符: file::variable function::variable 可以通过这种形式指定你所想查看的变量,无论是哪个文件中的或是哪个函数中的。 例如,查看文件test2.c中的全局变量x的值: gdb) p 'test2.c'::x 当然,“::”操作符会和C++中的类发生冲突,GDB能自动识别“::” 是否C++的操作符,所以你不必担心在调试C++程序时会出现异常。 另外,需要注意的是,如果你的程序编译时开启了优化选项,那么在用GDB调试被优化过的程序时,可能会发生某些变量不能访问,或是取值错误码的情况。这个是很正常的,因为优化程序会删改你的程序,整理你程序的语句顺序,剔除一些无意义的变量等,所以在GDB调试这种程序时,运行时的指令和你所编写指令就有不一样,也就会出现你所想象不到的结果。对付这种情况时,需要在编译程序时关闭编译优化。 一般来说,几乎所有的编译器都支持编译优化的开关,例如,GNU的C/C++编译器GCC,你可以使用“-gstabs”选项来解决这个问题。关于编译器的参数,还请查看编译器的使用说明文档。 数组 有时候,你需要查看一段连续的内存空间的值。比如数组的一段,或是动态分配的数据的大小。你可以使用GDB的“@”操作符,“@”的左边是第一个内存的地址的值,“@”的右边则你你想查看内存的长度。 例如,你的程序中有这样的语句: int *array = (int *) malloc (len * sizeof (int)); 于是,在GDB调试过程中,你可以以如下命令显示出这个动态数组的取值: p *array@len @的左边是数组的首地址的值,也就是变量array所指向的内容,右边则是数据的长度,其保存在变量len中,其输出结果,大约是下面这个样子的: (gdb) p *array@len $1 = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40} 如果是静态数组的话,可以直接用print数组名,就可以显示数组中所有数据的内容了。 输出格式 一般来说,GDB会根据变量的类型输出变量的值。但是,你也可以自定义GDB的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用GDB的数据显示格式: x 按十六进制格式显示变量 d 按十进制格式显示变量 u 按十六进制格式显示无符号整型 o 按八进制格式显示变量 t 按二进制格式显示变量 a 按十六进制格式显示变量 c 按字符格式显示变量 f 按浮点数格式显示变量 (gdb) p i $1 = 64 (gdb) p /a i $2 = 0x40 (gdb) p /x i $3 = 0x40 (gdb) p /c i $4 = 64 '@' (gdb) p /f i $5 = 8.96831017e-44 (gdb) p /o i $6 = 0100 (gdb) p /t i $7 = 1000000 查看内存 你可以使用examine命令(简写是x)来查看内存地址中的值。 x命令的语法如下所示: examine /<n/f/u> <addr> x /<n/f/u> <addr> 其中,n、f、u是可选的参数。 n是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。 f表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。 u表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。 <addr>表示一个内存地址。 n/f/u三个参数可以一起使用。例如: 命令:x /3uh 0x54320 该命令表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。 自动显示 你可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。 相关的GDB命令是display。 display <expr> display/<fmt> <expr> display/<fmt> <addr> expr是一个表达式,fmt表示显示的格式,addr表示内存地址,当你用display设定好了一个或多个表达式后,只要你的程序被停下来,GDB会自动显示你所设置的这些表达式的值。 格式i和s同样被display支持,一个非常有用的命令是: display/i $pc $pc是GDB的环境变量,表示着指令的地址,/i则表示输出格式为机器指令码,也就是汇编。于是当程序停下后,就会出现源代码和机器指令码相对应的情形,这是一个很有意思的功能。 下面是一些和display相关的GDB命令: undisplay <dnums...> delete display <dnums...> 删除自动显示,dnums意为所设置好了的自动显式的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示。 disable display <dnums...> enable display <dnums...> disable和enalbe不删除自动显示的设置,而只是让其失效和恢复。 info display 查看display设置的自动显示的信息。GDB会打出一张表格,向你报告当然调试中设置了多少个自动显示设置,其中包括,设置的编号,表达式,是否enable。 设置显示选项 GDB中关于显示的选项比较多,这里我只例举大多数常用的选项。 set print address set print address on 打开地址输出,当程序显示函数信息时,GDB会显出函数的参数地址。系统默认为打开的,如: (gdb) f #0 set_quotes (lq=0x34c78 "<<", rq=0x34c88 ">>") at input.c:530 530 if (lquote != def_lquote) set print address off 关闭函数的参数地址显示,结果如下: (gdb) f #0 set_quotes (lq= "<<", rq= ">>") at input.c:530 530 if (lquote != def_lquote) show print address 查看当前地址显示选项是否打开。 set print array set print array on 打开数组显示,打开后当数组显示时,每个元素占一行,如果不打开的话,每个元素则以逗号分隔。这个选项默认是关闭的。与之相关的两个命令如下,我就不再多说了。 set print array off show print array set print elements <number-of-elements> 这个选项主要是设置数组的,如果你的数组太大了,那么就可以指定一个来指定数据显示的最大长度,当到达这个长度时,GDB就不再往下显示了。如果设置为0,则表示不限制。 show print elements 查看print elements的选项信息。 set print null-stop <on/off> 如果打开了这个选项,那么当显示字符串时,遇到结束符则停止显示。这个选项默认为off。 set print pretty on 如果打开printf pretty这个选项,那么当GDB显示结构体时会比较漂亮。如: $1 = { next = 0x0, flags = { sweet = 1, sour = 1 }, meat = 0x54 "Pork" } set print pretty off 关闭printf pretty这个选项,GDB显示结构体时会如下显示: $1 = {next = 0x0, flags = {sweet = 1, sour = 1}, meat = 0x54 "Pork"} show print pretty 查看GDB是如何显示结构体的。 set print sevenbit-strings <on/off> 设置字符显示,是否按“/nnn”的格式显示,如果打开,则字符串或字符数据按/nnn显示,如“/065”。 show print sevenbit-strings 查看字符显示开关是否打开。 set print object <on/off> 在C++中,如果一个对象指针指向其派生类,如果打开这个选项,GDB会自动按照虚方法调用的规则显示输出,如果关闭这个选项的话,GDB就不管虚函数表了。这个选项默认是off。 show print object 查看对象选项的设置。 set print static-members <on/off> 这个选项表示,当显示一个C++对象中的内容是,是否显示其中的静态数据成员。默认是on。 show print static-members 查看静态数据成员选项设置。 set print vtbl <on/off> 当此选项打开时,GDB将用比较规整的格式来显示虚函数表时。其默认是关闭的。 show print vtbl 查看虚函数显示格式的选项。 历史记录 当你用GDB的print查看程序运行时的数据时,你每一个print都会被GDB记录下来。GDB会以$1, $2, $3 .....这样的方式为你每一个print命令编上号。于是,你可以使用这个编号访问以前的表达式,如$1。这个功能所带来的好处是,如果你先前输入了一个比较长的表达式,如果你还想查看这个表达式的值,你可以使用历史记录来访问,省去了重复输入。 GDB环境变量 你可以在GDB的调试环境中定义自己的变量,用来保存一些调试程序中的运行数据。要定义一个GDB的变量很简单只需。使用GDB的set命令。 GDB的环境变量和UNIX一样,也是以$起头。如: set $foo = *object_ptr 使用环境变量时,GDB会在你第一次使用时创建这个变量,而在以后的使用中,则直接对其賦值。环境变量没有类型,你可以给环境变量定义任一的类型。包括结构体和数组。 show convenience 该命令查看当前所设置的所有的环境变量。 这是一个比较强大的功能,环境变量和程序变量的交互使用,将使得程序调试更为灵活便捷。例如: set $i = 0 print bar[$i++]->contents 于是,当你就不必,print bar[0]->contents, print bar[1]->contents地输入命令了。输入这样的命令后,只用敲回车,重复执行上一条语句,环境变量会自动累加,从而完成逐个输出的功能。 查看寄存器 要查看寄存器的值,很简单,可以使用如下命令: info registers 查看寄存器的情况。(除了浮点寄存器) info all-registers 查看所有寄存器的情况。(包括浮点寄存器) info registers <regname ...> 查看所指定的寄存器的情况。 寄存器中放置了程序运行时的数据,比如程序当前运行的指令地址(ip),程序的当前堆栈地址(sp)等等。你同样可以使用print命令来访问寄存器的情况,只需要在寄存器名字前加一个$符号就可以了。如:p $eip。 改变程序的执行 一旦使用GDB挂上被调试程序,当程序运行起来后,你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值,这个强大的功能能够让你更好的调试你的程序,比如,你可以在程序的一次运行中走遍程序的所有分支。 修改变量值 修改被调试程序运行时的变量值,在GDB中很容易实现,使用GDB的print命令即可完成。如: (gdb) print x=4 x=4这个表达式是C/C++的语法,意为把变量x的值修改为4,如果你当前调试的语言是Pascal,那么你可以使用Pascal的语法:x:=4。 在某些时候,很有可能你的变量和GDB中的参数冲突,如: (gdb) whatis width type = double (gdb) p width $4 = 13 (gdb) set width=47 Invalid syntax in expression. 因为,set width是GDB的命令,所以,出现了“Invalid syntax in expression”的设置错误,此时,你可以使用set var命令来告诉GDB,width不是你GDB的参数,而是程序的变量名,如: (gdb) set var width=47 另外,还可能有些情况,GDB并不报告这种错误,所以保险起见,在你改变程序变量取值时,最好都使用set var格式的GDB命令。 跳转执行 一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由GDB的jump命令来完: jump <linespec> 指定下一条语句的运行点。<linespce>可以是文件的行号,可以是file:line格式,可以是+num这种偏移量格式。表示下一条运行语句从哪里开始。 jump <address> 这里的<address>是代码行的内存地址。 注意,jump命令不会改变当前的程序栈中的内容,所以,当你从一个函数跳到另一个函数时,当函数运行完返回时进行弹栈操作时必然会发生错误,可能结果还是非常奇怪的,甚至于产生程序Core Dump。所以最好是同一个函数中进行跳转。 熟悉汇编的人都知道,程序运行时,有一个寄存器用于保存当前代码所在的内存地址。所以,jump命令也就是改变了这个寄存器中的值。于是,你可以使用“set $pc”来更改跳转执行的地址。如: set $pc = 0x485 产生信号量 使用singal命令,可以产生一个信号量给被调试的程序。如:中断信号Ctrl+C。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号非常有利程序的调试。 语法是:signal <singal>,UNIX的系统信号量通常从1到15。所以<singal>取值也在这个范围。 signal命令和shell的kill命令不同,系统的kill命令发信号给被调试程序时,是由GDB截获的,而signal命令所发出一信号则是直接发给被调试程序的。 强制函数返回 如果你的调试断点在某个函数中,并还有语句没有执行完。你可以使用return命令强制函数忽略还没有执行的语句并返回。 return return <expression> 使用return命令取消当前函数的执行,并立即返回,如果指定了<expression>,那么该表达式的值会被认作函数的返回值。 强制调用函数 call <expr> 表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是void,那么就不显示。 另一个相似的命令也可以完成这一功能——print,print后面可以跟表达式,所以也可以用他来调用函数,print和call的不同是,如果函数返回void,call则不显示,print则显示函数返回值,并把该值存入历史数据中。
在讲述柔性数组之前,我们首先介绍一下不完整类型(incomplete type)。不完整类型是这样一种类型,它缺乏足够的信息例如长度去描述一个完整的对象。 incomplete types ( types that describe objects but lack information needed to determine their sizes). C与C++关于不完整类型的语义是一样的。 基本上没有什么书介绍过不完整类型,很多人初次遇到这个概念时脑袋会一片空白。事实上,我们在实际的工程设计中经常使用不完整类型,只不过不知道有这么个概念而已。前向声明就是一种常用的不完整类型。 class base; struct test; base和test只给出了声明,没有给出定义。不完整类型必须通过某种方式补充完整,才能使用它们进行实例化,否则只能用于定义指针或引用,大括号形式的初始化就是其中一种方式: int a[] = { 10, 15 }; 柔性数组成员(flexible array member)也叫做伸缩性数组成员,它的出现反映了C程序员对精炼代码的极致追求。这种代码结构产生于对动态结构体的需求。在日常的编程中,有时候需要在结构体中存放一个长度动态的字符串,一般的做法是,在结构体中定义一个指针成员,这个指针成员指向该字符串所在的动态内存空间中。例如: struct testClass { int a; double b; char *p; } 成员变量p指向字符串。但是,这种方法造成字符串与结构体是分离的,不利于操作。如果把字符串与结构体直接连接在一起,不是更好吗?于是,可以把代码修改为这样: char a[] = "Hello world."; struct testClass *pTC = (struct testClass *)malloc( sizeof(struct testClass) + strlen(a) + 1 ); strcpy( pTC + 1, a ); 这样一来,(char *)(pTC + 1)就是字符串”Hello world.”的地址了。这时候,p就成了多余的东西,可以去掉。 但是,这时又产生了另外一个问题:老是使用(char *)(pTC + 1)十分不方便。如果能够找出一种方法,既能直接引用该字符串,又不占用结构体的空间,那就完美了。符合这种条件的代码结构应该是一个非对象的符号地址,在结构体的尾部放置一个长度为0的数组是一个绝妙的解决方案。不过,C/C++标准规定不能定义长度为0的数组,因此,有些编译器就把0长度的数组成员作为自己的非标准扩展。例如: struct testClass { int a; double b; char c[0]; }; 其中,c就叫柔性数组成员,如果把pTC指向的动态分配内存看作一个整体,c就是一个长度可以动态变化的结构体成员,柔性一词来源于此。c的长度为0,因此它不占用testClass的空间,同时pTC->c就是”Hello world.”的首地址,不需要再使用( char* )( pTC + 1 )这么丑陋的语法了。 鉴于这种代码结构所产生的重要作用,C99甚至把它收入了标准中: As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. C99使用不完整类型实现柔性数组成员,标准形式是这样的: struct testClass { int a; double b; char c[]; }; c同样不占用testClass的空间,只作为一个符号地址存在,而且必须是结构体的最后一个成员。柔性数组成员不仅可以用于字符数组,还可以是元素为其它类型的数组,例如: struct testClass { int a; double b; float c[]; }; 应当尽量使用标准形式,在非C99的场合,可以使用指针方法。有些人使用char a[1],这是非常不可取的,把这样的a用作柔性数组成员会发生越界行为,虽然C/C++标准并没有规定编译器应当检查越界,但也没有规定不能检查越界,为了一个小小的指针空间而牺牲移植性,是不值得的。
本系列博文主要侧重于分析Netfilter的实现机制、原理和设计思想层面的东西,同时包括从用户态的iptables到内核态的Netfilter的交互过程和通信手段等。至于iptables的入门用法方面的东西,网上随便一搜罗就有一大堆,我这里不浪费笔墨了。 很多人在接触iptables之后就会这么一种感觉:我通过iptables命令配置的每一条规则,到底是如何生效的呢?内核又是怎么去执行这些规则匹配呢?如果iptables不能满足我当下的需求,那么我是否可以去对其进行扩展呢?这些问题,都是我在接下来的博文中一一和大家分享的话题。这里需要指出:因为Netfilter与IP协议栈是无缝契合的,所以如果你要是有协议栈方面的基础,在阅读本文时一定会感觉轻车熟路。当然,如果没有也没关系,因为我会在关键点就协议栈的入门知识给大家做个普及。只是普及哦,不会详细深入下去的,因为涉及的东西太多了,目前我还正在研究摸索当中呢。好了,废话不多说,进入正题。 备注:本人研究的内核版本是2.6.21,iptables的版本是1.4.0。 什么是Netfilter? 为了说明这个问题,我们首先看一个网络通信的基本模型: 在数据的发送过程中,从上至下依次是“加头”的过程,每到达一层,数据就会被加上该层的头部信息。与此同时,接收数据方就是个“剥头”的过程,当从网卡接收到数据包之后,在往协议栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。 那么,对于IPv4协议栈,其“栈”模式底层机制基本就是像下面这个样子: 对于接收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本地主机的就经过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的主机不是本机,那么就经过“C”点,然后顺着“E”点将该数据包发送出去。 对于欲发送的每个数据包,首先也有一个路由判决,以确定该数据包从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该数据包发送出去。 协议栈中的那五个关键点A、B、C、D和E就是我们Netfilter大展拳脚的地方了。 Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供了一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter在内核中的位置如下图所示: 上面这幅图,很直观地反映了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。 回到前面讨论的关于协议栈的那五个关键点“ABCDE”上来。Netfilter在netfilter_ipv4.h中将这五个点重新命了名,如下图所示,意思我就不再解释了,猫叫喵喵而已: /* IP Hooks */ /* After promisc drops, checksum checks. */ #define NF_IP_PRE_ROUTING 0 /* If the packet is destined for this box. */ #define NF_IP_LOCAL_IN 1 /* If the packet is destined for another interface. */ #define NF_IP_FORWARD 2 /* Packets coming from a local process. */ #define NF_IP_LOCAL_OUT 3 /* Packets about to hit the wire. */ #define NF_IP_POST_ROUTING 4 在每个关键点上,有很多已经按照优先级预先注册了的回调函数(后面再说这些函数是什么以及干什么用的)。有些人也喜欢把这些函数称为“钩子函数(Hooks)”,说的是同一个东西。这些函数被埋伏在这些关键点,形成了一条链。对于每个到来的或发出的数据包会依次被这些回调函数“调戏”一番后再视情况是将其放行,丢弃还是怎么滴。但是,无论如何,这些回调函数最后必须向Netfilter报告一下该数据包的死活情况,因为毕竟每个数据包都是Netfilter从别人协议栈那儿借调过来给兄弟们Happy的,别个再怎么滴也总得“活要见人,死要见尸”吧。每个钩子函数最后必须向Netfilter框架返回下列几个值之一: NF_ACCEPT 继续正常传输数据包。该返回值告诉Netfilter:到目前为止,该数据包还是被接受的,并且该数据包应当被递交给网络协议栈的下一个阶段; NF_DROP 丢弃该数据包,不再传输; NF_STOLEN 模块接管该数据包,告诉Netfilter”忘掉“该数据包,也就是说本模块”偷(stolen)“了这个数据包。该回调函数将从此开始对数据包进行处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter获取了该数据包的所有权。 NF_QUEUE 对该数据包进行排队(通常用于将数据包传递给用户空间的进程进行处理); NF_REPEAT 再次调用该回调函数,应当谨慎使用该值,以免造成死循环。 /* Responses from hook functions. */ #define NF_DROP 0 #define NF_ACCEPT 1 #define NF_STOLEN 2 #define NF_QUEUE 3 #define NF_REPEAT 4 #define NF_MAX_VERDICT NF_REPEAT 为了让我们显得更专业点,我们开始做些约定:上面提到的五个关键点后面我们就叫它们为hook点,每个hook点所注册的那些回调函数都将其称为hook函数。 Linux 2.6版内核的Netfilter目前支持IPv4、IPv6以及DECnet等协议栈,这里我们主要研究IPv4协议。关于协议类型、hook点、hook函数以及优先级,我们通过下图给大家做个详细展示: 对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂载了很多hook函数。这些hook函数就是用来处理数据包的。 Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在协议栈内部切入到Netfilter框架中。2.6版本内核对于该宏的定义如下: /* This is gross, but inline doesn't cut it for avoiding the function call in fast path: gcc doesn't inline (needs value tracking?). --RR */ /* HX: It's slightly less gross now. */ #define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh) \ ({int __ret; \ if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, thresh, 1)) == 1)\ __ret = (okfn)(skb); \ __ret;}) #define NF_HOOK_COND(pf, hook, skb, indev, outdev, okfn, cond) \ ({int __ret; \ if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, INT_MIN, cond)) == 1)\ __ret = (okfn)(skb); \ __ret;}) #define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \ NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN) 关于宏NF_HOOK各个参数的说明如下: 1. pf:协议族名称,Netfilter架构同样可以用于IP层之外,因此,该变量还可以有诸如PF_INET6、PF_DECnet等名字。 2. hook:hook点的名字,对于IP层,其值即为前面的五个关键点值; 3. skb:不解释,sk_buff结构体变量,即数据包指针; 4. indev:数据包进入的设备,以struct net_device结构表示; 5. outdev:数据包出去的设备,以struct net_device结构表示;后面可以看到,以上五个参数将传递给nf_register_hook中注册的处理函数。 6. okfn:函数指针,当所有的该hook点的所有注册函数被调用完之后,转而执行此流程。 对于NF_HOOK_THRESH,其定义如上代码。 我们发现NF_HOOK_THRESH宏只增加了一个thresh参数,该参数就是用来执行该宏去遍历hook函数时的优先级,同时,该宏内部又调用了nf_hook_thresh函数。 /** * nf_hook_thresh - call a netfilter hook * * Returns 1 if the hook has allowed the packet to pass. The function * okfn must be invoked by the caller in this case. Any other return * value indicates the packet has been consumed by the hook. */ static inline int nf_hook_thresh(int pf, unsigned int hook, struct sk_buff **pskb, struct net_device *indev, struct net_device *outdev, int (*okfn)(struct sk_buff *), int thresh, int cond) { if (!cond) return 1; #ifndef CONFIG_NETFILTER_DEBUG if (list_empty(&nf_hooks[pf][hook])) return 1; #endif return nf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh); } 该函数只增加了一个参数cond,该参数为0则放弃遍历,并且也不执行okfn函数;如果该参数为1,则进行下一步操作。对于下一步的调用,其是条件性的。如果没有设置CONFIG_NETFILTER_DEBUG环境变量,那么,下一步则直接执行nf_hook_slow函数。而如果设置了CONFIG_NETFILTER_DEBUG环境变量,那么,情况就有所不同了。内核需要首先检查对应协议族和对应hook点的注册钩子函数链是否为空,如果是的,则返回1,而NF_HOOK_THRESH宏的后续工作则为直接执行okfn函数指针对应的处理过程,反之,nf_hook_thresh就去执行nf_hook_slow函数。那么,nf_hook_slow函数到底干了什么事?见【R】处。 要清楚地说明这种特殊情况的行为,我们必须对list_empty(&nf_hooks[pf][hook])语句进行深入地分析。 在net/core/netfilter.c文件中,定义了一个二维的结构体数组,用来存储不同协议栈hook点的回调处理函数。 struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS]; 其中,行数NPROTO为32,即目前内核所支持的最大协议族数;列数NF_MAX_HOOKS为hook点的个数,目前在2.6内核中该值为8。因此,nf_hooks数组的最终结构如下图所示: 在include/linux/socket.h中IP协议AF_INET(PF_INET)的序号为2,因此我们就可以得到TCP/IP协议族的钩子函数挂载点为: PRE_ROUTING: nf_hooks[2][0] LOCAL_IN: nf_hooks[2][1] FORWARD: nf_hooks[2][2] LOCAL_OUT: nf_hooks[2][3] POST_ROUTING: nf_hooks[2][4] 同时我们看到,在2.6内核的IP协议栈中,从协议栈正常的流程切入到Netfilter框架中,然后顺序地依次地调用每个hook点所有的钩子函数的相关操作有如下几处: 1.net/ipv4/ip_input.c中的ip_rcv函数。该函数主要用来处理网络层的IP报文的入口函数,它到Netfilter框架的切入点为: NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish); 根据前面的理解,这句代码意义已经很直观明确了。那就是:如果协议栈当前接收到了一个IP报文(PF_INET),那么就把这个报文传到NF_IP_PRE_ROUTING过滤点,去检查【R】在那个过滤点(nf_hooks[2][0])是否有人注册了相关的用于处理数据包的钩子函数。如果有,则依次遍历链表nf_hooks[2][0]去需找匹配的match和相应的target,根据返回到Netfilter框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是交由ip_rcv_finish函数继续处理)。 【R】:刚才说到的所谓”检查“。其核心就是nf_hook_slow函数。该函数本质上做的事很简单,其根据优先级查找双向链表nf_hooks[][],找到对应的回调函数来处理数据包,详细代码如下: int nf_hook_slow(int pf, unsigned int hook, struct sk_buff *skb, struct net_device *indev, struct net_device *outdev, int (*okfn)(struct sk_buff *), int hook_thresh) { struct list_head *elem; unsigned int verdict; int ret = 0; /* We may already have this, but read-locks nest anyway */ rcu_read_lock(); #ifdef CONFIG_NETFILTER_DEBUG if (skb->nf_debug & (1 << hook)) { printk("nf_hook: hook %i already set.\n", hook); nf_dump_skb(pf, skb); } skb->nf_debug |= (1 << hook); #endif elem = &nf_hooks[pf][hook]; next_hook: verdict = nf_iterate(&nf_hooks[pf][hook], &skb, hook, indev, outdev, &elem, okfn, hook_thresh); if (verdict == NF_QUEUE) { NFDEBUG("nf_hook: Verdict = QUEUE.\n"); if (!nf_queue(skb, elem, pf, hook, indev, outdev, okfn)) goto next_hook; } switch (verdict) { case NF_ACCEPT: ret = okfn(skb); break; case NF_DROP: kfree_skb(skb); ret = -EPERM; break; } rcu_read_unlock(); return ret; } static unsigned int nf_iterate(struct list_head *head, struct sk_buff **skb, int hook, const struct net_device *indev, const struct net_device *outdev, struct list_head **i, int (*okfn)(struct sk_buff *), int hook_thresh) { /* * The caller must not block between calls to this * function because of risk of continuing from deleted element. */ list_for_each_continue_rcu(*i, head) { struct nf_hook_ops *elem = (struct nf_hook_ops *)*i; if (hook_thresh > elem->priority) continue; /* Optimization: we don't need to hold module reference here, since function can't sleep. --RR */ switch (elem->hook(hook, skb, indev, outdev, okfn)) { case NF_QUEUE: return NF_QUEUE; case NF_STOLEN: return NF_STOLEN; case NF_DROP: return NF_DROP; case NF_REPEAT: *i = (*i)->prev; break; #ifdef CONFIG_NETFILTER_DEBUG case NF_ACCEPT: break; default: NFDEBUG("Evil return from %p(%u).\n", elem->hook, hook); #endif } } return NF_ACCEPT; } 2.net/ipv4/ip_forward.c中的ip_forward函数,它的切入点为: NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev, ip_forward_finish); 在经过路由抉择后,所有需要本机转发的报文都会交由ip_forward函数进行处理。这里,该函数由NF_IP_FORWARD过滤点切入到Netfilter框架,在nf_hooks[2][2]过滤点执行匹配查找。最后根据返回值来确定ip_forward_finish函数的执行情况。 int ip_forward(struct sk_buff *skb) { struct iphdr *iph; /* Our header */ struct rtable *rt; /* Route we use */ struct ip_options * opt = &(IPCB(skb)->opt); if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb)) goto drop; if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb)) return NET_RX_SUCCESS; if (skb->pkt_type != PACKET_HOST) goto drop; skb->ip_summed = CHECKSUM_NONE; /* * According to the RFC, we must first decrease the TTL field. If * that reaches zero, we must reply an ICMP control message telling * that the packet's lifetime expired. */ iph = skb->nh.iph; if (iph->ttl <= 1) goto too_many_hops; if (!xfrm4_route_forward(skb)) goto drop; iph = skb->nh.iph; rt = (struct rtable*)skb->dst; if (opt->is_strictroute && rt->rt_dst != rt->rt_gateway) goto sr_failed; /* We are about to mangle packet. Copy it! */ if (skb_cow(skb, LL_RESERVED_SPACE(rt->u.dst.dev)+rt->u.dst.header_len)) goto drop; iph = skb->nh.iph; /* Decrease ttl after skb cow done */ ip_decrease_ttl(iph); /* * We now generate an ICMP HOST REDIRECT giving the route * we calculated. */ if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr) ip_rt_send_redirect(skb); skb->priority = rt_tos2priority(iph->tos); return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev, ip_forward_finish); sr_failed: /* * Strict routing permits no gatewaying */ icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0); goto drop; too_many_hops: /* Tell the sender its packet died... */ icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0); drop: kfree_skb(skb); return NET_RX_DROP; } 3.net/ipv4/ip_output.c中的ip_output函数,它的切入点为: NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev, ip_finish_output, !(IPCB(skb)->flags & IPSKB_REROUTED)); 这里,我们看到切入点从无条件宏NF_HOOK改成了有条件宏NF_HOOK_COND,调用该宏的条件是:如果协议栈当前所处理的数据包中没有重新路由的标记,数据包才会进入Netfilter框架。否则,直接调用ip_finish_output函数走协议栈去处理。除此之外,有条件宏和无条件宏再无其他任何差异。 如果需要陷入Netfilter框架,则数据包会在nf_hooks[2][4]过滤点去进行匹配查找。 4.net/ipv4/ip_input.c中的ip_local_deliver函数。干函数处理所有目的地址是本机的数据包,其切入点为: NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,ip_local_deliver_finish); 发往本机的数据包,首先会全部去往nf_hooks[2][1]过滤点上检测是否有相关数据包的回调处理函数,如果有则执行匹配动作,最后根据返回值执行ip_local_deliver_finish函数。 /* * Deliver IP Packets to the higher protocol layers. */ int ip_local_deliver(struct sk_buff *skb) { /* * Reassemble IP fragments. */ if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) { skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER); if (!skb) return 0; } return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish); } 5.net/ipv4/ip_output.c中的ip_push_pending_frame函数。该函数将IP分片重组成完整的IP报文,然后发送出去。进入Netfilter框架的切入点为: NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output); 对于所有从本机发出去的报文都会首先去Netfilter的nf_hooks[2][3]过滤点去过滤。一般情况下来来说,不管是路由器还是PC中端,很少有人限制自己机器发出去的报文。因为这样做的潜在风险也是显而易见的,往往会因为一些不恰当的设置导致某些服务失效,所以在这个过滤点上拦截数据包的情况非常少。当然也不排除真的有特殊需求的情况。 /* * Combined all pending IP fragments on the socket as one IP datagram * and push them out. */ int ip_push_pending_frames(struct sock *sk) { struct sk_buff *skb, *tmp_skb; struct sk_buff **tail_skb; struct inet_sock *inet = inet_sk(sk); struct ip_options *opt = NULL; struct rtable *rt = inet->cork.rt; struct iphdr *iph; int df = 0; __u8 ttl; int err = 0; if ((skb = __skb_dequeue(&sk->sk_write_queue)) == NULL) goto out; tail_skb = &(skb_shinfo(skb)->frag_list); /* move skb->data to ip header from ext header */ if (skb->data < skb->nh.raw) __skb_pull(skb, skb->nh.raw - skb->data); while ((tmp_skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) { __skb_pull(tmp_skb, skb->h.raw - skb->nh.raw); *tail_skb = tmp_skb; tail_skb = &(tmp_skb->next); skb->len += tmp_skb->len; skb->data_len += tmp_skb->len; skb->truesize += tmp_skb->truesize; __sock_put(tmp_skb->sk); tmp_skb->destructor = NULL; tmp_skb->sk = NULL; } /* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow * to fragment the frame generated here. No matter, what transforms * how transforms change size of the packet, it will come out. */ if (inet->pmtudisc != IP_PMTUDISC_DO) skb->local_df = 1; /* DF bit is set when we want to see DF on outgoing frames. * If local_df is set too, we still allow to fragment this frame * locally. */ if (inet->pmtudisc == IP_PMTUDISC_DO || (!skb_shinfo(skb)->frag_list && ip_dont_fragment(sk, &rt->u.dst))) df = htons(IP_DF); if (inet->cork.flags & IPCORK_OPT) opt = inet->cork.opt; if (rt->rt_type == RTN_MULTICAST) ttl = inet->mc_ttl; else ttl = ip_select_ttl(inet, &rt->u.dst); iph = (struct iphdr *)skb->data; iph->version = 4; iph->ihl = 5; if (opt) { iph->ihl += opt->optlen>>2; ip_options_build(skb, opt, inet->cork.addr, rt, 0); } iph->tos = inet->tos; iph->tot_len = htons(skb->len); iph->frag_off = df; if (!df) { __ip_select_ident(iph, &rt->u.dst, 0); } else { iph->id = htons(inet->id++); } iph->ttl = ttl; iph->protocol = sk->sk_protocol; iph->saddr = rt->rt_src; iph->daddr = rt->rt_dst; ip_send_check(iph); skb->priority = sk->sk_priority; skb->dst = dst_clone(&rt->u.dst); /* Netfilter gets whole the not fragmented skb. */ err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output); if (err) { if (err > 0) err = inet->recverr ? net_xmit_errno(err) : 0; if (err) goto error; } out: inet->cork.flags &= ~IPCORK_OPT; if (inet->cork.opt) { kfree(inet->cork.opt); inet->cork.opt = NULL; } if (inet->cork.rt) { ip_rt_put(inet->cork.rt); inet->cork.rt = NULL; } return err; error: IP_INC_STATS(IPSTATS_MIB_OUTDISCARDS); goto out; } 小结 整个Linux内核中Netfilter框架的HOOK机制可以概括如下: 在数据包流经内核协议栈的整个过程中,在一些已预定义的关键点上PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING,内核会根据数据包的协议族PF_INET去往这些hook点上去查找是否注册有钩子函数。如果没有,则直接返回okfn函数指针所指向的函数继续走协议栈;如果有,则调用nf_hook_slow函数,从而进入到Netfilter框架中进一步调用已注册在该过滤点下的钩子函数,再根据其返回值来确定是否继续执行由函数指针okfn所指向的函数。
A list of practical projects that anyone can solve in any programming language (See solutions). These projects are divided in multiple categories, and each category has its own folder. To get started, simply fork this repo. CONTRIBUTING See ways of contributing to this repo. You can contribute solutions (will be published in this repo) to existing problems, add new projects or remove existing ones. Make sure you follow all instructions properly. Solutions You can find implementations of these projects in many other languages by other users in this repo. Credits This repo was compiled by Karan Goel. Problems are motivated by the ones shared at: Martyr2’s Mega Project List Rosetta Code Table of Contents Numbers Classic Algorithms Graph Data Structures Text Networking Classes Threading Web Files Databases Graphics and Multimedia Security Numbers Find PI to the Nth Digit - Enter a number and have the program generate PI up to that many decimal places. Keep a limit to how far the program will go. Find e to the Nth Digit - Just like the previous problem, but with e instead of PI. Enter a number and have the program generate e up to that many decimal places. Keep a limit to how far the program will go. Fibonacci Sequence - Enter a number and have the program generate the Fibonacci sequence to that number or to the Nth number. Prime Factorization - Have the user enter a number and find all Prime Factors (if there are any) and display them. Next Prime Number - Have the program find prime numbers until the user chooses to stop asking for the next one. Find Cost of Tile to Cover W x H Floor - Calculate the total cost of tile it would take to cover a floor plan of width and height, using a cost entered by the user. Mortgage Calculator - Calculate the monthly payments of a fixed term mortgage over given Nth terms at a given interest rate. Also figure out how long it will take the user to pay back the loan. For added complexity, add an option for users to select the compounding interval (Monthly, Weekly, Daily, Continually). Change Return Program - The user enters a cost and then the amount of money given. The program will figure out the change and the number of quarters, dimes, nickels, pennies needed for the change. Binary to Decimal and Back Converter - Develop a converter to convert a decimal number to binary or a binary number to its decimal equivalent. Calculator - A simple calculator to do basic operators. Make it a scientific calculator for added complexity. Unit Converter (temp, currency, volume, mass and more) - Converts various units between one another. The user enters the type of unit being entered, the type of unit they want to convert to and then the value. The program will then make the conversion. Alarm Clock - A simple clock where it plays a sound after X number of minutes/seconds or at a particular time. Distance Between Two Cities - Calculates the distance between two cities and allows the user to specify a unit of distance. This program may require finding coordinates for the cities like latitude and longitude. Credit Card Validator - Takes in a credit card number from a common credit card vendor (Visa, MasterCard, American Express, Discoverer) and validates it to make sure that it is a valid number (look into how credit cards use a checksum). Tax Calculator - Asks the user to enter a cost and either a country or state tax. It then returns the tax plus the total cost with tax. Factorial Finder - The Factorial of a positive integer, n, is defined as the product of the sequence n, n-1, n-2, …1 and the factorial of zero, 0, is defined as being 1. Solve this using both loops and recursion. Complex Number Algebra - Show addition, multiplication, negation, and inversion of complex numbers in separate functions. (Subtraction and division operations can be made with pairs of these operations.) Print the results for each operation tested. Happy Numbers - A happy number is defined by the following process. Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers, while those that do not end in 1 are unhappy numbers. Display an example of your output here. Find first 8 happy numbers. Number Names - Show how to spell out a number in English. You can use a preexisting implementation or roll your own, but you should support inputs up to at least one million (or the maximum value of your language’s default bounded integer type, if that’s less). Optional: Support for inputs other than positive integers (like zero, negative integers, and floating-point numbers). Coin Flip Simulation - Write some code that simulates flipping a single coin however many times the user decides. The code should record the outcomes and count the number of tails and heads. Limit Calculator - Ask the user to enter f(x) and the limit value, then return the value of the limit statement Optional: Make the calculator capable of supporting infinite limits. Fast Exponentiation - Ask the user to enter 2 integers a and b and output a^b (i.e. pow(a,b)) in O(lg n) time complexity. Classic Algorithms Collatz Conjecture - Start with a number n > 1. Find the number of steps it takes to reach one using the following process: If n is even, divide it by 2. If n is odd, multiply it by 3 and add 1. Sorting - Implement two types of sorting algorithms: Merge sort and bubble sort. Closest pair problem - The closest pair of points problem or closest pair problem is a problem of computational geometry: given n points in metric space, find a pair of points with the smallest distance between them. Sieve of Eratosthenes - The sieve of Eratosthenes is one of the most efficient ways to find all of the smaller primes (below 10 million or so). Graph Graph from links - Create a program that will create a graph or network from a series of links. Eulerian Path - Create a program which will take as an input a graph and output either a Eulerian path or a Eulerian cycle, or state that it is not possible. A Eulerian Path starts at one node and traverses every edge of a graph through every node and finishes at another node. A Eulerian cycle is a eulerian Path that starts and finishes at the same node. Connected Graph - Create a program which takes a graph as an input and outputs whether every node is connected or not. Dijkstra’s Algorithm - Create a program that finds the shortest path through a graph using its edges. Minimum Spanning Tree - Create a program which takes a connected, undirected graph with weights and outputs the minimum spanning tree of the graph i.e., a subgraph that is a tree, contains all the vertices, and the sum of its weights is the least possible. Data Structures Inverted index - An Inverted Index is a data structure used to create full text search. Given a set of text files, implement a program to create an inverted index. Also create a user interface to do a search using that inverted index which returns a list of files that contain the query term / terms. The search index can be in memory. Text Fizz Buzz - Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”. Reverse a String - Enter a string and the program will reverse it and print it out. Pig Latin - Pig Latin is a game of alterations played on the English language game. To create the Pig Latin form of an English word the initial consonant sound is transposed to the end of the word and an ay is affixed (Ex.: “banana” would yield anana-bay). Read Wikipedia for more information on rules. Count Vowels - Enter a string and the program counts the number of vowels in the text. For added complexity have it report a sum of each vowel found. Check if Palindrome - Checks if the string entered by the user is a palindrome. That is that it reads the same forwards as backwards like “racecar” Count Words in a String - Counts the number of individual words in a string. For added complexity read these strings in from a text file and generate a summary. Text Editor - Notepad style application that can open, edit, and save text documents. Optional: Add syntax highlighting and other features. RSS Feed Creator - Given a link to RSS/Atom Feed, get all posts and display them. Quote Tracker (market symbols etc) - A program which can go out and check the current value of stocks for a list of symbols entered by the user. The user can set how often the stocks are checked. For CLI, show whether the stock has moved up or down. Optional: If GUI, the program can show green up and red down arrows to show which direction the stock value has moved. Guestbook / Journal - A simple application that allows people to add comments or write journal entries. It can allow comments or not and timestamps for all entries. Could also be made into a shout box. Optional: Deploy it on Google App Engine or Heroku or any other PaaS (if possible, of course). Vigenere / Vernam / Ceasar Ciphers - Functions for encrypting and decrypting data messages. Then send them to a friend. Regex Query Tool - A tool that allows the user to enter a text string and then in a separate control enter a regex pattern. It will run the regular expression against the source text and return any matches or flag errors in the regular expression. Networking FTP Program - A file transfer program which can transfer files back and forth from a remote web sever. Bandwidth Monitor - A small utility program that tracks how much data you have uploaded and downloaded from the net during the course of your current online session. See if you can find out what periods of the day you use more and less and generate a report or graph that shows it. Port Scanner - Enter an IP address and a port range where the program will then attempt to find open ports on the given computer by connecting to each of them. On any successful connections mark the port as open. Mail Checker (POP3 / IMAP) - The user enters various account information include web server and IP, protocol type (POP3 or IMAP) and the application will check for email at a given interval. Country from IP Lookup - Enter an IP address and find the country that IP is registered in. Optional: Find the Ip automatically. Whois Search Tool - Enter an IP or host address and have it look it up through whois and return the results to you. Site Checker with Time Scheduling - An application that attempts to connect to a website or server every so many minutes or a given time and check if it is up. If it is down, it will notify you by email or by posting a notice on screen. Classes Product Inventory Project - Create an application which manages an inventory of products. Create a product class which has a price, id, and quantity on hand. Then create an inventory class which keeps track of various products and can sum up the inventory value. Airline / Hotel Reservation System - Create a reservation system which books airline seats or hotel rooms. It charges various rates for particular sections of the plane or hotel. Example, first class is going to cost more than coach. Hotel rooms have penthouse suites which cost more. Keep track of when rooms will be available and can be scheduled. Company Manager - Create an hierarchy of classes - abstract class Employee and subclasses HourlyEmployee, SalariedEmployee, Manager and Executive. Every one’s pay is calculated differently, research a bit about it. After you’ve established an employee hierarchy, create a Company class that allows you to manage the employees. You should be able to hire, fire and raise employees. Bank Account Manager - Create a class called Account which will be an abstract class for three other classes called CheckingAccount, SavingsAccount and BusinessAccount. Manage credits and debits from these accounts through an ATM style program. Patient / Doctor Scheduler - Create a patient class and a doctor class. Have a doctor that can handle multiple patients and setup a scheduling program where a doctor can only handle 16 patients during an 8 hr work day. Recipe Creator and Manager - Create a recipe class with ingredients and a put them in a recipe manager program that organizes them into categories like deserts, main courses or by ingredients like chicken, beef, soups, pies etc. Image Gallery - Create an image abstract class and then a class that inherits from it for each image type. Put them in a program which displays them in a gallery style format for viewing. Shape Area and Perimeter Classes - Create an abstract class called Shape and then inherit from it other shapes like diamond, rectangle, circle, triangle etc. Then have each class override the area and perimeter functionality to handle each shape type. Flower Shop Ordering To Go - Create a flower shop application which deals in flower objects and use those flower objects in a bouquet object which can then be sold. Keep track of the number of objects and when you may need to order more. Family Tree Creator - Create a class called Person which will have a name, when they were born and when (and if) they died. Allow the user to create these Person classes and put them into a family tree structure. Print out the tree to the screen. Threading Create A Progress Bar for Downloads - Create a progress bar for applications that can keep track of a download in progress. The progress bar will be on a separate thread and will communicate with the main thread using delegates. Bulk Thumbnail Creator - Picture processing can take a bit of time for some transformations. Especially if the image is large. Create an image program which can take hundreds of images and converts them to a specified size in the background thread while you do other things. For added complexity, have one thread handling re-sizing, have another bulk renaming of thumbnails etc. Web Page Scraper - Create an application which connects to a site and pulls out all links, or images, and saves them to a list. Optional: Organize the indexed content and don’t allow duplicates. Have it put the results into an easily searchable index file. Online White Board - Create an application which allows you to draw pictures, write notes and use various colors to flesh out ideas for projects. Optional: Add feature to invite friends to collaborate on a white board online. Get Atomic Time from Internet Clock - This program will get the true atomic time from an atomic time clock on the Internet. Use any one of the atomic clocks returned by a simple Google search. Fetch Current Weather - Get the current weather for a given zip/postal code. Optional: Try locating the user automatically. Scheduled Auto Login and Action - Make an application which logs into a given site on a schedule and invokes a certain action and then logs out. This can be useful for checking web mail, posting regular content, or getting info for other applications and saving it to your computer. E-Card Generator - Make a site that allows people to generate their own little e-cards and send them to other people. Do not use Flash. Use a picture library and perhaps insightful mottos or quotes. Content Management System - Create a content management system (CMS) like Joomla, Drupal, PHP Nuke etc. Start small. Optional: Allow for the addition of modules/addons. Web Board (Forum) - Create a forum for you and your buddies to post, administer and share thoughts and ideas. CAPTCHA Maker - Ever see those images with letters a numbers when you signup for a service and then asks you to enter what you see? It keeps web bots from automatically signing up and spamming. Try creating one yourself for online forms. Files Quiz Maker - Make an application which takes various questions from a file, picked randomly, and puts together a quiz for students. Each quiz can be different and then reads a key to grade the quizzes. Sort Excel/CSV File Utility - Reads a file of records, sorts them, and then writes them back to the file. Allow the user to choose various sort style and sorting based on a particular field. Create Zip File Maker - The user enters various files from different directories and the program zips them up into a zip file. Optional: Apply actual compression to the files. Start with Huffman Algorithm. PDF Generator - An application which can read in a text file, html file or some other file and generates a PDF file out of it. Great for a web based service where the user uploads the file and the program returns a PDF of the file. Optional: Deploy on GAE or Heroku if possible. Mp3 Tagger - Modify and add ID3v1 tags to MP3 files. See if you can also add in the album art into the MP3 file’s header as well as other ID3v2 tags. Code Snippet Manager - Another utility program that allows coders to put in functions, classes or other tidbits to save for use later. Organized by the type of snippet or language the coder can quickly look up code. Optional: For extra practice try adding syntax highlighting based on the language. Databases SQL Query Analyzer - A utility application which a user can enter a query and have it run against a local database and look for ways to make it more efficient. Remote SQL Tool - A utility that can execute queries on remote servers from your local computer across the Internet. It should take in a remote host, user name and password, run the query and return the results. Report Generator - Create a utility that generates a report based on some tables in a database. Generates a sales reports based on the order/order details tables or sums up the days current database activity. Event Scheduler and Calendar - Make an application which allows the user to enter a date and time of an event, event notes and then schedule those events on a calendar. The user can then browse the calendar or search the calendar for specific events. Optional: Allow the application to create re-occurrence events that reoccur every day, week, month, year etc. Budget Tracker - Write an application that keeps track of a household’s budget. The user can add expenses, income, and recurring costs to find out how much they are saving or losing over a period of time. Optional: Allow the user to specify a date range and see the net flow of money in and out of the house budget for that time period. TV Show Tracker - Got a favorite show you don’t want to miss? Don’t have a PVR or want to be able to find the show to then PVR it later? Make an application which can search various online TV Guide sites, locate the shows/times/channels and add them to a database application. The database/website then can send you email reminders that a show is about to start and which channel it will be on. Travel Planner System - Make a system that allows users to put together their own little travel itinerary and keep track of the airline / hotel arrangements, points of interest, budget and schedule. Graphics and Multimedia Slide Show - Make an application that shows various pictures in a slide show format. Optional: Try adding various effects like fade in/out, star wipe and window blinds transitions. Stream Video from Online - Try to create your own online streaming video player. Mp3 Player - A simple program for playing your favorite music files. Add features you think are missing from your favorite music player. Watermarking Application - Have some pictures you want copyright protected? Add your own logo or text lightly across the background so that no one can simply steal your graphics off your site. Make a program that will add this watermark to the picture. Optional: Use threading to process multiple images simultaneously. Turtle Graphics - This is a common project where you create a floor of 20 x 20 squares. Using various commands you tell a turtle to draw a line on the floor. You have move forward, left or right, lift or drop pen etc. Do a search online for “Turtle Graphics” for more information. Optional: Allow the program to read in the list of commands from a file. GIF Creator A program that puts together multiple images (PNGs, JPGs, TIFFs) to make a smooth GIF that can be exported. Optional: Make the program convert small video files to GIFs as well. Security Caesar cipher - Implement a Caesar cipher, both encoding and decoding. The key is an integer from 1 to 25. This cipher rotates the letters of the alphabet (A to Z). The encoding replaces each letter with the 1st to 25th next letter in the alphabet (wrapping Z to A). So key 2 encrypts “HI” to “JK”, but key 20 encrypts “HI” to “BC”. This simple “monoalphabetic substitution cipher” provides almost no security, because an attacker who has the encoded message can either use frequency analysis to guess the key, or just try all 25 keys.
Ctrl+C:送SIGINT信号,默认进程会结束,但是进程自己可以重定义收到这个信号的行为。 Ctrl+Z:送SIGSTOP信号,进程只是被停止,再送SIGCONT信号,进程继续运行。 Ctrl+D:不是发送信号,而是表示一个特殊的二进制值,表示 EOF。 有些信号不能被屏蔽,比如中断,还应该有杀死进程的信号,要不然内核怎么做操作系统中的老大。实际上,SIGKILL和SIGSTOP信号是不能被屏蔽或阻止的,它们的默认动作总是会被执行的。 Ctrl+C和Ctrl+Z都是中断命令,但是它们的作用却不一样。 Ctrl+C是强制中断程序的执行; Ctrl+Z的作用是将任务中断,但是此任务并没有结束,它仍然以进程形式存在于系统中,它只是维持挂起的状态。用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行。 例如: 当你vim一个文件是,如果需要用shell执行别的操作命令,但是你又不打算关闭vim,因为你得存盘退出,你可以简单的按下ctrl+z,shell会将vim进程挂起。当结束了别的shell操作之后,你可以用fg命令继续vim你的文件。
Ctrl+C:送SIGINT信号,默认进程会结束,但是进程自己可以重定义收到这个信号的行为。 Ctrl+Z:送SIGSTOP信号,进程只是被停止,再送SIGCONT信号,进程继续运行。 Ctrl+D:不是发送信号,而是表示一个特殊的二进制值,表示 EOF。 有些信号不能被屏蔽,比如中断,还应该有杀死进程的信号,要不然内核怎么做操作系统中的老大。实际上,SIGKILL和SIGSTOP信号是不能被屏蔽或阻止的,它们的默认动作总是会被执行的。 Ctrl+C和Ctrl+Z都是中断命令,但是它们的作用却不一样。 Ctrl+C是强制中断程序的执行; Ctrl+Z的作用是将任务中断,但是此任务并没有结束,它仍然以进程形式存在于系统中,它只是维持挂起的状态。用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行。 例如: 当你vim一个文件是,如果需要用shell执行别的操作命令,但是你又不打算关闭vim,因为你得存盘退出,你可以简单的按下ctrl+z,shell会将vim进程挂起。当结束了别的shell操作之后,你可以用fg命令继续vim你的文件。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/45501241 一、安装MySQL服务器和客户端 执行以下命令: sudo apt-get install mysql-server-5.7 mysql-client-5.7 sudo apt-get install libmysqlclient-dev libmysqld-dev 二、Python安装MySQLdb库 执行以下命令: sudo apt-get install python-pip sudo apt-get install python-dev sudo pip install mysql-python 验证方法: 进入Python命令行界面: import MySQLdb 未报错即表示安装成功 P.S. 可使用 “apt-cache search 包名” 来查询安装包全名。 关于远程登录MySQL的一些问题,可参考 http://blog.csdn.net/zhaobryant/article/details/45192887
一、安装MySQL服务器和客户端 执行以下命令: sudo apt-get install mysql-server-5.6 mysql-client-5.6 sudo apt-get install libmysqlclient-dev libmysqld-dev 二、Python安装MySQLdb库 执行以下命令: sudo apt-get install python-pip sudo apt-get install python-dev sudo pip install mysql-python 验证方法: 进入Python命令行界面: import MySQLdb 未报错即表示安装成功 P.S. 可使用 “apt-cache search 包名” 来查询安装包全名。 关于远程登录MySQL的一些问题,可参考 http://blog.csdn.net/zhaobryant/article/details/45192887
1. 设置ubuntu14.04启动到字符界面 修改Grub配置文件: /etc/default/grub > 将配置语句 > GRUB_CMDLINE_LINUX_DEFAULT = "quiet splash" > 修改为 > GRUB_CMDLINE_LINUX_DEFAULT = "quiet splash text" 运行Shell命令: > sudo update-grub 运行Shell命令: > sudo shutdown -r 0 > 重启系统 2. 设置ubuntu14.04启动到图形界面 将上述配置修改的text删除即可,同时运行后面两条Shell命令。
1、设置ubuntu14.04启动到字符界面: 修改Grub配置文件: /etc/default/grub 将配置语句 GRUB_CMDLINE_LINUX_DEFAULT = “quiet splash” 修改为 GRUB_CMDLINE_LINUX_DEFAULT = “quiet splash text” 运行Shell命令: sudo update-grub 运行Shell命令: sudo shutdown -r 0 重启系统 2、设置ubuntu14.04启动到图形界面: 将上述配置修改的text删除即可,同时运行后面两条Shell命令。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/45246349 问题 当我运行mininet时,出现以下问题: ***Creating network ***Adding controller ***Adding hosts: h1 h2 h3 h4 h5 ***Adding switches: s1 ***Adding links: (h1, s1) (h2, s1) (h3, s1) (h4, s1) (h5, s1) ***Configuring hosts h1 h2 h3 h4 h5 ***Starting controller Cannot find required executable controller. Please make sure that it is installed and available in your $PATH: (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin) 解决 运行以下命令: sudo ln /usr/bin/ovs-controller /usr/bin/controller 即可解决。
问题 当我运行mininet时,出现以下问题: ***Creating network ***Adding controller ***Adding hosts: h1 h2 h3 h4 h5 ***Adding switches: s1 ***Adding links: (h1, s1) (h2, s1) (h3, s1) (h4, s1) (h5, s1) ***Configuring hosts h1 h2 h3 h4 h5 ***Starting controller Cannot find required executable controller. Please make sure that it is installed and available in your $PATH: (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin) 解决 运行以下命令: sudo ln /usr/bin/ovs-controller /usr/bin/controller 即可解决。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/45192887 问题描述 出于兴趣,本人在Windows和Ubuntu系统上均安装了MySQL服务器和客户端。现在有这样一些预备信息: Windows系统的IP地址: 192.168.5.196 Ubuntu系统的IP地址:192.168.5.228 现在我试图在Windows上远程连接Ubuntu终端主机。 首先,在远程Ubuntu主机上配置MySQL访问权限,具体过程如下: mysql> grant all privileges on . to ‘longlong’@’%’ identified by ‘123456’; 现在,在Ubuntu主机上查询MySQL权限信息如下: 现在,在Windows上远程登录Ubuntu主机MySQL数据库,结果为: 一直报出这种错误! 问题解决方法 在CSDN ASK提出这个问题后,尝试了各位么么哒网友的建议,比如通过mysql -u xx -p xxxx -h xxxx或是关闭Windows防火墙以及通过ufw disable命令关闭Ubuntu防火墙,仍然无法解决这个问题。 后来,得知Ubuntu上MySQL通过/etc/mysql/my.cnf配置文件配置整个MySQL。摘录一二: 41 [mysqld] 42 port = 3306 43 basedir = /usr 44 datadir = /var/lib/mysql 45 tmpdir = /tmp 46 #Instead of skip-networking the default is now to listen only 47 #on localhost which is more compatible and is not less secure. 48 bind-address = 127.0.0.1 注意,在上述配置文件中,存在一句配置命令: bind-address = 127.0.0.1 这句命令即表明,本Ubuntu主机在本地监听,仅处理本地的连接请求。 我们也可以通过netstat命令来查看相关端口监听信息: shell> netstat -anp | grep 3306 output> tcp 127.0.0.1:3306 LISTEN 现在,我们将my.cnf的bind-address语句前添#号注释掉,并重启MySQL服务: shell> sudo service mysql restart output> mysql stop/waiting mysql start/running, process 25765 现在,我们在Windows远程连接Ubuntu上的MySQL数据库,就不会报错了。 再次运行netstat命令: shell> netstat -anp | grep 3306 output> tcp 0.0.0.0:3306 LISTEN 这时,我们发现3306端口不再仅仅监听在本地,同时也可以监听来自远端的连接请求。
问题描述 出于兴趣,本人在Windows和Ubuntu系统上均安装了MySQL服务器和客户端。现在有这样一些预备信息: Windows系统的IP地址: 192.168.5.196 Ubuntu系统的IP地址:192.168.5.228 现在我试图在Windows上远程连接Ubuntu终端主机。 首先,在远程Ubuntu主机上配置MySQL访问权限,具体过程如下: mysql> grant all privileges on . to ‘longlong’@’%’ identified by ‘123456’; 现在,在Ubuntu主机上查询MySQL权限信息如下: 现在,在Windows上远程登录Ubuntu主机MySQL数据库,结果为: 一直报出这种错误! 问题解决方法 在CSDN ASK提出这个问题后,尝试了各位么么哒网友的建议,比如通过mysql -u xx -p xxxx -h xxxx或是关闭Windows防火墙以及通过 ufw disable 命令关闭Ubuntu防火墙,仍然无法解决这个问题。 后来,得知Ubuntu上MySQL通过/etc/mysql/my.cnf配置文件配置整个MySQL。摘录一二: 41 [mysqld] 42 port = 3306 43 basedir = /usr 44 datadir = /var/lib/mysql 45 tmpdir = /tmp 46 #Instead of skip-networking the default is now to listen only 47 #on localhost which is more compatible and is not less secure. 48 bind-address = 127.0.0.1 注意,在上述配置文件中,存在一句配置命令: bind-address = 127.0.0.1 这句命令即表明,本Ubuntu主机在本地监听,仅处理本地的连接请求。 我们也可以通过netstat命令来查看相关端口监听信息: shell> netstat -anp | grep 3306 output> tcp 127.0.0.1:3306 LISTEN 现在,我们将my.cnf的bind-address语句前添#号注释掉,并重启MySQL服务: shell> sudo service mysql restart output> mysql stop/waiting mysql start/running, process 25765 现在,我们在Windows远程连接Ubuntu上的MySQL数据库,就不会报错了。 再次运行netstat命令: shell> netstat -anp | grep 3306 output> tcp 0.0.0.0:3306 LISTEN 这时,我们发现3306端口不再仅仅监听在本地,同时也可以监听来自远端的连接请求。
发现问题 今天,在阅读Linux内核中关于socket的源代码时,遇到了下面一段代码: struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *addr, int *sockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, int optlen); int (*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); int (*sendmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len); int (*recvmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len, int flags); int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma); ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags); }; 在这段代码中,我们注意到proto_ops结构体的成员包括下面这样的成员变量: int (*release) (struct socket *sock); 这边是函数指针作为结构体成员变量的使用方法。 问题分析 首先,我们对C和C++中结构体以及C++类的区别进行一些说明: C中的结构体和C++中结构体的不同之处: 在C中的结构体只能自定义数据类型,结构体中不允许有函数; 而C++中的结构体可以加入成员函数。 C++中的结构体和类的异同: 相同之处: 结构体中可以包含函数;也可以定义public、private、protected数据成员;定义了结构体之后,可以用结构体名来创建对象。但C中的结构体不允许有函数;也就是说在C++当中,结构体中可以有成员变量,可以有成员函数,可以从别的类继承,也可以被别的类继承,可以有虚函数。 不同之处: 结构体定义中默认情况下的成员是public,而类定义中的默认情况下的成员是private的。类中的非static成员函数有this指针,(struct中没有是错误的,一直被误导啊,经过测试struct的成员函数一样具有this指针),类的关键字class能作为template模板的关键字,而struct不可以。 实际上,C中的结构体只涉及到数据结构,而不涉及到算法,也就是说在C中数据结构和算法是分离的,而到C++中一类或者一个结构体可以包含函数(这个函数在C++我们通常中称为成员函数),C++中的结构体和类体现了数据结构和算法的结合。 因此,我们在阅读纯C代码时,应该注意代码中使用函数指针成员变量来等效地实现成员函数过程。 示例代码 这里,我们使用一段代码来对函数指针成员进行相关说明: #include <stdio.h> #include <stdlib.h> int func1(int n) { printf("func1: %d\n", n); return n; } int func2(int n) { printf("func2: %d\n", n); return n; } int main() { int (*a[2])(int); a[0] = func1; a[1] = func2; a[0](1); a[1](2); return 0; } 我们注意上面代码中的 int (*a[2])(int); 在这句代码中,我们定义了这样一个数组: 数组保存指针,什么样的指针呢? 形如 int func(int input) 的 func函数指针,形参为int变量,返回int变量。 因此,数组保存的是形参为单一int变量和返回值为int值得函数指针。 现在,我们定义了这样一个数组,然后 a[0] = func1; a[1] = func2; 由于我们在main函数前声明和定义了func1和func2两个函数(这两个函数满足前面所提及的函数条件),这时,我们便可以使用这两个函数指针赋值函数指针数组。 然后,我们便可以使用数组成员来实现函数调用: a[0](1); a[1](2); 最终结果为:
发现问题 问题分析 示例代码 发现问题 今天,在阅读Linux内核中关于socket的源代码时,遇到了下面一段代码: struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *addr, int *sockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt)(struct socket *sock, int level, int optname, char __user *optval, int optlen); int (*getsockopt)(struct socket *sock, int level, int optname, char __user *optval, int __user *optlen); int (*sendmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len); int (*recvmsg) (struct kiocb *iocb, struct socket *sock, struct msghdr *m, size_t total_len, int flags); int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma); ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags); }; 在这段代码中,我们注意到proto_ops结构体的成员包括下面这样的成员变量: int (*release) (struct socket *sock); 这边是函数指针作为结构体成员变量的使用方法。 问题分析 首先,我们对C和C++中结构体以及C++类的区别进行一些说明: C中的结构体和C++中结构体的不同之处: 在C中的结构体只能自定义数据类型,结构体中不允许有函数; 而C++中的结构体可以加入成员函数。 C++中的结构体和类的异同: 相同之处: 结构体中可以包含函数;也可以定义public、private、protected数据成员;定义了结构体之后,可以用结构体名来创建对象。但C中的结构体不允许有函数;也就是说在C++当中,结构体中可以有成员变量,可以有成员函数,可以从别的类继承,也可以被别的类继承,可以有虚函数。 不同之处: 结构体定义中默认情况下的成员是public,而类定义中的默认情况下的成员是private的。类中的非static成员函数有this指针,(struct中没有是错误的,一直被误导啊,经过测试struct的成员函数一样具有this指针),类的关键字class能作为template模板的关键字,而struct不可以。 实际上,C中的结构体只涉及到数据结构,而不涉及到算法,也就是说在C中数据结构和算法是分离的,而到C++中一类或者一个结构体可以包含函数(这个函数在C++我们通常中称为成员函数),C++中的结构体和类体现了数据结构和算法的结合。 因此,我们在阅读纯C代码时,应该注意代码中使用函数指针成员变量来等效地实现成员函数过程。 示例代码 这里,我们使用一段代码来对函数指针成员进行相关说明: #include <stdio.h> #include <stdlib.h> int func1(int n) { printf("func1: %d\n", n); return n; } int func2(int n) { printf("func2: %d\n", n); return n; } int main() { int (*a[2])(int); a[0] = func1; a[1] = func2; a[0](1); a[1](2); return 0; } 我们注意上面代码中的 int (*a[2])(int); 在这句代码中,我们定义了这样一个数组: 数组保存指针,什么样的指针呢? 形如 int func(int input) 的 func函数指针,形参为int变量,返回int变量。 因此,数组保存的是形参为单一int变量和返回值为int值得函数指针。 现在,我们定义了这样一个数组,然后 a[0] = func1; a[1] = func2; 由于我们在main函数前声明和定义了func1和func2两个函数(这两个函数满足前面所提及的函数条件),这时,我们便可以使用这两个函数指针赋值函数指针数组。 然后,我们便可以使用数组成员来实现函数调用: a[0](1); a[1](2); 最终结果为:
问题表现 出现“文件或目录损坏且无法读取”这个问题的原因可能为不正常插拔,表现症状为: 文件或目录损坏且无法读取 磁盘属性为RAW格式,且容量为0 解决方法 DOS下有个磁盘修复的命令——chkdsk,可以用它来修复“主文件索引表”(MFT): 格式为:chkdsk x: /f x : 为损坏的盘符 /f : 参数指修复磁盘错误 更多参数说明及操作说明请运行 chkdsk /? 运行后出现提示 C:\Documents and Settings\Administrator>chkdsk x: /f 文件系统的类型是 NTFS。 CHKDSK 正在校验文件(3 的阶段 1)… 已处理 256 个文件记录。 文件校验完成。 已处理 0 个大型文件记录。 已处理 0 个错误的文件记录。 已处理 0 个 EA 记录。 已处理 0 个重新解析记录。 CHKDSK 正在校验索引(3 的阶段 2)… 完成百分比: 34。(共 917 个索引项,已处理 265) 修正文件 5 中的镜像错误。 已处理 917 个索引项。 索引校验完成。 CHKDSK 正在创建新的根目录。 CHKDSK 正在恢复丢失的文件。 ………… ………… ………… 已处理 33 未被索引的文件。 CHKDSK 正在校验安全描述符(3 的阶段 3)… 已处理 256 个安全描述符。 安全描述符校验完成。 已处理 41 个数据文件。 正在修复主文件表(MFT)镜像的错误。 CHKDSK 发现主文件表(MFT)位图中有标记为 已分配的可用空间。 正在修复卷位图的错误。 Windows 已更正文件系统。 至此大功告成,而且数据没有丢失。希望对大家有用。
问题表现 出现“文件或目录损坏且无法读取”这个问题的原因可能为不正常插拔,表现症状为: 文件或目录损坏且无法读取 磁盘属性为RAW格式,且容量为0 解决方法 DOS下有个磁盘修复的命令——chkdsk,可以用它来修复“主文件索引表”(MFT): 格式为:chkdsk x :/f x : 为损坏的盘符 /f : 参数指修复磁盘错误 更多参数说明及操作说明请运行 chkdsk /? 运行后出现提示 C:\Documents and Settings\Administrator>chkdsk x: /f 文件系统的类型是 NTFS。 CHKDSK 正在校验文件(3 的阶段 1)… 已处理 256 个文件记录。 文件校验完成。 已处理 0 个大型文件记录。 已处理 0 个错误的文件记录。 已处理 0 个 EA 记录。 已处理 0 个重新解析记录。 CHKDSK 正在校验索引(3 的阶段 2)… 完成百分比: 34。(共 917 个索引项,已处理 265) 修正文件 5 中的镜像错误。 已处理 917 个索引项。 索引校验完成。 CHKDSK 正在创建新的根目录。 CHKDSK 正在恢复丢失的文件。 ………… ………… ………… 已处理 33 未被索引的文件。 CHKDSK 正在校验安全描述符(3 的阶段 3)… 已处理 256 个安全描述符。 安全描述符校验完成。 已处理 41 个数据文件。 正在修复主文件表(MFT)镜像的错误。 CHKDSK 发现主文件表(MFT)位图中有标记为 已分配的可用空间。 正在修复卷位图的错误。 Windows 已更正文件系统。 至此大功告成,而且数据没有丢失。希望对大家有用。
Linux 上用来实现数据的图形可视化的应用程序有很多,从简单的 2-D 绘图到 3-D 制图,再到科学图形编程和图形模拟。幸运的是,这方面的工具有很多开放源码实现,包括 gnuplot、GNU Octave、Scilab、MayaVi、Maxima 等。每个工具都有自己的优缺点,并且都是针对不同的应用程序而设计的。对这些开放源码图形可视化工具进行一下探索,有助于我们更好地决定哪个工具最适合我们的应用程序。 内容 Gnuplot GNU Octave Scilab MayaVi Maxima 展望 内容 在本文中,将对很多流行的 Linux 数据可视化工具进行调查,并对其中一些工具进行更深入的探讨。例如,某个工具是否为进行数值计算而提供了一种语言?这个工具是交互式的还是提供了一种批处理模式来单独进行操作?可以使用这个工具进行图像或数字信号处理吗?这个工具是否提供了语言绑定来支持与用户应用程序的集成(例如 Python、Tcl、Java 编程语言等)?另外还将展示一些工具的图形化处理能力。最后,将分析每个工具的长处,从而帮助我们确定哪个工具最适合完成计算任务或数据可视化。 在本文中所探索的开放源码工具包括(同时还给出了每个工具所使用的许可证): * Gnuplot(GPL) * GNU Octave(GPL) * Scilab(Scilab) * MayaVi(BSD) * Maxima(GPL) Gnuplot Gnuplot 是一个非常好的可视化工具,它从 1986 年开始就存在了。如果没有 gnuplot 的图,就很难阅读论文。尽管 gnuplot 是命令行驱动的,但是它也在不断发展,现在也可以支持很多非交互式的应用程序了,例如它可以作为一个 GNU Octave 的绘图引擎使用。 Gnuplot 具有很好的可移植性,可以在 UNIX®、Microsoft® Windows®、 Mac OS® X 和很多其他平台上运行。它可以支持从 postscript 到新近的 PNG 等极为广泛的输出格式。 Gnuplot 可以以批处理模式进行操作,提供了一个命令脚本来生成一个图形,也是以非交互式模式来运行的,这让我们可以尝试一下它的特性来了解它们绘图的效果。 在 Gnuplot 中有一个对应于 UNIX 的数学库的标准的数学库可以使用。函数的参数支持整型、实型和复型。可以将数学库配置成弧度或角度(默认为弧度)。 为了进行绘图,Gnuplot 可以使用 plot 命令生成 2-D 图形,或使用 splot 命令生成 3-D 图形(作为 2-D projection)。使用 plot 命令,gnuplot 可以在直角二维坐标系中进行操作。splot 命令默认使用的是笛卡儿坐标系,不过也可以支持球面或柱面坐标系。也可以在图形中绘制等高线(如下面的图 1 所示)。有一种新风格的绘图 pm3d 可以支持绘制使用调色板进行映射的 3-D 和 4D 数据作为地图或地表图来使用。 下面是一个简单的 gnuplot 例子,它给出了一个具有等高线和隐线消除的 3-D 图形。清单 1 给出了所使用的 gnuplot 命令,图 1 给出了所生成的图形结果。 清单 1. 简单的 gnuplot 函数图 set samples 25 set isosamples 26 set title "Test 3D gnuplot" set contour base set hidden3d offset 1 splot [-12:12.01] [-12:12.01] sin(sqrt(x**2+y**2))/sqrt(x**2+y**2) 清单 1 充分显示了 gnuplot 的命令集是多么简单。采样速度和绘图密度是由 samples 和 isosamples 决定的,标题是由 title 参数为图形提供的。同时还启用了基本的等高线和隐线消除特性,最终的绘图是利用 splot 命令使用数学库内部的函数来创建的。结果如图 1 所示。 图 1. gnuplot 的一个简单绘图 除了创建函数图之外,gnuplot 还可以很好地对文件中包含的图形进行绘图。考虑如清单 2 所示的 x/y 数据对(这个文件的一个简短版本)。这个文件中给出的数据对表示一个两维空间中的 x 和 y 轴的数据。 清单 2. gnuplot 的示例数据文件(data.dat) 88 99 79 98 76 89 60 85 ... 60 22 如果希望在两维空间中绘制这些数据,并将每个数据点使用一条线连接起来,就可以使用清单 3 给出的 gnuplot 脚本。 清单 3. 对清单 2 中的数据进行绘图所使用的 Gnuplot 脚本 set title "Sample data plot" plot 'data.dat' using 1:2 t 'data points', \ "data.dat" using 1:2 t "lines" with lines 结果如图 2 所示。注意 gnuplot 自动给出了轴的刻度,但是如果需要标注图形的位置,就可以对其进行控制。 图 2. 在 gnuplot 中使用数据文件进行简单的绘图 Gnuplot 是一个很好的可视化工具,它非常出名,是很多 GNU/Linux 发行版的一部分。然而,如果希望进行基本的数据可视化和数值计算,那么 GNU Octave 可能是我们希望寻找的工具。 GNU Octave GNU Octave 是一种高级语言,主要设计用来进行数值计算,它是 MathWorks 出品的 Matlab 商业软件的一个强有力的竞争产品。除了 gnuplot 所提供的简单命令集之外,Octave 还为进行数学编程提供了一种丰富的语言。我们甚至可以使用 C 或 C++ 语言编写自己的应用程序,然后与 Octave 进行交互。 Octave 最初是在 1992 年作为化学反应堆设计教科书的一个辅助软件而编写的。其作者希望能够帮助学生解决反应堆的设计问题,而不用调试 Fortran 程序。结果获得了一种非常有用的语言,并为解决数值问题提供了交互式环境。 Octave 可以以一种脚本化模式非交互地进行操作,或者通过 C 和 C++ 语言绑定进行操作。Octave 本身就有一种非常丰富的语言,该语言看起来与 C 语言非常类似,并有一个很大的数学库,包括信号和图像处理、音频处理以及控制理论所使用的一些特殊函数。 由于 Octave 使用了 gnuplot 作为其后端实现,因此使用 gnuplot 可以绘制的所有东西都可以使用 Octave 进行绘制。Octave 的确有一种更丰富的语言来进行计算,它有很多明显的优点,但是仍然有 gnuplot 的一些限制。 在下面这个 Octave-Forge Web 站点上提供的例子中(SimpleExamples),绘制了一个 Lorentz Strange Attractor。清单 4 给出了在使用 Cygwin 的 Windows 平台上 Octave 所使用的交互式对话框。这个例子展示了 lsode 的用法,这是一个常见的微分方程解算器。 清单 4. 使用 Octave 呈现 Lorentz Strange Attractor GNU Octave, version 2.1.50 Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003 John W. Eaton. This is free software; see the source code for copying conditions. There is ABSOLUTELY NO WARRANTY; not even for MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, type `warranty'. Please contribute if you find this software useful. For more information, visit http://www.octave.org/help-wanted.html Report bugs to <bug-octave&bevo.che.wisc.edu>. >> function y = lorenz( x, t ) y = [10 * (x(2) - x(1)); x(1) * (28 - x(3)); x(1) * x(2) - 8/3 * x(3)]; endfunction >> x = lsode("lorenz", [3;15;1], (0:0.01:25)'); >> gset parametric >> gsplot x >> 图 3 给出的图是清单 4 中 Octave 代码的输出结果。 图 3. 使用 Octave 绘制的 Lorentz 图 GNU Octave(与 gnuplot 一致)可以使用 multiplot 特性在一个页面上呈现多个图形。使用这个特性,就可以定义要创建多少图形,然后使用 subwindow 命令来定制特定的图形。在定义好子窗口之后,就可以正常地生成自己的图形,然后再跳到下一个子窗口中(如清单 5 所示)。 清单 5. 在 Octave 中生成多个图形 >> multiplot(2,2) >> subwindow(1,1) >> t=0:0.1:6.0 >> plot(t, cos(t)) >> subwindow(1,2) >> plot(t, sin(t)) >> subwindow(2,1) >> plot(t, tan(t)) >> subwindow(2,2) >> plot(t, tanh(t)) 所生成的多图页面如图 4 所示。这是将相关图形搜集在一起进行比较和对比的一种很好的特性。 图 4. 使用 GNU Octave 绘制多图 我们可以认为 Octave 是一种使用 gnuplot 作为后台实现来进行可视化的高级语言。它提供了丰富的数学库,是 Matlab 的一个很好的免费替换产品。可以很容易利用用户开发的用来进行语音处理、优化、符号计算等的包对它进行扩展。Octave 在某些 GNU/Linux 的发行版中都有,例如 Debian,也可以在使用 Cygwin 的 Windows 和 Mac OS X 上使用。 Scilab Scilab 在启用数值计算和可视化方面都与 GNU Octave 非常类似。 Scilab 是世界上广泛存在的工程和科学应用程序所使用的一种解释器和高级语言。 Scilab 诞生于 1994 年,它是由法国的 INRIA(Institut national de recherche en informatique et en automatique)和 ENPC(École Nationale des Ponts et Chaussées)设计的。从 2003 年开始 Scilab 开始由 Scilab Consortium 进行维护。 Scilab 包括一个很大的数学函数库,可以利用使用 C 和 Fortran 之类的高级语言编写的程序进行扩充。它还有重载数据类型和操作的能力。它包括一个集成的高级语言,不过这种语言与 C 语言稍微有些区别。 Scilab 中有很多工具包提供了 2-D 和 3-D 的图形动画、优化、统计、图标和网络、信号处理、混合动态系统模拟和仿真以及其他许多由社区所贡献的功能。 在大部分 UNIX 系统上都可以使用 Scilab,在较新的 Windows 操作系统上也可以使用。与 GNU Octave 一样, Scilab 也有很好的文档。由于它是一个欧洲的项目,因此还可以找到很多使用除英语之外的其他语言所编写的文档和文章。 在启动 Scilab 之后,就会显示一个窗口让我们可以与之进行交互(如图 5 所示)。 图 5. 与 Scilab 进行交互 在这个例子中,先是创建了一个向量(t),其值的范围从 0 到 2PI(步进大小为 0.2)。然后生成了一个 3-D 图形(使用 z=f(x,y), 或者说是 xi,yi 点处的一个表面)。图 6 给出了所生成的图形。 图 6. 图 5 中的命令所生成的 Scilab 图 Scilab 中包括很多库和函数,它们可以使用最少的复杂性来绘制图形。下面是一个生成简单三维柱状图的例子: -->hist3d(5*(rand(5,5)); 首先,rand(5,5) 会构建一个 5,5 大小的矩阵,其中包含了一些随机值(其最大值为 5)。这个矩阵被传递给函数 hist3d。所生成的柱状图如图 7 所示。 图 7. 生成随机的三维柱状图 Scilab 和 Octave 非常类似。它们都具有很大的社区参与基础。Scilab 是使用 Fortran 77 编写的,而 Octave 则是使用 C++ 编写的。Octave 使用 gnuplot 来实现可视化;Scilab 则提供了自己的库。如果非常熟悉 Matlab,那么 Octave 就是一个很好的选择,因为它努力实现了与 Matlab 的兼容性。Scilab 包括了很多数学函数,因此非常适合进行信号处理。如果仍然不确定要使用哪个工具,可以全部尝试一下。它们都是很好的工具,可以使用它们来完成不同的任务。 MayaVi MayaVi 在梵语中的意思是魔术师,它是一种数据可视化工具,绑定了具有强大可视化工具包(VTK)的 Python 来进行图形化显示。MayaVi 还提供了一个使用 Tkinter 模块开发的图形用户界面(GUI)。Tkinter 是一个 Tk 界面,通常都与 Tcl 一起使用。 MayaVi 最初是为 Computational Fluid Dynamics(CFD)作为一个可视化工具而开发的。当人们认识到它在其他领域中的效用之后,它就作为一种通用科学数据可视化工具重新进行了设计。 MayaVi 背后的魔力来自于 VTK。VTK 是一个用来进行数据可视化和图像处理的开放源码系统,它在科学社区中被广泛地使用。VTK 通过为 Tcl/Tk、Java 编程语言以及 Python 加上 C++ 库所提供的脚本化接口而封装了很多功能。VTK 在很多操作系统上都是可移植的,包括 UNIX、Windows 和 MAC OS X。 围绕 VTK 的 MayaVi 外壳可以作为一个 Python 模块从其他 Python 程序中导入,并通过 Python 解释器来编写脚本。MayaVi 所提供的 tkinter GUI 允许进行过滤器的配置和应用,以及在可视化基础上渲染一些灯光效果。 图 8 是在 Windows 平台上使用 MayaVi 进行可视化的一个例子。 图 8. 使用 MayaVi/VTK 显示的 3-D 可视化(心脏 CT 扫描图) MayaVi 是在 Python 脚本语言中扩展 VTK 的一个有趣例子。 Maxima Maxima 是一个符号和数值计算程序,它是 Octave 和 Scilab 的血液。Maxima 最初的开发团队开始于 19 世纪 70 年代的 MIT(麻声理工学院),现在还继续在维护。最初的版本(一个计算机代数系统)名为 DOE Macsyma,它开辟了后来开发的更知名的应用程序(例如 Mathematica)的道路。 Maxima 提供了一组所期望的功能集(例如微积分、解析线性系统和非线性等式集)以及符号计算能力。在 Maxima 中还可以找到 Lisp 的一些线索(从引用之类的函数、map 和 apply 中)。Maxima 是使用 Lisp 编写的,可以在 Maxima 会话中执行 Lisp 代码。 Maxima 具有很好的在线帮助系统,它是基于超文本的。例如,如果希望了解某个特定的 Maxima 函数是如何工作的,那么就可以简单地输入 example( desolve ),然后它会提供很多样例用法。 Maxima 还有一些有趣的特性,例如规则和模式。这些规则和模式都是由用来简化表达式的程序所使用的。规则可以用于交换和非交换代数。 Maxima 与 Octave 和 Scilab 非常类似:其中解释器都可以用来与用户进行交互,结果都会直接在同一个窗口中提供,或者也可以在另外一个窗口中进行显示。在图 9 中,请求绘制一个简单的 3-D 图形。 图 9. 与 Maxima 进行交互 所生成的图形如图 10 所示。 图 10. 图 9 的命令所生成的 Maxima 图形 展望 在本文中介绍了几个开放源码 GNU/Linux 可视化工具。其他有用的工具还包括 Gri、PGPLOT、SciGraphica、plotutils、NCAR Graphics 和 ImLib3D。这些都是开放源码的,也就是说您可以看到它们是如何工作的;如果愿意,也可以对它们进行修改。另外,如果您正在寻找一个很好的图形仿真环境,那就请查看一下与 OpenGL 一起使用的 Open Dynamics Engine(ODE)。 具体的需要决定了哪种工具最适合使用。如果希望使用一个具有很多可视化算法的强大的可视化系统,那么 MayaVi 就是所要寻找的工具。对于具有可视化功能的数值计算来说,GNU Octave 和 Scilab 都非常适合。如果需要符号计算能力,那么 Maxima 就是一个非常好的选择。最后(但并非不重要),如果所需要的只是一些基本的绘图功能,那么 gnuplot 就可以很好地满足这一需求。 PS. 本文章由CSDN-Markdown编辑器写成,感觉十分好用!
CSDN-Markdown编辑器使用简介 本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦: Markdown和扩展Markdown简洁的语法 代码块高亮 图片链接和图片上传 LaTex数学公式 UML序列图和流程图 离线写博客 导入导出Markdown文件 丰富的快捷键 快捷键 加粗 Ctrl + B 斜体 Ctrl + I 引用 Ctrl + Q 插入链接 Ctrl + L 插入代码 Ctrl + K 插入图片 Ctrl + G 提升标题 Ctrl + H 有序列表 Ctrl + O 无序列表 Ctrl + U 横线 Ctrl + R 撤销 Ctrl + Z 重做 Ctrl + Y Markdown及扩展 Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— [ 维基百科 ] 使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接等,详细语法参考帮助?。 本编辑器支持 Markdown Extra , 扩展了很多好用的功能。具体请参考Github. 表格 Markdown Extra 表格语法: 项目 价格 Computer $1600 Phone $12 Pipe $1 可以使用冒号来定义对齐方式: 项目 价格 数量 Computer 1600 元 5 Phone 12 元 12 Pipe 1 元 234 定义列表 Markdown Extra 定义列表语法: 项目1 项目2 定义 A 定义 B 项目3 定义 C 定义 D 定义D内容 代码块 代码块语法遵循标准markdown代码,例如: @requires_authorization def somefunc(param1='', param2=0): '''A docstring''' if param1 > param2: # interesting print 'Greater' return (param2 - param1 + 1) or None class SomeClass: pass >>> message = '''interpreter ... prompt''' 脚注 生成一个脚注1. 目录 用 [TOC]来生成目录: CSDN-Markdown编辑器使用简介 快捷键 Markdown及扩展 表格 定义列表 代码块 脚注 目录 数学公式 UML 图 离线写博客 浏览器兼容 数学公式 使用MathJax渲染LaTex 数学公式,详见math.stackexchange.com. 行内公式,数学公式为:Γ(n)=(n−1)!∀n∈N。 块级公式: x=−b±b2−4ac−−−−−−−√2a 更多LaTex语法请参考 这儿. UML 图: 可以渲染序列图: Created with Raphaël 2.1.2张三张三李四李四嘿,小四儿, 写博客了没?李四愣了一下,说:忙得吐血,哪有时间写。 或者流程图: Created with Raphaël 2.1.2开始我的操作确认?结束yesno 关于 序列图 语法,参考 这儿, 关于 流程图 语法,参考 这儿. 离线写博客 即使用户在没有网络的情况下,也可以通过本编辑器离线写博客(直接在曾经使用过的浏览器中输入write.blog.csdn.net/mdeditor即可。Markdown编辑器使用浏览器离线存储将内容保存在本地。 用户写博客的过程中,内容实时保存在浏览器缓存中,在用户关闭浏览器或者其它异常情况下,内容不会丢失。用户再次打开浏览器时,会显示上次用户正在编辑的没有发表的内容。 博客发表后,本地缓存将被删除。 用户可以选择 把正在写的博客保存到服务器草稿箱,即使换浏览器或者清除缓存,内容也不会丢失。 注意:虽然浏览器存储大部分时候都比较可靠,但为了您的数据安全,在联网后,请务必及时发表或者保存到服务器草稿箱。 浏览器兼容 目前,本编辑器对Chrome浏览器支持最为完整。建议大家使用较新版本的Chrome。 IE9以下不支持 IE9,10,11存在以下问题 不支持离线功能 IE9不支持文件导入导出 IE10不支持拖拽文件导入 这里是 脚注 的 内容. ↩
QSqlTableModel类继承至QSqlQueryModel类,该类提供了一个可读写单张SQL表的可编辑数据模型,功能:修改,插入,删除,查询和排序。 常用函数 //获取水平头或垂直头标题 QVariant headerData ( intsection,Qt::Orientationorientation, introle= Qt::DisplayRole ) const //设置水平头或垂直头标题 bool setHeaderData ( intsection,Qt::Orientationorientation, constQVariant&value, introle= Qt::EditRole ) //返回行数 int rowCount ( constQModelIndex&parent= QModelIndex() ) const //返回列数 int columnCount ( constQModelIndex&index= QModelIndex() ) const //model->removeColumns (0)删除第一列 virtual bool removeColumns ( int column, int count, const QModelIndex & parent = QModelIndex() ) //提交所有被修改的数据,然后修改的数据被保存在数据库中 bool QSqlTableModel::submitAll () //撤销所有的修改,如果数据库已经被提交了修改,就不能通过撤销修改改回来了 void QSqlTableModel::revertAll () //恢复指定行的改变 virtual void revertRow ( int row ) //筛选,按照字符串filter对数据库进行筛选,相当于SQL中的WHERE语句 void QSqlTableModel::setFilter ( const QString & filter ) //在筛选和排序的条件下,将数据库中符合要求的在mode表格中显示出来 bool QSqlTableModel::select () //排序操作。按照列和Qt::SortOrder排序。Qt::SortOrder有升序和降序 void QSqlTableModel::setSort ( int column, Qt::SortOrder order ) //插入行 bool insertRow ( int row, const QModelIndex & parent = QModelIndex() ) // 插入列 bool insertColumn ( intcolumn, constQModelIndex&parent= QModelIndex() ) //设置保存策略为手动提交 model->setEditStrategy(QSqlTableModel::OnManualSubmit); 一、在QTableView中显示数据库中表的数据 QSqlTableModel *model = new QSqlTableModel(parentObject, database); model->setTable("employee"); model->setEditStrategy(QSqlTableModel::OnManualSubmit); model->select(); model->removeColumn(0); // don't show the ID model->setHeaderData(0, Qt::Horizontal, tr("Name")); model->setHeaderData(1, Qt::Horizontal, tr("Salary")); QTableView *view = new QTableView; view->setModel(model); view->show(); 二、修改QTableView中数据后的提交,加入事务处理 model->database().transaction(); //开始事务操作 if (model->submitAll()) // 提交所有被修改的数据到数据库中 { model->database().commit(); //提交成功,事务将真正修改数据库数据 } else { model->database().rollback(); //提交失败,事务回滚 QMessageBox::warning(this, tr(“tableModel”),tr(“数据库错误: %1″).arg(model->lastError().text())); } model->revertAll(); //撤销修改 三、查询操作 相当于SQL语句:SELECT * FROM 表名 WHERE name = "name变量" model->setFilter(QObject::tr(“name = ‘%1′”).arg(name)); //根据姓名进行筛选 model->select(); //显示结果 for (int i = 0; i < model.rowCount(); ++i) { QString name = model.record(i).value("name").toString(); // ... 在此处理每一条的记录 } // 在操作大数据集时,建议通过索引指定字段 int primaryKeyIndex = model.record().indexOf("id"); for (int i = 0; i < model.rowCount(); ++i) { QSqlRecord record = model.record(i); QString name = record.value("name").toString(); // ... 在此处理每一条的记录 } 四、排序操作 model->setSort(0,Qt::AscendingOrder); //id属性,即第0列,升序排列,Qt::DescendingOrder为降序排序 model->select(); 五、插入操作 int rowNum = model->rowCount(); //获得表的行数 int id = 最后一个ID+1; model->insertRow(rowNum); //添加一行,或者用insertRows(0,1),在0行添加1条记录,根据表的排序规则,可能移到与指定行不同的行位置上 model->setData(model->index(rowNum,0),id); //因为这里设置了ID为主键,所以必须给新行添加id属性值,id字段在第0列上 model->submitAll(); //可以直接提交 六、删除一条记录 首先要定位到待删除的行上 model.setFilter("id = 10"); model.select(); if (model.rowCount() == 1) { model.removeRows(0,1) // 如果要删除所有满足条件的记录则把1改成model.rowCount() model.submitAll(); } 在QTableView中删除选中的一行 int curRow = tableView->currentIndex().row(); model->removeRow(curRow); //删除一行 在QTableView中删除选中的多行 QAbstractItemView::SelectionModeselectionMode() const // 原型 QModelIndexListQItemSelectionModel::selectedIndexes() const //原型 QItemSelectionModel *selections = tableView->selectionModel(); //返回当前的选择模式 QModelIndexList selecteds = selections->selectedIndexes(); //返回所有选定的模型项目索引列表 foreach (QModelIndex index, selecteds) { int curRow = index.row(); //删除所有被选中的行 model->removeRow(curRow); } int ok = QMessageBox::warning(this,tr("删除选中的行!"),tr("你确定删除当前选取中的行吗?"),QMessageBox::Yes,QMessageBox::No); if(ok == QMessageBox::Yes) { model->submitAll(); //提交,在数据库中删除该行 } else { model->revertAll(); //如果不删除,则撤销 } 七、更新记录 必须先定位记录 model.setFilter("id = 10"); model.select(); if (model.rowCount() == 1) { model.setData(model.index(0,1),QObject::tr("小王")); model.submitAll(); } 可以看到这个模型很强大,而且完全脱离了SQL语句,就算你不怎么懂数据库,也可以利用它进行大部分常用的操作。这个模型提供了缓冲区,可以将所有修改先保存到model中,只有当我们执行提交修改后,才会真正写入数据库。当然这也是因为我们在最开始设置了它的保存策略: model->setEditStrategy(QSqlTableModel::OnManualSubmit); OnManualSubmit表明我们要提交修改才能使其生效。可以先将修改保存起来,当我们执行提交函数时,再去真正地修改数据库。当然,这个模型比前面的模型更高级,前面讲的所有操作,在这里都能执行。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/42971003 在Qt 5.3中使用数据库连接时,弹出下面的错误: QSqlDatabase: QMYSQL driver not loaded QSqlDatabase: available drivers: QSQLITE QMYSQL QMYSQL3 QODBC QODBC3 QPSQL QPSQL7 从上面的错误可以看出,错误发生在MySQL数据库驱动并未加载。 对于这种错误一般有两种解决方案: 第一种:无MySQL驱动。 在这种情况下,检查 Qt\5.3\msvc2013_64_opengl\plugins 目录下是否有qsqlmysql.dll,如果没有,就说明Qt没有相应的mysql驱动。这时,在QSqlDatabase: available drivers: QSQLITE QMYSQL QMYSQL3 QODBC QODBC3 QPSQL QPSQL7 报错中没有QMYSQL项。 解决方法是:拷贝qmysql.dll至plugins目录下。如何获取?http://blog.163.com/e_rommel/blog/static/187383045201292422139149/ 或 http://dev.wo.com.cn/bbs/viewthread.jsp?tid=140945&extra=page%3D1 。 第二种:库支持不完善。 解决方法是:将MySQL\MySQL Server 5.7\lib下的libmysql.dll拷贝至Qt\5.3\msvc2013_64_opengl\bin下即可。
在Qt 5中使用数据库连接时,弹出下面的错误: QSqlDatabase: QMYSQL driver not loaded QSqlDatabase: available drivers: QSQLITE QMYSQL QMYSQL3 QODBC QODBC3 QPSQL QPSQL7 从上面的错误可以看出,错误发生在MySQL数据库驱动并未加载。 对于这种错误一般有两种情况: 第一种:无MySQL驱动,那么检查 Qt\5.3\msvc2013_64_opengl\plugins 目录下是否有qsqlmysql.dll,如果没有,就说明Qt没有相应的mysql驱动。这时,在QSqlDatabase: available drivers: QSQLITE QMYSQL QMYSQL3 QODBC QODBC3 QPSQL QPSQL7 报错中没有QMYSQL项。 解决方法是:拷贝qmysql.dll至plugins目录下。如何获取?http://blog.163.com/e_rommel/blog/static/187383045201292422139149/ 或 http://dev.wo.com.cn/bbs/viewthread.jsp?tid=140945&extra=page%3D1 。 第二种:库支持不完善。 解决方法是:将MySQL\MySQL Server 5.7\lib下的libmysql.dll拷贝至Qt\5.3\msvc2013_64_opengl\bin下即可。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/42720629 在本文,我们将对Python中range和xrange进行对比分析。 range 函数说明:range([start,] stop [, step]),其根据start与stop指定的范围以及step设定的步长,生成一个序列。 range示例: >>> range(5) [0, 1, 2, 3, 4] >>> range(1,5) [1, 2, 3, 4] >>> range(0,6,2) [0, 2, 4] xrange 函数说明:xrange([start,] stop [, step]),其用法与range完全相同,不同的是生成的不是一个数组,而是一个生成器。 xrange示例: >>> xrange(5) xrange(5) >>> list(xrange(5)) [0, 1, 2, 3, 4] >>> xrange(1,5) xrange(1, 5) >>> list(xrange(1,5)) [1, 2, 3, 4] >>> xrange(0,6,2) xrange(0, 6, 2) >>> list(xrange(0,6,2)) [0, 2, 4] 结论 由上面的示例可以知道:要生成很大的数字序列的时候,用xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间。 对于range和xrange,它们基本上都是在循环的时候使用: for i in range(0, 100): print i for i in xrange(0, 100): print i 这两个输出的结果都是一样的,实际上有很多不同,range会直接生成一个list对象: a = range(0,100) print type(a) print a print a[0], a[1] 输出结果: <type 'list'> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99] 0 1 对于xrange,则不会直接生成一个list,而是每次调用返回其中的一个值: a = xrange(0,100) print type(a) print a print a[0], a[1] 输出结果: <type 'xrange'> xrange(100) 0 1 因此,xrange做循环的性能比range好,尤其是返回数据量很大的时候。因此,尽量使用xrange吧,除非需要返回一个列表。 下面,我们做这样一个对比实验: from time import time t1 = time() x = 1 for i in range(10000000): x = x+1 print time()-t1 t2 = time() for i in xrange(10000000): x = x+1 print time()-t2 其输出结果为: 1.7990000248 1.48600006104 可见,使用xrange的性能较range有一定程度的提升。
range 函数说明:range([start,] stop [, step]),根据start与stop指定的范围以及step设定的步长,生成一个序列。 range示例: >>> range(5) [0, 1, 2, 3, 4] >>> range(1,5) [1, 2, 3, 4] >>> range(0,6,2) [0, 2, 4] xrange 函数说明:用法与range完全相同,所不同的是生成的不是一个数组,而是一个生成器。 xrange示例: >>> xrange(5) xrange(5) >>> list(xrange(5)) [0, 1, 2, 3, 4] >>> xrange(1,5) xrange(1, 5) >>> list(xrange(1,5)) [1, 2, 3, 4] >>> xrange(0,6,2) xrange(0, 6, 2) >>> list(xrange(0,6,2)) [0, 2, 4] 由上面的示例可以知道:要生成很大的数字序列的时候,用xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间,这两个基本上都是在循环的时候用: for i in range(0, 100): print i for i in xrange(0, 100): print i 这两个输出的结果都是一样的,实际上有很多不同,range会直接生成一个list对象: a = range(0,100) print type(a) print a print a[0], a[1] 输出结果: <type 'list'> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99] 0 1 而xrange则不会直接生成一个list,而是每次调用返回其中的一个值: a = xrange(0,100) print type(a) print a print a[0], a[1] 输出结果: <type 'xrange'> xrange(100) 0 1 所以xrange做循环的性能比range好,尤其是返回很大的时候,尽量用xrange吧,除非你是要返回一个列表。 我们可以做这样一个实验: from time import time t1 = time() x = 1 for i in range(10000000): x = x+1 print time()-t1 t2 = time() for i in xrange(10000000): x = x+1 print time()-t2 其输出结果为: 1.7990000248 1.48600006104 可见,使用xrange的性能较range有一定程度的提升。
字符串提供了一系列的方法去实现复杂的文本处理任务。方法就是与特定的对象关联在一起的函数。方法调用同时进行了两次操作: 第一次:属性读取——具有object.attribute格式的表达式可以理解为“读取object对象的属性attribute的值”; 第二次:函数调用表达式——具有函数(参数)格式的表达式意味着“调用函数代码,传递零或者更多用逗号隔开的参数对象,最后返回函数的返回值”。 方法调用表达式对象,方法(参数)从左至右运行,也就是说Python首先读取对象方法,然后调用它,传递参数。如果一个方法计算出一个结果,它将会作为整个方法调用表达式的结果被返回。 这里,着重介绍这样几个方法:find、join、replace、split。 1、find方法: 使用方法: S.find( sub, [ , start, [ , end]]) 注:对于上面使用格式的说明:[]表示参数可选,未用此[]包含的参数为必须指定的参数。 find方法返回在子字符串出现处的偏移(默认从前向后开始搜索)或者未找到时返回-1。 >>> s = 'xxxxspamxxxxspamxxxx' >>> where = s.find('spam') >>> where 4 现在,如果我们试图去实现这样一个替换操作:将s字符串的spam字符串替换为eggs。 首先,如果仅仅是替换第一个spam,那么可以这样操作: >>> s1 = s[:where] + 'eggs' + s[(where+4):] >>> s1 'xxxxeggsxxxxspamxxxx' >>> 但是假如我们希望完全替换所有的spam为eggs,那么,我们有如下的代码更改: >>> s = 'xxxxspamxxxxspamxxxx' >>> where = s.find('spam') >>> while where != -1 : ... s = s[:where] + 'eggs' + s[(where+4):] ... where = s.find('spam') ... >>> s 'xxxxeggsxxxxeggsxxxx' >>> 这样的效率很低,代码量也很大,因此,我们可以使用replace方法: 2、replace方法: 使用方法:S.replace(old, new, [, maxsplit]) 该方法的第一个参数是原始子字符串(任意长度),替换原始子字符串的字符串(任意长度),之后进行全局搜索并替换。 >>> s = 'xxxxspamxxxxspamxxxx' >>> s.replace('spam', 'eggs') 'xxxxeggsxxxxeggsxxxx' >>> s 'xxxxspamxxxxspamxxxx' >>> 在这里,有几个点需要说明: (1)replace方法每次返回的字符串是一个新的字符串对象。由于字符串是不可变的,因此,每一种方法并不是真正在原处修改了字符串。 (2)我们可以通过控制第三个变量来实现只替换一次的目的: >>> s.replace('spam', 'eggs', 1) 'xxxxeggsxxxxspamxxxx' >>> 3、join方法: 使用方法:S.join(seq) join方法常常将字符串列表合并成一个字符串。 >>> s = 'spamy' >>> l = list(s) >>> l ['s', 'p', 'a', 'm', 'y'] >>> s 'spamy' >>> 如上所示,list可以将字符串分割成单字符列表,但是原字符串并未修改。join负责将列表字符合并成一个字符串。 >>> l ['s', 'p', 'a', 'm', 'y'] >>> s 'spamy' >>> ''.join(l) 'spamy' >>> '+'.join(l) 's+p+a+m+y' >>> t = '***' >>> t.join(l) 's***p***a***m***y' >>> 从这里,我们可以得出这样几个结论: (1)join会将调用该方法的字符串(我们称之为分隔符)插入至合并字符串; >>> '%'.join(['d','s','ld','f']) 'd%s%ld%f' (2)通过上面的join实例,我们可以看出:分隔符的个数始终等于列表元素个数-1。 (3)合并后的字符串会存储在内存中,而不会修改列表对象。 4、split方法: 使用方法:S.split([seq, [, maxsplit]]) split方法分割字符串并将子字符串存储在列表(list)对象中。 >>> x = 'iperf -u -c 192.168.1.1' >>> x.split() ['iperf', '-u', '-c', '192.168.1.1'] >>> x = 'iperf -u -c 192.168.1.1' >>> x.split() ['iperf', '-u', '-c', '192.168.1.1'] >>> 上面是split函数的默认使用情景,通过这些实例,我们可以看出,split默认方法的作用是以空格将字符串分割开来,而不管空格数是多少个(大于等于1个)。当然,通过上面的实例,我们可以看出,split默认方法常常用作命令行分析工具。 现在,对于可选参数,我们进行相关分析: >>> s = 'www.baidu.com' >>> s.split() ['www.baidu.com'] >>> s.split(',') ['www.baidu.com'] >>> s.split('.') ['www', 'baidu', 'com'] 通过上面的代码,可选参数即为指定分隔符,用以将字符串分割的标志。另外,split默认方法是不能分离无空格字符串的。 现在,我们进行另一种尝试: >>> s = r'www.baidu.com/zhidao/cpp/format/2015-1-5-22-10' >>> s 'www.baidu.com/zhidao/cpp/format/2015-1-5-22-10' >>> s.split() ['www.baidu.com/zhidao/cpp/format/2015-1-5-22-10'] >>> s.split('.') ['www', 'baidu', 'com/zhidao/cpp/format/2015-1-5-22-10'] >>> s.split(r'/') ['www.baidu.com', 'zhidao', 'cpp', 'format', '2015-1-5-22-10'] >>> s.split('-') ['www.baidu.com/zhidao/cpp/format/2015', '1', '5', '22', '10'] >>> s.split(['.','//','-']) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: expected a character buffer object 对于上面的一个具有多个有意义分隔符的字符串时,我们如果试图同时获取所希望的数据,那么,通过在可选参数中设定list对象,是不行的。 很多时候,要得到多个分隔符分割的结果,使用正则表达式无疑是更好的选择。
有哪些实用的计算机相关技能,可以在一天内学会? 这个问题来自 Quora 网友,题主还补充说: 注:这个问题特指和计算机打交道的技能。 寒假我有一个月的时间,我想学习很多大约一天就能学会的实用技能。我不期望(一天)精通,但有了良好理解后,我能做些基本操作。比如,我想学习如何使用 Eclipse 的调试器,如何创建 makefile,学习一些重要的 Linux 终端命令。 以下的列表是来自Quora网友回复的归纳总结。译者在有些技能下面添加了简明教程与技巧的文章,另外也推荐了一些相关联的简明课程。 技术技能 1)版本控制:Git、Github 和 SVN(Git – Getting Started ) 译注:推荐这个交互式的 Git 入门资源,号称 15 分钟就够了。入门课程推荐《版本管理工具介绍—Git篇》和《版本管理工具介绍—SVN篇》。 2)正则表达式 译注:推荐《30分钟学会正则表达式》 3)awk 译注:《「sed & awk」读书笔记之 awk》 4)sed 译注:《「sed & awk」读书笔记之 sed》 5)Grep 6)学习如何用 Vim 做你从来不知道可以这样的事情 译注:推荐阅读《简明Vim练级攻略》和《25个Vim教程、视频和资源》 7)做一个爬虫,可以抓取一些网页并能解析一些基本数据 译注:向熟悉Python的朋友推荐这个爬虫框架《Scrapy:Python的爬虫框架》和一篇入门教程《Scrapy 轻松定制网络爬虫》 8)做一个更大的爬虫,必须填写一到两个表单 9)做一个简单的线性代数库(矩阵、向量、乘法) 10)向上面这个库中增加“奇异值分解” SVD (注:奇异值分解(singular value decomposition)是线性代数中一种重要的矩阵分解) 11)向这个库中增加矩阵求逆 12)向这个库中增加最小二乘法 13)确保你的库能高效处理稀疏数据 14)学习如何使用 Python 中的列表 译注:推荐《Python入门》 15)注册一个StackOverflow 帐号,学习如何使用该站点 16)阅读你最喜欢编程语言的手册 17)自己实现一个简单的机器学习算法,包括完整的流水线 译注:推荐阅读《国外程序员整理的机器学习资源大全》 18)学习如何在 Excel 中做一个简单的线图 19)安装 Eclipse 20)学习 NoSQL 数据库的基本功能 译注:推荐阅读:《8种Nosql数据库系统对比》 21)学习 SQL 的大部分基本功能 译注:推荐阅读《十步完全理解SQL》 22)理解 SQL 和 NoSQL 之间的区别(优点、弱点、限制,使用场景,如何使用,为什么,等等) 23)熟悉 Linux 系统 译注:推荐课程《Linux Guide for Developers》、《Linux达人养成计划 I》和《Linux达人养成计划 II》 24)学习一到两个排序算法。(快速排序和合并排序) 译注:推荐两个资源《VisuAlgo:通过动画学习算法和数据结构》、《旧金山大学数据结构和算法的可视化学习工具》 25)学习 D3.js 库 译注:推荐课程《使用D3制作图表》 26)学习给代码做单元测试 27)了解一些 AWS 服务,还有其 API(根据你的语言喜欢来选) 28)基本图论 29)一天一个算法 译注:推荐关注这个包括上百篇算法文章的列表 30)理解分布式处理和分布式数据存储的需求和挑战(basics of CAP Theorem, MapReduce 算法, MySQL 或 PostgreSQL 数据库的集群) 31)具体落实到 Python 译注:推荐《Python入门》 仔细阅读 Python 的内置函数,理解如何在命令行玩转这些内置函数 通过遵循Flask 指南或修改 Tornado 示例,来创建一个网站 学习 itertools 模块 32)玩一玩 CheckIO 译注:checkio是一个通过游戏学习编程的站点。另外,同时推荐另外一个寓学于乐的网站CodeCombat 33)学习如何编辑维基百科的文章,修改语法问题,或依照维基媒体的原则(比如观点中立)来修改 34)学习用 Markdown写作 35)学习 LaTeX、BibTex 和 pgfplots 36)学习如何在命令行下工作 译注:《每个Linux用户都应该了解的命令行省时技巧》 37)学习 JavaScript (Eloquent JavaScript) 译注:推荐两门免费的课程《JavaScript入门篇》和《JavaScript进阶篇》 38)如果熟悉 OOP,那可以学习设计模式 译注:《23个设计模式的简明教程》 39)搞个树莓派板子深入研究 非技术技能 1)搞搞园林 2)酿啤酒(译注:没条件的童鞋,推荐试试酿米酒) 3)体验远离计算机的生活 4)学电焊 5)学打字 6)约会 转载自:http://blog.jobbole.com/82633/
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/42010859 一、宏macro 为什么要使用宏呢? 对于函数,其调用必须要将程序执行的顺序跳转到函数所在内存的某个地址,在将函数程序执行完成后,再跳转回去执行函数调用前的地方。这种跳转操作要求在函数执行前保存现场并记录当前执行地址,函数调用返回后要恢复现场,并按原来保存地址继续执行。因此,函数调用会有一定的时间和空间方面的开销,必将影响程序的运行效率。 对于宏,它只是在预处理的地方把代码展开,而不需要额外的空间和时间方面的开销,因此调用宏比调用函数更有效率。 但是,宏也有很多的问题和缺陷: (1)在C语言中,宏容易出现一些边界性的问题,容易产生歧义。 (2)在C++语言中,宏不可以调用C++类中的私有或受保护的成员。 举例说明: #define square(x) (x*x) 我们用一个数字去调用它,如square(5),这样看上去没有什么错误,结果返回25,显然是正确的,但是如果我们用squre(5+5)去调用的话,我们期望的结果是100,而宏的调用结果是(5+5*5+5),结果是35,这显然不是我们要得到的结果。避免这些错误的方法,一是给宏的参数都加上括号,如下所示: #define square(x) ((x)*(x)) 二、内联函数inline 从上面的阐述,可以看到宏有一些难以避免的问题,对于不能访问C++类中私有或者受保护的成员,我们应该如何解决呢? 内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(过程化集成)被编译器优化。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。 对于内联函数,其工作原理是: 对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。 这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。 声明内联函数看上去和普通函数非常相似: void f(int i, char c); 当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件: inline void f(int i, char c){ // ... } 内联函数必须是和函数体声明在一起才有效。 像这样的声明inline function(int i)是没有效果的,编译器只是把函数作为普通的函数申明,我们必须定义函数体。 inline int function(int i) { return i*i; } 这样我们才算定义了一个内联函数。我们可以把它作为一般的函数一样调用。但是执行速度确比一般函数的执行速度要快。 当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。 根据上面两者的特性,我们可以用内联函数完全取代预处理宏。 三、总结 对于内联函数,其优点与缺点如下所示: 优点: inline定义的内联函数,函数代码被放入符号表中,在使用时进行替换(像宏一样展开),效率很高。 类的内联函数也是函数。编绎器在调用一个内联函数,首先会检查参数问题,保证调用正确,像对待真正函数一样,消除了隐患及局限性。 inline可以作为类的成员函数,可以使用所在类的保护成员及私有成员。 缺点: 内联函数以复制为代价,活动产生开销。 如果函数的代码较长,使用内联将消耗过多内存 , 这种情况编译器可能会自动把它作为非内联函数处理。 如果函数体内有循环,那么执行函数代码时间比调用开销大。 对于内联函数与宏,它们的区别如下所示: 内联在编绎时展开,宏在预编译时展开。两者展开的时间不同。 编译内联函数可以嵌入到目标代码,宏只是简单文本替换。 内联会做类型和语法检查,而宏不具有这样功能。 宏不是函数,内联函数是函数。 宏定义小心处理宏参数(一般参数要括号起来),否则易出现二义性,而内联定义不会出现。
第一部分:宏 为什么要使用宏呢? 因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。而宏只是在预处理的地方把代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个函数更有效率。 但是宏也有很多的不尽人意的地方。 在C语言中: 1、宏容易出现一些边界性的问题,产生二义性; 在C++中: 2、宏又不可以调用C++类中的私有或者受保护的成员; 我们举个例子: #define square(x) (x*x) 我们用一个数字去调用它,如square(5),这样看上去没有什么错误,结果返回25,显然是正确的,但是如果我们用squre(5+5)去调用的话,我们期望的结果是100,而宏的调用结果是(5+5*5+5),结果是35,这显然不是我们要得到的结果。避免这些错误的方法,一是给宏的参数都加上括号。 #define square(x) ((x)*(x)) 说明:宏在调用的地方,仅仅是简单的代码替换,所以参数要用括号括起来,不会出现函数调用那种压栈、出栈时的时间和空间的开销,执行效率更高。 第二部分:内联函数 从上面的阐述,可以看到宏有一些难以避免的问题,对于不能访问C++类中私有或者受保护的成员,我们应该如何解决呢? 内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以像调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。 内联函数工作原理解释: 对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。 如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。 在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。 如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。 这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。 假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。 声明内联函数看上去和普通函数非常相似: void f(int i, char c); 当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件: inline void f(int i, char c){ // ... } 内联函数必须是和函数体申明在一起,才有效。 像这样的声明inline function(int i)是没有效果的,编译器只是把函数作为普通的函数申明,我们必须定义函数体。 inline int function(int i) { return i*i; } 这样我们才算定义了一个内联函数。我们可以把它作为一般的函数一样调用。但是执行速度确比一般函数的执行速度要快。 当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。 有上面的两者的特性,我们可以用内联函数完全取代预处理宏。 第三部分:总结 inline函数的优点与缺点—— 优点: 1)inline定义的内联函数,函数代码被放入符号表中,在使用时进行替换(像宏一样展开),效率很高。 2)类的内联函数也是函数。编绎器在调用一个内联函数,首先会检查参数问题,保证调用正确,像对待真正函数一样,消除了隐患及局限性。 3)inline可以作为类的成员函数,可以使用所在类的保护成员及私有成员。 缺点: 内联函数以复制为代价,活动产生开销。 1)如果函数的代码较长,使用内联将消耗过多内存 , 这种情况编译器可能会自动把它作为非内联函数处理。 2)如果函数体内有循环,那么执行函数代码时间比调用开销大。 inline与宏的区别 区别如下: 1)内联在编绎时展开,宏在预编译时展开。 展开的时间不同。 2)编译内联函数可以嵌入到目标代码,宏只是简单文本替换。 3)内联会做类型,语法检查,而宏不具这样功能。 4)宏不是函数,inline函数是函数。 5)宏定义小心处理宏参数(一般参数要括号起来),否则易出现二义性,而内联定义不会出现。
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/42010029 一、程序的内存分配 对于一个由C/C++编译的程序,其所占用的内存可以划分为以下几个部分: 栈区(stack)—— 由操作系统自动分配和释放,主要用于存放函数参数值,局部变量等。其操作方式类似于数据结构中的栈。 堆区(heap)—— 一般由程序员动态分配和释放,若程序员不主动释放,则程序结束后由操作系统回收。注意,它与数据结构中的堆是不同的,分配方式类似于链表。 BSS段——主要用于存放未初始化的静态变量和全局变量,可读写,它在程序结束后由操作系统进行释放。 数据段(data)——主要用于存放已初始化的静态变量和全局变量,可读写,它在程序结束后由操作系统释放。 代码段(text)——主要用于保存程序代码,包括CPU执行的机器指令,同时全局常量也是保存在代码段的,如字符串字面值。 二、程序实例 /main.cpp int a = 0; // 全局初始化区域 char *p1; // 全局未初始化区域 int main(){ int b; // 栈 char s[] = "adoryn"; // 栈 char *p2; // 栈 char *p3 = "zhaobryant"; // 字符串字面量存放在常量区,p3存放在栈上 static int c = 0; // 全局(静态)初始化区域 p1 = (char *)malloc(10); p2 = (char *)malloc(20); // 分配获得的10和20字节的内存区放在堆区 strcpy(p1, "zhaobryant"); // 字符串字面量存放在常量区,编译器可能会将它与p3所指向的"zhaobryant"优化为同一个地址 return 0; } 三、堆和栈的理论知识 1. 申请方式对比 栈stack: 由系统自动分配。例如,声明在函数中一个局部变量,即int b,系统自动在栈中为变量b开辟空间。 堆heap: 需要程序员自己申请,并指明大小。 在C中使用malloc函数,如p1 = (char *)malloc(10) 在C++中用new运算符,如p2 = new char[10]但是p1、p2本身是在栈中的。2. 申请后系统响应 栈stack: 只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将返回异常提示栈溢出。 堆heap: 首先应该知道操作系统有一个记录空闲内存块节点的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的内存块节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放内存空间。另外,由于找到的空闲内存块节点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 3. 申请大小的限制 栈stack: 在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。也就是说,栈顶的地址和栈的最大容量是系统预先规定好的。在Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示内存溢出。因此,能从栈获得的空间较小。 堆heap: 堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 4. 申请效率对比 栈由系统自动分配,速度较快,但程序员是无法控制的。 堆是由new/malloc进行内存分配,一般速度比较慢,且容易产生内存碎片,不过用起来最方便,速度快,也最灵活。 5. 存储内容对比 栈stack: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地 址,也就是主函数中的下一条指令,程序由该点继续运行。 堆heap: 一般是在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排。 6. 存取效率对比 对比两段代码: char s1[] = "aaaaaaaaaaaaaaa"; char *s2 = "bbbbbbbbbbbbbbbbb"; 如上,aaaaaaaaaaa是在运行时刻赋值的;而bbbbbbbbbbb是在编译时就确定的。 但是,在以后的存取中,在栈上的数组比指针所指向的字符串快。 例如: #include ... int main(){ char a = 1; char c[] = "1234567890"; char *p = "1234567890"; a = c[1]; a = p[1]; return 0; } 对应的汇编代码: 10: a = c[1]; 00401067 8A 4D F1 mov c1, byte ptr [ebp-0Fh] 0040106A 88 4D FC mov byte ptr [ebp-4], c1 11: a = p[1]; 0040106D 8B 55 EC mov edx, dword ptr [ebp-14h] 0040106D 8A 42 01 mov a1, byte ptr [edx+1] 00401073 88 45 FC mov byte ptr [ebp-4], a1 第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。 7. 小结 堆和栈的区别可以用如下的比喻来看出: 使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,其好处是快捷简单,但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。