首页 > 系统相关 >Fork - 进程管理中一个重要的函数

Fork - 进程管理中一个重要的函数

时间:2024-02-06 09:12:14浏览次数:29  
标签:Fork fork 调用 函数 pid 进程 vfork

思考问题:

  1. 当首次调用新创建进程时,其入口在哪里?
  2. 系统调用 fork 函数是如何创建进程的?
  3. 系统调用 exec 系列函数是如何更换进程的可执行代码的?

1.1 进程是如何创建的?

在 Linux 系统启动时,最早通过 init_task 产生的进程是 idle 进程,也称为 swapper 进程,其 pid 为 0 。该进程会调用 kernel_thread 创建一个内核线程,该线程进行一系列初始化动作后最终会执行 sbin 目录下的 init 文件,使得其运行模式从核心态切换到用户态,该线程演就变成了用户进程 init ,其 pid 为 1。
init 进程是 Linux 内核启动的第一个用户级进程,一切用户态进程都是它的后代进程。它可以启动其他子进程或者启动 shell ,用户每次在终端下面输入一行命令, shell 进程会接收这个命令并创建一个新的进程,这个新的进程还可以继续创建自己的子进程。在 Linux 中,这些进程的创建都是通过 fork 系统调用来实现的。

2.1 什么是 fork 函数?

又称为“复刻、派生、分支函数”,是 UNIX 或类 UNIX 中的分叉函数。 fork 函数将运行着的程序分成几乎相同的两个进程,两个进程具有相同的数据、连接关系和在程序同一处执行的连续性,每个进程都启动一个从代码的同一位置开始执行的线程。新生成的进程称为子进程,而原来的系统调用 fork 的进程称为父进程
这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

所以,fork 函数的作用,就是在 Linux 中创建一个新的进程
而在 Windows 中创建进程用到的是 createProcess 函数,勿混淆两者。

当进程调用 fork 函数,且控制转移到 Linux 内核中的 fork 代码后,内核会依次做四件事情:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝到子进程中
  3. 将子进程添加到系统进程列表中
  4. fork 返回,开始调度器调度

子进程从父进程处拷贝继承了整个进程的地址空间:包括进程上下文(进程执行活动全过程的静态描述)、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。子进程所独有的只是自己的进程号、计时器。
注意,子进程持有的是上述存储空间的“副本”,父子进程间并不共享这些存储空间。下面会举例说明。

子进程获得和父进程相同的数据,但是和父进程使用不同的数据段和堆栈段。子进程被创建以后,处于可运行状态,与父进程以及系统中的其他进程平等地参与系统调度。

创建新的子进程后,两个进程将执行 fork 系统调用之后的下一条指令。子进程使用和父进程相同的 PC(程序计数器),相同的 CPU 寄存器。

2.2 fork 的函数原型

#include <sys/types.h>
#include <unistd.h>
pid_t fork*(void);
  • pid_t 是一个宏定义,是 typedef 定义的类型,表示pid ,其实质是 int 被定义在 #include<sys/types.h> 中。
  • 使用 pid_t 代替 int 的原因:提高程序的可移植性。

fork函数的返回值可以分为三类:

  • 负值:创建子进程失败
  • 0 :返回到新创建的子进程
  • 正值:返回父进程或调用者。该值包含新创建的子进程的进程 ID

若成功调用一次会返回两个值,子进程返回 0 ,父进程返回子进程 ID ;出错则返回 -1 。

为什么会返回两个值?
因为子进程在复制阶段复制了父进程的堆栈段,所以两个进程都停留了在 fork 函数中,等待返回。

总而言之, fork 函数的特点概括起来就是“调用一次,返回两次”:在父进程中调用一次,在父进程和子进程中各返回一次。

2.3 在程序中调用 fork

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
    pid_t fpid;
    int count = 0;
    printf("i am the process, my process id is %d\\n", getpid());
    fpid = fork();
    if (fpid < 0)
        printf("error in fork!");
    else if (fpid == 0) {
        printf("i am the child process, my process id is %d\\n", getpid());
        count++;
    } else {
        printf("i am the parent process, my process id is %d\\n", getpid());
        count++;
    }
    printf("统计结果是: %d\\n", count);
    return 0;
}

