【Linux内核设计与实现】第三章——进程管理02
文章目录
- 7. 进程创建
- 7.1. 进程之间的关系
- 7.2. 进程创建的写时拷贝机制(copy-on-write)
- 7.3. fork() 函数的入口
- 7.4. 创建新进程的核心函数 kernel_clone()
- 7.4.1. 检查参数并调用 copy_process 创建并复制进程
- 7.4.2. 获取新进程 PID 作为返回值
- 7.4.3. 唤醒新进程加入进程调度队列,以及收尾工作
- 7.5. kernel_clone() 的核心函数 copy_process()
- 7.5.1. 参数检查阶段
- 7.5.2. 信号处理
- 7.5.3. 分配并初始化 task_struct
- 7.5.4. 初始化新进程的标志位 task_struct->flag
- 7.5.5. 为复制父进程状态做相关准备工作
- 7.5.6. 初始化新进程调度相关信息
- 7.5.7. 复制父进程内容并分配 PID
- 7.5.8. 进一步初始化新进程
- 7.5.9. 初始化 cgroup 子系统相关、时间戳、父子关系等
- 7.5.10. 将新进程添加进内核任务列表并返回
- 7.5.11. 错误处理部分
- #附
- #05. init 进程(待补充...)
- #上一篇
- #下一篇
[注]:本篇文章与上一篇紧密相关,若未阅读上一篇请移步上一篇阅读,在文末可以找到上一篇链接。
7. 进程创建
7.1. 进程之间的关系
Unix
系统的进程之间存在一个明显的继承关系,在 Linux
系统中也是如此。所有的进程都是 PID
为 1
的 init
进程的后代#05(关于这一点笔者将会在文末附中 #05
对其说明和验证)。内核在系统启动的最后阶段启动 init
进程。该进程读取系统的初始化脚本(initscript
)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个 task_struct
都包含一个指向其父进程 tast_struct
、叫做 parent
的指针,还包含一个称为 children
的子进程链表。
Unix
的进程创建很特别。许多其他的操作系统都提供了产生(spawn
)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix
采用了与众不同的实现方式它把上述步骤分解到两个单独的函数中去执行:fork()
和 exec()
。首先,fork()
通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于 PID
(每个进程唯一)、PPID
(父进程的进程号,子进程将其设置为被拷贝进程的 PID
)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()
)函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。
[注]:这里的 exec() 所指的是 exec 一族函数。
7.2. 进程创建的写时拷贝机制(copy-on-write)
Linux
的 fork()
使用写时拷贝(copy-on-write
)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个内存空间。只有在需要写人的时候,数据才会被复制,从而使各个进程拥有各自的内存空间。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(举例来说 fork()
后立即调用 exec()
) 它们就无须复制了。
fork()
的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于 Unix
强调进程快速执行的能力,所以这个优化是很重要的。
在 Linux
内核中,fork()
是一个用于创建新进程的系统调用。相信大家在平时的练习中应该也使用过 fork()
函数来创建一个子进程,通过其返回值来判断是子进程还是父进程,0
为子进程,大于零的值就是父进程(有关 fork()
是如何让用户程序看起来是获取到两个不同的返回值这一现象,笔者将会在后文解释)。它的实现涉及多个子系统,包括进程管理、内存管理和调度器。而本文仅涉及进程管理的内容,因此并不算完全分析该函数,在后面的文章中笔者将会和大家一同更加深入的学习完全透彻的理解整个过创建进程的所有细节过程。
7.3. fork() 函数的入口
fork()
是通过系统调用号进入内核的,其定义如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMUstruct kernel_clone_args args = {.exit_signal = SIGCHLD,};return kernel_clone(&args);
#else/* can not support in nommu mode */return -EINVAL;
#endif
}
-
SYSCALL_DEFINE0
:- 宏
SYSCALL_DEFINE0
用于定义没有参数的系统调用。 - 它会展开为一个函数定义,并将系统调用注册到内核的系统调用表中。
- 宏
-
fork
的实现:- 如果启用了内存管理单元(
CONFIG_MMU
),fork
调用kernel_clone
函数。 kernel_clone
是实际创建新进程的核心函数。SIGCHLD
表示子进程退出时会向父进程发送SIGCHLD
信号。
- 如果启用了内存管理单元(
-
CONFIG_MMU
检查:- 如果系统不支持内存管理单元(如某些嵌入式系统),
fork
返回-EINVAL
,表示不支持。
- 如果系统不支持内存管理单元(如某些嵌入式系统),
这里提到了系统调用号的概念,我们知道所有的用户进程都是通过系统调用陷入内核态执行,而操作系统则是通过系统调用号来区分当前所调用的是哪个系统调用函数,x86_64
系统的系统调用号定义在 arch/x86/entry/syscalls/syscall_64.tbl
文件中,其中 fork()
函数的调用号是 57
。
// Linux Kernel 6.15.0-rc2
// PATH: arch/x86/entry/syscalls/syscall_64.tbl
57 common fork sys_fork
7.4. 创建新进程的核心函数 kernel_clone()
从 fork()
函数的定义可以看出该函数的核心内容就是 kernel_clone()
。kernel_clone
是 Linux 内核中用于实现 fork
、vfork
和 clone
系统调用的核心函数。它负责根据传入的参数创建一个新的进程或线程,并完成必要的初始化工作。kernel_clone()
的函数定义如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
pid_t kernel_clone(struct kernel_clone_args *args)
{u64 clone_flags = args->flags;struct completion vfork;struct pid *pid;struct task_struct *p;int trace = 0;pid_t nr;/** For legacy clone() calls, CLONE_PIDFD uses the parent_tid argument* to return the pidfd. Hence, CLONE_PIDFD and CLONE_PARENT_SETTID are* mutually exclusive. With clone3() CLONE_PIDFD has grown a separate* field in struct clone_args and it still doesn't make sense to have* them both point at the same memory location. Performing this check* here has the advantage that we don't need to have a separate helper* to check for legacy clone().*/if ((clone_flags & CLONE_PIDFD) &&(clone_flags & CLONE_PARENT_SETTID) &&(args->pidfd == args->parent_tid))return -EINVAL;/** Determine whether and which event to report to ptracer. When* called from kernel_thread or CLONE_UNTRACED is explicitly* requested, no event is reported; otherwise, report if the event* for the type of forking is enabled.*/if (!(clone_flags & CLONE_UNTRACED)) {if (clone_flags & CLONE_VFORK)trace = PTRACE_EVENT_VFORK;else if (args->exit_signal != SIGCHLD)trace = PTRACE_EVENT_CLONE;elsetrace = PTRACE_EVENT_FORK;if (likely(!ptrace_event_enabled(current, trace)))trace = 0;}p = copy_process(NULL, trace, NUMA_NO_NODE, args);add_latent_entropy();if (IS_ERR(p))return PTR_ERR(p);/** Do this prior waking up the new thread - the thread pointer* might get invalid after that point, if the thread exits quickly.*/trace_sched_process_fork(current, p);pid = get_task_pid(p, PIDTYPE_PID);nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, args->parent_tid);if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);get_task_struct(p);}if (IS_ENABLED(CONFIG_LRU_GEN_WALKS_MMU) && !(clone_flags & CLONE_VM)) {/* lock the task to synchronize with memcg migration */task_lock(p);lru_gen_add_mm(p->mm);task_unlock(p);}wake_up_new_task(p);/* forking complete and child started to run, tell ptracer */if (unlikely(trace))ptrace_event_pid(trace, pid);if (clone_flags & CLONE_VFORK) {if (!wait_for_vfork_done(p, &vfork))ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);}put_pid(pid);return nr;
}
-
参数:
args
:包含创建新进程所需的参数,类型为struct kernel_clone_args
。flags
:控制新进程的行为(如是否共享内存、文件描述符等)。exit_signal
:子进程退出时发送给父进程的信号。- 其他字段如
stack
、tls
、pidfd
等,用于指定新进程的栈、线程本地存储等。
-
返回值:
- 成功时返回新进程的
PID
。 - 失败时返回负的错误码。
- 成功时返回新进程的
kernel_clone 函数的主要逻辑如下:
7.4.1. 检查参数并调用 copy_process 创建并复制进程
函数开始先对标志位进行检查,检查 CLONE_PIDFD
和 CLONE_PARENT_SETTID
是否同时设置,并确保它们没有指向相同的内存位置。如果条件不满足,返回 -EINVAL
。如果未设置 CLONE_UNTRACED
,根据 flags
确定是否需要报告 ptrace
事件。可能的事件包括 PTRACE_EVENT_FORK
、PTRACE_EVENT_CLONE
和 PTRACE_EVENT_VFORK
。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();if (IS_ERR(p))return PTR_ERR(p);
接下来调用核心函数 copy_process()
创建新进程,调用 copy_process
函数复制当前进程,创建一个新的 task_struct
。如果 copy_process
失败,返回错误码。
7.4.2. 获取新进程 PID 作为返回值
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, args->parent_tid);
get_task_pid
的作用是为新创建的任务/进程(task_struct
)获取其对应的 pid
结构体。此处的 pid
并非一个确切的数值,而是一个用来描述 pid
的结构体,该类型被定义在 include/linux/pid.h
。get_task_pid
函数的主要作用如下:
-
获取任务的
pid
结构体:- 每个任务(进程或线程)在内核中都有一个唯一的
pid
结构体,用于表示其在特定PID
命名空间中的标识符。 get_task_pid
会根据任务的类型(如PIDTYPE_PID
、PIDTYPE_TGID
等)返回对应的pid
结构体。
- 每个任务(进程或线程)在内核中都有一个唯一的
-
增加
pid
的引用计数:get_task_pid
会增加pid
的引用计数,确保在使用期间不会被释放。
-
pid_vnr(pid)
:- 将
pid
结构体转换为用户态可见的整数PID
值。
- 将
以下是 get_task_pid
的实现:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/pid.c
struct pid *get_task_pid(struct task_struct *task, enum pid_type type)
{struct pid *pid;rcu_read_lock();pid = get_pid(rcu_dereference(*task_pid_ptr(task, type))); // 增加引用计数rcu_read_unlock();return pid;
}
get_task_pid
为新创建的任务获取其对应的 pid
结构体,并增加引用计数。随后,pid_vnr
将其转换为用户态可见的整数 PID
值,用于返回给调用者。(记住此处的 nr
,这就是新创建的子进程的 pid
,笔者这里如此强调是为了下文解释 fork()
函数是如何实现对用户空间返回两个不同值做铺垫。)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);get_task_struct(p);
}
此处为了处理 vfork
,如果设置了 CLONE_VFORK
,初始化 vfork_done
,用于同步父子进程。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
wake_up_new_task(p);
7.4.3. 唤醒新进程加入进程调度队列,以及收尾工作
调用 wake_up_new_task
将新进程加入调度队列,使其可以被调度运行。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
if (unlikely(trace))ptrace_event_pid(trace, pid);
如果设置了 ptrace
事件,通知调试器。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
if (clone_flags & CLONE_VFORK) {if (!wait_for_vfork_done(p, &vfork))ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
如果设置了 CLONE_VFORK
,父进程等待子进程完成。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: kernel_clone()
put_pid(pid);
return nr;
清理和返回,释放 PID
的引用计数。返回新进程的 PID
。到此一个新进程也就创建完成。
7.5. kernel_clone() 的核心函数 copy_process()
在 kernel_clone()
函数中一个核心的函数就是 copy_process()
,该函数它负责根据传入的参数创建一个新的 task_struct
,并初始化新进程的各种资源(如内存、文件描述符、信号处理等)。该函数签名如下:
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
__latent_entropy struct task_struct *copy_process(struct pid *pid,int trace,int node,struct kernel_clone_args *args)
{int pidfd = -1, retval;struct task_struct *p;struct multiprocess_signals delayed;struct file *pidfile = NULL;const u64 clone_flags = args->flags;struct nsproxy *nsp = current->nsproxy;/** Don't allow sharing the root directory with processes in a different* namespace*/if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))return ERR_PTR(-EINVAL);if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))return ERR_PTR(-EINVAL);/** Thread groups must share signals as well, and detached threads* can only be started up within the thread group.*/if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))return ERR_PTR(-EINVAL);/** Shared signal handlers imply shared VM. By way of the above,* thread groups also imply shared VM. Blocking this case allows* for various simplifications in other code.*/if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))return ERR_PTR(-EINVAL);/** Siblings of global init remain as zombies on exit since they are* not reaped by their parent (swapper). To solve this and to avoid* multi-rooted process trees, prevent global and container-inits* from creating siblings.*/if ((clone_flags & CLONE_PARENT) &¤t->signal->flags & SIGNAL_UNKILLABLE)return ERR_PTR(-EINVAL);/** If the new process will be in a different pid or user namespace* do not allow it to share a thread group with the forking task.*/if (clone_flags & CLONE_THREAD) {if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||(task_active_pid_ns(current) != nsp->pid_ns_for_children))return ERR_PTR(-EINVAL);}if (clone_flags & CLONE_PIDFD) {/** - CLONE_DETACHED is blocked so that we can potentially* reuse it later for CLONE_PIDFD.*/if (clone_flags & CLONE_DETACHED)return ERR_PTR(-EINVAL);}/** Force any signals received before this point to be delivered* before the fork happens. Collect up signals sent to multiple* processes that happen during the fork and delay them so that* they appear to happen after the fork.*/sigemptyset(&delayed.signal);INIT_HLIST_NODE(&delayed.node);spin_lock_irq(¤t->sighand->siglock);if (!(clone_flags & CLONE_THREAD))hlist_add_head(&delayed.node, ¤t->signal->multiprocess);recalc_sigpending();spin_unlock_irq(¤t->sighand->siglock);retval = -ERESTARTNOINTR;if (task_sigpending(current))goto fork_out;retval = -ENOMEM;p = dup_task_struct(current, node);if (!p)goto fork_out;p->flags &= ~PF_KTHREAD;if (args->kthread)p->flags |= PF_KTHREAD;if (args->user_worker) {/** Mark us a user worker, and block any signal that isn't* fatal or STOP*/p->flags |= PF_USER_WORKER;siginitsetinv(&p->blocked, sigmask(SIGKILL)|sigmask(SIGSTOP));}if (args->io_thread)p->flags |= PF_IO_WORKER;if (args->name)strscpy_pad(p->comm, args->name, sizeof(p->comm));p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? args->child_tid : NULL;/** Clear TID on mm_release()?*/p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? args->child_tid : NULL;ftrace_graph_init_task(p);rt_mutex_init_task(p);lockdep_assert_irqs_enabled();
#ifdef CONFIG_PROVE_LOCKINGDEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endifretval = copy_creds(p, clone_flags);if (retval < 0)goto bad_fork_free;retval = -EAGAIN;if (is_rlimit_overlimit(task_ucounts(p), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {if (p->real_cred->user != INIT_USER &&!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))goto bad_fork_cleanup_count;}current->flags &= ~PF_NPROC_EXCEEDED;/** If multiple threads are within copy_process(), then this check* triggers too late. This doesn't hurt, the check is only there* to stop root fork bombs.*/retval = -EAGAIN;if (data_race(nr_threads >= max_threads))goto bad_fork_cleanup_count;delayacct_tsk_init(p); /* Must remain after dup_task_struct() */p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER | PF_IDLE | PF_NO_SETAFFINITY);p->flags |= PF_FORKNOEXEC;INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);rcu_copy_process(p);p->vfork_done = NULL;spin_lock_init(&p->alloc_lock);init_sigpending(&p->pending);p->utime = p->stime = p->gtime = 0;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIMEp->utimescaled = p->stimescaled = 0;
#endifprev_cputime_init(&p->prev_cputime);#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GENseqcount_init(&p->vtime.seqcount);p->vtime.starttime = 0;p->vtime.state = VTIME_INACTIVE;
#endif#ifdef CONFIG_IO_URINGp->io_uring = NULL;
#endifp->default_timer_slack_ns = current->timer_slack_ns;#ifdef CONFIG_PSIp->psi_flags = 0;
#endiftask_io_accounting_init(&p->ioac);acct_clear_integrals(p);posix_cputimers_init(&p->posix_cputimers);tick_dep_init_task(p);p->io_context = NULL;audit_set_context(p, NULL);cgroup_fork(p);if (args->kthread) {if (!set_kthread_struct(p))goto bad_fork_cleanup_delayacct;}
#ifdef CONFIG_NUMAp->mempolicy = mpol_dup(p->mempolicy);if (IS_ERR(p->mempolicy)) {retval = PTR_ERR(p->mempolicy);p->mempolicy = NULL;goto bad_fork_cleanup_delayacct;}
#endif
#ifdef CONFIG_CPUSETSp->cpuset_mem_spread_rotor = NUMA_NO_NODE;seqcount_spinlock_init(&p->mems_allowed_seq, &p->alloc_lock);
#endif
#ifdef CONFIG_TRACE_IRQFLAGSmemset(&p->irqtrace, 0, sizeof(p->irqtrace));p->irqtrace.hardirq_disable_ip = _THIS_IP_;p->irqtrace.softirq_enable_ip = _THIS_IP_;p->softirqs_enabled = 1;p->softirq_context = 0;
#endifp->pagefault_disabled = 0;#ifdef CONFIG_LOCKDEPlockdep_init_task(p);
#endif#ifdef CONFIG_DEBUG_MUTEXESp->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHEp->sequential_io = 0;p->sequential_io_avg = 0;
#endif
#ifdef CONFIG_BPF_SYSCALLRCU_INIT_POINTER(p->bpf_storage, NULL);p->bpf_ctx = NULL;
#endif/* Perform scheduler related setup. Assign this task to a CPU. */retval = sched_fork(clone_flags, p);if (retval)goto bad_fork_cleanup_policy;retval = perf_event_init_task(p, clone_flags);if (retval)goto bad_fork_sched_cancel_fork;retval = audit_alloc(p);if (retval)goto bad_fork_cleanup_perf;/* copy all the process information */shm_init_task(p);retval = security_task_alloc(p, clone_flags);if (retval)goto bad_fork_cleanup_audit;retval = copy_semundo(clone_flags, p);if (retval)goto bad_fork_cleanup_security;retval = copy_files(clone_flags, p, args->no_files);if (retval)goto bad_fork_cleanup_semundo;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;retval = copy_mm(clone_flags, p);if (retval)goto bad_fork_cleanup_signal;retval = copy_namespaces(clone_flags, p);if (retval)goto bad_fork_cleanup_mm;retval = copy_io(clone_flags, p);if (retval)goto bad_fork_cleanup_namespaces;retval = copy_thread(p, args);if (retval)goto bad_fork_cleanup_io;stackleak_task_init(p);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)) {retval = PTR_ERR(pid);goto bad_fork_cleanup_thread;}}/** This has to happen after we've potentially unshared the file* descriptor table (so that the pidfd doesn't leak into the child* if the fd table isn't shared).*/if (clone_flags & CLONE_PIDFD) {int flags = (clone_flags & CLONE_THREAD) ? PIDFD_THREAD : 0;/** Note that no task has been attached to @pid yet indicate* that via CLONE_PIDFD.*/retval = __pidfd_prepare(pid, flags | PIDFD_CLONE, &pidfile);if (retval < 0)goto bad_fork_free_pid;pidfd = retval;retval = put_user(pidfd, args->pidfd);if (retval)goto bad_fork_put_pidfd;}#ifdef CONFIG_BLOCKp->plug = NULL;
#endiffutex_init_task(p);/** sigaltstack should be cleared when sharing the same VM*/if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)sas_ss_reset(p);/** Syscall tracing and stepping should be turned off in the* child regardless of CLONE_PTRACE.*/user_disable_single_step(p);clear_task_syscall_work(p, SYSCALL_TRACE);
#if defined(CONFIG_GENERIC_ENTRY) || defined(TIF_SYSCALL_EMU)clear_task_syscall_work(p, SYSCALL_EMU);
#endifclear_tsk_latency_tracing(p);/* ok, now we should be set up.. */p->pid = pid_nr(pid);if (clone_flags & CLONE_THREAD) {p->group_leader = current->group_leader;p->tgid = current->tgid;} else {p->group_leader = p;p->tgid = p->pid;}p->nr_dirtied = 0;p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);p->dirty_paused_when = 0;p->pdeath_signal = 0;p->task_works = NULL;clear_posix_cputimers_work(p);#ifdef CONFIG_KRETPROBESp->kretprobe_instances.first = NULL;
#endif
#ifdef CONFIG_RETHOOKp->rethooks.first = NULL;
#endif/** Ensure that the cgroup subsystem policies allow the new process to be* forked. It should be noted that the new process's css_set can be changed* between here and cgroup_post_fork() if an organisation operation is in* progress.*/retval = cgroup_can_fork(p, args);if (retval)goto bad_fork_put_pidfd;/** Now that the cgroups are pinned, re-clone the parent cgroup and put* the new task on the correct runqueue. All this *before* the task* becomes visible.** This isn't part of ->can_fork() because while the re-cloning is* cgroup specific, it unconditionally needs to place the task on a* runqueue.*/retval = sched_cgroup_fork(p, args);if (retval)goto bad_fork_cancel_cgroup;/** From this point on we must avoid any synchronous user-space* communication until we take the tasklist-lock. In particular, we do* not want user-space to be able to predict the process start-time by* stalling fork(2) after we recorded the start_time but before it is* visible to the system.*/p->start_time = ktime_get_ns();p->start_boottime = ktime_get_boottime_ns();/** Make it visible to the rest of the system, but dont wake it up yet.* Need tasklist lock for parent etc handling!*/write_lock_irq(&tasklist_lock);/* CLONE_PARENT re-uses the old parent */if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {p->real_parent = current->real_parent;p->parent_exec_id = current->parent_exec_id;if (clone_flags & CLONE_THREAD)p->exit_signal = -1;elsep->exit_signal = current->group_leader->exit_signal;} else {p->real_parent = current;p->parent_exec_id = current->self_exec_id;p->exit_signal = args->exit_signal;}klp_copy_process(p);sched_core_fork(p);spin_lock(¤t->sighand->siglock);rv_task_fork(p);rseq_fork(p, clone_flags);/* Don't start children in a dying pid namespace */if (unlikely(!(ns_of_pid(pid)->pid_allocated & PIDNS_ADDING))) {retval = -ENOMEM;goto bad_fork_core_free;}/* Let kill terminate clone/fork in the middle */if (fatal_signal_pending(current)) {retval = -EINTR;goto bad_fork_core_free;}/* No more failure paths after this point. *//** Copy seccomp details explicitly here, in case they were changed* before holding sighand lock.*/copy_seccomp(p);init_task_pid_links(p);if (likely(p->pid)) {ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);init_task_pid(p, PIDTYPE_PID, pid);if (thread_group_leader(p)) {init_task_pid(p, PIDTYPE_TGID, pid);init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));init_task_pid(p, PIDTYPE_SID, task_session(current));if (is_child_reaper(pid)) {ns_of_pid(pid)->child_reaper = p;p->signal->flags |= SIGNAL_UNKILLABLE;}p->signal->shared_pending.signal = delayed.signal;p->signal->tty = tty_kref_get(current->signal->tty);/** Inherit has_child_subreaper flag under the same* tasklist_lock with adding child to the process tree* for propagate_has_child_subreaper optimization.*/p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||p->real_parent->signal->is_child_subreaper;list_add_tail(&p->sibling, &p->real_parent->children);list_add_tail_rcu(&p->tasks, &init_task.tasks);attach_pid(p, PIDTYPE_TGID);attach_pid(p, PIDTYPE_PGID);attach_pid(p, PIDTYPE_SID);__this_cpu_inc(process_counts);} else {current->signal->nr_threads++;current->signal->quick_threads++;atomic_inc(¤t->signal->live);refcount_inc(¤t->signal->sigcnt);task_join_group_stop(p);list_add_tail_rcu(&p->thread_node,&p->signal->thread_head);}attach_pid(p, PIDTYPE_PID);nr_threads++;}total_forks++;hlist_del_init(&delayed.node);spin_unlock(¤t->sighand->siglock);syscall_tracepoint_update(p);write_unlock_irq(&tasklist_lock);if (pidfile)fd_install(pidfd, pidfile);proc_fork_connector(p);sched_post_fork(p);cgroup_post_fork(p, args);perf_event_fork(p);trace_task_newtask(p, clone_flags);uprobe_copy_process(p, clone_flags);user_events_fork(p, clone_flags);copy_oom_score_adj(clone_flags, p);return p;bad_fork_core_free:sched_core_free(p);spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);
bad_fork_cancel_cgroup:cgroup_cancel_fork(p, args);
bad_fork_put_pidfd:if (clone_flags & CLONE_PIDFD) {fput(pidfile);put_unused_fd(pidfd);}
bad_fork_free_pid:if (pid != &init_struct_pid)free_pid(pid);
bad_fork_cleanup_thread:exit_thread(p);
bad_fork_cleanup_io:if (p->io_context)exit_io_context(p);
bad_fork_cleanup_namespaces:exit_task_namespaces(p);
bad_fork_cleanup_mm:if (p->mm) {mm_clear_owner(p->mm, p);mmput(p->mm);}
bad_fork_cleanup_signal:if (!(clone_flags & CLONE_THREAD))free_signal_struct(p->signal);
bad_fork_cleanup_sighand:__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:exit_fs(p); /* blocking */
bad_fork_cleanup_files:exit_files(p); /* blocking */
bad_fork_cleanup_semundo:exit_sem(p);
bad_fork_cleanup_security:security_task_free(p);
bad_fork_cleanup_audit:audit_free(p);
bad_fork_cleanup_perf:perf_event_free_task(p);
bad_fork_sched_cancel_fork:sched_cancel_fork(p);
bad_fork_cleanup_policy:lockdep_free_task(p);
#ifdef CONFIG_NUMAmpol_put(p->mempolicy);
#endif
bad_fork_cleanup_delayacct:delayacct_tsk_free(p);
bad_fork_cleanup_count:dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);exit_creds(p);
bad_fork_free:WRITE_ONCE(p->__state, TASK_DEAD);exit_task_stack_account(p);put_task_stack(p);delayed_free_task(p);
fork_out:spin_lock_irq(¤t->sighand->siglock);hlist_del_init(&delayed.node);spin_unlock_irq(¤t->sighand->siglock);return ERR_PTR(retval);
}
-
参数:
pid
:新进程的PID
(如果为NULL
,将在函数中分配)。trace
:是否需要跟踪(ptrace
)新进程。node
:NUMA
节点,用于分配内存。args
:包含创建新进程所需的参数(如flags
、stack
、tls
等)。
-
返回值:
- 成功时返回新创建的
task_struct
。 - 失败时返回错误指针(
ERR_PTR
)。
- 成功时返回新创建的
7.5.1. 参数检查阶段
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))return ERR_PTR(-EINVAL);if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))return ERR_PTR(-EINVAL);if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))return ERR_PTR(-EINVAL);if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))return ERR_PTR(-EINVAL);if ((clone_flags & CLONE_PARENT) &¤t->signal->flags & SIGNAL_UNKILLABLE)return ERR_PTR(-EINVAL);
这段内容对 clone_flags
所传递的标志组合进行合法性检查。
- CLONE_NEWNS 和 CLONE_FS: 检查是否允许共享根目录 ,如果设置了
CLONE_NEWNS
,则不能同时设置CLONE_FS
,因为CLONE_NEWNS
会创建一个新的挂载命名空间,而CLONE_FS
要求共享文件系统上下文,这两者是冲突的。; - CLONE_NEWUSER 和 CLONE_FS: 检查是否允许共享用户命名空间,同上,两者不可同时被设置;
- CLONE_THREAD 和 CLONE_SIGHAND: 检查线程组的信号和内存共享标志是否一致。如果设置了
CLONE_THREAD
标志(表示创建线程),但没有设置CLONE_SIGHAND
标志(表示共享信号处理表),则返回错误.线程组中的所有线程必须共享信号处理表(sighand_struct
),这是线程组的基本要求。
如果不共享信号处理表,线程之间无法正确处理信号,这会导致线程组行为异常; - CLONE_SIGHAND 和 CLONE_VM: 共享信号处理表的线程组必须共享内存地址空间(
mm_struct
),这是线程组的另一个基本要求。
如果线程组不共享内存地址空间,线程之间无法正确共享信号队列和其他资源,这会导致线程组行为不一致。设置CLONE_SIGHAND
必须同时设置CLONE_VM
,否则返回-EINVAL
。
通过这几个参数你应该发现了什么,没错,该函数不仅是用来创建进程的,同时也是拿来创建线程的,对于 Linux
内核而言事实上对进程与线程是一视同仁的(同类东西),通过后文的继续学习将会了解到这一点。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()if ((clone_flags & CLONE_PARENT) &¤t->signal->flags & SIGNAL_UNKILLABLE)return ERR_PTR(-EINVAL);if (clone_flags & CLONE_THREAD) {if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||(task_active_pid_ns(current) != nsp->pid_ns_for_children))return ERR_PTR(-EINVAL);}if (clone_flags & CLONE_PIDFD) {/** - CLONE_DETACHED is blocked so that we can potentially* reuse it later for CLONE_PIDFD.*/if (clone_flags & CLONE_DETACHED)return ERR_PTR(-EINVAL);}
- CLONE_PARENT 和 SIGNAL_UNKILLABLE:设置
CLONE_PARENT
标志表示新进程与当前进程共享父进程,SIGNAL_UNKILLABLE
表示进程不能被杀死,通常用于标记全局init
进程或容器的init
进程。如果设置了CLONE_PARENT
标志,并且当前进程的信号标志中包含SIGNAL_UNKILLABLE
,则返回错误;(如果当前进程是全局或容器的init
进程,则禁止使用CLONE_PARENT
创建兄弟进程。全局或容器的init
进程不能创建兄弟进程,因为这些兄弟进程在退出时会变成僵尸进程,无法被init
进程回收,导致多根进程树的问题) - CLONE_THREAD 和 CLONE_NEWUSER、CLONE_NEWPID: 设置
CLONE_THREAD
时,不能同时设置CLONE_NEWUSER
或CLONE_NEWPID
,且必须确保线程组的命名空间一致。这是因为线程组中的所有线程必须共享相同的用户和PID
命名空间。
如果创建线程时尝试切换到新的用户或PID
命名空间,会破坏线程组的基本约束; - CLONE_DETACHED 和 CLONE_PIDFD:设置
CLONE_PIDFD
时,不能同时设置CLONE_DETACHED
。CLONE_PIDFD
表示为新进程创建一个PID
文件描述符,CLONE_DETACHED
是一个废弃的标志,曾用于创建与父进程分离的子进程。为了避免冲突并保留CLONE_DETACHED
的潜在用途,内核禁止它与CLONE_PIDFD
同时使用。
总的来说就是,全局或容器的 init
进程不能创建兄弟进程(CLONE_PARENT
)。
创建线程(CLONE_THREAD
)时,不能切换用户或 PID
命名空间。
创建 PID
文件描述符(CLONE_PIDFD
)时,不能使用废弃的 CLONE_DETACHED
标志。
7.5.2. 信号处理
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()// 初始化 delayed.signal,将其设置为空信号集。保存拷贝进程操作期间需要延迟处理的信号。sigemptyset(&delayed.signal);// 初始化 delayed.node,这是一个哈希链表节点,用于将延迟的信号添加到当前进程的信号链表中。INIT_HLIST_NODE(&delayed.node);spin_lock_irq(¤t->sighand->siglock);if (!(clone_flags & CLONE_THREAD))hlist_add_head(&delayed.node, ¤t->signal->multiprocess);recalc_sigpending();spin_unlock_irq(¤t->sighand->siglock);retval = -ERESTARTNOINTR;if (task_sigpending(current))goto fork_out;
-
sigemptyset
:- 初始化
delayed.signal
,将其设置为空信号集; - 这是为了存储在拷贝进程操作期间需要延迟处理的信号。
- 初始化
-
INIT_HLIST_NODE
:- 初始化
delayed.node
,这是一个哈希链表节点,用于将延迟的信号添加到当前进程的信号链表中。
- 初始化
-
spin_lock_irq
:- 获取当前进程的信号锁(
siglock
),以保护信号相关的数据结构免受并发修改。
- 获取当前进程的信号锁(
-
if (!(clone_flags & CLONE_THREAD))
:- 检查是否设置了
CLONE_THREAD
标志:- 如果未设置,表示这是一个新进程,而不是线程。
- 将
delayed.node
添加到当前进程的multiprocess
信号链表中,用于延迟处理多进程信号。
- 检查是否设置了
-
hlist_add_head
:- 将
delayed.node
添加到current->signal->multiprocess
链表的头部。
- 将
-
recalc_sigpending
:- 重新计算当前进程的挂起信号状态。
- 如果有挂起的信号需要处理,会更新相关标志。
-
spin_unlock_irq
:- 释放信号锁,允许其他线程或进程访问信号相关的数据结构。
-
task_sigpending
:- 检查当前进程是否有挂起的信号需要处理。
- 如果有挂起信号,返回非零值。
-
goto fork_out
:- 如果有挂起信号,跳转到
fork_out
标签,终止当前拷贝操作,并返回错误代码-ERESTARTNOINTR
。 - 这通常表示系统调用需要被重新启动。
- 如果有挂起信号,跳转到
7.5.3. 分配并初始化 task_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()retval = -ENOMEM;p = dup_task_struct(current, node);if (!p)goto fork_out;
-
错误码
-ENOMEM
:- 表示内存不足(
Out of Memory
),这是内核中常见的错误码,用于指示内存分配失败。如果后续的dup_task_struct
调用失败,函数会跳转到fork_out
标签,并返回这个错误值。
- 表示内存不足(
-
调用
dup_task_struct
:- 复制当前进程(
current
)的task_struct
,并为新进程分配一个新的task_struct
。 task_struct
是内核中描述进程的核心数据结构(进程描述符),包含进程的状态、调度信息、内存管理信息等。-
参数说明:
current
:当前进程的task_struct
,作为复制的模板。node
:NUMA
节点,用于指定在哪个内存节点上分配新进程的task_struct
。
-
返回值:
- 如果成功,返回新分配的
task_struct
的指针。 - 如果失败,返回
NULL
。
- 如果成功,返回新分配的
-
- 复制当前进程(
7.5.4. 初始化新进程的标志位 task_struct->flag
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()// 清除 PF_KTHREAD 标志,假设新进程是普通用户进程。p->flags &= ~PF_KTHREAD;// 如果 args->kthread 为真if (args->kthread)// 设置 PF_KTHREAD 标志,表明新进程是一个内核线程。p->flags |= PF_KTHREAD;// 判断传入参数是否设置一个用户工作线程if (args->user_worker) {/** Mark us a user worker, and block any signal that isn't* fatal or STOP*/// 设置新task->flag标记为一个用户工作线程(User Worker Thread)。p->flags |= PF_USER_WORKER;// 屏蔽所有信号,除了 SIGKILL 和 SIGSTOPsiginitsetinv(&p->blocked, sigmask(SIGKILL)|sigmask(SIGSTOP));}if (args->io_thread)// 标记新进程falg是一个 IO 工作线程(IO Worker Thread)p->flags |= PF_IO_WORKER;if (args->name)// 根据参数设置新进程的名称,通常用于调试和监控。strscpy_pad(p->comm, args->name, sizeof(p->comm));/** 如果 args->name 不为空,则将其复制到 p->comm 中。* 使用 strscpy_pad 确保字符串被安全地复制,并填充到固定长度。* 进程名称的作用,例如,在 ps 命令中可以看到进程的名称。*/// 如果设置了 CLONE_CHILD_SETTID,则初始化 p->set_child_tid。p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? args->child_tid : NULL;/** Clear TID on mm_release()?*/// 如果设置了 CLONE_CHILD_CLEARTID,则初始化 p->clear_child_tid。p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? args->child_tid : NULL;
[注]:笔者将代码功能写入代码注释中,说明这里并非我们理解函数功能的主要代码段,根据注释简单了解此处的作用即可。下文此类操作同理。
这段代码主要是根据传入的参数设置新进程的标志位初始化进程的标记字段。并且设置线程 ID
相关字段(set_child_tid
和 clear_child_tid
)。这些设置确保新进程的行为符合调用者的要求,并为后续的调度和信号处理做好准备。
7.5.5. 为复制父进程状态做相关准备工作
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()// 初始化新进程的函数调用跟踪(Function Graph Tracing)相关数据。ftrace_graph_init_task(p);/** ftrace 是 Linux 内核中的一个跟踪框架,用于记录函数调用的执行路径和性能数据。* 该函数为新进程的 task_struct 设置跟踪相关的初始状态。* 如果启用了 ftrace 功能,内核可以跟踪函数调用的执行路径,帮助我们调试和分析性能。* 每个进程都需要独立的跟踪状态,因此在创建新进程时需要初始化。*/// 初始化新进程的实时互斥锁(RT Mutex)相关数据。rt_mutex_init_task(p);/** 主要用于支持实时调度(Real-Time Scheduling)和优先级继承(Priority Inheritance)。* 实时互斥锁是 Linux 内核中用于解决优先级反转问题的一种机制。* 在创建新进程时,需要初始化这些字段以支持实时调度。* 函数大致工作流程:* 初始化新进程的 pi_lock(优先级继承锁)。* 如果启用了 CONFIG_RT_MUTEXES 配置,还会初始化其他与实时互斥锁相关的字段,例如:* pi_waiters:等待该任务的实时互斥锁队列;* pi_top_task:当前阻塞的最高优先级任务;* pi_blocked_on:当前任务被哪个锁阻塞。*/// 确保在执行到此处时,中断(IRQs)是启用状态,如果中断被禁用,会触发警告或错误。// 内核中的许多操作需要在中断启用的情况下执行,以确保系统的正常运行。lockdep_assert_irqs_enabled();
#ifdef CONFIG_PROVE_LOCKING// 如果启用了 CONFIG_PROVE_LOCKING 配置,则检查新进程的 softirqs_enabled 状态。如果软中断(Soft IRQs)未启用,会触发警告。DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endifretval = copy_creds(p, clone_flags);if (retval < 0)goto bad_fork_free;/** is_rlimit_overlimit:* 检查当前用户是否超过了 RLIMIT_NPROC(最大进程数限制)。* 如果超过限制且用户不是 INIT_USER,并且没有 CAP_SYS_RESOURCE 或 CAP_SYS_ADMIN 权限,则返回错误。* PF_NPROC_EXCEEDED:* 如果进程数限制被触发,当前进程的标志位 PF_NPROC_EXCEEDED 会被设置。* 在这里清除该标志位,表示限制已被处理。*/retval = -EAGAIN;if (is_rlimit_overlimit(task_ucounts(p), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {if (p->real_cred->user != INIT_USER &&!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))goto bad_fork_cleanup_count;}current->flags &= ~PF_NPROC_EXCEEDED;/** If multiple threads are within copy_process(), then this check* triggers too late. This doesn't hurt, the check is only there* to stop root fork bombs.*//** nr_threads:* 当前系统中的线程总数。如果线程数超过了 max_threads(系统允许的最大线程数),返回错误。* 这是为了防止线程爆炸(例如 fork 炸弹)导致系统资源耗尽。*/retval = -EAGAIN;if (data_race(nr_threads >= max_threads))goto bad_fork_cleanup_count;// 初始化延迟统计(Delay Accounting)相关数据,用于记录进程的调度延迟、IO 延迟等。delayacct_tsk_init(p); /* Must remain after dup_task_struct() *//** 清除标志位:* PF_SUPERPRIV:表示进程具有超级用户权限。* PF_WQ_WORKER:表示进程是工作队列的工作线程。* PF_IDLE:表示进程是空闲线程。* PF_NO_SETAFFINITY:表示进程不能设置 CPU 亲和性。*/p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER | PF_IDLE | PF_NO_SETAFFINITY);// 设置标志位 PF_FORKNOEXEC:表示新进程尚未执行任何程序(exec)。p->flags |= PF_FORKNOEXEC;// 初始化子进程链表,用于记录当前进程的子进程。INIT_LIST_HEAD(&p->children);// 初始化兄弟进程链表,用于记录当前进程在父进程的子进程链表中的位置。INIT_LIST_HEAD(&p->sibling);// 初始化与 RCU(Read-Copy-Update)相关的数据结构。RCU 是一种高效的读写同步机制,用于内核中的并发操作。rcu_copy_process(p);// 用于 vfork 系统调用的同步机制,初始化为 NULL。p->vfork_done = NULL;// 初始化分配锁,用于保护进程的内存分配操作。spin_lock_init(&p->alloc_lock);// 初始化挂起信号队列,用于存储未处理的信号。init_sigpending(&p->pending);// 下面用于初始化 CPU 时间统计// 初始化用户时间、系统时间和全局时间统计。(utime(user time)、stime(system time)、gtime(global time))p->utime = p->stime = p->gtime = 0;
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIMEp->utimescaled = p->stimescaled = 0;
#endif// 初始化前一次 CPU 时间统计。prev_cputime_init(&p->prev_cputime);#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GENseqcount_init(&p->vtime.seqcount);p->vtime.starttime = 0;p->vtime.state = VTIME_INACTIVE;
#endif#ifdef CONFIG_IO_URINGp->io_uring = NULL;
#endif// 继承父进程的定时器松弛值。(current 获取当前进程 task_struct 的宏,即新进程的父进程)p->default_timer_slack_ns = current->timer_slack_ns;#ifdef CONFIG_PSIp->psi_flags = 0;
#endif// 初始化 IO 统计数据。task_io_accounting_init(&p->ioac);// 清除进程的会计统计数据。acct_clear_integrals(p);// 初始化 POSIX CPU 定时器。posix_cputimers_init(&p->posix_cputimers);// 初始化与时钟滴答相关的数据。tick_dep_init_task(p);p->io_context = NULL;audit_set_context(p, NULL);cgroup_fork(p);if (args->kthread) {if (!set_kthread_struct(p))goto bad_fork_cleanup_delayacct;}
/*
* 如果启用了 NUMA(非一致性内存访问),复制内存策略(mempolicy)。
* 如果复制失败,返回错误并清理。
*/
#ifdef CONFIG_NUMAp->mempolicy = mpol_dup(p->mempolicy);if (IS_ERR(p->mempolicy)) {retval = PTR_ERR(p->mempolicy);p->mempolicy = NULL;goto bad_fork_cleanup_delayacct;}
#endif
// 初始化 CPU 集相关的数据结构。
#ifdef CONFIG_CPUSETSp->cpuset_mem_spread_rotor = NUMA_NO_NODE;seqcount_spinlock_init(&p->mems_allowed_seq, &p->alloc_lock);
#endif
#ifdef CONFIG_TRACE_IRQFLAGSmemset(&p->irqtrace, 0, sizeof(p->irqtrace));// 初始化硬中断和软中断的跟踪信息。p->irqtrace.hardirq_disable_ip = _THIS_IP_;p->irqtrace.softirq_enable_ip = _THIS_IP_;p->softirqs_enabled = 1;p->softirq_context = 0;
#endifp->pagefault_disabled = 0;// 初始化锁依赖性检查相关数据。
#ifdef CONFIG_LOCKDEPlockdep_init_task(p);
#endif// 初始化互斥锁调试信息。
#ifdef CONFIG_DEBUG_MUTEXESp->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHEp->sequential_io = 0;p->sequential_io_avg = 0;
#endif
#ifdef CONFIG_BPF_SYSCALLRCU_INIT_POINTER(p->bpf_storage, NULL);p->bpf_ctx = NULL;
#endif
-
copy_creds
:- 复制当前进程的权限信息(
cred
),包括用户ID
、组ID
、能力(capabilities
)等。 - 如果复制失败,返回错误码并跳转到
bad_fork_free
标签进行清理。
每个进程都有独立的权限信息,copy_creds
确保新进程的权限信息与父进程一致,或者根据clone_flags
进行调整。
- 复制当前进程的权限信息(
这段代码的主要功能是检查权限和资源限制,确保新进程的创建符合系统约束。初始化新进程的各种数据结构,包括权限、锁、信号、时间统计等。设置新进程的标志位和链表结构,为后续的调度和管理做好准备。通过这些步骤,内核确保新进程的状态是干净且一致的,为后续的运行提供了基础。
7.5.6. 初始化新进程调度相关信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()/* Perform scheduler related setup. Assign this task to a CPU. *//** 调用 sched_fork 函数完成新进程的调度相关初始化。* 参数:* clone_flags:fork 或 clone 系统调用传递的标志,用于控制新进程的行为。* p:新创建的进程的 task_struct。* 主要工作:* 将新进程分配到一个 CPU。* 初始化调度器相关的数据结构(如优先级、时间片等)。* 设置新进程的调度策略。* 调度器是内核的核心组件之一,负责管理进程的运行。* 在创建新进程时,必须为其设置调度相关的初始状态。*/ retval = sched_fork(clone_flags, p);if (retval)// 清理调度相关的资源。goto bad_fork_cleanup_policy;/** 调用 perf_event_init_task 函数初始化新进程的性能事件(Performance Events)。* 为新进程分配性能监控相关的数据结构。* 继承父进程的性能事件上下文(如果需要)。* perf_event 是 Linux 内核中的性能监控框架,用于跟踪和分析系统性能。*/retval = perf_event_init_task(p, clone_flags);if (retval)// 清理性能事件相关的资源。goto bad_fork_sched_cancel_fork;/** 调用 audit_alloc 函数为新进程分配审计(Audit)相关的数据结构。* 初始化审计上下文。继承父进程的审计信息(如果需要)。* 审计子系统用于记录系统调用和其他安全相关事件。* 在创建新进程时,需要为其分配审计上下文,以便记录其活动。*/retval = audit_alloc(p);if (retval)// 清理审计相关的资源。goto bad_fork_cleanup_perf;
这段代码的主要确保新进程在调度、性能监控和审计方面的状态是正确的。如果任何步骤失败,代码会进行相应的清理,避免资源泄漏或状态不一致。
7.5.7. 复制父进程内容并分配 PID
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()/* copy all the process information */shm_init_task(p);retval = security_task_alloc(p, clone_flags);if (retval)goto bad_fork_cleanup_audit;retval = copy_semundo(clone_flags, p);if (retval)goto bad_fork_cleanup_security;retval = copy_files(clone_flags, p, args->no_files);if (retval)goto bad_fork_cleanup_semundo;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;retval = copy_mm(clone_flags, p);if (retval)goto bad_fork_cleanup_signal;retval = copy_namespaces(clone_flags, p);if (retval)goto bad_fork_cleanup_mm;retval = copy_io(clone_flags, p);if (retval)goto bad_fork_cleanup_namespaces;retval = copy_thread(p, args);if (retval)goto bad_fork_cleanup_io;stackleak_task_init(p);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)) {retval = PTR_ERR(pid);goto bad_fork_cleanup_thread;}}
这段代码是 copy_process
函数中对新进程的资源和上下文进行初始化和复制的部分,涉及共享内存、权限、安全性、文件系统、信号处理、内存管理、命名空间、IO 上下文、线程上下文等多个方面。
1. 共享内存初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
shm_init_task(p);
-
作用:
- 初始化新进程的共享内存(
System V
共享内存)相关数据结构。 - 如果父进程有共享内存段,新进程会继承这些信息。
- 初始化新进程的共享内存(
-
背景:
- 共享内存是进程间通信的一种方式,
shm_init_task
确保新进程能够正确处理共享内存。
- 共享内存是进程间通信的一种方式,
2. 安全性相关初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = security_task_alloc(p, clone_flags);
if (retval)goto bad_fork_cleanup_audit;
-
作用:
- 调用安全模块(如
SELinux
、AppArmor
)为新进程分配安全上下文。 - 根据
clone_flags
设置新进程的安全属性。
- 调用安全模块(如
-
返回值:
- 如果分配失败,返回错误码并跳转到
bad_fork_cleanup_audit
标签进行清理。
- 如果分配失败,返回错误码并跳转到
-
背景:
Linux
内核中的安全模块需要为每个进程维护独立的安全上下文,以实现访问控制。
3. 信号量撤销信息复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_semundo(clone_flags, p);
if (retval)goto bad_fork_cleanup_security;
-
作用:
- 复制父进程的信号量撤销(
semaphore undo
)信息到新进程。 - 如果父进程在使用
System V
信号量,新进程需要继承这些信息。
- 复制父进程的信号量撤销(
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_security
标签进行清理。
- 如果复制失败,返回错误码并跳转到
4. 文件描述符表复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_files(clone_flags, p, args->no_files);
if (retval)goto bad_fork_cleanup_semundo;
-
作用:
- 根据
clone_flags
决定是否共享文件描述符表。 - 如果未共享,调用
dup_fd
函数复制父进程的文件描述符表。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_semundo
标签进行清理。
- 如果复制失败,返回错误码并跳转到
5. 文件系统信息复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_fs(clone_flags, p);
if (retval)goto bad_fork_cleanup_files;
-
作用:
- 根据
clone_flags
决定是否共享文件系统信息(如当前工作目录和根目录)。 - 如果未共享,调用
copy_fs_struct
函数复制父进程的文件系统信息。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_files
标签进行清理。
- 如果复制失败,返回错误码并跳转到
6. 信号处理表复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_sighand(clone_flags, p);
if (retval)goto bad_fork_cleanup_fs;
-
作用:
- 根据
clone_flags
决定是否共享信号处理表。 - 如果未共享,调用
kmem_cache_alloc
分配新的信号处理表,并复制父进程的信号处理信息。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_fs
标签进行清理。
- 如果复制失败,返回错误码并跳转到
7.信号队列复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_signal(clone_flags, p);
if (retval)goto bad_fork_cleanup_sighand;
-
作用:
- 如果新进程是线程组的第一个线程,分配并初始化信号队列。
- 复制父进程的信号队列信息。
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_sighand
标签进行清理。
- 如果复制失败,返回错误码并跳转到
8. 内存管理信息复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_mm(clone_flags, p);
if (retval)goto bad_fork_cleanup_signal;
-
作用:
- 根据
clone_flags
决定是否共享内存地址空间。 - 如果未共享,调用
dup_mm
函数复制父进程的内存管理信息。此处也是实现fork()
函数写时拷贝(Copy-On-Write, COW
)机制的关键所在。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_signal
标签进行清理。
- 如果复制失败,返回错误码并跳转到
9. 命名空间复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_namespaces(clone_flags, p);
if (retval)goto bad_fork_cleanup_mm;
-
作用:
- 根据
clone_flags
决定是否共享命名空间(如网络命名空间、PID 命名空间等)。 - 如果未共享,调用
create_new_namespaces
函数创建新的命名空间。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_mm
标签进行清理。
- 如果复制失败,返回错误码并跳转到
10. IO 上下文复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_io(clone_flags, p);
if (retval)goto bad_fork_cleanup_namespaces;
-
作用:
- 根据
clone_flags
决定是否共享IO
上下文。 - 如果未共享,调用
copy_io_context
函数复制父进程的IO
上下文。
- 根据
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_namespaces
标签进行清理。
- 如果复制失败,返回错误码并跳转到
11. 进程上下文复制
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
retval = copy_thread(p, args);
if (retval)goto bad_fork_cleanup_io;
-
作用:
- 复制父进程的进程上下文(如寄存器状态、内核栈等)。
- 初始化新进程的内核栈和寄存器。
- 该函数将作为
fork()
函数实现向用户态函数返回两个不同值的关键步骤。
-
返回值:
- 如果复制失败,返回错误码并跳转到
bad_fork_cleanup_io
标签进行清理。
- 如果复制失败,返回错误码并跳转到
12. 堆栈泄漏保护初始化
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
stackleak_task_init(p);
- 作用:
- 初始化堆栈泄漏保护机制,清除新线程的内核栈内容。
- 这是一个安全功能,用于防止内核栈数据泄漏到用户空间。
13. PID 分配
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
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)) {retval = PTR_ERR(pid);goto bad_fork_cleanup_thread;}
}
-
作用:
- 为新进程分配一个唯一的
PID
。 - 如果新进程属于一个新的
PID
命名空间,则分配的PID
可能与父命名空间不同。
- 为新进程分配一个唯一的
-
返回值:
- 如果分配失败,返回错误码并跳转到
bad_fork_cleanup_thread
标签进行清理。
- 如果分配失败,返回错误码并跳转到
这段代码的主要为新进程复制父进程各种资源和上下文,包括共享内存、权限、安全性、文件系统、信号处理、内存管理、命名空间、IO
上下文和线程上下文。最后为新进程分配一个唯一的 PID
,完成进程创建的关键步骤。
7.5.8. 进一步初始化新进程
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()/** This has to happen after we've potentially unshared the file* descriptor table (so that the pidfd doesn't leak into the child* if the fd table isn't shared).*/// 如果设置了 CLONE_PIDFD 标志,内核会为新进程创建一个 PID 文件描述符(pidfd)。if (clone_flags & CLONE_PIDFD) {int flags = (clone_flags & CLONE_THREAD) ? PIDFD_THREAD : 0;/** Note that no task has been attached to @pid yet indicate* that via CLONE_PIDFD.*/// 为新进程分配一个 PID 文件描述符,并创建与之关联的文件对象。retval = __pidfd_prepare(pid, flags | PIDFD_CLONE, &pidfile);if (retval < 0)goto bad_fork_free_pid;pidfd = retval;// 将分配的 pidfd 写入用户空间的 args->pidfd 指针。retval = put_user(pidfd, args->pidfd);if (retval)goto bad_fork_put_pidfd;}// 如果启用了块设备支持,初始化新进程的 plug 字段为 NULL。
#ifdef CONFIG_BLOCK// plug 用于块设备 IO 操作的优化。p->plug = NULL;
#endif// 初始化新进程的 Futex(快速用户空间互斥锁)相关数据结构。// 确保新进程能够正确处理 Futex 操作。futex_init_task(p);/** sigaltstack should be cleared when sharing the same VM*/// 如果新进程与父进程共享内存地址空间(CLONE_VM),但未设置 CLONE_VFORK,则清除新进程的备用信号栈(sigaltstack)。// 这是为了避免共享内存时信号栈的冲突。if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)sas_ss_reset(p);/** Syscall tracing and stepping should be turned off in the* child regardless of CLONE_PTRACE.*/// 禁用新进程的单步调试功能。user_disable_single_step(p);// 清除新进程的系统调用跟踪标志(SYSCALL_TRACE)。// 如果启用了系统调用仿真(SYSCALL_EMU),也会清除相关标志。clear_task_syscall_work(p, SYSCALL_TRACE);
#if defined(CONFIG_GENERIC_ENTRY) || defined(TIF_SYSCALL_EMU)clear_task_syscall_work(p, SYSCALL_EMU);
#endif// 清除新进程的延迟跟踪数据。clear_tsk_latency_tracing(p);/* ok, now we should be set up.. */p->pid = pid_nr(pid);if (clone_flags & CLONE_THREAD) {p->group_leader = current->group_leader;p->tgid = current->tgid;} else {p->group_leader = p;p->tgid = p->pid;}// 初始化新进程的脏页计数为 0。p->nr_dirtied = 0;// 设置脏页计数的暂停阈值。p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);// 初始化脏页暂停时间戳为 0。p->dirty_paused_when = 0;// 初始化新进程的父进程死亡信号为 0。p->pdeath_signal = 0;// 初始化新进程的任务工作队列为 NULL。p->task_works = NULL;// 清除新进程的 POSIX CPU 定时器工作队列。clear_posix_cputimers_work(p);// 如果启用了 Kretprobe(内核返回探针),初始化相关字段为 NULL。
#ifdef CONFIG_KRETPROBESp->kretprobe_instances.first = NULL;
#endif
// 如果启用了 Rethook(返回钩子),初始化相关字段为 NULL。
#ifdef CONFIG_RETHOOKp->rethooks.first = NULL;
#endif
1. 设置 PID 和线程组信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {p->group_leader = current->group_leader;p->tgid = current->tgid;
} else {p->group_leader = p;p->tgid = p->pid;
}
-
p->pid
:- 设置新进程的
PID
。
- 设置新进程的
-
线程组设置:
- 如果设置了
CLONE_THREAD
,新进程属于与父进程相同的线程组:group_leader
指向线程组的组长。tgid
(线程组ID
)与父进程相同。
- 否则,新进程是一个独立的进程:
group_leader
指向自身。tgid
等于其PID
。
- 如果设置了
7.5.9. 初始化 cgroup 子系统相关、时间戳、父子关系等
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()// 调用 cgroup_can_fork 检查 cgroup 子系统是否允许新进程的创建。// cgroup 是 Linux 内核中的控制组,用于限制、记录和隔离进程的资源使用(如 CPU、内存、IO 等)。retval = cgroup_can_fork(p, args);if (retval)goto bad_fork_put_pidfd;/** Now that the cgroups are pinned, re-clone the parent cgroup and put* the new task on the correct runqueue. All this *before* the task* becomes visible.** This isn't part of ->can_fork() because while the re-cloning is* cgroup specific, it unconditionally needs to place the task on a* runqueue.*/// 调用 sched_cgroup_fork 根据 args 参数初始化新进程的调度器相关的 cgroup 信息。retval = sched_cgroup_fork(p, args);if (retval)goto bad_fork_cancel_cgroup;/** From this point on we must avoid any synchronous user-space* communication until we take the tasklist-lock. In particular, we do* not want user-space to be able to predict the process start-time by* stalling fork(2) after we recorded the start_time but before it is* visible to the system.*/p->start_time = ktime_get_ns();p->start_boottime = ktime_get_boottime_ns();/** Make it visible to the rest of the system, but dont wake it up yet.* Need tasklist lock for parent etc handling!*/write_lock_irq(&tasklist_lock);/* CLONE_PARENT re-uses the old parent */if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {p->real_parent = current->real_parent;p->parent_exec_id = current->parent_exec_id;if (clone_flags & CLONE_THREAD)p->exit_signal = -1;elsep->exit_signal = current->group_leader->exit_signal;} else {p->real_parent = current;p->parent_exec_id = current->self_exec_id;p->exit_signal = args->exit_signal;}// 调用 klp_copy_process 复制与内核实时补丁(Kernel Live Patching)相关的状态到新进程klp_copy_process(p);// 调用 sched_core_fork 初始化新进程的核心调度信息。sched_core_fork(p);// 获取当前进程的信号锁(siglock),确保对信号相关数据的修改是线程安全的spin_lock(¤t->sighand->siglock);// 如果启用了 RV(Runtime Verification),初始化新进程的 RV 相关状态。rv_task_fork(p);// 初始化新进程的重启序列(Restartable Sequences)状态。rseq_fork(p, clone_flags);/* Don't start children in a dying pid namespace */// 检查新进程所在的 PID 命名空间是否处于有效状态。if (unlikely(!(ns_of_pid(pid)->pid_allocated & PIDNS_ADDING))) {retval = -ENOMEM;goto bad_fork_core_free;}/* Let kill terminate clone/fork in the middle */// 检查当前进程是否有挂起的致命信号。if (fatal_signal_pending(current)) {retval = -EINTR;goto bad_fork_core_free;}
1. 设置进程的启动时间
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
p->start_time = ktime_get_ns();
p->start_boottime = ktime_get_boottime_ns();
-
p->start_time
:- 设置新进程的启动时间(以纳秒为单位),用于记录进程的创建时间。
-
p->start_boottime
:- 设置新进程的启动时间相对于系统启动时间的偏移量。
-
背景:
- 这些时间戳用于统计和监控进程的生命周期。
4. 获取 tasklist_lock
锁并设置父子关系
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
write_lock_irq(&tasklist_lock);if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {p->real_parent = current->real_parent;p->parent_exec_id = current->parent_exec_id;if (clone_flags & CLONE_THREAD)p->exit_signal = -1;elsep->exit_signal = current->group_leader->exit_signal;
} else {p->real_parent = current;p->parent_exec_id = current->self_exec_id;p->exit_signal = args->exit_signal;
}
-
tasklist_lock
:- 获取
tasklist_lock
锁,确保对进程列表的修改是线程安全的。
- 获取
-
父子关系设置:
- 如果设置了
CLONE_PARENT
或CLONE_THREAD
:- 新进程的父进程为当前进程的真实父进程(
real_parent
)。 - 如果是线程(
CLONE_THREAD
),设置exit_signal
为-1
,表示线程退出时不发送信号。 - 否则,继承父进程的
exit_signal
。
- 新进程的父进程为当前进程的真实父进程(
- 如果未设置
CLONE_PARENT
或CLONE_THREAD
:- 新进程的父进程为当前进程。
- 设置
exit_signal
为传入的参数值。
- 如果设置了
7.5.10. 将新进程添加进内核任务列表并返回
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()/** Copy seccomp details explicitly here, in case they were changed* before holding sighand lock.*/// 复制当前进程的 Seccomp(安全计算模式)状态到新进程。// Seccomp 是一种安全机制,用于限制进程可以调用的系统调用。// Seccomp 状态需要在新进程中继承,以确保安全策略的一致性。copy_seccomp(p);// 初始化新进程的 PID 链接节点(pid_links),为后续的 PID 分配和管理做好准备。init_task_pid_links(p);if (likely(p->pid)) {ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);init_task_pid(p, PIDTYPE_PID, pid);if (thread_group_leader(p)) {init_task_pid(p, PIDTYPE_TGID, pid);init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));init_task_pid(p, PIDTYPE_SID, task_session(current));if (is_child_reaper(pid)) {ns_of_pid(pid)->child_reaper = p;p->signal->flags |= SIGNAL_UNKILLABLE;}p->signal->shared_pending.signal = delayed.signal;p->signal->tty = tty_kref_get(current->signal->tty);/** Inherit has_child_subreaper flag under the same* tasklist_lock with adding child to the process tree* for propagate_has_child_subreaper optimization.*/p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||p->real_parent->signal->is_child_subreaper;list_add_tail(&p->sibling, &p->real_parent->children);list_add_tail_rcu(&p->tasks, &init_task.tasks);attach_pid(p, PIDTYPE_TGID);attach_pid(p, PIDTYPE_PGID);attach_pid(p, PIDTYPE_SID);__this_cpu_inc(process_counts);} else {current->signal->nr_threads++;current->signal->quick_threads++;atomic_inc(¤t->signal->live);refcount_inc(¤t->signal->sigcnt);task_join_group_stop(p);list_add_tail_rcu(&p->thread_node,&p->signal->thread_head);}attach_pid(p, PIDTYPE_PID);nr_threads++;}total_forks++;hlist_del_init(&delayed.node);spin_unlock(¤t->sighand->siglock);syscall_tracepoint_update(p);write_unlock_irq(&tasklist_lock);if (pidfile)fd_install(pidfd, pidfile);// 通知用户空间新进程的创建事件。proc_fork_connector(p);// 调度器的后续处理,为新进程分配运行队列。sched_post_fork(p);// cgroup 子系统的后续处理,将新进程添加到正确的 cgroup 中。cgroup_post_fork(p, args);// 初始化新进程的性能事件上下文。perf_event_fork(p);// 跟踪新任务的创建事件。trace_task_newtask(p, clone_flags);// 复制与用户探针(uprobes)相关的状态。uprobe_copy_process(p, clone_flags);// 处理与用户事件相关的 fork 操作。user_events_fork(p, clone_flags);// 复制父进程的 OOM(Out-Of-Memory)分数调整值到新进程。// OOM 分数用于决定进程在内存不足时被杀死的优先级。copy_oom_score_adj(clone_flags, p);// 返回新创建的进程的 task_struct 指针。return p;
1. 设置 PID 和线程组信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
// 检查新进程是否成功分配了有效的 PID。
if (likely(p->pid)) {// 初始化新进程的 ptrace(进程跟踪)状态。ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);// 将新进程的 PID 与其 task_struct 关联。init_task_pid(p, PIDTYPE_PID, pid);// 线程组领导者的特殊处理if (thread_group_leader(p)) {// 初始化线程组 ID(TGID),通常等于线程组领导者的 PID。init_task_pid(p, PIDTYPE_TGID, pid);// 初始化进程组 ID(PGID),继承父进程的进程组。init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));// 初始化会话 ID(SID),继承父进程的会话。init_task_pid(p, PIDTYPE_SID, task_session(current));// 检查是否是子进程回收者if (is_child_reaper(pid)) {ns_of_pid(pid)->child_reaper = p;// 将其标记为不可杀死(SIGNAL_UNKILLABLE)。// 子进程回收者是负责回收孤儿进程的特殊进程(如 PID 命名空间中的第一个进程)。p->signal->flags |= SIGNAL_UNKILLABLE;}// 继承父进程的挂起信号。p->signal->shared_pending.signal = delayed.signal;// 继承父进程的终端(TTY)引用。p->signal->tty = tty_kref_get(current->signal->tty);// 如果父进程是子进程回收者或具有子进程回收者标志,新进程也继承这些标志。p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||p->real_parent->signal->is_child_subreaper;// 将新进程添加到父进程的子进程链表中(children)。list_add_tail(&p->sibling, &p->real_parent->children);// 将新进程添加到全局任务链表中(init_task.tasks)。list_add_tail_rcu(&p->tasks, &init_task.tasks);// 将新进程的 TGID、PGID 和 SID 附加到全局 PID 哈希表中。attach_pid(p, PIDTYPE_TGID);attach_pid(p, PIDTYPE_PGID);attach_pid(p, PIDTYPE_SID);// 增加当前 CPU 的进程计数,用于统计。__this_cpu_inc(process_counts);} else { // 非线程组领导者的处理// 增加父进程的线程计数(nr_threads 和 quick_threads)。current->signal->nr_threads++;current->signal->quick_threads++;// 增加线程的活动计数(live)和引用计数(sigcnt)。atomic_inc(¤t->signal->live);refcount_inc(¤t->signal->sigcnt);// 将新线程加入线程组的停止状态。task_join_group_stop(p);// 将新线程添加到线程组的线程链表中(thread_head)。list_add_tail_rcu(&p->thread_node, &p->signal->thread_head);}//将新进程的 PID 附加到全局 PID 哈希表中。attach_pid(p, PIDTYPE_PID);// 增加全局线程计数(nr_threads)。nr_threads++;
}
-
ptrace_init_task
:- 初始化新进程的 ptrace(进程跟踪)状态。
- 如果设置了
CLONE_PTRACE
或启用了跟踪,则允许父进程跟踪新进程。
-
init_task_pid
:- 将新进程的
PID
与其task_struct
关联。PIDTYPE_PID
表示这是进程的唯一标识符。
- 将新进程的
-
线程组领导者:
- 如果新进程是线程组的领导者:
- 初始化线程组
ID
(TGID
)、进程组ID
(PGID
)和会话ID
(SID
)。 - 如果新进程是子进程回收者(
child_reaper
),将其标记为不可杀死(SIGNAL_UNKILLABLE
)。 shared_pending.signal
:- 继承父进程的挂起信号。
tty
:- 继承父进程的终端(
TTY
)引用。
- 继承父进程的终端(
- 如果父进程是子进程回收者或具有子进程回收者标志,新进程也继承这些标志。
- 将新进程添加到父进程的子进程链表中。
- 将新进程添加到全局任务链表中。
attach_pid
将新进程的TGID
、PGID
和SID
附加到全局PID
哈希表中。__this_cpu_inc
- 增加当前
CPU
的进程计数,用于统计。
- 增加当前
- 初始化线程组
- 非线程组领导者的处理:
- 增加父进程的线程计数(
nr_threads
和quick_threads
)。 - 增加线程的活动计数(
live
)和引用计数(sigcnt
)。 task_join_group_stop
:- 将新线程加入线程组的停止状态。
list_add_tail_rcu
:- 将新线程添加到线程组的线程链表中(
thread_head
)。
- 将新线程添加到线程组的线程链表中(
- 增加父进程的线程计数(
- 如果新进程是线程组的领导者:
-
attach_pid
:- 将新进程的
PID
附加到全局PID
哈希表中。
- 将新进程的
-
nr_threads
:- 增加全局线程计数。
2. 更新全局统计信息
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
total_forks++;
hlist_del_init(&delayed.node);
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);
-
total_forks
:- 增加全局 fork 计数。
-
hlist_del_init
:- 从挂起信号链表中删除延迟信号节点。
-
syscall_tracepoint_update
:- 更新新进程的系统调用跟踪点。
-
write_unlock_irq
:- 释放
tasklist_lock
锁,对应上面的write_lock_irq()
。
- 释放
3. 安装 PID 文件描述符
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()
if (pidfile)fd_install(pidfd, pidfile);
- 作用:
- 如果设置了
CLONE_PIDFD
,将新进程的PID
文件描述符安装到父进程的文件描述符表中。
- 如果设置了
这里主要初始化新进程的 PID
和线程组信息。更新全局统计信息并释放锁。安装 PID
文件描述符(如果需要)。通知用户空间并完成调度器、cgroup
和性能事件的后续处理。跟踪新任务的创建并处理用户事件。返回新创建的进程。通过这些操作,copy_process
函数也就完成了新进程的所有初始化工作,并将其交给调度器管理。
7.5.11. 错误处理部分
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_process()bad_fork_core_free:// 调用 sched_core_free 释放调度器核心相关的资源。sched_core_free(p);// 释放当前进程的信号锁(sighand->siglock)。spin_unlock(¤t->sighand->siglock);// 释放全局任务列表锁(tasklist_lock)。write_unlock_irq(&tasklist_lock);
bad_fork_cancel_cgroup:// 调用 cgroup_cancel_fork 撤销 cgroup 子系统中为新进程分配的资源cgroup_cancel_fork(p, args);
bad_fork_put_pidfd:// 如果设置了 CLONE_PIDFD 标志,释放与 PID 文件描述符相关的资源if (clone_flags & CLONE_PIDFD) {fput(pidfile);put_unused_fd(pidfd);}
bad_fork_free_pid:// 如果新进程的 PID 已分配,调用 free_pid 释放 PID。if (pid != &init_struct_pid)free_pid(pid);
bad_fork_cleanup_thread:// 调用 exit_thread 清理与线程相关的资源(如内核栈、线程信息等)。exit_thread(p);
bad_fork_cleanup_io:// 如果新进程的 IO 上下文已分配,调用 exit_io_context 释放 IO 上下文。if (p->io_context)exit_io_context(p);
bad_fork_cleanup_namespaces:// 调用 exit_task_namespaces 释放新进程的命名空间资源。exit_task_namespaces(p);
bad_fork_cleanup_mm:// 如果新进程的内存管理结构(mm)已分配:if (p->mm) {mm_clear_owner(p->mm, p);mmput(p->mm);}
bad_fork_cleanup_signal:// 如果新进程不是线程,调用 free_signal_struct 释放信号结构。if (!(clone_flags & CLONE_THREAD))free_signal_struct(p->signal);
bad_fork_cleanup_sighand:// 调用 __cleanup_sighand 释放信号处理结构。__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:// 调用 exit_fs 释放文件系统相关的资源(如当前工作目录和根目录)。exit_fs(p); /* blocking */
bad_fork_cleanup_files:// 调用 exit_files 释放文件描述符表。exit_files(p); /* blocking */
bad_fork_cleanup_semundo:// 调用 exit_sem 释放信号量撤销信息。exit_sem(p);
bad_fork_cleanup_security:// 调用 security_task_free 释放安全模块分配的资源。security_task_free(p);
bad_fork_cleanup_audit:// 调用 audit_free 释放审计相关的资源。audit_free(p);
bad_fork_cleanup_perf:// 调用 perf_event_free_task 释放性能事件相关的资源。perf_event_free_task(p);
bad_fork_sched_cancel_fork:// 调用 sched_cancel_fork 撤销调度器中为新进程分配的资源。sched_cancel_fork(p);
bad_fork_cleanup_policy:// 调用 lockdep_free_task 释放锁依赖性检查相关的资源。lockdep_free_task(p);
// 如果启用了 NUMA,调用 mpol_put 释放内存策略。
#ifdef CONFIG_NUMAmpol_put(p->mempolicy);
#endif
bad_fork_cleanup_delayacct:// 释放延迟统计相关的资源。delayacct_tsk_free(p);
bad_fork_cleanup_count:// 调用 dec_rlimit_ucounts 减少用户进程计数。dec_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);// 调用 exit_creds 释放新进程的权限信息。exit_creds(p);
bad_fork_free:// 将新进程的状态设置为 TASK_DEAD。WRITE_ONCE(p->__state, TASK_DEAD);// 调用 exit_task_stack_account 和 put_task_stack 释放内核栈。exit_task_stack_account(p);put_task_stack(p);// 调用 delayed_free_task 延迟释放任务结构。delayed_free_task(p);
fork_out:// 从挂起信号链表中删除延迟信号节点。spin_lock_irq(¤t->sighand->siglock);hlist_del_init(&delayed.node);spin_unlock_irq(¤t->sighand->siglock);// 返回错误指针,表示进程创建失败。return ERR_PTR(retval);
主要作用:
- 清理资源:在进程创建失败时,按顺序释放已分配的资源,确保系统状态一致。
- 错误处理路径:通过多个错误处理标签,逐步清理不同类型的资源。
- 返回错误:最终返回错误指针,通知调用者进程创建失败。
这部分为错误处理代码,用于在进程创建失败时清理已分配的资源,确保系统状态一致,避免资源泄漏。
#附
#05. init 进程(待补充…)
敬请期待…
#上一篇
【Linux内核设计与实现】第三章——进程管理01
#下一篇
【Linux内核设计与实现】第三章——进程管理03