<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;fid=10&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo-zh / 内存管理模块]]></title>
		<link>http://www.gentoo-zh.org/index.php</link>
		<description><![CDATA[Gentoo-zh 最近发表的主题。]]></description>
		<lastBuildDate>Sun, 09 Oct 2022 03:22:07 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（十）：内存管理——内存分配和释放关键函数分析&ZGC垃圾回收]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=452&amp;action=new</link>
			<description><![CDATA[<p>上文介绍了buddy和slab内存管理的思路，本文看看这些算法的关键代码都是怎么写的，这里用的是4.9版本的源码；重新把这个图贴出来，方便后续理解代码！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211220174954196-302988982.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 1、如上图所示，slab算法的入口就是kmem_cache结构体了，和其他重要结构体管理的方式类似，这里也统一采用数组的形式管理所有的kmem_cache结构体，所以也会根据用户输入的参数从数组里面找到合适的kmem_cache结构体，如下：</p><div class="codebox"><pre class="vscroll"><code>/*
 * Conversion table for small slabs sizes / 8 to the index in the
 * kmalloc array. This is necessary for slabs &lt; 192 since we have non power
 * of two cache sizes there. The size of larger slabs can be determined using
 * fls.
 * 通过size_index_elem求出8的倍数后，继续查找kmem_cache的index；
 * 从这里可以直观看到用户申请不同size内存对应的kmem_cache的index
 */
static s8 size_index[24] = {
    3,    /* 8 */
    4,    /* 16 */
    5,    /* 24 */
    5,    /* 32 */
    6,    /* 40 */
    6,    /* 48 */
    6,    /* 56 */
    6,    /* 64 */
    1,    /* 72 */
    1,    /* 80 */
    1,    /* 88 */
    1,    /* 96 */
    7,    /* 104 */
    7,    /* 112 */
    7,    /* 120 */
    7,    /* 128 */
    2,    /* 136 */
    2,    /* 144 */
    2,    /* 152 */
    2,    /* 160 */
    2,    /* 168 */
    2,    /* 176 */
    2,    /* 184 */
    2    /* 192 */
};
/*
8字节对齐，也就是8的多少倍?
*/
static inline int size_index_elem(size_t bytes)
{
    return (bytes - 1) / 8;
}

/*
 * Find the kmem_cache structure that serves a given size of
 * allocation
 */
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
    int index;

    if (unlikely(size &gt; KMALLOC_MAX_SIZE)) {
        WARN_ON_ONCE(!(flags &amp; __GFP_NOWARN));
        return NULL;
    }
    /*如果用户申请的size小于192byte*/
    if (size &lt;= 192) {
        if (!size)
            return ZERO_SIZE_PTR;

        index = size_index[size_index_elem(size)];
    } else
    /*如果用户申请的size大于192byte*/
        index = fls(size - 1);

#ifdef CONFIG_ZONE_DMA
    if (unlikely((flags &amp; GFP_DMA)))
        return kmalloc_dma_caches[index];

#endif
    return kmalloc_caches[index];
}</code></pre></div><p>&#160; 上面的size_index数组看起来可能还有些抽象，这里有个更直接的映射表：object的size和kmem_cache的index、名字之间的映射关系，比如用户申请96byte的内存，那么这个kmem_cache的index就是1，和size_index数组刚好吻合；</p><div class="codebox"><pre><code>/*
 * kmalloc_info[] is to make slub_debug=,kmalloc-xx option work at boot time.
 * kmalloc_index() supports up to 2^26=64MB, so the final entry of the table is
 * kmalloc-67108864.
 */
static struct {
    const char *name;
    unsigned long size;
} const kmalloc_info[] __initconst = {
    {NULL,                      0},        {&quot;kmalloc-96&quot;,             96},
    {&quot;kmalloc-192&quot;,           192},        {&quot;kmalloc-8&quot;,               8},
    {&quot;kmalloc-16&quot;,             16},        {&quot;kmalloc-32&quot;,             32},
    {&quot;kmalloc-64&quot;,             64},        {&quot;kmalloc-128&quot;,           128},
    {&quot;kmalloc-256&quot;,           256},        {&quot;kmalloc-512&quot;,           512},
    {&quot;kmalloc-1024&quot;,         1024},        {&quot;kmalloc-2048&quot;,         2048},
    {&quot;kmalloc-4096&quot;,         4096},        {&quot;kmalloc-8192&quot;,         8192},
    {&quot;kmalloc-16384&quot;,       16384},        {&quot;kmalloc-32768&quot;,       32768},
    {&quot;kmalloc-65536&quot;,       65536},        {&quot;kmalloc-131072&quot;,     131072},
    {&quot;kmalloc-262144&quot;,     262144},        {&quot;kmalloc-524288&quot;,     524288},
    {&quot;kmalloc-1048576&quot;,   1048576},        {&quot;kmalloc-2097152&quot;,   2097152},
    {&quot;kmalloc-4194304&quot;,   4194304},        {&quot;kmalloc-8388608&quot;,   8388608},
    {&quot;kmalloc-16777216&quot;, 16777216},        {&quot;kmalloc-33554432&quot;, 33554432},
    {&quot;kmalloc-67108864&quot;, 67108864}
};</code></pre></div><p>&#160; 最关键的就是index的计算方法了；小于192byte直接查表，大于192byte调用fls，find last bit set函数查找index，代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*
 * fls = Find Last Set in word
 * @result: [1-32]
 * fls(1) = 1, fls(0x80000000) = 32, fls(0) = 0
 */
static inline __attribute__ ((const)) int fls(unsigned long x)
{
    if (__builtin_constant_p(x))
           return constant_fls(x);

    return 32 - clz(x);
}

/*
 * Count the number of zeros, starting from MSB
 * Helper for fls( ) friends
 * This is a pure count, so (1-32) or (0-31) doesn&#039;t apply
 * It could be 0 to 32, based on num of 0&#039;s in there
 * clz(0x8000_0000) = 0, clz(0xFFFF_FFFF)=0, clz(0) = 32, clz(1) = 31
 */
static inline __attribute__ ((const)) int clz(unsigned int x)
{
    unsigned int res;

    __asm__ __volatile__(
    &quot;    norm.f  %0, %1        \n&quot;
    &quot;    mov.n   %0, 0        \n&quot;
    &quot;    add.p   %0, %0, 1    \n&quot;
    : &quot;=r&quot;(res)
    : &quot;r&quot;(x)
    : &quot;cc&quot;);

    return res;
}</code></pre></div><p>&#160; 核心作用是返回输入参数的最高有效bit位（从低位往左数最后的有效bit位）的序号，该序号与常规0起始序号不同，它是1起始的（当没有有效位时返回0）；</p><p>&#160; 要点：</p><p>&#160; &#160; &#160; kmem_cachess数组最多13个元素，也就是说最多有13种不同size的object；<br />&#160; &#160; &#160; &#160; &#160; &#160;如果用户申请的size小于192byte，直接查size_index数组就可以找到kmem_cache的index<br />&#160; &#160; &#160; &#160; &#160; &#160;如果用户申请的size大于192byte，返回输入参数的最高有效bit位（从低位往左数最后的有效bit位）的序号</p><p>&#160; 2、kmem_cache得到后，下一步就是找到slab了，linux是这样干的：</p><div class="codebox"><pre><code>static __always_inline void *
slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller)
{
    unsigned long save_flags;
    void *objp;

    flags &amp;= gfp_allowed_mask;
    cachep = slab_pre_alloc_hook(cachep, flags);
    if (unlikely(!cachep))
        return NULL;

    cache_alloc_debugcheck_before(cachep, flags);
    /*保存flag寄存器的状态，然后关中断，避免被中断切换进程*/
    local_irq_save(save_flags);
    /*得到了返回值*/
    objp = __do_cache_alloc(cachep, flags);
    /*重新打开中断*/
    local_irq_restore(save_flags);
    objp = cache_alloc_debugcheck_after(cachep, flags, objp, caller);
    prefetchw(objp);

    if (unlikely(flags &amp; __GFP_ZERO) &amp;&amp; objp)
        memset(objp, 0, cachep-&gt;object_size);

    slab_post_alloc_hook(cachep, flags, 1, &amp;objp);
    return objp;
}</code></pre></div><p>&#160; 函数的返回值objp是调用_do_cache_alloc返回的，重点继续分析这个函数（其他函数都是陪衬┭┮﹏┭┮）；经过一系列的调用链，最终来到了这里：返回值是objp，来自两个地方：先检查cpu的缓存(也就是array_cache数组)，如果还有就从缓存数组分配；如果没有就继续调用cache_alloc_refill从 3 个 slabs_(free/partial/full)中寻找可用的object，并将其填充到 array_cache 中；</p><div class="codebox"><pre class="vscroll"><code>static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
    void *objp;
    struct array_cache *ac;

    check_irq_off();
    /*先看array_cache缓存是不是还有*/
    ac = cpu_cache_get(cachep);
    if (likely(ac-&gt;avail)) {
        ac-&gt;touched = 1;
        objp = ac-&gt;entry[--ac-&gt;avail];

        STATS_INC_ALLOCHIT(cachep);
        goto out;
    }

    STATS_INC_ALLOCMISS(cachep);
    /*array_cache缓存没了再重新寻找可用的object*/
    objp = cache_alloc_refill(cachep, flags);
    /*
     * the &#039;ac&#039; may be updated by cache_alloc_refill(),
     * and kmemleak_erase() requires its correct value.
     */
    ac = cpu_cache_get(cachep);

out:
    /*
     * To avoid a false negative, if an object that is in one of the
     * per-CPU caches is leaked, we need to make sure kmemleak doesn&#039;t
     * treat the array pointers as a reference to the object.
     */
    if (objp)
        kmemleak_erase(&amp;ac-&gt;entry[ac-&gt;avail]);
    return objp;
}</code></pre></div><p>&#160; &#160;和objp相关的两个函数都操作了arry_cache，这究竟是个啥结构体了？如下：</p><div class="codebox"><pre><code>/*
 * struct array_cache
 *
 * Purpose:
 * - LIFO ordering, to hand out cache-warm objects from _alloc
 * - reduce the number of linked list operations
 * - reduce spinlock operations
 *
 * The limit is stored in the per-cpu structure to reduce the data cache
 * footprint.
 * 给每个cpu设置的对象缓存池
 */
struct array_cache {
    unsigned int avail;/*缓存池中可用的object数量*/
    unsigned int limit;/*最大object数量*/
    unsigned int batchcount;/*要从 slab_list 转移进本地高速缓存对象的数量，或从本地高速缓存中转移出去的 obj 数量*/
    unsigned int touched;/*从缓存池移除对象时设置为1，否则设置为0*/
    void *entry[];    /* 保存释放的object实例指针
             * Must have this definition in here for the proper
             * alignment of array_cache. Also simplifies accessing
             * the entries.
             */
};</code></pre></div><p>&#160; &#160;这个结构体带来的好处：</p><p>&#160; &#160; 提高cpu L1/L2/L3 cache的命中率：当此CPU上释放object后，如果再次申请一个相同大小的object时，原object很可能还在这个CPU的 L1/L2/L3 cache中，所以为每个CPU维护一个这样的链表，当需要新的object时，会优先尝试从当前CPU的本地CPU空闲object链表获取相应大小的object，此时很有可能命中cpu的L1/L2/L3 cache，提升分配效率；<br />&#160; &#160; 减少锁的竞争：假设多个CPU同时申请同样大小的object，为了互相同步，需要暂时多个cpu之间暂时互斥，导致分配效率降低；有个array_cache后，申请object时会先从cpu自己的array_cache查找，减少了cpu之间互斥的概率，提升分配效率；</p><p>&#160; &#160;结构体的字段确定后，就要填充了，在cache_alloc_refill函数种完成，代码如下：</p><div class="codebox"><pre class="vscroll"><code>static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
    int batchcount;
    struct kmem_cache_node *n;
    struct array_cache *ac, *shared;
    int node;
    void *list = NULL;
    struct page *page;
    /*确保中断已经关闭*/
    check_irq_off();
    node = numa_mem_id();

    ac = cpu_cache_get(cachep);
    batchcount = ac-&gt;batchcount;
    if (!ac-&gt;touched &amp;&amp; batchcount &gt; BATCHREFILL_LIMIT) {
        /*
         * If there was little recent activity on this cache, then
         * perform only a partial refill.  Otherwise we could generate
         * refill bouncing.
         */
        batchcount = BATCHREFILL_LIMIT;
    }
    n = get_node(cachep, node);

    BUG_ON(ac-&gt;avail &gt; 0 || !n);
    shared = READ_ONCE(n-&gt;shared);
    if (!n-&gt;free_objects &amp;&amp; (!shared || !shared-&gt;avail))
        goto direct_grow;

    spin_lock(&amp;n-&gt;list_lock);
    shared = READ_ONCE(n-&gt;shared);

    /* See if we can refill from the shared array */
    //从全局共享空闲对象中转移空闲对象到本地 CPU 空闲对象链表上
    if (shared &amp;&amp; transfer_objects(ac, shared, batchcount)) {
        shared-&gt;touched = 1;
        goto alloc_done;
    }

    while (batchcount &gt; 0) {
        /* Get slab alloc is to come from. */
        page = get_first_slab(n, false);//从 slab 中获取空闲 slab
        if (!page)
            goto must_grow;

        check_spinlock_acquired(cachep);
        /*从free slab中获取空闲object，转移到本地CPU空闲object链表上*/
        batchcount = alloc_block(cachep, ac, page, batchcount);
        fixup_slab_list(cachep, n, page, &amp;list);
    }

must_grow:
    n-&gt;free_objects -= ac-&gt;avail;
alloc_done:
    spin_unlock(&amp;n-&gt;list_lock);
    fixup_objfreelist_debug(cachep, &amp;list);

direct_grow:
    if (unlikely(!ac-&gt;avail)) {
        /* Check if we can use obj in pfmemalloc slab */
        if (sk_memalloc_socks()) {
            void *obj = cache_alloc_pfmemalloc(cachep, n, flags);

            if (obj)
                return obj;
        }
        /*当slab中都没有空闲object时，就从buddy系统中获取内存*/
        page = cache_grow_begin(cachep, gfp_exact_node(flags), node);

        /*
         * cache_grow_begin() can reenable interrupts,
         * then ac could change.
         */
        ac = cpu_cache_get(cachep);
        if (!ac-&gt;avail &amp;&amp; page)
            alloc_block(cachep, ac, page, batchcount);
        cache_grow_end(cachep, page);

        if (!ac-&gt;avail)
            return NULL;
    }
    ac-&gt;touched = 1;

    return ac-&gt;entry[--ac-&gt;avail];
}</code></pre></div><p>&#160; 整个array_cache填充过程总结如下：</p><p>&#160; &#160; 首先会从本地 CPU 空闲object链表中尝试获取一个object用于分配：如果失败，则检查所有CPU共享的空闲object链表链表中是否存在，并且空闲链表中是否存在空闲object，若有就转移 batchcount 个空闲object到本地 CPU空闲object链表中；<br />&#160; &#160; 如果上述失败，就尝试从 SLAB中分配；这时如果还失败，kmem_cache会尝试从页框分配器中获取一组连续的页框建立一个新的SLAB，然后从新的SLAB中获取一个对象。</p><p>&#160; &#160; &#160;相应的object释放流程（没在cache_alloc_refill）：</p><p>&#160; &#160; 首先会先将object释放到本地CPU空闲object链表中，如果本地CPU空闲object链表中对象过多，kmem_cache 会将本地CPU空闲object链表中的batchcount个对象移动到所有CPU共享的空闲object链表链表中<br />&#160; &#160; 如果所有CPU共享的空闲object链表链表的object也太多了，kmem_cache也会把所有CPU共享的空闲object链表链表中 batchcount 个数的object移回它们自己所属的SLAB中，<br />&#160; &#160; 这时如果SLAB中空闲对象太多，kmem_cache会整理出一些空闲的SLAB，将这些SLAB所占用的页框释放回页框分配器中。</p><p>&#160; cache_alloc_refill中比较关键的函数：从slab_partial和slab_free队列找空闲的object：</p><div class="codebox"><pre><code>static struct page *get_first_slab(struct kmem_cache_node *n, bool pfmemalloc)
{
    struct page *page;

    page = list_first_entry_or_null(&amp;n-&gt;slabs_partial,
            struct page, lru);
    if (!page) {
        n-&gt;free_touched = 1;
        page = list_first_entry_or_null(&amp;n-&gt;slabs_free,
                struct page, lru);
    }

    if (sk_memalloc_socks())
        return get_valid_first_slab(n, page, pfmemalloc);

    return page;
}</code></pre></div><p>&#160; 最后就是_kmalloc函数了：</p><div class="codebox"><pre><code>static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
    if (__builtin_constant_p(size)) {
        if (size &gt; KMALLOC_MAX_CACHE_SIZE)
            return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
        if (!(flags &amp; GFP_DMA)) {
            int index = kmalloc_index(size);

            if (!index)
                return ZERO_SIZE_PTR;

            return kmem_cache_alloc_trace(kmalloc_caches[index],
                    flags, size);
        }
#endif
    }
    return __kmalloc(size, flags);
}</code></pre></div><p>&#160; _kmalloc继续调用_do_kmalloc函数，核心调用的正式上面分析的kmalloc_slab函数和slab_alloc函数！</p><div class="codebox"><pre><code>/**
 * __do_kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 * @caller: function caller for debug tracking of the caller
 */
