Linux 进程的一生(一):进程与线程的创建机制解析

news2025/3/13 16:13:53

在 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 结构体中,两个关键字段——pidtgid——用于表示进程或线程的身份。

然而,对于进程和线程而言,这两个字段的含义有所不同:

  • 对于进程pidtgid是相同的。即,一个普通的进程,它的 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选项。可选的值如下:

进程/线程创建相关标志

宏定义作用
CSIGNAL0x000000ff子进程退出时向父进程发送的信号掩码(通常是 SIGCHLD
CLONE_VM0x00000100共享内存地址空间(进程间不会复制 mm_struct,用于线程)
CLONE_FS0x00000200共享文件系统信息chdir/chroot 影响所有线程)
CLONE_FILES0x00000400共享文件描述符表(多个线程可使用相同 fd
CLONE_SIGHAND0x00000800共享信号处理机制(信号处理函数共享)
CLONE_PIDFD0x00001000创建 pidfd(进程文件描述符),可用于进程管理
CLONE_PTRACE0x00002000子进程继承父进程的 ptrace 追踪状态(调试相关)
CLONE_VFORK0x00004000使用 vfork() 语义(子进程执行 exec() 之前,父进程阻塞)
CLONE_PARENT0x00008000让子进程与父进程共享相同的父进程(更改 ppid
CLONE_THREAD0x00010000使子进程与父进程处于同一线程组tgid 相同,即 pthread_create() 线程)

Namespace(命名空间)隔离相关标志

宏定义作用
CLONE_NEWNS0x00020000新挂载命名空间(独立 mount/umount
CLONE_NEWCGROUP0x02000000cgroup 命名空间(独立的 cgroup 控制组)
CLONE_NEWUTS0x04000000新 UTS(主机名)命名空间(隔离 hostnamedomainname
CLONE_NEWIPC0x08000000新 IPC(进程间通信)命名空间System V 共享内存、消息队列、信号量独立)
CLONE_NEWUSER0x10000000新用户命名空间(允许非 root 用户拥有 root 权限)
CLONE_NEWPID0x20000000新进程 ID 命名空间(进程 PID 独立,适用于容器)
CLONE_NEWNET0x40000000新网络命名空间(独立 IP网络接口端口
CLONE_NEWTIME0x00000080新时间命名空间(独立 CLOCK_REALTIMECLOCK_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_*
进程 IDpid,不同于父进程CLONE_THREAD相同 tgid(线程) 未设置 CLONE_THREAD不同 pid(进程)
地址空间复制 mm_struct(COW)CLONE_VM共享 mm_struct(线程)
文件描述符复制 files_structCLONE_FILES共享 files_struct
信号处理复制 sighand_structCLONE_SIGHAND共享 sighand_struct
性能最慢(COW 复制内存)fork()(可共享资源)
适用场景创建独立进程创建线程

欢乐的时光总是短暂的,我们下期再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2314385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32之I2C硬件外设

注意:硬件I2C的引脚是固定的 SDA和SCL都是复用到外部引脚。 SDA发送时数据寄存器的数据在数据移位寄存器空闲的状态下进入数据移位寄存器,此时会置状态寄存器的TXE为1,表示发送寄存器为空,然后往数据控制寄存器中一位一位的移送数…

windows版本的时序数据库TDengine安装以及可视化工具

了解时序数据库TDengine,可以点击官方文档进行详细查阅 安装步骤 首先找到自己需要下载的版本,这边我暂时只写windows版本的安装 首先我们需要点开官网,找到发布历史,目前TDengine的windows版本只更新到3.0.7.1,我们…

【AI】单台10卡4090 openEuler服务器离线部署kasm workspace 提供简单的GPU云服务 虚拟化桌面

下载网址 Downloads | Kasm Workspaces 文件连接 wget https://kasm-static-content.s3.amazonaws.com/kasm_release_plugin_images_amd64_1.16.1.98d6fa.tar.gz wget https://kasm-static-content.s3.amazonaws.com/kasm_release_1.16.1.98d6fa.tar.gz wget https://kasm-st…

NetAssist 5.0.14网络助手基础使用及自动应答使用方案

以下是NetAssist v5.0.14自动应答功能的详细使用步骤: 一、基础准备: 工具下载网址页面:https://www.cmsoft.cn/resource/102.html 下载安装好后,根据需要可以创建多个server,双击程序图标运行即可,下面…

47.HarmonyOS NEXT 登录模块开发教程(二):一键登录页面实现

温馨提示:本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦! HarmonyOS NEXT 登录模块开发教程(二):一键登录页面实现 文章目录 HarmonyOS NEXT 登录模块开发教程&#xff0…

5.1 程序调试

版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的 本节中为了演示方便,使用的代码如下: 【例 5.1】【项目:code5-001】程序的调试。 static void Ma…

Cursor初体验:excel转成CANoe的vsysvar文件

今天公司大佬先锋们给培训了cursor的使用,还给注册了官方账号!跃跃欲试,但是测试任务好重,结合第三方工具开发也是没有头绪。 但巧的是,刚好下午有同事有个需求,想要把一个几千行的excel转成canoe的系统变…

vue3-element-admin 前后端本地启动联调

一、后端环境准备 1.1、下载地址 gitee 下载地址 1.2、环境要求 JDK 17 1.3、项目启动 克隆项目 git clone https://gitee.com/youlaiorg/youlai-boot.git数据库初始化 执行 youlai_boot.sql 脚本完成数据库创建、表结构和基础数据的初始化。 修改配置 application-dev.y…

《MySQL数据库从零搭建到高效管理|库的基本操作》

目录 一、数据库的操作 1.1 展示数据库 1.2 创建数据库 1.3 使用数据库 1.4 查看当前数据库 1.5 删除数据库 1.6 小结 二、常用数据类型 2.1 数值类型 2.2 字符串类型 2.3 日期类型 一、数据库的操作 打开MySQL命令行客户端,安装完MySQL后会有两个客户端…

mac使用Homebrew安装miniconda(mac搭建python环境),并在IDEA中集成miniconda环境

一、安装Homebrew mac安装brew 二、使用Homebrew安装miniconda brew search condabrew install miniconda安装完成后的截图: # 查看是否安装成功 brew list环境变量(无需手动配置) 先执行命令看能不能正常返回,如果不能正常…

Linux基础开发工具—vim

目录 1、vim的概念 2、vim的常见模式 2.1 演示切换vim模式 3、vim命令模式常用操作 3.1 移动光标 3.2 删除文字 3.3 复制 3.4 替换 4、vim底行模式常用命令 4.1 查找字符 5、vim的配置文件 1、vim的概念 Vim全称是Vi IMproved,即说明它是Vi编辑器的增强…

【C++】数据结构 队列的实现

本篇博客给大家带来的是用C语言来实现数据结构的队列的实现! 🐟🐟文章专栏:数据结构 🚀🚀若有问题评论区下讨论,我会及时回答 ❤❤欢迎大家点赞、收藏、分享! 今日思想:你…

macOS 终端优化

macOS 安装、优化、还原、升级 Oh My Zsh 完全指南 🚀 Oh My Zsh 是 macOS 终端增强的利器,它能提供强大的自动补全、主题定制和插件支持,让你的终端更高效、更炫酷。本文将全面介绍 如何安装、优化、还原、重新安装和升级 Oh My Zsh&#x…

大语言模型-1.2-大模型技术基础

简介 本博客内容是《大语言模型》一书的读书笔记,该书是中国人民大学高瓴人工智能学院赵鑫教授团队出品,覆盖大语言模型训练与使用的全流程,从预训练到微调与对齐,从使用技术到评测应用,帮助学员全面掌握大语言模型的…

Flutter 按钮组件 TextButton 详解

目录 1. 引言 2. TextButton 的基本用法 3. 主要属性 4. 自定义按钮样式 4.1 修改文本颜色 4.2 添加背景色 4.3 修改按钮形状和边距 4.4 样式定制 5. 高级应用技巧 5.1 图标文本组合 5.2 主题统一配置 5.3 动态交互 6. 性能优化与注意事项 6.1 点击区域优化 6.…

Qt 数据库操作(Sqlite)

数据库简介 关于数据库的基础知识这里就不做介绍了,相关博客可以查看: SQL基础知识 数据库学霸笔记 上面博客都写的比较详细,本文主要介绍如何使用Qt进行数据库相关操作,数据库分为关系型数据库和非关系型数据,关系…

vue 自行封装组件,类似于el-tree组件结构

背景: 接口返回是平面数组,需要经过分类处理,转成多维数组,以满足封装组件的数据结构。 有用到插件lodash。 import { groupBy, flattenDeep } from "lodash"; 效果展示: 处理数据: 对于接口返回…

信奥赛CSP-J复赛集训(模拟算法专题)(10):P2356 弹珠游戏

信奥赛CSP-J复赛集训(模拟算法专题)(10):P2356 弹珠游戏 题目背景 题目描述 MedalPluS 和他的小伙伴 NOIRP 发掘了一个骨灰级别的游戏——超级弹珠。 游戏的内容是:在一个 n n n \times n nn 的矩阵里,有若干个敌人,你的弹珠可以摧毁敌人,但只能攻击你所在的行、…

Windows 图形显示驱动开发-WDDM 3.2- WDDM 功能的内核模式测试

概述 在某些情况下,引入了基于 WDDM 或 MCDM 的新计算设备,并且这些设备的驱动程序不支持 D3D 运行时。 为了帮助验证此类驱动程序,将功能添加到 Dxgkrnl ,以便仅使用内核模式 thunk 进行验证;也就是说,无需涉及 D3D …

SpringBoot 入门--工程创建

IDEA创建SpringBoot项目 SpringBoot 3.1.5 需要java17 ① 创建Maven工程 ② 导入spring-boot-stater-web起步依赖 ③ 编写Controller ④ 提供启动类 1.打开IDEA,新建项目 2.点击下一步,并勾选web开发相关依赖 3.勾选完点击Create,然后配置…