java开发系统内核:进程初体验及代码其实现1

更加具体的代码讲解和演示过程,请参看视频:
Linux kernel Hacker, 从零构建自己的内核

操作系统内核开发,一个及其重要的模块是进程以及进程调度。在大学的操作系统课堂上,研究进程和相关调度算法,是一块耗时耗力的内容。市面上,讲解操作系统进程概念以及调度算法的内容可谓是汗牛充栋,记得我以前读相关内容时,看到很多算法流程图,伪码说明等等,说了一大堆,但我就是无法动手实践,由此感觉那些树都是说大话的假把式,无论描述的如何详细,但只要我无法动手实践,那么也只能是隔靴搔痒,心中困顿,始终无法排解,从本节开始,我们看看,如何通过代码实践的方式,把各种天花乱坠的进程算法落地实现。

进程的创建,主要是为了实现多任务,就算只有一个CPU, 我们也应该可以一边听歌,一边写邮件。既然需要多个任务“同时进行”,那么就需要每个任务在运行时,不能互相干扰,一个任务对数据的读取,绝对不可以影响别的进程的数据。一般而言,对于单CPU硬件来说,多任务其实是一种假象,他们同时运行,其实不过是CPU快速在各个任务间切换的结果而已,当一个任务从前台切换到后台时,需要把当前进程运行所需要的各种信息保存好,当下次进程重新切换回前台时,需要把当时保存好的信息重新加载,这样进程就能顺利的”死灰复燃“了。

基本数据结构的说明

我们先看一个用户切换进程的数据结构,就能大概了解进程的相关特性,以及切换时需要保存什么内容了,(代码文件multi_task.h):

struct TSS32 {
    int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    int es, cs, ss, ds, fs, gs;
    int ldtr, iomap;
};

上面的数据结构,称为一个任务门描述符,是intel X86架构的CPU专门供给的。当发生任务切换时,CPU通过加装上面给定的数据结构,将当前进程的相关信息写入TSS32, 从而实现当前进程的运行环境保护。我们看看里面的相关字段。

eflags 是进程运行时的状态字段,这个字段用于决定当前硬件中断是否打开,是否有运算溢出等信息,在我们内核的汇编代码部分,有一个专门的函数叫io_load_eflags,这个函数就是专门用来加载或存储这个字段的。

当前进程需要保留的还有各个用于运行时的通用寄存器,像eax,ebx等等。需要关注的是cs, ss ,ds, 等段寄存器。这些寄存器指向的是全局描述符表中的相关表项,cs指向的全局描述符,说明的是一段内存的起始地址和大小,这段内存是当前进程代码所在地。ds指向的描述符,说明的内存是当前进程用于存储数据的内存,ss指向的描述符也说明一段内存,这段内存用来当做进程运行时的栈来使用,因此这一系列段寄存器必须小心保存,一旦他们的数值错误,进程的运行就会产生混乱甚至奔溃。

其他的字段我们暂时用不上,先不必花费精力来了解。TSS32数据结构,长度为104字节,但是我们的结构体总共有104字节,这多出的一字节,是为了使用方便而已,没有多余意义。

当我们初始化了TSS32后,在全局描述符表中,需要专门分配一个描述符来指向这块TSS32内存,这种描述符,成为任务门。

在代码文件multi_task.h中,还包含了对全局描述符数据结构的定义:

struct SEGMENT_DESCRIPTOR {
    short limit_low, base_low;
    char base_mid, access_right;
    char limit_high, base_high;
};

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar);

#define AR_TSS32        0x0089

我们在内核的汇编部分,有对全局描述符的数据结构定义,这两个定义完全是等价的,只不过一个用汇编来写,一个用C语言来写,相比较来看,可见C语言比汇编更加容易理解。

set_segmdesc这个函数用来实现对一个描述符的设置,同样,在内核的汇编部分,也存在对描述符进行设置的代码,这个函数其实就是把汇编部分的逻辑用C语言重新实现了一遍。

