在 Linux 操作系统中,每个任务都以「进程」的形式存在。但 Linux 下的「线程」又是什么?Linux 并没有单独定义一种全新数据结构来表示线程,而是将线程视为一种特殊的进程——一种共享资源的轻量级进程。然而,在具体实现和运行机制上,线程与进程又存在诸多关键差异,这也使得理解二者的区别和联系非常重要。
进程和线程共享许多核心机制,但在创建、调度、资源共享、同步和消亡等环节又体现出微妙的不同。例如:
- 进程通常通过
fork()
系统调用创建,而线程更多地使用pthread_create()
,但在内核中二者最终都归结到kernel_clone方法。 - 线程共享内存空间、文件描述符等资源,但仍有私有的数据,如线程局部存储(TLS)。
- 线程在同步机制上广泛使用
futex
原语来实现互斥锁和条件变量,体现出与进程级同步不同的性能优势。
为了深入理解进程与线程的生命周期,本系列博文将通过 Linux 内核6.12与 glibc 库的源码,详细探索如下关键问题:
- 进程与线程各自如何诞生?它们的创建方式在内核和glibc库中有哪些异同?
- 内核如何调度和管理进程与线程?为何说线程调度和进程调度实际上共享同一套机制?
- 线程和进程如何实现资源共享和隔离?同步、阻塞机制又如何在其中发挥作用?
- 最终,进程和线程如何结束自己的生命周期?内核又如何回收它们各自的资源?
1.进程和线程的概念
在探讨进程与线程的区别时,人们往往更关注它们在概念和资源管理上的差异。但实际上,在 Linux 内核中,进程与线程之间的相同点远远多于不同点。虽然经典操作系统理论中,我们通常会将进程抽象为进程控制块(Process Control Block, PCB),线程抽象为线程控制块(Thread Control Block, TCB),但 Linux 内核却做了更统一、更精巧的设计:Linux 内核统一使用了一个核心的数据结构——task_struct
,来同时表示进程和线程。
在 Linux 内核源码中,task_struct
结构定义如下(节选部分关键字段):
struct task_struct {
// 1. 进程或线程的当前状态(运行、阻塞、停止等)
unsigned int __state;
......
// 2. 进程或线程的优先级信息,用于内核调度
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
// 3. 进程或线程的地址空间描述符(虚拟内存管理)
struct mm_struct *mm;
struct mm_struct *active_mm;
// 4. 进程和线程的标识(PID与线程组ID)
pid_t pid;
pid_t tgid;
// 5. 进程或线程之间的父子、兄弟关系,用于构成进程树
struct task_struct __rcu *parent;
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
// 6. 进程或线程的文件系统上下文(根目录、当前工作目录等)
struct fs_struct *fs;
// 7. 进程或线程打开的文件描述符信息
struct files_struct *files;
// 8. 进程或线程所在的各种命名空间(网络、用户、PID等)
struct nsproxy *nsproxy;
......
};
通过task_struct
这个统一的数据结构,Linux 内核巧妙地封装并统一管理了进程和线程所涉及的众多资源。其中存在少数字段体现出进程与线程在内核管理上的差异。理解这些差异化的成员,有助于我们更清晰地认识 Linux 如何高效而灵活地实现进程与线程的统一管理。
下面,我们着重分析能体现进程与线程差异的关键成员。
1.1 进程、线程ID
我们知道,每个进程或线程在 Linux 内核中都有一个唯一的标识 ID。在 task_struct
结构体中,两个关键字段——pid
和 tgid
——用于表示进程或线程的身份。
然而,对于进程和线程而言,这两个字段的含义有所不同:
- 对于进程:
pid
和tgid
是相同的。即,一个普通的进程,它的pid
就是tgid
,代表整个进程的唯一标识。 - 对于线程:Linux 线程本质上是通过
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)
方式创建的轻量级进程。在同一进程内的多个线程:- 每个线程都有自己的
pid
,用于内核内部调度。 - 但它们共享相同的
tgid
,即所有线程的tgid
都等于主线程的pid
,表示它们属于同一个线程组。
- 每个线程都有自己的
图示如下,对用户程序来说,调用getpid方法返回的其实是tgid。:
1.2 进程、线程地址空间
在 Linux 内核中,mm_struct
结构体用于表示进程的虚拟内存空间,它是进程地址空间管理的核心数据结构。每个普通进程都会有一个独立的 mm_struct
,用于记录其虚拟地址空间的布局,包括代码段、数据段、堆、栈等。而对于线程,由于线程共享进程的地址空间,因此多个线程共用同一个 mm_struct
。
struct mm_struct {
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
unsigned long start_code, end_code, start_data, end_data;//代码段及数据段的开头和结尾
unsigned long start_brk, brk, start_stack;//堆内存和用户态堆栈的起始地址
unsigned long arg_start, arg_end, env_start, env_end;//进程命令行参数 (argv[]) 在地址空间中的位置
.......
}
进程的 mm_struct
组织了进程运行所需的所有内存资源,共同表示一个虚拟地址空间。
但是对于内核线程来说,又有不同点,由于其只工作在地址空间比较高的地方,所以不涉及对虚拟内存部分的使用,故其mm_struct是NULL。
另一个字段是active_mm
,该字段表示当前任务(进程或线程)在内核态执行时所使用的地址空间。它的存在主要是为了支持内核线程,因为内核线程本身没有用户态的 mm_struct
,但在某些情况下仍需要访问用户态的地址空间,比如进行 copy_from_user()
或 copy_to_user()
操作。
字段 | 适用于 | 作用 |
---|---|---|
mm | 进程(普通用户态任务) | 指向该进程的 mm_struct ,管理进程的地址空间 |
mm | 用户态线程 | 线程共享进程地址空间,因此 mm 指向同一 mm_struct |
mm | 内核线程 | NULL ,因为内核线程没有用户态地址空间 |
active_mm | 进程 | 等同于 mm ,即 active_mm == mm |
active_mm | 内核线程 | 继承上一个运行的普通进程的 mm_struct ,用于访问用户地址空间 |
2.进程的创建
进程的创建中最核心的就是fork系统调用。其源代码实现如下:
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
struct kernel_clone_args args = {
.exit_signal = SIGCHLD, // 子进程退出时发送 SIGCHLD 信号给父进程
};
return kernel_clone(&args);
#else
return -EINVAL; // 如果系统不支持 MMU,则返回错误
#endif
}
struct kernel_clone_args {
u64 flags;
......
}
这里传入的args,其实是传入了一个flag选项。可选的值如下:
进程/线程创建相关标志
宏定义 | 值 | 作用 |
---|---|---|
CSIGNAL | 0x000000ff | 子进程退出时向父进程发送的信号掩码(通常是 SIGCHLD ) |
CLONE_VM | 0x00000100 | 共享内存地址空间(进程间不会复制 mm_struct ,用于线程) |
CLONE_FS | 0x00000200 | 共享文件系统信息(chdir /chroot 影响所有线程) |
CLONE_FILES | 0x00000400 | 共享文件描述符表(多个线程可使用相同 fd ) |
CLONE_SIGHAND | 0x00000800 | 共享信号处理机制(信号处理函数共享) |
CLONE_PIDFD | 0x00001000 | 创建 pidfd (进程文件描述符),可用于进程管理 |
CLONE_PTRACE | 0x00002000 | 子进程继承父进程的 ptrace 追踪状态(调试相关) |
CLONE_VFORK | 0x00004000 | 使用 vfork() 语义(子进程执行 exec() 之前,父进程阻塞) |
CLONE_PARENT | 0x00008000 | 让子进程与父进程共享相同的父进程(更改 ppid ) |
CLONE_THREAD | 0x00010000 | 使子进程与父进程处于同一线程组(tgid 相同,即 pthread_create() 线程) |
Namespace(命名空间)隔离相关标志
宏定义 | 值 | 作用 |
---|---|---|
CLONE_NEWNS | 0x00020000 | 新挂载命名空间(独立 mount /umount ) |
CLONE_NEWCGROUP | 0x02000000 | 新 cgroup 命名空间(独立的 cgroup 控制组) |
CLONE_NEWUTS | 0x04000000 | 新 UTS(主机名)命名空间(隔离 hostname 和 domainname ) |
CLONE_NEWIPC | 0x08000000 | 新 IPC(进程间通信)命名空间(System V 共享内存、消息队列、信号量独立) |
CLONE_NEWUSER | 0x10000000 | 新用户命名空间(允许非 root 用户拥有 root 权限) |
CLONE_NEWPID | 0x20000000 | 新进程 ID 命名空间(进程 PID 独立,适用于容器) |
CLONE_NEWNET | 0x40000000 | 新网络命名空间(独立 IP 、网络接口 、端口 ) |
CLONE_NEWTIME | 0x00000080 | 新时间命名空间(独立 CLOCK_REALTIME 和 CLOCK_MONOTONIC ) |
但是在fork进程的时候,只传递了一个SIGCHLD,这就意味着在fork进程时,虚拟地址空间、文件系统信息、文件列表都要创建新的。而这些与命名空间隔离的都会与父进程共用。
kernel_clone方法:
pid_t kernel_clone(struct kernel_clone_args *args)
{
u64 clone_flags = args->flags; // 解析 clone 标志位
struct completion vfork; // 用于 vfork 机制的同步
struct pid *pid; // 新进程的 PID 结构
struct task_struct *p; // 新进程的 task_struct
int trace = 0; // Ptrace 事件类型
pid_t nr; // 最终返回的子进程 PID
.......
p = copy_process(NULL, trace, NUMA_NO_NODE, args);//复制 task_struct及其他结构体
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
......
trace_sched_process_fork(current, p);//通知调度器新进程已创建
pid = get_task_pid(p, PIDTYPE_PID);//获取新进程 pid 结构体
nr = pid_vnr(pid);
wake_up_new_task(p);//将新任务放入就绪队列等待调度器调度
return nr;
}
该方法主要调用copy_process方法生成子进程的task_struct,接着调用wake_up_new_task方法将新任务放入就绪队列等待调度器调度。其主要代码如下:
/**
* copy_process - 复制当前进程,创建一个新的进程(或线程)
* @pid: 指向新进程的 PID 结构体
* @trace: 跟踪标志
* @node: NUMA 结点
* @args: 进程克隆参数
*/
struct task_struct *copy_process(struct pid *pid, int trace, int node, struct kernel_clone_args *args)
{
struct task_struct *p; // 指向新创建进程的 task_struct
const u64 clone_flags = args->flags; // 进程克隆标志
struct nsproxy *nsp = current->nsproxy; // 继承当前进程的命名空间指针
......
// 复制当前进程的 task_struct 结构体,为新进程分配内存
p = dup_task_struct(current, node);
// 复制文件描述符表,根据 clone_flags 选择共享或复制
retval = copy_files(clone_flags, p, args->no_files);
if (retval)
goto bad_fork_cleanup_semundo;
// 复制文件系统信息(当前工作目录、root 目录等)
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
// 复制信号处理程序(包括信号屏蔽字、处理函数)
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
// 复制信号队列和信号状态
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
// 复制虚拟内存(地址空间),如果 CLONE_VM 设置,则共享地址空间
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
// 复制命名空间(PID、网络、IPC、挂载等)
retval = copy_namespaces(clone_flags, p);
......
// 如果新进程不是 init 进程,则为其分配新的 PID
if (pid != &init_struct_pid) {
pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid, args->set_tid_size);
if (IS_ERR(pid)) { // 检查分配 PID 是否成功
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
}
}
......
return p; // 返回新进程的 task_struct 指针
}
该方法首先调用dup_task_struct函数复制了一个新的task_struct内核对象,接着对各种核心对象进行复制处理,处理命名空间下的pid申请。
2.1 复制task_struct结构体
copy_process使用dup_task_struct复制新的task_struct内核对象:
p = dup_task_struct(current, node);
其参数current表示当前进程,即父进程;node表示NUMA架构下的某个结点。这次复制只会复制task_struct结构体本身,内部的成员仅仅是复制了指针,仍然指向父进程指针指向的,如图所示。
源代码如下:
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
struct task_struct *tsk;
......
tsk = alloc_task_struct_node(node);//申请task_struct结构体对象
if (!tsk)
return NULL;
......
err = arch_dup_task_struct(tsk, orig);//复制 task_struct 结构
if (err)
goto free_tsk;
err = alloc_thread_stack_node(tsk, node);//为新进程分配内核栈
if (err)
goto free_tsk;
.......
}
2.2 复制files_struct
在 copy_process()
中,copy_files()
负责复制或共享 files_struct
(文件描述符表),决定新进程如何管理文件描述符:
static int copy_files(unsigned long clone_flags, struct task_struct *tsk, int no_files)
{
struct files_struct *oldf, *newf;
/* 获取当前进程(父进程)的文件描述符表 */
oldf = current->files;
/* 如果当前进程没有打开任何文件(如某些后台进程),直接返回 */
if (!oldf)
return 0;
/* 如果 no_files 置位,新进程不继承文件描述符,files 设为空 */
if (no_files) {
tsk->files = NULL;
return 0;
}
/*
* 如果 clone_flags 设置了 CLONE_FILES,则新进程共享父进程的 files_struct,
* 仅增加 files_struct 的引用计数,而不复制文件描述符表。
* 这种情况通常用于多线程(pthread),所有线程共享相同的文件描述符。
*/
if (clone_flags & CLONE_FILES) {
atomic_inc(&oldf->count);
return 0;
}
/*
* 如果没有设置 CLONE_FILES(默认行为,fork 也在此情况),
* 需要为新进程创建一个独立的 files_struct,并复制父进程的文件描述符表。
*/
newf = dup_fd(oldf, NULL);
/* 如果复制失败,返回错误码 */
if (IS_ERR(newf))
return PTR_ERR(newf);
/* 绑定新进程的文件描述符表 */
tsk->files = newf;
return 0;
}
fork()
默认不传入 CLONE_FILES
,因此新进程不会共享 files_struct
,而是调用 dup_fd()
复制一份新的文件描述符表:
2.3 复制file_struct
在 copy_process()
中,copy_fs()
负责在 fork()
或 clone()
时处理 文件系统相关信息,即 fs_struct
。该函数决定新进程是共享还是复制父进程的文件系统信息,如当前工作目录、根目录等:
static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{
struct fs_struct *fs = current->fs; // 获取当前进程(父进程)的 fs_struct
/*
* 如果 clone_flags 设置了 CLONE_FS,则子进程共享父进程的 fs_struct,
* 例如 `chdir()` 或 `chroot()` 会影响所有线程。
*/
if (clone_flags & CLONE_FS) {
.......
/* 递增 fs_struct 的引用计数 */
fs->users++;
spin_unlock(&fs->lock);
return 0; // 共享成功,直接返回
}
/*
* 如果没有设置 CLONE_FS(默认行为,fork 情况),则复制 fs_struct,
* 使新进程拥有独立的 fs_struct。
*/
tsk->fs = copy_fs_struct(fs);
if (!tsk->fs)
return -ENOMEM; // 复制失败,返回内存不足错误
return 0;
}
fork()
默认不传入CLONE_FS
,所以进入copy_fs_struct方法:
struct fs_struct *copy_fs_struct(struct fs_struct *old)
{
struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL);
/* 如果分配失败,返回 NULL */
if (fs) {
fs->users = 1; // 初始化用户计数,表示该 fs_struct 仅被一个进程使用
fs->in_exec = 0; // 该进程未处于 exec() 过程中
spin_lock_init(&fs->lock); // 初始化锁,确保并发访问安全
seqcount_spinlock_init(&fs->seq, &fs->lock); // 初始化序列锁
fs->umask = old->umask; // 复制 umask(文件权限掩码)
spin_lock(&old->lock); // 加锁,防止并发修改
fs->root = old->root; // 复制根目录路径
path_get(&fs->root); // 增加根目录引用计数,防止被释放
fs->pwd = old->pwd; // 复制当前工作目录路径
path_get(&fs->pwd); // 增加工作目录引用计数
spin_unlock(&old->lock); // 释放锁
}
}
使用了当前进程的值对新进程的umask掩码、pwd、root目录进行初始化:
2.4 复制mm_struct
在 copy_process()
中,copy_mm()
负责复制或共享进程的内存管理结构 mm_struct
,即进程的虚拟地址空间。
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
tsk->min_flt = tsk->maj_flt = 0; // 次级/主缺页次数清零
tsk->nvcsw = tsk->nivcsw = 0; // 进程上下文切换计数清零
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
tsk->last_switch_time = 0; // 记录进程最后切换时间
#endif
tsk->mm = NULL; //指向进程的虚拟地址空间
tsk->active_mm = NULL;//用于内核线程,没有 mm,但仍可能访问用户地址空间
oldmm = current->mm;
if (!oldmm)
return 0;
if (clone_flags & CLONE_VM) {//如果 CLONE_VM 置位(如 pthread_create() 线程)
mmget(oldmm);//增加 mm_struct 引用计数
mm = oldmm;//共享 mm_struct,多个线程共享地址空间
}
else {//如果 CLONE_VM 未置位
mm = dup_mm(tsk, current->mm);//调用 dup_mm() 复制 mm_struct,子进程拥有独立地址空间
if (!mm)
return -ENOMEM;
}
tsk->mm = mm;//使子进程拥有虚拟地址空间
tsk->active_mm = mm;//进程访问的活动地址空间
sched_mm_cid_fork(tsk);
return 0;
}
在进程创建时,CLONE_VM
未置位,所以会调用dup_mm申请一个新的地址空间:
static struct mm_struct *dup_mm(struct task_struct *tsk,
struct mm_struct *oldmm)
{
struct mm_struct *mm;
int err;
mm = allocate_mm(); // 分配 mm_struct 结构体
if (!mm)
goto fail_nomem; // 如果分配失败,跳转到错误处理
memcpy(mm, oldmm, sizeof(*mm)); // 复制 oldmm 的内容到新 mm_struct
..........
}
在该方法中,通过allocate_mm申请了新的mm_struct,并且将当前进程的地址空间复制到新的mm_struct对象进行了初始化。故新进程的虚拟地址空间和当前进程的是一样的,所以父进程的虚拟地址空间中的数据,子进程都可以直接使用。
2.5 复制nsproxy
关于进程创建部分的nsproxy复制及pid的申请在该博文中详细分析过:
Linux资源隔离基础(二):namespace-CSDN博客
2.6 进入就绪队列
当copy_process方法执行完毕,新进程的task_struct对象就创建出来了,接下来内核会调用wake_up_new_task方法将该新进程加入就绪队列,等待调度。
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf; // 记录运行队列锁标志
struct rq *rq; // 运行队列指针
int wake_flags = WF_FORK; // 进程创建时的 wake flag
raw_spin_lock_irqsave(&p->pi_lock, rf.flags); // 加锁,防止并发访问
WRITE_ONCE(p->__state, TASK_RUNNING);//新进程的状态改为 TASK_RUNNING,表示可调度
#ifdef CONFIG_SMP
/*
* Fork 负载均衡(SMP 多核负载均衡)
*/
p->recent_used_cpu = task_cpu(p); // 记录最近使用的 CPU
rseq_migrate(p); // 处理 rseq 迁移(用于线程安全的 restartable sequences)
__set_task_cpu(p, select_task_rq(p, task_cpu(p), &wake_flags));//直接设置 task_struct->cpu,避免不必要的 sched_class::migrate_task_rq 调用。
#endif
rq = __task_rq_lock(p, &rf); // 锁定进程的运行队列
update_rq_clock(rq); // 更新运行队列时钟
post_init_entity_util_avg(p); // 初始化负载追踪数据(CFS调度)
activate_task(rq, p, ENQUEUE_NOCLOCK | ENQUEUE_INITIAL);//将新进程 p 加入 rq
trace_sched_wakeup_new(p); // 记录 wakeup 事件(用于性能分析)
wakeup_preempt(rq, p, wake_flags);//触发调度器检查新进程 p 是否应该抢占当前运行的任务
#ifdef CONFIG_SMP
if (p->sched_class->task_woken) {
rq_unpin_lock(rq, &rf); // 释放 `rq` 锁
p->sched_class->task_woken(rq, p); // 运行 `task_woken` 钩子
rq_repin_lock(rq, &rf); // 重新锁定 `rq`
}
#endif
task_rq_unlock(rq, p, &rf);
}
关于调度的部分,将在后续章节分析。【挖个坑】
3.线程的创建
在上一章节中,我们深入分析了 fork()
机制,探讨了 Linux 进程的创建过程,包括 task_struct
复制、以及 wake_up_new_task()
进程调度等关键步骤。进程创建虽然能提供独立的资源隔离,但进程间通信和资源共享的开销较大,因此,在多线程并发环境下,线程(Thread) 成为更高效的执行单元。
Linux 内核通过 clone()
系统调用实现线程创建,glibc 封装了 clone()
,并提供 pthread_create()
作为标准 API,供用户态程序使用。Linux 上的大多数用户态程序都依赖 glibc
来调用内核功能,它是用户程序和内核之间的桥梁。
在很多应用程序中,线程的创建由 pthread_create()
负责,而 glibc
通过 clone()
实现线程管理:
接下来深入glibc源码和linux内核源码分析线程的创建过程,首先是**__pthread_create_2_1方法,该方法调用create_thread**方法创建线程:
static int create_thread (struct pthread *pd, const struct pthread_attr *attr,
bool *stopped_start, void *stackaddr,
size_t stacksize, bool *thread_ran){
....
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);//构造 clone() 需要的 flags
TLS_DEFINE_INIT_TP (tp, pd);
struct clone_args args =
{
.flags = clone_flags,
.pidfd = (uintptr_t) &pd->tid,
.parent_tid = (uintptr_t) &pd->tid,
.child_tid = (uintptr_t) &pd->tid,
.stack = (uintptr_t) stackaddr,
.stack_size = stacksize,
.tls = (uintptr_t) tp,
};//组装 clone_args 结构
int ret = __clone_internal (&args, &start_thread, pd);//调用 __clone_internal() 进入 clone3() 系统调用,创建新线程。
if (__glibc_unlikely (ret == -1))
return errno;
}
这里调用了clone系统调用,clone和fork一样,最后都会执行kernel_clone方法,只是传入的参数不同。传入了CLONE_VM、CLONE_FS、CLONE_FILES等标志,这意味着,新线程与原线程或进程共用一套mm_struct、fs_struct、files_struct。
线程创建中的不同
与上述新建进程时每个新结构体都要新建不同,新建线程时每一个结构体都与创建它的进程或线程是共用一套的,这也就体现了进程和线程的本质区别:即进程有独立的虚拟地址空间,线程是与别的任务共用。最后创建结束后就是这样:
总结
在本系列博文中,我们从 Linux 内核源码与 glibc
线程库 的角度,深入剖析了 进程和线程的创建过程,并围绕fork()、clone() 的区别展开了详细讨论。
由上分析,进程的创建调用了fork系统调用,而线程的创建调用clone系统调用,在此也可以总结一下fork和clone的区别:
对比项 | fork() | clone() |
---|---|---|
资源共享 | 不共享,采用写时复制(COW) | 允许选择性共享资源(CLONE_* ) |
进程 ID | 新 pid ,不同于父进程 | CLONE_THREAD → 相同 tgid (线程) 未设置 CLONE_THREAD → 不同 pid (进程) |
地址空间 | 复制 mm_struct (COW) | CLONE_VM → 共享 mm_struct (线程) |
文件描述符 | 复制 files_struct | CLONE_FILES → 共享 files_struct |
信号处理 | 复制 sighand_struct | CLONE_SIGHAND → 共享 sighand_struct |
性能 | 最慢(COW 复制内存) | 比 fork() 快(可共享资源) |
适用场景 | 创建独立进程 | 创建线程 |
欢乐的时光总是短暂的,我们下期再见!