本文参考书籍
1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言 从实模式到保护模式
4.Linux内核设计的艺术
ps:基于x86硬件的pc系统
Linux0.12初始化续
在上一篇博文中根据main函数的执行;
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
...
sti(); // 开启中断
move_to_user_mode(); // 移到用户态执行
if (!fork()) { /* we count on this going ok */ // 在新建的子进程中执行init()函数
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;)
__asm__("int $0x80"::"a" (__NR_pause):"ax"); // 任务0倍调度时执行系统调用pause
}
此时我们继续从开启中断往下执行。
开启中断
sti函数主要是宏定义
#define sti() __asm__ ("sti"::)
内联汇编直接就开启中断;
当开启中断后,此时再前面初始化流程中的,如时间中断、硬盘中断、系统调用等中断都可以得到响应。
切换到用户态执行
move_to_user_mode函数就是切换到用户态执行,即从特权级0代码转移到特权级3的代码去运行;
除了进程0之外,所有进程都是由一个父进程在用户态下完成创建的,为了遵守这一规则,在进程0正式创建进程1之前,要将进程0由内核态转变为用户态。该函数使用的方法就是模拟中断调用返回过程,即利用iret指令来实现特权级的变更和堆栈的切换,从而把CPU执行控制流移动到进程0的环境中去运行,使用这种方法进行控制的转移是因为CPU保护机制造成,CPU允许低特权级的代码通过调用们或中断、陷阱门来调用或转移到高级别的代码中运行,但是从高特权级向低特权级转移则不行,所以内核采用了模拟iret返回低级别代码的方法。
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ // 保存堆栈指针esp到eax寄存器中
"pushl $0x17\n\t" \ // 将堆栈段选择符(SS)入栈
"pushl %%eax\n\t" \ // 保存堆栈指针值(esp)入栈
"pushfl\n\t" \ // 将标志寄存器内容入栈
"pushl $0x0f\n\t" \ // 将Task0代码段选择符(cs)入栈
"pushl $1f\n\t" \ // 将标号为1的偏移地址入栈
"iret\n" \ // 执行返回中断命令,则会跳入标号为1处执行
"1:\tmovl $0x17,%%eax\n\t" \ // 此时开始执行Task0
"mov %%ax,%%ds\n\t" \ // 初始化段寄存器
"mov %%ax,%%es\n\t" \
"mov %%ax,%%fs\n\t" \
"mov %%ax,%%gs" \
:::"ax")
在’pushl 0f’代表的是cs段选择符,也是用户特权级,从LDT的第2项取得用户态代码段描述符,当itet时,硬件把压栈的值恢复,此时返回执行的程序特权级就是用户态特权级。
进程0是一个特殊的进程,它的数据段和代码段直接映射到内核代码和数据空间,即从物理地址0开始的640KB的内存空间,其堆栈地址即是内核代码所使用的堆栈,因此堆栈中原SS的eip是将现有内核堆栈指针直接压入堆栈的。并且进程0任务设置成0可运行状态。
创建进程1
此时进行系统调用fork(),此时通过宏定义展开,此时会执行’int 0x80, 2’,此时会调用已经注册的system_call函数,该函数位于kernel/sys_call.s中;
.align 2
bad_sys_call:
pushl $-ENOSYS
jmp ret_from_sys_call
.align 2
reschedule:
pushl $ret_from_sys_call
jmp _schedule
.align 2
_system_call:
push %ds # 保存原段寄存器值
push %es
push %fs
pushl %eax # save the orig_eax # 保存eax原值
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters 将ebx,ecx,edx三个寄存器保存传入参数值
pushl %ebx # to the system call # ebx为第一个参数,ecx为第二个,edx为第三个
movl $0x10,%edx # set up ds,es to kernel space # 将ds、es指向内核数据段
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space # fs指向本次调用的用户程序的数据段
mov %dx,%fs
cmpl _NR_syscalls,%eax # 如果调用号超出范围就跳转
jae bad_sys_call # 跳转到错误系统调用处理函数
call _sys_call_table(,%eax,4) # 调用sys_call_table中的函数为[_sys_call_table + eax*4]处对应的函数
pushl %eax # 系统调用号入栈
2:
movl _current,%eax # 取当前任务的结构指针给eax
cmpl $0,state(%eax) # state # 获取当前任务的状态
jne reschedule # 如果不等于0(运行态),就重写调用
cmpl $0,counter(%eax) # counter # 如果时间片用完页执行调用
je reschedule
ret_from_sys_call:
movl _current,%eax
cmpl _task,%eax # task[0] cannot have signals # 比较当前任务是否是任务0,因为任务0在数组第一个
je 3f # 如果是任务0则直接跳转到3处返回
cmpw $0x0f,CS(%esp) # was old code segment supervisor ? # 比较调用程序是否是用户态程序
jne 3f # 如果不是则直接跳转返回
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? # 比较堆栈选择符不是0x17,则直接跳转返回
jne 3f
movl signal(%eax),%ebx # 获取当前任务的信号位图
movl blocked(%eax),%ecx # 获取当前任务的阻塞位图
notl %ecx # 每位取反
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx # 信号值入栈作为do_signal参数
call _do_signal # 调用kernel/signal.c中的do_signal处理
popl %ecx # 弹出入栈的信号值
testl %eax, %eax # 测试返回值
jne 2b # see if we need to switch tasks, or do more signals
3: popl %eax # 弹出保存的入栈值
popl %ebx
popl %ecx
popl %edx
addl $4, %esp # skip orig_eax # 跳过原有的eax值
pop %fs
pop %es
pop %ds
iret
通过该处理函数查找到_sys_fork函数;
.align 2
_sys_fork:
call _find_empty_process # 为新进程取得进程号
testl %eax,%eax # 测试取得的进程号如果返回为负数则跳转返回
js 1f
push %gs # 压入参数
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process # 调用copy_process函数
addl $20,%esp # 丢弃压栈的五个参数值
1: ret
此时我们继续查看kernel/fork.c代码中的find_empty_process函数;
int find_empty_process(void)
{
int i;
repeat:
if ((++last_pid)<0) last_pid=1; // last_pid全局变量,排除last_pid为0
for(i=0 ; i<NR_TASKS ; i++) // 操作系统最多支持64个任务
if (task[i] && ((task[i]->pid == last_pid) ||
(task[i]->pgrp == last_pid))) // 如果查找到的任务号再使用,就继续查找
goto repeat;
for(i=1 ; i<NR_TASKS ; i++)
if (!task[i]) // 返回除了任务0之外的可用任务号
return i;
return -EAGAIN;
}
此时,先查找到可用的任务号,即task数组中可用的项,当找到任务号之后调用copy_process函数;
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx, long orig_eax,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page(); // 为新任务数据结构分配内存
if (!p)
return -EAGAIN;
task[nr] = p; // 在find_empty_process中找到的空闲任务下标
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE; // 设置成不可中断
p->pid = last_pid; // 设置进程的pid
p->counter = p->priority; // 运行时间片的值
p->signal = 0; // 信号位图
p->alarm = 0; // 报警定时值
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0; // 用户态和核心态的运行时间
p->cutime = p->cstime = 0; // 子进程用户态和核心态的运行时间
p->start_time = jiffies; // 进程开始运行时间
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p; // 任务内核态栈指针
p->tss.ss0 = 0x10; // 内核态栈的段选择符(与内核态数据段相同)
p->tss.eip = eip; // 指令代码指针
p->tss.eflags = eflags; // 标志寄存器
p->tss.eax = 0; // 这就是fork返回时新进程会返回0
p->tss.ecx = ecx; // 设置相应的寄存器值
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff; // 设置段寄存器值
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr); // 设置任务局部表描述符的选择符
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current) // 是否使用数字协处理器
__asm__("clts ; fnsave %0 ; frstor %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) { // 复制进程页表,即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长,并复制页表
task[nr] = NULL; // 如果复制失败,则置空
free_page((long) p); // 失败并释放页表
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++) // 如果父进程中有文件打开,则将对应文件的打开次数增1
if (f=p->filp[i]) // 因为子进程会与父进程共享打开的文件
f->f_count++;
if (current->pwd) // 父进程的pwd,root,executable都加一
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
if (current->library)
current->library->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); // 设置tss在全局描述符中
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); // 设置ldt在全局描述符中
p->p_pptr = current; // 设置子进程的父进程
p->p_cptr = 0; // 复位子进程的最新子进程指针
p->p_ysptr = 0; // 复位子进程的比邻兄弟进程
p->p_osptr = current->p_cptr; // 设置子进程的兄弟指针
if (p->p_osptr)
p->p_osptr->p_ysptr = p; // 如果父进程有兄弟进程,则让兄弟进程指向新进程
current->p_cptr = p; // 设置父进程的最新子进程
p->state = TASK_RUNNING; /* do this last, just in case */ // 此时当前子进程加入调用
return last_pid;
}
在系统调用完成后,会返回到sys_call.s中的ret_from_sys_call,继续执行相关信号位图的相关处理后,就完成系统调用。
此时系统调用fork完成后就生成了一个子进程执行,此时该子进程执行的任务就是执行init()函数,父进程就执行;
for(;;)
__asm__("int $0x80"::"a" (__NR_pause):"ax");
进程0被调度的时候就直接执行sys_pause的系统调用,该系统调用如下;
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE; // 设置当前任务为可中断调度
schedule(); // 重新调度任务
return 0;
}
此时进程0就是空闲的时候的被调度的任务,子进程执行init()的流程留待下文分析。