05 Linux 内核启动流程

1、阅读 Linux 内核源码

学习 Linux 有两种路线:

1)按照 Linux 启动流程,梳理每个子系统。
2)把 Linux 所有用到的子系统学会,再组合起来。

博主选择第一种方式,可以快速上手,知道自己在学什么东西,在什么阶段起作用。

阅读 Linux 和 Android 源码:

https://elixir.bootlin.com/linux/latest/source
http://aospxref.com/

2、Makefile 与 Kconfig

1)Makefile

  • Makefile 是一种被广泛用于管理源代码的工具,特别是针对程序的编译和构建。它包含了一系列规则,指定了编译器如何编译源文件、链接器如何链接目标文件,以及如何清理生成的文件等操作。
  • Makefile 的作用:通过 Makefile,开发人员可以指定项目中源文件的依赖关系,使得只有受影响的文件被编译,而不是每次都编译整个项目,节省时间和资源。
  • Makefile 结构:典型的 Makefile 包含了变量定义、目标规则、依赖关系和命令等内容。

2)Kconfig

  • Kconfig 是 Linux 内核用于配置内核选项的工具,允许用户在编译内核时选择不同的配置选项以定制内核的功能。
  • Kconfig 文件:Kconfig 文件包含了内核配置选项的描述,格式为类似于菜单的层次结构,用户可以通过命令行或图形界面交互式地选择选项,以配置内核的特性和行为。
  • Kconfig 的作用:Kconfig 允许用户对内核进行高度定制,以适应不同的硬件平台、应用需求或性能要求,而无需重新编译整个内核。

3)总结

  • Makefile 是用于管理源代码编译和构建的工具,利用规则来指导建立整个项目。
  • Kconfig 是 Linux 内核的配置工具,用于选择编译内核时的不同配置选项,定制内核的功能和行为。

3、内核裁剪与内核移植

1)内核裁剪

make menuconfig 命令在 Linux 内核源码根目录执行,执行后效果如图:

[图片]

  • “方向按键”中的“左右”可以选择你需要的操作。“”表示进入选择的配置界面,“”表示返回,“< Help >”可以阅读帮助文档。
  • 输入“/”,可以进入搜索界面。

make menuconfig 是 Linux 内核构建过程中的一个命令,用于交互式地配置内核选项。通过运行 make menuconfig 命令,用户可以在终端中打开一个基于文本界面的菜单系统,以便于选择和配置 Linux 内核的各种功能选项、驱动程序和特性。

具体来说,make menuconfig 命令的作用包括以下几点:

  1. 配置内核选项:通过 make menuconfig,用户可以浏览各种内核选项,例如驱动程序、文件系统支持、网络配置等,并可以根据需求进行启用、禁用、选择或设置。
  2. 交互式选择:使用键盘进行上下左右移动,空格键选择选项,以及对选项进行开启或关闭操作。
  3. 依赖关系:make menuconfig 会显示选项之间的依赖关系,使得用户可以清晰地了解选择一个选项可能会影响到哪些其他选项。
  4. 保存配置:按 ESC 可以退出这个界面,会提示你是否要保存。配置的修改可以保存并生成 .config 文件,该文件包含了用户的配置选择,可用于后续编译内核。
  5. 定制内核:通过调整不同的配置选项,用户可以定制适合特定需求的内核,例如精简内核以提高性能,或者增加特定的功能支持。
    总的来说,make menuconfig 提供了一种方便、交互式的方式来配置 Linux 内核,使得用户可以根据自己的需求定制内核的功能和特性。

2)内核移植

[图片]

4、Linux 启动流程详解

建议大家深入学习 Linux kernel 某个子系统之前,先学习 Linux kernel 启动流程,这样能有一个框架,你学的子系统就不是孤立存在于你的知识系统中,你知道它是在哪个时刻被调用的。

从启动引导程序 bootloader(uboot)跳转到 Linux kernel 后,Linux 内核开始启动, 我们先看一下 Linux 内核启动入口。

linux4.14/arch/arm/kernel/vmlinux.lds.S

这里可以看到链接时候 Linux 入口是 stext 段,这里是 uboot 跳转过来的第一段 Linux kernel 代码:

OUTPUT_ARCH(arm)
ENTRY(stext)

1)Linux 入口地址

我们先看一下入口地址的确定,同一文件中找到 SECTIONS

SECTIONS
{/DISCARD/ : {*(.ARM.exidx.exit.text)*(.ARM.extab.exit.text)ARM_CPU_DISCARD(*(.ARM.exidx.cpuexit.text))ARM_CPU_DISCARD(*(.ARM.extab.cpuexit.text))ARM_EXIT_DISCARD(EXIT_TEXT)ARM_EXIT_DISCARD(EXIT_DATA)EXIT_CALL
#ifndef CONFIG_MMU*(.text.fixup)*(__ex_table)
#endif
#ifndef CONFIG_SMP_ON_UP*(.alt.smp.init)
#endif*(.discard)*(.discard.*)}. = PAGE_OFFSET + TEXT_OFFSET;.head.text : {_text = .;HEAD_TEXT}

这个 SECTIONS 比较长,只放一部分。在这里倒数第五行有个比较重要的东西:

. = PAGE_OFFSET + TEXT_OFFSET;

这一句表示了 Linux kernel 真正的启动地址。

PAGE_OFFSET 是 Linux kernel 空间的虚拟起始地址,定义在:

linux4.14/arch/arm64/include/asm/memory.h

#define VA_BITS                        (CONFIG_ARM64_VA_BITS)
#define VA_START                (UL(0xffffffffffffffff) - \(UL(1) << VA_BITS) + 1)
#define PAGE_OFFSET                (UL(0xffffffffffffffff) - \(UL(1) << (VA_BITS - 1)) + 1)
#define KIMAGE_VADDR                (MODULES_END)
#define MODULES_END                (MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR                (VA_START + KASAN_SHADOW_SIZE)
#define MODULES_VSIZE                (SZ_128M)
#define VMEMMAP_START                (PAGE_OFFSET - VMEMMAP_SIZE)
#define PCI_IO_END                (VMEMMAP_START - SZ_2M)
#define PCI_IO_START                (PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP                (PCI_IO_START - SZ_2M)

这里的地址都很重要,很多地方会用到。当然,这里的地址可能会随着 Linux kernel 版本的不同和硬件的不同,会变化。这里没有一个具体的数,因为 VA_BITS 中的数字是可选的,大家可以根据自己的平台算一下。 VA_BITS 是你的系统真正使用的位数,如果是 32 位系统就是 32,如果是 64 位系统可能是 39 位或者其他。

TEXT_OFFSET 定义在 linux4.14/arch/arm/Makefile 中:

textofs-y        := 0x00008000
The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)

这个值一般是 0x00008000 ,算出 PAGE_OFFSET 后加上这个值就是 Linux 内核的起始地址。

修改这个偏移量就可以使 Linux 内核拷贝到不同的地址,自己修改注意内存对齐。

2)stext 段

