linux多线(进)程编程——(2)身外化身fork()
前言(前情回顾)
你捡到了一个秘籍上面写着《linux操作系统》正式摆脱了练气期,并开启了修炼,突破了第一个瓶颈,你的识海中出现了岔路,应该往哪边走呢?
听说在迷宫里摸着右边的墙壁是总能走出去的,我们也试一下吧。你将神识触摸了右侧的光球,一些信息进入到了你的脑海,最前面写着两个字:进程。
进程
什么是进程?
官方解释
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
我的解释
小明回家打开了英雄联盟开黑,小刚打开了steam下载小黄油。这两个软件是进程。爱编程的小黄回家用C语言写了一个hello world程序并点击运行,这也是一个进程。总结来说,进程就是一个应用程序。其余那些描述先不要深究以免走火入魔哦。
多进程
现在我们的电脑上运行这不止一个软件,至少我知道你一定打开了浏览器,因为你在看我的文章,此外你还挂着微信,这就是多进程。每个进程都有一个PID,即进程ID用来标识这个进程,类似于之前的文件描述符fd。此外每个进程有一个PCB,不是画电路板那个PCB,他的全程是进程控制块(Process Control Block)。
进程控制块在这里是超纲内容,如果各位0基础根基不稳的道友不想走火入魔,就先别去探究他,忍住你的好奇心,后面我们会介绍它的。
编程中使用多进程技术
我该如何运行两个线程呢?写两个程序,并分别同时运行?有点蠢,而且对于现在的我们,这种方法没有意义。因此我要给大家解释我们这门功法的第一个神通:fork();
神通1身外化身:pid_t fork(void);
这个函数没有参数,只有一个pid_t类型的返回值(其实就是int)。它的作用确实十分强大,他可以让我们的程序进行实现身外化身。如下图所示,父进程在执行到fork()函数调用的位置时,会克隆出一个新的进程。注意我说的词语:克隆,这代表子进程将完全复制父进程的所有变量,缓冲区,堆栈等熟悉。
多说无益,我们直接上代码,使用fork()克隆出子进程,并且输出hello world
#include <stdio.h>
#include <fcntl.h>
int main() {
printf("hello, world!\n");
pid_t id = fork();
printf("hello, world!\n");
return 0;
}
请问大家下面这个代码应该输出什么,我们实际验证一下。
使用gcc编译(前置知识,与主线无关),大家就当没看见这个经过即可,之后运行我们的可执行文件,可以看到输出了3个hello, world!。相信不少同学都在脑海中直接推断出了这个结论。
fork()的克隆
为了让大家深刻理解克隆这个词的意义,我为大家提供了下面的案例。
#include <stdio.h>
#include <fcntl.h>
int main() {
printf("hello, world! ");
pid_t id = fork();
printf("hello, world!\n");
return 0;
}
在这个代码中我将第一个hello world后面的换行符给去掉换成了空格,现在这个程序应该输出什么呢?
可以发现输出了4个hello world,原因大家应该知道了,就是因为没有换行符导致的,但是真正的底层原因是什么呢?
这个问题涉及到printf()函数的底层机制。printf函数管理一个数据缓冲区,要输出打印的数据不会直接显示到屏幕上,而是会存在缓冲区中,等待缓冲区满了一起输出。而当缓冲区没满如何让它提前输出呢,就是加入一个换行符,当printf检测到换行符,就会将缓冲区内所有的数据打印出来。因此当我将第一个hello world后面的\n去掉后,第一个hello world会在缓冲区中,并且缓冲区会由于fork()的调用被克隆。因此当我第二次输出hello world时,两个进程都识别到了换行符,并清空缓冲区,最终输出4个hello world。
父子进程的辨认
经过刚才的学习我们已经知道了fork()会实现进程克隆,那么我们该如何区分进程的身份呢?这个时候就要轮到fork()的返回值出场了。
在fork()函数的执行期间内,系统会克隆出一个新进程,因此到fork()执行完毕,会产生两个返回值。对于父进程中,我们会得到一个大于0的返回值,表示子进程的PID。而在子进程中,返回值为0。(返回值为-1表示进程创建失败了)。为了验证上述理论的正确性,我们编写如下代码,此外我们需要一个新函数getpid(),这个函数能获取当前进程的PID。
#include <stdio.h>
#include <fcntl.h>
int main() {
// 在父进程中获取父进程的PID
// pid_t getpid(void) 获得当前进程的PID
printf("father's pid is %d!\n", getpid());
pid_t id = fork();
if(id < 0 ) {
printf("process create fail\n");
return 0;
}
if(id == 0) {
printf("I'm son, my pid is %d\n", getpid()); // 使用函数直接获得子进程的PID
}
else {
printf("I'm father, my son's pid is %d\n", id); // 使用id表示子进程的PID
}
return 0;
}
可以看到父进程中fork()的返回值与子进程的PID保持一直,且子进程中fork()的返回值为0。
小节
恭喜道友功力又精进了,为了巩固修为,还要巩固一下下面三点:
1、fork()函数的作用,以及如何辨认父子进程
2、进程PID的获取方法
3、fork()对父进程的克隆作用
4、附加题:printf()函数的缓冲区机制
fork()函数在后续课程中会大量使用,用于测试代码功能,一定要掌握。
下一集:linux多线(进)程编程——(3)进程间的传音术(匿名管道)
结束语
最后,祝各位道友早日神功大成!
(点亮了新的区域,但但是不知道的东西好像越来越多了。。。)