这个年假-流水账

依照惯例,过完了这个年假,还是要记录一下。就在今晚吧,听着音乐,静下来写写。

说实话,年前真的不是很想回家去,就想着能这样千篇一律的过着上班日子,不用回家接受一大波长辈的关心和询问,当然还有来自父母的从未停歇的说教,过年期间肯定会更甚。但整个小组都提前两天放假,我也不能固执非要迟迟不回去。不用抢火车票或许让整个回家的过程变得轻松了很多,没转几趟车就顺利到家。我妈在家照顾我奶奶在,恰巧我爸也是当天回家,于是一家人团圆了。

在家待了一天,调整休息了一下,然后就去了一趟LA,和高中挚友TY聚聚,这家伙去年谈了一个女朋友,发展的很快,去年我也经历不少不顺心,我们聊得很多,觉得还是要见面聊过瘾。他带上他的新女友顺便让我见见。曾经的同桌XC也放假回来了,于是也过来了一起。午饭过后,我们三个同学略带晕乎的首先去了学校逛逛。学校也已经放假了,门卫不让进,通过一番拉关系和恳求,最终进去了。两年前我来过一次,与上次相比,变化并不大,还是那些楼那些路,宣传栏、报亭也照旧,变化的只是上面的内容。我们一边逛一边怀怀旧,说说以前的那些同学和老师,还有学校的最近新闻和变化。想起那句铁打的营盘,流水的兵,一届一届的毕业生从这里走出去,我们都是这个营盘的流水兵,把最奋斗的三年光阴留在了这里。顺着河畔走走,这两个家伙都很逗,一起说笑很有意思。

腊月二十八,晚上我家炸元宝,我妈忙厨房,我和我爸负责团“元宝”,一家人一起忙活,很简单平常却很开心,只希望时间能停留在那一刻,忍不住拍下照片留存在朋友圈。经过去年一些不顺心的事情之后,与家里的联系比之前更加密切,交流的更多了,加上自己也成长了一些,愈加觉得一家人一起的温暖,对父母要多些关心,不能再象以前那样有时的任性和小脾气。有时看着他俩拌嘴,我从中调和一下,也是很温暖有意思的事。

从很小时起,每个年三十的过法都大致是以下的一套流程

早上帮衬着老妈准备晚上满桌丰盛饭菜,简单的午饭过后,和老爸一起贴春联。

现在的春联都是买的,而且也只是贴一个大门,其它门上的或者不用贴,或者就是以前的还完好的。整个贴春联过程极大的简便了。以前每户人家里贴的春联都是自己买的红纸,然后用毛笔现写的,每家都要写很多,几乎每个门窗都要贴。记得很小的时候,是我爸负责了大半个村子的春联,他毛笔字写的好,那个时候,每到过年前两天,家里只能是我妈一个人忙,我爸全部时间都放在写春联上,挨家挨户的去人家里写春联,这家写完就要马上去下一家,经常写到很晚,说句时髦的话,那时我爸档期排的很满,如果邀约迟了就只能靠后干等着别家的都写完了。我呢,就跟着我爸后,帮衬着添添墨水、折折纸,大多数时间是在一旁,和许多人一起,看着他挥墨,每个字都写的那么工整漂亮,众人一旁不时评论叫好,现在想想好有意思的场面,当时心里确实很崇拜我爸,要是能像他一样就好了。后来,就在小学快毕业时,我突然喜欢上写毛笔字,我妈顺势给我买了很多笔墨和纸张,生生让我的爱好变成了任务,练了很久,整体抄诗、抄文章、临摹,虽然有些不适,但慢慢的也就出道了。也是我爸有意让我展露,把一些春联的任务推给过了我,虽然到现在为止我都自认为写的很一般真的很普通,但是当时却受到了认可,也许是大家对我的鼓励吧。第二年我就承接了以前我爸的活,所有很有意思的是我和我爸的在写春联过程中的角色互换了。我主笔,他在一旁帮着添墨折纸、从对联宝典中选选对联,当然最重要的是指导,印象中还是鼓励居多。我写了几年,伴随着的是买春联的流行起来。后来写的不多了,也就没几家了,就送到我家里来,我写好再给送回去或来拿。这时,我妈就能凑过来指导了,听的最多的指导就是“写大点”,然后不时对某个字评论“尚好的”、“还照”、“写大点就好了”、“下一个字写好点”。写春联的同时,由于在镇上读初中离家也近,那个时候也恋家,每周末都回,村里几次婚庆的对联也被我妈承接给我了。婚庆的对联讲究,不是写错了换张就好了没什么事,而是一定不要写错了,说是不吉利,当然要是真写错了也只能换,对方也当然不会说什么,幸好没有出过什么差错。其实我当时是不愿意的,费力费神,关键我妈乐意接,估计我在学校的时候,她一定在听别人夸我心里得意洋洋,也是很好的体验,也算是我的一点回报哈哈。现在早已经是家家买春联了,但总会有一些选不到合适对联的场合,过年也偶尔会写个一两幅。想想现在朋友圈那些晒孩子的,当年我爸妈一旁看我写的时候,也是一种晒孩子估计心里也挺欢喜的吧。

贴完春联后,就是一些传统的祭拜活动。我爸和我会先给我从未蒙面的外公外婆烧些纸钱并跪拜以寄托情感。之后就是联络几个叔伯家,一起去爷爷墓前扫墓、烧纸钱跪拜。接下来就是最传统的祭祖活动了,一大家族老少(不包括女性,她们都各自在家准备年夜饭)汇集一起,由于这两年奶奶都是在我家过年,所以也就聚在我家,摆上一桌祭祀的鸡、鱼,后续还会有酒、米饭,通常是我爸或二叔提上一个灯笼,从屋外走一趟回来,接老祖上回来,桌子在堂屋的上方,众人在堂屋的下方靠边或站或坐,等待着老祖上用餐结束,不能碰桌子和板凳,也不能站在堂屋的中间或正挡着门口的位置。大约几分钟吧,老祖上用餐结束,送走老祖上,撤下酒和米饭。众人开始按照辈份和长幼,依次跪拜,每次跪拜分两步,第一次在桌子的后方,面朝大门,跪拜天,第二次在桌子的前方,面朝堂屋的中堂,跪拜地。跪拜结束之后,室内的祭拜就结束了,接下来就是所有去室外的空地烧些纸钱并跪拜,当这最后一轮的祭拜结束后,整个祭拜活动也就结束了。接下来就是各自回家,准备吃年夜饭了,此时夜幕也通常降临了。

当准备了很久的丰盛的食物摆满了桌子后,先不急着开吃,要在门外放上一串长长的鞭炮,等鞭炮响完之后,再燃放烟花,噼里啪啦的鞭炮声和绚丽的烟花预示着这家的主人就要开始一年中最重要的那一顿晚餐了 – 年夜饭。一般我家也就我爸妈和我三个人,这两年我奶奶刚好在我家,也更团圆了。几个人面对一桌子的菜肴,真是足够奢侈啊。于是一家人围坐在圆桌前,边吃边聊,回顾这一年的收获,家里的变化,由于爸妈的勤劳,家里过的都还不错,还算风顺,日子也一年胜过一年。通常是我爸洋洋得意的夸赞一翻,我妈接上几句肯定一下过去的收获和现在的生活,然后最后总结来一句“反正还是要好好干,希望明年不比今年差”。这些年我也开始从饮料变为喝白酒了,我会敬他俩酒让他们注意身体我的事放心保证继续好好努力,他们也会嘱咐我再接再厉,他俩也会互相敬酒,说些平时很少说的话,肯定一下对方,一派祥和温馨的场面。奶奶不在我家过年的时候,我和老爸吃完年夜饭,就要去奶奶所在的叔伯家敬奶奶酒,然后依次去几个叔伯家敬酒。这两年奶奶在我家,都是等叔伯及他们家的小辈过来敬酒,然后我们再过去。当挨家挨户都走到后,一桌麻将或扑克的人选也同时敲定了。

打从记事起,就没好好看过一场春晚。小的时候伙伴多,都是几个小伙一起放鞭炮烟花到处乱窜,感觉特别的好,因为很少能在晚上的时候在外玩,而年三十的晚上家家都通宵点着灯,天空不停绽放烟花,明暗恰到好处,几个小伙伴边玩边闹走在外面,有几分游侠的味道,等真正玩累了也就回去老老实实睡觉了。再大一些时候开始,就变成了几个小伙伴围在一起打麻将和扑克了,玩的都很小,主要是好玩娱乐。再后来,曾经的小伙伴有了不同的生活经历,联络少了,也就没有凑在一起打牌了,也就我们堂兄弟三人是个经常不变的组合,三缺一的时候就会叫上老叔一起,今年堂哥也没回来,于是二缺二,又叫上了二叔。不管是那个组合,从来都是一夜的牌桌鏖战,无一例外。记得有一年半夜的时候由于电力紧张停电了,我们点上蜡炬打开手电筒继续的坚持直到来电。今年因为要抢红包,我让我爸替我打了一会,等我抢了一番红包硕果累累之后,他去睡觉了,我继续。输赢都是小事,还是以娱乐为主,就是喜欢打牌时一起吵闹说笑的情景。通常在凌晨的5、6点我们才会结束,因为还要回去准备初一早上的一些事,而我也就这个时候才去床上躺一会,当然也就只能一会,8、9点的时候就要起来,吃早餐。

早餐是传统的惯例,面条、打卤、鸡蛋、粽子,虽然是很美味丰盛的,但由于熬夜,胃口都大大下降了,匆忙吃完之后就要开始串门拜年了。

正月在家的日子,我一般只走几家亲戚拜年,除非老爸老妈跑不过来,我会出门一下,其它时候都是在家,最多的事也就是看电视和打牌了。

年前,我不想回去,年后,我却又不想出门了。

年前,怕回去家里长辈的一大堆各种询问

年后,在家待的舒服了,又真不想出来上班

写写停停,一篇流水账写了好几天,先就这样吧,要是想到什么再补充吧(估计不了了之了,以我对自己的了解)。

随心的字

谁都会紧张

刚刚看完了新一期的《我是歌手》,踢馆歌手谭维维作为一个登上过大舞台参加过大比赛参演过电视剧的明星,表现的还是那么紧张,而本场的其它所有歌手都坦言有紧张情绪。可见紧张这种东西,不是什么大的缺点,陌生和压力下谁都会有,自己看淡一些,自己鼓励自己,少管一点其它人的看法,也就好了,做不到也正常,不用因紧张而加剧紧张。平时还是需要往脑袋里面多攒一些货,让自己底气多一点,减少紧张的出现。最强大的自信是让沉默都充满气场。

有些事迈出去了发现也就那样

自己好多时候不敢去尝试接触新鲜事物陌生的人,但是在慢慢熟悉之后,在一起又会特别自在和活跃,属于典型的慢热,想来主要因为不喜欢有心里负担的接触交流吧,总是希望能游刃有余的接触新事物,总是想给陌生人展现充满活力的一面。记得早前不是这样子的,那时我还是特别喜欢接触新鲜事物和陌生人的,后面受过几次打击挫败后,慢慢畏缩了。这段时间晚上一直有在夜跑,绕着湿地的小道,特别喜欢那种感觉。后来加了一个跑步群,为了接触陌生人,也为了跑步能够有时变得不一样,和几个群友夜走之后,发现也是挺有意思的,聊聊一些话题,了解到别人的生活和想法,自己也还算自在。

匆匆那年

看过原著,很感动,电影刚上映就去看了,但是着实不怎么样,把我构想中的男女主角都演的很作,本来很纯真的一段校园恋情,活生生被演成了不做不会死的剧情,很虐心。唯一的亮度就是电影的主题曲,王菲唱的匆匆那年,我已经单曲循环了很久,几句歌词很触动。

匆匆那年我们 一时匆忙撂下 难以承受的诺言
只有等别人兑现

每段恋情都会经历那个海誓山盟的阶段,感情越久越会把曾经的诺言当真,以为可以轻易实现,到最后,分岔路口,谁也记不清说过了些什么,只有一时的愤、怨、失望、毅然决然,往前继续走的路上,或许会偶尔想起那么几句,但是已无法兑现,无论当时觉得是多么自然简单的事,但诺言也一定会兑现,只是主角变为,前面的路上的等待的某个ta。

匆匆那年我们 见过太少世面 只爱看同一张脸
那么莫名其妙 那么讨人欢喜 闹起来又太讨厌

以为就是整个世界,以为一切会坍塌,以为无法继续呼吸,但是时间的友善就在于它会让一切都淡下去,不会太久,那种令人窒息的沉落的日子,都会走出来的,在未来的某个时刻,你会豁然开朗,那一个个辗转难眠的夜晚,那一个个茶饭不思的白天,那一罐罐一箱箱的啤酒,那无时无刻都能迸涌出来的悲伤情绪,到最后,都被塞进一个盒子,取名叫经历,当你强忍着把它们一个个塞进去的过程,就是磨砺和成长,再次经历会看的更淡然。其实不是事情本身有多么的难熬,只是见过太少的世面。

如果再见不能红着眼 是否还能红着脸
就像那年匆促 刻下永远一起 那样美丽的谣言
如果过去还值得眷恋 别太快冰释前嫌
谁甘心就这样 彼此无挂也无牵
我们要互相亏欠
我们要藕断丝连

再见红眼,彼此亏欠,藕断丝连,自然是未走出时的天真想法,没有彻底的无牵无挂,没有彻底的冰释前嫌,再见也只能低头闭眼擦肩。亏欠和藕断丝连看似唯美婉转,看似眷恋多情,其实只是自我麻醉催眠,是懦弱和退避。只有彻底之后,才会有平静的再见。

会写文字的人是幸运的

能够把自己所想记录下来,能够在需要的时候重温那时那刻的情绪,也是一种幸运的事,这两天在用简书这款app,很多人都是把它当作笔记来用,有想法就写一些,看过了一些人的文字,很容易就投入进文字间的情绪,感觉到文字作者的感情,很奇妙。

过好自己就是对生活最好的交代

大概一个月前就想到了这句,想把它设为状态,考虑了可能的消极影响之后还是放弃了。今天突然冲动想把它记下来。

搞技术爱跑步

阅读的越多,就会发现越多的技术知识和技术牛人,也越加发现自己的浅薄。这两天经由startup news论坛,延展阅读了许多技术性很强的文章,赞叹作者的深度。我有个习惯就是,遇到喜欢的文章,我一定会深挖文章的作者,了解其从业经历,拜读他其他的文章,而每一个牛人背后无一不是一段孤独,沉浸,专注,疯狂和钻研的奋斗史,总让我有些热血沸腾,恨不得立马来几个通宵达旦的钻一项技术,搞出点名堂。

总结下自己,对技术的兴趣确实很浓厚,也喜欢广泛涉猎和尝试,耐心是有点的,毅力也是有点的,但结果我认为还是深度欠缺,会有些事让我不够专注而选择浅尝辄止,大概是会很贪恋生活的舒坦的缘故吧。

看得到差距,不如趁着还算年轻有力,努力追赶一下大牛的脚步。静下心,好好弄,切勿被心浮气躁耽搁了时间而弄得一无所获。写给自己,记得沉下心来做事。

最近发现自己迷恋上了跑步,确实是一个好东西,可以释放压力让人心情舒畅,益于身心健康。尤其绕着环境和空气都不错的湿地景区的小道跑一跑,体会着自由,也是一种不可多得的享受。

写在25周岁-人生的分水岭

很多人都说25岁,对于男人来说是个分水岭,此时的你需要丢下以往的种种疯狂和不成熟,开始沉淀内心,为未来准备一个更好自己,简而言之就是男孩到男人的转变。对于我来说,这个25周岁的到来,恰如其分,之前的早已习惯的生活告以剧终,之后的未知期许的生活慢慢浮现。25岁前生活的主旋律是求学和青春,偏安在校园一隅,不想太遥远未来,或多或少的沉溺于享受青春的欢愉;25岁后的主题将是工作和家庭,前者刚刚起步,后者在这半年来愈加体现出其厚重的份量。

两年半的研究生生涯即将结束,只差最后的一个形式,笼罩了这么多年的一个“学生”身份也终将被摘去,开心大过于留恋。早已急切的就想证明自己,独立的生活,承担起更多的责任,接触更广阔的人事,磨砺更优秀的自己。

过去的一年,跌宕起伏,无论是在于感情还是工作,不管结局如何,却都是一种厚重的收获。几年的感情走到了头,来的那么突然,仿佛在最甜蜜的时刻瞬间终结,不堪一击,现实就是如此,抓不住的东西太多,想通了就好,没有什么好埋怨的,环境的力量总是太过强大,改变一个人,几近于不费力,熟悉到陌生,已是人类太老旧陈腐的桥段了,往前走,把回忆留在身后,方能不辜负自己和时光,乐观积极点,把微笑留给未来的随缘而至和不期而遇。年初的找实习意气风发,多年的努力即将看到收获,却遭遇开始的不顺当和莫名其妙,有过那么一会会的心灰意冷,后面峰回路转获得一个好机会并最终留下,我一直愿意相信自己可以,也感谢最终的结局让我维持了这份自信。见识了现实和人性中的冷暖,失去已不再是难以接受,让我得以重新拥抱了身边最真挚的朋友们,让我与父母沟通交流更多关系更加亲密,总结起来就是一些看淡和一些看重。很多事都是偶然,多给自己一次尝试,可能就会改变整个结局,正如当初自己找实习时偶然的一个尝试,却意料之外的让我获得了现在的工作。

以后的路,踏实一点的走,做好自己的工作,这是我想独立的资本,不怕多付出时间和精力,初入职场,有太多的东西要学,也庆幸自己的行业让我能更多将精力放在技术上。习惯现在的生活,接纳自己的生活节奏,正如这段时间这样,不去心浮气躁,世间的美好属于静下心来体会的人。感情的事不强求不抗拒,顺其自然,相信生活总是会让所有人结局都美好的。

25岁,告别校园走入职场,告别过去拥抱未来,一切都是崭新的,一切都在迈向稳定,保持这动力,继续努力!

 

 

 

选择了就是选对了

一个人生活过的不好的原因大抵是因为太过矫情

矫情的原因又大多是因为想太多

想太多的根源在于对未知的恐惧

而这种恐惧本质上是对自己的一种不信任

可越过得不好越会有强烈的不信任感

如此循环,消耗掉一个人的精力,渐噬掉原本的平顺的生活

都知道这样不好,都能自己认清或被他人催醒,都像跳出这个吃人的圈

改变却总会显得那么艰难,与自己的思想对垒,才是最富挑战

压抑不住的情绪,无处释放,会游走在身体每处,散发出悲观、懊恼夹杂自暴自弃的气息,一切回到原点。

———————————————

生活中的道理我们都懂,但不是懂的道理多就可以过好生活,不少陷入循环的人,一番道理的洗礼后,仿佛大彻大悟,誓要痛改前非,不经多时,不自觉又回到老路。

看多了知乎、豆瓣、朋友圈的励志鸡汤,但除了能温暖一时外,无他。

到底,路还是要自己走,改善自己的过程中艰辛只能自己品尝,没有谁能感同身受,在痛不能行的时候,找一个发泄途径,自己鼓励自己一下。

任何事物都有两面性,好或不好,都是自己,接纳不完美的自己

当可以更好的时候,不要犹豫,让自己改变

不用羡慕,生活好坏本来就是主观感受

