fork vfork clone pthread_create

本文详细介绍了Linux系统中进程和线程的区别与联系,重点讨论了fork、vfork及clone系统调用的功能与应用场景,并解释了Linux如何实现线程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽

象成各种数据对象:进程控制块、虚存空间、文件系统,文件I/O、信号处理函数。所以创建一个进程的

过程就是这些数据对象的创建过程。

在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于

父进程,具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,fifo,System V

IPC机制等,另外通过fork创建子进程系统开销很大,需要将上面描述的每种资源都复制一个副本。这样

看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出

一个子进程后,其子进程仅仅是为了调用exec执行另一个执行文件,那么在fork过程中对于虚存空间的复

制将是一个多余的过程(由于Linux中是采取了copy-on-write技术,所以这一步骤的所做的工作只是虚存

管理部分的复制以及页表的创建,而并没有包括物理也面的拷贝);另外,有时一个进程中具有几个独立

的计算单元,可以在相同的地址空间上基本无冲突进行运算,但是为了把这些计算单元分配到不同的处理

器上,需要创建几个子进程,然后各个子进程分别计算最后通过一定的进程间通讯和同步机制把计算结果

汇总,这样做往往有许多格外的开销,而且这种开销有时足以抵消并行计算带来的好处。

 

这说明了把计算单元抽象到进程上是不充分的,这也就是许多系统中都引入了线程的概念的原因。在

讲述线程前首先介绍以下vfork系统调用,vfork系统调用不同于fork,用vfork创建的子进程共享地址空

间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进

程所见。但是用vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。这样的好处是在子进

程被创建后仅仅是为了调用exec执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以

对地址空间的复制是多余的,通过vfork可以减少不必要的开销。

在Linux中, fork和vfork都是调用同一个核心函数

do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)

其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PID,CLONE_VFORK

等等标志位,任何一位被置1了则表明创建的子进程和父进程共享该位对应的资源。所以在vfork的实现中

,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,这表示子进程和父进程共享地址空间,同时

do_fork会检查CLONE_VFORK,如果该位被置1了,子进程会把父进程的地址空间锁住,直到子进程退出或

执行exec时才释放该锁。

 

在讲述clone系统调用前先简单介绍线程的一些概念。

线程是在进程的基础上进一步的抽象,也就是说一个进程分为两个部分:线程集合和资源集合。线程

是进程中的一个动态对象,它应该是一组独立的指令流,进程中的所有线程将共享进程里的资源。但是线

程应该有自己的私有对象:比如程序计数器、堆栈和寄存器上下文。

线程分为三种类型:

内核线程、轻量级进程和用户线程。

内核线程:

它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要

和一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度

并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过

态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。

轻量级进程:

轻量级进程是核心支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独

的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期

这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个LWP都应

该有自己的程序计数器、寄存器集合、核心栈和用户栈。

用户线程:

用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和

调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实

现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自

己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前

线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。

内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力。用户线程不是真正的调度

实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数

来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会

阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。

 

明确了这些概念后,来讲述Linux的线程和clone系统调用。

在许多实现了MT的操作系统中(如:Solaris,Digital Unix等), 线程和进程通过两种数据结构来

抽象表示: 进程表项和线程表项,一个进程表项可以指向若干个线程表项, 调度器在进程的时间片内再

调度线程。 但是在Linux中没有做这种区分, 而是统一使用task_struct来管理所有进程/线程,只是

线程与线程之间的资源是共享的,这些资源可是是前面提到过的:虚存、文件系统、文件I/O以及信号处

理函数甚至PID中的几种。


也就是说Linux中,每个线程都有一个task_struct,所以线程和进程可以使用同一调度器调度。其实

Linux核心中,轻量级进程和进程没有质上的差别,因为Linux中进程的概念已经被抽象成了计算状态加资

源的集合,这些资源在进程间可以共享。如果一个task独占所有的资源,则是一个HWP,如果一个task和

其它task共享部分资源,则是LWP。

clone系统调用就是一个创建轻量级进程的系统调用:

int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);

其中fn是轻量级进程所执行的过程,stack是轻量级进程所使用的堆栈,flags可以是前面提到的

CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的组合。Clone 和fork,vfork在实现时

都是调用核心函数do_fork。

do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs);

和fork、vfork不同的是,fork时clone_flag = SIGCHLD;

vfork时clone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD;

而在clone中,clone_flag由用户给出。

下面给出一个使用clone的例子。

Void * func(int arg)

{

. . . . . .

}

int main()

{

int clone_flag, arg;

. . . . . .

clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |

CLONE_FILES;

stack = (char *)malloc(STACK_FRAME);

stack += STACK_FRAME;

retval = clone((void *)func, stack, clone_flag, arg);

. . . . . .

}

看起来clone的用法和pthread_create有些相似,两者的最根本的差别在于clone是创建一个LWP,对

核心是可见的,由核心调度,而pthread_create通常只是创建一个用户线程,对核心是不可见的,由线程

库调度。

linux的pthread_create最终调用clone,pthread_create调用clone,并把开辟一个stack作为参数

thread 建立, 同步,销毁等由线程库负责,

 

 

看起来你在尝试理解 Linux 系统编程中的进程创建函数 `fork()`, `vfork()`, 和 `clone()` 的区别以及如何使用它们。这些都不是 shell 命令,所以当你直接在 bash 中输入它们的时候会提示“未找到命令”。下面是对这几个系统调用的一个简短介绍: ### fork() `fork()` 是 UNIX/Linux 操作系统中最常见的创建新进程的方式之一。它通过复制当前进程(称为父进程),生成一个新的进程(子进程)。父子进程几乎完全相同,除了 PID(进程 ID)、PPID(父进程 ID)等少数差异。 **特点** - 创建完整的独立副本 - 资源消耗相对较大 示例如下: ```c #include <unistd.h> #include <stdio.h> int main(){ pid_t ret = fork(); if(ret == -1){ perror("fork error"); return 1; } else if (ret > 0){ // Parent process. printf("I'm the parent.\n"); }else{ // Child process has a returned value of zero from fork(). printf("Hello World\n"); } return 0; } ``` ### vfork() `vfork()` 类似于 `fork()` ,但它提供了一种更高效的机制来启动新的程序或执行其他不需要修改原进程上下文的任务。与 `fork()` 不同的是,在 `vfork()` 返回之前不会真正地拷贝整个地址空间给子进程;相反,直到子进程中某个特定的操作完成前,如 `_exit()` 或者 `exec*()` 家族函数被调用时才会恢复对父进程的控制权,并允许继续运行。 **优点** - 减少了内存开销 - 对某些特殊场景优化了性能 但是需要注意的是由于其特殊的共享性质,它的安全性较低并且容易引起死锁等问题,因此通常建议避免使用除非确实需要这种行为带来的效率提升。 ### clone() `clone()` 提供了一个更为底层也更加通用的方式来创建线程或者轻量级的过程单位——Linux下的线程实际上是作为一种特别类型的进程实现的。它可以指定更多选项来自定义新创建实体的行为模式比如是否共享文件描述符表、栈信息等等。 简单来说就是你可以认为它是比标准库提供的pthread_create更加细粒度版本的东西。 **灵活性高** 以上三个都是 C 库里的 API 接口而非 Shell 内建指令,如果你想要测试上述任何一个功能点的话应该编写一段简单的 C/C++ 源码然后编译链接成二进制可执行文件再跑起来看效果而不能直接放到终端里面敲一行就期望得到结果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值