文章目录

  • riscv Linux 目录分布
  • 内核第一个运行的地方——head.S
  • 内核运行的第一个C文件——init/main.c
    • setup_arch()
      • parse_dtb()
      • setup_initial_init_mm()
      • early_ioremap_setup()
      • jump_label_init()
      • parse_early_param()
      • efi_init()
      • paging_init()
      • misc_mem_init()
      • init_resources()
      • sbi_init()
      • kasan_init()
      • setup_smp()
      • riscv_fill_hwcap()
    • trap_init()
    • mem_init()
    • init_IRQ()
    • time_init()
    • 关于设备树匹配函数分析

riscv Linux 目录分布

通过文章 将 Linux 移植到新的处理器架构,第 1 部分:基础 可知,我们进行一个新的处理器架构的移植,需要做到以下 3 点:
1、确定这是不是一个新的架构移植。
2、了解我们要移植的硬件。
3、了解内核的基本概念。
在riscv已经被移植支持的情况下,我们现在要做的是分析,Linux内核是如何支持riscv架构的。

- configs/:支持系统的默认配置 (i.e. *_defconfig files)
- include/asm/ :Linux源码内部使用的头文件
- include/uapi/asm: 对于要导出到用户空间(例如 libc )的头文件
- kernel/:通用内核管理
- lib/:优化过的那套函数 (e.g. memcpy(), memset(), etc.)
- mm/:内存管理
  • configs文件中主要是一些配置文件,编译时可以选择默认配置进行编译,配置项较多,我们暂时不进行分析。

  • include/asm/ 目录下定义了大量头文件,用于内核编译时使用。

  • include/uapi/asm目录下定义了很多结构体以及宏定义,可以供应用层使用,可以更方便的与内核统一使用一些定义好的数据。

  • kernel/目录下有许多C文件,包含CPU获取id,信号,中断,ops,smp,time等功能。

  • lib/目录下供9个文件,其中5个为汇编实现的代码。用于底层基础函数的实现。mm/目录下进行内存的管理,包括虚拟内存分配,页错误处理,cache刷新等。

架构相关的includ目录存在于架构相关文件夹,非架构相关的存在与include/asm-gereric目录下。

内核第一个运行的地方——head.S

kernel_entry*start_kernelsetup_arch*trap_init*mm_initmem_init*init_IRQ*time_init*rest_initkernel_threadkernel_threadcpu_startup_entry

内核的整体启动流程如上所示,我们从代码中进行分析,具体内核在启动过程中做了什么。
首先我们找到head.S文件。

ENTRY(_start_kernel)/* Mask all interrupts */csrw CSR_IE, zerocsrw CSR_IP, zero

在内核启动时,一开始就关闭了所有中断。Technical Report UCB/EECS-2016-129 一文中讲了,CSR 的寄存器分布。
关闭中断后,关闭了 FPU 功能,以检测内核空间内非法使用的定位点。后面是通过一系列的宏定义进行一些环境的配置,使得一些功能能够跑起来。
这些宏定义有

ENTRY(_start_kernel)关闭所有中断
#ifdef CONFIG_RISCV_M_MODE/* 刷新icache *//* 复位所有寄存器,除了 ra, a0, a1 *//*设置一个PMP以允许访问所有内存。有些机器可能不会实现pmp,因此我们设置了一个快速陷阱处理程序来跳过接触任何陷阱上的pmp。*//*a0中的hardtid稍后才会出现,我们没有固件可以处理它。*/
#endif /* CONFIG_RISCV_M_MODE *//* 加载全局指针 *//**关闭FPU,检测内核空间中非法使用浮点数的情况*/
#ifdef CONFIG_RISCV_BOOT_SPINWAIT/* 彩票系统只需要自旋等待启动方法 */
#ifndef CONFIG_XIP_KERNEL/* 选择一个hart来运行主启动序列 */
#else/* Hart_lottery在flash中包含一个神奇的数字 *//* 如果在RAM中没有设置hart_lottery,这是第一次 */
#endif /* CONFIG_XIP */
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
#ifdef CONFIG_XIP_KERNEL
/*恢复a-的复制*/
#endif
#ifndef CONFIG_XIP_KERNEL/*为展开的无ELF的镜像清除BSS段 */
#endif/* 保存hart ID和DTB物理地址*//* 初始化页表并重新定位到虚拟地址 */
#ifdef CONFIG_BUILTIN_DTB
#else
#endif /* CONFIG_BUILTIN_DTB */
#ifdef CONFIG_MMU
#endif /* CONFIG_MMU */* Restore C environment */
#ifdef CONFIG_KASAN
#endif/* 启动内核 */
#if CONFIG_RISCV_BOOT_SPINWAIT/* 设置陷阱向量永远旋转以帮助调试 *//*
这个人没有中彩票,所以我们等待中奖的人在启动过程中走得足够远,它应该继续。*//* FIXME:我们应该WFI,以节省一些能源在这里。*/
#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
END(_start_kernel)

