検索

关于高端内存的权威解释

Jun 07, 2016 pm 03:11 PM
についてメモリ説明するハイエンド

注:本文是我见到的所有 关于 高端内存 解释 的最详细、最清晰的 解释 ,其他帖子寥寥数语写的都是垃圾,保存下来只为方便后来人和我自己,感谢原文作者! 原文地址:http://bbs.chinaunix.net/thread-1938084-1-1.html 注:本文提及的物理地址空间可以理解

注:本文是我见到的所有关于高端内存解释的最详细、最清晰的解释,其他帖子寥寥数语写的都是垃圾,保存下来只为方便后来人和我自己,感谢原文作者!

原文地址:http://bbs.chinaunix.net/thread-1938084-1-1.html



注:本文提及的物理地址空间可以理解为就是物理内存,但是在某些情况下,把他们理解为物理内存是不对的。

本文讨论的环境是NON-PAE的i386平台,内核版本2.6.31-14

一.什么是高端内存

linux中内核使用3G-4G的线性地址空间,也就是说总共只有1G的地址空间可以用来映射物理地址空间。但是,如果内存大于1G的情况下呢?是不是超过1G的内存就无法使用了呢?为此内核引入了一个高端内存的概念,把1G的线性地址空间划分为两部分:小于896M物理地址空间的称之为低端内存,这部分内存的物理地址和3G开始的线性地址是一一对应映射的,也就是说内核使用的线性地址空间3G--(3G+896M)和物理地址空间0-896M一一对应;剩下的128M的线性空间用来映射剩下的大于896M的物理地址空间,这也就是我们通常说的高端内存区。

所谓的建立高端内存的映射就是能用一个线性地址来访问高端内存的页。如何理解这句话呢?在开启分页后,我们要访问一个物理内存地址,需要经过MMU的转换,也就是一个32位地址vaddr的高10位用来查找该vaddr所在页目录项,用12-21位来查找页表项,再用0-11位偏移和页的起始物理地址相加得到paddr,再把该paddr放到前端总线上,那么我们就可以访问该vaddr对应的物理内存了。在低端内存中,每一个物理内存页在系统初始化的时候都已经存在这样一个映射了。而高端内存还不存在这样一个映射(页目录项,页表都是空的),所以我们必须要在系统初始化完后,提供一系列的函数来实现这个功能,这就是所谓的高端内存的映射。那么我们为什么不再系统初始化的时候把所有的内存映射都建立好呢?主要原因是,内核线性地址空间不足以容纳所有的物理地址空间(1G的内核线性地址空间和最多可达4G的物理地址空间),所以才需要预留一部分(128M)的线性地址空间来动态的映射所有的物理地址空间,于是就产生了所谓的高端内存映射。

二.内核如何管理高端内存


 

 

 

上面的图展示了内核如何使用3G-4G的线性地址空间,首先解释下什么是high_memory

在arch/x86/mm/init_32.c里面由如下代码:

 

 

#ifdef CONFIG_HIGHMEM

        highstart_pfn = highend_pfn = max_pfn;

        if (max_pfn > max_low_pfn)

                highstart_pfn = max_low_pfn;

        e820_register_active_regions(0, 0, highend_pfn);

        sparse_memory_present_with_active_regions(0);

        printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",

                pages_to_mb(highend_pfn - highstart_pfn));

        num_physpages = highend_pfn;

          high_memory = (void *) __va(highstart_pfn * PAGE_SIZE-1)+1; 

#else

        e820_register_active_regions(0, 0, max_low_pfn);

        sparse_memory_present_with_active_regions(0);

        num_physpages = max_low_pfn;

         high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1)+1; 

#endif

 

 

high_memory是“具体物理内存的上限对应的虚拟地址”,可以这么理解:当内存内存小于896M时,那么high_memory = (void *)__va(max_low_pfn * PAGE_SIZE),max_low_pfn就是在内存中最后的一个页帧号,所以high_memory=0xc0000000+物理内存大小;当内存大于896M时,那么highstart_pfn= max_low_pfn,此时max_low_pfn就不是物理内存的最后一个页帧号了,而是内存为896M时的最后一个页帧号,那么high_memory=0xc0000000+896M.总之high_memory是不能超过0xc0000000+896M.

