6.S081——CPUスケジューリング部(CPU多重化とスケジューリング)——xv6ソースコード完全解析シリーズ(10)

0.簡単に言うと

最後に、これまでにさまざまな場所でカーネル コードを読んだとき、常にCPU スケジューリング部分を習慣的にバイパスする(収量関数など)。さて、いよいよ実際に調査してみましょう。今回はついにオペレーティング システム全体におけるパズルの重要なピースを組み立てることができました。

オペレーティング システムに関連する基本的な概念を持っている人は、次のことを知っておく必要があります。オペレーティング システムの重要な機能の 1 つは仮想化です。(仮想化)。これは、OSTEP が提案するオペレーティング システムの 3 つの主要テーマ (オペレーティング システム: Three Easy Pieces、中国語訳: オペレーティング システム入門) の 1 つであり、他の 2 つは次のとおりです。同時実行性と一貫性仮想化は主に 2 つの側面を指します。1 つはCPU 仮想化 (またはコンピューティング リソースの仮想化として広く理解されています)です。最も重要な方法の 1 つは CPU の多重化とスケジューリングです。各プロセスが独立して CPU を占有しているかのような錯覚を引き起こします

私たちが以前に詳しく研究した 2 番目の側面は、物理メモリの仮想化です。ページテーブルとリクエストページングメカニズム、オペレーティング システムができるようにするため、仮想アドレスから物理アドレスへのマッピングを動的かつ柔軟に完了しますまた、これにより、各プロセスが大規模な空きメモリを操作できるかのような錯覚が生じますが、実際には、実際の物理メモリのごく一部しか占有しないのに、仮想アドレス空間は非常に大きくなります。

最後に宣伝なのですが、上記のOSTEPという本をオススメしたいと思います。これはウィスコンシン大学のレメスとアンドレアによって書かれました。完全にオープンソースで無料のオペレーティング システムの教科書この記事では、UNIX システム (多数の Xv6 カーネル ソース コードも含む) を分析対象として取り上げ、オペレーティング システムのベールを少しずつ剥がしていきます。オペレーティング システムの教科書を 1 つだけお勧めするとしたら、それは間違いなく OSTEP です。

さらに近いところでは、Xv6 カーネルには CPU スケジューリングを引き起こす 2 つの状況があります。

  • 1 つ目は、以前に見たように、クロック割り込みによって引き起こされた現在のプロセスが CPU 使用権を放棄するというものです。これは非常に一般的なスケジューリング アルゴリズムであり、通常はこれと呼ばれますタイムスライスラウンドロビンスケジューリングアルゴリズム(Round Robin)
  • 2 番目のタイプは、スピンロック (スピンロック) の実装を研究したときに、スピンロックは非常に不経済なアプローチであると述べました。Test&Set アトミック命令への呼び出しをブロックし、その場で待機するこれにより、CPU リソースが大幅に消費されます。このため、Xv6 ではスリープ ロック (スリープロック) が導入され、スリープ&ウェイクアップ メカニズムが導入され、その下で CPU スケジューリングも行われます。

この記事では、最初のケースであるクロック割り込みによる CPU スケジューリングに焦点を当てます。2 番目のケースについては次回説明します。
このブログでは、カーネル コードは次のように読み取られます。

1.kernel/proc.c
2.kernel/swtch.S
3.kernel/main.c

1. ピットを埋める - クロック中断によって引き起こされる CPU スケジューリング (歩留まり) についての深い理解

1.1 メモリ - イールドがトリガーされたとき

クロック割り込みのトリガーと応答については、カーネル トラップとクロック割り込みに関するブログで説明しました。これは特殊な割り込みであり、応答プロセスは 2 つの段階に分かれています。最初の段階では M モードの S モードでソフト割り込みをトリガーし、次に 2 番目の段階で devintr 関数を通じてソフト割り込みに応答します。そして識別は最終的にはユーザートラップまたはカーネルトラップ関数に組み込まれます。devintr の戻り値が 2 であるため、yield() 関数をトリガーして現在の CPU を使用する権利を放棄します。コードのスケルトンは次のとおりです (ユーザートラップを例として取り上げます)。

