04-线程设计

来源:互联网 发布:单片机设计大赛 编辑:程序博客网 时间:2024/06/03 20:41

相信上下文切换的实验你已经写出来了,不过你肯定还很多不满意的地方:

  • 线程的切换太暴力
  • 代码看起来一箩筐没有组织
  • 看起来根本没有线程的样子

接下来,我们需要对上一节的程序进行改进,主要有以下几点:

  • 封装线程创建函数
  • 完成一个简单的调度算法

1. 线程结构体设计

在上一篇文章中根本没有线程结构体,说它是个线程实在是有点强人所难。我们标记线程的办法就只是使用了一个 task 数组,仅仅保存了线程的栈顶指针。现在我们要对它改良一下。

第一版的线程结构体非常简单,仅仅对之前的代码做了一个小小的扩充,除了要保存栈顶指针外,还有线程 id,线程过程函数还有栈:

#define STACK_SIZE 1024struct task_struct {  int id; // 线程 id  void (*th_fn)(); // 线程过程函数  int esp; // 栈顶指针  int stack[STACK_SIZE]; // 线程运行栈}

在后面,我们仍然使用 task 数组来保存线程,只不过这次的 task 数组保存的是线程结构体指针。

#define NR_TASKS 16 // 最大线程个数struct task_struct *task[NR_TASKS];

2. 封装线程创建函数

上一篇的上下文切换的实验里头,根本没有线程创建函数,而是直接在 main 函数里头构造线程要运行的栈这些东西,现在我们把那部分代码整合一下。

// tid 是传出参数,start_routine 是线程过程函数,// 这个函数和 pthread 库提供的 pthread_create 很像,// 只不过我们的线程过程函数没有参数。int thread_create(int *tid, void (*start_routine)()) {  int id = -1;  // 为线程分配一个结构体   struct task_struct *tsk = (struct task_struct*)malloc(sizeof(struct task_struct));  // 在任务槽中寻找一个空位置  while(++id < NR_TASKS && task[id]);  // 如果没找到就返回 -1  if (id == NR_TASKS) return -1;   // 将线程结构体指针放到空的任务槽中  task[id] = tsk;  // 将任务槽的索引号当作线程 id 号,传回到 tid  if (tid) *tid = id;   // 初始化线程结构体  tsk->id = id;   tsk->th_fn = start_routine;  int *stack = tsk->stack; // 栈顶界限  tsk->esp = (int)(stack+STACK_SIZE-11);  // 初始 switch_to 函数栈帧  stack[STACK_SIZE-11] = 7; // eflags  stack[STACK_SIZE-10] = 6; // eax  stack[STACK_SIZE-9] = 5; // edx  stack[STACK_SIZE-8] = 4; // ecx  stack[STACK_SIZE-7] = 3; // ebx  stack[STACK_SIZE-6] = 2; // esi  stack[STACK_SIZE-5] = 1; // edi  stack[STACK_SIZE-4] = 0; // old ebp  stack[STACK_SIZE-3] = (int)start; // ret to start  // start 函数栈帧,刚进入 start 函数的样子   stack[STACK_SIZE-2] = 100;// ret to unknown,如果 start 执行结束,表明线程结束  stack[STACK_SIZE-1] = (int)tsk; // start 的参数  return 0;}

需要注意的是在前一篇文章中,start 函数的参数是一个整数,而现在 start 函数的参数更改为了线程结构体指针,所以 stack[STACK_SIZE-1] 的位置传入的是线程结构体指针。

另外 start 函数所做的更改也很简单,这里留作一个练习,读者可以自行分析。

3. 上下文切换函数

由于我们添加了线程结构体,修正了 task 数组的类型,所以 switch_to 函数也会稍稍变动,它的原型如下:

void switch_to(struct task_struct *next);

switch_to 的参数现在表示下一个要运行的线程的结构体指针。这里暂时先不贴代码,我会放到文章后面。

4. 简单的调度算法

在上下文的实验中,我们切换“线程”的方法相当暴力,直接就在函数里 switch_to 到指定的“线程”了,现在我们将这种方式改变一下,比方在 fun1() 函数里,我们写成这样:

void fun1() {  while(1) {    printf("hello, I'm fun1\n");    sleep(1);    struct task_struct *next = pick();    if (next) {      switch_to(next);    }  }}

需要注意的是 pick 函数,它的任务是从 task 数组中挑选一个合适的线程,并返回其线程结构体指针。

一旦找到了下一个线程结构体指针,就可以使用 switch_to 函数切换上下文切过去啦。所以,这里我们看看 pick 函数的工作原理。

