「技术干货」fork,vfork,clone,Linux系统调用
前言:fork,vfork,clone都是linux系统调用,这三个函数分别调用sys_fork,sys_vfork,sys_clone,最终都会调用到do_fork函数。差别就在于参数的传递和一些准备工作的不同
一,进程的四要素
linux进程所必须的四个要素:
- 程序代码,有一段程序供其执行: 代码不一定是进程专有,可以与其它进程共享
- 有自己的专用系统堆栈空间:
- 有进程控制块(task_struct):
- 有独立的存储空间:
以上4条,缺一不可。如果缺少第四条,那么就称其为"线程"。如果完全没有用户空间,称其为”内核线程“;如果共享用户空间,则称其为”用户线程"。
二,fork
系统调用fork,允许父进程创建一个新的进程(子进程)。新的子进程是父进程的翻版:完全继承父进程的栈、数据段、堆和执行文本的拷贝。其接口如下:
NAME
fork - create a child process
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
完成对其调用后将存在两个进程,且每个进程都会从fork的返回处继续执行。这两个进程将执行相同的程序文本段,但却各自拥有不同的栈段、数据段以及堆段拷贝。子进程的栈、数据以及栈段开始时是对父进程内存相应各部分的完全复制。执行fork之后,每个进程均可修改各自的栈数据以及堆中的变量而不影响另一进程。

为调用进程创建一个一模一样的新进程,但父子进程需要改变时候,执行一个copy,但是任何修改都造成分裂,如:chroot, open, 写memory,mmap,sigaction….
更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取。

fork的示例
考虑以下代码的输出,假设test.txt中的内容”abcdefghijklmnopqrst…”
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<fcntl.h>
int main(void)
{
char str[10];
int count = 1;
int fd = open("test.txt", O_RDWR);
if(fork() == 0)
{
int cnt = read(fd, str, 10);
printf("Child process : %s\n", (char *)str);
printf("This is son, his count is: %d (%p). and his pid is: %d\n", ++count, &count, getpid());
}
else
{
int cnt = read(fd, str, 10);
printf("Child process : %s\n", (char *)str);
printf("This is father, his count is: %d (%p), his pid is: %d\n", count, &count, getpid());
}
return 0;
}
输出为:

- 从结果来看,子进程和父进程的PID不同,内存资源count是值的复制,子进程改变了count的值,而父进程中的count的值没有改变,这个过程请参考之前章节的写时复制技术。
- 两个进程共享了同一个指向文件的结构体,所以当子进程输出“abcdefghij”后,父进程就接着输出"klmnopqrst"

三,vfork
vfork也是创建子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行exec()或者exit()。vfork设计的最初是因为fork没有实现COW机制,很多情况下fork之后会紧跟着exec,而exec的执行相当于前面fork复制的空间全部变得无用,所以设计了vfork。而现在fork使用了COW,唯一的代价仅仅是复制父进程页表的代价,所以vfork的功能就变得越来越不重要。
NAME
vfork - create a child process and block parent
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork因为如下两个特性而更具效率,也是区别与fork所在:
无需为子进程复制虚拟内存页或页表,相反,子进程共享父进程的内存,直至其成功执行exec或调用exit退出
在子进程调用exit或exec之前,将暂停执行父进程,所以在使用vfork时,一般立即在vfork之后调用exec,如果exec调用失败,子进程应调用exit退出。

vfork示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
int count =1;
int child;
printf("Before create son, the father's count is %d\n",count);
if(!(child = vfork()))
{
int i = 0;
for( i = 0; i< 3; i++)
{
count++;
printf("This is son This i is: %d count: %d\n", i, count);
if(i == 2)
{
printf("This is son This pid is: %d count: %d\n", getpid(), count);
exit(1);
}
}
}
else
{
printf("This is father This pid is: %d count: %d\n", getpid(), count);
}
return 0;
}
输出:

用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程
子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。
值得注意的是用vfork创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,父进程就讲一直阻塞,出现异常
大家可以实际将上述例子中exit(1)这个注释掉后,会出现什么情况。对于Vfork和fork是类似的,除了下面两点:
- 1、阻塞父进程
- 2、不复制父进程的页表
之所以vfork要阻塞父进程是因为vfork后父子进程使用的是完全相同的mm_struct,也就是由完全相同的虚拟地址空间,包括栈也相同,所以两个进程就不能同时运行,否则栈就会乱掉。所以vfork后,父进程是阻塞的,直到调用了exec系列或者exit后,这个时候,子进程的mm需要释放,不再与父进程公用,这个时候就可以解除父进程的阻塞状态。
四,clone
clone是Linux为创建线程设计的,所以可以说clone是fork的升级版本,不仅可以创建进程或线程,还可以指定创建新的命名空间,有选择的继承父进程的内存、甚至可以将创建出来的进程编程父进程的兄弟进程等。
clone函数功能强大,待有很多参数,提供了一个非诚灵活自由的常见进程的方法,因此它创建进程要比前面两种方法更为复杂。clone可以有选择继承父进程的资源,你可以选择像vfork一样和父进程共享一个虚拟存储空间,也可以不和父进程共享,甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。
NAME
clone, __clone2 - create a child process
SYNOPSIS
/* Prototype for the glibc wrapper function */
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
fn为函数指针 | 此指针指向一个函数体,即想要创建进程的静态程序 |
child_stack | 为给子进程分配系统堆栈的指针 |
arg | 传给子进程的参数一般为(0) |
flags | 要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共享,在这里设置参数) |
下面是flaga可以取得值:
- CLONE_PARENT 建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
- CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
- CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
- CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
- CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
- CLONE_PTRACE 若父进程被trace,子进程也被trace
- CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
- CLONE_VM 子进程与父进程运行于相同的内存空间
- CLONE_PID 子进程在创建时PID与父进程一致
- CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#define FIBER_STACK 8192
int a;
void *stack;
int do_something()
{
a = 10;
printf("This is son, the pid is: %d, the a is: %d\n",getpid(), a);
free(stack);
exit(1);
}
int main(void)
{
void *stack;
a = 1;
stack = malloc(FIBER_STACK);
if(!stack)
{
printf("The stack failed\n");
exit(0);
}
printf("Create son thread \n");
clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM | CLONE_VFORK, 0);
printf("This is father, the pid is: %d, the a is: %d\n",getpid(), a);
return 0;
}
输出结果:

inux创建线程的API,本质上去调 clone。要求把P2的所有资源的指针,都指向P1。线程,也被称为 Light weight process。而Linux在clone线程时也十分灵活,可以选择共享/不共享部分资源。

POSIX标准要求,进程里面如果有多个线程,在用户空间 getpid() 看到的都是同一个id,这个id其实是TGID。一个进程里面创建了多个线程,在/proc 下 的是 tgid,/proc/tgid/task/{pidx,y,z}pthread_self() 看到的是用户空间pthread线程库里获得的id 。
五,总结
下面是三个接口的优缺点对比:

相关推荐
-
「PHP」MVC框架是什么?为什么要用它2025-02-25 00:25:41
-
如何用PHP写一个比较安全的API系统(实现)2025-02-25 00:19:49
-
php 解析url获取相关信息2025-02-25 00:15:37
-
mysql命令总结和PyMysql2025-02-25 00:11:35
-
MySQL特性:深入理解ICP2025-02-25 00:11:07