前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux内核14-clone()、fork()和vfork()的区别

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

作者头像
Tupelo
发布2022-08-15 15:46:28
1.6K0
发布2022-08-15 15:46:28
举报
文章被收录于专栏:嵌入式ARM和Linux

在分析这三个系统调用之前,先来理解一下进程的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_PTRACEdebug用,父进程被追踪,子进程也追踪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时,给父进程发送信号
  4. child_stack 指定子进程的用户态栈指针,存储在子进程的esp寄存器中。父进程总是给子进程分配一个新的栈。
  5. tls 指向为轻量级进程定义的TLS段数据结构的地址。只有CLONE_SETTLS标志设置了才有意义。
  6. ptid 指定父进程的用户态变量地址,用来保存新建轻量级进程的PID。只有CLONE_PARENT_SETTID标志设置了才有意义。
  7. ctid 指定新进程保存PID的用户态变量的地址。只有CLONE_CHILD_SETTID标志设置了才有意义。

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

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

代码语言:javascript
复制
#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(),只有子进程执行完并释放虚拟内存资源后,父进程执行。执行结果是:

代码语言:javascript
复制
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参数是当前父进程栈的指针。父进程和子进程暂时共享相同的用户态堆栈。然后采用 写时复制技术,不管是父进程还是子进程,在尝试修改堆栈时,立即获得刚才共享的用户态堆栈的一个副本。也就是成为了一个单独的进程。

代码语言:javascript
复制
#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;
}

上面代码的执行结果:

代码语言:javascript
复制
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创建的是一个线程,与父进程共享内存和堆栈空间:

我们看下面的示例代码:

代码语言:javascript
复制
#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);
    }
}

执行结果为:

代码语言:javascript
复制
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才会唤起父进程,看下面程序:

代码语言:javascript
复制
#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);
    }
}

执行结果为:

代码语言:javascript
复制
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就是一个创建完整进程的调用。
  2. clone、vfork和fork在内核层都是调用的_do_fork()这个函数。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式ARM和Linux 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. clone()
  • 2. fork()
  • 3. vfork()
  • 总结
相关产品与服务
SSL 证书
腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档