v0.0.8的项目地址:
github.com地址:
https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
Dropbox地址:
点此进入Dropbox网盘 (该版本位于zenglOX_v0.0.8的文件夹,该文件夹里的zip压缩包为源代码)
sourceforge地址:
https://sourceforge.net/projects/zenglox/files (该版本位于zenglOX_v0.0.8的文件夹,该文件夹里的zip压缩包为源代码)
另外再附加一个英特尔英文手册的共享链接地址:
https://www.dropbox.com/sh/brrv4dnher09s2d/AABa6QRpN-uc6tDpvEd7KtmMa (里面的intel_manual.pdf有任务管理相关的内容)
有关zenglOX的编译及gdb调试的方法,请参考zenglOX v0.0.1里的文章。
多任务系统是指在一个系统里同时运行几个任务,每个任务执行一小段时间,在发生时间中断时再迅速切换到另一个任务,这样几个任务看起来就像同时在运行一样。
实现多任务一般有两种方式:第一种是通过TSS(task-state segment)结构,以硬件方式来进行切换。第二种是通过软件方式来实现多任务。
第一种硬件方式可以参考上面英特尔英文手册的第2070页,即手册的第三卷第七章 - Task Management 任务管理章节。
zenglOX里使用的是第二种软件方式来实现的多任务,使用软件方式一方面可以增加可移植性(因为不依赖特殊的硬件结构),另一方面,硬件切换并不会比软件切换快多少。像Linux之类的系统也是采用软件的方式来实现多任务的,软件方式虽然也需要一个TSS结构,不过该TSS结构是全局性的,只是为了让用户模式能够切换到内核态运行而准备的,由于当前v0.0.8版本还是在内核态运行,所以目前还不需要用到TSS结构,在下一个版本里,由于要切换到用户态运行,所以才会添加一个全局的TSS结构,有关TSS结构的相关说明,可以参考英特尔手册的第2072页到2073页的内容,或者参考下一个版本的代码。
zenglOX里和多任务相关的代码主要定义在新增的zlox_task.c与zlox_process.s文件里,相关的结构体则定义在zlox_task.h头文件里。
zlox_task.h头文件的内容如下:
......................................
......................................
typedef struct _ZLOX_TASK ZLOX_TASK;
// This structure defines a 'task' - a process.
struct _ZLOX_TASK
{
ZLOX_SINT32 id; // Process ID.
ZLOX_UINT32 esp, ebp; // Stack and base pointers.
ZLOX_UINT32 eip; // Instruction pointer.
ZLOX_UINT32 init_esp; // stack top
ZLOX_PAGE_DIRECTORY * page_directory; // Page directory.
ZLOX_TASK * next; // The next task in a linked list.
};
......................................
......................................
|
上面的ZLOX_TASK就是task(任务)或者叫做process(进程)的结构体,在该结构体里第一个id字段是进程的标识符,每个任务都有一个唯一的标识符,esp,ebp,eip字段则是为了任务切换时保存和恢复当前的任务环境用的,esp字段用于存储任务的栈顶指针,ebp字段用于存储任务的基址指针,eip字段用于存储任务的指令指针。
init_esp字段用于存储任务的初始esp值,在创建一个新任务时,会根据该字段,为新任务创建一个独立的栈空间。page_directory字段用于存储任务的页目录地址,每个任务都有自己独立的页目录结构,这样在两个不同的任务里,相同的线性地址就可以通过页目录映射到不同的物理地址,目前版本里,所有任务的栈的线性地址都是一样的,但是这些任务的栈的实际物理地址都不相同,即都有自己独立的栈空间,这些就是通过每个任务独立的页目录来完成的。
next字段用于存储下一个任务的指针,通过next字段将所有的任务链接在一起,从而构成一个任务链表结构。
在讲解任务相关的代码之前,有必要先了解一下zenglOX里多任务的内存布局情况,如下图所示:
图1
从上图可以看出来,每个任务的线性地址空间都是一样的,只不过每个任务的栈的实际物理地址不同而已,内核代码(包括ram disk,内核页表在内)以及内核堆在系统里多任务之间都是共用的。
在zlox_task.c文件里有个zlox_fork函数,该函数的作用是:根据当前任务来创建一个新任务,该函数的代码如下:
ZLOX_SINT32 zlox_fork()
{
// We are modifying kernel structures, and so cannot interrupt
asm volatile("cli");
// Take a pointer to this process' task struct for later reference.
ZLOX_TASK * parent_task = (ZLOX_TASK *)current_task;
// Clone the address space.
ZLOX_PAGE_DIRECTORY * directory = zlox_clone_directory(current_directory,0);
// Create a new process.
ZLOX_TASK * new_task = (ZLOX_TASK *)zlox_kmalloc(sizeof(ZLOX_TASK));
new_task->id = next_pid++;
new_task->esp = new_task->ebp = 0;
new_task->eip = 0;
new_task->init_esp = current_task->init_esp;
new_task->page_directory = directory;
new_task->next = 0;
// Add it to the end of the ready queue.
ZLOX_TASK *tmp_task = (ZLOX_TASK *)ready_queue;
while (tmp_task->next)
tmp_task = tmp_task->next;
tmp_task->next = new_task;
// This will be the entry point for the new process.
ZLOX_UINT32 eip = _zlox_read_eip();
// We could be the parent or the child here - check.
if (current_task == parent_task)
{
// We are the parent, so set up the esp/ebp/eip for our child.
ZLOX_UINT32 esp; asm volatile("mov %%esp, %0" : "=r"(esp));
ZLOX_UINT32 ebp; asm volatile("mov %%ebp, %0" : "=r"(ebp));
ZLOX_UINT32 tmp_esp;
current_directory = new_task->page_directory;
// 为新进程拷贝栈数据
for(tmp_esp = esp; tmp_esp < new_task->init_esp ;tmp_esp += 0x1000)
{
zlox_page_copy(tmp_esp);
}
current_directory = current_task->page_directory;
new_task->esp = esp;
new_task->ebp = ebp;
new_task->eip = eip;
asm volatile("sti");
return new_task->id;
}
else
{
// We are the child.
return 0;
}
}
|
从上面的代码里可以看到,在创建新任务之前,会先通过zlox_clone_directory(current_directory,0)函数,根据当前任务的页目录来创建出一个新的页目录结构,稍候就可以根据新的页目录结构将新任务的栈映射到独立的物理内存里。另外,要创建新的任务,还需要为其准备一个ZLOX_TASK结构,这里是用zlox_kmalloc(sizeof(ZLOX_TASK))函数在内核堆里为其创建一个任务结构,接着,就可以对该结构体里的各个字段,如id,esp,eip等进行初始化,初始化完后,就可以将新任务的ZLOX_TASK结构体加入到任务链表里了,上面代码是用while循环结构先定位到当前任务链表里的最后一个任务,然后将新任务的结构体指针赋值给最后一个任务的next字段,从而加入到链表里。
在将新任务加入到任务链表里后,接着调用了一个_zlox_read_eip()函数,该函数定义在zlox_process.s文件里:
# zlox_process.s some functions relate to process
.global _zlox_read_eip
_zlox_read_eip:
pop %eax # Get the return address
jmp *%eax # Return. Can't use RET because return address popped off the stack.
|
由于在调用进入_zlox_read_eip函数时,原来的C程式会将返回地址压入栈,通过pop %eax指令就可以将函数的返回地址,也就是C程式里调用_zlox_read_eip的下一条指令的地址设置到EAX里(EAX是函数的返回值),通过jmp *%eax间接跳转回原函数后,原来的C程式就可以从返回值里得到下一条指令的地址了,我们可以将这个地址作为新任务的eip值,这样当任务切换到新任务时,新任务就可以从eip指向的指令开始执行。
在得到新任务的eip后,有一个if (current_task == parent_task)的条件判断,这是因为如果是新任务刚开始执行时,新任务根据eip值,也会到达这里,新任务到达这里时,current_task会是新任务的指针,parent_task则是新任务栈里保存的创建该任务的父任务的指针值,所以,新任务到达这里时,是通不过if的条件判断的,就会从else里直接return 0返回,所以新任务从zlox_fork里出来后,返回值会是0。
如果是创建新任务的父任务到达上面的if条件判断时,current_task等于parent_task,就会通过条件判断,然后执行if条件块里的代码,在if代码块里,主要是通过zlox_page_copy函数将新任务的栈映射到其他物理内存里,同时将当前任务的栈数据拷贝到新任务的栈里。最后设置新任务的esp,ebp和eip字段,最后返回新任务的id(进程标识符),所以父任务从zlox_fork里出来后,返回值会是所创建的新任务的进程id值。
在上面,我们提到了两个函数:zlox_clone_directory和zlox_page_copy函数,这两个函数都定义在zlox_paging.c文件里:
.....................................
.....................................
ZLOX_VOID zlox_page_copy(ZLOX_UINT32 copy_address)
{
ZLOX_UINT32 phys;
ZLOX_PAGE_TABLE * newTable;
copy_address /= 0x1000;
ZLOX_UINT32 table_idx = copy_address / 1024;
ZLOX_UINT32 page_idx = copy_address % 1024;
if( (current_directory->tablesPhysical[table_idx] & 0x2) == 0)
{
newTable = zlox_clone_table(current_directory->tables[table_idx],&phys,0);
}
ZLOX_PAGE_TABLE * oldTable = current_directory->tables[table_idx];
zlox_alloc_frame_do(&newTable->pages[page_idx], 0, 1);
if (oldTable->pages[page_idx].present) newTable->pages[page_idx].present = 1;
if (oldTable->pages[page_idx].user) newTable->pages[page_idx].user = 1;
if (oldTable->pages[page_idx].accessed) newTable->pages[page_idx].accessed = 1;
if (oldTable->pages[page_idx].dirty) newTable->pages[page_idx].dirty = 1;
newTable->pages[page_idx].rw = 1;
_zlox_copy_page_physical(oldTable->pages[page_idx].frame*0x1000,
newTable->pages[page_idx].frame*0x1000);
current_directory->tables[table_idx] = newTable;
current_directory->tablesPhysical[table_idx] = phys | 0x07;
}
.....................................
.....................................
static ZLOX_PAGE_TABLE * zlox_clone_table(ZLOX_PAGE_TABLE * src, ZLOX_UINT32 * physAddr, ZLOX_UINT32 needCopy)
{
// Make a new page table, which is page aligned.
ZLOX_PAGE_TABLE * table = (ZLOX_PAGE_TABLE *)zlox_kmalloc_ap(sizeof(ZLOX_PAGE_TABLE), physAddr);
// Ensure that the new table is blank.
zlox_memset((ZLOX_UINT8 *)table, 0, sizeof(ZLOX_PAGE_TABLE));
// For every entry in the table...
ZLOX_SINT32 i;
for (i = 0; i < 1024; i++)
{
// If the source entry has a frame associated with it...
if (src->pages[i].frame)
{
if(needCopy == 1)
{
// Get a new frame.
zlox_alloc_frame(&table->pages[i], 0, 0);
// Clone the flags from source to destination.
if (src->pages[i].present) table->pages[i].present = 1;
if (src->pages[i].rw) table->pages[i].rw = 1;
if (src->pages[i].user) table->pages[i].user = 1;
if (src->pages[i].accessed) table->pages[i].accessed = 1;
if (src->pages[i].dirty) table->pages[i].dirty = 1;
// Physically copy the data across. This function is in process.s.
_zlox_copy_page_physical(src->pages[i].frame*0x1000, table->pages[i].frame*0x1000);
}
else
{
table->pages[i] = src->pages[i];
table->pages[i].rw = 0; // 读写位清零,用于写时复制
}
}
}
return table;
}
ZLOX_PAGE_DIRECTORY * zlox_clone_directory(ZLOX_PAGE_DIRECTORY * src , ZLOX_UINT32 needCopy)
{
ZLOX_UINT32 phys;
// Make a new page directory and obtain its physical address.
ZLOX_PAGE_DIRECTORY * dir = (ZLOX_PAGE_DIRECTORY *)zlox_kmalloc_ap(sizeof(ZLOX_PAGE_DIRECTORY), &phys);
// Ensure that it is blank.
zlox_memset((ZLOX_UINT8 *)dir, 0, sizeof(ZLOX_PAGE_DIRECTORY));
// Get the offset of tablesPhysical from the start of the page_directory_t structure.
ZLOX_UINT32 offset = (ZLOX_UINT32)dir->tablesPhysical - (ZLOX_UINT32)dir;
// Then the physical address of dir->tablesPhysical is:
dir->physicalAddr = phys + offset;
// Go through each page table. If the page table is in the kernel directory, do not make a new copy.
ZLOX_SINT32 i;
for (i = 0; i < 1024; i++)
{
if (!src->tables[i])
continue;
if (kernel_directory->tables[i] == src->tables[i])
{
// It's in the kernel, so just use the same pointer.
dir->tables[i] = src->tables[i];
dir->tablesPhysical[i] = src->tablesPhysical[i];
}
else
{
// Copy the table.
ZLOX_UINT32 phys;
if(needCopy == 1)
{
dir->tables[i] = zlox_clone_table(src->tables[i], &phys , 1);
dir->tablesPhysical[i] = phys | 0x07;
}
else
{
dir->tables[i] = src->tables[i];
// 读写位清零,用于写时复制
dir->tablesPhysical[i] = src->tablesPhysical[i] & (~0x2);
}
}
}
return dir;
}
|
上面代码里,zlox_clone_directory函数会先通过zlox_kmalloc_ap(sizeof(ZLOX_PAGE_DIRECTORY), &phys)函数在内核堆里创建一个新的页目录结构,然后根据第一个src参数,将src对应的页目录里的页表信息拷贝到新的页目录里。
在上面的for循环中,如果是之前图1里的内核代码和内核堆的页表项,则直接拷贝到新的页目录里,这样,所有任务就都共用相同的内核代码和内核堆。
如果是和内核无关的页表项,则当needCopy参数为1时,就会调用zlox_clone_table函数来创建一个新的页表,同时将旧页表对应的物理内存里的数据,拷贝到新页表对应的物理内存里,当needCopy参数为0时,则直接将旧页表项复制到新的页目录里,并将旧页表项里的rw(用户可读写)位设置为0,这样当系统进入用户态运行时,对该页表项引用的线性地址区段进行写操作时,就可以触发分页错误,在分页错误里,就可以进行一些写时复制的操作。
在前面介绍的zlox_fork创建新任务的函数里,调用zlox_clone_directory函数时,第二个参数needCopy设置为0,也就是说,新建的任务里,对于非内核和非任务栈的线性地址都是采用写时复制的方式,这种方式只在需要时,才进行物理内存的分配工作,从而提高了物理内存的利用率,当然,目前的v0.0.8的版本里,由于代码都是在内核态运行,所以写时复制在目前的版本里不会起作用,只有ring 3的用户态程序才会触发写时复制,这里的代码只是提前做个准备。
至于上面代码里,zlox_page_copy函数的作用则是:当进行写时复制操作时,为参数copy_address对应的线性地址创建新的页表和新的4K物理页面,同时将该线性地址对应的旧物理内存里的数据拷贝到新的4K物理页面里(旧物理内存里的数据主要是父进程里的数据),这样父子进程里,相同的线性地址(除了内核部分)就对应不同的物理内存地址,即每个任务都拥有自己独立的物理内存空间。
zlox_page_copy函数还在zlox_fork里出现过,它在zlox_fork函数里主要是为新任务的栈分配独立的物理内存空间。
前面介绍的zlox_fork函数是父进程创建子进程的函数,那么系统的第一个进程又是在哪个函数里创建的呢? 答案是:zlox_initialise_tasking函数,该函数也位于zlox_task.c文件里:
ZLOX_VOID zlox_initialise_tasking()
{
// Rather important stuff happening, no interrupts please!
asm volatile("cli");
// Relocate the stack so we know where it is.
zlox_move_stack((ZLOX_VOID *)0xE0000000, 0x2000);
// Initialise the first task (kernel task)
current_task = ready_queue = (ZLOX_TASK *)zlox_kmalloc(sizeof(ZLOX_TASK));
current_task->id = next_pid++;
current_task->esp = current_task->ebp = 0;
current_task->eip = 0;
current_task->init_esp = 0xE0000000;
current_task->page_directory = current_directory;
current_task->next = 0;
// Reenable interrupts.
asm volatile("sti");
}
|
内核初始化时,就会调用上面的zlox_initialise_tasking函数来初始化第一个任务(也可以叫做第一个进程),在该函数里,一开始会调用zlox_move_stack((ZLOX_VOID *)0xE0000000, 0x2000);函数将栈搬迁到0xE0000000的线性地址处,内核刚启动时的栈是GRUB分配的,该栈的地址位于0到1M之间,当开启多任务后,由于1M以内的部分也属于内核页表部分,这样,当zlox_clone_directory函数为新任务创建新的页目录时,内核部分的页表都不会做处理,这样就没办法为新任务的栈创建独立的物理内存,所以需要先将栈移动到内核线性地址以外的部分,然后就可以调用zlox_page_copy函数为新任务的栈创建独立的物理内存空间了。
在转移栈空间后,就可以调用zlox_kmalloc(sizeof(ZLOX_TASK))函数来创建出第一个任务的结构体,并对该任务结构体里的各个字段依次进行初始化。
以上是多任务的创建和多任务的内存布局相关的内容,下面再看下系统是如何通过软件的方式来切换多任务的。
前面提到过,多任务切换是在PIT定时器中断时发生的(PIT相关的内容请参考之前v0.0.4版本的文章),定时器的中断处理函数是zlox_time.c文件里的zlox_timer_callback函数:
static ZLOX_VOID zlox_timer_callback(/*ZLOX_ISR_REGISTERS regs*/)
{
tick++;
//zlox_monitor_write("zenglOX Tick: ");
//zlox_monitor_write_dec(tick);
//zlox_monitor_write("\n");
zlox_switch_task();
}
|
从上面代码可以看到,zlox_timer_callback函数会通过zlox_switch_task函数来完成任务的切换,zlox_switch_task函数定义在zlox_task.c文件里:
ZLOX_VOID zlox_switch_task()
{
// If we haven't initialised tasking yet, just return.
if (!current_task)
return;
// Read esp, ebp now for saving later on.
ZLOX_UINT32 esp, ebp, eip;
asm volatile("mov %%esp, %0" : "=r"(esp));
asm volatile("mov %%ebp, %0" : "=r"(ebp));
// Read the instruction pointer. We do some cunning logic here:
// One of two things could have happened when this function exits -
// (a) We called the function and it returned the EIP as requested.
// (b) We have just switched tasks, and because the saved EIP is essentially
// the instruction after read_eip(), it will seem as if read_eip has just
// returned.
// In the second case we need to return immediately. To detect it we put a dummy
// value in EAX further down at the end of this function. As C returns values in EAX,
// it will look like the return value is this dummy value! (0x12345).
eip = _zlox_read_eip();
// Have we just switched tasks?
if (eip == 0x12345)
return;
// No, we didn't switch tasks. Let's save some register values and switch.
current_task->eip = eip;
current_task->esp = esp;
current_task->ebp = ebp;
// Get the next task to run.
current_task = current_task->next;
// If we fell off the end of the linked list start again at the beginning.
if (!current_task)
current_task = ready_queue;
eip = current_task->eip;
esp = current_task->esp;
ebp = current_task->ebp;
// Make sure the memory manager knows we've changed page directory.
current_directory = current_task->page_directory;
// Here we:
// * Stop interrupts so we don't get interrupted.
// * Temporarily puts the new EIP location in ECX.
// * Loads the stack and base pointers from the new task struct.
// * Changes page directory to the physical address (physicalAddr) of the new directory.
// * Puts a dummy value (0x12345) in EAX so that above we can recognise that we've just
// switched task.
// * Restarts interrupts. The STI instruction has a delay - it doesn't take effect until after
// the next instruction.
// * Jumps to the location in ECX (remember we put the new EIP in there).
/*asm volatile(
//"pushf\n\t"
//"cli\n\t"
"movl %0, %%ecx\n\t"
"movl %1, %%esp\n\t"
"movl %2, %%ebp\n\t"
"movl %3, %%cr3\n\t"
"movl $0x12345, %%eax\n\t"
//"popf\n\t"
"jmp *%%ecx"
: : "m"(eip), "m"(esp), "m"(ebp), "m"(current_directory->physicalAddr));*/
_zlox_switch_task_do(eip,esp,ebp,current_directory->physicalAddr);
}
|
上面代码其实就是先将当前任务的eip,esp,ebp之类的值存储到当前任务的结构体里(下一次切换回来时,就会从eip,esp所在的环境继续执行),然后从任务链表里找到下一个要执行的任务,并将该任务的eip,esp,ebp,page_directory(该任务的页目录)信息提取出来,最后将这些信息作为参数,通过调用_zlox_switch_task_do函数切换到另一个任务,_zlox_switch_task_do函数定义在zlox_process.s文件里:
.global _zlox_switch_task_do
_zlox_switch_task_do:
cli
movl 4(%esp),%ecx # eip
movl 8(%esp),%edx # esp tmp
movl 12(%esp),%ebp # ebp
movl 16(%esp),%eax # cr3 tmp
movl %eax,%cr3 # cr3
movl $0x12345, %eax # eax
movl %edx,%esp # esp
jmp *%ecx
|
上面代码在设置好要切换任务的esp,ebp,cr3(保存页目录的物理地址)寄存器后,最后通过jmp *%ecx间接跳转到该任务的eip字段所指向的指令处。
该版本除了添加了和多任务相关的代码外,还对zlox_common.c文件里的一些和内存拷贝相关的函数进行了调整,例如该文件里的zlox_memcpy函数:
// Copy len bytes from src to dest.
ZLOX_VOID zlox_memcpy(ZLOX_UINT8 *dest, const ZLOX_UINT8 *src, ZLOX_UINT32 len)
{
const ZLOX_UINT8 *sp = (const ZLOX_UINT8 *)src;
ZLOX_UINT8 *dp = (ZLOX_UINT8 *)dest;
if(len == 0)
return;
//for(; len != 0; len--)
// *dp++ = *sp++;
// 使用movsl和movsb来提高内存拷贝的效率
asm volatile (
"pushf\n\t"
"movl %%edx,%%ecx\n\t"
"shrl $2, %%ecx\n\t" // 长度除以4,得到的商使用movsl进行拷贝,因为每次movsl可以拷贝4个字节
"cld\n\t"
"rep movsl\n\t"
"movl %%edx, %%ecx\n\t"
"andl $3, %%ecx\n\t" // and运算得到除以4的余数,余数使用movsb每次拷贝一个字节
"rep movsb\n\t"
"popf" // 恢复eflags里的标志,主要是将上面cld清零的DF标志恢复为原始值
::"S"(sp),"D"(dp),"d"(len):"%ecx");
}
|
zlox_memcpy函数使用内联汇编代码来代替原来的C代码,使用movsl和movsb汇编指令来提高内存拷贝的效率,有关内联汇编和movsl指令的相关内容可以参考
汇编语言栏目里的相关文章。
最后,在zlox_kernel.c文件的zlox_kernel_main主入口函数里,通过zlox_initialise_tasking()和zlox_fork()函数来创建新的任务:
//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{
...................................
...................................
//初始化任务系统
zlox_initialise_tasking();
// Write out a sample string
zlox_monitor_write("hello world!\nwelcome to zenglOX v0.0.8!\n");
// Initialise the initial ramdisk, and set it as the filesystem root.
fs_root = zlox_initialise_initrd(initrd_location);
ZLOX_SINT32 ret = zlox_fork();
zlox_monitor_write("fork() returned ");
zlox_monitor_write_hex(ret);
zlox_monitor_write(", and getpid() returned ");
zlox_monitor_write_hex(zlox_getpid());
zlox_monitor_write("\n============================================================================\n");
// The next section of code is not reentrant so make sure we aren't interrupted during.
asm volatile("cli");
...................................
...................................
}
|
v0.0.8版本在bochs里的运行结果如下:
图2
OK,就到这里,休息,休息一下 o(∩_∩)o~~