您的位置:首页 > 软件问答

正点闹钟语音包(直播预告 | 疫情过后,我们还需要在线办公吗?)

导读正点闹钟语音包文章列表:1、直播预告 | 疫情过后,我们还需要在线办公吗?2、对话 CTO | 健身新物种,超级猩猩带来了哪些改变?3、果壳智能圆表闪耀上海服装展4、32年甘为火车司机

正点闹钟语音包文章列表:

正点闹钟语音包(直播预告 | 疫情过后,我们还需要在线办公吗?)

直播预告 | 疫情过后,我们还需要在线办公吗?

近几年来,一直有声音说在线办公是未来的趋势。突如其来的疫情无疑加快了在线办公的进程。大量公司无法正常返工导致了“远程协同办公”的需求集中爆发,一时间,在线办公成为了刚需。跟据钉钉的数据,2月3日当天,就有近2亿人开启在家办公模式。

在节后复工的过程中,我们看到有的企业迅速过渡到远程办公或在线办公模式,有条不紊地推进工作;也有大量的企业迫于形势,手忙脚乱地试用各种在线办公工具,临时开启“在线化”。

实际上,企业如何正确地开始远程办公,使用哪些在线办公工具,以及背后的管理方法都会对企业能否顺利度过疫情起到影响。

本期公益直播中,《中欧商业评论》联合“数字中欧”邀请到企业级研发管理工具ONES的创始人&CEO王颖奇,就远程办公背后的管理问题、在线办公与远程办公的特点与方式、企业落地在线办公的实践经验、以及疫情过后,我们还需不需要继续投入在线办公等话题进行分享。

讲师介绍

王颖奇

研发管理工具ONES 创始人&CEO

讲师资历

16年软件行业从业经历

曾任职于金山 WPS Office 核心研发团队,任金山安全“云安全”负责人,移动事业部负责人

2011年 创办正点科技,旗下产品正点闹钟、正点日历全球累计用户1亿

2014年任知名基金晨兴创投EIR,参与数百企业投资评估与咨询

2015年创办ONES,旗下企业级研发管理工具已服务于各行各业的5万余家企业及组织的研发团队。代表客户有人民日报新媒体中心、招商基金、喜茶、中国电信等各行业领军企业。

直播内容

本次公益直播中,王颖奇老师将为大家带来以下主题分享:

1、疫情冲击下,企业所凸显出来的日常管理问题和远程办公带来的问题

2、理解远程办公和在线办公的特点与区别

3、在线办公的经验分享

4、疫情之后,企业还要不要继续为在线办公投入?在线办公将有着怎样的前景?

2月26日19:00 直播等你来!

— END—

对话 CTO | 健身新物种,超级猩猩带来了哪些改变?

摘要:「对话 CTO」是极客公园的一档最新专栏,以技术人的视角聊聊研发管理者的发展和成长。

我们特别邀请到了 ONES 的创始人&CEO 王颖奇作为特邀访谈者。王颖奇曾参与金山软件 WPS、金山毒霸等大型软件的核心开发工作;2011 年创立了正点科技,旗下产品正点闹钟、正点日历在全球用户过亿;2014 年,王颖奇在知名美元基金晨兴资本任 EIR,并以个人身份参与十余家公司的管理咨询工作;2015 年,王颖奇创立 ONES,致力于提供企业级研发管理解决方案。

刚刚加入超级猩猩时,刻奇就在思考,如何解决大则几千平的传统健身房需要承担的高昂租金成本问题。从健身房选址到摆脱大面积店面束缚,通过技术实现了对健身房进行化整为零和精细化运营的改造,互联网出身的他将分布式概念带入了实体行业。

刻奇说,完全不同于传统健身房,超级猩猩省去了前台、销售的角色,将这些行为线上化,通过得到的运营数据去赋能各个部门。依靠按次收费、小程序约课、游戏化的线上运营手段……健身房、健身教练和用户之间的关系被重新「定义」了。

通过 IT 和技术实现打破健身房租金和选址的经营铁律,穿过坪效天花板,关于超级猩猩如何将线上化、数据化、智能化落地,利用技术和运营手段提高课程购买率和复购率,刻奇聊了聊他的想法。在他看来,(健身房)IT 化必然成为未来的行业标准。

化零为整,分布式概念建店

颖奇:非常感谢超级猩猩联合创始人刻奇接受我们今天的采访。外界看到超级猩猩可能是连锁的健身房,或者更多的是在线下的健身场景,同时也有订课小程序。作为产品或者技术负责人,您在超级猩猩里担任的角色是怎样?除了我们见到的部分,超级猩猩还有哪些东西是我们没有理解到的呢?

刻奇:对于外界来说,大家可能第一反应,超级猩猩是一个健身房。但从我们的角度,超级猩猩和过往的健身房是一个完全不同的定义。举个例子,传统健身房一定要有销售这些角色,但是在我们这里却没有。我们把所有的前台、销售都砍掉了,取而代之的是回归到健身的本质,回归到课程本身、内容本身。

用技术方案取代了健身房传统的必要人员,这样做之后,我们才能把所有的支付行为、交易行为、浏览行为都线上化,线上化之后才能数据化。这是我们自己的一个三步概念,首先线上化,第二数据化,第三智能化。

颖奇:这三方面真正落地,会是什么样子的?

刻奇:首先是线上化。我们可能是最早的一批做小程序的团队,超级猩猩全国接近一百家门店,做到了真的没有前台,所有的用户都是通过微信端进入到门店。

第二点是数据化。很早我们就内部成立了自己的数据平台组这样一个中台部门。所有的运营数据都能以各种自定义的报表形式呈现给不同的部门。比如,传统健身房教练是通过人来评定的,我们认为很主观也不公平。超级猩猩未来几百个几千个教练团队,我们直接通过自己的算法系统,基于一个核心指标:满员率,给到教练及时的反馈。

第三是还有一些我们在尝试的智能化的应用,比如完全自动化的排课。

颖奇:大家会说传统健身房是地产的生意,我觉得更多是生意难扩大,利润空间薄。超级猩猩利用技术化的手段让效率变高了,核心会变吗?

刻奇:核心也发生很大的变化。传统健身房的模式是这样一个商业模型,通过建几千平的大店要到一个相对低的租金。租金早期是一个诱惑,但是中期会上涨,因为无法在周围随便找到另一个相同面积的场地,所以间接被地产绑架了。传统健身房运营了七年八年,短则甚至两年三年,就会陷入这个问题。所以健身房几乎无法走到上市的环节,因为无法在财务上说服股东,保证租金成本不上涨。

但是我们一开始就强调大量拆散。我是互联网出身,所以我会将分布式这种概念应用到实业行业。拆散之后有两个好处,从用户角度来看,门店距离用户更近了。同时我们不受地产的控制,一两百平相对灵活,更换一个地方成本相对小。我可以在周围开很多家,客流是没有损失的。

颖奇:这应该是一个非常核心的逻辑了,我们把店变小变多,就更加需要IT的能力,否则的话我们也没法去管理。

刻奇:对,这个是跟技术相关的、支持超级猩猩从大变小的一个非常核心的逻辑。如果没有 IT 的支持,店面的增加会直接导致管理成本急剧增加。大的健身房管理上面就一个店,相对管理成本会变低一点,但是你分散的话,管理成本就会变高,但是通过提高 IT 能力,即便我们有很多店,我们依然没有前台,没有额外的管理成本,还是能够开这种小的店,能够保证同样的管理效率。

举个例子,可能十年、二十年前大超市是主力。而现在有越来越多的社区店、便利店,虽然它们很长时间内还是会并存,但社区店和便利店还是在蚕食大超市的生存空间。大健身房也面临同样的问题,租金水平不断上涨,小的健身房在蚕食市场。而且在这个过程中,小的依然离用户更近。这个也是我觉得很核心的一个逻辑。

颖奇:如果有些健身房没有太强的IT能力,可能就会买SaaS系统或者做外包,去做一些程序并进行一些运营,这个会不会形成一种行业标准?

