Cortex-A7双核处理器的双核启动调试记录
davinci双核启动调试记录
第一阶段
双核启动原理概述:
达芬奇双核启动的原理和流程如下图所示:
由上图可知,CPU1跳转是通过CPU0触发的,CPU0为CPU1启动做的工作如下:
(1)将secondary CPU启动的地址填写到USB的一个寄存器中,因为目前没有一块可以使用的UBOOT和kernel通信的内存空间,所以暂时使用USB的寄存器作为跳转触发条件;
(2)设置identity mapping地址映射空间,这段地址空间是虚拟地址和物理地址一一映射的。
实验过程:
实验前现象:
通过实验结果可以确定的是,CPU0针对CPU1设置好启动的条件和环境之后,CPU0通过USB寄存器通知到CPU1开始从secondary_startup启动成功,CPU1进入到kernel的启动阶段的汇编启动阶段。但是在使能MMU后,并没有预期的跳转到汇编标__secondary_switched的地址开始运行。
实验及步骤:
实验一:确定CPU0设置的identity mapping地址映射空间是设置正确,且可以正常访问工作。
步骤一:由于内核的驱动模块是最后启动,所以在内核的tty驱动中添加identity mapping的地址的打印;
步骤二:在drivers/tty/serial/8250/8250_core.c中的static int __init serial8250_init(void)函数中添加如下代码:
步骤三:启动达芬奇板卡,通过串口,输出如下,由下图可知identity mapping地址空间的物理地址和虚拟地址是一一映射的。
步骤四:确认kernel的链接脚本中,针对identity mapping地址空间的划分,是在text段中的划分。定义如下:
步骤五:在Jlink终端里,读取0xc0100000地址空间的数据,打印如下:
步骤六:反汇编vmlinux,找到0xc0100000地址位置,如下图所示。与步骤七里通过Jlink读取内存空间里的数据是一致的。
步骤七:在linux中,使用命令devmem访问identity mapping物理地址空间,打印如下:
实验一结论:由实验一可以得出,CPU1在identity mapping内存空间里进行的MMU使能,并且使能MMU前后物理地址和虚拟地址确实时一一映射的。可以保证CPU1运行在identity mapping地址空间使能MMU后,下一条指令的物理地址和虚拟地址是一样的。
实验二:验证CPU1使能MMU后,确保下一条汇编指令地址,送入MMU的是identity mapping地址,且是与使能MMU之前的地址连续的
步骤一:在kernel源代码arch/arm/kernel/head.S中,在CPU1打开MMU的位置,添加如下图调试代码,在CPU1开始MMU前后设置地址标号。汇编标号:__continue_run_40,设置在CPU1开启MMU之前;汇编标号:__continue_run_44,设置在CPU1开始MMU之后第一条汇编指令的地址位置;汇编标号:__continue_run_48——__continue_run_54,每隔一条汇编指令,设置一个汇编地址标号。
步骤二:反汇编内核image,即vmlinux,CPU1打开MMU前后的反汇编代码如下图所示。由下图反汇编代码可知CPU1打开MMU时是使用的是identity mapping地址空间。而且CPU1打开MMU前后的反汇编代码的地址是连续的,且均在identity mapping地址空间。
从反汇编代码可知汇编地址标号__continue_run_40——__continue_run_54 的值也在identity mapping地址空间内。所以从反汇编代码可知所有指令的地址均在identity mapping地址空间内。
步骤三:使用Jlink工具读取identity mapping地址空间的内容,内容如下图所示,从Jlink打印的汇编地址编号__continue_run_40——__continue_run_54 数据可知,与步骤二的地址一致。
步骤四:在linux环境下使用devmem工具读取汇编地址编号__continue_run_40——__continue_run_54 的地址,地址如下图所示,地址与步骤二、三的地址一致。
实验二结论:CPU1在打开MMU后第一条指令的地址,其地址值是物理内存和虚拟内存是同样数值。也就是说,打开MMU之后,CPU1发出的指令地址是与打开MMU之前地址连续的,物理地址和虚拟地址是同样的,且连续的。
实验二的现象:CPU1并没有运行到预期的汇编地址位置,因此需要做实验三,查找CPU1开启MMU之后,CPU1发出的真实地址值是多少。
实验三:验证CPU1打开MMU之后第一条指令实际的地址值
步骤一:参考实验二的步骤一,在同样代码位置添加如下代码,汇编地址标号__continue_run_44——__continue_run_54 设置在CPU1打开MMU之后的汇编代码位置。汇编地址标号__continue_run_48——__continue_run_54 的地址,在打开MMU之前,将汇编地址标号的地址存入地址0xc0100120——0xc010012c中。同样的地址标号__continue_run_48——__continue_run_54 的地址,在打开MMU之后,将汇编地址标号的地址存入地址0xc0100130——0xc010013c中。实验三的正确预期结果应该是:
地址0xc0100120内的数值 == 地址0xc0100130内的数值;
地址0xc0100124内的数值 == 地址0xc0100134内的数值;
地址0xc0100128内的数值 == 地址0xc0100138内的数值;
地址0xc010012c内的数值 == 地址0xc010013c内的数值。
步骤二:反汇编内核的image,即vmlinux,其反汇编的结果如下图所示。由下图可知CPU1在使能MMU前后,均运行在identity mapping地址空间,且地址空间是连续的,与实验二的步骤二的结果一致。
步骤三:步骤二之后Jlink打印的结果如下图1和图2所示,预期的CPU1开启MMU前后的指令地址的结果不一致。即如下:
地址0xc0100120内的数值 != 地址0xc0100130内的数值;
地址0xc0100124内的数值 != 地址0xc0100134内的数值;
地址0xc0100128内的数值 != 地址0xc0100138内的数值;
地址0xc010012c内的数值 != 地址0xc010013c内的数值。
图一
图二
图三
步骤四:为了确认0xc0100130——0xc010013c 地址内的内容没有其他程序或者操作会影响,在本实验三的步骤一代码基础上屏蔽掉对0xc0100130——0xc010013c 地址写入。代码修改如下图所示。其结果在Jlink里打印如上步骤三的图3所示。本步骤可以得出0xc0100130——0xc010013c 的地址只有下图屏蔽的代码修改。
步骤五:使用devmem命令读取步骤二之后的物理内存0xc0100118——0xc010013c地址空间内的值,如下图所示。与步骤三和步骤一的对应地址结果一致。
实验三结果分析:实验三的结果可以得出,在CPU1打开MMU之后,汇编指令的地址并不是程序要求的正确地址,CPU1的取指指令取到的地址并不是程序给的指令地址,程序给出了正确的指令地址,但是在CPU1里却变成了其他地址。另外,CPU1开启MMU之后的指令地址,即实验三的步骤三的0xc0100130——0xc010013c地址内的值,分别是 0xe1a0f003 、0xc010d5a0 、 0xee112f10 、 0xe3c22001。而这些数值其实质是0xc0100130——0xc010013c地址内本应该存储的ARM指令微码。也就是说CPU1开启MMU之后的指令都没有运行。也就是MMU映射的地址不正确。而往这四个字的地址空间内写入汇编指令地址标号的之前,就CPU1就将MMU关闭,所以写入的数据不存在写到其他地址的可能。而达芬奇的kernel的虚拟地址空间如下图1所示。而汇编执行的地址段,由实验一的步骤4可知在text段内,而CPU1打开MMU之后的四个汇编指令地址不连续,而且有三个地址超出了text段地址空间。下图2所示结果是identity mapping地址空间为0xc0100000 - 0xc0100154,0xc0100130——0xc010013c地址在其范围之内,所以可以确保是一一映射的。
图1
图2
图3
分析实验:打印出CPU1使能MMU前,CP15的C1寄存器的值,并且分析、验证各个寄存器设置无错误。(达芬奇设置MMU的代码是arm的特定指令集版本通用代码,其他cortex-A7芯片厂商均使用的是这段代码)。c1寄存器的值写入的是:0x10c5387d,通过对比分析手册文档《Cortex-A7_MPCore_Technical_Reference_Manual》里第4章节的寄存器的各个bit的意义,验证是设置无误的。
综上实验和分析得出的结论:CPU1使能MMU后,MMU映射的地址不正确。
第二阶段
在非kernel环境下,编写一个测试程序,对CPU1开启MMU后进行验证。因为一经上电,两个CPU core都会从0地址开始运行,而davinci的板卡设计是将Flash地址空间作为0地址,所以需要设计一个类似uboot一样的极简启动控制代码,目的是将两个core给hold住,loop一个指定的地址是否为0,非0则跳转到功能代码中。往指定地址写入非0数值的操作使用Jlink实现。极简的从Flash启动代码如下所示。0x4000BF00地址作为CPU0判断是否跳转到SRAM中运行的判断地址,0x4000BF04地址作为CPU1判断是否跳转到SRAM中运行的判断地址。
.text .global __start
__start:
ldr r0, =0x40004000 mov r1, #0 str r1,[r0]
ldr r0, =0x4000BF10 @ setup CPU0 signal mov r1, #0 str r1,[r0]
ldr r0, =0x4000BF14 @ plus 1 space mov r1, #0 str r1,[r0]
ldr r0, =0x4000BF20 @ setup CPU1 signal mov r1, #0 str r1,[r0]
ldr r0, =0x4000BF24 @ plus 1 space mov r1, #0 str r1,[r0]
mov r5, #0
MRC p15, 0, r0, c0, c0, 5 @ Read MPIDR ANDS r0, r0, #3 cmp r0, #0 beq cpu_0_wait
cpu_1_wait: ldr r4, =0x4000BF24 str r5, [r4] add r5, r5, #1
ldr r2, =0x4000BF04 @ stage test point ldr r3, =0x22220808 @ test value str r3, [r2]
ldr r0, =0x4000BF20 @ not 0 is signal for setup cpu1 ldr r1, [r0] cmp r1, #0 beq cpu_1_wait
ldr r7, =0x22 ldr pc, =0x40004000 @ bug point
cpu_0_wait: ldr r4, =0x4000BF14 str r5, [r4] add r5, r5, #1
ldr r2, =0x4000BF00 @ stage test point ldr r3, =0x11110808 @ test value str r3, [r2]
ldr r0, =0x4000BF10 @ not 0 is signal for setup cpu0 ldr r1, [r0] cmp r1, #0 beq cpu_0_wait
ldr r7, =0x11 ldr pc, =0x40004000 @ jump to SRAM text
loop: b loop |
进入到SRAM中,CPU0和CPU1的运行代码如下:
.text .global __start_ram
__start_ram: MRC p15, 0, r0, c0, c0, 5 @ Read MPIDR ANDS r0, r0, #3 cmp r0, #0 beq cpu_0_run b cpu_1_run
ldr r0, =0x4000BF00 ldr r1, =0xEEEEEEEE str r1, [r0]
ERR_loop: b ERR_loop
cpu_0_run: ldr sp, =0x4000B4FF ldr r0, =0x4000BF00 ldr r1, =0x1234abcd str r1, [r0]
/* setup page table.*/ ldr r0, =0x40000400 ldr r1, =0x40000c1e str r1, [r0]
/* enable mmu.*/ // mov r0, #0 // mcr p15, 0, r0, c7, c1, 0 @ clean ICaches and DCaches // // mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4 // // mcr p15, 0, r0, c8, c7, 0
// ldr r5, =0xFFFFFFFF // mcr p15, 0, r5, c3, c0, 0 @ load domain access register
// ldr r4, =0x40000000 // mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
// mrc p15, 0, r0, c1, c0, 0 // bic r0, r0, #0x3000 // bic r0, r0, #0x0300 // bic r0, r0, #0x0087 // orr r0, r0, #0x0002 // orr r0, r0, #0x0004 // orr r0, r0, #0x1000 orr r0, r0, #0x0001 mcr p15, 0, r0, c1, c0, 0
/* test CPU1 run after open MMU.*/ ldr r0, =0x4000BF30 ldr r1, =0xAAAAAAAA str r1, [r0] ldr r0, =0x4000BF34 ldr r1, =0xBBBBBBBB str r1, [r0] ldr r0, =0x4000BF38 ldr r1, =0xCCCCCCCC str r1, [r0]
loop_cpu0: b loop_cpu0
cpu_1_run: ldr sp, =0x4000B8FF ldr r0, =0x4000BF04 ldr r1, =0xabcd1234 str r1, [r0] /* enable mmu.*/ mov r0, r0 mov r0, r0 mov r0, r0
/* setup page table.*/ ldr r0, =0x40000400 ldr r1, =0x40000c1e str r1, [r0]
/* enable mmu.*/ // mov r0, #0 // mcr p15, 0, r0, c7, c1, 0 @ clean ICaches and DCaches // // mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4 // // mcr p15, 0, r0, c8, c7, 0
ldr r5, =0xFFFFFFFF mcr p15, 0, r5, c3, c0, 0 @ load domain access register
ldr r4, =0x40000000 mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
mrc p15, 0, r0, c1, c0, 0 // bic r0, r0, #0x3000 // bic r0, r0, #0x0300 // bic r0, r0, #0x0087 // orr r0, r0, #0x0002 // orr r0, r0, #0x0004 // orr r0, r0, #0x1000 orr r0, r0, #0x0001 mcr p15, 0, r0, c1, c0, 0
loop_test_0: /* test CPU1 run after open MMU.*/ ldr r0, =0x4000BF40 ldr r1, =0x33333333 str r1, [r0] ldr r0, =0x4000BF44 ldr r1, =0x55555555 str r1, [r0] ldr r0, =0x4000BF48 ldr r1, =0x77777777 str r1, [r0] b loop_test_0
mov r0, r0 mov r0, r0 mov r0, r0
loop_cpu1: b loop_cpu1 loop: b loop |
实验的结果是CPU0能够在使能MMU后运行正常,CPU1运行不正常。
根据上述实验结果,需要排除CPU1、CP15和MMU三者硬件设计正确。
鉴于第一阶段得出MMU映射不正确的推断,那么引申出来两个可能的原因。第一个原因是MMU硬件工作不正常,内存的页表设置正确;第二个原因是MMU硬件工作正常,内存的页表设置不正确。鉴于CPU0和CPU1使用的是相同的内存页表,而CPU0在此内存页表设置下是工作正常的,所以内存的页表设置是正确的。所以第二个原因不成立。此时有一个问题,就是既然第二个原因不成立,也就是说MMU硬件是工作正常的,那么CPU0和CPU1均应该正常才是。这个问题的回答是:假设CPU使用MMU仅仅是CPU的core和MMU两者的关系,那么上述问题是真命题,但是CPU使能MMU是通过CP15协处理器实现的,那么就应该有必要确认CPU1、CPU1的CP15、MMU三者在硬件上是设置正确,且硬件正确的。
所以,协调硬件的同事,在PC上进行仿真验证。因为PC上验证的模拟硬件资源与davinci板卡的硬件资源不同,所以需要针对PC验证设计一个验证程序。
验证程序需要有如下几个限制:
- 使用MMU的分段功能,因为identity mapping空间使用的是分段;
- 使能MMU的时候,关闭cache等,仅仅使能MMU;
- 手动填入页表的表项,确保至少映射了两块,flash和SRAM。
验证程序如下:
.text .global __start
__start: ldr r0, =0x40004000 mov r1, #0 str r1,[r0] ldr r0, =0x4000BF10 @ setup CPU0 signal mov r1, #0 str r1,[r0] ldr r0, =0x4000BF20 @ setup CPU1 signal mov r1, #0 str r1,[r0] MRC p15, 0, r0, c0, c0, 5 @ Read MPIDR ANDS r0, r0, #3 cmp r0, #0 beq cpu_0_wait
cpu_1_wait: ldr r2, =0x4000BF04 @ stage test point ldr r3, =0x22220808 @ test value str r3, [r2]
cpu_1_pause: ldr r0, =0x4000BF20 @ not 0 is signal for setup cpu1 ldr r1, [r0] cmp r1, #0 beq cpu_1_pause
mov r0, r0 mov r0, r0 mov r0, r0 /* setup page table.*/ ldr r0, =0x40000000 ldr r1, =0x00000c1e str r1, [r0] /* enable mmu.*/ // mov r0, #0 // mcr p15, 0, r0, c7, c1, 0 @ clean ICaches and DCaches // mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4 // mcr p15, 0, r0, c8, c7, 0 ldr r5, =0xFFFFFFFF mcr p15, 0, r5, c3, c0, 0 @ load domain access register ldr r2, =0x4000BF50 str r5, [r2] ldr r4, =0x40000000 mcr p15, 0, r4, c2, c0, 0 @ load page table pointer ldr r2, =0x4000BF54 str r4, [r2] mrc p15, 0, r0, c1, c0, 0 // bic r0, r0, #0x3000 // bic r0, r0, #0x0300 // bic r0, r0, #0x0087 // orr r0, r0, #0x0002 // orr r0, r0, #0x0004 // orr r0, r0, #0x1000 orr r0, r0, #0x0001 ldr r2, =0x4000BF58 ldr r3, =0x58585858 str r3, [r2] mcr p15, 0, r0, c1, c0, 0
loop_test_1: /* test CPU1 run after open MMU.*/ ldr r0, =0x4000BF40 ldr r1, =0x33333333 str r1, [r0] ldr r0, =0x4000BF44 ldr r1, =0x55555555 str r1, [r0] ldr r0, =0x4000BF48 ldr r1, =0x77777777 str r1, [r0] b loop_test_1 // ldr r7, =0x22 // ldr pc, =0x40004000 @ bug point
cpu_0_wait: ldr r2, =0x4000BF00 @ stage test point ldr r3, =0x11110808 @ test value str r3, [r2] // ldr r0, =0x4000BF10 @ not 0 is signal for setup cpu0 // ldr r1, [r0] // cmp r1, #0 // beq cpu_0_wait ldr r2, =0x4000BF20 ldr r3, =0x11111111 str r3, [r2] ldr r0, =0x4000BF30 ldr r1, =0xAAAAAAAA str r1, [r0] ldr r0, =0x4000BF34 ldr r1, =0xBBBBBBBB str r1, [r0] ldr r0, =0x4000BF38 ldr r1, =0xCCCCCCCC str r1, [r0] cpu_0_loop: b cpu_0_loop ldr r7, =0x11 ldr pc, =0x40004000 @ jump to SRAM text loop: b loop |
PC上仿真的结果是CPU1能够在使能MMU的情况下能够工作正常。可以将这个结论作为一个不坚定的正确假设。以此为基础做进一步的实验验证。梳理arm官方的core手册,分析CP15相关设置是否正确,MMU机制和TLB设置是否正确。使用最新版本的core手册,发现CP15的C2寄存器的设计不同,按照最新的手册更改程序后,裸机验证程序下,CPU0和CPU1均能在使能MMU后工作正常。(注:验证程序在配套的程序包的git分支master中)
第三阶段
经过上述两个阶段的实验和分析,得出如下结论:
- Kernel启动后,CPU0工作正常,CPU1没有引导注册到SMP子系统中;
- Kernel中,经过实验验证得出kernel里的identity mapping正确;
- CPU1、CPU1的CP15、MMU三者的硬件可以确认是正确的;
- CPU1使用MMU分段能够寻址正确。
梳理上述四个结论,可以初步确定是kernel的问题。
回归到最初的问题现象,问题定位的代码如下图所示:
问题的现象,是使能MMU后,不能使用下图所示代码打印:
通过kernel的反汇编,如下图所示,可知__error_p汇编功能模块链接的地址不是处于identity mapping地址空间中。所以这部分代码其实是需要使用二级分页进行跳转的,如果跳转不成功,就是上述现象。
在kernel中,使能MMU这段代码是运行在identity mapping地址空间中,这部分与裸机验证程序的设计是相同的,都是使用的分段,所以可以确认只要kernel里打开MMU后,在identity mapping空间里运行,肯定是正确的。而使用分页机制,是否正确,有待商榷。通过反汇编代码(如下图1所示),以及kernel的identity mapping空间(下图2所示)可知0xc0100044~0xc0100060是cpu_ca9mp_reset函数地址空间,暂时启动过程中用不到reset功能,所以可以将这段空间作为测试空间。
图1
图2
将arch/arm/kernel/head.S文件中,针对secondary CPU的启动的调试信息,都使用内存读写指令写到0xc0100044~0xc0100060中,然后使用Jlink查看。经过实验,发现是CPU1没有读取到swapper_pg_dir的值、secondary_data.stack的值和translation table base的值。但是通过第一阶段CPU0的打印的信息可知CPU0设置了这些值,但是CPU1没有得到。分析原因有如下可能:
- swapper_pg_dir和secondary_data.stack的值在bss段,而CPU1运行的时候,bss段的数据改变了;
- swapper_pg_dir和secondary_data.stack的值存储的位置,CPU1没有权限访问;
- CPU0和CPU1协同工作不正确。
验证第一种可能,在kernel单核启动成功后,使用Jlink查看bss段的数据,数据并没有改变,所以可以排除第一种可能。验证第二种可能,是配置CPU1的MMU和CP15的domain时候,设置成全地址空间可读写权限,问题依旧,故可以排除。
至于第三种情况,双核启动的设计是CPU1等待CPU0的启动信号后再进入kernel,而再kernel中,使CPU0不启动CPU1,使用Jlink命令查看CPU1写identity mapping的地址,发现依然写入,所以可以断定CPU1并没有按照预期停住。
分析uboot代码,CPU1期望使用wfene指令停住,kernel使用sev指令使CPU1进入kernel,但是这两条指令没有起到预期效果。不适用这个指令,使用寄存器的特定值唤起设计,问题解决。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
图数据库 Nebula Graph 的代码变更测试覆盖率实践
对于一个持续开发的大型工程而言,足够的测试是保证软件行为符合预期的有效手段,而不是仅仅依靠 code review 或者开发者自己的技术素质。测试的编写理想情况下应该完全定义软件的行为,但是通常情况都是很难达到这样理想的程度。而测试覆盖率就是检验测试覆盖软件行为的情况,通过检查测试覆盖情况可以帮助开发人员发现没有被覆盖到的代码。 测试覆盖信息搜集 Nebula Graph 主要是由 C++ 语言开发的,支持大部分 Linux 环境以及 gcc/clang 编译器,所以通过工具链提供的支持,我们可以非常方便地统计Nebula Graph的测试覆盖率。 gcc/clang 都支持 gcov 式的测试覆盖率功能,使用起来也是非常简单的,主要有如下几个步骤: 添加编译选项 --coverage -O0 -g 添加链接选项 --coverage 运行测试 使用 lcov,整合报告,例如 lcov --capture --directory . --output-file coverage.info 去掉外部代码统计,例如 lcov --remove coverage.info '*/opt/ve...
- 下一篇
Service Mesh 最火项目 Istio 分层架构,你真的了解吗?
作者 | 王夕宁 阿里巴巴高级技术专家 参与“阿里巴巴云原生”公众号文末留言互动,即有机会获得赠书福利! 本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio服务网格技术解析与实践》一书,文章从基础概念入手,介绍了什么是服务网格及Istio,针对 2020服务网格的三大发展趋势,体系化、全方位地介绍了 Istio 服务网格的相关知识。你只需开心参与文末互动,我们负责买单!技术人必备书籍《Istio 服务网格技术解析与实践》免费领~ Istio是一个开源的服务网格,可为分布式微服务架构提供所需的基础运行和管理要素。随着各组织越来越多地采用云平台,开发者必须使用微服务设计架构以实现可移植性,而运维人员必须管理包含混合云部署和多云部署的大型分布式应用。Istio采用一种一致的方式来保护、连接和监控微服务,降低了管理微服务部署的复杂性。 从架构设计上来看,Istio服务网格在逻辑上分为控制平面和数据平面两部分。其中,控制平面Pilot负责管理和配置代理来路由流量,并配置Mixer以实施策略和收集遥测数据;数据平面由一组以Sidecar方式部署的智能代理(Envoy)组成,这些代理可以调节和控...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Hadoop3单机部署,实现最简伪集群
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker快速安装Oracle11G,搭建oracle11g学习环境