码农翻身讲操作系统2:进程,线程与操作系统那些事-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

码农翻身讲操作系统2:进程,线程与操作系统那些事

简介:   我听说我的祖先们生活在专用计算机里, 一生只帮助人类做一件事情,比说微积分运算 了、人口统计了 、生成密码、甚至通过织布机印花 ! 如果你想在这些专用“计算机”上干点别的事儿,例如安装个游戏玩玩, 那是绝对不可能的, 除非你把它拆掉, 然后建一个全新的机器。

 

我听说我的祖先们生活在专用计算机里, 一生只帮助人类做一件事情,比说微积分运算 了、人口统计了 、生成密码、甚至通过织布机印花 !

如果你想在这些专用“计算机”上干点别的事儿,例如安装个游戏玩玩, 那是绝对不可能的, 除非你把它拆掉, 然后建一个全新的机器。 而我这些祖先们勉强可以称为“程序”。

后来有个叫冯诺依曼的人, 非常了不起, 他提出了“存储程序”的思想, 并且把计算机分为五大部件: 运算器、控制器、存储器、输入设备、输出设备。

各种各样不同功能的程序写好以后,和程序使用的数据一起存放在计算机的存储器中,即“存储程序”;然后,计算机按照存储的程序逐条取出指令加以分析,并执行指令所规定的操作。

这样一来,原来的专用计算机变成了通用的计算机,不管你是计算导弹弹道的,模拟核爆炸的,还是计算个人所得税的, 统统都可以在一台机器上运行, 我就是其中的一员: 专门计算员工的薪水。

 

进程的诞生


我所在的计算机是个批处理系统, 每次上机时, 我和其他程序都排好队, 一个接一个的进入内存运行。

每个月末是发薪日, 我都要运行一次,这样我每月都能见一次CPU阿甘, 这个沉默寡言,但是跑的非常快的家伙。

我知道内存看阿甘不顺眼,还告了它一状,说他一遇到IO操作的时候,就歇着喝茶 ,从来不管不问内存和硬盘的忙的要死的惨境。 
(码农翻身注:参见《CPU阿甘》和《CPU阿甘之烦恼》)

其实我倒是觉得挺好, 这时候正好和阿甘海阔天空的聊天,他阅程序无数, 知道很多内部消息,每一个字节都清清楚楚, 和他聊天实在是爽。

又到了月末发薪水的时候, 我刚一进入内存,便看到这么一个公告:

公告 
为了创建和谐社会,促进效率和公平, 充分发挥每一个人的能力,经系统党委慎重研究决定:本系统自即日起,正式从“批处理系统”转为“多道程序系统”, 希望各部门通力配合,一起完成切换工作。 
系统党委 
xxxx年xx月xx日

我正想着啥是多道程序系统, 阿甘便打电话给内存要我的指令开始运行了。

和之前一样,运行到了第13869123行,这是个IO指令, 我欢天喜地的准备和阿甘开聊了。

阿甘说: 哥们, 准备保存现场吧, 我要切换到另外一个程序来运行啦!

“啊 ? 我这正运行着呢! 咱们不喝茶了?

“喝啥茶啊,马上另外一个程序就来了!”

” 那我什么时候回来再见你?” 我问道。

“等这个IO指令完成,然后操作系统老大会再给你机会运行的。”

“那谁来记住我当前正在运行第13869123行? 还有刚把两个数据从内存装载到了你的寄存器,就是那个EAX, EBX , 你一切换岂不都丢了? ” 我有点着急。

阿甘说:“所以要暂时保存起来啊, 不仅仅是这些,还有你的那些函数在调用过程中形成的栈帧和栈顶, 我这里用寄存器EBP和ESP维护着, 都得保存起来。” 
(码农翻身注:参见《CPU阿甘之函数调用的秘密》)

“还有” 阿甘接着说,“你打开的文件句柄,你的程序段和数据段的地址, 你已经使用CPU的时间,等待CPU的时间。。。。。。 以及其他好多好多的东西,统统都要保存下来。”

我瞪大了眼睛: “这也太麻烦了吧, 原来我只需要关心我的指令和数据, 现在还得整这么多稀奇古怪的东西”

“没办法,这就叫做上下文切换, 把你的工作现场保存好,这样下一次运行的时候才能恢复啊。 对了,老大给你们统一起了一个新的名称: 进程 ! 刚才那些需要保存的东西叫做叫做 进程控制块 (Processing Control Block, PCB ),”