由于我们讨论的是物理内存大于896M的情况,所以high_memory实际上就是0xc0000000+896M,从high_memory开始的128M(4G-high_memory)就是用作用来映射剩下的大于896M的内存的,当然这128M还可以用来映射设备的内存(MMIO)。

从上图我们看到有VMALLOC_START,VMALLOC_END,PKMAP_BASE,FIX_ADDRESS_START等宏术语,其实这些术语划分了这128M的线性空间,一共分为三个区域:VMALLOC区域(本文不涉及这部分内容,关注本博客的其他文章),永久映射区(permanetkernelmappings), 临时映射区(temporary kernelmappings).这三个区域都可以用来映射高端内存,本文重点阐述下后两个区域是如何映射高端内存的。

 

 

三.永久映射区(permanet kernel mappings)

1.介绍几个定义:

PKMAP_BASE:永久映射区的起始线性地址。

pkmap_page_table:永久映射区对应的页表

LAST_PKMAP:pkmap_page_table里面包含的entry的数量=1024

pkmap_count[LAST_PKMAP]数组:每一个元素的值对应一个entry的引用计数。关于引用计数的值,有以下几种情况:

 

0:说明这个entry可用。

1:entry不可用,虽然这个entry没有被用来映射任何内存,但是他仍然存在TLBentry没有被flush, 

 

     所以还是不可用。

 

 

N:有N-1个对象正在使用这个页面

 

 

首先,要知道这个区域的大小是4M,也就是说128M的线性地址空间里面,只有4M的线性地址空间是用来作永久映射区的。至于到底是哪4M,是由PKMAP_BASE决定的,这个变量表示用来作永久内存映射的4M区间的起始线性地址。

在NON-PAE的i386上,页目录里面的每一项都指向一个4M的空间,所以永久映射区只需要一个页目录项就可以了。而一个页目录项指向一张页表,那么永久映射区正好就可以用一张页表来表示了,于是我们就用pkmap_page_table来指向这张页表。

                                                           

               pgd = swapper_pg_dir + pgd_index(vaddr);

        pud = pud_offset(pgd, vaddr);//pud==pgd

        pmd = pmd_offset(pud, vaddr);//pmd==pud==pgd

        pte = pte_offset_kernel(pmd, vaddr); 

        pkmap_page_table = pte;

 

 

2.具体代码分析(2.6.31)

 

                                                                                                                        

void *kmap(struct page *page)

{

        might_sleep();

        if (!PageHighMem(page))

                return page_address(page);

        return kmap_high(page);

}

kmap()函数就是用来建立永久映射的函数:由于调用kmap函数有可能会导致进程阻塞,所以它不能在中断处理函数等不可被阻塞的上下文下被调用,might_sleep()的作用就是当该函数在不可阻塞的上下文下被调用是,打印栈信息。接下来判断该需要建立永久映射的页是否确实属于高端内存,因为我们知道低端内存的每个页都已经存在和线性地址的映射了,所以,就不需要再建立了,page_address()函数返回该page对应的线性地址。(关于page_address()函数,参考本博客的专门文章有解释)。最后调用kmap_high(page),可见kmap_high()才真正执行建立永久映射的操作。

 

 

/**

* kmap_high - map a highmem page into memory

* @page: &struct page to map

*

* Returns the page's virtual memory address.

*

* We cannot call this from interrupts, as it may block.

*/

void *kmap_high(struct page *page)

{

        unsigned long vaddr;

        /*

         * For highmem pages, we can't trust "virtual" until

         * after we have the lock.

         */

        lock_kmap();

        vaddr = (unsigned long)page_address(page);

        if (!vaddr)

                vaddr = map_new_virtual(page);

        pkmap_count[PKMAP_NR(vaddr)]++;

        BUG_ON(pkmap_count[PKMAP_NR(vaddr)]  2);

        unlock_kmap();

        return (void*) vaddr;

}

 

kmap_high函数分析:首先获得对pkmap_page_table操作的锁,然后再调用page_address()来返回该page是否已经被映射,我们看到前面在kmap()里面已经判断过了,为什么这里还要再次判断呢?因为再获的锁的时候,有可能锁被其他CPU拿走了,而恰巧其他CPU拿了这个锁之后,也是执行这段code,而且映射的也是同一个page,那么当它把锁释放掉的时候,其实就表示该page的映射已经被建立了,我们这里就没有必要再去执行这段code了,所以就有必要在获得锁后再判断下。

