Page Translation

以《AMD64 Architecture Programmer's manual volums》从硬件角度说明一个虚拟地址如何转成对应物理页。AM64 地址转换分为 legacy 和 long mode两个模式,其中legacy x86主要支持将32位虚拟地址转换成32位物理地址(如果开启特殊模式,还支持更大物理内存地址,比如36位或者40 位)。long mode模式下支持将64位虚拟地址转换成52位物理内存地址,long模式下64位系统支持更小的虚拟地址与物理地址转换。为了简化,主要说明long mode模式下硬件是如何转换的

The legacy x86 architecture provides support for translating 32-bit virtual addresses into 32-bit physical addresses (larger physical addresses, such as 36-bit or 40-bit addresses, are supported as a special mode). The AMD64 architecture enhances this support to allow translation of 64-bit virtual addresses into 52-bit physical addresses, although processor implementations can support smaller virtual-address and physical-address spaces.

AMD64位虚拟地址转物理地址通过多级层次转换表实现,每一级表为一定数量的数组table,table中的每个entry存放有指向更低一级的table。每级table中包含数百上千条entry用户转换,指向更低一级table。最低一级table中的entry指向实际转换的物理页。

Virtual addresses are translated to physical addresses through hierarchical translation tables created and managed by system software. Each table contains a set of entries that point to the next-lower table in the translation hierarchy. A single table at one level of the hierarchy can have hundreds of entries,each of which points to a unique table at the next-lower hierarchical level. Each lower-level table can in turn have hundreds of entries pointing to tables further down the hierarchy. The lowest-level table in the hierarchy points to the translated physical page

long mod模式下,采用四级转换方式,如下图:

  • CR3寄存器 为X86专用寄存器,用于保存最高级别表基地址。(it contains the base address of the highest-level page-translation table, and aslo cintains cache controls for the specified tables).
  • 四级转换表分别为PML4E--> PDPE---> PDE--> PTE(分别对应内核PGD-->PUD-->PMD-->PTE,没有P4D表)。
  •  long 模式下支持物理页page 尺寸可以为:4K、2M和1G。

 开启page translation

CR0寄存器中PG位用于开启page translation功能,CR0寄存器如下:

  • PG 位(Paging Enable Bit):  当PG 设置位1时开启 page translation功能,注意只有在CPU处于protect mode模式(CR0.PE=1)是才能设置PG。如果CPU不处于保护模式,设置PG会报general-protection异常。arch\x86\boot\compressed\head_64.s文件中有使用汇编开启PG代码 :
 /* Enter paged protected Mode, activating Long Mode */movl  $(X86_CR0_PG | X86_CR0_PE), %eax /* Enable Paging and Protected mode 同时开启保护模式*/movl %eax, %cr0

与Page Size 相关几个配置

开启CR0.PG之后,根据实际情况需要选择page size大小,影响page size寄存器有::CR4.PAE、CR4.PSE、EFER.LMA,配置组合情况如下:

  • Long Mode 模式 CR4.PAE必须开启。
  • Long Mode模式下当PDPRE.PS和PDE.PS设置为0时,表明最低级别的表项为PTE,page size大小为 4K
  • Long Mode模式下当PDPRE.PS设置为0,和PDE.PS设置为1时,表明最低级别表项为PDE,page size大小为 2M,
  • Long Mode模式下当PDPRE.PS设置为1,不管PDE.PS是0还是1表明表明最低级别表项为PDPE,  page size为1G.

开启物理地址扩展功能

当CPU为 amd64位时,CPU采用位Long Mode模式,所转换物理地址最高位52位,此时必须开启CR4.PAE功能:

  •  physical-Address Extension(PAE):bit 5,  置1为开启PAE功能,CPU支持64位地址,允许将64位虚拟地址转换成52位物理地址。同样如果时legical 模式page size大小4M时页需要开启此功能,(arch\x86\boot\compressed\efi_thunk_64.s)文件中,有汇编开启方法:
 movl    %cr4, %eaxbtsl  $(X86_CR4_PAE_BIT), %eaxmovl    %eax, %cr4

CR3 寄存器