从上面的 ENTRY(stext)可以知道,一开始是运行 stext 段,这个段内的代码是 start_kernel 函数前汇编环境的初始化。

linux4.14/arch/arm64/kernel/head.S

ENTRY(stext)bl        preserve_boot_argsbl        el2_setup                        // Drop to EL1, w0=cpu_boot_modeadrp        x23, __PHYS_OFFSETand        x23, x23, MIN_KIMG_ALIGN - 1        // KASLR offset, defaults to 0bl        set_cpu_boot_mode_flagbl        __create_page_tablesbl        __cpu_setup                        // initialise processorb        __primary_switch
ENDPROC(stext)
  • preserve_boot_args 保存 uboot 传递过来的参数。
  • el2_setup 是设置 Linux 启动模式是 EL2。Linux 有 EL0、EL1、EL2、EL3 四种异常启动模式,这里设置一开始是 EL2,EL2 支持虚拟内存技术,然后注释说明后面又退回 EL1,在 EL1 启动 kernel。EL3 一般是只在安全模式使用。(ARM64情况下)
  • 4、5 行:这两行设定了 offset,是一种 KASLR 技术,有了这个技术才能使得 Linux kernel 被拷贝到不同的物理地址。
  • set_cpu_boot_mode_flag 保存上面 cpu 的启动模式。
  • __create_page_tables 创建页表。
  • __cpu_setup 初始化 CPU,这里主要是初始化和 MMU 内存相关的 CPU 部分。
  • __primary_switch 这里会进行跳转。

在同一个文件中,会跳转到这里:

__primary_switch:
#ifdef CONFIG_RANDOMIZE_BASEmov        x19, x0                                // preserve new SCTLR_EL1 valuemrs        x20, sctlr_el1                        // preserve old SCTLR_EL1 value
#endifbl        __enable_mmu
#ifdef CONFIG_RELOCATABLEbl        __relocate_kernel
#ifdef CONFIG_RANDOMIZE_BASEldr        x8, =__primary_switchedadrp       x0, __PHYS_OFFSETblr        x8

开启了 MMU。然后最重要的是跳转到 __primary_switched 函数。先把 __primary_switched 地址放到 x8 寄存器中,再跳转到 x8,也就是跳转到 __primary_switched。

__primary_switched:adrp        x4, init_thread_unionadd        sp, x4, #THREAD_SIZEadr_l        x5, init_taskmsr        sp_el0, x5                        // Save thread_infoadr_l        x8, vectors                        // load VBAR_EL1 with virtualmsr        vbar_el1, x8                        // vector table addressisbstp        xzr, x30, [sp, #-16]!mov        x29, spstr_l        x21, fdt_pointer, __x5                // Save FDT pointerldr_l        x4, kimage_vaddr                // Save the offset betweensub        x4, x4, x0                        // the kernel virtual andstr_l        x4, kimage_voffset, x5                // physical mappings// Clear BSSadr_l        x0, __bss_startmov        x1, xzradr_l        x2, __bss_stopsub        x2, x2, x0bl        __pi_memsetdsb        ishst                                // Make zero page visible to PTW#ifdef CONFIG_KASANbl        kasan_early_init
#endif
#ifdef CONFIG_RANDOMIZE_BASEtst        x23, ~(MIN_KIMG_ALIGN - 1)        // already running randomized?b.ne        0fmov        x0, x21                                // pass FDT address in x0bl        kaslr_early_init                // parse FDT for KASLR optionscbz        x0, 0f                                // KASLR disabled? just proceedorr        x23, x23, x0                        // record KASLR offsetldp        x29, x30, [sp], #16                // we must enable KASLR, returnret                                        // to __primary_switch()
0:
#endifadd        sp, sp, #16mov        x29, #0mov        x30, #0b        start_kernel
ENDPROC(__primary_switched)
  • 第一段: 初始化了 init 进程的内存信息,开辟了内存空间。
  • 第二段:设置了中断向量表。
  • 第三段: 保存了 FDT,也就是 flat device tree ,设备树。
  • 第四段:清除了BSS 段,我们知道一般是内存四区:堆区、栈区、全局区、代码区。其中全局区可以再分为 data 段和 BSS 段,BSS 段存储了未初始化的变量,这里将BSS段进行清零操作,否则内存中的值是不确定的,这是一个传统操作。
  • 最后:跳转到了我们熟悉的 start_kernel

3)start_kernel 函数

linux4.14/init/main.c,start_kernel 函数,从汇编环境跳转到了 C 环境。

kernel 4.14 中 start_kernel 函数一共调用了 86 个函数进行初始化 ,如下:

asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{char *command_line;char *after_dashes;set_task_stack_end_magic(&init_task);/*设置任务栈结束魔术数,用于栈溢出检测*/smp_setup_processor_id();/*跟 SMP 有关(多核处理器),设置处理器 ID*/debug_objects_early_init();/* 做一些和 debug 有关的初始化 */init_vmlinux_build_id();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 版本号、编译时间等信息 */early_security_init();/* 系统架构相关的初始化,此函数会解析传递进来的* ATAGS 或者设备树(DTB)文件。会根据设备树里面* 的 model 和 compatible 这两个属性值来查找* Linux 是否支持这个单板。此函数也会获取设备树* 中 chosen 节点下的 bootargs 属性值来得到命令* 行参数,也就是 uboot 中的 bootargs 环境变量的* 值,获取到的命令行参数会保存到 command_line 中*/setup_arch(&command_line);setup_boot_config();setup_command_line(command_line);/* 存储命令行参数 *//* 如果只是 SMP(多核 CPU)的话,此函数用于获取* CPU 核心数量,CPU 数量保存在变量 nr_cpu_ids 中。*/setup_nr_cpu_ids();setup_per_cpu_areas();/* 在 SMP 系统中有用,设置每个 CPU 的 per-cpu 数据 */smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */boot_cpu_hotplug_init();build_all_zonelists(NULL);/* 建立系统内存页区(zone)链表 */page_alloc_init();/* 处理用于热插拔 CPU 的页 *//* 打印命令行信息 */ pr_notice("Kernel command line: %s\n", saved_command_line);/* parameters may set static keys */jump_label_init();parse_early_param();/* 解析命令行中的 console 参数 */after_dashes = parse_args("Booting kernel",static_command_line, __start___param,__stop___param - __start___param,-1, -1, NULL, &unknown_bootoption);print_unknown_bootoptions();if (!IS_ERR_OR_NULL(after_dashes))parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,NULL, set_init_arg);if (extra_init_args)parse_args("Setting extra init args", extra_init_args,NULL, 0, -1, -1, NULL, set_init_arg);random_init_early(command_line);setup_log_buf(0);/* 设置 log 使用的缓冲区*/vfs_caches_init_early(); /* 预先初始化 vfs(虚拟文件系统)的目录项和索引节点缓存*/sort_main_extable();/* 定义内核异常列表 */trap_init();/* 完成对系统保留中断向量的初始化 */mm_init();/* 内存管理初始化 */ftrace_init();/* trace_printk can be enabled here */early_trace_init();sched_init();/* 初始化调度器,主要是初始化一些结构体 */if (WARN(!irqs_disabled(),"Interrupts were enabled *very* early, fixing it\n"))local_irq_disable();/* 检查中断是否关闭,如果没有的话就关闭中断 */radix_tree_init();/* 基数树相关数据结构初始化 */maple_tree_init();housekeeping_init();workqueue_init_early();rcu_init();/* 初始化 RCU,RCU 全称为 Read Copy Update(读-拷贝修改) *//* Trace events are available after this */trace_init();/* 跟踪调试相关初始化 */if (initcall_debug)initcall_debug_enable();context_tracking_init();/* 初始中断相关初始化,主要是注册 irq_desc 结构体变* 量,因为 Linux 内核使用 irq_desc 来描述一个中断。*/early_irq_init();init_IRQ();/* 中断初始化 */tick_init();/* tick 初始化 */rcu_init_nohz();init_timers();/* 初始化定时器 */srcu_init();hrtimers_init();/* 初始化高精度定时器 */softirq_init();/* 软中断初始化 */timekeeping_init();time_init();/* 初始化系统时间 */random_init();kfence_init();boot_init_stack_canary();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 内存分配器 *//* 初始化控制台,之前 printk 打印的信息都存放* 缓冲区中,并没有打印出来。只有调用此函数* 初始化控制台以后才能在控制台上打印信息。*/console_init();if (panic_later)panic("Too many boot %s vars at `%s'", panic_later,panic_param);lockdep_init();locking_selftest();/* 锁自测 */ mem_encrypt_init();#ifdef CONFIG_BLK_DEV_INITRDif (initrd_start && !initrd_below_start_ok &&page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",page_to_pfn(virt_to_page((void *)initrd_start)),min_low_pfn);initrd_start = 0;}
#endifsetup_per_cpu_pageset();numa_policy_init();acpi_early_init();if (late_time_init)late_time_init();sched_clock_init();/* 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能* BogoMIPS 设置越大,说明 CPU 性能越好。*/calibrate_delay();pid_idr_init();anon_vma_init();/* 生成 anon_vma slab 缓存 */ 
#ifdef CONFIG_X86if (efi_enabled(EFI_RUNTIME_SERVICES))efi_enter_virtual_mode();
#endifthread_stack_cache_init();cred_init();/* 为对象的每个用于赋予资格(凭证) */fork_init();/* 初始化一些结构体以使用 fork 函数 */proc_caches_init();/* 给各种资源管理结构分配缓存 */uts_ns_init();key_init();/* 初始化密钥 */security_init();/* 安全相关初始化 */dbg_late_init();net_ns_init();vfs_caches_init();/* 虚拟文件系统缓存初始化 */pagecache_init();signals_init();/* 初始化信号 */seq_file_init();proc_root_init();/* 注册并挂载 proc 文件系统 */nsfs_init();/* 初始化 cpuset,cpuset 是将 CPU 和内存资源以逻辑性* 和层次性集成的一种机制,是 cgroup 使用的子系统之一*/cpuset_init();cgroup_init();/* 初始化 cgroup */taskstats_init_early();/* 进程状态初始化 */delayacct_init();poking_init();check_bugs();/* 检查写缓冲一致性 */acpi_subsystem_init();arch_post_acpi_subsys_init();kcsan_init();/* 调用 rest_init 函数 *//* 创建 init、kthread、idle 线程 */arch_call_rest_init();prevent_tail_call_optimization();
}

[图片]

[图片]

[图片]

其中有七个函数较为重要,分别为:

setup_arch(&command_line);
mm_init();
sched_init();
init_IRQ();
console_init();
vfs_caches_init();
rest_init();
1、setup_arch(&command_line)

此函数是系统架构初始化函数,处理 uboot 传递进来的参数,不同的架构进行不同的初始化,也就是说每个架构都会有一个 setup_arch 函数。

linux4.14/arch/arm/kernel/setup.c

