Quantcast
Channel: 蜗窝科技
Viewing all 218 articles
Browse latest View live

内存初始化代码分析(三):创建系统内存地址映射

$
0
0

一、前言

经过内存初始化代码分析(一)内存初始化代码分析(二)的过渡,我们终于来到了内存初始化的核心部分:paging_init。当然本文不能全部解析完该函数(那需要的篇幅太长了),我们只关注创建系统内存地址映射这部分代码实现,也就是解析paging_init中的map_mem函数。

同样的,我们选择的是4.4.6的内核代码,体系结构相关的代码来自ARM64。

 

二、准备阶段

在进入实际的代码分析之前,我们首先回头看看目前内存的状态。偌大的物理地址空间中,系统内存占据了一段或者几段地址空间,这些信息被保存在了memblock模块中的memory type类型的数组中,数组中每一个memory region描述了一段系统内存信息(base、size以及node id)。OK,系统内存就这么多,但是也并非所有的memory type类型的数组中区域都是free的,实际上,有些已经被使用或者保留使用的内存区域需要从memory type类型的一段或者几段地址空间中摘取下来,并定义在reserved type类型的数组中。实际上,在整个系统初始化过程中(更具体的说是内存管理模块完成初始化之前),我们都需要暂时使用memblock这样一个booting阶段的内存管理模块来暂时进行内存的管理(收集内存布局信息只是它的副业),每次分配的内存都是在reserved type数组中增加一个新的item或者扩展其中一个memory region的size而已。

通过memblock模块,我们已经收集了内存布局的信息(memory type类型的数组),也知道了free memory资源的信息(memory type类型的数组减去reserved type类型的数组,从集合论的角度看,reserved type类型的数组是memory type类型的数组的真子集),但是,要管理这些珍贵的系统内存,首先要能够访问他们啊(顺便说一句:memblock中的那些数组定义的地址都是物理地址),通过前面的分析文章,我们知道有两段内存已经是可见的(完成了地址映射),一个是kernel image段,另外一个是fdt段。而广大的系统内存区域仍然在黑暗之中,等待我们去拯救(进行地址映射)。

最后,我们思考这样一个问题:是否memory type类型的数组代表了整个的系统内存的地址空间呢?当然不是,有些驱动可能会保留一段系统内存区域为自己使用,同时也不希望OS管理这段内存(或者说对OS不可见),而是自己创建该段内存的地址映射。如果你对dts中的memory reserve节点比较熟悉的话,那么实际上这样的reserved memory region是有no-map属性的。这时候,内核初始化过程中,在解析该reserved-memory节点的时候,会将该段地址从memblock模块中移除。而在map_mem函数中,为所有memory type类型的数组创建地址映射的时候,有no-map属性的那段内存地址将不会创建地址映射,也就不在OS的控制范围内了。

 

三、概览

创建系统内存地址映射的代码在map_mem中,如下:

static void __init map_mem(void)  {
    struct memblock_region *reg;
    phys_addr_t limit;

    limit = PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE;---------------(1)
    memblock_set_current_limit(limit);

    for_each_memblock(memory, reg) {------------------------(2)
        phys_addr_t start = reg->base;――确定该region的起始地址
        phys_addr_t end = start + reg->size; ――确定该region的结束地址

        if (start >= end)--参数检查
            break;

        if (ARM64_SWAPPER_USES_SECTION_MAPS) {----------------(3)
            if (start < limit)
                start = ALIGN(start, SECTION_SIZE);
            if (end < limit) {
                limit = end & SECTION_MASK;
                memblock_set_current_limit(limit);
            }
        }
        __map_memblock(start, end);-------------------------(4)
    }

    memblock_set_current_limit(MEMBLOCK_ALLOC_ANYWHERE);----------(5)
}

(1)首先限制了当前memblock的上限。之所以这么做是因为在进行mapping的时候,如果任何一级的Translation table不存在的话都需要进行页表内存的分配。而在这个时间点上,伙伴系统没有ready,无法动态分配。当然,这时候memblock已经ready了,但是如果分配的内存都还没有创建地址映射(整个物理内存布局已知并且保存在了memblock模块中的memblock模块中,但是并非所有系统内存的地址映射都已经建立好的,而我们map_mem函数的本意就是要创建所有系统内存的mapping),内核一旦访问memblock_alloc分配的物理内存,悲剧就会发生了。怎么破?这里采用了限定memblock上限的方法。一旦设定了上限,那么memblock_alloc分配的物理内存不会高于这个上限。

设定怎样的上限呢?基本思路就是在map_mem的调用过程中,不需要分配translation table,怎么做到呢?当然是尽量利用已经静态定义好的那些页表了。PHYS_OFFSET是物理内存的起始地址,SWAPPER_INIT_MAP_SIZE 是启动阶段kernel direct mapping的size。也就是说,从PHYS_OFFSET到PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE的区域,所有的页表(各个level的translation table)都已经OK,不需要分配,只需要把描述符写入页表即可。因此,如果将当前memblock分配的上限设定在这里将不会产生内存分配的动作(因为页表都已经ready)。

(2)对系统中所有的memory type的region建立对应的地址映射。由于reserved type的memory region是memory type的region的真子集,因此reserved memory 的地址映射也就一并建立了。

(3)如果不使用section map,那么我们在kernel direct mapping区域静态分配了PGD~PTE的页表,通过起始地址对齐以及对memblock limit的设定就可以保证在create_mapping()的时候不分配页表内存。但是在下面的情况下:

    (A)Memory block的start或者end地址并没有对齐在2M上

    (B)使用section map

在这种情况下,调用create_mapping()的时候会分配pte页表内存(没有对齐2M,无法进行section mapping)。怎么破?还好第一个memory block(也就是kernel image所在的block)的start address是必定对齐在2M地址上的,所以只要考虑end地址,这时候需要适当的缩小limit到end & SECTION_MASK就可以保证分配的页表内存是已经建立地址映射的了。

(4)__map_memblock代码如下:

static void __init __map_memblock(phys_addr_t start, phys_addr_t end)
{
    create_mapping(start, __phys_to_virt(start), end - start,
            PAGE_KERNEL_EXEC);
}

需要说明的是,在map_mem之后,所有之前通过__create_page_tables创建的描述符都被覆盖了,取而代之的是新的映射,并且memory attribute如下:

#define PAGE_KERNEL_EXEC    __pgprot(_PAGE_DEFAULT | PTE_UXN | PTE_DIRTY | PTE_WRITE)

大部分memory attribute保持不变(例如MT_NORMAL、PTE_AF 、 PTE_SHARED等),有几个bit需要说明一下:PTE_UXN,Unprivileged Execute-never bit,也就是限制userspace从这里取指执行。PTE_DIRTY是一个软件设定的bit,硬件并不操作这个bit,OS软件用这个bit标识该entry是clean or dirty,如果是dirty的话,说明该page的数据已经被写入过,如果该page需要被swapped out,那么还需要保存dirty的数据才能回收该page。关于PTE_WRITE的解释todo。

(5)所有的系统内存的地址映射已经建立完毕,取消之前的上限,让memblock模块可以自由的分配内存。

 

四、填充PGD中的描述符

create_mapping实际上是调用底层的__create_mapping函数完成地址映射的,具体代码如下:

static void __init create_mapping(phys_addr_t phys, unsigned long virt,
                  phys_addr_t size, pgprot_t prot)
{
    if (virt < VMALLOC_START) {
        pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside kernel range\n",
            &phys, virt);
        return;
    }
    __create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
             size, prot, early_alloc);
}

create_mapping的作用是将起始物理地址等于phys,大小是size的这一段物理内存mapping到起始虚拟地址是virt的虚拟地址空间,映射的memory attribute是prot。内核的虚拟地址空间从VMALLOC_START开始,低于这个地址就不对了,验证完虚拟地址,底层是调用__create_mapping函数,传递的参数情况是这样的,init_mm是内核空间的内存描述符,pgd_offset_k是根据给定的虚拟地址,在kernel space的pgd中找到对应的描述符的位置,early_alloc是在mapping过程中,如果需要分配内存的话(页表需要内存),调用该函数进行内存的分配。__create_mapping函数具体代码如下:

static void  __create_mapping(struct mm_struct *mm, pgd_t *pgd,
                    phys_addr_t phys, unsigned long virt,
                    phys_addr_t size, pgprot_t prot,
                    void *(*alloc)(unsigned long size))
{
    unsigned long addr, length, end, next;

    addr = virt & PAGE_MASK;------------------------(1)
    length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

    end = addr + length;
    do {----------------------------------(2)
        next = pgd_addr_end(addr, end);--------------------(3)
        alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);----------(4)
        phys += next - addr;
    } while (pgd++, addr = next, addr != end);
}

创建地址映射熟悉要明确地址空间,不同的进程有不同的地址空间,struct mm_struct就是描述一个进程的虚拟地址空间,当然,我们这里的场景是为内核虚拟地址空间而创建地址映射,因此传递的参数是init_mm。需要创建地址映射的起始虚拟地址是virt,该虚拟地址对应的PUD中的描述符是一个8B的内存,pgd就是指向这个描述符内存的指针。

(1)因为地址映射的最小单位是page,因此这里进行mapping的虚拟地址需要对齐到page size,同样的,长度也需要对齐到page size。经过对齐运算,(addr,length)定义的地址范围应该是囊括(virt,size)定义的地址范围,并且是对齐到page的。

(2)(addr,length)这个虚拟地址范围可能需要占据多个PGD entry,因此这里我们需要一个循环,不断的调用alloc_init_pud函数来完成(addr,length)这个虚拟地址范围的映射,当然,alloc_init_pud函数其实也会建立下游(例如PUD、PMD、PTE)翻译表的entry。

(3)pgd中的一个描述符只能mapping有限区域的虚拟地址(PGDIR_SIZE),pgd_addr_end的宏就是计算addr所在区域的end address。如果计算出来的end address小于传入的end地址参数,那么返回end参数值。也就是说,如果(addr,length)这个虚拟地址范围的mapping需要跨越多个pgd entry,那么next变量保存了下一个pgd entry的起始虚拟地址。

(4)这个函数有两个作用,一是填充pgd entry,二是创建后续的pud translation table(如果需要的话)并进行下游Translation table的建立。

 

五、分配PUD页表内存并填充相应的描述符

alloc_init_pud并非只是操作pud,实际上它是操作pgd的一个entry,并分配初始pud以及后续translation table的。填充PGD的entry需要给出对应PUD translation table的内存地址,如果PUD不存在,那么alloc_init_pud还需要分配PUD translation table(page size),只有得到PUD翻译表的物理内存地址,我们才能填充PGD entry。具体代码如下:

static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
                  unsigned long addr, unsigned long end,
                  phys_addr_t phys, pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pud_t *pud;
    unsigned long next;

    if (pgd_none(*pgd)) {--------------------------(1)
        pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
        pgd_populate(mm, pgd, pud);
    } 
    pud = pud_offset(pgd, addr); ---------------------(2)
    do { --------------------------------(3)
        next = pud_addr_end(addr, end);

        if (use_1G_block(addr, next, phys)) { ----------------(4)
            pud_t old_pud = *pud;
            set_pud(pud, __pud(phys | pgprot_val(mk_sect_prot(prot)))); -----(5)

            if (!pud_none(old_pud)) { ---------------------(6)
                flush_tlb_all(); ------------------------(7)
                if (pud_table(old_pud)) {
                    phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
                    if (!WARN_ON_ONCE(slab_is_available()))
                        memblock_free(table, PAGE_SIZE); ------------(8)
                }
            }
        } else {
            alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
        }
        phys += next - addr;
    } while (pud++, addr = next, addr != end);
}

(1)如果当前pgd entry是全0的话,说明还没有对应的下级PUD页表内存,因此需要进行PUD页表内存的分配。需要说明的是这时候,伙伴系统没有ready,分配内存仍然使用memblock模块,pgd_populate用来建立pgd entry和PUD 页表内存的关系。

(2)至此,pud的页表内存已经有了,但是addr对应PUD中的哪一个描述符呢?pud_offset给出了答案,其返回的指针指向传入参数addr地址对应的pud 描述符内存,而我们随后的任务就是填充pud entry了。

(3)虽然(addr,end)之间的虚拟地址范围共享一个pgd entry,但是这个地址范围对应的pud entry可能有多个,通过循环,逐一填充pud entry,同时分配并初始化下一阶页表。

(4)如果没有可能存在的1G block地址映射,这里的代码逻辑和上一节中的类似,只不过不断的循环调用alloc_init_pud改成alloc_init_pmd即可。然而,ARM64的MMU硬件提供了灰常强大的功能,系统支持1G size的block mapping,如果能够应用会获取非常多的好处:不需要分配下级的translation table节省了内存,更重要的是大大降低了TLB miss,提高了性能。既然这么好,当然要使用,不过有什么条件呢?首先系统配置必须是4k的page size,这种配置下,一个PUD entry可以覆盖1G的memory block。此外,起止虚拟地址以及映射到的物理地址都必须要对齐在1G size上。

(5)填写一个PUD描述符,一次搞定1G size的address mapping,没有PMD和PTE的页表内存,没有对PMD 和PTE描述符的访问,多么简单,多么美妙啊。假设系统内存4G,并且物理地址对齐在1G上(虚拟地址PAGE_OFFSET本来就是对齐在1G的),那么4个PUD的描述符就搞定了内核空间的线性地址映射区间。

(6)如果pud entry是非空的,那么就说明之前已经有了对该段地址的mapping(也许是只映射了部分)。一个简单的例子就是起始阶段的kernel image mapping,在__create_page_tables创建pud 以及pmd中entry。如果不能进行section mapping,那么还建立了PTE中的描述符,现在这些描述符都没有用了,我们可以丢弃它们了。

(7)虽然建立了新的页表,但是旧的页表还残留在了TLB中,必须将其“赶尽杀绝”,清除出TLB。

(8)如果pud指向了一个table描述符,也就是说明该entry指向一个PMD table,那么需要释放其memory。

 

六、分配PMD页表内存并填充相应的描述符

1G block mapping虽好,不一定适合所有的系统,下面我一起看看PUD entry中填充的是block descriptor的情况(描述符指向PMD translation table):

static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
                  unsigned long addr, unsigned long end,
                  phys_addr_t phys, pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pmd_t *pmd;
    unsigned long next;

    if (pud_none(*pud) || pud_sect(*pud)) {-------------------(1)
        pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));---分配pmd页表内存
        if (pud_sect(*pud)) {--------------------------(2)
            split_pud(pud, pmd);
        }
        pud_populate(mm, pud, pmd);---------------------(3)
        flush_tlb_all();
    }
    BUG_ON(pud_bad(*pud));

    pmd = pmd_offset(pud, addr);-----------------------(4)
    do {
        next = pmd_addr_end(addr, end);
        if (((addr | next | phys) & ~SECTION_MASK) == 0) {------------(5)
            pmd_t old_pmd =*pmd;
            set_pmd(pmd, __pmd(phys | pgprot_val(mk_sect_prot(prot))));

            if (!pmd_none(old_pmd)) {----------------------(6)
                flush_tlb_all();
                if (pmd_table(old_pmd)) {
                    phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
                    if (!WARN_ON_ONCE(slab_is_available()))
                        memblock_free(table, PAGE_SIZE);
                }
            }
        } else {
            alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
                       prot, alloc);
        }
        phys += next - addr;
    } while (pmd++, addr = next, addr != end);
}

 

(1)有两个场景需要分配PMD的页表内存,一个是该pud entry是空的,我们需要分配后续的PMD页表内存。另外一个是旧的pud entry是section 描述符,映射了1G的address block。但是现在由于种种原因,我们需要修改它,故需要remove这个1G block的section mapping。

(2)虽然是建立新的mapping,但是原来旧的1G mapping也要保留的,也许这次我们只是想更新部分地址映射呢。在这种情况下,我们先通过split_pud函数调用把一个1G block mapping转换成通过pmd进行mapping的形式(一个pud的section mapping描述符(1G size)变成了512个pmd中的section mapping描述符(2M size)。形式变了,味道不变,加量不加价,仍然是1G block的地址映射。

(3)修正pud entry,让其指向新的pmd页表内存,同时flush tlb的内容。

(4)下面这段代码的逻辑起始是和alloc_init_pud是类似的。如果不能进行2M的section mapping,那么就循环调用alloc_init_pte进行地址的mapping,这里我们就不多说了,重点看看2M section mapping的处理。

(5)如果满足2M section的要求,那么就调用set_pmd填充pmd entry。

(6)如果有旧的section mapping,并且指向一个PTE table,那么还需要释放这些不需要的PTE页表描述符占用的内存。

 

七、分配PTE页表内存并填充相应的描述符

static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
                  unsigned long end, unsigned long pfn,
                  pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pte_t *pte;

    if (pmd_none(*pmd) || pmd_sect(*pmd)) {----------------(1)
        pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
        if (pmd_sect(*pmd))
            split_pmd(pmd, pte);----------------------(2)
        __pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);--------(3)
        flush_tlb_all();
    }
    BUG_ON(pmd_bad(*pmd));

    pte = pte_offset_kernel(pmd, addr);
    do {
        set_pte(pte, pfn_pte(pfn, prot));-------------------(4)
        pfn++;
    } while (pte++, addr += PAGE_SIZE, addr != end);
}

 

(1)走到这个函数,说明后续需要建立PTE这一个level的页表描述符,因此,需要分配PTE页表内存,场景有两个,一个是从来没有进行过映射,另外一个是已经建立映射,但是是section mapping,不符合要求。

(2)如果之前有section mapping,那么我们需要将其分裂成512个pte中的page descriptor。

(3)让pmd entry指向新的pte页表内存。需要说明的是:如果之前pmd entry是空的,那么新的pte页表中有512个invalid descriptor,如果之前有section mapping,那么实际上这个新的PTE页表已经通过split_pmd填充了512个page descritor。

(4)循环设定(addr,end)这段地址区域的pte中的page descriptor。

 

参考文献:

1、ARMv8技术手册

2、Linux 4.4.6内核源代码

原创文章,转发请注明出处。蜗窝科技


蓝牙协议分析(9)_BLE安全机制之LL Privacy

$
0
0

1. 前言

在上一篇文章[1]中,我们介绍了BLE的白名单机制,这是一种通过地址进行简单的访问控制的安全机制。同时我们也提到了,这种安全机制只防君子,不防小人,试想这样一种场景:

A设备表示只信任B、C、D设备,因此就把它们的地址加入到了自己的白名单中,表示只愿意和它们沟通。与此同时,E设备对它们的沟通非常感兴趣,但A对自己不信任啊,肿么办?

E眼珠子一转,想出一个坏主意:把自己的地址伪装成成B、C、D中任意一个(这个还是很容易办到的,随便扫描一下就得它们的地址了)就行了,嘿嘿嘿!

那么问题来了,怎么摆脱“小人E“的偷听呢?不着急,我们还有手段:“链路层的Privacy(隐私)机制”。

2. LL Privacy机制介绍

总结来说,LL Privacy机制是白名单(white list)机制的进阶和加强,它在白名单的基础上,将设备地址转变成Private addresses[2]地址,以降低“小人E“窃得设备地址进而进行伪装的概率。

从白名单的角度看,Non-resolvable private address和Public Device Address/Static Device Address没有任何区别,因此LL Privacy机制主要指Resolvable Private Addresses。因此,LL Privacy机制的本质是:

通过Resolvable Private Addresses,将在空中传输的设备地址加密,让“小人E”无法窃得,从而增加其伪装的难度。

注1:当然,LL Privacy机制可以脱离白名单机制单独使用,不过这样的话好像没什么威力。

注2:有关Resolvable Private Addresses、Identity Address、IRK(Local/Peer IRK)等概念的详细介绍,可参考“蓝牙协议分析(6)_BLE地址类型[2]”,本文将会直接使用。

3. Resolving List

和白名单机制上的White List类似,如果设备需要使用LL Privacy机制,则需要在Controller端保存一个Resolving List,其思路为:

1)BLE设备要按照[1]中的介绍,配置并使能白名单机制,把那些受信任设备的地址(这里为Identity Address)加入到自己的白名单中,并采用合适的白名单策略。

2)如果设备需要使用LL Privacy策略,保护自己(以及对方)的地址不被窃取,则需要将自己(local)和对方(peer)的地址和加密key保存在一个称作Resolving List的列表中。

3)Resolving List的每一个条目,都保存了一对BLE设备的key/address信息,其格式为:Local IRK | Peer IRK | Peer Device Identity Address | Address Type。

Local IRK,本地的IRK,用于将本地设备的Identity Address转换为Resolvable Private Address。可以为0,表示本地地址直接使用Identity Address;

Peer IRK,对端的IRK,用于将对端设备的Resolvable Private Address解析回Identity Address。可以为零,表示对端地址是Identity Address;

Peer Device Identity Address、Address Type,对端设备的Identity Address及类型,用于和解析回的Identity Address进行比对。

4)Resolving List是Host通过HCI命令提供给Controller的,Controller的LL负责如下事项:

发送数据包[3]时:
如果有AdvA需要填充,则判断Resolving List是否有非0的Local IRK,如果有,则使用Local IRK将Identity Address加密为Resolvable Private Address,填充到AdvA中。否则,直接填充Identity Address;
同理,如果有InitA需要填充,则判断Resolving List是否有匹配的、非0的Peer IRK,如果有,则使用Peer IRK将对端的Identity Address加密为Resolvable Private Address,填充到InitA中。否则,直接填充Identity Address。

接收数据包时:
如果数据包中的AdvA或者InitA为普通的Identity Address,则直接做后续的处理;
如果它们为Resolvable Private Address,则会遍历Resolving List中所有的“IRK | Identity Address”条目,使用IRK解出Identity Address和条目中的对比,如果匹配,则地址解析成功,可以做进一步处理。如果不匹配,且使能了白名单/LL Privacy策略,则会直接丢弃。

5)需要重点说明的是,Controller和Host之间所有的事件交互(除了Resolving List操作之外),均使用Identity Address。也就是说,设备地址的加密、解密、比对等操作,都是在controller中完成的,对上层实体(HCI之上)是透明的。

4. 使用场景说明

结合上面2章的解释,罗列一下LL Privacy策略有关的使用场景(大部分翻译自蓝牙spec)。

4.1 设备处于广播状态(Advertising state)时

由[3]中的介绍可知,处于广播状态的BLE设备,根据需要可发送ADV_IND、ADV_DIRECT_IND、ADV_NONCONN_IND和ADV_SCAN_IND 4种类型的广播包,也就是说有4种不同的广播状态,它们的LL privacy策略有稍微的不同,下面分别描述。

1)ADV_IND

设备(Advertiser)发送ADV_IND时,其PDU(connectable undirected advertising event PDU)有一个AdvA字段,该字段的填充策略为(由controller的LL执行):

检查Resolving List,查看是否存在非0的Local IRK条目,如果有,则使用Local IRK将自己的Identity Address加密为Resolvable Private Addresses,并填充到AdvA中。否则,直接使用Identity Address。

Advertiser收到连接请求时,请求者的地址会包含在PDU的InitA中,该字段的解析策略为(由controller的LL执行):

如果InitA是Resolvable Private Addresses,且当前使能了地址解析功能,LL会遍历Resolving List中所有的“Peer IRK | Peer Device Identity Address | Address Type”条目,使用Peer IRK解析出Identity Address后和Peer Device Identity Address做比对,如果匹配,则解析成功,再基于具体的白名单策略,觉得是否接受连接;

如果解析不成功,则无法建立连接;

如果InitA不是Resolvable Private Addresses,则走正常的连接过程。

Advertiser收到扫描请求时,对ScanA的处理策略,和InitA类似。

2)ADV_DIRECT_IND

设备(Advertiser)发送ADV_DIRECT_IND 时,其PDU(connectable directed advertising event PDU)包含AdvA和InitA两个地址,它们填充策略为(由controller的LL执行):

检查Resolving List,查看是否存在非0的Local IRK条目,如果有,则使用Local IRK将自己的Identity Address加密为Resolvable Private Addresses,并填充到AdvA中。否则,直接使用Identity Address;

检查Resolving List,查看是否存在非0、和InitA的Identity Address匹配的Peer IRK条目,如果有,则使用Peer IRK将InitA的Identity Address加密为Resolvable Private Addresses,并填充到InitA中。否则,直接使用Identity Address。

Advertiser收到连接请求时,请求者的地址会包含在PDU的InitA中,该字段的解析策略为和上面ADV_IND类似,不再详细说明。

3)ADV_NONCONN_IND and ADV_SCAN_IND

这两个状态下,AdvA的填充策略和上面1)和2)一样,不再详细说明。

当Advertiser收到SCAN请求时,对ScanA的处理策略,和1)中InitA类似,不再详细说明。

4.2 设备处于扫描状态(Scanning state)时

处于Scanning状态的设备(Scanner)在接收到Advertiser发送的scannable的广播包时,需要按照4.1中解析InitA的方法,解析广播包中的AdvA,并根据当前的白名单策略,进行过滤。

Scanner发送scan请求时,需要指定ScanA和AdvA两个地址。其实ScanA的填充策略和4.1中的AdvA类似,不再详细说明。而AdvA,需要和接收到的广播包的AdvA完全一样,不能有改动。

4.3 设备处于连接状态(Initiating state)时

处于Initiating状态的设备(Initiator)在接收到Advertising发送的connectable的广播包时,需要按照4.1中解析InitA的方法,解析广播包中的AdvA,并根据当前的白名单策略,进行过滤。

同理,Initiator发送连接请求时,需要指定InitA和AdvA两个地址。其实InitA的填充策略和4.1中的AdvA类似,不再详细说明。而AdvA,需要和接收到的广播包的AdvA完全一样,不能有改动。

最后,Initiator在接收到Advertising发送的directed connectable广播包时,除了要解析AdvA,如果InitA是Resolvable Private Addresses,则需要使用Local IRK解析InitA。

5. 和LL Privacy有关的HCI命令

不太想写了,需要了解的直接去掰spec吧。

6. 参考文档

[1] 蓝牙协议分析(8)_BLE安全机制之白名单

[2] 蓝牙协议分析(6)_BLE地址类型

[3] 蓝牙协议分析(5)_BLE广播通信相关的技术分析

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-018-KERNEL-串口驱动开发之数据收发

$
0
0

1. 前言

本文是“X Project”串口驱动开发的第四篇,在第二篇“uart driver框架[1]”的基础上,实现基本的、可收发数据的uart驱动,并借助这个过程,学习如下知识:

中断的申请和使用;

利用中断发送和接收数据;

uart_ops中常用函数(.startup, .start_tx, etc.)的使用。

2. 中断的申请和使用