cr3寄存器在整个硬件page table中占用重要地位,是地址转换开始入口,里面存放有最高一级table 基地址即PML4表基地址,long mode CR3寄存器格式如下

  •  Page-map Table Base Address(12~51):存放PML4 表基地址,注意该地址为物理地址(如果为虚地址则将进入先有鸡还是先有蛋死循环中)PML4基地址 为4K对齐,所以支持地址可以到52位,其中0~11由于4K对齐要求,可以用作其他设置使用。
  • 当CP4.PCIDE=0时,3bit 位PBW位:page-level writethrough bit 3bit 表示最高级page table即PML4 是否配置write back或write through cace策略。如果PWT=0 则设置位write back cache策略,PWT=1 设置位writethrough cache策略。
  • 当CP4.PCIDE=0时,4bit 位时PCD 位:page-level cache disable, 是否开启PML4的cache功能。设置为1 关闭cache功能,设置为0,开启cache功能。
  • 当CP4.PCIDE=1时, 0~11bit 代表PCIDE(processcontext identifier). PCIDE特性允许处理器可以在TLB缓存多个进程的 转换表,每个进程分配一个单独的PCIDE,用于 区分每个进程自己独有的表项。这样好处就是可以避免在进程来回切换过程中刷新TLB的开销。

The Process Context Identifier (PCID) feature allows a logical processor to cache TLB mappings concurrently for multiple virtual address spaces. When enabled (by setting CR4.PCIDE=1), the processor associates the current 12-bit PCID with each TLB mapping it creates. Only entries matching the current PCID are used when performing address translations. In this way, the processor may retain cached TLB mappings for multiple contexts.
  The current PCID is the value in CR3[11:0]. When PCIDs are enabled the system software can store 12-bit Process Context Identifiers in CR3 for different address spaces. Subsequently, when system software switches address spaces (by writing the page table base pointer in CR3[62:12]), the processor may use TLB mappings previously stored for that address space and PCID. A MOV to CR4 that clears CR4.PCIDE causes all cached entries in the TLB for the logical processor to be invalidated. When PCIDs are not enabled (CR4.PCIDE=0) the current PCID is always zero and all TLB mappings are associated with PCID=0. 

CR3切换

内核在每次上下文切换过程中都需要相应进程的转发表,即将mm->pgd设置到CR3中,以便后续地址转换过程中查找自己进程的地址转换关系,主要过程如下:

---->__schedule()---->context_switch()---->switch_mm_irqs_off()---->load_new_mm_cr3()---->build_cr3/build_cr3_noflush---->write_cr3()
  • __schedule():为内核进程调度函数,当进程得到调度之前 调用context_switch进行上下文切换
  • context_switch:开始准备切换相应进程中的mm,并准备更新相关寄存器
  • load_new_mm_cr3:根据获取的进程asid以及mm->pgd 更新cr3寄存器

build_cr3

根据mm->pgd和asid 构建设置cr3寄存器的值:


static inline unsigned long build_cr3(pgd_t *pgd, u16 asid)
{if (static_cpu_has(X86_FEATURE_PCID)) {return __sme_pa(pgd) | kern_pcid(asid);} else {VM_WARN_ON_ONCE(asid != 0);return __sme_pa(pgd);}
}

如果开启了pcid,则将(asid + 1)作为pcid,主要是考虑到asid为0情况,故+1防止设置pcid为0,以及pgd地址组成cr3寄存器的值。

write_cr3

设置cr3寄存器的值:

static inline void write_cr3(unsigned long x)
{PVOP_VCALL1(mmu.write_cr3, x);
}

mmu.write_cr3为函数指针,根据架构不同调用不同,其中常用调用为native_write_cr3函数:

static inline void native_write_cr3(unsigned long val)
{asm volatile("mov %0,%%cr3": : "r" (val), "m" (__force_order));
}

各级table entry

每个级别table 设计大体相同,但是也略有差异

PML4E(PML4 entry)

