Linux内核启动-从入口到start_kernel
1. 内核启动要求
arch/arm64/boot/head.S开始的注释简单说明了内核启动的条件。更详细的内容可以查看内核文档Documentation/arm64/booting.rst或Documentation/translations/zh_CN/arm64/booting.txt。
/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * The requirements are:
 *   MMU = off, D-cache = off, I-cache = on or off,
 *   x0 = physical address to the FDT blob.
 *
 * This code is mostly position independent so you call this at
 * __pa(PAGE_OFFSET).
 *
 * Note that the callee-saved registers are used for storing variables
 * that are useful before the MMU is enabled. The allocations are described
 * in the entry routines.
 */
2. 内核启动入口
根据Makefile对KBUILD_LDS的定义,链接vmlinux使用的连接脚本为arch/$(SRCARCH)/kernel/vmlinux.lds。从链接脚本arch/arm64/kernel/vmlinux.lds可以查到,程序的入口为_text,镜像起始位置存放的是.head.text段生成的指令。搜索.head.text,可以找到include/linux/init.h对__HEAD定义.section    ".head.text","ax"。
OUTPUT_ARCH(aarch64)
ENTRY(_text)
SECTIONS
{
 . = ((((((-(((1)) << ((((48))) - 1)))) + (0x08000000))) + (0x08000000)));
 .head.text : {
  _text = .;
  KEEP(*(.head.text))
 }
 ...
}
/* For assembly routines */
#define __HEAD      .section    ".head.text","ax"
#define __INIT      .section    ".init.text","ax"
#define __FINIT     .previous
再看一下arch/arm64/kernel/vmlinux.lds是怎么生成的,编译日志中,会有LDS     arch/arm64/kernel/vmlinux.lds,scripts/Makefile.build中可以看到是对arch/arm64/kernel/vmlinux.lds.S进行预处理得到了最终的链接脚本。
# Linker scripts preprocessor (.lds.S -> .lds)
# ---------------------------------------------------------------------------
quiet_cmd_cpp_lds_S = LDS     $@
      cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -U$(ARCH) \
                             -D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<
$(obj)/%.lds: $(src)/%.lds.S FORCE
        $(call if_changed_dep,cpp_lds_S)
再搜索__HEAD,可以看到程序起始代码位于arch/arm64/kernel/head.S。
3. 概览:从入口到start_kernel
从入口到start_kernel的主要是汇编代码,后续的很多子系统都会依赖这部分代码做的初始化。
+-- _text()                                 // 内核启动入口
    \-- primary_entry()
        +-- preserve_boot_args()            // 保存x0~x3到boot_args[0~3]
        +-- init_kernel_el()                // 根据内核运行异常等级进行配置,返回启动模式
        |   +-- init_el1()                  // 通常情况下从EL1启动内核
        |   \-- init_el2()                  // 从EL2启动内核,用于开启VHE(Virtualization Host Extensions)
        +-- set_cpu_boot_mode_flag()        // 保存bootmode到__boot_cpu_mode[2]全局数组
        +-- __create_page_tables()          // 建立恒等映射idmap_pg_dir和内核镜像映射init_pg_dir的页表
        +-- __cpu_setup()                   // 为开启MMU做的CPU初始化
        \-- __primary_switch()
            +-- __enable_mmu()              // 开启MMU
            \-- __primary_switched()        // 初始化init_task栈,设置VBAR_EL1,保存FDT地址,计算kimage_voffset,清空bss段
                +-- early_fdt_map()
                |   +-- early_fixmap_init() // 尝试建立fixmap的页表,可能失败
                |   \-- fixmap_remap_fdt()  // 如果成功建立fixmap页表,将fdt映射到fixmap的FIX_FDT区域
                +-- init_feature_override() // 根据BootLoader传入的参数,对一些参数的改写
                +-- switch_to_vhe()         // 需要的话,开启VHE
                +-- start_kernel()          // 跳转到start_kernel执行