void __init setup_arch(char **cmdline_p)
{const struct machine_desc *mdesc;setup_processor();mdesc = setup_machine_fdt(__atags_pointer);if (!mdesc)mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);machine_desc = mdesc;machine_name = mdesc->name;dump_stack_set_arch_desc("%s", mdesc->name);if (mdesc->reboot_mode != REBOOT_HARD)reboot_mode = mdesc->reboot_mode;init_mm.start_code = (unsigned long) _text;init_mm.end_code   = (unsigned long) _etext;init_mm.end_data   = (unsigned long) _edata;init_mm.brk           = (unsigned long) _end;/* populate cmd_line too for later use, preserving boot_command_line */strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);*cmdline_p = cmd_line;early_fixmap_init();early_ioremap_init();parse_early_param();#ifdef CONFIG_MMUearly_mm_init(mdesc);
#endifsetup_dma_zone(mdesc);xen_early_init();efi_init();adjust_lowmem_bounds();arm_memblock_init(mdesc);adjust_lowmem_bounds();early_ioremap_reset();paging_init(mdesc);request_standard_resources(mdesc);if (mdesc->restart)arm_pm_restart = mdesc->restart;unflatten_device_tree();arm_dt_init_cpu_maps();psci_dt_init();
#ifdef CONFIG_SMPif (is_smp()) {if (!mdesc->smp_init || !mdesc->smp_init()) {if (psci_smp_available())smp_set_ops(&psci_smp_ops);else if (mdesc->smp)smp_set_ops(mdesc->smp);}smp_init_cpus();smp_build_mpidr_hash();}
#endifif (!is_smp())hyp_mode_check();reserve_crashkernel();#ifdef CONFIG_MULTI_IRQ_HANDLERhandle_arch_irq = mdesc->handle_irq;
#endif#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)conswitchp = &dummy_con;
#endif
#endifif (mdesc->init_early)mdesc->init_early();
}    
  • setup_processor():查找给定的机器 ID,根据机器 ID 设置 cache 相关标志位,再对处理器进行初始化。
  • setup_machine_fdt:根据设备树传递过来的指针,找到对应的内存,从内存中找到设备树传递的参数。
  • setup_machine_tags:根据机器 ID ,查找对应的 machine_desc 结构体,配置内存条信息。 machine_desc 结构体每个架构都有自己的,参数不一样。
  • setup_arch 中后面的操作基本都是围绕 machine_desc 结构体,对它进行填充或者调用。
  • paging_init(mdesc):页表初始化,这里主要是初始化了 bootmem 分配器,在初始化阶段担任内存分配任务(此时伙伴系统和 slab 并没有起来)。它首先通过检测出来的处理器类型进行处理器内核的初始化,然后通过 - bootmem_init() 函数根据系统定义的 meminfo 结构进行内存结构的初始化。
  • unflatten_device_tree():解析设备树,如果需要对设备树的内容进行深入研究,请从这里跟进去,看看是如何一步一步建立树状结构的。
  • setup_arch 是 start_kernel 阶段最重要的一个函数,/kernel/arch 下每个体系都有自己的 setup_arch 函数,具体编译哪个体现,看根目录 Makefile 中的 ARCH 变量。
2、mm_init

内存初始化函数,最主要作用是告诉系统有多少内存可以使用。
linux4.14/init/main.c

static void __init mm_init(void)
{page_ext_init_flatmem();mem_init();kmem_cache_init();pgtable_init();vmalloc_init();ioremap_huge_init();init_espfix_bsp();pti_init();
}

mem_init():关闭并释放 bootmem 分配器,释放未使用的页并入伙伴系统数据结构中,并转换为伙伴系统分配器。

kmem_cache_init():初始化 kmem_cache,启动 slab 分配器,用于小内存分配。

补充:

Linux 采用伙伴系统解决外部碎片的问题,采用 slab 解决内部碎片的问题。

内部碎片:内存已经被分配出去,明确属于某个进程,但却一直没有被使用。

外部碎片:内存还没有被分配出去,不属于任何进程,但由于其太小,夹在中间,无法满足新申请内存的需要,导致一直空闲。频繁的分配和回收物理页面会导致大量的、连续且小的页面夹在已经被分配的内存中间,产生外部碎片。

伙伴系统(buddy allocator)是以页为单位管理和分配内存,slab 以字节为单位分配内存(特别适合结构体的内存分配)。

注意,slab 不是和伙伴系统并列的,是基于伙伴系统的再一次分配,底层是伙伴系统。

slab、slub、slob 都是小内存分配器,slab 是基础,slub 和 slob 是优化版本。slub 适合大物理内存的大规模并行系统,slob 适合小型嵌入式系统,其实现只有 600 多行。具体使用哪个可以配置。

在这里插入图片描述

3、sched_init

调度器初始化。调度是一个子系统,内部非常复杂,最终目标是满足用户需求,使用户获得良好的系统体验。
Linux 内核实现了四种调度方式:deadline、realtime、CFS、idle

一般是采用 CFS 调度方式。作为一个普适性的操作系统,必须考虑各种需求,我们不能只按照中断优先级或者时间轮转片来规定进程运行的时间。作为一个多用户操作系统,必须考虑到每个用户的公平性。不能因为一个用户没有高级权限,就限制他的进程的运行时间,要考虑每个用户拥有公平的时间。

linux4.14/kernel/sched/core.c

