カーネルスタックスイッチングに基づくプロセススイッチングの実装-linux011

実験的な質問:

カーネルスタック切り替えに基づいたプロセス切り替えを実装する

実験の目的と要件:
カーネル スタックを構築するには、適切なリターン アドレスを適切なアドレスにプッシュし、カーネル スタックの外観に従って対応するアセンブリ コードを記述し、カーネル スタックのプッシュおよびポップ操作を慎重に完了します。戻りアドレスがポップアップ表示されるので、プロセスの切り替えがスムーズに完了します。同時に、カーネル スタックと PCB 間の関連付けが完了する必要があり、PCB が切り替えられる場合は、カーネル スタックの切り替えも完了する必要があります。

実験プロセス:
この実践的なプロジェクトは、Linux 0.11 で使用されている TSS スイッチング部分を削除し、スタックベースのスイッチング プログラムに置き換えることです。具体的には、Linux 0.11 の switch_to 実装が削除され、スタック スイッチングに基づいたコードに書き込まれます。
この実験の内容は、
アセンブリプログラムswitch_toの記述、
メインフレームの完成、
メインフレーム配下のPCB切り替え、カーネルスタック切り替え、LDT切り替え等を順次完了、
fork()の修正です。切り替えにはプロセスを作成する必要があります。カーネルスタックの切り替えは完了できるようです

PCB、つまり task_struct 構造を変更し、対応するコンテンツ フィールドを追加し、task_struct の変更によって生じる影響に対処します。

1. 現在のプロセスの PCB と新しいプロセスの PCB を見つけます。

現在のプロセスの PCB はグローバル変数 current (sched.c で定義) によって指されているため、 current は現在のプロセスの PCB を指しています
。新しいプロセスの PCB を取得するには、次のようにする必要があります。 schedule() 関数の変更:

if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;