程序分析:首先创建一个整型变量 fpid 进程,打印当前的进程,然后使该进程调用 fork 函数,用 fpid 接收调用之后的返回值。接着用三个 if 条件语句对应 fork 三种类型的返回值,判断调用失败 or 返回子进程 or 返回父进程。我们还定义了一个 count 变量来统计每种类型的值的返回次数。接下来我们来编译运行一下这个程序。

// 输出结果如下:
i am the process, my process id is 2573
i am the parent process, my process id is 2573
统计结果是: 1
i am the child process, my process id is 2574
统计结果是: 1

我们来分析输出结果:

  • 首先,第一个 printf 语句执行了一次,而 if-else 语句执行了两次,说明在调用了 fork 函数之后返回两个值,又或者说有两个进程执行相同的代码段。再看其中一个进程的 id 号和原进程的 id 号相同,则此进程为父进程,另一个进程为 fork 创建的子进程。
  • 其次,两个进程使 count++ 执行了两次,所以应该一个统计结果是 1 ,一个是 2 ,但是两次输出 count 的值都为 1 。这就强调了fork函数的一个特性:子进程虽然复制了父进程的数据段,但是和父进程不共享数据段。

可能你会以为父进程和子进程是同时运行的,又或者看到这个输出结果的顺序,认为是父进程先于子进程运行的。其实不一定!在不同的 UNIX 系统下,我们无法确定 fork 之后是子进程先运行还是父进程先运行,这依赖于系统的实现,取决于内核所使用的调度算法。

3.1 什么是 vfork 函数?

vfork 的作用与 fork 基本相同,也是通过拷贝父进程的数据结构来创建子进程,但是 vfork 创建的子进程并不完全拷贝父进程的数据段,而是和父进程共享数据段。

调用 vfork 函数返回之前,父进程被阻塞,子进程先运行。直到从 vfork 调用返回后,子进程继续执行,其可以调用 exec 执行新的进程,或调用 exit 结束其运行。此后,父进程才被唤醒,与子进程平等地被系统调度。因此,如果子进程在调用 exec 之前等待父进程,由于父进程执行 vfork 被阻塞,会造成死锁。

vfork 函数的主要用途:创建子进程以后,由子进程调用 exec 函数启动其他进程,使新启动的其他进程以该子进程的进程标识号身份执行,并且拥有自己的程序段和数据区

3.2 vfork 的函数原型

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

其返回值与 fork 的返回值相同。

3.3 在程序中调用 vfork

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int a = 10;
int main(){
	pid_t pid;
	int b = 20;
	pid = vfork();	// 创建进程
	if(pid < 0){
		perror("vfork");
	}
	if(0 == pid){ // 子进程
		a = 100, b = 200;
		printf("son: a = %d, b = %d\\n", a, b);
		sleep(2);
		_exit(0); // 退出子进程,必须
	}else if(pid > 0){ // 父进程
		printf("father: a = %d, b = %d\\n", a, b);
	}
	return 0;
}

以上程序验证两点:

  1. 通过 vfork() 创建的子进程会执行完后,才到父进程执行;
  2. 子进程共享父进程的地址空间。
// 输出结果:
son: a = 100, b = 200
// 2s 后
father: a = 100, b = 200

子进程在打印 2 秒之后,父进程才接着打印出来,说明通过 vfork() 创建的子进程执行完后,才到父进程执行。
若我们在子进程中修改了 a 和 b 的值,而父进程中不作修改,如果是 fork 函数的话,那么父进程应该打印的是 a=10,b=20 ,但是父进程打印的 a 和 b 的值和子进程一样的,说明 vfork 创建的子进程与父进程的数据段是共享的。

3.4 vfork 与 fork 的区别

