免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 10204 | 回复: 8
打印 上一主题 下一主题

ioremap_nocache 函数分析 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2011-04-03 12:05 |只看该作者 |倒序浏览
本帖最后由 zd零 于 2011-04-08 09:31 编辑

最近一直在研究 USB 看到了 EHCI 部分,在usb_hcd_pci_probe () 函数中:

hcd->regs = ioremap_nocache (hcd->rsrc_start, hcd->rsrc_len);

ioremap_nocache()函数我想大家都不陌生,现在我就把此函数分析一下,不当之处请大家谅解,欢迎指正!

开始吧:
对于EHCI 来说它把它本身的寄存器和内存映射到内存中区!但是站在CPU的角度来说,我们无法直接访问这块内存空间,需要将设备的总线地址映射成一个cpu可访问的线性地址!

代码:
内核版本 2.6.22.1
cat /proc/iomem
此时我们就以此区间(0xd8426800 – 0xd8426bff)表示EHCI


注意:此时我们假设没有打开PAE!

void __iomem *ioremap_nocache (unsigned long phys_addr, unsigned long size)
{
         phys_addr EHCI 总线地址
         size 区间大小(1023)


         unsigned long last_addr;
         void __iomem *p = __ioremap(phys_addr, size, _PAGE_PCD);
         #define _PAGE_PCD       0x010

         if (!p)
                 return p;

         /* Guaranteed to be > phys_addr, as per __ioremap() */
         last_addr = phys_addr + size - 1;

         if (last_addr < virt_to_phys(high_memory) - 1) {
                 struct page *ppage = virt_to_page(__va(phys_addr));            
                 unsigned long npages;

                 phys_addr &= PAGE_MASK;

                 /* This might overflow and become zero.. */
                 last_addr = PAGE_ALIGN(last_addr);

                 /* .. but that's ok, because modulo-2**n arithmetic will make
                 * the page-aligned "last - first" come out right.
                 */
                 npages = (last_addr - phys_addr) >> PAGE_SHIFT;

                 if (change_page_attr(ppage, npages, PAGE_KERNEL_NOCACHE) < 0) {
                         iounmap(p);
                         p = NULL;
                 }
                 global_flush_tlb();
         }

         return p;                                       
}


非连续映射地址空间

非连续映射地址空间用于将连续的线性地址映射到不连续的物理地址!同时它也提供了一种访问高物理内存的方法!

内核主要在一下三种情况使用非连续映射地址空间
        映射设备的I/O空间
        为内核模块分配空间
        为交换分区分配空间

非连续映射地址空间的起始地址在常规映射地址空间的结束地址后8MB-16MB之间,而且保证8MB对齐(地址的低24位为0)



void __iomem * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
{
        void __iomem * addr;
        struct vm_struct * area;
        unsigned long offset, last_addr;
        pgprot_t prot;

        /* Don't allow wraparound or zero size */
        last_addr = phys_addr + size - 1;
        总线地址末端

        if (!size || last_addr < phys_addr)
                return NULL;

        /*
         * Don't remap the low PCI/ISA area, it's always mapped..
         */
        if (phys_addr >= ISA_START_ADDRESS && last_addr < ISA_END_ADDRESS)
                return (void __iomem *) phys_to_virt(phys_addr);

        #define ISA_START_ADDRESS       0xa0000
        #define ISA_END_ADDRESS         0x100000


        640kb-1Mb 之间(此空洞用于连接到ISA总线上的设备)

        /*
         * Don't allow anybody to remap normal RAM that we're using..
         */
        if (phys_addr <= virt_to_phys(high_memory - 1)) {
        high_memory 为 896Mb 对应线性地址
        phys_addr 在小于896Mb的常规内存空间中


                char *t_addr, *t_end;
                struct page *page;

                t_addr = __va(phys_addr);
                转化成线性地址

                t_end = t_addr + (size - 1);
           
                若小于896MB 则此页框应该被设置为保留
                for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)
                        if(!PageReserved(page))
                                return NULL;
        }

        prot = __pgprot(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY
                        | _PAGE_ACCESSED | flags);
        #define __pgprot(x)     ((pgprot_t) { (x) }

        /*
         * Mappings have to be page-aligned
         */
        offset = phys_addr & ~PAGE_MASK;
        取一页页框的偏移

        phys_addr &= PAGE_MASK;
        总线地址按4KB 对齐

       #define PAGE_SHIFT      12
        #define PAGE_SIZE       (1UL << PAGE_SHIFT)
        #define PAGE_MASK       (~(PAGE_SIZE-1))


        size = PAGE_ALIGN(last_addr+1) - phys_addr;
       #define PAGE_ALIGN(addr)        (((addr)+PAGE_SIZE-1)&PAGE_MASK)

        对齐操作

        /*
         * Ok, go for it..
         */
        area = get_vm_area(size, VM_IOREMAP | (flags << 20));

        #define VM_IOREMAP      0x00000001      /* ioremap() and friends */

        申请一个非连续映射节点描述符

        if (!area)
                return NULL;
        area->phys_addr = phys_addr;
        总线地址

        addr = (void __iomem *) area->addr;
        起始线性地址

        if (ioremap_page_range((unsigned long) addr,(unsigned long) addr + size, phys_addr, prot)) {
                vunmap((void __force *) addr);
                return NULL;
        }

        return (void __iomem *) (offset + (char __iomem *)addr);
       offset + addr
        offset 一般为0


         addr 为线性地址,此地址被 CPU 用于读写 EHCI I/O mem 空间
        这也验证ULK3那句话:在X86平台上总线地址就是物理地址,此时就可以通过访问内存的指令访问EHCI 的寄存器空间



}


struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)
{
         return __get_vm_area(size, flags, VMALLOC_START, VMALLOC_END);
}


struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags,unsigned long start, unsigned long end)
{
         return __get_vm_area_node(size, flags, start, end, -1, GFP_KERNEL);
}


非连续映射地址空间
static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long flags,
                                          unsigned long start, unsigned long end,
                                             int node, gfp_t gfp_mask)
{
         struct vm_struct **p, *tmp, *area;
         unsigned long align = 1;
         unsigned long addr;

         BUG_ON(in_interrupt());
         if (flags & VM_IOREMAP) {
                 int bit = fls(size);

                 if (bit > IOREMAP_MAX_ORDER)
                         bit = IOREMAP_MAX_ORDER;
                 else if (bit < PAGE_SHIFT)
                         bit = PAGE_SHIFT;

                 align = 1ul << bit;
         }
         addr = ALIGN(start, align);
         size = PAGE_ALIGN(size);
         if (unlikely(!size))
                 return NULL;

         area = kmalloc_node(sizeof(*area), gfp_mask & GFP_LEVEL_MASK, node);
         分配虚拟页面结构

         if (unlikely(!area))
                 return NULL;

         /*
          * We always allocate a guard page.
          */
         size += PAGE_SIZE;
         作为隔离带

         write_lock(&vmlist_lock);

         查找到一个合理的虚拟地址空间
         查找之前的struct vmlist 链表(有序链表从小到大)

         for (p = &vmlist; (tmp = *p) != NULL ;p = &tmp->next) {
                 if ((unsigned long)tmp->addr < addr) {
                         if((unsigned long)tmp->addr + tmp->size >= addr)
                                 addr = ALIGN(tmp->size +
                                              (unsigned long)tmp->addr, align);
                         continue;
                 }
                 if ((size + addr) < addr)
                         goto out;
                回绕
                 if (size + addr <= (unsigned long)tmp->addr)
                         goto found;
                 addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align);

                 if (addr > end - size)
                         goto out;
         }

found:
         area->next = *p;

         *p = area;
          新申请的 struct vm_struct 加入到 vmlist 链表中

         area->flags = flags;
         area->addr = (void *)addr;
         此时我们就假设 addr = VMALLOC_START (线性地址)

         area->size = size;
         area->pages = NULL;
         area->nr_pages = 0;
         area->phys_addr = 0;
         write_unlock(&vmlist_lock);

         return area;

out:
         write_unlock(&vmlist_lock);
         kfree(area);
         if (printk_ratelimit())
                 printk(KERN_WARNING "allocation failed: out of vmalloc space - use vmalloc=<size> to increase size.\n");
         return NULL;
}



