Linux内核之浅谈内存寻址

前言

最近在看内存寻址的内容,略有所得,发此文与大家一起交流。我们知道计算机是由硬件和软件组成,硬件主要包括运算器、控制器、存储器、输入设备和输出设备,软件主要是操作系统和用户应用软件,其中操作系统是联系硬件和软件的桥梁。本文主要分享运算器关于内存寻址的重点内容,从内存寻址的硬件机制重点和内核代码动手实践两部分来分享,欢迎交流,文中若有错误之处,还请指出。

目录

  • Linux内核之浅谈内存寻址
    • 前言
  • 一、内存寻址硬件机制
    • 1、内存寻址
    • 2、分段机制
      • (1)段选择符
      • (2)段描述符
    • 3、分页机制
      • Linux中的分页
    • 4、保护模式
      • (1)实模式
      • (2)保护模式
  • 二、内存寻址内核代码实践
    • 1、程序源码
      • 必要的头文件
      • 打印页机制中的一些重要参数
      • 线性地址转换为物理地址
      • 加载内核模块
      • 卸载内核模块
      • 入口、出口,许可证
    • 2、Makefile
    • 3、编译加载模块
      • (1)编译模块
      • (2)加载模块
      • (3)查看模块
    • 4、查看结果

一、内存寻址硬件机制

1、内存寻址

计算机在访问内存的时候,一般我们用眼睛能看到的地址都是虚拟地址,而内存条上每个内存单元的实际地址就是物理地址,那我们是如何访问到计算机内存条上的物理地址的呢?这就要用到地址转换,x86以上的CPU转换地址过程如下:

MMU是内存管理单元,它和CPU在一起,是专门来支持虚拟内存管理的,32位以上的处理器才会有,它的作用就是把虚拟地址转换为物理地址。CPU把程序编译链接后形成的虚拟地址送给MMU,MMU将此虚拟地址转换成物理地址送给存储器,操作系统配合MMU把虚拟地址转换为物理地址。此过程中可分为两个阶段,分别引入了分段机制分页机制,第一阶段是用分段机制把二维的虚拟地址转换为线性地址,第二阶段是用分页机制是把线性地址转换为物理地址,此处所说的线性地址是一段连续的,不分段的,范围为0-4GB的地址空间,MMU地址转换过程示意图如下图。

2、分段机制

分段机制就是为了把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址,其转换关系如下图所示的段描述符表(段表)来描述。

  • 段号:描述的是虚拟地址空间段的编号
  • 基地址:是线性地址空间段的起始地址
  • 界限:在虚拟地址空间中,段内可以使用的最大偏移量。
  • 属性:表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。

虚拟地址到线性地址的转换方法:线性地址=段的起始地址+偏移量

保护模式下的其他描述符表:

  • 全局描述符表GDT(Gloabal Descriptor Table)
  • 中断描述符表IDT(Interrupt Descriptor Table)
  • 局部描述符表LDT(Local Descriptor Table)

(1)段选择符

段选择符(段选择子)是段的一个十六位标志符,如下图所示。段选择符并不直接指向段,而是指向段表中定义段的段号。 段选择符包括 3 个字段的内容:

  • RPL表示请求者的特权级(Requestor Privilege Level)

    保护模式提供了四个特权级,用0~3四个数字表示 ,很多操作系统(如Linux,Windwos)只使用了其中的最低和最高两个,即0表示最高特权级,对应内核态;3表示最低特权级,对应用户态。保护模式规定,高特权级可以访问低特权级,而低特权级不能随便访问高特权级。

  • TI(Table Index)

    TI = 0 ,表示描述符在GDT中,TI = 1,表示描述符在LDT中

  • 索引值

    给出了描述符在GDT或LDT表中的索引项号。

(2)段描述符

每个段描述符长度是 8 字节,含有三个主要字段:段基地址、段限长和段属性,其结构如下图。

定义段描述符的相关代码见Linux内核源码

/arch/x86/include/asm/segment.h