PML4表每个entry 如下:

  •  No Execute(NX)Bit: 占用63位,该标志位只有将EFER.NXE设置为1开启no-execute page-protection功能时,NX位才会被设置位1。如果EFER.NXE设置位0,则NX位被当作保留位使用。如果该标志位被置位,代表对应的物理页中的内容不可执行,如果从里面当作当作指令存储,取指令则将会报page fault异常。
  • Available(52~62位):保留暂未使用
  • Page-Driectory-Pointer Base Address(12~52):指向下一级PDPE表基地址,该地址位物理地址。
  •  AVL(9~11 bit): 硬件解析,由软件根据需要定义使用,定义宏为 _PAGE_BIT_SOFTW1, _PAGE_BIT_SOFTW2, _PAGE_BIT_SOFTW3
  • MBZ(7~8bit):保留不使用
  • IGN(6 bit): 忽略不使用
  • A(5 bit):Accessed 标记位,表明该entry是否被访问国 如果置1表明被访问过。该标记位不能被硬件cpu设清0,只能由软件清零,可以根据自标记位用于LRU算法,即查看一段时间是否被访问过。定义宏为_PAGE_BIT_ACCESSED
  • PCD(4 bit):page-level cache disable 表明该page table是否会缓存。如果清0 代表需要做缓存cace.如果置1,代表不需要cache,定义宏为_PAGE_BIT_PCD
  • PWT(3 bit):page-level writethrough 是否支持wrte back或write through连续写入缓存策略。当被设置位0,cache策略使用write back策略。当被设置位1,则支持write through缓存策略,可以连续写入。定义宏为_PAGE_BIT_PWT
  • U/S (2bit): user/supervisor表明 table中对 物理页的权限。如果被设置位1,表明user 和supervisoer都被允许访问到,如果设置0 则只允许supervisoer访问。定义宏 _PAGE_BIT_USER。
  • R/W (1 bit): 可读写权限 。设置为1 表明即可以读又可以写,设置为0.只可读。定义宏位_PAGE_BIT_RW
  • P (0 bit):present 该标记位表明page translationtable 地址转换对应的物理页是否在物理内存中。如果为0表明不存在,说明对应的物理内存页中的内容被置换到硬盘中。为1 表明对应物理页存在物理内存中。注意当该页被置换到硬盘swap中,该标记位将由内核将其清0。当物理内存页被加载到内存中,将被置1.
  • PML4对应的是kernel中的pgd表。

PDPE(PDP entry)

该标记位大部分都相同,不在记录重复部分

  • Page-Driectory-Pointer Base Address(12~52):记录下一级PDE表项物理地址。
  • 第7 bit 位Page Size(PS)bit:意思是是否是一个huge page。如果设置为0 表明当前不是一个huge page,如果是1 则表明是一个huge page,而该表项为最低一级表项。上述entry为page size 为4K 正常页映射,故PS为0。定义宏为_PAGE_BIT_PSE。
  • 其他标记位意思与PML4E相同。
  • PDP 对应kernel 中的pud表

PDE(PD entry)

  • 该标记位与PDPE中表述大部分都相同,唯一意思不一样的就是Page-Table Base Address指向的是PTE。
  • PD 对应kernel的pmd表

PTE(PT entry)

  • MPK(59~62 bit): memory protection key特性,可以《linux内核那些事之Memory protection keys(硬件原理)》进行了解,用于更加方便控制物理内存属性,而不刷新PTE,因此每次因为更新权限而刷新PTE,代价十分巨大。
  • G(8 bit): Global page 如果设置位1 表明该映射是一个全局页。全局页可以避免因进程切换带来刷新TLB开销。定义宏为 _PAGE_BIT_GLOBAL。
  • 与其他高级别的表不一样的是 12~51 在PTE中存放的是虚拟地址对应的最终的实际物理页地址即物理页帧号pfn。
  • 其他标记为意思相同。
  • PTE 对应kernel中的 PTE表

各个标记位定义

上述各个entry的标记位定义 对应的在(/arch/x86/include/asm/pgtable_types.h)文件,如下:

#define _PAGE_BIT_PRESENT    0   /* is present */
#define _PAGE_BIT_RW        1   /* writeable */
#define _PAGE_BIT_USER      2   /* userspace addressable */
#define _PAGE_BIT_PWT       3   /* page write through */
#define _PAGE_BIT_PCD       4   /* page cache disabled */
#define _PAGE_BIT_ACCESSED  5   /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY     6   /* was written to (raised by CPU) */
#define _PAGE_BIT_PSE       7   /* 4 MB (or 2MB) page */
#define _PAGE_BIT_PAT       7   /* on 4KB pages */
#define _PAGE_BIT_GLOBAL    8   /* Global TLB entry PPro+ */
#define _PAGE_BIT_SOFTW1    9   /* available for programmer */
#define _PAGE_BIT_SOFTW2    10  /* " */
#define _PAGE_BIT_SOFTW3    11  /* " */
#define _PAGE_BIT_PAT_LARGE 12  /* On 2MB or 1GB pages */
#define _PAGE_BIT_SOFTW4    58  /* available for programmer */
#define _PAGE_BIT_PKEY_BIT0 59  /* Protection Keys, bit 1/4 */
#define _PAGE_BIT_PKEY_BIT1 60  /* Protection Keys, bit 2/4 */
#define _PAGE_BIT_PKEY_BIT2 61  /* Protection Keys, bit 3/4 */
#define _PAGE_BIT_PKEY_BIT3 62  /* Protection Keys, bit 4/4 */
#define _PAGE_BIT_NX        63  /* No execute: only valid after cpuid check */#define _PAGE_BIT_SPECIAL _PAGE_BIT_SOFTW1
#define _PAGE_BIT_CPA_TEST  _PAGE_BIT_SOFTW1
#define _PAGE_BIT_UFFD_WP   _PAGE_BIT_SOFTW2 /* userfaultfd wrprotected */
#define _PAGE_BIT_SOFT_DIRTY    _PAGE_BIT_SOFTW3 /* software dirty tracking */
#define _PAGE_BIT_DEVMAP    _PAGE_BIT_SOFTW4/* If _PAGE_BIT_PRESENT is clear, we use these: */
/* - if the user mapped it with PROT_NONE; pte_present gives true */
#define _PAGE_BIT_PROTNONE  _PAGE_BIT_GLOBAL#define _PAGE_PRESENT   (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)
#define _PAGE_RW    (_AT(pteval_t, 1) << _PAGE_BIT_RW)
#define _PAGE_USER  (_AT(pteval_t, 1) << _PAGE_BIT_USER)
#define _PAGE_PWT   (_AT(pteval_t, 1) << _PAGE_BIT_PWT)
#define _PAGE_PCD   (_AT(pteval_t, 1) << _PAGE_BIT_PCD)
#define _PAGE_ACCESSED  (_AT(pteval_t, 1) << _PAGE_BIT_ACCESSED)
#define _PAGE_DIRTY (_AT(pteval_t, 1) << _PAGE_BIT_DIRTY)
#define _PAGE_PSE   (_AT(pteval_t, 1) << _PAGE_BIT_PSE)
#define _PAGE_GLOBAL    (_AT(pteval_t, 1) << _PAGE_BIT_GLOBAL)
#define _PAGE_SOFTW1    (_AT(pteval_t, 1) << _PAGE_BIT_SOFTW1)
#define _PAGE_SOFTW2    (_AT(pteval_t, 1) << _PAGE_BIT_SOFTW2)
#define _PAGE_SOFTW3    (_AT(pteval_t, 1) << _PAGE_BIT_SOFTW3)
#define _PAGE_PAT   (_AT(pteval_t, 1) << _PAGE_BIT_PAT)
#define _PAGE_PAT_LARGE (_AT(pteval_t, 1) << _PAGE_BIT_PAT_LARGE)
#define _PAGE_SPECIAL   (_AT(pteval_t, 1) << _PAGE_BIT_SPECIAL)
#define _PAGE_CPA_TEST  (_AT(pteval_t, 1) << _PAGE_BIT_CPA_TEST)
#ifdef CONFIG_X86_INTEL_MEMORY_PROTECTION_KEYS
#define _PAGE_PKEY_BIT0 (_AT(pteval_t, 1) << _PAGE_BIT_PKEY_BIT0)
#define _PAGE_PKEY_BIT1 (_AT(pteval_t, 1) << _PAGE_BIT_PKEY_BIT1)
#define _PAGE_PKEY_BIT2 (_AT(pteval_t, 1) << _PAGE_BIT_PKEY_BIT2)
#define _PAGE_PKEY_BIT3 (_AT(pteval_t, 1) << _PAGE_BIT_PKEY_BIT3)
#else
#define _PAGE_PKEY_BIT0 (_AT(pteval_t, 0))
#define _PAGE_PKEY_BIT1 (_AT(pteval_t, 0))
#define _PAGE_PKEY_BIT2 (_AT(pteval_t, 0))
#define _PAGE_PKEY_BIT3 (_AT(pteval_t, 0))
#endif#define _PAGE_PKEY_MASK (_PAGE_PKEY_BIT0 | \_PAGE_PKEY_BIT1 | \_PAGE_PKEY_BIT2 | \_PAGE_PKEY_BIT3)#if defined(CONFIG_X86_64) || defined(CONFIG_X86_PAE)
#define _PAGE_KNL_ERRATUM_MASK (_PAGE_DIRTY | _PAGE_ACCESSED)
#else
#define _PAGE_KNL_ERRATUM_MASK 0
#endif#ifdef CONFIG_MEM_SOFT_DIRTY
#define _PAGE_SOFT_DIRTY    (_AT(pteval_t, 1) << _PAGE_BIT_SOFT_DIRTY)
#else
#define _PAGE_SOFT_DIRTY    (_AT(pteval_t, 0))
#endif

