精华内容
下载资源
问答
  • 字节存储单元及struct内存分配

    千次阅读 2009-12-26 13:35:00
    在有限范围内的可计量数值几乎都可以用二进制数码串组合表示,计算机的内存由数以亿万计的比特位存储单元(晶体管)组成。由于一个位只能表示二元数值,所以单独一位的用处不大。通常将许多位组成一组作为一个基本...

    二进制

    当今的计算机系统使用的基本上都是由18世纪德国数理哲学大师莱布尼兹发现的二进制系统。二进制数字系统中只有两种二进制数码——0和1。

    “bit”(比特)被创造出来代表“binary digit”,1bit代表一个二进制数位。

    为方便起见,我们不妨将“比特”简单地理解为数字逻辑电路中的开关,键控导致电路的通断产生两种状态:断电(低电平)为0,上电(高电平)为1。

    存储单元

    2个比特可以组合出4(2^2)种状态,可表示无符号数值范围[0,3];32个比特可以组合出4294967296(2^32)种状态,可表示无符号数值范围[0,4294967295];……。

    在有限范围内的可计量数值几乎都可以用二进制数码串组合表示,计算机的内存由数以亿万计的比特位存储单元(晶体管)组成。由于一个位只能表示二元数值,所以单独一位的用处不大。通常将固定位数的位串作为一个基本存储单位,这样就可以存储范围较大的值。

    存储单元(byte)的地址,就像门牌号,敬请参考《指针》。

    打印字节串

    1byte=8bit,底层都是二进制位串进行移位实现相关操作。

    标准C++中的<bitset>提供了二进制位串操作接口,以下为打印单字节和通用数据类型二进制位串的示例程序,以直观地查看数据的二进制位串。

    typedef unsigned char uchar;
    
    // 枚举整数x二进制串中含有多少个1,也可以不停右移除以2看有多少个余数
    int enum_filled_bits(int x)
    {
        int countx = 0;
    
        while (x)
        {
           countx++;
           x = x & (x - 1);
        }
    
        return countx;
    }
    
    // 打印单字节数的二进制位串
    void binary_print_byte(uchar c)
    {
        for(int i = 0; i < 8; ++i)
        {
           if((c << i) & 0x80) // 左移
               cout << '1';
           else
               cout << '0';
        }
        
        cout << ' ';
    }
    
    // 打印通用类型的二进制位串
    template <class T>
    void binary_print_multibytes(T val)
    {
        void *f = &val; // 取地址
        size_t sz = sizeof(T);
        uchar *pByte = new uchar[sz];
        int i;
    
        for(i = 0; i < sz; i++)
           pByte[i] = *((uchar*)&f + i);   
    
    #ifdef _BIG_ENDIAN
        for(i = 0; i != sz; i++)
           binary_print_byte(pByte[i]);
    #else // for windoze(Intel X86)
        for(i = sz; i != 0; i--)
           binary_print_byte(pByte[i-1]);
    #endif 
    
        delete[] pByte;
    
        cout << endl;
    }

    字节串的组合析取

    以下测试小程序展示了三种字节析取情况:

    #include <stdio.h>
    #include <windows.h> 
    
    int main(int argc, const char * argv[])
    {
        int i;
        BYTE byte[9] = {48,49, 50, 51, 52, 53, 54, 55,0}; 
        printf("每1个byte的16进制BYTE值:\n");
        for(i = 0; i < 9; i++)
        {
            printf("byte[%d] =%x \n", i, byte[i]);
        }
    
        printf("----------------------------------\n");
        printf("字符串byte[9]:\n");
        BYTE *pBYTE = byte;
        printf("*pBYTE = %s\n", pBYTE);
    
        printf("----------------------------------\n");
        printf("每2个byte组合而成的16进制INT16值:\n");
        INT16 *pINT16 = (INT16*)pBYTE;
        for(i = 0; i < 4; i++)
        {
            INT16 i16 = *(pINT16 + i); // Debug
            printf("*(pINT16 +%d) = %x \n", i, *(pINT16 + i));
        }
    
        printf("----------------------------------\n");
        printf("每4个byte组合而成的16进制INT32值:\n");
        INT32 *pINT32 = (INT32*)pBYTE;
        for(i = 0; i < 2; i++)
        {
            INT32 i32 = *(pINT32 + i); // Debug
            printf("*(pINT32 +%d) = %x \n", i, *(pINT32 + i));
        }
        printf("----------------------------------\n");
        
        return 0;
    }

    说明:*(pINT16 + 0) = 3130而不是3031,这是因为x86架构体系的Windows操作系统为小尾端(little endian)系统,也即在起始地址处存放整数的低序号字节(低地址低字节)。关于字节的大小端问题,网络编程中将有所涉及,在嵌入式开发中经常遇到。

    这些位置的每一个都被称为字节(byte),每个字节都包含了存储一个字符所需要的位数

    100

    101

    102

    103

    104

    105

    106

    107

     

     

     

     

     

     

     


    在很多现代的机器上,每个字节包含8个位,可以存储无符号值0至255,或者有符号值-128只127,典型的如ASCII码。每个字节通过地址来标识,如上图中的数字所示。

    为了存储更大的值,我们把两个或更多个字节合在一起作为一个更大的内存单位。例如,很多机器以字为单位存储整数,每个字一般由2或4个字节组成。下图所示内存位置与上图相同,但这次它以4个字节的字来表示。

    100101102103104105106107
                                                    

    尽管一个字(INT32)包含了4个字节,它仍然只有一个地址。至于它的地址是它最左边那个字节的位置还是最右边那个字节的位置,不同的机器有不同的规定。另一个需要注意的硬件事项是边界对齐(boundary alignment)。在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。但这些问题是硬件设计者的事情,它们很少影响C程序员。我们只对两件事情感兴趣:

    1).内存中的每个位置由一个独一无二的地址标识。

    2).内存中的每个位置都包含一个值。

    在实际程序中我们经常根据需要借助强大的指针对一块内存进行操作,再按字节组合析取出所需数据,平时的程序中经常用到通用指针void*(LPVOID)的妙处就在于可以按照需要操作一块内存,以取所需值类型。

    强制类型转换

    下图为 MSDN 中 C++ Type System (Modern C++) 的 Fundamental (built-in) types 按字节宽度的层砌图(Layout of Source Language Data Types):

    一个8byte宽的存储单元,可以存储1个 long long(__int64)/double、2个连续的 long(int)/float、4个连续的 wchart_t(short)、8个连续的byte(char)。
    按照实际存储的有效数据单元,可按需对字节串进行组合析取或类型转换。
    以下程序示例了两种强制类型转换:

    // 多字节的截取(容易造成数据的丢失!)
    int i1 = 0x12345678; // 小序存放顺序4byte:0x78,0x56,0x34,0x12
    short s1 = (short)i1; // 析取2byte:0x5678
    char c1 = (char)i1; // 析取1byte:0x78
    // 短字节的扩展
    short s2 = 0x5678; // 小序存放顺序2byte:0x78,0x56
    int i2 = (int)s2; // 扩展2byte,高位补0:0x00005678

    关于字节序

     
    

    在移动嵌入式领域,统治市场的 MIPS 和 ARM 处理器可通过配置寄存器采用不同的字节序,默认采用 Little-Endian。

    但 ARM 始终采用 Big-Endian 存储浮点数。早期使用 PowerPC 处理器的 Mac 采用大字节序,如今的 Mac 同 Windows PC 一样都采用 Intel x86 芯片,因此也都是小字节序存储的。

    TCP/IP协议统一规定采用大端方式封装解析传输数据,也称为网络字节顺序(network byte order,TCP/IP-endian)。因此,在进行网络数据的收发时,都需要执行字节序转换

    以下为 MSDN 中关于 Packet byte/bit order 的阐述:

    For packets, the bit numbering convention followed is the same as that used in RFCs, namely: the high (most significant) bit of the first byte to hit the wire is in packet bit 0, and the low bit of the last byte to hit the wire is in packet bit 31 (so that the bits are shown from left-to-right in the order they naturally appear over the network).

    以下小程序用于测试输出 OS X/iOS 系统的字节序

    #import <Foundation/Foundation.h>
    #import <Foundation/NSByteOrder.h>
    
    // 预编译警告信息将在build report log中输出
    #if __DARWIN_BYTE_ORDER == __DARWIN_BIG_ENDIAN
    #pragma message("__DARWIN_BIG_ENDIAN")
    #elif __DARWIN_BYTE_ORDER == __DARWIN_LITTLE_ENDIAN
    #pragma message("__DARWIN_LITTLE_ENDIAN")
    #endif
    
    #if defined(__BIG_ENDIAN__)
    #pragma message("__BIG_ENDIAN__")
    #elif defined(__LITTLE_ENDIAN__)
    #pragma message("__LITTLE_ENDIAN__")
    #endif
    
    BOOL isBigEndian() {
        unsigned short v = 0x4321;
        return (*((unsigned char*)&v) == 0x43);
    }
    
    BOOL isLittleEndian()
    {
        static CFByteOrder bo = CFByteOrderUnknown;
        
        if (bo == CFByteOrderUnknown) { // run only once
            union w
            {
                short a;    // 2 byte
                char b;     // 1 byte
            } c;
            c.a = 1;
            bo = (c.b?CFByteOrderLittleEndian:CFByteOrderBigEndian); // 高位存储低权字节,则为小端
        }
        
        return bo;
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSLog(@"isBigEndian = %d", isBigEndian()); // 0
            NSLog(@"isLittleEndian = %d", isLittleEndian()); // 1
            NSLog(@"NSHostByteOrder = %ld", NSHostByteOrder()); // NS_LittleEndian=CFByteOrderLittleEndian=1
        }
        return 0;
    }

    以下简单梳理以下 OS X 和 iOS SDK 提供的字节序处理接口。

    (1)OS X和iOS SDK的usr/inclue/i386/endian.h、usr/inclue/arm/endian.h中定义了__DARWIN_BYTE_ORDER

    #define __DARWIN_BYTE_ORDER __DARWIN_LITTLE_ENDIAN
    (2)OS X和iOS SDK的<libkern/OSByteOrder.h>中的OSSwap*操作接口:
    _OSSwapInt32 // __builtin_bswap32,<libkern/i386/_OSByteOrder.h>、<libkern/arm/OSByteOrder.h>
    #define __DARWIN_OSSwapInt32(x) _OSSwapInt32(x) // <libkern/_OSByteOrder.h>
    #define OSSwapInt32(x) __DARWIN_OSSwapInt32(x) // <libkern/OSByteOrder.h>
    #define ntohl(x) __DARWIN_OSSwapInt32(x)
    #define htonl(x) __DARWIN_OSSwapInt32(x)
    <arpa/inet.h>中包含了<machine/endian.h>和<sys/_endian.h>。
    (3)Frameworks/CoreFoundation/CFByteOrder.h还是CF_USE_OSBYTEORDER_H,封装了系列CFSwap*操作接口。
    CF_INLINE uint32_t CFSwapInt32(uint32_t arg) {
    #if CF_USE_OSBYTEORDER_H
    return OSSwapInt32(arg);
    #else
    uint32_t result;
    result = ((arg & 0xFF) << 24) | ((arg & 0xFF00) << 8) | ((arg >> 8) & 0xFF00) | ((arg >> 24) & 0xFF);
    return result;
    #endif
    }
    (4)OS X和iOS SDK的Frameworks/Foundation/NSByteOrder.h中定义了基于CFSwap*操作接口进一步封装了NSSwap*操作接口。
    NS_INLINE unsigned int NSSwapInt(unsigned int inv) {
    return CFSwapInt32(inv);
    }

    (5)OS X和iOS SDK的usr/inclue/sys/_endian.h中定义了ntohs/htonsntohl/htonl等宏:

    struct存储分配

    请看下面的结构:

    struct MyStruct
    {
        double dda1;
        char dda;
        int type;
    };

    对结构MyStruct采用sizeof会出现什么结果呢?sizeof(MyStruct)为多少呢?

    也许你会这样求:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13

    但是当在VC中测试上面结构的大小时,你会发现 sizeof(MyStruct)=16。你知道为什么在VC中会得出这样一个结果吗?

    其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了对齐处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式(vs6.0&vs8.0,32位系统)

    类型

    对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)

    char

    偏移量必须为sizeof(char),即1的倍数

    short

    偏移量必须为sizeof(short),即2的倍数

    int

    偏移量必须为sizeof(int),即4的倍数

    float

    偏移量必须为sizeof(float),即4的倍数

    double

    偏移量必须为sizeof(double),即8的倍数

    各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

    下面用前面的例子来说明VC到底怎么样来存放结构的。

    struct MyStruct
    {
        double dda1;
        char dda;
        int type;
    };

    为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式,依次分配。

    1)先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同,刚好偏移量0刚好为sizeof(double)的倍数,该成员变量占用sizeof(double)=8个字节;

    2)接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用sizeof(char)=1个字节;

    3)接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是=4的倍数,所以把type存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节;

    4)这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。

    5)所以整个结构的大小为:sizeof(MyStruct)=8+1+3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。

    下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况:

    struct MyStruct
    {
        char dda;
        double dda1;
        int type;
    };

    这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(MyStruct)=24。结合上面提到的分配空间的一些原则,分析下VC怎么样为上面的结构分配空间的。(简单说明)

    struct MyStruct
    {
        /*偏移量为0,满足对齐方式,dda占用1个字节*/
        char dda;
        
        /*下一个可用的地址的偏移量为1,不是sizeof(double)=8的倍数,
        需要补足个字节才能使偏移量变为(满足对齐方式),因此VC自动填充7个字节,
        dda1存放在偏移量为8的地址上,它占用8个字节。*/
        double dda1;
        
        /*下一个可用的地址的偏移量为16,是sizeof(int)=4的倍数,满足int的对齐方式,
        所以不需要VC自动填充,type存放在偏移量为的地址上,它占用4个字节。*/
        int type; 
    };

    所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。

    所以该结构总的大小为:sizeof(MyStruct)1+7+8+4+4=24。其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。

    VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。

    VC中提供了#pragmapack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。下面举例说明其用法。

    #pragma pack(push) // 保存对齐状态
    #pragma pack(4) // 设定为4字节对齐
    struct test
    {
        char m1;
        double m4;
        int m3;
    };
    #pragma pack(pop) // 恢复对齐状态

    以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n,m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragmapack(4)改为#pragmapack(16),那么我们可以得到结构的大小为24。(请读者自己分析)


       VC6中,Project SettingsàC/C++àStruct member alignment中默认值为8Bytes *Struct member alignment用以指定数据结构中的成员变量在内存中是按几字节对齐的,根据计算机数据总线的位数,不同的对齐方式存取数据的速度不一样。这个参数对数据包网络传输等应用尤为重要,不是存取速度问题,而是数据位的精确定义问题,一般在程序中使用#pragma pack来指定。


    参考:

    C/C++基本数据类型

    Pointers》《Pointers and Memory》《Pointers in C》

    字节那些事儿》《字节序》《ARM Endian

    轻松记住大小端》《大端模式和小端模式

    大端与小端详解》《详解大端模式和小端模式


    The sizeof Operator

    VC中的sizeof的用法总结

    什么是内存对齐

    展开全文
  • 内存管理单元MMU

    千次阅读 2012-06-24 16:27:48
    一、内存管理单元MMU介绍 内存管理单元简称MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。MMU使得个用户进程拥有自己独立的地址空间,...一个程序在运行之前,没有必要全部装入内存,仅


      一、内存管理单元MMU介绍

    内存管理单元简称MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。MMU使得每个用户进程拥有自己独立的地址空间,并通过内存访问权限的检查保护每个进程所用的内存不被其他进程破坏。

    重点就在于地址映射:页表的结构与建立、映射的过程。

     

    1、S3C2440 MMU地址变换过程

    1)地址的分类

    一个程序在运行之前,没有必要全部装入内存,仅需要将那些要运行的部分先装入内存,其余部分在用到时从磁盘载入,当内存不足时,再将暂时不用的部分调出到磁盘。

    这使得大程序可以在较小的内存空间中运行,也使得内存中可以同时装入更多的程序并发执行,这样的存储器一般称为虚拟存储器。

    虚拟地址最终需要转换为物理地址才能读写实际的数据,通过将虚拟地址空间和物理空间划分为同样大小的空间(段或页),然后两个空间建立映射关系。

    由于虚拟地址空间远大于物理地址,可能多块虚拟地址空间映射到同一块物理地址空间,或者有些虚拟地址空间没有映射到具体的物理地址空间上去(使用到时再映射)。

     

    ARM cpu地址转换涉及三种地址:虚拟地址(VA,Virtual Address)、变换后的虚拟地址(MVA,Modified Virtual Address)、物理地址(PA,Physical Address)

    没有启动MMU时,CPU核心,cache,MMU,外设等所有部件使用的都是物理地址。

    启动MMU后,CPU核心对外发出虚拟地址VA;VA被转换为MVA供cache,MMU使用,在这里MVA被转换成PA;最后使用PA读取实际设备

     

    ①CPU核心看到和用到的只是虚拟地址VA,至于VA如果去对应物理地址PA,CPU核心不理会

    ②caches和MMU看不到VA,他们利用MVA转换得到PA

    ③实际设备看不到VA、MVA,读写它们使用的是物理地址PA

     

    MVA是除CPU核心外的其他部分看到的虚拟地址,VA与MVA的变化关系

    如果VA<32M,需要使用进程标识号PID(通过读CP15的C13获得)来转换为MVA

     

    if (VA  < 32M) then

            MVA = VA | (PID << 25)

    else

            MVA = VA

     

    使用MVA,而不使用VA的目的是,当有重叠的VA时,转换为MVA地址并不重叠,减小转换为PA的代价

    比如两个进程1、2,VA都是0-(32M-1),则MVA分别为0x02000000-0x03ffffff,0x04000000-0x05ffffff。

    下文说到虚拟地址,如果没有特别指出,就是指MVA

     

    2)虚拟地址到物理地址的转换过程

     arm cpu使用页表来进行转换,页表由一个个条目组成,每个条目存储一段虚拟地址对应的物理地址及访问权限,或者下一级页表的地址

     S3C2440最多会用到两级页表,以段(Section,1M)的方式进行转换时只用到一级页表,以页(Page)的方式进行转换时用到两级页表。

     页的大小有3种:大页(64KB),小页(4KB),极小页(1KB)。条目也称为描述符,有:段描述符、大页描述符、小页描述符、极小页描述符-保存段、大页、小页、极小页的起始物理地址;粗页表描述符、细页表描述符,它们保存二级页表的物理地址。

     

    下图为S3C2440的地址转换图

     

     

     

    TTB base代表一级页表的地址,将它写入协处理器CP15的寄存器C2(称为页表基址寄存器)即可,一级页表的地址是16K对齐,使用[31:14]存储页表基址,[13:0]为0

     一级页表使用4096个描述符来表示4GB空间,每个描述符对应1MB的虚拟地址,存储它对应的1MB物理空间的起始地址,或者存储下一级页表的地址。使用MVA[31:20]来索引一级页表(20-31一共12位,2^12=4096,所以是4096个描述符),得到一个描述符,每个描述符占4个字节。

    一级页表描述符格式如下:

     

     

    一级页表描述符

     

    最低两位:

     0b00:无效

     

     0b01:粗页表(Coarse page table)

            [31:10]为粗页表基址,此描述符低10位填充0后就是一个二级页表的物理地址,二级页表含256个条目(使用[9:2],2^8=256个),称为粗页表(Coarse page table)。其中每个条目表示4KB大小的物理地址空间,一个粗页表表示1MB物理地址

     

    0b10:段(Section)

            [31:20]为段基址,、此描述符低20位填充0后就是一块1MB物理地址空间的起始地址。MVA[19:0],用来在这1MB空间中寻址。描述符的位[31:20]和MVA[19:0]构成了这个虚拟地址MVA对应的物理地址

     

    以段的方式进行映射时,虚拟地址MVA到物理地址PA的转换过程如下:

    ①页表基址寄存器位[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到段描述符

    ②取出段描述符的位[31:20](段基址),它和MVA[19:0]组成一个32位的物理地址(这就是MVA对应的PA)

     

    段地址转换过程

     

    0b11:细页表(Fine page table)

            [31:12]为细页表基址(Fine page table base address),此描述符的低12位填充0后,就是一个二级页表的物理地址。此二级页表含1024个条目(使用[11:2],10位),其中每个条目表示大小1kb的物理地址空间,一个细页表表示1MB物理地址空间

     

    以大页(64KB),小页(4KB)或极小页(1KB)进行地址映射时,需要用到二级页表,二级页表有粗页表、细页表两种,二级页表描述符格式如下:

     

     

    二级页表描述符

     


    最低两位:

    0b00:无效

     

    0b01:大页描述符

            位[31:16]为大页基址,此描述符的低16位填充0后就是一块64KB物理地址空间的起始地址粗页表中的每个条目只能表示4KB物理空间,如果大页描述符保存在粗页表中,则连续16个条目都保存同一个大页描述符。类似的,细页表中每个条目只能表示1KB的物理空间,如果大页描述符保存在细页表中,则连续64个条目都保存同一个大页描述符。

    下面以保存在粗页表中的大页描述符为例,说明地址转化那过程

    ①页表基址寄存器[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到粗页表描述符

    ②取出粗页表描述符的[31:10](即粗页表基址),它和MVA[19:12]组成一个低两位为0的32位物理地址,通过这个地址找到大页描述符

    ③取出大页描述符的[31:16](即大页基址),它和MVA[15:0]组成一个32位的物理地址,即MVA对应的PA

    步骤②和③中,用于在粗页表中索引的MVA[19:12]、用于在大页内寻址的MVA[15:0]有重合的位[15:12],当位[15:12]从0b0000变化到0b1111时,步骤②得到的大页描述符相同,所以粗页表中有连续16个条目保存同一个大页描述符

     

    大页的地址转换过程(大页描述符保存在粗页表中)

     

    0b10:小页描述符

    [31:12]为小页基址(Small page base address),此描述符的低12位填充0后就是一块4kb([11:0],一共12位,2^12=4096)物理地址空间的起始地址。粗页表中每个条目表示4kb的物理空间,如果小页描述符保存在粗页表中,则只需要用一个条目来保存一个小页描述符。类似的,细页表中每个条目只能表示1kb的物理空间,如果小页保存在细页表中,则连续4个条目都保存同一个小页描述符。

    下面以保存在粗页表中的小页描述符为例,说明地址转换过程:

    ①页表基址[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到粗页表描述符

    ②取出粗页表描述符[31:10](即粗页表基址),它和MVA[19:12]组成一个低两位为0的32位物理地址,用这个地址找到小页描述符

    ③取出小页描述符的位[31:12](即小页基址),它和MVA[11:0]组成一个32位物理地址(即MVA对应的PA)

     

    小页描述符保存在细页表中,地址转换过程和上面类似。

     

    小页的地址转换过程(小页描述符保存在粗页表中)

     

    0b11:极小页描述符

            [31:10]为极小页基址(Tiny page base address),此描述符的低10位填充0后就是一块1KB物理地址空间的起始地址。极小页描述符只能保存在细页表中,用一个条目来保存一耳光极小页描述符

    下面是极小页的地址转换过程:

    ①页表基址寄存器[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU通过这个地址找到细页表描述符

    ②取出细页表描述符[31:12](即细页表基址),它和MVA[19:10]组成一个低两位为0的32位物理地址,通过这个地址即可找到极小页描述符

    ③取出极小页描述符[31:10](即极小页基址),它和MVA[9:0]组成一个32位的物理地址(即MVA对应的PA)

     

     

    极小页的地址转换过程(极小页描述符保存在粗页表中)

     

    从段、大页、小页、极小页的地址转换过程可知

    ①以段进行映射时,通过MVA[31:20]结合页表得到一段(1MB)的起始物理地址,MVA[19:0]用来在段中寻址

    ②以大页进行映射时,通过MVA[31:16]结合页表得到一个大页(64KB)的起始物理地址,MVA[15:0]用来在小页中寻址

    ③以小页进行映射时,通过MVA[31:12]结合页表得到一个小页(4KB)的起始物理地址,MVA[11:0]用来在小页中寻址

    ④以极小页进行映射时,通过MVA[31:10]结合页表得到一个极小页(1KB)的起始物理地址,MVA[9:0]用来在极小页中寻址

     

    2、内存的访问权限检查

    它决定一块内存是否允许读、是否允许写。这由CP15寄存器C3(域访问控制)、描述符的域(Domain)、CP15寄存器C1的R/S/A位、描述符的AP位共同决定。

    “域”决定是否对某块内存进行权限检查,“AP”决定如何对某块内容进行权限检查。

    S3C2440有16个域,CP15寄存器C3中每两位对应一个域(一共32位),用来表示这个域是否进行权限检查

    每两位数据的含义

    00:无访问权限(任何访问都将导致“Domain fault”异常)

    01:客户模式(使用段描述符、页描述符进行权限检查)

    10:保留(保留,目前相当于“无访问权限”)

    11:管理模式(不进行权限检查,允许任何访问)

    Domain占用4位,用来表示内存属于0-15,哪一个域

    例如:

    ①段描述符中的“Domain”为0b0010,表示1MB内存属于域2,如果域访问控制寄存器的[5:4]等于0b00,则访问这1MB空间都会产生“Domain fault”异常,如果等于0b01,则使用描述符中的“Ap”位进行权限检查

    ②粗页表中的“Domain”为0b1010,表示1MB内存属于域10,如果域访问控制寄存器的[21:20]等于0b01,则使用二级页表中的大页/小页描述符中的"ap3"、"ap2"、"ap1"、"ap0"位进行权限检查,如果等于0b11,则允许任何访问,不进行权限检查。

    如下图:

     

     

    一级页表描述符

     


    二级页表描述符

     

    AP、ap3、ap2、ap1、ap0结合CP15寄存器C1的R/S位,决定如何进行访问检查。

    段描述符中AP控制整个段(1MB)访问权限;大页描述符每个apx(0-3)控制一个大页(64KB)中1/4内存的访问权限,即ap3对应大页高端的16KB,ap0对应大页低端的16KB;小页描述符与大页描述符类似,每个apx(0-3)控制一个小页(4KB)的1/4内存的访问权限;极小页中的ap控制整个极小页(1KB)的访问权限。

    下表为AP、S、R的对照表

     

    AP

    S

    R

    特权模式

    用户模式

    说明

     

    00

    0

    0

    无访问权限

    无访问权限

    任何访问将产生“Permission fault”异常

     

    00

    1

    0

    只读

    无访问权限

    在超级权限下可以进行读操作

     

    00

    0

    1

    只读

    只读

    任何写操作将产生”Permission fault“异常

     

    00

    1

    1

    保留

    -

    -

     

    01

    x

    x

    读/写

    无访问权限

    只允许在超级模式下访问

     

    10

    x

    x

    读/写

    只读

    在用户模式下进行写操作将产生"Permission fault"异常

     

    11

    x

    x

    读/写

    读/写

    在所有模式下允许任何访问

     

    xx

    1

    1

    保留

    -

    -

     

     

     3、TLB的作用

    从MVA到PA的转换需要访问多次内存,大大降低了CPU的性能,有没有办法改进呢?

    程序执行过程中,用到的指令和数据的地址往往集中在一个很小的范围内,其中的地址、数据经常使用,这是程序访问的局部性。

    由此,通过使用一个高速、容量相对较小的存储器来存储近期用到的页表条目(段、大页、小页、极小页描述符),避免每次地址转换都到主存中查找,这样就大幅提高性能。这个存储器用来帮助快速地进行地址转换,成为转译查找缓存(Translation Lookaside Buffers, TLB)

    当CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查,否则MMU访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB中,下次再使用这个虚拟地址时就直接使用TLB用的描述符。

    使用TLB需要保证TLB中的内容与页表一致,在启动MMU之前,页表中的内容发生变化后,尤其要注意。一般的做法是在启动MMU之前使整个TLB无效,改变页表时,使所涉及的虚拟地址对应的TLB中条目无效。

     

    4、Cache的作用

    同样基于程序访问的局部性,在主存和CPU通用寄存器之间设置一个高速的、容量相对较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,供CPU在一段时间内使用,对提高程序的运行速度有很大作用。这个cache一般称为高速缓存。

    ①写穿式(Write Through)

    任一CPU发出写信号送到Cache的同时,也写入主存,保证主存的数据同步更新。优点是操作简单,但由于主存速度慢,降低了系统的写速度并占用了总线的时间。

    ②回写式(Write Back)

    数据一般只写到Cache,这样可能出现Cache中的数据得到更新而主存中的数据不变(数据陈旧)的情况。此时可在Cache中设一个标志地址及数据陈旧的信息,只有当Cache中的数据被换出或强制进行”清空“操作时,才将原更新的数据写入主存响应的单元中,保证了Cache和主存中数据一致。

     

    Cache有以下两个操作:

    ①”清空“(clean):把Cache或Write buffer中已经脏的(修改过,但未写入主存)数据写入主存

    ②”使无效“(Invalidate):使之不能再使用,并不将脏的数据写入主存。

     

    S2C2440内置了指令Cache(ICaches)、数据Cache(DCaches)、写缓存(Write buffer),需要用到描述符中的C位(Ctt)和B位(Btt)

    1)指令Cache(ICaches)

    系统刚上电或复位时,ICaches中的内容是无效的,并且ICaches功能关闭。往Icr位(CP15协处理器中寄存器1的第12位)写1可以启动ICaches,写0停止ICaches

    ICaches一般在MMU开启后使用,此时描述符的C位用来表示一段内存是否可以被Cache。若Ctt=1,允许Cache,否则不允许。如果MMU没有开启,ICaches也可以被使用,此时CPU读取指令时所涉及的内存都被当做允许Cache

    ICaches关闭时,CPU每次取指都要读取主存,性能低,所以通常尽早启动ICaches

    ICaches开启后,CPU每次取指时都会先在ICaches中查看是否能找到所用指令,而不管Ctt是0还是1。如果找到成为Cache命中,找不到称为Cache丢失,ICaches被开启后,CPU的取指有如下三种情况:

    ①Cache命中且Ctt为1时,从ICaches中取指,返回CPU

    ②Cache丢失且Ctt为1时,CPU从主存中取指,并且把指令缓存到Cache中

    ③Ctt为0时,CPU从主存中取指

    2)数据Cache(DCaches)

    与ICaches相似,系统刚上电或复位时,DCaches中的内容无效,并且DCaches功能关闭,Write buffer中的内容也是被废弃不用的。往Ccr位(CP15协处理器 中寄存器1的第二位)写1启动DCaches,写0停止DCaches。Write buffer和DCaches紧密结合,额米有专门的控制来开启和停止它

    与ICaches不同,DCaches功能必须在MMU开启之后才能被使用。

    DCaches被关闭时,CPU每次都去内存取数据。

    DCaches被开启后,CPU每次读写数据时都会先在DCaches中查看是否能找到所要的数据,不管Ctt是0还是1,找到了成为Cache命中,找不到成为Cache丢失。

    通过下表可知DCaches和Write buffer在Ccr,Ctt,Btt各种取值下,如何工作,Ctt and Ccr 意为 Ctt与Ccr进行逻辑与后的值

     

     

    Ctt and Ccr

    Btt

    DCaches、Write buffer 和主存的访问方式

     

    0

    0


    Non-cached,non-buffered(NCNB)

    读写数据时都是直接操作主存,并且可以被外设中止;

    写数据时不使用Write buffer,CPU会等待写操作完成;

    不会出现Cache命中

     

    0

    1


    Non-Cached buffered(NCB)

    读数据时都是直接操作主存;

    不会出现Cache命中;

    写数据时,数据线存入Write buffer,并在随后写入主存;

    数据存入Write buffer后,CPU立即继续执行;

    读数据时,可以被外设中止;

    写数据时,无法被外设中止

     

    1

    0


    Cached,write-through(写通)mode

    读数据时,如果Cache命中则从Cache中返回数据,不读取主存;

    读数据时,如果Cache丢失则从读主存中返回数据,并导致“linefill”的动作;

    写数据时,数据先存入Write buffer,并在随后写入主存;

    数据存入Write buffer后,CPU立即继续执行;

    写数据时,如果Cache命中则新数据也写入Cache中;

    写数据时,无法被外设中止

     

    1

    1


    Cached,write-back(写回) mode

    读数据时,如果Cache命中则从Cache中返回数据,不读取主存;

    读数据时,如果Cache丢失则从读主存中返回数据,并导致“linefile”的动作;

    写数据时,如果Cache丢失则将数据先存入Write buffer,存储完毕后CPu立即继续执行,这些数据在随后写入主存;

    写数据时,如果Cache命中则在Cache中更新数据,并设置这些数据为”脏的“,但是不会写入主存;

    无论Cache命中与否,写数据都无法被外设中止


     使用Cache时需要保证Cache、Write buffer的内容和主存内容一致,保证下面两个原则:

    ①清空DCaches,使主存数据得到更新

    ②使无效ICaches,使CPU取指时重新读取主存

    在实际编写程序时,要注意如下几点:

    ①开启MMU前,十五小ICaches,DCaches和Write buffer

    ②关闭MMU前,清空ICaches、DCaches,即将”脏“数据写到主存上

    ③如果代码有变,使无效ICaches,这样CPU取指时会从新读取主存

    ④使用DMA操作可以被Cache的内存时,将内存的数据发送出去时,要清空Cache;将内存的数据读入时,要使无效Cache

    ⑤改变页表中地址映射关系时也要慎重考虑

    ⑥开启ICaches或DCaches时,要考虑ICaches或DCaches中的内容是否与主存保持一致

    ⑦对于I/O地址空间,不使用Cache和Write buffer

     

    5、S3C2440 MMU、TLB、Cache的控制指令

    S3C2440除了ARM920T的CPU核心外,还有若干个协处理器,用来帮助主CPu完成一些特殊功能。对MMU、TLB、Cache等的操作涉及到协处理器。

    <MCR|MRC>{条件} 协处理器编码,协处理器操作码1,目的寄存器,源寄存器1,源寄存器2,协处理器操作码2

    <MCR|MRC> {cond} p#,<expression1>,Rd,cn,cm{,<expression2>}

    MRC        //从协处理器获得数据,传给ARM920T CPU核心寄存器

    MCR        //数据从ARM920T CPU核心寄存器传给协处理器

    {cond}        //执行条件,省略时表示无条件执行

    p#        //协处理器序号

    <expression1>        //一个常数

    Rd        //ARM920T CPU核心的寄存器

    cn和cm        //协处理器中的寄存器

    <expression2>        //一个常数

    其中,<expression1>、cn、cm、<expression2>仅供协处理器使用,它们的作用如何取决于具体的协处理器

     

    二、MMU使用实例:地址映射

    这个实例将开启MMU,并将虚拟地址0xA0000000-0xA0100000映射到物理地址0x56000000-0x56100000(GPBCON物理地址为0x56000010,GPBDAT物理地址为0x56000014),来驱动LED。

    将虚拟地址0xB0000000-0xB3FFFFFF映射到物理地址0x30000000-0x33FFFFFF,在连接程序时,将一部分代码的运行地址指定为0xB0004000.

    这个程序只使用一级页表,以段的方式进行地址映射,32位CPU虚拟地址空间达到4G,一级页表使用4096个描述符来表示4G空间(每个描述符对应1MB),每个描述符占4字节,所以一级页表占16KB。这个程序使用SDRAM的开始16KB存放一级页表,所以剩下的内存开始地址就为0x30004000,这个地址最终会对应虚拟地址0xB0004000(所以代码运行地址为0xB0004000)

    程序分为两部分:第一部分的运行地址为0,它用来初始化SDRAM,复制第二部分的代码到SDRAM中(存放在0x30004000)、设置页表、启动MMU,最后跳到SDRAM中(地址0xB0004000),第二部分运行地址设为0xB0004000,用来驱动LED

    先看连接文件mmu.lds

     

    SECTIONS {

      firtst    0x00000000 : { head.o init.o }

      second    0xB0004000 : AT(2048) { leds.o }

    }

    程序分两个段:first和second。first由head.o和init.o组成,加载和运行地址都是0,second由leds.o组成,加载地址为2048,重定位地址为0xB0004000。

     

     

    @*************************************************************************

    @ File:head.S

    @ 功能:设置SDRAM,将第二部分代码复制到SDRAM,设置页表,启动MMU,

    @       然后跳到SDRAM继续执行

    @*************************************************************************      

    .text

    .global _start

    _start:

        ldr sp, =4096                       @ 设置栈指针,以下都是C函数,调用前需要设好栈

        bl  disable_watch_dog               @ 关闭WATCHDOG,否则CPU会不断重启

        bl  memsetup                        @ 设置存储控制器以使用SDRAM

        bl  copy_2th_to_sdram               @ 将第二部分代码复制到SDRAM

        bl  create_page_table               @ 设置页表

        bl  mmu_init                        @ 启动MMU,启动以后下面代码都用虚拟地址

        ldr sp, =0xB4000000                 @ 重设栈指针,指向SDRAM顶端(使用虚拟地址)

        ldr pc, =0xB0004000                 @ 跳到SDRAM中继续执行第二部分代码

    halt_loop:

        b   halt_loop

     

     

    /*

     * init.c: 进行一些初始化,在Steppingstone中运行

     * 它和head.S同属第一部分程序,此时MMU未开启,使用物理地址

     */

    /* WATCHDOG寄存器 */

    #define WTCON           (*(volatile unsigned long *)0x53000000)

    /* 存储控制器的寄存器起始地址 */

    #define MEM_CTL_BASE    0x48000000

     

     

     

    /*

     * 关闭WATCHDOG,否则CPU会不断重启

     */

    void disable_watch_dog(void)

    {

        WTCON = 0;  // 关闭WATCHDOG很简单,往这个寄存器写0即可

    }

     


    /*

     * 设置存储控制器以使用SDRAM

     */

    void memsetup(void)

    {

        /* SDRAM 13个寄存器的值 */

        unsigned long  const    mem_cfg_val[]={ 0x22011110,     //BWSCON

                                                0x00000700,     //BANKCON0

                                                0x00000700,     //BANKCON1

                                                0x00000700,     //BANKCON2

                                                0x00000700,     //BANKCON3 

                                                0x00000700,     //BANKCON4

                                                0x00000700,     //BANKCON5

                                                0x00018005,     //BANKCON6

                                                0x00018005,     //BANKCON7

                                                0x008C07A3,     //REFRESH

                                                0x000000B1,     //BANKSIZE

                                                0x00000030,     //MRSRB6

                                                0x00000030,     //MRSRB7

                                        };

        int     i = 0;

        volatile unsigned long *p = (volatile unsigned long *)MEM_CTL_BASE;

        for(; i < 13; i++)

            p[i] = mem_cfg_val[i];        //循环复制13个寄存器到内存控制器基址

    }

     


    /*

     * 将第二部分代码复制到SDRAM

     */

    void copy_2th_to_sdram(void)

    {

        unsigned int *pdwSrc  = (unsigned int *)2048;        //第二段代码加载地址2048

        unsigned int *pdwDest = (unsigned int *)0x30004000;        //0x30004000前放页表

       

        while (pdwSrc < (unsigned int *)4096) //4kb最大4096

        {

            *pdwDest = *pdwSrc;

            pdwDest++;

            pdwSrc++;

        }

    }

     


    /*

     * 设置页表

     */

    void create_page_table(void)

    {

     


    /*

     * 用于段描述符的一些宏定义

     *[31:20]段基址,[11:10]AP,[8:5]Domain,[3]C,[2]B,[1:0]0b10为段描述符

     */

    #define MMU_FULL_ACCESS     (3 << 10)   /* 访问权限AP */

    #define MMU_DOMAIN          (0 << 5)    /* 属于哪个域 Domain*/

    #define MMU_SPECIAL         (1 << 4)    /* 必须是1 */

    #define MMU_CACHEABLE       (1 << 3)    /* cacheable C位*/

    #define MMU_BUFFERABLE      (1 << 2)    /* bufferable B位*/

    #define MMU_SECTION         (2)         /* 表示这是段描述符 */

    #define MMU_SECDESC         (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \

                                 MMU_SECTION)

    #define MMU_SECDESC_WB      (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | \

                                 MMU_CACHEABLE | MMU_BUFFERABLE | MMU_SECTION)

    #define MMU_SECTION_SIZE    0x00100000        /*每个段描述符对应1MB大小空间*/

     


        unsigned long virtuladdr, physicaladdr;

        unsigned long *mmu_tlb_base = (unsigned long *)0x30000000;        /*SDRAM开始地址存放页表*/

       

        /*

         * Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0,

         * 为了在开启MMU后仍能运行第一部分的程序,

         * 将0~1M的虚拟地址映射到同样的物理地址

         */

        virtuladdr = 0;

        physicaladdr = 0;

         //虚拟地址[31:20]用于索引一级页表,找到它对应的描述符,对应于(virtualaddr>>20)

         //段描述符中[31:20]保存段的物理地址,对应(physicaladdr & 0xFFF00000)

        *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \

                                                MMU_SECDESC_WB;

     


        /*

         * 0x56000000是GPIO寄存器的起始物理地址,

         * GPBCON和GPBDAT这两个寄存器的物理地址0x56000010、0x56000014,

         * 为了在第二部分程序中能以地址0xA0000010、0xA0000014来操作GPBCON、GPBDAT,

         * 把从0xA0000000开始的1M虚拟地址空间映射到从0x56000000开始的1M物理地址空间

         */

        virtuladdr = 0xA0000000;

        physicaladdr = 0x56000000;

        *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \

                                                MMU_SECDESC;

     


        /*

         * SDRAM的物理地址范围是0x30000000~0x33FFFFFF,

         * 将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF上,

         * 总共64M,涉及64个段描述符

         */

        virtuladdr = 0xB0000000;

        physicaladdr = 0x30000000;

        while (virtuladdr < 0xB4000000)

        {

            *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | \

                                                    MMU_SECDESC_WB;

            virtuladdr += 0x100000;        //右移20位就是1

            physicaladdr += 0x100000;        //右移20位就是1

        }

    }

     


    /*

     * 启动MMU

     */

    void mmu_init(void)

    {

        unsigned long ttb = 0x30000000;

     


    __asm__(

        "mov    r0, #0\n"

        "mcr    p15, 0, r0, c7, c7, 0\n"    /* 使无效ICaches和DCaches */

       

        "mcr    p15, 0, r0, c7, c10, 4\n"   /* drain write buffer on v4 */

        "mcr    p15, 0, r0, c8, c7, 0\n"    /* 使无效指令、数据TLB */

       

        "mov    r4, %0\n"                   /* r4 = 页表基址 */

        "mcr    p15, 0, r4, c2, c0, 0\n"    /* 设置页表基址寄存器 */

       

        "mvn    r0, #0\n"                  

        "mcr    p15, 0, r0, c3, c0, 0\n"    /* 域访问控制寄存器设为0xFFFFFFFF, 不进行权限检查*/   

        /*

         * 对于控制寄存器,先读出其值,在这基础上修改感兴趣的位,

         * 然后再写入

         */

        "mrc    p15, 0, r0, c1, c0, 0\n"    /* 读出控制寄存器的值 */

       

        /* 控制寄存器的低16位含义为:.RVI ..RS B... .CAM

         * R : 表示换出Cache中的条目时使用的算法,

         *     0 = Random replacement;1 = Round robin replacement

         * V : 表示异常向量表所在的位置,

         *     0 = Low addresses = 0x00000000;1 = High addresses = 0xFFFF0000

         * I : 0 = 关闭ICaches;1 = 开启ICaches

         * R、S : 用来与页表中的描述符一起确定内存的访问权限

         * B : 0 = CPU为小字节序;1 = CPU为大字节序

         * C : 0 = 关闭DCaches;1 = 开启DCaches

         * A : 0 = 数据访问时不进行地址对齐检查;1 = 数据访问时进行地址对齐检查

         * M : 0 = 关闭MMU;1 = 开启MMU

         */

       

        /* 

         * 先清除不需要的位,往下若需要则重新设置它们   

         */

                                            /* .RVI ..RS B... .CAM */

        "bic    r0, r0, #0x3000\n"          /* ..11 .... .... .... 清除V、I位 */

        "bic    r0, r0, #0x0300\n"          /* .... ..11 .... .... 清除R、S位 */

        "bic    r0, r0, #0x0087\n"          /* .... .... 1... .111 清除B/C/A/M */

     


        /*

         * 设置需要的位

         */

        "orr    r0, r0, #0x0002\n"          /* .... .... .... ..1. 开启对齐检查 */

        "orr    r0, r0, #0x0004\n"          /* .... .... .... .1.. 开启DCaches */

        "orr    r0, r0, #0x1000\n"          /* ...1 .... .... .... 开启ICaches */

        "orr    r0, r0, #0x0001\n"          /* .... .... .... ...1 使能MMU */

       

        "mcr    p15, 0, r0, c1, c0, 0\n"    /* 将修改的值写入控制寄存器 */

        : /* 无输出 */

        : "r" (ttb) );

    }

     

     


    /*

     * leds.c: 循环点亮4个LED

     * 属于第二部分程序,此时MMU已开启,使用虚拟地址

     */

     

    #define GPBCON      (*(volatile unsigned long *)0xA0000010)     // 物理地址0x56000010

    #define GPBDAT      (*(volatile unsigned long *)0xA0000014)     // 物理地址0x56000014

     

    #define GPB5_out    (1<<(5*2))

    #define GPB6_out    (1<<(6*2))

    #define GPB7_out    (1<<(7*2))

    #define GPB8_out    (1<<(8*2))

     

    /*

     * wait函数加上“static inline”是有原因的,

     * 这样可以使得编译leds.c时,wait嵌入main中,编译结果中只有main一个函数。

     * 于是在连接时,main函数的地址就是由连接文件指定的运行时装载地址。

     * 而连接文件mmu.lds中,指定了leds.o的运行时装载地址为0xB4004000,

     * 这样,head.S中的“ldr pc, =0xB4004000”就是跳去执行main函数。

     */

    static inline void wait(unsigned long dly)

    {

        for(; dly > 0; dly--);

    }

     

    int main(void)

    {

        unsigned long i = 0;

       

        // 将LED1-4对应的GPB5/6/7/8四个引脚设为输出

        GPBCON = GPB5_out|GPB6_out|GPB7_out|GPB8_out;      

     

        while(1){

            wait(30000);

            GPBDAT = (~(i<<5));     // 根据i的值,点亮LED1-4

            if(++i == 16)

                i = 0;

        }

     

        return 0;

    }

     

    最后是Makefile


    objs := head.o init.o leds.o


    mmu.bin : $(objs)

     arm-linux-ld -Tmmu.lds -o mmu_elf $^

     arm-linux-objcopy -O binary -S mmu_elf $@

     arm-linux-objdump -D -m arm mmu_elf > mmu.dis 

    %.o:%.c

     arm-linux-gcc -Wall -O2 -c -o $@ $<


    %.o:%.S

     arm-linux-gcc -Wall -O2 -c -o $@ $<


    clean:

     rm -f mmu.bin mmu_elf mmu.dis *.o  
    本文来源于:DoDo's Blog http://www.sectop.com/ , 原文地址:http://www.sectop.com/post/83.html


     

    展开全文
  • MMU内存管理单元详解

    千次阅读 2020-04-28 23:15:07
    应用程序直接访问物理内存,操作系统占用部分内存区。 操作系统的职责是“加载”应用程序,“运行”或“卸载”应用程序。 如果我们一直是单任务处理,则不会有任何问题,也或者应用程序所需的内存总是非常...

    MMU 诞生之前:

    在传统的批处理系统如 DOS 系统,应用程序与操作系统在内存中的布局大致如下图:

    • 应用程序直接访问物理内存,操作系统占用一部分内存区。

    • 操作系统的职责是“加载”应用程序,“运行”或“卸载”应用程序。

    如果我们一直是单任务处理,则不会有任何问题,也或者应用程序所需的内存总是非常小,则这种架构是不会有任何问题的。然而随着计算机科学技术的发展,所需解决的问题越来越复杂,单任务批处理已不能满足需求了。而且应用程序需要的内存量也越来越大。而且伴随着多任务同时处理的需求,这种技术架构已然不能满足需求了,早先的多任务处理系统是怎么运作的呢?

    程序员将应用程序分段加载执行,但是分段是一个苦力活。而且死板枯燥。此时聪明的计算机科学家想到了好办法,提出来虚拟内存的思想。程序所需的内存可以远超物理内存的大小,将当前需要执行的留在内存中,而不需要执行的部分留在磁盘中,这样同时就可以满足多应用程序同时驻留内存能并发执行了。

    从总体上而言,需要实现哪些大的策略呢?

    • 所有的应用程序能同时驻留内存,并由操作系统调度并发执行。需要提供机制管理 I/O 重叠,CPU 资源竞争访问。

    • 虚实内存映射及交换管理,可以将真实的物理内存,有可变或固定的分区,分页或者分段与虚拟内存建立交换映射关系,并且有效的管理这种映射,实现交换管理。

    这样,衍生而来的一些实现上的更具体的需求:

    • 竞争访问保护管理需求:需要严格的访问保护,动态管理哪些内存页/段或区,为哪些应用程序所用。这属于资源的竞争访问管理需求。

    • 高效的翻译转换管理需求:需要实现快速高效的映射翻译转换,否则系统的运行效率将会低下。

    • 高效的虚实内存交换需求:需要在实际的虚拟内存与物理内存进行内存页/段交换过程中快速高效。

    总之,在这样的背景下,MMU 应运而生,也由此可见,任何一项技术的发展壮大,都必然是需求驱动的。这是技术本身发展的客观规律。

     

    内存管理的好处

    • 为编程提供方便统一的内存空间抽象,在应用开发而言,好似都完全拥有各自独立的用户内存空间的访问权限,这样隐藏了底层实现细节,提供了统一可移植用户抽象。

    • 以最小的开销换取性能最大化,利用 MMU 管理内存肯定不如直接对内存进行访问效率高,为什么需要用这样的机制进行内存管理,是因为并发进程每个进程都拥有完整且相互独立的内存空间。那么实际上内存是昂贵的,即使内存成本远比从前便宜,但是应用进程对内存的寻求仍然无法在实际硬件中,设计足够大的内存实现直接访问,即使能满足,CPU 利用地址总线直接寻址空间也是有限的。

    内存管理实现总体策略

    从操作系统角度来看,虚拟内存的基本抽象由操作系统实现完成:

    • 处理器内存空间不必与真实的所连接的物理内存空间一致。

    • 当应用程序请求访问内存时,操作系统将虚拟内存地址翻译成物理内存地址,然后完成访问。

    从应用程序角度来看,应用程序(往往是进程)所使用的地址是虚拟内存地址,从概念上就如下示意图所示,MMU 在操作系统的控制下负责将虚拟内存实际翻译成物理内存。

    从而这样的机制,虚拟内存使得应用程序不用将其全部内容都一次性驻留在内存中执行:

    • 节省内存:很多应用程序都不必让其全部内容一次性加载驻留在内存中,那么这样的好处是显而易见,即使硬件系统配置多大的内存,内存在系统中仍然是最为珍贵的资源。所以这种技术节省内存的好处是显而易见的。

    • 使得应用程序以及操作系统更具灵活性

      • 操作系统根据应用程序的动态运行时行为灵活的分配内存给应用程序。

      • 使得应用程序可以使用比实际物理内存多或少的内存空间。

    MMU 以及 TLB

    MMU(Memory Management Unit)内存管理单元

    • 一种硬件电路单元负责将虚拟内存地址转换为物理内存地址

    • 所有的内存访问都将通过 MMU 进行转换,除非没有使能 MMU。

    TLB(Translation Lookaside Buffer)转译后备缓冲器: 本质上是 MMU 用于虚拟地址到物理地址转换表的缓存

    这样一种架构,其最终运行时目的,是为主要满足下面这样运行需求:

    多进程并发同时并发运行在实际物理内存空间中,而 MMU 充当了一个至关重要的虚拟内存到物理内存的桥梁作用。

    那么,这种框架具体从高层级的概念上是怎么做到的呢?事实上,是将物理内存采用分片管理的策略来实现的,那么,从实现的角度将有两种可选的策略:

    • 固定大小分区机制

    • 可变大小分区机制

    固定大小区片机制

    通过这样一种概念上的策略,将物理内存分成固定等大小的片:

    • 每一个片提供一个基地址

    • 实际寻址,物理地址=某片基址+虚拟地址

    • 片基址由操作系统在进程动态运行时动态加载

    这种策略实现,其优势在于简易,切换快速。但是该策略也带来明显的劣势:

    • 内部碎片:一个进程不使用的分区中的内存对其他进程而言无法使用

    • 一种分区大小并不能满足所有应用进程所需。

    可变大小分区机制

    内存被划分为可变大小的区块进行映射交换管理:

    • 需要提供基址以及可变大小边界,可变大小边界用于越界保护。

    • 实际寻址,物理地址=某片基址+虚拟地址

    那么这种策略其优势在于没有内部内存碎片,分配刚好够进程所需的大小。但是劣势在于,在加载和卸载的动态过程中会产生碎片。

    分页机制

    分页机制采用在虚拟内存空间以及物理内存空间都使用固定大小的分区进行映射管理。

    • 从应用程序(进程)角度看内存是连续的 0-N 的分页的虚拟地址空间。

    • 物理内存角度看,内存页是分散在整个物理存储中

    • 这种映射关系对应用程序不可见,隐藏了实现细节。

    分页机制是如何寻址的呢?这里介绍的设计理念,具体的处理器实现各有细微差异:

    • 虚拟地址包含了两个部分:虚拟页序号 VPN(virtual paging number)以及偏移量

    • 虚拟页序号 VPN页表(Page Table)的索引

    • 页表(Page Table)维护了页框号(Page frame number PFN)

    • 物理地址由PFN::Offset进行解析。

    举个栗子,如下图所示:

    还没有查到具体的物理地址,憋急,再看一下完整解析示例:

     

    如何管理页表

    对于 32 位地址空间而言,假定 4K 为分页大小,则页表的大小为 100MB,这对于页表的查询而言是一个很大的开销。那么如何减小这种开销呢?实际运行过程中发现,事实上只需要映射实际使用的很小一部分地址空间。那么在一级页机制基础上,延伸出多级页表机制。

    以二级分页机制为例:

    单级页表已然有不小的开销,查询页表以及取数,而二级分页机制,因为需要查询两次页表,则将这种开销再加一倍。那么如何提高效率呢?其实前面提到一个概念一直还没有深入描述 TLB,将翻译工作由硬件缓存 cache,这就是 TLB 存在的意义。

    • TLB 将虚拟页翻译成 PTE,这个工作可在单周期指令完成。

    • TLB 由硬件实现

      • 完全关联缓存(并行查找所有条目)

      • 缓存索引是虚拟页码

      • 缓存内容是 PTE

      • 则由 PTE+offset,可直接计算出物理地址

    TLB 加载

    谁负责加载 TLB 呢?这里可供选择的有两种策略:

    • 由操作系统加载,操作系统找到对应的 PTE,而后加载到 TLB。格式比较灵活。

    • MMU 硬件负责,由操作系统维护页表,MMU 直接访问页表,页表格式严格依赖硬件设计格式。

    总结一下

    从计算机大致发展历程来了解内存管理的大致发展策略,如何衍生出 MMU,以及固定分片管理、可变分片管理等不同机制的差异,最后衍生出单级分页管理机制、多级分页管理机制、TLB 的作用。从概念上相对比较易懂的角度描述了 MMU 的诞生、机制,而忽略了处理器的具体实现细节。作为从概念上更深入的理解 MMU 的工作机理的角度,还是不失为一篇浅显易懂的文章。

    展开全文
  • Oracle内存详解之 整体架构

    千次阅读 2011-09-02 21:25:23
    Oracle的内存配置与oracle性能息息相关。关于内存的配置,是最影响Oracle性能的配置。内存还直接影响到其他两...先看Oracle内存存储的主要内容是什么: 程序代码(PLSQL、Java); 关于已经连接的会话的信息,包括当

    Oracle的内存配置与oracle性能息息相关。关于内存的配置,是最影响Oracle性能的配置。内存还直接影响到其他两个重要资源的消耗:CPU和IO. 

     

    先看Oracle内存存储的主要内容是什么:

    程序代码(PLSQL、Java);

    关于已经连接的会话的信息,包括当前所有活动和非活动会话;

    程序运行时必须的相关信息,例如查询计划;

    Oracle进程之间共享的信息和相互交流的信息,例如锁;

    那些被永久存储在外围存储介质上,被cache在内存中的数据(如redo log条目,数据块)。

     

    每个Oracle数据库都是由Oracle Instance(实例)与数据库(数据文件,控制文件、重做日志文件)组成,其中所谓实例就是用户同数据库交互的媒介,用户通过于一个实例相连来操作数据库。而实例又是由统一的内存结构(SGA,PGA,UGA)和一批内存驻留进程组成。实例在操作系统中用ORACLE_SID来标识,在Oracle中用参数INSTANCE_NAME来标识, 它们两个的值是相同的。数据库启动时,系统首先在服务器内存中分配系统全局区(SGA), 构成了Oracle的内存结构,然后启动若干个常驻内存的操作系统进程,即组成了Oracle的 进程结构,内存区域和后台进程合称为一个Oracle实例。

     

     

    一. SGA 

     

    SGA是一组为系统分配的共享的内存结构,可以包含一个数据库实例的数据或控制信息。如果多个用户连接到同一个数据库实例,在实例的SGA中,数据可以被多个用户共享。   当数据库实例启动时,SGA的内存被自动分配;当数据库实例关闭时,SGA内存被回收。  SGA是占用内存最大的一个区域,同时也是影响数据库性能的重要因素。

     

    SGA区是可读写的。所有登录到实例的用户都能读取SGA中的信息,而在oracle做执行操作时,服务进程会将修改的信息写入SGA区。

    SGA主要包括了以下的数据结构:

    数据缓冲(Buffer Cache)

    重做日志缓冲(Redo Log Buffer)

    共享池(Shared Pool)

    Java池(Java Pool)

    大池(Large Pool)

    流池(Streams Pool --- 10g以后才有)

    数据字典缓存(Data Dictionary Cache)

    其他信息(如数据库和实例的状态信息)

     

    SQL> show sga

     

    Total System Global Area  612368384 bytes

    Fixed Size                  1250428 bytes

    Variable Size             192940932 bytes

    Database Buffers          411041792 bytes

    Redo Buffers                7135232 bytes

     

    SGA 中的数据字典缓存 和其他信息 会被实例的后台进程所访问,它们在实例启动后就固定在SGA中了,而且不会改变,所以这部分又称为固定SGA(Fixed SGA)。这部分区域的大小一般小于100K。

     Shared Pool、Java Pool、Large Pool和Streams Pool这几块内存区的大小是相应系统参数设置而改变的,所以有通称为可变SGA(Variable SGA)。

     

    截图出自Oracle 11g 的架构图。 下载地址:http://download.csdn.net/source/2346700

     

    通过下面的语句查询

    SQL> show parameter sga

    NAME                  TYPE        VALUE

    ------------------------------------ ----------- -------

    lock_sga                boolean     FALSE

    pre_page_sga            boolean     FALSE

    sga_max_size            big integer  584M

    sga_target               big integer  584M

     

    先对这几个参数做一下说明:

     SQL> select name,value ,ISSYS_MODIFIABLE from v$parameter where name like 'sga%';

    NAME            VALUE           ISSYS_MOD

    --------------- --------------- ---------

    sga_max_size    612368384       FALSE

    sga_target      612368384       IMMEDIATE


    如果ISSYS_MODIFIABLE 返回的是false,说明该参数无法用alter system语句动态修改,需要重启数据库。

    所以sga_max_size 是不可以动态调整的。但是我们可以对sga_target 进行动态的调整。 

     

    SGA_MAX_SIZE:

    SGA区包括了各种缓冲区和内存池,而大部分都可以通过特定的参数来指定他们的大小。但是,作为一个昂贵的资源,一个系统的物理内存大小是有限。尽管对于CPU的内存寻址来说,是无需关系实际的物理内存大小的,但是过多的使用虚拟内存导致page in/out,会大大影响系统的性能,甚至可能会导致系统crash。所以需要有一个参数来控制SGA使用虚拟内存的最大大小,这个参数就是SGA_MAX_SIZE。

    当实例启动后,各个内存区只分配实例所需要的最小大小,在随后的运行过程中,再根据需要扩展他们的大小,而他们的总和大小受到了SGA_MAX_SIZE的限制。

    当试图增加一个内存的大小,并且如果这个值导致所有内存区大小总和大于SGA_MAX_SIZE时,oracle会提示错误,不允许修改。

    当然,如果在设置参数时,指定区域为spfile时(包括修改SGA_MAX_SIZE本身),是不会受到这个限制的。这样就可能出现这样的情况,在spfile中,SGA各个内存区设置大小总和大于SGA_MAX_SIZE。这时,oracle会如下处理:当实例再次启动时,如果发现SGA各个内存总和大于SGA_MAX_SIZE,它会将SGA_MAX_SIZE的值修改为SGA各个内存区总和的值。

    SGA所分配的是虚拟内存,但是,在我们配置SGA时,一定要使整个SGA区都在物理内存中,否则,会导致SGA频繁的页入/页出,会极大影响系统性能。

    对于OLTP系统,一般的建议是将SGA_MAX_SIZE 设为物理内存的60%,PGA 设为20%。 但是现在服务器内存是相当大的。 几百G的内存随处可见。60%也就是几百G内存。 显然这样也是不合适的。 所以要根据自己系统来设定设定这个值。这个也就所说的DBA的经验。 这是是需要经验的积累。 

     

    下表的几个数值供参考。

     

    系统内存

    SGA_MAX_SIZE值

    1G

    400-500M

    2G

    1G

    4G

    2500M

    8G

    5G

     

    SGA的实际大小可以通过以下公式估算:

    SGA实际大小 = DB_CACHE_SIZE + DB_KEEP_CACHE_SIZE + DB_RECYCLE_CACHE_SIZE + DB_nk_CACHE_SIZE + SHARED_POOL_SIZE + LARGE_POOL_SIZE + JAVA_POOL_SIZE + STREAMS_POOL_SIZE(10g中的新内存池) + LOG_BUFFERS+11K(Redo Log Buffer的保护页) + 1MB + 16M(SGA内部内存消耗,适合于9i及之前版本)

     

    PRE_PAGE_SGA:

    oracle实例启动时,会只载入各个内存区最小的大小。而其他SGA内存只作为虚拟内存分配,只有当进程touch到相应的页时,才会置换到物理内存中。我们可以通过设置PRE_PAGE_SGA参数,让实例一启动后,所有SGA都分配到物理内存。

    这个参数的默认值为FALSE,即不将全部SGA置入物理内存中。当设置为TRUE时,实例启动会将全部SGA置入物理内存中。它可以使实例启动达到它的最大性能状态,但是,启动时间也会更长(因为为了使所有SGA都置入物理内存中,oracle进程需要touch所有的SGA页)。

    SQL> alter system set pre_page_sga=true scope=spfile;

     

    LOCK_SGA

    为了保证SGA都被锁定在物理内存中,而不必页入/页出,可以通过参数LOCK_SGA来控制。这个参数默认值为FALSE,当指定为TRUE时,可以将全部SGA都锁定在物理内存中。当然,有些系统不支持内存锁定,这个参数也就无效了。

     

    SGA_TARGET

    Oracle10g中引入的一个非常重要的参数。在10g之前,SGA的各个内存区的大小都需要通过各自的参数指定,并且都无法超过参数指定大小的值,尽管他们之和可能并没有达到SGA的最大限制。此外,一旦分配后,各个区的内存只能给本区使用,相互之间是不能共享的。拿SGA中两个最重要的内存区Buffer Cache和Shared Pool来说,它们两个对实例的性能影响最大,但是就有这样的矛盾存在:在内存资源有限的情况下,某些时候数据被cache的需求非常大,为了提高buffer hit,就需要增加Buffer Cache,但由于SGA有限,只能从其他区“抢”过来——如缩小Shared Pool,增加Buffer Cache;而有时又有大块的PLSQL代码被解析驻入内存中,导致Shared Pool不足,甚至出现4031错误,又需要扩大Shared Pool,这时可能又需要人为干预,从Buffer Cache中将内存夺回来。

    10g 以后有了新特性:自动共享内存管理(Automatic Shared Memory Management ASMM)。而控制这一特性的,也就仅仅是这一个参数SGA_TARGE。设置这个参数后,就不需要为每个内存区来指定大小了。SGA_TARGET指定了SGA可以使用的最大内存大小,而SGA中各个内存的大小由Oracle自行控制,不需要人为指定。Oracle可以随时调节各个区域的大小,使之达到系统性能最佳状态的个最合理大小,并且控制他们之和在SGA_TARGET指定的值之内。一旦给SGA_TARGET指定值后(默认为0,即没有启动ASMM),就自动启动了ASMM特性。如果不设置SGA_TARGET,则自动共享内存管理功能被禁止。

     

    设置了SGA_TARGET后,以下的SGA内存区就可以由ASMM来自动调整:

    共享池(Shared Pool)

    Java池(Java Pool)

    大池(Large Pool)

    数据缓存区(Buffer Cache)

    流池(Streams Pool)

    对于SGA_TARGET的限制,它的大小是不能超过SGA_MAX_SIZE的大小的。

     

    要注意的是:当指定SGA_TARGET小于SGA_MAX_SIZE,实例重启后,SGA_MAX_SIZE就自动变为和SGA_TARGET一样的值了。

     

    SGA_TARGET,它的值可以动态修改(在SGA_MAX_SIZE范围内)。在10g之前,如果需要修改SGA的大小(即修改SGA_MAX_SIZE的值)需要重启实例才能生效。当然,在10g中,修改SGA_MAX_SIZE的值还是需要重启的。但是有了SGA_TARGET后,可以将SGA_MAX_SIZE设置偏大,再根据实际需要调整SGA_TARGET的值(我个人不推荐频繁修改SGA的大小,SGA_TARGET在实例启动时设置好,以后不要再修改)。

    SGA_TARGET带来一个重要的好处就是,能使SGA的利用率达到最佳,从而节省内存成本。因为ASMM启动后,Oracle会自动根据需要调整各个区域的大小,大大减少了某些区域内存紧张,而某些区域又有内存空闲的矛盾情况出现。这也同时大大降低了出现4031错误的几率。

     

     

    1.1 Database Buffer Cache

    Buffer Cache是SGA区中专门用于存放从数据文件中读取的的数据块拷贝的区域。Oracle进程如果发现需要访问的数据块已经在buffer cache中,就直接读写内存中的相应区域,而无需读取数据文件,从而大大提高性能(内存的读取效率是磁盘读取效率的14000倍)。Buffer cache对于所有oracle进程都是共享的,即能被所有oracle进程访问。

    和Shared Pool一样,buffer cache被分为多个集合,这样能够大大降低多CPU系统中的争用问题。

     

    1.1.1 Buffer cache的管理

    Oracle对于buffer cache的管理,是通过两个重要的链表实现的:写链表和最近最少使用链表(the Least Recently Used LRU)。写链表所指向的是所有脏数据块缓存(即被进程修改过,但还没有被回写到数据文件中去的数据块,此时缓冲中的数据和数据文件中的数据不一致)。而LRU链表指向的是所有空闲的缓存、pin住的缓存以及还没有来的及移入写链表的脏缓存。空闲缓存中没有任何有用的数据,随时可以使用。而pin住的缓存是当前正在被访问的缓存。LRU链表的两端就分别叫做最近使用端(the Most Recently Used MRU)和最近最少使用端(LRU)。

     

    1)Buffer cache的数据块访问

    当一个Oracle进程访问一个缓存时,这个进程会将这块缓存移到LRU链表中的MRU。而当越来越多的缓冲块被移到MRU端,那些已经过时的脏缓冲(即数据改动已经被写入数据文件中,此时缓冲中的数据和数据文件中的数据已经一致)则被移到LRU链表中LRU端。

    当一个Oracle用户进程第一次访问一个数据块时,它会先查找buffer cache中是否存在这个数据块的拷贝。如果发现这个数据块已经存在于buffer cache(即命中cache hit),它就直接读从内存中取该数据块。如果在buffer cache中没有发现该数据块(即未命中cache miss),它就需要先从数据文件中读取该数据块到buffer cache中,然后才访问该数据块。命中次数与进程读取次数之比就是我们一个衡量数据库性能的重要指标:buffer hit ratio(buffer命中率),可以通过以下语句获得自实例启动至今的buffer命中率:

    SQL>select (1-(sum(decode(name, 'physical reads',value,0))/(sum(decode(name, 'db block gets',value,0))
             +sum(decode(name,'consistent gets',value,0))))) * 100 "Hit Ratio"  from v$sysstat;

     Hit Ratio

    ----------

    98.3471481

    一个良好性能的系统,命中率一般保持在95%左右。


    上面提到,如果未命中(missed),则需要先将数据块读取到缓存中去。这时,oracle进程需要从空闲列表种找到一个适合大小的空闲缓存。如果空闲列表中没有适合大小的空闲buffer,它就会从LRU端开始查找LRU链表,直到找到一个可重用的缓存块或者达到最大查找块数限制。在查找过程中,如果进程找到一个脏缓存块,它将这个缓存块移到写链表中去,然后继续查找。当它找到一个空闲块后,就从磁盘中读取数据块到缓存块中,并将这个缓存块移到LRU链表的MRU端。

    当有新的对象需要请求分配buffer时,会通过内存管理模块请求分配空闲的或者可重用的buffer。“free buffer requested”就是产生这种请求的次数;

    当请求分配buffer时,已经没有适合大小的空闲buffer时,需要从LRU链表上获取到可重用的buffer。但是,LRU链表上的buffer并非都是立即可重用的,还会存在一些块正在被读写或者已经被别的用户所等待。根据LRU算法,查找可重用的buffer是从链表的LRU端开始查找的,如果这一段的前面存在这种不能理解被重用的buffer,则需要跳过去,查找链表中的下一个buffer。“free buffer inspected”就是被跳过去的buffer的数目。

    如果Oracle用户进程达到查找块数限制后还没有找到空闲缓存,它就停止查找LRU链表,并且通过信号同志DBW0进程将脏缓存写入磁盘去。

     

    2) 全表扫描

    当发生全表扫描(Full Table Scan)时,用户进程读取表的数据块,并将他们放在LRU链表的LRU端(和上面不同,不是放在MRU端)。这样做的目的是为了使全表扫描的数据尽快被移出。因为全表扫描一般发生的频率较低,并且全表扫描的数据块大部分在以后都不会被经常使用到。

    而如果你希望全表扫描的数据能被cache住,使之在扫描时放在MRU端,可以通过在创建或修改表(或簇)时,指定CACHE参数。

     

    3) Flush Buffer

    回顾一下前面一个用户进程访问一个数据块的过程,如果访问的数据块不在buffer cache中,就需要扫描LRU链表,当达到扫描块数限制后还没有找到空闲buffer,就需要通知DBW0将脏缓存回写到磁盘。如果一个系统中存在大量的脏缓冲,那么就可能导致用户进程访问数据性能下降。

    我们可以通过人工干预将所有脏缓冲回写到磁盘去,这就是flush buffer。

    在9i,可以用以下语句:

    alter system set events = 'immediate trace name flush_cache'; --9i

    在10g,可以用以下方式(9i的方式在10g仍然有效):

    alter system flush buffer_cache; -- 10g

    另外,9i的设置事件的方式可以是针对系统全部的,也可以是对会话的(即将该会话造成的脏缓冲回写)。

     

    1.1.2 Buffer Cache的重要参数配置

     

     1)  Buffer Cache的大小配置

    由于Buffer Cache中存放的是从数据文件中来的数据块的拷贝,因此,它的大小的计算也是以块的尺寸为基数的。而数据块的大小是由参数db_block_size指定的。9i以后,块的大小默认是8K,它的值一般设置为和操作系统的块尺寸相同或者它的倍数。

    而参数db_block_buffers则指定了Buffer Cache中缓存块数。因此,buffer cache的大小就等于db_block_buffers * db_block_size。

    在9i以后,Oracle引入了一个新参数:db_cache_size。这个参数可以直接指定Buffer Cache的大小,而不需要通过上面的方式计算出。它的默认值48M,这个数对于一个系统来说一般是不够用的。

    10G中提供了自动内存管理,通过使用sga_target在在多个组件间自动分配内存以保证最有效的内存使用.如shared pool javapool largepool buffer cache都无需显式设置这些组件的大小,默认都是0,当某个组件需要内存时,可以通过内部自动调整机制请求内存转移.

     

    注意:db_cache_size和db_block_buffers是不能同时设置的,否则实例启动时会报错。

    SQL> alter system set db_block_buffers=16384 scope=spfile;

    system altered.

    SQL> alter system set db_cache_size=20M scope=memory;

    system altered.

    SQL> startup force

    ORA-00381: cannot use both new and old parameters for buffer cache size specification

     

    9i中,推荐使用db_cache_size来指定buffer cache的大小。

    在OLTP系统中,对于DB_CACHE_SIZE的设置,推荐配置是:

    DB_CACHE_SIZE = SGA_MAX_SIZE/2~ SGA_MAX_SIZE*2/3

    最后,DB_CACHE_SIZE是可以联机修改的,即实例无需重启,除非增大Buffer Cache导致SGA实际大小大于SGA_MAX_SIZE。

     

     

    2) 多种块尺寸系统中的Buffer Cache的配置

    从9i开始,Oracle支持创建不同块尺寸的表空间,并且可以为不同块尺寸的数据块指定不同大小的buffer cache。

    9i以后,除了SYSTEM表空间和TEMPORARY表空间必须使用标准块尺寸外,所有其他表空间都可以最多指定四种不同的块尺寸。而标准块尺寸还是由上面的所说的参数db_block_size来指定。而db_cache_size则是标致块尺寸的buffer cache的大小。

    非标准块尺寸的块大小可以在创建表空间(CREATE TABLESPACE)是通过BLOCKSIZE参数指定。而不同块尺寸的buffer cache的大小就由相应参数DB_nK_CACHE_SZIE来指定,其中n可以是2,4,8,16或者32。例如,你创建了一个块大小为16K的非标准块尺寸的表空间,你就可以通过设置DB_16K_CACHE_SIZE为来指定缓存这个表空间数据块的buffer cache的大小。

    任何一个尺寸的Buffer Cache都是不可以缓存其他尺寸的数据块的。因此,如果你打算使用多种块尺寸用于你的数据库的存储,你必须最少设置DB_CACHE_SIZE和DB_nK_CACHE_SIZE中的一个参数(10g后,指定了SGA_TARGET就可以不需要指定Buffer Cache的大小)。并且,你需要给你要用到的非标准块尺寸的数据块指定相应的Buffer Cache大小。这些参数使你可以为系统指定多达4种不同块尺寸的Buffer Cache。

    另外,注意一点,DB_nK_CACHE_SIZE参数不能设定标准块尺寸的缓冲区大小。举例来说,如果 DB_BLOCK_SIZE设定为 4K,就不能再设定 DB_4K_CACHE_SIZE参数。

     

    3)多缓冲池

    可以配置不同的buffer cache,可以达到不同的cache数据的目的。比如,可以设置一部分buffer cache缓存过的数据在使用后后马上释放,使后来的数据可以立即使用缓冲池;还可以设置数据进入缓冲池后就被keep住不再释放。部分数据库对象(表、簇、索引以及分区)可以控制他们的数据缓存的行为,而这些不同的缓存行为就使用不同缓冲池。

    保持缓冲池(Keep Buffer Pool)用于缓存那些永久驻入内存的数据块。它的大小由参数DB_KEEP_CACHE_SZIE控制;

    回收缓冲池(Recycle Buffer Pool)会立即清除那些不在使用的数据缓存块。它的大小由参数DB_RECYLE_CACHE_SIZE指定;

    默认的标准缓存池,也就是上面所说的DB_CACHE_SIZE指定。

     

    这三个参数相互之间是独立的。并且他们都只适用于标准块尺寸的数据块。与8i兼容参数DB_BLOCK_BUFFERS相应的,DB_KEEP_CACHE_SIZE对应有BUFFER_POOL_KEEP、DB_RECYLE_CACHE_SIZE对应有BUFFER_POOL_RECYCLE。同样,这些参数之间是互斥的,即DB_KEEP_CACHE_SIZE和BUFFER_POOL_KEEP之间只能设置一个。

     

    4) 缓冲池建议器

    从9i开始,Oracle提供了一些自动优化工具,用于调整系统配置,提高系统性能。建议器就是其中一种。建议器的作用就是在系统运行过程中,通过监视相关统计数据,给相关配置在不同情况下的性能效果,提供给DBA做决策,以选取最佳的配置。

    9i中,Buffer Cache就有了相应的建议器。参数db_cache_advice用于该建议器的开关,默认值为FALSE(即关)。当设置它为TRUE后,在系统运行一段时间后,就可以查询视图v$db_cache_advice来决定如何使之DB_CACHE_SIZE了。关于这个建议器和视图,我们会在下面的内容中介绍。

     

    5) 其他相关参数

    DB_BLOCK_LRU_LATCHES

    LRU链表作为一个内存对象,对它的访问是需要进行锁(latch)控制的,以防止多个用户进程同时使用一个空闲缓存块。DB_BLOCK_LRU_LATCHES设置了LUR latch的数量范围。Oracle通过一系列的内部检测来决定是否使用这个参数值。如果这个参数没有设置,Oracle会自动为它计算出一个值。一般来说,oracle计算出来的值是比较合理,无需再去修改。

    9i以后这个参数是隐含参数。对于隐含参数,我建议在没有得到Oracle支持的情况下不要做修改,否则,如果修改了,Oracle是可以拒绝为你做支持的。

     

    DB_WRITER_PROCESSES

    在前面分析Oracle读取Buffer Cache时,提到一个Oracle重要的后台进程DBW0,这个(或这些)进程负责将脏缓存块写回到数据文件种去,称为数据库书写器进程(Database Writer Process)。DB_WRITER_PROCESSES参数配置写进程的个数,各个进程以DBWn区分,其中n>=0,是进程序号。一般情况下,DB_WRITER_PROCESSES = MAX(1, TRUNC(CPU数/8))。也就是说,CPU数小于8时,DB_WRITER_PROCESSES为1,即只有一个写进程DBW0。这对于一般的系统来说也是足够用。当你的系统的修改数据的任务很重,并且已经影响到性能时,可以调整这个参数。这个参数不要超过CPU数,否则多出的进程也不会起作用,另外,它的最大值不能超过20。

    DBWn进程除了上面提到的在用户进程读取buffer cache时会被触发,还能被Checkpoint触发(Checkpoint是实例从redo log中做恢复的起始点)。

     

     

    1.2 Share Pool

    SGA中的共享池由库缓存(Library Cache)、字典缓存(Dictionary Cache)、用于并行执行消息的缓冲以及控制结构组成。

    Shared Pool的大小由参数SHARED_POOL_SIZE决定。9i中,在32位系统下,这个参数的默认值是8M,而64位系统下的默认值位64M。最大为4G。 10g 以后可以通过SGA_TARGET 参数来自动调整。 

    对于Shared Pool的内存管理,是通过修正过的LRU算法表来实现的。

     

    1.2.1 库缓存(Library Cache)

    Library Cache中包括共享SQL区(Shared SQL Areas)、PL/SQL存储过程以及控制结构(如锁、库缓存句柄)。

    任何用户都可以访问共享SQL区(可以通过v$sqlarea访问,随后会介绍这个重要视图)。因此库缓存存在于SGA的共享池中。

    1) 共享SQL区和私有SQL区

    Oracle会为每一条SQL语句运行(每运行一条语句Oracle都会打开一个游标)提供一个共享SQL区(Shared SQL Areas)和私有SQL区(Private SQL Areas属于PGA)。当发现两个(或多个)用户都在运行同一SQL语句时,Oracle会重新组织SQL区,使这些用户能重用共享SQL区。但他们还会在私有SQL区中保存一份这条SQL语句的拷贝。

    一个共享SQL区中保存了一条语句的解析树和查询计划。在多用户系统中,Oracle通过为SQL语句使用同一共享SQL区多次运行来节省内存。

    当一条新的SQL语句被解析时,Oracle从共享池中分配一块内存来存储共享SQL区。这块内存的大小与这条语句的复杂性相关。如果Shared Pool不够空间分配给共享SQL区,Oracle将释放从LRU链表中查找到最近最少使用的内存块,直到有足够空间给新的语句的共享SQL区。如果Oracle释放的是一个共享SQL区的内存,那么相应的语句在下次执行时需要再次解析并重新分配共享SQL区。而从解析语句到分配共享SQL区是一个比较消耗CPU的工程。这就是为什么我们提倡使用绑定变量的原因了。在没有使用绑定变量时,语句中的变量的数值不同,oracle就视为一条新的语句(9i后可以通过cursor_sharing来控制),重复上面的解析、内存分配的动作,将大大消耗系统资源,降低系统性能。

     

    2) PL/SQL程序单元

    Oracle对于PL/SQL程序单元(存储过程、函数、包、匿名PL/SQL块和触发器)的处理过程和对单个的SQL语句的处理过程相似。它会分配一个共享区来存储被解析、编译过的程序单元。同时分配一个私有区域来存放运行程序单元的会话所指定的程序单元的参数值(包括本地变量、全局变量和包变量——这也叫做包的实例化)和用于执行程序所需的内存。如果多个用户运行同一个程序单元,则他们共享同一个共享区域,并且各自保持一份私有区域,用于用户会话中指定的变量值。

    而一个PL/SQL程序单元中的每条单个SQL语句的处理过程则和上面描述的SQL语句的处理过程相同。要注意一点,尽管这些语句是从PL/SQL程序单元中来的,但是Oracle还是会为这些语句分配一块共享SQL区,同时为每个用户分配一个相应的私有SQL区。

     

     

    1.2.2 字典缓存(Dictionary Cache)

     

    数据字典是有关于数据库的参考信息、数据库的结构信息和数据库中的用户信息的一组表和视图的集合,如我们常用到的V$视图、DBA_视图都属于数据字典。在SQL语句解析的过程中,Oracle可以非常迅速的访问(如果需要的话)这些数据字典,在SQL Trace中,这种对数据字典的访问就被统计为回调(recursive calls)。

    因为Oracle对数据字典访问如此频繁,因此内存中有两处地方被专门用于存放数据字典。一个地方就是数据字典缓存(Data Dictionary Cache)。数据字典缓存也被称为行缓存(Row Cache),因为它是以记录行为单元存储数据的,而不像Buffer Cache是以数据块为单元存储数据。内存中另外一个存储数据字典的地方是库缓存。所有Oracle的用户都可以访问这两个地方以获取数据字典信息。

     

     

    1.2.3 共享池的内存管理

     

    通常来说,共享池是根据修正过的LRU算法来是否其中的对象(共享SQL区和数据自动记录行)的,否则这些对象就一直保持在共享池中。如果共享池需要为一个新对象分配内存,并且共享池中没有足够内存时,内存中那些不经常使用的对象就被释放掉。一个被许多会话使用过的共享池对象,即使最初创建它的进程已经结束,只要它是有用的,都会被修正过的LRU算法一直保持在共享池中。这样就使一个多用户的Oracle系统对SQL语句的处理和内存消耗最小。

    注意,即使一个共享SQL区与一个打开的游标相关,但如果它长时间没有被使用,它还是可能会被从共享池中释放出来。而此时如果打开的游标还需要运行它的相关语句,Oracle就会重新解析语句,并分配新的共享SQL区。

     

    当一条SQL语句被提交给Oracle执行,Oracle会自动执行以下的内存分配步骤:

    1. Oracle检查共享池,看是否已经存在关于这条语句的共享SQL区。如果存在,这个共享SQL区就被用于执行这条语句。而如果不存在,Oracle就从共享池中分配一块新的共享SQL区给这条语句。同时,无论共享SQL区存在与否,Oracle都会为用户分配一块私有SQL区以保存这条语句相关信息(如变量值)。

    2. Oracle为会话分配一个私有SQL区。私有SQL区的所在与会话的连接方式相关。

     

    在以下情况下,Oracle也会将共享SQL区从共享池中释放出来:

    1)当使用ANALYZE语句更新或删除表、簇或索引的统计信息时,所有与被分析对象相关的共享SQL区都被从共享池中释放掉。当下一次被释放掉的语句被执行时,又重新在一个新的共享SQL区中根据被更新过的统计信息重新解析。

    2) 当对象结构被修改过后,与该对象相关的所有共SQL区都被标识为无效(invalid)。在下一次运行语句时再重新解析语句。

    3)如果数据库的全局数据库名(Global Database Name)被修改了,共享池中的所有信息都会被清空掉。

    4)DBA通过手工方式清空共享池:ALTER SYSTEM FLUSH SHARED_POOL;

     

    Shared Pool能被分成几个区域,分别被不同的latch(latch数最大为7,可以通过隐含参数_kghdsidx_count设置)保护。

    表x$kghlu可以查看shared pool中的LRU列表。当满足以下条件之一时,shared pool会分为多个区,分别有不同的LRU链表管理:

    1)在10g之前版本,如果shared pool大于128M、CPU数量大于4;

    2)Oracle数据库版本为10g

    这时,在x$kghlu中就会对应不同记录。

     

    1.2.4 保留共享池

    前面提到,如果Oracle解析一个 PL/SQL程序单元,也需要从共享池中分配内存给这些程序单元对象。由于这些对象本一般比较大(如包),所以分配的内存空间也相对较大。系统经过长时间运行后,共享池可能存在大量内存碎片,导致无法满足对于大块内存段的分配。

    为了使有足够空间缓存大程序块,Oracle专门从共享池内置出一块区域来来分配内存保持这些大块。这个保留共享池的默认大小是共享池的5%。它的大小也可以通过参数SHARED_POOL_RESERVED_SIZE来调整。保留区是从共享池中分配,不是直接从SGA中分配的,它是共享池的保留部分,用于存储大块段。

    Shared Pool中内存大于5000字节的大段就会被存放在共享池的保留部分。而这个大小限制是通过隐含参数_SHARED_POOL_RESERVED_MIN_ALLOC来设定的(如前面所说,隐含参数不要去修改它)。除了在实例启动过程中,所有小于这个数的内存段永远都不会放到保留部分中,而大于这个值的大内存段也永远不会存放到非保留区中,即使共享池的空间不够用的情况下也是如此。

    保留区的空闲内存也不会被包含在普通共享池的空闲列表中。它会维护一个单独的空闲列表。保留池也不会在它的LRU列表中存放可重建(Recreatable关于内存段的各种状态我们在后面的内容中再介绍)段。当释放普通共享池空闲列表上的内存时是不会清除这些大段的,同样,在释放保留池的空闲列表上的大内存段时也不会清除普通共享池中内存。

    通过视图V$SHARED_POOL_RESERVED可以查到保留池的统计信息。其中字段REQUEST_MISSES记录了没有立即从空闲列表中得到可用的大内存段请求次数。这个值要为0。因为保留区必须要有足够个空闲内存来适应那些短期的内存请求,而无需将那些需要长期cache住的没被pin住的可重建的段清除。否则就需要考虑增大SHARED_POOL_RESERVED_SIZE了。

    可以通过观察视图V$SHARED_POOL_RESERVED的MAX_USED_SPACE字段来判断保留池的大小是否合适。大多数情况下,你会观察到保留池是很少被使用的,也就是说5%的保留池空间可能有些浪费。但这需要经过长期观察来决定是否需要调整保留池大小。

    保留区使用shared pool的LRU链表来管理内存块,但是在做扫描时,相互是不受影响的。例如,内存管理器扫描shared pool的LRU链表,清出空间以分配给一个小于5000字节的内存请求,是不会清出保留区的内存块的,相反亦然。

     

     

    1.2.5 将重要、常用对象保持(Keep)在共享池中

    根据LRU算法,一些一段时间没有使用到的内存块会被情况释放。这就可能导致一些重要的对象(如一个含有大量通用算法函数的包、被cache的序列)被从内存中清除掉。这些对象可能只是间歇被使用,但是因为他们的处理过程复杂(不仅包本身重新分配内存、解析,还要检查里面的所有语句),要在内存中重建他们的代价非常大。

    我们可以通过调用存储过程DBMS_SHARED_POOL.KEEP将这些对象保持在共享池中来降低这种风险。这个存储过程立即将对象及其从事对象载入library cache中,并将他们都标记为保持(Keeping)状态。对于这种对象,我们建议在实例启动时就Keep住,以减少内存碎片的几率。

    有一种观点认为那些大对象(如包)是没有必要被Keep住的,因为他们会被保持在共享池的保留区(如前所述,这个区通常使用率很低),所以一般不可能被清出。这个观点是错误滴!因为大多数大对象实际上是被分为多个小的内存段被载入共享池的,因此根本不会因为对象的大小而受到特别的保护。

    另外,也不要通过频繁调用某些对象以防止他们被从共享池中清出。如果共享池大小设置合理,在系统运行的高峰时期,LRU链表会相对较短,那些没有被pin住的对象会很快被清出,除非他们被keep住了。

     

    1.2.6  关于Shared Pool的重要参数

      1)  SHARED_POOL_SIZE

    它指定了Shared Pool的大小。9i下,在32位系统中,这个参数的默认值是8M,而64位系统中的默认值位64M。

    但是,在SGA中还存在一块叫内部SGA消耗(Internal SGA Overhead)的内存被放置在共享池中。在9i及之前版本,共享池的统计大小(通过v$sgastat视图统计)为SHARED_POOL_SIZE +内部SGA消耗大小。而10g以后,SHARED_POOL_SIZE就已经包含了这部分内存大小。因此在10g中,共享池的实际使用大小就是SHARED_POOL_SIZE -内部SGA消耗大小,这在配置共享池大小时需要考虑进去,否则,扶过SHARED_POOL_SIZE设置过小,在实例启动时就会报ORA-00371错误。

     

    2) SHARED_POOL_RESERVED_SIZE

    这个参数前面已经提到,指定了共享池中缓存大内存对象的保留区的大小。这里不再赘述。

     

    3)  _SHARED_POOL_RESERVED_MIN_ALLOC

    这个参数前面也已经介绍,设置了进入保留区的对象大小的阀值。

     

     

     

    1.3 重做日志缓存(Redo Log Buffer)

    Redo Log Buffer是SGA中一段保存数据库修改信息的缓存。这些信息被存储在重做条目(Redo Entry)中.重做条目中包含了由于INSERT、UPDATE、DELETE、CREATE、ALTER或DROP所做的修改操作而需要对数据库重新组织或重做的必须信息。在必要时,重做条目还可以用于数据库恢复。

    重做条目是Oracle数据库进程从用户内存中拷贝到Redo Log Buffer中去的。重做条目在内存中是连续相连的。后台进程LGWR负责将Redo Log Buffer中的信息写入到磁盘上活动的重做日志文件(Redo Log File)或文件组中去的。

    参数LOG_BUFFER决定了Redo Log Buffer的大小。它的默认值是512K(一般这个大小都是足够的),最大可以到4G。10g中可通过参数自动设置。当系统中存在很多的大事务或者事务数量非常多时,可能会导致日志文件IO增加,降低性能。这时就可以考虑增加LOG_BUFFER。

    但是,Redo Log Buffer的实际大小并不是LOB_BUFFER的设定大小。为了保护Redo Log Buffer,oracle为它增加了保护页(一般为11K):

    SQL> select * from v$sgastat where name = 'log_buffer';

     

    POOL         NAME       BYTES

    ------------ -------------------------- ----------

                 log_buffer     7135232

     

    SQL> show parameter log_buffer

     

    NAME                    TYPE        VALUE

    ------------------------------------ ----------- ----------------

    log_buffer                 integer     7024640

    SQL>

     

    1.4 大池(large pool)

    大池是SGA中的一块可选内存池,根据需要时配置。在以下情况下需要配置大池:

    1) 用于共享服务(Shared Server MTS方式中)的会话内存和Oracle分布式事务处理的Oracle XA接口

    2) 使用并行查询(Parallel Query Option PQO)时

    3)  IO服务进程

    4) Oracle备份和恢复操作(启用了RMAN时)

    通过从大池中分配会话内存给共享服务、Oracle XA或并行查询,oracle可以使用共享池主要来缓存共享SQL,以防止由于共享SQL缓存收缩导致的性能消耗。此外,为Oracle备份和恢复操作、IO服务进程和并行查询分配的内存一般都是几百K,这么大的内存段从大池比从共享池更容易分配得到。

    参数LARGE_POOL_SIZE设置大池的大小。大池是属于SGA的可变区(Variable Area)的,它不属于共享池。对于大池的访问,是受到large memory latch保护的。大池中只有两种内存段:空闲(free)和可空闲(freeable)内存段。它没有可重建(recreatable)内存段,因此也不用LRU链表来管理(这和其他内存区的管理不同)。大池最大大小为4G。

    为了防止大池中产生碎片,隐含参数_LARGE_POOL_MIN_ALLOC设置了大池中内存段的最小大小,默认值是16K(同样,不建议修改隐含参数)。

    此外,large pool是没有LRU链表的。

     

    1. 5 Java池(Java Pool)

    Java池也是SGA中的一块可选内存区,它也属于SGA中的可变区。

    Java池的内存是用于存储所有会话中特定Java代码和JVM中数据。Java池的使用方式依赖与Oracle服务的运行模式。

    Java池的大小由参数JAVA_POOL_SIZE设置。Java Pool最大可到1G。

    在Oracle 10g以后,提供了一个新的建议器——Java池建议器——来辅助DBA调整Java池大小。建议器的统计数据可以通过视图V$JAVA_POOL_ADVICE来查询。如何借助建议器调整Java池的方法和使用Buffer Cache建议器类似,可以参考Buffer Cache中关于建议器部分。

     

    1.6   流池(Streams Pool)

    流池是Oracle 10g中新增加的。是为了增加对流的支持。

    流池也是可选内存区,属于SGA中的可变区。它的大小可以通过参数STREAMS_POOL_SIZE来指定。如果没有被指定,oracle会在第一次使用流时自动创建。如果设置了SGA_TARGET参数,Oracle会从SGA中分配内存给流池;如果没有指定SGA_TARGET,则从buffer cache中转换一部分内存过来给流池。转换的大小是共享池大小的10%。

    Oracle同样为流池提供了一个建议器——流池建议器。建议器的统计数据可以通过视图V$STREAMS_POOL_ADVICE查询。使用方法参看Buffer Cache中关于优化器部分。

     

    二. PGA

    PGA(Program Global Area程序全局区)是一块包含一个服务进程的数据和控制信息的内存区域。它是Oracle在一个服务进程启动是创建的,是非共享的。一个Oracle进程拥有一个PGA内存区。一个PGA也只能被拥有它的那个服务进程所访问,只有这个进程中的Oracle代码才能读写它。因此,PGA中的结构是不需要Latch保护的。

    我们可以设置所有服务进程的PGA内存总数受到实例分配的总体PGA(Aggregated PGA)限制。

    在专有服务器(Dedicated Server)模式下,Oracle会为每个会话启动一个Oracle进程;而在多线程服务(Multi-Thread Server MTS)模式下,由多个会话共享通一个Oracle服务进程。

    PGA中包含了关于进程使用到的操作系统资源的信息,以及一些关于进程状态的信息。而关于进程使用的Oracle共享资源的信息则是在SGA中。这样做可以使在进程以外中止时,能够及时释放和清除这些资源。

      

    •Stack Space是用来存储用户会话变量和数组的存储区域;    

    •User Session Data是为用户会话使用的附加存储区。      

    |--Session Information     

    |--Sort Area     

    |--Cursor Information      

    注意Session information(用户会话信息)在独占服务器中与在共享服务器中所处的内存区域是不同的。

     

    2.1  PGA的组成

    PGA由两组区域组成:固定PGA和可变PGA(或者叫PGA堆,PGA Heap【堆——Heap就是一个受管理的内存区】)。固定PGA和固定SGA类似,它的大小时固定的,包含了大量原子变量、小的数据结构和指向可变PGA的指针。

    可变PGA是一个内存堆。它的内存段可以通过视图X$KSMPP(另外一个视图X$KSMSP可以查到可变SGA的内存段信息,他们的结构相同)查到。PGA堆包含用于存放X$表的的内存(依赖与参数设置,包括DB_FILES、CONTROL_FILES)。

     

    总的来说,PGA的可变区中主要分为以下三部分内容:

    1)私有SQL区;

    2)游标和SQL区

    3)会话内存

     

    2.1.1 私有SQL区(Private SQL Area)

    私有SQL区包含了绑定变量值和运行时期内存结构信息等数据。每一个运行SQL语句的会话都有一个块私有SQL区。所有提交了相同SQL语句的用户都有各自的私有SQL区,并且他们共享一个共享SQL区。因此,一个共享SQL区可能和多个私有共享区相关联。

     

    一个游标的私有SQL区又分为两个生命周期不同的区:

     永久区:包含绑定变量信息。当游标关闭时被释放。

     运行区:当执行结束时释放。

    创建运行区是一次执行请求的第一步。对于INSERT、UPDATE和DELETE语句,Oracle在语句运行结束时释放运行区。对于查询操作,Oracle只有在所有记录被fetch到或者查询被取消时释放运行区。

     

    2.1.2  游标和SQL区(Cursors and SQL Areas)

    一个Oracle预编译程序或OCI程序的应用开发人员能够很明确的打开一个游标,或者控制一块特定的私有SQL区,将他们作为程序运行的命名资源。另外,oracle隐含的为一些SQL语句产生的递归调用(前面有介绍,读取数据字典信息)也使用共享SQL区。

    私有SQL区是由用户进程管理的。如何分配和释放私有SQL区极大的依赖与你所使用的应用工具。而用户进程可以分配的私有SQL区的数量是由参数OPEN_CURSORS控制的,它的默认值是50。

    在游标关闭前或者语句句柄被释放前,私有SQL区将一直存在(但其中的运行区是在语句执行结束时被释放,只有永久区一直存在)下去。应用开发人员可以通过将所有打开的不再使用的游标都关闭来释放永久区,以减少用户程序所占用的内存。

     

    2.1.3 会话内存(Session Memory)

    会话内存是一段用于保存会话变量(如登录信息)和其他预会话相关信息的内存。对于共享服务器模式下,会话内存是共享的,而不是私有的。

    对于复杂的查询(如决策支持系统中的查询),运行区的很大一部分被那些内存需求很大的操作分配给SQL工作区(SQL Work Area)。这些操作包括:

    基于排序的操作(ORDER BY、GROUP BY、ROLLUP、窗口函数);

     Hash Join

    Bitmap merge

    Bitmap create

     

    例如,一个排序操作使用工作区(这时也可叫排序区Sort Area)来将一部分数据行在内存排序;而一个Hash Join操作则使用工作区(这时也可以叫做Hash区 Hash Area)来建立Hash表。如果这两种操作所处理的数据量比工作区大,那就会将输入的数据分成一些更小的数据片,使一些数据片能够在内存中处理,而其他的就在临时表空间的磁盘上稍后处理。尽管工作区太小时,Bitmap操作不会将数据放到磁盘上处理,但是他们的复杂性是和工作区大小成反比的。因此,总的来说,工作区越大,这些操作就运行越快。

    工作区的大小是可以调整的。一般来说,大的工作区能让一些特定的操作性能更佳,但也会消耗更多的内存。工作区的大小足够适应输入的数据和相关的SQL操作所需的辅助的内存就是最优的。如果不满足,因为需要将一部分数据放到临时表空间磁盘上处理,操作的响应时间会增长。

     

    2.2  PGA内存自动管理

    SQL工作区可以是自动的、全局的管理。DBA只要设置参数PGA_AGGREGATE_TARGET给一个实例的PGA内存指定总的大小。设置这个参数后,Oracle将它作为一个总的全局限制值,尽量使所有Oracle服务进程的PGA内存总数不超过这个值。

    在这个参数出现之前,DBA要调整参数SORT_AREA_SIZE、 HASH_AREA_SIZE,、BITMAP_MERGE_AREA_SIZE和CREATE_BITMAP_AREA_SIZE(关于这些参数,我们会在后面介绍),使性能和PGA内存消耗最佳。对这些参数的调整是非常麻烦的,因为即要考虑所有相关的操作,使工作区适合它们输入数据大小,又要使PGA内存不消耗过大导致系统整体性能下降。

    9i以后,通过设置了参数PGA_AGGREGATE_TARGET,使所有会话的工作区的大小都是自动分配。同时,所有*_AREA_SIZE参数都会失效。在任何时候,实例中可用于工作区的PGA内存总数都是基于参数PGA_AGGREGATE_TARGET的。工作区内存总数等于参数PGA_AGGREGATE_TARGET的值减去系统其他组件(如分配给会话的PGA内存)的内存消耗。分配给Oracle进程的PGA内存大小是根据它们对内存的需求情况来的。

    参数WORKAREA_SIZE_POLICY决定是否使用PGA_AGGREGATE_TARGET来管理PGA内存。它有两个值:AUTO和MANUAL。默认是AUTO,即使用PGA_AGGREGATE_TARGET来管理PGA内存。其实,从参数WORKAREA_SIZE_POLICY的名字上可以看出,Oracle的PGA内存自动管理只会调整工作区部分,而非工作区部分(固定PGA区)则不会受影响。

    还有注意一点就是:10g之前,PGA_AGGREGATE_TARGET只在专用服务模式下生效。而10g以后,PGA内存自动管理在专有服务模式(Dedicated Server)和MTS下都有效。另外,9i在OpenVMS系统上还不支持PGA内存自动管理,但10g支持。

    设置了PGA_AGGREGATE_TARGET以后,每个进程PGA内存的大小也是受限制的:

    串行操作时,每个进程可用的PGA内存为MIN(PGA_AGGREGATE_TARGET * 5%, _pga_max_size/2),其中隐含参数_pga_max_size的默认值是200M,同样不建议修改它。

    并行操作时,并行语句可用的PGA内存为PGA_AGGREGATE_TARGET * 30% / DOP(Degree Of Parallelism并行度)。

     

    2.3  专有服务(Dedicated Server)和共享服务(Shared Server)

    对PGA内存的管理和分配,很大程度上依赖与服务模式。下面这张表显示了在不同模式下,PGA内存不同部分的分配的异同:

    内存区

    专有服务

    共享服务

    会话内存

    私有的

    共享的

    永久区所在区域

    PGA

    SGA

    SELECT语句的运行区所在区域

    PGA

    PGA

    DML/DDL语句的运行区所在区域

    PGA

    PGA

      

    三. UGA (The User Global Area)

    PGA是一段包含一个Oracle服务或后台进程的数据和控制信息的内存。PGA的大小依赖与系统的配置。在专用服务(Dedicated Server)模式下,一个服务进程与一个用户进程相关,PGA就包括了堆空间和UGA。而UGA(User Global Area用户全局区)由用户会话数据、游标状态和索引区组成。在共享服务(MTS)模式下,一个共享服务进程被多个用户进程共享,此时UGA是Shared Pool或Large Pool的一部分(依赖与配置)。

    许多DBA都不理解PGA和UGA之间的区别。其实这种区别可以简单的理解为进程和会话直接的区别。在专用服务模式下,进程和会话是一对一的;而在MTS模式下,进程和会话是一对多的关系。PGA是服务于进程的,它包含的是进程的信息;而UGA是服务于会话的,它包含的是会话的信息。因此,MTS模式下,PGA和UGA之间的关系也是一对多的。

     

    UGA中包含了一个会话的信息,包括:

    1)打开游标的永久区和运行区;

    2)包的状态信息,特别是包的变量;

    3)Java会话的信息;

    4)激活的角色;

    5)激活的跟踪事件(ALTER SESSION SET EVENT …);

    6)起作用的NLS参数(SELECT * FROM NLS_SESSION_PARAMETERS;);

    7)所有打开的db link;

    8)会话对于信任的Oracle的托管访问标记(mandatory access control (MAC)

     

    和PGA一样,UGA也由两组区组成,固定UGA和可变UGA(或者说UGA堆)。固定UGA包含了大概70个原子变量、小的数据结构以及指向UGA堆的指针。

     

    UGA heap中的段可以通过表X$KSMUP查到(它的结构和X$KSMSP相同)。UGA堆包含了存储一些固定表(X$表)的永久内存(依赖与特定参数的设置,如OPEN_CURSORS,OPEN_LINKS和MAX_ENABLED_ROLES)。除此以外,大部分的UGA用于私有SQL区。UGA内存的所在依赖于会话的设置。在专用服务模式下,会话和进程是一对一的关系,UGA位于PGA中。固定UGA是PGA中的一段内存段,而UGA堆是PGA的子堆。在MTS模式下,固定UGA是shared pool中的一段内存段,而UGA堆是Large Pool的子堆,如果从large pool分配失败,则从shared pool中分配。

    MTS模式下,可以通过Profile中的PRIVATE_SGA项(通过dba_profiles查看)来控制每个UGA占用的SGA的总的大小,但是不建议这样做。

    Oracle 9.2以后,有一个新的隐含参数:_use_realfree_heap。当设置这个参数为true时,Oracle会为CGA、UGA单独分配堆,而不从PGA中分配。它的默认值为false,而当设置了pga_aggregate_target后,它的值自动被改为true。

     

    四. CGA (The Call Global Area)

    与其他的全局区不同,CGA(Call Global Area调用全局区)的存在是瞬间的。它只存在于一个调用过程中。对于实例的一些低层次的调用需要CGA,包括:

    1)解析一条SQL语句;

    2)执行一条SQL语句;

    3)取一条SELECT语句的输出值。

     

    如果语句产生了递归调用,则需要为每个递归调用分配一个CGA。如上所述,递归调用是在语句解析、优化器产生语句查询计划、DML操作时需要查询或修改数据字典信息的调用。

    无论UGA存在于PGA还是SGA,CGA都是PGA的subheap。因为无论那种模式,会话在做调用时总需要一个进行进行处理。这一点很重要,特别是在MTS模式下时,如果发现一次调用很久没有响应,则可能需要增加PGA的大小。

    当然,调用并不是只通过CGA中的数据结构来工作。实际上,调用所需要的大部分的重要数据结构都来自于UGA。例如私有SQL取和排序区都存放在UGA中,因为调用结束后,它们是被保留的。CGA中只包含了那些调用结束后可以被释放的数据。例如,CGA中包含了直接IO缓存、关于递归调用的信息、用于表达式评估(产生查询计划时)的的堆空间和其他一些临时数据。

    Java调用内存也分配在CGA中。它被分为三部分空间:堆空间、新空间和老空间。在调用期间(调用长短依赖于使用期长短和大小),在新空间和老空间中的内存段不再使用的内存段将被垃圾收集器回收。

     

    五.软件代码区(Software Code Area)

    软件代码区是一部分用于存放那些正在运行和可以被运行的代码(Oracle自身的代码)的内存区。Oracle代码一般存储在一个不同于用户程序存储区的软件代码区,而用户程序存储区是排他的、受保护的区域。

    软件区的大小一般是固定的,只有Oracle软件升级或重装后才会改变。在不同操作系统下,这部分区域所要求的大小也不同。

    软件区是只读的,可以被安装成共享的或非共享的。可能的情况下,Oracle代码是共享的,这样所有Oracle用户都可以直接访问这些代码,而不需要各自保存一份拷贝在自己的内存中。这样可以节省大量内存并提高整体性能。

    而用户程序也可以是共享的或非共享的。一些Oracle工具(如SQL Plus)能被安装成共享的,但有些不能。如果一台机器运行多个实例,这些实例可以使用同一个Oracle代码区。

    另外要注意的是:并不是所有操作系统都能将软件区安装成共享的,如Windows。


    展开全文
  • 本篇基本是韦东山书上的 一、内存管理单元MMU介绍 内存管理单元简称MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问权限检查。MMU使得个用户进程拥有自己独立的地址空间,并通过内存访问...一个
  • 目录 综述 使用malloc分配内存块 ...C 语言为内存的分配和管理提供了几函数。这些函数可以在 &lt;stdlib.h&gt; 头文件中找到。 序号 函数和描述 1 void *malloc(int num);  在堆...
  • 文章分2部分来记录,第部分就是讲述当虚拟地址传递给cpu后,cpu是如何解析它,把它装换成物理地址,再把这地址送到内存总线上;第二部分就是要讨论MMU是如何控制存储空间的访问权限的。这里我先述说第部分:...
  • . 内存 简介 1. 两大内存分类 ( 1 ) DRAM 简介 ( 定期刷新 | 速度慢 | 成本低 ) (2) SRAM 简介 ( 不需刷新 | 存取速度快 | 功耗大 | 成本高 ) 2. DRAM 分类 ( SDRAM | DDR | DDR2 ) (1) SDRAM 简介 ( 动态...
  • WinCE内存管理

    千次阅读 2012-08-08 10:02:16
    一个WinCE 系统可能只有4MB 的RAM,这相对于个人电脑来说是十分少的,因为个人电脑的标准配置已经到了128MB 甚至更多。事实上,运行WinCE 的机器的内存十分缺乏,以至于有时候有必要在写程序的时候为节约内存而...
  • Disruptor是LMAX公司开源的一个高效的内存无锁队列。这两天看了一下相关的设计文档和博客,下面尝试进行一下总结。 第一部分。引子 谈到并发程序设计,有几个概念是避免不了的。 1.锁 :锁是用来做并发最简单...
  • 内存泄露及检测

    千次阅读 2013-04-04 12:12:32
    内存泄露是指程序在运行过程中动态申请的内存空间不再使用后没有及时释放,从而很可能导致应用程序内存无线增长。更广义的内存泄露包括未对系统的资源的及时释放,比如句柄等。 内存溢出即用户在对其数据缓冲区...
  • tid=86980 第23章 STM32H7的MPU内存保护单元(重要) ...本章节为大家讲解STM32H7学习中的一个重要知识点MPU(Memory Protection Unit,内存保护单元),早在STM32F1和F4芯片上面也是有这个功能的,但是基本用不上...
  • 存储系统

    千次阅读 2016-12-16 14:13:12
    存储系统、主存、磁盘存储器
  • 内存泄露、内存溢出以及解决方法

    千次阅读 2017-07-03 11:28:24
    内存溢出即用户在对其数据缓冲区操作时,超过了其缓冲区的边界;尤其是对缓冲区写操作时,缓冲区的溢出很可能导致程序的异常。 A) 比如在程序中多使用strcpy_s、memcpy_s等具有缓冲区大小检查的函数,去取代strcpy...
  • 维数组占用内存空间,每个存储单元的地址是连续的,通过下标识别元素,它的下标就代表了他的存储单元序号,也就表示了它的位置。 查找顺序表中的元素是方便的,根据下标就可以取出要取的元素。 当顺序表的容量...
  • 数据结构 - 线性表链式存储结构

    千次阅读 2015-04-28 17:17:21
    存储链表中结点的组任意的存储单元可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。链表中结点的逻辑顺序和物理顺序不一定相同。(即不要求逻辑上相邻的元素在物理位置上也相邻) 为了...
  • 内存溢出和内存泄漏的区别

    千次阅读 2015-07-11 15:55:31
    比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少...
  • 种高效无锁内存队列的实现

    千次阅读 2017-02-22 17:49:33
    Disruptor是LMAX公司开源的一个高效的内存无锁队列。这两天看了一下相关的设计文档和博客,下面尝试进行一下总结。 第一部分。引子 谈到并发程序设计,有几个概念是避免不了的。 1.锁:锁是用来做并发最简单的方式,...
  • 在存储器中含有大量的基本单元,存储单元可以存放八个二进制位,即一个零到二百五十五之间的整数、一个字母或一个标点符号等,叫做一个字节。存储器的容量就是以字节为基本单位的,个单元都有唯一的序号,叫做...
  • 在数组中,个有定义的下标都与一个值对应,这个值称做数组元素。 个序对形如: (index,value) 数组的顺序表示和实现由于计算机的内存结构是一维的,因此用一维内存来表示多维数组,就必须按某种次序将数组元素...
  • Oracle 内存 架构

    千次阅读 2011-06-26 12:43:00
      Oracle 内存 架构 详解 Oracle的内存配置与oracle性能息息相关。关于内存的配置,是最影响Oracle性能的配置。... 先看Oracle内存存储的主要内容是什么:程序代码(PLSQL、Ja
  • 2、要求内存中可用的存储单元的地址必须是连续的。 优点: 1、存储密度大(=1),存储空间利用率高。 2、方法简单,各种高级语言中都有数组,容易实现。 3、不用为表示节点间的逻辑关系而增加额外的存储开销。 ...
  • 顺序存储二叉树

    千次阅读 2016-09-23 22:50:01
     二叉树的顺序存储,就是用一组连续的存储单元存放二叉树中的结点。 因此,必须把二叉树的所有结点安排成为一个恰当的序列,反映出节点中的逻辑关系 用编号的方法从树根起,自上层至下层,层自左至右地给...
  • 轻量级内存计算引擎

    千次阅读 2018-10-17 22:26:12
    内存计算指数据事先存储内存,各步骤中间结果不落硬盘的计算方式,适合性能要求较高,并发较大的情况。   HANA、TimesTen等内存数据库可实现内存计算,但这类产品价格昂贵结构复杂实施困难,总体拥有成本较高。...
  • Oracle 内存 架构 详解

    千次阅读 2013-05-05 15:06:08
    Oracle 内存 架构 详解  ...先看Oracle内存存储的主要内容是什么: 程序代码(PLSQL、Java); 关于已经连接的会话的信息,包括当前所有活动和非活动会话; 程序运行时必须的相关
  • 顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次... 将表中元素一个一个的存入一组连续的存储单元中,这种存储结构是顺序结构。  本代码默认list可以容纳的item数目为100
  • 逻辑地址-=>线性地址-=>物理地址前面我们提到了当使用80x86微处理器时,有三种不同的地址: 逻辑地址(logical address):包含在机器语言指令中用来指定一个...每一个逻辑地址都由一个段(segment)和偏移量(offset或dis

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,322
精华内容 7,328
关键字:

内存每一个存储单元序号