内核运行的第一个C文件——init/main.c

第一个运行的C语言函数为start_kernel,在该函数中进行内核的第一个线程的创建。在创建之前,会执行架构相关的函数,从而适配硬件。

kernel_entry*start_kernelsetup_arch*trap_init*mm_initmem_init*init_IRQ*time_init*rest_initkernel_threadkernel_threadcpu_startup_entry

setup_arch()

首先分析 setup_arch 这个函数,该函数属于架构相关函数,对应的文件在 arch/riscv/kernel 文件下。

parse_dtb()

这个函数首先要执行的是解析设备树,这说明 riscv 像 arm 一样,使用设备树进行设备驱动的管理,我们查看 x86 架构下的 setup_arch 则无设备树相关的配置。设备树解析函数通过 drivers/of 目录下的设备树驱动进行解析,并取出设备树中 model 名称。
设备树解析调用的函数时 parse_dtb,函数中调用了一个全局变量 dtb_early_va,这个变量是在 head.S 中进行的赋值,head.S 中调用该函数时,提前将变量放置于寄存器 a0 中,用于 C 函数的传参。
设备树地址传参代码:

#ifdef CONFIG_BUILTIN_DTBla a0, __dtb_startXIP_FIXUP_OFFSET a0
#elsemv a0, s1
#endif /* CONFIG_BUILTIN_DTB */call setup_vm

setup_initial_init_mm()

设备树解析完成后,进行了早期内存的初始化,给出了代码段的起始与结束位置,数据段的结束位置,堆地址结束位置。