在linux kernel中,使用中断进行数据传输是最基本的要求(更进阶的是DMA,我们会在后续的文章中介绍),因为强悍的CPU无法忍受乌龟般的外设速度,忙等待只会自断生路。下面将会以Bubblegum-96平台的UART driver为例,介绍中断的使用。

2.1 准备工作

关于中断,在使用之前,我们至少需要先理清如下内容(以Bubblegum-96平台的UART5为例进行说明):

1)该外设和中断控制器之间通过哪些中断线(IRQ line,也就是我们常说的中断号)进行连接。

2)每个中断线的触发方式为何,电平?边沿?

由[2]克制,Bubblegum-96平台的UART5的中断线为SPI 35,触发方式为高电平触发(外部GPIO中断比较关心触发方式,其它的外设中断,一般都是电平触发)。

3)在设备内部(例如这里的UART控制器),哪些行为可以产生中断?产生中断的条件为何?如何控制中断的使能?如何清除中断的pending状态?

对Bubblegum-96平台的UART5控制器来说,由[1]可知:

有TX和RX两种中断;

TX的FIFO大小为32bytes,只要空闲空间(empty)大于等于16bytes,就会触发中断;

RX的FIFO大小也为32bytes,当接收的数据大于等于16bytes时,就会触发中断;

TX/RX中断使能与否,可由UARTx_CTL(offset=0x0000)的bit19、bit18控制;

UART5中断产生时,可通过UARTx_STAT(offset=0x000c)的bit1(TX IRQ Pending)、bit0(RX IRQ Pending)获取具体的中断原因(TX还是RX),这两个bit均写1清除。

注1,这里存在一个问题,需要大家思考(这个问题是串口驱动最常见的):基于上面的描述,只有RX接收到大于等于16bytes的数据时,才会产生中断,那么接收少于16bytes的数据时,怎么办?后面实际调试的时候,再细说。

4)中断发生时,要做哪些事情?这些事情是否比较耗时?是否可以在线程中处理?

以UART控制器为例:

TX中断产生时,表明可以向TX FIFO写入数据;

RX中断产生时,表明RX FIFO中有数据需要读取;

在Linux serial framework下,上面两个动作都不是耗时的动作,因此不需要在线程中进行处理。

2.2 中断申请

对uart driver来说,中断的申请,包括3个步骤:

1)在DTS中,通过interrupts字段,指定该设备需要使用的中断号

        serial5: serial@e012a000 {
                compatible = "actions,s900-serial";
                reg = <0 0="" 0xe012a000="" 0x2000="">;  /* UART5_BASE */
+               interrupts = ;
        };

2)在platform driver的probe接口中,通过platform_get_irq接口,将DTS中指定的中断号取出并保存下来

@@ -253,6 +268,12 @@ static int owl_serial_probe(struct platform_device *pdev)
        }
        port->iotype = UPIO_MEM32;

+       port->irq = platform_get_irq(pdev, 0);
+       if (port->irq < 0) {
+               dev_err(&pdev->dev, "Failed to get irq\n");
+               return port->irq;
+       }
+
        port->line = of_alias_get_id(pdev->dev.of_node, "serial");

 

platform_get_irq第二个参数是dts中interrupts字段指定的中断编号,我们只使用一个,因此这里为0即可。

3)在uart port的.startup接口中,调用devm_request_irq,申请并使能该中断线

注2:这里的使能,是指UART控制器和GIC之间的使能。

+static irqreturn_t owl_serial_irq_handle(int irq, void *data)
+{
+       struct uart_port *port = data;
+
+       return IRQ_HANDLED;
+}
+
static int owl_serial_startup(struct uart_port *port)
{
        int ret = 0;

        dev_dbg(port->dev, "%s\n", __func__);

+       ret = devm_request_irq(port->dev, port->irq, owl_serial_irq_handle,
+                              0, "owl_serial", port);

+       if (ret < 0) {
+               dev_err(port->dev, "request irq(%d) failed(%d)\n",
+                       port->irq, ret);
+               return ret;
+       }
+
        return ret;
}

devm_request_irq有很多参数,其中“owl_serial_irq_handle”是中断的handler,0是flags(这里没有特殊的flag需要传递),"owl_serial"是名字(无关紧要),port是传入的私有数据,kernel irq core会在调用我们的handler的时候再传给我们(具体请参考上面owl_serial_irq_handle函数)。

2.3 RX和TX中断的使能

串口在启动(.startup被调用)之后,就要保证可以正常接收数据,因此RX中断需要在uart port的.startup中使能:

@@ -111,6 +120,9 @@ static int owl_serial_startup(struct uart_port *port)
                return ret;
        }

+       /* RX irq enable */
+       __PORT_SET_BIT(port, UART_CTL, UART_CTL_RXIE);
+
        return ret;
}

而TX中断可以放到uart port的.start_tx中再使能,如下:

static void owl_serial_start_tx(struct uart_port *port)
{
        dev_dbg(port->dev, "%s\n", __func__);
+
+       /* TX irq enable */
+       __PORT_SET_BIT(port, UART_CTL, UART_CTL_TXIE);
}

2.4 中断处理

中断处理的逻辑比较简单:

1)读取UARTx_STAT寄存器,判断中断类型。

2)如果是RX中断,从UARTx_RXDAT中读取数据,并调用tty_insert_flip_char将读取的数据保存在uart port的RX buffer中。具体可参考第4章的介绍。

3)如果是TX中断,则检查uart port的TX buffer,是否还有未发送的数据,如果有,则将数据写入到UARTx_TXDAT,由UART控制器发送出去。具体可参考第3章的介绍。

3. 收据收发前的其它操作

在前面的章节提到过,uart driver需要实现由struct uart_ops变量所代表的各种回调函数,这里列举一些和数据收发直接相关的函数,如下:

1).startup,除去上面2.3中申请终端、使能RX中断等操作,我们还需要在startup的时候,使能串口控制器,如下:

@@ -123,6 +123,9 @@ static int owl_serial_startup(struct uart_port *port)
        /* RX irq enable */
        __PORT_SET_BIT(port, UART_CTL, UART_CTL_RXIE);

+       /* enable serial port */
+       __PORT_SET_BIT(port, UART_CTL, UART_CTL_EN);
+
        return ret;
}

2).shutdown,.startup的反动作,disable串口控制器、disable RX中断、注销中断等,不再贴代码了。

3).stop_tx,disable TX中断,以及其它和数据发送有关的内容(具体可参考第4章的描述)。

4).stop_rx,disable RX中断,以及其它和数据接收有关的内容(具体可参考第5章的描述)。

5).tx_empty,判断TX FIFO是否为空,如下(如果为空,需要返回TIOCSER_TEMT,否则返回0即可):

@@ -153,7 +167,10 @@ static unsigned int owl_serial_tx_empty(struct uart_port *port)
{
        dev_dbg(port->dev, "%s\n", __func__);

-       return 0;
+       if (__PORT_TEST_BIT(port, UART_STAT, UART_STAT_TFES))
+               return TIOCSER_TEMT;
+       else
+               return 0;

}

3. 数据发送

上层软件需要通过uart port发送的数据时,会将数据暂存在一个struct circ_buf类型的环形缓冲区中(port->state->xmit),然后调用driver的.start_tx接口,我们可以在这个接口中从环形缓冲区读取数据要发送的数据,写入到UART控制器的TX FIFO。当然,TX FIFO可能很小,需要多次传输才能完成,因此我们需要在传输完成的中断中,再次从环形缓冲区读取数据,写入TX FIFO,直到所有数据发送完成为止。上述的发送流程图如下:

serial_tx_flow

图片1 数据发送流程

该流程比较简单(具体的代码实现可参考本文档对应的patch文件),不过有一些地方需要说明一下:

1).start_tx实在关中断的情况下调用的,因此不用考虑同步问题。

2)由于TX FIFO的限制,大多数情况下,数据发送是由TX中断推动的。但存在一种特殊情况:TX buffer的数据已经发送完成了,此时串口控制器没有数据在发送,TX中断不会产生,此时需要在.start_tx中重新写FIFO。上面图片1的流程,可以覆盖这个场景。

4. 数据接收

数据接收的流程更简单,流程图如下:

serial_rx_flow

图片2 数据接收流程

 

5. 串口数据发送和console之间的同步

由于console的write,和串口数据的发送,都是操作同一个TX FIFO,因此存在数据交叉的问题。要解决这个问题,一般遵守一个原则:保证一次console write的完整性,这样可以避免kernel输出日志被打乱。为了做到这一点,我们需要按照如下步骤改造console的write接口:

1)备份当前TX中断的状态,然后禁止TX中断。

2)按照原来的方式,调用uart_console_write将字符串通过串口发送出去。

3)恢复TX的中断状态。

代码如下:

static void owl_console_write(struct console *con, const char *s, unsigned n)
{
+       bool tx_irq_enabled;
+
        struct uart_driver *driver = con->data;
        struct uart_port *port = driver->state[con->index].uart_port;

+       /* save TX IRQ status */
+       tx_irq_enabled = __PORT_TEST_BIT(port, UART_CTL, UART_CTL_TXIE);
+
+       /* TX irq disable */
+       __PORT_CLEAR_BIT(port, UART_CTL, UART_CTL_TXIE);
+
        uart_console_write(port, s, n, owl_console_putchar);
+
+       /* restore TX IRQ status */
+       if (tx_irq_enabled)
+               __PORT_SET_BIT(port, UART_CTL, UART_CTL_TXIE);
}

6. 参考文档

[1] bubblegum-96/SoC_bubblegum96.pdf

[2] https://github.com/96boards-bubblegum/linux/blob/bubblegum96-3.10/arch/arm64/boot/dts/s900.dtsi

[3] Documentation/serial/driver

[4] patch文件,https://github.com/wowotechX/linux/commit/9328d8aa82dcf2c17eec03c1beccf9c5c0567a6a

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-020-ROOTFS-initramfs的制作和测试

$
0
0

1. 前言

我们在“X-015-KERNEL-ARM generic timer driver的移植”中移植完ARM generic timer之后,Linux的启动已经走完了内核空间的旅程,即将冲破kernel走向用户空间,有“诗”为证:

[    0.142156] ---[ end Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.

意思很明显,kernel找不到可执行的init文件,无法继续下去了。还好,基本的串口驱动[1]开发完毕之后,我们可以把魔抓伸向用户空间了。牵涉到用户空间,麻烦就大了,块设备、文件系统、rootfs等等,不是三言两语就能搞定的。不过没关系,我们可以从最简单的ramdisk入手。

因此,本文将以initramfs为例,介绍kernel把CPU的控制权交给用户空间程序的过程。与此同时,本文也是串口驱动[1]和GIC驱动[2]的测试用例(这两个驱动移植完成后,一直没有测试,机会终于来了)。

2. 知识汇整

由[1]的介绍可知,当前系统的console为serial console,对应UART5(ttyS5),结合[4]中的描述,Linux kernel在启动的过程中,会打开“/dev/console”设备(该设备其实对应的UART5的driver),并将stdin(0)、stdout(1)、stderr(2)三个文件句柄,和该设备对应。

然后,kernel会执行用户空间的init程序(如/sbin/init),并将上面三个文件句柄继承给init程序。因此,会有如下的结论:

init程序的stdin(如getchar、scanf等),对应UART5 driver的RX;

init程序的stdout(如printf等),对应UART5 driver的TX。

这就是本文后续描述的理论基础,请大家务必理解。

3. initramfs的制作

有关initramfs的概念和介绍,可以参考ooonebook同学写的一篇博文[3],这里只关注制作步骤。

3.1 编写一个简单的程序,冒充Linux系统的init程序

cd ~/work/xprj/build
touch init.c

该程序很简单,打印一些字符串之后,阻塞在getchar()函数上,每接收一个字符串,就把它打印出来,如下:

#include< stdio.h>

int main(void)
{
        char ch;

        printf("Test /sbin/init main....\n");
        printf("0123456789abcdefghijklmnopqrstuvwxyz\n");
        printf("ABCDEFGHIJKLMNOPQRSTUVWXYZ9876543210\n");

        while ((ch = getchar()) != 'q')
                printf("%c", ch);

        return 0;
}

由第2章的知识汇整可知,printf字符串输出,会经stdout(fd=1)交给serial driver的TX发送出去,而getchar将会读取stdin(fd=0)上的输入数据,这些数据是由serial driver的RX送过来的。

编写完成后,用静态链接的方式,编译生产init文件,留待后面使用:

pengo@ubuntu:~/work/xprj/build$ ./gcc-linaro-aarch64-linux-gnu-4.8-2013.12_linux/bin/aarch64-linux-gnu-gcc -static -o init init.c      

pengo@ubuntu:~/work/xprj/build$ file init

init: ELF 64-bit LSB executable, version 1 (SYSV), statically linked, for GNU/Linux 3.7.0, not stripped

3.2 将init文件copy到一个目录中,并打包成initramfs的格式

pengo@ubuntu:~/work/xprj/build$ mkdir out/rootfs/sbin/ -p
pengo@ubuntu:~/work/xprj/build$ cp init out/rootfs/sbin/init

打包的脚本如下(具体可参考后面的patch文件):

ROOTFS_OUT_DIR=$(OUT_DIR)/rootfs

initramfs:
        mkdir -p $(OUT_DIR)
        cd ${ROOTFS_OUT_DIR}; find . | cpio -H newc -o | gzip -9 -n > ${OUT_DIR}/initramfs.gz

编译并生成initramfs文件的过程如下:

pengo@ubuntu:~/work/xprj/build$ make initramfs
mkdir -p /home/pengo/work/xprj/build/out
cd /home/pengo/work/xprj/build/out/rootfs; find . | cpio -H newc -o | gzip -9 -n> /home/pengo/work/xprj/build/out/initramfs.gz
1331 blocks

3.3 修改.its文件[5],加入上面生成的initramfs文件

具体可参考如下的patch文件(不在详细解释):

https://github.com/wowotechX/build/commit/39d71bf8f922c5da4749ee73ccaa68e5660f808f

3.4 配置Linux kernel,使其支持RAM filesystem/RAM disk、ELF格式

make kernel-config

General setup  --->
       [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

Userspace binary formats  --->
        [*] Kernel support for ELF binaries

其中RAM filesystem用于支持initramfs,“ELF binaries”用于支持ELF格式的文件(3.1中编译生产的init文件即为该格式,这也是Linux应用程序的常用格式)。

3.5 修改kernel默认的命令行参数,加入root=、init=等参数

如下:

+CONFIG_CMDLINE="earlycon=owl_serial console=ttyS5 loglevel=8 root=/dev/ram0 init=/sbin/init"

3.6 重新编译kernel并运行

make kernel initramfs uImage

编译完成后,运行查看效果(u-boot会自动把initramfs image解压并传给kernel):

make spl-run

make kernel-run

运行的结果是,init程序可以正确运行,可是printf输出不完整,getchar()输入压根不起作用。不过没关系,慢慢解决就行了。

4. 问题汇总以及解决思路

在uart driver中增加一些打印,发现存在如下两个问题:

1)GIC无法产生中断。

2)串口发送时中断有关的处理逻辑不正确。

4.1 GIC无法产生中断

对于该问题,解决方案是:在u-boot中打开对GIC的支持(在头文件中增加GIC有关的定义即可)

注1:该方法只有在u-boot使用了armv8标准low_level_init函数时有效,具体可参考[6]。

注2:有关GICD_BASE、GICC_BASE的意义,可参考[2]以及u-boot的source code。

/* Generic Interrupt Controller Definitions */

+#define CONFIG_GICV2
+
#define GICD_BASE (0xe00f1000)
#define GICC_BASE (0xe00f2000)

这样做的原因是:

ARM64的GIC控制器,有一些特殊的设置,需要在EL3配置。而kernel启动时,会把运行级别切换为EL1[7],显然无法做这个事情。

参考u-boot的代码(arch/arm/cpu/armv8/start.S中lowlevel_init接口),u-boot在切换运行级别之前,做了这些事情:

#if defined(CONFIG_GICV2) || defined(CONFIG_GICV3)
        branch_if_slave x0, 1f
        ldr     x0, =GICD_BASE
        bl      gic_init_secure
1:
#if defined(CONFIG_GICV3)
        ldr     x0, =GICR_BASE
        bl      gic_init_secure_percpu
#elif defined(CONFIG_GICV2)
        ldr     x0, =GICD_BASE
        ldr     x1, =GICC_BASE
        bl      gic_init_secure_percpu
#endif

而此时,CPU刚从ROM code执行完毕,也就是说说,ROM code交出控制权的时候,需要确保运行级别为EL3。

4.2 串口发送异常

解决该问题的具体步骤,这里就不再罗列了,可参考如下patch:

https://github.com/wowotechX/linux/commit/62cc2deb788f56dc4c0e2a43e308fd1084875d8e

有一些串口编程的要点,总结如下(稍后我会更新“X-018-KERNEL-串口驱动开发之数据收发[1]”中的流程图):

1)只要没有数据发送(TX FIFO为空),串口将会一直产生TX中断,这对系统来说是一个灾难,因此串口TX中断的使能是有条件的,具体为:TX FIFO已经满了,TX buffer中还有未发送完成的数据。

此时我们需要使能TX中断,当TX FIFO剩余一定的空间之后,产生中断,在中断处理中继续发送。

除此之外,在任何时候,如果TX buffer已经没有数据了,我们需要把TX中断disable。

2)在串口的中断处理中,要以此对TX、RX分别处理一次,也就是说:

只能用if if语句,不能用if else。

3)在串口中断处理中处理RX数据时,在读完RX FIFO之后,需要调用tty_flip_buffer_push,将接收的数据交给上层的line discipline。

4.3 问题解决后,再次测试

已经okay了,结果如下:

[15:39:26.310] [    0.188249] This architecture does not have kernel memory protection.
[15:39:26.310] Test /sbin/init main....
[15:39:26.310] 0123456789abcdefghijklmnopqrstuvwxyz
[15:39:26.310] ABCDEFGHIJKLMNOPQRSTUVWXYZ9876543210
[15:39:28.220] 123455566
[15:39:32.140]
[15:39:32.140] 123455566
[15:39:32.140]
[15:39:32.800]
[15:39:33.380] 1212121
[15:39:34.320]
[15:39:34.320] 1212121
[15:39:55.320] q

5. 参考文档

[1] X-019-KERNEL-串口驱动开发之数据收发

[2] X-014-KERNEL-ARM GIC driver的移植

[3] http://blog.csdn.net/ooonebook/article/details/52624481

[4] Linux TTY framework(3)_从应用的角度看TTY设备

[5] u-boot FIT image介绍

[6] u-boot启动流程分析(2)_板级(board)部分

[7] ARM64的启动过程之(一):内核第一个脚印

[8] patch文件,786f2bd, 39d71bf, 29bf568, e2b1555, 62cc2de

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-021-ROOTFS-基于busybox的最简rootfs的制作

$
0
0

1. 前言

在前一篇文章[1]中,我们编写并成功运行了一个简单的init程序。于是,“【任务4】启动Linux kernel到命令行”就成了一个水到渠成的事情。当然,只有一个简单的程序还不够,我们要来点实在的,一个真正的根文件系统。

Linux系统中制作根文件系统的方法有很多种,基于一个个package一点点编译、利用buildroot、利用busybox、等等,本文将以嵌入式系统中普遍使用的busybox为例,介绍制作一个简单的rootfs的方法。

2. busybox介绍

嵌入式Linux相关的工程师,对busybox肯定不陌生,百度百科是这样解释的(我不想废话太多,直接copy吧):

BusyBox 是一个集成了一百多个最常用linux命令和工具的软件。BusyBox 包含了一些简单的工具,例如ls、cat和echo等等,还包含了一些更大、更复杂的工具,例grep、find、mount以及telnet。有些人将 BusyBox 称为 Linux 工具里的瑞士军刀。简单的说BusyBox就好像是个大工具箱,它集成压缩了 Linux 的许多工具和命令,也包含了 Android 系统的自带的shell。

busybox的官网可参考[2],为了便于开发,我在“X Project”的工具目录里面放了一个1.25.1版本的,如下:

https://github.com/wowotechX/tools/tree/master/common/busybox-1.25.1

3. busybox的配置

3.1 基本配置

和Linux kernel、u-boot等软件类似,busybox也有强大而灵活的配置项,也是通过menuconfig配置。按照“X Project”的一贯作风,拿到busybox的源代码之后,我们会对它进行简单的配置,并保存一份“X Project”专用的配置文件,过程如下:

cd work/xprj/tools/common/busybox-1.25.1/
make defconfig                          #基于busybox的默认配置(包括所有内容)
cp .config configs/xprj_defconfig  #将生成的.config文件保存为x project的defconfig文件,以便后续使用
make mrproper                          #最后将文件夹清一下

简单的配置后,需要在x project的编译脚本中(./work/xprj/build/Makefile)加入编译busybox的脚本,内容如下:

busybox-config:
        mkdir -p $(BUSYBOX_OUT_DIR)
        cp -f $(BUSYBOX_DIR)/configs/$(BUSYBOX_DEFCONFIG) $(BUSYBOX_OUT_DIR)/.config
        make -C $(BUSYBOX_DIR) O=$(BUSYBOX_OUT_DIR) menuconfig
        cp -f $(BUSYBOX_OUT_DIR)/.config $(BUSYBOX_DIR)/configs/$(BUSYBOX_DEFCONFIG)

busybox:
        mkdir -p $(BUSYBOX_OUT_DIR) $(ROOTFS_OUT_DIR)
        make -C $(BUSYBOX_DIR) CROSS_COMPILE=$(CROSS_COMPILE) O=$(BUSYBOX_OUT_DIR) $(BUSYBOX_DEFCONFIG)
        make -C $(BUSYBOX_DIR) CROSS_COMPILE=$(CROSS_COMPILE) O=$(BUSYBOX_OUT_DIR)
        make -C $(BUSYBOX_DIR) CROSS_COMPILE=$(CROSS_COMPILE) O=$(BUSYBOX_OUT_DIR) CONFIG_PREFIX=$(ROOTFS_OUT_DIR) install

busybox-clean:
        rm $(BUSYBOX_OUT_DIR) -rf

和编译linux kernel、u-boot的方法一样,我们在单独的文件夹中编译busybox(避免污染原有的source code),这是通过“O”参数实现的,另外几个参数,简单解释如下:

O,busybox的编译位置(中间文件会在这里生成),这里为./out/busybox。

CROSS_COMPILE,指定交叉编译器的目录和前缀。

CONFIG_PREFIX,busybox的安装位置,这里为./out/rootfs,后续会基于该文件夹制作initramfs[1]

3.2 使用静态链接的方式编译busybox

原理和[1]中编译init程序类似,我们暂时还没有从编译器、C库等位置copy Linux程序运行所必需的动态链接库,因此任何可执行文件只能先以静态链接的形式编译。

需要静态链接的形式编译busybox的话,非常简单,打开它的一个配置项即可,如下:

make busybox-config

Busybox Settings  --->
    Build Options  --->
        [*] Build BusyBox as a static binary (no shared libs)

配置完成后会更新“configs/xprj_defconfig ”文件,具体可参考后面的patch文件。

注1:这一个配置项,是本文唯一需要对busybox改动的地方,强大的开源软件啊!

4. 编译并运行

利用3.1中(以及[1]中initramfs)的编译脚本,编译busybox,并打包成initramfs之后,下载到开发板(以Bubblegum-96为例)中执行一把:

make busybox initramfs uImage        #编译

make spl-run                                   #按住ADFU键,运行SPL

make kernel-run                               #运行u-boot、kernel等

运行成功后,串口上输出如下内容:


[    0.226562] Freeing unused kernel memory: 136K (ffffff80081a8000 - ffffff80081ca000)
[    0.234312] This architecture does not have kernel memory protection.
can't run '/etc/init.d/rcS': No such file or directory
can't open /dev/tty4: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty2: No such file or directory
/ #
can't open /dev/tty4: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty2: No such file or directory
/ #
/ #
ls
bin      dev      linuxrc  root     sbin     usr
can't open /dev/tty4: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty2: No such file or directory

看来已经跑起来了(ls等命令已经可用了),但一直在打印“can't …”,没关系,再增加一点东西即可。

5. /etc/init.d/rcS和/etc/inittab

5.1 介绍

busybox的正常运行,需要一些必须的配置项,最不可缺少的有两个:

1)/etc/init.d/rcS,/sbin/init成功运行之后,会执行该脚本,我们可以在该脚本中执行后续的初始化操作,如mount文件系统、创建设备节点、等等。

2)/etc/inittab,/etc/init.d/rcS成功执行后,系统就已经处于ready状态,init进程会读取inittab中的配置,决定在哪个设备上启用控制台(当然不止这些)。

为了让第4章出现的命令行可以再正常一些,我们需要在打包rootfs之前,为其创建几个必要的脚本文件,具体如下。

5.2 在script脚本中创建rootfs目录

cd work/xprj/build/script/
mkdir rootfs

5.3 创建rcS文件

busybox的source code中有示例,copy过来就行:

mkdir rootfs/etc/init.d/ -p
cp /home/pengo/work/xprj/tools/common/busybox-1.25.1/examples/bootfloppy/etc/init.d/rcS rootfs/etc/init.d/

查看一下该文件的内容(可以先用着):

cat rootfs/etc/init.d/rcS   

#! /bin/sh
/bin/mount -a

5.4 在rcS中创建两个设备节点/dev/ttyS5和/dev/null

/dev/null是busybox的init进程必须的,/dev/ttyS5是我们的串口设备,需要被用作控制台设备。

cat rootfs/etc/init.d/rcS   


mkdir /dev -p

mknod /dev/ttyS5 c 20 5
mknod /dev/ttyS5 c 1 3

5.5 创建inittab

同rcS文件一样,busybox中有示例文件,copy过来:

cp /home/pengo/work/xprj/tools/common/busybox-1.25.1/examples/bootfloppy/etc/inittab rootfs/etc/

同时,按照examples/inittab的方法,添加ttyS5,如下(具体意义先不用理睬,只要懂115200是什么就行):

cat rootfs/etc/inittab

::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::respawn:/sbin/getty -L ttyS5 115200 vt100
::ctrlaltdel:/bin/umount -a -r

5.6 在编译脚本中,添加一个rootfs copy的命令,并在生成initramfs时执行该命令