因此,我们可以总结几点 fork() 和 vfork() 的区别:

  1. fork 函数的父子进程执行顺序不确定,而 vfork 函数保证子进程先执行,在它调用exit 函数退出进程或者调用 exec 函数族替换进程之后父进程才可能被调度运行。
  2. 执行 fork 时子进程拷贝父进程的数据段、代码段,而 vfork 中子进程与父进程共享数据段(在子进程调用exec或者exit之前)。

3.5 案例 - 调用死锁

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	pid_t pid;

	pid = vfork();	// 创建进程
	if(pid < 0){ // 出错
		perror("vfork");
	}

	if(0 == pid){ // 子进程

		printf("i am son\\n");
		sleep(1);

		// 子进程没有调用 exec 或 exit
	}else if(pid > 0){ // 父进程

		printf("i am father\\n");
		sleep(1);
	}

	return 0;
}

// 输出结果:
i am son
i am father
i am son
Segmentation fault	//死锁

4.1 什么是 exec 函数族?

前面我们学习 fork 函数可以通过系统调用创建一个与原来进程几乎完全相同的进程,这两个进程做着完全相同的事,但是这并不是使用 fork 函数的主要原因。

我们为什么要创建进程?是为了让一个程序同时走不同的分支,以实现不同的功能。

对于 exec 函数族来说,它的作用通俗来说就是使另一个可执行程序替换当前的进程,当我们在执行一个进程的过程中,通过 exec 函数使得另一个可执行程序 A 的数据段、代码段和堆栈段取代当前进程 B 的数据段、代码段和堆栈段,那么当前的进程就开始执行 A 中的内容,这一过程中不会创建新的进程,而且 PID 也没有改变。

所以,我们可以在 fork 后的子进程中调用 exec 函数族,从而使得“子进程替换原有的进程,和父进程做不同的事情”。

4.2 exec 的函数原型

#include<unistd.h>
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ...
											/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[],char *const envp[]);

这六个函数前面都是exec,只有后面的四个不同的字符决定了他们不同的功能。

字符 功能
l (list) 使用命令行参数列表
v (vector) 使用参数数组。应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
p (path) 使用文件名,系统会自动从PATH环境进行寻找可执行文件。
e (environment) 不继承任何Shell进程的环境变量,使用环境变量数组设置新加载程序运行的环境变量。
  • 带l的exec函数:(execl,execlp,execle):
    表示后边的参数以可变参数的形式给出且都以一个空指针结束。这里特别要说明的是,程序名也是参数,所以第一个参数就是程序名。
  • 带p的exec函数(execlp,execvp):
    表示第一个参数无需给出具体的路径,只需给出函数名即可,系统会在PATH环境变量中寻找所对应的程序,如果没找到的话返回-1。
  • 带v的exec函数(execv,execvp):
    表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL。
  • 带e的exec函数(execle):
    将环境变量传递给需要替换的进程,原来的环境变量不再起作用。

若调用成功,函数不会返回;若调用失败,则函数返回-1。

4.3 在程序中调用 exec

#include <unistd.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t fpid;
    int count = 0;
    fpid = fork();

    if (fpid < 0)
        printf("error in fork!");
    else if (fpid == 0) {
        printf("i am the children process, my process id is %d\\n", getpid());
        // 执行/bin目录下的ls, 第一参数为程序名ls, 第二个参数为"-l".
        if(execl("/bin/ls","ls","-l",NULL) == -1){
            perror("execl error");
        };
        count++;
    } else {
        printf("i am the parent process, my process id is %d\\n", getpid());
        count++;
    }

    printf("统计结果是: %d\\n", count);

    return 0;
}

// 输出结果:
i am the parent process, my process id is 3677
统计结果是: 1
i am the children process, my process id is 3678
total 48
-rwxr-xr-x. 1 root root 8600 May 14 17:39 execl
-rw-r--r--. 1 root root  608 May  9 17:44 execl.c
-rwxr-xr-x. 1 root root 8544 May 14 17:38 fork
-rw-r--r--. 1 root root  519 May 14 14:19 fork.c
-rwxr-xr-x. 1 root root 8672 May 14 17:38 vfork
-rw-r--r--. 1 root root  540 May 14 16:25 vfork.c