struct task_struct *pick() {  int current_id  = current->id;  int i = current_id;  struct task_struct *next = NULL;  // 寻找下一个不空的线程  while(1) {    i = (i + 1) % NR_TASKS;    if (task[i]) {      next = task[i];      break;    }  }  return next;}

pick 函数十分简单,它从当前线程所在的任务槽的下一个位置开始寻找不空的位置,找到就返回。这恐怕是世界上最简单的任务调度算法啦 ^_^,不过这已经能够满足我们的需求,后面我们肯定要对其进行改进。

5. 程序清单

5.1 main.c 程序

#include <stdio.h>#include <stdlib.h>#define NR_TASKS 16#define STACK_SIZE 1024struct task_struct {  int id;  void (*th_fn)();  int esp; // 保存 esp  int stack[STACK_SIZE];};static struct task_struct init_task = {0, NULL, 0, {0}};struct task_struct *current = &init_task;struct task_struct *task[NR_TASKS] = {&init_task,};void switch_to(struct task_struct *next);struct task_struct *pick() {  int current_id  = current->id;  int i = current_id;  struct task_struct *next = NULL;  while(1) {    i = (i + 1) % NR_TASKS;    if (task[i]) {      next = task[i];      break;    }  }  return next;}void fun1() {  while(1) {    printf("hello, I'm fun1\n");    sleep(1);    struct task_struct *next = pick();    if (next) {      switch_to(next);    }  }}void fun2() {  while(1) {    printf("hello, I'm fun2\n");    sleep(1);    struct task_struct *next = pick();    if (next) {      switch_to(next);    }  }}// 线程启动函数void start(struct task_struct *tsk) {  tsk->th_fn();  task[tsk->id] = NULL;  struct task_struct *next = pick();  if (next) {    switch_to(next);  }}int thread_create(int *tid, void (*start_routine)()) {  int id = -1;  struct task_struct *tsk = (struct task_struct*)malloc(sizeof(struct task_struct));  while(++id < NR_TASKS && task[id]);  if (id == NR_TASKS) return -1;  task[id] = tsk;  if (tid) *tid = id;  tsk->id = id;  tsk->th_fn = start_routine;  int *stack = tsk->stack; // 栈顶界限  tsk->esp = (int)(stack+STACK_SIZE-11);  // 初始 switch_to 函数栈帧  stack[STACK_SIZE-11] = 7; // eflags  stack[STACK_SIZE-10] = 6; // eax  stack[STACK_SIZE-9] = 5; // edx  stack[STACK_SIZE-8] = 4; // ecx  stack[STACK_SIZE-7] = 3; // ebx  stack[STACK_SIZE-6] = 2; // esi  stack[STACK_SIZE-5] = 1; // edi  stack[STACK_SIZE-4] = 0; // old ebp  stack[STACK_SIZE-3] = (int)start; // ret to start  // start 函数栈帧,刚进入 start 函数的样子   stack[STACK_SIZE-2] = 100;// ret to unknown,如果 start 执行结束,表明线程结束  stack[STACK_SIZE-1] = (int)tsk; // start 的参数  return 0;}int main() {  int tid1, tid2, tid3;  thread_create(&tid1, fun1);  printf("create thread %d\n", tid1);  thread_create(&tid2, fun2);  printf("create thread %d\n", tid2);  thread_create(&tid3, fun3);  printf("create thread %d\n", tid3);  int i = 5;  while(i--) {    printf("hello, I'm main\n");    sleep(1);    struct task_struct *next = pick();    if (next) {      switch_to(next);    }  }}

5.2 switch.s 程序

/*void switch_to(struct task_struct* next)*/.section .text.global switch_toswitch_to:  push %ebp  mov %esp, %ebp /* 更改栈帧,以便寻参 */  /* 保存现场 */  push %edi  push %esi  push %ebx  push %edx  push %ecx  push %eax  pushfl  /* 准备切换栈 */  mov current, %eax /* 保存当前 esp, eax 是 current 基址 */  mov %esp, 8(%eax)   mov 8(%ebp), %eax /* 取下一个线程结构体基址*/  mov %eax, current /* 更新 current */  mov 8(%eax), %esp /* 切换到下一个线程的栈 */  /* 恢复现场, 到这里,已经进入另一个线程环境了,本质是 esp 改变 */  popfl  popl %eax  popl %edx  popl %ecx  popl %ebx  popl %esi  popl %edi  popl %ebp  ret 

5.3 编译和运行

$ gcc main.c switch.s -o main$ ./main

下面是该程序的运行结果:


这里写图片描述
图1 运行结果

6. 总结

  • 线程结构体
  • 线程创建函数
  • 上下文切换函数
  • 简单的线程调度算法

练习 1:完成本文中的实验。
练习 2:解释实验运行结果。
练习 3:start 函数相比之前的区别在哪里?

0 0
原创粉丝点击