Big Ben

一个半吊子的编码爱好者

0%

memblock是Linux启动阶段的一种内存管理方式,是对bootmem的补充,是为了应对bootmem易产生外部碎片而产生。他的初始化函数arm_memblock_init和bootmem初始化所在的函数paging_init紧挨着,都在setup_arch中。
struct memblock {
    phys_addr_t current_limit;
    phys_addr_t memory_size;    /* Updated by memblock_analyze() */
    struct memblock_type memory;
    struct memblock_type reserved;
};

current_limit:表示memblock可分配的内存地址的上限值,其初始化的过程为
               paging_init -> sanity_check_meminfo -> memblock_set current_limit(limit)
               其中limit = __pa(vmalloc_min-1) + 1 = VMALLOC_END - 128M = 4G - 128M(high mem)
memory_size:记录总的内存数量,在memory_analyze中更新
memory,reserved:是两张表格,前者是系统所拥有的物理内存块,后者可以理解为已分配的内存块

1. 初始化
     1)mdesc->reserve是在arch/arm/mach-omap2/board-d7800.c中分配给machine_desc变量。
     2)meminfo的初始化可以参考另外一篇笔记meminfo初始化

2.内存分配
     1)MEMBLOCK_ALLOC_ACCESSIBLE的值为0,则后面的max_addr也为0,但并不意味着搜索的上限也为0。在memblock_find_base中end,也就是搜索的上限(其实是搜索的起始值,因为我们的搜索时从内存的最高值开始的,这么做是有用意的,详见第2点)赋为memblock.current_limit,它的值可以参考本文前面的介绍。
     2)为什么是从内存的高地址开始搜索,mm/memblock.c中的有注释道
         /* We do a top-down search, this tends to limit memory
     * fragmentation by keeping early boot allocs near the
     * top of memory
     */
看起来是为了解决碎片的问题,但为什么能解决,不清楚。
     3)代码中的跟内存区的始末位置有关的值都进行了对齐,用到如下的语句
               size = memblock_align_up(size, align)
         base = memblock_align_down((end - size), align)
          这些也都是为了解决碎片问题,mm/memblock.c 中有注释,道
         /* We align the size to limit fragmentation. Without this, a lot of
     * small allocs quickly eat up the whole reserve array on sparc
     */
     4)memblock_addrs_overlap中判断交叠的方法很值得学习。

1. u-boot中设置启动参数
     1)启动命令
          u-boot-env.txt
          bootcmd 
               if ${recovery}; then 
                    run nandrecovery; 
               else 
                    if mmc init; then 
                         if run loadbootscript; then 
                              run bootscript; 
                         else 
                              if run loaduImage; then 
                                   run mmcboot; 
                              else 
                                   run nandboot; 
                              fi; 
                         fi;
                    else 
                         run nandboot; 
                    fi; 
               fi
          nandboot 
               echo Booting from nand ...; 
               run nandargs; 
               nand read ${loadaddr} boot; 
               booti 0x82000008
          nandargs
                              setenv bootargs console=${console} init=${init} mem=${mem} ip=${ip} mpurate=${mpurate} omap_vout.vid1_static_vrfb_alloc=y vram=${vram} omapfb.vram=0:8M androidboot.console=${tty} root=${root_nand} rootfstype=${rootfstype_nand} ubi.mtd=${ubi_mtd}

     这一部分属于commandline,会在setup_commandline_tag中插入params数组,并标上ATAG_CMDLINE,会在parse_args的时候,用对应的处理函数来处理,具体如下
     
     2)环境变量
          include/configs/d7800.h
     #define CONFIG_EXTRA_ENV_SETTINGS \
    "init=/init\0" \
    "mem=256M\0" \
    "mpurate=800\0" \
   ……

2.kernel中用parse_args解析此bootarg,具体可参考另一份笔记Linux启动参数“init=xxx”分析
     parse_args -> parse_one -> unknown_bootoption -> obsolete_checksetup -> obs_kernel_param.setup_func = early_mem