(页目录)
int ioremap_page_range(unsigned long addr,
                       unsigned long end, unsigned long phys_addr, pgprot_t prot)
{
        pgd_t *pgd;
        unsigned long start;
        unsigned long next;
        int err;

        BUG_ON(addr >= end);

        start = addr;
        线性地址

        phys_addr -= addr;
        pgd = pgd_offset_k(addr);
        求出线性地址 addr 在页目录表中的地址
        
       #define pgd_offset_k(address)     pgd_offset(&init_mm, address)
       #define pgd_offset(mm, address)   ((mm)->pgd+pgd_index(address))
       #define pgd_index(address)    (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))

       页目录表中的偏移


       init_mm.pgd = swapper_pg_dir
       swapper_pg_dir 为页目录项地址


       #define PGDIR_SHIFT     22
       #define PTRS_PER_PGD    1024


        do {
                next = pgd_addr_end(addr, end);
                一般情况下返回 end(线性地址末端)

                err = ioremap_pud_range(pgd, addr, next, phys_addr+addr, prot);
                if (err)
                        break;
        } while (pgd++, addr = next, addr != end);

        flush_cache_vmap(start, end);

        return err;
        返回 0
}


#define pgd_addr_end(addr, end)                                         
({      unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;  
        (__boundary - 1 < (end) - 1)? __boundary: (end);               
})
PGDIR_SIZE = 1<<22
PGDIR_MASK = 3FFFFF


addr 与 end 之间大小不能超过4Mb,因为一个页目录项最多表示4Mb 内存


(页上层目录)
static inline int ioremap_pud_range(pgd_t *pgd, unsigned long addr,
                unsigned long end, unsigned long phys_addr, pgprot_t prot)
{
       参数:
        pgd 线性地址 addr 所表示的页目录地址
        addr 线性地址
        end 线性地址末端
        phys_addr =  EHCI 总线地址
        prot 标志


        pud_t *pud;
        unsigned long next;

        phys_addr -= addr;
        pud = pud_alloc(&init_mm, pgd, addr);
        返回的是pgd 页目录地址

        if (!pud)
                return -ENOMEM;
        do {
                next = pud_addr_end(addr, end);
                #define pud_addr_end(addr, end)                 (end)

                if (ioremap_pmd_range(pud, addr, next, phys_addr + addr, prot))
                        return -ENOMEM;
        } while (pud++, addr = next, addr != end);
        return 0;
}





static inline pud_t *pud_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{
        return (unlikely(pgd_none(*pgd)) && __pud_alloc(mm, pgd, address))?
                NULL: pud_offset(pgd, address);
}

static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
        return (pud_t *)pgd;
}




(页中间目录)
static inline int ioremap_pmd_range(pud_t *pud, unsigned long addr,
                unsigned long end, unsigned long phys_addr, pgprot_t prot)
{
        pud 为pgd

        pmd_t *pmd;
        unsigned long next;

        phys_addr -= addr;
        pmd = pmd_alloc(&init_mm, pud, addr);
        pmd 还是 pgd

        if (!pmd)
                return -ENOMEM;
        do {
                next = pmd_addr_end(addr, end);
                #define pmd_addr_end(addr, end)                 (end)

                if (ioremap_pte_range(pmd, addr, next, phys_addr + addr, prot))
                        return -ENOMEM;
        } while (pmd++, addr = next, addr != end);
        return 0;
}


static inline pmd_t *pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address)
{
        return (unlikely(pud_none(*pud)) && __pmd_alloc(mm, pud, address))?
                NULL: pmd_offset(pud, address);
}
static inline pmd_t * pmd_offset(pud_t * pud, unsigned long address)
{
        return (pmd_t *)pud;
}





