内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的8086cpu的分段式管理,到后来的80x86 系列的32位cpu推出的保护模式和段页式管理。在应用程序中我们无时不刻不在和内存打交道,我们总在不经意间的进行堆内存和栈内存的分配释放,所以内存是我们进行程序设计必不可少的部分。

CPU的内存管理方式

段寄存器怎么消失了?

在学习8086汇编语言时经常与寄存器打交道,其中8086CPU采用的内存管理方式为分段管理的方式,寻址时采用:短地址 * 16 + 偏移地址的方式,其中有几大段寄存器比如:CS、DS、SS、ES等等,每个段的偏移地址最大为64K,这样总共能寻址到2M的内存。但是到32位CPU之后偏移地址变成了32位这样每个段就可以有4GB的内存空间,这个空间已经足够大了,这个时候在编写相应的汇编程序时我们发现没有段寄存器的身影了,是不是在32位中已经没有段寄存器了呢,答案是否定了,32位CPU中不仅有段寄存器而且它们的作用比以前更大了。 在32位CPU中段寄存器不再作为段首地址,而是作为段选择子,CPU为了管理内存,将某些连续的地址内存作为一页,利用一个数据结构来说明这页的属性,比如是否可读写,大小,起始地址等等,这个数据结构叫做段描述符,而多个段描述符则组成了一个段描述符表,而段寄存器如今是用来找到对应的段描述符的,叫做段选择子。段寄存器仍然是16位其中高13位表示段描述符表的索引,第二位是区分LDT(局部描述符表)和GDT(全局描述符表),全局描述符表是系统级的而LDT是每个进程所独有的,如果第二位表示的是LDT,那么首先要从GDT中查询到LDT所在位置,然后才根据索引找到对应的内存地址,所以现在寻址采用的是通过段选择子查表的方式得到一个32位的内存地址。由于这些表都是由系统维护,并且不允许用户访问及修改所以在普通应用程序中没有必要也不能使用段寄存器。通过上面的说明,我们可以推导出来32位机器最多可以支持2^(13 + 1 + 32) = 64T内存。

段页式管理

通过查表方式得到的32位内存地址是否就是真实的物理内存的地址呢,这个也是不一定的,这个还要看系统是否开启了段页式管理。如果没有则这个就是真实的物理地址,如果开启了段页式管理那么这个只是一个线性地址,还需要通过页表来寻址到真实的物理内存。 32位CPU专门新赠了一个CR3寄存器用来完成分页式管理,通过CR3寄存器可以寻址到页目录表,然后再将32位线性地址的高10位作为页目录表的索引,通过这个索引可以找到相应的页表,再将中间10为作为页表的索引,通过这个索引可以寻址到对应物理内存的起始地址,最后通过这个其实地址和最后低12位的偏移地址找到对应真实内存。下面是这个过程的一个示例图:

为什么要使用分页式管理,直接让那个32位线性地址对应真实的内存不可以吗。当然可以,但是分页式管理也有它自身的优点:

1. 可以实现页面的保护:系统通过设置相关属性信息来指定特权级别和其他状态

2. 可以实现物理内存的共享:从上面的图中可以看出,不同的线性地址是可以映射到相同的物理内存上的,只需要更改页表中对应的物理地址就可以实现不同的线性地址对应相同的物理内存实现内存共享。

3. 可以方便的实现虚拟内存的支持:在系统中有一个pagefile.sys的交互页面文件,这个是系统用来进行内存页面与磁盘进行交互,以应对内存不够的情况。系统为每个内存页维护了一个值,这个值表示该页面多久未被访问,当页面被访问这个值被清零,否则每过一段时间会累加一次。当这个值到达某个阈值时,系统将页面中的内容放入磁盘中,将这块内存空余出来以便保存其他数据,同时将之前的线性地址做一个标记,表名这个线性地址没有对应到具体的内存中,当程序需要再次访问这个线性地址所对应的内存时系统会再次将磁盘中的数据写入到内存中。虽说这样做相当于扩大了物理内存,但是磁盘相对于内存来说是一个慢速设备,在内存和磁盘间进行数据交换总是会耗费大量的时间,这样会拖慢程序运行,而采用SSD硬盘会显著提高系统运行效率,就在于SSD提高了与内存进行数据交换的效率。如果想显著提高效率,最好的办法是加内存毕竟在内存和硬盘间倒换数据是要话费时间的。