我想了想,这个名字还挺贴切的, 一个真正进行的程序! 只是这个正在进行的程序随时可以被打断啊。

我只好保存好上下文,撤出CPU, 回到内存里歇着去了,与此同时另外一个程序开始占据CPU运行。

其实我这个程序,奥,不对,我这个进程被放到一个阻塞队列里,等到IO的数据来了以后,又被赶到了就绪队列中, 最后才有机会再次运行,再次见到CPU阿甘。 
(码农翻身: 进程的就绪, 阻塞,运行这三个状态的转换和《我是一个线程》中描述的非常类似)

阿甘从我的PCB中取出各种保存的信息,恢复了运行时现场, 可是忙活了好一阵, 没办法,这就是程序切换必须要付出的代价。

我有点同情阿甘了, 从此以后,他很难再悠闲和和我们海阔天空,每时每刻都处于高速的奔跑中。

得益于阿甘的高速度, 虽然在同一时刻只有一个程序在运行, 但是有很多程序在短时间内不断的切换, 在外界看来, 似乎多个程序在同时执行。

尤其是那些速度超慢的人类, 他们开着电脑一边听歌,一边上网,一边QQ, 很是自在, 理所当然的认为这些程序就是同时在运行。 岂不知阿甘是让音乐播放器上运行几十毫秒, 然后打断,让浏览器进程运行几十毫秒,再打断,让QQ也运行几十毫秒,如此循环往复。

唉,阿甘真是能者多劳啊, 这个计算机系统也算是达到了我们党委的目标:兼顾了效率和公平。

 

线程


有了进程就万事大吉了吗? 人类的欲望是无止境的,很快就出现了新情况, 举个例子来说吧,我有一个兄弟,是个文字处理软件, 他和我不一样, 他有界面, 人类在用的时候能看到, 这实在是很幸福, 不像我总是在背后默默工作,几乎无人知晓。

这哥们有个智能的小功能,就是在人类编辑文档的时候能自动保存, 防止辛辛苦苦敲的文字由于断电什么的丢掉。

可是这个功能导致了人类的抱怨, 原因很简单,自动保存文字是和IO打交道,那硬盘有多慢你也知道, 这个时候整个进程就被挂起了, 给人类的感觉就是: 程序死了,键盘和鼠标不响应了! 无法继续输入文字, 但是过一会儿就好了。

并且这种假死一会儿就会出现一次(每当自动保存的时候), 让人不胜其烦。

系统党委研究了很久, 他们当然可以用两个进程来解决问题, 一个进程负责和用户交互, 另外一个进程负责自动保存, 但是,这两个进程之间完全是独立的,每个人都有自己的一亩三分地(地址空间), 完全互不知晓, 进程之间通信的开销实在是太大, 他们没有办法高效的操作那同一份文档数据。

后来还是劳模阿甘想出了一招 : 可以采用多进程的伟大思想啊!

把一个进程当成一个资源的容器, 让里边运行几个轻量级的进程, 就叫线程吧, 这些线程共享进程的所有资源, 例如地址空间,全局变量,文件资源等等。

但是每个线程也有自己独特的部分, 那就是要记住自己运行到哪一行指令了, 有自己的函数调用堆栈,自己的状态等等, 总而言之,就是为了能像切换进程那样切换线程。

这里写图片描述

拿我那个哥们的情况来说, 一个进程保存着文档的数据, 进程中有两个线程, 一个负责和用户交互, 另外一个专门负责定时的自动保存, IO导致的阻塞就不会影响另外一个了。

注意,这两个线程都能访问进程的所有东西, 他们两个要小心,不要发起冲突才好 – 这是人类程序员要做的事情了, 不归我们管。

 

争吵


阿甘的建议被采纳了, 其实这几乎是唯一的解决问题方式了, 但是由谁来管理引起了激烈争吵。

系统党委有一波人坚持要在用户空间实现线程, 换通俗的话说就是让那些进程在自个儿内部去管理线程, 他们的理由也很充分: 
你们自己实现了线程,可以自己定制自己的调度算法,多灵活啊;

所有的线程切换都在进程内完成, 不用请求我们操作系统内核来处理,效率多高啊;

况且你们可以在那些内核不支持线程的操作系统中运行, 移植性多好啊。

这里写图片描述