3.kernel中设置对应bootarg的处理函数
     early_param("mem", early_mem); -> __setup_param(str, fn, fn, 1) -> 
     #define __setup_param(str, unique_id, fn, early)            \
     static const char __setup_str_##unique_id[] __initconst \
        __aligned(1) = str; \
     static struct obs_kernel_param __setup_##unique_id  \
        __used __section(.init.setup)           \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_##unique_id, fn, early }   

4.early_mem函数
          static int __init early_mem(char *p)
{
    static int usermem __initdata = 0;
    unsigned long size, start;
    char *endp;
    /*
     * If the user specifies memory size, we
     * blow away any automatically generated
     * size.
     */

    if (usermem == 0) {
        usermem = 1;
        meminfo.nr_banks = 0;
    }
        /*
     defined in arch/arm/plat-omap/include/plat/memory.h
     * PHYS_OFFSET  = UL(0x80000000)

     */
    start = PHYS_OFFSET;
    size  = memparse(p, &endp);
    if (*endp == '@')
        start = memparse(endp + 1, NULL);
    arm_add_memory(start, size);
    return 0;
}

最后遗留一个问题,假如有几块内存需要处理怎么办?
     没关系,Linux提供了对应的机制。就是__tagtable(ATAG_MEM, parse_tag_mem32);这是一张表格,代表了一组ATAG_MEM param,用parse_tag_mem32来处理。这一组param同样也是在u-boot阶段就加入了。即:
     do_bootm_linux -> setup_memory_tags (u-boot)
static void setup_memory_tags (bd_t *bd)
{
    int i;

    for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
        params->hdr.tag = ATAG_MEM;
        params->hdr.size = tag_size (tag_mem32);

        params->u.mem.start = bd->bi_dram[i].start;
        params->u.mem.size = bd->bi_dram[i].size;

        params = tag_next (params);
    }
}

     只不过在D7800中CONFIG_NR_DRAM_BANKS=1(configs/d7800.h)。

     meminfo中的内存地址全部是真正的物理地址。


     

1. device,configuration,interface,endpoint
我的理解:
     1)首先,一个device包含多个configuration,
                    一个configuration包含多个interface
                    一个interface使用多个endpoint来完成功能
     2)其次,interface对应到USB device的功能,可以理解为一个独立的逻辑设备。一个configuration就包含了多个这样的逻辑设备,它实际上就对应了某一个时刻的USB device。也就是说一个USB device在不同的时刻,需要有不同的用途,而这个用途由多个逻辑设备来达成。举例来说:一些USB设备需要下载固件到其上,则这些设备在不同的时候扮演不同的角色(存储设备,或者无线网卡等等)。

     usb_interface -- usb_host_interface (altsetting) -- usb_interface_descriptor -- bAlternateSetting:altsetting编号
                   |                                  |-                          + bInterfaceNumber:interface编号
                   |                                  |- extra: following usb_host_interface_descriptor in this interface (hence usb_interface)
                   |                                  |- usb_host_endpoint: 此配置下的endpoint数组,其中包含了每个ep的desc和urb_list
                   |- usb_host_interface (altsetting[1])
     3)如上图,每个interface可以包含多种配置,每一个配置是一个altsetting,类型是usb_host_interface,每个altsetting包含了一个descriptor(记录了当前interface和当前interface的altsetting的信息),和一组endpoint(包含了endpoint的descriptor和对应的urb_list)。
          Alternate settings allow a portion of the device configuration to be varied while other interfaces remain in operation.  -- USB 2.0 spec.
          The default setting for an interface is always alternate setting zero.The SetInterface() request is used to select an alternate setting or to return to the default setting.   -- USB 2.0 spec.
     4)注意不同的DescriptorType值,会有不同descriptor的格式


2. USB设备间的通讯的基本单位为endpoint,有4种ep,对应到4中USB传输类型:control,bulk,interrupt,isochronous。所谓的pipe,也就是指定了的目的ep,很形象的,可以想象成一个管道。表征起来就是一个int类型,包含了目的ep信息(8位)和一些其他信息。

3. USB sysfs名字解析
sysfs中只显示了和当前配置和接口,所有可选的其他配置可以通过查看usbfs(/proc/bus/usb)来获得需要的信息

