Linux内核14-clone()、fork()和vfork()的区别

简介: Linux内核14-clone()、fork()和vfork()的区别

在分析这三个系统调用之前,先来理解一下进程的4要素:

  • 执行代码
    每个进程或者线程都要有自己的执行代码,不论是独立的,还是共享的一段代码。
  • 私有堆栈空间
    进程具有自己独立的堆栈空间,但是对于线程来说它是共享的。
  • 进程控制块(task_struct)
    不论是进程还是线程,都有自己的task_struct。Linux对于线程的实现采用”轻进程”。

  • 有独立的内存空间

    线程具有独立的内存空间,而线程共享内存空间。

Linux内核用于创建进程的系统调用有3个,它们的实现分别为:fork、vfork、clone。它们的作用如下表所示:

调用 描述
clone 创建轻量级进程(也就是线程),pthread库基于此实现
vfork 父子进程共享资源,子进程先于父进程执行
fork 创建父进程的完整副本

下面我们来看一下3个函数的区别:


1. clone()


创建轻量级进程,其拥有的参数是:

  1. fn
    指定新进程执行的函数。当从函数返回时,子进程终止。函数返回一个退出码,表明子进程的退出状态。
  2. arg
    指向fn()函数的参数。
  3. flags
    一些标志位,低字节是表示当子进程终止时发送给父进程的信号,通常是SIGCHLD信号。其余的3个字节是一组标志,如下表所示:


名称 描述
CLONE_VM 共享内存描述符和所有的页表
CLONE_FS 共享文件系统
CLONE_FILES 共享打开的文件
CLONE_SIGHAND 共享信号处理函数,阻塞和挂起的信号等
CLONE_PTRACE debug用,父进程被追踪,子进程也追踪
CLONE_VFORK 父进程挂起,直到子进程释放虚拟内存资源
CLONE_PARENT 设置子进程的父进程是调用者的父进程,
也就是创建兄弟进程
CLONE_THREAD 共享线程组,设置相应的线程组数据
CLONE_NEWNS 设置自己的命令空间,也就是有独立的文件系统
CLONE_SYSVSEM 共享System V IPC可撤销信号量操作
CLONE_SETTLS 为轻量级进程创建新的TLS段
CLONE_PARENT_SETTID 写子进程PID到父进程的用户态变量中
CLONE_CHILD_CLEARTID 设置时,当子进程exit或者exec时,给父进程发送信号
  1. child_stack
    指定子进程的用户态栈指针,存储在子进程的esp寄存器中。父进程总是给子进程分配一个新的栈。
  2. tls
    指向为轻量级进程定义的TLS段数据结构的地址。只有CLONE_SETTLS标志设置了才有意义。
  3. ptid
    指定父进程的用户态变量地址,用来保存新建轻量级进程的PID。只有CLONE_PARENT_SETTID标志设置了才有意义。
  4. ctid
    指定新进程保存PID的用户态变量的地址。只有CLONE_CHILD_SETTID标志设置了才有意义。

clone()其实是一个C库中的封装函数,它建立新进程的栈并调用sys_clone()系统调用。sys_clone()系统调用没有参数fn和arg。事实上,clone()把fn函数的指针保存到子进程的栈中return地址处,指针arg紧随其后。当clone()函数终止时,CPU从栈上获取return地址并执行fn(arg)函数。

下面我们看一个C代码示例,看看clone()函数的使用:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <linux/sched.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#define FIBER_STACK 8192
int a;
void *stack;
int do_something()
{
    printf("This is the son, and my pid is:%d,
        and a = %d\n", getpid(), ++a);
    free(stack);
    exit(1);
}
int main()
{
    void * stack;
    a = 1;
    /* 为子进程申请系统堆栈 */
    stack = malloc(FIBER_STACK);
    if(!stack)
    {
        printf("The stack failed\n");
        exit(0);
    }
    printf("Creating son thread!!!\n");
    clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//创建子线程
    printf("This is the father, and my pid is: %d, and a = %d\n", getpid(), a);
    exit(1);
}

上面的代码就相当于实现了一个vfork(),只有子进程执行完并释放虚拟内存资源后,父进程执行。执行结果是:

Creating son thread!!!
This is the son, and my pid is:3733, and a = 2
This is the father, and my pid is: 3732, and a = 2

它们现在共享堆栈,所以a的值是相等的。

2. fork()

linux将fork实现为这样的clone()系统调用,其flags参数指定为SIGCHLD信号并清除所有clone标志,child_stack参数是当前父进程栈的指针。父进程和子进程暂时共享相同的用户态堆栈。然后采用 写时复制技术,不管是父进程还是子进程,在尝试修改堆栈时,立即获得刚才共享的用户态堆栈的一个副本。也就是成为了一个单独的进程。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
    int count = 1;
    int child;
    child = fork();
    if(child < 0){
        perror("fork error : ");
    }
    else if(child == 0){    // child process
        printf("This is son, his count is: %d (%p). and his pid is: %d\n",
                ++count, &count, getpid());
    }
    else{                   // parent process
        printf("This is father, his count is: %d (%p), his pid is: %d\n",
                count, &count, getpid());
    }
    return EXIT_SUCCESS;
}