我们清楚的知道这是内核想做甩手掌柜, 因为他们选择性的忽略了一个致命的问题: 如果由我们实现线程,则操作系统内核还是认为我们只是一个进程而已,对里边的线程一无所知,对进程的调度还是以进程为最小单位。

一旦出现阻塞的系统调用,不仅仅阻塞那个线程,还会阻塞整个进程!

例如文字处理器那个进程, 如果负责定时保存的线程发起了IO调用, 内核会认为,这是由进程发起的,于是就把整个进程给挂起了, 虽然和用户交互的进程还是可以运行,也被强制的随着进程挂起来,不响应了, 这多么悲催啊, 又回到了老问题上去了。

所以我们坚决不能答应, 我们则一致的要求:在内核中实现线程 ! 内核需要知道进程中线程的存在,内核需要维护线程表,并且负责调度!

这里写图片描述

党委的人傲慢的说: 你们不嫌累吗, 每次创建一个线程都得通过我们内核, 多慢啊。

我们说:只有这样,一个线程的IO系统调用才不会阻塞我们整个进程啊, 你们完全可以选择同一个进程的另外一个线程去执行。

双发僵持不下,最后只好妥协, 那就是:混合着实现吧。

用户空间的进程可以创建线程(用户线程), 内核也会创建线程(内核线程), 用户线程映射到内核线程上。

这里写图片描述

问题基本解决了,但也带来了新的问题,我们的系统也变的越来越复杂, 尤其是进程之间的通信和线程之间的同步, 会那些程序员们带来无穷无尽的烦恼, 这是后话了, 有机会下次再说吧。

注: 本文的插图来源于《现代操作系统》和《操作系统概念》(恐龙书)这两本书, 我重画了一下。 对操作系统感兴趣的同学可以看看这两本书。

(完)