4. MMU开启之前:primary_entry
在内核启动入口直接跳转到primary_entry,这是MMU开启之前所有函数的总流程。
/*
 * The following callee saved general purpose registers are used on the
 * primary lowlevel boot path:
 *
 *  Register   Scope                      Purpose
 *  x21        primary_entry() .. start_kernel()        FDT pointer passed at boot in x0
 *  x23        primary_entry() .. start_kernel()        physical misalignment/KASLR offset
 *  x28        __create_page_tables()                   callee preserved temp register
 *  x19/x20    __primary_switch()                       callee preserved temp registers
 *  x24        __primary_switch() .. relocate_kernel()  current RELR displacement
 */
SYM_CODE_START(primary_entry)
    bl  preserve_boot_args
    bl  init_kernel_el                  // w0=cpu_boot_mode
    adrp    x23, __PHYS_OFFSET          // 读取内核镜像入口`_text`的物理地址
    and x23, x23, MIN_KIMG_ALIGN - 1    // KASLR offset, defaults to 0
    bl  set_cpu_boot_mode_flag          // 保存bootmode到__boot_cpu_mode[2]
    bl  __create_page_tables            // 建立恒等映射idmap_pg_dir和内核镜像映射init_pg_dir的页表
    /*
     * The following calls CPU setup code, see arch/arm64/mm/proc.S for
     * details.
     * On return, the CPU will be ready for the MMU to be turned on and
     * the TCR will have been set.
     */
    bl  __cpu_setup                     // 为开启MMU做的CPU初始化
    b   __primary_switch                // 主要工作就是开启MMU,之后跳转到__primary_switched
SYM_CODE_END(primary_entry)
4.1. preserve_boot_args
主要工作是将FDT的基地址保存到x21寄存器,将启动参数(x0~x3)保存到boot_args数组,并使用。
/*
 * The recorded values of x0 .. x3 upon kernel entry.
 */
