找对了入口,才有可能找对出口 – 佚名

在前面几节内容中我简单将Linux ,以及linux Kernel的概念做了总结,然后又将编译以及内核镜像也做了也总结! 从本节内容开始,我将真正的进入到内核代码中去!加油吧,Keven! 
从上一节中我已经知道了vmlinux.lds链接文件中指定了内核的入口函数kernel_entry,此函数被定义在head.S文件中!,请跟着我去看看此函数到底做了什么!

1、head.S - kernel_entry

NESTED(kernel_entry, 16, sp)            # kernel entry point

    kernel_entry_setup          # cpu specific setup

    setup_c0_status_pri

    /* We might not get launched at the address the kernel is linked to,
    so we jump there.  */
    PTR_LA  t0, 0f      #Loading 数字标号0中的地址
    jr  t0
0:
    PTR_LA      t0, __bss_start             #Loading __bss_start(0xffffffff80dc0000) 到 t0
    LONG_S      zero, (t0)                  #对0xffffffff80dc0000这个地址内容清零(清除bss)
    PTR_LA      t1, __bss_stop - LONGSIZE   #Loading __bss_stop(0xffffffff80dc0000 + 0x00cc2ac0 - unsinged(8)<0x40> 8*8) = 
                                            0xffffffff80106ae8 此地址就是kernel_entry函数地址 到 t1
1:
    PTR_ADDIU   t0, LONGSIZE                #t0 = t0 + LONGSIZE (64位操作) = 0xffffffff80dc0000 + 0x40
    LONG_S      zero, (t0)                  #清零地址内容
    bne     t0, t1, 1b                      #判断 t0 是否等于 t1,如果不等于到当前位置前面的第一个标号1,循环加到等于后,执行下面指令

                                # firmware arguments
    LONG_S      a0, fw_arg0     #子程序的前4个参数存到a0~a3中
    LONG_S      a1, fw_arg1
    LONG_S      a2, fw_arg2
    LONG_S      a3, fw_arg3

    MTC0        zero, CP0_CONTEXT           # clear context register 清除 寄存器CP0的$4,这个寄存器保存的是页表的起始地址 
    PTR_LA      $28, init_thread_union 		#将init_thread_union地址Loading $28中(Ps.$28是全局指针寄存器),关于全局指针见 Note 1:
    /* Set the SP after an empty pt_regs.  */
    PTR_LI      sp, _THREAD_SIZE - 32 - PT_SIZE  #加载常数_THREAD_SIZE(0x10000) - 32(0x100) - PT_SIZE(0xc40) = 0xf2c0  详解见Note 2:
                                                    到sp寄存器(Ps.sp为堆栈寄存器)
    PTR_ADDU    sp, $28						#$sp = $sp + $28 (64位操作) ,sp指向union结构的0xf2c0 + 0xffffffff80c90000 详解见Note 2:
    back_to_back_c0_hazard                  # Note 3
    set_saved_sp    sp, t0, t1              # Note 4
    PTR_SUBU    sp, 4 * SZREG       # init stack pointer $sp = $sp - (4 * 8)<0x100> = ffffffff80cae480 ???

    j       start_kernel
    END(kernel_entry)

__CPUINIT

上述汇编指令的含义(64位指令):

PTR_LA          dla
LONG_S          sd
PTR_ADDIU       daddiu
MTC0            dmtc0
PTR_LI          dli
PTR_ADDU        daddu
PTR_SUBU        dsubu

对于上述代码还需要下面几点解述! 

Note 1: 

gp - 全局指针寄存器 – 为何gp指向init_thread_union? 