4. 1)USB设备之间的通信通过urb来实现,这是一种异步通信的方式,也可以通过同步通信的方式,即绕开urb直接传输数据,这种同步方式只有两个函数可以支持,分别为usb_bulk_msg和usb_control_msg。
    2)控制,中断,批量,等时四种urb只有等时urb需要自己手动的初始化。其他的均可以通过形为usb_fill_xxx_urb的函数来作初始化。
    3)通过usb_submit_urb来提交urb给USB core。urb被处理完后会调用urb注册的complete回调函数做结束工作。
5. usb_driver通过usb_device_id数组告诉USB core,本驱动支持什么样的USB设备。当有设备插入时,USB core来判断哪个驱动支持该设备,找到了之后,调用usb_driver->probe回调函数。并在退出时调用disconnect回调函数。

6. 为了使用传统的字符驱动程序的接口,可以调用usb_register_dev来注册USB设备



vmalloc调用流程:

2.6内核的工作队列做了很大的修改,新的工作队列机制被称为Concurrency Managed Work Queue(并发管理工作队列,简称cmwq)。其主旨是增强多核时的处理性能。

cmwq 的实现遵循了以下几个原则:

  • 与原有的工作队列接口保持兼容,cmwq 只是更改了创建工作队列的接口,很容易移植到新的接口(__create_workqueue -> alloc_workqueue)。
  • 工作队列共享 per-CPU 的线程池,提供灵活的并发级别而不再浪费大量的资源。
  • 自动平衡工作者线程池和并发级别,这样工作队列的用户不再需要关注如此多的细节。
create_workqueue (create_singlethread_workqueue) -> alloc_workqueue -> __alloc_workqueue_key
struct workqueue_struct*alloc_workqueue(char *name, unsigned int flags, int max_active);

其中:

name为工作队列的名字,而不像 2.6.36 之前实际是为工作队列服务的内核线程的名字。

flag 指明工作队列的属性,可以设定的标记如下:

  • WQ_NON_REENTRANT:默认情况下,工作队列只是确保在同一 CPU 上不可重入,即工作项不能在同一 CPU 上被多个工作者线程并发执行,但容许在多个 CPU 上并发执行。但该标志标明在多个 CPU 上也是不可重入的,工作项将在一个不可重入工作队列中排队,并确保至多在一个系统范围内的工作者线程被执行。
  • WQ_UNBOUND:工作项被放入一个由特定 gcwq 服务的未限定工作队列,该客户工作者线程没有被限定到特定的 CPU,这样,未限定工作者队列就像简单的执行上下文一般,没有并发管理。未限定的 gcwq 试图尽可能快的执行工作项。
  • WQ_FREEZEABLE:可冻结 wq 参与系统的暂停操作。该工作队列的工作项将被暂停,除非被唤醒,否者没有新的工作项被执行。
  • WQ_MEM_RECLAIM:所有的工作队列可能在内存回收路径上被使用。使用该标志则保证至少有一个执行上下文而不管在任何内存压力之下。
  • WQ_HIGHPRI:高优先级的工作项将被排练在队列头上,并且执行时不考虑并发级别;换句话说,只要资源可用,高优先级的工作项将尽可能快的执行。高优先工作项之间依据提交的顺序被执行。
  • WQ_CPU_INTENSIVE:CPU 密集的工作项对并发级别并无贡献,换句话说,可运行的 CPU 密集型工作项将不阻止其它工作项。这对于限定得工作项非常有用,因为它期望更多的 CPU 时钟周期,所以将它们的执行调度交给系统调度器。

max_active决定了一个 wq 在 per-CPU 上能执行的最大工作项。比如 max_active 设置为 16 表示一个工作队列上最多 16 个工作项能同时在 per-CPU 上同时执行。当前实行中,对所有限定工作队列,max_active 的最大值是 512,而设定为 0 时表示是 256;而对于未限定工作队列,该最大值为:MAX[512,4 * num_possible_cpus() ],除非有特别的理由需要限流或者其它原因,一般设定为 0 就可以了。

