新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 嵌入式Linux内核移植相关代码分析

嵌入式Linux内核移植相关代码分析

作者:时间:2012-01-23来源:网络收藏

C、B:这两位决定了该section的cache&write buffer属性,这与该段的用途(RO or RW)有密切关系。不同的用途要做不同的设置。

C B 具体含义
0 0 无cache,无写缓冲,任何对memory的读写都反映到总线上。对 memory 的操作过程中CPU需要等待。
0 1 无cache,有写缓冲,读操作直接反映到总线上。写操作CPU将数据写入到写缓冲后继续运行,由写缓冲进行写回操作。
1 0 有cache,写通模式,读操作首先考虑cache hit;写操作时直接将数据写入写缓冲,如果同时出现cache hit,那么也更新cache。
1 1 有cache,写回模式,读操作首先考虑cache hit;写操作也首先考虑cache hit。

由于ARM中section表项的权限位和page表项的位置不同, 以下根据struct map_desc 中的保护标志,分别计算页表项中的AP, Domain和CB标志位。
/*******************************************************************************/

prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read ? L_PTE_USER : 0) |
(md->prot_write ? L_PTE_WRITE : 0) |
(md->cacheable ? L_PTE_CACHEABLE : 0) |
(md->bufferable ? L_PTE_BUFFERABLE : 0);

prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read ? PMD_SECT_AP_READ : 0) |
(md->prot_write ? PMD_SECT_AP_WRITE : 0) |
(md->cacheable ? PMD_SECT_CACHEABLE : 0) |
(md->bufferable ? PMD_SECT_BUFFERABLE : 0);

/********************************************************************/
设置虚拟地址,偏移地址和内存length
/********************************************************************/
virt = md->virtual;
off = md->physical - virt;
length = md->length;


--------------------------------------------------------------------------------
Send_linux 回复于:2007-03-06 15:45:16

/********************************************************************/
建立虚拟地址到物理地址的映射
/********************************************************************/
while ((virt 0xfffff || (virt + off) 0xfffff) length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);

virt += PAGE_SIZE;
length -= PAGE_SIZE;
}

while (length >= PGDIR_SIZE) {
alloc_init_section(virt, virt + off, prot_sect);

virt += PGDIR_SIZE;
length -= PGDIR_SIZE;
}

while (length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);

virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
/*************************************************************************/
create_mapping的作用是设置虚地址virt 到物理地址virt + off_set的映射页目录和页表。
/*************************************************************************/

/* 映射中断向量表区域 */
init_maps->physical = virt_to_phys(init_maps);
init_maps->virtual = vectors_base();
init_maps->length = PAGE_SIZE;
init_maps->domain = DOMAIN_USER;
init_maps->prot_read = 0;
init_maps->prot_write = 0;
init_maps->cacheable = 1;
init_maps->bufferable = 0;

create_mapping(init_maps);

中断向量表的虚地址init_maps,是用alloc_bootmem_low_pages分配的,通常是在PAGE_OFF+0x8000前面的某一页,vectors_base()是个宏,ARM规定中断向量表的地址只能是0或0xFFFF0000,所以上述映射一页到0或0xFFFF0000,中断处理程序中的部分也被拷贝到这一页中。

5.3 parse_options()
引导程序发送给的启动选项,在初始化过程中按照某些选项运行,并将剩余部分传送给init进程。这些选项可能已经存储在配置文件中,也可能是由用户在系统启动时敲入的。但并不关心这些,这些细节都是内核引导程序关注的内容,系统更是如此。

5.4 trap_init()
这个函数用来做体系的中断处理的初始化,在该函数中调用__trap_init((void*)vectors_base())函数将exceptionvector设置到vectors_base开始的地址上。__trap_init函数位于entry-armv.S文件中,对于ARM处理器,共有复位、未定义指令、SWI、预取终止、数据终止、IRQ和FIQ几种方式。SWI主要用来实现系统调用,而产生了IRQ之后,通过exceptionvector进入中断处理过程,执行do_IRQ函数。
armnommu的trap_init()函数在arch/armnommu/kernel/traps.c文件中。vectors_base是写中断向量的开始地址,在include/asm-armnommu/proc-armv/system.h文件中设置,地址为0或0XFFFF0000。

ENTRY(__trap_init)
stmfd sp!, {r4 - r6, lr}