switch_to(next);
に変更されました:
if ((* p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i, pnext = *p;

switch_to(pnext, LDT(next));
のようなthis 、 pnext は次のプロセスの PCB を指します

2.switch_toを変更します

Linux 0.11 の元の switch_to 実装を削除し、スタック切り替えに基づいたコードを作成します。
カーネル上で正確な操作を行う必要があるため、switch_to を記述するにはアセンブリ コードが必要ですが
、switch_to の実装にはアセンブリが使用されるため、switch_to の実装は system_call.s に配置するのが最も適切です。この関数は
主に次の関数を順に実行します: アセンブリを呼び出す C 言語であるため、最初にアセンブリ内のスタック フレームの処理、つまり
ebp レジスタの処理が必要であり、次に次の処理を表すパラメーターを取り出す必要があります。現在と
等しい場合は何もする必要はなく、等しくない場合はプロセスの切り替えが開始され、
PCB の切り替え、TSS のカーネル スタック ポインタの書き換え、カーネル スタックが行われます。スイッチング、LDT スイッチング、PC ポインタ (つまり
CS) が順に完了します。 :EIP) switch_to
:
Pushl %ebp
movl %esp,%ebp
Pushl %ecx
Pushl %ebx
Pushl %eax

(1) 切り替え対象のプロセスと現在のプロセスが同じプロセスかどうかを判断します。

movl 8(%ebp),%ebx	/*%ebp+8就是从右往左数起第二个参数,也就是*pnext*/
cmpl %ebx,current	/* 如果当前进程和要切换的进程是同一个进程,就不切换了 */
je 1f
/*先得到目标进程的pcb,然后进行判断
如果目标进程的pcb(存放在ebp寄存器中) 等于   当前进程的pcb => 不需要进行切换,直接退出函数调用
如果目标进程的pcb(存放在ebp寄存器中) 不等于 当前进程的pcb => 需要进行切换,直接跳到下面去执行*/

(2)スイッチ基板

movl %ebx,%eax
xchgl %eax,current
/*ebx是下一个进程的PCB首地址,current是当前进程PCB首地址*/

(3) TSSにおけるカーネルスタックポインタの書き換え

movl tss,%ecx		/*%ecx里面存的是tss段的首地址,在后面我们会知道,tss段的首地址就是进程0的tss的首地址,
根据这个tss段里面的内核栈指针找到内核栈,所以在切换时就要更新这个内核栈指针。也就是说,
任何正在运行的进程内核栈都被进程0的tss段里的某个指针指向,我们把该指针叫做内核栈指针。*/
addl $4096,%ebx           /* 未加4KB前,ebx指向下一个进程的PCB首地址,加4096后,相当于为该进程开辟了一个“进程页”,ebx此时指向进程页的最高地址*/
movl %ebx,ESP0(%ecx)        /* 将内核栈底指针放进tss段的偏移为ESP0(=4)的地方,作为寻找当前进程的内核栈的依据*/
/* 由上面一段代码可以知道们的“进程页”是这样的,PCB由低地址向上扩展,栈由上向下扩展。
也可以这样理解,一个进程页就是PCB,我们把内核栈放在最高地址,其它的task_struct从最低地址开始扩展*/

(4) カーネルスタックの切り替え

# KERNEL_STACK代表kernel_stack在PCB表的偏移量,意思是说kernel_stack位于PCB表的第KERNEL_STACK个字节处,注意:PCB表就是task_struct
movl %esp,KERNEL_STACK(%eax)	/* eax就是上个进程的PCB首地址,这句话是将当前的esp压入旧PCB的kernel_stack。所以该句就是保存旧进程内核栈的操作。*/
movl 8(%ebp),%ebx		/*%ebp+8就是从左往右数起第一个参数,也就是ebx=*pnext ,pnext就是下一个进程的PCB首地址。至于为什么是8,请查看附录(I)*/
movl KERNEL_STACK(%ebx),%esp	/*将下一个进程的内核栈指针加载到esp*/

(5)スイッチLDT

movl 12(%ebp),%ecx         /* %ebp+12就是从左往右数起第二个参数,对应_LDT(next) */
lldt %cx                /*用新任务的LDT修改LDTR寄存器*/
下一个进程在执行用户态程序时使用的映射表就是自己的 LDT 表了,地址空间实现了分离

(6) ユーザモードメモリ空間ポインタのセレクタ fs をリセット

movl $0x17,%ecx
mov %cx,%fs
通过 fs 访问进程的用户态内存,LDT 切换完成就意味着切换了分配给进程的用户态内存地址空间,
所以前一个 fs 指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,
所以就需要用这两条指令来重取 fs。

3. フォークを変更する
CPU を取得する新しいプロセスの場合、カーネル スタックで押された SS:ESP および CS:EIP を通じて、プロセスのユーザー スタック、ユーザー プログラム、およびそのカーネル スタックを関連付ける必要があります。
さらに、fork() の意味は、親プロセスと子プロセスが同じコード、データ、スタックを共有できるようにすることであるため、fork() を変更する中心的な作業は、図に示すように子プロセスのカーネル スタック構造を形成することです。下に。
fork() への変更は、子プロセスのカーネル スタックを初期化することです。fork() *copy_process のコア実装では、p = (struct task_struct *) get_free_page(); がメモリ ページのアプリケーションを完了するために使用されます。子プロセスの PCB として、p ポインタとページ サイズを加算したものが子プロセスのカーネル スタックの場所であるため、ステートメント krnstack = (long *) (PAGE_SIZE + (long) p); でカーネル スタックを見つけることができます。子プロセスの場所。 *次のステップは、krnstack (カーネル スタック) を初期化することです。
*(–krnstack) = ss & 0xffff;
*(–krnstack) = esp;
*(–krnstack) = eflags;
*(–krnstack) = cs & 0xffff;
*(–krnstack) = eip;
これら 5 つのステートメントは完了です上図に示されている重要な関係は、ss、esp、およびその他の内容が copy_proces() 関数のパラメータであるためです。これらのパラメータは、親プロセスのユーザー スタックである copy_proces() を呼び出すプロセスのユーザー スタックから取得されます。

krnstack の初期化:
*(–krnstack) = ebp;
*(–krnstack) = ecx;
*(–krnstack) = ebx;
// ここでの 0 が最も興味深いもので、戻り値が 0 であることを意味し、親プロセス
*(– krnstack) = 0;

4 つのスタックのポップおよび ret 処理では 1 が使用

ます

switch_to() の ret 命令。この命令は実行のための EIP ジャンプとしてカーネル スタックから 32 桁の数値をポップするため、関数アドレスを取得する必要があります (これはまだアセンブリ プログラムであるため、このアドレスは関数の先頭です)このアセンブリ プログラム ラベル) をスタックに初期化します。first_return_from_kernel という名前のアセンブリ ラベルを作成し、ステートメント *(–krnstack) = (long) first_return_from_kernel; を使用して、このアドレスを子プロセスのカーネル スタックに初期化します。ret の実行後、実行のために first_return_from_kernel にジャンプします。

4. first_return_from_kernel を書き込みます
。PCB 切り替えが完了し、カーネル スタック切り替えが完了し、LDT 切り替えが完了した後、ユーザー スタックとユーザー コードを切り替える必要があります。依存するコア命令は iret です。もちろん、実行は切り替える前にサイトを復元する必要があります。主に eax、ebx、ecx、edx、esi、edi、gs、fs、es、ds、およびその他のレジスタの回復です。

first_return_from_kernel のコア コード:

ポップル %edx
ポップル %edi
ポップル %esi
ポップ %gs
ポップ %fs
ポップ %es
ポップ %ds
アイレット

最後に、switch_to() と first_return_from_kernel はどちらも system_call.s に実装されているため、schedule.c と fork.c でこれらを呼び出す場合は、system_call.s でこれら 2 つのラベルをグローバルとして宣言する必要があることに注意してください。 、それらを参照する .c ファイル内で外部変数として宣言します。
system_call.s でのグローバル宣言

.globl switch_to
.globl first_return_from_kernel

.c ファイルの外部変数宣言に対応します。

extern long switch_to;
extern long first_return_from_kernel;

Guess you like

Origin blog.csdn.net/perfectzxiny/article/details/122100920