这个头文件定义了一些访问CPU段寄存器或与段寄存器有关的内存操作函数,在Linux操作系统中,当用户通过系统调用开始执行内核代码时,内核程序会首先在段寄存器DS和ES中加载全局描述符表GDT中的内核数据段描述符。

/* Constructor for a conventional segment GDT (or LDT) entry */
/* This is a macro so it can be used in initializers */
#define GDT_ENTRY(flags, base, limit)           \((((base)  & _AC(0xff000000,ULL)) << (56-24)) | \(((flags) & _AC(0x0000f0ff,ULL)) << 40) |  \(((limit) & _AC(0x000f0000,ULL)) << (48-16)) | \(((base)  & _AC(0x00ffffff,ULL)) << 16) |  \(((limit) & _AC(0x0000ffff,ULL))))

3、分页机制

分页在分段之后进行,是继段机制把虚拟地址转换为线性地址后,进一步把该线性地址再转换为物理地址。

  • 是什么?实际上分页也就是就是将线性地址空间划分成若干大小相等的片,称为页。
  • 为什么?分页是为了让每个进程可以拥有自己独立的虚拟内存空间。
  • 怎么做?映射函数:Pa=f(va)
    • 时间的优化。因为访存很频繁,因此,映射函数f一定要简单,否则会效率很低,所以需要简单查表算法,这也就是页表引入的原因。
    • 空间的优化。因为内存空间是按字节编址的,地址一一进行映射的话,效率也很低,于是要按照一定的粒度(也就是页)进行映射,这样,粒度内的相对地址(也就是页内偏移量)在映射时保持不变。

线性地址到物理地址的转换过程描述如下:


一个线性地址由10位目录表+10位页表+12位偏移量组成,当给定一个线性地址时:

  • 第一步,用最高10位作为页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,获得相应目录项在内存的地址。
  • 第二步,从这个地址开始读取32位页目录项,取出其高20位,再给低12位补0,形成页表在内存的起始地址。
  • 第三步,用中间的10位作为页表中页表项的索引,将它乘以4,与页表的起始地址相加,获得相应页表项在内存的地址。
  • 第四步,从这个地址开始读取32位页表项,取出其高20位,再将线性地址的第11~0位放在低12位,形成最终32位页面物理地址。

Linux中的分页

Linux主要采用分页机制来实现虚拟存储器管理,这是因为以下两个原因:

  • Linux巧妙地绕过了段机制(线性地址=偏移量)
  • Linux设计目标之一就是具有可移植性,但很多CPU并不支持段。

    目前许多处理器都采用64位结构的,为了保持可移植性,Linux目前采用四级分页模式,为此,定义了四种类型的页表:

页全局目录PGD(Page Global Directory)
页上级目录PUD(Page Upper Directory)
页中间目录PMD(Page Middle Derectory)
页表PT(Page table)

页全局目录PGD包含若干页上级目录PUD的地址, 页上级目录PUD又依次包含若干页中间目录PMD的地址, 页中间目录又包含若干页表PT的地址, 每一个页表项指向一个页框。 因此线性地址因此被分成五个部分,而每一部分的大小与具体的计算机体系结构有关。

页表的相关代码见Linux内核源码

include/asm-generic/pgtable-nopud.h
include/asm-generic/pgtable-nopmd.h
arch/x86/include/asm/pgtable-2level*.h
arch/x86/include/asm/pgtable-3level*.h
arch/x86/include/asm/pgtable_64*.h
arch/x86/include/asm/pgtable_64_types.h


#ifndef __ASSEMBLY__
#include <linux/types.h>
/** These are used to make use of C type-checking..*/
typedef unsigned long   pteval_t;
typedef unsigned long   pmdval_t;
typedef unsigned long   pudval_t;
typedef unsigned long   pgdval_t;
typedef unsigned long   pgprotval_t;
typedef struct { pteval_t pte; } pte_t;
#endif  /* !__ASSEMBLY__ */##

4、保护模式

(1)实模式