在之前的代码中,一些用户依赖于 ST 中的严格执行顺序,这种行为在 cmwq 中可以将 max_active 设为 1,flag 设置为 WQ_UNBOUND 来获得相同的行为

cmwq 本质上是提供了一个公共的内核线程池的实现,其接口基本上和以前保持了兼容,只是更改了创建工作队列的函数的后端,它实际上是将工作队列和内核线程的一一绑定关系改为由内核来管理内核线程的创建,因此在 cmwq 中创建工作队列并不意味着一定会创建内核线程。



内核实现工作队列的基本框架如下图所示:
1. gcwq封装了线程池,worker封装了工作者线程,work就是工作者线程真正在处理的事务。
2. 针对每一个CPU,内核用一个数据结构global_cwq来表示cmwq,即gcwq,还有一个未和CPU绑定的gcwq,所以总共gcwq的数量为CPU数目+1.
3. gcwq中有三个重要的list,分别是: 1) 管理空闲worker的idlelist; 2)管理正在工作的worker的busy_hash; 3) 以及本CPU上等待处理的work列表worklist
4. worker才是Linux调度器进行调度的基本单位,worker有一个成员task_struct,调度器利用此结构来调度worker_thread,决定他何时休眠,合适唤醒

工作队列的工作原理
workqueue有两种形式:
1. ST (Single Thread):只在创建它的CPU上有cwq与之对应
2. MT (Multi Thread):在每个active CPU上均有cwq与之对应,对应到每个CPU上,有一个cpu_workqueue_struct与之对应,即cwq

wq与cwq的对应关系是在__alloc_workqueue_key中实现的:
1. 通过alloc_cwqs创建一组per-CPU变量
2. 用一个for_each_cwq_cpu循环遍历所有CPU,并为每个CPU的cwq结构初始化,指定cwq->gcwq和cwq->wq

work的处理流程如下图所示:


work在各个list之间的转换的关系如下图所示:


work如何处理: process_scheduled_work()
1. 在busy_hash table中找,是否有thread正在处理当前事务。如果有,则将本事务转移到那个worker thread的scheduled列表上,以保证同一个work不在同一CPU的不同thread上并发。
2. 配置worker->current_work = work, worker->current_cwq = cwq, 记录此时在处理的是哪个workqueue上的哪个work。由于此时gcwq->lock是锁上的,所以和其他本地thread同步没有问题
3. 记录CPU number到work->data,从scheduled列表中删除该work
4. 处理gcwq有GCWQ_HIGHPRI_PENDING标志的情况,加入下一个work仍然是高优先级,则去叫醒idle的worker来处理,如果没有则继续。保证在有高优先级pending的时候,尽量多的worker参与处理。
5. 解锁gcwq->lock
6. 为work清除pending标志,开始真正的处理该work,调用work->func
7. 锁住gcwq->lock
8. 做一些完成时的更新:1) 从busy_hash中删除该worker; 2) 将current_work和current_cwq赋为NULL

queue_work如果不指定cpu id的话,会默认调用get_cpu()获得当前thread所在的CPU号(也就是queue_work所在的CPU,或者说提交work所在的CPU),并将work加到CPU对应的gcwq的worklist上,以待进一步处理。

工作者线程如何调度
所有的线程通过gcwq->lock做同步,当在拷贝列表,以及前期处理时(修改标志位,manage worker),必须要获得锁,但真正处理时(可能是最耗时的操作),锁是释放状态的。当线程处理完以后,线程标志位设为TASK_INTERRUPTIBLE,并调用schedule(),以交出控制权,等待调度器将其唤醒,唤醒后,立即进入woke_up分支,并首先获得gcwq->lock锁。



 

spin_lock_irq & spin_unlock_irq
在获得锁之前会禁止中断,释放锁时会使能中断,而不管在调用此函数前中断状态是怎么样的
spin_lock_irqsave & spin_unlock_irqrestore
类似spin_lock_irq,但会记住调用此函数之前的中断状态(是否使能),在释放锁时进行恢复
spin_lock_bh & spin_unlock_bh
会在请求锁的时候,禁止软中断(bh = bottom half,底半部)

