找对了入口,才有可能找对出口 – 佚名
在前面几节内容中我简单将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
对于上述代码还需要下面几点解述!
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号进程见下面)!
sp 指向栈的第一个元素(栈底)(其实并不是真真意义上的栈底)!(占大小 - 32 - PT_SIZE
== 当前0号进程所在位置)
此时具体执行了一个汇编指令_ehb(ehb).Ps.关于ehb
为exception hazard barrier,由MIPS的流水线引起,大致可以理解为由于MIPS采用的流水线结构,即使在异常处理代码中(这里由于改变了状态寄存器情况类似),由于流水线的作用,异常处理结束时,其下一条(可能超过一条,依赖流水线的设计)仍然被预取执行,这样由于CPU的特权级别发生了改变,但被流水线预取的指令并不知道这些,因而导致严重的安全性问题。为了避免这种情况发生,MIPS专门使用了ehb
指令。还包括eret
,即从异常(原子的,atomically)返回!
将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的配置>:
#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
/*
* 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
/* 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)!
也就是需要使用两个物理页来存储..
来一张经典的图:
由上图可知,内核栈是逆增长的(从高地址 - 低地址),而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 - 内存管理
- 1、内核页表4k,64k的区别在哪里?
- 2、思考:内核页表的大小到底和什么因素有关?如何选择页面大小?
以后所有的问题单会单独在一篇文章中进行解答:
- Ps.如果对MIPS汇编指令有兴趣,参考MIPS指令集Wiki
By: Keven - 点滴积累