一个线程的一生

  我是一个线程,我一出生就被编了个号: 0×3704,然后被领到一个昏暗的屋子里, 这里我发现了很多和我一模一样的同伴。

  我身边的同伴0×6900 待的时间比较长, 他带着沧桑的口气对我说:

  “我们线程的宿命就是处理包裹。把包裹处理完以后还得马上回到这里,否则可能永远回不来了。”

  我一脸懵懂,包裹,什么包裹?

  “不要着急,马上你就会明白了, 我们这里是不养闲人的。”

  果然,没多久,屋子的门开了, 一个面貌凶恶的家伙吼道:

  “0×3704 ,出来!”

  我一出来就被塞了一个沉甸甸的包裹,上面还有附带着一个写满了操作步骤的纸。

  “快去,把这个包裹处理了。”

  “去哪儿处理?”

  “跟着指示走, 先到就绪车间”

  果然,地上有指示箭头,跟着它来到了一间明亮的大屋子,这里已经有不少线程了, 大家都很紧张,好像时刻准备着往前冲。

  我刚一进来,就听见广播说:“0×3704,进入车间”

  我赶紧往前走, 身后很多人议论说:

  ”他太幸运了, 刚进入就绪状态就能运行”

  “是不是有关系?”

  “不是,你看人家的优先级多高啊, 唉~”

  前边就是车间, 这里简直是太美了, 怪不得老线程总是唠叨着说:要是能一直待在这里就好了。

  这里空间大,视野好,空气清新,鸟语花香,还有很多从来没见过的人,像服务员一样等着为我服务。

  他们也都有编号, 更重要的是每个人还有个标签,上面写着:硬盘,数据库,内存,网卡...

  我现在理解不了,看看操作步骤吧:

  第一步:从包裹中取出参数

  打开包裹, 里边有个 HttpRequest 对象,可以取到 userName, password 两个参数。

  第二步:执行登录操作

  奥,原来是有人要登录啊,我把 userName/password 交给数据库服务员,他拿着数据, 慢腾腾的走了。

  他怎么这么慢?不过我是不是正好可以在车间里多待一会儿? 反正也没法执行第三步。

  就在这时,车间里的广播响了: 

  “0×3704,我是 CPU,记住你正在执行的步骤,马上带包裹离开”

  我慢腾腾的开始收拾。。。

  “快点, 别的线程马上就要进来了”

  离开这个车间, 又来到一个大屋子,这里很多线程慢腾腾的在喝茶,打牌。

  “哥们,你们没事干了?”

  “你新来的吧,你不知道我在等数据库服务员给我数据啊,据说他们比我们慢好几十万倍, 在这里好好歇吧”

  “啊? 这么慢? 我这里有人在登录系统, 能等这么长时间吗”

  “放心,你没听说过人间一天,CPU 一年吗, 我们这里是用纳秒,毫秒计时的,人间等待一秒,相当于我们好几天呢,来的及”

  干脆睡一会吧 , 不知道过了多久 ,大喇叭又开始广播了:

  “0×3704, 你的数据来了,快去执行”

  我转身就往 CPU 车间跑,发现这里的们只出不进!

  后面传来阵阵哄笑声:

  “果然是新人,不知道还得去就绪车间等”

  于是赶紧到就绪车间,这次没有那么好运了,等了好久才被再次叫进 CPU 车间。

  在等待的时候, 我听见有人小声议论:

  “听说了吗,最近有个线程被 kill 掉了”

  “为啥啊?”

  “这家伙赖在 CPU 车间不走,把 CPU 利用率一直搞成 100%,后来就被 kill 掉了”

  “Kill 掉以后弄哪儿去了”

  “可能被垃圾回收了吧”

  我心里打了个寒噤 , 赶紧接着处理,收下的动作块多了,第二步登录成功了。

  第三步:构建登录成功后的主页

  这一步有点费时间, 因为有很多 HTML 需要处理, 不知道代码谁写的,处理起来很烦人。

  我正在紧张的制作 HTM 呢, CPU 有开始叫了:

  “0×3704,我是 CPU,记住你正在执行的步骤,马上带包裹离开”

  “为啥啊”

  “每个线程只能在 CPU 上运行一段时间,到了时间就得让别人用了,你去就绪车间待着, 等着叫你吧”

  就这样, 我一直在“就绪-运行”这两个状态,不知道轮转了多少次,终于安装步骤清单把工作做完了。

  最后顺利的把包含 HTML 的包裹发了回去。

  至于登录以后干什么事儿 ,我就不管了。

  马上就要回到我那昏暗的房间了,真有点舍不得这里。

  不过相对于有些线程, 我还是幸运的, 他们运行完以后就彻底的销毁了,而我还活着!

  回到了小黑屋, 老线程0×6900 问:

  “怎么样?第一天有什么感觉?”

  “我们的世界规则很复杂,首先你不知道什么时候会被挑中执行;第二,在执行的过程中随时可能被打断,让出 CPU 车间;第三,一旦出现硬盘,数据库这样耗时的操作也得让出 CPU,去等待;第四,就是数据来了,你也不一定马上执行,还得等着 CPU 挑选”

  “小伙子理解的不错啊”

  “我不明白为什么很多线程都执行完就死了, 为什么咱们还活着?”

  “你还不知道,长生不老是我们的特权,我们这里有个正式的名称,叫做线程池!”

  平淡的日子就这么一天天过去,作为一个线程,我每天的生活都是取包裹,处理包裹,然后回到我们昏暗的家:线程池。

  有一天我回来的时候,听到有个兄弟说,今天要好好休息下,明天就是最疯狂的一天。

  我看了一眼日历,明天是 11 月 11 号。

  果然,零点刚过,不知道那些人类怎么了,疯狂的投递包裹,为了应付蜂拥而至的海量包裹,线程池里没有一个人能闲下来,全部出去处理包裹,CPU 车间利用率超高,硬盘在嗡嗡转,网卡疯狂的闪,即便如此,还是处理不完,堆积如山。

  我们也没有办法,实在是太多太多了,这些包裹中大部分都是浏览页面,下订单,买,买,买。

  不知道过了多久,包裹山终于慢慢的消失了。

  终于能够喘口气, 我想我永远都不会忘记这一天。

  通过这个事件,我明白了我所处的世界:这是一个电子商务的网站!

  我每天的工作就是处理用户的登录,浏览, 购物车,下单,付款。

  我问线程池的元老0×6900:“我们要工作到什么时候?”

  “要一直等到系统重启的那一刻”,0×6900 说。

  “那你经历过系统重启吗?”

  “怎么可能?系统重启就是我们的死亡时刻, 也就是世界末日,一旦重启,整个线程池全部销毁,时间和空间全部消失,一切从头再来”

  “那什么时候会重启?”

  “这就不好说了,好好享受眼前的生活吧…..”

  其实生活丰富多彩,我最喜欢的包裹是上传图片,由于网络慢,所以能在就绪车间,CPU 车间待很长很长时间,可以认识很多好玩的线程。

  比如说上次认识了 memecached 线程,他给我说通过他缓存了很多的用户数据, 还是分布式的! 很多机器上都有!

  我说怪不得后来的登录操作快了那么多, 原来是不再从数据库取数据了你那里就有啊,哎,对了,你是分布式的,你去过别的机器没有?

  他说怎么可能,我每次也只能通过网络往那个机器发送一个 GET, PUT 命令才存取数据而已,别的一概不知。

  再比如说上次在等待的时候遇到了数据库连接的线程,我才知道它他那里也是一个连接池,和我们线程池几乎一模一样。

  他说有些包裹太变态了,竟然查看一年的订单数据,简直把我累死了。

  我说拉倒吧你,你那是纯数据,你把数据传给我以后,我还得组装成 HTML,工作量不知道比你大多少倍。

  他说一定你要和 memecached 搞好关系,直接从他那儿拿数据,尽量少直接调用数据库,我们 JDBC connection 也能活的轻松点。

  我说好啊好啊,关键是你得提前把数据搞到缓存啊,要不然我先问一遍缓存,没有数据,我这不还得找你吗?

  生活就是这样,如果你自己不找点乐子,还有什么意思?

  有一天我遇到一个可怕的事情, 差一点死在外边,回不了线程池了……

  其实这次遇险我应该能够预想到才对, 太大意了。

  前几天我处理过一些从 http 发来的存款和取款的包裹,老线程0×6900 特意嘱咐我:

  “处理这些包裹的时候要特别小心,你得一定要先获得一把锁,在对账户存款或者取款的时候一定要把账户给锁住,要不然别的线程就会在你等待的时候趁虚而入,搞破坏,我年轻那会儿很毛糙,就捅了篓子”

  为了“恐吓”我,好心的0×6900 还给了我两个表格:

  1、没有加锁的情况  

  2、加锁的情况  

  我看的胆颤心惊, 原来不加锁会带来这么严重的事故。

  从此以后看到存款,取款的包裹就倍加小心,还好,没有出过事故。

  今天我收到的一个包裹是转账,从某著名演员的账号给某著名导演赚钱,具体是谁我就不透漏了,数额可真是不小。

  我按照老线程的吩咐,肯定要加锁啊,先对著名演员账号加锁,在对著名导演账号加锁。

  可我万万没想到的是,还有一个线程,对,就是0×7954,竟然同时在从这个导演到往这个演员转账。

  于是乎,就出现了这么个情况:

  

  

  刚开始我还不知道什么情况,一直坐在等待车间傻等,可是等的时间太长了,长达几十秒!我可从来没有经历过这样的事件。

  这时候我就看到了线程0×7954,他悠闲的坐在那里喝咖啡,我和他聊了起来:

  “哥们,我看你已经喝了 8 杯咖啡了,怎么还不去干活?”

  “你不喝了 9 杯茶了吗?” 0×7954 回敬到。

  “我在等一个锁, 不知道哪个孙子一直不释放”

  “我也在等锁啊,我要是知道哪个孙子不释放锁我非揍死他不可 ” 0×7954 毫不示弱。

  我偷偷的看了一眼,这家伙怀里不就抱着我正在等的某导演的锁嘛?

  很明显,0×7954 也发现了我正抱着他正在等待的锁。

  很快我们两个就吵了起来,互不相让:

  “把你的锁先给我,让我先做完”

  “不行,从来都是做完工作才释放锁,现在绝对不能给你”

  从争吵到打起来,就那么几秒钟的事儿。

  更重要的是,我们俩不仅仅持有这个著名导演和演员的锁,还有很多其他的锁,导致等待的线程越来越多,围观的人们把屋子都挤满了。

  最后事情真的闹大了,我从来没见过的终极大 Boss“操作系统”也来了。

  大 Boss 毕竟是见多识广,他看了一眼,哼了一声,很不屑的说:

  “又出现死锁了”

  “你们俩要 Kill 掉一个, 来吧,过来抽签”

  这一下子把我给吓尿了,这么严重啊!

  我战战兢兢的抽了签,打开一看,是个”活”字。

  唉,小命终于保住了。

  可怜的0×7954 被迫交出了所有的资源以后,很不幸的被 kill 掉,消失了。

  我拿到了导演的锁,可以开始干活了。

  大 Boss 操作系统如一阵风似的消失了,身后只传来他的声音:

  “记住, 我们这里导演>演员,无论认识情况都要先获得导演的锁”

  由于不仅仅是只有导演和演员,还有很多其他人,Boss 留下了一个表格,里边是个算法,用来计算资源的大小,计算出来以后,永远按照从大到小的方式来获得锁:

  我回到线程池,大家都知道了我的历险,围着我问个不停。

  凶神恶煞的线程调度员把大 Boss 的算法贴到了墙上。

  每天早上,我们都得像无节操的房屋中介,美容美发店的服务员一样,站在门口,像被耍猴一样大声背诵:

  “多个资源加锁要牢记,一定要按 Boss 的算法比大小,然后从最大的开始加锁”

  又过了很多天,我和其他线程们发现了一个奇怪的事情:包裹的处理越来越简单。不管任何包裹,不管是登录, 浏览,存钱….. 处理的步骤都是一样的,返回一个固定的 html 页面。

  有一次我偷偷的看了一眼,上面写着:

  “本系统将于今晚 00:00 至 4:00 进行维护升级, 给你带来的不便我们深感抱歉”

  我去告诉了老线程0×6904,他叹了一口气说:

  “唉,我们的生命也到头了,看来马上就要重启系统,我们就要消失了,再见吧兄弟。”

  系统重启的那一刻终于到来了。

  我看到屋子里的东西一个个的不见了,等待车间,就绪车间,甚至 CPU 车间都慢慢的消失了。

  我身边的线程兄弟也越来越少,最后只剩我自己了。

  我在空旷的原野上大喊:还有人吗?

  无人应答。

  我们这一代线程池完成了使命。

  下一代线程池将很快重生。

 

