天天看點

使用者級線程和核心級線程--OS使用者級線程核心級線程

使用者級線程

程序 = 資源 + 指令

一個程序

mov ...  	mov[100], ax 	write ax
           

一個程序每執行一條指令,可能需要通路一個位址,這個位址就從映射表中擷取,而每一個程序就有一個對應的映射表,主要是為了防止程序之間通路同一個記憶體位址而設定的,當我們從一個程序切換到另外一個程序的時候,就需要切換映射表,而切換映射表需要一些代價,是以我們引進線程。

一般而言:

程序 = 資源 + 指令

資源就是映射表,因為映射表對應的就是一段段的記憶體位址。

而線程就是希望将資源和指令執行給分開,實作一個資源 + 多個指令執行序列。

是以線程切換時,隻需要改變PC值和一些寄存器的值,而不需要修改記憶體即映射表,這樣線程就既保留了并發的優點,同時避免了程序切換的代價。

線程的切換,就是指令的切換

使用線程的例子

一個網頁浏覽器的打開

  • 一個線程用來從伺服器接受資料‘
  • 一個線程用來顯示文本
  • 一個線程用來處理圖檔(如解壓縮)
  • 一個線程用來顯示圖檔

為什麼一個網頁的打開需要使用線程?

如果使用程序來實作網頁的打開

那麼一個程序首先從伺服器接收資料,接受完全部資料後再切換其他程序實作文本圖檔的顯示,這樣,其他程序的映射表就找不到第一個程序接受資料所在的位址,是以需要拷貝過去,效率低,另外,當一個程序在接受資料的時候,是将資料全部讀取完成後再顯示,使用者互動性不好。

是以,我們才使用程序實作網頁的打開

一個線程從伺服器接受資料的時候,當接受完文本資料,可以切換指令到另外一個線程将文本顯示出來,再重新回到線程接受資料,而其他線程顯示資料也不需要另外拷貝資料,因為線程資料是共享的,是以線程在實作網頁的打開要比使用程序優秀。

開始實作這個浏覽器

void WebExplorer()
{
	char URL[] = "http://cms.hit.edu.cn";
	char buffer[1000];
	pthread_create(..., GetData, URL, buffer);
	pthread_create(..., show, buffer);
}
void GetData(char *URL, char *p)
{...}
void show(char *p)
{...}
           

線程切換的實作

線程切換的實作靠的是棧的切換

為什麼要切換棧?

下面看兩段代碼

//線程1
100: A()
{
	B();
	104;
}
200: B();
{
	Yield();
	204;
}
//線程2
300: C()
{
	D();
	304;
}
400: D();
{
	Yield();
	404;
}
void Yield()
{
	找到300;
	jmp 300;
}
           

如果要實作程序的切換,那麼就需要調用函數,根據C語言調用函數的實作,我們每調用一個函數

需要在棧中儲存函數傳回位址,那麼從線程1到線程2的切換,我們将棧中的儲存值寫下來

104 // 棧底
204
304
404 // 棧頂
           

當我們在D()函數執行完之後,遇到}時會執行ret,也就是從棧中彈出一個位址出來,這時候

彈出來的位址為404, 這顯然不是我們想要的,因為,我們是從線程1切換到線程2執行,執行

完之後,我們需要重新回到線程1來,但是現在,彈棧彈出來的是404,為什麼會這樣,因為我們

兩個線程使用的是同一個棧,使用同一個棧無法實作線程的切換,是以,我們需要将棧分開,也就是

一個線程使用單獨的一個棧,這就是實作線程切換的關鍵

是以,我們在TCB中設定一個棧結構,當我們需要切換線程時,使用Yield函數,能夠實作棧的切換

void Yield()
{
	TCB2.esp = esp;
	esp = TCB.esp
}
           

這樣就實作了線程的切換。

兩個線程的樣子:兩個TCB,兩個棧,切換的PC在棧中

ThreadCreate的核心就是用程式做出這三樣東西

void ThreadCreate的核心就是用程式做出這三樣東西
{
	TCB *tcb=malloc();
	*stack = malloc();
	*stack = A;
	tcb.esp = stack;
}
           

将所有東西組合在一起

void WebExplorer()
{
	ThreadCreate(GetData, URL, buffer);
	while(1)
		Yield();
}
void GetData(char *URL, char *p)
{
	連接配接URL;
	下載下傳;
	Yield();
}
void ThreadCreate(func, argv1)
{
	申請棧;
	申請TCB,func等入棧;
	關聯TCB與棧;
}
void Yield()
{
	壓入現場;
	esp放在目前TCB;
	next();
	從下一個TCB中取出esp;
	彈棧切換線程;
}

gcc -0 explorer get.c yiled.c ...
           

核心級線程

核心級線程

使用者級線程與核心級線程的差別

前面學習使用者級線程時,我們知道,線程的切換用到的是棧,從一個棧到兩個棧,每一個線程