void __init sched_init(void)
{int i, j;unsigned long alloc_size = 0, ptr;sched_clock_init();wait_bit_init();#ifdef CONFIG_FAIR_GROUP_SCHEDalloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHEDalloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endifif (alloc_size) {ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);#ifdef CONFIG_FAIR_GROUP_SCHEDroot_task_group.se = (struct sched_entity **)ptr;ptr += nr_cpu_ids * sizeof(void **);root_task_group.cfs_rq = (struct cfs_rq **)ptr;ptr += nr_cpu_ids * sizeof(void **);#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHEDroot_task_group.rt_se = (struct sched_rt_entity **)ptr;ptr += nr_cpu_ids * sizeof(void **);root_task_group.rt_rq = (struct rt_rq **)ptr;ptr += nr_cpu_ids * sizeof(void **);#endif /* CONFIG_RT_GROUP_SCHED */}

此函数主要是对进程调度器的数据结构进行初始化,如果是多核 CPU,就为每个 CPU 创建运行队列,初始调度策略为 CFS,后面根据上下文和进程的不同(实时进程、高优先级进程、特殊进程)可以切换调度策略。

GROUP_SCHED:主要是组调度相关,内核可以将进程或者线程分组,因为 Linux 是一个多用户操作系统,我们不希望因为某个用户的进程优先级一直很高,就使得其他用户的进程得不到运行。我们系统每个用户平分 CPU 时间,比如每个 APP 平分 CPU 时间。

cfs_rq 和 rt_rq:分别是 CFS 调度策略的 runqueue 和 Realtime 的 runqueue ,这里是对这个结构体进行初始化,方便后期调用。

不同调度策略出现的原因是我们有不同的进程:交互式进程(shell、vim)、批处理进程(文件 IO)、实时进程。根据进程的不同,组合各种调度策略,优化用户体验,满足用户需求。

CFS 调度器:完全公平调度,引入虚拟运行时间的概念,每次一个进程在 CPU 中执行了一段时间,就会增加它的虚拟运行时记录;每次选择要执行的进程时候,不是选择优先级最高的那个,而是选择虚拟运行时长最少的进程。(虚拟运行时间和真实运行时间不同,权重大的进程虚拟运行时间增长会缓慢)

deadline 调度器:按照进程最后截止期限来调度,选择快要截止的进程执行

realtime 调度器:实时调度器,为每个优先级维护一个队列,可以采用时间片轮转或者先进先出等方式。

idle 调度器:当系统空闲时处于这个调度方式。

多核情况下,在 CFS 调度策略中,每个 CPU 只维护本地的运行队列中的进程公平,多核之间需要定时进行负载均衡,做 load balance,不能出现一个 CPU 很忙,另外一个 CPU 很闲。

4、init_IRQ

中断初始化函数,这个很好理解,大家都用过中断。这里主要是在 irqchip_init() 函数中对 GIC 中断控制器进行初始化。

这部分就不多写了,不然乱了主次,本文主要聚焦启动流程,中断子系统我会单独写文章讲解。

linux4.14/arch/arm/kernel/irq.c

void __init init_IRQ(void)
{int ret;if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)irqchip_init();elsemachine_desc->init_irq();if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&(machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {if (!outer_cache.write_sec)outer_cache.write_sec = machine_desc->l2c_write_sec;ret = l2x0_of_init(machine_desc->l2c_aux_val,machine_desc->l2c_aux_mask);if (ret && ret != -ENODEV)pr_err("L2C: failed to init: %d\n", ret);}uniphier_cache_init();
}

中断子系统非常复杂,一般在 ARM 中,是 GIC 中断控制器,有 V1~V4 四个版本,V1 和 V2 是面向 32 位情况,V3 和 V4 是面向 64 位需求。这里会通过一系列调用,初始化 GIC 中断控制器。

GICV1 已经废弃,GICV2 在内核层面有三种中断类型:PPI、SPI、SGI。 GICV3 有四种中断类型: PPI、SPI、SGI、LPI。

在这里插入图片描述

第一个就是我们常用的配置的中断,本名叫异步中断。

第二个是陷阱,比如我们调用 open 函数这个系统调用,从用户态到内核态,就叫陷阱,是同步中断。所有的系统调用函数都是。

第三个是故障,比如内存缺页,如果内核可以修复,那OK,如果修复不了系统就会挂掉,大部分时间是修复不了的。

第四个是终止,就是系统直接挂掉的意思。

5、console_init

在这个函数初始化之前,你所有写的内核打印函数 printk 都打印不出东西,所有打印都会存在 buf 里,此函数初始化以后,会将 buf 里面的数据打印出来,你才能在终端看到 printk 打印的东西。

在此之前,如果确实有需要打印 log,可以使用 uboot 的打印函数。kernel 前期可以使用 early_printk。
tty 是 Linux 中的终端, _con_initcall_start 和_con_initcall_end 这两句的意思是执行所有两者之间的 initcall 函数。在两者中间会有串口驱动初始化的 init 函数。至于 initcall 机制,我会在驱动加载顺序的文章中进行讲解。

linux4.14/kernel/printk/printk.c

void __init console_init(void)
{initcall_t *call;/* Setup the default TTY line discipline. */n_tty_init();/** set up the console device so that later boot sequences can* inform about problems etc..*/call = __con_initcall_start;while (call < __con_initcall_end) {(*call)();call++;}
}

tty 驱动和其他驱动是不一样的,tty 的输入设备是键盘,输出设备是显示屏,而不像平常的设备输入输出是一个东西,因此驱动框架和普通的驱动不同:

tty架构,简单来分的话可以说成两层,下层我们的串口驱动层,它直接与硬件相接触,我们需要填充一个 struct uart_ops 的结构体,上层 tty 层,包括 tty 核心以及线路规程,它们各自都有一个 Ops 结构,用户空通过间是 tty 注册的字符设备节点来访问。

[图片]

tty 设备发送数据的流程为:tty 核心从一个用户获取将要发送给一个 tty 设备的数据,tty 核心将数据传递给 tty 线路规程驱动,接着数据被传到 tty 驱动,tty 驱动将数据转换为可以发给硬件的格式。

接收数据的流程为:从 tty 硬件接收到的数据向上交给 tty 驱动,接着进入 tty 线路规程驱动,再进入 tty 核心,在这里它被一个用户获取。

tty core 是 linux kernel 提供的,serial_driver 才是我们常说的串口驱动。
[图片]

由于输入输出不是一个设备,所以这里最底层只有 write,没有 read 函数,数据需要 push 上去。

6、vfs_caches_init

虚拟文件系统初始化,比如 sysfs,根文件系统等,就是在这一步进行挂载,proc 是内核虚拟的,用来输出内核数据结构信息,不算在这里。

vfs 虚拟文件系统,屏蔽了底层硬件的不同,提供了统一了接口,方便系统的移植和使用。使用户在不用更改应用代码的情况下直接移植代码到其他平台。

linux4.14/fs/dcache.c

void __init vfs_caches_init(void)
{names_cachep = kmem_cache_create("names_cache", PATH_MAX, 0,SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);dcache_init();inode_init();files_init();files_maxfiles_init();mnt_init();bdev_cache_init();chrdev_init();
}

这里的挂载主要在 mnt_init() 函数中:

linux4.14/fs/namespace.c

void __init mnt_init(void)
{int err;mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct mount),0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);mount_hashtable = alloc_large_system_hash("Mount-cache",sizeof(struct hlist_head),mhash_entries, 19,HASH_ZERO,&m_hash_shift, &m_hash_mask, 0, 0);mountpoint_hashtable = alloc_large_system_hash("Mountpoint-cache",sizeof(struct hlist_head),mphash_entries, 19,HASH_ZERO,&mp_hash_shift, &mp_hash_mask, 0, 0);if (!mount_hashtable || !mountpoint_hashtable)panic("Failed to allocate mount hash table\n");kernfs_init();err = sysfs_init();if (err)printk(KERN_WARNING "%s: sysfs_init error: %d\n",__func__, err);fs_kobj = kobject_create_and_add("fs", NULL);if (!fs_kobj)printk(KERN_WARNING "%s: kobj create error\n", __func__);init_rootfs();init_mount_tree();
}

虚拟文件系统有几百个,常用的有三个:

sysfs:/sys 目录下,主要用来反馈【驱动】信息。

procfs:/proc 目录下,主要用来【反馈】内核数据结构和进程信息。

debugfs:挂载在 /sys/kernel/debug,主要用来 debug。

