百科问答小站 logo
百科问答小站 font logo



为什么Linux下要把创建进程分为fork()和exec()(一系列函数)两个函数来处理? 第1页

  

user avatar   bei-ji-85 网友的相关建议: 
      

一堆人在那个说什么Linux设计哲学,最小、完整之类的,我只想说两个字:扯淡

有一个回答是对的 @王杰聪

Linux里的很多东西是学UNIX的,UNIX里,fork和exec这两个API刚设计出来的时候,连现代操作系统里的进程、线程概念都没有,什么最小且完整,连进程都没有,谈什么最小且完整

fork、exec在UNIX里的最初的目的是:shell要执行别的东西,干完活再返回给shell,但当年(应该是1960~1970年代)是没有进程的概念的,exec就是把老的shell给干掉,然后去干活,干完活再返回回来。注意,当年是没有进程的概念的,exec就是直接把shell从内存里拿掉。但这样做有一些坏处,就是每次要重新加载shell,于是fork就出现了,让新任务执行(复制一份),shell不动(具体是交换到磁盘上还是怎么操作不太了解),新任务干完活,shell继续跑。因为当年没有多任务的概念,fork相当于提供了一个虚假的多任务环境

我觉得真没必要鼓吹fork有多好,fork诞生的环境,是没有多任务概念的情况下,解决多任务的需求的,所以它的功能看上去很奇怪,因为不是解决今天的多任务场景的。所谓的最小且完整,这都是后人硬加上去的解释,最初根本没这么多考虑,而且fork和exec诞生于不同的时代

如今硬件软件都已经发展的足够好了,用CreateProcess没什么不好,哪怕pthread也比fork要先进的多。也正如 @陈硕 说的:fork对多任务其实不友好。

自己写一个操作系统,手工实现一个fork,就知道fork有多坑了,fork对寄存器使用很敏感,任何一个非标准的ABI访问都可能导致fork崩溃,当然高级语言开发者不需要考虑这个问题,因为高级语言的ABI都是完全符合规范的。在Linux,fork并非一个真正的系统调用,我印象里它走的是clone或者vfork

写完才发现,这个是一个老问题。

参考资料:en.wikipedia.org/wiki/F


补充一些:

fork被设计出来以后,UNIX开发者发现这个东西很好用,所以就一直保留下来,一直到今天,被Linux延续下来,但不代表说fork/exec的机制有多先进,不然后人也没必要搞pthread这套库了。有人用,并且用的人还挺多,只是因为它太古老了,支持的操作系统多。作为对比,这个东西就像printf一样,古老,但不一定多好用,比它们强大的API多的是,只不过兼容性不好,行为不好控制而已。


user avatar   wang-jie-cong-59 网友的相关建议: 
      

其实在威斯康大学出的 Operating System: Three Easy Pieces 的第一章 第三节时候作者对于这个问题进行解释:


大概意思就是: 这种做法是当年为了实现shell 这种interactive commands而设计的

其目的就是能够轻松改变process 的环境变量(file descriptor)从而实现pipes | redirect > 等这种强大处理的功能

例如 ps aux > 1.txt

shell 运行 这个command 的时候,会先fork 出一个自身的process 但是并没有run 然后把 file descriptor 1 (screen output) 替换成 1.txt 然后再去call exec 去 exec ps 这个command, 这样ps 的输出结果就自动写入 1.txt


user avatar   s.invalid 网友的相关建议: 
      

接口设计的一个指导原则是“完整且最小”。


“完整”的意思是,对外提供的接口必须能够满足任何使用要求。

“最小”的意思是,接口功能无重复(无重叠),以能达到“正交化”水平为最佳。


“完整且最小”的接口未必好用。有时候,为了使用方便或者性能或者其它种种原因,接口可以出现冗余;但冗余必然带来维护/学习等方面的代价。



linux的fork/exec就是一组典型的“完整且最小”的接口。


“fork”用来产生一个新进程,这个进程默认会复制自身——于是类似apache这样用到“进程池”的场景得到支持。

“fork”的“复制自身”操作又是“悬挂”的,如果紧接着调用exec,复制就会取消——于是启动另外一个进程的场景得到支持。


“exec”则是“启动参数指定的程序,代替自身进程”。

如果不配合fork使用,它是“当前进程结束,执行指定进程”;配合fork使用,就成了“当前进程启动另一个进程”。