刻奇:我觉得 IT 化一定会成为行业标准的。我们这个行业也就几家有自己的开发能力,另外很多小型公司确实都是买 SaaS,通过 SaaS 的形式去做这个东西。

迭代决策系统,数据化建模选址

颖奇:IT的这些能力是怎样帮助你们的?信息传递速度、服务能力上是否有进步?支撑起那么多小的店,提高聚集份额的速度等等这些事情上是否有起到辅助作用?

刻奇:是的,基于刚才提到的分布化,加上 IT 能力,标准化能力,我们门店标准的选址可能只需要 1-2 个月,甚至更快。2 个月门店就可以开起来,管理成本的其中一些部分都是 IT 化的,硬件一装,门店空间的硬件控制系统一装,课程也是标准化的,经过培训和招募优秀的教练,一个标准化的门店就直接下去了,是非常快捷的一个过程。

颖奇:我有个非常好奇的点,超级猩猩是如何选址进行快速扩张的,是否有一些数据建模的决策机制,有没有一些东西可以和大家聊一聊。

刻奇:首先我们自己有内部的一套系统,是基于过往运营情况,加上你对于当下城市,就是门店选址的地方,进行数据对比,然后你就能够大概有一个维度指标了。同时,我们也需要把一个区位的维度指标收集的更全。比如说它旁边的租金水平,旁边有多少个相关的消费品类,人流量等等。这套体系其实做得比较好的是海底捞,他们也是通过这种方式选址。当然,我觉得算法比重会越来越大,现在可能五五吧。一部分是基于算法的能力去理解这个地方合不合适,包括未来适合什么样的运营的方式、策略、排什么课;然后还有一半是能在过程中主观进行感知。

颖奇:数据选址是有别于传统的高效率选址方法。

刻奇:是的,我觉得数据维度越来越全面之后,人的维度会越来越小,未来我们可能 80% 的决策来自于数据的量化指标,20% 是由人的感性的直觉和经验去做这个决定,这是一个过程。当然我们选址其实也有一个自己的流程系统,这个流程系统使得内部的决策环节更科学更高效,并且一定是全公司一起参与进来响应。其他的部门都要参与到这个角色里面去,提供各自相应的专业分析,市场有自己市场的考量,门店有门店运营的考量,教练有教练的考量。如何把内部这套系统做的既科学又不要太繁琐。我们自己迭代了 80 多个版本去做这套内部的决策系统,并且保证高效,保持我们的科学性,而且持续的进行迭代。

颖奇:现在超级猩猩在全国有多少门店?

刻奇:差不多九个城市,一百家门店。

颖奇:今年大概会有一个什么样的目标?

刻奇:今年预计是 100 到 150 家的新增门店数。

颖奇:另一个问题,可能是跟所有健身房都有关系的。「健身」本身是个「反人性」的行为,超级猩猩是怎么解决「反人性」这个事情上的用户心理和体验呢?这里面会有哪些是通过我们的技术手段或者运营手段呢?

刻奇:有。所有的用户能够做到按次付费,也是线上技术支持之后能达到的一个效果。因为按次付费,所以来的人消费决策是认真的,投入是不一样的,和在传统健身房上免费团课的感觉是不一样的。

传统健身房按年收费,超级猩猩按次收费,我们的教练也知道课教得不好不仅会影响满员率,还会影响下次的直接收入。学员和教练的投入会营造一个氛围,一个人练会觉得「反人性」,但是当有了氛围之后,学员也觉得挺开心的。

颖奇:这解决了单次来的投入问题,那怎么解决让他每次都来的问题呢?

刻奇:对,这也是我们线上部分去做的,线上化运营之后可以引入很多游戏化的运营手段。比如我们去年做了「猩章」成长体系,现在很多人跟随我们也在做了。颁发游戏等级徽章,按不同课程类型细化,还有解锁,名人堂,累计排名等等。核心是这样设计之后给学员带来的是「多重的反馈」。学员会觉得「我不只是流了汗,我自己开心」,当然我们后面也在做新的尝试,比如做硬件相关的新的产品。就好像游戏能刺激玩家玩下去的原因一样,游戏中的反馈很丰富很及时,这也是提高用户粘性的一个重要方式。

另外,我们也给教练提供一些能力帮助他们去提高课程的体验。教练端产品会看到每一节课学员能力的分布,有多少学生是第一次来超级猩猩,有多少同学是第一次上这个教练的课,然后教练就能快速理解这节课他该怎么样动态地调整的授课方式和交流方式。

「规模」中打开边界,重新「发明」健身房

颖奇:接下来聊聊您自己,首先可以讲一下您的名字「刻奇」是怎么来的?

刻奇:我在青年时代喜欢米兰·昆德拉的《生命中不能承受之轻》,里面比较重要的一个概念就是「刻奇」。大家对这个的翻译有很多讨论,书里面可能会比较常规的翻译是「媚俗」这种概念去讲的,我个人觉得它是用一个词概括了一种社会心理状态。

像喝醉酒了之后会说很多的话,所有的情绪都带入进去,同时希望全世界都要陷入到这种情绪之中。这是在社会心理学里面非常常见的一种从众状态,比如一次灾难性事件,所有人都要在朋友圈去点蜡烛,类似这样的心理状态。「刻奇」这个词当时引发了我很深的思考,就作为我自己名字了。

颖奇:那是什么契机让您加入了超级猩猩呢?您在来超级猩猩之前,大概一些工作的履历是什么样的呢?

刻奇:我最早是在迅雷。虽然那时候是产品经理,但是之前也是技术出身。在迅雷的时候也比较有幸参与了迅雷当时可能是最盈利的部门——会员事业部,也经历了从 PC 到移动互联网迁移的过程。后面短暂的去了一下支付宝,在支付宝无线端负责账号和安全这部分的产品业务。然后就又回到了深圳参与超级猩猩的创业过程。

颖奇:最后您可以分享几本最近看的、认为比较好的书给大家。

刻奇:我最近看的一本比较好的是《规模》,有一章我印象很深,为什么在发展过程中大多数公司会越做越衰败,但城市会越来越大。为什么同样是在规模增长过程中会发生不同的命运,里面提到一个很重要的概念是维度性。公司随着增长它的维度是越来越低的,因为公司越来越大,里面的沟通效率、反馈效率在不停的降低,这几乎是常态,所有的公司慢慢的效率都会变低,沟通反馈机制会越来越慢。而城市不一样,城市随着规模扩大,它会不停的引进新的人、新的技术,并且边界不断放大,它永远是充满生命的这种活力,它的维度是随着它的规模增大在变高的。

颖奇:这更多可能还是你自己的规模在变大,实际上量变走向质变了。

刻奇:对,可能是发生质变了。以健身行业为例,为什么健身连锁在世界范围里都没有一个能够走到那么高的位置,它是不是在过程中有某种原因导致的。而比如说可口可乐公司能够增长如此快,能够跨越时间周期惯例周期,这个过程中是不是由他们不同的增长模式导致的?

颖奇:对,我觉得有些东西可能是设计出来的,有些东西可能是他早期基因决定的。包括现在我们作为观察者来看超级猩猩,也是看到超级猩猩在一点点改变,也不是完全设计出来的。我觉得你是在参与到重新发明健身房,包括重新发明健身房、用户和健身教练之间的关系之中的。

刻奇:所以我也觉得你们 ONES 很重要,因为所有的公司都应该是互联网公司,都一定要有自己的研发能力。现在如果哪个公司没有自己的研发团队,我都不太看好公司的前景,它可能只是一个短期的小生意。如果一个公司真的是要做一个企业长期发展,它一定会需要自己内部的技术基因。如果以前没有,现在一定要有,不然就会被时代淘汰。

颖奇:近期有一些传统行业例如家具厂商、海鲜企业也来采购我们的研发管理工具,就说明他们内部都有软件的部门了,这个发展是比我们之前想象的要快很多的。今天非常感谢您的分享。

本文作者:王颖奇

联系方式:wangyingqi@gmail.com

果壳智能圆表闪耀上海服装展