学着耐心,时间会帮摆平许多烦事,安静等待就好

学着平复内心的欲望,一切都会是水到渠成的事,不用急不可耐的逼自己去占有

最重要的

相信自己的选择,是选择成就了现在的自己,选择了就是选对了!

 

手机APP

我的草房子马上就要迎来一周岁生日了。

回想去年刚刚开起来时那份激动,没休没止的沉溺在这里,不断修改页面布局、增添功能,有点想法和代码就想着发表到这里,在那段火热的日子里,我保持了每天登陆,几乎一两天就会更新一篇日志,内容涉及代码、解决问题的方法、新事物的笔记,还有平常日子的一些感悟。

而现在,已经好久都没有登录了。

这个情况始于6月初实习日子的开始吧,开始实习的日子那么充实,白天在公司不适合更新博客,晚上回去就想着放松更无暇顾及博客的事。实习的后半段由于生活中发生了一些变化,对许多事物都提不起兴趣,更不用说找时间写博客的事,9月实习结束回到学校。为了排解心中不时涌上来的不适的感觉,我在晚上会选择看一部经典的电影,首先想到就是经典的大话西游系列。记得那晚看完之后感触颇深,像是懂得了很多,参悟了不少感情上的道理。于是就有了上一篇博客的文字。

前段时间开始,生活里满满的都是毕设和贷款的事情,整天忙于此,茫茫的往前走,忘了去总结和记录生活,许多不经意冒出的念头,我随手就记录在微信里,也没有整理。

总会在落寞的时候才想起来写点什么。暑期以来,经常会有这种感觉,每次都需要自己给自己鼓舞打气开导一番,然后保持好状态直到下一个低潮点。

今天突然想起来,要给我的草房子也要弄一个手机端的APP,类似网易博客APP那样,以后就可以随手记录了。记下来,如果找不到可以直接用的,考虑自己抽空写一个。

 

大话西游感悟

曾以为紫霞很傻,至尊宝一次次的拒绝、欺骗、伤害,甚至甘愿娶牛魔王妹妹也不肯接受她,她心碎、痛恨、拔剑指喉,但至尊宝只一番话两行泪,就感动放下了剑,深信他的话,甘愿为他去取月光宝盒,开心的笑着和他约定半夜二更见面。

为什么伤痛可以忘记的这么快? 为什么信任可以给的这么简单? 心碎和开心的转变快的难以置信?

经过了自己的一段感情挫折之后,才有了些许感悟。

源于爱的恨都是脆弱的不仅抚慰的

只言片语就会重新燃起,死心塌地的相信,只是因为自己不肯不相信,自己眼中的对方并不是现实中存在的那个,而是自己脑海中臆想的那个。

至尊宝是没有错的,他爱白晶晶,付出一切的想拿回宝盒回到过去,找回晶晶,他是伟大的,作为一个男人,他专一。

紫霞的爱没有错,只是她爱错了人,一个人再好,你对他再好,也不能期望换来他对你的爱,因为爱这东西本来就是无法平等的。付出和痛苦,只是因为自己把爱放错了位置。

缘分就是这样,只有两个人彼此都相信愿意走下去,才会有结果,一个人的挽留只是徒劳和懦弱。

不用问为什么,因为没有为什么,真正的为什么自己也是清楚的。

我相信,紫霞在某些时候静下来也会知道,至尊宝只是再一次的欺骗,也许只是一闪而过的念头,她还是会苦笑着告诉自己,不要怀疑,他没有骗她他会来娶她,宁愿让自己傻,也不愿让自己心痛。

———————————————

至尊宝所做的一切都是在为了回到500年前找到白晶晶,他不惜欺骗紫霞,辗转于牛魔王妹妹、铁扇公主、青霞之间。等到他遇到500年前的白晶晶的时候,却发现自己最爱的却已经成了紫霞。

昏迷中他默念晶晶98次,却默念紫霞784次,即便晶晶糊里糊涂中答应和他成亲,而夜里他却依旧默念着紫霞的名字,甚至多了一次。

尽管他不肯承认,但是最爱的已经在不知不觉中变成了紫霞。就是这样,当你为了一个人不顾一切的时候,在不顾一切的过程中,你绝对不会发觉自己已经在不知不觉中改变了。

你认为你爱一个人舍生忘死,但其实只是一种不停歇的脚步

当到达终点的时候,才会发现,一切都变了,你以为的自己不可能再爱上别人,但事实上真的已经爱上了另一个人。

在Visual Studio 2013 中使用C++单元测试

本文主要介绍在Visual Studio 2013中对代码进行单元测试的方法,包含了两方面的内容:对已有的Dll文件进行单元测试,以及对已有的源文件进行单元测试。

 

1. VS2013对DLL文件的单元测试

对已有的dll文件进行单元测试。

  • 首先,新建一个待测试的DLL项目CalculationDll,共包含3个文件: CalculationDll.cpp、CalculationDll.h、CalculationDll.def。
  • 然后,新建了一个单元测试项目CalculationTest。
  • 继而,在CalculationTest的项目属性Include Directories中包含了待测DLL项目CalculationDll的目录,
  • 继而,在CalculationTest的项目属性Common Properties > References 中添加了对待测DLL项目CalculationDll的引用
  • 继而,在CalculationTest中包含CalculationDll.h,撰写测试用例Test_Method
  • 最后,BuildCalculationTest项目,打开Test Exploerer,运行测试用例

[1] 创建和编写待测DLL项目 – CalculationDll

新建“Win32 Project”,命名为“CalculationDll”,Solution名称为”TestExample”, 类型选择为DLL,空项目。

clipboard.png

新建 cpp文件

clipboard1.png

新建 .h文件

clipboard2.png

新建def文件

clipboard3.png

最后 Build 该项目

[2] 创建Test项目 – CalculationTest

在当前的Solution中添加新的Test项目CalculationTest.

clipboard4.png

[3] Include Directory 包含头文件目录

CalculationTest的项目属性 > VC++ Directories > Include Directories 中添加 CalculationDll的目录

clipboard5.png

[4] 添加项目引用

CalculationTest的项目属性 >Common Properties > References > Add New References > 勾选CalculationDll.

clipboard6.png

[5] 编写测试代码

在CalculationTest 的 unittest1.cpp中编写测试代码,首先包含头文件,然后编写两个测试方法。

clipboard7.png

[6] Build ,运行测试

先Build CalculationDll项目,然后选择菜单“Test” > “Windows” > “Test Explorer” 打开 Test Explorer窗口

clipboard8.png

该窗口中可以运行测试用例

 


2. VS2013中对已有源文件的单元测试

对已有的C++项目中的源码进行单元测试。

  • 首先,新建一个待测试的项目Calculation,共包含2个文件: Calculation.cpp、Calculation.h。
  • 然后,新建了一个单元测试项目MyUnitTest。
  • 继而,在MyUnitTest的项目属性Include Directories中包含了待测项目Calculation的目录
  • 继而,将待测试的Calculation项目中的源码添加到MyUnitTest中
  • 继而,在MyUnitTest中包含Calculation.h,撰写测试用例Test_Method
  • 最后,BuildCalculationTest项目,打开Test Exploerer,运行测试用例

【1】新建待测试项目 – Calculation

新建“Win32 Console Application”类型的Project,名为“Calculation”,Solution 名为”TestExample”。并添加两个简单的函数add和sub,实现加减运算。

clipboard9.png

为了后续测试,先将_tmain函数注释掉。

【2】 添加头文件 – Calculation.h

为了能让add和sub函数可以被Test项目测试,需要新建一个头文件,在头文件中包含这两个函数的声明。这样Test项目就可以包含该头文件从而使用add和sub函数。

如图,头文件为”Calculation.h”

clipboard10.png

在Calculation.cpp 中包含该头文件

clipboard11.png

【3】 新建Test项目 – MyUnitTest

在Solution Explorer 中右键Solution名“TestExample”,选择“Add”> “New Project”,新建项目类型为”Test”,模板选择“Native Unit Test Project”,命名为”MyUnitTest”

clipboard12.png

【4】 包含目录

在MyUnitTest项目属性的”VC++ Directories” > “Include Directories”中添加 待测项目Calculation的目录。

clipboard13.png

【5】将已有源代码添加到Test项目中

右键”MyUnitTest”项目,选择“Add”> “Existing Item” ,将Calculation.h 和 calculation.cpp 添加到项目

clipboard14.png

【7】 编写测试

在MyUnitTest项目的unittest1.cpp中包含calculation.h ,然后编写两个测试方法TestAdd和TestSub

clipboard15.png

【8】 Build MyUnitTest项目

必须先Build MyUnitTest,才能在下面一步的Test Explorer中看到编写的测试用例

【0】 在Test Explorer中运行测试用例

菜单“Test”> “Windows” > “Test Explorer” 打开Test Explorer.

在这里运行测试用例。

clipboard16.png

 

本文主要讲解了整个测试过程,后续会补充具体的单元测试方法。

参考资料:

Unit testing existing C++ applications with Test Explorer

Unit testing native code with Test Explorer

Problem 50 Consecutive prime sum

Problem

The prime 41, can be written as the sum of six consecutive primes:

41 = 2 + 3 + 5 + 7 + 11 + 13

This is the longest sum of consecutive primes that adds to a prime below one-hundred.

The longest sum of consecutive primes below one-thousand that adds to a prime, contains 21 terms, and is equal to 953.

Which prime, below one-million, can be written as the sum of the most consecutive primes?

分析

求小于100万的一个质数,这个质数等于N个连续的质数的和,且N最大。(为了叙述方便,我称这N个连续的质数为链)

方法1 对每一个质数统计其最大的链长,再找到链长最长的质数。【较慢,因为存在重复计算】

首先保存所有小于100万的质数。对于每个质数n,从2开始累加,如果一旦累加值等于n,此时的累加的质数链最长(不可能存在一个不是从2开始的质数链累加值等于n,而且链长更长),如果累加值不能得到n,那么从3开始重新累加,到了某个开始值的时候一定能有累加值等于n(至少当从n开始的链长为1的质数链就一定满足)。

问题的关键是如何快速的累加,保证不重复计算。对于每一个质数,获取其最长连续质数链的算法如下:

image

所有小于100万的质数2,3,4,5…保存在顺序容器中(如数组,列表)。对于每个质数n,要得到和等于它的最长连续质数链,则可从容器开头开始累加。

start指向质数链的开头,nxt指向质数链的结尾。sum统计当前质数链的长度。

初始时:start = nxt = 0,指向开头。sum = 0

然后执行如下循环:

如果sum = n,则表示找到了最长的连续质数链,返回质数链的开头位置start以及质数链的长度nxt-start. 循环终止。比如n = 2,此时start=0,nxt=1,sum = 2。

如果sum > n,则表示累加start到nxt后的累加值sum>n。此时的处理时本算法的一个关键。我们不用重新从start+1 位置开始统计。而是让sum 减去 start位置的数,然后start++。如果sum还大于n,则继续循环减去start位置的数,start++,直到sum <n.

如果sum <n,则sum加上链的后一个数,即nxt位置的数,然后nxt++。

上面过程Python代码如下,all_primes列表中存放了所有的质数。

#return the max length of prime n, and the start number
def calc_len(n):
	start, nxt, sum = 0, 0, 0     #start: the start index of the chain. nxt: the end index of the chain
	while True:
	if sum == n:
		ret = (all_primes[start], nxt - start) #if n=2,then nxt=1,start=0,len = nxt-start
		return ret
	elif sum > n:
		while sum > n:
			sum -= all_primes[start]
			start += 1
	else:
		sum += all_primes[nxt]
		nxt += 1

这个方法思路很清晰,但是运行较慢。因为对每个质数的计算过程大致相同,存在着重复计算问题。

方法2   用一种快速的方法获得100万以下的所有质数。然后依次统计以2开头的链,以3开头的链…找到最长的链长 【很快,<1s】

一种快速获取质数的方法:排除法。思路是这样的,如果i是质数,那么从i*i +k*i  (k=0,1,2…)都不是质数。

比如获取小于N的所有质数的方法如下:

vector<int> primes;
 
bool prime[N];
for(int i = 0 ; i < N ; i++) {
	prime[i] = true;   //i is initially regarding as a prime
}
 
for(int i = 2 ; i < N ; i++){
	if (prime[i]){
		primes.push_back(i);
		if(i <= (int)sqrt((double)N)){    // i*i <= N
			for(int t = i*i ; t<N ; t+=i){ //set all i*i+k*i -> false
				prime[t] = false;
			}
		}
	}
}

bool prime[N] 数组用来表示小于N的每个数是否是质数。初始时,全部设置为true,表示是质数。然后从i=2开始循环,如果prime[i] 是质数,那么就把prime[i] 保存到primes容器中,然后将i*i 开始的,每次加 i 的数都设置为不是质数。即,如果2是质数,那么就把从4开始,增量为2的所有数4,6,8,10,12…全部设为false,即不是质数。 接下来要统计最长的链,方法是这样的: 初始时设置最长链长为max_len=0,拥有最长链的质数是maxsum =0 , 从i=2开始,依次计算2,2+3,2+3+5,2+3+5+7,2+3+5+7+9…的值,如果当前值是质数,而且链的长度大于max_len,则更新max_len和max_sum。然后从i=3开始,再做相同的统计。依次类推,直到 i 是小于100万的最后一个质数。代码描述如下:

int maxlen = 0, maxsum = 0, cursum = 0;
for(int i = 2 ; i < primes.size() ; i++){
	cursum = 0;
	for(int j = i ; j < primes.size() ; j++)
	{
		cursum+=primes[j];
		if (cursum>=N)
			break;
		if ( prime[cursum] && j - i > maxlen )
		{
			maxlen = j-i;
			maxsum = cursum;
		}
	}
}

感谢 Xizario, P-E论坛第三页最后一个。

C\C++

方法1 [思路同分析] 保存所有小于100万的质数,计算每个质数的最长连续质数链长,找到链最长的质数。

#include "stdafx.h"
#include <iostream>
#include <vector>
 
using namespace std;
 
vector<int> all_primes;  //保存所有小于100万的质数
 
/************************************************************************ 
 * 判断n是否是质数
 ************************************************************************/
bool is_prime(int n){
	if (n <= 1)	{
		return false;
	}else if (n == 2 || n == 3){
		return true;
	}else if (n % 2 == 0){
		return false;
	}else{
		for (int i=3;i<=(int)sqrt((float)n);i++){
			if (n % i == 0){
				return false;
			}
		}
		return true;
	}
}
 
 
/************************************************************************ 
 * 返回质数n的最长链长
 ************************************************************************/
int calc_len(int n){
	int start=0,nxt=0,sum=0;
	while(true){
		if (sum == n){
			return nxt-start;
		}else if(sum > n){
			while(sum >n){
				sum -= all_primes[start++];
			}
		}else{
			sum += all_primes[nxt];
			nxt++;
		}
	}
	return 0;
}
 
 
int _tmain(int argc, _TCHAR* argv[])
{
	//find and store all the primes below one-millon
	for(int i=2;i<1000000;i++){
		if(is_prime(i)){
			all_primes.push_back(i);
		}
	}
 
	//iterate all the primes to find the one that has the longest chain 
	int max_len = 0,max_prime=0;
	
	vector<int>::iterator it;
	for(it=all_primes.begin();it!=all_primes.end();it++){
		int len = calc_len(*it);
		if(len > max_len){
			max_len = len;
			max_prime = *it;
			cout<<max_prime<<" "<<max_len<<endl;
		}
	}
 
	cout<<"The prime has longest chain is: "<<max_prime<<endl;
 
	return 0;
}

 

方法2   [思路同分析] 用一种快速的方法获得100万以下的所有质数。然后依次统计以2开头的链,以3开头的链…找到最长的链长 【很快,<1s】

#include "stdafx.h"
#include <iostream>
#include <vector>
#include <math.h>
 
using namespace std;
 
#define N 1000000
 
int main()
{    
	int max = 0;
	vector<int> primes;
 
	bool prime[N];
	for(int i = 0 ; i < N ; i++) {
		prime[i] = true;   //i is initially regarding as a prime
	}
 
	for(int i = 2 ; i < N ; i++){
		if (prime[i]){
			primes.push_back(i);
			if(i <= (int)sqrt((double)N)){    // i*i <= N
				for(int t = i*i ; t<N ; t+=i){ //set all i*i+k*i -> false
					prime[t] = false;
				}
			}
		}
	}
 
	int maxlen = 0, maxsum = 0, cursum = 0;
	for(int i = 2 ; i < primes.size() ; i++){
		cursum = 0;
		for(int j = i ; j < primes.size() ; j++)
		{
			cursum+=primes[j];
			if (cursum>=N)
				break;
			if ( prime[cursum] && j - i > maxlen )
			{
				maxlen = j-i;
				maxsum = cursum;
			}
		}
	}
 
	cout<<maxsum<<endl;;
	return 0;
};

Python

方法1 [思路同分析] 保存所有小于100万的质数,计算每个质数的最长连续质数链长,找到链最长的质数。

#coding=utf-8

from math import sqrt

def is_prime(n):
    if n <= 1:
        return False
    elif n == 2 or n == 3:
        return True
    elif n % 2 == 0:
        return False
    else:
        for i in range(3, int(sqrt(n) + 1)):
            if n % i == 0:
                return False
        return True

#store all the primes below one-million
all_primes = [x for x in range(2,1000000) if is_prime(x)]
#print all_primes

#return the max length of prime n, and the start number
def calc_len(n):
    start, nxt, sum = 0, 0, 0     #start: the start index of the chain. nxt: the end index of the chain
    while True:
        if sum == n:
            ret = (all_primes[start], nxt - start) #if n=2,then nxt=1,start=0,len = nxt-start
            return ret
        elif sum > n:
            while sum > n:
                sum -= all_primes[start]
                start += 1
        else:
            sum += all_primes[nxt]
            nxt += 1
            #print start,nxt,sum,'c'

#find the longest chain of a prime below 1000000
max_len, start,max_prime = 0,0,0
for p in all_primes:
    ret = calc_len(p)
    if ret[1] > max_len:
        max_len, start, max_prime = ret[1], ret[0], p
        #print max_prime,max_len

print max_len, start, max_prime 
方法2 [思路同分析] C\C++方法2 的Python版本
#coding=utf-8N=1000000
#find all the primes below one-million
prime = [True for i in range(1,N+1)]
primes = []
for i in range(2, N):
    if prime[i]:
        primes.append(i)
        t = i*i
        while t < N:
            prime[t] = False
            t += i
#find the longest chain
max_len, max_sum = 0, 0
prime_cnt = len(primes)
for i in range(2, prime_cnt):
    cur_sum = 0
    for j in range(i, prime_cnt):
        cur_sum += primes[j]
        if cur_sum >= N:
            break
        if prime[cur_sum] and (j - i > max_len):
            max_len, max_sum = j-i, cur_sum
print max_sum,max_len

答案

997651

注:链的长度为543,链的起始质数为7

Windows 串口通信简单实例

工具软件

为了方便串口程序的调试,使用了如下两款工具软件。

[1]. 串口调试助手

clipboard.png

该软件可以通过串口收发数据。可以通过串口发送数据,也可以查看串口收到的数据。【下载地址

[2]. 虚拟串口

clipboard1.png

可以虚拟串口,每次虚拟出一对串口,这对串口相互连通。如图中虚拟出了串口COM2和COM3,这样通过COM2发送的数据,会由COM3接收到,反之亦然。【下载地址