-initramfs:
+rootfs_copy:
+       mkdir -p ${ROOTFS_OUT_DIR}
+       cp -rf ${SCRIPT_DIR}/rootfs/* ${ROOTFS_OUT_DIR}/
+
+initramfs: rootfs_copy

        mkdir -p $(OUT_DIR)
        cd ${ROOTFS_OUT_DIR}; find . | cpio -H newc -o | gzip -9 -n > ${OUT_DIR}/initramfs.gz

okay,按照第4章的方法,再次编译运行,出现如下的打印:

[    0.226749] Freeing unused kernel memory: 136K (ffffff80081a8000 - ffffff80081ca000)
[    0.234531] This architecture does not have kernel memory protection.
mount: can't read '/etc/fstab': No such file or directory
/ #
/ #
/ # ls
ls
bin      dev      etc      linuxrc  root     sbin     usr
/ # ls /dev
console  null     ram      ttyS5
/ # ls bin
bin
ash            dumpkmap       kbd_mode       mv             setarch
base64         echo           kill           netstat        setserial

成功了(虽然还有一个can’t,暂时不理它就是了)。

6. 参考文档

[1] X-020-ROOTFS-initramfs的制作和测试

[2] busybox官网,https://busybox.net/

[3] patch文件, 1eec32cb075163,786f2bd,8205b60,a0654a3

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

快讯:蓝牙5.0发布(新特性速览)

$
0
0

1. 前言

2016年12月6日,蓝牙SIG发布了5.0版本的核心规范,该规范从距离、速度等多个方面,对BLE进行了增强,蓝牙官网的总结如下[1]

With the launch of Bluetooth 5, Bluetooth® technology continues to evolve to meet the needs of the industry as the global wireless standard for simple, secure connectivity. With 4x range, 2x speed and 8x broadcasting message capacity, the enhancements of Bluetooth 5 focus on increasing the functionality of Bluetooth for the IoT. These features, along with improved interoperability and coexistence with other wireless technologies, continue to advance the IoT experience by enabling simple and effortless interactions across the vast range of connected devices.

相比蓝牙4.2,新增的特性包括[3]

Several new features are introduced in the Bluetooth Core Specification 5.0
Release. The major areas of improvement are:
• Slot Availability Mask (SAM)
• 2 Msym/s PHY for LE
• LE Long Range
• High Duty Cycle Non-Connectable Advertising
• LE Advertising Extensions
• LE Channel Selection Algorithm #2

下面对一些比较有意思的做个简单的介绍(后续有时间会做比较细致的分析)。

2. 2 Msym/s PHY for LE

在蓝牙4.2 1M符号速率(symbol rate)的PHY(称作LE 1M PHY)基础上,增加2M符号速率的PHY(称作LE 2M PHY),二者的区别为:

1)LE 1M PHY的符号速率为1Msym/s,为必选PHY(每个LE设备必须支持),支持ECC(error correction coding,可选),根据不同的编码方式,支持3种bit速率:1Mb/s(LE 1M)、500Bb/s(LE Coded)和125Kb/s(LE Coded)。

2)LE 2M PHY的符号速率为2Msym/s,为可选PHY,不支持ECC(error correction coding),bit速率为2Mb/s(LE 2M,uncoded)。

3. LE Long Range

将最大的发送功率,从4.0/4.1/4.2中的10mW增大到5.0的100mW(够粗暴,哈哈)。

关于BLE的发射功率,spec中有张表,贴过来供大家参考:

ble_tx_power

4. High Duty Cycle Non-Connectable Advertising

蓝牙4.0将Scannable Undirected和Non-connectable Undirected两种Advertising Event的advInterval的最小值限制为100ms,这就限制了BLE广播的最高速率(2.48kbps,参考[4])。而蓝牙5.0不再区别对待,将最小值统一限制为20ms,从理论上讲,最高的广播速率就可以提高5倍(12.4kbps)。

5. LE Advertising Extensions

这个扩展比较好玩。

蓝牙4.0/4.1/4.2的广播通道(可参考[4]),比较简单、直接,预留3个(可以更少)Physical Channel,用于传输Advertising Event。可传输的数据长度为6~37 octets(加上了协议开销)。

而蓝牙5.0,则搞出了新花样(实用性大增,从此之后就没有连接的必要了啊!),总结为:

1)抽象出primary advertising channel和secondary advertising channel的概念。

2)primary advertising channel就是蓝牙4.2及以前的、预留出的、用于传输Advertising Event。

3)而secondary advertising channel,则直接复用了剩余的37个data channel,用于传输扩展的Advertising Event(称作Extended Advertising Event)。此时可传输的数据长度为0 ~ 255 octets,相比之前的37,暴增了很多倍,好爽啊!!

4)因此,在原有的用于传输广播数据的PDU(ADV_IND、ADV_DIRECT_IND、ADV_NONCONN_IND以及ADV_SCAN_IND,称作legacy PDUs)的基础上,增加了扩展的PDU(ADV_EXT_IND、AUX_ADV_IND、AUX_SYNC_IND以及AUX_CHAIN_IND,称作extended advertising PDUs)。

5)相应的,Advertising Event也分为Legacy Advertising Event和Extenteded Advertising Event。

6. 参考文档

[1] https://www.bluetooth.com/specifications/bluetooth-core-specification/bluetooth5

[2] https://www.bluetooth.com/specifications/adopted-specifications

[3] https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=421043

[4] 蓝牙协议分析(5)_BLE广播通信相关的技术分析

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

Linux serial framework(1)_概述

$
0
0

1. 前言

串口设备(serial or uart,后面不再区分)是TTY设备的一种,Linux kernel为了方便串口驱动的开发,在TTY framework的基础上,封装了一层串口框架(serial framework)。该框架尽可能的屏蔽了TTY有关的技术细节(比较难懂),驱动工程师在编写串口驱动的时候,只需要把精力放在串口以及串口控制器本身即可。

本文将通过对serial framework的简单分析,理解上面的概念,并掌握基于该框架编写串口驱动的方法和步骤。

2. 软件架构

Linux kernel serial framework位于“drivers/tty/serial”目录中,其软件架构(如下面图片1所示)比较简单:

serial_framework_arch

图片1 Serial framework软件架构

Serial core是Serial framework的核心实现,对上封装、屏蔽TTY的技术细节,对下为具体的串口驱动提供简单、统一的编程API。

earlycon(early console)是serial framework中比较新的一个功能,它基于Kernel system console的框架,提供了一种比较简单的控制台实现方式。

最后就是具体的串口驱动(Serial drivers)了,不再详细介绍。

3. serial core

3.1 功能介绍

serial core主要实现如下三类功能(任何一个framework的core模块都提供类似的功能,这就是套路!~):

1)将串口设备有关的物理对象(及其操作方法)封装成一个一个的数据结构,以达到用软件语言描述硬件的目的。

2)向底层driver提供串口驱动的编程接口。

3)基于TTY framework所提供的TTY driver的编写规则,将底层driver看到的serial driver,转换为TTY driver,并将所有的serial操作,转换为对应的tty操作。

本文将重点介绍1)和2)两类功能,第3)类,属于TTY core的内部逻辑,由于比较简单就不多说了。

3.2 关键数据结构

数据结构是一个软件模块的灵魂和骨架,而对设备驱动来说,数据结构一般和具体的硬件实体对应,例如:

假如一个soc中有5个串口控制器(也可称作uart控制器,后面我们不再区分),每个uart控制器都可引出一个串口(uart port)。那么:

每个uart控制器,都是一个platform device,它们可由同一个patform driver驱动;

相对于uart控制器实实在在的存在,我们更为熟悉的串口(uart port),可以看作虚拟的设备,serial core将它们抽象为“struct uart_port”,并在platform driver的probe接口中,注册到kernel;

和platform device类似,这些虚拟的串口设备,也可由同一个虚拟的driver驱动,这就是serial core中的“struct uart_driver”。

下面我们将跟随上面的思路,介绍serial core提供的数据结构(具体可参考“include/linux/serial_core.h”)。

3.2.1 struct uart_port

在serial framework中,struct uart_port抽象虚拟的串口设备(具体的串口控制器,则为实实在在的硬件设备),这是一个庞大的数据结构,存放了五花八门的、各式各样的、有新有旧的、有用没有的和串口设备有关的信息,例如(不能全部罗列,大家在写driver的时候,可以有事没事去看看,说不定就有惊喜):

1)最基本、最必须的,需要驱动工程师根据实际的硬件自行填充的字段

dev,父设备的指针,通常是串口控制器所对应的platform device;

type,该串口的类型,是以PORT_为前缀的一个宏定义,可以根据需要在include/uapi/linux/serial_core.h中定义;

ops,该串口的操作函数集(struct uart_ops类型的指针),具体可参考3.2.3;

iotype,该串口的I/O类型,例如UPIO_MEM32(常用的通过寄存器访问的uart控制器);

mapbase,对应MEM类型的串口,保存它的寄存器基址(物理地址),一般是从DTS中解析得到的

membase,从mapbase ioremap得来(虚拟地址);

irq、irqflags,该串口对应的中断号(以及相应的终端flags), 一般是从DTS中解析得到的;

line,该串口的编号,和字符设备的次设备号等有关。

2)和运行时状态有关的字段

lock,一个自旋锁(spinlock_t类型),用于对该数据结构进行访问保护;

icount,一个struct uart_icount类型变量,用于保存该串口的统计信息,例如收发数据的统计等;

cons,console指针,如果该串口被注册为system console的话,将对应的console指针保存在这里;

state,struct uart_state指针,具体请参考3.2.2。

3)一些有用的函数指针(driver实现,serial core调用),例如

serial_in,读取该串口的某个寄存器;

serial_out,向该串口的某个寄存器写入某一value;

最后,serial core根据这两个函数指针,封装出两个公共的寄存器访问接口:serial_port_in和serial_port_out,以方便driver使用。

注1:struct uart_port中的内容非常多,因此在编写串口驱动的时候,要把握一个原则:不到万不得已的时候,不要新定义变量,多去struct uart_port找找,很有可能就找到了你想要的东西。

3.2.2 struct uart_state

struct uart_state中保存了串口使用过程中的动态信息(它们的生命周期是串口被打开到被关闭的过程),包括:

port,对应的struct tty_port变量(uart port是tty port的一个特例);

pm_state,电源管理有关的状态,例如UART_PM_STATE_ON、UART_PM_STATE_OFF和UART_PM_STATE_UNDEFINED三种;

xmit,用于保存TX数据的环形缓冲区(struct circ_buf,具体可参考include/linux/circ_buf.h);

uart_port,struct uart_port类型的指针,指向所属的串口。

3.2.3 struct uart_ops

使用struct uart_port抽象串口的同时,serial core将串口有关的操作函数封装在struct uart_ops中,底层驱动根据实际硬件情况,填充这些函数,serial core在合适的时候,帮忙调用。

因为在历史上,串口设备是一种非常复杂的设备,因此,和struct uart_port类似,struct uart_ops结构也非常庞大,包罗万象,这里简单介绍一些常用的(其它在用到的时候再关注,或者可参考kernel的帮助文档----Documentation/serial/driver):

startup,打开串口设备的时候,serial core会调用该接口,driver可以在这里进行串口的初始化操作,例如申请中断资源、使能clock、使能接收,等等;

shutdown,startup的反操作,在串口设备被关闭的时候调用;

start_tx,每当有一笔新的数据需要通过串口发送出去的时候,serial core会先把数据保存在TX的buffer中(参考3.2.2的介绍),然后调用start_tx通知driver。driver需要在该接口中,根据当前的状态(TX是否正在进行),决定是否需要发起一次传输;

stop_tx,停止正在进行中的TX;

stop_rx,停止RX;

tx_empty,判断硬件的TX FIFO是否为空,如果是,则返回TIOCSER_TEMT,否则返回0;

.set_mctrl,设置modem的control line,可以留空;

.set_termios,设置串口的termios(例如波特率、数据位、停止位等)。

3.2.4 struct uart_driver

上面介绍的几个数据结构,都在竭力描述串口设备,相应的,按照设备模型的惯例,需要一个和设备对应的、抽象driver数据结构,就是struct uart_driver:

struct uart_driver {
        struct module           *owner;
        const char              *driver_name;
        const char              *dev_name;
        int                      major;
        int                      minor;
        int                      nr;
        struct console          *cons;

        /*
         * these are private; the low level driver should not
         * touch these; they should be initialised to NULL
         */
        struct uart_state       *state;
        struct tty_driver       *tty_driver;
};

该数据结构非常简单,一般情况下,只要关注如下的字段:

driver_name,driver的名称;

dev_name,对应的设备名,例如“ttyS”;

nr,该驱动可以支持的串口的数量(serial core会根据这个值为每一个串口分配一些内部使用资源);

major、minor,主、次设备号,可以不指定(这样的话,TTY core会帮忙动态分配);

cons,如果需要将某一个串口当作system console,可以在driver中定义struct console变量,并将它的指针保存在这里,serial core会在注册串口的时候帮忙将console注册到系统中。

3.3 向具体driver提供的用于编写串口驱动的API

数据结构抽象完毕后,serial core向下层的driver提供了方便的编程API,主要包括:

1)uart driver有关的API

int uart_register_driver(struct uart_driver *uart);
void uart_unregister_driver(struct uart_driver *uart);

uart_register_driver,将定义并填充好的uart driver注册到kernel中,一般在驱动模块的init接口中被调用。

uart_unregister_driver,注销uart driver,在驱动模块的exit接口中被调用。

2)uart port有关的API

int uart_add_one_port(struct uart_driver *reg, struct uart_port *port);
int uart_remove_one_port(struct uart_driver *reg, struct uart_port *port);
int uart_match_port(struct uart_port *port1, struct uart_port *port2);

int uart_suspend_port(struct uart_driver *reg, struct uart_port *port);
int uart_resume_port(struct uart_driver *reg, struct uart_port *port);

static inline int uart_tx_stopped(struct uart_port *port)

extern void uart_insert_char(struct uart_port *port, unsigned int status,
                 unsigned int overrun, unsigned int ch, unsigned int flag);

uart_add_one_port、uart_remove_one_port,添加/删除一个uart port,一般在platform driver的probe/remove中被调用。

uart_suspend_port、uart_resume_port,suspend/resume uart port,在电源管理状态切换的时候被调用。

uart_tx_stopped,判断某一个uart port的tx是否处于停止状态。

uart_insert_char,驱动从串口接收到一个字符之后,可以调用该接口把该字符放到RX buffer中(相比tty_insert_flip_char,可以进行一些overrun的处理)。

3)system console有关的API

struct tty_driver *uart_console_device(struct console *co, int *index);
void uart_console_write(struct uart_port *port, const char *s,
                        unsigned int count,
                        void (*putchar)(struct uart_port *, int));

uart_console_device,通过console指针获取tty driver指针的帮助函数,通常情况下会把该函数赋值给串口console的.device指针,例如:

static struct console xxx_console = {
        …
        .device = uart_console_device,
        …
};

uart_console_write,向串口发送一个字符串的辅助接口,通常在console的.write接口中被调用。

4)其它辅助类的API

#define uart_circ_empty(circ)           ((circ)->head == (circ)->tail)
#define uart_circ_clear(circ)           ((circ)->head = (circ)->tail = 0)

#define uart_circ_chars_pending(circ)   \
        (CIRC_CNT((circ)->head, (circ)->tail, UART_XMIT_SIZE))

#define uart_circ_chars_free(circ)      \
        (CIRC_SPACE((circ)->head, (circ)->tail, UART_XMIT_SIZE))

为了方便driver操作环形缓冲区,serial core定义了一些状态判断的宏,例如是否为空(uart_circ_empty)、是否为初始状态(uart_circ_clear)、缓冲区中有效数据的个数(uart_circ_chars_pending)、缓冲区中空闲空间的个数(uart_circ_chars_free)、等等。

4. earlycon

early console是linux serial framework提供的一个可在kernel启动早期使用的console,最早可以在“start_kernel-->setup_arch-->parse_early_param”之后就可使用,对于kernel早期的debug很有帮助。

根据复杂程度,serial framework提供了两种API,供底层的driver使用。

4.1 静态定义的方式(不需要串口驱动,无法通过device tree动态选择串口)。

这种方法很简单,相关的API包括:

1)EARLYCON_DECLARE(定义一个early console)

#define EARLYCON_DECLARE(_name, fn)     OF_EARLYCON_DECLARE(_name, "", fn)

该函数有两个参数:_name是early console的名称,需要和后续在命令行参数中指定的一致;fn是一个原型为“int     (*setup)(struct earlycon_device *, const char *options);”的回调函数,用于在kernel启动的时候配置改console。

2)setup

driver需要在setup回调中做两件事情:

a)初始化early console有关的硬件,例如ioremap、初始化串口控制器、等等。

b)实现一个console->write[2]接口,并将函数指针保存在setup第一个参数的相应指针中(device->con->write)

3)命令行参数(earlycon=xxx)

完成上面1)和2)之后,一个earlycon driver就完成了,serial framework会在kernel启动的时候,调用我们提供的setup接口,对early console初始化,然后调用system console core提供的register_console接口,帮忙注册console[2]

如果需要使用这个console,可在kernel启动的时候传入命令行参数“earlycon=xxx”,xxx为early console的名称。

4.2 通过device tree动态选择的方式(和串口驱动配合使用,可以通过dts的stdout-path或者linux,stdout-path指定使用哪个串口)。

这种方法稍显麻烦,有违earlycon的初衷,暂不介绍了,感兴趣的同学可以自行研读代码。

5. 基于serial framework编写串口驱动的方法

可参考如下文章:

X-012-KERNEL-serial early console的移植

X-016-KERNEL-串口驱动开发之驱动框架

X-017-KERNEL-串口驱动开发之uart driver框架

X-018-KERNEL-串口驱动开发之serial console

X-019-KERNEL-串口驱动开发之数据收发

6. 参考文档

[1] TTY子系统,http://www.wowotech.net/sort/tty_framework

[2] Linux TTY framework(5)_System console driver

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

基于Hikey的"Boot from USB"调试

$
0
0

1. 前言

话说在半年前,乐美客送给蜗窝几块Hikey(乐美客版)开发板[1],不过由于太忙,就一直把它们放在角落里思考人生,因此甚是愧疚。这几天,闲来无事,翻了下Hikey的资料,觉得挺有意思,就想花点时间让“X Project”在这个板子上跑起来。当然,按照“规矩”,先从“【任务1】启动过程-Boot from USB”做起,记录如下。

2. Hikey介绍

说实话,作为一块开发板,Hikey有着国产平台的通病----资料匮乏,但它的source code却非常完整:

除ROM code之外的所有代码,都能在github上看到源码,这对ARM linux的学习(“X Project”的关注点)来说,是非常有用的。

另外,之所以觉得这块板子很有意思,是因为海思的这颗SOC----Hi6220V100[2],它有如下的处理器架构:

Hi6220V100_SOC_ARCH

图片1 Hi6220V100 SOC Architecture

该SOC包含两个处理器子系统:

MCU subsystem,里面有一个Cortex-M3的核,用于充电管理、电源管理等,也包括Boot code的USB download协议(猜测);

ACPU(这个缩写也太逗了,哈哈) subsystem,采用CCI-400互联总线(CoreLink)协议,支持8个Cortex-A53核(2个cluster[3],每个cluster包含4个完全一样 A53核)。

基于上述的架构,Hikey USB boot的流程如下:

Hi6220V100_USB_BOOT

图片2 Hi6220V100 USB BOOT

注1:网上没有官方的spec介绍USB boot流程,上面的流程是我根据github上的开源代码,推测出来的,具体可参考:

1)l-loader,https://github.com/96boards/l-loader.git,其中的start.S即为上面图片中的“l-loader”,它是一段运行于Cortex-M3的汇编代码。另外由链接脚本(l-loader.lds)可知,它的执行位置0xf9800800。

2)ARM Trusted Firmware BL1,https://github.com/96boards/arm-trusted-firmware/tree/hikey/bl1,l-loader正确初始化Cortex-A53之后,会将A53的控制权交给ARM Trusted Firmware,首先运行的是BL1,对Hikey来说,BL1的运行地址为0xf9801000(参考BL1_RO_BASE的定义)。

3)https://github.com/96boards/l-loader.git中有一个脚本(gen_loader.py),可以将l-loader和BL1打包生成一个二进制文件,然后通过hisi-idt.py脚本,将这个二进制文件下载到0xf9800800处执行。

注2:Hikey的官方Image并没有使用常规的bootloader(u-boot等),而是用ARM Trusted Firmware代替了bootloader的功能,因此它的boot过程为:l-loader---->ARM Trusted Firmware(BL1, BL2, BL3等)---->Linux kernel。另外,ARM Trusted Firmware也集成了Android的fast boot功能,用起来还是很方便的。感兴趣的同学可以研究一下相关的source code。

3. 移植u-boot SPL到Hikey中

本章将根据第2章罗列的信息,基于“X Project”有关的文档,尝试将u-boot的SPL跑在Hikey开发板上,并点亮一盏灯,步骤如下。

3.1 修改“X Project”的编译脚本,加入对Hikey的支持

不再详细描述修改步骤,具体可参考下面的patch:

[xprj, hikey] support hikey.

3.2 修改“X Project”的u-boot,增加对Hikey SPL的支持

mainline的u-boot已经支持了Hikey,但没有使能SPL功能,这刚好给我们大显身手的机会,步骤如下:

1)修改“arch/arm/Kconfig”,找到config TARGET_HIKEY配置项,增加select SUPPORT_SPL和select SPL,以支持SPL功能

config TARGET_HIKEY
bool "Support HiKey 96boards Consumer Edition Platform"
select ARM64
+ select SUPPORT_SPL
+ select SPL

select DM
select DM_GPIO
select DM_SERIAL

2)修改完成后,在build目录执行make uboot-config,打开配置界面后,直接保存退出,从新生成config文件,具体可参考:

https://github.com/wowotechX/u-boot/blob/1775dca1f14df5b3ba0884c54b665f91fff24933/configs/hikey_defconfig

3)修改include/configs/hikey.h文件,增加SPL有关的TEXT配置,如下(黄色部分比较重要):

#define CONFIG_SPL_TEXT_BASE 0xf9801000
#define CONFIG_SPL_MAX_SIZE (1024 * 20)

#define CONFIG_SPL_BSS_START_ADDR (CONFIG_SPL_TEXT_BASE + \
                                                                 CONFIG_SPL_MAX_SIZE)
#define CONFIG_SPL_BSS_MAX_SIZE (1024 * 12)
#define CONFIG_SPL_STACK (CONFIG_SPL_BSS_START_ADDR + \
                                              CONFIG_SPL_BSS_MAX_SIZE + \
                                              1024 * 12)

4)修改board/hisilicon/hikey/hikey.c,加入两个和SPL有关的函数实现,board_init_f和panic,并在board_init_f中点亮一盏LED灯,代码如下:

+#ifdef CONFIG_SPL_BUILD
+void board_init_f(ulong bootflag)
+{
+     /* GPIO4_2(User LED3), 0xF7020000, GPIODIR(0x400), GPIODAT2(0x10) */
+     writel(readl(0xF7020400) | (1 << 2), 0xF7020400);
+     writel(readl(0xF7020010) | 0xFF, 0xF7020010);
+     while (1);
+}
+
+void panic(const char *fmt, ...)
+{
+}
+#endif

其中LED有关的配置可参考Hikey(乐美客版)的原理图[7]以及Hi6220V100的spec[2]

修改完毕后,编译生成u-boot-spl.bin,留作后用。

3.3 单独编译出l-loader

https://github.com/96boards/l-loader.git中将l-loader单独编译出,并保存在“X Project”的tools/hisilicon目录中,留作后用,如下:

https://github.com/wowotechX/tools/blob/hikey/hisilicon/img_loader.bin

注3:l-loader的编译方法,这里不再详细介绍(无非就是下载一个gcc-arm-linux-gnueabihf交叉编译器,稍微修改一下l-loader.git的Makefile文件)。

3.4 将gen_loader.pyhisi-idt.py保存到“X Project”的tools/hisilicon目录中

如下:

https://github.com/wowotechX/tools/blob/hikey/hisilicon/gen_loader.py

https://github.com/wowotechX/tools/blob/hikey/hisilicon/hisi-idt.py

3.5 修改“X Project”的编译脚本,增加Hikey u-boot-spl的运行命令

如下:

img-loader=$(TOOLS_DIR)/$(BOARD_VENDOR)/img_loader.bin
uboot-spl-bin=$(UBOOT_OUT_DIR)/spl/u-boot-spl.bin
gen-loader=$(TOOLS_DIR)/$(BOARD_VENDOR)/gen_loader.py
hisi-idt=$(TOOLS_DIR)/$(BOARD_VENDOR)/hisi-idt.py

spl-run:
    # generate SPL image
    sudo python $(gen-loader) -o spl.img --img_loader=$(img-loader) --img_bl1=$(uboot-spl-bin)
    sudo python $(hisi-idt) --img1=spl.img -d /dev/ttyUSB0

    rm -f spl.img

主要思路为:

1)使用gen_loader.py脚本将img_loader.bin和u-boot-spl.bin打包在一起,生成spl.img文件。

2)使用hisi-idt.py将spl.img下载到板子中运行。

3.6 运行并测试

按照[1]中的步骤,为Hikey供电,并让它进入USB boot模式,然后在“X Project”的build目录中执行make spl-run即可。观察Hikey的LED灯,应该有一个亮了,说明移植成功。

4. 参考文档

[1] http://wiki.lemaker.org/HiKey(LeMaker_version):Quick_Start/zh-hans

[2] Hi6220V100

[3] Linux CPU core的电源管理(2)_cpu topology

[4] Hikey l-loader,https://github.com/96boards/l-loader.git

[5] Hikey USB download脚本,http://builds.96boards.org/releases/reference-platform/aosp/hikey/15.10/bootloader/hisi-idt.py

[6] ARM Trusted Firmware, https://github.com/96boards/arm-trusted-firmware.git

[7] Hikey(乐美客版)原理图,http://mirror.lemaker.org/LeMaker%20Hikey%20Schematic.pdf

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net


MMC/SD/SDIO介绍

$
0
0

1. 前言

熟悉Linux kernel的人都知道,kernel使用MMC subsystem统一管理MMC、SD、SDIO等设备,为什么呢?到底什么是MMC?SD和SDIO又是什么?为什么可以用MMC统称呢?

在分析Linux kernel的MMC subsystem之前,有必要先介绍一些概念,以便对MMC/SD/SDIO有一个大致的了解,这就是本文的目的。

2. 基本概念

MMC是MultiMediaCard的简称,从本质上看,它是一种用于固态非易失性存储的内存卡(memory card)规范[1],定义了诸如卡的形态、尺寸、容量、电气信号、和主机之间的通信协议等方方面面的内容。

从1997年MMC规范发布至今,基于不同的考量(物理尺寸、电压范围、管脚数量、最大容量、数据位宽、clock频率、安全特性、是否支持SPI mode、是否支持DDR mode、等等),进化出了MMC、SD、microSD、SDIO、eMMC等不同的规范(如下面图片1所示)。虽然乱花迷人,其本质终究还是一样的,丝毫未变,这就是Linux kernel将它们统称为MMC的原因。

mmc_sd_sdio_history