保护模式

在以前的16位CPU中采用的多是实模式,程序中使用的地址都是真实的物理地址,这样如果内存分配不合理,会造成一个程序将另外一个程序所在的内存覆盖这样对另外一个程序将造成严重影响,但是在32位保护模式下,不再会产生这种问题,保护模式将每个进程的地址空间隔离开来,还记得上面的LDT吗,在不同的程序中即使采用的是相同的地址,也会被LDT映射到不同的线性地址上。

保护模式主要体现在这样几个方面:

1.同一进程中,使用4个不同访问级别的内存段,对每个页面的访问属性做了相应的规定,防止错误访问的情况,同时为提供了4中不同代码特权,0特权的代码可以访问任意级别的内存,1特权能任意访问1…3级内存,但不能访问0级内存,依次类推。通常这些特权级别叫做ring0-ring3。

2. 对于不同的进程,将他们所用到的内存等资源隔离开来,一个进程的执行不会影响到另一个进程。

windows系统的内存管理

windows内存管理器

我们将系统中实际映射到具体的实际内存上的页面称为工作集。当进程想访问多余实际物理内存的内存时,系统会启用虚拟内存管理机制(工作集管理),将那些长时间未访问的物理页面复制到硬盘缓冲文件上,并释放这些物理页面,映射到虚拟空间的其它页面上;

系统的内存管理器主要由下面的几个部分组成:

1. 工作集管理器(优先级16):这个主要负责记录每个页面的年龄,也就有多久未被访问,当页面被访问这个年龄被清零,否则每过一段时间就进行累加1的操作。

2. 进程/栈交换器(优先级23):主要用于在进行进程或者线程切换时保存寄存器中的相关数据用以保存相关环境。

3. 已修改页面写出器(优先级17):当内存映射的内容发生改变时将这个改变及时的写入到硬盘中,防止由于程序意外终止而造成数据丢失

4. 映射页面写出器(优先级17):当页面的年龄达到一定的阈值时,将页面内容写入到硬盘中

5. 解引用段线程(优先级18):释放以写入到硬盘中的空闲页面

6. 零页面线程(优先级0):将空闲页面清零,以便程序下次使用,这个线程保证了新提交的页面都是干净的零页面

进程虚拟地址空间的布局

windows为每个进程提供了平坦的4GB的线性地址空间,这个地址空间被分为用户分区和内核分区,他们各占2GB大小,其中内核分区在高地址位,用户分区在低地址位,下面是内存分布的一个表格:

分区 地址范围
NULL指针区 0x00000000-0x0000FFFF
用户分区 0x00010000-0x7FFEFFFF
64K禁入区 0x7FFF0000-0x7FFFFFFF
内核分区 0x80000000-0xFFFFFFFF

从上面的图中可以看出,系统的内核分区是2GB而用户可用的分区并没有2GB,在用户分区的头64K和尾部的64K不允许用户使用。 另外我们可以压缩内核分区的大小,以便使用户分区占更多的内存,这就是/3GB方式,下面是这种方式的具体内存分布:

分区 地址范围
NULL指针区 0x00000000-0x0000FFFF
用户分区 0x00010000-0xBFFEFFFF
64K禁入区 0xBFFF0000-0xBFFFFFFF
内核分区 0xC0000000-0xFFFFFFFF

windows虚拟内存管理函数

VirtualAlloc

VirtualAlloc函数主要用于提交或者保留一段虚拟地址空间,通过该函数提交的页面是经过0页面线程清理的干净的页面。

LPVOID VirtualAlloc(
LPVOID lpAddress, //虚拟内存的地址
DWORD dwSize, //虚拟内存大小
DWORD flAllocationType,//要对这块的虚拟内存做何种操作
DWORD flProtect //虚拟内存的保护属性
);