Global Pages

 进程切换中不管是显示或者隐式加载CR3,都会使TLB无效,这样当正在进行地址转换过程中将TLB设置为无效会造成忙等待状态,一直等到该进行地址转换重新刷新到cache中。为了减少这样频繁切换导致的性能问题,将一些特殊的地址转换在PTE中标记为gloabl,可以避免此种场景。当进程切换时,并不会将gloal的地址转换设置成无效状态。

The processor invalidates the TLB whenever CR3 is loaded either explicitly or implicitly. After the TLB is invalidated, subsequent address references can consume many clock cycles until their translations are cached as new entries in the TLB. Invalidation of TLB entries for frequently-used or critical pages can be avoided by specifying the translations for those pages as global. TLB entries for global pages are not invalidated as a result of a CR3 load. Global pages are invalidated using the INVLPG instruction.

既然在进程切换过程中CR3切换,不能使global pages无效,如果要使所有global pages无效,即刷新所有global pages,可以通过先关闭CR4.PGE 即将CR4.PGE设置为0, 然后将CR4.PGE开启,方法刷新所有global pages,具体可以参考native_flush_tlb_global()函数


/** Flush everything*/
STATIC_NOPV void native_flush_tlb_global(void)
{unsigned long cr4, flags;if (static_cpu_has(X86_FEATURE_INVPCID)) {/** Using INVPCID is considerably faster than a pair of writes* to CR4 sandwiched inside an IRQ flag save/restore.** Note, this works with CR4.PCIDE=0 or 1.*/invpcid_flush_all();return;}/** Read-modify-write to CR4 - protect it from preemption and* from interrupts. (Use the raw variant because this code can* be called from deep inside debugging code.)*/raw_local_irq_save(flags);cr4 = this_cpu_read(cpu_tlbstate.cr4);/* toggle PGE */native_write_cr4(cr4 ^ X86_CR4_PGE);/* write old PGE again and flush TLBs */native_write_cr4(cr4);raw_local_irq_restore(flags);
}

如果要修改具有一个global page无效,即将global page去掉,需要通过INVLPG指令修改,具体实现如下:

/** Flush one page in the user mapping*/
STATIC_NOPV void native_flush_tlb_one_user(unsigned long addr)
{u32 loaded_mm_asid = this_cpu_read(cpu_tlbstate.loaded_mm_asid);asm volatile("invlpg (%0)" ::"r" (addr) : "memory");if (!static_cpu_has(X86_FEATURE_PTI))return;/** Some platforms #GP if we call invpcid(type=1/2) before CR4.PCIDE=1.* Just use invalidate_user_asid() in case we are called early.*/if (!this_cpu_has(X86_FEATURE_INVPCID_SINGLE))invalidate_user_asid(loaded_mm_asid);elseinvpcid_flush_one(user_pcid(loaded_mm_asid), addr);
}

访问物理内存权限

page translation转换过程中,每个级别表都有设置读写权限,最终的读写权限需要多个表结合组成 真正的读写权限。最终读写权限还需要和CR0.WP标记为相结合。如果CR0.WP
设置为为0,则表明superviosr权限的程序可以修改只读页面。如果设置为1,则表明supervisor不可以对只读页面进行写操作。

当CR0.WP设置为0,权限组合如下:

  • 当任何一个表entry,设置为supervisor,那么该页最终只有supervisor权限,用户user 没有操作权限。
  • 只有所有表entry,设置为user,user用户才拥有权限。 
  • 当page table中任何一个表entry 设置为supervisor权限,则R/W将不其作用,对物理页都读写权限。因为 CR0.WP设置为0,supervisor对页面直接拥有读写权限
  • 当page table都为user 权限时,如果任何一个表entry 只设了R 权限即只读权限,那么用户user 将只拥有读权限(但是注意此时supervisor超级用户拥有读写权限)
  • 当page table都为user 权限时,只有所有表entry 都设置为R 写权限时,user才拥有写权限。

当CR0.WP设置为1,权限组合如下

  •  当所有表都有写权限时,才拥有写权限
  • 当任何一个表设置为只读权限时,那么只有只读权限
  • 唯一差别就是,supervisor不能对只读页面 进行写操作,因为开启了写保护功能。

参考资料

《AMD64 Architecture Programmer's manual volums》