mrs r1, cpsr @ code from 2.0.38
bic r1, r1, #MODE_MASK @ clear mode bits /* 设置svc模式,disable IRQ,FIQ */
orr r1, r1, #I_BIT|F_BIT|MODE_SVC @ set SVC mode, disable IRQ,FIQ
msr cpsr, r1

adr r1, .LCvectors @ set up the vectors
ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr}
stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr} /* 拷贝异常向量 */

add r2, r0, #0x200
adr r0, __stubs_start @ copy stubs to 0x200
adr r1, __stubs_end
1: ldr r3, [r0], #4
str r3, [r2], #4
cmp r0, r1
blt 1b
LOADREGS(fd, sp!, {r4 - r6, pc})
__stubs_start到__stubs_end的地址中包含了异常处理的代码,因此拷贝到vectors_base+0x200的位置上。

5.5 init_IRQ()
void __init init_IRQ(void)
{
extern void init_dma(void);
int irq;

for (irq = 0; irq NR_IRQS; irq++) {
irq_desc[irq].probe_ok = 0;
irq_desc[irq].valid = 0;
irq_desc[irq].noautoenable = 0;
irq_desc[irq].mask_ack = dummy_mask_unmask_irq;
irq_desc[irq].mask = dummy_mask_unmask_irq;
irq_desc[irq].unmask = dummy_mask_unmask_irq;
}
CSR_WRITE(AIC_MDCR, 0x7FFFE); /* disable all interrupts */
CSR_WRITE(CAHCNF,0x0);/*Close Cache*/
CSR_WRITE(CAHCON,0x87);/*Flush Cache*/
while(CSR_READ(CAHCON)!=0);
CSR_WRITE(CAHCNF,0x7);/*Open Cache*/

init_arch_irq();
init_dma();
}
这个函数用来做体系的irq处理的初始化,irq_desc数组是用来描述IRQ的请求队列,每一个中断号分配一个irq_desc结构,组成了一个数组。NR_IRQS代表中断数目,这里只是对中断结构irq_desc进行了初始化。在默认的初始化完成后调用初始化函数init_arch_irq,先执行arch/armnommu/kernel/irq-arch.c文件中的函数genarch_init_irq(),然后就执行include/asm-armnommu/arch-xxxx/irq.h中的inline函数irq_init_irq,在这里对irq_desc进行了实质的初始化。其中mask用阻塞中断;unmask用来取消阻塞;mask_ack的作用是阻塞中断,同时还回应ack给硬件表示这个中断已经被处理了,否则硬件将再次发生同一个中断。这里,不是所有硬件需要这个ack回应,所以很多时候mask_ack与mask用的是同一个函数。
接下来执行init_dma()函数,如果不支持DMA,可以设置include/asm-armnommu/arch-xxxx/dma.h中的MAX_DMA_CHANNELS为0,这样在arch/armnommu/kernel/dma.c文件中会根据这个定义使用不同的函数。

5.6 sched_init()
初始化系统调度进程,主要对定时器机制和时钟中断的BottomHalf的初始化函数进行设置。与时间的初始化过程主要有两步:(1)调用init_timervecs()函数初始化内核定时器机制;(2)调用init_bh()函数将BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所对应的BH函数分别设置成timer_bh()、tqueue_bh()和immediate_bh()函数

5.7 softirq_init()
内核的软中断机制初始化函数。调用tasklet_init初始化tasklet_struct结构,软中断的个数为32个。用于bh的tasklet_struct结构调用tasklet_init()以后,它们的函数指针func全都指向bh_action()。bh_action就是tasklet实现bh机制的代码,但此时具体的bh函数还没有指定。

HI_SOFTIRQ用于实现bottom half,TASKLET_SOFTIRQ用于公共的tasklet。

open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); /* 初始化公共的tasklet_struct要用到的软中断 */
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL); /* 初始化tasklet_struct实现的bottom half调用 */

这里顺便讲一下软中断的执行函数do_softirq()。
软中断服务不允许在一个硬中断服务程序内部执行,也不允许在一个软中断服务程序内部执行,所以通过in_interrupt()加以检查。h->action 就是串行化执行软中断,当bh 的tasklet_struct链入的时候,就能在这里执行,在bh里重新锁定了所有CPU,导致一个时间只有一个CPU可以执行bh函数,但是do_softirq()是可以在多CPU上同时执行的。而每个tasklet_struct在一个时间上是不会出现在两个CPU上的。另外,只有当初始化完成开启中断后,中断系统才可以开始工作。