static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
                      unsigned long caller)
{
    struct kmem_cache *cachep;
    void *ret;
    /*根据用户参数找到高速缓存块*/
    cachep = kmalloc_slab(size, flags);
    if (unlikely(ZERO_OR_NULL_PTR(cachep)))
        return cachep;
    /*根据kmem_cache结构体和用户参数找到object，并返回地址*/
    ret = slab_alloc(cachep, flags, caller);

    kasan_kmalloc(cachep, ret, size, flags);
    trace_kmalloc(caller, ret,
              size, cachep-&gt;size, flags);

    return ret;
}</code></pre></div><p>总结：</p><p>&#160; &#160; &#160; &#160; &#160; 对于用户来说，需要用内存时直接调用malloc就行了，但是操作系统找空闲内存区域的时候可谓是大费周折（甚至还要关闭中断，避免分配的过程被打断；同时加锁，和其他cpu核之间互斥保持同步），这是malloc函数效率低的根本原因；如果客观条件允许，调用malloc函数后申请的内存建议反复使用（但是可能带来部分安全问题，这里需要平衡两者的权衡利弊），不要频繁地malloc和free！<br />&#160; &#160; &#160; &#160; &#160;slab机制比buddy复杂，各种概念伴随着层出不穷的结构体，但是万变不离其宗：（1）将内存进一步分割成更小的块，从8byte起步，尽可能避免碎片&#160; &#160;（2）不同size的内存块分开管理，提高分配和回收合并的效率； 以上所有的结构体都是围绕着这两点目的展开的，比如缓存array_cache、多级链式查找等！<br />&#160; &#160; &#160; &#160; &#160;多级链式建立和维护的时候比较复杂，比如下图的从kmem_cache_node→partital中分配object，最多需要经历4次链式跳转查询object，为啥不直接用bitmap管理，而是要搞得这么复杂了？注意观察：每次链式跳转查询都有不同的结构体承载，每个结构体都有自己特定的字段属性，由此达到分层解耦的目的！</p><div class="codebox"><pre><code>https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211221095658911-877821572.png</code></pre></div><p>&#160; &#160;内存操作函数概览：</p><div class="codebox"><pre><code>https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211221162242031-658490447.png</code></pre></div><br /><p>&#160; &#160; &#160; &#160;栈和堆的区别</p><p>&#160; 写过代码的人都知道，代码运行的时候数据主要存储在两块人为划分的区域：栈和堆；栈一般用来存参数、返回地址、局部变量；堆用来存new出来的对象实例、全局变量、静态变量等；碎片化问题主要集中在堆区域，重来没听过栈内存有碎片化问题的，这是为啥了？</p><p>&#160; 栈是通过sp指针管理的，sp减少或增加，意味着栈增加或减少，一般是函数调用前和函数调用完毕移动sp；栈内部存放的函数frame的大小就是函数参数、返回地址、局部变量的大小总和；编译器在编译阶段就能够精准计算函数frame的大小，所以函数调用前和调用后移动sp的距离也是能提前精准计算的，所以sp移动时给函数frame分配的空间也是精准的，这就是栈上没有碎片的根本原因！相比之下堆就不一样了: 堆要承担给用户app存放对象实例、全局变量、静态变量的重任，全局变量和静态变量还好，编译时就能确定大小，但是对象实例就不行了，这个完全依赖于运行时的情况确定，尤其时代码各种分支比较多的情况，所以堆内存的使用是没法提前预判的；为了增加内存分配的灵活性，就诞生了各种内存分配、GC的算法！</p><p>&#160; 3、尽管linux操作系统采取了buddy和slab等算法，但还是无法完全避免内存碎片；并且由于c/c++自生的限制，没有jvm这种自动垃圾回收的机制，导致了malloc或new出来的内存需要程序员人工手动释放；一旦忘记释放，就会一直占用着内存，直到程序运行结束，操作系统全部回收整个程序的内存才会释放。如果某些应用程序因为业务需要无法停止（比如服务器上跑的应用），必须一直运行，那么这块“被遗忘”的内存会一直被占用，可用内存会越来越少，最终可能导致内存严重不够，整个应用崩盘重来（这也是服务端开发java越来越流行的原因之一)！目前业界公认垃圾回收算法最牛的就属ZGC和shenandoah了，这里简单介绍一下ZGC算法的过程和牛逼之处！</p><p>&#160; （1）jvm也会把内存按照不同大小分块（官方给的称呼是分页，但是个cpu硬件对内存的分页不同），jvm分页的大小如下：</p><p>&#160; &#160; &#160; 小页面2MB，用于存储小于256KB的对象<br />&#160; &#160; &#160; &#160; &#160; &#160;中页面32MB，用于存储256KN~4MB的对象<br />&#160; &#160; &#160; &#160; &#160; &#160;大页面大小不固定，用于存储大于4MB的对象</p><p>&#160; （2）要回收垃圾，就要正确地识别垃圾，避免误伤正常的对象，ZGC是这样做的：找到GC root（线程栈变量、static变量、常量池、jni指针等），挨个查找这些对象内部的fileds，看看有没有引用其他的变量，由此找到直接的对象引用；</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231151215149-1756346668.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160;这个阶段称之为“初始标记”。为了确保标记的准确性，需要暂停所有线程的工作，官方称之为stop the world，简称STW；由于不需要像传统的GC算法那样扫描堆区域，所以再大的内存都不怕。从实测的数据看，此阶段耗时小于1ms；找到直接引用的对象后（比如下面的A对象），会把GC root指针的M0位置1，标明A对象正在经历GC流程！</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231153305109-1736810133.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; （3）经过初始标记，找到了GC root直连的对象，很明显此刻并不能回收，因为还有很多对象可能并不是CG&#160; root节点直接引用的，比如B、C节点（有可能是A节点用了链表、数组、new Object等方式生成了B、C节点）；为了提高效率，需要多个GC线程同时来标记所有的间接引用对象，此阶段称为“并发标记”；此阶段并不需要STW，所以并不影响业务线程的正常运行，时间长也不影响！B、C找到后，A-&gt;B和B-&gt;C引用指针的M0位同样标绿；此阶段有点像是多叉树或图的遍历！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231154119003-417479726.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;这一步并发标记方法称为“三色标记”法；整个阶段由于没有STW，并且是多个GC线程和业务线程一起协作，可能出现漏标的情况：GC1线程没有标记C节点，以为C是垃圾，要回收C节点的内存；但是GC2线程还未完成，此时业务线程执行了B.c=null，把B-&gt;C的引用去掉，CG2线程执行后也会人为C节点没有引用；实际上由于业务线程执行了A.c=C，导致C节点被A引用，本身并不是垃圾，明显被误伤了！ </p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231155858462-1024991803.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;为了避免C被误伤，jvm执行到A.c的时候需要把C先记录在remember set；为下一步的再标记做准备！使用的算法或技术叫snapshot at the begining，简称SATB，也就是在开始的时候拍快照，把代码中的引用关系保存下来，用作后续备查！</p><p>&#160; &#160; &#160; &#160;（4）上一步的并发标记由于没有STW，存在对象漏标的情况，可能导致正常引用的节点被当成垃圾回收；为了避免误伤，采用了remembor set的方式，一旦遇到A.c这种情况马上记录到set，然后通过set找到还在正常被使用的对象标记；为了精准标记、避免误伤，此阶段需要STW，让业务线程先暂停；由于只处理漏标的对象，实际测试看耗时小于1ms；此阶段称为再标记</p><p>&#160; &#160; （5）并发转移准备：经过前面3个阶段找到了垃圾，现在要着手准备删除垃圾、回收内存了；ZGC采用了copying算法：找到相同大小、还未使用过的页面，然后把正常引用的对象复制到新页面，旧页面存放的数据全部擦除，思路就是这么简单粗暴；此阶段没有STW，不影响正常的业务线程；</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231161734635-311632699.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; （6）初始转移：</p><p>&#160; &#160; &#160; &#160; &#160;由于需要移动对象，并且这些对象还有GC root的直接引用（没有引用的对象就是垃圾了，还需要转移么？），此阶段肯定要STW，耗时小于1ms（大部分是垃圾，需要转移的很少；并且此阶段只移动和GC root直连的对象，间接连接的对象此阶段部考虑）<br />&#160; &#160; &#160; &#160; &#160;把A复制到新页面，更改GC root的指针指向，同时更改颜色指针为蓝色，表示remapped移动阶段结束！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231164150005-2143514250.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;（7）并发转移</p><p>&#160; &#160; &#160; &#160; &#160;B和C转移到新页面，但是B和C的引用怎么转移了？在旧页面维护了转发表Forward table（和GC root不直连的对象都会在转发表），类似hashtable，把实例的新旧地址做了对应<br />&#160; &#160; &#160; &#160; &#160;此阶段没有STW的，如果有线程正在需要读B和C怎么办了？此时就需要读屏障了：等B和C搬迁到新地址后再读（通过转发表找到），这个有点类似自旋，汇编层面通过jmp slow_path实现；额外开销约4%；那么问题又来了：业务线程怎么知道B和C线程有没有搬迁到新地址了？还是通过对象引用指针的着色确定的；如果4个bit都是0，说明改引用是good color，对象可以直接读写；如果4bit中有任何bit是1，就是bad color，说明正处于GC流程，这种对象是不能马上使用的！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231164750759-1644807903.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; &#160;（8）还剩最后一步了：B和C复制到新页面后，原有的引用关系还没恢复了，这个问题什么时候解决了？这就要等到下一次GC启动后的第二阶段：对象重定位阶段进行！这个阶段A-&gt;B和B-&gt;C的指针都是绿色的，表示上次GC遗留下来没有更新的指针！最终的结果如下：A、B和C在新页面建立引用关系，指针改成红色！</p><p>&#160; &#160; B和C这种间接引用，为啥要放到第二次GC才做了？为啥不第一次就完结了？因为这种可达性分析没必要做两次，可以直接复用第一次的forward table！所以也不需要STW业务线程！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231200419393-2025546659.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; 最后通过实测，每个阶段耗时如下：需要STW的是初始标记、再标记和初始转移阶段，3个阶段加起来耗时约0.7ms，这效率杠杠滴！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211231181827709-16516782.png" alt="FluxBB bbcode 测试" /></span></p><p>参考：</p><p>1、https://cloud.tencent.com/developer/article/1622989&#160; slab分配一个object的流程分析</p><p>2、https://www.dingmos.com/index.php/archives/23/ slab分配器</p><p>3、https://www.bilibili.com/video/BV1cb4y117Vg?p=6&amp;spm_id_from=pageDriver&#160; ZGC垃圾回收过程</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 09 Oct 2022 03:22:07 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=452&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（九）：内存管理——buddy和slab]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=451&amp;action=new</link>
			<description><![CDATA[<p>cpu硬件管理内存是以页（4KB）为最小颗粒度的，因为页描述符设置内存属性就是按照页为单位设置的！这个颗粒度是非常大的，用户如果只要几十Byte的内存也分配4KB的话，再多的内存也会很快被败光，同时带来了内存碎片化的问题，所以迫切需要小颗粒度的内存分配方式！buddy和slab孕育而生！</p><p>&#160; 1、先看看buddy内存管理方式；linux早期版本（比如0.11）管理的方式比较简单粗暴，直接用bitmap的思路标记物理页是否被使用，这样做带来最直接的问题：内存碎片化！举例如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211217172223922-1576335679.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; &#160; &#160;比如标黄的是已经分配的物理页（由于是操作系统负责分配物理页，首次是可以按照顺序从低地址到高地址依次不留空隙地分配），没标注的是剩余的物理页。随着时间的推移，部分进程运行完毕后释放了物理页，可能变成了如下情况：进程释放了3个物理页，但这3个物理页还是分开的，并未连接起来；带来的问题：</p><p>&#160; &#160; &#160; 如果有进程需要4个连续的物理页，要么继续等，要么从其他内存地址开始寻找.......<br />&#160; &#160; &#160; &#160; &#160; &#160;bitmap也不能直观的快速寻址连续空闲的内存，每次都要从头开始遍历查找，效率也低......<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211217172455684-1214636798.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; 这里说个题外话：应用程序调用malloc分配内存的时候，操作系统会通过各种算法在空闲的内存找一块大小满足应用程序需求的内存，这是比较耗时的，所以站在提高效率的角度，建议一次性调用malloc申请足够的内存，然后反复使用；不过这样做的也带来了安全问题：逆向时如果用CE找到了这块内存，就可以继续定位关键的数据生成代码了！</p><p>&#160; 为了避免出现页级别的内存碎片，Linux内核中引入了伙伴系统算法(Buddy system)：把所有的空闲页框分组为11个块链表，每个块链表分别包含大小为1，2，4，8，16，32，64，128，256，512和1024个连续页框的页框块。最大可以申请1024个连续页框，对应4MB大小的连续内存，每个页框块的第一个页框的物理地址是该块大小的整数倍，如下：</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211217181654472-798857625.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;假设要申请一个256个页框的块，先从256个页框的链表中查找空闲块。如果没有，就去512个页框的链表中找，找到了则将页框块分为2个256个页框的块，一个分配给应用，另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块，继续向1024个页框的链表查找，如果仍然没有，则返回错误。页框块在释放时，会主动将两个连续的页框块合并为一个较大的页框块；和linux早期简单粗暴的bitmap管理方式对比，buddy算法明显是利用了链表结构管理物理页，不停地对页框做拆开合并拆开合并的动作，算法牛逼之处在于运用了世界上任何正整数都可以由2^n的和组成的原理，让任何物理页数量的分配都能够满足（只要空闲的物理页面足够）！</p><p>&#160; &#160;为了实现buddy算法，需要的几个结构体：list_head是双向循环链表，该链表包含每个空闲页框块(2^k)的起始页框的page。指向链表中相邻元素的指针存放在page的lru字段中（lru在页非空闲时用于其它目的），nr_free表示空闲块的个数；</p><div class="codebox"><pre><code>struct free_area {
    struct list_head    free_list;
    unsigned long        nr_free;
};</code></pre></div><p>&#160; zone结构体中&#160; &#160;struct free_area free_area[MAX_ORDER];&#160; &#160;free_area数组就快速用来定位链表起点；数组的第K个元素就是2^k个页框链表的起始点；</p><div class="codebox"><pre class="vscroll"><code>/**
 * 内存管理区描述符
 */
struct zone {
    /* Fields commonly accessed by the page allocator */
    /**
     * 管理区中空闲页的数目
     */
    unsigned long        free_pages;
    /**
     * Pages_min-管理区中保留页的数目
     * Page_low-回收页框使用的下界。同时也被管理区分配器为作为阈值使用。
     * pages_high-回收页框使用的上界，同时也被管理区分配器作为阈值使用。
     */
    unsigned long        pages_min, pages_low, pages_high;
    /*
     * We don&#039;t know if the memory that we&#039;re going to allocate will be freeable
     * or/and it will be released eventually, so to avoid totally wasting several
     * GB of ram we must reserve some of the lower zone memory (otherwise we risk
     * to run OOM on the lower zones despite there&#039;s tons of freeable ram
     * on the higher zones). This array is recalculated at runtime if the
     * sysctl_lowmem_reserve_ratio sysctl changes.
     */
    /**
     * 为内存不足保留的页框，分别为各种内存域指定了若干页
     * 用于一些无论如何都不能失败的关键性内存分配
     */
    unsigned long        lowmem_reserve[MAX_NR_ZONES];
    /**
     * 用于实现单一页框的特殊高速缓存。
     * 每内存管理区对每CPU都有一个。包含热高速缓存和冷高速缓存。
     * 内核使用这些列表来保存可用于满足实现的“新鲜”页。
     * 有些页帧很可能在CPU高速缓存中，因此可以快速访问，称之为热。
     * 未缓存的页帧称之为冷的。
     */
    struct per_cpu_pageset    pageset[NR_CPUS];

    /*
     * free areas of different sizes
     */
    /**
     * 保护该描述符的自旋锁
     */
    spinlock_t        lock;
    /**
     * 标识出管理区中的空闲页框块。
     * 包含11个元素，被伙伴系统使用。分别对应大小的1,2,4,8,16,32,128,256,512,1024连续空闲块的链表。
     * 第k个元素标识所有大小为2^k的空闲块。free_list字段指向双向循环链表的头。
     * free_list是free_area的内部结构，是个双向环回链表节点。
     */
    struct free_area    free_area[MAX_ORDER];

    /* 
     * 为了cache line对齐加的pad
     */
    ZONE_PADDING(_pad1_)

    /* Fields commonly accessed by the page reclaim scanner */
    /**
     * 活动以及非活动链表使用的自旋锁。
     */
    spinlock_t        lru_lock;    
    /**
     * 管理区中的活动页链表
     */
    struct list_head    active_list;
    /**
     * 管理区中的非活动页链表。
     */
    struct list_head    inactive_list;
    /**
     * 回收内存时需要扫描的活动页数。
     */
    unsigned long        nr_scan_active;
    /**
     * 回收内存时需要扫描的非活动页数目
     */
    unsigned long        nr_scan_inactive;
    /**
     * 管理区的活动链表上的页数目。
     */
    unsigned long        nr_active;
    /**
     * 管理区的非活动链表上的页数目。
     */
    unsigned long        nr_inactive;
    /**
     * 管理区内回收页框时使用的计数器。
     */
    unsigned long        pages_scanned;       /* since last reclaim */
    /**
     * 在管理区中填满不可回收页时此标志被置位
     */
    int            all_unreclaimable; /* All pages pinned */

    /*
     * prev_priority holds the scanning priority for this zone.  It is
     * defined as the scanning priority at which we achieved our reclaim
     * target at the previous try_to_free_pages() or balance_pgdat()
     * invokation.
     *
     * We use prev_priority as a measure of how much stress page reclaim is
     * under - it drives the swappiness decision: whether to unmap mapped
     * pages.
     *
     * temp_priority is used to remember the scanning priority at which
     * this zone was successfully refilled to free_pages == pages_high.
     *
     * Access to both these fields is quite racy even on uniprocessor.  But
     * it is expected to average out OK.
     */
    /**
     * 临时管理区的优先级。
     */
    int temp_priority;
    /**
     * 管理区优先级，范围在12和0之间。
     */
    int prev_priority;


    ZONE_PADDING(_pad2_)
    /* Rarely used or read-mostly fields */

    /*
     * wait_table        -- the array holding the hash table
     * wait_table_size    -- the size of the hash table array
     * wait_table_bits    -- wait_table_size == (1 &lt;&lt; wait_table_bits)
     *
     * The purpose of all these is to keep track of the people
     * waiting for a page to become available and make them
     * runnable again when possible. The trouble is that this
     * consumes a lot of space, especially when so few things
     * wait on pages at a given time. So instead of using
     * per-page waitqueues, we use a waitqueue hash table.
     *
     * The bucket discipline is to sleep on the same queue when
     * colliding and wake all in that wait queue when removing.
     * When something wakes, it must check to be sure its page is
     * truly available, a la thundering herd. The cost of a
     * collision is great, but given the expected load of the
     * table, they should be so rare as to be outweighed by the
     * benefits from the saved space.
     *
     * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the
     * primary users of these fields, and in mm/page_alloc.c
     * free_area_init_core() performs the initialization of them.
     */
    /**
     * 进程等待队列的散列表。这些进程正在等待管理区中的某页。
     */
    wait_queue_head_t    * wait_table;
    /**
     * 等待队列散列表的大小。
     */
    unsigned long        wait_table_size;
    /**
     * 等待队列散列表数组的大小。值为2^order
     */
    unsigned long        wait_table_bits;

    /*
     * Discontig memory support fields.
     */
    /**
     * 内存节点。
     */
    struct pglist_data    *zone_pgdat;
    /** 
     * 指向管理区的第一个页描述符的指针。这个指针是数组mem_map的一个元素。
     */
    struct page        *zone_mem_map;
    /* zone_start_pfn == zone_start_paddr &gt;&gt; PAGE_SHIFT */
    /**
     * 管理区的第一个页框的下标。
     */
    unsigned long        zone_start_pfn;

    /**
     * 以页为单位的管理区的总大小，包含空洞。
     */
    unsigned long        spanned_pages;    /* total size, including holes */
    /**
     * 以页为单位的管理区的总大小，不包含空洞。
     */
    unsigned long        present_pages;    /* amount of memory (excluding holes) */

    /*
     * rarely used fields:
     */
    /**
     * 指针指向管理区的传统名称：DMA、NORMAL、HighMem
     */
    char            *name;
} ____cacheline_maxaligned_in_smp;</code></pre></div><p>&#160; &#160;本质上讲，buddy算法相对于原始的bitmap，优势在于：</p><p>&#160; &#160; &#160;按照不同页框个数2^n重新组织了内存，便于快速查找用户所需大小的内存块（free_area数组的寻址时间复杂度是O(1)）<br />&#160; &#160; &#160; &#160;不停的分配、重组页框，在一定程度上减少了页框碎片</p><p>&#160; 2、buddy算法解决了页框颗粒度的碎片，但用户日常使用一般申请的内存大都是几十、顶多几百byte，直接分配4KB太大了，需要进一步把4KB的页框内存细分，以满足更小颗粒度的需求，slab算法由此诞生！其解决了如下3个问题：</p><p>&#160; &#160; &#160;解决buddy按照页的颗粒度分配小内存的碎片问题<br />&#160; &#160; &#160; &#160;缓存部分常用的数据结构（包括但不限于inode、dir_entry、task_struct等），减少操作系统分配、回收对象时调整内存的时间开销<br />&#160; &#160; &#160; &#160;通过着色更好地利用cpu硬件的高速缓存cache，允许不同缓存中的对象占用相同的缓存行，从而提高缓存的利用率并获得更好的性能</p><p>&#160; 整个slab机制可以用下面的图来概括：</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211218215042609-1084584328.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; &#160; 图从左往右看：</p><p>&#160; &#160; 内存会存储多个叫做kmem_cache的结构体实例，实例之间通过链表的形式连接！<br />&#160; &#160; 每个kmem_cache包含3个不同的slab队列，分别是free、partial、full，分别表示空间、部分使用和完全使用的slab队列<br />&#160; &#160; 每个slab又包含一个或多个page（一般情况是一个），这里就和buddy系统关联起来了<br />&#160; &#160; 每个slab包含多个object对象，但是不同slab包含对象的大小是不一样的，比如上图的第一个kmem_cache所包含object对象大小是32byte，第二个kmem_cache所包含object对象大小是128byte，最后一个是32KB；</p><p>&#160; &#160; &#160; &#160;为了实现slab机制，相应的结构体是少不了的，linux用的是kmem_cache结构体来承载和聚合各种信息的，如下：重要的属性都用中文注释了，比如object的大小、每个slab占用的页面数量、slab结构体数组等；</p><div class="codebox"><pre class="vscroll"><code>/*
 * Definitions unique to the original Linux SLAB allocator.
   slab描述符
 */

struct kmem_cache {
    struct array_cache __percpu *cpu_cache;/*本地cpu缓存池*/

/* 1) Cache tunables. Protected by slab_mutex */
    unsigned int batchcount;
    unsigned int limit;
    unsigned int shared;

    unsigned int size;
    struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc &amp; free from the backend */

    unsigned int flags;        /* constant flags */
    unsigned int num;        /* # of objs per slab：每个slab中object的数量 */

/* 3) cache_grow/shrink */
    /* order of pgs per slab (2^n) ：每个slab占用的页面数量*/
    unsigned int gfporder;

    /* force GFP flags, e.g. GFP_DMA */
    gfp_t allocflags;

    size_t colour;            /* cache colouring range：一个slab中不同cache line的数量 */
    unsigned int colour_off;    /* colour offset */
    struct kmem_cache *freelist_cache;/*打造单向链表*/
    unsigned int freelist_size;

    /* constructor func */
    void (*ctor)(void *obj);

/* 4) cache creation/removal */
    const char *name;/*slab描述符名字*/
    struct list_head list;
    int refcount;
    int object_size;/*onject对象的大小，每个kmem_cache可以个性化设置的*/
    int align;/*对齐长度*/

/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
    unsigned long num_active;
    unsigned long num_allocations;
    unsigned long high_mark;
    unsigned long grown;
    unsigned long reaped;
    unsigned long errors;
    unsigned long max_freeable;
    unsigned long node_allocs;
    unsigned long node_frees;
    unsigned long node_overflow;
    atomic_t allochit;
    atomic_t allocmiss;
    atomic_t freehit;
    atomic_t freemiss;
#ifdef CONFIG_DEBUG_SLAB_LEAK
    atomic_t store_user_clean;
#endif

    /*
     * If debugging is enabled, then the allocator can add additional
     * fields and/or padding to every object. size contains the total
     * object size including these internal fields, the following two
     * variables contain the offset to the user object and its size.
     */
    int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */

#ifdef CONFIG_MEMCG
    struct memcg_cache_params memcg_params;
#endif
#ifdef CONFIG_KASAN
    struct kasan_cache kasan_info;
#endif

#ifdef CONFIG_SLAB_FREELIST_RANDOM
    unsigned int *random_seq;
#endif
    /*slab链表*/
    struct kmem_cache_node *node[MAX_NUMNODES];
};</code></pre></div><p>&#160; 上面的图不是有3组slab链表么？都是在kmem_cache_node结构体中定义的，如下：前面3个slab_partial、slab_full、slab_free就是了！</p><div class="codebox"><pre class="vscroll"><code>#ifndef CONFIG_SLOB
/*
 * The slab lists for all objects.
 */
struct kmem_cache_node {
    spinlock_t list_lock;

#ifdef CONFIG_SLAB
    struct list_head slabs_partial;    /* partial list first, better asm code */
    struct list_head slabs_full;
    struct list_head slabs_free;
    unsigned long num_slabs;
    unsigned long free_objects;
    unsigned int free_limit;
    unsigned int colour_next;    /* Per-node cache coloring */
    struct array_cache *shared;    /* shared per node */
    struct alien_cache **alien;    /* on other nodes */
    unsigned long next_reap;    /* updated without locking */
    int free_touched;        /* updated without locking */
#endif

#ifdef CONFIG_SLUB
    unsigned long nr_partial;
    struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_t nr_slabs;
    atomic_long_t total_objects;
    struct list_head full;
#endif
#endif

};</code></pre></div><p>&#160; &#160;总的来说：</p><p>&#160; &#160; &#160; slab机制把4KB的内存进一步切小到16byte、32byte、64byte、128byte......等不同大小的object，满足小内存的使用的需求，由此诞生了kmem_cache结构体，里面又包含了slab结构体；所以说整个算法最核心的思路就是把4KB的页内存进一步切小成object，然后又用kmem_cache和slab来关系object；<br />&#160; &#160; &#160; &#160; &#160; &#160;kmem_cache结构体有多个实例，每个实例中包含的object大小都不同，这个思路和buddy算法的free_area数组没有任何本质区别，只是实例之间通过链表的形式连接了<br />&#160; &#160; &#160; &#160; &#160; &#160;某个kmem_cache结构体中，由于包含了多个同样大小的object，为了便于管理object，又进一步细分出了3个队列：slab_full、slab_partial、slab_free，每个队列都包含了多个slab；每个slab又包含多个object！所以kmem_cache、slab、objectz这3者是1对多对多的关系！</p><p>&#160; &#160;</p><p>&#160; 内存管理总结：</p><p>目标：</p><p>&#160; &#160; 1）避免碎片<br />&#160; &#160; 2）快速申请和释放</p><p>解决方法：</p><p>&#160; &#160; 1）按层级分区块。分区块管理，互相不污染。例如arena、chunk、run、region不同层级。这里说的污染是指碎片化<br />&#160; &#160; 2）分配时拆分和释放时合并。<br />&#160; &#160; 3）充分使用各种缓冲技术，提高性能。<br />&#160; &#160; 4）使用各种高效的数据结构及其算法，包括多级bitmap、链表、二叉树、红黑树、匹配堆，等等。<br />&#160; &#160; 5）减少管理数据meta data百分比。<br />&#160; &#160; 6）内存划分为各个池。使用池的概念，池中对象大小都相同。不同的池，对象大小可以不同。<br />&#160; &#160; 7）充分利用cpu的cache的优化。<br />&#160; &#160; 8）其他机制，例如减少锁的访问，局部锁代替全局锁，从而减少竞争出现的次数。</p><p> </p><p>参考：</p><p>1、https://r00tk1ts.github.io/2017/10/20/Linux%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86-%E9%A1%B5%E6%A1%86%E7%AE%A1%E7%90%86/&#160; &#160;Linux内核学习——内存管理之页框管理</p><p>2、https://zhuanlan.zhihu.com/p/36140017&#160; linux内存管理算法buddy和slab</p><p>3、 <a href="https://www.bilibili.com/video/BV1wk4y1y7gL/" rel="nofollow">https://www.bilibili.com/video/BV1wk4y1y7gL/</a>&#160; &#160;页框和伙伴算法以及slab机制</p><p>4、https://www.dingmos.com/index.php/archives/23/&#160; slab分配器</p><p>5、https://www.bilibili.com/video/BV1My4y1e7gF?from=search&amp;seid=15617570158469619634&amp;spm_id_from=333.337.0.0&#160; 内存管理思想</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 09 Oct 2022 03:11:07 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=451&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux源码解读（八）：内存管理——分页和分段]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=449&amp;action=new</link>
			<description><![CDATA[<p>1、计算的内存和磁盘都是用来存储数据的，作用上没有本质区别，但是这两种存储介质的特性却差异巨大：</p><p>&#160; 内存需要上电才能存储数据，一旦掉电数据就没了，磁盘却不需要用电也能保存数据<br />&#160; &#160; &#160; &#160;内存的速度很快，大约100ns就能读写数据，而磁盘是毫秒级别的，理论速度差了几万倍；<br />&#160; &#160; &#160; &#160;所以计算机运行时为了追求速度，会尽量把数据放内存，那么问题来了：内存因为价格原因，空间比磁盘小很多，怎么高效地管理和利用内存的存储空间了？</p><p>&#160; 2、（1）举个农业的例子：</p><p>&#160; 上千亩的农田，农场主肯定会根据时令季节、农产种类和价格、农田本身的肥沃度等因素把农田分成很多块，不同的农田种植不同的农产品，内存管理和种田的策略没有本质区别：需要把内存化整为零，不同的部分存储不同的数据，比如内核的代码段、数据段、堆栈段等，这就诞生了GDT表；同理用户的应用程序也有代码段、数据段、堆栈段，所以也诞生了LDT；这两种表需要使用到ds、cs、ss等段寄存器，这就是内存分段的原因；<br />&#160; 这里也引申出了另一个问题：为啥要把内核和用户程序分开了？原因也很简单：如果在一起，用户程序能随意读写内核代码指令或数据，岂不是很危险? 分开后，用户程序执行时如果要调用操作系统的某些接口（比如print、open、read、write、send等），需要通过软中断（int）或系统调用（syscall/sysenter）的方式提权进入内核才行，通过此方式保证用户程序不能随意更改内核代码或数据；<br />&#160; &#160; &#160; &#160; 这里有引申出另一个问题： cpu是典型的指哪打哪，只要eip指向哪，就从哪取数据当成指令来执行，那么cpu是怎么判断当前是内核态还是用户态的了？通过段寄存器的算选择子：一共有2位，可以表示4种不同的状态，一般只用了00和11这两种；00表示0，就是内核态，俗称0环；11表示3，就是用户程序，俗称3环；程序在运行时，如果eip跨越了段寄存器基址+limit的限制（LDT或GDT有标识的），cpu硬件会先检查段选择子的权限，看看是不是从3环到0环；如果是，就必须通过上述的中断或系统调用方式，否则直接在硬件层面出异常报错！当然，从3环到3环也是要报错的（还是通过LDT检查是不是已经超出了本应用程序的cs、ds、ss的范围），这就从硬件层面的机制上保证了A应用程序不会被B应用程序读写而导致数据泄露或程序被破坏（进程切换靠tss，也是cpu从硬件层面保证的机制）！<br />&#160; &#160; &#160; &#160;还有最后一个问题：为啥通过软中断或系统调用从3环进0环就是安全（当然这是相对的安全）的，而3环直接访问0环的代码或数据就是危险的了？ 中断或系统调用，3环只需要提供调用号就行了，不需要知道具体实现的代码是怎么写的，也完全看不见；只要操作系统内核保证系统调用的实现过程是安全的就ok了！软中断或系统调用：本质上就是提供了3环低权限到0环高权限的提权通道，让代码进入0环执行；但执行的代码又是操作系统实现定义好的，用户的应用程序是没法随意更改的，由此又保障了内核的代码和数据安全，一个字：绝！<br />&#160; &#160; &#160; &#160; 上述利用ds、cs、ss等寄存器把内存划分成一段一段地形式，不同的段分别给内核、应用程序使用；代码一旦要跨段运行，需要检查段的权限来确认是否能够跨段，这是早期操作系统和cpu硬件隔离程序的做法，现在的64位操作系统已经不是这么做的了！大家可以用windbg看看64位的windows系统，可以发现ds、cs、ss居然都是从0开始的，limit也都是0xffffffffffffff，用行话说要“平坦化”了，这种情况怎么隔离内核和应用程序了？-----分页<br />&#160; 每个进程都有自己的CR3值，通过CR3页表映射，把逻辑地址转成物理地址；不同的进程有不同的CR3和不同的页表映射，即使是相同的逻辑地址，也会映射到不同的物理地址，由此让不同的进程拥有不用的物理地址，和上述通过ds、cs、ss方式对内存分段的思路是一样的，只不过换了一种方式实现！<br />&#160; &#160; &#160; 分页的另一个好处：大块的物理内存经过长时间的分配和回收，可能已经支离破碎；经过虚拟内存映射后，把零散的物理内存映射成大整块的虚拟内存；<br />&#160; &#160; &#160; 分页的漏洞: 人为更改页表，把逻辑地址映射的物理地址改成其他的物理地址，导致应用程序读取的数据或代码是错误的，达到无痕hook的目的！windows下可以用来过PG保护的；<br />&#160; &#160; &#160; 内核代码不分页，永占物理内存，原因也很简单：内核代码是核心，需要一直运行，当然不能被换出内存了，举个例子：发生缺页异常了，需要把页面的数据从磁盘重新加载到内存，缺页异常的handler也在内核，如果连这部分代码都分页，这个页面被放入磁盘，岂不成了死循环？所以只有应用程序的内存需要分页管理！<br />&#160; （2）概念澄清：</p><p>&#160; &#160; 逻辑地址：程序员写代码看到和使用的地址，是操作系统分配给每个进程的独立地址空间，也就是CR3转换前的地址空间</p><p>&#160; &#160; 线性地址：等于段基址+逻辑地址；windows 64位的操作系统段平坦化以后，线性地址事实上等于逻辑地址了；</p><p>&#160; &#160; &#160; &#160; &#160; &#160; &#160; 物理地址：cpu物理总线表示的地址</p><p>&#160; &#160;（3） 页的属性也有很多位来表示，学名叫页表描述，如下：每位的作用在参考链接1有，这里不再追溯！</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211213183144127-1069412943.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160;这里重点说一下第5位access位：该页是否能被访问；如果不能，其他应用程序访问时会报错，这可以用于动态反调试 和反hook；这里有必要解释一下为啥低12bit可以用来表示各种属性，而不是用来表示地址了？</p><p>&#160; 由于cpu把物理内存按照4KB划分成1页，所以页目录和页表也是存储在物理页的；页目录和页表也是地址，每个item都是4byte，所以每个物理页能容纳1024个item；换句话说：页目录每个item指向一个装了1024个页表的物理页，由于物理页是4KB对齐的，导致页目录表的每个item的低12bit都是0；页目录同理：每个页目录的item都指向物理页的开始位置，导致页目录的每个item的第12bit都是0；所以第12bit实际上不可以不用来表示地址的，刚好就用来表示各种页属性了！整个示意如下：</p><p>这里容易混淆的就是页目录地址和页表项，因为存储这两类信息的内存本身是地址，恰好页目录和页表项的含义也是地址，两个地址在编码时非常容易混淆！明显的区别就是：页目录和页表项本身的地址是4字节递增的，但存储的内容是页首地址，以0x1000的颗粒度对齐的！<br />实际存储时，页目录项和页表项每个item的低12bit大概率会被用于指明各种属性，不太可能直接存0；实际计算下一级地址时需要清零；<br />后面的代码会大量涉及到*((unsigned long *) ((address&gt;&gt;20) &amp;0xffc))))和((address&gt;&gt;10) &amp; 0xffc)，分别是页目录表中页表项的值，和页表项的偏移，两者相加就是页表项的线性地址；</p><p><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211216161434621-1536455319.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; &#160; 3、内存管理</p><p>&#160; 大家还记得文件的inode和数据块是怎么管理的么？当时用了bitmap来标记的；逻辑块的最小使用大小是4KB。如果这4KB被分配使用，那么bitmap对应的bit设置为1；0.11版本的linux采用了相同的管理策略，仍然采用bitmap的方式标记内存页。如果某一页已经分配使用，那么bitmap对应的bit设置为1，否则设置为0；这里实际采用了名为mem_map的char数组标记3840个页面(15MB内存)被引用的次数；</p><div class="codebox"><pre><code>/* these are not to be changed without changing head.s etc */
// linux0.11内核默认支持的最大内存容量是16MB，可以修改这些定义适合更多的内存。
// 内存低端(1MB)
#define LOW_MEM 0x100000
// 分页内存15 MB，主内存区最多15M.
#define PAGING_MEMORY (15*1024*1024)
// 分页后的物理内存页面数（3840）
#define PAGING_PAGES (PAGING_MEMORY&gt;&gt;12)
// 指定地址映射为页号
#define MAP_NR(addr) (((addr)-LOW_MEM)&gt;&gt;12)
// 页面被占用标志.
#define USED 100

// 物理内存映射字节图（1字节代表1页内存）。每个页面对应的字节用于标志页面当前引
// 用（占用）次数。它最大可以映射15MB的内存空间。在初始化函数mem_init()中，对于
// 不能用做主内存页面的位置均都预先被设置成USED（100）.
static unsigned char mem_map [ PAGING_PAGES ] = {0,};</code></pre></div><p>&#160; &#160;查找空闲或使用的物理页面是，需要挨个遍历mem_map数组，linux 0.11是这样做的；同时也遍历pa_dir数组，看看哪些页目录和页表项已经被使用了！</p><div class="codebox"><pre><code>//// 计算内存空闲页面数并显示
// [?? 内核中没有其他地方调用该函数，Linus调试过程中用的]
void calc_mem(void)
{
    int i,j,k,free=0;
    long * pg_tbl;

    // 扫描内存页面映射数组mem_map[]，获取空闲页面数并显示。然后扫描所有的页目
    // 录项(除0，1项)，如果页目录项有效，则统计对应页表中有效页面数，并显示。页
    // 目录项0-3被内核使用，因此应该从第5个目录项(i＝4)开始扫描。
    for(i=0 ; i&lt;PAGING_PAGES ; i++)
        if (!mem_map[i]) free++;
    printk(&quot;%d pages free (of %d)\n\r&quot;,free,PAGING_PAGES);
    for(i=2 ; i&lt;1024 ; i++) {               // 初始值应该等于4；有1024个页目录
        if (1&amp;pg_dir[i]) {
            pg_tbl=(long *) (0xfffff000 &amp; pg_dir[i]);/*得到页表项；每个页目录有1024个页表项；每个页表项对应1024个物理页*/
            for(j=k=0 ; j&lt;1024 ; j++)
                if (pg_tbl[j]&amp;1)
                    k++;
            printk(&quot;Pg-dir[%d] uses %d pages\n&quot;,i,k);
        }
    }
}</code></pre></div><p>&#160; 当然，在正式使用前，必须先初始化的，方法如下：</p><div class="codebox"><pre class="vscroll"><code>// 物理内存管理初始化
// 该函数对1MB以上的内存区域以页面为单位进行管理前的初始化设置工作。一个页面长度
// 为4KB bytes.该函数把1MB以上所有物理内存划分成一个个页面，并使用一个页面映射字节
// 数组mem_map[]来管理所有这些页面。对于具有16MB内存容量的机器，该数组共有3840
// 项((16MB-1MB)/4KB)，即可管理3840个物理页面。每当一个物理内存页面被占用时就把
// mem_map[]中对应的字节值增1；若释放一个物理页面，就把对应字节值减1。若字节值为0，
// 则表示对应页面空闲；若字节值大于或等于1，则表示对应页面被占用或被不同程序共享占用。
// 在该版本的Linux内核中，最多能管理16MB的物理内存，大于16MB的内存将弃之不用。
// 对于具有16MB内存的PC机系统，在没有设置虚拟盘RAMDISK的情况下start_mem通常是4MB，
// end_mem是16MB。因此此时主内存区范围是4MB-16MB,共有3072个物理页面可供分配。而
// 范围0-1MB内存空间用于内核系统（其实内核只使用0-640Kb，剩下的部分被部分高速缓冲和
// 设备内存占用）。
// 参数start_mem是可用做页面分配的主内存区起始地址（已去除RANDISK所占内存空间）。
// end_mem是实际物理内存最大地址。而地址范围start_mem到end_mem是主内存区。
void mem_init(long start_mem, long end_mem)
{
    int i;

    // 首先将1MB到16MB范围内所有内存页面对应的内存映射字节数组项置为已占用状态，
    // 即各项字节全部设置成USED(100)。PAGING_PAGES被定义为(PAGING_MEMORY&gt;&gt;12)，
    // 即1MB以上所有物理内存分页后的内存页面数(15MB/4KB = 3840).
    HIGH_MEMORY = end_mem;                  // 设置内存最高端(16MB)
    for (i=0 ; i&lt;PAGING_PAGES ; i++)
        mem_map[i] = USED;
    // 然后计算主内存区起始内存start_mem处页面对应内存映射字节数组中项号i和主内存区页面数。
    // 此时mem_map[]数组的第i项正对应主内存区中第1个页面。最后将主内存区中页面对应的数组项
    // 清零(表示空闲)。对于具有16MB物理内存的系统，mem_map[]中对应4MB-16MB主内存区的项被清零。
    i = MAP_NR(start_mem);      // 主内存区(也就是用户程序内存)其实位置处页面号
    end_mem -= start_mem;
    end_mem &gt;&gt;= 12;             // 主内存区中的总页面数
    while (end_mem--&gt;0)
        mem_map[i++]=0;         // 主内存区页面对应字节值清零
}</code></pre></div><p>&#160; &#160; &#160; &#160; 根据mem_init推断早期linux的内存使用分布如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202112/2052730-20211214150726956-2013252918.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; 主内存（也就是用户应用程序）内存初始化完成后就可以按照页的颗粒度申请使用了；怎么找到空闲的物理页面了？当然是从mem_map数组开始找了，代码如下：</p><div class="codebox"><pre class="vscroll"><code>//// 在主内存区中取空闲屋里页面。如果已经没有可用物理内存页面，则返回0.
// 输入：%1(ax=0) - 0; %2(LOW_MEM)内存字节位图管理的其实位置；%3(cx=PAGING_PAGES);
// %4(edi=mem_map+PAGING_PAGES-1).
// 输出：返回%0(ax=物理内存页面起始地址)。
// 上面%4寄存器实际指向mem_map[]内存字节位图的最后一个字节。本函数从位图末端开
// 始向前扫描所有页面标志（页面总数PAGING_PAGE），若有页面空闲（内存位图字节为
// 0）则返回页面地址。注意！本函数只是指出在主内存区的一页空闲物理内存页面，但
// 并没有映射到某个进程的地址空间中去。后面的put_page()函数即用于把指定页面映射
// 到某个进程地址空间中。当然对于内核使用本函数并不需要再使用put_page()进行映射，
// 因为内核代码和数据空间（16MB）已经对等地映射到物理地址空间。
unsigned long get_free_page(void)
{
register unsigned long __res asm(&quot;ax&quot;);

__asm__(&quot;std ; repne ; scasb\n\t&quot;   // 置方向位，al(0)与对应每个页面的(di)内容比较
    &quot;jne 1f\n\t&quot;                    // 如果没有等于0的字节，则跳转结束(返回0).
    &quot;movb $1,1(%%edi)\n\t&quot;          // 1 =&gt; [1+edi],将对应页面内存映像bit位置1.
    &quot;sall $12,%%ecx\n\t&quot;            // 页面数*4k = 相对页面其实地址
    &quot;addl %2,%%ecx\n\t&quot;             // 再加上低端内存地址，得页面实际物理起始地址
    &quot;movl %%ecx,%%edx\n\t&quot;          // 将页面实际其实地址-&gt;edx寄存器。
    &quot;movl $1024,%%ecx\n\t&quot;          // 寄存器ecx置计数值1024
    &quot;leal 4092(%%edx),%%edi\n\t&quot;    // 将4092+edx的位置-&gt;dei（该页面的末端地址）
    &quot;rep ; stosl\n\t&quot;               // 将edi所指内存清零(反方向，即将该页面清零)
    &quot;movl %%edx,%%eax\n&quot;            // 将页面起始地址-&gt;eax（返回值）
    &quot;1:&quot;
    :&quot;=a&quot; (__res)
    :&quot;0&quot; (0),&quot;i&quot; (LOW_MEM),&quot;c&quot; (PAGING_PAGES),
    &quot;D&quot; (mem_map+PAGING_PAGES-1)
    );
return __res;           // 返回空闲物理页面地址(若无空闲页面则返回0).
}</code></pre></div><p>&#160; 用完之后必须要及时释放，不要“占着茅坑不拉屎”，释放的原理也很简单，把mem_map对应的内容清零就行了，如下：这里不得不吐槽一下，还是下面这种C代码看着简单；上面那种汇编语法看的人脑壳疼┭┮﹏┭┮</p><div class="codebox"><pre><code>//// 释放物理地址addr开始的1页面内存。
// 物理地址1MB以下的内容空间用于内核程序和缓冲，不作为分配页面的内存空间。因此
// 参数addr需要大于1MB.
void free_page(unsigned long addr)
{
    // 首先判断参数给定的物理地址addr的合理性。如果物理地址addr小于内存低端(1MB)
    // 则表示在内核程序或高速缓冲中，对此不予处理。如果物理地址addr&gt;=系统所含物
    // 理内存最高端，则显示出错信息并且内核停止工作。
    if (addr &lt; LOW_MEM) return;
    if (addr &gt;= HIGH_MEMORY)
        panic(&quot;trying to free nonexistent page&quot;);
    // 如果对参数addr验证通过，那么就根据这个物理地址换算出从内存低端开始记起的
    // 内存页面号。页面号 ＝ (addr - LOW_MEM)/4096.可见页面号从0号开始记起。此时
    // addr中存放着页面号。如果该页面号对应的页面映射字节不等于0，则减1返回。此
    // 时该映射字节值应该为0，表示页面已释放。如果对应页面字节原本就是0，表示该
    // 物理页面本来就是空闲的，说明内核代码出问题。于是显示出错信息并停机。
    addr -= LOW_MEM;
    addr &gt;&gt;= 12;
    if (mem_map[addr]--) return;
    mem_map[addr]=0;
    panic(&quot;trying to free free page&quot;);
}</code></pre></div><p>&#160; 当进程退出do_exit时，不仅要free_page，进程自己的页表、页目录项也要free，操作的代码如下： 找到页目录的基址，然后挨个遍历；遍历时找到页表项，进而找到使用的物理内存，挨个释放！ </p><div class="codebox"><pre class="vscroll"><code>/*
 * This function frees a continuos block of page tables, as needed
 * by &#039;exit()&#039;. As does copy_page_tables(), this handles only 4Mb blocks.
 */
//// 根据指定的线性地址和限长(页表个数)，释放对应内存页表指定的内存块并置表项为
// 空闲。页目录位于物理地址0开始处，共1024项，每项4字节，共占4K字节。每个目录项
// 指定一个页表。内核页表从物理地址0x1000处开始(紧接着目录空间)，共4个页表。每
// 个页表有1024项，每项4字节。因此占4K（1页）内存。各进程（除了在内核代码中的进
// 程0和1）的页表所占据的页面在进程被创建时由内核为其主内存区申请得到。每个页表
// 项对应1耶物理内存，因此一个页表最多可映射4MB的物理内存。
// 参数：from - 起始线性基地址；size - 释放的字节长度。
int free_page_tables(unsigned long from,unsigned long size)
{
    unsigned long *pg_table;
    unsigned long * dir, nr;

    // 首先检测参数from给出的线性基地址是否在4MB的边界处。因为该函数只能处理这
    // 种情况。若from=0,则出错。说明视图释放内核和缓冲所占空间。
    if (from &amp; 0x3fffff)/*4MB取整；4MB也是一个目录项（第二级）对应的地址空间*/
        panic(&quot;free_page_tables called with wrong alignment&quot;);
    if (!from)
        panic(&quot;Trying to free up swapper memory space&quot;);
    // 然后计算参数size给出的长度所占的页目录项数（4MB的进位整数倍），也即所占
    // 页表数。因为1个页表可管理4MB物理内存，所以这里用右移22位的方式把需要复制
    // 的内存长度值除以4MB.其中加上0x3fffff(即4MB-1)用于得到进位整数倍结果，即
    // 除操作若有余数则进1。例如，如果原size=4.01Mb，那么可得到结果sieze=2。接
    // 着结算给出的线性基地址对应的其实目录项。对应的目录项号＝from&gt;&gt;22.因为每
    // 项占4字节，并且由于页目录表从物理地址0开始存放，因此实际目录项指针＝目录
    // 项号&lt;&lt;2，也即(from&gt;&gt;20)。&amp; 0xffc确保目录项指针范围有效，即用于屏蔽目录项
    // 指针最后2位。因为只移动了20位，因此最后2位是页表项索引的内容，应屏蔽掉。
    size = (size + 0x3fffff) &gt;&gt; 22;
    dir = (unsigned long *) ((from&gt;&gt;20) &amp; 0xffc); /* _pg_dir = 0 */
    // 此时size是释放的页表个数，即页目录项数，而dir是起始目录项指针。现在开始
    // 循环操作页目录项，依次释放每个页表中的页表项。如果当前目录项无效（P位＝0）
    // 表示该目录项没有使用(对应的页表不存在)，则继续处理下一个目录项。否则从目
    // 录项总取出页表地址pg_table，并对该页表中的1024个表项进行处理。释放有效页
    // 表项(P位＝1)对应的物理内存页表。然后该页表项清零，并继续处理下一页表项。
    // 当一个页表所有表项都处理完毕就释放该页表自身占据的内存页面，并继续处理下
    // 一页目录项。最后刷新也页变换高速缓冲，并返回0.
    for ( ; size--&gt;0 ; dir++) {
        if (!(1 &amp; *dir))
            continue;
        pg_table = (unsigned long *) (0xfffff000 &amp; *dir);  // 取页表地址
        for (nr=0 ; nr&lt;1024 ; nr++) {
            if (1 &amp; *pg_table)                          // 若该项有效，则释放对应页。 
                free_page(0xfffff000 &amp; *pg_table);
            *pg_table = 0;                              // 该页表项内容清零。
            pg_table++;                                 // 指向页表中下一项。
        }
        free_page(0xfffff000 &amp; *dir);                   // 释放该页表所占内存页面。
        *dir = 0;                                       // 对应页表的目录项清零
    }
    invalidate();                                       // 刷新页变换高速缓冲。
    return 0;
}</code></pre></div><p>&#160; 和free_page_table对应的是copy_page_table，这个函数是在copy_mem里面被调用的，而copy_mem是在fork里面被调用的，说明父进程生成子进程时拷贝的内存并不是真正的物理地址，而是先拷贝了页目录项和页表项，物理内存暂时共用，等到缺页时才找空闲的物理页分配！</p><p>&#160; 整个函数的实现连linus都说在内存管理中是最复杂的之一！</p><div class="codebox"><pre class="vscroll"><code>/*
 *  Well, here is one of the most complicated functions in mm. It
 * copies a range of linerar addresses by copying only the pages.
 * Let&#039;s hope this is bug-free, &#039;cause this one I don&#039;t want to debug :-)
 *
 * Note! We don&#039;t copy just any chunks of memory - addresses have to
 * be divisible by 4Mb (one page-directory entry), as this makes the
 * function easier. It&#039;s used only by fork anyway.
 *
 * NOTE 2!! When from==0 we are copying kernel space for the first
 * fork(). Then we DONT want to copy a full page-directory entry, as
 * that would lead to some serious memory waste - we just copy the
 * first 160 pages - 640kB. Even that is more than we need, but it
 * doesn&#039;t take any more memory - we don&#039;t copy-on-write in the low
 * 1 Mb-range, so the pages can be shared with the kernel. Thus the
 * special case for nr=xxxx.
 */
//// 复制页目录表项和页表项：暂时不拷贝具体的页，提高进程fork的效率
// 复制指定线性地址和长度内存对应的页目录项和页表项，从而被复制的页目录和页表对
// 应的原物理内存页面区被两套页表映射而共享使用。复制时，需申请新页面来存放新页
// 表，原物理内存区将被共享。此后两个进程（父进程和其子进程）将共享内存区，直到
// 有一个进程执行写操作时，内核才会为写操作进程分配新的内存页(写时复制机制)。
// 参数from、to是线性地址，size是需要复制（共享）的内存长度，单位是byte.
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
    unsigned long * from_page_table;
    unsigned long * to_page_table;
    unsigned long this_page;
    unsigned long * from_dir, * to_dir;
    unsigned long nr;

    // 首先检测参数给出的原地址from和目的地址to的有效性。原地址和目的地址都需要
    // 在4Mb内存边界地址上。否则出错死机。作这样的要求是因为一个页表的1024项可
    // 管理4Mb内存。源地址from和目的地址to只有满足这个要求才能保证从一个页表的
    // 第一项开始复制页表项，并且新页表的最初所有项都是有效的。然后取得源地址和
    // 目的地址的其实目录项指针(from_dir 和 to_dir).再根据参数给出的长度size计
    // 算要复制的内存块占用的页表数(即目录项数)。
    if ((from&amp;0x3fffff) || (to&amp;0x3fffff))
        panic(&quot;copy_page_tables called with wrong alignment&quot;);
    from_dir = (unsigned long *) ((from&gt;&gt;20) &amp; 0xffc); /* _pg_dir = 0 */
    to_dir = (unsigned long *) ((to&gt;&gt;20) &amp; 0xffc);
    size = ((unsigned) (size+0x3fffff)) &gt;&gt; 22;
    // 在得到了源起始目录项指针from_dir和目的起始目录项指针to_dir以及需要复制的
    // 页表个数size后，下面开始对每个页目录项依次申请1页内存来保存对应的页表，并
    // 且开始页表项复制操作。如果目的目录指定的页表已经存在(P=1)，则出错死机。
    // 如果源目录项无效，即指定的页表不存在(P=1),则继续循环处理下一个页目录项。
    for( ; size--&gt;0 ; from_dir++,to_dir++) {
        if (1 &amp; *to_dir)
            panic(&quot;copy_page_tables: already exist&quot;);
        if (!(1 &amp; *from_dir))
            continue;
        // 在验证了当前源目录项和目的项正常之后，我们取源目录项中页表地址
        // from_page_table。为了保存目的目录项对应的页表，需要在住内存区中申请1
        // 页空闲内存页。如果取空闲页面函数get_free_page()返回0，则说明没有申请
        // 到空闲内存页面，可能是内存不够。于是返回-1值退出。
        from_page_table = (unsigned long *) (0xfffff000 &amp; *from_dir);
        if (!(to_page_table = (unsigned long *) get_free_page()))
            return -1;    /* Out of memory, see freeing */
        // 否则我们设置目的目录项信息，把最后3位置位，即当前目录的目录项 | 7，
        // 表示对应页表映射的内存页面是用户级的，并且可读写、存在(Usr,R/W,Present).
        // (如果U/S位是0，则R/W就没有作用。如果U/S位是1，而R/W是0，那么运行在用
        // 户层的代码就只能读页面。如果U/S和R/W都置位，则就有读写的权限)。然后
        // 针对当前处理的页目录项对应的页表，设置需要复制的页面项数。如果是在内
        // 核空间，则仅需复制头160页对应的页表项(nr=160),对应于开始640KB物理内存
        // 否则需要复制一个页表中的所有1024个页表项(nr=1024)，可映射4MB物理内存。
        *to_dir = ((unsigned long) to_page_table) | 7;
        nr = (from==0)?0xA0:1024;
        // 此时对于当前页表，开始循环复制指定的nr个内存页面表项。先取出源页表的
        // 内容，如果当前源页表没有使用，则不用复制该表项，继续处理下一项。否则
        // 复位表项中R/W标志(位1置0)，即让页表对应的内存页面只读。然后将页表项复制
        // 到目录页表中。
        for ( ; nr-- &gt; 0 ; from_page_table++,to_page_table++) {
            this_page = *from_page_table;
            if (!(1 &amp; this_page))
                continue;
            this_page &amp;= ~2;
            *to_page_table = this_page;
            // 如果该页表所指物理页面的地址在1MB以上，则需要设置内存页面映射数
            // 组mem_map[]，于是计算页面号，并以它为索引在页面映射数组相应项中
            // 增加引用次数。而对于位于1MB以下的页面，说明是内核页面，因此不需
            // 要对mem_map[]进行设置。因为mem_map[]仅用于管理主内存区中的页面使
            // 用情况。因此对于内核移动到任务0中并且调用fork()创建任务1时(用于
            // 运行init())，由于此时复制的页面还仍然都在内核代码区域，因此以下
            // 判断中的语句不会执行，任务0的页面仍然可以随时读写。只有当调用fork()
            // 的父进程代码处于主内存区(页面位置大于1MB)时才会执行。这种情况需要
            // 在进程调用execve()，并装载执行了新程序代码时才会出现。
            // *from_page_table = this_page; 这句是令源页表项所指内存页也为只读。
            // 因为现在开始有两个进程公用内存区了。若其中1个进程需要进行写操作，
            // 则可以通过页异常写保护处理为执行写操作的进程匹配1页新空闲页面，也
            // 即进行写时复制(copy on write)操作。
            if (this_page &gt; LOW_MEM) {
                *from_page_table = this_page;
                this_page -= LOW_MEM;
                this_page &gt;&gt;= 12;
                mem_map[this_page]++;
            }
        }
    }
    invalidate();
    return 0;
}</code></pre></div><p>&#160; 经过上面的线性地址虚拟化操作，给人感觉就是个活生生的“盗梦空间”：线性地址本身是人为虚拟构造出来的，硬件层面读写数据还要先转成物理内存，所以线性地址并不是真正的地址（这不废话么），32bit可以根据业务需求赋予各种不同的含义，只要在页目录表和页表项的对应关系映射好就行了！这里的映射具体怎么操作了？也很简单：就是给数组元素赋值，或者是给指针内容赋值；linux 0.11版本把页表项和物理地址映射的方法如下：</p><p>&#160; 最核心的“映射”代码其实很简单：page_table[(address&gt;&gt;12) &amp; 0x3ff] = page | 7;</p><div class="codebox"><pre class="vscroll"><code>/*
 * This function puts a page in memory at the wanted address.
 * It returns the physical address of the page gotten, 0 if
 * out of memory (either when trying to access page-table or
 * page.)
 */
//// 把一物理内存页面映射到线性地址空间指定处。
// 或者说是把线性地址空间中指定地址address出的页面映射到主内存区页面page上。主
// 要工作是在相关页面目录项和页表项中设置指定页面的信息。若成功则返回物理页面地
// 址。在处理缺页异常的C函数do_no_page()中会调用此函数。对于缺页引起的异常，由于
// 任何缺页缘故而对页表作修改时，并不需要刷新CPU的页变换缓冲(或称Translation Lookaside
// Buffer - TLB),即使页表中标志P被从0修改成1.因为无效叶项不会被缓冲，因此当修改
// 了一个无效的页表项时不需要刷新。在次就表现为不用调用Invalidate()函数。
// 参数page是分配的主内存区中某一页面(页帧，页框)的指针;address是线性地址。
unsigned long put_page(unsigned long page,unsigned long address)
{
    unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */

    // 首先判断参数给定物理内存页面page的有效性。如果该页面位置低于LOW_MEM（1MB）
    // 或超出系统实际含有内存高端HIGH_MEMORY，则发出警告。LOW_MEM是主内存区可能
    // 有的最小起始位置。当系统物理内存小于或等于6MB时，主内存区起始于LOW_MEM处。
    // 再查看一下该page页面是否已经申请的页面，即判断其在内存页面映射字节图mem_map[]
    // 中相应字节是否已经置位。若没有则需发出警告。
    if (page &lt; LOW_MEM || page &gt;= HIGH_MEMORY)
        printk(&quot;Trying to put page %p at %p\n&quot;,page,address);
    if (mem_map[(page-LOW_MEM)&gt;&gt;12] != 1)
        printk(&quot;mem_map disagrees with %p at %p\n&quot;,page,address);
    // 然后根据参数指定的线性地址address计算其在也目录表中对应的目录项指针，并
    // 从中取得二级页表地址。如果该目录项有效(P=1),即指定的页表在内存中，则从中
    // 取得指定页表地址放到page_table 变量中。否则就申请一空闲页面给页表使用，并
    // 在对应目录项中置相应标志(7 - User、U/S、R/W).然后将该页表地址放到page_table
    // 变量中。
    page_table = (unsigned long *) ((address&gt;&gt;20) &amp; 0xffc);
    if ((*page_table)&amp;1)
        page_table = (unsigned long *) (0xfffff000 &amp; *page_table);
    else {
        if (!(tmp=get_free_page()))
            return 0;
        *page_table = tmp|7;
        page_table = (unsigned long *) tmp;
    }
    // 最后在找到的页表page_table中设置相关页表内容，即把物理页面page的地址填入
    // 表项同时置位3个标志(U/S、W/R、P)。该页表项在页表中索引值等于线性地址位21
    // -- 位12组成的10bit的值。每个页表共可有1024项(0 -- 0x3ff)。
    page_table[(address&gt;&gt;12) &amp; 0x3ff] = page | 7;
/* no need for invalidate */
    return page;
}</code></pre></div><p>&#160; 4、（1）为了节约物理内存，不同进程可能会共享同样的物理页面，举个例子：同时打开两个notepad，操作系统会同时生成两个进程，但由于运行的是同样的程序，最起码代码段是可以共享的，所以这两个进程的代码段是可以设置成一样的！linux设置共享物理页的方式如下：</p><p>&#160; 因为共享的肯定是物理页面，所以要先根据线性地址算出物理地址；<br />&#160; &#160; &#160; &#160;修改目标进程的页表项，让某个页表项保存物理页起始地址（这种物理页的挂载思路在windows下叫shadow walker，可以用来过PG保护的）；</p><div class="codebox"><pre class="vscroll"><code>/*
 * try_to_share() checks the page at address &quot;address&quot; in the task &quot;p&quot;,
 * to see if it exists, and if it is clean. If so, share it with the current
 * task.
 *
 * NOTE! This assumes we have checked that p != current, and that they
 * share the same executable.
 */
//// 尝试对当前进程指定地址处的页面进行共享处理。
// 当前进程与进程p是同一执行代码，也可以认为当前进程是由p进程执行fork操作产生的
// 进程，因此它们的代码内容一样。如果未对数据段内容做过修改那么数据段内容也应一
// 样。参数address是进程中的逻辑地址，即是当前进程欲与p进程共享页面的逻辑页面地
// 址。进程P是将被共享页面的进程。如果P进程address出的页面存在并且没有被修改过的
// 话，就让当前进程与p进程共享之。同时还需要验证指定地址处是否已经申请了页面，若
// 是则出错，死机。返回：1 - 页面共享处理成功；0 - 失败。
static int try_to_share(unsigned long address, struct task_struct * p)
{
    unsigned long from;
    unsigned long to;
    unsigned long from_page;
    unsigned long to_page;
    unsigned long phys_addr;

    // 首先分别求的指定进程p中和当前进程中逻辑地址address对应的页目录项。为了计
    // 算方便先求出指定逻辑地址address出的&#039;逻辑&#039;页目录项号，即以进程空间(0 - 64 MB)
    // 算出的页目录项号。该&#039;逻辑&#039;页目录项号加上进程p在CPU 4G线性空间中的实际页目
    // 录项from_page。而&#039;逻辑&#039;页目录项号加上当前进程CPU 4G线性空间中起始地址对应
    // 的页目录项，即可最后得到当前进程中地址address处页面所对应的4G线性空间中的
    // 实际页目录项to_page。
    from_page = to_page = ((address&gt;&gt;20) &amp; 0xffc);
    from_page += ((p-&gt;start_code&gt;&gt;20) &amp; 0xffc);
    to_page += ((current-&gt;start_code&gt;&gt;20) &amp; 0xffc);
    // 在得到p进程和当前进程address对应的目录项后，下面分别对进程p和当前进程进行
    // 处理。下面首先对p进程的表项进行操作。目标是取得p进程中address对应的物理内
    // 存页面地址，并且该物理页面存在，而且干净(没有被修改过)。
    // 方法是先取目录项内容。如果该目录项无效(P=0),表示目录项对应的二级页表不存
    // 在，于是返回。否则取该目录项对应页表地址from，从而计算出逻辑地址address
    // 对应的页表项指针，并取出该页表项内容临时保存在phys_addr中。
/* is there a page-directory at from? */
    from = *(unsigned long *) from_page;
    if (!(from &amp; 1))
        return 0;
    from &amp;= 0xfffff000;
    from_page = from + ((address&gt;&gt;10) &amp; 0xffc);
    phys_addr = *(unsigned long *) from_page;//终于找到物理地址了
/* is the page clean and present? */
    // 接着看看页表项映射的物理页面是否存在并且干净。0x41对应页表项中的D(dirty)
    // 和P（present）标志。如果页面不干净或无效则返回。然后我们从该表项中取出物
    // 理页面地址再保存在phys_addr中。最后我们再检查一下这个物理页面地址的有效性，
    // 即它不应该超过机器最大物理地址值，也不应该小于内存低端(1 MB).
    if ((phys_addr &amp; 0x41) != 0x01)
        return 0;
    phys_addr &amp;= 0xfffff000;
    if (phys_addr &gt;= HIGH_MEMORY || phys_addr &lt; LOW_MEM)
        return 0;
    // 下面首先对当前进程的表项进行操作。目标是取得当前进程中address对应的页表
    // 项地址，并且该页表项还没有映射物理页面，即其P=0。
    // 首先取当前进程页目录项内容-&gt;to.如果该目录项无效(P=0),即目录项对应的二级
    // 页表不存在，则申请一空闲页面来存放页表，并更新目录项to_page内容，让其指向
    // 内存页面。
    to = *(unsigned long *) to_page;
    if (!(to &amp; 1)) {
        if ((to = get_free_page()))
            *(unsigned long *) to_page = to | 7;
        else
            oom();
    }
    // 否则取目录项中的页表地址-&gt;to，加上页表项索引值&lt;&lt;2，即页表项在表中偏移地址，
    // 得到页表地址-&gt;to_page.针对页表项，如果我们此时我们检查出其对应的物理页面
    // 已经存在，即页表的存在位P=1，则说明原本我们想共享进程p中对应的物理页面，
    // 但现在我们自己已经占有了(映射有)物理页面。于是说明内核出错，死机。
    to &amp;= 0xfffff000;
    to_page = to + ((address&gt;&gt;10) &amp; 0xffc);
    if (1 &amp; *(unsigned long *) to_page)
        panic(&quot;try_to_share: to_page already exists&quot;);
    // 在找到了进程p中逻辑地址address处对应的干净且存在的物理页面，而且也确定了
    // 当前进程中逻辑地址address所对应的耳机页表项地址之后，我们现在对他们进行
    // 共享处理。方法很简单，就是首先对p进程的页表项进行修改，设置其写保护(R/W=0,
    // 只读)标志，然后让当前进程复制p进程的这个页表项。此时当前进程逻辑地址address
    // 处页面即被映射到p进程逻辑地址address处页面映射的物理页面上。
/* share them: write-protect */
    *(unsigned long *) from_page &amp;= ~2;
    *(unsigned long *) to_page = *(unsigned long *) from_page;
    // 随后刷新页变换高速缓冲。计算所操作屋里页面的页面号，并将对应页面映射字节数
    // 组项中的引用递增1。最后返回1，表示共享处理成功。
    invalidate();
    phys_addr -= LOW_MEM;
    phys_addr &gt;&gt;= 12;
    mem_map[phys_addr]++;
    return 1;
}</code></pre></div><p>&#160; （2）当发生缺页时，首先看看有没有运行同样文件的进程；如果有，先共享一下改进程的物理页面，达到节约内存的目的：注意这里面有个count字段，可以用来检测app多开的！由此也引申出了另一个沙箱的概念：虚拟出另一块内存，但是count就是1，避开检测！</p><div class="codebox"><pre class="vscroll"><code>//// 共享页面处理。
// 在发生缺页异常时，首先看看能否与运行同一个执行文件的其他进程进行页面共享处理
// 该函数首先判断系统是否有另一个进程也在运行当前进程一样的执行文件。若有，则在
// 系统当前所有任务中寻找这样的任务。若找到了这样的任务就尝试与其共享指定地址处
// 的页面。若系统中没有其他任务正在运行与当前进程相同的执行文件，那么共享页面操
// 作的前提条件不存在，因此函数立刻退出。判断系统中是否有另一个进程也在执行同一
// 个执行文件的方法是零用进程任务数据结构中的executable字段。该字段指向进程正在
// 执行程序在内存中的i节点。根据该i节点的引用次数i_count我们可以进行这种判断。若
// executable-&gt;i_count值大于1，则表明系统中可能有两个进程运行同一个执行文件，于
// 是可以再对task struct数组中所有任务比较是否有相同的executable字段来最后确定多个
// 进程运行着相同执行文件的情况。
// 参数address是进程中的逻辑地址，即是当前进程欲与p进程共享页面的逻辑页面地址。
// 返回：1 - 共享操作成功，0 - 失败。
static int share_page(unsigned long address)
{
    struct task_struct ** p;

    // 首先检查一下当前进程的executable字段是否指向某执行文件的i节点，以判断本
    // 进程是否有对应的执行文件。如果没有，则返回0.如果executable的确指向某个i
    // 节点，则检查该i节点引用计数值。如果当前进程运行的执行文件的内存i节点引用
    // 计数等于1(executable-&gt;i_count=1),表示当前系统中只有1个进程(即当前进程)在
    // 运行该执行文件。因此无共享可言，直接退出函数。
    if (!current-&gt;executable)
        return 0;
    if (current-&gt;executable-&gt;i_count &lt; 2)
        return 0;
    // 否则搜索任务数组中所有任务。寻找与当前进程可共享页面的进程，即运行相同的
    // 执行文件的另一个进程，并尝试对指定地址的页面进行共享。如果找到某个进程p，
    // 其executable字段值与当前进程的相同，则调用try_to_share()尝试页面共享。若
    // 共享操作成功，则函数返回1。否则返回0，表示共享页面操作失败.
    for (p = &amp;LAST_TASK ; p &gt; &amp;FIRST_TASK ; --p) {
        if (!*p)
            continue;
        if (current == *p)
            continue;
        // 如果executable不等，表示运行的不是与当前进程相同的执行文件，因此也继续
        // 寻找。
        if ((*p)-&gt;executable != current-&gt;executable)
            continue;
        if (try_to_share(address,*p))
            return 1;
    }
    return 0;
}</code></pre></div><p>&#160; （3）发生缺页后，linux搜先检查能不能和其他进程共享物理页；如果还是不行，那么再重新分配物理页，从磁盘把对应的可执行文件读到物理页（注意：这里是把可执行文件的数据拷贝到内存，和windows下有个隐藏的pagefile.sys文件不一样，后者存放的是临时换出来的内存物理页），最后把物理页挂载到进程的页表项，整个过程就是do_no_page:</p><div class="codebox"><pre class="vscroll"><code>//// 执行缺页处理
// 是访问不存在页面处理函数。页异常中断处理过程中调用的函数。在page.s程序中被调
// 用。函数参数error_code和address是进程在访问页面时由CPU因缺页产生异常而自动生
// 成。该函数首先尝试与已加载的相同文件进行页面共享，或者只是由于进程动态申请内
// 存页面而只需映射一页物理内存即可。若共享操作不成功，那么只能从相应文件中读入
// 所缺的数据页面到指定线性地址处。
void do_no_page(unsigned long error_code,unsigned long address)
{
    int nr[4];
    unsigned long tmp;
    unsigned long page;
    int block,i;

    // 首先取线性空间中指定地址address处页面地址。从而可算出指定线性地址在进程
    // 空间相对于进程基地址的偏移长度值tmp，即对应的逻辑地址。
    address &amp;= 0xfffff000;
    tmp = address - current-&gt;start_code;
    // 若当进程的executable节点指针空，或者指定地址超出(代码+数据)长度，则申请
    // 一页物理内存，并映射到指定的线性地址处。executable是进程正在运行的执行文
    // 件的i节点结构。由于任务0和任务1的代码在内核中，因此任务0，任务1以及任务1
    // 派生的没有调用过execute()的所有任务的executable都为0.若该值为0，或者参数
    // 指定的线性地址超出代码加数据长度，则表明进程在申请新的内存页面存放堆或栈
    // 中数据。因此直接调用取空闲页面函数get_empty_page()为进程申请一页物理内存
    // 并映射到指定线性地址处。进程任务结构字段start_code是线性地址空间中进程代
    // 码段地址，字段end_data是代码加数据长度。对于Linux0.11内核，它的代码段和
    // 数据段其实基址相同。
    if (!current-&gt;executable || tmp &gt;= current-&gt;end_data) {
        get_empty_page(address);
        return;
    }
    if (share_page(tmp))
        return;
    if (!(page = get_free_page()))
        oom();
/* remember that 1 block is used for header */
    // 因为块设备上存放的执行文件映象第1块数据是程序头结构，因此在读取该文件时
    // 需要跳过第1块数据。所以需要首先计算缺页所在数据块号。因为每块数据长度为
    // BLOCK_SIZE=1KB，因此一页内存课存放4个数据块。进程逻辑地址tmp除以数据块大
    // 小再加上1即可得出缺少的页面在执行映象文件中的起始块号block。根据这个块号
    // 和执行文件的i节点，我们就可以从映射位图中找到对应块设备中对应的设备逻辑块
    // 号(保存在nr[]数组中)。利用bread_page()即可把这4个逻辑块读入到物理页面page中。
    block = 1 + tmp/BLOCK_SIZE;
    for (i=0 ; i&lt;4 ; block++,i++)
        nr[i] = bmap(current-&gt;executable,block);
    bread_page(page,current-&gt;executable-&gt;i_dev,nr);
    // 在读设备逻辑块操作时，可能会出现这样一种情况，即在执行文件中的读取页面位
    // 置可能离文件尾不到1个页面的长度。因此就可能读入一些无用的信息，下面的操作
    // 就是把这部分超出执行文件end_data以后的部分清零处理。
    i = tmp + 4096 - current-&gt;end_data;
    tmp = page + 4096;
    while (i-- &gt; 0) {
        tmp--;
        *(char *)tmp = 0;
    }
    // 最后把引起缺页异常的一页物理页面映射到指定线性地址address处。若操作成功
    // 就返回。否则就释放内存页，显示内存不够。
    if (put_page(page,address))
        return;
    free_page(page);
    oom();
}</code></pre></div><p>&#160; &#160;（4）当两个进程共享代码、甚至数据段的物理内存时，如果一个进程改写了物理内存，那么另一个进程是不是也要受影响了？比如同时打开两个notepad，其中一个编辑，另一个不变，是不是另一个也能实时跟新编辑内容了？显然不行（如何行那还的了？）!&#160; 其中一个编辑，会更改数据段的内容，此时如果想要另一个进程免受影响，会启动“copy on write”机制，给其中一个进程单独分配物理页，并重新挂载到页目录项，实现代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*
 * This routine handles present pages, when users try to write
 * to a shared page. It is done by copying the page to a new address
 * and decrementing the shared-page counter for the old page.
 *
 * If it&#039;s in code space we exit with a segment error.
 */
//// 执行写保护页处理。
// 是写共享页面处理函数。是页异常中断处理过程中调用的C函数。在page.s程序中被调用。
// 参数error_code是进程在写写保护页面时由CPU自动产生，address是页面线性地址。
// 写共享页面时，需复制页面（写时复制）.
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
    if (CODE_SPACE(address))
        do_exit(SIGSEGV);
#endif
    // 调用上面函数un_wp_page()来处理取消页面保护。但首先需要为其准备好参数。参
    // 数是线性地址address指定页面在页表中的页表项指针，其计算方法是：
    // 1.((address&gt;&gt;10) &amp; 0xffc): 计算指定线性地址中页表项在页表中的偏移地址；因
    // 为根据线性地址结构，(address&gt;&gt;12)就是页表项中的索引，但每项占4个字节，因
    // 此乘4后：(address&gt;&gt;12)&lt;&lt;2=(address&gt;&gt;10)&amp;0xffc就可得到页表项在表中的偏移
    // 地址。与操作&amp;0xffc用于限制地址范围在一个页面内。又因为只移动了10位，因此
    // 最后2位是线性地址低12位中的最高2位，也应屏蔽掉。因此求线性地址中页表项在
    // 页表中偏移地址直观一些的表示方法是(((address&gt;&gt;12)&amp;ox3ff)&lt;&lt;2).
    // 2.(0xfffff000 &amp; *((address&gt;&gt;20) &amp;0xffc)):用于取目录项中页表的地址值；其中，
    // ((address&gt;&gt;20) &amp;0xffc)用于取线性地址中的目录索引项在目录表中的偏移地址。
    // 因为address&gt;&gt;22是目录项索引值，但每项4个字节，因此乘以4后：(address&gt;&gt;22)&lt;&lt;2
    // = (address&gt;&gt;20)就是指定在目录表中的偏移地址。&amp;0xffc用于屏蔽目录项索引值中
    // 最后2位。因为只移动了20位，因此最后2位是页表索引的内容，应该屏蔽掉。而
    // *((address&gt;&gt;20) &amp;0xffc)则是取指定目录表项内容中对应页表的物理地址。最后与
    // 上0xfffff000用于屏蔽掉页目录项内容中的一些标志位(目录项低12位)。直观表示为
    // (0xfffff000 &amp; *(unsigned log *) (((address&gt;&gt;22) &amp; 0x3ff)&lt;&lt;2)).
    // 3.由1中页表项中偏移地址加上2中目录表项内容中对应页表的物理地址即可得到页
    // 表项的指针(物理地址)。这里对共享的页面进行复制。
    un_wp_page((unsigned long *)
        (((address&gt;&gt;10) &amp; 0xffc) + (0xfffff000 &amp;
        *((unsigned long *) ((address&gt;&gt;20) &amp;0xffc)))));

}</code></pre></div><p>&#160; &#160;上面只调用了一个函数，核心功能就是：（1）去掉页面写保护&#160; &#160;（2）从新找个未使用的物理页挂载到页表项（第二级） （3）物理页数据复制</p><div class="codebox"><pre class="vscroll"><code>//// 取消写保护页面函数。用于页异常中断过程中写保护异常的处理(写时复制)。
// 在内核创建进程时，新进程与父进程被设置成共享代码和数据内存页面，并且所有这些
// 页面均被设置成只读页面。而当新进程或原进程需要向内存页面写数据时，CPU就会检测
// 到这个情况并产生页面写保护异常。于是在这个函数中内核就会首先判断要写的页面是
// 否被共享。若没有则把页面设置成可写然后退出。若页面是出于共享状态，则需要重新
// 申请一新页面并复制被写页面内容，以供写进程单独使用。共享被取消。本函数供下面
// do_wp_page()调用。
// 输入参数为页表项指针，是物理地址。[up_wp_page -- Un-Write Protect Page]
void un_wp_page(unsigned long * table_entry)
{
    unsigned long old_page,new_page;

    // 首先取参数指定的页表项中物理页面位置(地址)并判断该页面是否是共享页面。如
    // 果原页面地址大于内存低端LOW_MEM（表示在主内存区中），并且其在页面映射字节
    // 图数组中值为1（表示页面仅被引用1次，页面没有被共享），则在该页面的页表项
    // 中置R/W标志(可写),并刷新页变换高速缓冲，然后返回。即如果该内存页面此时只
    // 被一个进程使用，并且不是内核中的进程，就直接把属性改为可写即可，不用再重
    // 新申请一个新页面。
    old_page = 0xfffff000 &amp; *table_entry;
    if (old_page &gt;= LOW_MEM &amp;&amp; mem_map[MAP_NR(old_page)]==1) {
        *table_entry |= 2;
        invalidate();
        return;
    }
    // 否则就需要在主内存区申请一页空闲页面给执行写操作的进程单独使用，取消页面
    // 共享。如果原页面大于内存低端(则意味着mem_map[]&gt;1,页面是共享的)，则将原页
    // 面的页面映射字节数组递减1。然后将指定页表项内容更新为新页面地址，并置可读
    // 写等标志（U/S、R/W、P）。在刷新页变换高速缓冲之后，最后将原页面内容复制
    // 到新页面上。
    if (!(new_page=get_free_page()))
        oom();
    if (old_page &gt;= LOW_MEM)
        mem_map[MAP_NR(old_page)]--;
    *table_entry = new_page | 7;
    invalidate();
    copy_page(old_page,new_page);
}</code></pre></div><p>&#160; （5）page_fault的编号是0x14，linux的handler是这样的。里面有两个最重要的函数调用：缺页时调用do_no_page；写入只读页时调用do_wp_page；</p><div class="codebox"><pre class="vscroll"><code>/*
 * page.s contains the low-level page-exception code.
 * the real work is done in mm.c
 */

.globl page_fault       # 声明为全局变量。将在traps.c中用于设置页异常描述符。

page_fault:
    xchgl %eax,(%esp)       # 取出错码到eax；发生异常后，cpu会把error_code自动入栈，不需要软件设置干预
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    movl $0x10,%edx         # 置内核数据段选择符
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
    movl %cr2,%edx          # 取引起页面异常的线性地址；发生异常后，cpu会把异常的地址放入CR2寄存器，不需要软件设置干预
    pushl %edx              # 将该线性地址和出错码压入栈中，作为将调用函数的参数
    pushl %eax
    testl $1,%eax           # 测试页存在标志P（为0），如果不是缺页引起的异常则跳转
    jne 1f
    call do_no_page         # 调用缺页处理函数
    jmp 2f
1:    call do_wp_page         # 调用写保护处理函数
2:    addl $8,%esp            # 丢弃压入栈的两个参数，弹出栈中寄存器并退出中断。
    pop %fs
    pop %es
    pop %ds
    popl %edx
    popl %ecx
    popl %eax
    iret</code></pre></div><p>其他：</p><p>&#160; （1）判断给定线性地址是否位于当前进程的代码段中，这个思路可用于检测自己的so是否被第三方调用了；</p><div class="codebox"><pre><code>// CODE_SPACE(addr)((((addr)+0xfff)&amp;~0xfff)&lt;current-&gt;start_code+current-&gt;end_code).
// 该宏用于判断给定线性地址是否位于当前进程的代码段中，&quot;(((addr)+4095)&amp;~4095)&quot;
// 用于取得线性地址addr所在内存页面的末端地址。
/*
    ~4095=0xF000,作用是把低12bit清零，只保留高4bit；
    (addr)+4095：相当于页的进位相加，比如addr=2048，那么结果等于6143=0x17ff
    (((addr)+4095)&amp;~4095) = 0x17ff&amp;0xF000=0x1000，也就是说虚拟地址2048所在页的末端地址是0x1000=4096
*/
#define CODE_SPACE(addr) ((((addr)+4095)&amp;~4095) &lt; \
current-&gt;start_code + current-&gt;end_code)</code></pre></div><p>&#160; （2）史上最快内存数据复制函数：直接用movsl批量复制</p><div class="codebox"><pre><code>// 从from处复制1页内存到to处(4K字节)。
#define copy_page(from,to) \
__asm__(&quot;cld ; rep ; movsl&quot;::&quot;S&quot; (from),&quot;D&quot; (to),&quot;c&quot; (1024))</code></pre></div><p>&#160; &#160;（3）如果运行的用户程序实在太多，物理内存确实不够用了，需要物理内存的应用程序会被中止，并报OOM的错误（java的码农同学是不是很熟悉了？）</p><div class="codebox"><pre><code>//// 显示内存已用完出错信息，并退出。
static inline volatile void oom(void)
{
    printk(&quot;out of memory\n\r&quot;);
    // do_exit应该使用退出代码，这里用了信号值SIGSEGV(11)相同值的出错码含义是
    // “资源暂时不可用”，正好同义。
    do_exit(SIGSEGV);
}</code></pre></div><p>&#160; （4）源码中有大量的这种代码，这是用来干啥的了？</p><div class="codebox"><pre><code>*((unsigned long *) ((address&gt;&gt;20) &amp;0xffc))</code></pre></div><p>&#160; 我们挨个分解:</p><p> address&gt;&gt;22是目录项索引值（地址移位后只剩10bit了，也就是第一级的页目录表）<br />由于是索引值，需要乘以4得到地址，所以(address&gt;&gt;22)&lt;&lt;2 = address&gt;&gt;20了<br />由于只移动了20位，还有2位要清零，所以&amp;FFC<br />最后 *((address&gt;&gt;20) &amp;0xffc) 则是取指定目录表项（第一级）内容中对应页表的物理地址（取出来的物理地址0x1000对齐）<br />&#160; </p><p>参考：</p><p>1、https://zhuanlan.zhihu.com/p/67053210&#160; 页表描述符</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 08 Oct 2022 05:13:41 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=449&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[操作系统——内存管理]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=42&amp;action=new</link>
			<description><![CDATA[<p>文章目录</p><p>&#160; &#160; 1 内存管理的概念<br />&#160; &#160; &#160; &#160; 1.1 内存管理的基本原理和要求<br />&#160; &#160; &#160; &#160; 1.2 覆盖与交换<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.2.1 覆盖<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.2.2 交换<br />&#160; &#160; &#160; &#160; 1.3 连续分配管理方式<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.3.1 单一连续分配（无外部碎片，有内部碎片）<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.3.2 固定分区分配（无外部碎片，有内部碎片）<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.3.3 动态分区分配（没有内部碎片，但有外部碎片）<br />&#160; &#160; &#160; &#160; 1.4 非连续分配管理方式<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.4.1 基本分页存储管理方式<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.4.2 基本分段存储管理方式<br />&#160; &#160; &#160; &#160; &#160; &#160; 1.4.3 段页式管理方式<br />&#160; &#160; 2. 虚拟内存管理<br />&#160; &#160; &#160; &#160; 2.1 虚拟内存的基本概念<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.1.1 传统存储管理方式的特征<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.1.2 局部性原理<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.1.3 虚拟存储器的定义和特性<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.1.4 虚拟内存技术的实现<br />&#160; &#160; &#160; &#160; 2.2 请求分页管理方式<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.2.1 页表机制<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.2.2 缺页中断机构<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.2.3 地址变换机构<br />&#160; &#160; &#160; &#160; 2.3 页面置换算法<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.3.1 最佳置换算法（OPT）<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.3.2 先进先出页面置换算法（FIFO）<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.3.3 最近最久未使用置换算法（LRU）<br />&#160; &#160; &#160; &#160; &#160; &#160; 2.3.4 时钟置换算法（CLOCK）<br />&#160; &#160; &#160; &#160; 2.4 页面分配策略<br />&#160; &#160; &#160; &#160; 2.5 抖动<br />&#160; &#160; &#160; &#160; 2.6 工作集</p><p>1 内存管理的概念<br />1.1 内存管理的基本原理和要求</p><p>计算机不可能将所有用户进程和系统所需要的全部程序和数据放入主存，因此操作系统必须对内存空间进行合理的划分和有效的动态分配。内存管理的概念就是操作系统对内存的划分和动态分配。</p><p>内存管理功能：</p><p>&#160; &#160; 内存空间的分配与回收：由操作系统完成主存储器空间的分配和管理，是程序员摆脱存储分配的麻烦，提高编程效率。<br />&#160; &#160; 地址转换：将逻辑地址转换成相应的物理地址。<br />&#160; &#160; 内存空间的扩充：利用虚拟存储技术或自动覆盖技术，从逻辑上扩充主存。<br />&#160; &#160; 存储保护：保证各道作业在各自的存储空间内运行，互不干扰。</p><p>创建进程首先要将程序和数据装入内存。将用户源程序变为可在内存中执行的程序，通常需要以下几个步骤：</p><p>&#160; &#160; 编译：由编译程序将用户源代码编译成若干目标模块（把高级语言翻译成机器语言）<br />&#160; &#160; 链接：由链接程序将编译后形成的一组目标模块及所需的库函数连接在一起，形成一个完整的装入模块（由目标模块生成装入模块，链接后形成完整的逻辑地址）<br />&#160; &#160; 装入：由装入程序将装入模块装入内存运行，装入后形成物理地址</p><p>程序的链接有以下三种方式：</p><p>&#160; &#160; 静态链接：在程序运行之前，先将各目标模块及它们所需的库函数连接成一个完整的可执行文件（装入模块），之后不再拆开。<br />&#160; &#160; 装入时动态链接：将各目标模块装入内存时，边装入边链接的链接方式。<br />&#160; &#160; 运行时动态链接：在程序执行中需要该目标模块时，才对它进行链接。其优点是便于修改和更新，便于实现对目标模块的共享。</p><p>内存的装入模块在装入内存时，有以下三种方式：</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/97e5143c52dd49d0909f31d20bdda23e.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span> </p><p>重定位：根据内存的当前情况，将装入模块装入内存的适当位置，装入时对目标程序中的指令和数据的修改过程称为重定位。</p><p>&#160; &#160; 静态重定位：地址的变换通常是在装入时一次完成的。一个作业装入内存时，必须给它分配要求的全部内存空间，若没有足够的内存，则不能装入该作业。此外，作业一旦装入内存，整个运行期间就不能在内存中移动，也不能再申请内存空间。<br />&#160; &#160; 动态重定位：需要重定位寄存器的支持。可以将程序分配到不连续的存储区中；在程序运行之前可以只装入它的部分代码即可投入运行，然后在程序运行期间，根据需要动态申请分配内存。</p><p>内存分配前，需要保护操作系统不受用户进程的影响，同时保护用户进程不受其他用户进程的影响。内存保护可采取如下两种方法：</p><p>&#160; &#160; 在CPU中设置一对上、下限寄存器，存放用户作业在主存中的上限和下限地址，每当CPU要访问一个地址时，分别和两个寄存器的值相比，判断有无越界。<br />&#160; &#160; 采用重定位寄存器（或基址寄存器）和界地址寄存器（又称限长存储器）来实现这种保护。重定位寄存器包含最小的物理地址值，界地址寄存器含逻辑地址的最大值。每个逻辑地址值必须小于界地址寄存器；内存管理机构动态得将逻辑地址与界地址寄存器进行比较，若未发生地址越界，则加上重定位寄存器的值后映射成物理地址，再送交内存单元。</p><p>1.2 覆盖与交换</p><p>覆盖与交换技术是在多道程序环境下用来扩充内存的两种方法，目的是减少程序占用的主存空间来扩充内存。<br />覆盖是在同一个程序或进程中的，交换是在不同进程之间的。<br />1.2.1 覆盖</p><p>覆盖技术的基本思想：将程序分为多个段，常用的段常驻内存，不常用的段在需要时调入内存。</p><p>特点：打破了必须将一个进程的全部信息装入主存后才能运行的限制，解决了程序大小超过物理内存总和的问题。但对用户不透明，增加了编程的负担<br />1.2.2 交换</p><p>交换技术的基本思想：内存空间紧张时，系统将内存中的某些进程暂时换出外存，把外存中某些已具备运行条件的进程换入内存。</p><p>交换的时机选择的策略：</p><p>&#160; &#160; 进程不用或很少再用的就换出<br />&#160; &#160; 内存空间不够或者有不够的危险时，启动交换程序换出</p><p>1.3 连续分配管理方式</p><p>连续分配方式是指为一个用户程序分配一个连续的内存空间。主要包括单一连续分配、固定分区分配和动态分区分配。</p><p>&#160; &#160; 内部碎片：分配给某进程的内存区域中，有些部分没用上<br />&#160; &#160; 外部碎片：是指内存中的某些空闲分区由于太小而难以利用</p><p>1.3.1 单一连续分配（无外部碎片，有内部碎片）</p><p>在单一连续分配方式中，内存被分为系统区和用户区。系统区通常位于内存的低地址部分，用于存放操作系统相关数据;用户区用于存放用户进程相关数据。<br />内存中只能有一道用户程序，用户程序独占整个用户区空间。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/68b786497455494b8930bdf636944d82.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_11,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; 优点：实现简单;无外部碎片；可以采用覆盖技术扩充内存；不一定需要采取内存保护 。<br />&#160; &#160; 缺点：只能用于单用户、单任务的操作系统中；有内部碎片；存储器利用率极低。</p><p>1.3.2 固定分区分配（无外部碎片，有内部碎片）</p><p>固定分区分配是最简单的一种多道程序存储管理方式，它将用户内存空间划分为若干固定大小的分区，每个分区只装入一道作业，当有空闲分区时，便可再从外存的后备队列中选择适当大小的作业装入该分区。</p><p>固定分区分配分为分区大小相等和分区大小不等两种方式。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/60d1c84953234bbaa0e1cd584bfafdbb.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_9,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; 分区大小相等：适用于利用一台计算机去控制多个相同对象的场合，缺乏灵活性。程序太小则浪费内存，程序太大的分区装不下。<br />&#160; &#160; 分区大小不等：划分多个较小的分区、适量的中等分区和少量的大分区，增加了灵活性。</p><p>1.3.3 动态分区分配（没有内部碎片，但有外部碎片）</p><p>动态分区分配不预先分配内存，而是在进程装入内存时，根据进程的大小动态地建立分区，并使分区的大小正好适合进程的需要。系统中分区的大小和数目是可变的。</p><p>动态分区分配会产生外部碎片，克服外部碎片可以采用紧凑技术来解决，即操作系统不时地对进程进行移动和整理。但这需要重定位寄存器的支持，且相对费时。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/d35ff11ebb904277b75353174c1ed288.png" alt="FluxBB bbcode 测试" /></span></p><p>动态分区分配的策略有以下几种算法：</p><p>&#160; &#160; 首次适应算法：按地址从小到大为序，分配第一个符合条件的分区。<br />&#160; &#160; 最佳适应算法：按空间从小到大为序，分配第一个符合条件的分区。<br />&#160; &#160; 最坏适应算法：按空间从大到小为序，分配第一个符合条件的分区。<br />&#160; &#160; 邻近适应算法：与首次适应相似，从上次查完的结束位置开始查找。</p><p>其中，首次适应算法不仅是最简单的，而且通常也是最好和最快的，但是会使得内存的低地址部分出现很多小的空闲分区，每次分配查找时，都要经过这些分区，因此增加了查找的开销。邻近适应算法试图解决这个问题，但实际上，它常常导致在内存的末尾分配空间分裂成小碎片，通常比首次适应算法的效果要差。<br />最佳适应算法会产生最多的外部碎片。<br />最坏适应算法会很快导致没有可用的大内存块。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/fe1068736ed14660a78ebc7a96b5ffe4.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>1.4 非连续分配管理方式</p><p>非连续分配允许一个程序分散地装入不相邻的内存分区，但这也需要额外的存储空间去存储它们的索引，使得非连续分配方式的存储密度低于连续存储方式。<br />非连续分配管理方式根据分区的大小是否固定，分为分页存储管理方式和分段存储管理方式。在分页存储管理方式中，又根据运行作业时是否要把作业的所有页面都装入内存才能运行，分为基本分页存储管理方式和请求分页存储管理方式。<br />1.4.1 基本分页存储管理方式</p><p>不会产生外部碎片，只会产生少量的内部碎片<br />分页的基本思想：把主存空间划分为大小相等的块，块相对较小，作为主存的基本单位。每个进程也以块为单位进行划分，进程在执行时，以块为单位逐个申请主存中的块空间。</p><p>分页存储的基本概念如下：</p><p>&#160; &#160; 页面和页面大小。进程中的块称为页，内存中的块称为页框，外存也以同样的单位进行划分，直接称为块。<br />&#160; &#160; 页面大小应该适中，页面太小会使进程的页面数过多，这样页表就会过长，占用大量内存，而且也会增加硬件地址转换的开销，降低页面换入/换出的效率；页面太大又会使页内碎片增多，降低内存利用率。<br />&#160; &#160; 地址结构。地址结构包含两部分，前一部分为页号P，后一部分为页内偏移量W。地址结构决定了虚拟内存的寻址空间有多大。<br />&#160; &#160; 页号 = 逻辑地址 / 页面长度<br />&#160; &#160; 页内偏移量 = 逻辑地址 % 页面长度<br />&#160; &#160; 页表。记录进程页面和实际存放的内存块之间的对应关系。页表由页表项构成，一般存放在内存中。页表的作用是实现从页号到物理块号的映射。页表项的作用是找到该页在内存中的位置。<br />&#160; &#160; 页表寄存器。存放页表的起始地址和页表长度。</p><p>逻辑地址到物理地址的变换过程如下：</p><p>&#160; &#160; 计算页号Р和页内偏移量W，P=A/L，W=A%L<br />&#160; &#160; 比较页号P和页表长度M，若P≥M，则产生越界中断，否则继续执行。（注意：页号是从0开始的,而页表长度至少是1，因此 P=M时也会越界)<br />&#160; &#160; 查询页表，找到页号对应的页表项，确定页面存放的内存块号<br />&#160; &#160; 用内存块号和页内偏移量得到物理地址<br />&#160; &#160; 访问目标内存单元</p><p>分页管理方式存在的两个主要问题：</p><p>&#160; &#160; 每次访存操作都需要进行逻辑地址到物理地址的转换，地址转换过程必须足够快，否则访存速度会降低。<br />&#160; &#160; 每个进程引入页表，用于存储映射机制，页表不能太大，否则内存利用率会降低。</p><p>具有快表的地址变换机构：</p><p>&#160; &#160; 时间局部性：如果执行了程序中的某条指令，那么不久后这条指令很有可能再次执行;如果某个数据被访问过，不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)<br />&#160; &#160; 空间局部性：一旦程序访问了某个存储单元，在不久之后，其附近的存储单元也很有可能被访问。（因为很多数据在内存中都是连续存放的)</p><p>若页表全部放在内存中，则存取一个数据或一条指令至少要访问两次内存，第一次是访问页表，确定所存取的数据或指令的物理地址；第二次是根据该地址存取数据或指令。<br />在地址变换机构中增设一个具有并行查找能力的高速缓冲存储器——快表，又称相联存储器（TLB），用来存放当前访问的若干页表项，以加速地址变换过程。</p><p>引入快表后的地址变换过程如下：</p><p>&#160; &#160; CPU给出逻辑地址，由某个硬件算得页号、页内偏移量，将页号与快表中的所有页号进行比较。<br />&#160; &#160; 如果找到匹配的页号，说明要访问的页表项在快表中有副本，则直接从中取出该页对应的内存块号，再将内存块号与页内偏移量拼接形成物理地址，最后，访问该物理地址对应的内存单元。因此，若快表命中，则访问某个逻辑地址仅需一次访存即可。<br />&#160; &#160; 如果没有找到匹配的页号，则需要访问内存中的页表，找到对应页表项，得到页面存放的内存块号，再将内存块号与页内偏移量拼接形成物理地址，最后，访问该物理地址对应的内存单元。因此，若快表未命中，则访问某个逻辑地址需要两次访存（注意:在找到页表项后，应同时将其存入快表，以便后面可能的再次访问。但若快表已满，则必须按照一定的算法对旧的页表项进行替换)</p><p>两级页表<br />单级页表存在的问题：</p><p>&#160; &#160; 所有的页表必须连续存放，页表过大时需要很大的连续空间<br />&#160; &#160; 在一段时间并非所有的页面都用得到，因此没必要让整个页表常驻内存</p><p>为了压缩页表，采用二级页表机制。在进程执行时，只需要将这一页的上一级页表调入内存即可，进程的页表和进程本身的页面可在后面的执行中再调入内存。</p><p>建立多级页表的目的在于建立索引，以便不用浪费主存空间去存储无用的页表项，也不用盲目地顺序式查找页表项。若采用多级页表机制，则各项页表的大小不能超过一个页面。<br />1.4.2 基本分段存储管理方式</p><p>不会产生内部碎片，会产生外部碎片</p><p>&#160; &#160; 分页管理方式是从计算机地角度考虑设计的，目的是提高内存利用率，提升计算机的性能。分页通过硬件机制实现，对用户完全透明。<br />&#160; &#160; 分段管理方式的提出则考虑了用户和程序员，以满足方便编程、信息保护和共享、动态增长及动态链接等多方面的需要。</p><p>分段存储管理的相关概念如下：</p><p>&#160; &#160; 分段<br />&#160; &#160; 段式管理按照用户进程中的自然段划分逻辑空间，段内要求连续，段间不要求连续，整个作业的地址空间是二维的，其逻辑地址由段号S和段内偏移量W两部分组成。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/fe7913d7c78c4a5fa0ea9afb1495a8c1.png" alt="FluxBB bbcode 测试" /></span></p><p>段号的位数决定了每个进程最多可以分为几个段，段内地址的位数决定了每个段的最大长度是多少。<br />段表<br />每个进程都有一张逻辑空间与内存空间映射的段表，每个段表项对于进程的一段，段报项记录该段在内存中的起始和长度，段表用于实现从逻辑段到物理内存区的映射。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/1d3bfbf8af50448ebcda413f42d60cff.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>地址变换机构<br />为了实现进程从逻辑地址到物理地址的变换功能，在系统中设置了段表寄存器，用于存放段表始址F和段表长度M。<br />地址变换过程如下所示：</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/6299357da65b4bb09353c7b2c59daefc.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; 段的共享与保护<br />&#160; &#160; 在分段系统中，段的共享是通过两个作业的段表中相应表项指向被共享的段的同一个物理副本来实现的。不能修改的代码称为可重入代码（不属于临界资源），这样的代码和不能修改的数据可以共享，而可修改的代码和数据不能共享。<br />&#160; &#160; 分段管理的保护方法主要有两种，一种是存取控制保护，另一种是地址越界保护。地址越界保护将段表寄存器中的段表长度与逻辑地址中的段号比较，若段号大于段表长度，则产生越界中断；再将段表项中的段长与逻辑地址中的段内偏移进行比较，若段内偏移大于段长，也会产生越界中断。分页管理中的越界保护只需要判断页号是否越界，页内偏移是不可能越界的。</p><p>分页与分段的对比</p><p>&#160; &#160; 页是信息的物理单位。分页的主要目的是为了实现离散分配，提高内存利用率。分页仅仅是系统管理上的需要，完全是系统行为，对用户是不可见的。<br />&#160; &#160; 段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的，用户编程时需要显式地给出段名。<br />&#160; &#160; 页的大小固定且由系统决定。段的长度却不固定，决定于用户编写的程序。<br />&#160; &#160; 分页的用户进程地址空间是一维的，程序员只需给出一个记忆符即可表示一个地址。<br />&#160; &#160; 分段的用户进程地址空间是二维的，程序员在标识一个地址时，既要给出段名，也要给出段内地址。<br />&#160; &#160; 分段比分页更容易实现信息的共享和保护。不能被修改的代码称为纯代码或可重入代码（不属于临界资源），这样的代码是可以共享的。可修改的代码是不能共享的<br />&#160; &#160; 访问一个逻辑地址的访存次数<br />&#160; &#160; 分页（单级页表)︰第一次访存――查内存中的页表，第二次访存――访问目标内存单元。总共两次访存<br />&#160; &#160; 分段:第一次访存――查内存中的段表，第二次访存――访问目标内存单元。总共两次访存</p><p>1.4.3 段页式管理方式</p><p>页式管理方式能有效地提高内存利用率，而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方式结合起来，便形成了段页式存储管理方式。</p><p>在段页式系统中，作业的地址空间首先被分成若干逻辑段，每段有自己的段号，然后将每段分成若干大小固定的页。对内存空间的管理仍然和分页存储管理一样，将其分成若干和页面大小相同的存储块，对内存的分配以存储块为单位。</p><p>段页式系统中的作业逻辑地址分为三部分：段号、页号和页内偏移量。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/cedbd2cebf1f4ba9a37458746da33916.png" alt="FluxBB bbcode 测试" /></span></p><p>段号的位数决定了每个进程最多可以分几个段<br />页号位数决定了每个段最大有多少页<br />页内偏移量决定了页面大小、内存块大小是多少</p><p>其中，在一个进程中，段表只有一个，而页表可能有多个。进行一次访问需要查段表、查页表和访问目标单元三次访存。</p><p>段页式系统的地址变换机构如下所示：</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/081567b460994922934a1de1a514ac27.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>2. 虚拟内存管理<br />2.1 虚拟内存的基本概念<br />2.1.1 传统存储管理方式的特征</p><p>&#160; &#160; 一次性：作业必须一次性全部装入内存后，才能开始运行。<br />&#160; &#160; 驻留性：作业被装入内存后，就一直驻留在内存中，其任何部分都不会被换出，直至作业结束运行。</p><p>2.1.2 局部性原理</p><p>高速缓存技术利用的是局部性原理，将频繁使用的数据放到更高速的存储器中。</p><p>&#160; &#160; 时间局部性：如果执行了程序中的某条指令，那么不久后这条指令很有可能再次执行;如果某个数据被访问过，不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)<br />&#160; &#160; 空间局部性：一旦程序访问了某个存储单元，在不久之后，其附近的存储单元也很有可能被访问。（因为很多数据在内存中都是连续存放的)</p><p>2.1.3 虚拟存储器的定义和特性</p><p>基于局部性原理，在程序装入时，可以将程序中很快会用到的部分装入内存，暂时用不到的部分留在外存，就可以让程序开始执行。在程序执行过程中，当所访问的信息不在内存时，由操作系统负责将所需信息从外存调入内存，然后继续执行程序。若内存空间不够，由操作系统负责将内存中暂时用不到的信息换出到外存。在操作系统的管理下，在用户看来似乎有一个比实际内存大得多的内存，这就是虚拟内存。</p><p>虚拟存储器的最大容量由计算机的地址结构决定，实际容量 = min{内存和外存的容量之和，CPU的寻址范围}。并不是简单的内外存容量相加。</p><p>虚拟存储器有以下三个特性：</p><p>&#160; &#160; 多次性：无需在作业运行时一次性全部装入内存，而是允许被分成多次调入内存。<br />&#160; &#160; 对换性：在作业运行时无需一直常驻内存，而是允许在作业运行过程中，将作业换入、换出。<br />&#160; &#160; 虚拟性：从逻辑上扩充了内存的容量，使用户看到的内存容量，远大于实际的容量。</p><p>2.1.4 虚拟内存技术的实现</p><p>虚拟内存技术的实现需要建立在离散分配的内存管理方式的基础上。</p><p>虚拟内存的实现有以下三种方式：</p><p>&#160; &#160; 请求分页存储管理<br />&#160; &#160; 请求分段存储管理<br />&#160; &#160; 请求段页式存储管理</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/a1b08374b2ba47b0b595fd37fb2a8682.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>不管哪种实现方式，都需要硬件技术的支持。一般需要的支持有以下几个方面：</p><p>&#160; &#160; 一定容量的内存和外出<br />&#160; &#160; 页表机制（或段表机制）作为主要的数据结构<br />&#160; &#160; 中断机构：当用户程序要访问的部分尚未调入内存时，则产生中断<br />&#160; &#160; 地址变换机构：实现逻辑地址到物理地址的变换</p><p>2.2 请求分页管理方式</p><p>请求分页产生内部碎片，请求分段产生外部碎片</p><p>请求分页系统建立在基本分页系统的基础之上，为了支持虚拟存储器功能而增加了请求调页和页面置换功能。在请求分页系统中，只要求将当前需要的一部分页面装入内存，便可以启动程序运行。<br />2.2.1 页表机制</p><p>请求分页系统的页表机制不同于基本分页系统，请求分页系统在一个作业运行之前不要求全部一次性调入内存，因此在作业运行过程中，必然会出现要访问的页面不在内存中的情况，如何发现和处理这种情况是请求分页系统必须解决的两个基本问题。为此，在请求页表项中增加了4个字段，如下图所示。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/886b3404733a47fb9abb0f3c0b040ba2.png" alt="FluxBB bbcode 测试" /></span></p><br /><p>&#160; &#160; 状态位：用于指示该页是否已调入内存，供程序访问时参考。<br />&#160; &#160; 访问字段：用于记录本页在一段时间内被访问的次数，或记录本页最近已有多长时间未被访问，供置换算法换出页面时参考。<br />&#160; &#160; 修改位：标识该页在调入内存后是否被修改过。<br />&#160; &#160; 外存地址：用于指出该页在外存上的地址，通常是物理块号，供调入该页时参考。</p><p>2.2.2 缺页中断机构</p><p>在请求分页系统中，每当要访问的页面不在内存中时，便产生一个缺页中断，请求操作系统将所缺的页调入内存。此时应将缺页的进程阻塞（调页完成唤醒），若内存中有空闲块，则分配一个块，将要调入的页装入该块，并修改页表中的相应页表项，若此时内存中没有空闲块，则要淘汰某页，若被淘汰的页在内存期间被修改过，则要将其写回外存。</p><p>缺页中断与一般中断的不同：</p><p>&#160; &#160; 在指令执行期间而非一条指令执行完后产生和处理中断信号，属于内部中断。<br />&#160; &#160; 一条指令在执行期间，可能产生多次缺页中断。</p><p>2.2.3 地址变换机构</p><p>在进行地址变换时，先检索快表：</p><p>&#160; &#160; 若找到要访问的页，则修改页表项中的访问位，然后利用页表项中给出的物理块号和页内地址形成物理地址。<br />&#160; &#160; 若未找到该页的页表项，则应到内存中去查找页表，再对比页表项中的状态位P，看该页是否已调入内存，未调入内存则产生缺页中断，请求从外存把该页调入内存。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/251e863bad114fea88803280ee7d6ea3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>请求分页中的地址变换过程如下所示：</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/be419119c8e74b9fb05063f59087aa77.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>2.3 页面置换算法</p><p>进程运行时，若其访问的页面不在内存中而需将其调入，但内存已无空闲空间时，就需要从内存中调出一页程序或数据，送入磁盘的对换区。<br />选择调出页面的算法就称为页面置换算法。用页面置换算法决定应该换出哪个页面。页面的换入换出需要有磁盘的I/O，会有较大的开销，因此好的页面置换算法需要追求更少的缺页率。<br />2.3.1 最佳置换算法（OPT）</p><p>最佳页面置换算法选择的被淘汰页面是以后永不使用的页面，或是在最长时间内不再被访问的页面，以便保证获得最低的缺页率。但是，人们无法预知进程在内存下的若干页面中的哪个是未来最长时间内不再被访问的，因为该算法无法实现。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/a707cc7824544294b8681b69aeb035cb.png" alt="FluxBB bbcode 测试" /></span></p><p>2.3.2 先进先出页面置换算法（FIFO）</p><p>优先淘汰最早进入内存的页面，即再内存中驻留时间最久的页面。<br />FIFO算法还会产生所分配的物理块数增大而页故障数不减反增的异常现象，这成为Belady异常。只有FIFO算法可能出现Belady异常。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/6712f91f01504a16a4debd92ec7072ac.png" alt="FluxBB bbcode 测试" /></span></p><p>Belady异常如下图所示：<br /><span class="postimg"><img src="https://img-blog.csdnimg.cn/99113762dcd6482b838c0e63012a549b.png" alt="FluxBB bbcode 测试" /></span></p><p>物理块数由三个增加到四个，缺页数反而增加。<br />2.3.3 最近最久未使用置换算法（LRU）</p><p>选择最近最长时间未访问过的页面予以淘汰，它认为过去一段时间内未访问过的页面，在最近的将来可能也不会被访问。该算法为每个页面设置一个访问字段，来记录页面自上次被访问以来所经历的时间，淘汰页面时选择现有页面中值最大的予以淘汰。<br />LRU算法需要寄存器和栈的硬件支持。</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/c4f87e7783e54faca35eaf8a111c1502.png" alt="FluxBB bbcode 测试" /></span></p><br /><p>2.3.4 时钟置换算法（CLOCK）</p><p>最佳置换算法性能最好，但无法实现;先进先出置换算法实现简单，但算法性能差;最近最久未使用置换算法性能好，是最接近OPT算法性能的，但是实现起来需要专门的硬件支持，算法开销大。<br />时钟置换算法是一种性能和开销较均衡的算法，又称CLOCk算法，或最近未用算法（NRU，NotRecently Used)</p><p>简单的CLOCK算法实现方法<br />为每个页面设置一个访问位，再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时，其访问位置为1。当需要淘汰一个页面时，只需检查页的访问位。如果是o，就选择该页换出;如果是1，则将它置为0，暂不换出，继续检查下一个页面，若第一轮扫描中所有页面都是1，则将这些页面的访问位依次置为o后，再进行第二轮扫描（第二轮扫描中一定会有访问位为0的页面，因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描)</p><p>改进型的时钟算法<br />简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上，如果被淘汰的页面没有被修改过，就不需要执行I/O操作写回外存。只有被淘汰的页面被修改过时，才需要写回外存。<br />因此，除了考虑一个页面最近有没有被访问过之外，操作系统还应考虑页面有没有被修改过。在其他条件都相同时，应优先淘汰没有修改过的页面，避免I/O操作。这就是改进型的时钟置换算法的思想。修改位=0，表示页面没有被修改过;修改位=1，表示页面被修改过。</p><p>为方便讨论，用（访问位，修改位）的形式表示各页面状态。如（1，1）表示一个页面近期被访问过，且被修改过。</p><p>算法规则:将所有可能被置换的页面排成一个循环队列<br />第一轮:从当前位置开始扫描到第一个(0,0）的帧用于替换。本轮扫描不修改任何标志位<br />第二轮:若第一轮扫描失败，则重新扫描，查找第一个（0,1）的帧用于替换。本轮将所有扫描过的帧访问位设为o<br />第三轮:若第二轮扫描失败，则重新扫描，查找第一个(0,0）的帧用于替换。本轮扫描不修改任何标志位<br />第四轮:若第三轮扫描失败，则重新扫描，查找第一个(0,1）的帧用于替换。<br />由于第二轮已将所有帧的访问位设为0，因此经过第三轮、第四轮扫描一定会有一个帧被选中，因此改进型cLOcK置换算法选择一个淘汰页面最多会进行四轮扫描.<br />2.4 页面分配策略</p><p>驻留集：指请求分页存储管理中给进程分配的物理块的集合。</p><p>&#160; &#160; 固定分配：操作系统为每个进程分配一组固定数目的物理块，在进程运行期间不再改变。即，驻留集大小不变</p><p>&#160; &#160; 可变分配：先为每个进程分配一定数目的物理块，在进程运行期间，可根据情况做适当的增加或减少。即，驻留集大小可变</p><p>&#160; &#160; 局部置换：发生缺页时只能选进程自己的物理块进行置换。</p><p>&#160; &#160; 全局置换：可以将操作系统保留的空闲物理块分配给缺页进程，也可以将别的进程持有的物理块置换到外存，再分配给缺页进程。</p><p>页面分配、置换策略</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/14fa87769f2d4298bd667e4e0f2be348.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>调入页面的时机</p><p>&#160; &#160; 预调页策略：将预计在不久之后便会被访问的页面预先调入内存。成功率约为50%，因此这种策略主要用于进程的首次调入。<br />&#160; &#160; 请求调页策略：进程在运行中需要访问的页面不在内存而提出请求，由系统将所需页面调入内存。缺点是每次只调入一页，调入、调出页面数多时会花费过多的I/O开销。</p><p>从何处调入页面<br /><span class="postimg"><img src="https://img-blog.csdnimg.cn/17d41eddc9364bccb64a3efe00d18339.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbm93IH4gdHJ5,size_20,color_FFFFFF,t_70,g_se,x_16" alt="FluxBB bbcode 测试" /></span></p><p>2.5 抖动</p><p>刚刚换出的页面马上又换入主存，刚刚换入的页面马上又换出主存，这种频繁的页面调度行为称为抖动或颠簸。<br />抖动发生的主要原因是，进程频繁访问的页面数目高于可用的物理页帧数目，即分配给进程的物理块不够。<br />2.6 工作集</p><p>工作集指在某段时间间隔内，进程要访问的页面集合。基于局部性原理，可以用最近访问过的页面来确定工作集。</p><p>若工作集窗口大小为4，则各时刻工作集如下所示：</p><p><span class="postimg"><img src="https://img-blog.csdnimg.cn/a177491581c544d084a983763a9333ac.png" alt="FluxBB bbcode 测试" /></span></p><p>为了防止抖动现象，一般来说给进程分配的物理块数（即驻留集大小）要大于工作集大小。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Mon, 08 Aug 2022 15:14:08 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=42&amp;action=new</guid>
		</item>
	</channel>
</rss>