图片1 MMC/SD/SDIO evolution

关于该图片,这里强调几点(其它的,大家可参考[1][2],不再详细介绍):

MMC、SD、SDIO的技术本质是一样的(使用相同的总线规范,等等),都是从MMC规范演化而来;

MMC强调的是多媒体存储(MM,MultiMedia);

SD强调的是安全和数据保护(S,Secure);

SDIO是从SD演化出来的,强调的是接口(IO,Input/Output),不再关注另一端的具体形态(可以是WIFI设备、Bluetooth设备、GPS等等)。

3. 规范简介

MMC分别从卡(Card Concept)、总线(Bus Concept)以及控制器(Host Controller)三个方面,定义MMC system的行为,如下面图片2所示:

mmc_sd_sdio_hw_block

图片2 mmc_sd_sdio_hw_block

不同岗位的工程师,可以根据自己的工作性质,重点理解某一部分的规范,下面从嵌入式软件工程师的视角,简单的介绍一下。

3.1 卡的规范

卡的规范主要规定卡的形状、物理尺寸、管脚,内部block组成、寄存器等等,以eMMC为例[3]

Card Concept(eMMC)

图片3 Card Concept(eMMC)

1)有关形状、尺寸的内容,这里不再介绍,感兴趣的同学可参考[1]。

2)卡的内部由如下几个block组成:

Memory core,存储介质,一般是NAND flash、NOR flash等;

Memory core interface,管理存储介质的接口,用于访问(读、写、擦出等操作)存储介质;

Card interface(CMD、CLK、DATA),总线接口,外界访问卡内部存储介质的接口,和具体的管脚相连;

Card interface controller,将总线接口上的协议转换为Memory core interface的形式,用于访问内部存储介质;

Power模块,提供reset、上电检测等功能;

寄存器(图片1中位于Card interface controller的左侧,那些小矩形),用于提供卡的信息、参数、访问控制等功能。

3)卡的管脚有VDD、GND、RST、CLK、CMD和DATA等,VDD和GND提供power,RST用于复位,CLK、CMD和DATA为MMC总线协议(具体可参考3.2小节)的物理通道:

CLK有一条,提供同步时钟,可以在CLK的上升沿(或者下降沿,或者上升沿和下降沿)采集数据;

CMD有一条,用于传输双向的命令。

DATA用于传说双向的数据,根据MMC的类型,可以有一条(1-bit)、四条(4-bit)或者八条(8-bit)。

4)以eMMC为例,规范定义了OCR, CID, CSD, EXT_CSD, RCA 以及DSR 6组寄存器,具体含义后面再介绍。

3.2 总线规范

前面我们提到过,MMC的本质是提供一套可以访问固态非易失性存储介质的通信协议,从产业化的角度看,这些存储介质一般集成在一个独立的外部模块中(卡、WIFI模组等),通过物理总线和CPU连接。对任何有线的通信协议来说,总线规范都是非常重要的。关于MMC总线规范,简单总结如下:

1)物理信号有CLK、CMD和DATA三类。

2)电压范围为1.65V和3.6V(参考上面图片2),根据工作电压的不同,MMC卡可以分为两类:

High Voltage MultiMediaCard,工作电压为2.7V~3.6V。

Dual Voltage MultiMediaCard,工作电压有两种,1.70V~1.95V和2.7V~3.6V,CPU可以根据需要切换。

3)数据传输的位宽(称作data bus width mode)是允许动态配置的,包括1-bit (默认)模式、4-bit模式和8-bit模式。

注1:不使用的数据线,需要保持上拉状态,这就是图片2中的DATA中标出上拉的原因。另外,由于数据线宽度是动态可配的,这要求CPU可以动态的enable/disable数据线的那些上拉电阻。

4)MMC规范定义了CLK的频率范围,包括0-20MHz、0-26MHz、0-52MHz等几种,结合数据线宽度,基本决定了MMC的访问速度。

5)总线规范定义了一种简单的、主从式的总线协议,MMC卡位从机(slave),CPU为主机(Host)。

6)协议规定了三种可以在总线上传输的信标(token):

Command,Host通过CMD线发送给Slave的,用于启动(或结束)一个操作(后面介绍);

Response,Slave通过CMD线发送给Host,用于回应Host发送的Command;

Data,Host和Slave之间通过数据线传说的数据。方向可以是Host到Slave,也可以是Slave到Host。数据线的个数可以是1、4或者8。在每一个时钟周期,每根数据线上可以传输1bit或者2bits的数据。

7)一次数据传输过程,需要涉及所有的3个信标。一次数据传输的过程也称作Bus Operation,根据场景的不同,MMC协议规定了很多类型的Bus Operation(具体可参考相应的规范)。

3.3 控制器规范

Host控制器是MMC总线规范在Host端的实现,也是Linux驱动工程师比较关注的地方,后面将会结合Linux MMC framework的有关内容,再详细介绍。

4. 总结

本文对MMC/SD/SDIO等做了一个简单的介绍,有了这些基本概念之后,在Linux kernel中编写MMC驱动将不再是一个困难的事情(因为MMC是一个协议,所有有关协议的事情,都很简单,因为协议是固定的),我们只需要如下步骤即可完成:

1)结合MMC的规范,阅读Host MMC controller的spec,理解有关的功能和操作方法。

2)根据Linux MMC framework的框架,将MMC bus有关的操作方法通过MMC controller实现。

具体可参考后续MMC framework的分析文档。

5. 参考文档

[1] https://en.wikipedia.org/wiki/MultiMediaCard

[2] https://en.wikipedia.org/wiki/Secure_Digital

[3] eMM spec(注册后可免费下载),http://www.jedec.org/standards-documents/results/jesd84-b51

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-022-OTHERS-git操作记录之合并远端分支的更新

$
0
0

1. 前言

本文将以“X Project”的开发过程为例,介绍“合并远端分支的更新”的方法。事情的起因如下:

X Project”是一个学习嵌入式Linux开发全过程的小项目,项目开始的时候,u-boot、linux kernel等代码,都是直接从官方仓库的当前状态获取的(具体可参考[2])。以u-boot为例,“X Project”的u-boot[3]是2016年4月23日从u-boot的官方仓库[1]拷贝而来的。

随着时间的推移,官方仓库可能有很多更新,例如修复bug、添加新功能等,在合适的时间点,需要将这些更新合并。下面就以“X Project”的u-boot为例,介绍合并的步骤。

2. 操作原理

我们知道,git是一种分布式的版本管理工具。所谓的分布式,是指代码仓库可以保存在多个地方,例如保存在本地计算机中的,称作本地仓库(local),保存在服务器上的,称作远端仓库(remote)。我们使用git clone命令从远端下载仓库到本地后,git会将该远端仓库在本地保存为名称为origin的引用,以“X Project”的u-boot为例,如下:

pengo@ubuntu:~/work/xprj/u-boot$ git remote show
origin

当然,我们可以使用git remote add命令,将多个远端仓库添加到本地的索引中,同样以u-boot为例,我们可以将u-boot的官方仓库[1]添加进来(命令的具体用法,请参考git的帮助文档):

pengo@ubuntu:~/work/xprj/u-boot$ git remote add denx http://git.denx.de/u-boot.git

pengo@ubuntu:~/work/xprj/u-boot$ git remote show
denx
origin

其实,“X Project”新建u-boot的仓库时,就是先从denx仓库clone到origin仓库,然后下载到本地仓库中,如下图所示:

git_remote_merge

本文所需要做的,就是将denx仓库中master分支的改动,更新(合并)到origin仓库的master和x_intergration分支中。具体步骤请参考下面章节。

3. 操作步骤

3.1 添加denx远端仓库

1)在本地查看当前默认的远端仓库(origin)及其分支

pengo@ubuntu:~/work/xprj/u-boot$ git remote show
origin

pengo@ubuntu:~/work/xprj/u-boot$ git branch -r
  origin/HEAD -> origin/x_integration
  origin/hikey
  origin/master
  origin/next
  origin/origin
  origin/tiny210
  origin/u-boot-2009.11.y
  origin/u-boot-2013.01.y
  origin/x_integration

2)添加denx远端仓库并查看其信息

pengo@ubuntu:~/work/xprj/u-boot$ git remote add denx http://git.denx.de/u-boot.git

pengo@ubuntu:~/work/xprj/u-boot$ git remote show     
denx
origin

pengo@ubuntu:~/work/xprj/u-boot$ git remote show denx

* remote denx
  Fetch URL: http://git.denx.de/u-boot.git
  Push  URL: http://git.denx.de/u-boot.git
  HEAD branch: master
  Remote branches:
    master           new (next fetch will store in remotes/denx)
    next             new (next fetch will store in remotes/denx)
    origin           new (next fetch will store in remotes/denx)
    u-boot-2009.11.y new (next fetch will store in remotes/denx)
    u-boot-2013.01.y new (next fetch will store in remotes/denx)
u-boot-2016.09.y new (next fetch will store in remotes/denx)

3)获取远端仓库的分支信息

pengo@ubuntu:~/work/xprj/u-boot$ git fetch denx
From http://git.denx.de/u-boot
* [new branch]      master     -> denx/master
* [new branch]      next       -> denx/next
* [new branch]      origin     -> denx/origin
* [new branch]      u-boot-2009.11.y -> denx/u-boot-2009.11.y
* [new branch]      u-boot-2013.01.y -> denx/u-boot-2013.01.y
* [new branch]      u-boot-2016.09.y -> denx/u-boot-2016.09.y
* [new tag]         v2016.05   -> v2016.05
* [new tag]         v2016.05-rc3 -> v2016.05-rc3
* [new tag]         v2016.07   -> v2016.07
* [new tag]         v2016.07-rc1 -> v2016.07-rc1
* [new tag]         v2016.07-rc2 -> v2016.07-rc2
* [new tag]         v2016.07-rc3 -> v2016.07-rc3
* [new tag]         v2016.09   -> v2016.09
* [new tag]         v2016.09-rc1 -> v2016.09-rc1
* [new tag]         v2016.09-rc2 -> v2016.09-rc2
* [new tag]         v2016.09.01 -> v2016.09.01
* [new tag]         v2016.11   -> v2016.11
* [new tag]         v2016.11-rc1 -> v2016.11-rc1
* [new tag]         v2016.11-rc2 -> v2016.11-rc2
* [new tag]         v2016.11-rc3 -> v2016.11-rc3
* [new tag]         v2017.01-rc1 -> v2017.01-rc1
* [new tag]         v2017.01-rc2 -> v2017.01-rc2

4)查看获取后的分支信息(已经出现新添加的远端仓库的信息)

pengo@ubuntu:~/work/xprj/u-boot$ git branch -r
  denx/master
  denx/next
  denx/origin
  denx/u-boot-2009.11.y
  denx/u-boot-2013.01.y
  denx/u-boot-2016.09.y

  origin/HEAD -> origin/x_integration
  origin/hikey
  origin/master
  origin/next
  origin/origin
  origin/tiny210
  origin/u-boot-2009.11.y
  origin/u-boot-2013.01.y
  origin/x_integration

3.2 将denx/master合并到origin/master

1)基于origin/master新建一个本地分支xdev_merge,并checkout到该分支

pengo@ubuntu:~/work/xprj/u-boot$ git checkout origin/master -b xdev_merge
Branch xdev_merge set up to track remote branch master from origin.
Switched to a new branch 'xdev_merge'

2)将denx/master合并到本地分支上

pengo@ubuntu:~/work/xprj/u-boot$ git merge denx/master

注1:由于origin/master完全是denx/master的一个快照,没有任何改动,因此merge不会产生任何冲突。

3)查看合并后的日志(合并成功)

pengo@ubuntu:~/work/xprj/u-boot$ git log
commit 3d3a74cc8c774345be7d1661b215555ad41f4515
Author: Masahiro Yamada <yamada.masahiro@socionext.com>
Date:   Wed Dec 7 22:10:30 2016 +0900
    mmc: move MMC_SDHCI_IO_ACCESSORS to Kconfig
..

4)将合并后的本地分支提交到origin/master,工作完成

pengo@ubuntu:~/work/xprj/u-boot$ git push origin xdev_merge:master
Password:
Counting objects: 56980, done.
Compressing objects: 100% (8730/8730), done.
Writing objects: 100% (52590/52590), 9.76 MiB | 224 KiB/s, done.
Total 52590 (delta 44572), reused 51261 (delta 43413)
remote: Resolving deltas: 100% (44572/44572), completed with 2815 local objects.
To https://pengoor@github.com/wowotechX/u-boot.git
   ee8b25f..3d3a74c  xdev_merge -> master

3.3 将denx/master合并到“X Project”的开发分支(origin/x_integration)

1)基于origin/x_integration新建一个本地分支xdev_merge,并checkout到该分支(步骤略)。

2)将denx/master合并到本地分支上(步骤略)。

3)合并的过程中,会有冲突,如下:

pengo@ubuntu:~/work/xprj/u-boot$ git status
# On branch xdev
# Changes to be committed:
#
#       modified:   .mailmap
#       modified:   .travis.yml
#       modified:   Kconfig

# Unmerged paths:
#   (use "git add/rm ..." as appropriate to mark resolution)
#
#       both modified:      arch/arm/dts/Makefile
#       both modified:      drivers/gpio/s5p_gpio.c
#       both modified:      drivers/pinctrl/Makefile
#       both modified:      drivers/serial/Kconfig
#       both modified:      drivers/serial/Makefile
#

需要解决冲突,具体的解决过程(就是修改指定的冲突文件)不再详细说明,需要注意的时,冲突解决后,需要使用git add告诉git工具冲突已解决,例如:

pengo@ubuntu:~/work/xprj/u-boot$ git add arch/arm/dts/Makefile

最后,全家解决完毕后,调用git commit提交这次merge的内容:

pengo@ubuntu:~/work/xprj/u-boot$ git commit

直接提交,日志也不用改。。。。

4)最后,尝试编译,并解决编译错误(过程略,具体可参考下面的patch)

https://github.com/wowotechX/u-boot/commit/7401205742ac88d1e4bc25a216c88d03d7fa9315

5)下载到板子上,验证ok后,将本地分支提交到origin/x_integration即可

pengo@ubuntu:~/work/xprj/u-boot$ git push origin xdev_merge:x_integration
Password:
Counting objects: 89, done.
Compressing objects: 100% (34/34), done.

4. 参考文档

[1] http://git.denx.de/u-boot.git

[2] X-000-PRE-开发环境搭建

[3] https://github.com/wowotechX/u-boot

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

eMMC 原理 1 :Flash Memory 简介

$
0
0

eMMC 是 Flash Memory 的一类,在详细介绍 eMMC 之前,先简单介绍一下 Flash Memory。

Flash Memory 是一种非易失性的存储器。在嵌入式系统中通常用于存放系统、应用和数据等。在 PC 系统中,则主要用在固态硬盘以及主板 BIOS 中。另外,绝大部分的 U 盘、SDCard 等移动存储设备也都是使用 Flash Memory 作为存储介质。

+

1. Flash Memory 的主要特性

与传统的硬盘存储器相比,Flash Memory 具有质量轻、能耗低、体积小、抗震能力强等的优点,但也有不少局限性,主要如下:

  1. 需要先擦除再写入
    Flash Memory 写入数据时有一定的限制。它只能将当前为 1 的比特改写为 0,而无法将已经为 0 的比特改写为 1,只有在擦除的操作中,才能把整块的比特改写为 1。

  2. 块擦除次数有限
    Flash Memory 的每个数据块都有擦除次数的限制(十万到百万次不等),擦写超过一定次数后,该数据块将无法可靠存储数据,成为坏块。
    为了最大化的延长 Flash Memory 的寿命,在软件上需要做擦写均衡(Wear Leveling),通过分散写入、动态映射等手段均衡使用各个数据块。同时,软件还需要进行坏块管理(Bad Block Management,BBM),标识坏块,不让坏块参与数据存储。(注:除了擦写导致的坏块外,Flash Memory 在生产过程也会产生坏块,即固有坏块。)

  3. 读写干扰
    由于硬件实现上的物理特性,Flash Memory 在进行读写操作时,有可能会导致邻近的其他比特发生位翻转,导致数据异常。这种异常可以通过重新擦除来恢复。Flash Memory 应用中通常会使用 ECC 等算法进行错误检测和数据修正。

  4. 电荷泄漏
    存储在 Flash Memory 存储单元的电荷,如果长期没有使用,会发生电荷泄漏,导致数据错误。不过这个时间比较长,一般十年左右。此种异常是非永久性的,重新擦除可以恢复。

2. NOR Flash 和 NAND Flash

根据硬件上存储原理的不同,Flash Memory 主要可以分为 NOR Flash 和 NAND Flash 两类。 主要的差异如下所示:

  • NAND Flash 读取速度与 NOR Flash 相近,根据接口的不同有所差异;
  • NAND Flash 的写入速度比 NOR Flash 快很多;
  • NAND Flash 的擦除速度比 NOR Flash 快很多;
  • NAND Flash 最大擦次数比 NOR Flash 多;
  • NOR Flash 支持片上执行,可以在上面直接运行代码;
  • NOR Flash 软件驱动比 NAND Flash 简单;
  • NOR Flash 可以随机按字节读取数据,NAND Flash 需要按块进行读取。
  • 大容量下 NAND Flash 比 NOR Flash 成本要低很多,体积也更小;

(注:NOR Flash 和 NAND Flash 的擦除都是按块块进行的,执行一个擦除或者写入操作时,NOR Flash 大约需要 5s,而 NAND Flash 通常不超过 4ms。)

2.1 NOR Flash

NOR Flash 根据与 CPU 端接口的不同,可以分为 Parallel NOR Flash 和 Serial NOR Flash 两类。
Parallel NOR Flash 可以接入到 Host 的 SRAM/DRAM Controller 上,所存储的内容可以直接映射到 CPU 地址空间,不需要拷贝到 RAM 中即可被 CPU 访问,因而支持片上执行。Serial NOR Flash 的成本比 Parallel NOR Flash 低,主要通过 SPI 接口与 Host 连接。


图片: Parallel NOR Flash 与 Serial NOR Flash


鉴于 NOR Flash 擦写速度慢,成本高等特性,NOR Flash 主要应用于小容量、内容更新少的场景,例如 PC 主板 BIOS、路由器系统存储等。

更多 NOR Flash 的相关细节,请参考 NOR Flash 章节。

2.2 NAND Flash

NAND Flash 需要通过专门的 NFI(NAND Flash Interface)与 Host 端进行通信,如下图所示:


图片:NAND Flash Interface


NAND Flash 根据每个存储单元内存储比特个数的不同,可以分为 SLC(Single-Level Cell)、MLC(Multi-Level Cell) 和 TLC(Triple-Level Cell) 三类。其中,在一个存储单元中,SLC 可以存储 1 个比特,MLC 可以存储 2 个比特,TLC 则可以存储 3 个比特。

NAND Flash 的一个存储单元内部,是通过不同的电压等级,来表示其所存储的信息的。在 SLC 中,存储单元的电压被分为两个等级,分别表示 0 和 1 两个状态,即 1 个比特。在 MLC 中,存储单元的电压则被分为 4 个等级,分别表示 00 01 10 11 四个状态,即 2 个比特位。同理,在 TLC 中,存储单元的电压被分为 8 个等级,存储 3 个比特信息。


图片: SLC、MLC 与 TLC


NAND Flash 的单个存储单元存储的比特位越多,读写性能会越差,寿命也越短,但是成本会更低。Table 1 中,给出了特定工艺和技术水平下的成本和寿命数据。


Table 1

SLC MLC TLC
制造成本 30-35 美元 / 32GB 17 美元 / 32GB 9-12 美元 / 32GB
擦写次数 10万次或更高 1万次或更高 5000次甚至更高
存储单元 1 bit / cell 2 bits / cell 3 bits / cell

(注:以上数据来源于互联网,仅供参考)


相比于 NOR Flash,NAND Flash 写入性能好,大容量下成本低。目前,绝大部分手机和平板等移动设备中所使用的 eMMC 内部的 Flash Memory 都属于 NAND Flash。PC 中的固态硬盘中也是使用 NAND Flash。

更多 NAND Flash 的相关细节,请参考 NAND Flash 章节。

3. Raw Flash 和 Managed Flash

由于 Flash Memory 存在按块擦写、擦写次数的限制、读写干扰、电荷泄露等的局限,为了最大程度的发挥 Flash Memory 的价值,通常需要有一个特殊的软件层次,实现坏块管理、擦写均衡、ECC、垃圾回收等的功能,这一个软件层次称为 FTL(Flash Translation Layer)。

在具体实现中,根据 FTL 所在的位置的不同,可以把 Flash Memory 分为 Raw Flash 和 Managed Flash 两类。


图片: Raw Flash 和 Managed Flash


Raw Flash
在此类应用中,在 Host 端通常有专门的 FTL 或者 Flash 文件系统来实现坏块管理、擦写均衡等的功能。Host 端的软件复杂度较高,但是整体方案的成本较低,常用于价格敏感的嵌入式产品中。
通常我们所说的 NOR Flash 和 NAND Flash 都属于这类型。

Managed Flash
Managed Flash 在其内部集成了 Flash Controller,用于完成擦写均衡、坏块管理、ECC校验等功能。相比于直接将 Flash 接入到 Host 端,Managed Flash 屏蔽了 Flash 的物理特性,对 Host 提供标准化的接口,可以减少 Host 端软件的复杂度,让 Host 端专注于上层业务,省去对 Flash 进行特殊的处理。
eMMCSD CardUFS、U 盘等产品是属于 Managed Flash 这一类。

4. 参考资料

  1. NOR NAND Flash Guide: Selecting a Flash Storage Solution [PDF]
  2. Wiki: Common Flash Memory Interface [Web]
  3. Quick Guide to Common Flash Interface [PDF]
  4. MICRON NOR Flash Technology [Web]
  5. MICRON NAND Flash Technology [Web]
  6. Wiki:闪存 [Web]
  7. Wiki:Flash File System [Web]
  8. Wear Leveling in Micron® NAND Flash Memory [PDF]
  9. Understanding Flash: The Flash Translation Layer [Web]
  10. 谈NAND Flash的底层结构和解析 [Web]
  11. 闪存基础 [Web]
  12. Open NAND Flash Interface (ONFI) [Web]

Linux MMC framework(1)_软件架构

$
0
0

1. 前言

由[1]中MMC、SD、SDIO的介绍可知,这三种技术都是起源于MMC技术,有很多共性,因此Linux kernel统一使用MMC framework管理所有和这三种技术有关的设备。

本文将基于[1]对MMC技术的介绍,学习Linux kernel MMC framework的软件架构。

2. 软件架构

Linux kernel的驱动框架有两个要点(尽管本站前面的文章已经多次强调,本文还是要再说明一下,因为这样的设计思想,说一千遍都不会烦):

1)抽象硬件(硬件架构是什么样子,驱动框架就应该是什么样子)。

2)向“客户”提供使用该硬件的API(之前我们提到最多的客户是“用户空间的Application”,不过也有其它“客户”,例如内核空间的其它driver、其它framework)。

以本文的描述对象为例,MMC framework的软件架构如下面“图片1”所示:

mmc_architecture

图片1 Linux MMC framework软件架构

MMC framework分别有“从左到右”和“从下到上”两种层次结构。

1) 从左到右

MMC协议是一个总线协议,因此包括Host controller、Bus、Card三类实体(从左到右)。相应的,MMC framework抽象出了host、bus、card三个软件实体,以便和硬件一一对应:

host,负责驱动Host controller,提供诸如访问card的寄存器、检测card的插拔、读写card等操作方法。从设备模型的角度看,host会检测卡的插入,并向bus注册MMC card设备;

bus,是MMC bus的虚拟抽象,以标准设备模型的方式,收纳MMC card(device)以及对应的MMC driver(driver);

card,抽象具体的MMC卡,由对应的MMC driver驱动(从这个角度看,可以忽略MMC的技术细节,只需关心一个个具有特定功能的卡设备,如存储卡、WIFI卡、GPS卡等等)。

2)从下到上

MMC framework从下到上也有3个层次(老生常谈了):

MMC core位于中间,是MMC framework的核心实现,负责抽象host、bus、card等软件实体,负责向底层提供统一、便利的编写Host controller driver的API;

MMC host controller driver位于底层,基于MMC core提供的框架,驱动具体的硬件(MMC controller);

MMC card driver位于最上面,负责驱动MMC core抽象出来的虚拟的card设备,并对接内核其它的framework(例如块设备、TTY、wireless等),实现具体的功能。

3. 工作流程

基于图片1中的软件架构,Linux MMC framework的工作流程如下:

mmc_opt_flow

图片2 MMC操作流程

暂时不进行详细介绍,感兴趣的同学可以照着代码先看看。后续其它文章会逐一展开。

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

eMMC 原理 2 :eMMC 简介

$
0
0

eMMC 是 embedded MultiMediaCard 的简称。MultiMediaCard,即 MMC, 是一种闪存卡(Flash Memory Card)标准,它定义了 MMC 的架构以及访问 Flash Memory 的接口和协议。而 eMMC 则是对 MMC 的一个拓展,以满足更高标准的性能、成本、体积、稳定、易用等的需求。

eMMC 的整体架构如下图片所示:

图片: eMMC 整体架构


eMMC 内部主要可以分为 Flash Memory、Flash Controller 以及 Host Interface 三大部分。

1. Flash Memory

Flash Memory 是一种非易失性的存储器,通常在嵌入式系统中用于存放系统、应用和数据等,类似与 PC 系统中的硬盘。

目前,绝大部分手机和平板等移动设备中所使用的 eMMC 内部的 Flash Memory 都属于 NAND Flash,关于 NAND Flash 的更多细节可以参考 Flash Memory 章节。

eMMC 在内部对 Flash Memory 划分了几个主要区域,如下图所示:

图片:eMMC 内部分区


  1. BOOT Area Partition 1 & 2
    此分区主要是为了支持从 eMMC 启动系统而设计的。
    该分区的数据,在 eMMC 上电后,可以通过很简单的协议就可以读取出来。同时,大部分的 SOC 都可以通过 GPIO 或者 FUSE 的配置,让 ROM 代码在上电后,将 eMMC BOOT 分区的内容加载到 SOC 内部的 SRAM 中执行。

  2. RPMB Partition
    RPMB 是 Replay Protected Memory Block 的简称,它通过 HMAC SHA-256 和 Write Counter 来保证保存在 RPMB 内部的数据不被非法篡改。
    在实际应用中,RPMB 分区通常用来保存安全相关的数据,例如指纹数据、安全支付相关的密钥等。

  3. General Purpose Partition 1~4
    此区域则主要用于存储系统或者用户数据。 General Purpose Partition 在芯片出厂时,通常是不存在的,需要主动进行配置后,才会存在。

  4. User Data Area
    此区域则主要用于存储系统和用户数据。
    User Data Area 通常会进行再分区,例如 Android 系统中,通常在此区域分出 boot、system、userdata 等分区。