串口通信的基本步骤

(1) 通过CreateFile(“COMx:“,…) 打开串口

(2) 通过配置DCB结构体和SetCommState函数,设置串口的参数。

(3) 通过ReadFile()和WriteFile 读写串口

Windows 串口通信实例

封装了一个串口通信的C++类CSerial,通过CSerial类的OpenSerialPort()可以打开一个串口,串口打开后后自动新建线程读取串口数据,并通过MessageBox简单的显示出数据。通过CSerial类的SendData()方法可以向串口发送数据。

主函数中,新建了一个CSerial类对象,打开串口2,然后简单的通过一个消息框循环来控制向串口不断的发送数据。

程序的效果图如下:

clipboard2.png

使用虚拟串口工具虚拟出串口对COM2和COM3,本实例程序读写COM2,使用串口调试助手打开COM3。

-> 在“是否向串口发送数据”消息框中,点击”是(Y)” 会向串口发送一条”This is a example” 数据。如图,在串口调试助手中收到该数据包

-> 在串口调试助手中,手动发送”jarvischu”,程序会读取到该数据并弹出消息框显示。

源码如下:

Serial.h

#pragma once

#include <windows.h>

class CSerial
{
public:
	CSerial(void);
	~CSerial(void);

	//打开串口
	BOOL OpenSerialPort(TCHAR* port,UINT baud_rate,BYTE date_bits,BYTE stop_bit,BYTE parity=NOPARITY);

	//发送数据
	BOOL SendData(char* data,int len);
public:
	HANDLE m_hComm;
};

Serial.cpp

#include "StdAfx.h"
#include "Serial.h"
#include <process.h>

typedef unsigned (__stdcall *PTHREAD_START) (void *);

CSerial::CSerial(void)
{
	m_hComm = INVALID_HANDLE_VALUE;
}

CSerial::~CSerial(void)
{
	if(m_hComm != INVALID_HANDLE_VALUE){
		CloseHandle(m_hComm);
	}
}

/*********************************************************************************************
 * 功能    :	读串口线程回调函数 
 * 描述	   :	收到数据后,简单的显示出来
 ********************************************************************************************/
DWORD WINAPI CommProc(LPVOID lpParam){

	CSerial* pSerial = (CSerial*)lpParam;  //

	//清空串口
	PurgeComm(pSerial->m_hComm,PURGE_RXCLEAR|PURGE_TXCLEAR);

	char buf[512];
	DWORD dwRead;
	while(pSerial->m_hComm != INVALID_HANDLE_VALUE){
		BOOL bReadOK = ReadFile(pSerial->m_hComm,buf,512,&dwRead,NULL);
		if(bReadOK && (dwRead > 0)){
			buf[dwRead] = '\0';
			MessageBoxA(NULL,buf,"串口收到数据",MB_OK);
		}
	}
	return 0;
}

/*******************************************************************************************
 * 功能     :	打开串口
 * port     :	串口号, 如_T("COM1:")
 * baud_rate:	波特率
 * date_bits:	数据位(有效范围4~8)
 * stop_bit :	停止位
 * parity   :	奇偶校验。默认为无校验。NOPARITY 0; ODDPARITY 1;EVENPARITY 2;MARKPARITY 3;SPACEPARITY 4
 ********************************************************************************************/
BOOL CSerial::OpenSerialPort(TCHAR* port,UINT baud_rate,BYTE date_bits,BYTE stop_bit,BYTE parity){
	//打开串口
	m_hComm = CreateFile(port,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);//独占方式打开串口

	TCHAR err[512];

	if(m_hComm == INVALID_HANDLE_VALUE){
		_stprintf(err,_T("打开串口%s 失败,请查看该串口是否已被占用"),port);
		MessageBox(NULL,err,_T("提示"),MB_OK);
		return FALSE;
	}	

	//MessageBox(NULL,_T("打开成功"),_T("提示"),MB_OK);

	//获取串口默认配置
	DCB dcb;
	if(!GetCommState(m_hComm,&dcb)){
		MessageBox(NULL,_T("获取串口当前属性参数失败"),_T("提示"),MB_OK);
	}

	//配置串口参数
	dcb.BaudRate = baud_rate;	//波特率
	dcb.fBinary = TRUE;			//二进制模式。必须为TRUE
	dcb.ByteSize = date_bits;	//数据位。范围4-8
	dcb.StopBits = ONESTOPBIT;	//停止位

	if(parity == NOPARITY){
		dcb.fParity = FALSE;	//奇偶校验。无奇偶校验
		dcb.Parity = parity;	//校验模式。无奇偶校验
	}else{
		dcb.fParity = TRUE;		//奇偶校验。
		dcb.Parity = parity;	//校验模式。无奇偶校验
	}

	dcb.fOutxCtsFlow = FALSE;	//CTS线上的硬件握手
	dcb.fOutxDsrFlow = FALSE;	//DST线上的硬件握手
	dcb.fDtrControl = DTR_CONTROL_ENABLE; //DTR控制
	dcb.fDsrSensitivity = FALSE;
	dcb.fTXContinueOnXoff = FALSE;//
	dcb.fOutX = FALSE;			//是否使用XON/XOFF协议
	dcb.fInX = FALSE;			//是否使用XON/XOFF协议
	dcb.fErrorChar = FALSE;		//是否使用发送错误协议
	dcb.fNull = FALSE;			//停用null stripping
	dcb.fRtsControl = RTS_CONTROL_ENABLE;//
	dcb.fAbortOnError = FALSE;	//串口发送错误,并不终止串口读写

	//设置串口参数
	if (!SetCommState(m_hComm,&dcb)){
		MessageBox(NULL,_T("设置串口参数失败"),_T("提示"),MB_OK);
		return FALSE;
	}

	//设置串口事件
	SetCommMask(m_hComm,EV_RXCHAR); //在缓存中有字符时产生事件
	SetupComm(m_hComm,16384,16384);

	//设置串口读写时间
	COMMTIMEOUTS CommTimeOuts;
	GetCommTimeouts(m_hComm,&CommTimeOuts);
	CommTimeOuts.ReadIntervalTimeout = MAXDWORD;
	CommTimeOuts.ReadTotalTimeoutMultiplier = 0;
	CommTimeOuts.ReadTotalTimeoutConstant = 0;
	CommTimeOuts.WriteTotalTimeoutMultiplier = 10;
	CommTimeOuts.WriteTotalTimeoutConstant = 1000;

	if(!SetCommTimeouts(m_hComm,&CommTimeOuts)){
		MessageBox(NULL,_T("设置串口时间失败"),_T("提示"),MB_OK);
		return FALSE;
	}

	//创建线程,读取数据
	HANDLE hReadCommThread = (HANDLE) _beginthreadex(NULL,0,(PTHREAD_START) CommProc,(LPVOID) this,0,NULL);

	return TRUE;
}

/********************************************************************************************
 * 功能    :	通过串口发送一条数据
 ********************************************************************************************/
BOOL CSerial::SendData(char* data,int len){
	if(m_hComm == INVALID_HANDLE_VALUE){
		MessageBox(NULL,_T("串口未打开"),_T("提示"),MB_OK);
		return FALSE;
	}

	//清空串口
	PurgeComm(m_hComm,PURGE_RXCLEAR|PURGE_TXCLEAR);

	//写串口
	DWORD dwWrite = 0;
	DWORD dwRet = WriteFile(m_hComm,data,len,&dwWrite,NULL);

	//清空串口
	PurgeComm(m_hComm,PURGE_RXCLEAR|PURGE_TXCLEAR);

	if(!dwRet){
		MessageBox(NULL,_T("发送数据失败"),_T("提示"),MB_OK);
		return FALSE;
	}
	return TRUE;
}

主程序  ManipulateCom.cpp

#include <windows.h>
#include "Serial.h"
#include <string.h>

int main(int argc, _TCHAR* argv[])
{
	CSerial serial;
	serial.OpenSerialPort(_T("COM2:"),9600,8,1);  //打开串口后,自动接收数据

	//向串口发送数据
	char* data = "This is a example\n";
	int ret = 1;
	while(ret != IDNO ){
		serial.SendData(data,strlen(data));
		ret = MessageBox(NULL,_T(""),_T("是否向串口发送数据"),MB_YESNO); //YES继续发送一条数据,NO不发送,退出
	}
	return 0;
}

Windows 核心编程 学习笔记 (第四部分)

目录

19. 用户态的线程同步方式

  •     互锁函数
  •     循环锁
  •     CRITICAL_SECTION 临界区

20. 内核态的线程同步方式

  •     内核对象用于线程同步
  •     等待函数
  •     Event – 事件
  •     WaitableTimer – 等待定时器
  •     Semaphore – 信号量
  •     Mutex – 互斥对象
  •     其他线程同步函数

21. 一个互斥写和共享读锁的实现例子

19. 用户态的线程同步方式

(1) 互锁函数 – InterLocked 函数族

常用的互锁函数有InterLockedAdd, InterLockedExchange,InterLockedExchangeAdd,InterLockedCompareExchange等

//原子操作:加法 c <- a+b
LONG __cdecl InterlockedAdd(		//返回 和
	__inout  LONG volatile *Addend, //指向被加数的指针
	__in     LONG Value             //加数
	);
 
//原子操作:交换  a <- b
LONG __cdecl InterlockedExchange(	//返回 Target交换之前的值
	__inout  LONG volatile *Target, //指向要交换的变量的指针
	__in     LONG Value				//用来交换的值
	);
 
//原子操作:对一个变量做加法 a += b
LONG __cdecl InterlockedExchangeAdd( //返回加之前的值
	__inout  LONG volatile *Addend,  //指向要做加法的变量的指针
	__in     LONG Value              //叠加的值
	);
 
//原子操作:先比较,如果Destination==Comparand,则将Exchange赋值给Destination,否则不赋值
LONG __cdecl InterlockedCompareExchange(  //返回Destination的原始值
	__inout  LONG volatile *Destination,
	__in     LONG Exchange,
	__in     LONG Comparand
	);

互锁函数运行速度很快,通常只需几个CPU周期(通常小于50)的执行时间,并且不会从用户方式转化为内核方式。

(2) 循环锁

BOOL g_fResourceInUse = FALSE; //全局标识,用于互斥访问
 
void Fun(){
	//循环等待,直到标识的前一状态是FALSE
	while (InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE){
		Sleep(0); //
	}
 
	//...访问互斥资源
	
	//标识置为 FALSE
	InterlockedExchange(&g_fResourceInUse,FALSE);
}

缺点:浪费CPU时间。CPU会不断的执行循环(即便使用Sleep,也会存在浪费,因为Sleep(0) 实际是只是放弃当前的CPU时间片,线程依然是可调度的)。可以通过延长Sleep的时间来减少CPU时间的浪费。

应该避免在单个CPU的计算机上使用循环锁。在多处理器的计算机上可以使用,因为此时一个线程循环执行时,另一个线程可以在另一个CPU上运行。

(3) CRITICAL_SECTION 临界区 (关键代码段)

[参考 C++线程同步总结(笔记)]

优点:内部使用了互锁函数,所以运行效率高。

缺点:无法对多个进程中的线程进程同步

[1] 定义临界区

CRITICAL_SECTION 结构体的定义在WinNT.h中

typedef struct _RTL_CRITICAL_SECTION {
	PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
 
	//
	//  The following three fields control entering and exiting the critical
	//  section for the resource
	//
 
	LONG LockCount;
	LONG RecursionCount;
	HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
	HANDLE LockSemaphore;
	ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

我们不需要关心其内部的成员变量。只要会用CRITICAL_SECTION 即可。

CRITICAL_SECTION cs;   //[1]定义临界区结构体

[2] 初始化临界区 – InitializeCritialSection

void WINAPI InitializeCriticalSection(            //初始化临界区
	__out  LPCRITICAL_SECTION lpCriticalSection  //指向临界区变量的指针
	);

如果线程进入未初始化的临界区,其结果是未定义的。

InitializeCriticalSection(&cs); //[2]初始化临界区

还可以使用InitializeCriticalSectionAndSpinCount 函数初始化一个自旋的临界区。所谓自旋,指当一个线程试图进入一个已锁的临界区,线程会进入一个循环,检测锁是否被释放,如果锁没有被释放,则线程进入sleep.(自旋就是循环检测,如果已锁,则sleep一下,醒来继续检测)

BOOL WINAPI InitializeCriticalSectionAndSpinCount(
	__out  LPCRITICAL_SECTION lpCriticalSection,  //临界区
	__in   DWORD dwSpinCount                      //自旋次数,即循环的次数
	);

在单处理器系统上,自旋次数参数会被忽略且设置为0。在多处理器系统上,如果临界区已锁,线程会自旋dwSpinCount次,如果自旋dwSpinCount次后锁仍为释放,则进入等待(wait)。[sleep会自动唤醒,wait需等待其它线程唤醒]

[3] 进入临界区 – EnterCriticalSection

void WINAPI EnterCriticalSection(                   //进入临界区:等待(堵塞),直到获取锁
	_Inout_  LPCRITICAL_SECTION lpCriticalSection
	);

进入等待状态的线程不消耗CPU。系统会记住该线程想要访问该资源且自动更新CRITICAL_SECTION的成员变量,一旦目前访问该资源的线程调用了LeaveCriticalSection,等待的线程就切换到可调度状态。

还可以使用TryEnterCriticalSection 函数,无论临界区是否已锁,都会立即返回。

BOOL WINAPI TryEnterCriticalSection(              //如果当前线程成功进入临界区或已经进入临界区,返回非0值。
	_Inout_  LPCRITICAL_SECTION lpCriticalSection //如果临界区被其他线程占用,则返回0
	);

[4] 退出临界区 – LeaveCriticalSection

void WINAPI LeaveCriticalSection(
	_Inout_  LPCRITICAL_SECTION lpCriticalSection
	);

一个线程可以多次EnterCriticalSection 而不会死锁,但一个Enter必须对应一个Leave。

[5] 删除临界区 – DeleteCriticalSection

void WINAPI DeleteCriticalSection(
	_Inout_  LPCRITICAL_SECTION lpCriticalSection
	);

—————————————————————————————————————————————

临界区使用的技巧

-> 每个共享资源使用一个CRITICAL_SECTION

-> 同时Enter多个临界区时,注意Enter和Leave的顺序,避免形成死锁。如果两个线程同时执行下面的代码,会造成死锁。

EnterCriticalSection(&cs1);
EnterCriticalSection(&cs2);
//...
LeaveCriticalSection(&cs1);  //会产生死锁。应该先Leave cs2,再Leave cs1.
LeaveCriticalSection(&cs2);

-> 不要长时间运行临界区。临界区访问越短越好。

20. 内核态的线程同步方式

(1) 内核对象用于线程同步

Windows中的一些内核对象拥有已通知(TRUE)和未通知(FALSE) 两种状态(即有信号和无信号两种状态),可以用于线程同步。每个对象的状态切换都有自己的一套规则。

这些内核对象包括:进程、线程、作业、文件、控制台输入、文件修改通知、可等待定时器、事件、Semaphore、Mutex。

以进程为例:进程总是在未通知状态中创建,正在运行的进程对象处于未通知状态,进程终止时就变味已通知状态。

所以可以使用WaitForSingleObject 等待进程的句柄来判断进程是否终止。

其中:可等待定时器、Event、Semaphore、Mutex 是Windows提供的专门用于线程同步的内核对象。

当线程等待的对象处于未通知状态,则线程不可调度的,当等待的对象变为已通知状态,则线程切换到可调度状态。

(2) 等待函数

等待函数使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态。

[1] WaitForSingleObject – 等待一个内核对象

DWORD WINAPI WaitForSingleObject( //等待一个内核对象变为已通知状态
	_In_  HANDLE hHandle,      //等待的内核对象句柄
	_In_  DWORD dwMilliseconds //等待时间(单位ms),可以传INFINITE表示一直等待,直到等待对象变为已通知
	);</pre>
<pre>DWORD dwRet = WaitForSingleObject(hThread,5000);
 
switch(dwRet){
case WAIT_OBJECT_0:
	//等待内核对象变为已通知。如线程已结束
	break;
case WAIT_TIMEOUT:
	//等待超时
	break;
case WAIT_FAILED:
	//函数调用失败(如INVALID_HANDLE)。使用GetLastError获取具体信息.
	break;
}

[2] WaitForMultipleObjects – 等待多个内核对象

DWORD WINAPI WaitForMultipleObjects( //等待多个内核对象
	_In_  DWORD nCount,           //等待的内核对象个数。最大为MAXIMUM_WAIT_OBJECTS,且不能为0
	_In_  const HANDLE *lpHandles,//等待的内核对象数组。
	_In_  BOOL <b>bWaitAll</b>,//TRUE:所有等待的内核对象都为已通知状态才返回。FALSE:只要有一个内核对象为已通知状态就返回
	_In_  DWORD dwMilliseconds    //等待时间(单位ms)
	);</pre>
<pre>HANDLE h[2];
h[0] = hThread1;
h[1] = hThread2;
 
DWORD dwRet = WaitForMultipleObjects(2,h,FALSE,5000);
switch(dwRet){
case WAIT_OBJECT_0:
	//hThread1 为已通知状态。(即Thread1线程结束)
	break;
case WAIT_OBJECT_0 + 1:
	//hThread2 为已通知状态。(即Thread2线程结束)
	break;
case WAIT_TIMEOUT:
	//等待超时
	break;
case WAIT_FAILED:
	//函数调用失败(如INVALID_HANDLE)。使用GetLastError获取具体信息.
	break;
case WAIT_ABANDONED_0 + 1:
	//hThread2 内核对象被遗弃
	break;	
}

[3] 成功等待的副作用

对于某些内核对象,成功的调用了WaitForSingleObject和WaitForMultipleObject时会改变对象的状态,这就是成功等待的副作用。

不同的内核对象的成功等待的副作用不一样。如自动重置事件(AutoResetEvent)对象的成功等待的副作用是 即WaitFor函数返回前,该对象会被置为未通知状态。有些内核对象如线程、进程成功等待也不会有副作用

等待不成功绝不会有副作用。

-> WaitForMultipleObject 是以原子方式运行的,可以避免死锁的产生。如两个线程同时运行下面的代码:

HANDLE h[2];
h[0] = hAutoResetEvent1; //初始为未通知状态
h[1] = hAutoResetEvent2; //初始为未通知状态
WaitForMultipleObjects(2,h,TRUE,INFINITE);

初始时两个对象都为未通知状态,两个线程都进入等待。

然后,当hAutoResetEvent1变为已通知时,两个线程都能发现该状态改变,但是都无法唤醒,因为hAutoResetEvent2仍处于未通知,所有两个线程都没有成功等待,不会对hAutoResetEvent1对象产生副作用(即没有将它置为未通知状态)

然后,当红AutoResetEvent2变为已通知时,两个线程中的一个线程发现该变化,两个对象都变为已通知状态,成功等待,由于副作用的存在,会把两个对象会置为未通知状态,该线程变为可调度线程。此时,另一个线程将继续等待,直到前一个线程的WaitForMultpleObject函数返回,两个对象重新变会已通知状态。

这样就避免了两个线程的死锁。

如果WaitForMultipleObject不是原子操作的,那么可能发生一个线程将hAutoResetEvent1置为未通知状态,另一个线程将hAutoResetEvent2置为未通知状态,两个线程就会互相等待,形成死锁。

【至于两个线程同时发现内核对象已通知状态后,系统究竟会执行哪个,这个是不确定的。Microsoft只说使用的算法是公平的,但具体的算法不知道。实际操作是FIFO算法,等待时间最长的线程获得该对象,但也不是绝对】

(3) Event – 事件内核对象

Event是最基本的内核对象。包括一个使用计数(所有内核对象都有)、一个指明该Event是自动重置事件还是手动重置事件的BOOL值、一个指明该Event处在已通知状态还是未通知状态的BOOL值。

-> 自动重置事件:事件被设置为已通知(有信号)状态后,会唤醒一个等待线程,然后自动恢复为未通知(无信号)状态。

-> 手动重置事件:事件被设置为已通知状态后,事件会保持已通知状态,直到使用ResetEvent重置为未通知状态。当事件处于已通知状态时,所有等待事件的线程都会等到事件,并被唤醒为可调度状态。

可见两者的区别在于:自动重置事件一次只能唤醒一个等待线程,手动重置事件会唤醒所有的等待线程

[自动重置事件会有成功等待副作用(如上),手动重置事件没有成功等待副作用]

[1] 创建事件对象 – CreateEvent

HANDLE WINAPI CreateEvent( //创建事件对象,返回其句柄
	_In_opt_  LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,可以为NULL
	_In_      BOOL bManualReset, //TRUE表示手动重置事件,FALSE表示自动重置事件
	_In_      BOOL bInitialState,//初始状态,TRUE表示已通知,FALSE表示未通知
	_In_opt_  LPCTSTR lpName     //内核对象名称,可以为NULL
	);

[2] 设置事件对象状态 – SetEvetn,ResetEvent

BOOL SetEvent( __in HANDLE hEvent );  //事件hEvent置为已通知状态。成功返回非0,否则返回0
BOOL ResetEvent( __in HANDLE hEvent );//事件hEvent置为未通知状态。成功返回非0,否则返回0

(4) WaitableTimer – 等待定时器内核对象

等待定时器对象是一个在某个时间或每隔规定时间间隔变为有信号(已通知)状态的内核对象。

-> 自动重置等待定时器(又称同步定时器):一个等待线程等到之后,会置为未通知状态。只能唤醒一个等待线程。

-> 手动重置等待定时器:会保持已通知状态,直到SetWaitableTimer。可以唤醒所有的等待线程。

[1] CreateWaitableTimer – 创建等待定时器内核对象

HANDLE WINAPI CreateWaitableTimer(  //创建等待定时器内核对象,初始为未通知状态
	_In_opt_  LPSECURITY_ATTRIBUTES lpTimerAttributes,
	_In_      BOOL bManualReset,  //TRUE:手动重置等待定时器,FALSE:自动重置等待定时器
	_In_opt_  LPCTSTR lpTimerName
	);

[2] SetWaitableTimer – 设置等待定时器内核对象

刚创建的定时器对象是未通知状态的,必须使用SetWaitableTimer设置其合适变为已通知状态

BOOL WINAPI SetWaitableTimer(   _In_      HANDLE hTimer,                  //等待定时器对象句柄
	_In_      const LARGE_INTEGER *pDueTime,  //第一次报时的时间,和FILETIME结构体格式相同,UTC时间。正值表示绝对时间,负值表示相对时间
	_In_      LONG lPeriod,                   //每隔多少毫秒报时一次。0表示只报时一次。
	_In_opt_  PTIMERAPCROUTINE pfnCompletionRoutine, //APC函数。(相当于回调函数)
	_In_opt_  LPVOID lpArgToCompletionRoutine, //传递给APC函数的参数
	_In_      BOOL fResume //用于支持暂停和恢复的计算机。通常设为FALSE。
	);

-> pDueTime。正值表示一个绝对时间,负值表示相对时间,即SetWaitableTimer函数调用后多久报时。

->fResume。TRUE表示当定时器报时时,它是计算机摆脱暂停状态(如果它处在暂停状态的话),并唤醒等待定时器的线程。FALSE表示当定时器报时时,如果计算机处于暂停状态,它唤醒的线程必须等到计算机恢复运行后才能被调度。

绝对时间报时的例子

#include <windows.h>
 
HANDLE hTimer;  //定时器
 
//线程回调函数
DWORD WINAPI ThreadProc(LPVOID lpParam){
	int nCnt = 0;
	while(TRUE){
		DWORD dwRet = WaitForSingleObject(hTimer,INFINITE);
		if(dwRet == WAIT_OBJECT_0){
			MessageBox(NULL,_T("报时"),_T("提示"),MB_OK);
			nCnt++;
		}
		if(nCnt >= 10 ) break; //10次后结束
	}
 
	return 0;
}
 
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine,int nCmdShow)
{
	
	SYSTEMTIME st;
	FILETIME ftLocal,ftUTC;
	LARGE_INTEGER liUTC;
 
	//创建自动重置的等待定时器
	hTimer = CreateWaitableTimer(NULL,FALSE,NULL);
 
	//设定定时器时间(绝对时间) 2014-5-7 10:23:0
	st.wYear = 2014;
	st.wMonth = 5;
	st.wDayOfWeek = 0; //忽略
	st.wDay = 7;
	st.wHour = 10;
	st.wMinute = 23;
	st.wSecond = 0;
	st.wMilliseconds = 0;
 
	SystemTimeToFileTime(&st,&ftLocal);      //系统时间转化为本地FileTime
	LocalFileTimeToFileTime(&ftLocal,&ftUTC);//本地FileTime转换为UTC FileTime
 
	liUTC.LowPart = ftUTC.dwLowDateTime;
	liUTC.HighPart = ftUTC.dwHighDateTime;
 
	//设置等待定时器的时间
	SetWaitableTimer(hTimer,&liUTC,10*1000,NULL,NULL,FALSE);//每隔10s报时一次
 
 
	//启动线程
	HANDLE hThread;
	hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);
 
	//等待线程结束
	WaitForSingleObject(hThread,INFINITE);//等待线程结束
 
	CloseHandle(hThread);
	CloseHandle(hTimer);
	return 0;
}

