了解前提: 《汇编: asm函数》
3特权级是普通进程
0特权级是内核级进程
// 计算机中有了一个名副其实的3特权级的进程0,第一项工作fork创建进程1
if (!fork()) { /* we count on this going ok */ //fork返回值1 ,不执行 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(;;) pause();
// fork函数实际上执行的是 unistd.h中的宏函数 syscall0 中去
//include/unistd.h
#define __NR_setup 0 /* used only by init, to get system going */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
...
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
// include/linux/sys.h
extern int sys_fork();
extern int sys_read();
extern int sys_write();
extern int sys_open();
extern int sys_close();
...
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, ... }
// _syscall0 调用fork 展开后
int fork(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \ // int 0x80 是所有系统调用函数的总入口 fork是其中之一
: "=a" (__res) \ // 第一个冒号后是输出部分,将_res赋给 eax,返回的 last_pid
: "0" (__NR_fork)); \ // 第二个冒号后是出入部分,"0" 同上是 eax
if (__res >= 0) \ // __NR_fork 是2, 将2给 eax
return (int) __res; \ // int 0x80 中断返回后,将执行这一句
errno = -__res; \
return -1; \
}
// __NR_fork 编号赋值给eax,即sys_fork函数在 sys_call_table中的偏移值
// 紧接着执行0x80软中断,CPU从3特权级进程0代码跳到0特权级内核代码中执行
// int 0x80 导致CPU硬件自动将ss,esp,eflags,cs,eip 按顺序压入init_task中进程0内核栈!
// CPU自动压栈完成后,跳转到 system_call.s 中的 _system_call: 处执行,继续
// 将DS,ES,FS,EDX,ECX,EBX 压栈(为了后面调用 copy_process 函数中初始化进程1中的 TSS 准备)
/* 汇编中对应C语言的函数名在前面多加一个下划线 _ , 如 _system_call 其实对应的C语言中的 system_call*/
// 其实也算是约定俗成,这样做的目的是为了防止符号名冲突,因为在一个程序中往往是包含汇编和C文件的,
// 汇编用于启动部分,C文件用于应用程序,最终通过编译器实现编译,对于编译器来说,汇编和C是一视同仁的,
// 那么就会有个问题,如果在汇编和C文件中使用了同一个名字,这是很可能出现的,毕竟汇编相当于机器码也算是稍微高级的语言,
// 在定义子程序或函数时,也是可以用英文拼写的,而C文件中,更会习惯用英文拼写。所以为了防止类似的符号名冲突,
// UNIX下的C语言就规定,C语言的源代码文件中的所有全局变量和函数经过编译后,相应的符号名前面会自动的加上下划线“_”。这样做的好处,
// 就是方便是程序开发人员,不用太小心翼翼的起名,避免了与汇编文件中的符号名的冲突。
// kernel/system_call.s
_system_call: # int 0x80 系统调用的总入口
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds # 下面6个push都是为了copy_process()的参数,记住压栈的顺序,
push %es # 在此之前还压了5个寄存器的值进栈(进程0)
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call _sys_call_table(,%eax,4) # eax是2,看成 call (_sys_call_table +2*4)就是 _sys_fork入口
# 转移到调用的子程序 _sys_fork, 4的意思就是每一项有4个字节
pushl %eax
movl _current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
_sys_fork:
call _find_empty_process # 调用 find_empty_process
testl %eax,%eax # 如果返回-EAGAIN(11),说明已经有64个进程在运行
js 1f
push %gs # 5个push也作为 copy_process 参数初始
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process # 调用 copy_process
addl $20,%esp
1: ret
...
// 总结下:
// 1. _syscall0(int,fork)->
_system_call: call _sys_call_table(,%eax,4)->
sys_call_table[2] ->
sys_fork: (_sys_fork)
// 2. _sys_fork -> 1. _find_empty_process: (find_empty_process)
2. _copy_process:(copy_process)
// sched_init() 函数已经对 task[64] 除0项以外的所有项清空。现在调用
// find_empty_process为进程1获得一个可用的进程号和task[64]中一个位置
// 内核使用全局变量 last_pid 存放系统开机以来累计的进程数,也将此变量用作新建进程的进程号。
// 内核第一次遍历task[64] "&&" 条件成立说明last_pid已被使用。 则++last_pid ,直到获取可用的。
int find_empty_process(void)
{
int i;
repeat:
if ((++last_pid)<0) last_pid=1; // ++last_pid 如果溢出,则置位1
for(i=0 ; i<NR_TASKS ; i++) // 现在,++last_pid后, last_pid 为 1
if (task[i] && task[i]->pid == last_pid) goto repeat;
for(i=1 ; i<NR_TASKS ; i++) // 返回第一个空闲的i
if (!task[i])
return i;
return -EAGAIN; // 如果task满了,返回 -11
}
// 进程0已经是一个可以创建子进程的父进程,内核中有进程0的task_struct和页表项
// 进程0将在copy_process函数中进行子进程的创建工作
1. 为进程1创建 task_struct,将进程0的task_struct的内容复制给进程1
2. 为进程1的 task_struct ,tss 个性化设计
3. 为进程1创建第一个页表,将进程0的页表内容赋给这个页表
4. 进程1共享进程0的文件
5. 设置进程1的GDT项
6. 最后将进程1设置为就绪态,使其可以参与进程间的轮询。
// copy_process 函数的参数都是前面的代码做了压栈!
// C语言函数如 func(x,y,z), 入栈是从右到左,这样最后入栈的 eax 就是 对应第一个 nr
// C语言栈底是低地址,栈顶是高地址。 正是由于是从右往左入栈,C语言才能实现可变长参数。
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
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;
// 在16MB内存的最高端获取一页,强制类型转换为 task_struct用
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p; //nr等于1,为什么? call _copy_process之前pushl %eax,eax就是 find_empty_process返回值
// current 指向当前进程的task_struct 的指针,当前进程是进程0,下面将父进程赋给子进程。
// 这是父子进程创建机制的重要机制,父子进程的task_struct将完全一样
// 注意类型指针,只复制 task_struct,并未将4KB都复制,即进程0的内核栈并未复制
// 父进程的进程属性复制给了子进程,子进程继承了父进程的大部分能力,这是父子进程创建机制特点之一。
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE; // 只有内核代码中明确表示将该进程设置为就绪状态才能唤醒
// 除此之外,没有任何办法将其唤醒
// 开始子进程个性化设置
p->pid = last_pid; // 设置进程pid,1进程就是1
p->father = current->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;
// 开始设置子进程的TSS
p->tss.back_link = 0;
// 这里需要注意! esp0是内核栈指针,而p是一个分页的首地址4KB,
// 内核栈指针又是从尾往前增加,所以 PAGE_SIZE + (long) p 到达尾部
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10; // 0x10 就是 10000, 0特权级,GDT,数据段
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0; // 重要!决定main()函数中 if(!fork())后面的分支走向
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); // 挂接子进程的 LDT
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %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)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); // 设置GDT中与子进程相关的项
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */ // 设置子进程为就绪态
return last_pid;
}
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t" // 找不到空闲页,跳转到1
"movb $1,1(%%edi)\n\t" // 将1赋给edi+1位置,在mem map[]中
"sall $12,%%ecx\n\t" // ecx算数左移12位,页的相对地址
"addl %2,%%ecx\n\t" // LOW MEN + ecx ,页的物理地址
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t" // 将edx + 4 KB 的有效地址赋给 edi
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
:"di","cx","dx"); // 程序中改变过的量
return __res;
}
// 重点来了!!!
// 介绍进程0的时候,static union task_union init_task = {INIT_TASK,}; // 进程0的task_struct
union task_union {
struct task_struct task; // task_struct 与内核栈的共用体
char stack[PAGE_SIZE];
};
// task_union 的设计是颇具匠心的,前面是task_struct,后面是内核栈,增长的方向正好相反,
// 正好占用一页,顺应分页机制,分配内存非常方便,而且操作系统肯定是经过反复测试,保证了
内核代码所以可能的调用导致压栈长度不会覆盖前面的task_struct.
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x0f); // 0x0f即1111(3特权级,LDT,代码段)
data_limit=get_limit(0x17); // 0x17即二进制的10111 (3特权级,LDT,数据段)
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000; // nr现在是1,,64MB
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base); // 设置子进程代码段基址
set_base(p->ldt[2],new_data_base); // 设置子进程数据段基址
// 为进程1创建第一个页表,复制进程0的页表,设置进程1的页目录项
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
/*
0x3fffff 是4MB,是一个页表的管辖范围,二进制是22个1,||的两边必须同为0,所以,
from 和 to 后22位必须都为0,即4MB的整数倍,意思是一个页表对应4MB的连续的线性地址空间
必须是从 0x00000 开始的4MB的整数倍的线性地址,不能是任意地址开始的4MB,才符合分页要求
*/
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
/*
一个页目录项的管理范围是4MB,一项是4字节,项的地址就是项数*4,也就是项管理的线性地址
起始地址的M数,比如:0项地址是0,管理范围是0~4MB,1项地址是4,管理范围是4~8MB,
2项地址是8,管理范围是 8~12MB >>20就是地址的MB数, &0xffc 就是 &1111 1111 1100b
就是4MB以下部分清零的地址MB数,也就是页目录项的地址
*/
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22; // >> 22 是 4MB数
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
/*
*from_dir 是页目录项中的地址,0xfffff000 是将低12位清零(0xffc)
高20位是页表的地址
*/
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024; // 0xA0 = 160 ,复制页表的项数
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) { // 复制父进程页表
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2; // 设置页表项属性,2是010,~2是101,代表用户、只读、存在
*to_page_table = this_page;
if (this_page > LOW_MEM) { // 1MB 以内的内核区不参与用户分页管理
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++; // 增加引用计数
}
}
}
invalidate(); // 重置CR3为0,刷新“页面换高速缓存”
return 0;
}
// 上面操作之后,此时进程1只是一个空架子,还没有应用程序,仅仅只是从进程0
// 拷贝页表,等有了自己的程序,再解除关系
// copy_process 中最后将进程1设置为就绪态,进程1就可以参与进程调度了。 最后返回进程号1
// 再看 system_call.s
_system_call: # int 0x80 系统调用的总入口
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds # 下面6个push都是为了copy_process()的参数,记住压栈的顺序,
push %es # 在此之前还压了5个寄存器的值进栈(进程0)
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call _sys_call_table(,%eax,4) # eax是2,看成 call (_sys_call_table +2*4)就是 _sys_fork入口
# 转移到调用的子程序 _sys_fork, 4的意思就是每一项有4个字节
pushl %eax # sys_fork返回到这里执行,eax就是返回值 last_pid
movl _current,%eax # 当前进程是进程0
cmpl $0,state(%eax) # state
jne reschedule # 如果进程0不是就绪态,则进程调度
cmpl $0,counter(%eax) # counter
je reschedule # 如果进程0没有时间片,则进程调度
_sys_fork:
call _find_empty_process # 调用 find_empty_process
testl %eax,%eax # 如果返回-EAGAIN(11),说明已经有64个进程在运行
js 1f
push %gs # 5个push也作为 copy_process 参数初始
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process # 调用 copy_process
addl $20,%esp #esp+=20 就是esp清20字节的栈,也就是前面压的 gs,esi,edi,ebp,eax
1: ret #返回_system_call中的 pushl %eax 执行
...
void main(void)
{
if (!fork()) { /* we count on this going ok */ //fork返回值1 ,不执行 init
init();
}
for(;;) pause();
}
/*
pause 函数调用与fork 调用一样。
syscall0 -> call _sys_call_table(,%eax,4) -> sys_pause()
*/
int sys_pause(void)
{
// 将当前进程0设置为可中断等待状态,如果产生中断,或者其他进程给这个进程发送特定信号
// 才有可能改变这个进程的状态为就绪态
current->state = TASK_INTERRUPTIBLE;
schedule();
return 0;
}
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) { // 如果设置了定时或定时已过
(*p)->signal |= (1<<(SIGALRM-1)); // 设置 SIGALRM
(*p)->alarm = 0; // alarm清0
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE) // 现在还不是这种情况
(*p)->state=TASK_RUNNING;
}
/* this is the scheduler proper: */
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
//找出就绪态中 counter 最大的进程
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
}
/*
* Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall
* 4-TSS0, 5-LDT0, 6-TSS1 etc ...
*/
#define FIRST_TSS_ENTRY 4
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
#define switch_to(n) {\
struct {long a,b;} __tmp; \ // 为ljmp的CS、EIP准备的数据结构
__asm__("cmpl %%ecx,_current\n\t" \
"je 1f\n\t" \ // 如果进程n是当前进程,没必要切换,退出
"movw %%dx,%1\n\t" \ // EDX的低字赋给*&__tmp.b,即把CS赋给.b
"xchgl %%ecx,_current\n\t" \ // task[n]与task[current]交换
"ljmp %0\n\t" \ // ljmp到 __tmp,__tmp中有偏移、段选择符,
"cmpl %%ecx,_last_task_used_math\n\t" \ // 比较上次是否使用过协处理器
"jne 1f\n\t" \
"clts\n" \ // 清除CR0中的切换任务标志
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \ //.a 对应EIP, .b 对应CS
"d" (_TSS(n)),"c" ((long) task[n])); \ // EDX是TSS n的索引号 ECX=task[n]
}
总结:
pause()函数通过int 0x80中断从3特权级的进程0代码翻转到0特权级的内核代码执行
_system_call 中的 call _sys_call_table(,%eax,4) 调用 sys_pause()->
schedule()-> switch_to()
接下来,轮到进程1执行。