这三个文件系统创建文件夹和文件的函数都不一样,需要区分。

7、rest_init

这个函数可以算是 start_kernel 函数调用的最后一个函数,在这里产生了最重要的两个内核线程 kernel_init 和 kthreadd,kernel_init 后面会从内核空间跳转到用户空间,变成用户空间的 init 进程,PID=1,而 kthreadd ,PID=2,是内核进程,专门用来监听创建内核进程的请求,它维护了一个链表,如果有创建内核进程的需求,就会在链表上创建。

至此,用户空间最重要的 init 进程已经出来,后面用户空间的进程都由 init 进程来 fork。如果是安卓系统,init 进程会 fork 出一个 zygote 进程,他是所有安卓系统进程的父进程。

linux4.14/init.main.c

static noinline void __ref rest_init(void)
{struct task_struct *tsk;int pid;rcu_scheduler_starting();pid = kernel_thread(kernel_init, NULL, CLONE_FS);rcu_read_lock();tsk = find_task_by_pid_ns(pid, &init_pid_ns);set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));rcu_read_unlock();numa_default_policy();pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);rcu_read_lock();kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);rcu_read_unlock();system_state = SYSTEM_SCHEDULING;complete(&kthreadd_done);/** The boot idle thread must execute schedule()* at least once to get things moving:*/schedule_preempt_disabled();/* Call into cpu_idle with preempt disabled */cpu_startup_entry(CPUHP_ONLINE);
}

第 8 行创建了 kernel_init 进程,第 16 行创建了 kthreadd 进程,这两个都是内核进程。23 行通知 kernel_init 进程 kthreadd 已经创建完毕。也就是说,实际上是 kthreadd 先运行,kernel_init 再运行。

8、kernel_init

kernel4.14/init/main.c

static int __ref kernel_init(void *unused)
{int ret;kernel_init_freeable();async_synchronize_full();ftrace_free_init_mem();free_initmem();mark_readonly();system_state = SYSTEM_RUNNING;numa_default_policy();rcu_end_inkernel_boot();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);}if (execute_command) {ret = run_init_process(execute_command);if (!ret)return 0;panic("Requested init %s failed (error %d).",execute_command, ret);}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.");
}

10 行:设置系统为运行状态。

16 行:运行 init 进程,如果根目录没找到 /init,就从 30-33 行的目录去找,如果还找不到,系统就启动失败了。

9、kthreadd

kernel4.14/kernel/kthread.c

int kthreadd(void *unused)
{struct task_struct *tsk = current;/* Setup a clean context for our children to inherit. */set_task_comm(tsk, "kthreadd");ignore_signals(tsk);set_cpus_allowed_ptr(tsk, cpu_all_mask);set_mems_allowed(node_states[N_MEMORY]);current->flags |= PF_NOFREEZE;cgroup_init_kthreadd();for (;;) {set_current_state(TASK_INTERRUPTIBLE);if (list_empty(&kthread_create_list))schedule();__set_current_state(TASK_RUNNING);spin_lock(&kthread_create_lock);while (!list_empty(&kthread_create_list)) {struct kthread_create_info *create;create = list_entry(kthread_create_list.next,struct kthread_create_info, list);list_del_init(&create->list);spin_unlock(&kthread_create_lock);create_kthread(create);spin_lock(&kthread_create_lock);}spin_unlock(&kthread_create_lock);}return 0;
}

16 行:kthread_create_list 判断这个链表是否为空,不为空说明有人申请创建内核线程,开始创建。如果为空就被调度去休眠。

18 行:如果不为空,设置为运行态。

20 行:上锁,在内核做事情必须时刻注意并发。

24 行:获取需要创建线程的信息。

27 行:解锁。也就是获取信息的时候需要加锁。

29 行:创建内核线程。

31 行:加锁。

也就是在 while 循环中,获取完信息先解锁,创建内核线程后加锁。在 while 循环外部,有一个加锁和解锁的操作,这样加锁解锁刚好匹配。

其余的函数大家可以参照下面的文章去理解:

https://www.cnblogs.com/andyfly/p/9410441.html
https://www.cnblogs.com/lifexy/p/7366782.html

5、Android 启动流程

Andorid 系统是在嵌入式行业中广泛应用的系统,手机、平板、机器人、汽车中控系统都有使用安卓系统的,在应用方面的优势是安卓系统自带 UI,使用 Linux 需要自己用 QT 开发界面,并且界面没那么好看。

Android 系统架构图:

在这里插入图片描述

由此图可知,Android 系统基于 Linux 内核。

Android 系统启动流程如下:

在这里插入图片描述

  1. Boot ROM 是固化在硬件中的一段代码,它一般是固定的,它的作用是检测基本的硬件是否存在,比如检测 EMMC 是否存在,存在的话,把 bootloader 从 EMMC 拷贝到 SRAM ,启动系统,后面交给 bootloader 。
  2. Andorid 系统的启动引导程序 bootloader 不是 uboot,是 LK(little kernel),专门用来启动安卓系统。
  3. bootloader 初始化完成后跳转到 Linux 内核的 start_kernel 函数,此函数最后产生了 kernel_init 和 kthreadd,kernel_init 后面会从内核空间跳转到用户空间,变成用户空间的 init 进程,PID=1,而 kthreadd(PID=2)是内核进程,专门用来监听创建内核进程的请求,它维护了一个链表,如果有创建内核进程的需求,就会在链表上创建。当然你可以看到图中还有一个swapper(PID=0)进程,它是系统中唯一一个不使用 fork 创建的进程,kernel_init 和 kthreadd 就是由它创建的,swapper 也叫 idle 进程,空闲进程,它运行的时候就是系统处于空闲状态的时候。
  4. 当 kernel_init 转变为用户空间的 init 进程以后,会在 Andorid 系统中创建一个最重要的进程:Zygote,此进程会创建安卓所需要的所有进程。

1)Android 层级分析

这个图中 kernel 和 native 层的通信是 syscall,这个大家都很熟悉,就是系统调用,毕竟 C++ 调用 C 语言还是很简单的。

而 FrameWork 层和 Native 层通信就比较复杂了,java 如何调用 C++ 语言,这里会有一个 JNI 机制,JNI 有特定的语法,类似于 C 语言但又不是 C 语言,他可以实现 java 调用 C++的函数,这个过程需要 Android Runtime(ART) 安卓虚拟机的配合。

在 Native 层中,有很多 C++ 写的系统服务,供上层使用,比如最重要的ServiceManager,管理所有其他服务的服务。

2)调用实例分析

手机 app 想要控制喇叭、LED 等硬件,要从 app 传递到 kernel,操作硬件,这个流程要比 Linux 的应用程序复杂得多。并且方式不止一种,举例:

  1. app 通过直接读写 kernel 节点,向其写入数据,这就类似于在 Linux 命令行直接 echo,这是最简单的方式。因为 java 本身也有文件读写函数,有按字节读写和字符串读写两种方式。
  2. app 的 java 语言调用 JNI 文件,JNI 调用 C 语言,在 C 函数中去操作节点。
  3. 也可以用 C++ 写一个 Native 服务,APP 通过 binder 通信访问这个服务,在这个服务中操作节点。当然 socket 通信也可以。

3)Android 权限问题

以上所有的操作都需要权限,安卓系统的权限限制的比较严格,防止黑客破解。

如果你有 root 权限,那么你可以在安卓系统命令行中输入 setenforce 0 来关闭 Android 系统的 SELinux 检查机制,那基本上你所有操作都可以被允许。

Linux 系统安全机制是:我是 root,我派出去的程序访问任何东西也应该是 root 权限,没有人可以阻止我。

Andorid 系统安全机制是:不管你是谁,做任何事情都要提前申请,否则会被 SELinux 检查,没有提前申请的行为都会被拒绝,看 log 会发现很多 avc deny。

举个形象一点的例子:一个公司老板,派他的儿子去自己的公司上班,按理来说是应该类似于 root 权限,谁能挡我?实际上呢,到公司上班可以,因为老板提前说了,但是去卫生间要申请权限,用电脑要申请权限,用打印机要申请权限,除了过来上班,其余任何没有提前说明的行为都会被拒绝。

这就是 Andorid 系统严格的安全机制,防止了黑客破解 root 权限以后乱搞你的手机,比如:内置一个程序,定期访问你的 xxx 文件,然后通过网络发出去。

6、Linux 驱动加载顺序分析

从上文可以得出,start_kernel 函数最后调用的是 rest_init 函数,其实 rest_init 函数不光产生了最重要的 kernel_init (PID=1)和 kthreadd (PID=2)内核进程。

kernel_init 最后演变为用户空间 init 进程(PID=1)。

rest_init 函数还有一个重要的分支:加载驱动模块,调用流程如下:

start_kernel|--->rest_init |--->kernel_init|--->kernel_init_freeable|--->do_basic_setup|--->driver_init|--->do_initcalls|--->do_initcall_level|--->do_one_initcall

注意,这里就是驱动的初始化和驱动模块的加载。

我们知道在 rest_init 函数中,最重要的 1 号进程和 2 号进程都已经起来了,也就是说系统已经真正起来了。1 号 2 号进程起来之前,文件系统的挂载是在调用 rest_init 函数之前就挂载好了,此时加载驱动是可以的。

那么这里是如何挂载的呢?

流程中 driver_init 函数会对各个驱动入口函数进行初始化,也就是在内存中对驱动初始化函数进行寻址。而 do_initcalls 函数中,会按照驱动的优先级,对驱动一个一个进行挂载。

linux4.14/init/main.c
[图片]
[图片]

驱动的优先级:Linux 把系统中需要挂载的各种东西,都分为14个等级,分别为 1–1s–2–2s–3–3s–4–4s–5–5s–6–6s–7–7s,数字越小优先级越高,定义在:

linux4.14/include/linux/init.h
[图片]

一般我们自己写的驱动模块,文件最后会声明一个 module_init 和 module_exit ,实际上被定义为 device_initcall,优先级为6,是要比架构初始化模块和文件系统模块优先级低。

如果驱动模块之间有依赖,需要更改模块挂载顺序,有三种方式:

  1. 增加一个优先级,比如 8。或者把自己的驱动模块声明成其他优先级,也就是不用 module_init 去声明,可以用 fs_initcall 去声明。
  2. 对于同一优先级的驱动模块,可以在 Makefile 中更改其编译和链接的顺序,就会切换其挂载的顺序。(静态编译)
  3. 动态加载驱动模块:等 Linux 系统起来以后,手动执行 insmod 和 rmmod 即可挂载和卸载驱动,顺序自己决定。测试成功后,再搞到内核中静态编译。

虽然可以更改挂载顺序,但还是希望大家写驱动模块的时候,能够做到高内聚、低耦合,自己的模块最好不要依赖其他模块,防止其他模块加载失败导致自己的模块不可用。

如何看驱动挂载顺序?有两种方式:

1、找到编译后的 Linux 内核源码,根目录下面有个 System.map 文件,这里记载了 Linux 内核所做的所有的事情,是按顺序记载的(也有可能在其他输出目录)。

一共有三列:地址、区域、操作。在操作中我们可以看到我们声明的驱动的名字。

在这里插入图片描述

2、如果你驱动模块有加一些打印,可以直接看 log。

7、initcall 机制

普通我们写一个程序,想要它被调用,需要在主流程中调用这个函数,才算被调用。

那么这种方式如果放在 Linux 中,是难以想象的,我们自己写的代码要在多少个地方声明。

而你如果采用 initcall 机制,意思就是说,你使用一个字符串声明你的驱动初始化函数,那么所有的驱动初始化函数都存在内存中一个连续的段中,系统启动以后,会从这个段的第一个函数开始,一个一个遍历,进而一个一个调用,这就是 initcall 机制。这就是为什么我们写驱动只需要使用 module_init 声明,编译进去即可自动被调用的原因!

8、System.map

编译后的内核根目录 System.map 文件记载了所有的驱动加载顺序,如果你不确定驱动的加载顺序,在这里查看就可以,每次编译 Linux 内核就会产生一个新的 System.map。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/346777.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5.mongodb 备份与恢复

mongodb备份工具介绍&#xff1a; 1.mongoexport(备份)/mongoimport(恢复) mongoexport是MongoDB提供的一个工具&#xff0c;用于将数据从MongoDB实例导出到JSON或CSV格式的文件中&#xff0c;这个工具对于数据迁移、数据备份或者在不同的数据库之间同步数据非常有用 2.mongodu…

鱼泡-伙伴匹配系统

