使用者級線程
程序 = 資源 + 指令
一個程序
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
總結一下
核心線程切換就是、
- 引起中斷進入核心
- 發生阻塞,需要切換線程,首先修改esp所指内容,也就是核心棧切換
-
中斷出口,因為進入其他核心棧,實際運作的線程的程式主要在使用者态中,是以需要調用
中斷出口,傳回使用者态指向使用者态線程程式。
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

linux-0.11中的切換主要是用TSS實作的,主要思路就是
- 将CPU中的内容即eax,ebx等等資料,根據TR所指向的段,儲存到裡邊
- 然後将新的選擇子指派給目前的TR
- 目前的TR再從新的段中找到eax,edx等内容指派到cpu中運作,其中就包括了esp的指派,也就是核心棧的切換也在這裡完成了。
linux 0.11用tss切換,但也可以用棧切換,因為tss中的資訊也可以寫到核心棧中。
另外故事ThreadCreate就順了
建立一個新的程序的時候,使用函數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)
總結一下, 子程序要跳到自己代碼處需要做上面?
- 調用exec中斷進入作業系統核心
- 調用_sys_execve, 将子程序要執行代碼的位址指派給eax
- 調用_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)
總結一下, 子程序要跳到自己代碼處需要做上面?
- 調用exec中斷進入作業系統核心
- 調用_sys_execve, 将子程序要執行代碼的位址指派給eax
- 調用_do_execve, 将入口位址指派給eip[0], 将SS:SP指派給eip[3], 也就是将一個棧指派給eip;