void
usertrap(void)
{
    
    
  int which_dev = 0;
  if(r_scause() == 8){
    
    
    // system call
  } else if((which_dev = devintr()) != 0){
    
    
    // ok
  } else {
    
    
	// exception
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  // 译:如果是时钟中断,则让出当前CPU的使用权
  if(which_dev == 2)
    yield();
  
  // 从用户陷阱中准备返回
  usertrapret();
}

現在、このクロック割り込みに応答するプロセス P が存在すると仮定します。その制御フローは、上記の場所から yield() 関数に入ります。, ということで、次の話はyield関数になります。

1.2 収量関数

yield 関数は非常に単純で、コードは次のとおりです。まず、現在のプロセスとロックを取得します。これは、オペレーティング システムを実行するプラットフォームには 4+1 コア (1 つのモニター コアと 4 つのアプリケーション コア) があるためです。複数のコアでもプロセスにアクセスするときに同時実行の問題が発生する可能性がありますしたがって、各プロセス構造にもロック保護が必要です。したがって、プロセスの状態を RUNNNABLE に変更すると(プロセスの状態は本質的に不変条件を維持します、プロセスの特定の状態に対応します。状態を変更するが実際に対応する状態に入らない前に、不変条件のセキュリティを維持するためにロックする必要があります), したがって、最初にプロセスのロックを取得する必要があります。

// Give up the CPU for one scheduling round.
// 译:放弃CPU来开启一个调度轮
void
yield(void)
{
    
    
  // 获取当前进程并上锁,加锁是为了保护进程状态结构体的访问互斥性
  // 防止在修改进程状态时导致的不变量临时为假的情况
  // 这个锁在scheduler中被释放(跨进程)
  struct proc *p = myproc();
  acquire(&p->lock);
  
  // 首先修改进程状态为RUNNABLE
  p->state = RUNNABLE;

  // 调用sched完成当前进程上下文的保存
  // 控制流自此会转向调度(scheduler)线程
  sched();
  
  // 进程再次返回时才可以释放之前持有的锁
  // 这个锁之前由scheduler函数获取,在这里释放(跨进程)
  release(&p->lock);
}

ちなみに、Xv6 のプロセスには 6 つの状態があります。これらの状態間の遷移が表示されます、できるなら参加します最後に、これらのプロセス状態遷移の概要を示します。

// Xv6中定义的6个状态
// 未使用、已使用、睡眠、可运行、正在运行、僵尸
enum procstate {
    
     UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

1.3 sched - 古いユーザープロセスとカーネルプロセスのコンテキストを交換する

ここで sched 関数について詳しく説明する前に、Xv6 のコンテキスト切り替えに関するいくつかの特別な概念を説明することが非常に必要です。プロセスコンテキストカーネルプロセス待って。さらに、スイッチ関数交換の本質を理解することが非常に必要であるため、このセクションではいくつかのサブセクションに分けて上記の問題を 1 つずつ説明します。

1.3.1 プロセスコンテキスト(コンテキスト)

分析スケジュールを入力する前に、Xv6 が必要である必要があります。カーネル スレッドの切り替えメカニズムについて、簡単かつ必要な説明をいくつか示します。オペレーティング システムの原則では、プロセスを切り替えるときは次のことを行う必要があるとよく言われます。コンテキスト (コンテキスト) の保存と復元, 同じ作業が Xv6 でも行われます。いくつかの汎用レジスタを格納するために特別に使用される構造コンテキストがあります。定義は次のとおりです (kernel/proc.h:2):

// Saved registers for kernel context switches.
// 译:内核上下文切换时保存的寄存器
struct context {
    
    
  // 返回地址寄存器和内核栈指针
  // 单独列出来,这是上下文切换的关键
  // 它们本质上控制着控制流和内核栈
  uint64 ra;
  uint64 sp;

  // callee-saved
  // 译:callee-saved寄存器
  // 这些是被调用者应该保存的寄存器
  uint64 s0;
  uint64 s1;
  uint64 s2;
  uint64 s3;
  uint64 s4;
  uint64 s5;
  uint64 s6;
  uint64 s7;
  uint64 s8;
  uint64 s9;
  uint64 s10;
  uint64 s11;
};

この構造体で宣言された s0 ~ s11 レジスタは、呼び出し先保存レジスタ (呼び出し先保存レジスタ)背景知識をいくつか挿入するだけで、関数が呼び出されるとき、レジスタ セット内のすべてのレジスタはCaller-SavedCallee-Savedの 2 つのタイプに分類できます。

  • 呼び出し側が保存したレジスタ、つまり関数呼び出しが発生する前に、C コンパイラはこの関数で使用されるレジスタをスタックに自動的にプッシュして保存します。、まで待ってください関数から戻った後、これらのレジスタは自動的にポップされて復元されます。呼び出し先がこれらのレジスタを関数内で安全に使用できるようにするため、呼び出し元が保存したレジスタをコンテキストに保存する必要がない理由は、 swtch (後述) 関数の実行時にコンパイラが自動的にレジスタを保存するためです
  • 呼び出し先によって保存されたレジスタ。呼び出される関数は関数の先頭にある必要があります。使用する関数は率先してスタックに保存してください、存在する関数関数が完了して返される前に、スタックをポップして 1 つずつ再開します。、そして最後に戻ってきます。RISC-V 標準では、さまざまなレジスタの保存責任を次のように規定しています。
    ここに画像の説明を挿入

それがわかりますs0 ~ s11 は呼び出し先が保存したレジスタです、新しいプロセスに切り替えた後はこれらのレジスタを必ず使用するため、コンテキストはそれらを保存する必要があります。スタック ポインタ sp を保存します。プロセスが後で再スケジュールされると、カーネル スタックは正常に復元されます。、戻りアドレスを保存する ra は、プロセスがスケジューリングを再開すると、命令を継続的に実行できます。コンテキストの切り替えにより命令フローが別のプロセスに切り替わるため、元の ra レジスタの内容が改ざんされることになります。これについては、すぐに swtch アセンブリ コードで確認します :)

したがって、このような単純な結論を導き出すことができます。Xv6では、 context = callee-saved registers + ra + sp です。これまで多くのオペレーティング システムの教科書を見てきましたが、明確に理解したのはこれが初めてです。プロセスコンテキストとは何ですか、私たちプロセスコンテキストはすべてのレジスタの集合であるとよく考えられていますが、これは明らかに私たちの認識を覆します。

1.3.2 スレッドのスケジューリングと切り替えを実現する 2 つの交換 - swtch 関数

Xv6では渡されます複数交換の方法プロセス切り替えを実現するには、swtch 関数を 1 回呼び出して、スワップ アクションを完了します。これは、次のようにコンパイルされたコード (kernel/swtch.S:8) の一部です。

# Context switch
#
#   void swtch(struct context *old, struct context *new);
# 
# Save current registers in old. Load from new.	
# 译:上下文切换
# 函数原型如下:void swtch(struct context *old, struct context *new);
# 向old指向的context存放寄存器,从new指向的context加载寄存器
.globl swtch
swtch:
		# 向old指向的context存放寄存器
        sd ra, 0(a0)
        sd sp, 8(a0)
        sd s0, 16(a0)
        sd s1, 24(a0)
        sd s2, 32(a0)
        sd s3, 40(a0)
        sd s4, 48(a0)
        sd s5, 56(a0)
        sd s6, 64(a0)
        sd s7, 72(a0)
        sd s8, 80(a0)
        sd s9, 88(a0)
        sd s10, 96(a0)
        sd s11, 104(a0)
        #----------------以上是汇编的前半段---------------------
		#----------------以下是汇编的后半段---------------------
		# 从new指向的context加载寄存器
        ld ra, 0(a1)
        ld sp, 8(a1)
        ld s0, 16(a1)
        ld s1, 24(a1)
        ld s2, 32(a1)
        ld s3, 40(a1)
        ld s4, 48(a1)
        ld s5, 56(a1)
        ld s6, 64(a1)
        ld s7, 72(a1)
        ld s8, 80(a1)
        ld s9, 88(a1)
        ld s10, 96(a1)
        ld s11, 104(a1)
        
        # 注意:因为修改了ra寄存器,这里执行ret
        # 之后,会切换到新线程上次被调度出去的地方继续执行
        # 同时因为修改了sp寄存器,内核栈也切换到了新的进程内核栈
        ret

ここのコードロジックは非常に明確に見えますが、これは、レジスタのバッチを保存し、レジスタのバッチをロードすることに他なりません。中でもraレジスタとspレジスタは最も重要な部分であり、それらの切り替えにより、基本的に制御フローとカーネル スタックの切り替えが完了します。しかし、上記のコードには実際には 3 つのプロセス コンテキスト、1 つのハードウェア コンテキストと 2 つのソフトウェア コンテキスト (そのうちの 1 つは特別なカーネル プロセスのコンテキスト) が含まれています。

Xv6 のカーネル プロセスは次のとおりです。アプリケーションプロセスグループ proc (kernel/proc.c:11) から独立した特別なプロセス、持っている独立した静的に割り当てられたカーネル スタック stack0 (kernel/start.c:11)であり、CPU コアに強く結合されています (たとえば、カーネル プロセスのコンテキストは、通常のプロセスのような proc 構造ではなく、cpu 構造に保存されます)。起動時に動作を開始し、コアの初期化タスクを完了し、システム起動完了後はスケジューリングプロセスとして機能し、CPUスケジューリングの中間プロセスに参加し、次にスケジュールされる新しいプロセスを選択し、プロセスの切り替えを完了します。

swtch 関数に戻ると、まず概念を持たなければなりません。上で述べたコンテキストはソフトウェアの概念です、対応する記憶域はメモリ内にあります。そしてCPU ハードウェア コア自体にもレジスタ セットがあります、そしてこれはハードウェアによって実装されます。Xv6のプロセス切り替え処理全体をより分かりやすく図化、次のようになります。

ここに画像の説明を挿入

  • ①:スケジューリングが発生する前に、古いプロセス P1 のコンテキストは CPU コアのハードウェア レジスタ ファイルを占有し、独自のロジックを実行しますスケジューリングが発生した後、swtch の最初のアクション (アセンブリの前半に相当) は、元々ハードウェア レジスタ ファイルで実行されていたコンテキストを、メモリ内にある P1 コンテキスト構造に格納し直すことです。
  • ②: CPU はカーネル プロセスのコンテキストをハードウェア レジスタ ファイルにロードし、スケジューリング プロセスを開始しますスイッチコードの後半に相当します
  • ③: スケジューラは、スケジュールできる適切なプロセスを見つけ、カーネル プロセスが再びスワップアウトされ、ハードウェア レジスタ ファイルの値がカーネル プロセス コンテキストに格納されます。これは、次のようになります。swtch が 2 回目に呼び出されたときのコードの前半の動作
  • ④:新しいプロセス コンテキストがハードウェア レジスタ ファイルにロードされますswtch が 2 回目に呼び出されたときのコードの後半に相当します、そこからコアが新しいプロセスで実行を開始します。

したがって、Xv6 は、上記の 2 つの交換プロセス (2 つのスイッチ) を通じて、プロセスのスケジューリングと切り替えを実現することに成功しています。コードの観点から、上の図に含まれる詳細を注意深く調べてください。

1.3.3 sched関数の実装(最初のswtch)

見出しに戻り、sched 関数の実装を見てみましょう。sched 関数のコード ロジックは依然として非常に単純です。まず、一連の合法性チェックが行われます、 それから復帰時に回復をスケジュールする前に、現在の CPU の元の割り込み状態を記録します。次に、swtch 関数を呼び出して、制御フローをカーネル スレッドに正常に切り替えます。カーネル スレッドでは、スレッドの選択とスケジューリングが実行されます

// Switch to scheduler.  Must hold only p->lock
// and have changed proc->state. Saves and restores
// intena because intena is a property of this
// kernel thread, not this CPU. It should
// be proc->intena and proc->noff, but that would
// break in the few places where a lock is held but
// there's no process.
// 译:切换到scheduler线程,必须持有旧进程的锁
// 并且已经改变了进程的执行状态
// 保存并恢复intena,intena是内核线程的属性,而不是CPU的属性
// 这本来应该是proc->intena和proc->noff表示的,但在有些罕见情况下
// 比如持有锁却无进程可以调度时,这个规律是不成立的
void
sched(void)
{
    
    
  int intena;
  struct proc *p = myproc();
  
  // sched函数会先做一系列的合法性检查
  // 是否持有当前进程的锁
  if(!holding(&p->lock))
    panic("sched p->lock");
  
  // 锁链长度是否为1
  // 这是为了确认当前CPU除了持有当前进程锁之外
  // 释放了其他所有的的锁
  if(mycpu()->noff != 1)
    panic("sched locks");
  
  // 如果当前进程状态为仍在运行,则出错
  if(p->state == RUNNING)
    panic("sched running");
  
  // 中断是否关闭?事实上,在我们成功获取进程锁之后
  // 在正常情况下中断就已经关闭了
  if(intr_get())
    panic("sched interruptible");
    
  // 记录当前CPU原始的中断开关状态
  // 新进程可能会影响这个标志
  intena = mycpu()->intena;
  
  // 第一次调用swtch,换入内核线程,并换出当前的旧线程
  // 请注意:旧进程下一次再次被调度时,应当从下一行代码开始执行
  swtch(&p->context, &mycpu()->context);
  
  // 下一次再次返回时恢复进程在此CPU的原始中断开关状态
  mycpu()->intena = intena;
}

1.4 新しいプロセスのスケジューリングとスワップイン - スケジューラ機能 (2 番目のスワップ)

上の話では、私たちの制御フローはカーネル プロセスに正常に切り替えられました。カーネル プロセスはどこから実行を開始するでしょうか?? ここで、まずスケジューラ関数の中から結論を述べます。カーネル プロセスのコードはスケジューラ関数内でどのように待機するのかと疑問に思われるかもしれません。これは、第 2 部 (バックトラック - ストーリーの始まり) で詳しく説明する部分です。ここに秘密があります :)