实模式下存储器地址的分段允许的最大寻址空间为1MB(因为8086/8088地址总线宽度是20为 2^20=1048576=1024k=1M),其他的微处理器也为1M, 实际上实模式就是为8086/8088而设计的工作方式,它要解决在16位字长的机器里怎么提供20位地址的问题,而解决的方法是采用存储器地址分段的方法。

从0地址,每16个字节为一小段,而在1MB存储器里每个储存单元都有一个唯一的20位物理地址,便于CPU访问存储器,所以这个20位物理地址由16位段地址和16位偏移地址组成,把段地址(因为是首地址,所以低四位全为0,只取高16位)左移4位再加上偏移地址值就形成物理地址,即物理地址=段地址+偏移地址

(2)保护模式

x86处理器地址总线位数增加到24位,可以访问16M地址空间。于是引入保护模式,这种模式下,内存段的访问受到了限制。访问内存时不能直接从段寄存器获得段起始地址了,而要经过额外转换和检查。为与过去兼容,80286内存寻址有两种方式:保护模式和实模式。系统启动时处理器处于实模式,只能访问1M内存空间,经过处理可以进入保护模式,可访问16M内存空间,但要从保护模式回到实模式必须重启机器。由于实模式只提供了1MB的寻址空间,不够用,而且随着多任务出现对寻址空间的要求越来越高,如80826就提供了16MB,80836就提供了达4GB的地址空间,而且虚拟存储器也能扩展空间,而保护模式寻址则对虚拟存储特性有很好的支持。

保护有两层含义:

  • 任务间保护:多任务操作系统中,一个任务不能破坏另一个任务的代码,这是通过内存分页以及不同任务的内存页映射到不同物理内存上来实现的。
  • 任务内保护:系统代码与应用程序代码虽处于同一地址空间,但系统代码具有高优先级,应用程序代码处于低优先级,规定只能高优先级代码访问低优先级代码,这样杜绝用户代码破坏系统代码。

二、内存寻址内核代码实践

1、程序源码

下面的代码主要功能是在内核中先申请一个页面,然后利用内核提供的函数按照寻页的步骤一步步查询各级页目录,最终找到所对应的物理地址。具体过程为首先根据pid我们可以得到进程的task_struct,进而通过task_struct得到mm,通过mm和虚拟地址得到pgd,通过pgd和虚拟地址得到p4d,通过p4d和虚拟地址得到pud,通过pud和虚拟地址得到pmd,通过pmd和虚拟地址得到pte,有了页表pte我们就可以计算物理地址了,页框的物理地址 page_addr = pte_val(*pte) & PAGE_MASK,页偏移地址page_offset = vaddr & ~PAGE_MASK,最终要求的物理地址paddr = page_addr | page_offset。

必要的头文件

#include <linux/init.h>//包含了模块的初始化的宏定义及一些其他函数的初始化函数
#include <linux/module.h>//内核模块必备头文件
#include <linux/mm.h>// 内存管理相关头文件,含有页面大小定义和一些页面释放函数原型。
#include <linux/mm_types.h>//内存管理相关头文件
#include <linux/sched.h>//进程调度相关头文件
#include <linux/export.h>//必要的头文件
#include <linux/delay.h>//延时函数头文件
//定义全局变量
static unsigned long cr0,cr3;//定义CR0和CR3
static unsigned long vaddr = 0;//定义虚拟地址的全局变量

打印页机制中的一些重要参数

static void get_pgtable_macro(void)
{cr0 = read_cr0();//获得CR0寄存器的值 cr3 = read_cr3_pa();//获得CR3寄存器的值 printk("cr0 = 0x%lx, cr3 = 0x%lx\n",cr0,cr3);//打印CR0和CR3的值//_SHIFT宏用来描述线性地址中相应字段所能映射区域大小的位数printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);//打印页全局目录项能映射的区域大小的位数printk("P4D_SHIFT = %d\n",P4D_SHIFT);//打印P4D目录项能映射的区域大小的位数printk("PUD_SHIFT = %d\n", PUD_SHIFT);//打印页上级目录项能映射的区域大小的位数printk("PMD_SHIFT = %d\n", PMD_SHIFT);//打印页中间目录项可以映射的区域大小的位数printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);//打印page_offset字段所能映射区域大小的位数//指示相应页目录表中项的个数printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);//打印页全局目录项数printk("PTRS_PER_P4D = %d\n", PTRS_PER_P4D);//打印P4D目录项数printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);//打印页上级目录项数printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);//打印页中级目录项数printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);//打印页表项数printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);//页内偏移掩码,屏蔽page_offset字段
}