linux那些事之 page translation(硬件篇)相关推荐

  1. linux那些事之page fault(AMD64架构)(user space)(2)

    do_user_addr_fault 用户空间地址处理是page fault主要处理流程,x86 64位系统主要是do_user_addr_fault()函数 该处理部分是x86架构特有部分 即与架构 ...

  2. linux那些事之page table

    内核中page table决定了一个虚拟地址如何查找到对应的物理地址关键,维护着整个整个虚拟地址与物理地址对应信息,是实现整个内存虚拟化的基础.在虚拟地址到物理地址的转换过程中为了减少整个物理空间的占 ...

  3. linux那些事之page cache

    page cache page cache又称高速缓存,主要是针对文件文件系统,为了减少不必要的磁盘IO操作(读/写)造成卡顿问题,内核将磁盘文件中的内容缓存到内存中,并选择适当时机对磁盘进行读写操作 ...

  4. 基于Linux+ARM的远程视频监控--硬件篇

    硬件资源 本视频监控系统主要采用的硬件是GEC210为主板的开发板和USB数码高清摄像头,开发板将从USB摄像头收集到的视频数据发送到网络服务器,然后电脑和手机客户端通过网络服务器将视频数据接收并显示 ...

  5. linux那些事之 page table基本操作

    通过遍历page table查找一个虚拟地址已经做了对应的物理内存映射是一个很常用的操作,因此整个walk遍历过程耗时要尽量小. show_pte() 以show_pte()函数为例说明如何根据一个虚 ...

  6. linux那些事之page fault(AMD64架构)(1)

    应用程序或者内核都是运行在虚拟内存空间之中,kernel 启动完成之后如果一个虚拟地址要访问物理内存需要通过CPU MMU硬件进行地址转换,整个虚拟地址访问物理内存逻辑过程如下: kernel 启动完 ...

  7. linux那些事之TLB(Translation-Lookaside Buffer)无效操作

    TLB 为了加速虚拟地址转换物理地址过程,CPU内部一般都集成TLB硬件单元,通过缓存存取虚拟地址与物理地址映射关系,避免再通过MMU 通过多级查表引入多次内存开销,直接将映射关系存储到硬件单元中,本 ...

  8. 明明白白你的Linux服务器——硬件篇

    原文地址:http://os.51cto.com/art/201006/208330.htm 一.如何查看服务器的CPU 今天安装了9台Linux服务器,型号完全不一样(有DELL.HP和IBM服务器 ...

  9. linux usb3.0改2.0,TX1入门教程硬件篇-切换USB2.0与USB3.0

    TX1入门教程硬件篇-切换USB2.0与USB3.0 说明: 介绍如何切换TX1USB口的为2.0或3.0版本 步骤: 编辑extlinux.conf文件,修改usb_port_owner_info= ...

最新文章

  1. android androidx版本,Android AndroidX 简介与迁移
  2. Mysql安装两种方法
  3. 在linux系统中查看组管理信息命令,Linux用户和组管理常用命令
  4. ES6之Module的语法(2)
  5. 答应我,安装chromedriver,按照版本号,v70就安装v2.42,
  6. Data Lake Analytics的Geospatial分析函数 1
  7. springboot的三种启动方式
  8. 主线剧情02-ARM-Linux基础学习记录
  9. 混淆矩阵(Confusion Matrix)
  10. 一卡通管理系统需求分析
  11. 单片机之步进电机驱动篇(一)
  12. MongoDB查询命令详解
  13. XSSFWorkbook 设置单元格样式_CVA高校精英计划第二弹:执行最佳操作,做好设置准备...
  14. 自动化测试金字塔与反模式
  15. 2018-12-24:企业微信分享功能
  16. Windows服务应用程序
  17. FBI针对HTTPS网络钓鱼发布警告
  18. Mux VLAN 原理
  19. Android应用开发高效工具集1---ant构建简单Android项目
  20. 修改unity代码编辑器

热门文章

  1. hadoop--日志聚集功能的配置
  2. postgres+socket.io+nodejs实时地图应用实践
  3. 开发环境中Docker的使用
  4. JavaEE互联网轻量级框架整合开发(书籍)阅读笔记(6):Spring IOC容器学习(概念、作用、Bean生命周期)...
  5. 创建定制的ASP.NET AJAX非可视化客户端组件
  6. Android NDK调试出错Unknown Application ABI, Unable to detect application ABI#39;s的解决方式...
  7. docker 容器中不支持中文的解决方法
  8. 使用Listener准备application作用域数据
  9. Atitit.遍历图像像素点rgb java attilax总结
  10. 文件的复制、移动、压缩等对SELinux属性关系详解