我们可以指定第一个参数来告知系统,我们希望操作哪块内存,如果这个地址对应的内存已经被保留了那么将向下偏移至64K的整数倍,如果这块内存已经被提交,那么地址将向下偏移至4K的整数倍,也就是说保留页面的最小粒度是64K,而提交的最小粒度是一页4K。 第三个参数是指定分配的类型,主要有以下几个值

含义
MEM_COMMIT 提交,也就是说将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用
MEM_RESERVE 保留,告知系统以这个地址开始到后面的dwSize大小的连续的虚拟内存程序要使用,进程其他分配内存的操作不得使用这段内存。
MEM_TOP_DOWN 从高端地址保留空间(默认是从低端向高端搜索)
MEM_LARGE_PAGES 开启大页面的支持,默认一个页面是4K而大页面是2M(这个视具体系统而定)
MEM_WRITE_WATCH 开启页面写入监视,利用GetWriteWatch可以得到写入页面的统计情况,利用ResetWriteWatch可以重置起始计数
MEM_PHYSICAL 用于开启PAE

第四个参数主要是页面的保护属性,参数可取值如下:

含义
PAGE_READONLY 只读
PAGE_READWRITE 可读写
PAGE_EXECUTE 可执行
PAGE_EXECUTE_READ 可读可执行
PAGE_EXECUTE_READWRITE 可读可写可执行
PAGE_NOACCESS 不可访问
PAGE_GUARD 将该页设置为保护页,如果试图对该页面进行读写操作,会产生一个STATUS_GUARD_PAGE 异常

下面是该函数使用的几个例子:

1. 页面的提交/保留与释放

//保留并提交
LPVOID pMem = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
srand((unsigned int)time(NULL)); float* pfMem = (float*)pMem;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
pfMem[i] = rand();
} //释放
VirtualFree(pMem, 4 * 4096, MEM_RELEASE); //先保留再提交
LPBYTE pByte = (LPBYTE)VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE, PAGE_READWRITE);
VirtualAlloc(pByte + 4 * 4096, 4096, MEM_COMMIT, PAGE_READWRITE);
pfMem = (float*)(pByte + 4 * 4096);
for (int i = 0; i < 4096/sizeof(float); i++)
{
pfMem[i] = rand();
} //释放
VirtualFree(pByte + 4 * 4096, 4096, MEM_DECOMMIT);
VirtualFree(pByte, 1024 * 1024, MEM_RELEASE);
  1. 大页面支持
//获得大页面的尺寸
DWORD dwLargePageSize = GetLargePageMinimum();
LPVOID pBuffer = VirtualAlloc(NULL, 64 * dwLargePageSize, MEM_RESERVE, PAGE_READWRITE);
//提交大页面
VirtualAlloc(pBuffer, 4 * dwLargePageSize, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
VirtualFree(pBuffer, 4 * dwLargePageSize, MEM_DECOMMIT);
VirtualFree(pBuffer, 64 * dwLargePageSize, MEM_RELEASE);

VirtualProtect

VirtualProtect用来设置页面的保护属性,函数原型如下:

BOOL VirtualProtect(
LPVOID lpAddress, //虚拟内存地址
DWORD dwSize, //大小
DWORD flNewProtect, //保护属性
PDWORD lpflOldProtect //返回原来的保护属性
);

这个保护属性与之前介绍的VirtualAlloc中的保护属性相同,另外需要注意的一点是一般返回原来的属性的话,这个指针可以为NULL,但是这个函数不同,如果第四个参数为NULL,那么函数调用将会失败

LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
pfArray[i] = 1.0f * rand();
} //将页面改为只读属性
DWORD dwOldProtect = 0;
VirtualProtect(pBuffer, 4 * 4096, PAGE_READONLY, &dwOldProtect);
//写入数据将发生异常
pfArray[9] = 0.1f;
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualQuery

这个函数用来查询某段虚拟内存的属性信息,这个函数原型如下:

DWORD VirtualQuery(
LPCVOID lpAddress,//地址
PMEMORY_BASIC_INFORMATION lpBuffer, //用于接收返回信息的指针
DWORD dwLength //缓冲区大小,上述结构的大小
);

结构MEMORY_BASIC_INFORMATION的定义如下:

typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; //该页面的起始地址
PVOID AllocationBase;//分配给该页面的首地址
DWORD AllocationProtect;//页面的保护属性
DWORD RegionSize; //页面大小
DWORD State;//页面状态
DWORD Protect;//页面的保护类型 DWORD Type;//页面类型
} MEMORY_BASIC_INFORMATION;
typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION;

AllocationProtect与Protect所能取的值与之前的保护属性的值相同。 State的取值如下: MEM_FREE:空闲 MEM_RESERVE:保留 MEM_COMMIT:已提交 Type的取值如下: MEM_IMAGE:映射类型,一般是映射到地址控件的可执行模块如DLL,EXE等 MEM_MAPPED:文件映射类型 MEM_PRIVATE:私有类型,这个页面的数据为本进程私有数据,不能与其他进程共享 下面是这个的使用例子:

#include<windows.h>
#include <stdio.h>
#include <tchar.h>
#include <atlstr.h> CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi);
int _tmain(int argc, TCHAR *argv[])
{
SYSTEM_INFO sm = {0};
GetSystemInfo(&sm);
LPVOID dwMinAddress = sm.lpMinimumApplicationAddress;
LPVOID dwMaxAddress = sm.lpMaximumApplicationAddress; MEMORY_BASIC_INFORMATION mbi = {0};
_putts(_T("BaseAddress\tAllocationBase\tAllocationProtect\tRegionSize\tState\tProtect\tType\n")); for (LPVOID pAddress = dwMinAddress; pAddress <= dwMaxAddress;)
{
if (VirtualQuery(pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == 0)
{
break;
} _putts(GetMemoryInfo(&mbi));
//一般通过BaseAddress(页面基地址) + RegionSize(页面长度)来寻址到下一个页面的的位置
pAddress = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
} } CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi)
{
CString lpMemoryInfo = _T(""); int iBaseAddress = (int)(pmi->BaseAddress);
int iAllocationBase = (int)(pmi->AllocationBase); CString szProtected = _T("\0");
if (pmi->Protect & PAGE_READONLY)
{
szProtected = _T("R");
}else if (pmi->Protect & PAGE_READWRITE)
{
szProtected = _T("RW");
}else if (pmi->Protect & PAGE_WRITECOPY)
{
szProtected = _T("WC");
}else if (pmi->Protect & PAGE_EXECUTE)
{
szProtected = _T("X");
}else if (pmi->Protect & PAGE_EXECUTE_READ)
{
szProtected = _T("RX");
}else if (pmi->Protect & PAGE_EXECUTE_READWRITE)
{
szProtected = _T("RWX");
}else if (pmi->Protect & PAGE_EXECUTE_WRITECOPY)
{
szProtected = _T("WCX");
}else if (pmi->Protect & PAGE_GUARD)
{
szProtected = _T("GUARD");
}else if (pmi->Protect & PAGE_NOACCESS)
{
szProtected = _T("NOACCESS");
}else if (pmi->Protect & PAGE_NOCACHE)
{
szProtected = _T("NOCACHE");
}else
{
szProtected = _T(" ");
} CString szAllocationProtect = _T("\0");
if (pmi->AllocationProtect & PAGE_READONLY)
{
szProtected = _T("R");
}else if (pmi->AllocationProtect & PAGE_READWRITE)
{
szProtected = _T("RW");
}else if (pmi->AllocationProtect & PAGE_WRITECOPY)
{
szProtected = _T("WC");
}else if (pmi->AllocationProtect & PAGE_EXECUTE)
{
szProtected = _T("X");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_READ)
{
szProtected = _T("RX");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_READWRITE)
{
szProtected = _T("RWX");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_WRITECOPY)
{
szProtected = _T("WCX");
}else if (pmi->AllocationProtect & PAGE_GUARD)
{
szProtected = _T("GUARD");
}else if (pmi->AllocationProtect & PAGE_NOACCESS)
{
szProtected = _T("NOACCESS");
}else if (pmi->AllocationProtect & PAGE_NOCACHE)
{
szProtected = _T("NOCACHE");
}else
{
szProtected = _T(" ");
} DWORD dwRegionSize = pmi->RegionSize;
CString strState = _T("");
if (pmi->State & MEM_FREE)
{
strState = _T("Free");
}else if (pmi->State & MEM_RESERVE)
{
strState = _T("Reserve");
}else if (pmi->State & MEM_COMMIT)
{
strState = _T("Commit");
}else
{
strState = _T(" ");
} CString strType = _T("");
if (pmi->Type & MEM_IMAGE)
{
strType = _T("Image");
}else if (pmi->Type & MEM_MAPPED)
{
strType = _T("Mapped");
}else if (pmi->Type & MEM_PRIVATE)
{
strType = _T("Private");
} lpMemoryInfo.Format(_T("%08X %08X %s %d %s %s %s\n"), iBaseAddress, iAllocationBase, szAllocationProtect, dwRegionSize, strState, szProtected, strType);
return lpMemoryInfo;
}

