思考问题:
- 当首次调用新创建进程时,其入口在哪里?
- 系统调用 fork 函数是如何创建进程的?
- 系统调用 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 代码后,内核会依次做四件事情:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝到子进程中
- 将子进程添加到系统进程列表中
- 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;
}
以上程序验证两点:
- 通过 vfork() 创建的子进程会执行完后,才到父进程执行;
- 子进程共享父进程的地址空间。
// 输出结果:
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() 的区别:
- fork 函数的父子进程执行顺序不确定,而 vfork 函数保证子进程先执行,在它调用exit 函数退出进程或者调用 exec 函数族替换进程之后父进程才可能被调度运行。
- 执行 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