这样一来,各种使用场景就都得到了支持;再加上内部优化,写出“性能绝佳”的“多进程协作”程序就成了可能——于是linux甚至有相当一段时间都不支持线程,因为“fork的效率实在太好了,没必要支持线程”(另一个后遗症是,虽然现在linux内核有了线程支持,但线程和fork之间的关系极为复杂,以至于几乎只能在多进程/多线程两个方案中间选择其一)。


当然,为了便于使用,linux也提供了一个用起来简单一些的system调用。它和createProcess有点像,但内部仍然由fork+exec实现;此外,它执行时是阻塞的,同时还可能有很多信号之类的技术问题需要处理。



现在,我们拿fork+exec和windows对比一下。

如果我们需要做一个“多进程协作”的网络服务框架,要求每个工作进程在处理了若干次服务请求后退出(这是个很讨巧的、保证系统7X24稳定性的经典设计);但相应的,这就对进程创建效率提出了极高要求(哪怕按处理50个请求退出、且请求频率是相当初级的5000次/秒,也需要每秒创建100个进程;更不要说后来更为丧心病狂的10K、100K问题了)——这种设计在windows上也能保证性能吗?为什么?


当然不行。因为windows相关API被封装的太“重”了。它用起来的确方便;但方便是有代价的。起码它不能“完整”的支持多进程协作的高性能服务框架……


linux的fork/exec方案也有缺陷。它的机制比较复杂,使得初学者难以理解;另外就是,当线程出现后,这个方案和多线程八字不合,混用的话有许多许多的坑等着你。

反倒是windows,一旦有了线程支持,多进程互相监视保证稳定性、多线程同时执行提高效率,各种使用场景就全都被覆盖了——换句话说,现在它“完整”了,你改下方案就行。


所以你看,过去毋庸置疑是linux的接口更灵活更强大(当然了,不是方便初学者的那种强大);可一旦多了线程支持,windows方案反倒显得更“正交”了┑( ̄Д  ̄)┍


但即便如此,Unix系仍然可以选择进程池、仍然可以做到“一个进程提供N次服务后就杀掉”;而Windows呢,要杀一次杀,杀完花五分钟重新初始化……

相对于Windows的“完全做不了”,Unix系付出的代价仅仅是——用户你们都自觉点!用了线程就别再fork了!就好像你们用线程也要自觉的不乱摸公共变量、摸之前自觉加锁一样。

线程这个模型把进程内部数据的状态搅的一团乱,你还能怎么地?这是线程这个模型的缺陷——毕竟用了线程你就得自觉维护状态,不能推到fork身上,对吧。


好吧,看来不得不加点补充说明,也反驳一些奇谈怪论。


简单说,如果你没有在10年或者更早前做过桌面/服务器软件开发,那么在你的脑子里,很可能就会觉得“createProcess和thread就是亘古长存的、就是傻子都看得出的、从一开始就伟大光荣正确到结束的完美方案;至于fork-exec,那是老糊涂们偶然犯下的、短暂的错误”。


但是“短暂的错误”持续了差不多40年,“长久的辉煌”最近十年才占了上风;甚至于,哪怕线程被炒的火热之时,Linus也坚持不在Linux内核中引入线程,而是“抱残守缺”于那个“持续了40年的、短暂的错误”方案,也就是fork+exec。


当然,最终,Linus败了,举了白旗;线程也终于进了Linux内核——以某种和fork风格格格不入的形式。

但是,哪怕只是看到我这寥寥几句,你都不太可能毫无障碍的接受“fork是个不假思索的错误”这一套一套了吧。


其他人的答案提到“exec是为了实现Linux shell的古怪行为”——嗯,这个说法似乎符合事实,但却是倒果为因。


比如,DOS也是这么干的。

最初大家都要挤占实模式下的640k运行内存,这块内存在DOS启动后会先被command.com占据,就是它提供了dos的shell;当用户敲了一个“外部命令”(也就是其它程序)时,command.com里面的一小段代码就会把这个“外部命令”对应的目标应用加载进来、覆盖掉自己(exe和com还各有不同执行方式),只保留加载器所在的那一丁点内存;等用户程序执行结束、控制器返回加载器代码,这段加载代码就把command.com重新加载回内存。

有时候用户程序可能特别大,640k都不够用(其实刨去其它零碎也就600k不到能用);那么用户还要自己搞个ovl文件,自己加载进来(并把自己之前占用的空间覆盖掉,所以叫“覆盖文件”;当然也会留下执行加载的那点代码不覆盖,不然就没得恢复了),然后跳转到ovl入口继续执行代码逻辑——有的程序可能需要载入N个不同的ovl才能完成自己的工作(有的大型软件一套几十张软盘,运行时需要依照提示在不同时刻插入不同的软盘)。