相对时间报时的例子:与绝对时间类似,只是时间设置不同

LARGE_INTEGER li;
 
//创建自动重置的等待定时器
hTimer = CreateWaitableTimer(NULL,FALSE,NULL);
 
//设定定时器时间(相对时间) 5s之后报时
const int nTimerUnitsPerSecond = 10000000; //每秒包含的 100ns 个数
li.QuadPart = -(5 * nTimerUnitsPerSecond);  //5s
 
//设置等待定时器的时间
SetWaitableTimer(hTimer,&li,10*1000,NULL,NULL,FALSE);//每隔10s报时一次

[3] CancelWaitableTimer – 取消定时器

不再报时

[4] WaitableTimer 和 SetTimer比较

SetTimer 创建的定时器称为用户定时器。

-> 需要使用窗口UI结构,产生WM_TIMER消息,消息只能被调用线程获取,即只有一个线程能得到通知。WM_TIMER是优先级最低的消息,只有消息队列中没有其他消息时,才会获取该消息。

WaitableTimer

-> 内核对象,可供多个线程共享,且是安全的。可以唤醒多个等待线程。

(5) Semaphore – 信号量内核对象

信号量用于对资源进行计数,可以实现资源的互斥和共享,能够控制同时访问资源的线程数量。

信号量有两个计数:一个是最大的资源数量,一个是当前资源数量。

-> 当前资源数量 >0,则发出信号

-> 当前资源数量 =0,则不发出信号

-> 当前资源数量不会 < 0

-> 当前资源数量不能超过最大资源数量

-> Wait函数(WaitForSingleObject,WaitForMultipleObject)成功等待,会自动将信号量的当前资源数量 -1

[1] CreateSemaphore – 创建信号量

HANDLE WINAPI CreateSemaphore(  //创建信号量
	_In_opt_  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //
	_In_      LONG lInitialCount,	//初始资源数量
	_In_      LONG lMaximumCount,	//最大资源数量
	_In_opt_  LPCTSTR lpName		//
	);

[2] ReleaseSemaphore – 释放信号量

BOOL WINAPI ReleaseSemaphore(   //释放信号量
	_In_       HANDLE hSemaphore,
	_In_       LONG lReleaseCount,    //释放资源的数量
	_Out_opt_  LPLONG lpPreviousCount //释放前的资源数量
	);

使用Wait函数是信号量当前资源数量-1。使用多次Wait函数可以递减当前资源数量多次。

ReleaseSemaphore可以是当前资源数量增加lReleaseCount个,如果当前资源数量+lReleaseCount > 最大资源数量,则函数不会修改当前资源数量的值,并且返回FALSE。

[3] Semaphore的两个典型应用场景

-> 应用程序使用信号量来限制同时使用资源的线程数量。线程在使用资源前使用Wait函数等待资源可用,Wait成功等待后,会递减当前资源的数量,线程在使用完资源后,使用ReleaseSemaphore递增当前资源数量。

-> 应用程序在初始化时使用信号量。应用程序创建一个初始资源数量为0的信号量,这样所有其他要访问资源的线程都会堵塞等待,当应用程序初始化完成后,使用ReleaseSemaphore释放最大资源数量个资源,允许等待的线程访问资源。

(6) Mutex- 互斥对象内核对象

互斥对象能够保证对单个资源的互斥访问。属于最常用的内核对象之一。

包括:一个使用数量(同其他内核对象一样)、一个线程ID(指明当前拥有Mutex的线程,0表示没有线程拥有该Mutex)、一个递归计数器(同一个线程拥有Mutex的次数)。

-> 线程ID为0,表示互斥对象不被任何线程拥有,互斥对象处于已通知状态

-> 线程ID非0,表示互斥对象已被线程拥有,互斥对象处于未通知状态。

如果当前拥有Mutex的线程再次等待该Mutex,系统判断线程ID与Mutex的线程ID相同,系统会使该线程继续保持可调度状态,Mutex的递归计数器值递增。ReleaseMutex使递归计数器递减,如果递归计数器值变为0,则Mutex的线程ID为被置为0,Mutex处于未通知状态。

[1] CreateMutex – 创建互斥对象

HANDLE WINAPI CreateMutex(  //创建互斥对象
	_In_opt_  LPSECURITY_ATTRIBUTES lpMutexAttributes,
	_In_      BOOL bInitialOwner, //当前线程是否是互斥对象的初始拥有者,通常设为FALSE:不拥有
	_In_opt_  LPCTSTR lpName
	);

[2] ReleaseMutex – 释放互斥对象

BOOL WINAPI ReleaseMutex(
	_In_  HANDLE hMutex
	);

互斥对象有个“线程所有权”的概念,如果不是拥有Mutex的线程调用ReleaseMutex释放,操作会失败。

如果拥有Mutex的线程直到终止都没有释放Mutex,那么系统会将Mutex视为已经被放弃的,等待的线程会得到一个WAIT_ABANDONED的返回值,表明拥有Mutex的线程直到终止都未释放Mutex,此时共享资源的额情况是未知的,但Mutex是可用的,获得通知的线程决定自身后续怎么做。

case WAIT_ABANDONED:
	//拥有等待的内核对象的线程直到终止都没有释放内核对象.内核对象会自动被调用线程拥有,初始状态为未通知。

(7) 其他的线程同步函数

[1] WaitForInputIdle – 等待进程,直到其处于空闲状态

DWORD WINAPI WaitForInputIdle(  //等待进程hProcess,直到该进程在<b>创建应用程序的第一个窗口的线程</b>中已经没有尚未处理的输入为止。
	_In_  HANDLE hProcess,      //即该进程第一个创建窗口的线程已初始化完成,并处于等待用户输入状态。
	_In_  DWORD dwMilliseconds
	);
如父进程创建了子进程后,想要获取子进程的创建的窗口的句柄,就可以通过调用WaitForInputIdle等待子进程窗口的初始化完成。
#include <windows.h>
 
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine,int nCmdShow)
{
	STARTUPINFO si = {sizeof(si)};
	PROCESS_INFORMATION pi;
	//CreateProcess(_T("C:\\Windows\\System32\\cmd.exe"),NULL,NULL,NULL,FALSE,NULL,NULL,NULL,&si,&pi);         //总是返回WAIT_FAILED,因为cmd没有窗口
	CreateProcess(_T("C:\\Program Files\\TTPlayer\\TTPlayer.exe"),NULL,NULL,NULL,FALSE,NULL,NULL,NULL,&si,&pi);//正常,返回0
	
	DWORD dwRet = WaitForInputIdle(pi.hProcess,INFINITE);
	switch(dwRet){
	case 0:
		MessageBox(NULL,_T("子进程初始化成功"),_T("提示"),MB_OK);
		break;
	case WAIT_TIMEOUT:
		MessageBox(NULL,_T("子进程初始化超时"),_T("提示"),MB_OK);
		break;
	case WAIT_FAILED:
		MessageBox(NULL,_T("子进程初始化失败"),_T("提示"),MB_OK);
		break;
	}
 
	WaitForSingleObject(pi.hProcess,INFINITE);
 
	return 0;
}

[2] MsgWaitForMultipleObject – 等待内核对象 和 消息

DWORD WINAPI MsgWaitForMultipleObjects(  //等待内核对象和消息(类似于WaitForMultipleObjects,不过可以等待消息)
	_In_  DWORD nCount,				//等待句柄的个数
	_In_  const HANDLE *pHandles,	//等待句柄
	_In_  BOOL bWaitAll,			//是否等到所有句柄都为已通知状态才返回
	_In_  DWORD dwMilliseconds,		//
	_In_  DWORD dwWakeMask			//等待的消息类型,QS_ALLEVENTS,QS_ALLINPUT,QS_HOTKEY
	);

21. 一个互斥写和共享读锁的实现例子

#include <stdio.h>
#include <Windows.h>
#include <process.h>
#include <time.h>
 
#define THREAD_COUNT 10
 
typedef unsigned (__stdcall *PTHREAD_START) (void *);
 
//控制一个线程写,多个线程读
class SingleWriteMultipleReadGuard{
public:
	SingleWriteMultipleReadGuard();
	~SingleWriteMultipleReadGuard();
 
	void WaitToRead();  //调用本函数以获取 共享读锁
	void WaitToWrite(); //调用本函数以获取 排他写锁
	void Done();		//释放资源
private:
	CRITICAL_SECTION m_cs;	//控制成员函数操作的原子性
	int m_nActive;			//共享资源的当前状态。0表示没有线程在访问,>0 表示当前读取资源的线程数量。
							//负数表示当前有一个线程正在写资源,唯一有效负值是-1
	int m_nWaitingReaders;	//想要读资源的线程数量。初始为0,当m_nActive为-1时,调用一次WaitToRead,该值+1
	int m_nWaitingWriters;	//想要写资源的线程数量。初始为0,当m_nActive不为0时,调用一次WaitToWrite,该值+1
	HANDLE m_hsemReaders;	//读锁的信号量
	HANDLE m_hsemWriters;	//写锁的信号量
};
 
//构造函数
SingleWriteMultipleReadGuard::SingleWriteMultipleReadGuard(){
	m_nWaitingReaders = m_nWaitingWriters = m_nActive = 0;	//初始化等待读写的线程和当前读写线程的数量都为0
	m_hsemReaders = CreateSemaphore(NULL,MAXLONG,MAXLONG,NULL);	//读锁的信号量,初始资源数为最大,可控资源为最大
	m_hsemWriters = CreateSemaphore(NULL,1,MAXLONG,NULL);		//写锁的信号量,初始资源数为1,可控资源为最大
	InitializeCriticalSection(&m_cs);
}
 
//析构函数
SingleWriteMultipleReadGuard::~SingleWriteMultipleReadGuard(){
	DeleteCriticalSection(&m_cs);
	CloseHandle(m_hsemReaders);
	CloseHandle(m_hsemWriters);
}
 
//等待 读
void SingleWriteMultipleReadGuard::WaitToRead(){
	//进入临界区。防止其他线程访问成员变量
	EnterCriticalSection(&m_cs);
 
	//判断当前是否有线程在等待写资源,或正在写资源 (保证了写操作优先)
	BOOL fResourceWritePending = (m_nWaitingWriters || (m_nActive < 0)); //m_nActive=-1表示有线程正在写
 
	if(fResourceWritePending){  //有等待写资源的线程或正在写资源的线程
		m_nWaitingReaders ++;	//等待读资源的线程数量 ++
	}else{                      //可以读
		m_nActive++;			//当前读资源的额线程数量++
	}
 
	//离开临界区。允许其他线程发出读写资源请求
	LeaveCriticalSection(&m_cs);
 
	//等待读信号量 有信号
	WaitForSingleObject(m_hsemReaders,INFINITE);
}
 
//等待 写
void SingleWriteMultipleReadGuard::WaitToWrite(){
	//进入临界区。防止其他线程访问成员变量
	EnterCriticalSection(&m_cs);
 
	//判断是否有其他线程在读\写资源
	BOOL fResourceOwned = (m_nActive != 0); //资源是否被其他线程占用
 
	if(fResourceOwned){         //资源被其他线程占用
		m_nWaitingWriters ++;
	}else{						//资源空闲
		m_nActive = -1;			
	}
 
	//离开临界区。允许其他线程发出读写资源请求
	LeaveCriticalSection(&m_cs);
 
	//资源被占用,等待
	if(fResourceOwned){
		WaitForSingleObject(m_hsemWriters,INFINITE);
	}
}
 
//读写完成,释放资源
void SingleWriteMultipleReadGuard::Done(){
	//进入临界区。防止其他线程访问成员变量
	EnterCriticalSection(&m_cs);
 
	if(m_nActive > 0){  //都是读线程
		m_nActive --;   //读线程数量 --
	}
	else if(m_nActive == -1){ //当前要释放的是写线程
		m_nActive = 0;
	}
 
	HANDLE hsem = NULL; //保存要释放读信号量还是写信号量
	LONG lCount = 1;    //保存要释放的资源数量。写信号量为1
 
	//释放之后,没有线程读写资源,则唤醒等待的读或写线程
	if(m_nActive == 0){		
		if(m_nWaitingWriters > 0){  //有线程等待写,则唤醒一个写进程
			m_nActive = -1;
			m_nWaitingWriters --;
			hsem = m_hsemWriters;
		}else if(m_nWaitingReaders > 0){  //有线程等待读,则唤醒所有的读线程
			m_nActive = m_nWaitingReaders; //
			m_nWaitingReaders = 0;
			hsem = m_hsemReaders;
			lCount = m_nActive;            //释放m_nActive个,唤醒所有等待读的线程
		}
	}
 
	//离开临界区
	LeaveCriticalSection(&m_cs);
 
	//释放资源
	if(hsem != NULL){
		ReleaseSemaphore(hsem,lCount,NULL);
	}
}
 
 
SingleWriteMultipleReadGuard guard;
 
//读线程 回调函数
DWORD WINAPI ReadThreadProc(LPVOID lpParam){
	printf("wait to read \n");
	guard.WaitToRead();
	printf("read\n");
	guard.Done();
	printf("read done\n\n");
	return 0;
}
 
//写线程回调函数
DWORD WINAPI WriteThreadProc(LPVOID lpParam){
	printf("wait to write\n");
	guard.WaitToWrite();
	printf("write\n");
	guard.Done();
	printf("write done\n\n");
	return 0;
}
 