页表描述宏
页表描述宏是在arch/x86/include/asm/pgtable_64 .h中定义的,linux中使用下列宏简化了页表处理,对于每一级页表都使用有以下三个关键描述宏:

宏字段 描述
XXX_SHIFT 定义offset字段的位数
XXX_SIZE 页的大小
XXX_MASK 定义offset的所有位

四级页表宏字段分别为PGDIR、PUD、PMD、PAGE

宏字段 描述
PGDIR 页全局目录(Page Global Directory)
PUD 页上级目录(Page Upper Directory)
PMD 页中级目录(Page Middle Directory)
PAGE 页表(Page Table)

举个例子
PGDIR(页全局目录)的宏,其它页如PUD、PMD等用法类似

宏字段 描述
PGDIR_SHIFT 指定offset字段的位数
PGDIR_SIZE 页的大小
PGDIR_MASK 定义offset的所有位
  • 当PAE 被禁止时, PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同), PGDIR_SIZE 产生的值为 222 或 4 MB, PGDIR_MASK 产生的值为 0xffc00000。
  • 当PAE被激活时, PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air), PGDIR_SIZE 产生的值为230 或 1 GB PGDIR_MASK产生的值为0xc0000000
  • PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD 用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。

宏定义相关代码见Linux内核源码

/arch/x86/include/asm/page_types.h

#include <linux/types.h>
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT  12
#define PAGE_SIZE   (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK   (~(PAGE_SIZE-1))

用于x86处理器时,PAGE_SHIFT返回的值为12。 页内所有地址都必须放在offset字段, x86系统的页的大小PAGE_SIZE是4096字节。 PAGE_MASK宏产生的值为0xfffff000,用以屏蔽offset字段的所有位。

线性地址转换为物理地址

static unsigned long vaddr2paddr(unsigned long vaddr)
{//创建变量保存页目录项pgd_t *pgd;p4d_t *p4d;pud_t *pud;pmd_t *pmd;pte_t *pte;unsigned long paddr = 0;unsigned long page_addr = 0;unsigned long page_offset = 0;//获取页全局目录PGD,第一个参数当前进程的mm_struct,所有进程共享一个内核页表pgd = pgd_offset(current->mm,vaddr);//获得pgd的地址printk("pgd_val = 0x%lx, pgd_index = %lu\n", pgd_val(*pgd),pgd_index(vaddr));//打印pgd地址和索引if (pgd_none(*pgd))//判断pgd页表项是否为空{printk("not mapped in pgd\n");return -1;}//获取P4D,新的Intel芯片的MMU硬件规定可以进行5级页表管理,内核在PGD和PUD之间,增加了一个叫P4D的页目录p4d = p4d_offset(pgd, vaddr);//获得p4d的地址printk("p4d_val = 0x%lx, p4d_index = %lu\n", p4d_val(*p4d),p4d_index(vaddr));//打印p4d地址和索引if(p4d_none(*p4d))//判断p4d页表项是否为空{ printk("not mapped in p4d\n");return -1;}//获取页上级目录PUDpud = pud_offset(p4d, vaddr);//获得pud的地址printk("pud_val = 0x%lx, pud_index = %lu\n", pud_val(*pud),pud_index(vaddr));//打印pud地址和索引if (pud_none(*pud)) //判断pud页表项是否为空{printk("not mapped in pud\n");return -1;}//获取页中间目录PMD pmd = pmd_offset(pud, vaddr);获得pmd的地址printk("pmd_val = 0x%lx, pmd_index = %lu\n", pmd_val(*pmd),pmd_index(vaddr));//打印pmd地址和索引if (pmd_none(*pmd)) 判断pmd页表项是否为空{printk("not mapped in pmd\n");return -1;}pte = pte_offset_kernel(pmd, vaddr);//获得pte的地址printk("pte_val = 0x%lx, ptd_index = %lu\n", pte_val(*pte),pte_index(vaddr));//打印pte地址和索引if (pte_none(*pte)) //判断pte页表项是否为空{printk("not mapped in pte\n");return -1;}page_addr = pte_val(*pte) & PAGE_MASK;//获得页框的物理地址page_offset = vaddr & ~PAGE_MASK;//获得页偏移地址paddr = page_addr | page_offset;//获得物理地址printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);//打印虚拟地址和转换后的物理地址return paddr;
}