更多 eMMC 分区相关的细节,请参考 eMMC 分区管理 章节。

2. Flash Controller

NAND Flash 直接接入 Host 时,Host 端通常需要有 NAND Flash Translation Layer,即 NFTL 或者 NAND Flash 文件系统来做坏块管理、ECC等的功能。

eMMC 则在其内部集成了 Flash Controller,用于完成擦写均衡、坏块管理、ECC校验等功能。相比于直接将 NAND Flash 接入到 Host 端,eMMC 屏蔽了 NAND Flash 的物理特性,可以减少 Host 端软件的复杂度,让 Host 端专注于上层业务,省去对 NAND Flash 进行特殊的处理。同时,eMMC 通过使用 Cache、Memory Array 等技术,在读写性能上也比 NAND Flash 要好很多。

图片:NAND Flash 与 eMMC

3. Host Interface

eMMC 与 Host 之间的连接如下图所示:

图片:eMMC Interface


各个信号的用途如下所示:

CLK
用于同步的时钟信号

Data Strobe
此信号是从 Device 端输出的时钟信号,频率和 CLK 信号相同,用于同步从 Device 端输出的数据。该信号在 eMMC 5.0 中引入。

CMD
此信号用于发送 Host 的 command 和 Device 的 response。

DAT0-7
用于传输数据的 8 bit 总线。

Host 与 eMMC 之间的通信都是 Host 以一个 Command 开始发起的。针对不同的 Command,Device 会做出不同的响应。详细的通信协议相关内容,请参考 eMMC 总线协议 章节。

4. 参考资料

  1. Embedded Multi-Media Card (e•MMC) Electrical Standard (5.1) [PDF]

进程切换(context_switch)代码分析:基本逻辑

$
0
0

一、前言

本文主要是以context_switch为起点,分析了整个进程切换过程中的基本操作和基本的代码框架,很多细节,例如tlb的操作,cache的操作,锁的操作等等会在其他专门的文档中描述。进程切换包括体系结构相关的代码和系统结构无关的代码。第二、三、四分别描述了context_switch的代码脉络,后面的章节是以ARM64为例子,讲述了具体进程地址空间的切换过程和硬件上下文的切换过程。

 

二、context_switch代码分析

在kernel/sched/core.c中有一个context_switch函数,该函数用来完成具体的进程切换,代码如下(本文主要描述进程切换的基本逻辑,因此部分代码会有删节):

static inline struct rq * context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)------------------(1)
{
    struct mm_struct *mm, *oldmm;

    mm = next->mm;
    oldmm = prev->active_mm;-------------------(2)

    if (!mm) {---------------------------(3)
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);-----------------(4)
    } else
        switch_mm(oldmm, mm, next); ---------------(5)

    if (!prev->mm) {------------------------(6)
        prev->active_mm = NULL;
        rq->prev_mm = oldmm;
    }

    switch_to(prev, next, prev);------------------(7)
    barrier();

    return finish_task_switch(prev);
}

(1)一旦调度器算法确定了pre task和next task,那么就可以调用context_switch函数实际执行进行切换的工作了,这里我们先看看参数传递情况:

  rq:在多核系统中,进程切换总是发生在各个cpu core上,参数rq指向本次切换发生的那个cpu对应的run queue
  prev:将要被剥夺执行权利的那个进程
  next:被选择在该cpu上执行的那个进程

(2)next是马上就要被切入的进程(后面简称B进程),prev是马上就要被剥夺执行权利的进程(后面简称A进程)。mm变量指向B进程的地址空间描述符,oldmm变量指向A进程的当前正在使用的地址空间描述符(active_mm)。对于normal进程,其任务描述符(task_struct)的mm和active_mm相同,都是指向其进程地址空间。对于内核线程而言,其task_struct的mm成员为NULL(内核线程没有进程地址空间),但是,内核线程被调度执行的时候,总是需要一个进程地址空间,而active_mm就是指向它借用的那个进程地址空间。

(3)mm为空的话,说明B进程是内核线程,这时候,只能借用A进程当前正在使用的那个地址空间(prev->active_mm)。注意:这里不能借用A进程的地址空间(prev->mm),因为A进程也可能是一个内核线程,不拥有自己的地址空间描述符。

(4)如果要切入的B进程是内核线程,那么调用体系结构相关的代码enter_lazy_tlb,标识该cpu进入lazy tlb mode。那么什么是lazy tlb mode呢?如果要切入的进程实际上是内核线程,那么我们也暂时不需要flush TLB,因为内核线程不会访问usersapce,所以那些无效的TLB entry也不会影响内核线程的执行。在这种情况下,为了性能,我们会进入lazy tlb mode。进程切换中和TLB相关的内容我们会单独在一篇文章中描述,这里就不再赘述了。

(5)如果要切入的B进程是内核线程,那么由于是借用当前正在使用的地址空间,因此没有必要调用switch_mm进行地址空间切换,只有要切入的B进程是一个普通进程的情况下(有自己的地址空间)才会调用switch_mm,真正执行地址空间切换。

如果切入的是普通进程,那么这时候进程的地址空间已经切换了,也就是说在A--->B进程的过程中,进程本身尚未切换,而进程的地址空间已经切换到了B进程了。这样会不会造成问题呢?还好,呵呵,这时候代码执行在kernel space,A和B进程的kernel space都是一样一样的啊,即便是切了进程地址空间,不过内核空间实际上保持不变的。

(6)如果切出的A进程是内核线程,那么其借用的那个地址空间(active_mm)已经不需要继续使用了(内核线程A被挂起了,根本不需要地址空间了)。除此之外,我们这里还设定了run queue上一次使用的mm struct(rq->prev_mm)为oldmm。为何要这么做?先等一等,下面我们会统一描述。

(7)一次进程切换,表面上看起来涉及两个进程,实际上涉及到了三个进程。switch_to是一个有魔力的符号,和一般的调用函数不同,当A进程在CPUa调用它切换到B进程的时候,switch_to一去不回,直到在某个cpu上(我们称之CPUx)完成从X进程(就是last进程)到A进程切换的时候,switch_to返回到A进程的现场。这一点我们会在下一节详细描述。switch_to完成了具体prev到next进程的切换,当switch_to返回的时候,说明A进程再次被调度执行了。

 

三、switch_to为什么需要三个参数呢?

switch_to定义如下:

#define switch_to(prev, next, last)                    \
    do {                                \
        ((last) = __switch_to((prev), (next)));            \
    } while (0)

一个switch_to将代码分成两段:

AAA

switch_to(prev, next, prev);

BBB

一次进程切换,涉及到了三个进程,prev和next是大家都熟悉的参数了,对于进程A(下图中的右半图片),如果它想要切换到B进程,那么:
    prev=A
    next=B
switch-to

这时候,在A进程中调用 switch_to 完成A到B进程的切换。但是,当经历万水千山,A进程又被重新调度的时候,我们又来到了switch_to返回的这一点(下图中的左半图片),这时候,我们是从哪一个进程切换到A呢?谁知道呢(在A进程调用switch_to 的时候是不知道的)?在A进程调用switch_to之后,cpu就执行B进程了,后续B进程切到哪个进程呢?随后又经历了怎样的进程切换过程呢?当然,这一切对于A进程来说它并不关心,它唯一关心的是当切换回A进程的时候,该cpu上(也不一定是A调用switch_to切换到B进程的那个CPU)执行的上一个task是谁?这就是第三个参数的含义(实际上这个参数的名字就是last,也基本说明了其含义)。也就是说,在AAA点上,prev是A进程,对应的run queue是CPUa的run queue,而在BBB点上,A进程恢复执行,last是X进程,对应的run queue是CPUx的run queue。

 

四、在内核线程切换过程中,内存描述符的处理

我们上面已经说过:如果切入内核线程,那么其实进程地址空间实际上并没有切换,该内核线程只是借用了切出进程使用的那个地址空间(active_mm)。对于内核中的实体,我们都会使用引用计数来根据一个数据对象,从而确保在没有任何引用的情况下释放该数据对象实体,对于内存描述符亦然。因此,在context_switch中有代码如下:

if (!mm) {
    next->active_mm = oldmm;
    atomic_inc(&oldmm->mm_count);-----增加引用计数
    enter_lazy_tlb(oldmm, next);
}

既然是借用别人的内存描述符(地址空间),那么调用atomic_inc是合理的,反正马上就切入B进程了,在A进程中提前增加引用计数也OK的。话说有借有还,那么在内核线程被切出的时候,就是归还内存描述符的时候了。

这里还有一个悖论,对于内核线程而言,在运行的时候,它会借用其他进程的地址空间,因此,在整个内核线程运行过程中,需要使用该地址空间(内存描述符),因此对内存描述符的增加和减少引用计数的操作只能在在内核线程之外完成。假如一次切换是这样的:…A--->B(内核线程)--->C…,增加引用计数比较简单,上面已经说了,在A进程调用context_switch的时候完成。现在问题来了,如何在C中完成减少引用计数的操作呢?我们还是从代码中寻找答案,如下(context_switch函数中,去掉了不相关的代码):

if (!prev->mm) {
    prev->active_mm = NULL;
    rq->prev_mm = oldmm;---在rq->prev_mm上保存了上一次使用的mm struct
}

借助其他进程内存描述符的东风,内核线程B欢快的运行,然而,快乐总是短暂的,也许是B自愿的,也许是强迫的,调度器最终会剥夺B的执行,切入C进程。也就是说,B内核线程调用switch_to(执行了AAA段代码),自己挂起,C粉墨登场,执行BBB段的代码。具体的代码在finish_task_switch,如下:

static struct rq *finish_task_switch(struct task_struct *prev)
{
    struct rq *rq = this_rq();
    struct mm_struct *mm = rq->prev_mm;――――――――――――――――(1)

    rq->prev_mm = NULL;

    if (mm)
        mmdrop(mm);――――――――――――――――――――――――(2)
}

(1)我们假设B是内核线程,在进程A调用context_switch切换到B线程的时候,借用的地址空间被保存在CPU对应的run queue中。在B切换到C之后,通过rq->prev_mm就可以得到借用的内存描述符。

(2)已经完成B到C的切换后,借用的地址空间可以返还了。因此在C进程中调用mmdrop来完成这一动作。很神奇,在A进程中为内核线程B借用地址空间,但却在C进程中释放它。

 

五、ARM64的进程地址空间切换

对于ARM64这个cpu arch,每一个cpu core都有两个寄存器来指示当前运行在该CPU core上的进程(线程)实体的地址空间。这两个寄存器分别是ttbr0_el1(用户地址空间)和ttbr1_el1(内核地址空间)。由于所有的进程共享内核地址空间,因此所谓地址空间切换也就是切换ttbr0_el1而已。地址空间听起来很抽象,实际上就是内存中的若干Translation table而已,每一个进程都有自己独立的一组用于翻译用户空间虚拟地址的Translation table,这些信息保存在内存描述符中,具体位于struct mm_struct中的pgd成员中。以pgd为起点,可以遍历该内存描述符的所有用户地址空间的Translation table。具体代码如下:

static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next,
      struct task_struct *tsk)----------------(1)
{
    unsigned int cpu = smp_processor_id();

    if (prev == next)--------------------(2)
        return;

    if (next == &init_mm) {-----------------(3)
        cpu_set_reserved_ttbr0();
        return;
    }

    check_and_switch_context(next, cpu);
}

(1)prev是要切出的地址空间,next是要切入的地址空间,tsk是将要切入的进程。

(2)要切出的地址空间和要切入的地址空间是一个地址空间的话,那么切换地址空间也就没有什么意义了。

(3)在ARM64中,地址空间的切换主要是切换ttbr0_el1,对于swapper进程的地址空间,其用户空间没有任何的mapping,而如果要切入的地址空间就是swapper进程的地址空间的时候,将(设定ttbr0_el1指向empty_zero_page)。

(4)check_and_switch_context中有很多TLB、ASID相关的操作,我们将会在另外的文档中给出细致描述,这里就简单略过,实际上,最终该函数会调用arch/arm64/mm/proc.S文件中的cpu_do_switch_mm将要切入进程的L0 Translation table物理地址(保存在内存描述符的pgd成员)写入ttbr0_el1。

 

六、ARM64的的进程切换

由于存在MMU,内存中可以有多个task,并且由调度器依次调度到cpu core上实际执行。系统有多少个cpu core就可以有多少个进程(线程)同时执行。即便是对于一个特定的cpu core,调度器可以可以不断的将控制权从一个task切换到另外一个task上。实际的context switch的动作也不复杂:就是将当前的上下文保存在内存中,然后从内存中恢复另外一个task的上下文。对于ARM64而言,context包括:

(1)通用寄存器

(2)浮点寄存器

(3)地址空间寄存器(ttbr0_el1和ttbr1_el1),上一节已经描述

(4)其他寄存器(ASID、thread process ID register等)

__switch_to代码(位于arch/arm64/kernel/process.c)如下:

struct task_struct *__switch_to(struct task_struct *prev,
                struct task_struct *next)
{
    struct task_struct *last;

    fpsimd_thread_switch(next);--------------(1)
    tls_thread_switch(next);----------------(2)
    hw_breakpoint_thread_switch(next);--和硬件跟踪相关
    contextidr_thread_switch(next); --和硬件跟踪相关

    dsb(ish); 
    last = cpu_switch_to(prev, next); ------------(3)

    return last;
}

(1)fp是float-point的意思,和浮点运算相关。simd是Single Instruction Multiple Data的意思,和多媒体以及信号处理相关。fpsimd_thread_switch其实就是把当前FPSIMD的状态保存到了内存中(task.thread.fpsimd_state),从要切入的next进程描述符中获取FPSIMD状态,并加载到CPU上。

(2)概念同上,不过是处理tls(thread local storage)的切换。这里硬件寄存器涉及tpidr_el0和tpidrro_el0,涉及的内存是task.thread.tp_value。具体的应用场景是和线程库相关,具体大家可以自行学习了。

(3)具体的切换发生在arch/arm64/kernel/entry.S文件中的cpu_switch_to,代码如下:

ENTRY(cpu_switch_to) -------------------(1)
    mov    x10, #THREAD_CPU_CONTEXT ----------(2)
    add    x8, x0, x10 --------------------(3)
    mov    x9, sp
    stp    x19, x20, [x8], #16----------------(4)
    stp    x21, x22, [x8], #16
    stp    x23, x24, [x8], #16
    stp    x25, x26, [x8], #16
    stp    x27, x28, [x8], #16
    stp    x29, x9, [x8], #16
    str    lr, [x8] ---------A
    add    x8, x1, x10 -------------------(5)
    ldp    x19, x20, [x8], #16----------------(6)
    ldp    x21, x22, [x8], #16
    ldp    x23, x24, [x8], #16
    ldp    x25, x26, [x8], #16
    ldp    x27, x28, [x8], #16
    ldp    x29, x9, [x8], #16
    ldr    lr, [x8] -------B
    mov    sp, x9 -------C
    ret -------------------------(7)
ENDPROC(cpu_switch_to)

(1)进入cpu_switch_to函数之前,x0,x1用做参数传递,x0是prev task,就是那个要挂起的task,x1是next task,就是马上要切入的task。cpu_switch_to和其他的普通函数没有什么不同,尽管会走遍万水千山,但是最终还是会返回调用者函数__switch_to。

在进入细节之前,先想一想这个问题:cpu_switch_to要如何保存现场?要保存那些通用寄存器呢?其实上一小段描述已经做了铺陈:尽管有点怪异,本质上cpu_switch_to仍然是一个普通函数,需要符合ARM64标准过程调用文档。在该文档中规定,x19~x28是属于callee-saved registers,也就是说,在__switch_to函数调用cpu_switch_to函数这个过程中,cpu_switch_to函数要保证x19~x28这些寄存器值是和调用cpu_switch_to函数之前一模一样的。除此之外,pc、sp、fp当然也是必须是属于现场的一部分的。

(2)得到THREAD_CPU_CONTEXT的偏移,保存在x10中

(3)x0是pre task的进程描述符,加上偏移之后就获取了访问cpu context内存的指针(x8寄存器)。所有context的切换的原理都是一样的,就是把当前cpu寄存器保存在内存中,这里的内存是在进程描述符中的 thread.cpu_context中。

(4)一旦定位到保存cpu context(各种通用寄存器)的内存,那么使用stp保存硬件现场。这里x29就是fp(frame pointer),x9保存了stack pointer,lr是返回的PC值。到A代码处,完成了pre task cpu context的保存动作。

(5)和步骤(3)类似,只不过是针对next task而言的。这时候x8指向了next task的cpu context。

(6)和步骤(4)类似,不同的是这里的操作是恢复next task的cpu context。执行到代码B处,所有的寄存器都已经恢复,除了PC和SP,其中PC保存在了lr(x30)中,而sp保存在了x9中。在代码C出恢复了sp值,这时候万事俱备,只等PC操作了。

(7)ret指令其实就是把x30(lr)寄存器的值加载到PC,至此现场完全恢复到调用cpu_switch_to那一点上了。

 

参考文献:

1、ARM标准过程调用文档(IHI0056C_beta_aaelf64.pdf)

2、Linux 4.4.6内核源代码

 

原创文章,转发请注明出处。蜗窝科技

eMMC 原理 3 :分区管理

$
0
0


1. Partitions Overview

eMMC 标准中,将内部的 Flash Memory 划分为 4 类区域,最多可以支持 8 个硬件分区,如下图所示:

1.1 概述

一般情况下,Boot Area Partitions 和 RPMB Partition 的容量大小通常都为 4MB,部分芯片厂家也会提供配置的机会。General Purpose Partitions (GPP) 则在出厂时默认不被支持,即不存在这些分区,需要用户主动使能,并配置其所要使用的 GPP 的容量大小,GPP 的数量可以为 1 - 4 个,各个 GPP 的容量大小可以不一样。User Data Area (UDA) 的容量大小则为总容量大小减去其他分区所占用的容量。更多各个分区的细节将在后续小节中描述。

1.2 分区编址

eMMC 的每一个硬件分区的存储空间都是独立编址的,即访问地址为 0 - partition size。具体的数据读写操作实际访问哪一个硬件分区,是由 eMMC 的 Extended CSD register 的 PARTITION_CONFIG Field 中 的 Bit[2:0]: PARTITION_ACCESS 决定的,用户可以通过配置 PARTITION_ACCESS 来切换硬件分区的访问。也就是说,用户在访问特定的分区前,需要先发送命令,配置 PARTITION_ACCESS,然后再发送相关的数据访问请求。更多数据读写相关的细节,请参考 eMMC 总线协议 章节。

eMMC 的各个硬件分区有其自身的功能特性,多分区的设计,为不同的应用场景提供了便利。

2. Boot Area Partitions

Boot Area 包含两个 Boot Area Partitions,主要用于存储 Bootloader,支持 SOC 从 eMMC 启动系统。

2.1 容量大小

两个 Boot Area Partitions 的大小是完全一致的,由 Extended CSD register 的 BOOT_SIZE_MULT Field 决定,大小的计算公式如下:

Size = 128Kbytes x BOOT_SIZE_MULT

一般情况下,Boot Area Partition 的大小都为 4 MB,即 BOOT_SIZE_MULT 为 32,部分芯片厂家会提供改写 BOOT_SIZE_MULT 的功能来改变 Boot Area Partition 的容量大小。BOOT_SIZE_MULT 最大可以为 255,即 Boot Area Partition 的最大容量大小可以为 255 x 128 KB = 32640 KB = 31.875 MB。

2.2 从 Boot Area 启动

eMMC 中定义了 Boot State,在 Power-up、HW reset 或者 SW reset 后,如果满足一定的条件,eMMC 就会进入该 State。进入 Boot State 的条件如下:

Original Boot Operation
CMD 信号保持低电平不少于 74 个时钟周期,会触发 Original Boot Operation,进入 Boot State。

Alternative Boot Operation
在 74 个时钟周期后,在 CMD 信号首次拉低或者 Host 发送 CMD1 之前,Host 发送参数为 0xFFFFFFFA 的 COM0时,会触发 Alternative Boot Operation,进入 Boot State。

在 Boot State 下,如果有配置 BOOT_ACK,eMMC 会先发送 “010” 的 ACK 包,接着 eMMC 会将最大为 128Kbytes x BOOT_SIZE_MULT 的 Boot Data 发送给 Host。传输过程中,Host 可以通过拉高 CMD 信号 (Original Boot 中),或者发送 Reset 命令 (Alternative Boot 中) 来中断 eMMC 的数据发送,完成 Boot Data 传输。

Boot Data 根据 Extended CSD register 的 PARTITION_CONFIG Field 的 Bit[5:3]:BOOT_PARTITION_ENABLE 的设定,可以从 Boot Area Partition 1、Boot Area Partition 2 或者 User Data Area 读出。

Boot Data 存储在 Boot Area 比在 User Data Area 中要更加的安全,可以减少意外修改导致系统无法启动,同时无法更新系统的情况出现。

(更多 Boot State 的细节,请参考 eMMC 工作模式 的 Boot Mode 章节)

2.3 写保护

通过设定 Extended CSD register 的 BOOT_WP Field,可以为两个 Boot Area Partition 独立配置写保护功能,以防止数据被意外改写或者擦出。

eMMC 中定义了两种 Boot Area 的写保护模式:

  1. Power-on write protection,使能后,如果 eMMC 掉电,写保护功能失效,需要每次 Power on 后进行配置
  2. Permanent write protection,使能后,即使掉电也不会失效,主动进行关闭才会失效

3. RPMB Partition

RPMB(Replay Protected Memory Block)Partition 是 eMMC 中的一个具有安全特性的分区。
eMMC 在写入数据到 RPMB 时,会校验数据的合法性,只有指定的 Host 才能够写入,同时在读数据时,也提供了签名机制,保证 Host 读取到的数据是 RPMB 内部数据,而不是攻击者伪造的数据。

RPMB 在实际应用中,通常用于存储一些有防止非法篡改需求的数据,例如手机上指纹支付相关的公钥、序列号等。RPMB 可以对写入操作进行鉴权,但是读取并不需要鉴权,任何人都可以进行读取的操作,因此存储到 RPMB 的数据通常会进行加密后再存储。

3.1 容量大小

两个 RPMB Partition 的大小是由 Extended CSD register 的 BOOT_SIZE_MULT Field 决定,大小的计算公式如下:

Size = 128Kbytes x BOOT_SIZE_MULT

一般情况下,Boot Area Partition 的大小为 4 MB,即 RPMB_SIZE_MULT 为 32,部分芯片厂家会提供改写 RPMB_SIZE_MULT 的功能来改变 RPMB Partition 的容量大小。RPMB_SIZE_MULT 最大可以为 128,即 Boot Area Partition 的最大容量大小可以为 128 x 128 KB = 16384 KB = 16 MB。

3.2 Replay Protect 原理

使用 eMMC 的产品,在产线生产时,会为每一个产品生产一个唯一的 256 bits 的 Secure Key,烧写到 eMMC 的 OTP 区域(只能烧写一次的区域),同时 Host 在安全区域中(例如:TEE)也会保留该 Secure Key。

在 eMMC 内部,还有一个RPMB Write Counter。RPMB 每进行一次合法的写入操作时,Write Counter 就会自动加一 。

通过 Secure Key 和 Write Counter 的应用,RMPB 可以实现数据读取和写入的 Replay Protect。

RPMB 数据读取

RPMB 数据读取的流程如下:

  1. Host 向 eMMC 发起读 RPMB 的请求,同时生成一个 16 bytes 的随机数,发送给 eMMC。
  2. eMMC 将请求的数据从 RPMB 中读出,并使用 Secure Key 通过 HMAC SHA-256 算法,计算读取到的数据和接收到的随机数拼接到一起后的签名。然后,eMMC 将读取到的数据、接收到的随机数、计算得到的签名一并发送给 Host。
  3. Host 接收到 RPMB 的数据、随机数以及签名后,首先比较随机数是否与自己发送的一致,如果一致,再用同样的 Secure Key 通过 HMAC SHA-256 算法对数据和随机数组合到一起进行签名,如果签名与 eMMC 发送的签名是一致的,那么就可以确定该数据是从 RPMB 中读取到的正确数据,而不是攻击者伪造的数据。

通过上述的读取流程,可以保证 Host 正确的读取到 RPMB 的数据。

RPMB 数据写入

RPMB 数据写入的流程如下:

  1. Host 按照上面的读数据流程,读取 RPMB 的 Write Counter。
  2. Host 将需要写入的数据和 Write Counter 拼接到一起并计算签名,然后将数据、Write Counter 以及签名一并发给 eMMC。
  3. eMMC 接收到数据后,先对比 Write Counter 是否与当前的值相同,如果相同那么再对数据和 Write Counter 的组合进行签名,然后和 Host 发送过来的签名进行比较,如果签名相同则鉴权通过,将数据写入到 RPMB 中。

通过上述的写入流程,可以保证 RPMB 不会被非法篡改。

更多 RPMB 相关的细节,可以参考 eMMC RPMB 章节。

4. General Purpose Partitions

eMMC 提供了 General Purpose Partitions (GPP),主要用于存储系统和应用数据。在很多使用 eMMC 的产品中,GPP 都没有被启用,因为它在功能上与 UDA 类似,产品上直接使用 UDA 就可以满足需求。

4.1 容量大小

eMMC 最多可以支持 4 个 GPPs,每一个 GPP 的大小可以单独配置。用户可以通过设定 Extended CSD register 的以下三个 Field 来设 GPPx (x=1~4) 的容量大小:

  • GP_SIZE_MULT_x_2
  • GP_SIZE_MULT_x_1
  • GP_SIZE_MULT_x_0

GPPx 的容量计算公式如下:

Size = (GP_SIZE_MULT_x_2 * 2^16 + GP_SIZE_MULT_x_1 * 2^8 + GP_SIZE_MULT_x_0 * 2^0) * (Write protect group size)

Write protect group size = 512KB * HC_ERASE_GRP_SIZE * HC_WP_GRP_SIZE

  • eMMC 中,擦除和写保护都是按块进行的,上述表达式中的 HC_WP_GRP_SIZE 为写保护的操作块大小,HC_ERASE_GRP_SIZE 则为擦除操作的快的大小。
  • eMMC 芯片的 GPP 的配置通常是只能进行一次 (OTP),一般会在产品量产阶段,在产线上进行。

4.2 分区属性

eMMC 标准中,为 GPP 定义了两类属性,Enhanced attribute 和 Extended attribute。每个 GPP 可以设定两类属性中的一种属性,不可以同时设定多个属性。

Enhanced attribute

  • Default, 未设定 Enhanced attribute。
  • Enhanced storage media, 设定 GPP 为 Enhanced storage media。