再后来内存/磁盘越来越大,计算机运行起来就不再需要这么捉襟见肘了。但由于这个历史,从那个时代走来的OS上的exec类系统调用往往都有一个“干掉发起调用的进程的副作用”。


没办法,当时内存太金贵了,像现在这样同时加载1024个进程到内存占着茅坑不拉屎是不可想象的。甚至同一个进程都还要用ovl文件分段加载执行呢,你敢想象一个进程启动了另一个进程、结果自己还赖着不走、不肯积极给人家腾地方?

所以exec的含义必然只能是“这活我得找人干,我自己暂时先退下了;等它干完再拉我起来”——单进程多进程你都得这么干。只要你得等下家出结果才能继续,你就得主动让贤。可不仅仅是shell。

也因为“主动让贤”策略深入人心,很多OS设计时就会默认“程序员自己会安排好一切、确保多个进程负责的逻辑相互错开,绝不会搞出‘两个进程同时执行’的幺蛾子”——最基础的“高内聚低耦合”原则而已,搞不定就别来捣乱。

如此一来,exec等于“起一个新进程的同时干掉旧进程”就顺理成章了。


哪怕到了后来的Windows3.1/Windows95/98/me时代,内存仍然是捉襟见肘的。除了音乐播放器和杀毒软件之类小打小闹的东西随便你玩;但稍微像点样子的任务,当你的程序需要多进程协作时,自己主动退出内存、腾地方给别人仍然是尽快完成任务的不二法门——256M内存或许1小时就搞定了,你占住128M内存不放或许三小时都搞不定。

甚至,哪怕现在台式机动辄插32G128G内存条的时代,全特效玩赛博朋克2077你敢双开三开吗?玩在线游戏时steam为什么会停止下载更新?

没有那么多资源挥霍,对吧。


当然,Windows系的exec倒是没有这个毛病。但你很难说这是“高瞻远瞩”——说成是为了Windows的一贯战略、为了用户易用性而牺牲性能还差不多。

当时互联网服务器Unix系是绝对主流,因为它有一个“独门绝技”就是fork。这是因为互联网服务从一开始就设计成了“无状态”的,应用只做内容分发(提供计算服务的小型机乃至中大型机是另一回事,当然它们也是unix系的优势领域);既然应用无状态,那么自然无需在其中保存什么数据,需要的时候拉起来即可。

于是就有了进程池这个概念,比如apache配置好了就可能同时起若干个apache进程,网络请求来了就分配给其中一个apache进程服务;但服务久了、apache跑的脚本内部可能就会出各种各样的问题,那么就应该杀掉这个apache进程然后重新拉起——所以如果你配过apache,就知道里面有个参数可以指定每个apache进程最多响应多少次请求,到了数量就会主动杀掉然后再拉起一个新的(默认值一般是50~200,当然也可能有其它值)。

可想而知,如果每次拉起apache都必须重新读取配置文件完成各种初始化那得多麻烦;有了fork,这一切就全都免了,创建进程就仅仅是一次比memcpy重不了多少的内存操作而已——这种超高性能甚至造就了fork bomb。

至于Windows……就它那可怜巴巴的进程创建速度,玩蛋去吧……


你看,这个时候,谁敢说Linux的fork-exec是个错误?明明Windows的createProcess才是个愚蠢、缓慢的错误有木有。家庭娱乐你找它;想干正事?你看着办。


总之,最初,fork的表现是如此的惊艳,以至于线程出现很久了,Linus都不愿提供支持。因为Linux的fork-exec套装表现的实在太好了,根本就不是Windows所能撼动的。


线程最初是为了解决图形界面刷新的问题而搞出来的。

过去的程序都是单进程的;那么刷新UI时势必无法计算;忙于计算时就无法刷新UI——你当然可以安排个定时器,时间到了就刷新UI,Windows开发者最初就是这么做的;但这样也有很多困难,第一是如果刷新太频繁了就很容易影响计算性能;第二是这样的程序很难写,尤其如果你在循环中不停检查是否需要刷新屏幕…那性能实在太美;第三是究竟刷新哪些部分呢?全屏刷新对当时CPU来说是个太大的负担;刷新部分?那怎么触发呢?丢消息循环?那什么时候做计算?


