当前位置: 首页 > news >正文

linux0.11内核源码修仙传第十章——进程调度始化

🚀 前言

    本文是非常非常非常重要的一节,是后续进程调度的基础,对应于书中的第18回。希望各位给个三连,拜托啦,这对我真的很重要!!!

目录

  • 🚀 前言
  • 🏆 任务状态段(TSS)与局部描述符(LDT)
    • 📃 什么是TSS与LDT
    • 📃 初始化TSS0与LDT0
  • 🏆 为进程调度做铺垫
    • 📃 初始化剩下的TSS与LDT
    • 📃 告诉内存TSS与LDT的位置
  • 🏆 设置时钟中断与系统调用
    • 📃 设置时钟中断
    • 📃 设置系统调用
  • 🎯总结
  • 📖参考资料

🏆 任务状态段(TSS)与局部描述符(LDT)

📃 什么是TSS与LDT

    TSS是在操作系统进程管理过程中,任务,也就是进程或线程,在切换时保存的上下文信息。其实里面存放的所谓上下文信息就是各个寄存器的情况,这样进程切换的时候,才能做到保存和恢复上下文,继续执行。具体可以看下面的结构体:

struct tss_struct {
	long	back_link;	/* 16 high bits zero */
	long	esp0;
	long	ss0;		/* 16 high bits zero */
	long	esp1;
	long	ss1;		/* 16 high bits zero */
	long	esp2;
	long	ss2;		/* 16 high bits zero */
	long	cr3;
	long	eip;
	long	eflags;
	long	eax,ecx,edx,ebx;
	long	esp;
	long	ebp;
	long	esi;
	long	edi;
	long	es;		/* 16 high bits zero */
	long	cs;		/* 16 high bits zero */
	long	ss;		/* 16 high bits zero */
	long	ds;		/* 16 high bits zero */
	long	fs;		/* 16 high bits zero */
	long	gs;		/* 16 high bits zero */
	long	ldt;		/* 16 high bits zero */
	long	trace_bitmap;	/* bits: trace 0, bitmap 16-31 */
	struct i387_struct i387;
};

    对照下面的图食用效果更好:
在这里插入图片描述

    LDT 叫局部描述符表,对应的就是全局描述符(GDT),为什么做出这样的区分呢?GDT的作用是为整个系统定义了全局的段描述符。它包含了系统中所有进程和线程所使用的公共段描述符;LDT 是进程本地的,为每个进程定义了私有的段描述符。讲人话就是, 通过LDT,每个进程可以定义自己的代码段、数据段、堆栈段等。这样,不同进程之间的内存可以隔离开来,一个进程无法直接访问其他进程的内存

📃 初始化TSS0与LDT0

    在看代码之前,先来回顾一下内存与GDT,不记得GDT与段描述符的话可以查看这篇博客:linux0.11内核源码修仙传第二章——setup.s。这里贴出目前的内存分布:

在这里插入图片描述

    来看一下进程调度算法中如何初始化TSS与LDT的,FIRST_TSS_ENTRYFIRST_LDT_ENTRY 表明其在GDT中的位置:

#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)

void sched_init(void)
{
	···
	set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
	···
}

    这两个函数的作用是初始化TSS与LDT并添加进GDT后面两个,具体函数的定义如下:

#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
	"movw %%ax,%2\n\t" \
	"rorl $16,%%eax\n\t" \
	"movb %%al,%3\n\t" \
	"movb $" type ",%4\n\t" \
	"movb $0x00,%5\n\t" \
	"movb %%ah,%6\n\t" \
	"rorl $16,%%eax" \
	::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
	 "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
	)

#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")

    这两个函数共同调用_set_tssldt_desc,通过参数type区分TSS和LDT。其实这两个也是段描述符,汇编所做的事情就是设置TSS与LDT对应的短描述符各个位。下面是段描述符,详情依旧可以参看linux0.11内核源码修仙传第二章——setup.s。

在这里插入图片描述

    有了这个,就可以看一看汇编具体对应的设置了:

// 操作数约束:输入和内存操作数
::"a" (addr),               // 输入:addr存入eax寄存器
  "m" (*(n)),               // 内存操作数:n对应%1(Limit低16位)
  "m" (*(n+2)),             // 内存操作数:n+2对应%2(Base低16位)
  "m" (*(n+4)),             // 内存操作数:n+4对应%3(Base中间8位)
  "m" (*(n+5)),             // 内存操作数:n+5对应%4(Type)
  "m" (*(n+6)),             // 内存操作数:n+6对应%5(Limit高4位 + Flags)
  "m" (*(n+7))              // 内存操作数:n+7对应%6(Base高8位)
指令含义
movw $104, %1设置段限长(0~16位)
movw %%ax, %2Base低16位
rorl $16, %%eax     movb %%al, %3Base中间8位
movb $" type ", %4设置Type至P的8位,type参数指定了描述符类型(0x89对应TSS)
movb $0x00, %5设置Limit往前共8位
movb %%ah, %6设置Base高8位

    设置完成之后,内存如下所示(主要看右侧的GDT分布变化,多了TSS与LDT):

在这里插入图片描述

🏆 为进程调度做铺垫

📃 初始化剩下的TSS与LDT

    接着往下看sched_init函数:

typedef struct desc_struct {
	unsigned long a,b;
} desc_table[256];

struct task_struct * task[64] = {&(init_task.task), };

void sched_init(void)
{
	···
	struct desc_struct * p;
	p = gdt+2+FIRST_TSS_ENTRY;	// 在TSS基础上再往后挪两个单位,即GDT下一个空白处
	for(i=1;i<64;i++) {
		task[i] = NULL;
		p->a=p->b=0;
		p++;
		p->a=p->b=0;
		p++;
	}
	···
}

    这里做的事情还是比较好懂的,其实就是初始化了一个64长度的task_struct数组并赋上初值,同时给GDT剩下的位置填充上0。先来看看task_struct数组,初始化后数组如下:

在这里插入图片描述
    这个数组里面每个都是task_struct 结构体,这个结构体很重要,里面存放了每一个进程的信息:

struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	struct sigaction sigaction[32];
	long blocked;	/* bitmap of masked signals */
/* various fields */
	int exit_code;
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,father,pgrp,session,leader;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	long alarm;
	long utime,stime,cutime,cstime,start_time;
	unsigned short used_math;
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};

    这个结构体可以先看着,后面等进程调度实际应用时可以再细看。

    接下来上上强度,下面图是未来整个内存的规划,目前还没有涉及到进程,下面的图可以先不用理解的很细。每个进程都有自己的LDT,每个LDT都有对应的数据段,代码段等,和GDT最开始的一样。

在这里插入图片描述
    这里还有一个很重要的东西,就是最开始循环之前两行代码设置的TSS0和LDT0。为什么说很重要呢,那是因为虽然目前还没有创建进程,但是我们目前正在运行的代码就是未来的一个进程的指令流 ,也就是在后面进程调度建立起来后,当前代码就会变为进程0。因此需要TSS0和LDT0将这些信息提前存好。等到后面进程调度建立后会更加直观。

📃 告诉内存TSS与LDT的位置

    接下来看后面两行:

#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))

void sched_init(void)
{
	···
	ltr(0);
	lldt(0);
	···
}

    之前我们就有介绍,lidt指令是给idtr寄存器赋值,告诉CPU中断描述符IDT在内存的位置。详情可以见linux0.11内核源码修仙传第二章——setup.s。这里的ltrlldt与之前的类似,分别给tr寄存器和ldt寄存器赋值,告诉CPU,LDT与TSS在内存中的位置。如下所示:

在这里插入图片描述

    这样,CPU 之后就能通过 tr 寄存器找到当前进程的任务状态段信息,也就是上下文信息,以及通过 ldt 寄存器找到当前进程在用的局部描述符表信息。至于这么多LDT与TSS,放哪个,那自然是最开始的LDT0和TSS0,之后的只需要依据指针加减即可获得。

🏆 设置时钟中断与系统调用

📃 设置时钟中断

    在接下来的代码中,还需要设置时钟中断。所谓时钟中断就是一个定时器会持续以一定频率向CPU发送中断信号:

void sched_init(void)
{
	···
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */
	set_intr_gate(0x20,&timer_interrupt);
	···
}

    好了,上面解释了时钟中断的来源,既然是外界的信息,那自然又涉及到了CPU交互。这个中断信号是一个可编程定时芯片,CPU要与其进行交互,就需要先写入指定端口对应的指令,具体设置方式需要参考芯片手册。

    之后就需要开启一个中断处理函数,给的中断号是0x20,中断处理函数为timer_interrupt,每次定时器向CPU发送中断后便会执行这个函数。 这个定时器的触发,以及时钟中断函数的设置,是操作系统主导进程调度的一个关键! 没有他们这样的外部信号不断触发中断,操作系统就没有办法作为进程管理的主人,通过强制的手段收回进程的 CPU 执行权限。

📃 设置系统调用

    第二个设置的中断叫系统调用 system_call,中断号是 0x80,这个中断又是个非常非常非常非常非常非常非常重要的中断,所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行。比如 Java 程序员写一个 read,底层会执行汇编指令 int 0x80,这就会触发系统调用这个中断,最终调用到 Linux 里的 sys_read 方法。

void sched_init(void)
{
	···
	set_system_gate(0x80,&system_call);
}

    截止目前为止,设置的中断如下:

中断号中断处理函数
0 ~ 0x10trap_init 里设置的一堆
0x20timer_interrupt
0x21keyboard_interrupt
0x80system_call

    0-0x10 这 17 个中断是 trap_init 里初始化设置的,是一些基本的中断,比如除零异常等。设置见linux0.11内核源码修仙传第六章——中断初始化。 0x21是键盘中断,设置见linux0.11内核源码修仙传第八章——控制台初始化。0x20是定时器中断,0x80是系统调用中断。

🎯总结

    本文就干了三件事:第一,我们往全局描述符表写了两个结构,TSS 和 LDT,作为未来进程 0 的任务状态段和局部描述符表信息。第二,我们初始化了一个结构为 task_struct 的数组,未来这里会存放所有进程的信息,并且我们给数组的第一个位置附上了 init_task.init 这个具体值,也是作为未来进程 0 的信息。第三,设置了时钟中断 0x20 和系统调用 0x80,一个作为进程调度的起点,一个作为用户程序调用操作系统功能的桥梁。

    其实整个操作系统就是靠中断驱动,各个模块不断初始化各种中断处理函数,并且开启指定的外设开关,让操作系统自己慢慢“活”了起来,逐渐通过中断忙碌于各种事情中。


📖参考资料

[1] linux源码趣读
[2] 一个64位操作系统的设计与实现
[3] 任务状态段(Task State Segment)
[4] Linux 内存管理(二)之GDT与LDT

相关文章:

  • Retinexformer:基于 Retinex 的单阶段 Transformer 低光照图像增强方法
  • 【消息队列】几个mq组件的对比: redis stream/rabbitmq/rocketmq/kafka
  • 【Python网络编程基础】
  • 六、小白学JAVA-类和对象
  • 【漏洞修复】Android 10 系统源码中的 glibc、curl、openssl、cups、zlib 更新到最新版本
  • ubuntu 22.04 一键安装 lxd
  • 【git拉取冲突解决】Please move or remove them before you merge. Aborting
  • RTMP推流+EasyDSS云服务+边缘AI分析的无人机监控系统设计
  • 【C++游戏引擎开发】《线性代数》(1):环境配置与基础矩阵类设计
  • PHP安装HTML转图片的扩展GD库的使用
  • 江西核威环保科技:打造世界前沿的固液分离设备高新企业
  • 【C++】httplib:轻量级的 HTTP 服务器和客户端
  • 神奇的FlexBox弹性布局
  • R语言——循环
  • vue 自定义 tabs 控件,可自动左右滑动使得选中项居中显示
  • VulnHub-FALL通关攻略
  • CSS3学习教程,从入门到精通,CSS3 弹性盒子(Flexbox)布局全面指南(20)
  • linux ACL权限控制之用户权限控制程序设计
  • HO与OH差异之Navigation三
  • 【leetcode刷题日记】lc.53-最大子数组和
  • 上海金桥建设监理有限公司网站/新型网络营销方式
  • 网站建设维护杭州/百度网盘搜索引擎入口在哪里
  • 哪个网站做首饰批发好/seo综合查询 站长工具
  • 佛山制作网站软件/ 今日头条
  • 梦织做网站/外贸软件排行榜
  • 苏州中车建设工程有限公司网站/营销咨询公司排名