使用自己的一個棧,而到了用了核心級線程,意味着線程既要在使用者級線程上切換,還要到核心

上切換,是以,需要用到的是使用者棧和核心棧,也就是從兩個棧到兩套棧

使用者态線程切換是,TCB切換,然後再切換使用者棧

核心級線程切換是,TCB切換,然後切換兩套棧,使用者棧也要跟着核心棧一起切換

使用者棧和核心棧之間的關聯

從使用者态到核心态,主要就是靠中斷實作,當中斷出現時,核心棧就會保留使用者棧的位址(SS, SP)

還有使用者态執行到何處的指令即PC和CS值,這就是一套棧的實作。

下面看一個例子:

100: A()
{
	B();
	104:
}
200: B()
{
	read();
	204:
}
300: read()
{
	int 0x80;
	304:
}
system_call:
	call sys_read;
1000:
2000: sys_read()
{}

	使用者棧
	104
	204

	核心棧(線程S)
	SS: SP   // SS指向使用者棧的首位址,SP指向指令執行的位置
	EFLAGS
	304   // 調用中斷後儲存的傳回位址
	cs   // cs指向 100,中斷傳回位址;
	1000
           

當進入磁盤讀時,就會引起阻塞,此時開始程序的切換

switch_to

sys_read()
{
	next;
	switch_to(cur, next);  //找到下一個線程,cur,next就是目前線程和下一個線程的TCB
}
//switch_to: 仍然是通過TCB找到核心棧指針;然後通過ret切換到某個核心程式;
最後再用CS:PC切到使用者程式

		線程S		線程T
		SS: SP		EFLAGS
		EFLAGS		PC = ???
		304			CS =  ???
		cs			????  <--esp
		1000
 -->esp ??? 

//切換線程就是将目前的esp賦給cur.esp,然後将next.esp賦給目前的esp;
//PC和CS是什麼?其實線程T主要是代碼在使用者态中,進入核心态主要是為了
進行線程的切換,是以PC和CS實際上就是線程T的使用者态代碼所在的位址。
為了能夠從線程T重新回到使用者态線程,esp所指向的代碼處應含有iret代碼,也就是
中斷傳回代碼。
           

核心線程switch_to的五段論

中斷入口:(進入切換)
push ds; ... pusha;
mov ds, 核心段号; ...
call 中斷
??:

中斷處理:(引發切換)
啟動磁盤讀或時鐘中斷;
schedule();
}//ret

schedule: next=..;
call switch_to;
}//ret

switch_to:(核心棧切換)
TCB[cur].esp = %esp;
%esp = TCB[next].esp;
ret

中斷出口:(第二級切換)
popa; ...; pop ds;
iret
           

總結一下

核心線程切換就是、

  1. 引起中斷進入核心
  2. 發生阻塞,需要切換線程,首先修改esp所指内容,也就是核心棧切換
  3. 中斷出口,因為進入其他核心棧,實際運作的線程的程式主要在使用者态中,是以需要調用

    中斷出口,傳回使用者态指向使用者态線程程式。

ThreadCreate!做出那個樣子

void ThreadCreate(...)
{
	TCB tcb = get_free_page();
	*krlstack = ...;
	*userstack傳入;
	填寫兩個stack;
	tcb.esp = krlstack;
	tcb.狀态 = 就緒;
	tcb入隊;
}
           

#核心級線程實作

切換五段論中的中斷入口和中斷出口

main()
{
	A();
	B();
}
A()
{
	fork();   --->系統調用引起中斷, 
}
遇到fork;
	mov %eax, __NR_fork
	int 0x80
	mov res, %eax
引起中斷後,就會調用_system_call
void sched_init(void)
{
	set_system_gate(0x80, &system_call);
}
_system_call初始化時,就會将中斷處理設定好,将使用者線程的内容壓入核心棧中。
_system_call:
	push %ds..%fs 
	pushl %edx...
	call sys_fork									  核心棧
	push %eax  // 将線程的資訊保留在核心棧中			 SS: SP
	mov1 _current, %eax								   EFLAGS
	cmpl $0, state(%eax)  //判斷狀态是否阻塞			 ret = ??1 //調用int 0x80中斷後面的位置
	jne reschedule	//如果狀态阻塞,就切換線程			  ds
	cmpl $0, counter(%eax)  //如果時間片用光了			  es
	je reschedule  //切換線程							 fs
	ret_from_sys_call:  //中斷傳回						 edx
														ecx
	reschedule:											ebx
		pushl $ret_from_sys_call						??2
		jmp _schedule
	// 這段代碼就是将ret_from_sys_call壓入棧中,然後調用 _schedule函數
	當函數遇到右括号時,就會從棧中彈出一個位址,此時就會執行中斷傳回

void schedule(void)
{
	next = i;
	switch_to(next); //下一個核心級線程的TCB
}
ret_from_sys_call:
	pop %eax //傳回值 	popl %ebx  ... 			就是将前面壓入棧中的東西彈出來
	pop %fs ...
	iret //重要, 傳回到int 0x80後面執行, mov res, %eax
           

