30日OS自作入門16日目(Win10)

はじめに

今回は本格的なマルチタスクを行っていくみたいです。優先順位やウィンドウを増やすことや、はてまてスリープまでさせるようです。

目次

マルチタスク-2

タスク管理の自動化(harib13a)

15日の最後の内容で、タスク管理を本格的にOSの管理下に置いたわけです。しかし、タスクを3個に増やしにくい…タイマやメモリのように*_alloc();を走らせたら自動的に増えていってほしいということで修正をするみたいです。

まずは、管理するための構造体を定義していきます。

/* mtask.c */
#define MAX_TASKS   1000    /* 最大タスク数 */
#define TASK_GDT0   3       /* TSSをGDTの何番目から割り当てるのか */
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, ds, fs, gs;
    int ldtr, iomap;
};
struct TASK {
    int sel, flags; /* sel(selector)はGDTの番号のこと */
    struct TSS32 tss;   /* バックアップする各種レジスタ */
};
struct TASKCTL {
    int running;   /* running:動作しているタスクの数 */
    int now;    /* now:"現在"動作しているタスクがどれだかわかるようにするるための変数 */
    struct TASK *tasks[MAX_TASKS];  /* タスク管理構造体 */
    struct TASK tasks0[MAX_TASKS];  /* タスク管理構造体 */
};
extern struct TIMER *task_timer;
struct TASK *task_init(struct MEMMAN *memman);  /* TASKCTLは巨大なのでメモリを確保 */
struct TASK *task_alloc(void);
void task_run(struct TASK *task);
void task_switch(void);

各種構造体と構造体にメモリ割り当て等を行っています。

プログラムにタスクとしての情報を与え、アドレスを返す関数は

struct TASK *task_init(struct MEMMAN *memman) /* タスク割り当てプログラム(タスクの管理下におかれる) */
{
  int i;
  struct TASK *task;
  struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
  taskctl = (struct TASKCTL *) memman_alloc_4k(memman, sizeof (struct TASKCTL));
  for (i = 0; i < MAX_TASKS;i++) {
    taskctl->tasks0[i].flags = 0;
    taskctl->tasks0[i].sel   = (TASK_GDT0 + i) * 8;
    set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss,AR_TSS32); /* タスクをGDTに登録 */
  }
  task = task_alloc();
  task->flags = 2;  /* 動作中のマーク */
  taskctl->running = 1;
  taskctl->now = 0;
  taskctl->tasks[0] = task;
  load_tr(task->sel);
  task_timer = timer_alloc();
  timer_settime(task_timer, 2);
  return task;  /* アドレスが帰ってくる */
}

タスク構造体を確保する場合は

struct TASK *task_alloc(void) /* タスク構造体の確保 */
{
  int i;
  struct TASK *task;
  for (i = 0; i < MAX_TASKS; i++) {
    if (taskctl->tasks0[i].flags == 0) {  /* 初期化されてないことの確認 */
      task = &taskctl->tasks0[i];
      task->flags = 1; /* 使用中のマーク */
      task->tss.eflags = 0x00000202; /* IF = 1;(STI後のフラグ) */
      task->tss.eax = 0; /* とりあえず0にしておくことにする */
      task->tss.ecx = 0;
      task->tss.edx = 0;
      task->tss.ebx = 0;
      task->tss.ebp = 0;
      task->tss.esi = 0;
      task->tss.edi = 0;
      task->tss.es = 0;
      task->tss.ds = 0;
      task->tss.fs = 0;
      task->tss.gs = 0;
      task->tss.ldtr = 0;
      task->tss.iomap = 0x40000000;
      return task;
    }
  }
  return 0; /* もう全部使用中 */
}

このレジスタの初期値は適当で、bootpack,.cで設定すればいいみたい

タスクを追加する関数とタスクスイッチを行う関数は

void task_run(struct TASK *task)  /* タスクを1つ追加する関数 */
{
  task->flags = 2; /* 動作中マーク */
  taskctl->tasks[taskctl->running] = task;
  taskctl->running++;
  return;
}

void task_switch(void)
{
  timer_settime(task_timer, 2);
  if (taskctl->running >= 2) {  /* タスクが1つの時の処理 */
    taskctl->now++;
    if (taskctl->now == taskctl->running) { /* 一番後ろだったら一番前にする */
      taskctl->now = 0;
    }
    farjmp(0, taskctl->tasks[taskctl->now]->sel);
  }
  return;
}

メタデータにもとづいてフラグを操作する感じみたいです。

timer.cも変更しているらしく、以下が変更されています。(ここの記載がなかった?ので注意)

    /* タイムアウト */
    timer->flags = TIMER_FLAGS_ALLOC;
    if (timer != task_timer) {
      fifo32_put(timer->fifo, timer->data);
    } else {
      ts = 1; /* task_timer(マルチタスク用のタイマ)がタイムアウトした */
    }
    timer = timer->next; /* 次のタイマの番地をtimerに代入 */
  }
  timerctl.t0 = timer;
  timerctl.next = timer->timeout;
  if (ts != 0){
    task_switch();  /* タスクスイッチを行う */
  }
  return;
}

実行すると

こんな感じでって…15日からめちゃくちゃ数値上がってない…?なんでぇ…

スリープしてみる(harib13b)

PCのスリープモード⁉かと思いましたが、違うみたいですね…(グゥ)
マルチタスク用語としてのスリープみたいです。タスクAで何もしていないときはHLTしているのですが、それでは資源がもったいない…タスクBにその分を回してあげるために、tasksという舞台から一時的に退場してもらうことみたいです。

だけど、スリープしている間にFIFOバッファにデータが来たら対応しなければなりません、その場合はtask_runするといったプログラムを追加するようです。

タスクを操作するプログラムでの、sleepする関数は

void task_sleep(struct TASK *task)
{
  int i;
  char ts = 0;  /* タスクスイッチするタイミングを最後にするためのフラグ */
  if (task->flags == 2) {   /* 指定タスクがもし起きていたら */
    if (task == taskctl->tasks[taskctl->now]) {
      ts = 1; /* 自分自身を寝かせるので、あとでタスクスイッチする */
    }
    /* taskがどこにいるかを探す */
    for (i = 0; i < taskctl->running; i++) {
      if (taskctl->tasks[i] == task) {
        /* ここにいた */
        break;
      }
    }
    taskctl->running--; /* 動作するタスクの記録用メタデータを減らす */
    if (i < taskctl->now) { /* 現在動作しているメタデータをずらす */
      taskctl->now--; /* ずれるので、これもあわせておく */
    }
    /* ずらし */
    for (; i < taskctl->running; i++) {
      taskctl->tasks[i] = taskctl->tasks[i + 1];
    }
    task->flags = 1; /* 動作をしていない状態 */
    if (ts != 0) {
      /* タスクスイッチをする */
      if (taskctl->now >= taskctl->running) {
        /* nowがおかしな値になっていたら、修正する */
        taskctl->now = 0;
      }
      farjmp(0, taskctl->tasks[taskctl->now]->sel); /* タスクスイッチ */
    }
  }
  return;
}

一見複雑そうですすが、自分のタスクをスリープさせるためにタスクを管理する構造体の情報の整合性を合わせて、その後タスクスイッチを行っている流れに見えます。

次にfifo側でデータが流れてきたときに起動できるようにしてあげるみたいです。 fifoバッファの構造体をタスクのデータも扱えるように設定しておき

struct FIFO32 {
    int *buf; /* バッファの位置を特定するためのアドレス(intに修正) */
    int p, q, size, free, flags; /* p:書き込み位置,q:読み込み位置,size:バッファの大きさ,free:バッファの空き容量,flags:あふれの確認フラグ, */
    struct TASK *task;  /* 起こしたいタスクのデータ */
};

次に、初期化も対応させると

void fifo32_init(struct FIFO32 *fifo, int size, int *buf, struct TASK *task)
/* FIFOバッファの初期化 */
{
  fifo->size = size;
  fifo->buf  = buf;
  fifo->free = size; /* 空き */
  fifo->flags = 0;
  fifo->p = 0; /* 書き込み位置 */
  fifo->q = 0; /* 読み込み位置 */
  fifo->task = task; /* データが入った時に起こすタスク */
  return;
}

最後に、タスクが寝ていることを確認してから起こす条件分岐を追加します。

int fifo32_put(struct FIFO32 *fifo, int data)
/* FIFOへデータを送り込んで蓄える */
{
  if (fifo->free == 0) {  /* もし空きがなければ */
    /* 空きがなくてあふれた */
    fifo->flags |= FLAGS_OVERRUN;
    return -1;      /* -1を返す */
  }
  fifo->buf[fifo->p] = data;
  fifo->p++;                      /* 書き込み位置を進める */
  if (fifo->p == fifo->size) {    /* 書き込み位置が最後に来たら */
    fifo->p = 0;                  /* 0に移動 */
  }
  fifo->free--;                   /* バッファの空きが減った */
  if (fifo->task != 0) {
    if (fifo->task->flags != 2) { /* タスクが寝ていたら */
      task_run(fifo->task); /* 起こしてあげる */
    }
  }
  return 0;                       /* 0を返す */
}

後は、Harimainのタスク管理の項目をfifoも対応するように書き換えます。
その際に、以下のように関数fifo_initでTASKの情報を渡すときに0を渡しています。これは、最初の0は、マルチタスクの初期化がまだで、2個目ではタスクBではタスクAをスリープさせる必要がないからみたいです。

実行すると

数値が2倍以上も上がるのは衝撃的だった。

ウィンドウを増やそう(harib13c)

ぐぉ…来てしまった…Harimainをかなり書き直すみたいですが、ウィンドウのため頑張ります…

変数actを使ってwindowの色を調整できているのは面白いですね…後々クリックしたら色が変わるとかにしていくのかな?
コードは、Githubの修正箇所をたどってみたほうが見やすいかも…

github.com

こういうときに見やすいから、githubちゃんは便利ですよね!

実行すると

エミュレータだとバラツキが出ると書いてありましたが、割と安定しましたね。

優先順位をつけよう(1)(harib13d)

タスクが4個できたし、その中でも優先順位をつけていきたいので 優先順位の付け方は、割り当てる切り替えまでの処理時間の大きさで優先順位を付けるようです。

struct TASK {
    int sel, flags; /* sel(selector)はGDTの番号のこと */
    int priority;   /* priority:優先度 */
    struct TSS32 tss;   /* バックアップする各種レジスタ */
};

priorityという変数に優先順位を管理させます。

task->priority = 2; /* 初期値は0.02秒 */

初期値は2=0.02秒であり、task_runで優先度の変更を行います。

void task_run(struct TASK *task, int priority)  /* タスクを1つ追加する関数 */
{
  if (priority > 0) { /* 優先度0は優先度を変えたくない時に使用 */
    task->priority = priority;  /* 優先度の変更 */
  }

また、時間として渡す場合はtask_switchのtimer_settimeの第二引数に渡すことにより、時間として渡します。

スリープ状態に関しては0を変更しない数値として扱い、起こす際に0を指定することで優先度が変わってしまうことを防いでいるようです。

実行すると

優先順位をつけよう(2)(harib13e)

(1)で優先度をつけることをしたが、これまでの状態ではマウスや音楽・ネットワーク処理とキー入力といった組み合わせのように優先度が高いものどうしが衝突した際は、先にタスクスイッチしたもの勝ちになるそうです。

なのでレベル別にタスクを区分けし、同レベルにあるタスクがすべてスリープ状態になったら次のレベルのタスクが実行可能になる制御を行うことで、調整を行うようです。

この章で、注意することがあったんですけど

    task_run(task_a, 1, 2); /* レベルは1 */

HarimainのタスクAの優先度がCDだと2なのですが、本だと0になっていました。これだとスリープしてしまう?一応2で進めます。
コードの処理を見直した結果、優先度が0であろうが2であろうが、結果は変わらなさそうです。要はどっちでも処理は少し違っても結果は同じってことみたいです。

実行してみると

あまり変化がないのが不安

メモ

FIFOバッファはあまり使わないようにすると早い
・タスクがHLTする時間があるなら、スリープを使って他のタスクに資源を回した方がいい
・優先度は、各タスクの処理時間の割り当てを変更することで実現
・タスクの優先度を割り振る際に、スリープ状態のことを考える

最後に

これでマルチタスクのベースが完成のようです。次は、コンソール…端末だぁ!