三种环境:
进程上下文,软中断,硬中断
优先级依次增高,即后者能将前者打断,所以在低优先级环境中请求共享资源时,需要屏蔽掉高优先级运行,可以调用相应的spin_lock函数来达到这个目的。如果不这样做,就会发生死锁。
举例:A进程用spin_lock获得了某一资源,还未释放,此时中断来了,进入中断处理流程,若此时需要操作共享资源,则必须要用spin_lock请求锁,但这时假如进程A和中断处理流程在同一CPU上,则A没有机会再释放锁,导致中断处理流程中死锁。所以,进程A中必须要在获取锁之前屏蔽中断(只需屏蔽本地中断即可,因为假如中断处理流程在另外一个CPU上时,换一种说法,就是另外一个CPU产生了中断,其实和本CPU没有关系,进程A的操作不受影响,可以顺利释放锁)。

因为在不同的CPU上出现中断不会导致

进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会

获得CPU,执行并退出临界区。


*注:spin_lock_irq包含了local_irq_disable(禁止中断),preempt_disable(禁止内核抢占),以及spin_lock本身的操作。

假如只考虑多CPU同步的情况,spin_lock足够,因为不同CPU上运行的软/硬中断,对本地CPU并没有影响。

入口点在head.S中,软件执行的第一条指令从这里开始

__HEAD  // 0xc000_8000
ENTRY(stext)

……

ENDPROC(stext)

从head-common.S中跳转到第一个C函数start_kernel

基本流程如下图所示:

 

 



1. UBOOT部分
U-Boot使用了一个结构体gd_t来存储全局数据区的数据。UBOOT通过以下语句,将一个指向这个结构体放在r8寄存器中,此时,r8不会再做他用
DECLARE_GLOBAL_DATA_PTR
#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")
在start_armboot中对该指针进行初始化
gd = (gd_t*)(_armboot_start - CONFIG_SYS_MALLOC_LEN - sizeof(gd_t));
在bootm command中对tag结构进行初始化。
run_command("bootm") -> do_bootm_linux -> setup_start_tag -> setup_start_tag(初始化params区域(数组),params = bd->bi_boot_params = (OMAP34xx_SDRC_CS0+0x100) = 0x8000_0100,d7800.c/board_init)
->……->setup_commandline_tag (将环境变量bootargs放入params数组,其tag为ATAG_CMDLINE)
        params数组以tag为ATAG_NONE的tag作结束。params其实就是一组struct tag,定义为:
struct tag {
     struct tag_header hdr;
     union { // 各种tag
     ……
     struct tag_cmdline  cmdline;
     ……
     }u;
}

2. kernel部分
在board-d7800.c中对mdesc进行初始化
MACHINE_START(D7800, "HSM D7800 Board")                                                                                 
     /* Maintainer: Syed Mohammed Khasim - Texas Instruments */                                                          
     .boot_params    = 0x80000100,  // 与UBOOT一致                                                                                      
     .map_io     = omap3_map_io,                                                                                         
     .reserve    = omap_reserve,                                                                                         
     .init_irq   = d7800_init_irq,                                                                                       
     .init_machine   = d7800_init,                                                                                       
     .timer      = &omap_timer,                                                                                          
MACHINE_END 
在setup_arch(&command_line);中引用parse_tags(tags),以tag.hdr.size = 0为循环终止条件,遍历params区域(即struct tag数组),并通过该param的tag(ATAG_xxx)在.taglist.init区域中寻找合适的处理函数。对于ATAG_CMDLINE,其对应的处理函数即为parse_tag_cmdline,即将tag->u.cmdline.cmdline copy到default_command_line数组中,在setup_arch函数中,就是from数组。
tag->u.cmdline.cmdline => default_command_line => boot_command_line => comdline_p => command_line => static_command_line (setup_command_line中实现)
对于.taglist.init的配置,是通过引用一系列的如下的宏来实现的
__tagtable(ATAG_CORE, parse_tag_core);
__tagtable(ATAG_MEM, parse_tag_mem32);
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
这些宏其实就是将一组struct tagtable放到.taglist.init区域中,此区域可以用__taglist_begin, __taglist_end来表示