第一次直播 项目介绍 帮助找到志同道合的伙伴 需求分析 标签分类 主动搜索 组队 创建队伍加入队伍根据标签查询队伍 前端项目初始化 项目初始化文件夹中一定不要带空格&#xff01; 使用npm&#xff08;node包管理器&#xff09;来安装 Vite 脚手架&#xff1a; vue cl…

USB (5)

USB是一个poll的总线。并且数据传输是对齐到time-line的。 对于比如鼠标这样的设备&#xff0c;主机会最快125us poll一次&#xff0c;看是否有输入。这也就是interrupt transfer类型。 对于isochronous transfer类型&#xff0c;数据是在固定的时隙传输的&#xff0c;但不保证…

Python魔法之旅-魔法方法(22)

目录 一、概述 1、定义 2、作用 二、应用场景 1、构造和析构 2、操作符重载 3、字符串和表示 4、容器管理 5、可调用对象 6、上下文管理 7、属性访问和描述符 8、迭代器和生成器 9、数值类型 10、复制和序列化 11、自定义元类行为 12、自定义类行为 13、类型检…

怎么改图片分辨率的dpi数值?简单调整图片dpi的方法

图片分辨率的dpi是目前使用图片时比较常见的要求之一&#xff0c;在网上上传图片时比如证件照类型&#xff0c;都经常会对图片dpi数值有要求。在使用图片的时候&#xff0c;如果dpi的数值不满足用户使用&#xff0c;那么就会无法正常上传使用&#xff0c;那么修改图片api具体该…

42.vue-element-admin界面上的search字段配置

vue-element-admin界面上的search字段&#xff08;下图红色部分&#xff09;是可配置&#xff0c;使用*.vue里的search关键字进行配置。 一、配置方法 1.如果这个字段要放到search区域&#xff0c;则&#xff1a; search: {hidden: false}, 2.如果这个字段不要放到search区域…

实验七、创建小型实验拓扑《计算机网络》

早检到底是谁发明出来的。 一、实验目的 完成本实验后&#xff0c;您将能够&#xff1a; • 设计逻辑网络。 • 配置物理实验拓扑。 • 配置 LAN 逻辑拓扑。 • 验证 LAN 连通性。 二、实验任务 在本实验中&#xff0c;将要求您连接网络设备并配置主机实现基本的网络…

贪心算法-数组跳跃游戏(mid)

目录 一、问题描述 二、解题思路 1.回溯法 2.贪心算法 三、代码实现 1.回溯法实现 2.贪心算法实现 四、刷题链接 一、问题描述 二、解题思路 1.回溯法 使用递归的方式&#xff0c;找到所有可能的走步方式&#xff0c;并记录递归深度&#xff08;也就是走步次数&#x…

【AI法官】人工智能判官在线判案?

概述 AI法官是一款为用户提供专业法律分析和判决建议的智能体应用。用户只需简要描述案情&#xff0c;AI法官便会利用其强大的法律知识和逻辑推理能力&#xff0c;快速且准确地梳理出判决结果。该应用的目标是为用户提供高效、准确、合法的判决建议。 角色任务 任务描述 作为…

小程序 UI 风格魅力非凡

小程序 UI 风格魅力非凡

Oracle的优化器

sql优化第一步&#xff1a;搞懂Oracle中的SQL的执行过程 从图中我们可以看出SQL语句在Oracle中经历了以下的几个步骤&#xff1a; 语法检查&#xff1a;检查SQL拼写是否正确&#xff0c;如果不正确&#xff0c;Oracle会报语法错误。 语义检查&#xff1a;检查SQL中的访问对象…

海南聚广众达电子商务咨询有限公司打造一站式电商服务

在数字经济的浪潮中&#xff0c;电商行业蓬勃发展&#xff0c;各种平台和服务商如雨后春笋般涌现。其中&#xff0c;海南聚广众达电子商务咨询有限公司凭借其专业的团队和丰富的经验&#xff0c;在抖音电商服务领域独树一帜&#xff0c;成为业界的佼佼者。 海南聚广众达电子商…

判断对称树

leetcode - 101 - 对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&a…

注册小程序

每个小程序都需要在 app.js 中调用 App 方法注册小程序实例&#xff0c;绑定生命周期回调函数、错误监听和页面不存在监听函数等。 详细的参数含义和使用请参考 App 参考文档 。 整个小程序只有一个 App 实例&#xff0c;是全部页面共享的。开发者可以通过 getApp 方法获取到全…

【递归、搜索与回溯】穷举vs暴搜vs深搜vs回溯vs剪枝

穷举vs暴搜vs深搜vs回溯vs剪枝 1.全排列2.子集 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 管他什么深搜、回溯还是剪枝&#xff0c;画出决…

【Java】解决Java报错:NoClassDefFoundError

文章目录 引言1. 错误详解2. 常见的出错场景2.1 类路径配置错误2.2 依赖库缺失2.3 类文件被删除或损坏2.4 类加载器问题 3. 解决方案3.1 检查类路径配置3.2 检查依赖库3.3 检查类文件3.4 调试类加载器问题 4. 预防措施4.1 使用构建工具管理依赖4.2 定期进行构建和测试4.3 使用I…

【Unity+AI01】在Unity中调用DeepSeek大模型!实现AI对话功能!

要在Unity中调用DeepSeek的API并实现用户输入文本后返回对话的功能&#xff0c;你需要遵循以下步骤&#xff1a; 获取API密钥&#xff1a; 首先&#xff0c;你需要从DeepSeek获取API密钥。这通常涉及到注册账户&#xff0c;并可能需要订阅相应的服务。 集成HTTP请求库&#xf…

【APP移动端自动化测试】第一节.环境配置和adb调试工具

文章目录 前言一、Java环境搭建二、AndroidSDK环境搭建三、Android模拟器安装四、adb调试工具基本介绍 4.1 adb构成和基本原理 4.2 adb获取包名&#xff0c;界面名 4.3 adb文件传输 4.4 adb获取app启动时间 4.5 adb获取手机日志 4.6 adb其他有关…

python的resample()函数

介绍 在Python中,resample()函数是一个常用的工具,用于对时间序列数据进行重新采样。这个函数可以将时间序列数据从一个频率转换为另一个频率,比如将每天的数据转换为每月的数据。在本教程中,我将向你展示如何使用resample()函数,并解释每个步骤的具体含义。 整体流程 首先…

独具魅力的 App UI 风格才能称之为优秀

独具特色的App UI 长什么样&#xff01;看这里