Linux电源管理——PSCI初始化流程和多核启动流程
目录
一、PSCI 初始化流程
1、PSCI设备树节点
2、PSCI kernel初始化流程
get_set_conduit_method
set_conduit
psci_probe
二、CPU PSCI 操作初始化流程
1、CPU 设备树节点
2、 struct cpu_operations
3、kernel 流程
cpu_read_bootcpu_ops
smp_init_cpus
三、CPU PSCI多核启动流程
1、boot cpu 启动流程
2、secondary CPU 启动流程
QEMU Version:qemu-7.2.0
Linux Version:linux-5.4.239
本文主要分析了在ARM64架构中的PSCI电源管理接口在Linux内核中的实现流程,并分析了linux系统中如何通过 PSCI 接口启动 CPUs。
一、PSCI 初始化流程
PSCI(Power State Coordination Interface
),是由ARM定义的电源管理接口规范,Linux系统可以通过smc/hvc
指令来进入不同的Exception Level
,而调用对应的实现函数,下面将对 PSCI 设备树和 linux kernel 源码中 PSCI 的初始化流程进行简单分析。
1、PSCI设备树节点
psci {migrate = <0xc4000005>;cpu_on = <0xc4000003>;cpu_off = <0x84000002>;cpu_suspend = <0xc4000001>;method = "hvc";compatible = "arm,psci-1.0\0arm,psci-0.2\0arm,psci";
};
migrate:定义 CPU 任务迁移操作的函数入口地址。
cpu_on:定义启动 CPU 的函数入口地址。
cpu_off:定义关闭 CPU 的函数入口地址。
cpu_suspend:定义 CPU 挂起操作的函数入口地址,以将 CPU 置于低功耗。
method:指定调用 PSCI 函数的方式,hvc 表示通过 Hypervisor Call 指令触发 PSCI 操作,这里还有 smc(Secure Monitor Call)以陷入 ATF 。
compatible:声明设备树节点兼容的 PSCI 规范版本,优先匹配最旧的版本,即 arm,psci-0.2
2、PSCI kernel初始化流程
从 start_kernel 函数开始分析,如下:
start_kernel init/main.c
setup_arch arch/arm64/kernel/setup.c
setup_machine_fdt(__fdt_pointer)
psci_dt_init
在 setup_arch 函数中首先会调用 setup_machine_fdt 函数解析设备树,其中参数__fdt_pointer是从 arch/arm64/kernel/head.S 文件中传过来的,如下:
/** The following fragment of code is executed with the MMU enabled.** x0 = __PHYS_OFFSET*/
__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)
在解析完设备树之后会调用 psci_dt_init 函数初始化 PSCI,如下:
// drivers/firmware/psci/psci.ctypedef int (*psci_initcall_t)(const struct device_node *);static const struct of_device_id psci_of_match[] __initconst = {{ .compatible = "arm,psci", .data = psci_0_1_init},{ .compatible = "arm,psci-0.2", .data = psci_0_2_init},{ .compatible = "arm,psci-1.0", .data = psci_1_0_init},{},
};int __init psci_dt_init(void)
{struct device_node *np;const struct of_device_id *matched_np;psci_initcall_t init_fn;int ret;np = of_find_matching_node_and_match(NULL, psci_of_match, &matched_np);if (!np || !of_device_is_available(np))return -ENODEV;init_fn = (psci_initcall_t)matched_np->data;ret = init_fn(np);of_node_put(np);return ret;
}
of_find_matching_node_and_match
函数用于在设备树中查找与指定 psci_of_match 匹配表兼容的节点并初始化matched_np,并且这里会优先使用“arm,psci-1.0” 字段进行匹配并向后兼容。
在初始化完 matched_np 之后再初始化 init_fn 为 matched_np->data,也即是 init_fn = psci_1_0_init(compatible = "arm,psci-1.0")。
最后调用 init_fn 即 psci_1_0_init,如下:
static int __init psci_1_0_init(struct device_node *np)
{int err;err = psci_0_2_init(np);if (err)return err;if (psci_has_osi_support())pr_info("OSI mode supported.\n");return 0;
}
再进入 psci_0_2_init 函数,如下:
/** PSCI init function for PSCI versions >=0.2** Probe based on PSCI PSCI_VERSION function*/
static int __init psci_0_2_init(struct device_node *np)
{int err;err = get_set_conduit_method(np);if (err)return err;/** Starting with v0.2, the PSCI specification introduced a call* (PSCI_VERSION) that allows probing the firmware version, so* that PSCI function IDs and version specific initialization* can be carried out according to the specific version reported* by firmware*/return psci_probe();
}
psci_0_2_init 函数就是用来初始化 PSCI 相关函数的,并且PSCI版本需要大于等于0.2(PSCI < v0.2 调用 psci_0_1_init) 如下:
get_set_conduit_method
static int get_set_conduit_method(struct device_node *np)
{const char *method;pr_info("probing for conduit method from DT.\n");if (of_property_read_string(np, "method", &method)) {pr_warn("missing \"method\" property\n");return -ENXIO;}if (!strcmp("hvc", method)) {set_conduit(PSCI_CONDUIT_HVC);} else if (!strcmp("smc", method)) {set_conduit(PSCI_CONDUIT_SMC);} else {pr_warn("invalid \"method\" property: %s\n", method);return -EINVAL;}return 0;
}
该函数首先会解析PSCI设备树节点 np
中的 method
属性,以确定通信指令,如果需要陷入到 hypervisor 则为 hvc,否则如果需要陷入到 ATF,则为 smc,具体需要看PSCI节点的配置。
set_conduit
typedef unsigned long (psci_fn)(unsigned long, unsigned long,unsigned long, unsigned long);
static psci_fn *invoke_psci_fn;static unsigned long __invoke_psci_fn_hvc(unsigned long function_id,unsigned long arg0, unsigned long arg1,unsigned long arg2)
{struct arm_smccc_res res;arm_smccc_hvc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res);return res.a0;
}static unsigned long __invoke_psci_fn_smc(unsigned long function_id,unsigned long arg0, unsigned long arg1,unsigned long arg2)
{struct arm_smccc_res res;arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res);return res.a0;
}static void set_conduit(enum psci_conduit conduit)
{switch (conduit) {case PSCI_CONDUIT_HVC:invoke_psci_fn = __invoke_psci_fn_hvc;break;case PSCI_CONDUIT_SMC:invoke_psci_fn = __invoke_psci_fn_smc;break;default:WARN(1, "Unexpected PSCI conduit %d\n", conduit);}psci_ops.conduit = conduit;
}
set_conduit 函数主要是根据 conduit 的不同而对函数指针 invoke_psci_fn 进行初始化,以方便后面调用。
psci_probe
在分析psci_probe函数之前先看一个PSCI结构体,如下:
struct psci_operations {u32 (*get_version)(void);int (*cpu_suspend)(u32 state, unsigned long entry_point);int (*cpu_off)(u32 state);int (*cpu_on)(unsigned long cpuid, unsigned long entry_point);int (*migrate)(unsigned long cpuid);int (*affinity_info)(unsigned long target_affinity,unsigned long lowest_affinity_level);int (*migrate_info_type)(void);enum psci_conduit conduit;enum smccc_version smccc_version;
};
struct psci_operations
是 Linux 内核中用于抽象 PSCI 接口的核心数据结构,里面定义了操作系统与 Hypervisor/ATF 之间交互的电源管理函数集,如下:
get_version:返回 PSCI 版本号
cpu_suspend:将 CPU 置于指定低功耗状态(
state
),并在唤醒时跳转到entry_point
cpu_off:关闭CPU
cpu_on:开启指定 CPU(
cpuid
),并设置其启动地址为entry_point
migrate:将当前任务迁移到指定 CPU(
cpuid
)affinity_info:查询 CPU 拓扑的亲和性信息
migrate_info_type:返回迁移信息的类型编码
conduit:
PSCI_CONDUIT_HVC
或PSCI_CONDUIT_SMC
smccc_version:
SMCCC_VERSION_1_0
或SMCCC_VERSION_1_1
通过 get_set_conduit_method 函数设置好PSCI通信方法之后,则调用 psci_probe 函数初始化对应版本的电源管理接口,如下:
/** Probe function for PSCI firmware versions >= 0.2*/
static int __init psci_probe(void)
{u32 ver = psci_get_version();pr_info("PSCIv%d.%d detected in firmware.\n",PSCI_VERSION_MAJOR(ver),PSCI_VERSION_MINOR(ver));if (PSCI_VERSION_MAJOR(ver) == 0 && PSCI_VERSION_MINOR(ver) < 2) {pr_err("Conflicting PSCI version detected.\n");return -EINVAL;}psci_0_2_set_functions();psci_init_migrate();if (PSCI_VERSION_MAJOR(ver) >= 1) {psci_init_smccc();psci_init_cpu_suspend();psci_init_system_suspend();psci_init_system_reset2();}return 0;
}
函数首先获取 PSCI的版本号,看是否符合大于等于 0.2 的标准,如果符合则调用 psci_0_2_set_functions 函数,如下:
static void __init psci_0_2_set_functions(void)
{pr_info("Using standard PSCI v0.2 function IDs\n");psci_ops.get_version = psci_get_version;psci_function_id[PSCI_FN_CPU_SUSPEND] =PSCI_FN_NATIVE(0_2, CPU_SUSPEND);psci_ops.cpu_suspend = psci_cpu_suspend;psci_function_id[PSCI_FN_CPU_OFF] = PSCI_0_2_FN_CPU_OFF;psci_ops.cpu_off = psci_cpu_off;psci_function_id[PSCI_FN_CPU_ON] = PSCI_FN_NATIVE(0_2, CPU_ON);psci_ops.cpu_on = psci_cpu_on;psci_function_id[PSCI_FN_MIGRATE] = PSCI_FN_NATIVE(0_2, MIGRATE);psci_ops.migrate = psci_migrate;psci_ops.affinity_info = psci_affinity_info;psci_ops.migrate_info_type = psci_migrate_info_type;arm_pm_restart = psci_sys_reset;pm_power_off = psci_sys_poweroff;
}
可以看到 psci_0_2_set_functions 函数其实就是对 psci_operations 进行初始化,设置相应的回调函数,比如当操作系统需要hotplug时,就会调用到 psci_cpu_off/psci_cpu_on 函数,到此为止 PSCI 的初始化就完成,下面将介绍对应 CPU 的相关初始化。
二、CPU PSCI 操作初始化流程
1、CPU 设备树节点
cpus {#size-cells = <0x00>;#address-cells = <0x01>;......cpu@0 {phandle = <0x8004>;reg = <0x00>;enable-method = "psci";compatible = "arm,cortex-a53";device_type = "cpu";};cpu@1 {phandle = <0x8003>;reg = <0x01>;enable-method = "psci";compatible = "arm,cortex-a53";device_type = "cpu";};......
};
这里只关注“enable-method”字段,该字段就描述了该 CPU 的启动方式,这里是启动方式是 “PSCI”,还有一种“spin-table”启动方式这里将不再说明。
2、 struct cpu_operations
/*** struct cpu_operations - Callback operations for hotplugging CPUs.** @name: Name of the property as appears in a devicetree cpu node's* enable-method property. On systems booting with ACPI, @name* identifies the struct cpu_operations entry corresponding to* the boot protocol specified in the ACPI MADT table.* @cpu_init: Reads any data necessary for a specific enable-method for a* proposed logical id.* @cpu_prepare: Early one-time preparation step for a cpu. If there is a* mechanism for doing so, tests whether it is possible to boot* the given CPU.* @cpu_boot: Boots a cpu into the kernel.* @cpu_postboot: Optionally, perform any post-boot cleanup or necesary* synchronisation. Called from the cpu being booted.* @cpu_can_disable: Determines whether a CPU can be disabled based on* mechanism-specific information.* @cpu_disable: Prepares a cpu to die. May fail for some mechanism-specific* reason, which will cause the hot unplug to be aborted. Called* from the cpu to be killed.* @cpu_die: Makes a cpu leave the kernel. Must not fail. Called from the* cpu being killed.* @cpu_kill: Ensures a cpu has left the kernel. Called from another cpu.* @cpu_init_idle: Reads any data necessary to initialize CPU idle states for* a proposed logical id.* @cpu_suspend: Suspends a cpu and saves the required context. May fail owing* to wrong parameters or error conditions. Called from the* CPU being suspended. Must be called with IRQs disabled.*/
struct cpu_operations {const char *name;int (*cpu_init)(unsigned int);int (*cpu_prepare)(unsigned int);int (*cpu_boot)(unsigned int);void (*cpu_postboot)(void);
#ifdef CONFIG_HOTPLUG_CPUbool (*cpu_can_disable)(unsigned int cpu);int (*cpu_disable)(unsigned int cpu);void (*cpu_die)(unsigned int cpu);int (*cpu_kill)(unsigned int cpu);
#endif
#ifdef CONFIG_CPU_IDLEint (*cpu_init_idle)(unsigned int);int (*cpu_suspend)(unsigned long);
#endif
};
struct cpu_operations
是 Linux 内核中定义 CPU hotplug 与电源管理操作的核心数据结构,为不同架构(如 ARM、x86)提供统一的接口,这也是 linux 常用的一种方法了。如下:
name:该操作集的名称,
即 cpu
节点的enable-method
属性cpu_init:初始化指定 CPU 的特定数据(如寄存器配置)
cpu_prepare:准备启动 CPU,验证其可启动性
cpu_boot:启动 CPU,(会跳转到
PSCI_CPU_ON
)cpu_postboot:在目标 CPU 上执行启动后的清理或同步操作
cpu_can_disable:查 CPU 是否可安全禁用
cpu_disable:禁用 CPU 的中断和定时器,准备关闭
cpu_die:使 CPU 退出内核(如进入 WFI )
cpu_kill:确认 CPU 已完全关闭(从其他 CPU 调用)
cpu_init_idle:初始化 CPU idle
cpu_suspend:suspend CPU 并保存上下文
3、kernel 流程
介绍完cpu_operations数据结构后再回到 setup_arch 函数中:
setup_arch
cpu_read_bootcpu_ops (CPU0 cpu_operations 的初始化)
smp_init_cpus (CPUx cpu_operations 的初始化)
cpu_read_bootcpu_ops
// arch/arm64/kernel/cpu_ops.c/** Read a cpu's enable method and record it in cpu_ops.*/
int __init cpu_read_ops(int cpu)
{const char *enable_method = cpu_read_enable_method(cpu);if (!enable_method)return -ENODEV;cpu_ops[cpu] = cpu_get_ops(enable_method);if (!cpu_ops[cpu]) {pr_warn("Unsupported enable-method: %s\n", enable_method);return -EOPNOTSUPP;}return 0;
}static inline void __init cpu_read_bootcpu_ops(void)
{cpu_read_ops(0);
}
通过注释就能够看出cpu_read_ops函数是用来读取CPU0的enable方法并将其记录在cpu_ops中,cpu_read_enable_method 函数就是从CPU0的设备树节点中读取“enable-method”属性值,并初始化给 enable_method 变量(通过上面 CPU0 的设备数节点知道这个值为 PSCI),因为这个函数功能比较简单,这里将不再展开分析。
再继续往下走,首先注意到一个全局的指针数组,如下:
const struct cpu_operations *cpu_ops[NR_CPUS] __ro_after_init;
这里 cpu_ops
是一个 指针数组,包含 NR_CPUS
个元素,每个元素是 const struct cpu_operations*
类型的指针,主要是用于为每个 CPU 提供独立的操作函数集。所以这里首先是对 CPU0 设置 cpu_operations 函数操作集。
cpu_get_ops 函数如下:
// arch/arm64/kernel/cpu_ops.cextern const struct cpu_operations smp_spin_table_ops;
extern const struct cpu_operations cpu_psci_ops;const struct cpu_operations *cpu_ops[NR_CPUS] __ro_after_init;static const struct cpu_operations *const dt_supported_cpu_ops[] __initconst = {&smp_spin_table_ops,&cpu_psci_ops,NULL,
};static const struct cpu_operations * __init cpu_get_ops(const char *name)
{const struct cpu_operations *const *ops;ops = acpi_disabled ? dt_supported_cpu_ops : acpi_supported_cpu_ops;while (*ops) {if (!strcmp(name, (*ops)->name))return *ops;ops++;}return NULL;
}
cpu_get_ops 函数其实就是通过 acpi_disabled 值的不同对 CPU0 的 cpu_operations 进行不同的初始化,acpi_disabled 默认是1(include/linux/acpi.h),所以设置 dt_supported_cpu_ops 为 CPU0 的操作函数集,前面 cpu_read_bootcpu_ops 函数获取了 CPU0 的 enable-method = PSCI,并传到了 cpu_get_ops 函数,所以这里通过 strcmp 函数最终确定了该 CPU0 的 cpu_operations 为
cpu_psci_ops,如下:
// arch/arm64/kernel/psci.c
static int __init cpu_psci_cpu_init(unsigned int cpu)
{return 0;
}static int __init cpu_psci_cpu_prepare(unsigned int cpu)
{if (!psci_ops.cpu_on) {pr_err("no cpu_on method, not booting CPU%d\n", cpu);return -ENODEV;}return 0;
}static int cpu_psci_cpu_boot(unsigned int cpu)
{int err = psci_ops.cpu_on(cpu_logical_map(cpu), __pa_symbol(secondary_entry));if (err)pr_err("failed to boot CPU%d (%d)\n", cpu, err);return err;
}#ifdef CONFIG_HOTPLUG_CPU
static bool cpu_psci_cpu_can_disable(unsigned int cpu)
{return !psci_tos_resident_on(cpu);
}static int cpu_psci_cpu_disable(unsigned int cpu)
{/* Fail early if we don't have CPU_OFF support */if (!psci_ops.cpu_off)return -EOPNOTSUPP;/* Trusted OS will deny CPU_OFF */if (psci_tos_resident_on(cpu))return -EPERM;return 0;
}static void cpu_psci_cpu_die(unsigned int cpu)
{/** There are no known implementations of PSCI actually using the* power state field, pass a sensible default for now.*/u32 state = PSCI_POWER_STATE_TYPE_POWER_DOWN <<PSCI_0_2_POWER_STATE_TYPE_SHIFT;psci_ops.cpu_off(state);
}static int cpu_psci_cpu_kill(unsigned int cpu)
{int err;unsigned long start, end;if (!psci_ops.affinity_info)return 0;/** cpu_kill could race with cpu_die and we can* potentially end up declaring this cpu undead* while it is dying. So, try again a few times.*/start = jiffies;end = start + msecs_to_jiffies(100);do {err = psci_ops.affinity_info(cpu_logical_map(cpu), 0);if (err == PSCI_0_2_AFFINITY_LEVEL_OFF) {pr_info("CPU%d killed (polled %d ms)\n", cpu,jiffies_to_msecs(jiffies - start));return 0;}usleep_range(100, 1000);} while (time_before(jiffies, end));pr_warn("CPU%d may not have shut down cleanly (AFFINITY_INFO reports %d)\n",cpu, err);return -ETIMEDOUT;
}
#endifconst struct cpu_operations cpu_psci_ops = {.name = "psci",.cpu_init = cpu_psci_cpu_init,.cpu_prepare = cpu_psci_cpu_prepare,.cpu_boot = cpu_psci_cpu_boot,
#ifdef CONFIG_HOTPLUG_CPU.cpu_can_disable = cpu_psci_cpu_can_disable,.cpu_disable = cpu_psci_cpu_disable,.cpu_die = cpu_psci_cpu_die,.cpu_kill = cpu_psci_cpu_kill,
#endif
};
cpu_psci_ops 里面就为 CPU0 初始化了 cpu_operations 结构体,可以看到里面的函数指针其实又会调用到前面初始化的 PSCI 操作集函数,这也就是linux系统中常用的分层操作,对上提供统一接口,而对下则对应不同的硬件平台,比如启动 CPU0 时,就会调用 cpu_psci_cpu_boot 函数,进而调用 PSCI 操作集中的 psci_cpu_on 函数。
smp_init_cpus
因为现在大部分都是SMP(symmetrical mulit-processing)操作系统,所以不可能只有一个 CPU,在介绍完CPU0 的cpu_operations 初始化之后,再介绍一下 CPUx(secondary CPU)cpu_operations 的初始化。如下:
// arch/arm64/kernel/smp.c
/** Enumerate the possible CPU set from the device tree or ACPI and build the* cpu logical map array containing MPIDR values related to logical* cpus. Assumes that cpu_logical_map(0) has already been initialized.*/
void __init smp_init_cpus(void)
{int i;if (acpi_disabled)of_parse_and_init_cpus();elseacpi_parse_and_init_cpus();if (cpu_count > nr_cpu_ids)pr_warn("Number of cores (%d) exceeds configured maximum of %u - clipping\n",cpu_count, nr_cpu_ids);if (!bootcpu_valid) {pr_err("missing boot CPU MPIDR, not enabling secondaries\n");return;}/** We need to set the cpu_logical_map entries before enabling* the cpus so that cpu processor description entries (DT cpu nodes* and ACPI MADT entries) can be retrieved by matching the cpu hwid* with entries in cpu_logical_map while initializing the cpus.* If the cpu set-up fails, invalidate the cpu_logical_map entry.*/for (i = 1; i < nr_cpu_ids; i++) {if (cpu_logical_map(i) != INVALID_HWID) {if (smp_cpu_setup(i))set_cpu_logical_map(i, INVALID_HWID);}}
}
smp_init_cpus
函数是 Linux 内核中用于初始化多核处理器的核心函数,主要职责是根据硬件描述(设备树或 ACPI)遍历 CPU 标识所有可用的 CPU ,并建立映射。
这里只对 secondary CPU 的 cpu_operations 结构体进行分析也就是 smp_cpu_setup 函数,其它细节这里将不再赘述,如下:
// arch/arm64/kernel/smp.c
/** Initialize cpu operations for a logical cpu and* set it in the possible mask on success*/
static int __init smp_cpu_setup(int cpu)
{if (cpu_read_ops(cpu))return -ENODEV;if (cpu_ops[cpu]->cpu_init(cpu))return -ENODEV;set_cpu_possible(cpu, true);return 0;
}
和 CPU0 一样,这里也是调用 cpu_read_ops 函数进行初始化 cpu_operations 的,只是传入的 CPU id 不一样,所以将不再赘述。
在初始化完 cpu_operations 之后就调用了 cpu_ops[cpu]->cpu_init(cpu) 也就是 cpu_psci_cpu_init 函数,但在linux-5.4.239版本中这个函数好像没有做什么操作,如下:
// arch/arm64/kernel/psci.c
static int __init cpu_psci_cpu_init(unsigned int cpu)
{return 0;
}
smp_cpu_setup 函数最后调用 set_cpu_possible 如下:
// include/linux/cpumask.hstatic inline void
set_cpu_possible(unsigned int cpu, bool possible)
{if (possible)cpumask_set_cpu(cpu, &__cpu_possible_mask);elsecpumask_clear_cpu(cpu, &__cpu_possible_mask);
}
该函数会设置指定 CPU 的掩码位,以告知内核此 CPU 可能已经存在,反之清楚掩码位,让内核忽略此 CPU,不再为其分配任务。
还有一些类似的函数如下:
// 标记 CPU 是否物理存在
static inline void
set_cpu_present(unsigned int cpu, bool present)
{if (present)cpumask_set_cpu(cpu, &__cpu_present_mask);elsecpumask_clear_cpu(cpu, &__cpu_present_mask);
}// 标记 CPU 已在线(已启动并加入调度)需平台相关代码实现
void set_cpu_online(unsigned int cpu, bool online);// 标记 CPU 是否参与负载均衡(允许任务迁移)
static inline void
set_cpu_active(unsigned int cpu, bool active)
{if (active)cpumask_set_cpu(cpu, &__cpu_active_mask);elsecpumask_clear_cpu(cpu, &__cpu_active_mask);
}
这些函数都是用来标记 CPU 的不同状态的,以确保 Linux 内核能够高效的为电源管理、热插拔等功能提供服务。
三、CPU PSCI多核启动流程
分析到这里 PSCI 的初始化流程和CPU 的 cpu_operations 结构体初始化就完成了,那初始化的这些回调函数在什么时候被调用呢?下面将分析使用 PSCI 的 CPU 启动流程(只包含kernel部分)。
1、boot cpu 启动流程
在 secondary CPU 没有被启动之前所有的操作默认都是由 boot CPU 执行的,所以这里对 boot CPU 的启动只做简单分析。
start_kernel
........
boot_cpu_init
........
arch_call_rest_init
rest_init
boot_cpu_init:
// kernel/cpu.c/** Activate the first processor.*/
void __init boot_cpu_init(void)
{int cpu = smp_processor_id();/* Mark the boot cpu "present", "online" etc for SMP and UP case */set_cpu_online(cpu, true);set_cpu_active(cpu, true);set_cpu_present(cpu, true);set_cpu_possible(cpu, true);#ifdef CONFIG_SMP__boot_cpu_id = cpu;
#endif
}
boot_cpu_init
是 Linux 内核启动引导 CPU(第一个启动的 CPU)的核心函数,确保其状态掩码被正确标记以支持后续的多核调度。
函数首先会获取执行当前代码的 CPU 逻辑 ID,因为其它 CPU 还没有启动,所以这里一般
为主 CPU 的 id 并且恒为 0
。
设置 CPU 状态掩码:
set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);
set_cpu_present
:标记 CPU 物理真实存在
set_cpu_possible
:声明逻辑支持(受CONFIG_NR_CPUS
限制)
set_cpu_online
:启用调度(允许任务分配)
set_cpu_active
:参与负载均衡(允许任务迁移)
必须按 present → possible → online → active
顺序设置 CPU 状态掩码。
最后,如果开启了 CONFIG_SMP 配置,即 SMP 系统,则将 boot CPU id 保存到 __boot_cpu_id 以方便系统需要获取 boot CPU id。
rest_init 函数如下:
// init/main.c
noinline void __ref rest_init(void)
{struct task_struct *tsk;int pid;rcu_scheduler_starting();/** We need to spawn init first so that it obtains pid 1, however* the init task will end up wanting to create kthreads, which, if* we schedule it before we create kthreadd, will OOPS.*/pid = kernel_thread(kernel_init, NULL, CLONE_FS);/** Pin init on the boot CPU. Task migration is not properly working* until sched_init_smp() has been run. It will set the allowed* CPUs for init to the non isolated CPUs.*/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();/** Enable might_sleep() and smp_processor_id() checks.* They cannot be enabled earlier because with CONFIG_PREEMPTION=y* kernel_thread() would trigger might_sleep() splats. With* CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled* already, but it's stuck on the kthreadd_done completion.*/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);
}
rest_init
是 Linux 内核启动流程中的核心函数,负责初始化关键系统进程和多核调度环境,下面将对该函数进行分析,如下:
创建 Init 进程:
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
kernel_init
是内核线程入口函数,最终执行用户空间的 /sbin/init
,并强制分配 PID 1 给 init
进程。
绑定 init 进程到启动 CPU:
tsk = find_task_by_pid_ns(pid, &init_pid_ns);
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
在 SMP 调度初始化(sched_init_smp
)未完成时,如果出现任务迁移可能导致系统崩溃,init
进程固定在启动 CPU(CPU 0)运行,直到调度器准备就绪。
创建 kthreadd 守护进程:
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
创建内核线程管理器(PID 2),即所有内核线程的父进程,这里需要注意的是 kthreadd
必须准备就绪后,其他内核线程才能安全创建,即这里的 complete(&kthreadd_done),如下:
同步与调度激活:
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
system_state 用来标记系统进入可调度状态,complete(&kthreadd_done)则是通知 kernel_init
进程 kthreadd
已经就绪,可以继续往下运行。
然后再强制执行一次调度激活任务队列,并将 CPU 设置成 CPUHP_ONLINE 状态,等待被中断或任务唤醒。
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);
这里有一个同步的问题,即需要先创建 kthreadd
,再创建 init
,并通过 kthreadd_done
确保 init
在 kthreadd
准备就绪后再继续往下执行,若 init
进程在 kthreadd
准备就绪前创建线程,会因无效的 PID 2 而导致崩溃。
2、secondary CPU 启动流程
前面分析了这么长的流程好像都还没有初始化 secondary CPUs,那 secondary CPUs 是在哪里被开启的呢?直接进入到
kernel_init ,如下:
kernel_init
kernel_init_freeable
// Wait until kthreadd is all set-up.
wait_for_completion(&kthreadd_done);
smp_init();
在 kernel_init_freeable 函数中会先等待 kthreadd 准备就绪之后再往下执行,进入到
smp_init 函数,如下:
// kernel/smp.c/* Called by boot processor to activate the rest. */
void __init smp_init(void)
{int num_nodes, num_cpus;unsigned int cpu;idle_threads_init();cpuhp_threads_init();pr_info("Bringing up secondary CPUs ...\n");/* FIXME: This should be done in userspace --RR */for_each_present_cpu(cpu) {if (num_online_cpus() >= setup_max_cpus)break;if (!cpu_online(cpu))cpu_up(cpu);}num_nodes = num_online_nodes();num_cpus = num_online_cpus();pr_info("Brought up %d node%s, %d CPU%s\n",num_nodes, (num_nodes > 1 ? "s" : ""),num_cpus, (num_cpus > 1 ? "s" : ""));/* Any cleanup work */smp_cpus_done(setup_max_cpus);
}
smp_init 函数是被 boot CPU 进行调用的,即主要功能就是启动所有可用的 secondary CPU,
首先会初始化两个线程:
idle_threads_init
:为每个 CPU 创建idle
线程,用于无任务时的低功耗等待。
cpuhp_threads_init
:初始化 CPU 热插拔线程,以动态的打开/关闭 CPU。
然后就开始启动 secondary CPUs:
pr_info("Bringing up secondary CPUs ...\n");/* FIXME: This should be done in userspace --RR */for_each_present_cpu(cpu) {if (num_online_cpus() >= setup_max_cpus)break;if (!cpu_online(cpu))cpu_up(cpu);}
遍历 CPU: for_each_present_cpu
会遍历所有由 __cpu_present_mask
标记的 CPU ,即所有物理存在的 CPU
限制数量:setup_max_cpus
(内核配置参数)防止超过硬件支持的 CPU 数量
启动 CPU:如果当前CPU不在线,则调用 cpu_up(cpu)
开始 secondary CPUs 的启动流程
cpu_up 流程如下:
cpu_up // kernel/cpu.c
do_cpu_up(cpu, CPUHP_ONLINE);
_cpu_up(cpu, 0, target)
cpuhp_up_callbacks
......
这里关于 cpu_up 的后续流程可以参考:Linux电源管理——CPU Hotplug 流程
再对 nodes 和 CPU 进行统计:
num_nodes = num_online_nodes(); // 统计激活的 NUMA 节点数
num_cpus = num_online_cpus(); // 统计已启动的 CPU 核心数
最后清理资源并告知 kernel
secondary CPUs 初始化完成:
smp_cpus_done(setup_max_cpus);
到目前为止所以 boot CPU 和 secondary CPUs 都已经启动完毕,并都已经进入 idle 线程,等待执行任务。
相关文章:
Linux电源管理——PSCI初始化流程和多核启动流程
目录 一、PSCI 初始化流程 1、PSCI设备树节点 2、PSCI kernel初始化流程 get_set_conduit_method set_conduit psci_probe 二、CPU PSCI 操作初始化流程 1、CPU 设备树节点 2、 struct cpu_operations 3、kernel 流程 cpu_read_bootcpu_ops smp_init_cpus 三、CPU…...
Linux问题排查-引起服务器带宽使用率高的内鬼
Linux网络流量监控与瓶颈分析全攻略:从基础命令到进程级方案 一、网络带宽查询与实时流量监控 1. 查询主机网络带宽 网卡理论带宽 通过ethtool命令查看网卡最大支持速率,例如: ethtool eth0 # 替换为实际网卡名(如ens33&#x…...
《微服务架构设计模式》笔记
思维导图 1-3章 4-6 章 5-13 章 资料 配套代码仓库:https://github.com/microservices-patterns/ftgo-application 作者网站:https://microservices.io/...
基于JDBC的信息管理系统,那么什么是JDBC呢?
JDBC 即 Java Database Connectivity,是 Java 语言中用于与数据库进行交互的一套 API。它提供了一种标准的方式,让 Java 程序能够连接到各种不同类型的数据库,并执行 SQL 语句来实现对数据库的查询、插入、更新和删除等操作。 主要功能 建立…...
百度地图的地铁图API所有城市的城市名和citycode的对照关系列表
百度地图的地铁图API所有城市的城市名和citycode的对照关系列表 城市keywordcitycode北京beijing131上海shanghai289广州guangzhou257深圳shenzhen340重庆chongqing132天津tianjin332石家庄shijiazhuang150南京nanjing315成都chengdu75沈阳shenyang58杭州hangzhou179武汉wuhan2…...
信息学奥赛一本通 1853:【08NOIP提高组】传纸条 | 洛谷 P1006 [NOIP 2008 提高组] 传纸条
【题目链接】 ybt 1853:【08NOIP提高组】传纸条 洛谷 P1006 [NOIP 2008 提高组] 传纸条 【题目考点】 1. 动态规划:坐标型动态规划 【解题思路】 抽象问题,存在m乘n的网格,每个格子中有一个数值,即同学愿意帮忙的…...
APM32小系统键盘PCB原理图设计详解
APM32小系统键盘PCB原理图设计详解 一、APM32小系统简介 APM32微控制器是国内半导体厂商推出的一款高性能ARM Cortex-M3内核微控制器,与STM32高度兼容,非常适合DIY爱好者用于自制键盘、开发板等电子项目。本文将详细讲解如何基于APM32 CBT6芯片设计一款…...
【Linux我做主】探秘进程与fork
进程和fork 父子进程和forkgithub地址前言1. 进程的标识符PID1.1 查看系统内所有的进程1.2 kill杀掉进程1.3 获取进程的PID1.4 bash与父子进程 2. 创建进程与fork2.1 fork创建子进程2.2 fork困惑的解释0. fork的工作原理1. 为什么给子进程返回0,给父进程返回子进程P…...
学习python day4
1.顺序语句结构 #赋值语句 name张三 age20 a,b,c,droom#字符串分解赋值 print(a,b,c,d) #输入输出也是典型的顺序结构 nameinput(请输入您的姓名:) ageeval(input(请输入您的年龄:)) #使用eval进行转换 print(姓名:,name) print(年龄&#x…...
如何通过外链建设提升Shopify独立站的权重和排名
一、外链质量评估与筛选原则 相关性优先 选择与自身行业、产品或目标用户群体高度相关的网站(如行业论坛、垂直媒体、评测博客)交换外链,避免低相关性垃圾链接导致搜索引擎惩罚。 权威性指标 关注外链来源网站的域名权威(DA…...
高并发内存池|六、page cache的设计
六、page cache的设计 1. page cache的结构 page cache 也是一个哈希桶结构,但它的映射结构与前两个 cache 不同。它的每一个桶是容量从 1 到 128 页大小的内存块,桶中的每个内存块由 SpanList 管理。page cache 的内存由其向系统申请所得,…...
C++线程池实现
C线程池实现 知识点补充为什么要实现线程池线程池的实现过程 知识点补充 在C11中引入了对线程库的支持,接下来我们介绍一下跟线程相关的一些知识点: 线程对象的构造方式 在C11中主要提供的三种线程的构造方式:无参构造、带参构造和调用移动构…...
#Redis缓存篇#(七)分布式缓存
目录 一 单节点Redis 1 单节点的问题 二 分布式缓存 1 Redis持久化 (1 RDB持久化 (2 AOF持久化 2 Redis主从集群 (1 搭建主从架构 (2 主从数据同步原理 3 Redis哨兵 (1 哨兵的作用和原理 (2 搭…...
【VSCode】安装与 ssh 免密登录
【VSCode】安装与 ssh 免密登录 下载SSH 登录设置免密登录关闭远程连接删除ssh连接(慎用!!!删除了建立的连接就没有了!!) 下载 https://code.visualstudio.com/docs/?dvwin64user 选择安装路径…...
【Python解决八皇后问题】回溯算法与优化策略全解析
目录 🌟 前言🏗️ 技术背景与价值🩹 当前技术痛点🛠️ 解决方案概述👥 目标读者说明🧠 一、技术原理剖析📊 核心概念图解💡 核心作用讲解🔧 关键技术模块说明⚖️ 技术选型对比🛠️ 二、实战演示⚙️ 环境配置要求💻 核心代码实现基础回溯实现位运算优化…...
判断一个元素是否在可视区域
判断元素是否在可视区域的方法 方法一:offsetTop 和 scrollTop 通过计算元素的 offsetTop 和容器的 scrollTop 来判断元素是否位于视口内。这种方法适用于简单的垂直滚动场景。 优点: 实现简单,性能较好。缺点: 不支持复杂的布局结构(如嵌套滚动),无法处理水平方向上的可…...
作物遗传与种质创新利用全国重点实验室-随笔10
作物遗传与种质创新利用全国重点实验室依托于南京农业大学,2022年11月完成国家重点实验室重组工作,由原名称“作物遗传与种质创新国家重点实验室”正式更名为“作物遗传与种质创新利用全国重点实验室”。 实验室面向国家粮食安全和农业高质量发展的重大战…...
分布式电源的配电网无功优化
分布式电源(Distributed Generation, DG)的大规模接入配电网,改变了传统单向潮流模式,导致电压波动、功率因数降低、网损增加等问题,无功优化成为保障配电网安全、经济、高效运行的关键技术。 1. 核心目标 电压稳定性:抑制DG并网点(PCC)及敏感节点的电压越限(如超过5%…...
游戏引擎学习第301天:使用精灵边界进行排序
回顾并为今天的内容做准备 昨天,我们解决了一些关于排序的问题,这对我们清理长期存在的Z轴排序问题很有帮助。这个问题我们一直想在开始常规游戏代码之前解决。虽然不确定是否完全解决了问题,但我们提出了一个看起来合理的排序标准。 有两点…...
网络框架二次封装:基于Kotlin的高扩展性网络请求框架完整实现
完整目录结构 1. 架构设计1.1 分层架构1.2 核心组件1.3 接口关系图2. 基础配置实现2.1 NetworkConfig完整代码2.2 CacheConfig完整代码3. 核心网络客户端3.1 SmartHttpClient完整实现3.2 单例管理3.3 服务创建与执行4. DSL请求构建器4.1 NetworkRequest完整实现4.2 生命周期绑…...
高噪声下扩展边缘检测算子对检测边缘的影响
目录 一、常见的边缘检测算子 二、扩展边缘检测算子对检测边缘的影响 三、结论 一、常见的边缘检测算子 Sobel 算子: Prewitt算子;...
Linux 内核音视频架构(V4L2 )介绍
一.概述 Linux 内核中的 V4L2(Video for Linux Two)框架 是用于管理音视频设备(如摄像头、电视调谐器、视频采集卡等)的核心子系统。 它提供了一套统一的接口,使得用户空间应用程序能够方便地访问和控制硬件设备&…...
专业 YouTube SEO 方案:打造高排名视频的关键步骤
YouTube 是全球订阅量最高的社交媒体平台之一。YouTube 为发布创意视频内容和针对特定受众开展营销活动提供了无限可能,是任何品牌内容营销策略的重要组成部分。 但是,为了发展您的 YouTube 频道并消除噪音,优化您的视频内容以便可以在搜索结…...
基于STM32的智能台灯_自动亮度_久坐提醒仿真设计(Proteus仿真+程序设计+设计报告+讲解视频)
这里写目录标题 1.主要功能2.仿真设计3.程序设计4.设计报告5.下载链接 基于STM32的智能台灯_自动亮度_久坐提醒仿真设计 (Proteus仿真程序设计设计报告讲解视频) 仿真图Proteus 8.9 程序编译器:keil 5 编程语言:C语言 设计编号࿱…...
labview硬件部分——压力测量
0kg的电压需要我们手动输入!在不放东西的时候的电压,先运行一次程序,将其记录后写到程序中的0kg输入按键即可。 整体的程序:...
Mysql索引实战1
对于上面这两种 name>‘a’ 和 name>‘zzz’ 的执行结果,mysql最终是否选择走索引或者一张表涉及多个索引,mysql最终如何选择索引,我们可以用trace工具来一查究竟,开启trace工具会影响mysql性能,所以只能临时分析…...
在实际网络部署中,静态路由的优先级通常高于RIP
是的,在实际网络部署中,静态路由的优先级通常高于RIP,尤其是在中小型网络或对可控性要求高的场景中。以下是关键原因和典型应用场景分析: 1. 为何静态路由比RIP更受青睐? (1) 简单性与可靠性 静态路由: 手…...
Linux系统编程-DAY02
一、标准io 1.写文件 fgets函数中判断有多少行,且判断最后一个是不是终止符 if( buf[strlen(buf) - 1] \n ) 2. wc命令行:字符统计 wc -l 文件名 行数 文件名 3. write 用于操作二进制的文件(文办文件和图片文件也可以…...
【C++ 真题】P5736 【深基7.例2】质数筛
P5736 【深基7.例2】质数筛 题目描述 输入 n n n 个不大于 10 5 10^5 105 的正整数。要求全部储存在数组中,去除掉不是质数的数字,依次输出剩余的质数。 输入格式 第一行输入一个正整数 n n n,表示整数个数。 第二行输入 n n n 个正…...
自制操作系统day6(GDTR、段描述符、PIC、实模式和保护模式、16位到32位切换、中断处理程序、idt的设定、EFLAG寄存器)(ai辅助整理)
day6 分割源文件(harib03a) 优点 按照处理内容进行分类,如果分得好的话,将来进行修改时,容易找到地方。如果Makefile写得好,只需要编译修改过的文件,就可以提高make的速度。单个源文件都不长。…...
大模型评测与可解释性
随着大模型在各个领域展现出惊人的能力,我们对其性能的评估和对其决策过程的理解变得尤为重要。一个模型即使在基准测试中表现出色,也可能在实际应用中遇到意想不到的问题。同时,由于大模型的复杂性,它们常常被视为“黑箱”,这给其在关键领域的应用带来了挑战。 本章将深…...
【TTS回顾】StyleTTS 深度剖析:TTS+风格迁移
写在前面 这篇博客我们回顾一下StyleTTS,当时的背景是,文本转语音(TTS)技术,早已不再满足于仅仅将文字转化为可听的语音。行业需要的是“真人TTS”,AI 不仅能“说得清楚”,更能“说得生动”、“说得有感情”,甚至能模仿特定人物的说话风格。富有表现力的语音合成,即能…...
GStreamer (四)交叉编译
交叉编译 下载链接库交叉编译1、下载Gstreamer (方式二 ),进入到编译目录2、在gst-build目录下创建交叉编译配置文件cross_file.txt3、修改meson_options.txt中libmount选项为false,否则编译前需要先编译libmount。4、在gst-build…...
电路设计基础
只有当电容两端的电压等于0伏的时候,就是这一点的电压和这一点电压之间没有压差的时候,我门才可以把电容当成是一根导线,如果当我电容比如说它己经充到有一个1伏的电压了,这个时候我们是不可以把电容当成是导线的,所以…...
C语言——函数递归与迭代
(1)递归的例子: 顺序打印一个整数,打印整数的每一位。 例如: input:1234 output:1 2 3 4 input:520 output:5 2 0 我们可能会想到用这种方法:(但是运行之后,我们发现结果是事…...
详解 C# 中基于发布-订阅模式的 Messenger 消息传递机制:Messenger.Default.Send/Register
🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C、C#等开发语言,熟悉Java常用开…...
8 种快速易用的Python Matplotlib数据可视化方法
你是否曾经面对一堆复杂的数据,却不知道如何让它们变得直观易懂?别慌,Python 的 Matplotlib 库是你数据可视化的最佳伙伴!它简单易用、功能强大,能将枯燥的数字变成引人入胜的图表。无论是学生、数据分析师还是程序员&…...
嵌入式开发学习日志(linux系统编程--文件读写函数(2))Day25
一、linux操作命令 【wc】:指定字符统计; 【file 文件名】:可以查看文件的类型; 二、写入函数【fwrite】————可写入二进制文件 形式: size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE…...
离线服务器Python环境配置指南
离线服务器Python环境配置指南:避坑与实战 0. 场景分析:当服务器与世隔绝时 典型困境: 无法访问国际网络(如PyPI、Conda官方源)服务器处于内网隔离环境安全策略限制在线安装 解决方案矩阵: 方法适用场…...
Java线程池调优与实践经验
在Java面试中,线程池调优是一个常见且重要的考察点,尤其是当涉及Spring生态时,ThreadPoolTaskExecutor的使用经验通常会被深入追问。以下是针对该问题的结构化回答,结合原理、实践和调优经验: 1. 线程池调优的核心参数…...
Python 包管理工具核心指令uvx解析
uvx 是 Python 包管理工具 uv 的重要组成部分,主要用于在隔离环境中快速运行 Python 命令行工具或脚本,无需永久安装工具包。以下是其核心功能和使用场景的详细解析: 一、uvx 的定位与核心功能 工具执行器的角色 uvx 是 uv tool run 的别名&a…...
力扣-三数之和
1.题目描述 2.题目链接 LCR 007. 三数之和 - 力扣(LeetCode) 3.题目代码 import java.util.*; class Solution {public List<List<Integer>> threeSum(int[] nums) {Arrays.sort(nums);int tempnums.length-1;Set<List<Integer>…...
【AI模型学习】ESM2
文章目录 1. 版本2. 开始2.1 安装2.2 使用预训练模型2.2.1 代码2.2.2 讲解 2.2 结构预测 3. 任务类型总结1. 蛋白质结构预测(ESMfold)2. 特征嵌入提取(esm-extract)3. 零镜头变体预测(ESM-1v/ESM-2)4. 逆向…...
c++11特性——可变参数模板及emplace系列接口
文章目录 可变参数模板基本语法和使用sizeof...运算符 从语法角度理解可变参数模板包扩展通过编译时递归解析参数包直接对解析行为展开 emplace系列接口举例讲解emplace_back的实现 可变参数模板 可变参数模板是c11新特性中极其重要的一节。前文我们提到过,c11中对…...
深入理解 Pre-LayerNorm :让 Transformer 训练更稳
摘要 在超深 Transformer 与大语言模型(LLM)时代,归一化策略直接决定了模型能否稳定收敛、推理性能能否最大化。把归一化层从 “残差之后” 挪到 “子层之前”(Pre-LayerNorm,Pre-LN),再将传统…...
vue3:十三、分类管理-表格--分页功能
一、实现效果 实现分页功能,并且可对分页功能和搜索框功能能动态显示 1、显示分页 2、分页功能和搜索栏隐藏 二、基础搭建 1、官网参考 Pagination 分页 | Element Plus 使用分页的附加功能 2、表格中底部写入分页 (1)样式class 在全局js中写入顶部外边距样式margin-t…...
工商总局可视化模版-Echarts的纯HTML源码
概述 基于ECharts的工商总局数据可视化HTML模版,帮助开发者快速搭建专业级工商广告数据展示平台。这款模版设计规范,功能完善,适合各类工商监管场景使用。 主要内容 本套模版采用现代化设计风格,主要包含以下核心功能模块&…...
8.2 线性变换的矩阵
一、线性变换的矩阵 本节将对每个线性变换 T T T 都指定一个矩阵 A A A. 对于一般的列向量,输入 v \boldsymbol v v 在空间 V R n \pmb{\textrm V}\pmb{\textrm R}^n VRn 中,输出 T ( v ) T(\boldsymbol v) T(v) 在空间 W R m \textrm{\pmb W}\…...
工业路由器WiFi6+5G的作用与使用指南,和普通路由器对比
工业路由器的技术优势 在现代工业环境中,网络连接的可靠性与效率直接影响生产效率和数据处理能力。WiFi 6(即802.11ax)和5G技术的结合,为工业路由器注入了强大的性能,使其成为智能制造、物联网和边缘计算的理想选择。…...
Nginx核心服务
一.正向代理 正向代理(Forward Proxy)是一种位于客户端和原始服务器之间的代理服务器,其主要作用是将客户端的请求转发给目标服务器,并将响应返回给客户端 Nginx 的 正向代理 充当客户端的“中间人”,代…...