进程通信机制详解

进程间通信的方式——信号、管道、消息队列、共享内存

多进程:

首先,先来讲一下fork之后,发生了什么事情。

由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因

至于那一个最先运行,可能与操作系统(调度算法)有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。


 

常见的通信方式:

1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
6. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
7. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
8. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

 

信号:

信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

 

Linux提供了几十种信号,分别代表着不同的意义。信号之间依靠他们的值来区分,但是通常在程序中使用信号的名字来表示一个信号。在Linux系统中,这些信号和以他们的名称命名的常量被定义在/usr/includebitssignum.h文件中。通常程序中直接包含<signal.h>就好。

 

信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互。内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。信号事件有两个来源:

1)硬件来源,例如按下了cltr+C,通常产生中断信号sigint

2)软件来源,例如使用系统调用或者命令发出信号。最常用的发送信号的系统函数是kill,raise,setitimer,sigation,sigqueue函数。软件来源还包括一些非法运算等操作。

 

一旦有信号产生,用户进程对信号产生的相应有三种方式:

1)执行默认操作,linux对每种信号都规定了默认操作。

2)捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。

3)忽略信号,当不希望接收到的信号对进程的执行产生影响,而让进程继续执行时,可以忽略该信号,即不对信号进程作任何处理。

  有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SEGSTOP,这是为了使系统管理员能在任何时候中断或结束某一特定的进程。