如果发现vaddr不为空,那么就是刚才说的,已经被其他cpu上执行的任务给建立了,这里只需要把表示该页引用计数的pkmap_count[]再加一就可以了。同时调用BUG_ON来确保该引用计数确实是不小于2的,否则就是有问题的了。然后返回vaddr,整个建立就完成了。

如果发现vaddr为空呢?调用map_new_virtual()函数,到此我们看到,其实真正进行建立映射的代码在这个函数里面

 

 

static inline unsigned long map_new_virtual(struct page *page)

{       

        unsigned long vaddr;

        int count; 

                        

start:                  

        count = LAST_PKMAP;//LAST_PKMAP=1024

        /* Find an empty entry */

        for (;;) {      

                last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;

                if (!last_pkmap_nr) {

                        flush_all_zero_pkmaps();

                        count = LAST_PKMAP;

                }

                if (!pkmap_count[last_pkmap_nr])

                        break;  /* Found a usable entry */

                if (--count)

                        continue;

        

                /*

                 * Sleep for somebody else to unmap their entries

                 */     

                {       

                        DECLARE_WAITQUEUE(wait, current);

                

                        __set_current_state(TASK_UNINTERRUPTIBLE);

                        add_wait_queue(&pkmap_map_wait, &wait);

                        unlock_kmap();

                        schedule();

                        remove_wait_queue(&pkmap_map_wait, &wait);

                        lock_kmap();

 

                        /* Somebody else might have mapped it while we slept */

                        if (page_address(page))

                                return (unsigned long)page_address(page);

 

                        /* Re-start */

                        goto start;

                }

        }

        vaddr = PKMAP_ADDR(last_pkmap_nr);

        set_pte_at(&init_mm, vaddr,

                   &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));

 

        pkmap_count[last_pkmap_nr] = 1;

        set_page_address(page, (void *)vaddr);

 

        return vaddr;

}

 

last_pkmap_nr:记录上次被分配的页表项在pkmap_page_table里的位置,初始值为0,所以第一次分配的时候last_pkmap_nr等于1。

 

接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了,这时候就需要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[]计数为1的页表项在TLB里面的entry给flush掉,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置为1的时候也就是解除映射的同时把TLB也flush呢?个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。

 

再判断pkmap_count[last_pkmap_nr]是否为0,0的话就表示这个页表项是可用的,那么就跳出循环了到下面了。

 

PKMAP_ADDR(last_pkmap_nr)返回这个页表项对应的线性地址vaddr.

 

#definePKMAP_ADDR(nr) (PKMAP_BASE + ((nr)

 

set_pte_at(mm,addr, ptep, pte)函数在NON-PAE i386上的实现其实很简单,其实就等同于下面的代码:

 

staticinline void native_set_pte(pte_t *ptep , pte_t pte)

{

       *ptep = pte;

}

我们已经知道页表的线性起始地址存放在pkmap_page_table里面,那么相应的可用的页表项的地址就是&pkmap_page_table[last_pkmap_nr],得到了页表项的地址,只要把相应的pte填写进去,那么整个映射不就完成了吗?

pte由两部分组成:高20位表示物理地址,低12位表示页的描述信息。

怎么通过page查找对应的物理地址呢(参考page_address()一文)?其实很简单,用(page- mem_map) 再移PAGE_SHIFT位就可以了。

低12位的页描述信息是固定的:kmap_prot=(_PAGE_PRESENT| _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL).

下面的代码就是做了这些事情:

 

mk_pte(page,kmap_prot));

#definemk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))

#definepage_to_pfn __page_to_pfn

#define__page_to_pfn(page) ((unsigned long)((page) - mem_map) + \

                               ARCH_PFN_OFFSET)

staticinline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)

{

       return __pte(((phys_addr_t)page_nr

                   massage_pgprot(pgprot));

}

 

接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗,既然映射已经建立好了,应该赋值为2呀,其实这个操作是在他的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).

到此为止,整个映射就完成了,再把page和对应的线性地址加入到page_address_htable哈希链表里面就可以了(参考page_address一文)。

 

 

我们继续看所有的页表项都已经用了的情况下,也就是1024个页表项全已经映射了内存了,如何处理。此时count==0,于是就进入了下面的代码:

 

/*

                * Sleepfor somebody else to unmap their entries

                */

               {

                      DECLARE_WAITQUEUE(wait, current);

               

                      __set_current_state(TASK_UNINTERRUPTIBLE);

                      add_wait_queue(&pkmap_map_wait, &wait);

                      unlock_kmap();

                      schedule();

                      remove_wait_queue(&pkmap_map_wait, &wait);

                      lock_kmap();

 

                      /* Somebody else might have mapped it while we slept */

                      if (page_address(page))

                              return (unsignedlong)page_address(page);

 

                      /* Re-start */

                      goto start;

               }

这段代码其实很简单,就是把当前任务加入到等待队列pkmap_map_wait,当有其他任务唤醒这个队列时,再继续gotostart,重新整个过程。这里就是上面说的调用kmap函数有可能阻塞的原因。

那么什么时候会唤醒pkmap_map_wait队列呢?当调用kunmap_high函数,来释放掉一个映射的时候。

kunmap_high函数其实页很简单,就是把要释放的页表项的计数减1,如果等于1的时候,表示有可用的页表项了,再唤醒pkmap_map_wait队列

 

/**

*kunmap_high - map a highmem page into memory

* @page:&struct page to unmap

*

* IfARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called

* onlyfrom user context.

*/

voidkunmap_high(struct page *page)

{

       unsigned long vaddr;

       unsigned long nr;

       unsigned long flags;

       int need_wakeup;

 

       lock_kmap_any(flags);

       vaddr = (unsigned long)page_address(page);

       BUG_ON(!vaddr);

       nr = PKMAP_NR(vaddr);

 

       /*

        * A count must never go down to zero

        * without a TLB flush!

        */

       need_wakeup = 0;

       switch (--pkmap_count[nr]) {//减一

       case 0:

               BUG();

       case 1:

               /*

                * Avoidan unnecessary wake_up() function call.

                * Thecommon case is pkmap_count[] == 1, but

                * nowaiters.

                * Thetasks queued in the wait-queue are guarded

                * by boththe lock in the wait-queue-head and by

                * thekmap_lock. As the kmap_lock is held here,

                * no needfor the wait-queue-head's lock. Simply

                * test ifthe queue is empty.

                */

               need_wakeup =waitqueue_active(&pkmap_map_wait);

       }

       unlock_kmap_any(flags);

 

       /* do wake-up, if needed, race-free outside ofthe spin lock */

       if (need_wakeup)

              wake_up(&pkmap_map_wait);

}

声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
Innodb Redoログの役割を説明し、ログを元に戻します。Innodb Redoログの役割を説明し、ログを元に戻します。Apr 15, 2025 am 12:16 AM

INNODBは、レドログと非論的なものを使用して、データの一貫性と信頼性を確保しています。 1.レドログは、クラッシュの回復とトランザクションの持続性を確保するために、データページの変更を記録します。 2.Undologsは、元のデータ値を記録し、トランザクションロールバックとMVCCをサポートします。

説明出力(タイプ、キー、行、追加)で探す重要なメトリックは何ですか?説明出力(タイプ、キー、行、追加)で探す重要なメトリックは何ですか?Apr 15, 2025 am 12:15 AM

説明コマンドのキーメトリックには、タイプ、キー、行、および追加が含まれます。 1)タイプは、クエリのアクセスタイプを反映しています。値が高いほど、constなどの効率が高くなります。 2)キーは使用されているインデックスを表示し、nullはインデックスがないことを示します。 3)行はスキャンされた行の数を推定し、クエリのパフォーマンスに影響します。 4)追加の情報を最適化する必要があるというFilesortプロンプトを使用するなど、追加情報を提供します。

説明の一時的なステータスを使用し、それを回避する方法は何ですか?説明の一時的なステータスを使用し、それを回避する方法は何ですか?Apr 15, 2025 am 12:14 AM

Temporaryを使用すると、MySQLクエリに一時テーブルを作成する必要があることが示されています。これは、異なる列、またはインデックスされていない列を使用して順番に一般的に見られます。インデックスの発生を回避し、クエリを書き直し、クエリのパフォーマンスを改善できます。具体的には、expliect出力に使用を使用する場合、MySQLがクエリを処理するために一時テーブルを作成する必要があることを意味します。これは通常、次の場合に発生します。1)個別またはグループビーを使用する場合の重複排除またはグループ化。 2)Orderbyに非インデックス列が含まれているときに並べ替えます。 3)複雑なサブクエリを使用するか、操作に参加します。最適化方法には以下が含まれます。1)OrderbyとGroupB

