linux内核分析之-进程管理
作者:郎勇
前言
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
因此,学习进程的原理,进程是如何创建的,进程是如何调度的,是深入理解操作系统原理的重中之重。我们将在本篇实验中尝试在linux内核之上,运行一个简单的时间片轮转多道程序。用来模拟计算机中最基本的进程调度行为。
本博客的编写初衷是为了完成网易云课堂的课程任务,对系统学习linux内核感兴趣的同学,可以前往《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
基本概念与术语
PCB
PCB是Process Control Block(进程控制块)的缩写。存放进程的管理和控制信息的数据结构称为进程控制块。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
通常情况下,PCB中包含的进程信息有:进程标识符、进程当前状态、进程相应的程序和数据地址、进程优先级、CPU现场保护区、进程同步与通信机制、进程所在队列PCB的链接字以及与进程有关的其他信息等。
中断
中断是由于软件的或硬件的信号,使得CPU放弃当前的任务,转而去执行另一段子程序。负责处理中断的程序称为中断处理程序,它可以处理中断请求,保存当前进程上下文,切换进程上下文以及恢复进程上下文等。
在本次实验中,我们的中断处理逻辑比较简单,只是在程序一个计数器达到一定步长时后就启动中断处理函数,主动中断。
c语言嵌入式汇编
“__asm__” 表示后面的代码为内嵌汇编,“asm”是“__asm__”的别名。
“__volatile__” 表示编译器不要优化代码,后面的指令保留原样,“volatile”是它的别名。
括号里面是汇编指令。
内嵌汇编语法如下:
__asm__(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分
)
内嵌汇编常用限定符
分类 | 限定符 | 描述 |
---|---|---|
通用寄存器 | “a” | 将输入变量放入eax |
“b” | 将输入变量放入ebx | |
“c” | 将输入变量放入ecx | |
“d” | 将输入变量放入edx | |
“s” | 将输入变量放入esi | |
“d” | 将输入变量放入edi | |
“q” | 将输入变量放入eax | |
“r” | 将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个 | |
“A” | 把eax和edx合成一个64 位的寄存器(use long longs) | |
内存 | “m” | 内存变量 |
“o” | 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址 | |
“V” | 操作数为内存变量,但寻址方式不是偏移量类型 | |
“ ” | 操作数为内存变量,但寻址方式为自动增量 | |
“p” | 操作数是一个合法的内存地址(指针) | |
寄存器或内存 | “g” | 将输入变量放入eax,ebx,ecx,edx中的一个,或者作为内存变量 |
“X” | 操作数可以是任何类型 | |
立即数 | “I” | 0-31之间的立即数(用于32位移位指令) |
“J” | 0-63之间的立即数(用于64位移位指令) | |
“N” | 0-255之间的立即数(用于out指令) | |
“i” | 立即数 | |
“n” | 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i” | |
匹配 | “0” | 表示用它限制的操作数与某个指定的操作数匹配 |
“1” … | 也即该操作数就是指定的那个操作数,例如“0” | |
“9” | 去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的0-9 与指令中的“%0”-“%9”的区别,前者描述操作数,后者代表操作数。 | |
& | 该输出操作数不能使用过和输入操作数相同的寄存器 | |
操作数类型 | “=” | 操作数在指令中是只写的(输出操作数) |
“+” | 操作数在指令中是读写类型的(输入输出操作数) | |
浮点数 | “f” | 浮点寄存器 |
“t” | 第一个浮点寄存器 | |
“u” | 第二个浮点寄存器 | |
“G” | 标准的80387浮点常数 | |
% | 该操作数可以和下一个操作数交换位置 例如addl的两个操作数可以交换顺序(当然两个操作数都不能是立即数) | |
# | 部分注释,从该字符到其后的逗号之间所有字母被忽略 | |
* | 表示如果选用寄存器,则其后的字母被忽略 |
本实验中使用c语言嵌入汇编的目的是建立、保存及恢复进程的调用堆栈。
搭建实验环境
为了能够模拟linux内核的运行,我们需要在计算机中安装QEMU。
QEMU 是一个面向完整 PC 系统的开源仿真器。使用它来模拟linux内核的硬件环境。同时我们需要下载linux3.9.4的内核源代码以及它的相关补丁。
搭建环境的流程如下:
sudo apt-get install qemu
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz
’wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch
xz -d linux-3.9.4.tar.xz
tar -xvf linux-3.9.4.tar
cd linux-3.9.4
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
make allnoconfig
make
qemu -kernel arch/x86/boot/bzImage
当执行到最后一步的时候我们就可以在qemu窗口上看到linux内核的运行情况。
此时我们进入到mykernel目录下。我们就可以看到mymain.c以及myinterrupt.c的代码了。前者是内核启动的入口,后者是处理cpu中断的处理程序。当然,默认情况下,里面是并没有实现任何内容的,下面我们就可以在此基础上,稍加修改,实现我们自己的进程调度程序。
时间片轮转多道程序代码
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的基本数据结构
属性名 | 属性描述 |
---|---|
pid | 进程标识符 |
state | 进程状态 |
stack | 进程堆栈 |
thread | 用来保存eip和esp寄存器内容的结构体 |
task_entry | 进程入口地址 |
next | 进程链表中下一个待调度的PCB的指针 |
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函数中,在时钟中断每发生1000次时,把need_schedule变量置为1。
my_schedule是进程主动触发进程调度的函数。这里有两种情况,一种是待调度的进程已经启动,另一种是情况是待调度的进程还没有启动。二者在调度处理上略有差异。这里着重说明一下进程调用堆栈的切换过程。
进程已经启动的情况:
"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)
将当前进程的esp和eip保存到thread中。将下一个进程的esp和eip恢复。从而实现进程上下文的切换。其中$1f的意思是标号1:的位置。
程序未启动的情况:
"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)
跟进程已经启动的情况是相似的,只是多了恢复ebp的过程。
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);
}
}
}
my_start_kernel中初始化了MAX_TASK_NUM个进程(都是从0号task进程复制的),进程的入口地址是函数my_process的入口地址。
然后通过嵌入式汇编初始化了0号进程的调用堆栈。此时eip指向的是my_process的入口地址。执行my_process函数的过程中,每循环执行10000000次,就会主动执行my_schedule函数进行时间片轮转调度。
实验结果
我们可以看到随着时间的推移,进程依次被调度并执行。
结论
程序通过中断机制,可以几乎在程序运行的任意时刻终止,转而去执行其他任务。这是的计算机的运行更加灵活、高效。在中断发生时,中断处理程序必须正确的保存当前进程上下文,并能在将来某个时间点,完全恢复进程上下文,使得原进程可以在中断点处继续执行。
通过本实验内容,使我理解了计算机执行程序的过程,理解了入口函数是如何得以执行的、进程之间是如何进行切换的。