Linux学习笔记(三):Linux启动从rest_init 到 kernel_init创建1号进程(上)

三、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_VFORKCLONE_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(),忽略错误。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值