三、rest_init 到 kernel_init
创建1号进程
整体流程是这样的:
rest_init()
└─ user_mode_thread(kernel_init, NULL, CLONE_FS)
├─ 构造 args: [.fn=kernel_init, .flags=XXX, .exit_signal=XXX]
└─ kernel_clone(&args)
├─ copy_process()
│ ├─ dup_task_struct()
│ └─ copy_thread()
│ ├─ thread->cpu_context.r5 = (unsigned long)args->fn;
│ └─ thread->cpu_context.pc = (unsigned long)ret_from_fork;
└─ wake_up_new_task()
└─ 调度执行:
├─ ret_from_fork → 切用户态
└─ kernel_init()
├─ kernel_init_freeable()
└─ execve("/sbin/init")
/linux-6.6.10/init/main.c
690 /*
691 * 我们需要首先启动init进程,使其获取pid 1。然而
692 * init任务最终会需要创建内核线程(kthreads),如果
693 * 我们在创建kthreadd线程之前就调度它,将会导致OOPS(内核错误)。
694 */
695 pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
/linux-6.6.10/kernel/fork.c
2977 pid_t user_mode_thread(int (*fn)(void *), void *arg, unsigned long flags)
2978 {
2979 struct kernel_clone_args args = {
2980 .flags = ((lower_32_bits(flags) | CLONE_VM |
2981 CLONE_UNTRACED) & ~CSIGNAL),
2982 .exit_signal = (lower_32_bits(flags) & CSIGNAL),
2983 .fn = fn,
2984 .fn_arg = arg,
2985 };
2986
2987 return kernel_clone(&args);
2988 }
此时的结构体 args:
args: [.fn=kernel_init, .flags=CLONE_VM|CLONE_FS|CLONE_UNTRACED, .exit_signal=0]
这里,.fn=kernel_init 表示新创建的线程(或进程)将执行 kernel_init
函数
.flags
表示新线程将:
- 与父进程共享内存空间(
CLONE_VM
) - 共享文件系统信息(
CLONE_FS
) - 不会被调试器跟踪(
CLONE_UNTRACED
) - 信号位被清零(由
& ~CSIGNAL
保证)
.exit_signal = 0
表示新线程退出时不会向父进程发送任何信号。
3.1、kernel_clone
kernel_clone()
是创建新进程/线程的底层实现,主要完成:
- 复制父进程资源(通过
copy_process()
) - 启动新任务(通过
wake_up_new_task()
) - 处理特殊标志(如
CLONE_VFORK
、CLONE_PIDFD
等)
代码重要部分:
1、禁止Ptrace 调试事件处理
/linux-6.6.10/kernel/fork.c
2870 u64 clone_flags = args->flags;
...
2897 if (!(clone_flags & CLONE_UNTRACED)) {
2898 if (clone_flags & CLONE_VFORK)
2899 trace = PTRACE_EVENT_VFORK;
2900 else if (args->exit_signal != SIGCHLD)
2901 trace = PTRACE_EVENT_CLONE;
2902 else
2903 trace = PTRACE_EVENT_FORK;
2904
2905 if (likely(!ptrace_event_enabled(current, trace)))
2906 trace = 0;
2907 }
根据 args->flags 类型决定copy_process喊出传递的trace类型,根据代码逻辑,内核设置这个标志以使CLONE_PTRACE标志失去作用(用来禁止内核线程跟踪进程)。
2、核心进程复制
2909 p = copy_process(NULL, trace, NUMA_NO_NODE, args);
copy_process()
:实际复制进程描述符(task_struct
)和资源(内存、文件描述符等)。
- 根据
args->flags
决定共享哪些资源(如CLONE_VM
共享内存空间)。 - 若指定了
args->fn
,则创建内核线程。
3、新任务启动
2940 wake_up_new_task(p);
- 将新任务加入调度队列,等待CPU执行。
3.2、copy_process
主要做了以下几个关键事情:
1. 参数检查和初始化阶段
// 检查不允许在不同命名空间中共享根目录
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
// 检查线程组必须共享信号处理
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
// 检查共享信号处理必须共享VM
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
// 防止全局init进程创建兄弟进程
if ((clone_flags & CLONE_PARENT) && current->signal->flags & SIGNAL_UNKILLABLE)
return ERR_PTR(-EINVAL);
// 检查不同PID或用户命名空间的线程共享限制
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);
}
// PID文件描述符相关标志检查
if (clone_flags & CLONE_PIDFD) {
if (clone_flags & (CLONE_DETACHED | CLONE_THREAD))
return ERR_PTR(-EINVAL);
}
首先先看一下各个The flags mask的含义
CLONE_CHILD_CLEARTID
:子进程在调用exec
函数或退出时,会将tid
置为0。CLONE_CHILD_SETTID
:子进程在调用exec
函数或退出时,会将tid
置为子进程ID。CLONE_FILES
:子进程与父进程共享文件描述符表。CLONE_FS
:子进程与父进程共享文件系统信息。CLONE_IO
:子进程与父进程共享I/O上下文。CLONE_NEWIPC
:子进程创建一个新的IPC命名空间。CLONE_NEWNET
:子进程创建一个新的网络命名空间。CLONE_NEWNS
:子进程创建一个新的挂载命名空间。CLONE_NEWPID
:子进程创建一个新的PID命名空间。CLONE_NEWUTS
:子进程创建一个新的UTS命名空间。CLONE_NEWUSER
:子进程创建一个新的用户命名空间。CLONE_NEWCGROUP
:子进程创建一个新的控制组命名空间。CLONE_PARENT
:子进程的父进程为调用clone
的进程。CLONE_PARENT_SETTID
:子进程的父进程ID会存储在ptid
中。CLONE_PTRACE
:子进程与父进程共享ptrace
。CLONE_SETTLS
:子进程的线程局部存储与父进程相同。CLONE_SIGHAND
:子进程与父进程共享信号处理程序。CLONE_SYSVSEM
:子进程与父进程共享System V信号量。CLONE_THREAD
:子进程创建为与父进程共享线程组的线程。CLONE_UNTRACED
:子进程创建后不会受到ptrace
跟踪。CLONE_VFORK
:子进程使用父进程的页表直到调用exec
或者调用_exit
退出。CLONE_VM
:子进程与父进程共享内存空间。
因此,该段是对clone_flags所传递的标志组合进行合法性检查。当出现以下几种情况时,返回出错代号:
(1)CLONE_NEWNS与CLONE_FS的互斥性检查:CLONE_NEWNS表示子进程需要自己的命名空间,CLONE_FS 则代表子进程共享父进程的根目录和当前工作目录,当同时设置这两个标志时,会产生根本性矛盾,会导致内核无法确定应该如何处理文件系统上下文,因此必须禁止
(2)CLONE_NEWUSER与CLONE_FS的互斥性检查:CLONE_NEWUSER代表子进程创建一个新的用户命名空间,CLONE_FS 则代表子进程共享父进程的根目录和当前工作目录,
(3)CLONE_THREAD设置后CLONE_SIGHAND必须被设置:如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。
(4)CLONE_THREAD设置后CLONE_VM必须被设置。如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。
在 kernel_clone --> copy_process 的调用.flags=CLONE_VM|CLONE_FS|CLONE_UNTRACED,表示此时子进程与父进程共享文件系统信息,以及共享内存空间。
2、任务结构复制阶段
p = dup_task_struct(current, node);
这个函数的主要目的是创建一个新的task_struct
结构体,它是通过复制一个现有的task_struct
来实现的。在这个过程中,它会为新的进程或线程分配必要的资源,如线程栈、内核栈并初始化各种字段和标志。
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
...
err = arch_dup_task_struct(tsk, orig); // 体系结构相关的复制操作
...
err = alloc_thread_stack_node(tsk, node); // 为新进程分配线程栈
...
setup_thread_stack(tsk, orig); // 设置线程栈
clear_user_return_notifier(tsk); // 清除用户返回通知器
clear_tsk_need_resched(tsk); // 清除需要调度的标志
set_task_stack_end_magic(tsk); // 设置栈结束魔法数字
clear_syscall_work_syscall_user_dispatch(tsk); // 清除系统调用工作分发
...
}
(1) 完全复制父进程的task_struct给子进程tsk
arch_dup_task_struct: 在ARM V7中为 *dst = *src, 即把orig中的内容完全复制给tsk,把父进程的task_struct完全复制给新建立的子进程
(2)为新创建的子进程tsk分配内核栈
alloc_thread_stack_node:为任务(线程)分配内核栈的函数,为指定的任务 tsk
在 NUMA 节点 node
上分配一个内核栈(thread stack
),并初始化相关元数据。
(3)设置新创建线程的线程栈信息
setup_thread_stack 函数的目的是初始化新创建线程的thread_info
结构,使其包含与原始线程相同的信息,并确保thread_info
中的task
字段正确指向新线程的task_struct
。这是线程创建过程中的一部分,确保新线程具有正确的内核栈和线程信息。
3、资源复制阶段
复制进程主要数据:files、fs、signal、mm、io等数据,需要关注的有:
copy_signal:如果创建的是进程,则会将父进程的信号屏蔽并安排复制到子进程中。如果创建的是线程,直接返回0。
copy_mm:如果设置了CLONE_VM
标志,表示新进程需要与原进程共享内存空间。这时,函数会增加原进程内存管理信息对象的引用计数,并将新进程的内存管理信息指针指向原进程的内存管理信息。如果没有设置CLONE_VM
标志,函数会调用dup_mm
来复制原进程的内存管理信息到新进程。
// 复制信号量undo列表
retval = copy_semundo(clone_flags, p);
// 复制文件描述符表
retval = copy_files(clone_flags, p, args->no_files);
// 复制文件系统信息
retval = copy_fs(clone_flags, p);
// 复制信号处理程序
retval = copy_sighand(clone_flags, p);
// 复制信号结构
retval = copy_signal(clone_flags, p);
// 复制内存空间(实现COW机制的关键)
retval = copy_mm(clone_flags, p);
// 复制命名空间
retval = copy_namespaces(clone_flags, p);
// 复制IO上下文
retval = copy_io(clone_flags, p);
// 复制线程特定数据
retval = copy_thread(p, args);
其中copy_thread:
/**
* copy_thread - 为新任务设置线程相关的CPU上下文和寄存器状态
* @p: 指向新任务的任务结构体(task_struct)指针
* @args: 包含线程创建参数的结构体(kernel_clone_args)
*
* 功能:
* 1. 初始化新任务的线程信息(thread_info)和寄存器状态(pt_regs)
* 2. 根据创建参数设置不同的执行上下文:
* - 普通进程复制:复制当前寄存器状态并做适当修改
* - 内核线程创建:设置特殊执行上下文
* 3. 处理线程本地存储(TLS)相关设置
* 4. 设置新任务的初始执行点为ret_from_fork
*
* 返回值:始终返回0表示成功
*/
int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
{
// 从参数中提取关键标志和信息
unsigned long clone_flags = args->flags; // 克隆标志位
unsigned long stack_start = args->stack; // 用户栈起始地址(用户线程)
unsigned long tls = args->tls; // 线程本地存储(TLS)值
struct thread_info *thread = task_thread_info(p); // 新任务的线程信息
struct pt_regs *childregs = task_pt_regs(p); // 新任务的寄存器保存区域
// 清空CPU上下文结构
memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
#ifdef CONFIG_CPU_USE_DOMAINS
/*
* 如果配置了CPU域支持,从当前线程复制域访问控制寄存器的初始值
* thread->addr_limit已通过setup_thread_stack()从当前线程复制
*/
thread->cpu_domain = get_domain();
#endif
// 判断是创建普通进程还是内核线程
if (likely(!args->fn)) {
/* 普通进程复制情况 */
// 复制当前进程的寄存器状态
*childregs = *current_pt_regs();
// 设置子进程返回值寄存器为0(表示子进程)
childregs->ARM_r0 = 0;
// 如果指定了用户栈地址,则更新栈指针
if (stack_start)
childregs->ARM_sp = stack_start;
} else {
/* 内核线程创建情况 */
// 清空寄存器状态
memset(childregs, 0, sizeof(struct pt_regs));
// 设置内核线程参数(r4)和入口函数(r5)
thread->cpu_context.r4 = (unsigned long)args->fn_arg;
thread->cpu_context.r5 = (unsigned long)args->fn;
// 设置CPU状态为管理模式(SVC_MODE)
childregs->ARM_cpsr = SVC_MODE;
}
// 设置新任务的执行上下文:
// pc寄存器指向ret_from_fork(任务首次调度的返回路径)
thread->cpu_context.pc = (unsigned long)ret_from_fork;
// sp寄存器指向保存的寄存器区域
thread->cpu_context.sp = (unsigned long)childregs;
// 清除硬件断点设置
clear_ptrace_hw_breakpoint(p);
// 处理线程本地存储(TLS)设置
if (clone_flags & CLONE_SETTLS)
thread->tp_value[0] = tls; // 设置用户TLS值
thread->tp_value[1] = get_tpuser(); // 设置内核TLS值
// 通知其他子系统线程复制事件
thread_notify(THREAD_NOTIFY_COPY, thread);
return 0;
}
主要做了以下事情:
(1)初始化新任务的线程信息(thread_info)和寄存器状态(pt_regs)
(2)根据创建参数设置不同的执行上下文:
- 普通进程复制:复制当前寄存器状态并做适当修改
- 内核线程创建:设置特殊执行上下文:
thread->cpu_context.r5 = (unsigned long)args->fn;
thread->cpu_context.pc = (unsigned long)ret_from_fork; 即设置了:
r5
:保存线程的入口函数地址(args->fn
)。r4
:保存入口函数的参数(args->fn_arg
)。pc
:设置为ret_from_fork
,这是线程首次被调度时的起点。sp
:指向新线程的内核栈(childregs
)。
(3)处理线程本地存储(TLS)相关设置
(4)设置新任务的初始执行点为ret_from_fork
4、PID分配与设置
// 分配PID(非init进程)
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文件描述符处理
if (clone_flags & CLONE_PIDFD) {
retval = __pidfd_prepare(pid, O_RDWR | O_CLOEXEC, &pidfile);
if (retval < 0)
goto bad_fork_free_pid;
pidfd = retval;
retval = put_user(pidfd, args->pidfd);
}
(1)检查当前分配的PID是否属于init
进程(PID 1)。init_struct_pid
是内核中表示init进程的静态PID结构体。只有非init进程才需要动态分配PID。
(2)调用alloc_pid()
函数为新进程分配PID
(3)检查clone_flags
是否包含CLONE_PIDFD
标志。该标志表示调用者(如clone3()
)需要获取一个PID文件描述符(pidfd),用于异步监控进程生命周期。
(4)如果需要,创建PID文件描述符,并返回pidfd给用户空间
开始
│
├─ 是否为init进程? → 否 → alloc_pid()动态分配PID
│ │
│ ├─ 成功? → 继续
│ └─ 失败? → 清理线程资源并返回错误
│
├─ 需要pidfd? (CLONE_PIDFD) → 是 → __pidfd_prepare()
│ │
│ ├─ 成功? → 返回pidfd给用户空间
│ └─ 失败? → 释放PID并返回错误
│
└─ 继续进程创建的其他逻辑
5、进程关系建立
// 设置进程ID和线程组关系
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader; // 线程共享组leader
p->tgid = current->tgid; // 线程共享线程组ID
} else {
p->group_leader = p; // 进程自己是leader
p->tgid = p->pid; // 进程的线程组ID就是自己的PID
}
// 初始化各种列表
INIT_LIST_HEAD(&p->thread_group);
p->task_works = NULL;
主要做了下面的事情:
(1)PID分配结果 → 确定唯一标识。:从PID结构体(支持命名空间)中提取全局唯一的数值PID,赋值给新任务p->pid
。
(2)线程组关系 → 决定它是独立进程还是某线程组的成员。如果是线程(CLONE_THREAD
),新线程与父线程属于同一线程组,tgid
相同。如果是进程(非CLONE_THREAD
),新进程独立成组
(3)链表初始化 → 初始化一个空的双向链表头,用于链接该线程组内的所有线程。如果是进程(主线程),链表初始为空,后续创建的线程会加入此链表。如果是线程,会被添加到父线程的thread_group
链表中
6、进程树整合
// 获取任务列表锁
write_lock_irq(&tasklist_lock);
// 设置父进程关系
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
p->real_parent = current->real_parent; // 共享父进程
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 :
current->group_leader->exit_signal;
} else {
p->real_parent = current; // 当前进程是父进程
p->exit_signal = args->exit_signal;
}
// 将新进程加入进程树
if (likely(p->pid)) {
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));
// 如果是子进程收割者(如init)
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
// 添加到父进程的子进程列表
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
} else {
// 线程的处理
current->signal->nr_threads++;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
attach_pid(p, PIDTYPE_PID); // 附加PID
nr_threads++; // 增加线程计数
}
total_forks++; // 增加fork计数
主要做了下面的事情:
(1)确立父子关系:决定信号传递和资源继承的链条。
(2)加入组织架构:进程加入进程树,线程加入线程组。
(3)全局状态更新:维护PID哈希表、线程计数等全局一致性。
3.2、wake_up_new_task
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
/* 获取任务的 pi_lock 并保存中断状态 */
raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
/* 将任务状态设置为 TASK_RUNNING */
WRITE_ONCE(p->__state, TASK_RUNNING);
#ifdef CONFIG_SMP
/*
* Fork 负载均衡,在这里而不是更早进行是因为:
* - cpus_ptr 在 fork 路径中可能会改变
* - 任何先前选择的 CPU 可能通过热插拔消失
*
* 使用 __set_task_cpu() 避免调用 sched_class::migrate_task_rq,
* 因为我们还没有完全设置好。
*/
p->recent_used_cpu = task_cpu(p); // 记录最近使用的 CPU
rseq_migrate(p); // 迁移 rseq
/* 为任务选择一个新的运行队列 */
__set_task_cpu(p, select_task_rq(p, task_cpu(p), WF_FORK));
#endif
/* 锁定任务的目标运行队列 */
rq = __task_rq_lock(p, &rf);
/* 更新运行队列的时钟 */
update_rq_clock(rq);
/* 初始化任务的利用率统计 */
post_init_entity_util_avg(p);
/* 将任务激活并加入运行队列 */
activate_task(rq, p, ENQUEUE_NOCLOCK);
/* 记录唤醒新任务的事件 */
trace_sched_wakeup_new(p);
/* 检查是否可以抢占当前任务 */
check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
/* 如果调度类定义了 task_woken 回调,则调用它 */
if (p->sched_class->task_woken) {
/*
* 之后没有任何代码依赖 rq->lock,所以可以安全地暂时释放它。
*/
rq_unpin_lock(rq, &rf);
p->sched_class->task_woken(rq, p);
rq_repin_lock(rq, &rf);
}
#endif
/* 解锁运行队列 */
task_rq_unlock(rq, p, &rf);
}
在 Linux 内核中,wake_up_new_task()
函数负责将新创建的任务(task_struct
)加入运行队列(Runqueue),使其可以被调度器选中执行。任务被加入运行队列后,具体何时被调度执行,取决于调度器的决策机制。
activate_task(rq, p, ENQUEUE_NOCLOCK); // 将任务加入运行队列
check_preempt_curr(rq, p, WF_FORK); // 检查是否抢占当前任务
activate_task()
:
- 调用
enqueue_task()
(调度类特定方法,如enqueue_task_fair()
for CFS)。 - 将任务加入 红黑树(CFS) 或 优先级队列(RT调度类)。
当线程首次被调度到CPU时,由于在 copy_process 函数中已经设置了:
thread->cpu_context.r5 = (unsigned long)args->fn;
thread->cpu_context.pc = (unsigned long)ret_from_fork;
因此,硬件会从ret_from_fork
开始执行(pc
寄存器的值),其中,ret_from_fork:
ENTRY(ret_from_fork)
bl schedule_tail
cmp r5, #0
movne r0, r4
badrne lr, 1f
retne r5
1: get_thread_info tsk
b ret_slow_syscall
ENDPROC(ret_from_fork)
因此会跳转到args->fn绑定的函数上去执行,即 args->fn=kernel_init
3.3、kernel_init
/**
* kernel_init - Linux 内核初始化主函数
* @unused: 未使用的参数(保留)
*
* 该函数是内核启动后第一个由 `init` 进程(PID=1)执行的函数,负责:
* 1. 等待 `kthreadd` 完成初始化。
* 2. 执行内核初始化剩余工作(释放初始化内存、设置系统状态等)。
* 3. 尝试加载用户空间的 `init` 程序(如 `/sbin/init`)。
* 4. 如果所有尝试失败,触发内核恐慌(panic)。
*/
static int __ref kernel_init(void *unused)
{
int ret;
/*
* 等待 kthreadd 完成初始化。
* kthreadd 是内核线程守护进程(PID=2),负责创建其他内核线程。
*/
wait_for_completion(&kthreadd_done);
/* 执行内核可释放部分的初始化(如驱动、文件系统等) */
kernel_init_freeable();
/* 等待所有异步 __init 代码完成(避免内存释放后异步操作访问已释放内存) */
async_synchronize_full();
/* 标记系统状态为 "正在释放初始化内存" */
system_state = SYSTEM_FREEING_INITMEM;
/* 释放初始化阶段使用的探测、跟踪、调试等内存 */
kprobe_free_init_mem(); // 释放 kprobe 相关内存
ftrace_free_init_mem(); // 释放 ftrace 相关内存
kgdb_free_init_mem(); // 释放 kgdb 相关内存
exit_boot_config(); // 清理启动配置
free_initmem(); // 释放所有 __init 段内存
mark_readonly(); // 标记内核只读段(防止篡改)
/*
* 内核地址映射已固定 - 更新用户空间页表以完成 PTI(页表隔离)。
* PTI 是缓解 Meltdown 漏洞的安全机制。
*/
pti_finalize();
/* 标记系统状态为 "正常运行" */
system_state = SYSTEM_RUNNING;
/* 设置 NUMA 默认内存策略 */
numa_default_policy();
/* 结束 RCU 内核启动阶段(RCU 是内核同步机制) */
rcu_end_inkernel_boot();
/* 处理内核命令行参数(如 sysctl 配置) */
do_sysctl_args();
/*
* 尝试执行用户空间 init 程序:
* 1. 先尝试 ramdisk 中的 init(如 initrd/initramfs 指定的命令)。
* 2. 然后尝试内核参数指定的 init(如 `init=/bin/bash`)。
* 3. 最后尝试默认路径(/sbin/init、/etc/init、/bin/init、/bin/sh)。
*/
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0; // 执行成功,退出
pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret);
}
/* 尝试执行内核命令行指定的 init 程序(优先级高于默认路径) */
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).", execute_command, ret); // 失败则崩溃
}
/* 尝试编译时指定的默认 init 程序 */
if (CONFIG_DEFAULT_INIT[0] != '\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (ret)
pr_err("Default init %s failed (error %d)\n", CONFIG_DEFAULT_INIT, ret);
else
return 0;
}
/* 尝试常见 init 路径 */
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
/* 如果所有尝试均失败,触发内核恐慌 */
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
}
代码关键部分解读:
1. 等待 kthreadd
就绪
wait_for_completion(&kthreadd_done)
:
kthreadd
(PID=2)是内核线程管理守护进程,负责创建其他内核线程(如工作线程、驱动程序线程)。
必须等待它初始化完成,否则后续依赖线程可能无法创建。
2. 内核初始化收尾工作
kernel_init_freeable()
:
完成剩余内核初始化(如设备驱动、文件系统、网络栈等)。
async_synchronize_full()
:
确保所有异步初始化操作(标记为 __init
的代码)完成,避免内存释放后访问。
释放初始化内存:
free_initmem()
释放 __init
段内存(内核启动后不再需要的代码和数据)。
3. 系统状态与安全设置
system_state
标记:
从 SYSTEM_FREEING_INITMEM
(释放初始化内存)到 SYSTEM_RUNNING
(正常运行)。
pti_finalize()
:
完成页表隔离(PTI)设置,防止 Meltdown 漏洞攻击。
mark_readonly()
:
将内核代码段设为只读,防止恶意修改。
4. 启动用户空间 init
进程
优先级顺序:
ramdisk_execute_command
(如 initrd
指定的 /init
)。
execute_command
(内核参数 init=
指定的路径,如 init=/bin/bash
)。
编译时默认 CONFIG_DEFAULT_INIT
(通常为空)。
常见路径 /sbin/init
、/etc/init
、/bin/init
、/bin/sh
。
关键函数:
run_init_process()
:通过 execve
执行用户空间程序。
try_to_run_init_process()
:封装 run_init_process()
,忽略错误。