上次我们说到进程的数据结构,现在我们需要从整体上看一下操作系统是如何进行进程调度的
还记得我们之前说过操作系统要利用时钟来进行计数,发起时钟中断的中断向量号是0x20,那么时钟中断向量到底干了什么事儿呢?
system_call.s
_timer_interrupt:
...
// 增加系统滴答数
incl _jiffies
...
// 调用函数 do_timer
call _do_timer
- 将系统滴答数 jiffies这个变量+1
- 调用do_timer这个函数
sched.c
void do_timer(long cpl) {
...
// 当前线程还有剩余时间片,直接返回
if ((--current->counter)>0) return;
// 若没有剩余时间片,调度
schedule();
}
我们看这个do_timer这个函数,非常的简单,就是将当前的counter–看它是否大于0.如果等于0就进行进程调度schedule();
至于进程调度是如何进行的,也要继续看源码。
void schedule(void) {
int i, next, c;
struct task_struct ** p;
...
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
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);
}
- 拿到剩余时间片最大并且在runnable状态(state==0)的进程号next;
- 如果所有的runnable进程时间片都是0,将所有进程的counter重新赋值(counter=counter/2+priority),然后执行步骤1;
- 最后拿到进程号next,调用switch_to(next),切换到这个进程去执行。
接下来去看switch_to这个函数好了
sched.h
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
"je 1f\n\t" \
"movw %%dx,%1\n\t" \
"xchgl %%ecx,_current\n\t" \
"ljmp %0\n\t" \
"cmpl %%ecx,_last_task_used_math\n\t" \
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
主要就干了一件事,就是 ljmp 到新进程的 tss 段处。
CPU 规定,如果 ljmp 指令后面跟的是一个 tss 段,那么,会由硬件将当前各个寄存器的值保存在当前进程的 tss 中,并将新进程的 tss 信息加载到各个寄存器。
保存当前进程上下文,恢复下一个进程的上下文,跳过去!
fork函数
static _inline _syscall0(int,fork)
#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; \
}
上述的代码有一个关键指令就是0x80软中断的触发 int 0x80。
还有一个eax寄存器里面的参数是__NR_fork,这也是一个宏定义,值是2。
set_system_gate(0x80, &system_call);
_system_call:
...
call [_sys_call_table + eax*4]
...
所以说那个eax里面的值就是说去这个sys_call_table里面找下标为2的位置的那个函数,然后跳转过去。sys_call_table
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
这是一个由各种函数指针组成的一个数组,就是一个系统调用函数表。
那么从0开始的第2项就是sys_fork函数
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
通过上面的分析,我们可以知道操作系统通过系统调用提供给用户的功能都在这个系统调用表里面了,系统调用统一通过0x80这个中断号进来,然后传进来一个eax寄存器里面的值寻找到下标,在表里面找到具体的函数。
fork函数调用模板函数时,用的是syscall0.表示这个参数个数是0,而在unistd.h头文件里面,还定义了syscall0~sysycall3一共四个宏
#define _syscall0(type,name)
#define _syscall1(type,name,atype,a)
#define _syscall2(type,name,atype,a,btype,b)
#define _syscall3(type,name,atype,a,btype,b,ctype,c)
参数 a 被放在了 ebx 寄存器,参数 b 被放在了 ecx 寄存器,参数 c 被放在了 edx 寄存器。
sys_fork
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
这里调用了两个函数,我们来看一下
find_empty_process
找到控销的进程槽位。之前我们讲过存储进程的数据结构是一个task[64]数组,只有进程0,其他的都是空。一个进程的数据结构task_struct之前我们也说过了一小部分。
long last_pid = 0;
int find_empty_process(void) {
int i;
repeat:
if ((++last_pid)<0) last_pid=1;
for(i=0 ; i<64 ; i++)
if (task[i] && task[i]->pid == last_pid) goto repeat;
for(i=1 ; i<64; i++)
if (!task[i])
return i;
return -EAGAIN;
}
这里的last_pid就是当前已经分配到的进程号,现在只有0号进程被分配了,这里有一个判断就是我们将要分配的这个进程号是不是<0,如果小于0就代表溢出了,所以我们要从0开始分配,之后就是从这个task数组当中找找看是不是有和当前进程号相同的进程,如果有的话就再从头开始分配一个新的进程号,如果既没有溢出又没有用过那么就找到了进程号并且在task数组中找到一个空位去存放这个新进程。
好的,接下来的问题就是如何去构造这个新的进程的数据结构,因为我们是进程复制,所以大多数的内容和我们的0号进程是一样的,但是肯定也需要改动一些东西,那么这些需要改动的东西就是很有意思的啦!
copy_process
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;
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
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;
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;
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"::"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++)
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));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}
首先,get_free_page会在主内存末端申请一个空闲页面,也就是遍历mem_map[]这个数组找到值为0的一项,然后置1,拿到这个页的起始地址。
然后把这个地址赋值为task_struct*p,把p放到task的空位上。
把当前进程的task_struct全部赋值给即将创建的进程p。然后他们就完全一样了。
接下来就是个性化处理了一些不一样的值。
- 进程元信息:state pid counter
- tss保存的各种寄存器的信息,就是上下文。
- ss0 esp0表示0特权级【内核态】时的ss:esp指向。根据代码是指向task_struct所在的4K内存页的最顶端,之后的每个进程都是这样被设置的
然后我们看到copy_process中还有一个函数很重要copy_mem
copy_mem
int copy_mem(int nr,struct task_struct * p) {
// 局部描述符表 LDT 赋值
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);
data_limit = get_limit(0x17);
new_code_base = nr * 0x4000000;
new_data_base = nr * 0x4000000;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
// 拷贝页表
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
copy_page_tables(old_data_base,new_data_base,data_limit);
return 0;
}
这个函数就是新进程LDT表项的赋值以及页表的拷贝。
首先,局部描述符表LDT的赋值
其中段限长和进程0相同,640K
c code_limit = get_limit(0x0f); data_limit = get_limit(0x17);
段基址取决于当前是几号进程,也就是nr的值
c new_code_base = nr * 0x4000000; new_data_base = nr * 0x4000000;
0x4000000=64M,也就是说今后每个进程通过段基址的手段,分别在线性地址空间中占用64M空间,并且紧挨着。
把LDT设置进了LDT表里
c set_base(p->ldt[1],new_code_base); set_base(p->ldt[2],new_data_base);
进过上述的一系列步骤,通过分段的方式,将进程映射到了相互隔离的线性地址空间,这就是段氏管理。
但是别忘了我们还有页式管理,所有我们要实现段页式管理还需要进行页表的复制。
页表的复制
// old=0, new=64M, limit=640K
copy_page_tables(old_data_base,new_data_base,data_limit)
原来的进程0有一个页目录表和四个页表,将线性地址空间原封不动映射到了物理地址空间。
/*
* Well, here is one of the most complicated functions in mm. It
* copies a range of linerar addresses by copying only the pages.
* Let's hope this is bug-free, 'cause this one I don't want to debug :-)
*/
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;
from_dir = (unsigned long *) ((from>>20) & 0xffc);
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
to_page_table = (unsigned long *) get_free_page()
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024;
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
现在进程 0 的线性地址空间是 0 - 64M,进程 1 的线性地址空间是 64M - 128M。我们现在要造一个进程 1 的页表,使得进程 1 和进程 0 最终被映射到的物理空间都是 0 - 64M,这样进程 1 才能顺利运行起来,不然就乱套了。
除此之外,fork出来的新进程,页表项都是只读的,而且导致源进程的页表项也是只读的,这个就是写时复制的基础,新老进程一开始共享同一个物理内存空间,如果只有读那就相安无事,如果任何一方有写操作将会发生缺页中断,然后分配一块新的物理内存给产生写操作的那个进程,这块内存就不再共享了。
好的,OK,今天就说到这里吧,有问题欢迎评论区留言讨论!!!