从打印结果可以看到,子进程在调用 execl() 之前仍在执行原来的代码段,调用 execl() 后, ls -l 命令代替了原来的代码段,转而显示该文件夹下的所有文件的详细信息。注意是代替而不是插入,子进程在执行完命令后,不会再执行下面剩余的代码段。

事实上,这 6 个函数中真正的系统调用只有 execve() ,其他 5 个都是库函数,它们最终都会调用 execve() 。

标签:Fork,fork,调用,函数,pid,进程,vfork
From: https://www.cnblogs.com/stuzhang/p/18009116

相关文章

  • 无涯教程-LOG10E函数
    E的以10为底的对数,约为0.434。LOG10E-语法Math.LOG10ELOG10E-示例console.log(Math.LOG10E)//thebase10logarithmofMath.E:0.434运行上面代码输出0.4342944819032518参考链接https://www.learnfk.com/es6/es6-math-property-log10e......
  • GPT 中的函数调用(function call)是什么?
    在OpenAIChatGPTAPI和GoogleGeminiAPI中我们可以看到函数调用的功能。这个功能是做什么用的?下面大概讲解。以GoogleGeminiAPI函数调用一节中的内容为例,该章节举了一个例子:大语言模型(LLMs)往往无法进行准确的数学运算。比如说,给Gemini两个数\(a\)和\(b\),让它计......
  • 无涯教程-LN10函数
    自然对数为10,约为2.302。LN10-语法Math.LN2LN10-示例console.log(Math.LN10)//thenaturallogarithmof10:~2.303运行上面代码输出2.302585092994046参考链接https://www.learnfk.com/es6/es6-math-property-ln10.html......
  • Springboot在编写CRUD时,访问对应数据函数返回null
    1.我遇到了什么问题我在学习springboot,其中在编写CRUD时发现访问数据的函数执行下去返回值是null但是其它部分正常。下面是我的错误代码pojopublicclassBot{@TableId(type=IdType.AUTO)privateIntegerid;privateIntegeruser_id;privateStr......
  • 无涯教程-LN2函数
    它返回2的自然对数,大约为0.693。LN2-语法Math.LN2LN2-示例console.log(Math.LN10)//thenaturallogarithmof10:~2.303运行上面代码输出0.6931471805599453参考链接https://www.learnfk.com/es6/es6-math-property-ln2.html......
  • 无涯教程-E函数
    这是一个欧拉常数,是自然对数的底数,大约为2.718。E-语法Math.EE-示例console.log(Math.E)//therootofthenaturallogarithm:~2.718运行上面代码输出2.718281828459045参考链接https://www.learnfk.com/es6/es6-math-property-e.html......
  • 函数的周期性的作用
    前言函数的周期性到底对研究函数有什么作用?作用列举做函数的图象由\(y=\sinx\),\(x\in[0,2\pi]\)的图像拓展到\(y=\sinx\),\(x\inR\)的图像,就是利用的函数的周期的作用。解三角不等式,常常是涉及有周期性的函数;比如求解不等式的思路:\(\sinx>\cfrac{1}{2}\),更多......
  • 无涯教程-valueOf()函数
    valueOf方法返回Date对象的原始值,即自UTC1970年1月1日午夜以来的毫秒数。valueOf()-语法Date.valueOf()valueOf()-返回值返回Date对象的原始值。valueOf()-示例vardateobject=newDate(1993,6,28,14,39,7);console.log(dateobject.valueOf());......
  • Eralng 学习笔记第六天, Fun,进程,电子邮件,数据库,端口
    ErlangFun  示例:-module(helloworld). -export([start/0]). start() ->    A = fun(X) ->       io:fwrite("~p~n",[X])       end,    A(5).输出5----------------------------------------------------module(helloworld). -export(......
  • 无涯教程-toTimeString()函数
    此方法把Date对象的时间部分转换为字符串。toTimeString()-语法Date.toTimeString()toTimeString()-返回值形式返回Date对象的时间部分。toTimeString()-示例vardateobject=newDate(1993,6,28,14,39,7);console.log(dateobject.toTimeString());......