[0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[0.000000] Machine model: riscv-virtio,qemu
[0.000000]start_code=0x80002000,end_code=0x806ae52c,end_data=0x812d2a00,brk=0x81322000

通过以上打印信息可知各个段的分配地址。CPU 内部的 RAM 寻址需要预留一些空间,所有 ram 起始地址就从 0x80000000 开始,地址空间分配完成之后将 boot_command_line 地址传出,供后续使用。

early_ioremap_setup()

早期 ioremap 初始化,将 I/O 的物理地址映射到虚拟地址。当 CPU 读取一段物理地址时,它可以读取到映射了 I/O 设备的物理 RAM 区域。ioremap 就是用来把设备内存映射到内核地址空间的。
该函数是一个架构不相关的函数,位于 mm/early_ioremap.c,

jump_label_init()

架构无关函数,位于 kernel 目录下,初始化 jump-label 子系统,jump-label 用于取消 if 判断分支,通过运行时修改代码,来提高执行的效率。

parse_early_param()

架构无关函数,解析早期传入的参数。

efi_init()

paging_init()

完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作,这一个函数执行完成之后,就可以通过虚拟地址来访问实际的物理地址了。

misc_mem_init()

  • 测试 ram 是否正常
  • numa 架构初始化
  • 内存模型 sparse 初始化
  • 初始化 zone,用于管理物理内存地址区域
  • 保留内核崩溃时内核信息导出时所用的内存区域。
  • 打印内存分配情况 __memblock_dump_all(),实际未输出

init_resources()

初始化内存资源,把系统的 ram 以及其他需要保留的 ram 进行保留

sbi_init()

函数相关打印如下,具体作用暂未分析

[    0.000000] SBI specification v0.2 detected
[    0.000000] SBI implementation ID=0x1 Version=0x9
[    0.000000] SBI TIME extension detected
[    0.000000] SBI IPI extension detected
[    0.000000] SBI RFENCE extension detected
[    0.000000] SBI HSM extension detected

kasan_init()

初始化 kasan 动态监测内存错误的工具,初始化完成之后,可以在内存使用越界或者释放后访问时,产生出错报告,帮助分析内核异常。

setup_smp()

配置 SMP 系统,使芯片可以多核运行。

riscv_fill_hwcap()

打印信息如下:
[ 0.000000] riscv: ISA extensions acdfimsu
[ 0.000000] riscv: ELF capabilities acdfim
具体作用暂未分析。

trap_init()

未定义该函数

mem_init()

mem_init() 是架构相关函数,我们分析一下该函数具体做了哪些工作。

void __init mem_init(void)
{
#ifdef CONFIG_FLATMEMBUG_ON(!mem_map);
#endif /* CONFIG_FLATMEM */#ifdef CONFIG_SWIOTLBif (swiotlb_force == SWIOTLB_FORCE ||max_pfn > PFN_DOWN(dma32_phys_limit))swiotlb_init(1);//软件DMA映射,解决部分DMA外设无法访问高地址内存的问题。elseswiotlb_force = SWIOTLB_NO_FORCE;
#endifmemblock_free_all();//释放空闲页面给伙伴分配器print_vm_layout();//打印内存分布情况
}

init_IRQ()

中断初始化是一个架构相关的函数,首先从设备树中取出中断控制器(()interrupt-controller)这一节点。
通过命令将 qemu 的 DTB 文件导出.

sudo env PATH=/labs/linux-lab/boards/riscv64/virt/bsp/qemu/v6.0.0/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin  qemu-system-riscv64 -bios /labs/linux-lab/boards/riscv64/virt/bsp/bios/opensbi/generic/fw_jump.elf -M virt,dumpdtb=my.dtb  -m 128M -net nic,model=virtio -net tap -device virtio-net-device,netdev=net0,mac=$(tools/qemu/macaddr.sh) -netdev tap,id=net0 -smp 4 -kernel /labs/linux-lab/build/riscv64/virt/linux/v5.17/arch/riscv/boot/Image -no-reboot  -drive if=none,file=/labs/linux-lab/build/riscv64/virt/bsp/root/2019.05/rootfs.ext2,format=raw,id=virtio-vda -device virtio-blk-device,drive=virtio-vda -nographic   -append "route=$(ifconfig br0 | grep 'inet ' | tr -d -c '^[0-9. ]' | awk '{print $1}') iface=eth0 rw fsck.repair=yes rootwait root=/dev/vda console=ttyS0"

并将 dtb 文件反编译成 dts 文件。

dtc -I dtb -O dts -o qemu-virt.dts my.dtb

init_IRQ()->irqchip_init()->of_irq_init()->
在 of_irq_init() 中遍历设备树,通过 __irq_of_table 进行匹配,匹配成功后进行初始化 irq。
查看设备树,找到 interrupt-controller 的 compatible 为 riscv,cpu-intc

cpu@0 {phandle = <0x07>;device_type = "cpu";reg = <0x00>;status = "okay";compatible = "riscv";riscv,isa = "rv64imafdcsu";mmu-type = "riscv,sv48";interrupt-controller {#interrupt-cells = <0x01>;interrupt-controller;compatible = "riscv,cpu-intc";phandle = <0x08>;};
};

通过匹配,最终调用的驱动是driver/irqchip/irq-riscv-intc.c

static int __init riscv_intc_init(struct device_node *node,struct device_node *parent)
{int rc, hartid;pr_info("[nfk test] %s-%s-%d\r\n",__FILE__,__FUNCTION__,__LINE__);hartid = riscv_of_parent_hartid(node);//获取CPU idif (hartid < 0) {pr_warn("unable to find hart id for %pOF\n", node);return 0;}else{pr_info("[nfk test] get hartid=%d\r\n",hartid);}/** The DT will have one INTC DT node under each CPU (or HART)* DT node so riscv_intc_init() function will be called once* for each INTC DT node. We only need to do INTC initialization* for the INTC DT node belonging to boot CPU (or boot HART).*/if (riscv_hartid_to_cpuid(hartid) != smp_processor_id())return 0;//每一个CPU都会有其DT NODE,当前我们只需要初始化//boot CPU 的DT NODEintc_domain = irq_domain_add_linear(node, BITS_PER_LONG,&riscv_intc_domain_ops, NULL);//向系统注册一个irq domain,//最终调用__irq_domain_add(),进行内存申请,domain回调函数配置,此处仅完成了irq_domain的注册,后面的中断映射关系还需要在具体驱动中实现。if (!intc_domain) {//intc_domain就是interrupt-controller的软件抽象pr_err("unable to add IRQ domain\n");return -ENXIO;}rc = set_handle_irq(&riscv_intc_irq);//配置中断处理函数if (rc) {pr_err("failed to set irq handler\n");return rc;}cpuhp_setup_state(CPUHP_AP_IRQ_RISCV_STARTING,"irqchip/riscv/intc:starting",riscv_intc_cpu_starting,riscv_intc_cpu_dying);//对热插拔函数进行配置pr_info("%d local interrupts mapped\n", BITS_PER_LONG);return 0;
}

[ 0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99
[ 0.000000] riscv-intc: get hartid=0
[ 0.000000] riscv-intc: hartid 0,cpuid 1 not smp processor_id
[ 0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99
[ 0.000000] riscv-intc: get hartid=1
[ 0.000000] riscv-intc: hartid 1,cpuid 2 not smp processor_id
[ 0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99
[ 0.000000] riscv-intc: get hartid=2
[ 0.000000] riscv-intc: hartid 2,cpuid 3 not smp processor_id
[ 0.000000] riscv-intc: [nfk test] drivers/irqchip/irq-riscv-intc.c-riscv_intc_init-99
[ 0.000000] riscv-intc: get hartid=3
[ 0.000000] riscv-intc: 64 local interrupts mapped

中断初始化的打印如上所示。

time_init()

架构相关函数time_init(),

void __init time_init(void)
{struct device_node *cpu;u32 prop;/*设备树中解析CPU,并且读取他的timebase-frequency*/cpu = of_find_node_by_path("/cpus");if (!cpu || of_property_read_u32(cpu, "timebase-frequency", &prop))panic(KERN_WARNING "RISC-V system with no 'timebase-frequency' in DTS\n");of_node_put(cpu);//减少引用计数riscv_timebase = prop;lpj_fine = riscv_timebase / HZ; //遍历设备树,进行时钟初始化,类似于of_irq_init(),linux-lab-disk中的虚拟开发板当前匹配为空of_clk_init(NULL);timer_probe();
}

timer_probe()中遍历设备树,通过__timer_of_table进行匹配,匹配成功后进行初始化timer。

void __init timer_probe(void)
{struct device_node *np;const struct of_device_id *match;of_init_fn_1_ret init_func_ret;unsigned timers = 0;int ret;pr_info("[nfk test] %s-%s-%d\n",__FILE__,__FUNCTION__,__LINE__);for_each_matching_node_and_match(np, __timer_of_table, &match) {//遍历设备树,匹配timerif (!of_device_is_available(np))continue;pr_info("[nfk test] %s-%s-%d\n",__FILE__,__FUNCTION__,__LINE__);init_func_ret = match->data;ret = init_func_ret(np);//timer初始化if (ret) {if (ret != -EPROBE_DEFER)pr_err("Failed to initialize '%pOF': %d\n", np,ret);continue;}timers++;}timers += acpi_probe_device_table(timer);//注册timerif (!timers)pr_crit("%s: no matching timers found\n", __func__);pr_info("[nfk test] %s-%s-%d\n",__FILE__,__FUNCTION__,__LINE__);
}

添加调试信息,打印如下:

[ 0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-23
[ 0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[ 0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[ 0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[ 0.000000] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-28
[ 0.000000] riscv_timer_init_dt: Registering clocksource cpuid [0] hartid [3]
[ 0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[ 0.000126] sched_clock: 64 bits at 10MHz, resolution 100ns, wraps every 4398046511100ns
[ 0.002668] [nfk test] drivers/clocksource/timer-probe.c-timer_probe-46

通过以上信息,可知,匹配到了4次timer,通过中间的相关打印信息,找到驱动drivers/clocksource/timer-riscv.c。

static int __init riscv_timer_init_dt(struct device_node *n)
{int cpuid, hartid, error;struct device_node *child;struct irq_domain *domain;hartid = riscv_of_processor_hartid(n);//获取node所在的hartidif (hartid < 0) {pr_warn("Not valid hartid for node [%pOF] error = [%d]\n",n, hartid);return hartid;}cpuid = riscv_hartid_to_cpuid(hartid);//获取cpu idif (cpuid < 0) {pr_warn("Invalid cpuid for hartid [%d]\n", hartid);return cpuid;}if (cpuid != smp_processor_id())return 0;//判断是否未boot cpudomain = NULL;child = of_get_compatible_child(n, "riscv,cpu-intc");if (!child) {//获取中断的domainpr_err("Failed to find INTC node [%pOF]\n", n);return -ENODEV;}domain = irq_find_host(child);of_node_put(child);if (!domain) {pr_err("Failed to find IRQ domain for node [%pOF]\n", n);return -ENODEV;}riscv_clock_event_irq = irq_create_mapping(domain, RV_IRQ_TIMER);//建立中断映射if (!riscv_clock_event_irq) {pr_err("Failed to map timer interrupt for node [%pOF]\n", n);return -ENODEV;}pr_info("%s: Registering clocksource cpuid [%d] hartid [%d]\n",__func__, cpuid, hartid);error = clocksource_register_hz(&riscv_clocksource, riscv_timebase);//注册timerif (error) {pr_err("RISCV timer register failed [%d] for cpu = [%d]\n",error, cpuid);return error;}sched_clock_register(riscv_sched_clock, 64, riscv_timebase);error = request_percpu_irq(riscv_clock_event_irq,riscv_timer_interrupt,"riscv-timer", &riscv_clock_event);//注册中断处理函数if (error) {pr_err("registering percpu irq failed [%d]\n", error);return error;}error = cpuhp_setup_state(CPUHP_AP_RISCV_TIMER_STARTING,"clockevents/riscv/timer:starting",riscv_timer_starting_cpu, riscv_timer_dying_cpu);//热插拔配置if (error)pr_err("cpu hp setup state failed for RISCV timer [%d]\n",error);return error;
}

关于设备树匹配函数分析

以下函数是进行循环匹配的函数。

for_each_matching_node_and_match(np, __timer_of_table, &match)
for_each_matching_node_and_match(np, __irqchip_of_table, &match)

我们找到他的根本调用,参数描述如下,分别是设备树节点,要扫描的结构体,匹配到的结构体。

/*** of_find_matching_node_and_match - Find a node based on an of_device_id*                                   match table.* @from:       The node to start searching from or NULL, the node*              you pass will not be searched, only the next one*              will; typically, you pass what the previous call*              returned. of_node_put() will be called on it* @matches:    array of of device match structures to search in* @match:      Updated to point at the matches entry which matched** Return: A node pointer with refcount incremented, use* of_node_put() on it when done.*/

搞清楚入参之后,我们找一下__timer_of_table从何处定义。

#define TIMER_OF_DECLARE(name, compat, fn) \OF_DECLARE_1_RET(timer, name, compat, fn)

下一层宏定义

#define OF_DECLARE_1_RET(table, name, compat, fn) \ _OF_DECLARE(table, name, compat, fn, of_init_fn_1_ret)

下一层宏定义

#define _OF_DECLARE(table, name, compat, fn, fn_type)                   \static const struct of_device_id __of_table_##name              \__used __section("__" #table "_of_table")               \__aligned(__alignof__(struct of_device_id))             \= { .compatible = compat,                              \.data = (fn == (fn_type)NULL) ? fn : fn  }

所以我们根据宏定义TIMER_OF_DECLARE寻找与设备树节点可以匹配的驱动,我们首先看一下传入的node的信息。

truct device_node {const char *name;phandle phandle;const char *full_name;struct fwnode_handle fwnode;struct  property *properties;struct  property *deadprops;    /* removed properties */struct  device_node *parent;struct  device_node *child;struct  device_node *sibling;
#if defined(CONFIG_OF_KOBJ)struct  kobject kobj;
#endifunsigned long _flags;void    *data;
#if defined(CONFIG_SPARC)unsigned int unique_id;struct of_irq_controller *irq_trans;
#endif
};

最终调用匹配的函数如下所示:

static int __of_device_is_compatible(const struct device_node *device,const char *compat, const char *type, const char *name)
{struct property *prop;const char *cp;int index = 0, score = 0;/* Compatible match has highest priority */if (compat && compat[0]) {prop = __of_find_property(device, "compatible", NULL);for (cp = of_prop_next_string(prop, NULL); cp;cp = of_prop_next_string(prop, cp), index++) {if (of_compat_cmp(cp, compat, strlen(compat)) == 0) {score = INT_MAX/2 - (index << 2);break;}}if (!score)return 0;}/* Matching type is better than matching name */if (type && type[0]) {if (!__of_node_is_type(device, type))return 0;score += 2;}/* Matching name is a bit better than not */if (name && name[0]) {if (!of_node_name_eq(device, name))return 0;score++;}return score;
}

可以看到,能够搜到的
drivers/clocksource/timer-riscv.c
TIMER_OF_DECLARE(riscv_timer, “riscv”, riscv_timer_init_dt);

riscv 启动流程分析相关推荐

  1. RISC-V Linux 启动流程分析

    " Author:  通天塔 985400330@qq.com Date:    2022/05/15 Revisor: lzufalcon falcon@tinylab.org Proje ...

  2. 解析并符号 读取dll_Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  3. Zygote进程启动流程分析

    文中的源代码版本为api23 Zygote进程启动流程分析 先说结论,zygote进程启动过程中主要做了下面这些事情: 启动DVM虚拟机 预加载部分资源,如一些通用类.通用资源.共享库等 启动syst ...

  4. c++builder启动了怎么停止_App 竟然是这样跑起来的 —— Android App/Activity 启动流程分析...

    在我的上一篇文章: AJie:按下电源键后竟然发生了这一幕 -- Android 系统启动流程分析​zhuanlan.zhihu.com 我们分析了系统在开机以后的一系列行为,其中最后一阶段 AMS( ...

  5. SpringBoot启动流程分析(四):IoC容器的初始化过程

    SpringBoot系列文章简介 SpringBoot源码阅读辅助篇: Spring IoC容器与应用上下文的设计与实现 SpringBoot启动流程源码分析: SpringBoot启动流程分析(一) ...

  6. Exynos4412 Uboot 移植(二)—— Uboot 启动流程分析

    uboot启动流程分析如下: 第一阶段: a -- 设置cpu工作模式为SVC模式 b -- 关闭中断,mmu,cache v -- 关看门狗 d -- 初始化内存,串口 e -- 设置栈 f -- ...

  7. bootloader启动流程分析

    bootloader启动流程分析 1.Bootloader的概念和作用 Bootloader是嵌入式系统的引导加载程序,它是系统上电后运行的第一段程序.在完成对系统的初始化任务之后,它会将Flash中 ...

  8. MyBatis启动流程分析

    目录 MyBatis简单介绍 启动流程分析 简单总结 附录 MyBatis内置别名转换 参考 MyBatis简单介绍 MyBatis是一个持久层框架,使用简单,学习成本较低.可以执行自己手写的SQL语 ...

  9. NameNode之启动流程分析

    NameNode启动流程分析 public staticvoid main(Stringargv[]) throws Exception { if (DFSUtil.parseHelpArgument ...

最新文章

  1. SqlServer中获取所有数据库,所有表,所有字段
  2. Call to undefined function mysqli_connect()
  3. C语言 什么时候用取地址什么时候不用取地址,符号讲解
  4. 当铺密码解密脚本--[GKCTF2020]汉字的秘密
  5. 21行满分代码:1039 到底买不买 (20分)
  6. Servlet 的实例对象
  7. 【英语学习】【Level 07】U02 Live Work L6 A countryside house
  8. 从零开始学前端:CSS盒子模型属性 --- 今天你学习了吗?(CSS:Day14)
  9. python雷达图怎么做_使用Python绘制雷达图
  10. Mysql学习总结(32)——MySQL分页技术详解
  11. 数据丢包怎么修复_网络丢包率如何解决
  12. 通过升级.NET框架提升实体框架性能
  13. 【读书记录】TAOCP卷一:开始
  14. linux基础ppt下载,《Linux基础》PPT课件.ppt
  15. 渗透测试之敏感信息收集
  16. 十大项目管理知识-进度管理
  17. 超实用的 Python 技巧,异步操作数据库!
  18. 使用dd命令克隆整个系统
  19. 传奇服务器的角色文件在,传奇版本等一些软件放到服务器里的方法
  20. 在linux上安装navicat 出现使用一段时间后闪退的情况

热门文章

  1. python爬取趣事百科文本笑话,另中间两个for能同时输出不解,请看到的朋友帮忙解答
  2. 并行与分布式计算导论—性能分析
  3. linux svn拒绝,Linux SVN的安装以及配置
  4. 经典ARP协议讲解,一定要看
  5. 机器学习的激励函数的故事
  6. ceph-deploy Some monitors have still not reached quorum
  7. 两个求和符号,Matlab写法
  8. JPA 组合查询之AND和OR组合查询
  9. 使用docker简单编译k20pro内核
  10. iOS在图层上使用CATransform3D制做三维动画