代码改变世界

2017-2018-1 20179226《Linux内核原理与分析》第三周作业

2017-10-15 16:38  20179226任逸飞  阅读(209)  评论(1编辑  收藏

实验楼实验

完成一个简单的时间片轮转多道程序内核
1.使用实验楼的虚拟机打开shell
2.用cd LinuxKernel/linux-3.9.4进入linux-3.9.4
3.执行命令qemu -kernel arch/x86/boot/bzImage会发现弹出新的窗口,代表内核启动了

4.然后cd mykernel在mykernel目录输入命令vi mymain.cvi myinterrupt.c可以查看mymain.c和myinterrupt.c文件

首先是mymain.c文件


前面是头文件,然后有一个my_start_kernel()函数,该函数是整个操作系统的入口, 变量i不停的自加1,if循环是执行i每当加到1000000的整数倍的时候就打印出当前的i的值,我们也可以通过修改if的条件使打印加快或减慢一些 。
my_start_kernel函数的代码

void __init my_start_kernel(void)

{

int pid = 0;

/* Initialize process 0/

task[pid].pid = pid;

task[pid].state = 0;/ -1 unrunnable, 0 runnable, 0 stopped /// set task 0 execute entry address to my_process

task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;

task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];

task[pid].next = &task[pid];/fork more process /

for(pid=1;pid<MAX_TASK_NUM;pid++)

{

memcpy(&task[pid],&task[0],sizeof(tPCB));

task[pid].pid = pid;

task[pid].state = -1;

task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];

task[pid].priority=get_rand(PRIORITY_MAX);//each time all tasks get a random priority

}

task[MAX_TASK_NUM-1].next=&task[0];

printk(KERN_NOTICE "\n\n\n\n\n\n system begin :>>>process 0 running!!!<<<\n\n");/ start process 0 by task[0] /

pid = 0;

my_current_task = &task[pid];

asm volatile(

"movl %1,%%esp\n\t" / set task[pid].thread.sp to esp /

"pushl %1\n\t" / push ebp /

"pushl %0\n\t" / push task[pid].thread.ip /

"ret\n\t" / pop task[pid].thread.ip to eip /

"popl %%ebp\n\t"

:

: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) / input c or d mean %ecx/%edx*/

);

该函数首先是初始化0号进程的数据结构,然后创建多个进程,启动0号进程,将task[0].thread.sp放入esp,对ebp进行压栈,然后把task[0].thread.ip进行压栈,通过ret指令,将task[0].thread.ip即程序入口my_process放到eip,ret之后0号进程正式启动了,然后对ebp进行出栈。
下面是my_process函数代码

void my_process(void)

{

int i = 0;

while(1)

  {

  i++;

  if(i%10000000 == 0)

    {

    if(my_need_sched == 1)

      {
   
      my_need_sched = 0;

      sand_priority();

      my_schedule();
   
      }

    }

  }

}

该函数是循环1000万次才有一次机会判断是否需要调度。

然后是myinterrupt.c文件


前面还是头文件,然后有一个my_timer_handler函数,里面只有一条prink的打印语句,每次时钟中断都调用这个printk()输出,所以屏幕会不停的有信息打印出来。
my_timer_handler函数代码如下

void my_timer_handler(void)

{

//if 1

// make sure need schedule after system circle 2000 times.

if(time_count%2000 == 0 && my_need_sched != 1)

{

my_need_sched = 1;

//time_count=0;

}

time_count ++ ;

//endif

return;

}

该代码是设置时间片的大小,时间片用完时设置一下调度标志。
my_timer_handler函数每隔1000次把my_need_sched的值改成1.这时my_process中if部分开始,把my_need_sched复位为0,并调用my_schedul函数。
my_schedule函数代码如下:

if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped /

{

//save current scene

/ switch to next process /

asm volatile(

"pushl %%ebp\n\t" / save ebp /

"movl %%esp,%0\n\t" / save esp /

"movl %2,%%esp\n\t" / restore esp /

"movl $1f,%1\n\t" / save eip /

"pushl %3\n\t"

"ret\n\t" / restore eip /

"1:\t" / next process start here */

"popl %%ebp\n\t"

: "=m" (prev->thread.sp),"=m" (prev->thread.ip)

: "m" (next->thread.sp),"m" (next->thread.ip)

);

my_current_task = next;//switch to the next task

printk(KERN_NOTICE " switch from %d process to %d process\n >>>process %d running!!!<<<\n\n",prev->pid,next->pid,next->pid);

}

else

{

next->state = 0;

my_current_task = next;

printk(KERN_NOTICE " switch from %d process to %d process\n >>>process %d running!!!<<<\n\n\n",prev->pid,next->pid,next->pid);


 /* switch to new process */
 asm volatile(  
     "pushl %%ebp\n\t" /* save ebp */
     "movl %%esp,%0\n\t" /* save esp */
     "movl %2,%%esp\n\t" /* restore esp */
     "movl %2,%%ebp\n\t" /* restore ebp */
     "movl $1f,%1\n\t" /* save eip */   
     "pushl %3\n\t"
     "ret\n\t" /* restore eip */
     : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
     : "m" (next->thread.sp),"m" (next->thread.ip)
 );
}

然后跳转到下一个进程。

阅读教材第3、5章

教材主要讲述了进程管理相关的知识,包括什么是进程、进程描述符及任务结构、如何创建进程、linux中线程的实现方法、如何终结进程以及系统调用的相关知识。

系统调用

1.系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。
2.一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
3.要访问系统调用,通常通过C库中定义的系统调用来进行。系统调用还会通过一个long类型的返回值来表示成功或者错误。负的返回值表示错误,返回0通常表明成功。
4.Linux中每个系统调用都有一个系统调用号。通过这个独一无二大的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称。系统调用的列表存储在sys_call_table中。
5.Linux系统调用比其他许多操作系统执行要快。Linux很短的上下文切换时间是一个重要原因,进出内核都被优化的简洁高效。另外一个原因是系统调用处理程序和每个系统调用本身也都非常简洁。
6.内核驻留在受保护的地址空间上,所以用户空间的程序不能直接调用内核空间中的函数。通常通过软中断,触发一个异常促使系统切换到内核执行系统调用处理程序。在x86上预定义的软中断中断号是128.
在x86上,系统调用号是通过eax寄存器传递给内核的。
7.内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。
8.在进程上下文中,内核可以休眠并且可以被抢占。能够休眠说明系统调用可以使用内核提供的绝大部分功能。在进程上下文中能够内抢占表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证系统调用是可重入的。
9.建立一个新的系统调用有利有弊,需谨慎使用。
10.常用的系统调用函数有fork、wait、getpid等。

进程管理

1.进程就是处于执行期的程序。但不仅仅是代码,通常还包括其他资源,如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,还有用来存放全局变量的数据段等。
2.线程是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象不是进程而是线程。一个进程可以包含多个线程。但linux系统的线程实现并不特别区分进程和线程。
3.在linux系统中,通常通过调用fork()系统创建进程。该系统通过复制一个现有进程来创建一个全新的进程。调用fork()的进程为父进程,新产生的进程为子进程。通常新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。最终程序通过exit()系统调用退出执行。
4.exit这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。
进程通过一个唯一的进程标识值或PID来标识每个进程。

进程的创建

linux进程创建分两步:fork()和exec()
fork():通过拷贝当前进程创建一个子进程
exec():读取可执行文件并将其载入地址空间开始运行。
进程创建流程:
(1)调用调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程完全相同。
(2)检查并确保新创建子进程后,当前用户所拥有的进程数目没有超出分配限制。
(3)清理新进程的信息(比如PID置0等),使之与父进程区别开。
(4)设置新进程状态为 TASK_UNINTERRUPTIBLE。
(5)更新task_struct的flags成员。
(6)调用alloc_pid()为新进程分配一个有效的PID。
(7)根据clone()的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
(8)做一些扫尾工作并返回指向子进程的指针。

进程终结

(1)设置task_struct中的标识成员设置为PF_EXITING
(2)调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
(3)do_exit()调用acct_update_integrals()输出记账信息。
(4)调用exit_mm()释放进程占用的mm_struct。
(5)调用sem__exit(),使进程离开等待IPC信号的队列。
(6) 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源。
(7)把task_struct的exit_code设置为进程的返回值。
(8)调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE。
(9)切换到新进程继续执行。