スケジューラー関数 (kernel/proc.c:437) の実装を検討してみましょう。この関数にはよく考えなければならないことがたくさんあります。以下で説明します。まず、完全なコードの表示とコメント

// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
// 译:每个CPU都有一个的进程的调度器
// 每个CPU都会在初始化完成之后调用scheduler()函数
// scheduler函数永不返回,而是保持死循环做以下事情:
// -选择一个进程执行
// -调用swtch(第二次调用swtch)来执行新进程
// -最终进程会将控制权通过swtch再次转回scheduler(即下一次调度)
void
scheduler(void)
{
    
    
  // 获取当前CPU,这是在系统完成初始化首次
  // 进入此函数时才会执行的代码
  struct proc *p;
  struct cpu *c = mycpu();
  
  // 设置当前CPU核心正在运行的进程为空
  // 这条设置至关重要,它保证了内核进程不会被时钟中断打断
  // 事实上在系统刚启动时,所有核心都没有进程在运行
  c->proc = 0;
 
  // 永不退出的死循环
  for(;;){
    
    
    // Avoid deadlock by ensuring that devices can interrupt.
    // 译:通过允许设备中断来避免死锁
    // 这个地方非常的细节和困难,分成两个部分来说:
    // 1.假设当前进程组中有一个进程,它正好沉睡着等待一个设备中断:
    // 一个典型的例子是在我们之前讲的串口通信中的uartputc函数
    // 一个进程可能会发现当前UART发送缓冲区已满,而开始睡眠并让出CPU
    // 这时候它在等待一个uartstart函数来唤醒它
    // 因为uartstart函数可以读取发送缓冲区中的字符并让出空间
    // 此时只有允许设备中断(uart中断),才可以在uartintr函数中调用uartstart函数
    // 进而唤醒这个进程,否则这个进程会一直沉睡下去
    
    // 2.你可能会问,那时钟中断会打断这个内核进程吗?内核进程会发生自交换吗?
    // 事实上,在内核陷阱程序kerneltrap中调用yield前
    // 会首先判断当前是否是内核进程在执行(myproc()!=0)
    // 所以内核线程不会触发yield()函数
    intr_on();
	
	// 扫描一次进程组,找出其中可以被调度的进程
    for(p = proc; p < &proc[NPROC]; p++) {
    
    
      // 调度之前先获取此进程的锁
      // 这个锁将会在返回新进程的yield时释放(跨进程)
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
    
    
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        // 译:切换到选中的进程,释放它自己的锁
        // 并在下次跳回到本函数之前再次获取锁都是进程本身的职责
        // 修改进程状态,并调用swtch(第二次调用)
        // 此时新进程将被调度
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->context, &p->context);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        // 译:进程至此已经完成运行
        // 它应该在返回之前将其状态p->state改变
        // c->proc=0表示运行在内核进程
        c->proc = 0;
      }
      
      // 释放yield函数中持有的进程锁,否则可能导致死锁(跨进程)
      release(&p->lock);
    }
  }
}