页表处理函数
以pgd为例,其他页pud,pmd等用法类似,更多页表处理函数可继续深入阅读源代码

函数名 说明
pgd_index(addr) 找到线性地址 addr 对应的的目录项在页全局目录中的索引
pgd_offset(mm, addr) 以内存描述符地址 mm 和线性地址 addr 作为参数,得到地址addr 在页全局目录中相应表项的线性地址,通过内存描述符 mm 内的一个指针可以找到这个页全局目录
pgd_page(pgd) 通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址

加载内核模块

static int __init v2p_init(void)
{unsigned long vaddr = 0 ;printk("vaddr to paddr module is running..\n");get_pgtable_macro();//打印主要参数printk("\n");vaddr = __get_free_page(GFP_KERNEL);//在内核ZONE_NORMAL中申请一块页面if (vaddr == 0) {printk("__get_free_page failed..\n");return 0;}sprintf((char *)vaddr, "hello world from kernel");printk("get_page_vaddr=0x%lx\n", vaddr);vaddr2paddr(vaddr);//调用线性地址转换物理地址的函数ssleep(600);//延时return 0;
}

卸载内核模块

static void __exit v2p_exit(void)
{printk("vaddr to paddr module is leaving..\n");free_page(vaddr);
}

入口、出口,许可证

module_init(v2p_init);//内核入口函数
module_exit(v2p_exit);//内核出口函数
MODULE_LICENSE("GPL"); //许可证

2、Makefile

本程序Makefile文件代码如下

#产生目标文件
obj-m:=v2p.o
#路径变量,指明当前路径
CURRENT_PATH:=$(shell pwd)
#指明内核版本号
LINUX_KERNEL:=$(shell uname -r)
#指明内核源码的绝对路径
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#编译模块
all:make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#清理模块
clean:make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

插播一条Makefile小知识

  • obj-m 意思是将后面的内容编译成内核模块
  • obj-y 编译进内核
  • obj-n 不编译

3、编译加载模块

(1)编译模块

使用命令make后,生成如下文件

(2)加载模块

使用命令

sudo insmod v2p.ko

将v2p.ko加载到内核中

(3)查看模块

使用命令

lsmod

查看系统已插入的内核模块,如下图模块已经加载到内核中

4、查看结果

使用命令

dmesg

查看系统日志,结果如下图所示


从图中可以看到CR0、CR3寄存器的值,PGDIR、PUD、P4D、PMD、PAGE目录offset字段的位数、目录项数、地址和索引等,虚拟地址vaddr = ffff95590e2f4000, 转换后的物理地址paddr = 800000000e2f4000。