上面代码的执行结果:

This is father, his count is: 1 (0xbfdbb384), his pid is: 3994
This is son, his count is: 2 (0xbfdbb384). and his pid is: 3995

可以看出,父子进程的PID是不一样的,而且堆栈也是独立的(count计数一个是1,一个是2)。

3. vfork()

将vfork实现为这样的clone()系统调用,flags参数指定为SIGCHLD|CLONE_VM|CLONE_VFORK信号,child_stack参数等于当前父进程栈指针。

vfork其实是一种过时的应用,vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,阻塞父进程,直到子进程执行了exec()或exit()。vfork最初是因为fork没有实现COW机制,而在很多情况下fork之后会紧接着执行exec,而exec的执行相当于之前的fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。

实际上,vfork创建的是一个线程,与父进程共享内存和堆栈空间:

我们看下面的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    int count = 1;
    int child;
    printf("Before create son, the father's count is:%d\n", count);
    if(!(child = vfork())) {
        printf("This is son, his pid is: %d and the count is: %d\n",
                getpid(), ++count);
        exit(1);
    }
    else {
        printf("This is father, pid is: %d and count is: %d\n",
                getpid(), count);
    }
}

执行结果为:

Before create son, the father's count is:1
This is son, his pid is: 3564 and the count is: 2
This is father, pid is: 3563 and count is: 2

从运行结果看,vfork创建的子进程(线程)共享了父进程的count变量,所以,子进程修改count,父进程的count值也改变了。

另外由vfork创建的子进程要先于父进程执行,子进程执行时,父进程处于挂起状态,子进程执行完,唤醒父进程。除非子进程exit或者execve才会唤起父进程,看下面程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    int count = 1;
    int child;
    printf("Before create son, the father's count is:%d\n", count);
    if(!(child = vfork())) {
        int i;
        for(i = 0; i < 100; i++)
        {
            printf("Child process & i = %d\n", i);
            count++;
            if(i == 20)
            {
                printf("Child process & pid = %d;count = %d\n",
                        getpid(), ++count);
                exit(1);
            }
        }
    }
    else {
        printf("Father process & pid = %d ;count = %d\n",
                getpid(), count);
    }
}

执行结果为:

Before create son, the father's count is:1
Child process & i = 0
Child process & i = 1
Child process & i = 2
Child process & i = 3
Child process & i = 4
Child process & i = 5
Child process & i = 6
Child process & i = 7
Child process & i = 8
Child process & i = 9
Child process & i = 10
Child process & i = 11
Child process & i = 12
Child process & i = 13
Child process & i = 14
Child process & i = 15
Child process & i = 16
Child process & i = 17
Child process & i = 18
Child process & i = 19
Child process & i = 20
Child process & pid = 3755;count = 23
Father process & pid = 3754 ;count = 23

从上面的结果可以看出,父进程总是等子进程执行完毕后才开始继续执行。


总结


  1. clone、vfork和fork是根据不同的需求而开发的。
  • clone 参数比较多,可以实现的控制就比较多,clone的设计初衷是给pthread线程库的开发提供支持的。其实用它也完全可以实现另外两种系统调用。
  • vfork是一个过时的系统调用,当时是因为写时复制(COW)技术还没有。所以才设计了这个子进程先于父进程的执行的创建进程的系统调用。
  • fork就是一个创建完整进程的调用。
  1. clone、vfork和fork在内核层都是调用的_do_fork()这个函数。
相关文章
|
1天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
1天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
17 6
|
3天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
16 6
|
3天前
|
机器学习/深度学习 负载均衡 算法
深入探索Linux内核调度机制的优化策略###
本文旨在为读者揭开Linux操作系统中至关重要的一环——CPU调度机制的神秘面纱。通过深入浅出地解析其工作原理,并探讨一系列创新优化策略,本文不仅增强了技术爱好者的理论知识,更为系统管理员和软件开发者提供了实用的性能调优指南,旨在促进系统的高效运行与资源利用最大化。 ###
|
2天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####
|
4天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
21 4
|
5天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
安全 Linux 调度
关于linux系统如何实现fork的研究(二)【转】
转自:http://www.aichengxu.com/linux/7166015.htm 本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言   前一篇关于linux系统如何实现fork的研究(一)通过代码已经说明了从用户态怎么通过软中断实现调用系统调用clone函数,而clone函数的精华copy_process函数就在此篇文章中进行分析。
1027 0
|
Linux C语言
关于linux系统如何实现fork的研究(一)【转】
转自:http://www.aichengxu.com/linux/4157180.htm 引言 fork函数是用于在linux系统中创建进程所使用,而最近看了看一个fork()调用是怎么从应用到glibc,最后到内核中实现的,这片文章就聊聊最近对这方面研究的收获吧。
1083 0
|
6天前
|
缓存 监控 Linux