于是,我们就见到了许许多多一忙起来窗口一片惨白的程序,鼠标一拖满屏都是窗口拖影;没人知道这个应用是忙完了还能过来、还是就这么卡死了,一着急强行关机的比比皆是。

当然,也有一些表现的好一些——它们在忙碌于计算时,虽然窗口同样一片惨白,但还有个蓝蓝的进度条不时动一动……


那么,有个线程专门负责刷新UI的话,这一切是不是就不成问题了?


Linux:谢谢。但我们不需要GUI。我劝你多搞搞严肃的、提高服务性能的正事,少耍点花活。

你看,当时只有服务器上才有多CPU系统,而且是SMP(对称多处理器)架构;进程、轻量级进程能从中得到更多的好处;至于线程……同一个进程的两个线程,分别处于不同的处理器缓存甚至内存,却时时刻刻要共享同一个变量?你在瞎胡闹知道吗?太不专业了。


但是,硬件环境在悄悄起变化……

具体时间不太清楚了,我记得是奔三或者奔四时代,intel直接在CPU里提供了多线程支持,单核多线程CPU普及;再往后AMD干脆直接推双核CPU,我们现在使用的这种看似一颗、实际上可能是10核20线程的处理器这才登上历史舞台……

有了硬件支持,线程这才慢慢取得了性能等其它方面的好处。

然后,Linus不得不顺应潮流,Linux内核这才开始支持线程。


但起码直到08年,我在工作中使用Linux线程仍然遇到不少问题,仍然有很多Linux发行版无法支持内核线程。

哪怕能用内核线程了,线程也和fork先天不合。因为它们是匆匆捏到一起的,压根不能一起用。

没错,重复一遍:选择fork-exec方案不是匆匆忙忙缺乏考虑;选择在一个以fork-exec为基础的系统上支持内核线程,这才是匆匆忙忙缺乏考虑——或者说,就好像C++一样,此时的Linux是“多范式”的,你可以自由选择多进程还是多线程方案;但如果要在一个方案里糅合多进程和多线程……你在自讨苦吃。


总之,fork-exec绝对不是什么错误的选择更不是什么权宜之计。它是深思熟虑且经过实践检验的、效果绝佳的金点子;只是后来CPU设计思路变了,这个方案才变得不合时宜——不然你猜Linus怎么就那么蠢,线程摆在面前他都坚决不碰、绝不考虑fork之外的其它方案?

这个态度最起码可以证明:当时的Linus绝对不会认为fork是个缺乏考虑的、错误的设计;而且,他起码也说服了参与Linux kernel开发的绝大多数人,不然这个政策不会坚持这么久。


当然了,现在的fork和线程配合稀烂是事实。因为越是精巧的设计,在遇到预料之外的变化时就越是拙劣,远不如“没有丝毫内涵”的简单设计更能“不变应万变”——你看,fork-exec方案本来对标的是SMP;结果呢?突然大家都在一个CPU壳子里玩命的多塞起核心来,把研究了多年的、成熟的SMP扔到了一边。

就好像你做了十年做题家,却突然被大字不识几个的、“未曾被应试教育毒害的纯真眼神”扫进了垃圾堆一样。这你能找谁说理去。


但话又说回来了:当真的需要时,人家有作业控制有高性能的fork可以用;不需要时就fork+exec二连照样等于一个createProcess,没有付出任何代价——你那“未曾被应试教育毒害的纯真眼神”,又能比人家高贵到哪?就凭你不会解方程?


更可笑的是吹嘘什么“createProcess才真正深入思考了什么是进程”的……嘿嘿,你知道linux是什么抽象吗?

linux的抽象是:我们面对的任务是一组“作业”,也就是jobs;一个jobs由一组worker组合起来完成;我们可以启动一个“领班”的worker搭建舞台;舞台搭建结束了,worker携带着关于舞台的记忆开始分叉(fork);fork出的一大堆worker按照“领班”给自己的安排分头行动,比如内存数据处理这个worker就可能先把内存安排好、然后fork出一堆相同/不同的worker围绕着内存开始忙活;负责文件处理的worker把文件分成若干组,然后也fork出一堆worker各自领一组文件开始处理……万一哪个worker遇到什么奇葩情况误入岐途了(比如apache的worker被网上病态报文搞混乱了),没关系,杀掉它,再分裂一个补上就好。

注意这里的worker类似私有继承的类对象,继承过来就和原对象脱离了关系,从而避免互相干扰。因此才能做到“哪个出问题了就杀掉再拉起一个”——有fork,这就是个比memcpy重不了多少的任务而已。