首先gp为全局指针寄存器(x86中并没有此寄存器,而是用ss和sp来做处理,为何没有呢,据我所知应该是x86的cpu寄存器太少的缘故,故而没有专门设置这样的寄存器(这里请熟悉x86的朋友指正),他的作用就是在进程切换的时候保存当前进程的thread_info指针到当前gp中!此寄存器对于调试而言好处大大的!那么为什么在启动阶段首先要将init_thread_union放入gp中呢?从init_thread_union说起,其实这就是是0号进程所在(Ps.0号进程见下面)! 

Note 2: 

sp 指向栈的第一个元素(栈底)(其实并不是真真意义上的栈底)!(占大小 - 32 - PT_SIZE == 当前0号进程所在位置) 

Note 3: 

此时具体执行了一个汇编指令_ehb(ehb).Ps.关于ehb为exception hazard barrier,由MIPS的流水线引起,大致可以理解为由于MIPS采用的流水线结构,即使在异常处理代码中(这里由于改变了状态寄存器情况类似),由于流水线的作用,异常处理结束时,其下一条(可能超过一条,依赖流水线的设计)仍然被预取执行,这样由于CPU的特权级别发生了改变,但被流水线预取的指令并不知道这些,因而导致严重的安全性问题。为了避免这种情况发生,MIPS专门使用了ehb指令。还包括eret,即从异常(原子的,atomically)返回! 

Note 4: 

将sp地址保存到kernelsp[NR_CPUS]中!

2、内核的0号进程

关于进程的相关信息,因为比较庞杂我这里暂时先不去做总结!简单的说一下pid 0。

keven@keven-2015:~/kernel/linux-3.10.92$ ps -el
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
4 S     0     1     0  0  80   0 -  6108 poll_s ?        00:00:01 init

ps -el 可查看当前所有的进程详细的信息。 

我们熟悉的1号进程的父亲是0号进程,那么0号进程是怎么来的呢?作用是什么呢?为什么不把0号进程释放呢? 

在上面kernel_entry中,我说了init_thread_union就是0号进程所在!那么我去看一下init_thread_union 究竟是什么东东? 

堆栈定义:

#define init_thread_info    (init_thread_union.thread_info)
/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
/*
 * Initial thread structure. Alignment of this is handled by a special
 * linker map entry.
 */
union thread_union init_thread_union __init_task_data =
    { INIT_THREAD_INFO(init_task) };
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

上面的代码表示,对于每一个进程,内核为其单独分配了一个内存区域,这个区域存储的是内核栈和该进程所对应的一个轻量级的描述符 - thread_info!

struct thread_info {
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    unsigned long       flags;      /* low level flags */
    unsigned long       tp_value;   /* thread pointer */
    __u32           cpu;        /* current CPU */
    int         preempt_count;  /* 0 => preemptable, <0 => BUG */

    mm_segment_t        addr_limit; /*
                         * thread address space limit:
                         * 0x7fffffff for user-thead
                         * 0xffffffff for kernel-thread
                         */
    struct restart_block    restart_block;
    struct pt_regs      *regs;
};

但是此结构并没有直接包含与进程处理相关的字段,而是通过task指向进程描述符,这里它的大小为2M! 

Ps。。关于内核大页放到后面在详解! 

THREAD_SIZE是怎么分配的呢? 

对于这个问题,我一开始其实是比较拒绝的!haha.. 

我们来看下内核页表(CONFIG_PAGE_SIZE_xKB)的配置会影响到什么? 

全局搜索了一下,大抵会影响到下面的几个宏<我这里只列出4K和64K的配置>: 

a.默认页大小配置 <<通过内核配置 

#ifdef CONFIG_PAGE_SIZE_4KB 

#define PM_DEFAULT_MASK PM_4K 

#elif defined(CONFIG_PAGE_SIZE_64KB) 

#define PM_DEFAULT_MASK PM_64K 

#else 

#error Bad page size configuration! 

#endif 

b.默认tlb大小配置 <<通过内核配置

/*
 * Default huge tlb size for a given kernel configuration
 */
#ifdef CONFIG_PAGE_SIZE_4KB
#define PM_HUGE_MASK    PM_1M
#elif defined(CONFIG_PAGE_SIZE_64KB)
#define PM_HUGE_MASK    PM_256M
#elif defined(CONFIG_MIPS_HUGE_TLB_SUPPORT)
#error Bad page size configuration for hugetlbfs!
#endif

c.PAGE_SHIFT 通过此宏和另外一个宏配置默认的THREAD_SIZE

/*
 * PAGE_SHIFT determines the page size
 */
#ifdef CONFIG_PAGE_SIZE_4KB
#define PAGE_SHIFT  12
#endif
#ifdef CONFIG_PAGE_SIZE_64KB
#define PAGE_SHIFT  16

d.线程信息配置宏,此宏觉得了THREAD_SIZE的大小

/* thread information allocation */
#if defined(CONFIG_PAGE_SIZE_4KB) && defined(CONFIG_32BIT)
#define THREAD_SIZE_ORDER (1)
#endif
#if defined(CONFIG_PAGE_SIZE_4KB) && defined(CONFIG_64BIT)
#define THREAD_SIZE_ORDER (2)
#ifdef CONFIG_PAGE_SIZE_64KB
#define THREAD_SIZE_ORDER (0)
#endif

基本上感觉和我要去探索的问题相关的宏就以上几个了! 

上面一直在提到THREAD_SIZE的大小,它的定义如下: 

#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) 

PAGE_SIZE的大小则是PAGE_SHIFT来觉得:

#ifdef __ASSEMBLY__
#define PAGE_SIZE   (1 << PAGE_SHIFT)
#else
#define PAGE_SIZE   (1UL << PAGE_SHIFT)
#endif

如果内核页表大小配置为4K且为32位OS的话: 

THREAD_SIZE = (1 << 12) << 1; == 8k 

4K&&64位OS的话: 

THREAD_SIZE = (1 << 12) << 2; == 16k 

如果是64K的话: 

THREAD_SIZE 大抵通过代码将4K以及64的区别是决定了THREAD_SIZE的大小! 

THREAD_SIZE 为谁所用? 

上面我们说了init_thread_union的定义:

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

加入当前我们配置的是64K,那么stack的大小就是8192(8k)! 

也就是需要使用两个物理页来存储.. 

来一张经典的图: 

全面解析Linux 内核 3.10.x - 内核入口函数__kernel_entry_#endif

 

由上图可知,内核栈是逆增长的(从高地址 - 低地址),而thread_info结构则是从该区域的开始处正(低地址 - 高地址)增长。内核栈的栈顶地址存储在gp寄存器中。所以,当进程从用户态切换到内核态后,gp寄存器指向这个区域的末端! 

THREAD_SIZE决定了stack数组的大小,那么为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)放在一起? 

原因就是内核可以很容易的通过gp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址!

/* How to get the thread information struct from C.  */
static inline struct thread_info *current_thread_info(void)
{
    register struct thread_info *__current_thread_info __asm__("$28");

    return __current_thread_info;
}

我们最常用的current宏其实返回就是thread_info的task成员.

#define get_current() (current_thread_info()->task)
#define current get_current()

好,这几个问题算是弄清除了! 
那么问题以及思考的事情就来了..

  • 深入理解Linux 内核3.10.x Q&A - 内存管理

以后所有的问题单会单独在一篇文章中进行解答:


By: Keven - 点滴积累