每一个全局描述符,都有一个字段,用于记录该描述符描述的对象是什么性质,例如用0x9a来说明,描述符指向的内存是一段代码,那么0x89用于说明描述符用于指向一块内存,这块内存就是一个TSS32数据结构。

进程切换代码说明

我们再看看multi_task.c的实现:

#include "multi_task.h"

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
    if (limit > 0xfffff) {
        ar |= 0x8000; /* G_bit = 1 */
        limit /= 0x1000;
    }
    sd->limit_low    = limit & 0xffff;
    sd->base_low     = base & 0xffff;
    sd->base_mid     = (base >> 16) & 0xff;
    sd->access_right = ar & 0xff;
    sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
    sd->base_high    = (base >> 24) & 0xff;
    return;
}

上面这段代码,作用是设置一个全局描述符,它的功能跟我们在内核汇编部分实现的一模一样。

当我们初始化好一个TSS32数据结构,同时构造一个全局描述符指向这个TSS32数据块后,然后通过一条CPU指令,把这个数据库加载到CPU中,这条指令是LTR,我们在内核的汇编部分专门封装了这条指令,以便内核的C语言部分调用,代码如下(kernel.asm):

load_tr:
        LTR  [esp + 4]
        ret

这条指令执行后,当有任务切换时,CPU会把当前进程的相关信息写入到TSS32数据结构中,这个结构就是通过上面指令存入CPU的。同时,我们的内核创建一个新的TSS32数据结构,把要切换的进程的相关信息写入到这个数据结构中,CPU把老进程的信息存储到第一个TSS32中,从第二个TSS32中把新进程的信息加载起来,这样就实现了进程的新老交替。

我们现在内核的汇编部分添加几个描述符用于指向不同的TSS32结构,代码如下(kernel.asm):

LABEL_GDT:
....

LABEL_DESC_6:       Descriptor        0,      0fffffh,       0409Ah

LABEL_DESC_7:       Descriptor        0,      0,       0

LABEL_DESC_8:       Descriptor        0,      0,       0

LABEL_DESC_9:       Descriptor        0,      0,       0

LABEL_DESC_6, LABEL_DESC_7, LABEL_DESC_8,LABEL_DESC_9这几个描述符是为了实现任务切换而新增的,具体使用,我们下面会详细说明,Descriptor是内核的汇编部分对全局描述符的定义,其跟C语言部分的SEGMENT_DESCRIPTOR是完全等价的。

我们看内核的C语言部分,在CMain函数里:

void CMain(void) {
....
static struct TSS32 tss_a, tss_b;
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *)get_addr_gdt();
    tss_a.ldtr = 0;
    tss_a.iomap = 0x40000000;
    tss_b.ldtr = 0;
    tss_b.iomap = 0x40000000;
    set_segmdesc(gdt + 7, 103, (int) &tss_a, AR_TSS32);

    set_segmdesc(gdt + 8, 103, (int) &tss_a, AR_TSS32);

    set_segmdesc(gdt + 9, 103, (int) &tss_b, AR_TSS32);

    set_segmdesc(gdt + 6, 0xffff, task_b_main, 0x409a);

    load_tr(7*8);

    taskswitch8();
....
}

我们先定义了两个TSS32结构,分别是tss_a, tss_b,这两个结构将分别对应两个不同的任务。然后初始化两个字段ldtr 和 iomap.这两个字段的作用我们先不用关心,但它们的值不能乱写。gdt是全局描述符表的头地址,根据首地址片偏移7,对应的就是前面我们说的LABEL_DESC_7,其余的同理。接着,通过seg_segmdesc把tss_a的起始地址写入到描述符中,注意,我们对LABEL_DESC_8也同样写入tss_a, 这是一个小技巧,纯粹是为了进行技术说明,下面我们会看到它的使用。

set_segmdesc(gdt + 9, 103, (int) &tss_b, AR_TSS32);

