一、根据实验楼引导,搭建内核
# 注意路径是区分大小的
$ cd ~/LinuxKernel/linux-3.9.4
$ rm -rf mykernel
$ patch -p1 < ../mykernel_for_linux3.9.4sc.patch
$ make allnoconfig
# 编译内核请耐心等待
$ make
$ qemu -kernel arch/x86/boot/bzImage
指令分析:
- cd~/LinuxKernel/linux-3.9.4:切换到用户的主目录下的LinuxKernel文件夹中的linux-3.9.4目录。
- rm -rf mykernel:删除当前目录下的mykernel目录及其所有内容,-rf参数表示递归删除且不提示确认。
- patch -p1 < …/mykernel_for_linux3.9.4sc.patch:应用一个补丁文件到当前目录。-p1参数表示补丁文件相对于源文件的路径偏移量,<是将补丁文件的内容重定向到patch命令。
- make allnoconfig:使用make工具来配置内核,allnoconfig目标会生成一个默认的配置文件,这个配置文件会禁用所有可选的内核配置项,但保留所有核心功能。
- make:开始编译内核。
- qemu-kernel arch/x86/boot/bzImage:使用QEMU模拟器来启动编译好的内核。arch/x86/boot/bzImage是内核映像文件的路径,QEMU会使用这个映像文件来模拟启动一个x86架构的Linux系统。
使用make编译完成后的情况如下图所示。
使用qmeu启动镜像,运行结果如下。
二、编写时间片轮转多道程序
进入mykernel目录,使用vim编辑mymain.c、myinterrupt.c,并创建mypcb.h文件。
修改后的文件代码如下:
1、mypcb.h
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8
/* CPU-specific state of this task */
struct Thread {
unsigned long ip;
unsigned long sp;
};
typedef struct PCB{
int pid;
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
char stack[KERNEL_STACK_SIZE];
/* CPU-specific state of this task */
struct Thread thread;
unsigned long task_entry;
struct PCB *next;
}tPCB;
void my_schedule(void);
这段代码是操作系统中用于管理进程的核心部分,它定义了进程控制块(PCB)的结构,并包含了一些基本的进程管理功能。下面是对代码的分析:
- 宏定义
- MAX_TASK_NUM:定义了系统能够支持的最大任务(进程)数量。
- KERNEL_STACK_SIZE:定义了每个任务的内核栈大小。
- 线程状态结构体
- struct Thread:定义了一个结构体,用于存储与CPU相关的任务状态。
- PCB结构体
- typedef struct PCB:定义了进程控制块的结构体,并给它一个别名tPCB。
- 调度函数
- void my_schedule(void):定义了一个名为my_schedule的函数,这个函数通常用于决定下一个要运行的进程,是操作系统调度算法的一部分。
2、mymain.c
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>
#include "mypcb.h"
tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;
void my_process(void);
void __init my_start_kernel(void)
{
int pid = 0;
int i;
/* Initialize process 0*/
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
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(i=1;i<MAX_TASK_NUM;i++)
{
memcpy(&task[i],&task[0],sizeof(tPCB));
task[i].pid = i;
task[i].state = -1;
task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
task[i].next = task[i-1].next;
task[i-1].next = &task[i];
}
/* 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*/
);
}
void my_process(void)
{
int i = 0;
while(1)
{
i++;
if(i%10000000 == 0)
{
printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
if(my_need_sched == 1)
{
my_need_sched = 0;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}
代码分析如下:
- 定义了一些基本的进程控制块(PCB),每个块都包含了进程的基本信息,如进程ID、状态、内核栈等。
- 初始化了内核,创建了几个进程,并将它们链接成一个链表。第一个进程被设置为可运行状态,其他的则设置为不可运行。
- 通过汇编语言切换到第一个进程的执行上下文,开始执行一个名为my_process的函数。
- my_process函数是一个简单的循环,它会定期打印进程信息,并在需要时调用调度函数my_schedule来切换到其他进程。
3、myinterrupt.c
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>
#include "mypcb.h"
extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;
/*
* Called by timer interrupt.
* it runs in the name of current running process,
* so it use kernel stack of current running process
*/
void my_timer_handler(void)
{
#if 1
if(time_count%1000 == 0 && my_need_sched != 1)
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
my_need_sched = 1;
}
time_count ++ ;
#endif
return;
}
void my_schedule(void)
{
tPCB * next;
tPCB * prev;
if(my_current_task == NULL
|| my_current_task->next == NULL)
{
return;
}
printk(KERN_NOTICE ">>>my_schedule<<<\n");
/* schedule */
next = my_current_task->next;
prev = my_current_task;
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
{
/* 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;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
}
else
{
next->state = 0;
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->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)
);
}
return;
}
代码分析如下:
- 定时器中断处理
- 代码中的my_timer_handler函数是定时器中断的处理程序。每当定时器触发中断时,这个函数就会被调用。
- time_count变量用于记录中断发生的次数。每当time_count达到1000的倍数时,my_timer_handler函数会通过printk函数打印一条消息,并设置my_need_sched变量为1,这表示系统需要进行进程调度。
- 进程调度准备
- my_schedule函数是进程调度的核心,它负责选择下一个要执行的进程。
- 如果当前没有正在运行的进程(my_current_task为NULL)或者没有下一个进程可调度(my_current_task->next为NULL),my_schedule函数将直接返回,不进行调度。
- 上下文切换
- 当确定需要调度时,my_schedule函数会保存当前进程的状态(包括栈指针和指令指针),并加载下一个进程的状态。
- 这个过程通过内联汇编实现,它直接操作CPU寄存器来完成上下文切换。这相当于告诉CPU停止执行当前进程的代码,转而执行下一个进程的代码。
- 更新进程状态和打印日志
- 一旦完成上下文切换,my_schedule函数会更新my_current_task指针,指向新的当前进程。
- 同时,它会打印一条消息,显示进程切换的信息,包括从哪个进程切换到了哪个进程。
- 如果下一个进程的状态不是可运行(状态不是0),则将其状态设置为可运行,并执行相同的上下文切换操作。
返回上级目录,重新进行编译
编译成功
三、实验结果
使用qemu -kernel arch/x86/boot/bzImage运行
执行过程从process1到process2再到process3,最后到precess0;循环往复。
四、实验总结
操作系统(OS)是计算机系统中负责管理硬件资源和提供用户程序运行环境的软件。它的核心功能之一是进程管理,包括进程的创建、调度、同步和通信。下面从进程的角度对操作系统工作原理的描述:
-
进程创建:
操作系统在启动时会创建初始进程,这个初始进程通常负责加载其他程序和创建更多的进程。在mymain.c文件中,展示了如何初始化0号进程,并通过复制0号进程的进程控制块(PCB)来创建新的进程。这些进程被组织成链表,以便操作系统进行管理和调度。 -
进程控制块(PCB):
PCB是操作系统用于跟踪和管理进程状态的数据结构。它包含了进程ID、状态、内核栈、CPU寄存器状态(如程序计数器和栈指针)等信息。操作系统通过PCB来监控进程的生命周期,包括进程的创建、运行、等待和终止状态。 -
进程调度:
操作系统需要在多个进程之间进行切换,以确保每个进程都能获得CPU时间。在myinterrupt.c文件中,展示了一个简单的时钟中断处理程序,它定期触发,以检查是否需要进行进程调度。如果需要,调度函数my_schedule会保存当前进程的状态,并选择下一个进程来执行。这个过程确保了进程能够公平地共享CPU资源。 -
进程同步:
操作系统提供了多种机制来同步进程的执行,以避免竞态条件和数据不一致。这些机制包括互斥锁、信号量、条件变量等。通过这些同步机制,操作系统确保了进程在访问共享资源时的安全性。 -
进程通信:
操作系统还提供了进程间通信(IPC)机制,允许进程之间交换信息。这些机制包括管道、消息队列、共享内存和套接字等。通过IPC,进程可以协调它们的活动,共享数据,并协同工作。 -
中断处理:
操作系统通过中断机制来响应硬件事件,如用户输入、设备完成操作等。中断处理程序运行在当前进程的上下文中,处理完中断后,操作系统可以选择是否进行进程切换。
总结来说,操作系统通过管理进程的生命周期、调度进程的执行、同步进程的访问共享资源、提供进程间通信机制以及处理中断,来实现其核心功能。这些机制确保了计算机系统能够高效地运行多个进程,并在硬件资源之间进行合理分配。通过这些内核文件,我们可以看到操作系统是如何在底层实现这些功能的,从而为应用程序提供稳定和高效的运行环境。
“20242817李臻 原创作品转载请注明出处 《Linux内核分析》《Linux内核分析》MOOC课程Linux内核分析MOOC课程”