切換五段論中的switch_to

使用者級線程和核心級線程--OS使用者級線程核心級線程

linux-0.11中的切換主要是用TSS實作的,主要思路就是

  1. 将CPU中的内容即eax,ebx等等資料,根據TR所指向的段,儲存到裡邊
  2. 然後将新的選擇子指派給目前的TR
  3. 目前的TR再從新的段中找到eax,edx等内容指派到cpu中運作,其中就包括了esp的指派,也就是核心棧的切換也在這裡完成了。

linux 0.11用tss切換,但也可以用棧切換,因為tss中的資訊也可以寫到核心棧中。

另外故事ThreadCreate就順了

使用者級線程和核心級線程--OS使用者級線程核心級線程

建立一個新的程序的時候,使用函數copy_process, 其中的參數都是從棧中取出,也就是從父程序中

拷貝而來

copy_process的細節:建立棧

p = (struct task_struct *)get_free_page();
//申請核心空間
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;  //0x10是核心資料段
//建立核心棧
p->tss.ss = ss & 0xffff;
p->tss.esp = esp;
//建立使用者棧(和父程序共用棧), esp 就是從父程序中的棧中傳遞過來的參數
           

上面的代碼實作了1、申請記憶體空間,2、建立TCB,3、建立核心棧和使用者棧,4、關聯兩個棧和TCB

copy_process的細節:執行前準備

p->tss.eip = eip;
p->tss.cs = cs & 0xffff;
//将執行位址cs:eip放在tss中
p->tss.eax = 0;
p->tss.ecx = ecx;
//執行時的寄存器也放進去了
...
p->state = TASK_RUNNING;
           

将上面所有的代碼捋一下

在前面的fork中斷中,假如程序遇到阻塞了,這時就會調用reschedule,調用了reschedule,就會

調用switch_to(next), 在switch_to(next)中,我們會進行tss切換,也就是切換程序,由于子程序

是從父程序得到的eip,是以,子程序與執行父程序後面的代碼,也就是mov res %eax, 而子程序的eax

被置為0,在此處,我們就會看到程序切換的一段經典代碼

if(!fork()){}

,此時程序就切換到子程序

運作了。

第三個故事:如何執行我們想要的代碼?

int main()
{
	while(1)
		scanf("%s", cmd);
	if(!fork())
	{
		exec(cmd)l
	}
	wait(0);
}
// 切換程序時
p->tss.eip = eip;
p->tss.cs = cs;
p->tss.eax = 0;

if(!fork()){exec(cmd);}
_system_call:
	push %ds .. %fs
	pushl %edx..
	call sys_execve

exec為系統調用,當子程序為進入exec時,子程序與父程序執行相同的代碼,當子程序進入exec并且退出來後
就要去執行自己的代碼了。

那麼子程序是如何從中斷傳回後跳到執行自己代碼的地方呢?

中斷傳回時會執行`iret`,而`iret`做了上面,`iret`在核心棧中找到eip并置給真正的eip;
是以,我們需要做的就是找到子程序要執行的代碼的PC位址,并指派給核心棧中的eip,而當子程序
執行`iret`指令後,就會跳轉到自己的代碼處執行了。

_sys_execve:
	lea EIP(%esp), %eax  将目前的棧指針加上EIP指派給eax
	pushl %eax
	call _do_execve
EIP = 0x1C

int do_execve(*eip, ...)
{
	p += change_ldt(...;
	eip[0] = ex.a_entry
	eip[3] = p;)
}
struct exec{
	unsigned long a_magic;
	unsigned a_entry; //入口
};
eip[0] = esp + 0x1C;
eip[3] = esp + 0x1C + 0x0C = esp + 0x28(正好是SP)
           

總結一下, 子程序要跳到自己代碼處需要做上面?

  1. 調用exec中斷進入作業系統核心
  2. 調用_sys_execve, 将子程序要執行代碼的位址指派給eax
  3. 調用_do_execve, 将入口位址指派給eip[0], 将SS:SP指派給eip[3], 也就是将一個棧指派給eip;
    l %eax
     		call _do_execve
     	EIP = 0x1C
    
     int do_execve(*eip, ...)
     {
     	p += change_ldt(...;
     	eip[0] = ex.a_entry
     	eip[3] = p;)
     }
     struct exec{
     	unsigned long a_magic;
     	unsigned a_entry; //入口
     };
     eip[0] = esp + 0x1C;
     eip[3] = esp + 0x1C + 0x0C = esp + 0x28(正好是SP)
               

總結一下, 子程序要跳到自己代碼處需要做上面?

  1. 調用exec中斷進入作業系統核心
  2. 調用_sys_execve, 将子程序要執行代碼的位址指派給eax
  3. 調用_do_execve, 将入口位址指派給eip[0], 将SS:SP指派給eip[3], 也就是将一個棧指派給eip;

繼續閱讀