8.1实现两个小任务的切换(并发)
被一个c语言的函数小知识卡了3个小时
一个.c文件里面的函数调用另一个.c文件里面的函数
//3.c
#include<stdio.h>
#include "3.h"
void liqin(void){
printf("我的年龄是 %d", 24);
}
//1.c
#include <stdio.h>
#include "3.c"
void lq(void) {
printf("1123");
}
int main()
{
liqin();
ly();
system("pause");
return 0;
}
显示liqin函数重复定义了
include进来的文件什么时候和原始文件链接上的呢?
首先,预编译/预处理(preprocessing):
预编译其实简单理解就是替换那些预处理命令,对于C语言来说就是那些以"#"井号开头的一些指令,比如#include, #ifndef, #define等,它会把include的文件填充进来,确定把#ifdef之类地方是否需要编译,以及替换宏定义等
接着就是对这个预处理后的文件进行编译,但不包括汇编
我们经过编译,得到一个从C源程序到汇编源程序的文件
在经过编译将C程序翻译为汇编后,距离机器码只有一步之遥。也就是将汇编指令翻译成机器码。我们可以使用gcc命令进行汇编:
实际上汇编器生成一个目标文件时,除了翻译汇编指令为机器指令以外,它会生成很多的sections, headers以及tables之类的数据,关于它生成的这些数据的作用请参考不同的目标文件格式(如ELF)。我们需要知道汇编器生成的未经过链接的目标文件时,它实际上并不知道数据和代码要在内存的什么位置运行,它更不知道那些本地引用的外部定义的全局变量和函数的位置。所以汇编器只管挖好这些坑,让链接器后面来填。
我们经过编译,得到一个从C源程序到汇编源程序的文件
include "3.h"
不是只包含头文件,而是你在链接时链接了你所include的函数所在的目标文件。你虽然没在程序里写,但是链接的时候需要指定,即使你自己没手动指定,也不代表你的集成编译环境没有为你做。
正确写法
挖个坑:为什么include "3.c"
不行?自己也一知半解
#include <stdio.h>
#include "3.h"
void lq(void) {
printf("1123");
}
int main()
{
liqin();
lq();
system("pause");
return 0;
}
遇到一个大牛,醉卧沙场
C 语言为什么只需要 include<stdio.h> 就能使用里面声明的函数?
c语言中,如果main函数的末尾没有return语句将会有什么影响?
添加任务状态段(TSS)
Intel手册里里面TSS的结构
我们需要创建一个结构体来表示这个TSS的数据,这个TSS类似于PCB(进程控制块)
//D:\code\x86\code\start\start\source\kernel\include\cpu\cpu.h
/**
* tss描述符
*/
typedef struct _tss_t {
uint32_t pre_link;// 没有用到
uint32_t esp0, ss0, esp1, ss1, esp2, ss2; //和栈有关,保护模式下不同特权级的栈,ss0,esp0,代码段和栈的指针
uint32_t cr3; //当前程序所使用的页表,与虚存有关,后面课时会用
uint32_t eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; //cpu内核寄存器相关的信息
uint32_t es, cs, ss, ds, fs, gs; //段寄存器
uint32_t ldt; // 没有用到
uint32_t iomap;// 没有用到
}tss_t; //类似于PCB,进程控制块
执行空间放在内存中,每个进程在内存里面的空间是私有的,TSS里面包含了一个个寄存器
切换进程时候需要保存运行环境
函数 void init_task_entry(void)
//init.c
void init_task_entry(void) {
int count = 0;
for (;;) {
log_printf("init task: %d", count++);
task_switch_from_to(&init_task, &first_task);
}
}
void init_main(void) {
// int a = 3 / 0;
log_printf("Kernel is running....");
log_printf("Version: %s", OS_VERSION);
log_printf("%d %d %x %c", -123, 123456, 0x12345, 'a');
// 初始化任务
task_init(&init_task, (uint32_t)init_task_entry, (uint32_t)&init_task_stack[1024]); //因为栈是从大到小增长的
task_init(&first_task, 0, 0); //init_main自己有栈
write_tr(first_task.tss_sel); //对TR寄存器初始化
int count = 0;
for (;;) {
log_printf("first task: %d", count++);
task_switch_from_to(&first_task, &init_task);
}
// irq_enable_global();
// for (;;) {}
}
我们希望这个函数和main函数可以并发执行
初始化各个任务的TSS
我们先要到GDT里面添加上TSS Desc(描述符)
和GDT很Seg Desc格式很类似
所以套用void segment_desc_set(int selector, uint32_t base, uint32_t limit, uint16_t attr)
函数
//task.h
#ifndef TASK_H
#define TASK_H
#include "comm/types.h"
#include "cpu/cpu.h"
/**
* @brief 任务控制块结构
*/
typedef struct _task_t {
tss_t tss; // 任务的TSS段
uint16_t tss_sel; // tss选择子
}task_t;
int task_init (task_t *task, uint32_t entry, uint32_t esp);
void task_switch_from_to (task_t * from, task_t * to);
#endif
//cpu.h
#define SEG_TYPE_TSS (9 << 0) // 和段描述符不一样的地方,表示32位TSS
#define EFLAGS_IF (1 << 9)
#define EFLAGS_DEFAULT (1 << 1)
//task.c
static int tss_init (task_t * task, uint32_t entry, uint32_t esp) { //PCB入口地址,程序入口地址和栈顶指针
// tss段初始化
int tss_sel = gdt_alloc_desc();
if (tss_sel < 0) { //分配失败的话
log_printf("alloc tss failed.\n");
return -1;
}
segment_desc_set(tss_sel, (uint32_t)&task->tss, sizeof(tss_t),
SEG_P_PRESENT | SEG_DPL0 | SEG_TYPE_TSS);
kernel_memset(&task->tss, 0, sizeof(tss_t)); //先整个清0,后面再单独设置
task->tss.eip = entry; //当前执行指令的位置
task->tss.esp = task->tss.esp0 = esp; //程序需要用到的栈顶指针,esp0不懂
task->tss.ss0 = KERNEL_SELECTOR_DS; //段寄存器,我们用的是平坦模型,全局一个代码段一个数据段
task->tss.eflags = EFLAGS_DEFAULT | EFLAGS_IF; //恢复程序运行环境以后需要我们手动打开中断,TSS保存寄存器中的值时候,硬件自动关中断,如下图(王道)
task->tss.es = task->tss.ss = task->tss.ds
= task->tss.fs = task->tss.gs = KERNEL_SELECTOR_DS; // 暂时写死,我们用的是平坦模型
task->tss.cs = KERNEL_SELECTOR_CS; // 暂时写死
task->tss.iomap = 0; //
task->tss_sel = tss_sel;
return 0;
}
/**
* @brief 初始化任务
*/
int task_init (task_t *task, uint32_t entry, uint32_t esp) {
ASSERT(task != (task_t *)0);
tss_init(task, entry, esp);
return 0;
}
//init.c
static task_t first_task; // 第一个任务
static uint32_t init_task_stack[1024]; // 给init_task_entry函数设置的空闲任务堆栈
static task_t init_task;
void init_task_entry(void) {
int count = 0;
for (;;) {
log_printf("init task: %d", count++);
task_switch_from_to(&init_task, &first_task);
}
}
void init_main(void) {
// int a = 3 / 0;
log_printf("Kernel is running....");
log_printf("Version: %s", OS_VERSION);
log_printf("%d %d %x %c", -123, 123456, 0x12345, 'a');
// 初始化任务
task_init(&init_task, (uint32_t)init_task_entry, (uint32_t)&init_task_stack[1024]); //因为栈是从大到小增长的,所以从1024开始
task_init(&first_task, 0, 0); //init_main自己有栈
write_tr(first_task.tss_sel); //对TR寄存器初始化
int count = 0;
for (;;) {
log_printf("first task: %d", count++);
task_switch_from_to(&first_task, &init_task);
}
// irq_enable_global();
// for (;;) {}
}
int_main自己自带一个栈,所以我们要手动给init_task_entry(void)一个栈,定义一个数组,每个单元大小为4字节
如图,我们还要初始化一下TR寄存器,每次TR寄存器存的都是正在运行的进程的TSS信息
为什么栈那么特殊需要自己手动创建,堆为什么不需要手动创建?
我的理解是堆是malloc创建的,不算所有进程都有,自己可以指定地址malloc,但是栈肯定是要有的
write_tr(first_task.tss_sel);
//D:\code\x86\code\start\start\source\comm\cpu_instr.h
static inline void write_tr (uint32_t tss_selector) {
__asm__ __volatile__("ltr %%ax"::"a"(tss_selector));
} //写TR寄存器
然后需要一个任务切换函数来实现任务切换
用汇编JMP来实现
//D:\code\x86\code\start\start\source\kernel\core\task.c
void task_switch_from_to (task_t * from, task_t * to) {
switch_to_tss(to->tss_sel);
}
//D:\code\x86\code\start\start\source\kernel\cpu\cpu.c
void switch_to_tss (uint32_t tss_selector) {
far_jump(tss_selector, 0); //偏移量为0,这个函数在内联汇编文件里面有实现
}
所以在init.c
里面
void init_task_entry(void) {
int count = 0;
for (;;) {
log_printf("init task: %d", count++);
task_switch_from_to(&init_task, &first_task);
}
}
```
```
```
void init_main(void) {
```
```
```
int count = 0;
for (;;) {
log_printf("first task: %d", count++);
task_switch_from_to(&first_task, &init_task);
}
}
测试
成功实现多进程