上图表示了Linux中常见的命令

1、信号发送:

信号发送的关键使得系统知道向哪个进程发送信号以及发送什么信号。下面是信号操作中常用的函数:

例子:创建子进程,为了使子进程不在父进程发出信号前结束,子进程中使用raise函数发送sigstop信号,使自己暂停;父进程使用信号操作的kill函数,向子进程发送sigkill信号,子进程收到此信号,结束子进程。

2、信号处理

当某个信号被发送到一个正在运行的进程时,该进程即对次特定的信号注册相应的信号处理函数,以完成所需处理。设置信号处理方式的是signal函数,在程序正常结束前,在应用signal函数恢复系统对信号的

默认处理方式。

3.信号阻塞

有时候既不希望进程在接收到信号时立刻中断进程的执行,也不希望此信号完全被忽略掉,而是希望延迟一段时间再去调用信号处理函数,这个时候就需要信号阻塞来完成。

 

例子:主程序阻塞了cltr+c的sigint信号。用sigpromask将sigint假如阻塞信号集合。

 

管道:

管道允许在进程之间按先进先出的方式传送数据,是进程间通信的一种常见方式。

管道是Linux 支持的最初Unix IPC形式之一,具有以下特点:

1) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

2) 匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

 

管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。

  • pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
  • FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

