如无说明,假定本文所有文件名为main。
这一节的核心是一道经典的面试题。先放简单变体:
#include<unistd.h>
#include<stdio.h>
int main(){
for(int i=0;i<2;++i){
printf("-\n");
fork();
}
return 0;
}
执行命令和结果:
> ./main
-
-
> -
读者应该已经注意到,本来应该属于用户输入的提示文本,被夹在程序输出中间了。之后我们会更明确地看到一点,通常而言,shell在它启动的进程结束后,会立即输出提示文本并接收用户输入,但其他由该进程启动的进程仍在运行。
这里首先介绍一个可以创建进程的函数,方便简单理解之后的内容。
fork进程
(笔者实在是不知道这东西怎么翻译。。。)
基本使用
/*
@brief 用于创建一个几乎一样的进程(指令段和多数数据)。
@return 创建者得到新建进程的进程pid,被创建者得到0。
返回值通常写的是pid_t,一般就是int。
*/
pid_t fork(void)
从本文最开始的代码中可以看出它的用法。
从这里能看出为什么它总共输出了三个进程。
我们用两个函数来验证一下。
获取进程pid
基本用法
/*
@brief 用于获取进程pid
@return 当前进程的pid
*/
pid_t getpid(void)
获取父进程pid
基本用法
/*
@brief 获取父进程的pid
@return 父进程的pid
*/
pid_t getppid(void)
进程信息样例代码
#include<unistd.h>
#include<stdio.h>
int main(){
for(int i=0;i<2;++i){
printf("pid:%d,ppid:%d\n",getpid(),getppid());
fork();
}
return 0;
}
执行指令和结果:
> ./main
pid:64686,ppid:57387
pid:64686,ppid:57387
> pid:64687,ppid:1325
这里也能看出,fork并不是直接创建进程的。至少很多情况下是这样。
#include<unistd.h>
#include<stdio.h>
int main(){
printf("pid:%d,fork():%d\n",getpid(),fork());
return 0;
}
执行指令和结果:
> ./main
pid:64772,fork():64773
> pid:64773,fork():0
这里我们看到,想要直接获取fork所得进程的pid,应直接使用fork的返回值。
一些有趣的东西
接下来,我们看看开篇提到的面试题吧:
#include<unistd.h>
#include<stdio.h>
int main(){
for(int i=0;i<2;++i){
printf("-");
fork();
}
return 0;
}
会输出几个-字符?
相比本节开始的简化版,只减少了用于换行的\n转义符。那么应该还是三个。
真的吗?
> ./main
--> ------
实测一下,发现是八个。我们换个更能说明问题的写法。
#include<cstdio>
#include<pthread.h>
#include<unistd.h>
int main(){
for(int i=0;i<2;i++){
printf("(%d)",getpid());
fork();
}
return 0;
}
执行指令和结果:
> ./main
(64899)(64899)> (64899)(64899)(64899)(64900)(64899)(64900)
看上去更诡异了,很多pid是一样的……
为什么呢?想一想,进程数据中还有哪些很关键的东西?显然,这里涉及的是输出缓冲区。没有\n而且缓冲区不满,一般是不会输出的。永远不要忘了它,否则指不定什么时候就被它坑了。
观察到总共四个进程,每个进程缓冲区内最后有两个字符,所以共计八个。同理,我们来看看pid输出情况。假定pid就是0开始计(实际情形中,除非你把系统内核改了,否则基本不涉及pid真的是0的情形)。
由于笔者不太会用mermaid,这里先把括号略去了……读者们明白就好。
这里我们也可以大致猜测,进程2实际上在进程1之前进行。这也告诉我们一点,不要试图依赖fork的执行顺序。此外,你的输出可能误导你对程序运行状况的判断,不要过度依赖输出。一定要用输出来调试的话,尽可能在每次输出后立即用\n强制输出和清空缓冲区内容。如果你是竞赛生,你的老师可能会持有相反的观点,但这实际上不是本质问题。本质问题是,不要混淆竞赛风格和工程风格,二者的代码写出来天差地别。
此外补充一点,fork后调用exec系列函数可能代价比较大,建议这时使用vfork。但如果fork后不进行进程体替换则不能使用vfork。如果fork后仍要执行exec最多就是性能问题,但vfork后不执行exec系列函数则可能引发致命错误。