等worker们任务都搞定了,纷纷返回,然后由父进程收集报告、汇总,最后你就可以通过jobs查询执行结果了。

你看,多完善的一套体系。


那你windows的抽象是什么?

进程就是进程。你启动了lol.exe这进程,你就可以玩英雄联盟。lol.exe启动另一个进程,谁知道它要干嘛。

你把这个叫“高级抽象”?


至于fork和thread直接的冲突……

说白了就这么回事:前面提到过,fork彼此默认private权限,因此子进程们可以随时杀随时重新拉起,不会一损俱损;而thread呢,它们默认相互public彼此的一切:所有thread共用同一组内存,谁都不是外人。因此一旦thread启动,程序状态就谁也说不清了,除非程序员自觉。

那么,很容易想到,一旦thread启动,fork时的状态,是不是也不再可能像过去那样清晰了?再调用fork,fork出来的东西会不会乱掉?比如持有的锁什么的,会不会……

答案是:当然会。搞不好就给你脏读脏写或者互相锁死……因此除非你通过特殊手法确保程序状态可控,否则一旦启动了thread,那就别再调用fork了,伺候不了你。

这个要求,和程序员要自觉不乱用thread一样,只能要求程序员自觉。


你看,多高级的抽象。高级到没法用——现在除了少数针对多线程优化的较好的程序,多数程序仍然只认单核性能。一个是没有合适的业务模型,难以用上多线程;另一个是缺乏有足够水平、可以hold住多线程模型的程序员,遇到能上多线程的场景也上不去。毕竟多进程都还大把人搞不定甚至听不懂呢。

为了确保低水平程序员也能正确使用线程,如今做的比较好的也就openmp之类;但与之同时nvidia搞了CUDA,可以利用显卡内的海量处理单元对规整数据爆出更强的性能,留给多线程优化的空间就更小了。

最终,反倒是可以规避数据冲突、且又足够易用的协程模型越来越耀眼。


总之,不要想当然。wiki关于fork的页面并没有一星半点“fork太古老因此即将废弃/应该废弃”的意思;恰恰相反,人家着重说明的是:这是一个1962年提出的优良设计,经过了近60年风风雨雨的考验却老而弥坚。

哪怕不以老资格压你;最最起码,当你自以为先进时,是否问过自己一句:我有什么独门绝技是别人不会的?我们能力,究竟谁是谁的超集?

掰着手指数数吧。性能优势安全优势都可以给你算上。总不能厚着脸皮说“我的优势就是比起你,我是这也不行那也不行”吧。


user avatar   xie-shi-bi-ya-11 网友的相关建议: 
      

先说结论:不是历史问题,fork()和exec()是非常精妙的设计。

南京大学蒋炎岩老师对这个问题分析的很透彻:

一个进程它不止包括自己寄存器,堆栈上的数据,还会涉及到操作系统的对象。而fork的调用使进程可以持有操作系统的对象!举一个简单的例子,我们用一个管道将一个程序的输出传输成另一个程序的输入。此时我去复制其中一个程序时,我们应该把程序中的管道的写口(或读口)也复制进去,这样才是完整的。

另一个例子:我们在做 I/O重定向操作时,fork会使子进程继承父进程的文件描述符的table,使得它也能打开相同的文件。

总结:fork-exec会继承原先程序持有的操作系统的对象。如果设计一个二者合并的系统调用,我们还得去考虑配置管道,文件等等的对象,这样太复杂了




  

相关话题

  为什么Linux和window系统镜像大小差距这么大? 
  如何从零开始写一个简单的操作系统? 
  为什么没有国产 Windows 的诞生? 
  Linux 内核中,多线程栈空间模型是怎样的? 
  在没有GUI的时代(只有一个文本界面),人们是怎么运行多个程序的? 
  UNIX/Linux最伟大的技术是什么? 
  如何不依赖任何外界现成软件和其他计算机设备运行没安装操作系统的电脑? 
  如何在 Surface 中安装 XP 系统? 
  操作系统是不是也是加载到内存中再执行的? 
  各个 Linux 发行版的风格和哲学分别是怎样的? 

前一个讨论
操作系统能不能继续分两部分:硬件相关和硬件无关?并且让驱动只依赖硬件相关部分而不依赖操作系统?
下一个讨论
汉字在计算机中的表示方式有哪些?





© 2024-11-08 - tinynew.org. All Rights Reserved.
© 2024-11-08 - tinynew.org. 保留所有权利