Linux系统移植篇(十一)Linux 内核启动流程
要分析 Linux 启动流程,同样需要先编译一下 Linux 源码,因为有很多文件是需要编译才 会生成的。首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以 找到 Linux 内核的第一行程序是从哪里执行的。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 }
ENTRY 指明了了 Linux 内核入口,入口为 stext,stext 定义在文件 arch/arm/kernel/head.S 中 , 因 此 要 分 析 Linux 内 核 的 启 动 流 程 , 就 得 先 从 文 件 arch/arm/kernel/head.S 的 stext 处开始分析。
Linux 内核入口 stext。
stext 是 Linux 内核的入口地址,在文件 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. .....
*/
①、关闭 MMU。 ②、关闭 D-cache。 ③、I-Cache 无所谓。 ④、r0=0。 ⑤、r1=machine nr(也就是机器 ID)。 ⑥、r2=atags 或者设备树(dtb)首地址。
Linux 内核的入口点 stext 其实相当于内核的入口函数,stext 函数内容如下:
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 中的定义如下:
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 函数。
这里重点说一下内核为什么要关闭MMU,也就是内存映射,后面又要打开?这个过程是为什么?
在内核启动的早期阶段,系统处于物理地址模式下(即 MMU 处于关闭状态),这样可以直接访问物理内存,方便初始化关键数据结构和加载必要的代码。具体原因包括:
初始引导环境简化操作:上电后,处理器默认运行在关闭 MMU 的状态,此时所有地址都是物理地址,避免了虚拟地址转换的复杂性。这有助于启动代码(bootloader 和内核早期代码)顺利加载并执行。
页表构建:内核在启动过程中需要构建页表,以建立虚拟地址到物理地址的映射。此时必须依赖物理地址操作来确保数据和代码能被正确访问。构建好页表之前,开启 MMU 会导致地址转换错误或不可预知的行为。
切换到虚拟内存管理:一旦内核完成了页表的设置(例如通过调用 __create_page_tables),它就可以启用 MMU,这时所有内存访问都将通过页表进行地址转换。这样做能够提供虚拟内存、内存保护、缓存管理等高级功能,对后续内核和应用程序的稳定性与安全性至关重要。
系统安全与隔离:开启 MMU 后,内核可以实现内核空间和用户空间的隔离,以及各种内存保护机制,这些都是现代操作系统必备的特性。
__mmap_switched 函数定义在文件 arch/arm/kernel/head-common.S 中
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)
最终调用 start_kernel 来启动 Linux 内核,start_kernel 函数定义在文件 init/main.c 中。
start_kernel 通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作,由于 start_kernel 函数里面调用的子函数太多,而这些子函数又很复杂,因此我们简单的来看一下一些重要的子函数。精简并添加注释后的 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 函数定义在文件 init/main.c 中,函数内容如下:
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 进程。
init 进程的 PID 为 1,kthreadd 进程的 PID 为 2。之所以图 36.2.4.1 中没有显示 PID 为 0 的 idle 进程,那是因为 idle 进程是内核进程。init 进程,kernel_init 就是 init 进程的进程函数。
kernel_init 函数就是 init 进程具体做的工作,定义在文件 init/main.c 中。
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 中,缩减后的函数内容如下:
示例代码 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 }
现在总结一下流程吧。
内核入口与解压内核
内核入口代码通常位于各架构目录下的汇编文件(如 x86 的 arch/x86/boot
或 ARM 的 arch/arm/kernel/head.S
)。在这一步:
内核首先解压自身(如果内核是压缩的)。
完成最基本的 CPU 和内存环境设置。
设置处理器模式与寄存器
启动时系统一般在关闭 MMU 的物理地址模式下运行,便于直接访问物理内存。早期代码还会根据处理器 ID 找到对应的 procinfo
结构,并做一些必要的寄存器初始化(例如将对应的结构地址保存到特定寄存器中)。
建立页表与启用 MMU
内核在这一阶段构建页表,将物理地址映射到虚拟地址空间。这包括:
调用函数如 __create_page_tables
来创建页表。
通过调用 __enable_mmu
(最终由 __turn_mmu_on
实现)来开启 MMU,实现从物理地址模式到虚拟地址模式的切换。
切换后,所有内存访问均通过虚拟地址进行,便于实现内存保护、缓存管理等功能。
启动第一个用户进程:init
内核初始化完成后,启动第一个用户空间进程(通常为 init 进程,PID 为 1),它负责加载和启动后续用户空间的各项服务。
init 进程可能采用 System V init、systemd 或其它初始化系统,进一步启动登录服务、网络服务、图形界面等。
系统进入正常运行状态
随着 init 进程和相关服务的启动,整个系统逐步进入稳定的多任务运行状态,用户可以开始使用系统。
相关文章:
Linux系统移植篇(十一)Linux 内核启动流程
要分析 Linux 启动流程,同样需要先编译一下 Linux 源码,因为有很多文件是需要编译才 会生成的。首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以 找到 Linux 内核的第一行程序是从哪里执行的。vmlinux.lds …...
React19源码系列之Hooks(useId)
useId的介绍 https://zh-hans.react.dev/reference/react/useId useId 是 React 18 引入的一个新 Hook,主要用于生成全局唯一的 ID。在开发中,我们经常需要为元素(如表单元素、模态框等)生成唯一 ID,以便在 JavaScri…...
深度学习-149-langchain之如何不使用with_structured_output()从模型中返回结构化数据
文章目录 1 不使用with_structured_output()方法1.1 问题背景1.2 输出解析器1.3 远程deepseek大模型API2 基于提示词2.1 直接使用提示词2.2 少样本提示词3 直接提示和解析模型输出3.1 使用PydanticOutputParser3.1.1 构建解析器3.1.2 构建提示模板3.1.3 调用大模型3.1.4 调用链…...
SAP DOI EXCEL应用
【应用场景】采用DOI方式打开填充EXCEL数据 *&---------------------------------------------------------------------* *& 包含 ZFI1009R_TOP *&---------------------------------------------------------------------* TABLES:bkpf,bseg. D…...
RS485电路设计注意事项
(一)RS485的电平标准: RS485用缆线两端的电压差值来表示传递信号,逻辑“1”以两线间的电压差为2V~6V标识,逻辑“0”以两线间的电压差为-2V~-6V标识。由此可见,接口信号电平较低,不易损坏接口电路…...
Git 回退操作详解:带示例的“小白”指南
前言 在日常开发中,我们难免会遇到: 改错代码:推送之前才发现某些行根本就不该动提交错误:commit 信息打错、提交到错误分支想回到之前版本:测试时发现之前版本是好的,需要回去查看 这就需要用到 Git 的…...
PyQt5库 各种导入项的作用
from PyQt5.QtCore import QIODevice, QSharedMemory, pyqtSignal 这行代码是从 PyQt5 库中导入了几个类和信号,用于开发桌面应用程序。下面是每个导入项的详细解释: QIODevice: QIODevice 是 PyQt5 中的一个类,提供了对输入输出设备的抽象…...
Git下载安装(保姆教程)
目录 1、Git下载 2、Git安装(windows版) (1)启动安装程序 (2)阅读许可协议 (3)选择安装路径 (4)选择组件 (5)选择开始菜单文件夹…...
关系数据库设计理论
目录 一、数据依赖——重点 (1)平凡依赖/非平凡函数依赖 (2)完全/部分函数依赖 (3)传递函数依赖 二、范式(NF) (1)第一范式 (2)…...
图解LLM智能体(LLM Agents):构建与运作机制的全面解析
LLM智能体:构建与运作机制 LLM智能体(LLM Agents)正在迅速普及,似乎逐渐取代了我们熟悉的传统对话式LLM。这些令人惊叹的能力并非凭空而来,而是需要多个组件协同工作。 本文包含超过60张定制插图,将深入探讨LLM智能体的领域、其核心组件以及多智能体框架的工作原理。 文…...
Anaconda 入门指南
Anaconda 入门指南 一、下载安装 Anaconda 1、下载地址:Anaconda 推荐下载 python3 版本, 毕竟未来 python2 是要停止维护的。 2、安装 Anaconda 按照安装程序提示一步步安装就好了, 安装完成之后会多几个应用: Anaconda Navigtor :用于管…...
YOLOv11小白的进击之路(九)创新YOLO11损失函数之NWD损失函数源码解读
之前的博客也有对YOLO11的损失函数进行过源码分析,可以参考:YOLOv11小白的进击之路(六)创新YOLO的iou及损失函数时的源码分析_yolov11的损失函数是什么-CSDN博客最近在做小目标检测的时候注意到了NWD损失函数,这里对其…...
【c++】内存序 和 内存一致性模型
c 11 中为了支持并发,定义了内存序和内存一致性模型。这个概念听起来非常高深,好像是在多线程编程领域浸淫多年之后的神级程序员才能搞明白,并用明白的东西。 本文尝试用最简单的方式说清楚这个概念。因为这个概念真的超级简单,大…...
力扣128. 最长连续序列 || 452. 用最少数量的箭引爆气球
最长连续列 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 输入:nums [100,4,200,1,3,2] 输出:4 解释&…...
从零开始写C++3D游戏引擎(开发环境VS2022+OpenGL)之十一点二五 光照贴图(lighting maps)的实现 细嚼慢咽逐条读代码系列
写在篇前的话 作为一个曾经在代码堆里面苦苦挣扎的萌新,困惑的事情在于库,各种依赖,包换文件,链接库,纠结于代码的作用意义。尤其在3D引擎开发的问题上,很多人都被各种困难给阻拦,放弃了在3D渲染,3D游戏引擎上大涨鸿图的机会。 当然关于3D游戏引擎的教程已经汗牛充栋…...
优先级队列(PriorityQueue)_1_模拟实现优先级队列
1、概念 前面介绍过队列,队列是一种先进先出(FIFO)的数据结构 ,但有些情况下, 操作的数据可能带有优先级,一般出队 列时,可能需要优先级高的元素先出队列 ,这时候,使用队列显然不合适了。 在这种…...
java后端怎么写好根据角色控制查询不同数据,
z总的思路,先把不带查询条件的包含角色控制场景(比如:总公司经理角色可以查看所有数据,但是暂存的话只能查自己暂存的,分公司,只能查自己所属分公司的,)的所有数据查出来 例如查询的…...
《量子比特:AI复杂算法破局的关键力量》
在科技飞速发展的今天,人工智能(AI)与量子计算成为了推动人类进步的两大引擎。而量子比特,作为量子计算的基石,正逐渐展现出其在实现复杂AI算法时相较于传统比特的独特优势,为AI领域带来了前所未有的变革潜…...
MVC_Publish-Subscriber 模式中的事件处理程序
MVC_Publish-Subscriber 模式中的事件处理程序 MVC 中的事件处理:发布者-订阅者模式 程序启动时,controlRecipes将被传入addHandlerRender; addHandlerRender会侦听事件(addEventListener),并使用controlRecipes作为回调…...
RxSwift 学习笔记第四篇之RxSwift在项目中的简单应用
目录 前言 一、RxCocoa在项目中的用法 1.Target Action 2.代理 3.闭包回调 4.通知 二、一个计时器的例子 前言 在上面的两篇文章中,我们了解到了RxSwift中的Observable和Observer,本篇文章我们主要介绍下RxSwift项目中的使用。 一、RxCocoa在项目中的用法 RxCocoa 给 …...
Java面试黄金宝典2
1. 什么是 Concurrent 包 java.util.concurrent(简称 Concurrent 包)是 Java 5 引入的一个用于并发编程的工具包。它提供了一系列用于处理多线程编程的类和接口,帮助开发者更方便、安全地进行并发编程。 原理 该包基于 Java 的多线程机制和锁…...
【在数轴上找最优位置,使移动距离最短】
L1-4 破碎的心,无法挽回的距离 题目描述: YFffffff 最近在感情上遭受了失败,他的心也破碎成了n块碎片,散落在了数轴上的 n 个位置。 你是一个情感修复师,作为 YFffffff 的好友,你试图将这些破碎的心重新聚集到一个位…...
3D标定中的平面约束-平面方程的几何意义
平面方程的一般形式为 AxByCzD0,其中系数 A、B、C、D共同决定了平面的几何特性。 系数对平面姿态的影响 1. 法向量方向2. 平面位置3. 比例关系4. 姿态变换5.平面空间变换 1. 法向量方向 法向量方向由 A、B、C 决定 核心作用:系数 A、B、C 构成的向量 (…...
singleInstance 和 singleTask的 重要 区别
singleInstance 和 singleTask 是 Android 中 Activity 的两种启动模式,它们主要用于控制 Activity 在任务栈中的行为。以下是两者的区别: 1. singleTask 定义: 每个 singleTask 模式的 Activity 在一个任务栈中只会存在一个实例。行为: 如果该 Activi…...
【如何在OpenWebUI中使用FLUX绘画:基于硅基流动免费API的完整指南】
如何在OpenWebUI中使用FLUX绘画:基于硅基流动免费API的完整指南 注册并获取硅基流动秘钥OpenWebUI中使用函数配置自定义模型-提示词配置效果验证 ) FLUX绘画是一种强大的AI绘图工具,本文将详细介绍如何在OpenWebUI中集成并使用FLUX绘画功能,…...
【Linux】浅谈环境变量和进程地址空间
一、环境变量 基本概念 环境变量(Environment Variables)是操作系统提供的一种机制,用于存储和传递配置信息、系统参数、用户偏好设置等。 环境变量的作用 配置程序行为: 程序可以通过环境变量获取配置信息,例如日…...
vue数字公式篇(一)
一、使用插件来创造数字公式 因为只是展示和编辑 我看这个公式挺多,也对公式不太了解所以就这样,开始我的代码展示了 1、安装mathlive cnpm install mathlive2、页面 <template><div><label>输入 LaTeX 公式:</label><div …...
基于PMU的14节点、30节点电力系统状态估计MATLAB程序
“电气仔推送”获得资料(专享优惠) 程序简介: 程序采用三种方法对14节点和30节点电力系统状态进行评估: ①PMU同步向量测量单元结合加权最小二乘法(WLS)分析电力系统的电压幅值和相角状态; …...
5 分钟用满血 DeepSeek R1 搭建个人 AI 知识库(含本地部署)
最近很多朋友都在问:怎么本地部署 DeepSeek 搭建个人知识库。 老实说,如果你不是为了研究技术,或者确实需要保护涉密数据,我真不建议去折腾本地部署。 为什么呢? 目前 Ollama 从 1.5B 到 70B 都只是把 R1 的推理能力…...
QT QML实现音频波形图进度条,可点击定位或拖动进度
前言 本项目实现了使用QT QML创建一个音频波形图进度条的功能。用户可以在界面上看到音频波形图,并且可以点击进度条上的位置进行定位,也可以拖动进度条来调整播放进度。可以让用户更方便地控制音频的播放进度,并且通过音频波形图可以直观地…...
浅谈StarRocks SQL性能检查与调优
StarRocks性能受数据建模、查询设计及资源配置核心影响。分桶键选择直接决定数据分布与Shuffle效率,物化视图可预计算复杂逻辑。执行计划需关注分区裁剪、谓词下推及Join策略,避免全表扫描或数据倾斜。资源层面,需平衡并行度、内存限制与网络…...
味觉传送器E-Taste:开启虚拟世界的味觉之门
味觉传送器E-Taste:开启虚拟世界的味觉之门 一、发明背景与动机 随着虚拟现实(VR)和增强现实(AR)技术的飞速发展,人们在虚拟世界中的沉浸感不断提升,视觉和听觉体验已经取得了显著的突破。然而…...
ISE 14.7 IP核 Block Memory Generator 更换coe
ISE 14.7 IP核 Block Memory Generator 更换coe 打开XCO 后缀,修改下面的coe_file 目录,并且重新regenerate ip 核即可...
【Auto-Scroll-List 组件设计与实现分析】
Auto-Scroll-List 组件设计与实现分析 gitee代码仓库 https://gitee.com/chennaiyuan/dayup-record/tree/master/%E4%B8%80%E4%BA%9B%E7%BB%84%E4%BB%B6/auto-scroll-list 1. 组件概述 我们封装的 AutoScrollList 是一个自动滚动列表组件,主要用于展示需要自动循…...
用hexo初始化博客执行hexo init时碰到的问题
用hexo初始化博客执行hexo init时碰到的问题 $ hexo init myblog INFO Cloning hexo-starter https://github.com/hexojs/hexo-starter.git fatal: unable to access https://github.com/hexojs/hexo-starter.git/: SSL certificate problem: unable to get local issuer cer…...
【C++真题】P1739 表达式括号匹配
P1739 表达式括号匹配 题目描述 假设一个表达式有英文字母(小写)、运算符(、-、*、/)和左右小(圆)括号构成,以 作为表达式的结束符。请编写一个程序检查表达式中的左右圆括号是否匹配&#x…...
Java1.8与testNg兼容问题:bad class file和SocketTimeoutException: Read timed out
背景: 公司 java JDK默认用的是1.8版本,已经在跑的一个项目使用的testng用的是6.14.3,我拿到后通过Test 运行失败,因为这个是一直在用的项目,就没想到是版本兼容问题,折腾了好一阵(原开发者是通…...
高项第十二章——项目质量管理
项目质量管理包括把组织的质量政策应用于规划、管理、控制项目和产品质量要求,以满足干系人目标的各个过程。 项目质量管理针对的是项目过程中所涉及的活动 可交付成果质量管理针对的是项目生产的具体可交付成果,与可交付成果的性质和特性紧密相关 12…...
C# WPF编程-Menu
C# WPF编程-Menu 布局:代码:效果 在WPF(Windows Presentation Foundation)中,Menu控件用于创建下拉菜单或上下文菜单,它提供了丰富的定制选项来满足不同的应用需求。下面将介绍如何在WPF应用程序中使用Menu…...
python日期
导入包 from datetime import datetime现在时间 now datetime.now() print("当前时间:", now)当前时间: 2025-03-18 23:51:08.418953 格式化 formatted_now datetime.now().strftime("%Y-%m-%d %H:%M:%S") print("格式化后的时间:", forma…...
flutter 专题 一百零三
前不久,谷歌官方正式发布了Flutter的首个发布预览版(Release Preview 1),这标志着谷歌进入了Flutter正式版(1.0)发布前的最后阶段,同时作为Google的重量级跨平台开发方案,此次更新也…...
【conda activate无效】 conda: error: argument COMMAND: invalid choice: ‘activate‘
conda activate失效了 在使用conda activate时出现报错: usage: conda [-h] [-v] [--no-plugins] [-V] COMMAND ... conda: error: argument COMMAND: invalid choice: activate (choose from clean, compare, config, create, info, init, install, list, notice…...
Chainlit 自定义元素开发指南:使用 JSX 和受限导入实现交互式界面
自定义元素 Custom Element 类允许你渲染一个自定义的 .jsx 代码片段。.jsx 文件应当放置在 public/elements/ELEMEN_NAME.jsx 目录下。 属性 name 字符串 自定义元素的名称。它应该与你的JSX文件名相匹配(不包括 .jsx扩展名)。 props 字典 传递给 JSX 的属性。 display El…...
CEF 控制台添加一函数,枚举 注册的供前端使用的CPP交互函数有哪些
一、前序知识 1、设置单进程模式,方便调试 void ClientApp::OnBeforeCommandLineProcessing(const CefString& process_type, CefRefPtr<CefCommandLine> command_line) {if (process_type.empty()){//cef 在debug模式下有问题#ifdef _DEBUGcommand_line->Appe…...
C++之list类及模拟实现
目录 list的介绍 list的模拟实现 定义节点 有关遍历的重载运算符 list的操作实现 (1)构造函数 (2)拷贝构造函数 (3)赋值运算符重载函数 (4)析构函数和clear成员函数 (5)尾…...
C++八大常见的设计模式的实现与实践指南
目录 创建型模式 单例模式工厂方法模式抽象工厂模式 结构型模式 适配器模式装饰者模式代理模式 行为型模式 观察者模式策略模式命令模式 高级主题 现代C特性影响模式性能对比典型应用案例 设计模式分类 一、创建型模式 1. 单例模式(Singleton) 现代…...
02 windows qt配置ffmpeg开发环境搭建
版本说明 首先我使用ffmpeg版本是4.2.1QT使用版本5.14.2我选择是c编译...
什么是状态管理?有何种方式可以实现?它们之间有什么区别?
目录 一、状态管理的核心概念 二、常见状态管理方案及对比 1. 基础方案:setState 2. 官方推荐:Provider 3. 事件驱动:Bloc (Business Logic Component) 4. 响应式增强:Riverpod 5. 轻量级全能库:GetX 三、方案对比与选型指南 四、实战建议 在 Flutter 中,状态管…...
tf1.x和tf2.x在使用上的区别和联系是什么
tf1.x和tf2.x在使用上的区别和联系是什么 TensorFlow 1.x 和 2.x 在使用上有显著差异,主要体现在编程范式、API 设计和易用性上,但二者仍共享相同的核心目标(深度学习框架)和底层计算引擎。以下是主要区别和联系: 主要…...
Elasticsearch使用记录
一、配环境 1.docker版本部署es 8.x系列可以关掉ssl(本地测试时),去docker的/usr/share/elasticsearch/config/elasticsearch.yml里面的“xpack.security.enabled:”设置成true就可以 2.window docker部署推荐教程:基于Docker安…...