Linux 内核启动流程

目录

  • 链接脚本vmlinux.lds
  • [Linux 内核启动流程分析](#Linux 内核启动流程分析)
    • [Linux 内核入口stext](#Linux 内核入口stext)
    • [__mmap_switched 函数](#__mmap_switched 函数)
    • [start_kernel 函数](#start_kernel 函数)
    • [rest_init 函数](#rest_init 函数)
    • [init 进程](#init 进程)

看完Linux 内核的顶层Makefile 以后再来看Linux 内核的大致启动流程,Linux 内核的启动流程要比uboot 复杂的多,涉及到的内容也更多,因此本章我们就大致的了解一下Linux 内核的启动流程。

链接脚本vmlinux.lds

要分析Linux 启动流程,同样需要先编译一下Linux 源码,因为有很多文件是需要编译才会生成的。首先分析Linux 内核的连接脚本文件arch/arm/kernel/vmlinux.lds,通过链接脚本可以找到Linux 内核的第一行程序是从哪里执行的。vmlinux.lds 中有如下代码:

bash 复制代码
示例代码36.1.1 vmlinux.lds 链接脚本
492 OUTPUT_ARCH(arm)
493 ENTRY(stext)
494 jiffies = jiffies_64;
495 SECTIONS
496 {
497 /*
498 * XXX: The linker does not define how output sections are
499 * assigned to input sections when there are multiple statements
500 * matching the same input section name. There is no documented
501 * order of matching.
502 *
503 * unwind exit sections must be discarded before the rest of the
504 * unwind sections get included.
505 */
506 /DISCARD/ : {
507 *(.ARM.exidx.exit.text)
508 *(.ARM.extab.exit.text)
509
......
645 }

第493 行的ENTRY 指明了了Linux 内核入口,入口为stext,stext 定义在文件arch/arm/kernel/head.S 中,因此要分析Linux 内核的启动流程,就得先从文件arch/arm/kernel/head.S 的stext 处开始分析。

Linux 内核启动流程分析

Linux 内核入口stext

stext 是Linux 内核的入口地址,在文件arch/arm/kernel/head.S 中有如下所示提示内容:

bash 复制代码
示例代码36.2.1.1 arch/arm/kernel/head.S 代码段
/*
* Kernel startup entry point.
* ---------------------------
*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr, r2 = atags or dtb pointer.
.....
*/

根据示例代码36.2.1.1 中的注释,Linux 内核启动之前要求如下:

①、关闭MMU。

②、关闭D-cache。

③、I-Cache 无所谓。

④、r0=0。

⑤、r1=machine nr(也就是机器ID)。

⑥、r2=atags 或者设备树(dtb)首地址。

Linux 内核的入口点stext 其实相当于内核的入口函数,stext 函数内容如下:

bash 复制代码
示例代码36.2.1.2 arch/arm/kernel/head.S 代码段
80 ENTRY(stext)
......
91 @ ensure svc mode and all interrupts masked
92 safe_svcmode_maskall r9
93
94 mrc p15, 0, r9, c0, c0 @ get processor id
95 bl __lookup_processor_type @ r5=procinfo r9=cpuid
96 movs r10, r5 @ invalid processor (r5=0)?
97 THUMB( it eq ) @ force fixup-able long branch encoding
98 beq __error_p @ yes, error 'p'
99
......
107
108 #ifndef CONFIG_XIP_KERNEL
......
113 #else
114 ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case
115 #endif
116
117 /*
118 * r1 = machine no, r2 = atags or dtb,
119 * r8 = phys_offset, r9 = cpuid, r10 = procinfo
120 */
121 bl __vet_atags
......
128 bl __create_page_tables
129
130 /*
131 * The following calls CPU specific code in a position independent
132 * manner. See arch/arm/mm/proc-*.S for details. r10 = base of
133 * xxx_proc_info structure selected by __lookup_processor_type
134 * above. On return, the CPU will be ready for the MMU to be
135 * turned on, and r0 will hold the CPU control register value.
136 */
137 ldr r13, =__mmap_switched @ address to jump to after
138 @ mmu has been enabled
139 adr lr, BSYM(1f) @ return (PIC) address
140 mov r8, r4 @ set TTBR1 to swapper_pg_dir
141 ldr r12, [r10, #PROCINFO_INITFUNC]
142 add r12, r12, r10
143 ret r12
144 1: b __enable_mmu
145 ENDPROC(stext)

第92 行,调用函数safe_svcmode_maskall 确保CPU 处于SVC 模式,并且关闭了所有的中断。safe_svcmode_maskall 定义在文件arch/arm/include/asm/assembler.h 中。

第94 行,读处理器ID,ID 值保存在r9 寄存器中。

第95 行,调用函数__lookup_processor_type 检查当前系统是否支持此CPU,如果支持就获取procinfo 信息。procinfo 是proc_info_list 类型的结构体,proc_info_list 在文件arch/arm/include/asm/procinfo.h 中的定义如下:

bash 复制代码
示例代码36.2.1.3 proc_info_list 结构体
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};

Linux 内核将每种处理器都抽象为一个proc_info_list 结构体,每种处理器都对应一个procinfo。因此可以通过处理器ID 来找到对应的procinfo 结构,__lookup_processor_type 函数找到对应处理器的procinfo 以后会将其保存到r5 寄存器中。

继续回到示例代码36.2.1.2 中,第121 行,调用函数__vet_atags 验证atags 或设备树(dtb)的合法性。函数__vet_atags 定义在文件arch/arm/kernel/head-common.S 中。

第128 行,调用函数__create_page_tables 创建页表。

第137 行,将函数__mmap_switched 的地址保存到r13 寄存器中。__mmap_switched 定义在文件arch/arm/kernel/head-common.S,__mmap_switched 最终会调用start_kernel 函数。

第144 行,调用__enable_mmu 函数使能MMU ,__enable_mmu 定义在文件

arch/arm/kernel/head.S 中。__enable_mmu 最终会通过调用__turn_mmu_on 来打开MMU,

__turn_mmu_on 最后会执行r13 里面保存的__mmap_switched 函数。

__mmap_switched 函数

__mmap_switched 函数定义在文件arch/arm/kernel/head-common.S 中,函数代码如下:

bash 复制代码
示例代码36.2.2.1 __mmap_switched 函数
81 __mmap_switched:
82 adr r3, __mmap_switched_data
83
84 ldmia r3!, {r4, r5, r6, r7}
85 cmp r4, r5 @ Copy data segment if needed
86 1: cmpne r5, r6
87 ldrne fp, [r4], #4
88 strne fp, [r5], #4
89 bne 1b
90
91 mov fp, #0 @ Clear BSS (and zero fp)
92 1: cmp r6, r7
93 strcc fp, [r6],#4
94 bcc 1b
95
96 ARM( ldmia r3, {r4, r5, r6, r7, sp})
97 THUMB( ldmia r3, {r4, r5, r6, r7} )
98 THUMB( ldr sp, [r3, #16] )
99 str r9, [r4] @ Save processor ID
100 str r1, [r5] @ Save machine type
101 str r2, [r6] @ Save atags pointer
102 cmp r7, #0
103 strne r0, [r7] @ Save control register values
104 b start_kernel
105 ENDPROC(__mmap_switched)

第104 行最终调用start_kernel 来启动Linux 内核,start_kernel 函数定义在文件init/main.c中。

start_kernel 函数

start_kernel 通过调用众多的子函数来完成Linux 启动之前的一些初始化工作,由于start_kernel 函数里面调用的子函数太多,而这些子函数又很复杂,因此我们简单的来看一下一些重要的子函数。精简并添加注释后的start_kernel 函数内容如下:

c 复制代码
示例代码36.2.3.1 start_kernel 函数
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
lockdep_init(); /* lockdep是死锁检测模块,此函数会初始化
* 两个hash表。此函数要求尽可能早的执行!
*/
set_task_stack_end_magic(&init_task);/* 设置任务栈结束魔术数,
*用于栈溢出检测
*/
smp_setup_processor_id(); /* 跟SMP有关(多核处理器),设置处理器ID。
* 有很多资料说ARM架构下此函数为空函数,那是因
* 为他们用的老版本Linux,而那时候ARM还没有多
* 核处理器。
*/
debug_objects_early_init(); /* 做一些和debug有关的初始化*/
boot_init_stack_canary(); /* 栈溢出检测初始化*/
cgroup_init_early(); /* cgroup初始化,cgroup用于控制Linux系统资源*/
local_irq_disable(); /* 关闭当前CPU中断*/
early_boot_irqs_disabled = true;
/*
* 中断关闭期间做一些重要的操作,然后打开中断
*/
boot_cpu_init(); /* 跟CPU有关的初始化*/
page_address_init(); /* 页地址相关的初始化*/
pr_notice("%s", linux_banner);/* 打印Linux版本号、编译时间等信息*/
setup_arch(&command_line); /* 架构相关的初始化,此函数会解析传递进来的
* ATAGS或者设备树(DTB)文件。会根据设备树里面
* 的model和compatible这两个属性值来查找
* Linux是否支持这个单板。此函数也会获取设备树
* 中chosen节点下的bootargs属性值来得到命令
* 行参数,也就是uboot中的bootargs环境变量的
* 值,获取到的命令行参数会保存到
*command_line中。
*/
mm_init_cpumask(&init_mm); /* 看名字,应该是和内存有关的初始化*/
setup_command_line(command_line); /* 好像是存储命令行参数*/
setup_nr_cpu_ids(); /* 如果只是SMP(多核CPU)的话,此函数用于获取
* CPU核心数量,CPU数量保存在变量
* nr_cpu_ids中。
*/
setup_per_cpu_areas(); /* 在SMP系统中有用,设置每个CPU的per-cpu数据*/
smp_prepare_boot_cpu();
build_all_zonelists(NULL, NULL); /* 建立系统内存页区(zone)链表*/
page_alloc_init(); /* 处理用于热插拔CPU的页*/
/* 打印命令行信息*/
pr_notice("Kernel command line: %s\n", boot_command_line);
parse_early_param(); /* 解析命令行中的console参数*/
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
set_init_arg);
jump_label_init();
setup_log_buf(0); /* 设置log使用的缓冲区*/
pidhash_init(); /* 构建PID哈希表,Linux中每个进程都有一个ID,
* 这个ID叫做PID。通过构建哈希表可以快速搜索进程
* 信息结构体。
*/
vfs_caches_init_early(); /* 预先初始化vfs(虚拟文件系统)的目录项和
* 索引节点缓存
*/
sort_main_extable(); /* 定义内核异常列表*/
trap_init(); /* 完成对系统保留中断向量的初始化*/
mm_init(); /* 内存管理初始化*/
sched_init(); /* 初始化调度器,主要是初始化一些结构体*/
preempt_disable(); /* 关闭优先级抢占*/
if (WARN(!irqs_disabled(), /* 检查中断是否关闭,如果没有的话就关闭中断*/
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
idr_init_cache(); /* IDR初始化,IDR是Linux内核的整数管理机
* 制,也就是将一个整数ID与一个指针关联起来。
*/
rcu_init(); /* 初始化RCU,RCU全称为Read Copy Update(读-拷贝修改) */
trace_init(); /* 跟踪调试相关初始化*/
context_tracking_init();
radix_tree_init(); /* 基数树相关数据结构初始化*/
early_irq_init(); /* 初始中断相关初始化,主要是注册irq_desc结构体变
* 量,因为Linux内核使用irq_desc来描述一个中断。
*/
init_IRQ(); /* 中断初始化*/
tick_init(); /* tick初始化*/
rcu_init_nohz();
init_timers(); /* 初始化定时器*/
hrtimers_init(); /* 初始化高精度定时器*/
softirq_init(); /* 软中断初始化*/
timekeeping_init();
time_init(); /* 初始化系统时间*/
sched_clock_postinit();
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), "Interrupts were enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable(); /* 使能中断*/
kmem_cache_init_late(); /* slab初始化,slab是Linux内存分配器*/
console_init(); /* 初始化控制台,之前printk打印的信息都存放
* 缓冲区中,并没有打印出来。只有调用此函数
* 初始化控制台以后才能在控制台上打印信息。
*/
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,
panic_param);
lockdep_info();/* 如果定义了宏CONFIG_LOCKDEP,那么此函数打印一些信息。*/
locking_selftest() /* 锁自测*/
......
page_ext_init();
debug_objects_mem_init();
kmemleak_init(); /* kmemleak初始化,kmemleak用于检查内存泄漏*/
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
sched_clock_init();
calibrate_delay(); /* 测定BogoMIPS值,可以通过BogoMIPS来判断CPU的性能
* BogoMIPS设置越大,说明CPU性能越好。
*/
pidmap_init(); /* PID位图初始化*/
anon_vma_init(); /* 生成anon_vma slab缓存*/
acpi_early_init();
......
thread_info_cache_init();
cred_init(); /* 为对象的每个用于赋予资格(凭证) */
fork_init(); /* 初始化一些结构体以使用fork函数*/
proc_caches_init(); /* 给各种资源管理结构分配缓存*/
buffer_init(); /* 初始化缓冲缓存*/
key_init(); /* 初始化密钥*/
security_init(); /* 安全相关初始化*/
dbg_late_init();
vfs_caches_init(totalram_pages); /* 为VFS创建缓存*/
signals_init(); /* 初始化信号*/
page_writeback_init(); /* 页回写初始化*/
proc_root_init(); /* 注册并挂载proc文件系统*/
nsfs_init();
cpuset_init(); /* 初始化cpuset,cpuset是将CPU和内存资源以逻辑性
* 和层次性集成的一种机制,是cgroup使用的子系统之一
*/
cgroup_init(); /* 初始化cgroup */
taskstats_init_early(); /* 进程状态初始化*/
delayacct_init();
check_bugs(); /* 检查写缓冲一致性*/
acpi_subsystem_init();
sfi_init_late();
if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_late_init();
efi_free_boot_services();
}
ftrace_init();
rest_init(); /* rest_init函数*/
}

start_kernel 里面调用了大量的函数,每一个函数都是一个庞大的知识点,如果想要学习Linux 内核,那么这些函数就需要去详细的研究。本教程注重于嵌入式Linux 入门,因此不会去讲太多关于Linux 内核的知识。start_kernel 函数最后调用了rest_init,接下来简单看一下rest_init函数。

rest_init 函数

rest_init 函数定义在文件init/main.c 中,函数内容如下:

c 复制代码
示例代码36.2.4.1 rest_init 函数
383 static noinline void __init_refok rest_init(void)
384 {
385 int pid;
386
387 rcu_scheduler_starting();
388 smpboot_thread_init();
389 /*
390 * We need to spawn init first so that it obtains pid 1, however
391 * the init task will end up wanting to create kthreads, which,
392 * if we schedule it before we create kthreadd, will OOPS.
393 */
394 kernel_thread(kernel_init, NULL, CLONE_FS);
395 numa_default_policy();
396 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
397 rcu_read_lock();
398 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
399 rcu_read_unlock();
400 complete(&kthreadd_done);
401
402 /*
403 * The boot idle thread must execute schedule()
404 * at least once to get things moving:
405 */
406 init_idle_bootup_task(current);
407 schedule_preempt_disabled();
408 /* Call into cpu_idle with preempt disabled */
409 cpu_startup_entry(CPUHP_ONLINE);
410 }

第387 行,调用函数rcu_scheduler_starting,启动RCU 锁调度器

第394 行,调用函数kernel_thread 创建kernel_init 进程,也就是大名鼎鼎的init 内核进程。

init 进程的PID 为1。init 进程一开始是内核进程(也就是运行在内核态),后面init 进程会在根文件系统中查找名为"init"这个程序,这个"init"程序处于用户态,通过运行这个"init"程序,init 进程就会实现从内核态到用户态的转变。

第396 行,调用函数kernel_thread 创建kthreadd 内核进程,此内核进程的PID 为2。kthreadd进程负责所有内核进程的调度和管理。

第409 行,最后调用函数cpu_startup_entry 来进入idle 进程,cpu_startup_entry 会调用cpu_idle_loop,cpu_idle_loop 是个while 循环,也就是idle 进程代码。idle 进程的PID 为0,idle进程叫做空闲进程,如果学过FreeRTOS 或者UCOS 的话应该听说过空闲任务。idle 空闲进程就和空闲任务一样,当CPU 没有事情做的时候就在idle 空闲进程里面"瞎逛游",反正就是给CPU 找点事做。当其他进程要工作的时候就会抢占idle 进程,从而夺取CPU 使用权。其实大家应该可以看到idle 进程并没有使用kernel_thread 或者fork 函数来创建,因为它是有主进程演变而来的。

在Linux 终端中输入"ps -A"就可以打印出当前系统中的所有进程,其中就能看到init 进程和kthreadd 进程,如图36.2.4.1 所示:

图36.2.4.1 Linux 系统当前进程

从图36.2.4.1 可以看出,init 进程的PID 为1,kthreadd 进程的PID 为2。之所以图36.2.4.1中没有显示PID 为0 的idle 进程,那是因为idle 进程是内核进程。我们接下来重点看一下init进程,kernel_init 就是init 进程的进程函数。

init 进程

kernel_init 函数就是init 进程具体做的工作,定义在文件init/main.c 中,函数内容如下:

c 复制代码
示例代码36.2.5.1 kernel_init 函数
928 static int __ref kernel_init(void *unused)
929 {
930 int ret;
931
932 kernel_init_freeable(); /* init进程的一些其他初始化工作*/
933 /* need to finish all async __init code before freeing the
memory */
934 async_synchronize_full(); /* 等待所有的异步调用执行完成*/
935 free_initmem(); /* 释放init段内存*/
936 mark_rodata_ro();
937 system_state = SYSTEM_RUNNING; /* 标记系统正在运行*/
938 numa_default_policy();
939
940 flush_delayed_fput();
941
942 if (ramdisk_execute_command) {
943 ret = run_init_process(ramdisk_execute_command);
944 if (!ret)
945 return 0;
946 pr_err("Failed to execute %s (error %d)\n",
947 ramdisk_execute_command, ret);
948 }
949
950 /*
951 * We try each of these until one succeeds.
952 *
953 * The Bourne shell can be used instead of init if we are
954 * trying to recover a really broken machine.
955 */
956 if (execute_command) {
957 ret = run_init_process(execute_command);
958 if (!ret)
959 return 0;
960 panic("Requested init %s failed (error %d).",
961 execute_command, ret);
962 }
963 if (!try_to_run_init_process("/sbin/init") ||
964 !try_to_run_init_process("/etc/init") ||
965 !try_to_run_init_process("/bin/init") ||
966 !try_to_run_init_process("/bin/sh"))
967 return 0;
968
969 panic("No working init found. Try passing init= option to kernel. "
970 "See Linux Documentation/init.txt for guidance.");
971 }

第932 行,kernel_init_freeable 函数用于完成init 进程的一些其他初始化工作,稍后再来具体看一下此函数。

第940 行,ramdisk_execute_command 是一个全局的char 指针变量,此变量值为"/init",也就是根目录下的init 程序。ramdisk_execute_command 也可以通过uboot 传递,在bootargs 中使用"rdinit=xxx"即可,xxx 为具体的init 程序名字。

第943 行,如果存在"/init"程序的话就通过函数run_init_process 来运行此程序。

第956 行,如果ramdisk_execute_command 为空的话就看execute_command 是否为空,反正不管如何一定要在根文件系统中找到一个可运行的init 程序。execute_command 的值是通过uboot 传递,在bootargs 中使用"init=xxxx"就可以了,比如"init=/linuxrc"表示根文件系统中的linuxrc 就是要执行的用户空间init 程序。

第963~966 行,如果ramdisk_execute_command 和execute_command 都为空,那么就依次查找"/sbin/init"、"/etc/init"、"/bin/init"和"/bin/sh",这四个相当于备用init 程序,如果这四个也不存在,那么Linux 启动失败!

第969 行,如果以上步骤都没有找到用户空间的init 程序,那么就提示错误发生!

最后来简单看一下kernel_init_freeable 函数,前面说了,kernel_init 会调用此函数来做一些init 进程初始化工作。kernel_init_freeable 定义在文件init/main.c 中,缩减后的函数内容如下:

c 复制代码
示例代码36.2.5.2 kernel_init_freeable 函数
973 static noinline void __init kernel_init_freeable(void)
974 {
975 /*
976 * Wait until kthreadd is all set-up.
977 */
978 wait_for_completion(&kthreadd_done);/* 等待kthreadd进程准备就绪*/
......
998
999 smp_init(); /* SMP初始化*/
1000 sched_init_smp(); /* 多核(SMP)调度初始化*/
1001
1002 do_basic_setup(); /* 设备初始化都在此函数中完成*/
1003
1004 /* Open the /dev/console on the rootfs, this should never fail */
1005 if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
1006 pr_err("Warning: unable to open an initial console.\n");
1007
1008 (void) sys_dup(0);
1009 (void) sys_dup(0);
1010 /*
1011 * check if there is an early userspace init. If yes, let it do
1012 * all the work
1013 */
1014
1015 if (!ramdisk_execute_command)
1016 ramdisk_execute_command = "/init";
1017
1018 if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
1019 ramdisk_execute_command = NULL;
1020 prepare_namespace();
1021 }
1022
1023 /*
1024 * Ok, we have completed the initial bootup, and
1025 * we're essentially up and running. Get rid of the
1026 * initmem segments and start the user-mode stuff..
1027 *
1028 * rootfs is available now, try loading the public keys
1029 * and default modules
1030 */
1031
1032 integrity_load_keys();
1033 load_default_modules();
1034 }

第1002 行,do_basic_setup 函数用于完成Linux 下设备驱动初始化工作!非常重要。

do_basic_setup 会调用driver_init 函数完成Linux 下驱动模型子系统的初始化。

第1005 行,打开设备"/dev/console",在Linux 中一切皆为文件!因此"/dev/console"也是一个文件,此文件为控制台设备。每个文件都有一个文件描述符,此处打开的"/dev/console"文件描述符为0,作为标准输入(0)。

第1008 和1009 行,sys_dup 函数将标准输入(0)的文件描述符复制了2 次,一个作为标准输出(1),一个作为标准错误(2)。这样标准输入、输出、错误都是/dev/console 了。console 通过uboot 的bootargs 环境变量设置,"console=ttymxc0,115200"表示将/dev/ttymxc0 设置为console,也就是I.MX6U 的串口1。当然,也可以设置其他的设备为console,比如虚拟控制台tty1,设置tty1 为console 就可以在LCD 屏幕上看到系统的提示信息。

第1020 行,调用函数prepare_namespace 来挂载根文件系统。根文件系统也是由命令行参数指定的,就是uboot 的bootargs 环境变量。比如"root=/dev/mmcblk1p2 rootwait rw"就表示根文件系统在/dev/mmcblk1p2 中,也就是EMMC 的分区2 中。

Linux 内核启动流程就分析到这里,Linux 内核最终是需要和根文件系统打交道的,需要挂载根文件系统,并且执行根文件系统中的init 程序,以此来进去用户态。这里就正式引出了根文件系统,根文件系统也是我们系统移植的最后一片拼图。Linux 移植三巨头:uboot、Linux kernel、rootfs(根文件系统)。关于根文件系统后面章节会详细的讲解,这里我们只需要知道Linux内核移植完成以后还需要构建根文件系统即可。

相关推荐
AlfredZhao8 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户97183563346614 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪15 小时前
linux 拷贝文件或目录到指定的位置
linux
大树881 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质1 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
小宇宙Zz1 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工1 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信