Linux内核源代码情景分析-外部设备存储空间的地址映射

   随着计算机技术的发展,人们发现单纯的I/O映射方式是不能满足要求的。此种方式只适合于早期的计算机技术,那时候一个外设通常都只有几个寄存器,通过这几个寄存器就可以完成对外设的所有操作了。而现在的情况却不大一样。例如,在PC机上可以插上一块图像卡,带有2MB的存储器,甚至还可能带有一块ROM,里面装有可执行代码。所以要将外设卡上的存储器映射到内存空间,实际上是虚拟空间的手段。在Linux内核中,这样的映射是通过函数ioremap()来建立的。

 

    对于内存页面的管理,通常 我们都是先在虚拟空间分配一个虚拟空间,然后为此区间分配相应的物理内存页面并建立起映射。而且这样的映射也并不是一次就建立完毕,可以在访问这些虚拟页面引起页面异常时逐步地建立。

    但是,ioremap()则不同,首先我们先有一个物理存储区间,其地址就是外设卡上的存储器出现在总线的地址(不是存储单元在外设卡上局部的物理地址)。在Linux系统中,CPU不能按物理地址来访问存储空间,而必须使用虚拟地址,所以必须”反向“地从物理地址出发找到一片虚拟空间并建立起映射。其次,这样的需求只发生于对外部设备的操作,而这是内核的事,所以相应的虚拟空间是在系统空间(3GB以上)。

 

    我们先看ioremap,代码如下:

 

  1. extern inline void * ioremap (unsigned long offset, unsigned long size)  
  2. {  
  3.     return __ioremap(offset, size, 0);  
  4. }  


    __ioremap,代码如下:

 

 

  1. void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)  
  2. {  
  3.     void * addr;  
  4.     struct vm_struct * area;  
  5.     unsigned long offset, last_addr;  
  6.   
  7.     /* Don‘t allow wraparound or zero size */  
  8.     last_addr = phys_addr + size - 1;  
  9.     if (!size || last_addr < phys_addr)  
  10.         return NULL;  
  11.   
  12.     /* 
  13.      * Don‘t remap the low PCI/ISA area, it‘s always mapped.. 
  14.      */  
  15.     if (phys_addr >= 0xA0000 && last_addr < 0x100000)  
  16.         return phys_to_virt(phys_addr);  
  17.   
  18.     /* 
  19.      * Don‘t allow anybody to remap normal RAM that we‘re using.. 
  20.      */  
  21.     if (phys_addr < virt_to_phys(high_memory)) {  
  22.         char *t_addr, *t_end;  
  23.         struct page *page;  
  24.   
  25.         t_addr = __va(phys_addr);  
  26.         t_end = t_addr + (size - 1);  
  27.          
  28.         for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)  
  29.             if(!PageReserved(page))  
  30.                 return NULL;  
  31.     }  
  32.         //我们假设符合条件,执行到这里  
  33.     /* 
  34.      * Mappings have to be page-aligned 
  35.      */  
  36.     offset = phys_addr & ~PAGE_MASK;  
  37.     phys_addr &= PAGE_MASK;//物理地址  
  38.     size = PAGE_ALIGN(last_addr) - phys_addr; //从起始物理地址到结束共多少个字节  
  39.           
  40.     /* 
  41.      * Ok, go for it.. 
  42.      */  
  43.     area = get_vm_area(size, VM_IOREMAP);//在vmlist_lock找出合适的虚拟地址,并将vm_struct插入vmlist_lock  
  44.     if (!area)  
  45.         return NULL;  
  46.     addr = area->addr;//虚拟地址  
  47.     if (remap_area_pages(VMALLOC_VMADDR(addr), phys_addr, size, flags)) {//建立虚拟地址和物理地址之间的映射  
  48.         vfree(addr);  
  49.         return NULL;  
  50.     }  
  51.     return (void *) (offset + (char *)addr);//返回的是虚拟地址加上物理页面的后12位偏移  
  52. }  


    get_vm_area,在vmlist_lock找出合适的虚拟地址,并将vm_struct插入vmlist_lock,代码如下:

 

 

  1. struct vm_struct * get_vm_area(unsigned long size, unsigned long flags)  
  2. {  
  3.     unsigned long addr;  
  4.     struct vm_struct **p, *tmp, *area;  
  5.   
  6.     area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL);  
  7.     if (!area)  
  8.         return NULL;  
  9.     size += PAGE_SIZE;  
  10.     addr = VMALLOC_START;  
  11.     write_lock(&vmlist_lock);  
  12.     for (p = &vmlist; (tmp = *p) ; p = &tmp->next) {//在vmlist_lock找出合适的虚拟地址  
  13.         if ((size + addr) < addr) {  
  14.             write_unlock(&vmlist_lock);  
  15.             kfree(area);  
  16.             return NULL;  
  17.         }  
  18.         if (size + addr < (unsigned long) tmp->addr)  
  19.             break;  
  20.         addr = tmp->size + (unsigned long) tmp->addr;  
  21.         if (addr > VMALLOC_END-size) {  
  22.             write_unlock(&vmlist_lock);  
  23.             kfree(area);  
  24.             return NULL;  
  25.         }  
  26.     }  
  27.     area->flags = flags;  
  28.     area->addr = (void *)addr;//虚拟地址  
  29.     area->size = size;//大小  
  30.     area->next = *p;//将vm_struct插入vmlist_lock  
  31.     *p = area;  
  32.     write_unlock(&vmlist_lock);  
  33.     return area;  
  34. }  

 

    其中,vm_struct 结构如下:

 

  1. struct vm_struct {  
  2.     unsigned long flags;  
  3.     void * addr;  
  4.     unsigned long size;  
  5.     struct vm_struct * next;  
  6. };  

 

 

  1. struct vm_struct * vmlist;  


    回到__ioremap,开始执行remap_area_pages,建立虚拟地址和物理地址之间的映射,代码如下:

 

 

  1. static int remap_area_pages(unsigned long address, unsigned long phys_addr,  
  2.                  unsigned long size, unsigned long flags)//address为虚拟地址,phys_addr为物理地址  
  3. {  
  4.     pgd_t * dir;  
  5.     unsigned long end = address + size;  
  6.   
  7.     phys_addr -= address;//减去address,后来又加上了address  
  8.     dir = pgd_offset(&init_mm, address);//指向页目录项  
  9.     flush_cache_all();  
  10.     if (address >= end)  
  11.         BUG();  
  12.     do {  
  13.         pmd_t *pmd;  
  14.         pmd = pmd_alloc_kernel(dir, address);//中转一下,还是指向页目录项  
  15.         if (!pmd)  
  16.             return -ENOMEM;  
  17.         if (remap_area_pmd(pmd, address, end - address,  
  18.                      phys_addr + address, flags))//end-address为大小,phys_addr + address是物理地址  
  19.             return -ENOMEM;  
  20.         address = (address + PGDIR_SIZE) & PGDIR_MASK;  
  21.         dir++;//指向下一个页目录项  
  22.     } while (address && (address < end));  
  23.     flush_tlb_all();  
  24.     return 0;  
  25. }  


    remap_area_pmd,代码如下:

 

 

  1. static inline int remap_area_pmd(pmd_t * pmd, unsigned long address, unsigned long size,  
  2.     unsigned long phys_addr, unsigned long flags)  
  3. {  
  4.     unsigned long end;  
  5.   
  6.     address &= ~PGDIR_MASK;  
  7.     end = address + size;  
  8.     if (end > PGDIR_SIZE)  
  9.         end = PGDIR_SIZE;  
  10.     phys_addr -= address;  
  11.     if (address >= end)  
  12.         BUG();  
  13.     do {  
  14.         pte_t * pte = pte_alloc_kernel(pmd, address);//指向了页表项  
  15.         if (!pte)  
  16.             return -ENOMEM;  
  17.         remap_area_pte(pte, address, end - address, address + phys_addr, flags);//end-address为大小,phys_addr + address是物理地址  
  18.         address = (address + PMD_SIZE) & PMD_MASK;  
  19.         pmd++;//指向下一个页目录项  
  20.     } while (address && (address < end));  
  21.     return 0;  
  22. }  

 

 

    remap_area_pte,代码如下:

 

  1. static inline void remap_area_pte(pte_t * pte, unsigned long address, unsigned long size,  
  2.     unsigned long phys_addr, unsigned long flags)  
  3. {  
  4.     unsigned long end;  
  5.   
  6.     address &= ~PMD_MASK;  
  7.     end = address + size;  
  8.     if (end > PMD_SIZE)  
  9.         end = PMD_SIZE;  
  10.     if (address >= end)  
  11.         BUG();  
  12.     do {  
  13.         if (!pte_none(*pte)) {  
  14.             printk("remap_area_pte: page already exists\n");  
  15.             BUG();  
  16.         }  
  17.         set_pte(pte, mk_pte_phys(phys_addr, __pgprot(_PAGE_PRESENT | _PAGE_RW |   
  18.                     _PAGE_DIRTY | _PAGE_ACCESSED | flags)));//页表项指向对应的页面,且页面的属性被设置为_PAGE_PRESENT,_PAGE_RW,_PAGE_DIRTY,_PAGE_ACCESSED  
  19.         address += PAGE_SIZE;  
  20.         phys_addr += PAGE_SIZE;  
  21.         pte++;//指向下一个页表项  
  22.     } while (address && (address < end));  
  23. }  


    回到__ioremap,最后返回的是虚拟地址加上物理页面的后12位偏移为最终的虚拟地址,前面已经建立了映射(页目录项和页表项),这样,根据这个虚拟地址就可以访问到物理地址的第一行代码。

 

 

    由于内核mm_struct结构init_mm是单独的,从任何一个进程的task结构中都到达不了init_mm。所以,kswapd根本就看不到init_mm中的虚拟区间,这些区间的页面就自然不会被换出而常驻内存。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。