把tss_b的地址写入到描述符LABEL_DESC_9。然后把描述符LABEL_DESC_7通过ltr指令加载到CPU中,我们知道LABEL_DESC_7对应的是tss_a, 所以通过调用

load_tr(7*8);

CPU就知道tss_a的存在了。需要说明的是,上面代码中的7对应的就是描述符在整个表中的下标,为什么要乘以8呢?乘以8相当于把下标数值左移3位,这是x86架构的规定,当要访问全局描述符表中的某个表项时,必须把下标左移3位,这样就会空出3个比特位,这3个位是有重要用处的,以后我们会涉及到。

接着通过调用taskswitch8(); 这时将进行一次任务切换,也就是进程的调度,这里需要我们注意理解,先看taskswitch8的代码实现,它的实现在内核的汇编部分kernel.asm:

taskswitch8:
        jmp  8*8:0
        ret

    taskswitch7:
        jmp  7*8:0
        ret

    taskswitch6:
        jmp  6*8:0
        ret

    taskswitch9:
        jmp 9*8:0
        ret

我们最开始实现从实模式向保护模式跳转的时候,就使用过
jump 全局描述符下标*8 : 偏移地址
这种格式的代码指令,taskswitch8 的实现,就是让CPU跳转到下标为8的描述符所指向的内存,乘以8的原因,我们在前面解释了。

下标为8的描述符对应的就是LABEL_DESC_8,我们前面曾经用代码:
set_segmdesc(gdt + 8, 103, (int) &tss_a, AR_TSS32);
来设置过,也就是说,这个描述符指向的就是tss_a结构,并且这个描述符的属性是AR_TSS32, 当CPU把该描述符加载后,读取该描述符的属性,发现属性是AR_TSS32,于是CPU知道当前这个描述符是指向一个TSS32结构的,那么加载这样的描述符就意味着要进行一次任务切换,于是它把当前任务的运行环境,也就是,当前的各个寄存器的值,先存储到早先通过ltr加载的tss32结构中,然后再从此次加载的tss32结构中读取相关信息,进而执行新的任务。

这里要注意了,先前加载的TSS32结构是tss_a, 此次加载的TSS32结构还是tss_a, 也就是说,CPU会把当前进程的运行环境相关信息写入到tss_a, 然后再从tss_a中把信息重新装载进CPU, 也就是说,CPU先把当前运行着CMain函数的任务的相关信息写入到tss_a中,然后在把写入信息后的tss_a加载,从写入信息后的tss_a中得到新任务的信息,这样的话,老任务的信息和新任务的信息是完全一样的。

也就是说,我们先把当前运行着CMain的任务切换到后台,然后通过读取tss_a中的数据,再次把切换回后台的任务重新加载执行。这样我们就是实现了一个任务的自我切换。

那么怎么证明一个任务从自己切换到自己呢,我们知道,当我们定义了tss_a结构时,只初始化了两个字段,分别是ldtr 和iomap, 其他字段默认为0,由于发生了任务切换,CPU会把相关寄存器信息写入到tss_a的对应字段,这样,我们只要把其他字段打印出来,如果他们的值不再是0的话,那就意味着曾经有任务切换过,并且CPU把被切换的任务的相关信息写入到了tss_a数据结构中,于是我们通过代码打印出tss_a的相关字段:

char *p = intToHexStr(tss_a.eflags);
    showString(shtctl, sht_back, 0, 0, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.esp);
    showString(shtctl, sht_back, 0, 16, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.es / 8);
    showString(shtctl, sht_back, 0, 32, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.cs / 8);
    showString(shtctl, sht_back, 0, 48, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.ss / 8);
    showString(shtctl, sht_back, 0, 64, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.ds / 8);
    showString(shtctl, sht_back, 0, 80, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.gs / 8);
    showString(shtctl, sht_back, 0, 96, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.fs / 8);
    showString(shtctl, sht_back, 0, 112, COL8_FFFFFF, p);

    p = intToHexStr(tss_a.cr3);
    showString(shtctl, sht_back, 0, 128, COL8_FFFFFF, p);