int main()
{
	int i=0;
	HANDLE h[THREAD_COUNT];
 
	//随机产生读写线程
	srand((unsigned int)time(0));
	for (i=0;i<THREAD_COUNT;i++){
		if(rand()%2){
			h[i] = (HANDLE)_beginthreadex(NULL,0,(PTHREAD_START) ReadThreadProc,NULL,0,NULL);
		}else{
			h[i] = (HANDLE)_beginthreadex(NULL,0,(PTHREAD_START) WriteThreadProc,NULL,0,NULL);
		}
	}
 
	WaitForMultipleObjects(THREAD_COUNT,h,TRUE,INFINITE);
	for (i=0;i<THREAD_COUNT;i++){
		CloseHandle(h[i]);
	}
 
	printf("End\n");
	return 0;
}

Windows 核心编程 学习笔记 (第三部分)

目录

16. 作业

  •     作业
  •     一个简单例程
  •     CreateJobObject 创建作业
  •     作业限制和 SetInformationJobObject
  •     AssignProcessToJobObject 将进程添加到作业
  •     终止作业
  •     QueryInformationJobObject 查询作业的统计信息
  •     作业的通知消息

17.  线程

  •     线程
  •     线程的回调函数(入口函数)
  •     CreateThread和_beginthreadex的区别
  •     CreateThread 创建线程
  •     线程的终止
  •     线程创建和初始化的细节
  •     _beginthreadex函数

18. 线程调度

  •     线程挂起和恢复
  •     Sleep函数
  •     切换线程
  •     线程执行时间
  •     CONTEXT上下文
  •     Windows线程调度
  •     进程优先级
  •     线程相对优先级
  •     动态提高线程的优先级
  •     亲缘性

16. 作业

(1) 作业

[MSDN] 作业对象允许一组进程被当做一个单元进行管理。作业对象是可命名的、安全的、共享的对象,它能够控制它包含的所有进程的属性。执行在作业上的操作会影响作业包含的所有进程。

作业可视为进程的容器,可以对其中的所有进程加上限制条件。

使用CrateJobObject函数,创建一个作业

使用SetInformationJobObject函数,为作业添加限制条件

使用AssignProcessToJobObject函数,将进程添加到作业

使用IsProcessInJob函数,判断一个进程是否属于一个作业。

(2) 一个简单例程

#include <windows.h>
#include <tchar.h>
 
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR lpCmdLine,int nShowCmd){
 
	//创建一个作业内核对象
	HANDLE hJob = CreateJobObject(NULL,NULL); //
 
	////////////////////////////////////////////////////////////
	//为作业添加一些基本限制
 
	//基本限制结构体
	JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = {0};
 
	//作业的优先级
	jobli.PriorityClass = IDLE_PRIORITY_CLASS; //
 
	//作业的CPU时间限制
	jobli.PerJobUserTimeLimit.QuadPart = 10000000; //1秒,单位是100纳秒
 
	//指明限制条件
	jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS|JOB_OBJECT_LIMIT_JOB_TIME;
 
	//设定作业限制
	SetInformationJobObject(hJob,JobObjectBasicLimitInformation,&jobli,sizeof(jobli));
 
	////////////////////////////////////////////////////////////
	//为作业添加一些基本UI限制
 
	//基本UI限制结构体
	JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;
 
	//初始无限制
	jobuir.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE; //
 
	//增加限制:作业(进程)不能注销操作系统
	jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;
 
	//增加限制:作业(进程)不能访问 系统的用户对象(如其他窗口)
	jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;
 
	//设定作业限制
	SetInformationJobObject(hJob,JobObjectBasicUIRestrictions,&jobuir,sizeof(jobuir));
 
	////////////////////////////////////////////////////////////
	//创建进程,并添加到作业中。进程初始化时必须是挂起状态,保证在添加到作业前不会执行任何代码
 
	//创建进程
	STARTUPINFO si={sizeof(si)};
	PROCESS_INFORMATION pi;
	CreateProcess(_T("C:\\Windows\\System32\\cmd.exe"),NULL,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi); //CREATE_SUSPENDED
 
	//将进程添加到作业
	AssignProcessToJobObject(hJob,pi.hProcess);
 
	//唤醒进程(的主线程)
	ResumeThread(pi.hThread);
	
	//关闭句柄
	CloseHandle(pi.hThread);
 
	////////////////////////////////////////////////////////////
	//等待进程结束或作业CPU时间耗完
	HANDLE h[2];
	h[0] = pi.hProcess;
	h[1] = hJob;
 
	DWORD ret = WaitForMultipleObjects(2,h,FALSE,INFINITE);
	switch(ret-WAIT_OBJECT_0){
		case 0:
			//进程结束
			MessageBox(NULL,_T("进程结束"),_T("提示"),MB_OK);
			break;
		case 1:
			//作业分配的CPU时间耗完
			MessageBox(NULL,_T("时间耗尽"),_T("提示"),MB_OK);
			break;
	}
 
	//关闭句柄
	CloseHandle(pi.hProcess);
	CloseHandle(hJob);
 
	return 0;
}

(3) CreateJobObject 创建作业

HANDLE WINAPI CreateJobObject( //创建作业内核对象
	__in_opt  LPSECURITY_ATTRIBUTES lpJobAttributes, //安全结构体
	__in_opt  LPCTSTR lpName   //名称,可以为NULl
	);

(4)作业限制 和 SetInformationJobObject

作业限制类型有:基本限制、扩展限制、UI限制、安全性限制

使用SetInformationJobObject可以为作业指定限制。

BOOL WINAPI SetInformationJobObject(  //设置作业限制
	__in  HANDLE hJob,                            //要添加限制的作业
	__in  JOBOBJECTINFOCLASS JobObjectInfoClass,  //限制的类型
	__in  LPVOID lpJobObjectInfo,                 //限制的值
	__in  DWORD cbJobObjectInfoLength             //限制的值的长度
	);
限制类型
说明
第二个参数的值
第三个参数的结构
基本限制
CPU分配限制
JobObjectBasicLimitInformation
JOBOBJECT_BASIC_LIMIT_INFORMATION
扩展限制
基本限制+内存分配限制
JobObjectExtendedLimitInformation
JOBOBJECT_EXTENDED_LIMIT_INFORMATION
基本UI限制
防止作业中进程改变UI
JobObjectBasicUIRestictions
JOBOBJECT_BASIC_UI_RESTRICTIONS
安全性限制
防止作业中进程访问保密资源
JobObjectSecurityLimitInformation
JOBOBJECT_SECURITY_LIMIT_INFORMATION

[1] 基本限制

//基本限制:CPU限制
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION {
	LARGE_INTEGER PerProcessUserTimeLimit; //如果LimitFlags含有JOB_OBJECT_LIMIT_PROCESS_TIME,则此参数表示分配给每个进程的用户模式执行时间,单位100ns.超时进程会被终止
	LARGE_INTEGER PerJobUserTimeLimit;     //如果LimitFlags含有JOB_OBJECT_LIMIT_JOB_TIME,则此参数表示分配给作业的用户模式执行时间,超时作业会被终止
	DWORD         LimitFlags;              //指明哪些限制对作业有效
	SIZE_T        MinimumWorkingSetSize;   //如果LimitFlags含有JOB_OBJECT_LIMIT_WORKINGSET,则此参数表示作业中每个进程的最小工作集大小
	SIZE_T        MaximumWorkingSetSize;   //同上,最大工作集大小
	DWORD         ActiveProcessLimit;      //如果LimitFlags含有JOB_OBJECT_LIMIT_ACTIVE_PROCESS,则此参数表示作业中可以同时运行的最大进程数量
	ULONG_PTR     Affinity;                //如果LimitFlags含有JOB_OBJECT_LIMIT_AFFINITY,则此参数表示能够运行的进程的CPU子集
	DWORD         PriorityClass;           //如果LimitFlags含有JOB_OBJECT_LIMIT_PRIORITY_CLASS,则此参数表示作业中所有进程的优先级
	DWORD         SchedulingClass;         //如果LimitFlags含有JOB_OBJECT_LIMIT_SCHEDULING_CLASS,则此参数表示相同优先级的作业的调度优先级(0-9,默认5),值越大,CPU时间越长
} JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;

[2] 扩展限制

//扩展限制:基本限制+内存限制
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
	JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; //基本限制
	IO_COUNTERS                       IoInfo;                //保留不用。IO计数器
	SIZE_T                            ProcessMemoryLimit;    //每个进程能使用的内存量(“基本限制”参数的LimitFlags需含有JOB_OBJECT_LIMIT_PROCESS_MEMORY)
	SIZE_T                            JobMemoryLimit;        //作业(所有进程)能使用的内存量(“基本限制”参数的LimitFlags需含有JOB_OBJECT_LIMIT_JOB_MEMORY )
	SIZE_T                            PeakProcessMemoryUsed; //只读。单个进程需要使用的内存最大值
	SIZE_T                            PeakJobMemoryUsed;     //只读。作业需要使用的内存最大值
} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION;

[3] 基本UI限制

//基本UI限制
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS {
	DWORD UIRestrictionsClass; //下表标志中一个或是组合
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;
说明
JOB_OBJECT_UILIMIT_EXITWINDOWS
防止进程通过ExitWindowsEx函数退出、关闭、重启或关闭系统电源
JOB_OBJECT_UILIMIT_READCLIPBOARD
防止进程读取剪切板的内容
JOB_OBJECT_UILIMIT_WRITECLIPBOARD 防止进程写剪切板内容
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS
防止进程通过SystemParametersInfor函数来改变系统参数
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS
防止进程通过ChangeDisplaySettings函数来改变显示设置
JOB_OBJECT_UILIMIT_GLOBALATOMS
防止进程访问全局的基本结构表,为作业分配自己的基本结构表,作业中进程只能访问该表。
JOB_OBJECT_UILIMIT_DESKTOP
防止进程使用CreateDesktop或SwitchDesktop函数创建或转换桌面
JOB_OBJECT_UILIMIT_HANDLES
防止进程使用作业外部的进程创建的用户对象的句柄(如HWND)

[4] 安全性限制

Windows XP(不包括XP)之后的系统不再支持该限制,需要为每个进程单独指定安全设置。

(5) AssignProcessToJobObject 将进程添加到作业

要添加到作业的进程在创建时,需使用CREATE_SUSPEND标志,防止加入作业前进程执行任何代码。

BOOL WINAPI AssignProcessToJobObject(  __in  HANDLE hJob,    //作业句柄
	__in  HANDLE hProcess //进程句柄
	);

一个进程加入到一个作业后,不能再转到另一个作业。

作业中的进程生成的新进程会自动成为作业的一部分。可以通过下面两种方法改变这种特性:

[1] 打开JOBOBJECT_BASIC_LIMIT_INFROMATION 的LimitFlags成员的JOB_OBJECT_BREAKAWAY_OK标志,告诉系统,新生成的进程可以在作业外部运行。同时使用CREATE_BREAKAWAY_FROM_JOB 标志调用CreateProcess创建新进程

[2] 打开JOBOBJECT_BASIC_LIMIT_INFROMATION 的LimitFlags成员的JOB_OBJECT_SILENT_BREAKAWAY_OK标志,告诉系统,新生成的进程可以在作业外部运行。

(6) 终止作业

BOOL WINAPI TerminateJobObject(
	__in  HANDLE hJob,    //作业
	__in  UINT uExitCode  //退出码。作业中所有进程的退出码自动设为uExitCode
	);

(7) QueryInformationJobObject 查询作业的统计信息

(8) 作业的通知消息

创建一个IO完成端口(IO Completion Port)内核对象,然后将作业对象或多个作业对象与完成端口关联起来(使用SetInformationJobObject函数),然后让一个或多个线程在完成端口上等待作业通知的到来。

#include <windows.h>
#include <process.h>  //_beginthreadex
#include <tchar.h>
 
#define CMPKEY_JOBOBJECT 1
#define CMPKEY_TERMINATE 2
 
typedef unsigned (__stdcall *PTHREAD_START) (void *);
 
//IO完成端口监听线程回调函数
DWORD WINAPI JobNotify(LPVOID lpParam)
{
	HANDLE hIOCP = (HANDLE)lpParam;
 
	while (TRUE)
	{
		DWORD dwBytesTransferred;
		ULONG_PTR CompKey;
		LPOVERLAPPED po;
 
		//从IO完成端口中获取一个消息
		GetQueuedCompletionStatus(hIOCP,&dwBytesTransferred,&CompKey,&po,INFINITE);
 
		//退出消息
		if (CompKey == CMPKEY_TERMINATE)
		{
			MessageBox(NULL,_T("监听线程退出"),_T("提示"),MB_OK);
			break;
		}
 
		//来自作业对象hJob的消息
		if(CompKey == CMPKEY_JOBOBJECT)
		{
			MessageBox(NULL,_T("收到来自作业的消息"),_T("提示"),MB_OK);
 
			switch(dwBytesTransferred){
			case JOB_OBJECT_MSG_END_OF_JOB_TIME:
				MessageBox(NULL,_T("作业限制时间耗尽"),_T("提示"),MB_OK);
				break;
			case JOB_OBJECT_MSG_END_OF_PROCESS_TIME:
				{
					TCHAR szProcessName[MAX_PATH];
					HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ,FALSE,(DWORD)po);
 
					if(hProcess == NULL){
						_stprintf(szProcessName,_T("%s"),_T("未知进程名"));
					}
					else{
						DWORD dwSize = (DWORD)MAX_PATH;
						QueryFullProcessImageName(hProcess,0,szProcessName,&dwSize);
						CloseHandle(hProcess);
					}
 
					TCHAR info[MAX_PATH];
					_stprintf(info,_T("进程%s(ID=%d)限制时间耗尽 "),szProcessName,po);
 
					MessageBox(NULL,info,_T("提示"),MB_OK);
				}
				break;
			case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT:
				MessageBox(NULL,_T("运行的进程超过限制"),_T("提示"),MB_OK);
				break;
			case JOB_OBJECT_MSG_NEW_PROCESS:
				MessageBox(NULL,_T("作业中产生新进程"),_T("提示"),MB_OK);
				break;
			case JOB_OBJECT_MSG_EXIT_PROCESS:  {
					TCHAR szProcessName[MAX_PATH];
					HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ,FALSE,(DWORD)po);
 
					if(hProcess == NULL){
						_stprintf(szProcessName,_T("%s"),_T("未知进程名"));
					}
					else{
						DWORD dwSize = (DWORD)MAX_PATH;
						QueryFullProcessImageName(hProcess,0,szProcessName,&dwSize);
						CloseHandle(hProcess);
					}
 
					TCHAR info[MAX_PATH];
					_stprintf(info,_T("进程%s(ID=%d)终止 "),szProcessName,po);
 
					MessageBox(NULL,info,_T("提示"),MB_OK);
				}
				break;
			}
		}
	}
	return 0;
}
 
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR lpCmdLine,int nShowCmd){
 
	//创建一个作业内核对象
	HANDLE hJob = CreateJobObject(NULL,NULL); //
 
	//创建一个IO完成端口
	HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
 
	//创建一个线程监听IO完成端口通知消息
	HANDLE hThreadIOCP = (HANDLE)_beginthreadex(NULL,0,(PTHREAD_START)JobNotify,(LPVOID)hIOCP,0,NULL);
 
	//将IO完成端口与作业关联
	JOBOBJECT_ASSOCIATE_COMPLETION_PORT jobacp;
	jobacp.CompletionKey = (PVOID)CMPKEY_JOBOBJECT;  //任意一个全局唯一的值
	jobacp.CompletionPort = hIOCP;                   //IO完成端口句柄
	SetInformationJobObject(hJob,JobObjectAssociateCompletionPortInformation,&jobacp,sizeof(jobacp)); //关联
	
	////////////////////////////////////////////////////////////
	//创建进程,并添加到作业中。进程初始化时必须是挂起状态,保证在添加到作业前不会执行任何代码
 
	STARTUPINFO si={sizeof(si)};
	PROCESS_INFORMATION pi;
	CreateProcess(_T("C:\\Windows\\System32\\cmd.exe"),NULL,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi); //CREATE_SUSPENDED
	AssignProcessToJobObject(hJob,pi.hProcess);//将进程添加到作业
	MessageBox(NULL,_T("111"),_T("Tips"),MB_OK);
	ResumeThread(pi.hThread);//唤醒进程(的主线程)
	CloseHandle(pi.hThread); //关闭句柄
	CloseHandle(pi.hProcess);
	MessageBox(NULL,_T("MESSAGE"),_T("Tips"),MB_OK);
 
	//发送一条消息给IO完成端口,结束IO完成端口线程
	PostQueuedCompletionStatus(hIOCP,0,CMPKEY_TERMINATE,NULL);
 
	//等待IO完成端口线程终止
	WaitForSingleObject(hThreadIOCP,INFINITE);
 
	//关闭句柄
	CloseHandle(hIOCP);
	CloseHandle(hThreadIOCP);
	CloseHandle(hJob);
	return 0;
}

17. 线程

(1) 线程

线程由两部分组成

-> 线程的内核对象。操作系统用来管理线程的数据结构,也是用来存放线程统计信息的结构

-> 线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量。

进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。

(2) 线程回调函数(入口函数)

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
	DWORD dwResult = 0;
	//...
	return dwResult;
}

(3) CreateThread 和_beginthreadex 区别

-> CreateThread是Windows API函数,_beginthreadex是C\C++ Runtime Libarary 函数。

-> _beginthreadex内部调用了CreateThread. [源码在thread.c中]

[理解]

C\C++运行库中一些函数使用了全局变量(如errno),直接使用CreateThread会出现多线程同步时的不安全,_beginthreadex则为这些全局变量做了处理,放到了一个_tiddata结构体中,并传递给了线程,使得每个线程了都有一份独立的“全局变量”(TLS,Thread Local Storage)。

[如果使用C\C++ Runtime Libaray函数(更准确的说是使用了_tiddata结构体的函数),则应该使用_beginthreadex,防止内存泄露和多线程不安全问题] 具体的情况有:

-> 使用了malloc和free,或者new和delete

-> 使用了stdio.h或io.h中的函数

-> 使用了浮点变量或浮点运算函数

-> 调用了任何一个使用了静态缓冲区的Runtime函数,如asctime(),strtok()或rand()

(4) CreateThread 创建线程

HANDLE WINAPI CreateThread(
	__in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes, //NULL,或者将其的bInheritHandle成员置为TRUE后传入,使该线程内核对象句柄可被子进程继承。
	__in       SIZE_T dwStackSize,                       //线程的堆栈大小(单位字节)。0表示使用默认的堆栈大小
	__in       LPTHREAD_START_ROUTINE lpStartAddress,    //线程回调函数
	__in_opt   LPVOID lpParameter,                       //传递给回调函数的参数
	__in       DWORD dwCreationFlags,                    //标志。0,创建后立即运行;CREATE_SUSPENDED,挂起,调用ResumeThread时再运行;
	__out_opt  LPDWORD lpThreadId                        //线程ID
	);

-> dwStackSize 参数,设置线程的堆栈大小,0表示使用系统默认的大小

在Visual Studio 2010中,默认的线程堆栈大小是 1MB,由链接器的/Stack:reserve 开关控制。可以在Liker ->System ->Stack Reserve Size中修改默认的堆栈大小,单位是字节B。

clipboard.png

(5) 线程的终止

-> 线程回调函数的返回 (推荐): 能正确释放所有C++对象,正确释放线程堆栈,设置线程退出码,递减线程内核对象的计数

-> ExitThread : 不能释放C++对象

