本章节所有代码托管在miniOS_32
章节任务介绍
任务简介
上一节,我们初步完成了内核的内存管理部分的内容
本节我们将正式开始操作系统进程管理的相关内容
本节的主要任务有:
创建并初始化PCB
模拟pthread_create函数创建线程并执行线程函数
任务目标
#include<pthread.h>
#include<stdio.h>void* thread_work(void* args){char* str=(char*)args;printf("args is %s\n",str);return NULL;
}int main(){pthread_t tid;pthread_create(&tid,NULL,thread_work,"pthread_create\n");pthread_join(tid,NULL);return 0;
}
本节我们将实现一个类似于pthread_create
的函数,用于创建一个线程并执行传入的执行函数,最终实现的调用代码如下所示
/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
void thread_work(void *arg);int main(void)
{put_str("I am kernel\n");init_all();thread_start("thread_work", 31, thread_work, "pthread_create\n");while (1);return 0;
}/* 线程执行函数 */
void thread_work(void *arg)
{char *para = (char *)arg;int i = 10;while (i--)put_str(para);
}
PCB简介
如同上一节中的位图,位图是管理内存的数据结构,对于线程或者进程,也需要有一个数据结构对其进行管理,这个数据结构就是PCB。
PCB(Process Control Block,进程控制块)是操作系统内部用于存储进程信息的数据结构。
操作系统通过PCB来管理和调度进程。
PCB 的生命周期:
进程创建时:每当操作系统创建一个新的进程时,系统会为该进程分配一个PCB,初始化进程的各种信息;
进程执行时:进程在运行时,操作系统通过 PCB 来管理和调度进程。每当进程状态发生变化(如从就绪变为运行,或从运行变为阻塞),操作系统会更新 PCB;
进程终止时:当进程执行完毕或被终止时,操作系统会回收该进程的 PCB,并释放相关资源。
PCB的内容:
PCB中包含了进程执行所需的各种信息,如进程状态、寄存器值、内存使用情况、I/O 状态等。
PCB 的主要功能:
进程管理:每个进程都有一个唯一的 PCB,操作系统通过它来追踪进程的状态、资源等信息。
上下文切换:当操作系统切换执行进程时,它会保存当前进程的 PCB,并加载下一个进程的 PCB,从而实现进程的上下文切换。
进程调度:操作系统通过PCB来选择下一个运行的进程。调度器根据进程的状态、优先级等信息做出决策。
以下是PCB的示意结构图
在内核空间中创建并运行线程
代码目录结构
.
├── bin
│ ├── bitmap.o
│ ├── debug.o
│ ├── init.o
│ ├── interrupt.o
│ ├── kernel.bin
│ ├── kernel.o
│ ├── loader
│ ├── main.o
│ ├── mbr
│ ├── memory.o
│ ├── print.o
│ ├── string.o
│ └── thread.o
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.S
│ └── mbr.S
├── kernel
│ ├── debug.c
│ ├── debug.h
│ ├── global.h
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c
│ ├── interrupt.h
│ ├── kernel.S
│ ├── main.c
│ ├── memory.c
│ └── memory.h
├── lib
│ ├── kernel
│ │ ├── bitmap.c
│ │ ├── bitmap.h
│ │ ├── io.h
│ │ ├── print.h
│ │ └── print.S
│ ├── stdint.h
│ ├── string.c
│ └── string.h
├── Makefile
├── start.sh
└── thread├── thread.c└── thread.h
数据结构定义
/thread/thread.h
定义进程或者线程的任务状态
/*定义进程或者线程的任务状态*/
enum task_status
{TASK_RUNNGING,TASK_READY,TASK_BLOCKED,TASK_WAITING,TASK_HANGING,TASK_DIED,
};
定义线程栈,存储线程执行时的运行信息
/*定义线程栈,存储线程执行时的运行信息*/
struct thread_stack
{uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;// 一个函数指针,指向线程执行函数,目的是为了实现通用的线程函数调用void (*eip)(thread_func *func, void *func_args);// 以下三条是模仿call进入thread_start执行的栈内布局构建的,call进入就会压入参数与返回地址,因为我们是ret进入kernel_thread执行的// 要想让kernel_thread正常执行,就必须人为给它造返回地址,参数void(*unused_retaddr); // 一个栈结构占位thread_func *function;void *func_args;
};
定义PCB,PCB的信息庞大复杂,我们将来一点点对其进行填充,本节只需要以下信息即可
/*PCB结构体*/
struct task_struct
{// 线程栈的栈顶指针uint32_t *self_kstack;// 线程状态enum task_status status;// 线程的优先级uint8_t priority;// 线程函数名char name[16];// 用于PCB结构体的边界标记uint32_t stack_magic;
};
代码讲解
代码逻辑
向内存申请一页空间,分配给要创建的线程
初始化该线程的PCB
通过PCB中的栈顶指针进一步初始化线程栈的运行信息
正式运行线程执行函数
如下所示,thread_start
就是我们最终要实现的用以模拟pthread_create
的函数
其包含了我们上述说的代码逻辑
/*根据线程栈的运行信息开始运行线程函数*/
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_args)
{/*1.分配一页空间给线程作为线程执行的栈空间*/struct task_struct *thread = get_kernel_pages(1);/*2.初始化PCB,PCB里存放了线程的基本信息以及线程栈的栈顶指针*/init_thread(thread, name, prio);/*3.根据线程栈的栈顶指针,初始化线程栈,也就是初始化线程的运行信息比如线程要执行的函数,以及函数参数*/thread_create(thread, function, func_args);/*4.上述准备好线程运行时的栈信息后,即可运行执行函数了*/asm volatile("movl %0,%%esp; \pop %%ebp; \pop %%ebx; \pop %%edi; \pop %%esi; \ret":: "g"(thread->self_kstack): "memory");return thread;
}
关于最后执行运行函数的内联汇编代码,主要与线程栈的栈空间布局有关,我们在最后初始化栈空间的运行信息之后进行详细说明
初始化PCB
/*PCB结构体*/
struct task_struct
{// 线程栈的栈顶指针uint32_t *self_kstack;// 线程状态enum task_status status;// 线程的优先级uint8_t priority;// 线程函数名char name[16];// 用于PCB结构体的边界标记uint32_t stack_magic;
};
PCB的初始化也就是对上述结构体进行初始化
/*初始化PCB*/
void init_thread(struct task_struct *pthread, char *name, int prio)
{memset(pthread, 0, sizeof(*pthread));strcpy(pthread->name, name);pthread->status = TASK_RUNNGING;pthread->priority = prio;/*一个线程的栈空间分配一页空间,将PCB放置在栈底pthread是申请的一页空间的起始地址,因此加上一页的大小,就是栈顶指针*/pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);/*PCB的边界标记,防止栈顶指针覆盖掉PCB的内容*/pthread->stack_magic = 0x19991030;
}
以下是创建的线程栈内存示意图
初始化线程栈运行信息
/*根据PCB信息,初始化线程栈的运行信息*/
void thread_create(struct task_struct *pthread, thread_func function, void *func_args)
{/*给线程栈空间的顶部预留出中断栈信息的空间*/pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));/*给线程栈空间的顶部预留出线程栈信息的空间*/pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));// 初始化线程栈,保存线程运行时需要的信息struct thread_stack *kthread_stack = (struct thread_stack *)pthread->self_kstack;// 线程执行函数kthread_stack->eip = kernel_thread;kthread_stack->function = function;kthread_stack->func_args = func_args;kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0;
}
其中线程执行函数如下所示
static void kernel_thread(thread_func *function, void *func_args)
{function(func_args);
}
以下是初始化线程栈后的内存示意图
创建并运行线程
/*4.上述准备好线程运行时的栈信息后,即可运行执行函数了*/asm volatile("movl %0,%%esp; \pop %%ebp; \pop %%ebx; \pop %%edi; \pop %%esi; \ret":: "g"(thread->self_kstack): "memory");
如下所示,当线程栈初始化结束之后,栈顶指针首先弹出了寄存器映像
pop %%ebp; \pop %%ebx; \pop %%edi; \pop %%esi; \
这样栈顶指针就指向了通用执行函数kernel_thread
,这样接下来只需要调用kernel_thread
,就调用了用户的执行函数
于是接下来代码执行ret
指令,ret
指令会做两件事
将当前栈顶指针的值弹出,然后赋值给指令寄存器EIP,这样就相当于调用了
kernel_thread
由于弹出了栈顶指针的值,因此栈顶指针会回退
最后的结果如下所示
于是接下来,根据c语言的函数调用约定,kernel_thread
会取出占位的返回地址上边的两个参数,也就是执行函数的地址与执行函数的参数,然后调用执行函数运行
完整代码
/thread/thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H#include "stdint.h"
/*定义执行函数*/
typedef void thread_func(void *);/*定义进程或者线程的任务状态*/
enum task_status
{TASK_RUNNGING,TASK_READY,TASK_BLOCKED,TASK_WAITING,TASK_HANGING,TASK_DIED,
};/*中断发生时调用中断处理程序的压栈情况*/
struct intr_stack
{uint32_t vec_no;// pushad的压栈情况uint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy;uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;// 中断调用时处理器自动压栈的情况uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;uint32_t err_code;void (*eip)(void);uint32_t cs;uint32_t eflags;void *esp;uint32_t ss;
};/*定义线程栈,存储线程执行时的运行信息*/
struct thread_stack
{uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;// 一个函数指针,指向线程执行函数,目的是为了实现通用的线程函数调用void (*eip)(thread_func *func, void *func_args);// 以下三条是模仿call进入thread_start执行的栈内布局构建的,call进入就会压入参数与返回地址,因为我们是ret进入kernel_thread执行的// 要想让kernel_thread正常执行,就必须人为给它造返回地址,参数void(*unused_retaddr); // 一个栈结构占位thread_func *function;void *func_args;
};/*PCB结构体*/
struct task_struct
{// 线程栈的栈顶指针uint32_t *self_kstack;// 线程状态enum task_status status;// 线程的优先级uint8_t priority;// 线程函数名char name[16];// 用于PCB结构体的边界标记uint32_t stack_magic;
};/*初始化PCB*/
void init_thread(struct task_struct *pthread, char *name, int prio);
/*根据PCB信息,初始化线程栈的运行信息*/
void thread_create(struct task_struct *pthread, thread_func function, void *func_args);
/*根据线程栈的运行信息开始运行线程函数*/
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_args);#endif
/thread/thread.c
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"#define PG_SIZE 4096static void kernel_thread(thread_func *function, void *func_args)
{function(func_args);
}/*初始化PCB*/
void init_thread(struct task_struct *pthread, char *name, int prio)
{memset(pthread, 0, sizeof(*pthread));strcpy(pthread->name, name);pthread->status = TASK_RUNNGING;pthread->priority = prio;/*一个线程的栈空间分配一页空间,将PCB放置在栈底*/pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);pthread->stack_magic = 0x19991030;
}
/*根据PCB信息,初始化线程栈的运行信息*/
void thread_create(struct task_struct *pthread, thread_func function, void *func_args)
{/*给线程栈空间的顶部预留出中断栈信息的空间*/pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));/*给线程栈空间的顶部预留出线程栈信息的空间*/pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));// 初始化线程栈,保存线程运行时需要的信息struct thread_stack *kthread_stack = (struct thread_stack *)pthread->self_kstack;// 线程执行函数kthread_stack->eip = kernel_thread;kthread_stack->function = function;kthread_stack->func_args = func_args;kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0;
}/*根据线程栈的运行信息开始运行线程函数*/
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_args)
{/*1.分配一页的空间给线程作为线程执行的栈空间*/struct task_struct *thread = get_kernel_pages(1);/*2.初始化PCB,PCB里存放了线程的基本信息以及线程栈的栈顶指针*/init_thread(thread, name, prio);/*3.根据线程栈的栈顶指针,初始化线程栈,也就是初始化线程的运行信息比如线程要执行的函数,以及函数参数*/thread_create(thread, function, func_args);/*4.上述准备好线程运行时的栈信息后,即可运行执行函数了*/asm volatile("movl %0,%%esp; \pop %%ebp; \pop %%ebx; \pop %%edi; \pop %%esi; \ret":: "g"(thread->self_kstack): "memory");return thread;
}
运行结果如下所示
可以看到,最后如期打印了执行函数中的信息