在 eMMC 标准中,实际上并未定义设定 Enhanced attribute 后对 eMMC 的影响。Enhanced attribute 的具体作用,由芯片制造商定义。
在实际的产品中,设定 Enhanced storage media 后,一般是把该分区的存储介质从 MLC 改变为 SLC,提高该分区的读写性能、寿命以及稳定性。由于 1 个存储单元下,MLC 的容量是 SLC 的两倍,所以在总的存储单元数量一定的情况下,如果把原本为 MLC 的分区改变为 SLC,会减少 eMMC 的容量,就是说,此时 eMMC 的实际总容量比标称的总容量会小一点。(MLC 和 SLC 的细节可以参考 Flash Memory 章节内容)

Extended attribute

  • Default, 未设定 Extended attribute。
  • System code, 设定 GPP 为 System code 属性,该属性主要用在存放操作系统类的、很少进行擦写更新的分区。
  • Non-Persistent,设定 GPP 为 Non-Persistent 属性,该属性主要用于存储临时数据的分区,例如 tmp 目录所在分区、 swap 分区等。

在 eMMC 标准中,同样也没有定义设定 Extended attribute 后对 eMMC 的影响。Extended attribute 的具体作用,由芯片制造商定义。Extended attribute 主要是跟分区的应用场景有关,厂商可以为不用应用场景的分区做不同的优化处理。

5. User Data Area

User Data Area (UDA) 通常是 eMMC 中最大的一个分区,是实际产品中,最主要的存储区域。

5.1 容量大小

UDA 的容量大小不需要设置,在配置完其他分区大小后,再扣除设置 Enhanced attribute 所损耗的容量,剩下的容量就是 UDA 的容量。

5.2 软件分区

为了更合理的管理数据,满足不同的应用需求,UDA 在实际产品中,会进行软件再分区。目前主流的软件分区技术有 MBR(Master Boot Record)和 GPT(GUID Partition Table)两种。这两种分区技术的基本原理类似,如下图所示:

软件分区技术一般是将存储介质划分为多个区域,既 SW Partitions,然后通过一个 Partition Table 来维护这些 SW Partitions。在 Partition Table 中,每一个条目都保存着一个 SW Partition 的起始地址、大小等的属性信息。软件系统在启动后,会去扫描 Partition Table,获取存储介质上的各个 SW Partitions 信息,然后根据这些信息,将各个 Partitions 加载到系统中,进行数据存取。

MBR 和 GPT 此处不展开详细介绍,更多的细节可以参考 wikipedia 上 MBR 和 GPT 相关介绍。

5.3 区域属性

eMMC 标准中,支持为 UDA 中一个特定大小的区域设定 Enhanced attribute。与 GPP 中的 Enhanced attribute 相同,eMMC 标准也没有定义该区域设定 Enhanced attribute 后对 eMMC 的影响。Enhanced attribute 的具体作用,由芯片制造商定义。

Enhanced attribute

  • Default, 未设定 Enhanced attribute。
  • Enhanced storage media, 设定该区域为 Enhanced storage media。

在实际的产品中,UDA 区域设定为 Enhanced storage media 后,一般是把该区域的存储介质从 MLC 改变为 SLC。通常,产品中可以将某一个 SW Partition 设定为 Enhanced storage media,以获得更好的性能和健壮性。

6. eMMC 分区应用实例

在一个 Android 手机系统中,各个分区的呈现形式如下:

  • mmcblk0 为 eMMC 的块设备;
  • mmcblk0boot0 和 mmcblk0boot1 对应两个 Boot Area Partitions;
  • mmcblk0rpmb 则为 RPMB Partition,
  • mmcblk0px 为 UDA 划分出来的 SW Partitions;
  • 如果存在 GPP,名称则为 mmcblk0gp1、mmcblk0gp2、mmcblk0gp3、mmcblk0gp4;
root@xxx:/ # ls /dev/block/mmcblk0*
/dev/block/mmcblk0
/dev/block/mmcblk0boot0
/dev/block/mmcblk0boot1
/dev/block/mmcblk0rpmb
/dev/block/mmcblk0p1
/dev/block/mmcblk0p2
/dev/block/mmcblk0p3
/dev/block/mmcblk0p4
/dev/block/mmcblk0p5
/dev/block/mmcblk0p6
/dev/block/mmcblk0p7
/dev/block/mmcblk0p8
/dev/block/mmcblk0p9
/dev/block/mmcblk0p10
/dev/block/mmcblk0p11
/dev/block/mmcblk0p12
/dev/block/mmcblk0p13
/dev/block/mmcblk0p14
/dev/block/mmcblk0p15
/dev/block/mmcblk0p16
/dev/block/mmcblk0p17
/dev/block/mmcblk0p18
/dev/block/mmcblk0p19
/dev/block/mmcblk0p20
/dev/block/mmcblk0p21
/dev/block/mmcblk0p22
/dev/block/mmcblk0p23
/dev/block/mmcblk0p24
/dev/block/mmcblk0p25
/dev/block/mmcblk0p26
/dev/block/mmcblk0p27
/dev/block/mmcblk0p28
/dev/block/mmcblk0p29
/dev/block/mmcblk0p30
/dev/block/mmcblk0p31
/dev/block/mmcblk0p32

每一个分区会根据实际的功能来设定名称。

root@xxx:/ # ls -l /dev/block/platform/mtk-msdc.0/11230000.msdc0/by-name/
lrwxrwxrwx root root 2015-01-03 04:03 boot -> /dev/block/mmcblk0p22
lrwxrwxrwx root root 2015-01-03 04:03 cache -> /dev/block/mmcblk0p30
lrwxrwxrwx root root 2015-01-03 04:03 custom -> /dev/block/mmcblk0p3
lrwxrwxrwx root root 2015-01-03 04:03 devinfo -> /dev/block/mmcblk0p28
lrwxrwxrwx root root 2015-01-03 04:03 expdb -> /dev/block/mmcblk0p4
lrwxrwxrwx root root 2015-01-03 04:03 flashinfo -> /dev/block/mmcblk0p32
lrwxrwxrwx root root 2015-01-03 04:03 frp -> /dev/block/mmcblk0p5
lrwxrwxrwx root root 2015-01-03 04:03 keystore -> /dev/block/mmcblk0p27
lrwxrwxrwx root root 2015-01-03 04:03 lk -> /dev/block/mmcblk0p20
lrwxrwxrwx root root 2015-01-03 04:03 lk2 -> /dev/block/mmcblk0p21
lrwxrwxrwx root root 2015-01-03 04:03 logo -> /dev/block/mmcblk0p23
lrwxrwxrwx root root 2015-01-03 04:03 md1arm7 -> /dev/block/mmcblk0p17
lrwxrwxrwx root root 2015-01-03 04:03 md1dsp -> /dev/block/mmcblk0p16
lrwxrwxrwx root root 2015-01-03 04:03 md1img -> /dev/block/mmcblk0p15
lrwxrwxrwx root root 2015-01-03 04:03 md3img -> /dev/block/mmcblk0p18
lrwxrwxrwx root root 2015-01-03 04:03 metadata -> /dev/block/mmcblk0p8
lrwxrwxrwx root root 2015-01-03 04:03 nvdata -> /dev/block/mmcblk0p7
lrwxrwxrwx root root 2015-01-03 04:03 nvram -> /dev/block/mmcblk0p19
lrwxrwxrwx root root 2015-01-03 04:03 oemkeystore -> /dev/block/mmcblk0p12
lrwxrwxrwx root root 2015-01-03 04:03 para -> /dev/block/mmcblk0p2
lrwxrwxrwx root root 2015-01-03 04:03 ppl -> /dev/block/mmcblk0p6
lrwxrwxrwx root root 2015-01-03 04:03 proinfo -> /dev/block/mmcblk0p13
lrwxrwxrwx root root 2015-01-03 04:03 protect1 -> /dev/block/mmcblk0p9
lrwxrwxrwx root root 2015-01-03 04:03 protect2 -> /dev/block/mmcblk0p10
lrwxrwxrwx root root 2015-01-03 04:03 recovery -> /dev/block/mmcblk0p1
lrwxrwxrwx root root 2015-01-03 04:03 rstinfo -> /dev/block/mmcblk0p14
lrwxrwxrwx root root 2015-01-03 04:03 seccfg -> /dev/block/mmcblk0p11
lrwxrwxrwx root root 2015-01-03 04:03 secro -> /dev/block/mmcblk0p26
lrwxrwxrwx root root 2015-01-03 04:03 system -> /dev/block/mmcblk0p29
lrwxrwxrwx root root 2015-01-03 04:03 tee1 -> /dev/block/mmcblk0p24
lrwxrwxrwx root root 2015-01-03 04:03 tee2 -> /dev/block/mmcblk0p25
lrwxrwxrwx root root 2015-01-03 04:03 userdata -> /dev/block/mmcblk0p31 

7. 参考资料

  1. Embedded Multi-Media Card (e•MMC) Electrical Standard (5.1) [PDF]
  2. Disk partitioning [Web]
  3. Master Boot Record [Web]
  4. GUID Partition Table [Web]



进程切换分析(2):TLB处理

$
0
0

一、前言

进程切换是一个复杂的过程,本文不准备详细描述整个进程切换的方方面面,而是关注进程切换中一个小小的知识点:TLB的处理。为了能够讲清楚这个问题,我们在第二章描述在单CPU场景下一些和TLB相关的细节,第三章推进到多核场景,至此,理论部分结束。在第二章和第三章,我们从基本的逻辑角度出发,并不拘泥于特定的CPU和特定的OS,这里需要大家对基本的TLB的组织原理有所了解,具体可以参考本站的《TLB操作》一文。再好的逻辑也需要体现在HW block和SW block的设计中,在第四章,我们给出了linux4.4.6内核在ARM64平台上的TLB代码处理细节(在描述tlb lazy mode的时候引入部分x86架构的代码),希望能通过具体的代码和实际的CPU硬件行为加深大家对原理的理解。

 

二、单核场景的工作原理

1、block diagram

我们先看看在单核场景下,和进程切换相关的逻辑block示意图:

tlb

CPU上运行了若干的用户空间的进程和内核线程,为了加快性能,CPU中往往设计了TLB和Cache这样的HW block。Cache为了更快的访问main memory中的数据和指令,而TLB是为了更快的进行地址翻译而将部分的页表内容缓存到了Translation lookasid buffer中,避免了从main memory访问页表的过程。

假如不做任何的处理,那么在进程A切换到进程B的时候,TLB和Cache中同时存在了A和B进程的数据。对于kernel space其实无所谓,因为所有的进程都是共享的,但是对于A和B进程,它们各种有自己的独立的用户地址空间,也就是说,同样的一个虚拟地址X,在A的地址空间中可以被翻译成Pa,而在B地址空间中会被翻译成Pb,如果在地址翻译过程中,TLB中同时存在A和B进程的数据,那么旧的A地址空间的缓存项会影响B进程地址空间的翻译,因此,在进程切换的时候,需要有tlb的操作,以便清除旧进程的影响,具体怎样做呢?我们下面一一讨论。

2、绝对没有问题,但是性能不佳的方案

当系统发生进程切换,从进程A切换到进程B,从而导致地址空间也从A切换到B,这时候,我们可以认为在A进程执行过程中,所有TLB和Cache的数据都是for A进程的,一旦切换到B,整个地址空间都不一样了,因此需要全部flush掉(注意:我这里使用了linux内核的术语,flush就是意味着将TLB或者cache中的条目设置为无效,对于一个ARM平台上的嵌入式工程师,一般我们会更习惯使用invalidate这个术语,不管怎样,在本文中,flush等于invalidate)。

这种方案当然没有问题,当进程B被切入执行的时候,其面对的CPU是一个干干净净,从头开始的硬件环境,TLB和Cache中不会有任何的残留的A进程的数据来影响当前B进程的执行。当然,稍微有一点遗憾的就是在B进程开始执行的时候,TLB和Cache都是冰冷的(空空如也),因此,B进程刚开始执行的时候,TLB miss和Cache miss都非常严重,从而导致了性能的下降。

3、如何提高TLB的性能?

对一个模块的优化往往需要对该模块的特性进行更细致的分析、归类,上一节,我们采用进程地址空间这样的术语,其实它可以被进一步细分为内核地址空间和用户地址空间。对于所有的进程(包括内核线程),内核地址空间是一样的,因此对于这部分地址翻译,无论进程如何切换,内核地址空间转换到物理地址的关系是永远不变的,其实在进程A切换到B的时候,不需要flush掉,因为B进程也可以继续使用这部分的TLB内容(上图中,橘色的block)。对于用户地址空间,各个进程都有自己独立的地址空间,在进程A切换到B的时候,TLB中的和A进程相关的entry(上图中,青色的block)对于B是完全没有任何意义的,需要flush掉。

在这样的思路指导下,我们其实需要区分global和local(其实就是process-specific的意思)这两种类型的地址翻译,因此,在页表描述符中往往有一个bit来标识该地址翻译是global还是local的,同样的,在TLB中,这个标识global还是local的flag也会被缓存起来。有了这样的设计之后,我们可以根据不同的场景而flush all或者只是flush local tlb entry。

4、特殊情况的考量

我们考虑下面的场景:进程A切换到内核线程K之后,其实地址空间根本没有必要切换,线程K能访问的就是内核空间的那些地址,而这些地址也是和进程A共享的。既然没有切换地址空间,那么也就不需要flush 那些进程特定的tlb entry了,当从K切换会A进程后,那么所有TLB的数据都是有效的,从大大降低了tlb miss。此外,对于多线程环境,切换可能发生在一个进程中的两个线程,这时候,线程在同样的地址空间,也根本不需要flush tlb。

4、进一步提升TLB的性能

还有可能进一步提升TLB的性能吗?有没有可能根本不flush TLB?

当然可以,不过这需要我们在设计TLB block的时候需要识别process specific的tlb entry,也就是说,TLB block需要感知到各个进程的地址空间。为了完成这样的设计,我们需要标识不同的address space,这里有一个术语叫做ASID(address space ID)。原来TLB查找是通过虚拟地址VA来判断是否TLB hit。有了ASID的支持后,TLB hit的判断标准修改为(虚拟地址+ASID),ASID是每一个进程分配一个,标识自己的进程地址空间。TLB block如何知道一个tlb entry的ASID呢?一般会来自CPU的系统寄存器(对于ARM64平台,它来自TTBRx_EL1寄存器),这样在TLB block在缓存(VA-PA-Global flag)的同时,也就把当前的ASID缓存在了对应的TLB entry中,这样一个TLB entry中包括了(VA-PA-Global flag-ASID)。

有了ASID的支持后,A进程切换到B进程再也不需要flush tlb了,因为A进程执行时候缓存在TLB中的残留A地址空间相关的entry不会影响到B进程,虽然A和B可能有相同的VA,但是ASID保证了硬件可以区分A和B进程地址空间。

 

三、多核的TLB操作

1、block diagram

完成单核场景下的分析之后,我们一起来看看多核的情况。进程切换相关的TLB逻辑block示意图如下:

tlb-mp

在多核系统中,进程切换的时候,TLB的操作要复杂一些,主要原因有两点:其一是各个cpu core有各自的TLB,因此TLB的操作可以分成两类,一类是flush all,即将所有cpu core上的tlb flush掉,还有一类操作是flush local tlb,即仅仅flush本cpu core的tlb。另外一个原因是进程可以调度到任何一个cpu core上执行(当然具体和cpu affinity的设定相关),从而导致task处处留情(在各个cpu上留有残余的tlb entry)。

2、TLB操作的基本思考

根据上一节的描述,我们了解到地址翻译有global(各个进程共享)和local(进程特定的)的概念,因而tlb entry也有global和local的区分。如果不区分这两个概念,那么进程切换的时候,直接flush该cpu上的所有残余。这样,当进程A切出的时候,留给下一个进程B一个清爽的tlb,而当进程A在其他cpu上再次调度的时候,它面临的也是一个全空的TLB(其他cpu的tlb不会影响)。当然,如果区分global 和local,那么tlb操作也基本类似,只不过进程切换的时候,不是flush该cpu上的所有tlb entry,而是flush所有的tlb local entry就OK了。

对local tlb entry还可以进一步细分,那就是了ASID(address space ID)或者PCID(process context ID)的概念了(global tlb entry不区分ASID)。如果支持ASID(或者PCID)的话,tlb操作变得简单一些,或者说我们没有必要执行tlb操作了,因为在TLB搜索的时候已经可以区分各个task上下文了,这样,各个cpu中残留的tlb不会影响其他任务的执行。在单核系统中,这样的操作可以获取很好的性能。比如A---B--->A这样的场景中,如果TLB足够大,可以容纳2个task的tlb entry(现代cpu一般也可以做到这一点),那么A再次切回的时候,TLB是hot的,大大提升了性能。

不过,对于多核系统,这种情况有一点点的麻烦,其实也就是传说中的TLB shootdown带来的性能问题。在多核系统中,如果cpu支持PCID并且在进程切换的时候不flush tlb,那么系统中各个cpu中的tlb entry则保留各种task的tlb entry,当在某个cpu上,一个进程被销毁,或者修改了自己的页表(也就是修改了VA PA映射关系)的时候,我们必须将该task的相关tlb entry从系统中清除出去。这时候,你不仅仅需要flush本cpu上对应的TLB entry,还需要shootdown其他cpu上的和该task相关的tlb残余。而这个动作一般是通过IPI实现(例如X86),从而引入了开销。此外PCID的分配和管理也会带来额外的开销,因此,OS是否支持PCID(或者ASID)是由各个arch代码自己决定(对于linux而言,x86不支持,而ARM平台是支持的)。

 

四、进程切换中的tlb操作代码分析

1、tlb lazy mode

在context_switch中有这样的一段代码:

if (!mm) {
    next->active_mm = oldmm;
    atomic_inc(&oldmm->mm_count);
    enter_lazy_tlb(oldmm, next);
} else
    switch_mm(oldmm, mm, next);

这段代码的意思就是如果要切入的next task是一个内核线程(next->mm == NULL )的话,那么可以通过enter_lazy_tlb函数标记本cpu上的next task进入lazy TLB mode。由于ARM64平台上的enter_lazy_tlb函数是空函数,因此我们采用X86来描述lazy TLB mode。

当然,我们需要一些准备工作,毕竟对于熟悉ARM平台的嵌入式工程师而言,x86多少有点陌生。

到目前,我们还都是从逻辑角度来描述TLB操作,但是在实际中,进程切换中的tlb操作是HW完成还是SW完成呢?不同的处理器思路是不一样的(具体原因未知),有的处理器是HW完成,例如X86,在加载cr3寄存器进行地址空间切换的时候,hw会自动操作tlb。而有的处理是需要软件参与完成tlb操作,例如ARM系列的处理器,在切换TTBR寄存器的时候,HW没有tlb动作,需要SW完成tlb操作。因此,x86平台上,在进程切换的时候,软件不需要显示的调用tlb flush函数,在switch_mm函数中会用next task中的mm->pgd加载CR3寄存器,这时候load cr3的动作会导致本cpu中的local tlb entry被全部flush掉。

在x86支持PCID(X86术语,相当与ARM的ASID)的情况下会怎样呢?也会在load cr3的时候flush掉所有的本地CPU上的 local tlb entry吗?其实在linux中,由于TLB shootdown,普通的linux并不支持PCID(KVM中会使用,但是不在本文考虑范围内),因此,对于x86的进程地址空间切换,它就是会有flush local tlb entry这样的side effect。

另外有一点是ARM64和x86不同的地方:ARM64支持在一个cpu core执行tlb flush的指令,例如tlbi vmalle1is,将inner shareablity domain中的所有cpu core的tlb全部flush掉。而x86不能,如果想要flush掉系统中多有cpu core的tlb,只能是通过IPI通知到其他cpu进行处理。

好的,至此,所有预备知识都已经ready了,我们进入tlb lazy mode这个主题。虽然进程切换伴随tlb flush操作,但是某些场景亦可避免。在下面的场景,我们可以不flush tlb(我们仍然采用A--->B task的场景来描述):

(1)如果要切入的next task B是内核线程,那么我们也暂时不需要flush TLB,因为内核线程不会访问usersapce,而那些进程A残留的TLB entry也不会影响内核线程的执行,毕竟B没有自己的用户地址空间,而且和A共享内核地址空间。

(2)如果A和B在一个地址空间中(一个进程中的两个线程),那么我们也暂时不需要flush TLB。

除了进程切换,还有其他的TLB flush场景。我们先看一个通用的TLB flush场景,如下图所示:

lazy

一个4核系统中,A0 A1和A2 task属于同一个进程地址空间,CPU_0和CPU_2上分别运行了A0和A2 task,CPU_1有点特殊,它正在运行一个内核线程,但是该内核线程正在借用A1 task的地址空间,CPU_3上运行不相关的B task。

当A0 task修改了自己的地址翻译,那么它不能只是flush CPU_0的tlb,还需要通知到CPU_1和CPU_2,因为这两个CPU上当前active的地址空间和CPU_0是一样的。由于A1 task的修改,CPU_1和CPU_2上的这些缓存的TLB entry已经失效了,需要flush。同理,可以推广到更多的CPU上,也就是说,在某个CPUx上运行的task修改了地址映射关系,那么tlb flush需要传递到所有相关的CPU中(当前的mm等于CPUx的current mm)。在多核系统中,这样的通过IPI来传递TLB flush的消息会随着cpu core的增加而增加,有没有办法减少那些没有必要的TLB flush呢?当然有,也就是上图中的A1 task场景,这也就是传说中的lazy tlb mode。

我先回头看看代码。在代码中,如果next task是内核线程,我们并不会执行switch_mm(该函数会引起tlb flush的动作),而是调用enter_lazy_tlb进入lazy tlb mode。在x86架构下,代码如下:

static inline void enter_lazy_tlb(struct mm_struct *mm, struct task_struct *tsk)
{
#ifdef CONFIG_SMP
    if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK)
        this_cpu_write(cpu_tlbstate.state, TLBSTATE_LAZY);
#endif
}

在x86架构下,进入lazy tlb mode也就是在该cpu的cpu_tlbstate变量中设定TLBSTATE_LAZY的状态就OK了。因此,进入lazy mode的时候,也就不需要调用switch_mm来切换进程地址空间,也就不会执行flush tlb这样毫无意义的动作了。 enter_lazy_tlb并不操作硬件,只要记录该cpu的软件状态就OK了。

切换之后,内核线程进入执行状态,CPU_1的TLB残留进程A的entry,这对于内核线程的执行没有影响,但是当其他CPU发送IPI要求flush TLB的时候呢?按理说应该立刻flush tlb,但是在lazy tlb mode下,我们可以不执行flush tlb操作。这样问题来了:什么时候flush掉残留的A进程的tlb entry呢?答案是在下一次进程切换中。因为一旦内核线程被schedule out,并且切入一个新的进程C,那么在switch_mm,切入到C进程地址空间的时候,所有之前的残留都会被清除掉(因为有load cr3的动作)。因此,在执行内核线程的时候,我们可以推迟tlb invalidate的请求。也就是说,当收到ipi中断要求进行该mm的tlb invalidate的动作的时候,我们暂时没有必要执行了,只需要记录状态就OK了。

2、ARM64中如何管理ASID?

和x86不同的是:ARM64支持了ASID(类似x86的PCID),难道ARM64解决了TLB Shootdown的问题?其实我也在思考这个问题,但是还没有想明白。很显然,在ARM64中,我们不需要通过IPI来进行所有cpu core的TLB flush动作,ARM64在指令集层面支持shareable domain中所有PEs上的TLB flush动作,也许是这样的指令让TLB flush的开销也没有那么大,那么就可以选择支持ASID,在进程切换的时候不需要进行任何的TLB操作,同时,由于不需要IPI来传递TLB flush,那么也就没有特别的处理lazy tlb mode了。

既然linux中,ARM64选择支持ASID,那么它就要直面ASID的分配和管理问题了。硬件支持的ASID有一定限制,它的编址空间是8个或者16个bit,最大256或者65535个ID。当ASID溢出之后如何处理呢?这就需要一些软件的控制来协调处理。 我们用硬件支持上限为256个ASID的情景来描述这个基本的思路:当系统中各个cpu的TLB中的asid合起来不大于256个的时候,系统正常运行,一旦超过256的上限后,我们将全部TLB flush掉,并重新分配ASID,每达到256上限,都需要flush tlb并重新分配HW ASID。具体分配ASID代码如下:

static u64 new_context(struct mm_struct *mm, unsigned int cpu)
{
    static u32 cur_idx = 1;
    u64 asid = atomic64_read(&mm->context.id);
    u64 generation = atomic64_read(&asid_generation);

    if (asid != 0) {-------------------------(1)
        u64 newasid = generation | (asid & ~ASID_MASK); 
        if (check_update_reserved_asid(asid, newasid))
            return newasid; 
        asid &= ~ASID_MASK;
        if (!__test_and_set_bit(asid, asid_map))
            return newasid;
    }


    asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, cur_idx);---(2)
    if (asid != NUM_USER_ASIDS)
        goto set_asid;

    generation = atomic64_add_return_relaxed(ASID_FIRST_VERSION,----(3)
                         &asid_generation);
    flush_context(cpu);

    asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, 1); ------(4)

set_asid:
    __set_bit(asid, asid_map);
    cur_idx = asid;
    return asid | generation;
}

(1)在创建新的进程的时候会分配一个新的mm,其software asid(mm->context.id)初始化为0。如果asid不等于0那么说明这个mm之前就已经分配过software asid(generation+hw asid)了,那么new context不过就是将software asid中的旧的generation更新为当前的generation而已。

(2)如果asid等于0,说明我们的确是需要分配一个新的HW asid,这时候首先要找一个空闲的HW asid,如果能够找到(jump to set_asid),那么直接返回software asid(当前generation+新分配的hw asid)。

(3)如果找不到一个空闲的HW asid,说明HW asid已经用光了,这是只能提升generation了。这时候,多有cpu上的所有的old generation需要被flush掉,因为系统已经准备进入new generation了。顺便一提的是这里generation变量已经被赋值为new generation了。

(4)在flush_context函数中,控制HW asid的asid_map已经被全部清零了,因此,这里进行的是new generation中HW asid的分配。

3、进程切换过程中ARM64的tlb操作以及ASID的处理

代码位于arch/arm64/mm/context.c中的check_and_switch_context:

void check_and_switch_context(struct mm_struct *mm, unsigned int cpu)
{
    unsigned long flags;
    u64 asid;

    asid = atomic64_read(&mm->context.id); -------------(1)

    if (!((asid ^ atomic64_read(&asid_generation)) >> asid_bits) ------(2)
        && atomic64_xchg_relaxed(&per_cpu(active_asids, cpu), asid))
        goto switch_mm_fastpath;

    raw_spin_lock_irqsave(&cpu_asid_lock, flags); 
    asid = atomic64_read(&mm->context.id);
    if ((asid ^ atomic64_read(&asid_generation)) >> asid_bits) { ------(3)
        asid = new_context(mm, cpu);
        atomic64_set(&mm->context.id, asid);
    }

    if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending)) ------(4)
        local_flush_tlb_all();

    atomic64_set(&per_cpu(active_asids, cpu), asid);
    raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);

switch_mm_fastpath:
    cpu_switch_mm(mm->pgd, mm);
}

看到这些代码的时候,你一定很抓狂:本来期望支持ASID的情况下,进程切换不需要TLB flush的操作了吗?怎么会有那么多代码?呵呵~~实际上理想很美好,现实很骨干,代码中嵌入太多管理asid的内容了。

(1)现在准备切入mm变量指向的地址空间,首先通过内存描述符获取该地址空间的ID(software asid)。需要说明的是这个ID并不是HW asid,实际上mm->context.id是64个bit,其中低16 bit对应HW 的ASID(ARM64支持8bit或者16bit的ASID,但是这里假设当前系统的ASID是16bit)。其余的bit都是软件扩展的,我们称之generation。

(2)arm64支持ASID的概念,理论上进程切换不需要TLB的操作,不过由于HW asid的编址空间有限,因此我们扩展了64 bit的software asid,其中一部分对应HW asid,另外一部分被称为asid generation。asid generation从ASID_FIRST_VERSION开始,每当HW asid溢出后,asid generation会累加。asid_bits就是硬件支持的ASID的bit数目,8或者16,通过ID_AA64MMFR0_EL1寄存器可以获得该具体的bit数目。

当要切入的mm的software asid仍然处于当前这一批次(generation)的ASID的时候,切换中不需要任何的TLB操作,可以直接调用cpu_switch_mm进行地址空间的切换,当然,也会顺便设定active_asids这个percpu变量。

(3)如果要切入的进程和当前的asid generation不一致,那么说明该地址空间需要一个新的software asid了,更准确的说是需要推进到new generation了。因此这里调用new_context分配一个新的context ID,并设定到mm->context.id中。

(4)各个cpu在切入新一代的asid空间的时候会调用local_flush_tlb_all将本地tlb flush掉。

 

参考文献:

1、64-ia-32-architectures-software-developer-manual-325462.pdf

2、DDI0487A_e_armv8_arm.pdf

3、Linux 4.4.6内核源代码

 

原创文章,转发请注明出处。蜗窝科技

u-boot的makefile体系分析

$
0
0

u-boot的makefile体系分析

目录

0 引言    1

1 U-boot的Makefile体系分析    1

1.1 makefile体系的结构    1

1.2 顶层makefile    1

1.2.1 _all依赖关系    1

1.2.2 u-boot依赖关系    3

1.3 通用规则    5

1.3.1 Makefile.build    5

1.3.2 Makefile.clean    6

1.3.3 Makefile.lib    6

1.3.4 Kbuild.include    6

1.3.5 小结    6

1.4 arch/$(ARCH)/Makefile    7

2 如何利用通用makefile规则库    7

2.1 功能需求    8

2.2 功能实现简述    8

2.3 使用简介    9

3 结语    9

 

 

0 引言

本人之前也一直有接触过u-boot,但是总感觉还有不甚了解的地方;通过拜读wowo之前发表的《u-boot分析》博文,让我对u-boot的认识更加深入,能够站在一个全局的角度来理解u-boot的体系结构。但唯有makefile方面还不能整体上理解;于是有分析u-boot的makefile的想法,最终形成此文。另外,linux内核的makefile与u-boot的makefile思路一致,掌握他们的构建方法,不仅对于研究linux内核有益,而且也可以作为日常使用。

1 U-boot的Makefile体系分析

根据readme,uboot的makefile可以分为5个组成部分,如1.1节所示。实际上,本人认为分为两个部分更简明扼要:

  • 顶层makefile;
  • scrpits/目录下的通用规则。

顶层makefile总揽全局,决定需要编译的子目录;而通用规则依据各子目录的实际情况(子目录下的makefile配置)来决定编译内容。

1.1 makefile体系的结构

u-boot的makefile体系类似于linux内核,主要分为以下几个部分:

 图1-1 makefile体系结构.png

图1-1 makefile体系结构

  • 顶层 Makefile

它是所有Makefile文件的核心,从总体上控制着内核的编译、连接

  • arch/$(ARCH)/Makefile

对应体系结构的Makefile,它用来决定哪些体系结构相关的文件参与内核的生成,并提供一些规则来生成特定格式的内核映像

  • scripts/Makefile.*

Makefile公用的通用规则、脚本等

  • 子目录kbuild Makefiles

确切说,他们可能算不上makefile。各级子目录的Makefile相对简单,被上一层Makefile.build调用来编译当前目录的文件。

  • 顶层.config

配置文件,配置内核时生成。所有的Makefile文件(包括顶层目录和各级子目录)都是根据.config来决定使用哪些文件的

 

1.2 顶层makefile

1.2.1 _all依赖关系

(1)默认目标依赖关系

顶层makefile中,首先找到的目标是_all伪目标:

PHONY := _all

_all:

然后_all目标又依赖于all:

PHONY += all

_all: all

all自身依赖于$(ALL-y):

all:        $(ALL-y)

$(ALL-y)定义了多个目标:

# Always append ALL so that arch config.mk's can add custom ones

ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map u-boot.cfg binary_size_check

 

ALL-$(CONFIG_ONENAND_U_BOOT) += u-boot-onenand.bin

 

ifeq ($(CONFIG_SPL_FSL_PBL),y)

    ALL-$(CONFIG_RAMBOOT_PBL) += u-boot-with-spl-pbl.bin

else

    ifneq ($(CONFIG_SECURE_BOOT), y)

        ALL-$(CONFIG_RAMBOOT_PBL) += u-boot.pbl

    endif

endif

 

ALL-$(CONFIG_SPL) += spl/u-boot-spl.bin

ALL-$(CONFIG_SPL_FRAMEWORK) += u-boot.img

ALL-$(CONFIG_TPL) += tpl/u-boot-tpl.bin

ALL-$(CONFIG_OF_SEPARATE) += u-boot.dtb

 

ifeq ($(CONFIG_SPL_FRAMEWORK),y)    

    ALL-$(CONFIG_OF_SEPARATE) += u-boot-dtb.img

endif

 

ALL-$(CONFIG_OF_HOSTFILE) += u-boot.dtb

 

ifneq ($(CONFIG_SPL_TARGET),)

    ALL-$(CONFIG_SPL) += $(CONFIG_SPL_TARGET:"%"=%)

endif

 

ALL-$(CONFIG_REMAKE_ELF) += u-boot.elf

ALL-$(CONFIG_EFI_APP) += u-boot-app.efi

ALL-$(CONFIG_EFI_STUB) += u-boot-payload.efi

分析$(ALL-y)的定义:

a、ALL-y += u-boot.srec u-boot.bin u-boot.sym System.map u-boot.cfg binary_size_check是必选的;

b、ALL-$(xxxx)则是有条件才会选中。

另外。我们还可以看到spl的定义:

ALL-$(CONFIG_SPL) += spl/u-boot-spl.bin    

在定义CONFIG_SPL的情况下,将构建u-boot-spl.bin,《X-003-UBOOT-基于Bubblegum-96平台的u-boot移植说明》即是这种情况。

本文只分析$(ALL-y)必选的目标:u-boot.srec u-boot.bin u-boot.sym System.map u-boot.cfg binary_size_check。

(2)u-boot.srec

u-boot.hex u-boot.srec: u-boot FORCE

$(call if_changed,objcopy)

u-boot.srec依赖于u-boot和FORCE。

(3)u-boot.bin

ifeq ($(CONFIG_OF_SEPARATE),y)

u-boot-dtb.bin: u-boot-nodtb.bin dts/dt.dtb FORCE

$(call if_changed,cat)

 

u-boot.bin: u-boot-dtb.bin FORCE

$(call if_changed,copy)

else

u-boot.bin: u-boot-nodtb.bin FORCE

$(call if_changed,copy)

endif

可以看到,当使能uboot的device tree时,将增加对dts/dt.dtb的依赖。

(4)u-boot.sym

u-boot.sym: u-boot FORCE

$(call if_changed,sym)

u-boot.sym也依赖于u-boot和FORCE。

(5)System.map

System.map: u-boot

@$(call SYSTEM_MAP,$<) > $@

u-boot.map依赖于u-boot

(6)u-boot.cfg

u-boot.cfg: include/config.h FORCE

$(call if_changed,cpp_cfg)

u-boot.cfg依赖于include/config.h。

(7)binary_size_check

binary_size_check: u-boot-nodtb.bin FORCE

    … …

binary_size_check依赖于u-boot-nodtb.bin。

顶层makefile的这些依赖关系可以绘制为图1-2。

图1-2 u-boot顶层makefile依赖关系.png 

图1-2 u-boot顶层makefile依赖关系

1.2.2 u-boot依赖关系

u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE

$(call if_changed,u-boot__)

ifeq ($(CONFIG_KALLSYMS),y)

$(call cmd,smap)

$(call cmd,u-boot__) common/system_map.o

endif

通常CONFIG_KALLSYMS等于空。

则u-boot依赖于:

$(u-boot-init) $(u-boot-main) u-boot.lds FORCE

其中$(u-boot-init)和$(u-boot-main)被定义为:

u-boot-init := $(head-y)

u-boot-main := $(libs-y)

(1)$(u-boot-init),即head-y

head-y是定义在arch/$(ARCH)/Makefile文件中,该文件被顶层makefile包含:

include arch/$(ARCH)/Makefile:

head-y := arch/arm/cpu/$(CPU)/start.o

(2)$(u-boot-main),即libs-y

在整个源码下搜索"libs-y",可以发现它在两个文件中定义:

  • 顶层Makefile
  • arch/$(ARCH)/Makefile

首先定义的时候是类似"xxx/",然后经过一个函数加工为"xxx/built-in.o"。部分截图如下:

图1-3 libs-y部分内容

实际上,这些只是$(u-boot-init)和$(u-boot-main)的定义,而他们的依赖如下:

$(sort $(u-boot-init) $(u-boot-main)): $(u-boot-dirs) ;

继续看u-boot-dirs的定义:

u-boot-dirs    := $(patsubst %/,%,$(filter %/, $(libs-y))) tools examples

u-boot-dirs定义就是$(libs-y)除去最后一个'/'后,再加上tools和examples。

而它的依赖为:

PHONY += $(u-boot-dirs)

$(u-boot-dirs): prepare scripts

$(Q)$(MAKE) $(build)=$@

 

(3)u-boot.lds

u-boot.lds: $(LDSCRIPT) prepare FORCE

$(call if_changed_dep,cpp_lds)

u-boot.lds的依赖稍微麻烦:

$(LDSCRIPT) prepare FORCE

  • $(LDSCRIPT)

ifndef LDSCRIPT

#LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds.debug

ifdef CONFIG_SYS_LDSCRIPT

# need to strip off double quotes

LDSCRIPT := $(srctree)/$(CONFIG_SYS_LDSCRIPT:"%"=%)

endif

endif

 

# If there is no specified link script, we look in a number of places for it

ifndef LDSCRIPT

ifeq ($(wildcard $(LDSCRIPT)),)

LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds

endif

ifeq ($(wildcard $(LDSCRIPT)),)

LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.lds

endif

ifeq ($(wildcard $(LDSCRIPT)),)

LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds

endif

endif

该段脚本意思是优先使用LDSCRIPT;然后使用CONFIG_SYS_LDSCRIPT;最后再依次使用如下的lds文件:

$(srctree)/board/$(BOARDDIR)/u-boot.lds

$(srctree)/$(CPUDIR)/u-boot.lds

$(srctree)/arch/$(ARCH)/cpu/u-boot.lds

本人使用的zed板,就直接定义了CONFIG_SYS_LDSCRIPT="arch/arm/mach-zynq/u-boot.lds"。

  • prepare

该目标主要完成编译前的准备工作,其依赖关系如图1-4所示。

 

图1-4 prepare的依赖关系.png 

图1-4 prepare的依赖关系

 

  • FORCE

保证每次都重新生成目标。

(4)FORCE

保证每次都重新生成目标。

最后,可以归纳为图1-5所示的u-boot依赖关系图。

 图1-5 u-boot依赖关系图.png

图1-5 u-boot依赖关系图

其中的prepare参考图1-4。

1.3 通用规则

顶层makefile是special的,是专为"u-boot"定制的,而script目录下的通用规则,则是makefile提炼出的核心:

  • Makefile.build
  • Makefile.clean
  • Makefile.lib
  • Kbuild.include

1.3.1 Makefile.build

被顶层Makefile所调用,与各级子目录的Makefile合起来构成一个完整的Makefile文件,定义built-in.o、.lib以及目标文件.o的生成规则。这个Makefile文件生成了子目录的.lib、built-in.o以及目标文件.o

通过图1-5,要构建u-boot的核心步骤是构建$(u-boot-dirs)目标。再看看它的构建规则:

$(u-boot-dirs): prepare scripts

$(Q)$(MAKE) $(build)=$@

$(Q)$(MAKE) $(build)=$@中,$(Q)可以忽略,主要看$(build),它定义在script/Kbuild.include文件中,它被顶层makefile包含:

build := -f $(srctree)/scripts/Makefile.build obj

因而$(Q)$(MAKE) $(build)=$@可展开为:

make -f ./scripts/Makefile.build obj=$@

意思是执行./scripts/Makefile.build,传递的参数为obj=$@。

首先看Makefile.build的默认目标:

PHONY := __build

__build:

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \

$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \

$(subdir-ym) $(always)

@:

由于在根makefile下:

KBUILD_MODULES :=

KBUILD_BUILTIN := 1

因此__build的依赖是:

__build: $(builtin-target) $(lib-target) $(extra-y) $(subdir-ym) $(always)

@:

这些依赖与传递进来的obj参数相关。

大概的依赖关系如图1-6所示。

 图1-6 Makefile.build依赖关系示意图.png

图1-6 Makefile.build依赖关系示意图

从图1-6可知,目标最终,终于遍历到了源码文件。

Makefile.build总结:

首先根据传递进来的obj目录,包含obj目录下的Makefile或Kbuild(Kbuild优先)。通过该Makefile,获得obj-y/lib-y/extra-y变量值;

然后根据obj-y/lib-y/extra-y的值类型(xx.o或adc/),结合包含的Makefile.lib目录,对obj-y中的xxx/类型的目标,做变换获得:

    obj-y    := $(obj)/adc/built-in.o $(obj)/xx.o

    subdir-ym := adc

最后回到Makefile.build,来构建builtin-target := $(obj)/built-in.o,它依赖于$(obj)/adc/built-in.o $(obj)/xx.o。

a、根据$(obj)/%.o目标,可以编译出$(obj)/xx.o;

b、根据$(subdir-ym)目标,将嵌套执行:

            make -f ./scripts/Makefile.build obj=$(subdir-ym)

        在该命令下,将会有obj-y=adc-uclass.o,然后编译出adc/adc-uclass.o,链接出adc/built-in.o;

c、在a、b步骤完成后,就得到了builtin-target所需的$(obj)/adc/built-in.o $(obj)/xx.o,进一步链接为$(obj)/built-in.o,完成链接工作。

1.3.2 Makefile.clean

被顶层Makefile所调用,用来删除目标文件等

1.3.3 Makefile.lib

被Makefile.build所调用,主要完成对一些变量的处理:

  • subdir-ym

将obj-y中的目录赋值给subdir-ym。该subdir-ym目标将嵌套调用Makefile.build。

  • obj-y

将obj-y中的xx.o目标添加目录前缀:xxx/xx.o

1.3.4 Kbuild.include

被Makefile.build所调用,定义了一些函数,如if_changed、if_changed_rule、echo-cmd

1.3.5 小结

对于图1-7所示的编译目录,各个通用规则的编译示例如图1-8所示。

图1-7 编译目录

图1-8 通用规则编译示例.png

 

图1-8 通用规则编译示例

图1-8中的绿色框即是源文件了,通过源文件一步步即可构建出顶层目标。

图1-8看起来有点冗繁,图1-9看起来更简洁。

图1-9 通用规则示例.png 

图1-9 通用规则示例

1.4 arch/$(ARCH)/Makefile

该makefile被顶层makefile包含,主要作用是实现了head-y的赋值:

head-y := arch/arm/cpu/$(CPU)/start.o

head-y将被链接到特殊位置。

顺便还为顶层makefile的libs-y变量增加了值:

libs-y += arch/arm/cpu/$(CPU)/

libs-y += arch/arm/cpu/

libs-y += arch/arm/lib/

… …

这些变量全部为顶层makefile所使用。

2 如何利用通用makefile规则库

原计划是打算直接使用uboot的scripts/目录下的通用规则来编译我自己编写的简单c源码,可是折腾半天,发现挺复杂;倒不如依据前面对uboot的makefile体系的分析,重新编写一个更精简的makefile体系。

2.1 功能需求

(1)模仿uboot的编译模式:先在顶层makefile中指定需要编译的子目录(每个子目录构建一个built-in.o);然后在每个子目录下调用通用规则,生成该目录下的built-in.o。

(2)通用规则要满足以下功能:

a. 能自动遍历该子目录的子孙目录,并按秩序进行嵌套编译;

b. 可在该子目录的makefile里,进一步指定需要编译或不参加编译的源文件;

c. 同a,要能够嵌套的删除子孙目录下的生成的依赖文件、目标文件和built-in文件。

2.2 功能实现简述

依据上述的功能需求,参考uboot的makefile体系,编写了《my_prj3-v0.2》。

该实现基本按照uboot的makefile体系思路构成;不同之处是本人额外自己添加的自动生成目标文件的依赖关系,保证依赖能覆盖所有源文件和头文件。gcc的-MM选项再结合sed命令可实现上述功能。

测试的工程目录结构如图2-1所示。

图2-1 测试的工程目录结构

该工程由3个目录构成:app、lib1、lib2。其中lib2目录下还有另一个源码子目录:sub-func/。

Makefile和工程源码可自行下载参考,在此不再敖述。编译执行效果如图2-2所示。

如2-2 编译执行结果

清除命令的执行效果如图2-3所示。

图2-3 清除命令的执行结果

2.3 使用简介

以一个例子说明。

在2.2中的工程即是一个最简单的使用场景;如果需要增加lib3目录,并添加func3.c源文件,则需要做如下改动。

(1)顶层makefile的改动

在顶层makefile中增加:

libs-y += lib3/

(2)子目录makefile的改动

新建lib3/Makefile,内容如下:

obj-y += func3.o

如此即能将lib3目录添加到构建过程中来。

3 结语

本文首先分析了uboot的makefile,并详细描述了主要目标u-boot的依赖关系;然后依照uboot的思路,重新设计了一个简洁的makefile体系。

本文分析的内容是u-boot的makefile的一个最重要、最基本的功能,除此之外,它还有很多高大上的功能未分析。

ARM Linux上的系统调用代码分析

$
0
0

一、前言

当用户空间的程序调用swi指令发起内核服务请求的时候,实际上程序其实是完成了一次“穿越”,该进程从用户态穿越到了内核态。这个过程有点象周末你在家里看片,突然有些内急,随手按下了pause按键,电影里面的世界嘎然而止了。程序世界亦然,一个swi后,用户空间的代码执行暂停了、stack(用户栈)上的数据,正文段、静态数据区、heap去的数据……一切都停下来了,程序的执行突然就转入另外一个世界,使用的栈变成了内核栈、正在执行的正文段程序变成vector_swi开始的binary code、与之匹配数据区也变化了……

一切是怎么发生的呢?CPU只有一套而已,这里硬件做了哪些动作?软件又搞了什么鬼?穿越到另外的世界当然有趣,但是如何找到回来的路?这一切疑问希望能在这样的一篇文档中讲述清楚。

本文的代码来自4.4.6内核,用ARM处理器为例子描述。

 

二、构建内核栈上的用户现场

代码如下(忽略Cortex-M处理器的代码,忽略THUMB指令集的代码):

ENTRY(vector_swi)
    sub    sp, sp, #S_FRAME_SIZE
    stmia    sp, {r0 - r12}            @ Calling r0 - r12