在start_kernel中,系统调用以下函数,真正 的处理params
parse_args("Booting kernel", static_command_line, __start___param,
            __stop___param - __start___param,
            &unknown_bootoption);
该函数从static_command_line中取param出来,并用parse_one处理。parse_one会先以param的name去__param section中查找是否已经有此变量的定义,
1)如果有此定义,则直接调用该param的kernel_param_ops(例如param_ops_bool = {.set = param_set_bool; .get = param_get_bool})
2)如果没有定义,则调用handle_unknown,即unknown_bootoption
unknown_bootoption -> obsolete_checksetup(param)
obsolete_checksetup函数会遍历__setup_start~__setup_end区域,假如在此区域中找到param对应的obs_kernel_param,则调用该obs_kernel_param的setup_func(其实init参数就是在此时被处理的)。
3)如果param在__setup_start~__setup_end中仍然没有对应的处理的话,则会放到envp_init中

* obsolete是过时的意思,从中可以看出linux中认为param参数是以后主要使用的启动参数传递方式,将慢慢摒弃__setup的形式,通过module_param的方式可以在启动参数中为驱动的参数赋值,这种赋初值的方式将成为主流
* 关于__setup_start~__setup_end区域的初始化,参考以下内容
init/main.c里有如下一行
__setup("init=", init_setup);
展开后
__setup_param(str, fn, fn, 0)
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }

一些编译参数:

__initconst: __section(.init.rodata),将该变量放入名字为“.init.rodata”的段中

__used:告诉编译器无论 GCC 是否发现这个函数的调用实例,都要使用这个函数。这对于从汇编代码中调用 C 函数有帮助

__attribute__:gcc的标志,为函数和变量设置属性,以增强编译的功能,可用编译时参数WALL打开


在start_kernel->setup_arch中对启动参数的处理:
1.mdesc = setup_machine(machine_arch_type);所获得的machine_desc没有fixup成员。其定义在arch/arm/mach-omap/board-d7800.c中
2.依上,则char *from = default_command_line = CONFIG_CMDLINE = "root=/dev/mmcblk0p2 rootwait console=ttyO2,115200"
3.strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
4.strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
5.*cmdline_p = cmd_line; parse_early_param();

start_kernel调用
parse_args("Booting kernel", static_command_line, __start___param,
            __stop___param - __start___param,
            &unknown_bootoption);
来处理,定义在以下段中的param
__start___param = .; *(__param);  __stop___param = .

gd指针只有在需要使用它的函数中定义为一个局部变量:
DECLARE_GLOBAL_DATA_PTR
因为在u-boot中,这个指针使用一个固定的寄存器来保存它,所以即使是局部变量,但是它在所有使用到它的地方都是一样的值,这个值在board_init_f中初始化,指向CFG_GBL_DATA_ADDR









1. 每一块实际的物理内存对应一个node(struct pglistdata,pg_data_t),所有的内存由一个链表来管理。
每一个node对应的物理内存被分割成最多三个zone(normal,dma,highmem)(struct zone_struct, zone_t)。
以x86为例:
ZONEDMA First 16MiB of memory
ZONENORMAL 16MiB - 896MiB
ZONEHIGHMEM 896 MiB - End
all the structs are kept in a global mem_map array, which is usually stored at the beginning of ZONE_NORMAL or just after the area reserved for the loaded kernel image in low memory machines.
2. Zone Watermarks
1)watermark指zone状态的三个转折点,在这三个临界值时,系统采用不同的分配策略来满足系统要求与性能最大化的平衡
2)有3个watermark:pages_min, pages_low and pages_high (1:2:3) 

 

 

 

 

ARM处理器共有37个寄存器
31个通用寄存器(PC,R0~R14,特殊模式下的Rx:其中PC,R0~R7是所有ARM7个模式共有的寄存器)
6个状态寄存器(ARM体系结构包含一个当前程序状态寄存器(CPSR)和五个备份的程序状态寄存器(SPSRs))