VirtualLock和VirtualUnlock

这两个函数用于锁定和解锁页面,前面说过操作系统会将长时间不用的内存中的数据放入到系统的磁盘文件中,需要的时候再放回到内存中,这样来回倒腾,必定会造成程序效率的底下,为了避免这中效率底下的操作,可以使用VirtualLock将页面锁定在内存中,防止页面交换,但是不用了的时候需要使用VirtualUnlock来解锁,不然一直锁定而不解锁会造成真实内存的不足。 另外需要注意的是,不能一次操作超过工作集规定的最大虚拟内存,这样会造成程序崩溃,我们可以通过函数SetProcessWorkingSetSize来设置工作集规定的最大虚拟内存的大小。下面是一个使用例子:

SetProcessWorkingSetSize(GetCurrentProcess(), 1024 * 1024, 2 * 1024 * 1024);
LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE, PAGE_READWRITE);
//不能锁定超过进程工作集大小的虚拟内存
VirtualLock(pBuffer, 3 * 1024 * 1024);
//不能一次提交超过进程工作集大小的虚拟内存
VirtualAlloc(pBuffer, 3 * 1024 * 1024, MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4096 / sizeof(float); i++)
{
pfArray[i] = 1.0f * rand();
} VirtualUnlock(pBuffer, 4096);
VirtualFree(pBuffer, 4096, MEM_DECOMMIT);
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualFree

VirtualFree用于释放申请的虚拟内存。这个函数支持反提交和释放,这两个操作由第三个参数指定: MEM_DECOMMIT:反提交,这样这个线性地址就不再映射到具体的物理内存,但是这个地址仍然是保留地址。 MEM_RELEASE:释放,这个范围的地址不再作为保留地址

