百科问答小站 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如何赋予用户sudo权限? 
  还有哪些像 Unix,C/C++ 一样经久耐用的软件技术? 
  如何评价 Windows 11 窗口 N 代同堂以及一个系统三个终端? 
  学了三年的 Linux 了,我现在不能立马写出一个简单的 shell 脚本,怎么回事啊? 
  Windows 10 有哪些忍不了的设计? 
  Linus 真心诚意地夸过哪些人或技术? 
  如何看待余承东回应今年先不发布手机鸿蒙OS,明年有可能? 
  微软宣布 Windows 11 全部免费升级,还有哪些值得关注的信息? 
  KDE如何配置得漂亮大气? 
  为什么Interrupt需要存储PSW(程序状态字)但子程序调用不需要? 

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





© 2024-05-17 - tinynew.org. All Rights Reserved.
© 2024-05-17 - tinynew.org. 保留所有权利