-> TerminateThread:异步函数,函数返回时线程不一定终止了,可使用WaitForSingleObject来等待终止。父进程终止时才会撤销线程的堆栈

(6) 线程创建和初始化的细节

clipboard1.png

BaseThreadStart是一个为文档化的函数,它首先创建一个结构化异常处理帧(SHE,使线程产生的异常能得到系统默认处理),然后调用线程的回调函数,线程返回时,调用ExitThread。

(7) _beginthreadex 函数

uintptr_t _beginthreadex( // NATIVE CODE
	void *security,
	unsigned stack_size,
	unsigned ( __stdcall *start_address )( void * ),
	void *arglist,
	unsigned initflag,
	unsigned *thrdaddr  );

_beginthreadex 只存在于C\C++ Runtime Libarary 的多线程版本中。_beginthreadex的参数类型不依赖Windows API,参数功能与CreateThread大致相同,可以通过宏实现转换

typedef unsigned (__stdcall *PTHREAD_START) (void *);
 
#define chBEGINTHREADEX(psa, cbStackSize, pfnStartAddr, \
   pvParam, dwCreateFlags, pdwThreadId)                 \
      ((HANDLE)_beginthreadex(                          \
         (void *)        (psa),                         \
         (unsigned)      (cbStackSize),                 \
         (PTHREAD_START) (pfnStartAddr),                \
         (void *)        (pvParam),                     \
         (unsigned)      (dwCreateFlags),               \
         (unsigned *)    (pdwThreadId)))

_beginthreadex 内部首先创建一个tiddata结构体,然后将回调函数的地址(函数名)和参数保存到tiddata结构体,然后调用CreateThead函数,CreateThread函数中回调函数为_threadstartex,传给_threadstartex的参数为tiddata结构体。

18. 线程调度

每个线程都拥有一个上下文结构体(CONTEXT),该结构体保存在线程的内核对象中,该结构体保存了线程上次运行时该线程的CPU寄存器的状态。每隔20ms左右,Windows查看当前存在的所有线程内核对象,CPU选择一个可调度的内核对象(不需要调度的线程如:暂停计数器 >= 1,即处于暂停状态的线程;等待事件发生的线程),将它加载到CPU寄存器中,这个操作称为上下文切换(Context Swiche).

(1) 线程挂起和恢复

一个线程可以挂起若干次(最大为MAXIMUM_SUSPEND_COUNT),如线程被挂起3次,则必须恢复3次,它才可以被分配CPU。使用CREATE_SUSPEND标志创建一个挂起状态的线程或者DWORD SuspendThread(HANDLE hThread) 可以挂起一个线程。ResumeThread可以恢复一次。

(2) Sleep 函数

VOID WINAPI Sleep(	__in  DWORD dwMilliseconds	);

-> 系统保证在dwMilliseconds 时间内不调度线程,但过后不保证马上唤醒

-> dwMilliseconds = INFINITE,永不调度线程。最好不要这样做

-> dwMilliseconds = 0, 线程放弃剩余的时间片,迫使系统调度另一个线程

(3) 切换线程

BOOL WINAPI SwitchToThread(void);

让操作系统调度另一个线程,如果发生切换,返回非0值,没有切换,返回0。

(4) 线程执行时间

BOOL WINAPI GetThreadTimes(
	__in   HANDLE hThread,             //线程句柄
	__out  LPFILETIME lpCreationTime,  //创建时间
	__out  LPFILETIME lpExitTime,      //退出时间。如果线程仍在运行,则为未定义
	__out  LPFILETIME lpKernelTime,    //内核时间(单位100ns)
	__out  LPFILETIME lpUserTime       //用户时间 (单位100ns)
	);

 

#include <windows.h>
#include <process.h>  //_beginthreadex
#include <tchar.h>
 
DWORD WINAPI ThreadFun(LPVOID lpParam)
{
	DWORD dwResult = 0;
	return dwResult;
}
 
//FILETIME 抓换为 __int64
__int64 FileTimeToQuadWord(PFILETIME pft) {
	return (Int64ShllMod32(pft->dwHighDateTime, 32) | pft->dwLowDateTime);
}
 
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR lpCmdLine,int nShowCmd){
 
	HANDLE hThread = CreateThread(NULL,0,ThreadFun,NULL,0,NULL);
 
	FILETIME ftCreate;
	FILETIME ftExit;
	FILETIME ftKernel;
	FILETIME ftUser;
 
	//获取线程的FILETIME时间
	BOOL ret = GetThreadTimes(hThread,&ftCreate,&ftExit,&ftKernel,&ftUser);
	if(!ret)
		MessageBox(NULL,_T("获取时间失败"),_T("提示"),MB_OK);
 
	//创建时间 - 转化为本地时间
	SYSTEMTIME stUTCCreate,stLocalCreate;
	FileTimeToSystemTime(&ftCreate,&stUTCCreate);                     //FILETIME转换为UTC时间
	SystemTimeToTzSpecificLocalTime(NULL,&stUTCCreate,&stLocalCreate);//UTC时间转换为本地时间	
 
	TCHAR msg_create[256];
	_stprintf(msg_create,_T("创建时间:%d-%d-%d %d:%d:%d %d"),stLocalCreate.wYear,stLocalCreate.wMonth,stLocalCreate.wDay,
		stLocalCreate.wHour,stLocalCreate.wMinute,stLocalCreate.wSecond,stLocalCreate.wMilliseconds);
	MessageBox(NULL,msg_create,_T("时间"),MB_OK);
 
 
	WaitForSingleObject(hThread,INFINITE);  //等待线程结束,再统计内核时间
 
	//内核时间 - 转化为具体数值	
	__int64 kernelTime = FileTimeToQuadWord(&ftKernel);
 
	TCHAR msg_kernel[256];
	_stprintf(msg_kernel,_T("内核时间:%I64d"),kernelTime);
	MessageBox(NULL,msg_kernel,_T("时间"),MB_OK);
 
	CloseHandle(hThread);
	return 0;
}

(5) CONTEXT 上下文

CONTEXT结构体包含了特定处理器的寄存器数据。系统使用CONTEXT结构执行各种内部操作。目前已经存在为Intel、MIPS、Alpha、PowerPC处理器定义的CONTEXT结构。具体定义在WinNT.h 中。

(6) Windows 线程调度

每个线程都会被赋予一个从0(最低)到31(最高)的优先级。

系统引导时,会创建一个特殊的线程,称为0页线程,优先级为0,它是整个系统中唯一一个优先级为0的线程。当系统没有任何线程需要执行时,0页线程负责将系统中的所有空闲RAM页面置0

-> Microsoft 没有将调度程序的行为特性完全固定下来

-> Microsoft 没有让应用程序充分利用调度程序的特性

-> Microsoft 声称调度程序的算法是变化的,在编写代码时应有所准备。

(7) 进程优先级

Windows 中定义了如下6个优先级类

优先级
标识符
实时
REALTIME_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
高于正常
ABOVE_NORMAL_PRIORITY_CLASS
正常
NORMAL_PRIORITY_CLASS
低于正常
BELOW_NORMAL_PRIORITY_CLASS
空闲
IDLE_PRIORITY_CLASS

->CreateProcess时,通过标识位参数设置,标识位参数取值为上表中一个。

-> 通过SetPriorityClass函数设置优先级。

SetPriorityClass(GetCurrentProcess(),IDLE_PRIORITY_CLASS);

通过GetPriorityClass函数获取优先级

DWORD GetPriorityClass(HANDLE hProcess); 

(8) 线程的相对优先级

clipboard2.png

线程的相对优先级有7个等级:关键时间、最高、高于正常、正常、低于正常、最低、空闲。

进程的优先级类和线程相对优先级的映射

线程相对优先级
空闲
低于正常
正常
高于正常
实时
线程相对优先级标识符
关键时间
15
15
15
15
15
31
THREAD_PRIORITY_TIME_CRITICAL
最高
6
8
10
12
15
26
THREAD_PRIORITY_HIGHEST
高于正常
5
7
9
11
14
25
THREAD_PRIORITY_ABOVE_NORMAL
正常
4
6
8
10
13
24
THREAD_PRIORITY_NORMAL
低于正常
3
5
7
9
12
23
THREAD_PRIORITY_BELOW_NORMAL
最低
2
4
6
8
11
22
THREAD_PRIORITY_LOWEST
空闲
1
1
1
1
1
16
THREAD_PRIORITY_IDLE

通过SetThreadPriority函数设置线程的优先级.

BOOL WINAPI SetThreadPriority(
	__in  HANDLE hThread,
	__in  int nPriority   //线程相对优先级标识符
	);

GetThreadPriority 函数返回线程的相对优先级。

(9) 动态提高线程的优先级

综合考虑线程的相对优先级和线程的进程优先级,系统就可以确定线程的优先级等级,称为线程的基本优先级等级。

系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等IO事件作出响应。

系统只能为基本优先级等级在1~15之间的线程提高其优先级等级(称为动态优先级范围),但等级绝不会提高到实时范围(等级高于15)。实时范围的线程能够执行大多数操作系统的函数,因此给等级的提高规定一个范围,就可以防止应用程序干扰操作系统的运行。

通过SetProcessPriorityBoost可以启用或停用系统自动临时提升线程优先级的功能。

BOOL WINAPI SetProcessPriorityBoost(  //停用或启用系统自动对进程中线程优先级的临时提升
	__in  HANDLE hProcess,            __in  BOOL DisablePriorityBoost  //TRUE 停用,FALSE 启用
	);

通过GetProcessPriorityBoost可以获取当前的启用或停用状态。

(10) 亲缘性

-> 软亲缘性: 按照默认设置,系统将线程分配给CPU时,如果其他因素相同,它会优先在上次运行的CPU上运行线程。[让线程留在单个CPU上,有助于重复使用仍在CPU cache中的数据]

-> 硬亲缘性: 直接控制线程在某个CPU上运行。

[1] SetProcessAffinityMask 设置进程的CPU亲缘性

BOOL WINAPI SetProcessAffinityMask( //设置进程所有线程的CPU亲缘性
	__in  HANDLE hProcess,
	__in  DWORD_PTR dwProcessAffinityMask //亲缘性掩码。位屏蔽,指明可以在哪些CPU上运行。
	);

如 dwProcessAffinityMask = 0x00000005,则表明只可以在CPU0 和 CPU1上运行(5->101).

通过GetProcessAffinityMask函数可以获取进程的亲缘性掩码。

[2] SetThreadAffinityMask 设置线程的亲缘性

DWORD_PTR WINAPI SetThreadAffinityMask( //设置线程的CPU亲缘性
	__in  HANDLE hThread,
	__in  DWORD_PTR dwThreadAffinityMask//亲缘性掩码
	);

强制给线程分配一个CPU的做法,有时不妥当。可以通过SetThreadIdealProcessor函数为线程选择一个理想的CPU

DWORD WINAPI SetThreadIdealProcessor(  //设置线程的首选CPU
	__in  HANDLE hThread,
	__in  DWORD dwIdealProcessor       //CPU的编号。如0表示CPU0,1表示CPU1。最大为MAXIMUM_PROCESSORS (32)
	);

Windows 核心编程 学习笔记 (第二部分)

目录

8. 进程

  • 进程定义
  • Windows支持两种类型的应用程序
  • Windows应用程序的入口地址

9. WinMain\wWinMain函数

  • 进程的实例句柄
  • 命令行

10. 进程的环境变量

11. 进程的错误模式

12. 操作系统的版本

13. CreateProcesse函数

14. 进程的终止

15. 子进程

8. 进程

(1) 进程定义

进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:

-> 操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程统计信息的地方

-> 地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态分配的内存。如线程堆栈和堆分配空间。

当创建进程时,系统会自动创建它的第一个线程,称为主线程。

(2) Windows支持两种类型的应用程序

-> 基于图像用户界面的GUI应用程序

-> 基于控制台用户界面的CUI应用程序

链接程序根据开关(/SUBSYSTEM:CONSOLE  或 /SUBSYSTEM:WINDOWS)将相应的子系统嵌入产生的可执行程序。

(3) Windows应用程序的入口地址

Windows应用程序入口有四种,WimMain\wWinMain\main\wmain。但实际上操作系统并不调用你编写的入口函数。它调用的是C\C++ Runtime  Startup 函数,该函数负责对C\C++ Runtime Library 进行初始化,这样就可以调用malloc和free之类的函数。它还能确保已经声明的任何全局对象和静态C++对象能够子啊代码执行以前正确的创建。

应用程序类型 入口点 嵌入可执行文件的启动函数
需要ANSI字符和字符串的GUI应用程序 WinMain WinMainCRTStartup
需要Unicode字符和字符串的GUI应用程序 wWinMain wWinMainCRTStartup
需要ANSI字符和字符串的CUI应用程序 main mainCRTStartup
需要Unicode字符和字符串的CUI应用程序 wmain wmainCRTStartup

链接程序负责在它链接可执行文件时选择相应的C\C++ Rumtime Startup 函数。

如果设定了/SUBSYSTEM:WINDOWS,则链接程序期望找到一个WinMain或wWinMain函数,然后分别选择WinMainCRTStartup或wWinMainCRTStartup函数。/SUBSYSTEM:CONSOLE 同理。

如果程序没有设定/SUBSYSTEM:WINDOWS 或 /SUBSYSTEM:CONSOLE, 链接程序会根据代码中(main,wmain,WinMain,wWinMain)自动确定应该链接到哪个子系统。

如果要写一个控制台程序,却新建了一个Win32项目,可有如下4种解决方法:

->[1] 将main函数改为WinMain                   【不好,WinMain就不是我们想要的控制台程序了】

->[2] 新建一个控制台项目,将源码拷贝过去 【不好,麻烦】

->[3] 将Project Setting 的Link选项中,将/SUBSYSTEM:WINDOWS 改为 /SUBSYSTEM:CONSOLE。【较好】

->[4] 将Project Setting 的Link选项中,删除/SUBSYSTEM:WINDOWS,让链接程序自动选择。【推荐】

下面是在VC6.0 和VS2010中具体设置的位置

clipboard.png clipboard1.png

Startup 函数功能

四个Runtime Startup 函数基本功能相同,区别只在处理ANSI字符串还是Unicode字符串,以及在对C\C++ Runtime Library进行初始化后调用哪个入口函数。这四个 Startup函数都包含在crt0.c源文件中,它们的功能如下:

-> 检索指向新进程的完整命令行的指针

-> 检索指向新进程的环境变量的指针

-> 对C\C++ Runtime 的全局变量进行初始化。如果包含了StdLib.h文件,代码就能访问这些变量。

-> 为所有全局和静态的C++类对象调用构造函数。

-> 之后,调用应用程序的入口函数(main,wmain,WinMain,wWinMain)。调用的方式分别如下:

wWinMain

GetStartupInfo(&StartupInfo);
int nMainRetVal = wWinMain(GetModuleHandle(NULL),NULL,pszCommandLineUnicode,
	StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT);

WinMain

GetStartupInfo(&StartupInfo);
int nMainRetVal = WinMain(GetModuleHandle(NULL),NULL,pszCommandLineAnsi,
	StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT);

wmain

int nMainRetVal = wmain(__argc,__wargv,_wenviron);

main

int nMainRetVal = main(__argc,__argv,_environ);

->入口函数返回后,Startup函数调用C\C++ Runtime 的 exit函数,并将nMainRetVal传递给它。exit函数功能如下:

[1] 调用由_onexit函数的调用而注册的任何函数。

[2] 为所有的全局的和静态的C++类对象调用析构函数

[3] 调用操作系统的ExitProcess函数,将nMainRetVal传递给它。这使得该操作系统能够撤销进程并设置它的exit代码。

9. WinMain\wWinMain 函数

int WINAPI WinMain(  HINSTANCE	hInstance,		//当前应用程序实例的句柄
	HINSTANCE  hPrevInstance,	//当前应用程序前一个实例的句柄,必须置为NULL
	LPSTR  lpCmdLine,		//命令行参数    [wWinMain的区别 LPWSTR]
	int  nCmdShow		//窗口显示标志
	);

(1) 进程的实例句柄

加载到进程地址空间的每一个可执行文件或DLL文件均被赋予一个独一无二的实例句柄。

可执行文件的实例句柄(当前进程的实例)作为(w)WinMain的第一个参数hInstance传递进入该函数。对于加载资源的函数调用来说,通常需要该句柄,如LoadIcon(HINSTANCE hInstance, PCTSTR pszIcon)  第一个参数指明哪个文件(可执行文件或DLL文件)包含你想加载的资源。

HINSTANCE 和 HMODULE 完全相同,16位Windows中不同

WinMain的hInstance参数实际值是系统将可执行文件的映像加载到进程的地址空间时使用的基地址。这个基地址的值有链接程序决定,不同的链接程序可以使用不同的默认基地址。

Visual C++ 6.0 是 0x00400000

Visual Studio 2010 是 不确定的。

clipboard2.png

GetModuleHandle函数

使用GetModuleHandle 函数可以返回可执行文件或DLL加载到进程地址空间时所用的句柄(即基地址)

HMODULE WINAPI GetModuleHandle(
	__in_opt  LPCTSTR lpModuleName //可执行文件或DLL文件的名称,NULL返回当前程序实例句柄
	);

GetModuleHandle只查看调用进程的地址空间,一个模块(可执行文件或DLL文件)虽然已经加载到系统了,但是没有加载到本进程,GetModuleHandle依然返回NULL。

(2) 命令行

创建进程时,会传递一个命令行,该命令行的第一个参数就是程序的完整名称。当C\C++ Runtime Startup函数运行时,它会跳过程序的名称,将其余部分传递给(w)WinMain函数。

使用GetCommandLine()可以获取完整的命令行参数

LPSTR pCmdLine = GetCommandLine();     //完整的命令行参数

分割命令行的各部分可以使用全局变量__argc (或__wargv) 或者使用CommandLineToArgvW (只有Unicode版本)

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd){
	LPSTR arg = (LPSTR) malloc(sizeof(char)*100);
	for(int i=0;i<__argc;i++){
		sprintf(arg,"%s",__argv[i]);
		MessageBox(NULL,arg,"提示",MB_OK);
	}
	return 0;
}
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR lpCmdLine,int nShowCmd){
	int nNumArgs;
	PWSTR *ppArgv = CommandLineToArgvW(lpCmdLine,&nNumArgs);

	for(int i=0;i<nNumArgs;i++){
		MessageBox(NULL,ppArgv[i],L"参数",MB_OK);
	}

	HeapFree(GetProcessHeap(),0,ppArgv);  //释放内存

	return 0;
}

10. 进程的环境变量

每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串,其形式如下:

VarName1=VarValue1\0

VarName2=VarValue2\0

\0

形式为:环境变量名称=环境变量具体值\0。环境块最后还有一个\0.

[环境变量中空格是有意义的,Var1=ABC 和 Var2= ABC 是不一样的]

子进程环境变量

子进程可以继承父进程的环境变量,实际上是对父进程环境块的拷贝,父进程和子进程不共享环境块,两者可以各自操作自己的环境块。

父进程可以控制子进程继承哪些环境变量。

通过GetEnvironmentVariable函数可以获取环境变量的值,通过SetEnvironmentVairable函数设置一项环境变量。