Linux内核之浅谈内存寻址相关推荐

  1. 鸿蒙使用linux内核微内核,浅谈鸿蒙操作系统的微内核

    描述 华为在松山湖的华为开发者大会上正式宣布了鸿蒙操作系统,该系统其中一个亮点是 -- 微内核.华为声称,微内核的启用,使其速度大大提升,并且在安全性上产生变革性突破,微内核打破了宏内核下root即可 ...

  2. linux 内核fpic,浅谈-fPIC与-fpic

    -fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code), 则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意 ...

  3. linux内核md5,浅谈MD5及简单使用

    原理简介: MD5即Message-Digest Algorithm 5(信息-摘要算法 第5版),用于确保信息传输完整一致.是计算机广泛使用的杂凑算法之一(又名:摘要算法.哈希算法),主流编程语言普 ...

  4. 多线程之旅之四——浅谈内存模型和用户态同步机制

     用户态下有两种同步结构的 volatile construct: 在简单数据类型上原子性的读或者写操作   interlocked construct:在简单数据类型上原子性的读和写操作 (在这里还 ...

  5. Linux内核源代码情景分析-内存管理

    用户空间的页面有下面几种: 1.普通的用户空间页面,包括进程的代码段.数据段.堆栈段.以及动态分配的"存储堆". 2.通过系统调用mmap()映射到用户空间的已打开文件的内容. 3 ...

  6. [Linux学习笔记] 浅谈信号(文章含不少学习资源)

    百金买骏马,千金买美人,万金买爵禄,何处买青春? 目录 信号的概念 信号的种类: kill -l 命令可以查看信号列表 man 7 signal 查看信号详细内容 信号的产生 补充知识 Core Du ...

  7. 【Linux 内核】Linux 内核体系架构 ( 进程调度 | 内存管理 | 中断管理 | 设备管理 | 文件系统 )

    文章目录 一.进程调度 二.内存管理 三.中断管理 四.设备管理 五.文件系统 一.进程调度 进程调度 : 进程 是 系统中 进行 资源分配 的 基本单位 ; 每个进程 在 运行时 , 都 感觉自己占 ...

  8. linux 线程_浅谈Linux线程模型

    Thread Basic 基础概念 线程是操作系统能够调度和执行的基本单位,在Linux中也被称之为轻量级进程.从定义中可以看出,线程它是操作系统的概念,在不同的操作系统中的实现是不同的,不过今天分享 ...

  9. 阅读 Linux 内核源码——共享内存

    介绍 我看的是linux-4.2.3的源码.参考了<边干边学--Linux内核指导>(鬼畜的书名)第16章内容,他们用的是2.6.15的内核源码. 现在linux中可以使用共享内存的方式有 ...

最新文章

  1. java报错MalformedURLException: unknown protocol: c
  2. JAVA正则表达式高级用法(分组与捕获)
  3. [jQuery]使用jQuery.Validate进行客户端验证(高级篇-下)——不使用微软验证控件的理由...
  4. USACO training 2.4.5 Fractions to Decimals题解
  5. cocos2dx 背景用小尺寸图片滚动填充的方法
  6. sqlserver中某列转成以逗号连接的字符串及逆转、数据行转列列转行
  7. leetcode_53 Maximum Subarray
  8. Windows Server 2016 安装.NET Framework 3.5 错误处理
  9. 致远互联蜂巢计划3.0:三维进化的协同创新生态
  10. matlab相机标定工具箱讲解,matlab 相机标定工具箱
  11. windows7 热键查看_创建快捷方式或热键以在Windows 7或Vista中打开任务管理器的“所有用户”视图...
  12. JSP实用教程-第三章Tag文件与Tag标记
  13. 3.DesignForVias\1.CreateAutoVia(ShieldGnd)
  14. 概论_第8章_假设检验的基本步骤__假设检验的类型
  15. 使用Fiddler抓取微信小程序二维码请求地址
  16. 全网19套超热门表情包,小狗头、国王排名等我全部整理来了
  17. 怎样才能在技术领域走的更远?
  18. 传奇3私服架设技术教程
  19. word2vec是这样演变到bert的
  20. ECLISPE安装阿里巴巴规范校验插件P3C

热门文章

  1. DynamicData for Asp.net Mvc留言本实例 中篇 新建.删除.数据验证
  2. 设计模式 之 命令模式
  3. Swift 对象内存模型探究(一)
  4. 两行 CSS 代码实现图片任意颜色赋色技术
  5. oracle查找重复记录
  6. 架构设计之设计模式 (二) 静态代理和动态代理--间接“美”
  7. skyline B/S模式下脚本实现输出视频
  8. 一起谈.NET技术,在Mono 2.8上部署ASP.NET MVC 2
  9. 闲话能力管理(Capacity Management)
  10. WinAPI: GetTickCount - 获取系统已启动的时间