查看原文
其他

膨胀了,敢说 Linux fork 有 bug?

IT服务圈儿 2023-02-06

The following article is from 程序员石头 Author 石头哥

来源丨经授权转自 程序员石头(ID:tangleithu)

作者丨石头哥

背景

大家好,我是石头哥。

今天的文章估计你也会踩到坑里,信不信?请看完(文章较短,保持耐心看到最后)。

来看这段代码?

fork Demo

很简短的一段代码,对吧?

多年前就知道了, fork() 函数是用来创建子进程的,一次调用,两次返回。在子进程中返回的pid=0,父进程中返回的是子进程的实际的pid,子进程是从fork()之后执行的。

那么问题来了,Hello, 程序员石头 打印几次?

思考几秒钟~ 🤔

答案

既然,子进程是从fork 后开始执行的,自然Hello, 程序员石头 只会打印一次,对吧。

当然,既然在这样问题,答案可能就是“反常”的,如下图:

打印 2 次

居然打印了 2 次!是不是很神奇?

难道第 8 行的代码执行了两遍?不会是 fork 有 bug 吧?应该不可能,不过你有啥简便的方法能证伪么?

继续探究

我们再改一下代码的第 8 行,加个\n,再看看?

加换行符

结果只打印了一次了。

打印 1 次

当然,因为一个\n 的不同,导致结果的不同。你大概应该能猜到了,应该是缓冲区的问题。

fork() 函数创建子进程时,操作系统会在进程表中为该进程建立新的表项,子进程与父进程共享代码段,但数据空间是相互独立的。

fork 出来的子进程数据空间的内容是父进程的完整拷贝,执行上下文(包括程序计数器)也完全相同。因此,子进程会接着父进程执行的地方继续执行。

默认情况下 printf 其实是将内容输出到标准输出流,fork后,子进程其实也一样,但因为标准输出流(stdout)在指向终端(terminal)时是行缓存,即遇到换行符\n时会强制刷新缓存。

比如,我们再改改代码,去掉 \n,强制 flush 标准输出。

强制 fflush

也会得到一样的答案(只打印1次),演示过程如下图:

主动 flush,只打印一次

行缓存,全缓存

其实,在 标准C 中的缓存除了有行缓存外,还有全缓存,当然还有不缓存。

  • 行缓存:即每行(遇到换行符\n)刷缓存一下,刷缓存就是把缓存内容写出去。
  • 全缓存:当缓存满时才会刷缓存内容;

当然当程序结束时,exit 或从 main 函数 return,或者强制 fflush,都会刷缓存。

我们再回到行缓存,第 8 行还是用\n,即:

printf("%s, pid=%d\n", str, getpid());

我们再看看,假设用管道会怎样?输出到文件又会怎样?

试试管道或文件

神奇的地方来了吧, 用管道或输出到文件,即使加上 \n 也仍然会输出 2 遍。

因为用管道或者重定向到文件,其实都是属于上面的“全缓存”,即程序最终结束的时候,才刷缓存,因此子进程也有一份。完整的演示内容如下:

管道或文件-演示

好了,今天文章就到这里,很基础但却容易忽视,刻意花了不少时间做了演示动图,还请大家多多分享支持。

总结

其实,今天的内容在经典书籍 ——《UNIX 环境高级编程》中就有实例讲解了这个例子,如下图所示:

APUE 讲解

经典书籍还是应该多读读。确实,不管上层应用、各种技术框架千变万化,底层基础技术始终就是些内容啊。

后面的文字,其实是模板每次带上的,一方面为了引流,另外一个方面,确实如果能帮助到读者,就再好不过了。记住我说的,如果有心进大厂,搞文末资料,基本问题不会太大。

看到这里,真心希望你能帮忙点赞、分享支持一下,😝这将是我持续输出优质文章的最强动力!

1、初创数据库公司的疯狂行为:删掉花7个月开发的27万行C++代码,用Rust全部重写一遍

2、你深夜访问的那些敏感网站,都被浏览器悄悄记录了......

3、微软NB!Android 12.1来了

4、腾讯一面:内存满了,会发生什么?

5、继公众号后,微信另一王牌业务也将公开展示用户IP

点分享

点点赞

点在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存