Window虚拟内存管理(转)的更多相关文章

  1. 十问 Linux 虚拟内存管理 (glibc) (二)

    版权声明:本文由陈福荣原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/184 来源:腾云阁 https://www.qclo ...

  2. SoC嵌入式软件架构设计II:没有MMU的CPU虚拟内存管理的设计和实现方法

    大多数的程序代码是必要的时,它可以被加载到内存中运行.手术后,可直接丢弃或覆盖其它代码. 我们PC然在同一时间大量的应用,地址空间差点儿能够整个线性地址空间(除了部分留给操作系统或者预留它用).能够觉 ...

  3. SoC嵌入式软件架构设计II:否MMU的CPU虚拟内存管理的设计与实现方法

    大多数的程序代码是必要的时,它可以被加载到内存中运行.手术后,可直接丢弃或覆盖其他代码.我们PC然在同一时间大量的应用,能够整个线性地址空间(除了部分留给操作系统或者预留它用),能够觉得每一个应用程序 ...

  4. windows虚拟内存管理

    内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的8086cpu的分段式管理,到后来的80x86 系列的32位cpu推出的保护模式和段页式管理.在应用程序中 ...

  5. OS之内存管理 --- 虚拟内存管理(二)

    关于虚拟内存管理之前的请看:OS之内存管理 - 虚拟内存管理(一) 帧分配 每个进程对的最小帧数是由操作系统的体系结构决定的,但是最大帧数是由可用物理内存的数量决定的.所以在这之间,对于进程的帧的分配 ...

  6. linux内核内存分配(三、虚拟内存管理)

    在分析虚拟内存管理前要先看下linux内核内存的具体分配我開始就是困在这个地方.对内核内存的分类不是非常清晰.我摘录当中的一段: 内核内存地址 ============================ ...

  7. Linux的虚拟内存管理-如何分配和释放内存,以提高服务器在高并发情况下的性能,从而降低了系统的负载

    Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和free是如何分配和释放内存?如何查看堆内内存的碎片情况?既然堆内内存brk和sbrk不能直接释放,为什么不全 ...

  8. Unix系统编程()虚拟内存管理

    在之前学到过进程的内存布局中忽略了一个事实:这一布局存在于虚拟文件中. 因为对虚拟内存的理解将有助于后续对fork系统调用.共享内存和映射文件之类的主题阐述,这里还要学习一下有关虚拟内存的详细内容. ...

  9. Linux虚拟内存管理(glibc)

    转:https://blog.csdn.net/tengxy_cloud/article/details/53067396 https://www.cnblogs.com/purpleraintear ...

随机推荐

  1. object references an unsaved transient instance - save the transient instance before flushing: com.jspxcms.core.domain.ScTeam

    object references an unsaved transient instance - save the transient instance before flushing: com.j ...

  2. Codeforces 1030E 【暴力构造】

    LINK 题目大意:给你n个数,你可以交换一个数的任意二进制位,问你可以选出多少区间经过操作后异或和是0 思路 充分必要条件: 区间中二进制1的个数是偶数 区间中二进制位最多的一个数的二进制个数小于等 ...

  3. python(一):python语言基础

    一.python语言基本的8个要素 Python语言的8个要素:数据类型.对象引用.组合数据类型.逻辑操作符.运算操作符.控制流语句.输入/输出.函数的创建与引用.除此之外还有一个非常重要且无处不在的 ...

  4. python笔记-20 django进阶 (model与form、modelform对比,三种ajax方式的对比,随机验证码,kindeditor)

    一.model深入 1.model的功能 1.1 创建数据库表 1.2 操作数据库表 1.3 数据库的增删改查操作 2.创建数据库表的单表操作 2.1 定义表对象 class xxx(models.M ...

  5. 字符编码:ASCII,Unicode和UTF-8

    字符编码是计算机技术的基石,想要熟练使用计算机,就必须懂得一点字符编码的知识. 1. ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串.每一个二进制位(bit)有0和1两 ...

  6. ofdm理解(转载)

    说明:以下文字,灰色为吹水文,黑色为正文,蓝色为采用实际应用中的参数所作的说明. 起因是这样的.时间回到07年底,4G方兴之时,同桌隔壁的隔壁"小白"同学说看不太明白OFDMA的原 ...

  7. vuejs2.0的生命周期解读

    每个 Vue 实例在被创建之前都要经过一系列的初始化过程.例如,实例需要配置数据观测(data observer).编译模版.挂载实例到 DOM ,然后在数据变化时更新 DOM .下图展示的就是一个v ...

  8. springboot注册bean失败

    启动的主类应该放在和其他包一样的目录,不能放在一个目录里面

  9. 微软Azure平台 cloud service动态申请证书并绑定证书碰到的坑

    我们有一个saas平台 部分在azure的cloud service 使用lets encrypt来申请证书.每一个商家申请域名之后就需要通过Lets encrypt来得到证书并绑定证书. 主要碰到的 ...

  10. golang的interface到其他类型的数据转换

    以string为例 package main import "fmt" func main() { var a interface{} var b string a = " ...