さまざまなSQLトランザクションの分離レベル(読み取り、commited、繰り返し読み取り、シリアル化可能、シリアル化可能)とmysql/innodbの意味を説明してください。さまざまなSQLトランザクションの分離レベル(読み取り、commited、繰り返し読み取り、シリアル化可能、シリアル化可能)とmysql/innodbの意味を説明してください。Apr 15, 2025 am 12:11 AM

MySQL/INNODBは、4つのトランザクション分離レベルをサポートしています。 1.ReadunCommittedは、知らないデータを読み取ることができます。 2。読み込みは汚い読み取りを回避しますが、繰り返しのない読みが発生する可能性があります。 3. RepeatablerEadはデフォルトレベルであり、汚い読み取りと非回復不可能な読みを避けますが、幻の読み取りが発生する可能性があります。 4. Serializableはすべての並行性の問題を回避しますが、同時性を低下させます。適切な分離レベルを選択するには、データの一貫性とパフォーマンス要件のバランスをとる必要があります。

MySQL対その他のデータベース:オプションの比較MySQL対その他のデータベース:オプションの比較Apr 15, 2025 am 12:08 AM

MySQLは、Webアプリケーションやコンテンツ管理システムに適しており、オープンソース、高性能、使いやすさに人気があります。 1)PostgreSQLと比較して、MySQLは簡単なクエリと高い同時読み取り操作でパフォーマンスが向上します。 2)Oracleと比較して、MySQLは、オープンソースと低コストのため、中小企業の間でより一般的です。 3)Microsoft SQL Serverと比較して、MySQLはクロスプラットフォームアプリケーションにより適しています。 4)MongoDBとは異なり、MySQLは構造化されたデータおよびトランザクション処理により適しています。

MySQL Index Cardinalityはクエリパフォーマンスにどのように影響しますか?MySQL Index Cardinalityはクエリパフォーマンスにどのように影響しますか?Apr 14, 2025 am 12:18 AM

MySQLインデックスのカーディナリティは、クエリパフォーマンスに大きな影響を及ぼします。1。高いカーディナリティインデックスは、データ範囲をより効果的に狭め、クエリ効率を向上させることができます。 2。低カーディナリティインデックスは、完全なテーブルスキャンにつながり、クエリのパフォーマンスを削減する可能性があります。 3。ジョイントインデックスでは、クエリを最適化するために、高いカーディナリティシーケンスを前に配置する必要があります。

MySQL:新規ユーザー向けのリソースとチュートリアルMySQL:新規ユーザー向けのリソースとチュートリアルApr 14, 2025 am 12:16 AM

MySQL学習パスには、基本的な知識、コアの概念、使用例、最適化手法が含まれます。 1)テーブル、行、列、SQLクエリなどの基本概念を理解します。 2)MySQLの定義、作業原則、および利点を学びます。 3)インデックスやストアドプロシージャなどの基本的なCRUD操作と高度な使用法をマスターします。 4)インデックスの合理的な使用や最適化クエリなど、一般的なエラーのデバッグとパフォーマンス最適化の提案に精通しています。これらの手順を通じて、MySQLの使用と最適化を完全に把握できます。

実際のmysql:例とユースケース実際のmysql:例とユースケースApr 14, 2025 am 12:15 AM

MySQLの実際のアプリケーションには、基本的なデータベース設計と複雑なクエリの最適化が含まれます。 1)基本的な使用法:ユーザー情報の挿入、クエリ、更新、削除など、ユーザーデータの保存と管理に使用されます。 2)高度な使用法:eコマースプラットフォームの注文や在庫管理など、複雑なビジネスロジックを処理します。 3)パフォーマンスの最適化:インデックス、パーティションテーブル、クエリキャッシュを使用して合理的にパフォーマンスを向上させます。

See all articles

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

AtomエディタMac版ダウンロード

AtomエディタMac版ダウンロード

最も人気のあるオープンソースエディター

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強力な PHP 統合開発環境

VSCode Windows 64 ビットのダウンロード

VSCode Windows 64 ビットのダウンロード

Microsoft によって発売された無料で強力な IDE エディター

WebStorm Mac版

WebStorm Mac版

便利なJavaScript開発ツール