上面代码执行后,在桌面上打印出的信息如下:
这里写图片描述

大家看做上角的一排数字,对应的就是tass_a相关字段的内容,tss_a初始化时,这些字段都是默认为0的,但打印出来的时候,有一些不是0,我们又没有在代码里主动进行设置,这么说来,这些字段的设置,只能是CPU亲手写入的,也就是说,我们实现了一次当前任务到其自身的切换!

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
主要特性Java 语言是简单的:Java 语言的语法与 C 语言和 C++ 语言很接近,使得大多数程序员很容易学习和使用。另一方面,Java 丢弃了 C++ 中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。特别地,Java 语言不使用指针,而是引用。并提供了自动分配和回收内存空间,使得程序员不必为内存管理而担忧。Java 语言是面向对象的:Java 语言提供类、接口和继承等面向对象的特性,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实机制(关键字为 implements)。Java 语言全面支持动态绑定,而 C++语言只对虚函数使用动态绑定。总之,Java语言是一个纯的面向对象程序设计语言。Java语言是分布式的:Java 语言支持 Internet 应用的开发,在基本的 Java 应用编程接口中有一个网络应用编程接口(java net),它提供了用于网络应用编程的类库,包括 URL、URLConnection、Socket、ServerSocket 等。Java 的 RMI(远程方法激活)机制也是开发分布式应用的重要手段。Java 语言是健壮的:Java 的强类型机制、异常处理、垃圾的自动收集等是 Java 程序健壮性的重要保证。对指针的丢弃是 Java 的明智选择。Java 的安全检查机制使得 Java 更具健壮性。Java语言是安全的:Java通常被用在网络环境中,为此,Java 提供了一个安全机制以防恶意代码的攻击。除了Java 语言具有的许多安全特性以外,Java 对通过网络下载的类具有一个安全防范机制(类 ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代码检查,并提供安全管理机制(类 SecurityManager)让 Java 应用设置安全哨兵。Java 语言是体系结构中立的:Java 程序(后缀为 java 的文件)在 Java 平台上被编译为体系结构中立的字节码格式(后缀为 class 的文件),然后可以在实这个 Java 平台的任何系统中运行。这种途径适合于异构的网络环境和软件的分发。Java 语言是可移植的:这种可移植性来源于体系结构中立性,另外,Java 还严格规定了各个基本数据类型的长度。Java 系统本身也具有很强的可移植性,Java 编译器是用 Java的,Java 的运行环境是用 ANSI C 实的。Java 语言是解释型的:如前所述,Java 程序在 Java 平台上被编译为字节码格式,然后可以在实这个 Java 平台的任何系统中运行。在运行时,Java 平台中的 Java 解释器对这些字节码进行解释执行,执行过程中需要的类在联接阶段被载入到运行环境中。Java 是高性能的:与那些解释型的高级脚本语言相比,Java 的确是高性能的。事实上,Java 的运行速度随着 JIT(Just-In-Time)编译器技术的发展越来越接近于 C++。Java 语言是多线程的:在 Java 语言中,线程是一种特殊的对象,它必须由 Thread 类或其子(孙)类来创建。通常有两种方法来创建线程:其一,使用型构为 Thread(Runnable) 的构造子类将一个实了 Runnable 接口的对象包装成一个线程,其二,从 Thread 类派生出子类并重写 run 方法,使用该子类创建的对象即为线程。值得注意的是 Thread 类已经实了 Runnable 接口,因此,任何一个线程均有它的 run 方法,而 run 方法中包含了线程所要运行的代码。线程的活动由一组方法来控制。Java 语言支持多个线程的同时执行,并提供多线程之间的同步机制(关键字为 synchronized)。Java 语言是动态的:Java 语言的设计目标之一是适应于动态变化的环境。Java 程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类。这也有利于软件的升级。另外,Java 中的类有一个运行时刻的表示,能进行运行时刻的类型检查。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值