5.8 time_init()
这个函数用来做体系相关的timer的初始化,armnommu的在arch/armnommu/kernel/time.c。这里调用了在include/asm-armnommu/arch-xxxx/time.h中的inline函数setup_timer,setup_timer()函数的设计与硬件设计紧密相关,主要是根据硬件设计情况设置时钟中断号和时钟频率等。
void __inline__ setup_timer (void)
{
/*----- disable timer -----*/
CSR_WRITE(TCR0, xxx);

CSR_WRITE (AIC_SCR7, xxx); /* setting priority level to high */
/* timer 0: 100 ticks/sec */
CSR_WRITE(TICR0, xxx);

timer_irq.handler = xxxxxx_timer_interrupt;
setup_arm_irq(IRQ_TIMER, timer_irq); /* IRQ_TIMER is the interrupt number */

INT_ENABLE(IRQ_TIMER);
/* Clear interrupt flag */
CSR_WRITE(TISR, xxx);

/* enable timer */
CSR_WRITE(TCR0, xxx);
}

5.9 console_init()
控制台初始化。控制台也是一种驱动程序,由于其特殊性,提前到该处完成初始化,主要是为了提前看到输出信息,据此判断内核运行情况。很多操作系统由于没有在/dev目录下正确配置console设备,造成启动时发生诸如unable to open an initialconsole的错误。

/*******************************************************************************/
init_modules()函数到smp_init()函数之间的代码一般不需要作修改,
如果平台具有特殊性,也只需对相关函数进行必要修改。
这里简单注明了一下各个函数的功能,以便了解。
/*******************************************************************************/
5.10 init_modules()
模块初始化。如果编译内核时使能该选项,则内核支持模块化加载/卸载功能

5.11 kmem_cache_init()
内核Cache初始化。

5.12 sti()
使能中断,这里开始,中断系统开始正常工作。


--------------------------------------------------------------------------------
Send_linux 回复于:2007-03-06 15:46:04

5.13 calibrate_delay()
近似计算BogoMIPS数字的内核函数。作为第一次估算,calibrate_delay计算出在每一秒内执行多少次__delay循环,也就是每个定时器滴答(timer tick)D百分之一秒内延时循环可以执行多少次。这种计算只是一种估算,结果并不能精确到纳秒,但这个数字供内核使用已经足够精确了。
BogoMIPS的数字由内核计算并在系统初始化的时候打印。它近似的给出了每秒钟CPU可以执行一个短延迟循环的次数。在内核中,这个结果主要用于需要等待非常短周期的设备驱动程序DD例如,等待几微秒并查看设备的某些信息是否已经可用。
计算一个定时器滴答内可以执行多少次循环需要在滴答开始时就开始计数,或者应该尽可能与它接近。全局变量jiffies中存储了从内核开始保持跟踪时间开始到现在已经经过的定时器滴答数, jiffies保持异步更新,在一个中断内——每秒一百次,内核暂时挂起正在处理的内容,更新变量,然后继续刚才的工作。

5.14 mem_init()
内存初始化。本函数通过内存碎片的重组等方法标记当前剩余内存, 设置内存上下界和页表项初始值。

5.15 kmem_cache_sizes_init()
内核内存管理器的初始化,也就是初始化cache和SLAB分配机制。

5.16 pgtable_cache_init()
页表cache初始化。

5.17 fork_init()
这里根据硬件的内存情况,如果计算出的max_threads数量太大,可以自行定义。

5.18 proc_caches_init();
为proc文件系统创建高速缓冲

5.19 vfs_caches_init(num_physpages);
为VFS创建SLAB高速缓冲

5.20 buffer_init(num_physpages);
初始化buffer

5.21 page_cache_init(num_physpages);
页缓冲初始化

5.22 signals_init();
创建信号队列高速缓冲

5.23 proc_root_init();
在内存中创建包括根结点在内的所有节点

5.24 check_bugs();
检查与处理器相关的bug

5.25 smp_init();

5.26 rest_init(); 此函数调用kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)函数。

5.26.1 kernel_thread()函数
这里调用了arch/armnommu/kernel/process.c中的函数kernel_thread,kernel_thread函数中通过 __syscall(clone) 创建新线程。__syscall(clone)函数参见armnommu/kernel目录下的entry-common.S文件。