DWORD WINAPI GetEnvironmentVariable(   //获取环境变量的值,成功则返回缓存中实际存储的字符数目,失败返回0。GetLastError返回ERROR_ENVVAR_NOT_FOUND
	__in_opt   LPCTSTR lpName,   //环境变量的名称
	__out_opt  LPTSTR lpBuffer,  //接受环境变量值的缓存
	__in       DWORD nSize       //缓存的大小(单位字符)
	);
BOOL WINAPI SetEnvironmentVariable( //设置一个环境变量
	__in      LPCTSTR lpName,	//环境变量的名称
	__in_opt  LPCTSTR lpValue   //环境变量的值
	);
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR lpCmdLine,int nShowCmd){

	SetEnvironmentVariable(_T("Var1"),_T("First Variable"));
	SetEnvironmentVariable(_T("Var2"),_T("Second Variable"));

	TCHAR buf[256];
	GetEnvironmentVariable(_T("Var1"),buf,sizeof(buf)/sizeof(TCHAR));
	MessageBox(NULL,buf,_T("提示"),MB_OK); //First Variable
	return 0;
}

11. 进程的错误模式

进程的错误模式是指与每个进程相关联的一组标志,用于告诉系统,进程对严重的错误应该如何作出反映,包括磁盘介质故障、未处理的异常情况、文件查找失败和数据没有对齐等。进程可以通过调用SetErrorMode函数通知系统如何处理故障。

UINT SetErrorMode(UINT uMode);

uMode是下面参数或参数的组合(|)

标志
说明
0
使用系统默认,即显示所有的错误对话框
SEM_FAILCRITICALERRORS
系统不显示关键错误句柄消息框,并将错误返回给调用进程
SEM_NOGOFAULTERRORBOX
系统不显示一般保护故障消息框。本标志只应该由采用异常情况处理程序来处理一般保护(GP)故障的调试应用程序来设定
SEM_NOOPENFILEERRORBOX
当系统找不到文件时,它不显示消息框
SEM_NOALIGNMENTFAULTEXCEPT
系统自动排除内存没有对齐的故障,并使应用程序看不到这些故障。本标识对x86处理器不起作用。

默认情况下,子进程继承父进程的错误模式标志。

11. 进程的当前驱动器和目录

当不提供全路径名时,Windows会在当前驱动器的当前目录中查找文件和目录。操作系统内部会跟踪进程的当前驱动和和目录。(按照进程维护的,所以所有线程共享当前驱动器和目录)

通过 SetCurrentDirectory和GetCurrentDirectory 函数可以设置和获取当前目录。

也可以使用C Runtime 函数 _chdir来更改当前目录,该函数内部调用了SetCurrentDirectory。

12. 操作系统版本

通过GetVersionEx函数获取系统的版本号,通过GetProductInfo获取对应版本号的具体发行版本(Ultimate\HomeBasic等)

BOOL WINAPI GetVersionEx(
	__inout  LPOSVERSIONINFO lpVersionInfo
	);
typedef struct _OSVERSIONINFOEX {
	DWORD dwOSVersionInfoSize;    //结构体的大小,设置为sizeof(OSVERSIONINFOEX)
	DWORD dwMajorVersion;         //系统的主版本号
	DWORD dwMinorVersion;         //系统的次版本号
	DWORD dwBuildNumber;          //系统的Build号
	DWORD dwPlatformId;           //当前操作系统的平台ID。VER_PLATFORM_WIN32_NT(Windows NT/2000)
	TCHAR szCSDVersion[128];      //当前系统最新的Service Pack描述。如"Service Pack 3"
	WORD  wServicePackMajor;      //Service Pack的主版本号
	WORD  wServicePackMinor;      //Service Pace的次版本号
	WORD  wSuiteMask;             //指示系统安装的产品套件
	BYTE  wProductType;           //系统附加信息。
	BYTE  wReserved;              //保留
} OSVERSIONINFOEX, *POSVERSIONINFOEX, *LPOSVERSIONINFOEX;
Operating system Version number
Windows 7 6.1
Windows Server 2008 R2 6.1
Windows Server 2008 6.0
Windows Vista 6.0
Windows Server 2003 R2 5.2
Windows Server 2003 5.2
Windows XP 5.1
Windows 2000 5.0
#include "stdafx.h"
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <strsafe.h>

#pragma comment(lib, "User32.lib")

#define BUFSIZE 256

typedef void (WINAPI *PGNSI)(LPSYSTEM_INFO);
typedef BOOL (WINAPI *PGPI)(DWORD, DWORD, DWORD, DWORD, PDWORD);

BOOL GetOSDisplayString( LPTSTR pszOS)
{
	OSVERSIONINFOEX osvi;
	SYSTEM_INFO si;
	PGNSI pGNSI;
	PGPI pGPI;
	BOOL bOsVersionInfoEx;
	DWORD dwType;

	ZeroMemory(&si, sizeof(SYSTEM_INFO));
	ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));

	osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);

	if( !(bOsVersionInfoEx = GetVersionEx ((OSVERSIONINFO *) &osvi)) )
		return 1;

	// Call GetNativeSystemInfo if supported or GetSystemInfo otherwise.

	pGNSI = (PGNSI) GetProcAddress(
		GetModuleHandle(TEXT("kernel32.dll")),  "GetNativeSystemInfo");
	if(NULL != pGNSI)
		pGNSI(&si);
	else GetSystemInfo(&si);

	if ( VER_PLATFORM_WIN32_NT==osvi.dwPlatformId &&  osvi.dwMajorVersion > 4 )
	{
		StringCchCopy(pszOS, BUFSIZE, TEXT("Microsoft "));

		// Test for the specific product.

		if ( osvi.dwMajorVersion == 6 )
		{
			if( osvi.dwMinorVersion == 0 )
			{
				if( osvi.wProductType == VER_NT_WORKSTATION )
					StringCchCat(pszOS, BUFSIZE, TEXT("Windows Vista "));
				else StringCchCat(pszOS, BUFSIZE, TEXT("Windows Server 2008 " ));
			}

			if ( osvi.dwMinorVersion == 1 )
			{
				if( osvi.wProductType == VER_NT_WORKSTATION )
					StringCchCat(pszOS, BUFSIZE, TEXT("Windows 7 "));
				else StringCchCat(pszOS, BUFSIZE, TEXT("Windows Server 2008 R2 " ));
			}

			pGPI = (PGPI) GetProcAddress(
				GetModuleHandle(TEXT("kernel32.dll")),  "GetProductInfo");

			pGPI( osvi.dwMajorVersion, osvi.dwMinorVersion, 0, 0, &dwType);  //或者直接调用GetProductInfo()

			switch( dwType )
			{
			case PRODUCT_ULTIMATE:
				StringCchCat(pszOS, BUFSIZE, TEXT("Ultimate Edition" ));
				break;
			case PRODUCT_PROFESSIONAL:
				StringCchCat(pszOS, BUFSIZE, TEXT("Professional" ));
				break;
			case PRODUCT_HOME_PREMIUM:
				StringCchCat(pszOS, BUFSIZE, TEXT("Home Premium Edition" ));
				break;
			case PRODUCT_HOME_BASIC:
				StringCchCat(pszOS, BUFSIZE, TEXT("Home Basic Edition" ));
				break;
			case PRODUCT_ENTERPRISE:
				StringCchCat(pszOS, BUFSIZE, TEXT("Enterprise Edition" ));
				break;
			case PRODUCT_BUSINESS:
				StringCchCat(pszOS, BUFSIZE, TEXT("Business Edition" ));
				break;
			case PRODUCT_STARTER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Starter Edition" ));
				break;
			case PRODUCT_CLUSTER_SERVER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Cluster Server Edition" ));
				break;
			case PRODUCT_DATACENTER_SERVER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Datacenter Edition" ));
				break;
			case PRODUCT_DATACENTER_SERVER_CORE:
				StringCchCat(pszOS, BUFSIZE, TEXT("Datacenter Edition (core installation)" ));
				break;
			case PRODUCT_ENTERPRISE_SERVER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Enterprise Edition" ));
				break;
			case PRODUCT_ENTERPRISE_SERVER_CORE:
				StringCchCat(pszOS, BUFSIZE, TEXT("Enterprise Edition (core installation)" ));
				break;
			case PRODUCT_ENTERPRISE_SERVER_IA64:
				StringCchCat(pszOS, BUFSIZE, TEXT("Enterprise Edition for Itanium-based Systems" ));
				break;
			case PRODUCT_SMALLBUSINESS_SERVER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Small Business Server" ));
				break;
			case PRODUCT_SMALLBUSINESS_SERVER_PREMIUM:
				StringCchCat(pszOS, BUFSIZE, TEXT("Small Business Server Premium Edition" ));
				break;
			case PRODUCT_STANDARD_SERVER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Standard Edition" ));
				break;
			case PRODUCT_STANDARD_SERVER_CORE:
				StringCchCat(pszOS, BUFSIZE, TEXT("Standard Edition (core installation)" ));
				break;
			case PRODUCT_WEB_SERVER:
				StringCchCat(pszOS, BUFSIZE, TEXT("Web Server Edition" ));
				break;
			}
		}

		if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 2 )
		{
			if( GetSystemMetrics(SM_SERVERR2) )
				StringCchCat(pszOS, BUFSIZE, TEXT( "Windows Server 2003 R2, "));
			else if ( osvi.wSuiteMask & VER_SUITE_STORAGE_SERVER )
				StringCchCat(pszOS, BUFSIZE, TEXT( "Windows Storage Server 2003"));
			else if ( osvi.wSuiteMask & VER_SUITE_WH_SERVER )
				StringCchCat(pszOS, BUFSIZE, TEXT( "Windows Home Server"));
			else if( osvi.wProductType == VER_NT_WORKSTATION &&
				si.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_AMD64)
			{
				StringCchCat(pszOS, BUFSIZE, TEXT( "Windows XP Professional x64 Edition"));
			}
			else StringCchCat(pszOS, BUFSIZE, TEXT("Windows Server 2003, "));

			// Test for the server type.
			if ( osvi.wProductType != VER_NT_WORKSTATION )
			{
				if ( si.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_IA64 )
				{
					if( osvi.wSuiteMask & VER_SUITE_DATACENTER )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Datacenter Edition for Itanium-based Systems" ));
					else if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Enterprise Edition for Itanium-based Systems" ));
				}

				else if ( si.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_AMD64 )
				{
					if( osvi.wSuiteMask & VER_SUITE_DATACENTER )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Datacenter x64 Edition" ));
					else if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Enterprise x64 Edition" ));
					else StringCchCat(pszOS, BUFSIZE, TEXT( "Standard x64 Edition" ));
				}

				else
				{
					if ( osvi.wSuiteMask & VER_SUITE_COMPUTE_SERVER )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Compute Cluster Edition" ));
					else if( osvi.wSuiteMask & VER_SUITE_DATACENTER )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Datacenter Edition" ));
					else if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Enterprise Edition" ));
					else if ( osvi.wSuiteMask & VER_SUITE_BLADE )
						StringCchCat(pszOS, BUFSIZE, TEXT( "Web Edition" ));
					else StringCchCat(pszOS, BUFSIZE, TEXT( "Standard Edition" ));
				}
			}
		}

		if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 1 )
		{
			StringCchCat(pszOS, BUFSIZE, TEXT("Windows XP "));
			if( osvi.wSuiteMask & VER_SUITE_PERSONAL )
				StringCchCat(pszOS, BUFSIZE, TEXT( "Home Edition" ));
			else StringCchCat(pszOS, BUFSIZE, TEXT( "Professional" ));
		}

		if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 )
		{
			StringCchCat(pszOS, BUFSIZE, TEXT("Windows 2000 "));

			if ( osvi.wProductType == VER_NT_WORKSTATION )
			{
				StringCchCat(pszOS, BUFSIZE, TEXT( "Professional" ));
			}
			else  {
				if( osvi.wSuiteMask & VER_SUITE_DATACENTER )
					StringCchCat(pszOS, BUFSIZE, TEXT( "Datacenter Server" ));
				else if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
					StringCchCat(pszOS, BUFSIZE, TEXT( "Advanced Server" ));
				else StringCchCat(pszOS, BUFSIZE, TEXT( "Server" ));
			}
		}

		// Include service pack (if any) and build number.

		if( _tcslen(osvi.szCSDVersion) > 0 )
		{
			StringCchCat(pszOS, BUFSIZE, TEXT(" ") );
			StringCchCat(pszOS, BUFSIZE, osvi.szCSDVersion);
		}

		TCHAR buf[80];

		StringCchPrintf( buf, 80, TEXT(" (build %d)"), osvi.dwBuildNumber);
		StringCchCat(pszOS, BUFSIZE, buf);

		if ( osvi.dwMajorVersion >= 6 )
		{
			if ( si.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_AMD64 )
				StringCchCat(pszOS, BUFSIZE, TEXT( ", 64-bit" ));
			else if (si.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_INTEL )
				StringCchCat(pszOS, BUFSIZE, TEXT(", 32-bit"));
		}

		return TRUE;  }

	else
	{   printf( "This sample does not support this version of Windows.\n");
		return FALSE;
	}
}

int __cdecl _tmain()
{
	TCHAR szOS[BUFSIZE];

	if( GetOSDisplayString( szOS ) )
		_tprintf( TEXT("\n%s\n"), szOS ); //Microsoft Windows 7 Ultimate Edition Service Pack 1 (build 7601), 32-bit
}

还可以通过 VerifyVersionInfo 函数来确定当前操作系统是否满足要求。

//设置Windows 7 的版本号和平台ID
OSVERSIONINFOEX osver;
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwMajorVersion = 6;     //Windows 7 版本号为6.1  osver.dwMinorVersion = 1;     //Windows Server 2008 R2 也是6.1,通过PlatformId区分
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;

//设置条件掩码
DWORDLONG dwlConditionMask = 0; //必须初始化为0
VER_SET_CONDITION(dwlConditionMask,VER_MAJORVERSION,VER_EQUAL); //要判断主版本号是否相等
VER_SET_CONDITION(dwlConditionMask,VER_MINORVERSION,VER_EQUAL); //要判断次版本号是否相等
VER_SET_CONDITION(dwlConditionMask,VER_PLATFORMID,VER_EQUAL);   //要判断平台ID号是否相等

//测试系统是否为Windows 7
if(VerifyVersionInfo(&osver,VER_MAJORVERSION|VER_MINORVERSION|VER_PLATFORMID,dwlConditionMask)){
	//此操作系统是Windows 7
}
else{
	//此操作系统不是Window 7
}

13. CreateProcess函数

BOOL WINAPI CreateProcess(
	__in_opt     LPCTSTR lpApplicationName,  //要执行的模块的名称,如"cmd.exe",可以为NULL,则名称为lpCommandLine的第一个参数
	__inout_opt  LPTSTR lpCommandLine,       //命令行参数
	__in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes, //指向进程安全属性结构体的指针,用于确定指向新进程的句柄能否被它的子进程继承
	__in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,  //指向线程安全属性结构体的指针,用于确定指向新线程的句柄能否被它的子进程继承
	__in         BOOL bInheritHandles, //是否继承父进程的内核对象句柄
	__in         DWORD dwCreationFlags,       //标志
	__in_opt     LPVOID lpEnvironment,        //运行环境,NULL则使用父进程的环境
	__in_opt     LPCTSTR lpCurrentDirectory,  //当前目录,NULL则与父进程相同
	__in         LPSTARTUPINFO lpStartupInfo, //
	__out        LPPROCESS_INFORMATION lpProcessInformation//
	);

当调用CreateProcess时,系统会创建一个进程内核对象,初始计数值为1,该内核对象不是进程本身,只是用来管理进程的一个较小的数据结构;然后系统会为新进程创建一个虚拟地址空间,并将可执行文件和必要的DLL文件的代码和数据加载到该地址空间中;然后系统为新进程的主线程创建一个线程内核对象,初始计数值为1;然后调用C\C++ Runtime Startup函数,使主线程开始运行,最终调用WinMain等入口函数;最后成功则返回TRUE;

#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>

int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR lpCmdLine,int nShowCmd){

	STARTUPINFO si;
	PROCESS_INFORMATION pi;

	ZeroMemory(&si,sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi,sizeof(pi));

	//创建进程
	if(!CreateProcess(_T("C:\\Program Files\\The KMPlayer\\KMPlayer.exe"),//lpApplicationName
		NULL,  //命令行
		NULL,  //进程句柄不可继承
		NULL,  //线程句柄不可继承
		FALSE, //不继承父进程的内核对象
		0,     //标志
		NULL,  //使用父进程的环境
		NULL,  //当前目录与父进程相同。
		&si,    &pi
		)
	){
		MessageBox(NULL,_T("CreateProcess Failed"),_T("Info"),MB_OK);
	}

	//等待进程结束
	WaitForSingleObject(pi.hProcess,INFINITE);

	//关闭句柄
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);

	return 0;
}

14. 进程的终止

终止进程的运行,有四种方式