上記の関数は一見非常に単純に見えますが、詳細に検討する価値のあるポイントがたくさんあります。簡単に言えばこの関数はCPUスケジューリングを実装します。ですが、スケジューリング アルゴリズムは非常に単純で、プロセス グループを何度も走査し、RUNNABLE 状態を満たす最初のプロセスを見つけてスケジューリングを開始します各サイクルは最後の位置から逆方向にスキャンします。相対的な公平性、さもないとID 番号が小さいプロセスは継続的にスケジュールされます

さらに、次の 2 つの点に注意する必要があります。

1.4.1 注 1 - スケジューラとイールドのクロスプロセス ロック メカニズム

スケジューラ機能とイールド機能における珍しいロック機構これは、最初に 1 つのプロセスによって取得され、別のプロセスによって解放されますこのプロセスを画像として描画する効果は次のとおりです。
ここに画像の説明を挿入
古いプロセスが現在の CPU を放棄するとき、同時実行エラーを防ぐために、最初にプロセスのロックを取得する必要があります。それからsched 関数はカーネル プロセスにスケジュールされます。、カーネルプロセス前回終了したところから再開します(つまり、c->proc=0 は実行される命令です)。カーネル プロセスの次の操作はロックを解放することです (release(&p->lock))。このいわゆる p は、以前に CPU 上で実行されていた古いプロセスです。