2016中国国际服装服饰博览会(春季)近日在国家会展中心举行启幕。本届展会汇聚了全球20多个国家及地区的1177家企业、1300余个品牌参展。作为唯一一家在展会上亮相的智能手表产品,果壳智能手表获得了不少参展观众和展商人士的好评。

果壳智能圆表是世界上第一块采用圆形屏幕的智能手表。据悉,在果壳智能圆表最初立项阶段,果壳电子做过一次10万人规模的调查,得出的一个非常重要的结论是——对于智能手表来说,外表的美观成都比智能化更重要。所以果壳电子将果壳智能圆表的表盘设计成了圆形,外形沿用了奢侈名表的CushionRound造型,因为这样才能传承传统名表的气质和优雅,消费者才会经常佩带在手腕上。

果壳智能圆表还拥有不少独特的创新点和人性化功能,比如可以“常显”时间的屏幕,远超同类产品的待机时间,内置应用商店以及Wi-Fi、蓝牙多种连接方式,标准24mm表带轴,消费者可以随自己所好任意更换表带等等。

除了外观非常吸引人之外,果壳智能圆表的视网膜屏幕分辨率为320*320,像素密度为359PPI,并且能在电子墨水显示模式和高清真彩显示模式之间任意切换,不用抬手或者点击屏幕就可以看清楚时间,这块屏幕也是目前已知智能手表分辨率最高的,丰富的视觉表现,和个性简洁的交互给予用户更好的使用体验。

在待机时间上,果壳智能圆表的极限待机时间长达18天,即使在正常使用的情况下,也能做到待机5天。

另外,果壳智能圆表标配头层牛皮的表带,但考虑到用户的个人喜好复杂多样,果壳智能圆表采用24MM的标准表带轴,用户可以自由选择各种类型、材质的表带,随心搭配。

在功能上,果壳智能圆表同时也有丰富的实用功能。它是一个终极遥控器,也是用户的健康助理,可以通过传感器实时测量心率、血氧等数据。它也是用户的贴身天气预报助手,会提示用户穿衣指数,防晒指数,PM2.5数据等。还会同步接收手机上的电话、短信、微信等各种提示,成为用户开车或者开会时的小秘书,重要信息从此不再错过。

另外果壳智能圆表在设计时进行了高达157项的系统级的优化,所以其正常使用的电量能以周为单位,是同类产品的5倍。

果壳智能圆表的应用商店名为果壳市场,已经提供包括“心率监测”“计步器”“WiFi万能钥匙”“正点闹钟”以及音乐遥控播放、录音、PPT控制、智能家居设备遥控等应用。

此外,果壳智能圆表还可以随时更换表盘,目前内置表盘中既有简洁的两针、三针表盘,也有清晰易读的数字表盘,最赞的是非常对机械表迷胃口的透窗、陀飞轮样式表盘。

果壳智能圆表提供多种网络连接方式,除了用蓝牙与几亿的蓝牙设备连接之外,还配置Wi-Fi功能,能独立上网,或者通过蓝牙代理上网,其内置“WiFi万能钥匙”应用能让果壳智能圆表一键链接全国10亿免费热点,任何时候可以接收到来自互联网、日程和应用程序的通知提醒,不会错过任何一条短信和来电。

32年甘为火车司机当活闹钟,只为数万趟次列车正点开车

红网时刻2月25日讯(通讯员 陈智敏)2月24日(农历正月十三),节后春运第13天。当天晚上17时30分,鲁新萍提前30分来到备班楼接班。

今年50岁鲁新萍,现是中国铁路广州局集团公司株洲机务段郴州运用车间备班楼一名叫班员、党员工长。1988年10月,她参加工作起就一直在备班楼担任服务员工作。2009年7月份,她光荣地加入了中国共产党。

按照政策规定:2月28日,是鲁新萍光荣退休的日子。2月24日的晚班也是她最后一次在备班楼当班。

“备班楼”和“叫班员”是铁路系统独有的名词,“备班楼”有点类似宾馆,火车司机在晚上开车前,必须先到备班楼进行充分休息。“叫班员”被火车司机美称为“活闹钟”,她们的工作职责就是把火车司机正点叫醒、叫起、叫走,确保让火车司机正点出乘开车。

她的上班期间不但要负责整栋备班楼6层48个房间的卫生清理,卧具整理,还要负责本车间待乘火车司机的叫班工作。

鲁新萍在电脑叫班系统里核对当天晚上的行车计划。

鲁新萍接班后的第一项工作,就是在备班楼值班室的电脑里的备班系统中核对列车车次、开点和人员信息。

这几天,湖南地区气温渐渐升高。为了让待乘的司机有温暖舒适的休息环境,鲁新萍提前更换了床上的被子。

现在正是京广线郴州至广州区段都会迎来临客列车与货物列车交替开行的旺季,在车间备班楼待乘的火车司机也随之增多。

车间备班楼以前是人工叫班方式,叫班员先把列车车次和开车时间抄好以后,再拿个本子去火车司机休息的房间轻轻地敲门,去通知火车司机。一旦遇到瞌睡重的火车司机,叫班员还得敲第二门,再叫一次班,直至火车司机起床。

10年前,由人工叫班改为广播呼叫班。叫班员坐在值班室里通过广播与火车司机直接对话叫班。现在只要根据列车开行计划输入叫班数据信息,就能通过电脑系统自动叫班。随着科技改革,叫班员的劳动强度渐渐减少,工作效率逐步提高。

鲁新萍根据天气变化情况更换备班楼房间里的卧具。

据鲁新萍介绍,叫班员的工作看似简单,但责任重大。叫班时间早了,影响火车司机休息,埋下安全风险。叫班时间晚了,造成火车司机出勤晚到,就会影响列车的正点开行。要根据司机接班地点的不同,分别提前不同的时间叫班,能让火车司机多休息一分钟,就是对安全最大的保证。

据统计:32年,她甘为火车司机当“活闹钟”,为火车司机准时叫班数万次,为数万趟次列车安全正点保驾护航。

正点原子I.MX6U嵌入式Linux C应用编程 第八章 信号

信号:基础

本章将讨论信号,虽然信号的基本概念比较简单,但是其所涉及到的细节内容比较多,所以本章篇幅也会相对比较长。事实上,在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法,所以信号机制在Linux早期版本中就已经提供了支持,随着Linux内核版本的更新迭代,其对信号机制的支持更加完善。

本章将会讨论如下主题内容。

信号的基本概念;

信号的分类、Linux提供的各种不同的信号及其作用;

发出信号以及响应信号,信号由“谁”发送、由“谁”处理以及如何处理;

进程在默认情况下对信号的响应方式;

使用进程信号掩码来阻塞信号、以及等待信号等相关概念;

如何暂停进程的执行,并等待信号的到达。

基本概念

信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。

信号的目的是用来通信的

一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号可以由“谁”发出呢?以下列举的很多情况均可以产生信号:

硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为0、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。

用于在终端下输入了能够产生信号的特殊字符。譬如在终端上按下CTRL C组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下CTRL Z组合按键可以产生暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程。

进程调用kill()系统调用可将任意信号发送给另一个进程或进程组。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root超级用户。

用户可以通过kill命令将信号发送给其它进程。kill命令想必大家都会使用,通常我们会通过kill命令来“杀死”(终止)一个进程,譬如在终端下执行"kill -9 xxx"来杀死PID为xxx的进程。kill命令其内部的实现原理便是通过kill()系统调用来完成的。

发生了软件事件,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的CPU时间超限、进程的某个子进程退出等等情况)。

进程同样也可以向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核。

以上便是可以产生信号的多种不同的条件,总的来看,信号的目的都是用于通信的,当发生某种情况下,通过信号将情况“告知”相应的进程,从而达到同步、通信的目的。

信号由谁处理、怎么处理

信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:

忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有发出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是SIGKILL和SIGSTOP,这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。

捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了signal()系统调用可用于注册信号的处理函数,将会在后面向大家介绍。

执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,8.3小节中对此有进行介绍。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。

信号是异步的

信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。

信号本质上是int类型数字编号

信号本质上是int类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字1开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名(也就是宏定义)。

这些信号在<signum.h>头文件中定义,每个信号都是以SIGxxx开头,如下所示:

示例代码 8.1.1 信号定义

/* Signals. */

#define SIGHUP 1 /* Hangup (POSIX). */

#define SIGINT 2 /* Interrupt (ANSI). */

#define SIGQUIT 3 /* Quit (POSIX). */

#define SIGILL 4 /* Illegal instruction (ANSI). */

#define SIGTRAP 5 /* Trace trap (POSIX). */

#define SIGABRT 6 /* Abort (ANSI). */

#define SIGIOT 6 /* IOT trap (4.2 BSD). */

#define SIGBUS 7 /* BUS error (4.2 BSD). */

#define SIGFPE 8 /* Floating-point exception (ANSI). */

#define SIGKILL 9 /* Kill, unblockable (POSIX). */

#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */

#define SIGSEGV 11 /* Segmentation violation (ANSI). */

#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */

#define SIGPIPE 13 /* Broken pipe (POSIX). */

#define SIGALRM 14 /* Alarm clock (POSIX). */

#define SIGTERM 15 /* Termination (ANSI). */

#define SIGSTKFLT 16 /* Stack fault. */

#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */

#define SIGCHLD 17 /* Child status has changed (POSIX). */

#define SIGCONT 18 /* Continue (POSIX). */

#define SIGSTOP 19 /* Stop, unblockable (POSIX). */

#define SIGTSTP 20 /* Keyboard stop (POSIX). */

#define SIGTTIN 21 /* Background read from tty (POSIX). */

#define SIGTTOU 22 /* Background write to tty (POSIX). */

#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */

#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */

#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */

#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */

#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */

#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */

#define SIGPOLL SIGIO /* Pollable event occurred (System V). */

#define SIGIO 29 /* I/O now possible (4.2 BSD). */

#define SIGPWR 30 /* Power failure restart (System V). */

#define SIGSYS 31 /* Bad system call. */

#define SIGUNUSED 31

不存在编号为0的信号,从示例代码 8.1.1中也可以看到,信号编号是从1开始的,事实上kill()函数对信号编号0有着特殊的应用,关于这个文件将会在后面的内容向大家介绍。

信号的分类

Linux系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号,本小节将对这些信号的分类进行简单地介绍。

可靠信号与不可靠信号

Linux信号机制基本上是从UNIX系统中继承过来的,早期UNIX系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,它的主要问题是:

进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新为该信号绑定相应的处理函数。

因此导致,早期UNIX下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。在Linux系统下,信号值小于SIGRTMIN(34)的信号都是不可靠信号,这就是"不可靠信号"的来源,所以示例代码 8.1.1中所列举的信号都是不可靠信号。

随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种UNIX版本分别在这方面进行了研究,力图实现"可靠信号"。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号(SIGRTMIN~SIGRTMAX),并在一开始就把它们定义为可靠信号,在Linux系统下使用"kill -l"命令可查看到所有信号,如下所示:

图 8.2.1 kill命令查看所有信号

Tips:括号" ) "前面的数字对应该信号的编号,编号1~31所对应的是不可靠信号,编号34~64对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了SIGRTMIN N或SIGRTMAX-N的方式来表示。

可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数sigqueue()及信号绑定函数sigaction()。

早期UNIX系统只定义了31种信号,而Linux 3.x支持64种信号,编号1-64(SIGRTMIN=34,SIGRTMAX=64),将来可能进一步增加,这需要得到内核的支持。前31种信号已经有了预定义值,每个信号有了确定的用途、含义以及对应的名字,并且每种信号都有各自的系统默认操作。如按键盘的CTRL C时,会产生SIGINT信号,对该信号的系统默认操作就是终止进程,后32个信号表示可靠信号。

实时信号与非实时信号

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX标准的一部分,可用于应用进程。

一般我们也把非实时信号(不可靠信号)称为标准信号,如果文档中用到了这个词,那么大家要知道,这里指的就是非实时信号(不可靠信号)。关于更多实时信号相关内容将会在8.11小节中介绍。

常见信号与默认行为

前面说到,Linux下对标准信号(不可靠信号、非实时信号)的编号为1~31,如示例代码 8.1.1所示,接下来将介绍这些信号以及这些信号所对应的系统默认操作。

SIGINT

当用户在终端按下中断字符(通常是CTRL C)时,内核将发送SIGINT信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行。所以通常我们都会使用CTRL C来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。

SIGQUIT

当用户在终端按下退出字符(通常是CTRL )时,内核将发送SIGQUIT信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。进程如果陷入无限循环、或不再响应时,使用SIGQUIT信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符CTRL C、也可以按下退出字符CTRL 来终止,当然前提条件是,此进程会将SIGINT信号或SIGQUIT信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。

SIGILL

如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行。

SIGABRT

当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT信号。该信号的系统默认操作是终止进程、并生成核心转储文件。

SIGBUS

产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。

SIGFPE

该信号因特定类型的算术错误而产生,譬如除以0。该信号的系统默认操作是终止进程。

SIGKILL

此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。使用SIGINT信号和SIGQUIT信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,如果使用SIGINT或SIGQUIT无法终止进程,那就使用“必杀信号”SIGKILL吧。Linux下有一个kill命令,kill命令可用于向进程发送信号,我们会使用"kill -9 xxx"命令来终止一个进程(xxx表示进程的pid),这里的-9其实指的就是发送编号为9的信号,也就是SIGKILL信号。

SIGUSR1

该信号和SIGUSR2信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。

SIGSEGV

这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对内存无效引用的原因很多,C语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程。

SIGUSR2

与SIGUSR1信号相同。

SIGPIPE

涉及到管道和socket,当进程向已经关闭的管道、FIFO或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。

SIGALRM

与系统调用alarm()或setitimer()有关,应用程序中可以调用alarm()或setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送SIGALRM信号给该应用程序,关于alarm()或setitimer()函数的使用,后面将会进行讲解。该信号的系统默认操作是终止进程。

SIGTERM

这是用于终止进程的标准信号,也是kill命令所发送的默认信号(kill xxx,xxx表示进程pid),有时我们会直接使用"kill -9 xxx"显式向进程发送SIGKILL信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获SIGTERM信号、并为其绑定一个处理函数,当该进程收到SIGTERM信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用SIGKILL信号终止进程,从而跳过了SIGTERM信号的处理函数,通常SIGKILL终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用SIGTERM,实在不行再使用最后手段SIGKILL。

SIGCHLD

当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。

SIGCLD

与SIGCHLD信号同义。

SIGCONT

将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。

SIGSTOP

这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。

SIGTSTP

这也是一个停止信号,当用户在终端按下停止字符(通常是CTRL Z),那么系统会将SIGTSTP信号发送给前台进程组中的每一个进程,使其停止运行。

SIGXCPU

当进程的CPU时间超出对应的资源限制时,内核将发送此信号给该进程。

SIGVTALRM

应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程。

SIGWINCH