u64 __cacheline_aligned boot_args[4];
/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
SYM_CODE_START_LOCAL(preserve_boot_args)
    mov    x21, x0                  // x21=FDT,x0是uboot传入的第一个参数,记录fdt的基地址,将x0的值保存到x21寄存器备份
    adr_l    x0, boot_args          // 读取boot_args变量的当前地址到x0,此时MMU处于关闭状态,访问的是物理地址
    stp    x21, x1, [x0]            // record the contents of x0 .. x3 at kernel entry
    stp    x2, x3, [x0, #16]        // 将x0~x3保存到boot_args[0~3]
    dmb    sy                       // needed before dc ivac with MMU off
                                    // 保证stp指令完成
    add    x1, x0, #0x20            // 4 x 8 bytes,boot_args数组的大小
    b    dcache_inval_poc           // 使boot_args[]数组对应的高速缓存失效
SYM_CODE_END(preserve_boot_args)
dmb sy在全系统高速缓冲范围内做一次内存屏障,保证前面的stp指令运行顺序正确,保证stp在调用dcache_inval_poc前完成。
dcache_inval_poc传入参数为boot_args数组的起始和结束地址,函数的作用是使boot_args数组对应的高速缓存失效,并清除这些缓存。
/*
 * dcache_inval_poc(start, end)
 *
 * Ensure that any D-cache lines for the interval [start, end)
 * are invalidated. Any partial lines at the ends of the interval are
 * also cleaned to PoC to prevent data loss.
 *
 * - start   - kernel start address of region
 * - end     - kernel end address of region
 */
4.2. init_kernel_el
判断启动的模式是EL2还是非安全模式的EL1,并进行相关级别的系统配置(ARMv8中EL2是hypervisor模式,EL1是标准的内核模式),然后使用w0返回启动模式(BOOT_CPU_MODE_EL1或BOOT_CPU_MODE_EL2)。
#define BOOT_CPU_MODE_EL1	(0xe11)
#define BOOT_CPU_MODE_EL2	(0xe12)
/* Current Exception Level values, as contained in CurrentEL */
#define CurrentEL_EL1		(1 << 2)
#define CurrentEL_EL2		(2 << 2)
/*
 * Starting from EL2 or EL1, configure the CPU to execute at the highest
 * reachable EL supported by the kernel in a chosen default state. If dropping
 * from EL2 to EL1, configure EL2 before configuring EL1.
 *
 * Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
 * SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in w0 if
 * booted in EL1 or EL2 respectively.
 */
SYM_FUNC_START(init_kernel_el)
    mrs	x0, CurrentEL           // 读取当前EL等级
    cmp	x0, #CurrentEL_EL2
    b.eq    init_el2            // 如果是EL2则跳转到init_el2,否则继续向下执行init_el1的代码
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
    ...
    eret
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
    ...
    ret
SYM_FUNC_END(init_kernel_el)
4.2.1. init_el1
配置CPU的大小端模式,将启动模式BOOT_CPU_MODE_EL1写入w0,然后返回到primary_entry。
#define INIT_SCTLR_EL1_MMU_OFF \
    (ENDIAN_SET_EL1 | SCTLR_EL1_RES1)
SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
    mov_q   x0, INIT_SCTLR_EL1_MMU_OFF  // MMU关闭时,对sctlr_el1的赋值
    msr sctlr_el1, x0                   // 配置CPU的大小端模式,EE域用来配置EL1,E0E域用来配置EL0
    isb                                 // 配置CPU大小端模式后,确保前面的指令都运行完成。
    mov_q   x0, INIT_PSTATE_EL1
    msr     spsr_el1, x0                // 将INIT_PSTATE_EL1写入spsr_el1
    msr     elr_el1, lr                 // 将返回地址写入elr_el1,lr是primary_entry中`bl init_kernel_el`的下一条指令地址。
    mov     w0, #BOOT_CPU_MODE_EL1      // 记录启动模式
    eret                                // 通过eret来使用ELR_ELx和SPSR_ELx来恢复PC和PSTATE
4.3. set_cpu_boot_mode_flag
将启动模式保存到__boot_cpu_mode[2]全局数组。
/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in w0. See arch/arm64/include/asm/virt.h for more info.
 */
SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
    adr_l   x1, __boot_cpu_mode // x1记录__boot_cpu_mode[]的地址
    cmp w0, #BOOT_CPU_MODE_EL2  // w0记录启动时的异常等级
    b.ne    1f                  // 如果不是从EL2启动,则跳转到1处
    add x1, x1, #4              // 如果是从EL2启动,地址指向__boot_cpu_mode[1]
1:  str w0, [x1]                // Save CPU boot mode,保存启动模式到x1指向的地址,如果是从EL1启动,地址指向__boot_cpu_mode[0]
    dmb sy                      // 保证str指令执行完成
    dc  ivac, x1                // Invalidate potentially stale cache line,使高速缓存失效
    ret
SYM_FUNC_END(set_cpu_boot_mode_flag)
    if (w0 == BOOT_CPU_MODE_EL2) {
        __boot_cpu_mode[1] = BOOT_CPU_MODE_EL2;
    } else {
        __boot_cpu_mode[0] = BOOT_CPU_MODE_EL1;
    }
4.4. __create_page_tables
创建内存映射,一共两张,一张存放在idmap_pg_dir(恒等映射,物理地址和虚拟地址相同),一张存放在init_pg_dir(线性映射)。
4.5. __cpu_setup
为开启MMU而初始化处理器相关的代码,配置MMU,配置访问权限,内存地址划分等。
函数返回是x0记录了SCTLR_EL1要写入的值,最后传给__enable_mmu。
5. 开启MMU:__primary_switch
__primary_switch表示重要的切换,这个非常重要的切换就是开启MMU。开启MMU(__enable_mmu)之前,CPU使用物理地址访问内存,自__primary_switched开始,CPU会以虚拟地址来访问内存。
SYM_FUNC_START_LOCAL(__primary_switch)
#ifdef CONFIG_RANDOMIZE_BASE
    ...
#endif
    /*
     *  x0  = SCTLR_EL1 value for turning on the MMU.
     *  x1  = TTBR1_EL1 value
     */
    adrp    x1, init_pg_dir
    bl  __enable_mmu
#ifdef CONFIG_RELOCATABLE
    ...
#endif
    ldr x8, =__primary_switched     // __primary_switched的链接地址
    adrp    x0, __PHYS_OFFSET       // 内核镜像起始的物理地址
    br  x8                          // 跳转到__primary_switched虚拟地址运行
SYM_FUNC_END(__primary_switch)
5.1. __enable_mmu
主要工作:
- 检查CPU是否支持软件设置的页面大小,如果不支持,CPU会在停止在这里。
- 将idmap_pg_dir和init_pg_dir分别加载到TTBR0_EL1和TTBR1_EL1。
- 开启MMU,并使本地icache失效。
6. 开启MMU之后:__primary_switched
开启MMU之后,CPU访问的是虚拟地址。
- 准备0号进程内核栈
- 中断向量表配置
- 计算kimage_voffset
- 清空BSS段
- 尝试fixmap映射
- 在__primary_switch的最后,跳转到虚拟地址之前,使用adrp x0, __PHYS_OFFSET记录了_text的地址,也就是内核镜像起始的物理地址。
#define __PHYS_OFFSET   KERNEL_START
#define KERNEL_START    _text
/*
 * The following fragment of code is executed with the MMU enabled.
 *
 *   x0 = __PHYS_OFFSET
 */
SYM_FUNC_START_LOCAL(__primary_switched)
6.1. 初始化init_task栈空间
主要工作:
- 设置SP_EL0、SP_ELx、x29(FP)寄存器,配置init_task的栈
- 将per_cpu_offset写入TPIDR_ELx
    adr_l   x4, init_task
    init_cpu_task x4, x5, x6
6.1.1. init_cpu_task
先看一下涉及到的几个宏。
#define TSK_STACK       24      /* offsetof(struct task_struct, stack) */
#define S_STACKFRAME    304     /* offsetof(struct pt_regs, stackframe) */
#define PT_REGS_SIZE    336     /* sizeof(struct pt_regs) */
#define TSK_CPU         64      /* offsetof(struct task_struct, cpu) */
在task_pt_regs(current)->stackframe创建一个最终帧记录,这样unwinder就可以根据任务堆栈中的位置来识别任何任务的最终帧记录。保留整个pt_regs空间使用户任务和kthread保持一致性。
    /*
     * Initialize CPU registers with task-specific and cpu-specific context.
     *
     * Create a final frame record at task_pt_regs(current)->stackframe, so
     * that the unwinder can identify the final frame record of any task by
     * its location in the task stack. We reserve the entire pt_regs space
     * for consistency with user tasks and kthreads.
     */
    .macro  init_cpu_task tsk, tmp1, tmp2
    msr sp_el0, \tsk                    // 将init_task的地址写入sp_el0,内核空间中会使用sp_el0来作为current
    ldr \tmp1, [\tsk, #TSK_STACK]       // 获取init_task的栈地址,offsetof(struct task_struct, stack)
    add sp, \tmp1, #THREAD_SIZE         // 栈是由高地址向下生长的,所以SP_ELx要加上THREAD_SIZE
    sub sp, sp, #PT_REGS_SIZE           // 为struct pt_regs留出空间
    stp xzr, xzr, [sp, #S_STACKFRAME]   // 将struct pt_regs的u64 stackframe[2]清零
    add x29, sp, #S_STACKFRAME          // x29(FP)指向栈中pt_regs的stackframe
    scs_load \tsk                       // 用于Clang Shadow Call Stack,此处为空操作
    adr_l   \tmp1, __per_cpu_offset     // 读取__per_cpu_offset[NR_CPUS]数组基地址
    ldr w\tmp2, [\tsk, #TSK_CPU]        // offsetof(struct task_struct, cpu)
    ldr \tmp1, [\tmp1, \tmp2, lsl #3]   // tmp1 = __per_cpu_offset[init_task.cpu << 3],通常来说,bootcpu为0
    set_this_cpu_offset \tmp1           // 将当前cpu的per_cpu变量的offset值写入TPIDR_ELx
    .endm
几个寄存器的最终结果:
SP_EL0 = &init_task
SP_ELx = init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)
x29(FP) = SP_ELx + S_STACKFRAME
6.2. 中断向量表配置
将中断向量表的起始虚拟地址写入到VBAR_EL1。
    adr_l   x8, vectors             // load VBAR_EL1 with virtual
    msr vbar_el1, x8                // vector table address
    isb
6.3. 备份寄存器
此时sp的值为init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。主要工作如下:
- 将x29(FP)和x30(LR)分别保存到sp-16和sp-8的地址上,然后sp -= 16。
- 将sp的值写入到x29(FP)
这是实现了ARM64函数调用标准规定的栈布局,为后续函数调用的入栈出栈做好了准备。
    stp x29, x30, [sp, #-16]!
    mov x29, sp
6.4. 保存设备树物理地址到__fdt_pointer
    str_l   x21, __fdt_pointer, x5  // Save FDT pointer
6.5. 计算kimage_voffset
kimage_voffset记录了内核镜像映射后的虚拟地址与内核镜像在内存中的物理地址之间的差值。kimage_vaddr记录了_text的链接地址,也就是最终_text的虚拟地址,x0作为传入参数记录了_text的物理地址,相减即可得kimage_voffset。
    ldr_l   x4, kimage_vaddr        // Save the offset between
    sub x4, x4, x0                  // the kernel virtual and
    str_l   x4, kimage_voffset, x5  // physical mappings
6.6. 清空BSS段
    // Clear BSS
    adr_l   x0, __bss_start // 起始地址
    mov x1, xzr             // 要写入的值,xzr是一个特殊的寄存器,值为64位的0
    adr_l   x2, __bss_stop  // 结束地址
    sub x2, x2, x0          // size = __bss_stop - __bss_start
    bl  __pi_memset         // memset(x0, x1, x2)
    dsb ishst               // Make zero page visible to PTW
6.7. early_fdt_map
early_fdt_map主要为KASLR服务,可能会失败,如果失败,会在setup_arch重新映射。
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
    bl  kasan_early_init
#endif
    mov x0, x21                     // pass FDT address in x0
    bl  early_fdt_map               // Try mapping the FDT early
    bl  init_feature_override       // Parse cpu feature overrides
#ifdef CONFIG_RANDOMIZE_BASE
    tst x23, ~(MIN_KIMG_ALIGN - 1)  // already running randomized?
    b.ne    0f
    bl  kaslr_early_init            // parse FDT for KASLR options
    cbz x0, 0f                      // KASLR disabled? just proceed
    orr x23, x23, x0                // record KASLR offset
    ldp x29, x30, [sp], #16         // we must enable KASLR, return
    ret                             // to __primary_switch()
0:
#endif
    bl  switch_to_vhe               // Prefer VHE if possible
6.8. 跳转start_kernel
从栈中恢复x29(FP)和x30(LR),sp重新指向init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。
    ldp x29, x30, [sp], #16     // 从栈中恢复x29(FP)和x30(LR),sp += 16
    bl  start_kernel
    ASM_BUG()                   // start_kernel返回到这里说明出错了
SYM_FUNC_END(__primary_switched)
7. 参考资料
- Documentation/arm64/booting.rst
- Documentation/translations/zh_CN/arm64/booting.txt
- Linux 内核启动分析-BugMan-ChinaUnix博客
- 中断管理基础学习笔记 - 5.1 ARM64底层中断处理
- Linux kernel ARM64 寄存器tpidr_el1 的用处
- arm64: Extract early FDT mapping from kaslr_early_init()
Linux内核启动-从入口到start_kernel的更多相关文章
- linux内核启动以及文件系统的加载过程
		Linux 内核启动及文件系统加载过程 当u-boot 开始执行 bootcmd 命令,就进入 Linux 内核启动阶段.普通 Linux 内核的启动过程也可以分为两个阶段.本文以项目中使用的 lin ... 
- Linux内核启动及根文件系统载入过程
		上接博文<u-boot之u-boot-2009.11启动过程分析> Linux内核启动及文件系统载入过程 当u-boot開始运行bootcmd命令,就进入Linux内核启动阶段.与u-bo ... 
- 通过从代码层面分析Linux内核启动来探知操作系统的启动过程
		通过从代码层面分析Linux内核启动来探知操作系统的启动过程 前言说明 本篇为网易云课堂Linux内核分析课程的第三周作业,我将围绕Linux 3.18的内核中的start_kernel到init进程 ... 
- Linux内核启动代码分析二之开发板相关驱动程序加载分析
		Linux内核启动代码分析二之开发板相关驱动程序加载分析 1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c start_ke ... 
- 【内核】linux内核启动流程详细分析
		Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ... 
- 【内核】linux内核启动流程详细分析【转】
		转自:http://www.cnblogs.com/lcw/p/3337937.html Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件 ... 
- Linux内核启动过程概述
		版权声明:本文原创,转载需声明作者ID和原文链接地址. Hi!大家好,我是CrazyCatJack.今天给大家带来的是Linux内核启动过程概述.希望能够帮助大家更好的理解Linux内核的启动,并且创 ... 
- 【转载】linux内核启动android文件系统过程分析
		主要介绍linux 内核启动过程以及挂载android 根文件系统的过程,以及介绍android 源代码中文件系统部分的浅析. 主要源代码目录介绍Makefile (全局的Makefile)bioni ... 
- Linux内核启动
		Linux内核启动过程概述 Linux的启动代码真的挺大,从汇编到C,从Makefile到LDS文件,需要理解的东西很多.毕竟Linux内核是由很多人,花费了巨大的时间和精力写出来的.而且直到现在,这 ... 
- Linux内核启动流程分析(一)【转】
		转自:http://blog.chinaunix.net/uid-25909619-id-3380535.html 很久以前分析的,一直在电脑的一个角落,今天发现贴出来和大家分享下.由于是word直接 ... 
随机推荐
- 浅析容器运行时奥秘——OCI标准
			背景 2013年Docker开源了容器镜像格式和运行时以后,为我们提供了一种更为轻量.灵活的"计算.网络.存储"资源虚拟化和管理的解决方案,在业界迅速火了起来. 2014年更是容器 ... 
- Python: 取消numpy科学计数法
			Numpy中默认是使用科学计数法来显示数据的,但是这种做法往往不利于我们观测数据,比如坐标数据等.那么如何取消numpy科学计数法呢,请往下看. np.set_printoptions() impor ... 
- 【C学习笔记】day4-1 在屏幕上输出以下图案
			1.在屏幕上输出以下图案: * *** ***** ******* ********* *********** ************* *********** ********* ******* ... 
- 基于Spring AOP切面实现请求入参出参加解密
			1.Mavne导入加密解密所需的依赖 <dependency> <groupId>org.apache.commons</groupId> <artifact ... 
- python读取Excel指定单元格的值
			使用openpyxl实现 只支持xlsx文件,不支持xls import openpyxl def read_cell(io, sheet, cell='A2'): """ ... 
- QT 连接SQLIte数据库
			1.新建一个qt应用程序 2.在.pro文件中添加 sql (下图,可以查看使用方法) 添加结构查看: 3.开始连接数据库 4.运行结果查看: 5.查看数据库文件: 6.使用可视化工具创建一个 ... 
- springcloud  zuul网关整合swagger2,swagger被拦截问题
			首先感谢一位博主的分享https://www.cnblogs.com/xiaohouzai/p/8886671.html 话不多说直接上图和代码 首先我们要有一个springcloud分布式项目 我就 ... 
- 清除Linux登录记录
			CentOS cat /dev/null > /var/log/wtmp cat /dev/null > /var/log/btmp cat /dev/null > /var/log ... 
- ElasticSearch、ElasticSearch-head的安装和问题解决
			前言:elasticsearch作为一个基于Lucene的分布式搜索引擎,其搜索功能的强大之处不用多说,而elasticsearch-head作为一个node项目,能够轻松管理elasticsearc ... 
- echars中国地图
			vue中使用echars做出中国地图 这里只是个小demo,根据流程操作可以实现基础的中国地图,样式等后面根据需要自己去调 1.下载中国地图 echars官网示例中,没有中国地图的json,需要自己去 ... 