あなたはこれに困惑し、この結論が正しいかどうかさえ疑問に思うかもしれないので、考えてみましょうロックを解除しない場合、または古いプロセスの代わりに他のプロセスのロックを解除する場合何が起こるか ここでプロセスのロックが解除されていない場合、現在のシステムで 1 つのプロセスのみが実行できるシナリオを想定してみましょう。次に、カーネル プロセスがプロセス グループをスキャンすると、スケジュールできる唯一のプロセスが見つかり、そのロックの取得を試みます。yield 関数と競合して無限ループに陥るXv6 カーネルは比較的正しいことがわかっているため、上記の状況は当てはまりませんが、反証方法により、ここで解放されるのは古いプロセスのロックであることがわかります。

同様に、scheduler() は新しいプロセスをスケジュールする前にロックも取得します。、このロックは新しいプロセスが yield() 関数に戻るときに、カーネル プロセスによって追加されたロックを解放します。、このプロセス間ロック機構の使用は非常にまれであり、ここで注目する価値があります。

1.4.2 注意点2 - スケジューラ機能における割り込み動作

カーネル スレッドは中断されるため、スケジューラ内で常に実行される必要はないことに注意してください。割り込みを開くことが非常に必要です。そうしないとデッドロックが発生する可能性があります。、具体的な例は上記のコードですでに示されているため、ここでは繰り返しません。注目に値するのは、割り込みを開くとクロック割り込みにも応答し、クロック割り込みによって現在の CPU が放棄される可能性があります。

