浅谈linux内核的一些很妙的算法(1)
(内核版本是0.11)阅读linux内核时发现了很多难以理解的程序,查找课外书后才发现这些程序的实现算法相当的精妙,不由佩服linus的编程技巧,下面简单说一些我自己认为很妙的程序供大家参考。
对于linux系统的内存规划。大家都知道linux系统以其精简而受到大家的热爱,对于内存规划便是很重要的一件事。Linus考虑好了各部分程序的代码量,从bootsect.s到setup.s再到head.s,无一不体现了linux的内存安排之紧凑。不断地出现下一个程序覆盖上一个程序的代码,最后就连head.s也不“放过”,所以在head.s执行完毕后,其代码原来存在的位置已被覆盖得只剩184B了。
1、精彩代码一、(代码路径:boot/bootsect.s)
54 rep
55 movw
56 jmpi go INITSEG
57 go:
mov ax,cs
58 mov ds,ax
..........
为什么说这段代码很巧妙呢?这段代码是出现在由BIOS加载到位置0x07c00处的bootsect.s代码后,将自己复制到0x90000处时出现的。此时已经复制完了,然后咋办呢?我要让代码从复制后的地址执行,而且又不能从刚开始的地方执行,咋办?这就是linus的高明之处了,怎么实现呢?看吧!在0x07c00处的代码现在与0x90000处的一样,而且在0x07c00处已经执行到了movw这条语句,现在执行jmpi时出现跳转,跳到哪呢?当然是INITSEG处的代码,在前面已经规划好了INITSEG是0x90000了,所以跳到了该处,但是记住jmpi的用法哦!Jmpi+偏移地址+段地址,所以前面还有一个go,这样当跳转时便会自动跳到新代码的go处执行而不会重头再来了!
3、精彩代码二、(代码路径:boot/setup.s)
115 do_move:
116 mov es,ax
117add ax,#0x1000
118cmp ax,#0x9000
119jz end_move
120mov ds,ax
121sub di,di
122sub si,si
123mov cx,#0x8000
124rep
125movsw
126jmp do_move
这段代码只是为了说明一下刚才提到的内存规划之紧凑的情况,实现了将0x10000处的系统代码块移到0x00000处的目的,这就覆盖了放在这里的中断向量表以及BIOS数据,所以把原来的东西给清除了,从这里开始,操作系统就开始规划自己的内存了。
3、精彩代码三、(代码路径:boot/head.s)
193jmpi 0,8
这么简单的一行却是体现了编程语言的灵活与高深!首先我们都知道了jmpi的用法,所以说0是段内偏移地址,那么8是什么呢?“8”是保护模式下的段选择符。从gdt中选择将要执行的地址。8换为二进制是1000,最低两位代表特权级别,00是内核级别,11是用户级别。第三位是选择的表是什么,0选择GDT,1选择LDT,第四、五位合一起标识选择的是GDT或LDT表项的第1项(从0项开始数的)。所以这段代码的实现的是从段基址0x00000000、偏移地址为0处执行(这时查GDT表的第1项得知的),这就意味着跳到了head.s处执行了!
4、精彩代码四、(代码路径:boot/head.s)
49jmp after_page_tables
.......
135after_page_tables:
136pushl $0
137pushl $0
138pushl $0
139pushl $L6
140pushl $_main
141jmp setup_paging
142L6:
143jmp L6
......
198setup_paging:
......
218ret
这一大段代码实现了将main函数的地址压栈后继续执行创建页机制,而后执行ret进而把main函数的地址弹出传给EIP,达到了跳转到main函数执行目的了。当main函数返回时进而跳到L6执行,造成死循环。Linus是模拟一个CALL的过程,这相当的有意思,它进栈的不是setup_paging之前执行的那条代码的后一条指令的地址,而是人工地将main函数的地址进栈,从而达到了目的。注意这里不是用CALL,而是用jmp。
5、精彩代码五(代码路径:kernel/fork.c与kernel/system_call.s、init/main.c)
在kernel/fork.c中的代码:
97p->tss.eax=0;
在kernel/system_call.s的代码:
101ret_from_sys_call:
102movl _current,%eax
.......
1213:popl % eax
......
128iret
208_sys_fork:
.....
210testl % eax,% eax
211js lf
212push %gs
.....
216pushl % eax
217call _copy_process
......
在init/main.c的代码:
137move_to_user_mode;
138if(!fork())
139{
140init();
141}
......
之所以取牵涉这么多得代码正是因为这些代码每一行都有着和其他代码千丝万缕的关系。我们逐条讲解。首先从move_to_user_mode开始执行,后调用fork函数创建新进程1,这就转到了unisted.h的宏函数syscall0中去,在该函数中执行软中断,调用sys_fork,进而开始复制进程0的信息,在这之前先将一些数据压栈,这就是上面的_sys_fork中列的那些代码,记住,这时保存的是进程0的重要数据。其中%eax存的值是从find_empty_process返回的新进程号1。之后进入copy_process函数中执行复制进程。从而执行了一条上述列举的指令“97p->tss.eax=0;”,这是一条很妙的指令,不亚于上面提到的go语句。具体到后面再讲,记住这里的eax是属于进程1的,但不是说eax有两个,只是在不同进程时用到的不一样而已,这由TSS来决定的。当切换进程时会将这些进程的寄存器数据压栈的。这时很关键的一点,要不你待会儿会很迷惑。当复制完所有的信息后便跳到了kernel/system_call.s中的“101ret_from_sys_call:”,然后便是执行“121 3:popl %eax”,此时仍在进程0中,别乱了啊!所以弹出的eax值是1。软中断结束后将此值传递给syscall0的res变量,这就是fork函数的返回值,这时在main函数中判断一下,不成立,也就不到花括号里面执行init()函数了!这便是进程0执行的结果,最后跳到了进程1中,此时记住刚才提到的那些重要数据以及当前环境。进程1开始执行的代码是unisted.h的宏函数syscall0中的“if(_res>=0)"这一条指令,结果发现此时的eax传递给res的值变成了0,所以返回时就会时fork得值为0,也就是main函数中的if判断语句成立,执行花括号里的init函数。这就是我认为本段代码的精彩之处,条条经典,其中的思想之精密严谨确实很值得大家学习啊!
(未完待续。。。。。。。。。)
参考书籍:《linux内核设计的艺术》新设计团队(著)
《linux内核完全注释》陈炯(著)