ARM(    add    r8, sp, #S_PC        )
ARM(    stmdb    r8, {sp, lr}^        )    @ Calling sp, lr
    mrs    r8, spsr            @ called from non-FIQ mode, so ok.
    str    lr, [sp, #S_PC]            @ Save calling PC
    str    r8, [sp, #S_PSR]        @ Save CPSR
    str    r0, [sp, #S_OLD_R0]        @ Save OLD_R0

当执行vector_swi的时候,硬件已经做了不少的事情,包括:

(1)将CPSR寄存器保存到SPSR_svc寄存器中,将返回地址(用户空间执行swi指令的下一条指令)保存在lr_svc中

(2)设定CPSR寄存器的值。具体包括:CPSR.M = '10011'(svc mode),CPSR.I = '1'(disable IRQ),CPSR.IT = '00000000'(TODO),CPSR.J = '0'(),CPSR.T = SCTLR.TE(J和Tbit和Instruction set state有关,和本文无关),CPSR.E = SCTLR.EE(字节序定义,和本文无关)。

(3)PC设定为swi异常向量的地址

随后的行为都是软件行为了,因为代码中涉及压栈动作,所以首先要确定的就是当前在哪里这个问题。sp_svc早在进程切换的时候就已经设定好了,就是该进程的内核栈。

未命名

当Task A切换到Task B的时候,有一个很重要的步骤就是HW context的切换,由于Task A和Task B都在同一个CPU上运行,因此需要把当前CPU的各种寄存器以及状态信息保存在一块memory data block中(也就是硬件上下文了),并且用Task B的硬件上下文的数值来加载CPU,这里面就包括sp_svc。在内核态,完成进程切换后,最终会返回task B的用户空间执行,但是这时候Task B对应的内核栈(sp_svc0)是确定的了。

当通过系统调用进入内核的时候,内核栈已经是准备好了,不过这时候内核栈上是空的,执行完上述代码之后,在内核栈上形成如下的用户空间现场:

systemcall

代码我们就不走读了,很简单,大家可自行阅读即可。顺便一提的是:你看到这个保存的现场是不是觉得很熟悉?可以看看ARM中断处理这篇文档,中断保存的现场和系统调用是一样的。另外,保存在内核栈上的用户空间现场并不是全部的HW Context,HW context是一段内存中的数据,保存了某个时刻CPU的全部状态,不仅仅是core register,还有很多CPU内部其他的HW block状态,例如FPU的寄存器和状态。这时候,问题来了,在通过系统调用进入内核态的时候,仅仅保存core register够不够?够不够是和系统调用接口的约定相关,实际上,对于linux,我们约定如下:内核态的代码是不允许执行浮点操作指令的(这里用FPU例子,其他类似),如果一定要这样的话,那么需要在内核使用FPU的代码前后增加FPU上下文的保存和恢复的代码,确保在返回用户空间的时候,FPU的上下文是保持不变的。

最后一个有趣的问题是:为何r0被两次压栈?一个是r0,另外一个是old r0。其实在系统调用过程中,r0有两个角色,一个是传递参数,另外一个是返回值。刚进入系统调用现场的时候,old r0和r0其实都是保存了本次系统调用的参数,而在完成系统调用之后,r0保存了返回用户空间的return value。不过你可能觉得用一个r0就OK了,具体为何如此我们后面还会进行描述。

 

三、几个简单的初始化操作

代码如下:

zero_fp
alignment_trap r10, ip, __cr_alignment
enable_irq
ct_user_exit
get_thread_info tsk

zero_fp用来清除frame pointer,在debugger做栈的回溯的时候,当fp等于0的时候也就意味着到了最外层函数。对于kernel而言,来到这里,函数的调用跟踪就结束了,我们不可能一直回溯到用户空间的函数调用。上一节,我们说过了,硬件会关闭irq,在这里,我们通过enable_irq开启本cpu的中断处理。ct_user_exit和Context tracking subsystem相关的内容,这里就不深入了,关于对齐,可以多聊几句。ARM64的硬件是支持非对齐操作的,当然仅仅限于对normal memory的访问(对memory order没有要求的那些访问,例如exclusive load/store和load-acquire 或者 store-release 指令就不支持了)。由于取指而产生的内存访问或者是访问device type的memory都是必须要对齐的。当指令是非对齐的访问的时候,可以有两个选择(SCTLR_ELx.A控制):一个是产生fault,另外一个是执行非对齐访问(由硬件完成)。对内存的非对齐的访问在总线上被分解成两个transaction。所有的ARMv8的处理器的硬件都支持非对齐访问,因此,在ARM64应该不需要软件来实现非对齐的访问。

支持ARMv8的处理器当然不需要考虑对齐问题,不过对于ARM processor,有些硬件是不支持非对齐的访问的,这时候,内核配置(CONFIG_ALIGNMENT_TRAP)可以用软件的方法来实现非对齐访问(这是在硬件不支持的情况下的无奈之举),但是对性能的杀伤力极大,不到万不得已不能打开。具体代码很简单,这里就不说明了。

 

三、如何获取系统调用号?

系统调用有两种规范,一种是老的OABI(系统调用号来自swi指令中),另外一种是ARM ABI,也就是EABI(系统调用号来自r7)。如果想要兼容旧的OABI,那么我们需要定义OABI_COMPAT,这会带来一点系统调用的开销,同时让内核变大一点,对应的好处是使用旧的OABI规格的用户程序也可以运行在内核之上。当然,如果我们确定用户空间只是服从EABI规范,那么可以考虑不定义CONFIG_OABI_COMPAT。

相关的代码如下:

#if defined(CONFIG_OABI_COMPAT) 

USER(    ldr    r10, [lr, #-4]        )    @ get SWI instruction
ARM_BE8(rev    r10, r10)            @ little endian instruction

#elif defined(CONFIG_AEABI)

#else
    /* Legacy ABI only. */
USER(    ldr    scno, [lr, #-4]        )    @ get SWI instruction
#endif

如果是符合EABI规范,那么直接从r7中获取系统调用号即可,不需要特别的代码,因此CONFIG_AEABI的情况下,代码是空的。如果是老的规范,那么我们需要从SWI指令那里获取系统调用号,这时候,我们需要lr(实际上就是lr_svc,该寄存器保存了swi指令的下一条指令)来找到swi那一条指令码。

    uaccess_disable tbl

    adr    tbl, sys_call_table        @ load syscall table pointer

#if defined(CONFIG_OABI_COMPAT)
    bics    r10, r10, #0xff000000
    eorne    scno, r10, #__NR_OABI_SYSCALL_BASE
    ldrne    tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
    bic    scno, scno, #0xff000000        @ mask off SWI op-code
    eor    scno, scno, #__NR_SYSCALL_BASE    @ check OS number
#endif

取出swi指令中的低24bit就可以得出系统调用号,当然,对于EABI标准,我们使用r7传递系统调用号,因此陷入内核的时候永远使用“swi 0”这样的方式,因此,如果swi指令中的低24bit是0,则说明是服从EABI规范。

执行完上面的代码后,r7(scno)中保存了系统调用号,r8(tbl)中是syscall table pointer,通过r7和r8的值,我们已经知道后续的路该如何走了。

 

四、参数传递

使用swi指令的代码位于glibc中,我们可以大概把代码认为是如下的格式:

……

return value = swi( 参数1,参数2,……);

……

从这个角度看,系统调用和一个普通的c程序调用是类似的,都有参数和返回值的概念。当然,由于模式也发生了切换,因此这里的参数传递不能使用stack压栈的方式(swi产生了stack的切换),只能使用寄存器的方式。

对于ARM处理器,标准过程调用约定使用r0~r3来传递参数,其余的参数压入栈中。经过前面两个小节的描述,我们已经找到系统调用号和系统调用表,下面准备调用内核的系统调用函数,对于内核态的系统调用函数,其格式如下:

……

return value = sys_xxx( 参数1,参数2,……);

……

因此,我们还需要点代码来过渡到sys_xxx,如下:

local_restart:
    ldr    r10, [tsk, #TI_FLAGS]        @ check for syscall tracing
    stmdb    sp!, {r4, r5}            @ push fifth and sixth args

    tst    r10, #_TIF_SYSCALL_WORK        @ are we tracing syscalls?
    bne    __sys_trace

    cmp    scno, #NR_syscalls        @ check upper syscall limit
    badr    lr, ret_fast_syscall        @ return address
    ldrcc    pc, [tbl, scno, lsl #2]        @ call sys_* routine

我们这里需要模拟一个c函数调用,因此需要在栈上压入系统调用可能存在的第五和第六个参数(有些系统调用超过4个参数,他们使用r0~r5在swi接口上传递参数)。如果参数OK的话,那么ldrcc    pc, [tbl, scno, lsl #2]代码将直接把控制权交给对应的sys_xxx函数。需要注意的是返回地址的设定,我们无法使用bl这样的汇编指令,因此只能是手动设定lr寄存器了(badr    lr, ret_fast_syscall )。

 

五、返回用户空间

在返回用户空间之前会处理很多的事情,例如信号处理、进程调度等,这是通过检查struct thread_info中的flag标记来完成的,代码如下:

disable_irq_notrace            @ disable interrupts
ldr    r1, [tsk, #TI_FLAGS]        @ re-check for syscall tracing
tst    r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK
bne    fast_work_pending

restore_user_regs fast = 1, offset = S_OFF

fast_work_pending:
    str    r0, [sp, #S_R0+S_OFF]!        @ returned r0

……

这里面最著名的flag就是_TIF_NEED_RESCHED,有了这个flag,说明有调度需求。由此可知在系统调用返回用户空间的时候上有一个调度点。其他的flag和我们这里的场景无关,这里就不描述了,总而言之,如果需要有其他额外的事情要处理,我们需要跳转到fast_work_pending ,否则调用restore_user_regs返回用户空间现场。这里有一个小小的细节:如果需要有额外的事项处理(例如有pending signal),那么r0寄存器实际上会被破坏掉,也就破坏了sys_xxx函数的返回值,这时候,我们把r0保存到了用户现场(pt_regs)中的S_R0的位置,这也是为何pt_regs有S_R0和S_OLD_R0两个和r0相关的域。

恢复用户空间的代码(restore_user_regs )如下:

    mov    r2, sp
    ldr    r1, [r2, #\offset + S_PSR]    @ get calling cpsr
    ldr    lr, [r2, #\offset + S_PC]!    @ get pc
    msr    spsr_cxsf, r1            @ save in spsr_svc
    .if    \fast
    ldmdb    r2, {r1 - lr}^            @ get calling r1 - lr
    .else
    ldmdb    r2, {r0 - lr}^            @ get calling r0 - lr
    .endif
    mov    r0, r0                @ ARMv5T and earlier require a nop
                        @ after ldm {}^
    add    sp, sp, #\offset + S_FRAME_SIZE
    movs    pc, lr                @ return & move spsr_svc into cpsr

整个代码比较简单,就是用进入系统调用时候压入内核栈的值来进行用户现场的恢复,其中一个细节是内核栈的操作,在调用movs    pc, lr 返回用户空间现场之前,add    sp, sp, #\offset + S_FRAME_SIZE指令确保用户栈上是空的。此外,我们需要考虑返回用户空间时候的r0设置问题,毕竟它承载了本次系统调用的返回值,这时候的r0有两种情况:

(1)在没有pending work的情况下(fast等于1),r0保存了sys_xxx函数的返回值

(2)在有pending work的情况下(fast等于0),struct pt_regs(返回用户空间的现场)中的r0保存了sys_xxx函数的返回值

restore_user_regs还有一个参数叫做offset,我们知道,在进入系统调用的时候,我们把参数5和参数6压入栈上,因此产生了到pt_regs 8个字节的偏移,这里需要补偿回来。

 

原创文章,转发请注明出处。蜗窝科技

Linux系统如何标识进程?

$
0
0

一、前言

其实两年前,本站已经有了一篇关于进程标识的文档,不过非常的简陋,而且代码是来自2.6内核。随着linux container、pid namespace等概念的引入,进程标识方面已经有了天翻地覆的变化,因此我们需要对这部分的内容进行重新整理。

本文主要分成四个部分来描述进程标识这个主题:在初步介绍了一些入门的各种IDs基础知识后,在第三章我们描述了pid、pid number、pid namespace等基础的概念。第四章重点描述了内核如何将这些基本概念抽象成具体的数据结构,最后我们简单分析了内核关于进程标识的源代码(代码来自linux4.4.6版本)。

 

二、各种ID概述

所谓进程其实就是执行中的程序而已,和静态的程序相比,进程是一个运行态的实体,拥有各种各样的资源:地址空间(未必使用全部地址空间,而是排布在地址空间上的一段段的memory mappings)、打开的文件、pending的信号、一个或者多个thread of execution,内核中数据实体(例如一个或者多个task_struct实体),内核栈(也是一个或者多个)等。针对进程,我们使用进程ID,也就是pid(process ID)。通过getpid和getppid可以获取当前进程的pid以及父进程的pid。

进程中的thread of execution被称作线程(thread),线程是进程中活跃状态的实体。一方面进程中所有的线程共享一些资源,另外一方面,线程又有自己专属的资源,例如有自己的PC值,用户栈、内核栈,有自己的hw context、调度策略等等。我们一般会说进程调度什么的,但是实际上线程才是是调度器的基本单位。对于Linux内核,线程的实现是一种特别的存在,和经典的unix都不一样。在linux中并不区分进程和线程,都是用task_struct来抽象,只不过支持多线程的进程是由一组task_struct来抽象,而这些task_struct会共享一些数据结构(例如内存描述符)。我们用thread ID来唯一标识进程中的线程,POSIX规定线程ID在所属进程中是唯一的,不过在linux kernel的实现中,thread ID是全系统唯一的,当然,考虑到可移植性,Application software不应该假设这一点。在用户空间,通过gettid函数可以获取当前线程的thread ID。对于单线程的进程,process ID和thread ID是一样的,对于支持多线程的进程,每个线程有自己的thread ID,但是所有的线程共享一个PID。

为了方便shell进行Job controll,我们需要把一组进程组织起来形成进程组。关于这方面的概念,在进程和终端文档中描述的很详细,这里就不赘述了。为了标识进程组,我们需要引入进程组ID的概念。我们一般把进程组中的第一个进程的ID作为进程组的ID,进程组中的所有进程共享一个进程组ID。在用户空间,通过setpgid、getpgid、setpgrp和getpgrp等接口函数可以访问process group ID。

经过thread ID、process ID、process group ID的层层递进,我们终于来到最顶层的ID,也就是session ID,这个ID实际上是用来标识计算机系统中的一次用户交互过程:用户登录入系统,不断的提交任务(即Job或者说是进程组)给计算机系统并观察结果,最后退出登录,销毁该session。关于session的概念,在进程和终端文档中描述的也很详细,大家可以参考那份文档,这里就不赘述了。在用户空间,我们可以通过getsid、setsid来操作session ID。

 

三、基础概念

1、用户空间如何看到process ID

我们用下面这个block diagram来描述用户空间和内核空间如何看待process ID的:

getpid

从用户空间来看,每一个进程都可以调用getpid来获取标识该进程的ID,我们称之PID,其类型是pid_t。因此,我们知道在用户空间可以通过一个正整数来唯一标识一个进程(我们称这个正整数为pid number)。在引入容器之后,事情稍微复杂一点,pid这个正整数只能是唯一标识容器内的进程。也就是说,如果有容器1和容器2存在于系统中,那么可以同时存在两个pid等于a的进程,分别位于容器1和容器2。当然,进程也可以不在容器里,例如进程x和进程y,它们就类似传统的linux系统中的进程。当然,你也可以认为进程x和进程y位于一个系统级别的顶层容器0,其中包括进程x和进程y以及两个容器。同样的概念,容器2中也可以嵌套一个容器,从而形成了一个container hierarchy。

容器(linux container)是一个OS级别的虚拟化方法,基本上是属于纯软件的方法来实现虚拟化,开销小,量级轻,当然也有自己的局限。linux container主要应用了内核中的cgroup和namespace隔离技术,当然这些内容不是我们这份文档关心的,我们这里主要关心pid namespace。

当一个进程运行在linux OS之上的时候,它拥有了很多的系统资源,例如pid、user ID、网络设备、协议栈、IP以及端口号、filesystem hierarchy。对于传统的linux,这些资源都是全局性的,一个进程umount了某一个文件系统挂载点,改变了自己的filesystem hierarchy视图,那么所有进程看到的文件系统目录结构都变化了(umount操作被所有进程感知到了)。有没有可能把这些资源隔离开呢?这就是namespace的概念,而PID namespace就是用来隔离pid的地址空间的。

进程是感知不到pid namespace的,它只是知道能够通过getpid获取自己的ID,并不知道自己实际上被关在一个pid namespace的牢笼。从这个角度看,用户空间是简单而幸福的,内核空间就没有这么幸运了,我们需要使用复杂的数据结构来抽象这些形成层次结构的PID。

最后顺便说一句,上面的描述是针对pid而言的,实际上,tid、pgid和sid都是一样的概念,原来直接使用这些ID就可以唯一标识一个实体,现在我们需要用(pid namespace,ID)来唯一标识一个实体。

2、内核空间如何看到process ID

虽然从用户空间看,一个pid用一个正整数表示就足够了,但是在内核空间,一个正整数肯定是不行的,我们用一个2个层次的pid namespace来描述(也就是上面图片的情形)。pid namespace 0是pid namespace 1和2的parent namespace,在pid namespace 1中的pid等于a的那进程,对应pid namespace 0中的pid等于m的那进程,也就是说,内核态实际需要两个不同namespace中的正整数来记录一个进程的ID信息。推广开来,我们可以这么描述,在一个n个level的pid namespace hieraray中,位于x level的进程需要x个正整数ID来表示该该进程。

除此之外,内核还有记录pid namespace之间的关系:谁是根,谁是叶,父子关系……

 

四、内核态的数据抽象

1、如何抽象pid number?

struct upid {
    int nr;
    struct pid_namespace *ns;
    struct hlist_node pid_chain;
};

虽然用户空间使用一个正整数来表示各种IDs,但是对于内核,我们需要使用(pid namespace,ID number)这样的二元组来表示,因为单纯的pid number是没有意义的,必须限定其pid namespace,只有这样,那个ID number才是唯一的。这样,upid中的nr和ns成员就比较好理解了,分别对应ID number和pid namespace。此外,当userspace传递ID number参数进入内核请求服务的时候(例如向某一个ID发送信号),我们必须需要通过ID number快速找到其对应的upid数据对象,为了应对这样的需求,内核将系统内所有的upid保存在哈希表中,pid_chain成员是哈希表中的next node。

2、如何抽象tid、pid、sid、pgid?

struct pid
{
    atomic_t count;
    unsigned int level;
    struct hlist_head tasks[PIDTYPE_MAX];
    struct rcu_head rcu;
    struct upid numbers[1];
};

虽然其名字是pid,不过实际上这个数据结构抽象了不仅仅是一个thread ID或者process ID,实际上还包括了进程组ID和session ID。由于多个task struct会共享pid(例如一个session中的所有的task struct都会指向同一个表示该session ID的struct pid数据对象),因此存在count这样的成员也就不奇怪了,表示该数据对象的引用计数。

在了解了pid namespace hierarchy之后,level成员也不难理解,任何一个系统分配的PID都是隶属于某一个namespace的,而这个namespace又是位于整个pid namespace hierarchy的某个层次上,pid->level指明了该PID所属的namespace的level。由于pid对其parent pid namespace也是可见的,因此,这个level值其实也就表示了这个pid对象在多少个pid namespace中可见。

在多少个pid namespace中可见,就会有多少个(pid namespace,pid number)对,numbers就是这样的一个数组,表示了在各个level上的pid number。tasks成员和使用该struct pid的task们关联,我们在下一节描述。

3、进程描述符中如何体现tid、pid、sid、pgid?

由于多个task共享ID(泛指上面说的四种ID),因此在设计数据结构的时候我们要考虑两种情况:

(1)从task struct快速找到对应的struct pid

(2)从struct pid能够遍历所有使用该pid的task

在这样的要求下,我们设计了一个辅助数据结构:

struct pid_link
{
    struct hlist_node node;
    struct pid *pid;
};

其中node是将task串接到struct pid的task struct链表中的节点,而pid指向具体的struct pid。这时候,我们可以在task struct中嵌入一个pid_link的数组:

struct task_struct {
……
struct pid_link pids[PIDTYPE_MAX];
……
}

Task struct中的pids成员是一个数组,分别表示该task的tid(pid)、pgid和sid。我们定义pid的类型如下:

enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};

一直以来我们都是说四种type,tid、pid、sid、pgid,为何这里少定义一种呢?其实开始版本的内核的确是定义了四种type的pid,但是后来为了节省内存,tid和pid合二为一了。OK,现在已经引入太多的数据结构,下面我们用一幅图片来描述数据结构之间的关系:

pid-arch

对于一个进程中的多个线程而言,每一个线程都可以通过task->pids[PIDTYPE_PID].pid找到该线程对应的表示thread ID的那个struct pid数据对象。当然,任何一个线程都有其所属的进程,也就是有表示其process id的那个struct pid数据对象。如何找到它呢?这需要一个桥梁,也就是task struct中定义的thread group 成员(task->group_leader),通过该指针,一个线程总是很容易的找到其对应的线程组leader,而线程组leader对应的pid就是该线程的process ID。因此,对于一个线程,其task->group_leader->pids[PIDTYPE_PID].pid就指向了表示其process id的那个struct pid数据对象。当然,对于线程组leader,其thread ID和process ID的struct pid数据对象是一个实体,对于非线程组leader的那些普通线程,其thread ID和process ID的struct pid数据对象指向不同的实体。

struct pid有三个链表头,如果该pid仅仅是标识一个thread ID,那么其pid链表头指向的链表中只有一个元素,就是使用该pid的task struct。如果该pid表示的是一个process ID,那么pid链表头指向的链表中多个task struct,每一个元素表示了属于该进程的线程的task struct,链表中第一个task struct是thread group leader。如果该pid并不表示一个process group ID或者session ID,那么struct pid中的pgid链表头和session链表头都是指向null。如果该pid表示一个process group ID的时候,其结构如下图所示:

pgid-arch

对于那些multi-thread进程,内核有若干个task struct和进程对应,不过为了简单,在上面图片中,进程x 对应的task struct实际上是thread group leader对应的那个task struct。这些task struct的pgid指针(task->pids[PIDTYPE_PGID].pid)指向了该进程组对应的struct pid数据对象。而该pid中的pgid链表头串联了所有使用该pid的task struct(仅仅是串联thread group leader对应的那些task struct),而链表中的第一个节点就是进程组leader。

session pid的概念是类似的,大家可以自行了解学习。

4、如何抽象 pid namespace?

好吧,这个有点复杂,暂时TODO吧。

 

五、代码分析

1、如何根据一个task struct得到其对应的thread ID?

static inline struct pid *task_pid(struct task_struct *task)
{
    return task->pids[PIDTYPE_PID].pid;
}

同样的道理,我们也可以很容易得到一个task对应的pgid和sid。process ID有一点绕,我们首先要找到该task的thread group leader对应的task,其实一个线程的thread group leader对应的那个task的thread ID就是该线程的process ID。

2、如何根据一个task struct得到当前的pid namespace?

struct pid_namespace *task_active_pid_ns(struct task_struct *tsk)
{
    return ns_of_pid(task_pid(tsk));
}

这个操作可以分成两步,第一步首先找到其对应的thread ID,然后根据thread ID找到当前的pid namespace,代码如下:

static inline struct pid_namespace *ns_of_pid(struct pid *pid)
{
    struct pid_namespace *ns = NULL;
    if (pid)
        ns = pid->numbers[pid->level].ns;
    return ns;
}

一个struct pid实体是有层次的,对应了若干层次的(pid namespace,pid number)二元组,最顶层是root pid namespace,最底层(叶节点)是当前的pid namespace,pid->level表示了当前的层次,因此pid->numbers[pid->level].ns说明的就是当前的pid namespace。

3、getpid是如何实现的?

当陷入内核后,我们很容易获取当前的task struct(根据sp_svc的值),这是起点,后续的代码如下:

static inline pid_t task_tgid_vnr(struct task_struct *tsk)
{
    return pid_vnr(task_tgid(tsk));
}

通过task_tgid可以获取该task对应的thread group leader的thread ID,其实也就是process ID。此外,通过task_active_pid_ns亦可以获取当前的pid namespace,有了这两个参数,可以调用pid_nr_ns获取该task对应的pid number:

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
    struct upid *upid;
    pid_t nr = 0;

    if (pid && ns->level <= pid->level) {
        upid = &pid->numbers[ns->level];
        if (upid->ns == ns)
            nr = upid->nr;
    }
    return nr;
}

一个pid可以贯穿多个pid namespace,但是并非所有的pid namespace都可以检视pid,获取相应的pid number。因此,在代码的开始会进行验证,如果pid namespace的层次(ns->level)低于pid当前的pid namespace的层次,那么直接返回0。如果pid namespace的level是OK的,那么要检查该namespace是不是pid当前的那个pid namespace,如果是,直接返回对应的pid number,否则,返回0。

对于gettid和getppid这两个接口,整体的概念是和getpid类似的,不再赘述。

4、给定线程ID number的情况下,如何找对应的task struct?

这里给定的条件包括ID number、当前的pid namespace,在这样的条件下如何找到对应的task呢?我们分成两个步骤,第一个步骤是先找到对应的struct pid,代码如下:

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
    struct upid *pnr;

    hlist_for_each_entry_rcu(pnr,
            &pid_hash[pid_hashfn(nr, ns)], pid_chain)
        if (pnr->nr == nr && pnr->ns == ns)
            return container_of(pnr, struct pid,
                    numbers[ns->level]);

    return NULL;
}

整个系统有那么多的struct pid数据对象,每一个pid又有多个level的(pid namespace,pid number)对,通过pid number和namespace来找对应的pid是一件非常耗时的操作。此外,这样的操作是一个比较频繁的操作,一个简单的例子就是通过kill向指定进程(pid number)发送信号。正是由于操作频繁而且耗时,系统建立了一个全局的哈希链表来解决这个问题,pid_hash指向了若干(具体head的数量和内存配置有关)哈希链表头。这个哈希表用来通过一个指定pid namespace和id number,来找到对应的struct upid。一旦找了upid,那么通过container_of找到对应的struct pid数据对象。

第二步是从struct pid找到task struct,代码如下:

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
    struct task_struct *result = NULL;
    if (pid) {
        struct hlist_node *first;
        first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
                          lockdep_tasklist_lock_is_held());
        if (first)
            result = hlist_entry(first, struct task_struct, pids[(type)].node);
    }
    return result;
}

5、getpgid是如何实现的?

SYSCALL_DEFINE1(getpgid, pid_t, pid)
{
    struct task_struct *p;
    struct pid *grp;
    int retval;

    rcu_read_lock();
    if (!pid)
        grp = task_pgrp(current);
    else {
        retval = -ESRCH;
        p = find_task_by_vpid(pid);
        if (!p)
            goto out;
        grp = task_pgrp(p);
        if (!grp)
            goto out;

        retval = security_task_getpgid(p);
        if (retval)
            goto out;
    }
    retval = pid_vnr(grp);
out:
    rcu_read_unlock();
    return retval;
}

当传入的pid number等于0的时候,getpgid实际上是获取当前进程的process groud ID number,通过task_pgrp可以获取该进程的使用的表示progress group ID对应的那个pid对象。如果调用getpgid的时候给出了非0的process ID number,那么getpgid实际上是想要获取指定pid number的gpid。这时候,我们需要调用find_task_by_vpid找到该pid number对应的task struct。一旦找到task struct结构,那么很容易得到其使用的pgid(该实体是struct pid类型)。至此,无论哪一种参数情况(传入的参数pid number等于0或者非0),我们都找到了该pid number对应的struct pid数据对象(pgid)。当然,最终用户空间需要的是pgid number,因此我们需要调用pid_vnr找到该pid在当前namespace中的pgid number。

getsid的代码逻辑和getpid是类似的,不再赘述。

 

原创文章,转发请注明出处。蜗窝科技

Debian8 内核升级实验

$
0
0

一、前言

一直以来,我都是在使用一台ThinkPad T450 + Debian 8的机器来研究内核,Debian 8上缺省的内核版本是3.16,为什么不把内核升级到4.4.6版本上呢?反正现在蜗窝主要分析的也是这个版本的内核?

本文主要记录了整个升级过程,方便后续重复使用,哈哈,也许哪天要升级到8.8版本的内核呢,到时候可以把这份文档调出来轻松升级。

 

二、搞清楚/boot目录下的东西

首先列车/boot目录下的文件如下:

-rw-r--r-- 1 root root   157726 Feb 29  2016 config-3.16.0-4-amd64
drwxr-xr-x 5 root root     4096 Apr  1  2016 grub
-rw-r--r-- 1 root root 16873678 Sep 26 16:14 initrd.img-3.16.0-4-amd64
-rw-r--r-- 1 root root  2676357 Feb 29  2016 System.map-3.16.0-4-amd64
-rw-r--r-- 1 root root  3119232 Feb 29  2016 vmlinuz-3.16.0-4-amd64

vmlinuz-3.16.0-4-amd64是Debian 8使用的kernel image,这个image是如何配置的呢?config-3.16.0-4-amd64就是对应的内核配置文件,System.map-3.16.0-4-amd64是对应的内核符号表,调试内核的时候也许会用到它。initrd.img-3.16.0-4-amd64是内核使用的inital ramdisk image。

OK,收集了这些信息后,我们先理一理思路:要想升级到4.4.6内核,估计必须要提供上面四个文件,配置文件可以沿用,并且用这个配置文件编译出一个新版本的内核image,initrd image有点麻烦,可以考虑沿用,也可以看看是否可以生成一个。符号表文件最简单,编译好4.4.6内核自然就有了。搞定!

 

三、编译4.4内核

1、准备好4.4.6的源代码

去kernel.org上去下载一个linux-4.4.6.tar.xz,然后解压在自己的工作目录,例如:/home/linuxer/work/linux-4.4.6/。

xd –d linux-4.4.6.tar.xz

tar –xf linux-4.4.6.tar

2、准备好内核配置文件

自己生成一个配置文件比较费事,借用当前kernel的配置文件是一个不错的主意。

cd /home/linuxer/work/linux-4.4.6

cp /boot/config-3.16.0-4-amd64 ./.config

3、配置内核

make olddefconfig

从3.16到4.4,内核的配置项一定会有变化,因此使用旧的内核配置文件存在这样的问题:4.4内核中新的配置项如何选择?如果你的内心足够强大,可以考虑使用make oldconfig,这样,那些新的配置项就可以逐一列出了并请你进行配置。当然,我是没有那么多耐心了,直接使用make olddefconfig,让那些新增加的配置项选择default值吧。

4、生成内核包

make clean

make deb-pkg

这条命令是用来编译kernel package的。输完该命令后,我建议你可以外出走走,上个厕所,喝杯茶,考虑一下诗和远方什么的,反正随后的一段时间,你的计算机屏幕只是有一行行的字符不断的翻滚而已。当一切归于平静的时候,在kernel source目录的上一级目录可以看到不少的*.deb的包文件,当然,最重要的就是那个linux-image-4.4.6_4.4.6-2_amd64.deb,这个是内核安装包。

 

四、安装内核包

下面的命令用来安装内核包:

dkpg -i linux-image-4.4.6_4.4.6-2_amd64.deb

作为一个有情怀的工程师,我们当然想知道到底安装了哪些文件,这个信息其实可以从/var/lib/dpkg/info/linux-image-4.4.6.list文件中得到,我们简单整理如下:

(1)/boot目录下的内核镜像(vmlinuz-4.4.6)、内核配置文件(config-4.4.6)和内核符号(System.map-4.4.6)

(2)内核的模块(/lib/modules/4.4.6/*)

(3)一些内核安装相关的处理脚本(/etc/kernel/*)。主要是和initrd image以及grub更新相关的工作。通过这些脚本生成了/boot目录下的initrd.img-4.4.6文件(哈哈,initrd image的问题解决了)并且修改了/boot/grub/menu.lst文件。

 

五、加载内核

很遗憾,安装内核package之后重启系统,一切都没有什么变化,仍然是旧的内核,稍微看了看网上的文章,似乎是和grub的配置相关。升级内核这事以前也做过,对于grub而言,修改menu.lst就OK了,现在的grub2似乎变化了,其配置文件是grub.cfg,而且建议不要手工修改,好吧,看来还是要更新grub的知识啊。

grub的配置文件包括:

(1)/boot/grub/grub.cfg

(2)/etc/grub.d/

(3)/etc/default/grub

有一个能够自动生成配置文件的命令grub-mkconfig,我们可以用这个命令来生成配置文件:

grub-mkconfig –o /boot/grub/grub.cfg

执行完该命令后,系统会扫描boot目录的内容,以及etc目录下的配置文件,最终生成一个可以使用的grub.cfg文件。OK,现在可以重启享受4.4.6内核吧。

 

原创文章,转发请注明出处。蜗窝科技

Viewing all 218 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>