在窗口环境中,当终端窗口尺寸发生变化时(譬如用户手动调整了大小,应用程序调用ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号。

SIGPOLL/SIGIO

这两个信号同义。这两个信号将会在高级IO章节内容中使用到,用于提示一个异步IO事件的发生,譬如应用程序打开的文件描述符发生了I/O事件时,内核会向应用程序发送SIGIO信号。

SIGSYS

如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。

以上就是关于这些信号的简单介绍内容,以上所介绍的这些信号并不包括Linux下所有的信号,仅给大家介绍了一下常见信号,表 8.3.1将对这些信号进行总结。

表 8.3.1 Linux信号总结

信号名称

编号

描述

系统默认操作

SIGINT

2

终端中断符

term

SIGQUIT

3

终端退出符

term core

SIGILL

4

非法硬件指令

term core

SIGABRT

6

异常终止(abort)

term core

SIGBUS

7

内存访问错误

term core

SIGFPE

8

算术异常

term core

SIGKILL

9

终极终止信号

term

SIGUSR1

10

用户自定义信号1

term

SIGSEGV

11

无效的内存引用

term core

SIGUSR2

12

用户自定义信号2

term

SIGPIPE

13

管道关闭

term

SIGALRM

14

定时器超时(alarm)

term

SIGTERM

15

终止进程

term

SIGCHLD/SIGCLD

17

子进程终止或停止

ignore

SIGCONT

18

使停止状态的进程继续运行

cont

SIGSTOP

19

停止进程

stop

SIGTSTP

20

终端停止符

stop

SIGXCPU

24

超过CPU限制

term core

SIGVTALRM

26

虚拟定时器超时

term

SIGWINCH

28

终端窗口尺寸发生变化

ignore

SIGPOLL/SIGIO

29

异步I/O

term/ignore

SIGSYS

31

无效系统调用

term core

Tips:上表中,term表示终止进程;core表示生成核心转储文件,核心转储文件可用于调试,这个便不再给介绍了;ignore表示忽略信号;cont表示继续运行进程;stop表示停止进程(注意停止不等于终止,而是暂停)。

进程对信号的处理

当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。Linux系统提供了系统调用signal()和sigaction()两个函数用于设置信号的处理方式,本小节将向大家介绍这两个系统调用的使用方法。

signal()函数

本节描述系统调用signal(),signal()函数是Linux系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:

#include <signal.h>

typedef void (*sig_t)(int);

sig_t signal(int signum, sig_t handler);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。

handler:sig_t类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数handler既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为SIG_IGN或SIG_DFL,SIG_IGN表示此进程需要忽略该信号,SIG_DFL则表示设置为系统默认操作。sig_t函数指针的int类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。

Tips:SIG_IGN、SIG_DFL分别取值如下:

/* Fake signal functions. */

#define SIG_ERR ((sig_t) -1) /* Error return. */

#define SIG_DFL ((sig_t) 0) /* Default action. */

#define SIG_IGN ((sig_t) 1) /* Ignore signal. */

返回值:此函数的返回值也是一个sig_t类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回SIG_ERR,并会设置errno。

由此可知,signal()函数可以根据第二个参数handler的不同设置情况,可对信号进行不同的处理。

测试

signal()函数的用法其实非常简单,为信号设置相应的处理方式,接下来编写一个简单地示例代码对signal()函数进行测试。

示例代码 8.4.1 signal()函数使用示例

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

static void sig_handler(int sig)

{

printf("Received signal: %dn", sig);

}

int main(int argc, char *argv[])

{

sig_t ret = NULL;

ret = signal(SIGINT, (sig_t)sig_handler);

if (SIG_ERR == ret) {

perror("signal error");

exit(-1);

}

/* 死循环 */

for ( ; ; ) { }

exit(0);

}

在上述示例代码中,我们通过signal()函数将SIGINT(2)信号绑定到了一个用户自定的处理函数上sig_handler(int sig),当进程收到SIGINT信号后会执行该函数然后运行printf打印语句。当运行程序之后,程序会卡在for死循环处,此时在终端按下中断符CTRL C,系统便会给前台进程组中的每一个进程发送SIGINT信号,我们测试程序便会收到该信号。

运行测试:

图 8.4.1 测试结果

当运行程序之后,程序会占用终端称为一个前台进程,此时按下中断符便会打印出信息(^C表示按下了中断符)。平时大家使用CTRL C可以终止一个进程,而这里却不能通过这种方式来终止这个测试程序,原因在于测试程序中捕获了该信号,而对应的处理方式仅仅只是打印一条语句、而并不终止进程。

那此时该怎么关闭这个测试程序呢?前面给大家介绍了“一击必杀”信号SIGKILL(编号为9),可向该进程发送SIGKILL暴力终止该进程,当然一般不推荐大家这样使用,如果实在没办法才采取这种措施。新打开一个终端,使用ps命令找到该进程的pid号,再使用kill命令,如下所示:

图 8.4.2 一击必杀

此时测试程序就会强制终止:

图 8.4.3 测试程序被终止

Tips:普通用户只能杀死该用户自己的进程,无权限杀死其它用户的进程。

我们再执行一次测试程序,这里将测试程序放在后台运行,然后再按下中断符:

图 8.4.4 测试结果

按下中断符发现进程并没有收到SIGINT信号,原因很简单,因为进程并不是前台进程、而是一个后台进程,按下中断符时系统并不会给后台进程发送SIGINT信号。可以使用kill命令手动发送信号给我们的进程:

图 8.4.5 测试结果

两种不同状态下信号的处理方式

通过上面的介绍,以及我们的测试实验,不知大家是否出现了一个疑问?如果程序中没有调用signal()函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到signal()处,那么这时进程接收到一个信号后是如何处理的呢?带着这个问题来聊一聊。

程序启动

当一个应用程序刚启动的时候(或者程序中没有调用signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。所以如果在我们的程序当中,没有调用signal()为信号设置处理方式,则默认的处理方式便是系统默认操作。

所以为什么大家平时都可以使用CTRL C中断符来终止一个进程,因为大部分情况下,应用程序中并不会为SIGINT信号设置处理方式,所以该信号的处理方式便是系统默认操作,当接收到信号之后便执行系统默认操作,而SIGINT信号的系统默认操作便是终止进程。

进程创建

当一个进程调用fork()创建子进程时,其子进程将会继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中是有意义的。

sigaction()函数

除了signal()之外,sigaction()系统调用是设置信号处理方式的另一选择,事实上,推荐大家使用sigaction()函数。虽然signal()函数简单好用,而sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性。

sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

signum:需要设置的信号,除了SIGKILL信号和SIGSTOP信号之外的任何信号。

act:act参数是一个struct sigaction类型指针,指向一个struct sigaction数据结构,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数act不为NULL,则表示需要为信号设置新的处理方式;如果参数act为NULL,则表示无需改变信号当前的处理方式。

oldact:oldact参数也是一个struct sigaction类型指针,指向一个struct sigaction数据结构。如果参数oldact不为NULL,则会将信号之前的处理方式等信息通过参数oldact返回出来;如果无意获取此类信息,那么可将该参数设置为NULL。

返回值:成功返回0;失败将返回-1,并设置errno。

struct sigaction结构体

示例代码 8.4.2 struct sigaction结构体

struct sigaction {

void (*sa_handler)(int);

void (*sa_sigaction)(int, siginfo_t *, void *);

sigset_t sa_mask;

int sa_flags;

void (*sa_restorer)(void);

};

结构体成员介绍:

sa_handler:指定信号处理函数,与signal()函数的handler参数相同。

sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过siginfo_t参数获取,稍后介绍该数据结构;sa_handler和sa_sigaction是互斥的,不能同时设置,对于标准信号来说,使用sa_handler就可以了,可通过SA_SIGINFO标志进行选择。

sa_mask:参数sa_mask定义了一组信号,当进程在执行由sa_handler所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现,如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数sa_mask来完成(此参数是sigset_t类型变量,关于该类型的介绍信息请看8.8小节内容,关于信号掩码还会在8.9小节中进一步介绍),信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。

sa_restorer:该成员已过时,不要再使用了。

sa_flags:参数sa_flags指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志(多个标志使用位或" | "组合):

SA_NOCLDSTOP

如果signum为SIGCHLD,则子进程停止时(即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU中的一种时)或恢复(即它们接收到SIGCONT)时不会收到SIGCHLD信号。

SA_NOCLDWAIT

如果signum是SIGCHLD,则在子进程终止时不要将其转变为僵尸进程。

SA_NODEFER

不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了SA_NODEFER标志,则表示不对它进行阻塞。

SA_RESETHAND

执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。

SA_RESTART

被信号中断的系统调用,在信号处理完成之后将自动重新发起。

SA_SIGINFO

如果设置了该标志,则表示使用sa_sigaction作为信号处理函数、而不是sa_handler,关于sa_sigaction信号处理函数的参数信息。

以上就是关于struct sigaction结构体相关的内容介绍了,接下编写程序进行实战测试。

siginfo_t结构体

示例代码 8.4.3 siginfo_t结构体

siginfo_t {

int si_signo; /* Signal number */

int si_errno; /* An errno value */

int si_code; /* Signal code */

int si_trapno; /* Trap number that caused hardware-generated signal(unused on most architectures) */

pid_t si_pid; /* Sending process ID */

uid_t si_uid; /* Real user ID of sending process */

int si_status; /* Exit value or signal */

clock_t si_utime; /* User time consumed */

clock_t si_stime; /* System time consumed */

sigval_t si_value; /* Signal value */

int si_int; /* POSIX.1b signal */

void *si_ptr; /* POSIX.1b signal */

int si_overrun; /* Timer overrun count; POSIX.1b timers */

int si_timerid; /* Timer ID; POSIX.1b timers */

void *si_addr; /* Memory location which caused fault */

long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */

int si_fd; /* File descriptor */

short si_addr_lsb; /* Least significant bit of address(since Linux 2.6.32) */

void *si_call_addr; /* Address of system call instruction(since Linux 3.5) */

int si_syscall; /* Number of attempted system call(since Linux 3.5) */

unsigned int si_arch; /* Architecture of attempted system call(since Linux 3.5) */

}

这个结构体就不给大家介绍了,使用man手册查看sigaction()函数帮助信息时,在下面会有介绍。

测试

这里使用sigaction()函数实现与示例代码 8.4.1相同的功能。

示例代码 8.4.4 sigaction()函数使用示例

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

static void sig_handler(int sig)

{

printf("Received signal: %dn", sig);

}

int main(int argc, char *argv[])

{

struct sigaction sig = {0};

int ret;

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

ret = sigaction(SIGINT, &sig, NULL);

if (-1 == ret) {

perror("sigaction error");

exit(-1);

}

/* 死循环 */

for ( ; ; ) { }

exit(0);

}

运行测试:

图 8.4.6 测试结果

关于信号处理函数说明

一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗CPU时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

向进程发送信号

与kill命令相类似,Linux系统提供了kill()系统调用,一个进程可通过kill()向另一个进程发送信号;除了kill()系统调用之外,Linux系统还提供了系统调用killpg()以及库函数raise(),也可用于实现发送信号的功能,本小节将向大家进行介绍。

kill()函数

kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程,其函数原型如下所示:

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

使用该函数需要包含头文件<sys/types.h>和<signal.h>。

函数参数和返回值含义如下:

pid:参数pid为正数的情况下,用于指定接收此信号的进程pid;除此之外,参数pid也可设置为0或-1以及小于-1等不同值,稍后给说明。

sig:参数sig指定需要发送的信号,也可设置为0,如果参数sig设置为0则表示不发送信号,但任执行错误检查,这通常可用于检查参数pid指定的进程是否存在。

返回值:成功返回0;失败将返回-1,并设置errno。

参数pid不同取值含义:

如果pid为正,则信号sig将发送到pid指定的进程。

如果pid等于0,则将sig发送到当前进程的进程组中的每个进程。

如果pid等于-1,则将sig发送到当前进程有权发送信号的每个进程,但进程1(init)除外。

如果pid小于-1,则将sig发送到ID为-pid的进程组中的每个进程。

进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户ID或有效用户ID必须等于接收者进程的实际用户ID或有效用户ID。

从上面介绍可知,当sig为0时,仍可进行正常执行的错误检查,但不会发送信号,这通常可用于确定一个特定的进程是否存在,如果向一个不存在的进程发送信号,kill()将会返回-1,errno将被设置为ESRCH,表示进程不存在。

测试

(1)使用kill()函数向一个指定的进程发送信号。

示例代码 8.5.1 kill()函数使用示例

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <signal.h>

#include <stdlib.h>

int main(int argc, char *argv[])

{

int pid;

/* 判断传参个数 */

if (2 > argc)

exit(-1);

/* 将传入的字符串转为整形数字 */

pid = atoi(argv[1]);

printf("pid: %dn", pid);

/* 向pid指定的进程发送信号 */

if (-1 == kill(pid, SIGINT)) {

perror("kill error");

exit(-1);

}

exit(0);

}

以上代码通过kill()函数向指定进程发送SIGINT信号,可通过外部传参将接收信号的进程pid传入到程序中,再执行该测试代码之前,需要运行先一个用于接收此信号的进程,接收信号的进程直接使用示例代码 8.4.3程序。

运行测试:

图 8.5.1 测试结果

testApp1是示例代码 8.4.3对应的程序,testApp则是示例代码 8.5.1对应的程序,首先执行"./testApp1 &"将接收信号的程序置于后台运行(其进程pid为21825),接着执行"./testApp 21825"向接收信号的进程发送SIGINT信号。

raise()

有时进程需要向自身发送信号,raise()函数可用于实现这一要求,raise()函数原型如下所示(此函数为C库函数):

#include <signal.h>

int raise(int sig);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

sig:需要发送的信号。

返回值:成功返回0;失败将返回非零值。

raise()其实等价于:

kill(getpid(), sig);

Tips:getpid()函数用于获取进程自身的pid。

测试

示例代码 8.5.2 raise()函数使用示例

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig)

{

printf("Received signal: %dn", sig);

}

int main(int argc, char *argv[])

{

struct sigaction sig = {0};

int ret;

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

ret = sigaction(SIGINT, &sig, NULL);

if (-1 == ret) {

perror("sigaction error");

exit(-1);

}

for ( ; ; ) {

/* 向自身发送SIGINT信号 */

if (0 != raise(SIGINT)) {

printf("raise errorn");

exit(-1);

}

sleep(3); // 每隔3秒发送一次

}

exit(0);

}

运行结果:

图 8.5.2 测试结果

alarm()和pause()函数

本小节向大家介绍两个系统调用alarm()和pause()。

alarm()函数

使用alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送SIGALRM信号,其函数原型如下所示:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

函数参数和返回值:

seconds:设置定时时间,以秒为单位;如果参数seconds等于0,则表示取消之前设置的alarm闹钟。

返回值:如果在调用alarm()时,之前已经为该进程设置了alarm闹钟还没有超时,则该闹钟的剩余值作为本次alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0。

参数seconds的值是产生SIGALRM信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,每个进程只能设置一个alarm闹钟;虽然SIGALRM信号的系统默认操作是终止进程,但是如果程序当中设置了alarm闹钟,但大多数使用闹钟的进程都会捕获此信号。

需要注意的是alarm闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在SIGALRM信号处理函数中再次调用alarm()函数设置定时器。

测试

使用alarm()来设计一个闹钟。

示例代码 8.6.1 alarm()函数使用示例

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig)

{

puts("Alarm timeout");

exit(0);

}

int main(int argc, char *argv[])

{

struct sigaction sig = {0};

int second;

/* 检验传参个数 */

if (2 > argc)

exit(-1);

/* 为SIGALRM信号绑定处理函数 */

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

if (-1 == sigaction(SIGALRM, &sig, NULL)) {

perror("sigaction error");

exit(-1);

}

/* 启动alarm定时器 */

second = atoi(argv[1]);

printf("定时时长: %d秒n", second);

alarm(second);

/* 循环 */

for ( ; ; )

sleep(1);

exit(0);

}

运行测试:

图 8.6.1 测试结果

pause()函数

pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将errno设置为EINTR。其函数原型如下所示:

#include <unistd.h>

int pause(void);

测试

通过alarm()和pause()函数模拟sleep功能。

示例代码 8.6.2 alarm()和pause()模拟sleep

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig)

{

puts("Alarm timeout");

}

int main(int argc, char *argv[])

{

struct sigaction sig = {0};

int second;

/* 检验传参个数 */

if (2 > argc)

exit(-1);

/* 为SIGALRM信号绑定处理函数 */

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

if (-1 == sigaction(SIGALRM, &sig, NULL)) {

perror("sigaction error");

exit(-1);

}

/* 启动alarm定时器 */

second = atoi(argv[1]);

printf("定时时长: %d秒n", second);

alarm(second);

/* 进入休眠状态 */

pause();

puts("休眠结束");

exit(0);

}

运行测试:

图 8.6.2 测试结果

信号集

通常我们需要有一个能表示多个信号(一组信号)的数据类型---信号集(signal set),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如sigaction()函数、sigprocmask()函数、sigpending()函数等。本小节向大家介绍信号集这个数据类型。

信号集其实就是sigset_t类型数据结构,来看看:

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))

typedef struct

{

unsigned long int __val[_SIGSET_NWORDS];

} sigset_t;

使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中,当然Linux系统了用于操作sigset_t信号集的API,譬如sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember(),接下来向大家介绍。

初始化信号集

sigemptyset()和sigfillset()用于初始化信号集。sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),函数原型如下:

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

使用这些函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

set:指向需要进行初始化的信号集变量。

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例

初始化为空信号集:

sigset_t sig_set;

sigemptyset(&sig_set);

初始化信号集,使其包含所有信号:

sigset_t sig_set;

sigfillset(&sig_set);

向信号集中添加/删除信号

分别使用sigaddset()和sigdelset()函数向信号集中添加或移除一个信号,函数原型如下:

#include <signal.h>

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

函数参数和返回值含义如下:

set:指向信号集。

signum:需要添加/删除的信号。

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例

向信号集中添加信号:

sigset_t sig_set;

sigemptyset(&sig_set);

sigaddset(&sig_set, SIGINT);

从信号集中移除信号:

sigset_t sig_set;

sigfillset(&sig_set);

sigdelset(&sig_set, SIGINT);

测试信号是否在信号集中

使用sigismember()函数可以测试某一个信号是否在指定的信号集中,函数原型如下所示:

#include <signal.h>

int sigismember(const sigset_t *set, int signum);

函数参数和返回值含义如下:

set:指定信号集。

signum:需要进行测试的信号。

返回值:如果信号signum在信号集set中,则返回1;如果不在信号集set中,则返回0;失败则返回-1,并设置errno。

使用示例

判断SIGINT信号是否在sig_set信号集中:

sigset_t sig_set;

......

if (1 == sigismember(&sig_set, SIGINT))

puts("信号集中包含SIGINT信号");

else if (!sigismember(&sig_set, SIGINT))

puts("信号集中不包含SIGINT信号");

获取信号的描述信息

在Linux下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于sys_siglist数组中,sys_siglist数组是一个char *类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。譬如,可以使用sys_siglist[SIGINT]来获取对SIGINT信号的描述。我们编写一个简单地程序进行测试:

Tips:使用sys_siglist数组需要包含<signal.h>头文件。

示例代码 8.8.1 从sys_siglist数组获取信号描述信息

#include <signal.h>

#include <stdio.h>

#include <stdlib.h>

int main(void)

{

printf("SIGINT描述信息: %sn", sys_siglist[SIGINT]);

printf("SIGQUIT描述信息: %sn", sys_siglist[SIGQUIT]);

printf("SIGBUS描述信息: %sn", sys_siglist[SIGBUS]);

exit(0);

}

运行结果:

图 8.8.1 测试结果

从图中打印信息可知,这个描述信息其实非常简洁,没什么太多的信息。

strsignal()函数

除了直接使用sys_siglist数组获取描述信息之外,还可以使用strsignal()函数。较之于直接引用sys_siglist数组,更推荐使用strsignal()函数,其函数原型如下所示:

#include <string.h>

char *strsignal(int sig);

使用strsignal()函数需要包含头文件<string.h>,这是一个库函数。

调用strsignal()函数将会获取到参数sig指定的信号对应的描述信息,返回该描述信息字符串的指针;函数会对参数sig进行检查,若传入的sig无效,则会返回"Unknown signal"信息。

使用示例

示例代码 8.8.2 strsignal()函数使用示例

#include <signal.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int main(void)

{

printf("SIGINT描述信息: %sn", strsignal(SIGINT));

printf("SIGQUIT描述信息: %sn", strsignal(SIGQUIT));

printf("SIGBUS描述信息: %sn", strsignal(SIGBUS));

printf("编号为1000的描述信息: %sn", strsignal(1000));

exit(0);

}

测试结果:

图 8.8.2 测试结果

psignal()函数

psignal()可以在标准错误(stderr)上输出信号描述信息,其函数原型如下所示:

#include <signal.h>

void psignal(int sig, const char *s);

调用psignal()函数会将参数sig指定的信号对应的描述信息输出到标准错误,并且还允许调用者添加一些输出信息,由参数s指定;所以整个输出信息由字符串s、冒号、空格、描述信号编号sig的字符串和尾随的换行符组成。

使用示例

示例代码 8.8.3 psignal()函数使用示例

#include <signal.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

int main(void)

{

psignal(SIGINT, "SIGINT信号描述信息");

psignal(SIGQUIT, "SIGQUIT信号描述信息");

psignal(SIGBUS, "SIGBUS信号描述信息");

exit(0);

}

运行结果:

图 8.8.3 运行结果

信号掩码(阻塞信号传递)

内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。

向信号掩码中添加一个信号,通常有如下几种方式:

当应用程序调用signal()或sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于sigaction()而言,是否会如此,需要根据sigaction()函数是否设置了SA_NODEFER标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。

使用sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过sa_mask参数进行设置,参考8.4.2小节内容。

除了以上两种方式之外,还可以使用sigprocmask()系统调用,随时可以显式地向信号掩码中添加/移除信号。

sigprocmask()函数原型如下所示:

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

how:参数how指定了调用函数时的一些行为。

set:将参数set指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数set为NULL,则表示无需对当前信号掩码作出改动。

oldset:如果参数oldset不为NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在oldset所指定的信号集中;如果为NULL则表示不获取当前的信号掩码。

返回值:成功返回0;失败将返回-1,并设置errno。

参数how可以设置为以下宏:

SIG_BLOCK:将参数set所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与set的并集。

SIG_UNBLOCK:将参数set指向的信号集内的所有信号从进程信号掩码中移除。

SIG_SETMASK:进程信号掩码直接设置为参数set指向的信号集。

使用示例

将信号SIGINT添加到进程的信号掩码中:

int ret;

/* 定义信号集 */

sigset_t sig_set;

/* 将信号集初始化为空 */

sigemptyset(&sig_set);

/* 向信号集中添加SIGINT信号 */

sigaddset(&sig_set, SIGINT);

/* 向进程的信号掩码中添加信号 */

ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);

if (-1 == ret) {

perror("sigprocmask error");

exit(-1);

}

从信号掩码中移除SIGINT信号:

int ret;

/* 定义信号集 */

sigset_t sig_set;

/* 将信号集初始化为空 */

sigemptyset(&sig_set);

/* 向信号集中添加SIGINT信号 */

sigaddset(&sig_set, SIGINT);

/* 从信号掩码中移除信号 */

ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);

if (-1 == ret) {

perror("sigprocmask error");

exit(-1);

}

下面我们编写一个简单地测试代码,验证信号掩码的作用,测试代码如下所示:

示例代码 8.9.1 测试信号掩码的作用

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig)

{

printf("执行信号处理函数...n");

}

int main(void)

{

struct sigaction sig = {0};

sigset_t sig_set;

/* 注册信号处理函数 */

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

if (-1 == sigaction(SIGINT, &sig, NULL))

exit(-1);

/* 信号集初始化 */

sigemptyset(&sig_set);

sigaddset(&sig_set, SIGINT);

/* 向信号掩码中添加信号 */

if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))

exit(-1);

/* 向自己发送信号 */

raise(SIGINT);

/* 休眠2秒 */

sleep(2);

printf("休眠结束n");

/* 从信号掩码中移除添加的信号 */

if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))

exit(-1);

exit(0);

}

上述代码中,我们为SIGINT信号注册了一个处理函数sig_handler,当进程接收到该信号之后就会执行它;然后调用sigprocmask函数将SIGINT信号添加到信号掩码中,然后再调用raise(SIGINT)向自己发送一个SIGINT信号,如果信号掩码没有生效、也就意味着SIGINT信号不会被阻塞,那么调用raise(SIGINT)之后应该就会立马执行sig_handler函数,从而打印出"执行信号处理函数..."字符串信息;如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在2秒后才执行,因为程序中使用sleep(2)休眠了2秒钟之后,才将SIGINT信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。

编译测试结果如下:

图 8.9.1 测试结果

阻塞等待信号sigsuspend()