(页表) 开始干正经事了
static int ioremap_pte_range(pmd_t *pmd, unsigned long addr,
                unsigned long end, unsigned long phys_addr, pgprot_t prot)
{
        pte_t *pte;
        unsigned long pfn;

        pfn = phys_addr >> PAGE_SHIFT;
        EHCI 控制器总线地址所对应的页框号

        pte = pte_alloc_kernel(pmd, addr);
        创建页表(如果不存在)
        pte 为线性地址addr 在页表中的地址


        if (!pte)
                return -ENOMEM;
        do {
                BUG_ON(!pte_none(*pte));
                set_pte_at(&init_mm, addr, pte, pfn_pte(pfn, prot));
                #define pfn_pte(pfn, prot)      __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

                很明显最后一个参数为 EHCI 的总线地址加上标志位(设置到页表中的地址为总线地址
               
                设置页表

                pfn++;

        } while (pte++, addr += PAGE_SIZE, addr != end);
        此时 EHCI映射到内存的 I/O MEM大小为 1023Kb,此处会循环设置

        return 0;
}



#define pte_alloc_kernel(pmd, address)                  \
        ((unlikely(!pmd_present(*(pmd))) && __pte_alloc_kernel(pmd, address))? \
                NULL: pte_offset_kernel(pmd, address))




int __pte_alloc_kernel(pmd_t *pmd, unsigned long address)
{
        pte_t *new = pte_alloc_one_kernel(&init_mm, address);
        申请一页页框作为页表

        if (!new)
                return -ENOMEM;

        spin_lock(&init_mm.page_table_lock);
        if (pmd_present(*pmd))          /* Another has populated it */
                pte_free_kernel(new);
        else
                pmd_populate_kernel(&init_mm, pmd, new);
                设置到页目录表中去(将页表地址填入到页目录中)

        spin_unlock(&init_mm.page_table_lock);
        return 0;
}


pte_t *pte_alloc_one_kernel(struct mm_struct *mm, unsigned long address)
{
        return (pte_t *)__get_free_page(GFP_KERNEL|__GFP_REPEAT|__GFP_ZERO);
}



#define pte_offset_kernel(dir, address) \
       ((pte_t *) pmd_page_vaddr(*(dir)) +  pte_index(address))

线性地址addr 在页表中的地址

#define pmd_page_vaddr(pmd) \
                 ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))

#define pte_index(address) \
                (((address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))


取线性地址的中间10位

#define PTRS_PER_PTE    1024



调用ioremap_nocache()函数之后,返回一个线性地址,此时CPU 可以访问设备的内存(已经将其映射到了线性地址空间中了),此时CPU可以使用访问内存的指令访问设备的内存空间(host bridge 判断访问物理内存还是设备中的内存),此时我们就可以像访问内存一样来访问设备的内存(寄存器)!

现在我们就可以使用 readl() 或writel() 函数读取或写入 EHCI 中 Capability  Registers Operational Registers,此时我们就可以对EHCI 编程了!


图:

论坛徽章:
22
丑牛
日期:2014-08-15 14:32:0015-16赛季CBA联赛之同曦
日期:2017-12-14 15:28:14黑曼巴
日期:2017-08-10 08:14:342017金鸡报晓
日期:2017-02-08 10:39:42黑曼巴
日期:2016-11-15 15:48:38CU十四周年纪念徽章
日期:2016-11-09 13:19:1015-16赛季CBA联赛之同曦
日期:2016-04-08 18:00:03平安夜徽章
日期:2015-12-26 00:06:30程序设计版块每日发帖之星
日期:2015-12-03 06:20:002015七夕节徽章
日期:2015-08-21 11:06:17IT运维版块每日发帖之星
日期:2015-08-09 06:20:002015亚冠之吉达阿赫利
日期:2015-07-03 08:39:42
2 [报告]
发表于 2011-04-03 13:07 |只看该作者
好文~~
收藏

论坛徽章:
0
3 [报告]
发表于 2011-04-06 14:16 |只看该作者
没接触过硬件,看到讨论ioremap好几回了。这个函数是不是说,外设的寄存器可memory-mapped IO方式访问,然后把这个物理地址通过MMU映射到其它虚拟地址?这样的话跟映射一般物理内存没啥区别吧。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
4 [报告]
发表于 2011-04-06 15:19 |只看该作者
感谢 LZ 分享。

论坛徽章:
0
5 [报告]
发表于 2011-04-06 16:17 |只看该作者
回复 3# tempname3


    对,应该就是这样!

论坛徽章:
0
6 [报告]
发表于 2011-04-29 09:08 |只看该作者
【此时CPU可以使用访问内存的指令访问设备的内存空间(host bridge 判断访问物理内存还是设备中的内存)】
我想知道host bridge是怎么知道这个地址是内存地址还是设备空间地址的?

论坛徽章:
0
7 [报告]
发表于 2011-04-29 09:20 |只看该作者
办公软件白痴路过,真心请教楼主你的图是用什么软件做的,ppt吗

论坛徽章:
0
8 [报告]
发表于 2011-04-29 09:51 |只看该作者
回复 6# 懂医术的厨师
可以参考一下这个帖子!
http://bbs.chinaunix.net/viewthread.php?tid=1934861&from=favorites

论坛徽章:
0
9 [报告]
发表于 2011-04-29 10:00 |只看该作者
回复  懂医术的厨师
可以参考一下这个帖子!
zd零 发表于 2011-04-29 09:51


原来是硬件完成的区分,理解了,多谢!
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP