Rongfeng's profile凤影渡霞PhotosBlogListsMore ![]() | Help |
|
|
August 02 PERFORMANCE-MONITORINGPerformance-Monitoring 是Intel提供的可以监测统计CPU内部所产生事件的一组方法。在Intel的手册上介绍了两类CPU事件监测方法:architectural performance monitoring 和 non-architectural performance monitoring。Architectural performance monitoring与平台(CPU系列)无关但所能监测的事件少;non-architectural performance monitoring与平台密切相关,能监测大量事件。我仅关注architectural performance monitoring。
Architectural performance monitoring介绍 CPU通过两个寄存器来完成事件监测工作:事件选择寄存器IA32_PERFEVTSELx ( programming performance event select registers)和计数器IA32_PMCx (performance monitoring counter)。在计数前,设置事件选择寄存器并将计数器清零;计数结束时,读取计数器。 IA32_PERFEVTSELx与IA32_PMCx都是成对使用,共同完成计数工作。IA32_PMCx寄存器对应于从0xc1开始的一段连续地址,IA32_PERFEVTSELx寄存器对应于从0x186开始的一段连续地址。每种CPU的寄存器位数和可以使用的寄存器对数可能不一样,但可以通过CUPID.0AH:EAX指令来获取这些元信息: Bits 07 - 00: Version ID of architectural performance monitoring, If >0, architectural performance monitoring capability is supported. Bits 15- 08: Number of general-purpose performance monitoring counter per logical processor Bits 23 - 16: Bit width of general-purpose, performance monitoring counter Bits 31 - 24: Length of EBX bit vector to enumerate architectural performance monitoring events 事件选择寄存器IA32_PERFEVTSELx的配置 Event select field (bits 0 through 7):事件选择码区填写需要监测的事件码,这些事件码都是事先定义好的,可以在Intel的手册中查找。 Unit mask (UMASK) field (bits 8 through 15):掩码区填写与事件选择码对应掩码,掩码与事件码共同使用来确定要监测的事件,掩码与事件码一样是事先定义好的,可在Intel手册上查找。 USR (user mode) flag (bit 16):标识是否统计CPU处于用户态(CPU处于特权级别为:1、2、3)下发生的事件。可以与下面的OS位配合使用。 OS (operating system mode) flag (bit 17):标识是否统计CPU处于系统态(CPU处于特权级别为0)下发生的事件。可以与上面的USR位配合使用。 EN (Enable Counters) Flag (bit 22):计数允许位。注意:在写计数器IA32_PMCx之前,必须清除计数允许位(设为0)。 Counter mask (CMASK) field (bits 24 through 31):计算器掩码,如它不为零,但事件发生是,只有它小于计数器的值,计数器计数才会增加1。 计数示例 下面代码是统计事件DTLB_MISSES.ANY,其事件码为0x08,掩码为0x01。 //寄存器地址码 #define IA32_PMC0 0xc1 #define IA32_PERFEVTSEL0 0x186 //事件码及其掩码 #define EVENT 0x08 #define MASK 0x01 Int nEventMask, nCount; Int nEventRegisterHigh,nEventRegisterLow; nEventMask = IA32_PERFEVTSEL0;
nCount = IA32_PMC0; nEventRegisterHigh=nEventRegisterLow=0; //设置事件码及掩码 nEventRegisterLow |= EVENT; nEventRegisterLow |= MASK<<8; //设置用户态系统态标识位 nEventRegisterLow |= 1<<16; nEventRegisterLow |= 1<<17; //清楚计数允许位 nEventRegisterLow &= ~(1<<22); //设置事件选择寄存器 wrmsr(nEventMask, -(u32)( nEventRegisterLowl), -1); //计数器清零 wrmsr(nCount, -(u32)( 0), -1); //设置计数允许位 nEventRegisterLow |= 1<<22; wrmsr(nEventMask, -(u32)( nEventRegisterLowl), -1); 。。。。//计数中
//读起计数结果
rdmsr(nCount, nEventRegisterLow, nEventRegisterHigh); Non-architectural performance monitoring介绍 (还不太了解这方面的知识!) Linux内存管理Linux采用的非一致内存访问(NUMA)模型将内存划分为多个节点(node)。但在80x86结构中,使用一个单独的节点来管理所有物理内存。 Linux将每个内存节点的物理内存划分为3个管理区: ZONE_DMA: 包含低于16M的内存页框,为外设准备的 ZONE_NORMAL: 16M—896M内存页框 ZONE_HIGHMEM: 高于896M的内存页框,内核直接寻址空间0—896M,高地址必须通过内存映射才能访问,如:永久内核映射、临时内核映射。 当内核调用一个内存分配函数时必须指明请求页框所在分区。 内核动态分配内存时,可能因为空闲内存不足而进行内存回收,进而导致内核控制路径的阻塞。但是有些情况下(如:中断、临界区)是不能阻塞内核控制路径的,为此Linux开辟了保留内存池来提高这种原子内存分配请求的成功率。保留内存池就是在每个管理区中划出一小块内存区域,一般的内存请求下不分配出去而留给原子性内存请求。当管理区空闲内存小于某个阈值时就启动页框回收算法,来保证保留内存池的安全。 伙伴系统(buddy system) 伙伴系统关注大块内存分配,致力于解决外碎片问题。它将所有的空闲页框分成11组,保存在11个链表中,每个链表分别包含大小为1、2、4、8、16、32、64、128、256、512、1024个连续页框。 分配算法:首先遍历不小于所需内存大小的最小链表,找到空闲块则分配出去;否则,遍历下一个链表(块大小刚好是上一个链表的两倍)找到空闲块,将空闲块一分为二,其一插入上一个链表,其二分配出去;若本链表也无空闲块,则遍历下一链表直到第11个链表(含有1024个连续页框,即每块4M);否则分配失败。 回收算法:回收是分配的逆过程,试图合并两个连续的块成一个更大的块。 Slab分配器 (Slab分配器其中的细节还不甚了解) Slab分配器关注小块内存(<1K)分配,致力于解决内碎片问题。它将内存区看作对象,有构造函数与析构函数。这些对象的构造初始化耗费大量时间。slab分配器把那些析构后的对象保存在内存中,而不是将它释放掉,在内核下次分配请求时就可直接使用这些保存的对象,大大提高效率。 动态内存管理函数 struct page * alloc_pages(unsigned int gfp_mask, unsigned int order) 分配2order 个连续页,返回第一个页框描述符地址。可用通过以下函数得到内存地址: void * page_address(struct page *page) 或者通过以下函数直接得到内存地址: unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order) 如果只需分配一页内存可以调用以下两个函数: struct page * alloc_page(unsigned int gfp_mask) unsigned long __get_free_page(unsigned int gfp_mask) 以下函数可以获得初始化为0的一页内存: unsigned long get_zeroed_page(unsigned int gfp_mask) 以上分配内存的函数对应的释放内存函数: void __free_pages(struct page *page, unsigned int order) void free_pages(unsigned long addr, unsigned int order) void free_page(unsigned long addr) 上面的内存分配函数都是以页为单位,下面介绍两对以字节为单位的内核内存分配函数: void * kmalloc(size_t size, int flags)/void kfree(const void *ptr) 分配物理上连续的内存区域; void * vmalloc(unsigned long size)/ void vfree(void *addr) 分配在虚拟内存空间连续的内存区域,其对应物理内存不一定连续。 June 17 参透Linux内核中断机制1. 中断概述与中断控制器 中断就是打断CPU执行指令序列的事件,这事件可能产生于CPU之内也可能产生于外设。中断可分为同步中断和异步中断两类。同步中断又称为异常,它由CPU控制单元产生,如:缺页异常、除0异常;异步中断是由外设产生,如:各类IO中断。有时候,将异步中断直接称为中断。 x86CPU为实现中断机制提供了两根引脚:NMI 和 INTR。其中 NMI 是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号。 中断信号不是由外部硬件直接发送给CPU引脚的,而是通过中断控制器来传达。在早期的系统中,CPU通过INTF引脚连到一个8259A可编程中断控制器(PIC)上。8259A有8个引脚,因而可以同时连接8个外设;同时8259A可以两个级联在一起,形成主辅两级(其中主8259A直接与CPU相连,从8259A连到主8259A的一个引脚上),这样可以提供15个引脚。为了提高对称多处理器系统(SMP)的并发性,出现了高级可编程中断控制器(APIC)。该系统中每个CPU都有一个本地APIC,于本地APIC相连的是IO APIC。当中断信号从外设到达IO APIC时,IO APIC可以公平地将中断信号传给各个本地APIC。每个APIC由24个引脚,系统可以同时有8个IO APIC。 目前大部分单处理器系统都包含一个 IO APIC 芯片,它可以通过以下两种方式来对这种芯片进行配置: 1): 作为一种标准的 8259A 工作方式。本地 APIC 被禁止,外部 I/O APIC 连接到 CPU,两条 LINT0 和 LINT1 分别连接到 INTR 和 NMI 引脚。 2): 作为一种标准外部 IO APIC。本地 APIC 被激活,且所有的外部中断都通过 I/O APIC 接收。 2. Linux下中断分类 前面介绍过中断可以分为异常与异步中断;而根据异常时CPU状态和异常处理方式和返回行为的不同,异常又可以可分为故障(fault)、陷阱(trap)、终止(abort)三类。
Linux系统定义了19种异常,比较有名的有:Divide error、Debug、 Page fault等。 3. Linux中断处理机制 Linux系统为了管理各个中断定义了中断描述表(IDT),它保存各个中断或异常向量和对应的中断处理程序入口地址。在系统启动时初始化IDT。IDT中的记录分为三类,分别称为:任务门描述符、中断门描述符、陷阱门描述符。Linux用中断门处理中断,用陷阱门处理异常。与中断不同,异常并不会引起进程切换。但它们都会引起一个内核控制路径,即代表当前进程在内核态执行单独的指令序列。内核控制路径是可以嵌套的,即中断处理程序可以被中断。 通常内核为了快速地处理异常,会在异常到来时向引起异常的进程发送一个信号通知一个反常条件的出现。然后直到该进程接收到这个信号才开始处理这个异常。一般异常处理程序的结构是: 1.在内核堆栈保存大多数寄存器的内容 2.异常处理 3.返回恢复寄存器内容ret_from_exception() 与异常处理不同在中断信号到来时,内核不能向异常处理一样向进程发送信号,因为中断可能会导致进程的切换。一个挂起很长时间的进程,等待的中断信号到来后,一个完全无关的进程可能正在运行,给当前进程发送信号是毫无意义的。为了让内核更快地处理中断,Linux将中断处理分成top half与bottom half 两部分。top half 是内核必须马上处理的,包括读中断信号、设置中断寄存器状态等;bottom half则实现各个中断的处理逻辑,中断的主要操作、大部分处理时间都是在这里完成。bottom half的处理是可以推延的,因而可以让CPU很快地响应完中断。根据bottom half 的处理方式不同,Linux提供了softirq, tasklet, work queue等处理机制。不管采用何种方式,中断处理程序的总体结构是: 1.在内核堆栈保存IRQ值和寄存器的内容 2.为正在给IRQ服务的PIC一个应答,允许PIC进一步发出中断 3.执行共享这个IRQ的所有设备的中断服务例程(ISR) 4.返回恢复寄存器内容ret_from_intr() 前面提到中断控制器上IRQ线总是是有限的,为了满足众多中断请求和便于扩展的需求,Linux提供IRQ线的动态分配的方法为多个设备共享同一条IRQ线。动态分配共享IRQ线的方法如下: 1.在激活一个准备利用IRQ线的设备之前,其驱动程序会调用request_irq()创建一个新的irqaction描述符。 int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char *devname, void *dev_id) 2.然后调用setup_irf()将这个描述符插入到合适的IRQ链表。 3.当设备操作结束时,驱动程序调用free_irq()释放这个描述符。 void free_irq(unsigned int irq, void *dev_id) 如前所述, softirq是内核提高中断响应的一种技术,Linux提供open_softirq()来初始化软中断、raise_softirq()来激活软中断、raise_softirq_irqoff()清除软中断。 tasklet是在softirq的基础上可以在运行时初始化的一种延时中断处理技术,它的调用比softirq简单多,是IO驱动程序实现可延时函数的首选方法。 与softirq和taslet的可延时函数运行在中断上下文中不同,work queue中的函数是运行在进程上下文的。因而执行可阻塞的函数就必须使用work queue。work queue函数是通过一种叫做work thread的内核线程来执行的,因而具有可阻塞的特性。 August 21 Optimization 工作快三个月了,一直致力于程序优化。试过递归改迭代;做过加法换乘法;建过快表;也曾苦苦研究牛顿下山法;甚至写过汇编代码!遍尝各路武艺后,便觉如鱼得水,心也飘飘,欣然自喜,自己俨然是位领域专家。可今天读到一篇大作后才知自己多么可笑。
这文章写的太好了!A reasonably skilled programmer will not write a grossly inefficient program. At least not deliberately. Optimization is what you do when the performance is insufficient. Sometimes the optimizations are easy, sometimes they are hard. Sometimes they fly in the face of your original design, and sometimes they require that you grossly violate your beautiful abstractions in your class system. But always, and I repeat, always, my experience has been that no programmer has ever been able to predict or analyze where performance bottlenecks are without data. No matter where you think the time is going, you will be surprised to discover that it is going somewhere else.
... ...
You optimize because you have a problem in performance. Sometimes it is computational optimization: your bitmap manipulation is just too slow. Sometimes it is data access optimization: it just takes too long to get the data into the machine. And sometimes it is algorithmic optimization: you're doing it wrong. If you don't understand the difference between an n2 sort and an n log n sort, you're probably already in trouble, but that knowledge alone is not useful.
... ...
Optimization matters only when it matters. When it matters, it matters a lot, but until you know that it matters, don't waste a lot of time doing it. Even if you know it matters, you need to know where it matters. Without performance data, you won't know what to optimize, and you'll probably optimize the wrong thing.
The result will be obscure, hard to write, hard to debug, and hard to maintain code that doesn't solve your problem. Thus it has the dual disadvantage of increasing software development and software maintenance costs, and having no performance effect at all.
... ...
真是不见大海不知自己之渺小;不读佳作不识自己之无知! |
|
|