上一小节已经说明,更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的关键代码段。如果希望对一个信号解除阻塞后,然后调用pause()以等待之前被阻塞的信号的传递,这将如何?譬如有如下代码段:

sigset_t new_set, old_set;

/* 信号集初始化 */

sigemptyset(&new_set);

sigaddset(&new_set, SIGINT);

/* 向信号掩码中添加信号 */

if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set))

exit(-1);

/* 受保护的关键代码段 */

......

/**********************/

/* 恢复信号掩码 */

if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL))

exit(-1);

pause();/* 等待信号唤醒 */

执行受保护的关键代码时不希望被SIGINT信号打断,所以在执行关键代码之前将SIGINT信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。最后调用了pause()阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从pause返回继续执行;考虑到这样一种情况,如果信号的传递恰好发生在第二次调用sigprocmask()之后、pause()之前,如果确实发生了这种情况,就会产生一个问题,信号传递过来就会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到pause()被阻塞,知道下一次信号发生时才会被唤醒,这有违代码的本意。

虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能,这必然是一个缺陷,要避免这个问题,需要将恢复信号掩码和pause()挂起进程这两个动作封装成一个原子操作,这正是sigsuspend()系统调用的目的所在,sigsuspend()函数原型如下所示:

#include <signal.h>

int sigsuspend(const sigset_t *mask);

使用该函数需要包含头文件#include <signal.h>。

函数参数和返回值含义如下:

mask:参数mask指向一个信号集。

返回值:sigsuspend()始终返回-1,并设置errno来指示错误(通常为EINTR),表示被信号所中断,如果调用失败,将errno设置为EFAULT。

sigsuspend()函数会将参数mask所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数mask所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是mask信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend()会将进程的信号掩码恢复成调用前的值。

调用sigsuspend()函数相当于以不可中断(原子操作)的方式执行以下操作:

sigprocmask(SIG_SETMASK, &mask, &old_mask);

pause();

sigprocmask(SIG_SETMASK, &old_mask, NULL);

使用示例

示例代码 8.10.1 sigsuspend()函数使用示例

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig)

{

printf("执行信号处理函数...n");

}

int main(void)

{

struct sigaction sig = {0};

sigset_t new_mask, old_mask, wait_mask;

/* 信号集初始化 */

sigemptyset(&new_mask);

sigaddset(&new_mask, SIGINT);

sigemptyset(&wait_mask);

/* 注册信号处理函数 */

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

if (-1 == sigaction(SIGINT, &sig, NULL))

exit(-1);

/* 向信号掩码中添加信号 */

if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))

exit(-1);

/* 执行保护代码段 */

puts("执行保护代码段");

/******************/

/* 挂起、等待信号唤醒 */

if (-1 != sigsuspend(&wait_mask))

exit(-1);

/* 恢复信号掩码 */

if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))

exit(-1);

exit(0);

}

在上述代码中,我们希望执行受保护代码段时不被SIGINT中断信号打断,所以在执行保护代码段之前将SIGINT信号添加到进程的信号掩码中,执行完受保护的代码段之后,调用sigsuspend()挂起进程,等待被信号唤醒,被唤醒之后再解除SIGINT信号的阻塞状态。

实时信号

如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()函数获取。

sigpending()函数

其函数原型如下所示:

#include <signal.h>

int sigpending(sigset_t *set);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

set:处于等待状态的信号会存放在参数set所指向的信号集中。

返回值:成功返回0;失败将返回-1,并设置errno。

使用示例

判断SIGINT信号当前是否处于等待状态

/* 定义信号集 */

sigset_t sig_set;

/* 将信号集初始化为空 */

sigemptyset(&sig_set);

/* 获取当前处于等待状态的信号 */

sigpending(&sig_set);

/* 判断SIGINT信号是否处于等待状态 */

if (1 == sigismember(&sig_set, SIGINT))

puts("SIGINT信号处于等待状态");

else if (!sigismember(&sig_set, SIGINT))

puts("SIGINT信号未处于等待状态");

发送实时信号

等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之,如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一。

实时信号较之于标准信号,其优势如下:

实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用:SIGUSR1和SIGUSR2。

内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反,对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。

当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。

不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。

Linux内核定义了31个不同的实时信号,信号编号范围为34~64,使用SIGRTMIN表示编号最小的实时信号,使用SIGRTMAX表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数。

应用程序当中使用实时信号,需要有以下的两点要求:

发送进程使用sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。

接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction指针指向的处理函数,而不是sa_handler,当然允许应用程序使用sa_handler,但这样就不能获取到实时信号的伴随数据了。

使用sigqueue()函数发送实时信号,其函数原型如下所示:

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

pid:指定接收信号的进程对应的pid,将信号发送给该进程。

sig:指定需要发送的信号。与kill()函数一样,也可将参数sig设置为0,用于检查参数pid所指定的进程是否存在。

value:参数value指定了信号的伴随数据,union sigval数据类型。

返回值:成功将返回0;失败将返回-1,并设置errno。

union sigval数据类型(共用体)如下所示:

typedef union sigval

{

int sival_int;

void *sival_ptr;

} sigval_t;

携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针。

使用示例

(1)发送进程使用sigqueue()系统调用向另一个进程发送实时信号

示例代码 8.11.1 使用sigqueue()函数发送信号

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

int main(int argc, char *argv[])

{

union sigval sig_val;

int pid;

int sig;

/* 判断传参个数 */

if (3 > argc)

exit(-1);

/* 获取用户传递的参数 */

pid = atoi(argv[1]);

sig = atoi(argv[2]);

printf("pid: %dnsignal: %dn", pid, sig);

/* 发送信号 */

sig_val.sival_int = 10; //伴随数据

if (-1 == sigqueue(pid, sig, sig_val)) {

perror("sigqueue error");

exit(-1);

}

puts("信号发送成功!");

exit(0);

}

(2)接收进程使用sigaction()函数为信号绑定处理函数

示例代码 8.11.2 使用sigaction()函数为实时信号绑定处理函数

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig, siginfo_t *info, void *context)

{

sigval_t sig_val = info->si_value;

printf("接收到实时信号: %dn", sig);

printf("伴随数据为: %dn", sig_val.sival_int);

}

int main(int argc, char *argv[])

{

struct sigaction sig = {0};

int num;

/* 判断传参个数 */

if (2 > argc)

exit(-1);

/* 获取用户传递的参数 */

num = atoi(argv[1]);

/* 为实时信号绑定处理函数 */

sig.sa_sigaction = sig_handler;

sig.sa_flags = SA_SIGINFO;

if (-1 == sigaction(num, &sig, NULL)) {

perror("sigaction error");

exit(-1);

}

/* 死循环 */

for ( ; ; )

sleep(1);

exit(0);

}

异常退出abort()函数

在3.3小节中给大家介绍了应用程序中结束进程的几种方法,譬如使用exit()、_exit()或_Exit()这些函数来终止进程,然后这些方法使用于正常退出应用程序,而对于异常退出程序,则一般使用abort()库函数,使用abort()终止进程运行,会生成核心转储文件,可用于判断程序调用abort()时的程序状态。

abort()函数原型如下所示:

#include <stdlib.h>

void abort(void);

函数abort()通常产生SIGABRT信号来终止调用该函数的进程,SIGABRT信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用abort()函数之后,内核会向进程发送SIGABRT信号。

使用示例

示例代码 8.12.1 abort()终止进程

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

static void sig_handler(int sig)

{

printf("接收到信号: %dn", sig);

}

int main(int argc, char *argv[])

{

struct sigaction sig = {0};

sig.sa_handler = sig_handler;

sig.sa_flags = 0;

if (-1 == sigaction(SIGABRT, &sig, NULL)) {

perror("sigaction error");

exit(-1);

}

sleep(2);

abort(); // 调用abort

for ( ; ; )

sleep(1);

exit(0);

}

运行测试:

图 8.12.1 测试结果

从打印信息可知,即使在我们的程序当中捕获了SIGABRT信号,但是程序依然会无情的终止,无论阻塞或忽略SIGABRT信号,abort()调用均不收到影响,总会成功终止进程。

免责声明:本文由用户上传,如有侵权请联系删除!