Linux0号进程,1号进程,2号进程
Posted leetcode
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux0号进程,1号进程,2号进程相关的知识,希望对你有一定的参考价值。
本节我们将从linux启动的第一个进程说起,以及后面第一个进程是如何启动1号进程,然后启动2号进程。然后系统中所有的进程关系图做个简单的介绍
一、0号进程
0号进程,通常也被称为idle进程,或者也称为swapper进程。
0号进程是linux启动的第一个进程,它的task_struct的comm字段为"swapper",所以也称为swpper进程。
1 #define INIT_TASK_COMM "swapper"
当系统中所有的进程起来后,0号进程也就蜕化为idle进程,当一个core上没有任务可运行时就会去运行idle进程。一旦运行idle进程则此core就可以进入低功耗模式了,在ARM上就是WFI。
我们本节重点关注是0号进程是如何启动的。在linux内核中为0号进程专门定义了一个静态的task_struct的结构,称为init_task。
1 /*
2 * Set up the first task table, touch at your own risk!. Base=0,
3 * limit=0x1fffff (=2MB)
4 */
5 struct task_struct init_task
6 = {
7 #ifdef CONFIG_THREAD_INFO_IN_TASK
8 .thread_info = INIT_THREAD_INFO(init_task),
9 .stack_refcount = ATOMIC_INIT(1),
10 #endif
11 .state = 0,
12 .stack = init_stack,
13 .usage = ATOMIC_INIT(2),
14 .flags = PF_KTHREAD,
15 .prio = MAX_PRIO - 20,
16 .static_prio = MAX_PRIO - 20,
17 .normal_prio = MAX_PRIO - 20,
18 .policy = SCHED_NORMAL,
19 .cpus_allowed = CPU_MASK_ALL,
20 .nr_cpus_allowed= NR_CPUS,
21 .mm = NULL,
22 .active_mm = &init_mm,
23 .tasks = LIST_HEAD_INIT(init_task.tasks),
24 .ptraced = LIST_HEAD_INIT(init_task.ptraced),
25 .ptrace_entry = LIST_HEAD_INIT(init_task.ptrace_entry),
26 .real_parent = &init_task,
27 .parent = &init_task,
28 .children = LIST_HEAD_INIT(init_task.children),
29 .sibling = LIST_HEAD_INIT(init_task.sibling),
30 .group_leader = &init_task,
31 RCU_POINTER_INITIALIZER(real_cred, &init_cred),
32 RCU_POINTER_INITIALIZER(cred, &init_cred),
33 .comm = INIT_TASK_COMM,
34 .thread = INIT_THREAD,
35 .fs = &init_fs,
36 .files = &init_files,
37 .signal = &init_signals,
38 .sighand = &init_sighand,
39 .blocked = {{0}},
40 .alloc_lock = __SPIN_LOCK_UNLOCKED(init_task.alloc_lock),
41 .journal_info = NULL,
42 INIT_CPU_TIMERS(init_task)
43 .pi_lock = __RAW_SPIN_LOCK_UNLOCKED(init_task.pi_lock),
44 .timer_slack_ns = 50000, /* 50 usec default slack */
45 .thread_pid = &init_struct_pid,
46 .thread_group = LIST_HEAD_INIT(init_task.thread_group),
47 .thread_node = LIST_HEAD_INIT(init_signals.thread_head),
48 };
49 EXPORT_SYMBOL(init_task);
这个结构体中的成员都是静态定义了,为了简单说明,对这个结构做了简单的删减。同时我们只关注这个结构中的以下几个字段,别的先不关注。
-
- .thread_info = INIT_THREAD_INFO(init_task), 这个结构在thread_info和内核栈的关系中有详细的描述
- .stack = init_stack, init_stack就是内核栈的静态的定义
- .comm = INIT_TASK_COMM, 0号进程的名称。
在这么thread_info和stack都涉及到了Init_stack, 所以先看下init_stack在哪里设置的。
最终发现init_task是在链接脚本中定义的。
1 #define INIT_TASK_DATA(align) \\
2 . = ALIGN(align); \\
3 __start_init_task = .; \\
4 init_thread_union = .; \\
5 init_stack = .; \\
6 KEEP(*(.data..init_task)) \\
7 KEEP(*(.data..init_thread_info)) \\
8 . = __start_init_task + THREAD_SIZE; \\
9 __end_init_task = .;
在链接脚本中定义了一个INIT_TASK_DATA的宏。
其中__start_init_task就是0号进程的内核栈的基地址,当然了init_thread_union=init_task=__start_init_task的。
而0号进程的内核栈的结束地址等于__start_init_task + THREAD_SIZE, THREAD_SIZE的大小在ARM64一般是16K,或者32K。则__end_init_task就是0号进程的内核栈的结束地址。
idle进程由系统自动创建, 运行在内核态,idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换。
二、Linux内核的启动
熟悉linux内核的朋友都知道,linux内核的启动 ,一般都是有bootloader来完成装载,bootloader中会做一些硬件的初始化,然后会跳转到linux内核的运行地址上去。
如果熟悉ARM架构的盆友也清楚,ARM64架构分为EL0, EL1, EL2, EL3。正常的启动一般是从高特权模式向低特权模式启动的。通常来说ARM64是先运行EL3,再EL2,然后从EL2就trap到EL1,也就是我们的Linux内核。
我们来看下Linux内核启动的代码。
1 代码路径:arch/arm64/kernel/head.S文件中
2 /*
3 * Kernel startup entry point.
4 * ---------------------------
5 *
6 * The requirements are:
7 * MMU = off, D-cache = off, I-cache = on or off,
8 * x0 = physical address to the FDT blob.
9 *
10 * This code is mostly position independent so you call this at
11 * __pa(PAGE_OFFSET + TEXT_OFFSET).
12 *
13 * Note that the callee-saved registers are used for storing variables
14 * that are useful before the MMU is enabled. The allocations are described
15 * in the entry routines.
16 */
17
18 /*
19 * The following callee saved general purpose registers are used on the
20 * primary lowlevel boot path:
21 *
22 * Register Scope Purpose
23 * x21 stext() .. start_kernel() FDT pointer passed at boot in x0
24 * x23 stext() .. start_kernel() physical misalignment/KASLR offset
25 * x28 __create_page_tables() callee preserved temp register
26 * x19/x20 __primary_switch() callee preserved temp registers
27 */
28 ENTRY(stext)
29 bl preserve_boot_args
30 bl el2_setup // Drop to EL1, w0=cpu_boot_mode
31 adrp x23, __PHYS_OFFSET
32 and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
33 bl set_cpu_boot_mode_flag
34 bl __create_page_tables
35 /*
36 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
37 * details.
38 * On return, the CPU will be ready for the MMU to be turned on and
39 * the TCR will have been set.
40 */
41 bl __cpu_setup // initialise processor
42 b __primary_switch
43 ENDPROC(stext)
上面就是内核在调用start_kernel之前做的主要工作了。
-
- preserve_boot_args用来保留bootloader传递的参数,比如ARM上通常的dtb的地址
- el2_setup:从注释上来看是, 用来trap到EL1,说明我们在运行此指令前还在EL2
- __create_page_tables: 用来创建页表,linux才有的是页面管理物理内存的,在使用虚拟地址之前需要设置好页面,然后会打开MMU。目前还是运行在物理地址上的
- __primary_switch: 主要任务是完成MMU的打开工作
1 __primary_switch: 2 adrp x1, init_pg_dir 3 bl __enable_mmu 4 ldr x8, =__primary_switched 5 adrp x0, __PHYS_OFFSET 6 br x8 7 ENDPROC(__primary_switch)
-
- 主要是调用__enable_mmu来打开mmu,之后我们访问的就是虚拟地址了
- 调用__primary_switched来设置0号进程的运行内核栈,然后调用start_kernel函数
1 /* 2 * The following fragment of code is executed with the MMU enabled. 3 * 4 * x0 = __PHYS_OFFSET 5 */ 6 __primary_switched: 7 adrp x4, init_thread_union 8 add sp, x4, #THREAD_SIZE 9 adr_l x5, init_task 10 msr sp_el0, x5 // Save thread_info 11 12 adr_l x8, vectors // load VBAR_EL1 with virtual 13 msr vbar_el1, x8 // vector table address 14 isb 15 16 stp xzr, x30, [sp, #-16]! 17 mov x29, sp 18 19 str_l x21, __fdt_pointer, x5 // Save FDT pointer 20 21 ldr_l x4, kimage_vaddr // Save the offset between 22 sub x4, x4, x0 // the kernel virtual and 23 str_l x4, kimage_voffset, x5 // physical mappings 24 25 // Clear BSS 26 adr_l x0, __bss_start 27 mov x1, xzr 28 adr_l x2, __bss_stop 29 sub x2, x2, x0 30 bl __pi_memset 31 dsb ishst // Make zero page visible to PTW 32 33 add sp, sp, #16 34 mov x29, #0 35 mov x30, #0 36 b start_kernel 37 ENDPROC(__primary_switched)
- init_thread_union就是我们在链接脚本中定义的,也就是0号进程的内核栈的栈底
- add sp, x4, #THREAD_SIZE: 设置堆栈指针SP的值,就是内核栈的栈底+THREAD_SIZE的大小。现在SP指到了内核栈的顶端
- 最终通过b start_kernel就跳转到我们熟悉的linux内核入口处了。
至此0号进程就已经运行起来了。
三、1号进程
3.1 1号进程的创建
当一条b start_kernel指令运行后,内核就开始的内核的全面初始化操作。
1 asmlinkage __visible void __init start_kernel(void)
2 {
3 char *command_line;
4 char *after_dashes;
5
6 set_task_stack_end_magic(&init_task);
7 smp_setup_processor_id();
8 debug_objects_early_init();
9
10 cgroup_init_early();
11
12 local_irq_disable();
13 early_boot_irqs_disabled = true;
14
15 /*
16 * Interrupts are still disabled. Do necessary setups, then
17 * enable them.
18 */
19 boot_cpu_init();
20 page_address_init();
21 pr_notice("%s", linux_banner);
22 setup_arch(&command_line);
23 /*
24 * Set up the the initial canary and entropy after arch
25 * and after adding latent and command line entropy.
26 */
27 add_latent_entropy();
28 add_device_randomness(command_line, strlen(command_line));
29 boot_init_stack_canary();
30 mm_init_cpumask(&init_mm);
31 setup_command_line(command_line);
32 setup_nr_cpu_ids();
33 setup_per_cpu_areas();
34 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
35 boot_cpu_hotplug_init();
36
37 build_all_zonelists(NULL);
38 page_alloc_init();
39 。。。。。。。
40 acpi_subsystem_init();
41 arch_post_acpi_subsys_init();
42 sfi_init_late();
43
44 /* Do the rest non-__init\'ed, we\'re now alive */
45 arch_call_rest_init();
46 }
47
48 void __init __weak arch_call_rest_init(void)
49 {
50 rest_init();
start_kernel函数就是内核各个重要子系统的初始化,比如mm, cpu, sched, irq等等。最后会调用一个rest_init剩余部分初始化,start_kernel在其最后一个函数rest_init的调用中,会通过kernel_thread来生成一个内核进程,后者则会在新进程环境下调 用kernel_init函数,kernel_init一个让人感兴趣的地方在于它会调用run_init_process来执行根文件系统下的 /sbin/init等程序。
1 noinline void __ref rest_init(void)
2 {
3 struct task_struct *tsk;
4 int pid;
5
6 rcu_scheduler_starting();
7 /*
8 * We need to spawn init first so that it obtains pid 1, however
9 * the init task will end up wanting to create kthreads, which, if
10 * we schedule it before we create kthreadd, will OOPS.
11 */
12 pid = kernel_thread(kernel_init, NULL, CLONE_FS);
13 /*
14 * Pin init on the boot CPU. Task migration is not properly working
15 * until sched_init_smp() has been run. It will set the allowed
16 * CPUs for init to the non isolated CPUs.
17 */
18 rcu_read_lock();
19 tsk = find_task_by_pid_ns(pid, &init_pid_ns);
20 set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
21 rcu_read_unlock();
22
23 numa_default_policy();
24 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
25 rcu_read_lock();
26 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
27 rcu_read_unlock();
28
29 /*
30 * Enable might_sleep() and smp_processor_id() checks.
31 * They cannot be enabled earlier because with CONFIG_PREEMPT=y
32 * kernel_thread() would trigger might_sleep() splats. With
33 * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
34 * already, but it\'s stuck on the kthreadd_done completion.
35 */
36 system_state = SYSTEM_SCHEDULING;
37
38 complete(&kthreadd_done);
39
40 }
在这个rest_init函数中我们只关系两点:
-
- pid = kernel_thread(kernel_init, NULL, CLONE_FS);
- pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
1 /*
2 * Create a kernel thread.
3 */
4 pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
5 {
6 return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
7 (unsigned long)arg, NULL, NULL, 0);
8 }
很明显这是创建了两个内核线程,而kernel_thread最终会调用do_fork根据参数的不同来创建一个进程或者内核线程。关系do_fork的实现我们在后面会做详细的介绍。当内核线程创建成功后就会调用设置的回调函数。
当kernel_thread(kernel_init)成功返回后,就会调用kernel_init内核线程,其实这时候1号进程已经产生了。1号进程的执行函数就是kernel_init, 这个函数被定义init/main.c中,接下来看下kernel_init主要做什么事情。
1 static int __ref kernel_init(void *unused)
2 {
3 int ret;
4
5 kernel_init_freeable();
6 /* need to finish all async __init code before freeing the memory */
7 async_synchronize_full();
8 ftrace_free_init_mem();
9 free_initmem();
10 mark_readonly();
11
12 /*
13 * Kernel mappings are now finalized - update the userspace page-table
14 * to finalize PTI.
15 */
16 pti_finalize();
17
18 system_state = SYSTEM_RUNNING;
19 numa_default_policy();
20
21 rcu_end_inkernel_boot();
22
23 if (ramdisk_execute_command) {
24 ret = run_init_process(ramdisk_execute_command);
25 if (!ret)
26 return 0;
27 pr_err("Failed to execute %s (error %d)\\n",
28 ramdisk_execute_command, ret);
29 }
30
31 /*
32 * We try each of these until one succeeds.
33 *
34 * The Bourne shell can be used instead of init if we are
35 * trying to recover a really broken machine.
36 */
37 if (execute_command) {
38 ret = run_init_process(execute_command);
39 if (!ret)
40 return 0;
41 panic("Requested init %s failed (error %d).",
42 execute_command, ret);
43 }
44 if (!try_to_run_init_process("/sbin/init") ||
45 !try_to_run_init_process("/etc/init") ||
46 !try_to_run_init_process("/bin/init") ||
47 !try_to_run_init_process("/bin/sh"))
48 return 0;
49
50 panic("No working init found. Try passing init= option to kernel. "
51 "See Linux Documentation/admin-guide/init.rst for guidance.");
52 }
-
- kernel_init_freeable函数中就会做各种外设驱动的初始化。
- 最主要的工作就是通过execve执行/init可以执行文件。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号...的若干终端注册进程getty。每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。
我们通常将init称为1号进程,其实在刚才kernel_init的时候1号线程已经创建成功,也可以理解kernel_init是1号进程的内核态,而我们所熟知的init进程是用户态的,调用execve函数之前属于内核态,调用之后就属于用户态了,执行的代码段与0号进程不在一样。
1号内核线程负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。
至此1号进程就完美的创建成功了,而且也成功执行了init可执行文件。
3.2 init进程
随后,1号进程调用do_execve运行可执行程序init,并演变成用户态1号进程,即init进程。
init进程是linux内核启动的第一个用户级进程。init有许多很重要的任务,比如像启动getty(用于用户登录)、实现运行级别、以及处理孤立进程。
它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号…的若干终端注册进程getty。
每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数do_execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。
上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程
注意,上述过程描述中提到:1号内核进程调用执行init函数并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
-
- kernel_init函数在内核态运行,是内核代码
- init进程是内核启动并运行的第一个用户进程,运行在用户态下。
- 一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。
当内核启动了自己之后(已被装入内存、已经开始运行、已经初始化了所有的设备驱动程序和数据结构等等),通过启动用户级程序init来完成引导进程的内核部分。因此,init总是第一个进程(它的进程号总是1)。
当init开始运行,它通过执行一些管理任务来结束引导进程,例如检查文件系统、清理/tmp、启动各种服务以及为每个终端和虚拟控制台启动getty,在这些地方用户将登录系统。
在系统完全起来之后,init为每个用户已退出的终端重启getty(这样下一个用户就可以登录)。init同样也收集孤立的进程:当一个进程启动了一个子进程并且在子进程之前终止了,这个子进程立刻成为init的子进程。对于各种技术方面的原因来说这是很重要的,知道这些也是有好处的,因为这便于理解进程列表和进程树图。init的变种很少。绝大多数Linux发行版本使用sysinit(由Miguel van Smoorenburg著),它是基于System V的init设计。UNIX的BSD版本有一个不同的init。最主要的不同在于运行级别:System V有而BSD没有(至少是传统上说)。这种区别并不是主要的。在此我们仅讨论sysvinit。 配置init以启动getty:/etc/inittab文件。
3.3 init程序
1号进程通过execve执行init程序来进入用户空间,成为init进程,那么这个init在哪里呢
内核在几个位置上来查寻init,这几个位置以前常用来放置init,但是init的最适当的位置(在Linux系统上)是/sbin/init。如果内核没有找到init,它就会试着运行/bin/sh,如果还是失败了,那么系统的启动就宣告失败了。
因此init程序是一个可以又用户编写的进程, 如果希望看init程序源码的朋友,可以参见。
init包 | 说明 | 学习链接 |
sysvinit |
早期一些版本使用的初始化进程工具, 目前在逐渐淡出linux历史舞台, sysvinit 就是 system V 风格的 init 系统,顾名思义,它源于 System V 系列 UNIX。它提供了比 BSD 风格 init 系统更高的灵活性。是已经风行了几十年的 UNIX init 系统,一直被各类 Linux 发行版所采用。 |
浅析 Linux 初始化 init 系统(1):sysvinit |
upstart | debian, Ubuntu等系统使用的initdaemon | 浅析 Linux 初始化 init 系统(2): UpStart |
systemd | Systemd 是 Linux 系统中最新的初始化系统(init),它主要的设计目标是克服 sysvinit 固有的缺点,提高系统的启动速度 | 浅析 Linux 初始化 init 系统(3) Systemd |
Ubuntu等使用deb包的系统可以通过dpkg -S查看程序所在的包
CentOS等使用rpm包的系统可以通过rpm -qf查看系统程序所在的包
四、2号进程
2号进程,是由1号进程创建的。而且2号进程是所有内核线程父进程。
2号进程就是刚才rest_init中创建的另外一个内核线程。kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
当kernel_thread(kthreadd)返回时,2号进程已经创建成功了。而且会回调kthreadd函数。
1 int kthreadd(void *unused)
2 {
3 struct task_struct *tsk = current;
4
5 /* Setup a clean context for our children to inherit. */
6 set_task_comm(tsk, "kthreadd");
7 ignore_signals(tsk);
8 set_cpus_allowed_ptr(tsk, cpu_all_mask);
9 set_mems_allowed(node_states[N_MEMORY]);
10
11 current->flags |= PF_NOFREEZE;
12 cgroup_init_kthreadd();
13
14 for (;;) {
15 set_current_state(TASK_INTERRUPTIBLE);
16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);
19
20 spin_lock(&kthread_create_lock);
21 while (!list_empty(&kthread_create_list)) {
22 struct kthread_create_info *create;
23
24 create = list_entry(kthread_create_list.next,
25 struct kthread_create_info, list);
26 list_del_init(&create->list);
27 spin_unlock(&kthread_create_lock);
28
29 create_kthread(create);
30
31 spin_lock(&kthread_create_lock);
32 }
33 spin_unlock(&kthread_create_lock);
34 }
35
36 return 0;
37 }
这段代码大概的意思也很简单明显;
-
- 设置当前进程的名字为"kthreadd",也就是task_struct的comm字段
- 然后就是while循环,设置当前的进程的状态是TASK_INTERRUPTIBLE是可以中断的
- 判断kthread_create_list链表是不是空,如果是空则就调度出去,让出cpu
- 如果不是空,则从链表中取出一个,然后调用kthread_create去创建一个内核线程。
- 所以说所有的内核线程的父进程都是2号进程,也就是kthreadd。
五、总结
linux启动的第一个进程是0号进程,是静态创建的,称为idle进程或者swapper进程。
在0号进程启动后会接连创建两个进程,分别是1号进程和2和进程。
以上是关于Linux0号进程,1号进程,2号进程的主要内容,如果未能解决你的问题,请参考以下文章
[架构之路-31]:目标系统 - 系统软件 - Linux OS 什么是Linux1号进程? init进程与systemD的比较?
Android 逆向Android 系统文件分析 ( /proc/pid 进程号对应进程目录 | oom_adj | maps | smaps | mem | task | environ )(代码片
Android 逆向Android 系统文件分析 ( /proc/pid 进程号对应进程目录 | oom_adj | maps | smaps | mem | task | environ )(代码片