我是靠谱客的博主 傻傻音响,最近开发中收集的这篇文章主要介绍Linux fork那些隐藏的开销,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

fork是一个拥有50年历史的陈年系统调用,它是一个传奇!时至今日,它依旧灿烂。

一个程序员可以永远不用read/write,也可以不懂mmap,但必须懂fork。这是一种格调!

fork没有参数,它是如此简单,是UNIX哲学的布道者或者说卫道者们的首选,它被写进了几乎每一本操作系统教科书里,成了 创建新进程的绝佳范式 ,fork站在原地,似乎在闭着眼睛蔑视 Windows的CreateProcess ,它的参数是如此之多,如此之复杂,在UNIX的世界,简单就是一切!

然而UNIX却不是整个世界!

似乎在对立的另一面,响荡着不同的声音,fork看起来是如此诡异,颠覆了初学者的认知,并且,fork开销巨大…

如果你知道fork开销巨大,那为何不用clone呢?? 诚然,clone并非标准,且参数多,复杂,麻烦,不美观,不雅致,看上去并不是很符合UNIX的价值观…

本文就站在上述这个对立面的立场,为fork再泼一盆冷水。

fork是诡异的
C语言教科书没法安安静静地讲fork,因为fork不符合C函数的调用规范。

C语言和操作系统原本就是两门正交的课程,你可以认为它们是无关的,C函数可以在没有操作系统的单片机上被调用,但是fork似乎不行。

若想理解fork的返回值,你就要先理解操作系统进程,换句话说,对fork的理解依赖操作系统,不然老师在C语言课程上讲fork时,一下子进掉进操作系统的窟窿里了,哦,或者说,C语言的老师估计也不懂操作系统原理。

不要觉得自己现在理解fork了就觉得它一开始就是这么简单,说到底还是被灌输的,回忆一下自己第一次接触fork时的场景,懵圈吗?是不是想了好久也没想明白为什么一个函数可以返回两次?按照对C函数的认知,创建进程的API明显应该是这样子的啊:

// 创建一个进程,成功返回0,否则返回-1,新进程从start开始运行
int create_process(void*(*start)(void *), void *arg, …);
然后,告诉学生,你可以在start里面调用exec加载新的程序映像。

和上述create_process比较,fork简直就是一个丑陋的幽灵,不知道如此诡异的东西怎么在50年间被吹捧成了简单的典范,若不是UNIX卫道士们的鼓吹和灌输,fork应该是反面教材才对!

或者至少,Linux不也还有clone调用么?

#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void arg, …
/
pid_t *ptid, void *newtls, pid_t *ctid */ );
我们看下clone的manual:

clone() creates a new process, in a manner similar to fork(2). … When the child process is created with clone(), it commences execution by calling the function pointed to by the argument fn. (This differs from fork(2), where execution continues in the child from the point of the fork(2) call.) The arg argument is passed as the argument of the function fn.

但是你看看它的参数,跟Windows API的风格有一拼,这不是UNIX的风格,尤其是其历史远不如fork久远,故没有fork受待见。

我的天,UNIX/Linux在瞬息让你拥抱变化的互联网时代,其文化竟然跟经典白酒葡萄酒一样,越陈年越香。

站在这帮UNIX卫道士们的立场上,你不懂UNIX进程创建原理那是你自己的问题,当你懂了,fork那就是简单的。

fork是懒惰导致trick
你看,fork没有一个参数,你没法在创建一个新进程之前去设置这个新进程的任何参数,比如优先级等,因为在fork调用前,什么都没有,连个新进程创建的计划都没有,然而一旦fork调用返回,就什么都有了,也就是说,新进程继承父进程的一切!

记住,是一切都继承父进程,连代码也是。所以说,你要想设置新进程的优先级,你就必须显式的在新进程里手工进行:

if (fork() == 0) {
//设置优先级
nice(-3);
} else {

}
你没法像下面这样:

// prio为新进程的nice增量。
int prio = -3;
ret = create_process(new_process, &argv[0], prio, …);
那么, 为什么一切都继承父进程的呢? 因为懒!这可是UNIX作者Dennis Ritchie自己说的。

Genie分时系统 被认为是首先实现fork的系统,而不是UNIX。Genie的fork远比UNIX的fork灵活的多,后来UNIX上位,就鸠占鹊巢了。

UNIX的fork调用其实是对Genie fork的拙劣模仿,也就是想照抄Genie分时系统的fork的样子,然而抄了一半觉得太麻烦了,干脆就全部把父进程复制一遍拉倒。

这是明显没有经过设计直接上线的典范,我们每个人在工作和生活中遇到事情几乎都会采用这种临时的投机取巧的方案来应对。

换句话说,没有参数的fork调用就是UNIX的一种临时取巧的方案,这种方案最终会留下很多坑,令人惊讶的是,这些填坑的方案竟然也成了经典!

这在互联网行业叫做 “快速迭代,小步快跑”。 我一向的观点就是 互联网行业无精品。 根本原因就是快速迭代小步快跑,然而,搞不好这是正确的呢?精品观念也许会成为历史呢…比如,优衣库秒杀巴黎高级成衣,扎啤秒杀法国高档红酒…

还真是,多少牛逼的专家都是解bug解出来的,他们是得多么感激当初写bug的人啊。

UNIX fork的取巧实现留下了坑,促使了后来的写时复制,即COW(copy on write)来填坑,却还是没有填平。

在UNIX刚刚出现的那几年,当时内存很小,一般的进程也都是很小的,所以fork中完全复制父进程没有问题,然而随着大进程的出现,内存开销开始越来越大,所以才采用了写时复制技术来缓解这种大的内存开销。

即便是内存页面写时复制了,但是地址空间的数据结构的复制操作仍然少不了,这一点我在后面会用demo来证实。

Linux内核是一个类UNIX系统内核,而且代码唾手可得,懂它的人也不在少数,现如今只要提到UNIX,Linux均可作为替代,也就是说,AIX,Solaris,HP-HX这种老牌经典UNIX太不容易得到了,而且也没有x86版本,所以一般都用Linux来替代。

我下面的demo也将全部基于Linux。

最后

以上就是傻傻音响为你收集整理的Linux fork那些隐藏的开销的全部内容,希望文章能够帮你解决Linux fork那些隐藏的开销所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(63)

评论列表共有 0 条评论

立即
投稿
返回
顶部