linux内核分析之-进程管理

本文详细介绍了如何在Linux内核之上运行一个简单的时间片轮转多道程序,模拟基本的进程调度行为。通过实验环境搭建、进程控制块(PCB)概念、中断处理、内嵌汇编技术,实现了进程的创建、调度和切换。重点展示了如何通过内嵌汇编语法建立、保存及恢复进程调用堆栈,以及如何在实验中实现时间片轮转多道程序代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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的内核源代码以及它的相关补丁。

搭建环境的流程如下:

  1. sudo apt-get install qemu
  2. sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
  3. wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz
  4. wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch
  5. xz -d linux-3.9.4.tar.xz
  6. tar -xvf linux-3.9.4.tar
  7. cd linux-3.9.4
  8. patch -p1 < ../mykernel_for_linux3.9.4sc.patch
  9. make allnoconfig
  10. make
  11. 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函数进行时间片轮转调度。


实验结果

这里写图片描述

我们可以看到随着时间的推移,进程依次被调度并执行。

结论

程序通过中断机制,可以几乎在程序运行的任意时刻终止,转而去执行其他任务。这是的计算机的运行更加灵活、高效。在中断发生时,中断处理程序必须正确的保存当前进程上下文,并能在将来某个时间点,完全恢复进程上下文,使得原进程可以在中断点处继续执行。

通过本实验内容,使我理解了计算机执行程序的过程,理解了入口函数是如何得以执行的、进程之间是如何进行切换的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值