(1) 入口函数的返回 (推荐

这是保证所有线程资源能够得到正确释放的唯一方法。

(2) ExitProcess 函数

VOID WINAPI ExitProcess(  //退出一个进程及其所有线程
	__in  UINT uExitCode //退出码
	);

C\C++ Runtime Startup 函数在释放完C Runtime 资源后,调用了ExitProcess函数退出进程。(所以方法(1) 才可行)

直接调用ExitProcess,会导致C\C++ Runtime资源没有释放。

#include <windows.h>
#include <stdio.h>

class CSomeObj{
public:
	CSomeObj(){printf("Constructor\n");}
	~CSomeObj(){printf("Destructor\n");}
};

int main(){
	CSomeObj obj;      //如果不使用ExitProcess,通过入口函数返回,则obj的构造和析构都会得到正确的调用
	//ExitProcess(0);  //如果使用ExitProcess退出,则obj的析构函数不会被调用,obj的资源就无法释放。
	return 0;
}

(3) TerminateProcess 函数

BOOL WINAPI TerminateProcess( //终止进程及其所有线程
	__in  HANDLE hProcess, //要终止的进程句柄
	__in  UINT uExitCode   //退出码
	);

TerminateProcess可以用来终止本进程或另一个进程。

TernimateProcess是一个异步函数,它会通知系统终止某个进程,但是函数返回时,无法保证该进程已经被终止。要确保终止可以使用WaitForSingleObject函数。

15. 子进程

PROCESS_INFORMATION pi;
DWORD dwExitCode;

BOOL fSuccess = CreateProcess(...,&pi);
if(fSuccess){
	//关闭新进程的主线程句柄(越早越好,因为它不需要)
	CloseHandle(pi.hThread);

	//等待新进程结束
	WaitForSingleObject(pi.hProcess,INFINITE);

	//获取新进程的退出代码
	GetExitCodeProcess(pi.hProcess,&dwExitCode);

	//关闭新进程的进程句柄
	CloseHandle(pi.hProcess);
}

CreateProcess后,立即关闭了子进程的主线程内核对象句柄pi.hThread,这不会导致子进程主线程的终止,只是递减了其引用计数。这样做的好处是如果子进程的主线程生成了另一个线程,然后主线程退出了,由于父进程没有拥有其句柄,系统就可以释放其内核对象资源。

如果只是启动子进程,然后让子进程独立运行。

PROCESS_INFORMATION pi;
DWORD dwExitCode;

BOOL fSuccess = CreateProcess(...,&pi);
if(fSuccess){
	//关闭句柄
	CloseHandle(pi.hThread);
	CloseHandle(pi.hProcess);
}

Windows 核心编程 学习笔记 (第一部分)

目录

1. 错误处理

  •     Windows函数常见返回类型
  •     每个错误的三种表示法
  •     错误的内部机制
  •     错误代码转化为英文描述
  •     自定义错误代码

2. Unicode

  •     ANSI/UNICODE及通用宏
  •     Windows字符串函数

3. Unicode和ANSI之间的转换

  •     MultiByteToWideChar
  •     WideCharToMultiByte

4. 内核对象

5. 区分内核对象和用户对象或GDI对象

6. 进程的内核对象句柄表

7. 跨进程共享内核对象

  •     (1) 利用对象句柄的继承性
  •     (2) 利用命名对象 (实现程序只有一个实例)
  •     (3) 利用DuplicateHandle复制句柄

1. 错误处理

Windows函数常见返回类型

函数返回值类型
意义
VOID
该函数运行不可能失败。Windows函数返回值很少是VOID
BOOL
失败返回0,否则非0.    [注:不要测试返回值是否为TRUE]
HANDLE
失败通常返回NULL,也可能是INVALID_HANDLE_VALUE(它被定义为-1)。成功则返回一个可用的HANDLE
PVOID
失败返回NULL,否则返回PVOID,以标识数据块的内存地址
LONG/DWORD
返回数量的函数通常返回LONG或DWORD。如果由于某种 原因,函数无法对想要进行计数的对象进行计数,那么该函数通常返回0或- 1(根据函数而定)

Windows编译了一个所有可能的错误代码的列表,并且为每个错误代码分配了一个32位的数值(号码),存放在WinError.h中

//
// MessageId: ERROR_SUCCESS
//
// MessageText:
//
// The operation completed successfully.
//
#define ERROR_SUCCESS 0L 

每个错误都有三种表示法

(1)  消息ID,如ERROR_SUCCESS,可以用来和GetLastError返回值进行比较。

(2)  消息文本,如”The operation completed successfully”, 是对错误的英文描述

(3)  数值(号码),如0L,避免使用错误的数值。

Visual Studio中提供了Error Lookup工具可以查看消息ID对应的英文描述。

clipboard.png clipboard1.png

内部机制

从系统内部来讲,当一个Windows函数检测到一个错误之后,它会使用一个称为线程本地存储器(thread-local storage)的机制,将相应的错误代码与调用的线程关联起来。这使得线程能够互相独立的运行,而不影响各自的错误代码。

函数返回时,它的返回值就能指明一个错误已经发生。若要确定具体是什么错误,使用GetLastError()函数,它返回错误的具体32位数值。

DWORD WINAPI GetLastError(void);

调试代码的过程中,可以在Watch窗口中,输入@err,hr来查看最后的错误代码及其具体的英文描述。

将错误代码转换为英文描述

程序中通过GetLastError得到错误ID之后,可以通过FormatMessage函数转换为相应的英文描述

#include <windows.h>
 
int main()
{
    LPVOID lpMsgBuf;
 
    SetLastError(ERROR_INVALID_FUNCTION);           
 
    DWORD dw = GetLastError();
 
    //Translate Error Message
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,//dwFlags
        NULL,                                     //lpSource
        dw,                                       //dwMessageID
        MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT), //dwLanguageID
        (LPTSTR)&lpMsgBuf,                        //lpBuffer
        0,                                        //nSize
        NULL                                      //va_list* Arguments
        );
 
    //Display Error Message
    MessageBox(NULL,(LPCTSTR)lpMsgBuf,TEXT("Error"),MB_OK);
 
    return 0;
}

自定义错误代码

31~30
29
28
27~16
15~0
内容含义
严重性
0=成功   1=供参考
2=警告   3=错误
区分Microsoft/客户
0=Microsoft
1=客户自定义错误
保留
必须是0
设备代码
由Microsoft公司定义
异常代码
由Microsoft/客户定义

2. Unicode

WinCE只支持Unicode函数,不支持ANSI函数。

typdef unsigned short wchar_t

所有的Unicode字符串函数均以wcs开头,只需要将ANSI字符串函数的前缀str替换成wcs,即可得到相应的Unicode字符串函数。

//字符串拷贝
char* strcpy(char*,const char*);
wchar_t* wcscpy(wchar_t*,const wchar_t*);

//字符串长度
size_t strlen(const char*);
size_t wcslen(const wchar_t*);

ANSI/UNICODE 及通用宏

ANSI
Unicode
通用
char
wchar_t
TCHAR
“中文abc”
L”中文abc”
_T(“中文abc”) 或_TEXT(“中文abc”)
strlen,strcpy…
wcslen,wcscpy…
_tcslen,_tcscpy… [C运行时]   或
lstrlen,lstrcpy…   [Windows运行库]

通用宏会根据是否define了_UNICODE宏,来转换成相应的char或wchar_t。 【包含在tchar.h头文件中,提供了C运行时对Unicode的支持】

Windows对Unicode的支持

WCHAR     Unicode字符
PWSTR      指向Unicode字符串的指针
PCWSTR    指向一个const型Unicode字符串的指针

通用  PTSTR  PCTSTR【需要定义UNICODE宏】

 

  • _UNICODE 宏用于C运行时头文件
  • UNICODE 宏用于Windows头文件

当编译源代码模块时,通常必须同时定义这两个宏

Windows字符串函数

shlWApi.h头文件下 StrCat, StrChr, StrCmp, StrCpy 等字符串函数 推荐使用Windows字符串函数以提高效率。因为这些函数通常被操作系统的应用程序使用,所以当你的程序运行时,它们可能已经装入RAM了。

函数
说明
lstrcat
字符串连接
lstrcmp
字符串比较
lstrcmpi
字符串比较(忽略大小写)
lstrcpy
字符串拷贝
lstrlen
字符串长度

3. Unicode和ANSI之间的转换

MultiByteToWideChar:多字节字符串转换为宽字节字符串

int MultiByteToWideChar(
	__in   UINT CodePage,  //用来执行转换的代码页。如CP_ACP(系统缺省的ANSI代码页)
	__in   DWORD dwFlags,  //
	__in   LPCSTR lpMultiByteStr, //需要被转化的MultiByte字符串
	__in   int cbMultiByte,       //需要被转化的MultiByte字符串 的字节数。-1表示整个源字符串,包括'\0'.
	__out  LPWSTR lpWideCharStr,  //指向存储转换后的宽字节字符串的缓存的指针
	__in   int cchWideChar        //缓存的大小
	);
char * pMultiByte = "多字节字符串";
int wlen = MultiByteToWideChar(CP_ACP,0,pMultiByte,-1,NULL,0);//计算需要的宽字节数组大小
wchar_t* buf = new wchar_t[wlen];                             //宽字节缓存
MultiByteToWideChar(CP_ACP,0,pMultiByte,-1,buf,wlen);         //多字节编码转换成宽字节编码

MessageBox(NULL,buf,_T("Caption"),MB_OK);

WideCharToMultiByte: 宽字节字符串转多字节字符串

int WideCharToMultiByte(
	__in   UINT CodePage,			//用来执行转换的代码页。如CP_ACP(系统缺省的ANSI代码页)
	__in   DWORD dwFlags,           //
	__in   LPCWSTR lpWideCharStr,	//需要被转化的宽字节字符串
	__in   int cchWideChar,		//需要被转化的宽字节字符串 的字节数
	__out  LPSTR lpMultiByteStr,    //指向存储转换后的多字节字符串的缓存的指针
	__in   int cbMultiByte,         //缓存的大小
	__in   LPCSTR lpDefaultChar,    //
	__out  LPBOOL lpUsedDefaultChar //
	);
wchar_t* pWideChar = L"宽字节字符串";
int len = WideCharToMultiByte(CP_ACP,0,pWideChar,-1,NULL,0,NULL,NULL); //计算需要的字节数
char* buf = new char[len];                                             //多字节缓存
WideCharToMultiByte(CP_ACP,0,pWideChar,-1,buf,len,NULL,NULL);          //宽字节转多字节

printf("%s\n",buf);

每个内核对象只是内核分配的一个内存卡,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护对象的各种信息。

进程只能调用Windows API创建内核对象,并通过将内核对象的句柄传递给Windows API 从而访问和操作内核对象。

为了是操作系统更加健壮,这些句柄值是与进程密切相关的。不能直接将一个进程中的句柄传递给另一个进程使用。(跨进程的内核对象共享也是可以实现的)

内核对象由内核所拥有,而不是由进程所拥有。进程结束,内核对象不一定被撤销(可能另一个进程正在访问该内核对象)。

每个内核对象有一个引用计数,创建时为1,当另一个进程访问现有的内核对象时,计数值+1,进程结束-1,计数值为0时,内核就撤销该对象。

5. 区分内核对象和用户对象或GDI对象

菜单、窗口、光标、字体、图标等等,属于用户对象或GDI对象,不是内核对象。

区分的方法是看创建函数有没有PSECURITY_ATTRIBUTES这个参数,有则为内核对象,无则不是内核对象。如CreateFile和CreateIcon。

HANDLE WINAPI CreateFile(
	__in      LPCTSTR lpFileName,
	__in      DWORD dwDesiredAccess,
	__in      DWORD dwShareMode,
	__in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
	__in      DWORD dwCreationDisposition,
	__in      DWORD dwFlagsAndAttributes,
	__in_opt  HANDLE hTemplateFile
	);>
HICON CreateIcon(      
	HINSTANCE 	hInstance,
	int 		nWidth,
	int 		nHeight,
	BYTE 		cPlanes,
	BYTE 		cBitsPixel,
	const BYTE* lpbANDbits,
	const BYTE* lpbXORbits
	);

6. 进程的内核对象句柄表

当一个进程被初始化时,系统要为它分配一个句柄表。该句柄表只用于内核对象,不用于用户对象或GDI对象。句柄表大概如下(没有具体的官方文档资料):

索引 内核对象内存块的指针 访问屏蔽(标志位的DWORD) 标志(标志位的DWORD)
1 0xXXXXXX 0xXXXXXX 0xXXXXXX
2 0xXXXXXX 0xXXXXXX 0xXXXXXX

(1) 初始时,句柄表是空的

(2) 创建一个内核对象时,首先为内核对象分配内存并初始化,然后扫描进程的句柄表,找出一个空项,依据创建的内核对象设置该项。

创建内核对象的所有函数均返回与进程相关的句柄。该句柄实际上是进程句柄表中的索引。

通常创建失败返回NULL,但是也有返回INVALID_HANDLE_VALUE的情况。如CreateFile返回的是INVALID_HANDLE_VALUE.

(3) CloseHandle() 关闭内核对象。它会清除进程句柄表中的该项,同时使内核对象的引用计数-1,如果引用计数减为0,内核会撤销该对象。

7. 跨进程共享内核对象

(1) 利用对象句柄的继承性

只有当进程具有父子关系时,才能使用对象句柄的继承性。父进程在生成子进程时,可以赋予子进程对内核对象的访问权。

【注意:内核对象句柄具有继承性,但是内核对象本身不具备继承性

第一步:父进程创建内核对象,并通过SECURITY_ATTRIBUTES结构体的bInheritHandle参数指明该内核对象句柄可继承。

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; //指明该句柄可继承

HANDLE hMutex = CreateMutex(&sa,FALSE,NULL);

第二步:父进程生成子进程,指明子进程可继承父进程的内核对象句柄。设置bInheritHandles为TRUE.

BOOL WINAPI CreateProcess(
	__in_opt     LPCTSTR lpApplicationName,  //要执行的模块的名称,如"cmd.exe",可以为NULL,则名称为lpCommandLine的第一个参数
	__inout_opt  LPTSTR lpCommandLine,       //命令行参数
	__in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes, //指向进程安全属性结构体的指针,用于确定指向新进程的句柄能否被它的子进程继承
	__in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,  //指向线程安全属性结构体的指针,用于确定指向新线程的句柄能否被它的子进程继承
	__in         BOOL bInheritHandles, //是否继承父进程的内核对象句柄
	__in         DWORD dwCreationFlags,       //标志
	__in_opt     LPVOID lpEnvironment,        //运行环境,NULL则使用父进程的环境
	__in_opt     LPCTSTR lpCurrentDirectory,  //当前目录,NULL则与父进程相同
	__in         LPSTARTUPINFO lpStartupInfo, //
	__out        LPPROCESS_INFORMATION lpProcessInformation//
	);

bInheritHandles为TRUE时,系统会遍历父进程的句柄表,然后将包含可继承句柄的项,拷贝到子进程句柄表的相同位置(这就保证了句柄值相同),之后系统会递增内核对象的引用计数。

内核对象句柄的继承性有一个特性:子进程不知道它已经继承了任何句柄

通常可以将句柄值作为命令行参数传递给子进程,子进程使用sscanf函数进行分析,取出句柄值。或者,使用环境变量的方法传递(GetEnvironmentVariable())。

可以通过SetHandleInformation函数实现只允许某一个子进程继承内核对象的句柄。

SetHandleInformation函数

BOOL WINAPI SetHandleInformation(  //设置句柄对象的属性(其实就是设置句柄表中的一项)
	_In_  HANDLE hObject,  //要设置的句柄
	_In_  DWORD dwMask,    //要设置的标志的掩码(与dwFlags相同)
	_In_  DWORD dwFlags    //标志的具体值 HANDLE_FLAG_INHERIT 或 HANDLE_FLAG_PROTECT_FROM_CLOSE
	);
SetHandleInformation(hObject,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT); //打开内核对象句柄的可继承性
SetHandleInformation(hObject,HANDLE_FLAG_INHERIT,0);                   //关闭内核对象句柄的可继承性

SetHandleInformation(hObject,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);//内核对象受保护,不可关闭
SetHandleInformation(hObject,HANDLE_FLAG_PROTECT_FROM_CLOSE,0);        //内核对象不受保护,可以关闭(CloseHandle)

很少有需要将句柄设置为受保护的,但是有一种情况下需要:父进程生成了子进程,子进程生成了孙进程,父进程需要和孙进程通信,所以不希望子进程在创建孙进程之前把句柄关闭了,这是就需要设置为受保护,子进程就关闭不了,孙进程就能继承,继而可以和父进程通信。

相应的还有个GetHandleInformation()函数。

(2) 利用命名对象

不是所有的内核对象都是可以命名的。

HANDLE WINAPI CreateMutex(
	__in_opt  LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性
	__in      BOOL bInitialOwner, //TRUE表示如果调用者创建了该Mutex,则其属于调用者。FALSE表示调用者没有该Mutex的拥有权
	__in_opt  LPCTSTR lpName      //Mutex的名称
	);

如果lpName设置为NULL, 则创建的是一个未命名对象。lpName是一个 \0 结尾的字符串,最长为MAX_PATH。

问题:所有命名对象的名称是保存在系统单个名空间中的,所以可能存在命名冲突。

HANDLE hMutex = CreateMutex(NULL,FALSE,"MyMutex");

进程创建一个命名对象时,系统首先会查找是否已存在同名的内核对象,如果存在,则再判断是否是相同类型的对象,如果是相同类型的对象,则会执行安全检查,确定是否拥有该对象的访问权,如果有访问权,则会在当前进程的句柄表中找一空项,初始化该项并指向现有的内核对象。类型不匹配或拒绝访问,创建都会失败。

当然,也可以使用Open*系列函数来打开已有的内核对象句柄。

HANDLE WINAPI OpenMutex(
	__in  DWORD dwDesiredAccess,
	__in  BOOL bInheritHandle,
	__in  LPCTSTR lpName
	);

Create*系列函数和Open*系列函数的区别是:如果命名对象不存在,Create* 会创建,Open* 会失败。

为了保证对象的唯一性,建议创建一个GUID,使用GUID的字符串作为对象名。

保证程序只有一个运行实例的实现:

int WINAPI WinMain( HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
	HANDLE h = CreateMutex(NULL,FALSE,"{FA53CC1-0911-AF32-AFCE-1921213}"); //创建内核对象
	if(GetLastError() == ERROR_ALREADY_EXISTS){
		//已经存在,则表明已有一个程序实例在运行,退出
		return 0;
	}

	//...
	CloseHandle(h);   //程序退出时,关闭句柄
	return 0;
}

(3) 利用DuplicateHandle复制句柄

该函数取出一个进程的句柄表中的一项,并将该项拷贝到另一个进程的句柄表中

BOOL WINAPI DuplicateHandle(    //复制句柄(取出一个进程句柄表的一项,并拷贝到另一个进程的句柄表中)
	__in   HANDLE hSourceProcessHandle,   //进程的句柄,该进程拥有要复制的句柄
	__in   HANDLE hSourceHandle,          //要复制的句柄
	__in   HANDLE hTargetProcessHandle,   //进程的句柄,该进程接受复制的句柄
	__out  LPHANDLE lpTargetHandle,       //指针,指向接受复制的句柄的变量
	__in   DWORD dwDesiredAccess,         //新句柄的访问控制
	__in   BOOL bInheritHandle,           //句柄是否可继承
	__in   DWORD dwOptions                //其他可选项(可置0 或 DUPLICATE_SAME_ACCESS 和 DUPLICATE_CLOSE_SOURCE的任何组合
	);

DUPLICATE_SAME_ACCESS: 新句柄和源句柄具有相同的访问屏蔽,此时会忽略dwDesiredAccess参数。

DUPLICATE_CLOSE_SOURCE:  关闭源进程中的句柄。该标志实现了内核对象句柄在进程间的传递

STL deque容器不支持通过指针偏移读取元素(cplusplus.com意见反馈)

问题反馈过程

http://www.cplusplus.com 是介绍标准c++语言的一个权威网站,在其对deque容器的介绍中,有这样一句(反馈后现已更改):

But, unlike vectors, deques are not guaranteed to store all its elements in contiguous storage locations, thus not allowing direct access by offsetting pointers to elements.

意思是deque容器与vector容器不同,它不保证将元素的存储在连续的内存区域内,所以不能通过指针的偏移去访问数据。

我在Visual Studio 中编写了如下的代码:

deque<int> d;
d.push_back(3);
d.push_front(2);
d.push_front(1);   //d: 1 2 3

deque<int>::iterator it;
it = d.begin();

cout<<*it<<endl;      //1
cout<<*(it+1)<<endl;  //2  access by offset

最后一行中,我可以通过对it加上偏移1,实现元素访问。 于是我就对原文中那句话产生了疑问。随后我给网站反馈了这点。不久之后,就收到了网站管理员给我的回复。

page1page2

 

问题的理解

原文中使用的是pointer,实际指的是真实的指针,不包括迭代器iterator。所以不能使用实际指针的偏移去访问元素,因为内存是不连续的,但是迭代器就不存在这个问题。其次,使用指针偏移不是就一定会出错,网站原文表述的也准确。

为了更好的理解这个问题,需要了解deque容器的内部实现方式: deque容器的实现实际上采用的确实不是连续存储,它将元素存放在不同的内存块中,每块内存块内的元素是连续存放的,再通过一个索引结构指向这些内存块的地址,形成了两层的结构。这种实现方式,就是为了提供更高效的头部和尾部操作。但是其内存确实不连续,使用指针的偏移可能会出错(注意:是可能会出错,如果偏移量小,在一个内存块中,则不会出错,如果超过了一个内存块,则必然出错)。

问题的解决

管理员对我反馈的那句话已经进行了更改,现在的说法更准确清晰。

But, unlike vectorsdeques are not guaranteed to store all its elements in contiguous storage locations: accessing elements in a deque by offsetting a pointer to another element causes undefined behavior.

 

page3