カーネルトラップ kerneltrap では、次のように扱われます (kernel/trap.c:152)。カーネルプロセスがクロック割り込みに応答しないようにすることで、この問題をうまく回避できます

// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void 
kerneltrap()
{
    
    
  // 以上代码从略...
  // give up the CPU if this is a timer interrupt.
  // 译:如果是时钟中断则放弃当前CPU
  // 注意这里判断了myproc() != 0,筛去了内核进程的可能
  if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
    yield();

   // 以下代码从略...
}

1.5 概要 - クロック割り込みによる CPU スケジューリング

表す絵をあげてください完全なスケジュール設定プロセス、次のように:
ここに画像の説明を挿入

2. バックトラック – 物語の始まり

上記でキーを販売したため、カーネル プロセスはスケジューラ関数で待機しています。これがすべての問題の原因であり、実際、コードのこの部分は main.c で比較的簡単に確認できます。ファイル kenrel/main.c に目を向けます。main関数を実行するプロセスはカーネルプロセスです以下のコードでは、モニター コアとアプリケーション コアの両方が最後にスケジューラー関数に転送されることがわかります。したがって、後でクロックの中断のために CPU を放棄するとき、カーネルプロセスはスケジューラ機能で常に待機しています。この時点で、物語全体は完全に完了しています。

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
    
    
  if(cpuid() == 0){
    
    
    // monitor core负责整个操作系统的初始化
    // 此处详细步骤从略
    started = 1;
  } else {
    
    
    // application core的初始化步骤
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }
  
  // 无论是monitor core还是application core上的内核进程
  // 自此都转入scheduler函数,在scheduler函数中偶尔的中断并不会让
  // 内核进程让出当前CPU,它会一直扫描可以调度的用户进程
  scheduler();        
}


おすすめ

転載: blog.csdn.net/zzy980511/article/details/131519246