5.26.2 init()完成下列功能:
Init()函数通过kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL)的回调函数执行,完成下列功能。
do_basic_setup()
在该函数里,sock_init()函数进行网络相关的初始化,占用相当多的内存,如果所开发系统不支持网络功能,可以把该函数的执行注释掉。
do_initcalls()实现驱动的初始化, 这里需要与vmlinux.lds联系起来看才能明白其中奥妙。
static void __init do_initcalls(void)
{
  initcall_t *call;

  call = __initcall_start;
  do {
   (*call)();
   call++;
  } while (call __initcall_end);

  /* Make sure there is no pending stuff from the initcall sequence */
  flush_scheduled_tasks();
}

查看 /arch/i386/vmlinux.lds,其中有一段代码
 __initcall_start = .;
 .initcall.init : { *(.initcall.init) }
 __initcall_end = .;
其含义是__initcall_start指向代码节.initcall.init的节首,而__initcall_end指向.initcall.init的节尾。

do_initcalls所作的是系统中有关驱动部分的初始化工作,那么这些函数指针数据是怎样放到了.initcall.init节呢?在include/linux/init.h文件中有如下3个定义:
1. #define __init_call   __attribute__ ((unused,__section__ (.initcall.init)))
__attribute__的含义就是构建一个在.initcall.init节的指向初始函数的指针。
2. #define __initcall(fn) static initcall_t __initcall_##fn __init_call = fn
##意思就是在可变参数使用宏定义的时候构建一个变量名称为所指向的函数的名称,并且在前面加上__initcall_
3. #define module_init(x) __initcall(x);
很多驱动中都有类似module_init(usb_init)的代码,通过该宏定义逐层解释存放到.initcall.int节中。

blkmem相关的修改(do_initcalls()初始化驱动时执行此代码)
在blkmem_init()函数中,调用了blk_init_queue()函数,blk_init_queue()函数调用了 blk_init_free_list()函数,blk_init_free_list()函数又调用了blk_grow_request_list() 函数,在这个函数中会kmem_cache_alloc出nr_requests个request结构体。
这里如果nr_requests的值太大,则将占用过多的内存,将造成硬件内存不够,因此可以根据实际情况将其替换成了较小的值,比如32、16等。

free_initmem
这个函数在arch/armnommu/mm/init.c文件中,其作用就是对init节的释放,也可以通过修改代码指定为不释放。

5.26.3 init执行过程
在内核引导结束并启动init之后,系统就转入用户态的运行,在这之后创建的一切进程,都是在用户态进行。这里先要清楚一个概念:就是init进程虽然是从内核开始的,即在前面所讲的init/main.c中的init()函数在启动后就已经是一个核心线程,但在转到执行init程序(如 /sbin/init)之后,内核中的init()就变成了/sbin/init程序,状态也转变成了用户态,也就是说核心线程变成了一个普通的进程。这样一来,内核中的init函数实际上只是用户态init进程的入口,它在执行execve(/sbin/init,argv_init, envp_init)时改变成为一个普通的用户进程。这也就是exec函数的乾坤大挪移法,在exec函数调用其他程序时,当前进程被其他进程“灵魂附体”。
  除此之外,它们的代码来源也有差别,内核中的init()函数的源代码在/init/main.c中,是内核的一部分。而/sbin/init程序的源代码是应用程序。
init程序启动之后,要完成以下任务:检查文件系统,启动各种后台服务进程,最后为每个终端和虚拟控制台启动一个getty进程供用户登录。由于所有其它用户进程都是由init派生的,因此它又是其它一切用户进程的父进程。
  init进程启动后,按照/etc/inittab的内容进程系统设置。很多系统用的是BusyBox的init,它与一般所使用的init不一样,会先执行/etc/init.d/rcS而非/etc/rc.d/rc.sysinit。

小结:
本想多整理一些相关资料,无奈又要开始新项目的奔波,start_kernel()函数也刚好差不多讲完了,的不是很深入,希望对嵌入式的网友们有一些帮助。最后列举下面几处未整理的知识点,有兴趣的网友可作进一步探讨。
text.init和data.init说明
__init标示符在gcc编译器中指定将该函数置于内核的特定区域。在内核完成自身初始化之后,就试图释放这个特定区域。实际上,内核中存在两个这样的区域,.text.init和.data.initDD第一个是代码初始化使用的,另外一个是数据初始化使用的。另外也可以看到 __initfunc和__initdata标志,前者和__init类似,标志初始化专用代码,后者则标志初始化专用数据。
System.map内核符号表
irq的处理过程
Linux内核调度过程

本文引用地址:http://www.eepw.com.cn/article/149812.htm
linux操作系统文章专题:linux操作系统详解(linux不再难懂)

上一页 1 2 下一页

评论


相关推荐

技术专区

关闭