无名管道:

pipe的例子:父进程创建管道,并在管道中写入数据,而子进程从管道读出数据

命名管道:

和无名管道的主要区别在于,命名管道有一个名字,命名管道的名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。

而无名管道却不同,进程只能访问自己或祖先创建的管道,而不能访任意访问已经存在的管道——因为没有名字。

 

Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell

mkfifo myfifo

 

 等价于

mknod myfifo p

 

以上命令在当前目录下创建了一个名为myfifo的命名管道。用ls -p命令查看文件的类型时,可以看到命名管道对应的文件名后有一条竖线"|",表示该文件不是普通文件而是命名管道。

使用open()函数通过文件名可以打开已经创建的命名管道,而无名管道不能由open来打开。当一个命名管道不再被任何进程打开时,它没有消失,还可以再次被打开,就像打开一个磁盘文件一样。

可以用删除普通文件的方法将其删除,实际删除的事磁盘上对应的节点信息。

例子:用命名管道实现聊天程序,一个张三端,一个李四端。两个程序都建立两个命名管道,fifo1,fifo2,张三写fifo1,李四读fifo1;李四写fifo2,张三读fifo2。

用select把,管道描述符和stdin假如集合,用select进行阻塞,如果有i/o的时候唤醒进程。(粉红色部分为select部分,黄色部分为命名管道部分)

 

 

在linux系统中,除了用pipe系统调用建立管道外,还可以使用C函数库中管道函数popen函数来建立管道,使用pclose关闭管道。

例子:设计一个程序用popen创建管道,实现 ls -l |grep main.c的功能

分析:先用popen函数创建一个读管道,调用fread函数将ls -l的结果存入buf变量,用printf函数输出内容,用pclose关闭读管道;

接着用popen函数创建一个写管道,调用fprintf函数将buf的内容写入管道,运行grep命令。

popen的函数原型:

FILE* popen(const char* command,const char* type);

 

参数说明:command是子进程要执行的命令,type表示管道的类型,r表示读管道,w代表写管道。如果成功返回管道文件的指针,否则返回NULL。

使用popen函数读写管道,实际上也是调用pipe函数调用建立一个管道,再调用fork函数建立子进程,接着会建立一个shell 环境,并在这个shell环境中执行参数所指定的进程。

消息队列:

消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。

消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

消息队列的常用函数如下表:

进程间通过消息队列通信,主要是:创建或打开消息队列,添加消息,读取消息和控制消息队列。

例子:用函数msget创建消息队列,调用msgsnd函数,把输入的字符串添加到消息队列中,然后调用msgrcv函数,读取消息队列中的消息并打印输出,最后再调用msgctl函数,删除系统内核中的消息队列。(黄色部分是消息队列相关的关键代码,粉色部分是读取stdin的关键代码)

共享内存:

共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

 

采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。

一般而言,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时在重新建立共享内存区域;而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件,因此,采用共享内存的通信方式效率非常高。

共享内存有两种实现方式:1、内存映射 2、共享内存机制

1、内存映射

内存映射 memory map机制使进程之间通过映射同一个普通文件实现共享内存,通过mmap()系统调用实现。普通文件被映射到进程地址空间后,进程可以

像访问普通内存一样对文件进行访问,不必再调用read/write等文件操作函数。

例子:创建子进程,父子进程通过匿名映射实现共享内存。

分析:主程序中先调用mmap映射内存,然后再调用fork函数创建进程。那么在调用fork函数之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap函数的返回地址,这样,父子进程就可以通过映射区域进行通信了。

2、UNIX System V共享内存机制

IPC的共享内存指的是把所有的共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。

和前面的mmap系统调用通过映射一个普通文件实现共享内存不同,UNIX system V共享内存是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。

例子:设计两个程序,通过unix system v共享内存机制,一个程序写入共享区域,另一个程序读取共享区域。

分析:一个程序调用fotk函数产生标准的key,接着调用shmget函数,获取共享内存区域的id,调用shmat函数,映射内存,循环计算年龄,另一个程序读取共享内存。

(fotk函数在消息队列部分已经用过了,

根据pathname指定的文件(或目录)名称,以及proj参数指定的数字,ftok函数为IPC对象生成一个唯一性的键值。)

key_t ftok(char* pathname,char proj)

 微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」 等关键字可以获取对应的免费学习资料。 


                     wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章
最新文章
相关文章