linux ucontext 类型,协程:posix::ucontext用户级线程实现原理分析 | WalkerTalking
在聽完leader的課程后,對其中協程的實現方式有了基本的了解,無論的POSIX的ucontex,boost::fcontext,還是libco,都是通過保存和恢復寄存器狀態,來進行各個協程上下文的保存和切換。所以有了這篇對ucontext實現原理的分析。
文章首先初略的溫習了一下匯編的一些基礎知識,其次就是ucontext源碼分析,最后是一個ucontext示例極其調試過程。
歡迎批評指正
1. 匯編基礎
8086/8088的cpu的寄存器都是16位的,我相信這是我們最熟悉的CPU,因為大學的時候大家的匯編語言課程千篇一律都是講這一代cpu的,到了80x86的時候,cpu都是32位的,再后來都是64位的時代了。
1.1 寄存器分類
下面是一些對開發者來說,相對重要的寄存器,也是分析ucontext源碼的全部寄存器:
register.png
這些寄存器是最基本也是匯編中直接使用的寄存器最多的。
其中AX,BX,CX,DX這些寄存器用來保存操作數和運算結果等信息,從而節省讀取操作數所需占用總線和訪問存儲器的時間,可以認為隨便用。
指針寄存器:SP,BP,用于維護和訪問堆棧存儲單元。
BP為基指針(Base Pointer)寄存器,用它可直接存取堆棧中的數據;
SP為堆棧指針(Stack Pointer)寄存器,用它只可訪問棧頂。
變址寄存器:寄存器SI,DI稱為變址寄存器(Index Register),它們主要用于存放存儲單元在段內的偏移量,
指令指針寄存器
指令指針IP(Instruction Pointer)是存放下次將要執行的指令在代碼段的偏移量。在具有預取指令功能的系統中,下次要執行的指令通常已被預取到指令隊列中,除非發生轉移情況。
這里沒有列出所有的寄存器,比如說各個段寄存器CS,DS,ES,SS,GS,訪問進程各個段空間的基本地址。這些寄存器伴隨這每一條指令的執行,所以很重要,但這里不詳解,因為這些寄存器的使用,對匯編代碼的開發者說,很多時候是透明的。
1.2.指令格式
下面列出了簡單的匯編指令,知道這些指令的含義看匯編代碼就不會有什么問題了:
指令類型
名稱
通用數據傳輸指令
mov, push,pop, lea
算術指令
add,adc,inc,sub,sbb,dec,neg,cmp,mul,imul,div,idiv
邏輯指令
and,or,xor,not,shl,shr,test
控制轉移指令
jmp,call,ret,retf
這里要說明的是:在like unix系統上的匯編語法格式采用AT&T 格式,和Wins下面的intel風格有很大差異,這里只列出三點:
AT&T格式和Intel格式的指令的源操作數和目的操作數的順序是相反的
AT&T格式
Intel格式
mov %rax %rdi
mov rdi rax
上面指令的含義是將rax寄存器的值存入rdi寄存器中
AT&T格式寄存器操作數前面要加上‘%’,Intel格式不需要,從上面一點可以看出;
AT&T格式指令如果要控制操作數長度是通過指令來進行的,Intel格式是通過在操作數前加限定來進行:
AT&T格式
Intel格式
movb val, %al
mov al byte ptr val
所以你會在AT&T格式的指令中,看到各種mov:movb,movw,movl,movq,分別代表8bits, 16bits, 32bits, 64bits。同樣其他指令也是這樣。
為了能夠清晰的看懂ucontext族的glibc的匯編實現,這里還要說明幾點:
匯編中的尋址方式:立即數尋址, 直接尋址,間接尋址,變址尋址;下面是AT&T指令格式示例:
尋址方式
指令
AT&T格式
立即數尋址
movl $0x123, %edx
數字->寄存器
直接尋址
movl 0x123, %edx
0x123指向內存數據->寄存器
間接尋址
movl (%ebx), %edx
ebx寄存器指向內存數據-> edx寄存器
變址尋址
movl 4(%ebx), %edx
ebx+4指向內存數據-> edx寄存器
lea指令,裝入有效地址到寄存器;
跳轉指令:call,ret
cpu執行call跳轉指令時,cpu做了如下操作:1
2
3
4
5
6rsp = rsp – 8
rsp = rip
//即跳轉之前會將下一條要執行語句指令地址壓入棧頂
//call等同于以下兩條語句,但call本身就一條指令
push %rip
jmp 標號
類似ret指令會將棧頂的內容彈出到rip寄存器中,繼續執行:1
2
3
4rip = rsp
rsp = rsp – 8
//等同于
pop %rip
1.3. gcc關于寄存器的使用
GCC中對這些寄存器的調用規則如下:
rax 作為函數返回值使用。
rsp 棧指針寄存器,指向棧頂;
rdi,rsi,rdx,rcx,r8,r9 用作函數參數,依次對應第1參數,第2參數。。。當參數超過6個,才會通過壓棧的方式傳參數。
rbx,rbp,r12,r13,r14,r15 用作數據存儲,遵循被調用者使用規則,簡單說就是隨便用,調用子函數之前要備份它,以防他被修改;
r10,r11 用作數據存儲,遵循調用者使用規則,簡單說就是使用之前要先保存原值;
要想看懂linux下的匯編源碼,這些規則是很很很重要的,否則下面ucontext族的匯編實現會看的一臉蒙逼.
2. ucontext分析
協程切換的時候,保存當前協程的上下文,主要是各個寄存器和信號狀態。我們先看看POSIX標準提供的用于用戶級線程切換的接口ucontext族函數:1
2
3
4
5
6
7
8
9/* Userlevel context. */
typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext *uc_link;//后繼上下文
stack_t uc_stack;//用戶自定義棧
mcontext_t uc_mcontext;//保存當前上下文,即各個寄存器的狀態
__sigset_t uc_sigmask;//保存當前線程的信號屏蔽掩碼
} ucontext_t;
ucontext提供的一套api接口,有以下四個個:1
2
3
4int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
具體功能:getcontext獲取線程的當前上下文;setcontext相反是從ucp中恢復出上下文;makecontext是修改ucp指向的上下文環境,swapcontext是保存當前上下文,并切換到新的上下文。下面看他們的具體實現:
2.1. getcontext實現
我們看看getcontext的glibc實現:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52ENTRY(__getcontext)
/* Save the preserved registers, the registers used for passing
args, and the return address. */
movq%rbx, oRBX(%rdi)
movq%rbp, oRBP(%rdi)
movq%r12, oR12(%rdi)
movq%r13, oR13(%rdi)
movq%r14, oR14(%rdi)
movq%r15, oR15(%rdi)
movq%rdi, oRDI(%rdi)
movq%rsi, oRSI(%rdi)
movq%rdx, oRDX(%rdi)
movq%rcx, oRCX(%rdi)
movq%r8, oR8(%rdi)
movq%r9, oR9(%rdi)
movq(%rsp), %rcx
movq%rcx, oRIP(%rdi)
leaq8(%rsp), %rcx/* Exclude the return address. */
movq%rcx, oRSP(%rdi)
/* We have separate floating-point register content memory on the
stack. We use the __fpregs_mem block in the context. Set the
links up correctly. */
leaqoFPREGSMEM(%rdi), %rcx
movq%rcx, oFPREGS(%rdi)
/* Save the floating-point environment. */
fnstenv(%rcx)
fldenv(%rcx)
stmxcsr oMXCSR(%rdi)
/* Save the current signal mask with
rt_sigprocmask (SIG_BLOCK, NULL, set,_NSIG/8). */
leaqoSIGMASK(%rdi), %rdx
xorl%esi,%esi
#if SIG_BLOCK == 0
xorl%edi, %edi
#else
movl$SIG_BLOCK, %edi
#endif
movl$_NSIG8,%r10d
movl$__NR_rt_sigprocmask, %eax
syscall
cmpq$-4095, %rax/* Check %rax for error. */
jaeSYSCALL_ERROR_LABEL/* Jump to error handler if error. */
/* All done, return 0 for success. */
xorl%eax, %eax
ret
PSEUDO_END(__getcontext)
getcontext的匯編代碼中,第一部分就是保存當前上下文中的各個寄存器到第一個參數rdi中,即ucontext_t中,其中目標操作數(%rdi)前面的oRBX,oRBP…的含義如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16sysdeps/unix/sysv/linux/x86_64/ucontext_i.sym
#define ucontext(member)offsetof (ucontext_t, member)
#define mcontext(member)ucontext (uc_mcontext.member)
#define mreg(reg)mcontext (gregs[REG_##reg])
oRBPmreg (RBP)
oRSPmreg (RSP)
oRBXmreg (RBX)
oR8mreg (R8)
oR9mreg (R9)
oR10mreg (R10)
oR11mreg (R11)
oR12mreg (R12)
oR13mreg (R13)
oR14mreg (R14)
所以oRBP = offsetof(ucontext_t, un_mcontext.greps[REG_RBP]),即ucontext_t結構中用于保存各個寄存器的相對位移。
getcontext中,第一部分保存各個寄存器狀態值如下:
getcontext.png
進入getcontext之后
首先保存rbx,rbp,r12,r13,r14,r15,這6個數據寄存器,因為他們遵循被調用者使用,所以需要保存,
然后是保存rdi,rsi,rdx,rcx,r8,r9這6個寄存器,因為它用于保存函數參數,也是遵循被調用者使用。但大家發現沒有,getcontext只有一個ucontext參數,所以保存后面5個寄存器是多余的。
其次,讀取rsp寄存器指向的進程stack棧頂中的RIP值,該棧頂的值,是在調用getcontext時,即執行call指令時,默認會做的事情:將下一條指令地址push進棧頂空間。讀取后會將該值保存到ucontext中,當恢復時,恢復到RIP寄存器中。
再次,將棧頂指針加8,即獲得調用getcontext()之前的棧頂指定,并保存到ucontext中,當恢復時,恢復到RSP寄存器中。
getcontext的第二部分設置浮點計數器, 第三部分就是保存當前線程的信號屏蔽掩碼;
2.2. makecontext實現1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67void
__makecontext (ucontext_t *ucp, void (*func) (void), int argc, ...)
{
extern void __start_context (void);
greg_t *sp;
unsigned int idx_uc_link;
va_list ap;
int i;
/* Generate room on stack for parameter if needed and uc_link. */
sp = (greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp
+ ucp->uc_stack.ss_size);
sp -= (argc > 6 ? argc - 6 : 0) + 1;
/* Align stack and make space for trampoline address. */
sp = (greg_t *) ((((uintptr_t) sp) & -16L) - 8);
idx_uc_link = (argc > 6 ? argc - 6 : 0) + 1;
/* Setup context ucp. */
/* Address to jump to. */
ucp->uc_mcontext.gregs[REG_RIP] = (uintptr_t) func;
/* Setup rbx.*/
ucp->uc_mcontext.gregs[REG_RBX] = (uintptr_t) &sp[idx_uc_link];
ucp->uc_mcontext.gregs[REG_RSP] = (uintptr_t) sp;
/* Setup stack. */
sp[0] = (uintptr_t) &__start_context;
sp[idx_uc_link] = (uintptr_t) ucp->uc_link;
va_start (ap, argc);
/* Handle arguments.
The standard says the parameters must all be int values. This is
an historic accident and would be done differently today. For
x86-64 all integer values are passed as 64-bit values and
therefore extending the API to copy 64-bit values instead of
32-bit ints makes sense. It does not break existing
functionality and it does not violate the standard which says
that passing non-int values means undefined behavior. */
for (i = 0; i < argc; ++i)
switch (i)
{
case 0:
ucp->uc_mcontext.gregs[REG_RDI] = va_arg (ap, greg_t);
break;
case 1:
ucp->uc_mcontext.gregs[REG_RSI] = va_arg (ap, greg_t);
break;
case 2:
ucp->uc_mcontext.gregs[REG_RDX] = va_arg (ap, greg_t);
break;
case 3:
ucp->uc_mcontext.gregs[REG_RCX] = va_arg (ap, greg_t);
break;
case 4:
ucp->uc_mcontext.gregs[REG_R8] = va_arg (ap, greg_t);
break;
case 5:
ucp->uc_mcontext.gregs[REG_R9] = va_arg (ap, greg_t);
break;
default:
/* Put value on stack. */
sp[i - 5] = va_arg (ap, greg_t);
break;
}
va_end (ap);
}
makecontext用于修改已經獲取的上下文信息,其支持將運行stack切換為用戶自定義棧,并可以修改ucontext上下文中保存的RIP指針,這樣當恢復此ucontext的上下文時,就會將RIP寄存器的恢復為ucontext中的RIP字段值,跳到指定的代碼處進行執行,這也是協程運行的基本要求。
makecontex的glibc實現中,
首先是對用戶自定義棧進行處理,將sp移動到棧底(棧空間是遞減的),然后進行對齊,并預留出8字節的trampoline空間(防止相互遞歸的發生)。
然后,將傳入的上下文ucontext中的rip字段設置為fun函數的地址,rbx字段指向繼承上下文,rsp字段指向自定義棧的棧頂
其次就是將start_context和uc_link,存入棧中
最后,是將makecontext的參數存入ucontext的上下文中,對于多余的參數,進行壓棧操作。
修改后的ucontext上下文如下:makecontext.png
makecontext支持后繼上下文的功能,即當前ucontext執行完畢后,會執行ucontext中設置的uc_link所指向的另一個ucontext,這個功能就是通過__start_context()來實現的,上面的圖中可知,makecontext()中將用戶自定義棧的棧頂push進了start_context,當makecontext()修改的上下文執行結束后,會將棧頂的start_context指針 pop到當RIP寄存器中,然后執行,下面是__start_context()的glibc匯編源碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22ENTRY(__start_context)
/* This removes the parameters passed to the function given to
'makecontext' from the stack. RBX contains the address
on the stack pointer for the next context. */
movq%rbx, %rsp
/* Don't use pop here so that stack is aligned to 16 bytes. */
movq(%rsp), %rdi/* This is the next context. */
testq%rdi, %rdi
je2f/* If it is zero exit. */
call__setcontext
/* If this returns (which can happen if the syscall fails) we'll
exit the program with the return error value (-1). */
movq%rax,%rdi
2:
callHIDDEN_JUMPTARGET(exit)
/* The 'exit' call should never return. In case it does cause
the process to terminate. */
hlt
END(__start_context)
該代碼首先就是將當前寄存器中的rbx值賦給rsp寄存器,我們知道rbx的值,是從ucontext的rbx字段中恢復出來的,其是指向棧頂的uc_link,所以,就是將當前的棧頂指針指向uc_link,即pop出了makecontext時,傳入的所有參數,然后會調用setcontext()來恢復后繼上下文的環境,參數rdi就是uc_link的值。整個流程如下圖:
startcontext.png
2.3. swapcontext實現1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83ENTRY(__swapcontext)
/* Save the preserved registers, the registers used for passing args,
and the return address. */
movq%rbx, oRBX(%rdi)
movq%rbp, oRBP(%rdi)
movq%r12, oR12(%rdi)
movq%r13, oR13(%rdi)
movq%r14, oR14(%rdi)
movq%r15, oR15(%rdi)
movq%rdi, oRDI(%rdi)
movq%rsi, oRSI(%rdi)
movq%rdx, oRDX(%rdi)
movq%rcx, oRCX(%rdi)
movq%r8, oR8(%rdi)
movq%r9, oR9(%rdi)
movq(%rsp), %rcx
movq%rcx, oRIP(%rdi)
leaq8(%rsp), %rcx/* Exclude the return address. */
movq%rcx, oRSP(%rdi)
/* We have separate floating-point register content memory on the
stack. We use the __fpregs_mem block in the context. Set the
links up correctly. */
leaqoFPREGSMEM(%rdi), %rcx
movq%rcx, oFPREGS(%rdi)
/* Save the floating-point environment. */
fnstenv(%rcx)
stmxcsr oMXCSR(%rdi)
/* The syscall destroys some registers, save them. */
movq%rsi, %r12
/* Save the current signal mask and install the new one with
rt_sigprocmask (SIG_BLOCK, newset, oldset,_NSIG/8). */
leaqoSIGMASK(%rdi), %rdx
leaqoSIGMASK(%rsi), %rsi
movl$SIG_SETMASK, %edi
movl$_NSIG8,%r10d
movl$__NR_rt_sigprocmask, %eax
syscall
cmpq$-4095, %rax/* Check %rax for error. */
jaeSYSCALL_ERROR_LABEL/* Jump to error handler if error. */
/* Restore destroyed registers. */
movq%r12, %rsi
/* Restore the floating-point context. Not the registers, only the
rest. */
movqoFPREGS(%rsi), %rcx
fldenv(%rcx)
ldmxcsr oMXCSR(%rsi)
/* Load the new stack pointer and the preserved registers. */
movqoRSP(%rsi), %rsp
movqoRBX(%rsi), %rbx
movqoRBP(%rsi), %rbp
movqoR12(%rsi), %r12
movqoR13(%rsi), %r13
movqoR14(%rsi), %r14
movqoR15(%rsi), %r15
/* The following ret should return to the address set with
getcontext. Therefore push the address on the stack. */
movqoRIP(%rsi), %rcx
pushq%rcx
/* Setup registers used for passing args. */
movqoRDI(%rsi), %rdi
movqoRDX(%rsi), %rdx
movqoRCX(%rsi), %rcx
movqoR8(%rsi), %r8
movqoR9(%rsi), %r9
/* Setup finally %rsi. */
movqoRSI(%rsi), %rsi
/* Clear rax to indicate success. */
xorl%eax, %eax
ret
PSEUDO_END(__swapcontext)
swapcontext就是在getcontext的基礎上,將參數二中上下文中保存的各個寄存器字段恢復到當前進程的各個寄存器中,和getcontext的流程相反。就不細說,有興趣的可以自己看。
3. ucontext示例
下面從最簡單的代碼來解析ucontext切換的過程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37#include
#include
#include
#include
ucontext_t uc, ucm;
void foo()
{
printf("%s\n", __FUNCTION__);
}
int main()
{
// allocate stack
size_t co_stack_size = 64*1024;
char * co_stack = (char *)malloc(co_stack_size);
memset(co_stack, 0, co_stack_size);
//get current context
getcontext(&uc);
// make ucontext to run foo
uc.uc_stack.ss_sp = co_stack;
uc.uc_stack.ss_size = co_stack_size;
uc.uc_link = &ucm;
makecontext(&uc, &foo, 0);
// switching back-and-forth for 100 times
for (int i = 0; i < 100; i++)
{
swapcontext(&ucm, &uc);
}
free(co_stack);
return 0;
}
getcontext調用時,進程上下文信息,以及ucontext的變化情況:
ucontext_eg_1.png
在調用getcontext之前,進程棧空間只有兩個值stack_size和p_stack,當執行getcontext后,會將下一調指令地址即RIP寄存器值push入棧, 然后進入getcontext中后,會將當前進程的上下文全部保存到傳入的ucontext參數中,以及上面說的進程的信號屏蔽掩碼。在getcontext操作中保存的兩個最重要的寄存器信息就是rip和rsp了,分別用于恢復上下后所要執行的指令地址和棧頂指針。
當執行makecontext后:當前進程上下文沒有任何變化,只是對傳入的ucontext上下文進行操作,變化如下:
ucontext_eg_2.png
當執行到swapcontext時,當前進程的棧空間會切換到uc自定義的棧空間,且會從uc上下文uc_mcontext字段中恢復出各個寄存器的值,
其中rbx用于uc執行完畢后,執行其后繼上下文;
rip中指向下一條要執行的指令,即函數foo()的地址
rsp指向uc自定義棧空間的棧頂;
foo函數執行完畢后,會將uc自定義棧的局部變量全部彈出,然后棧空間又恢復到剛進入foo()的狀態,此時會彈出棧頂的start_context()到RIP中,其做完函數執行完畢的下一條指令,下面start_context()的執行就是將自定義stack的uc_link所指向的上下文恢復執行。所以此時進程的上下文狀態就會回到swapcontext的時候的下一條語句繼續執行。
下面是執行的結果:1
2
3foo
foo
Segmentation fault (core dumped)
為什么會coredump呢,我是想他輸出100個foo的,那就gdb調試看看自定義棧出了什么問題:
gdb進入后在swapcontext之前打斷點,如下:自定義棧的起始地址為0x602010,
ucontext_gdb_1.png
自定義棧大小為:1size_t co_stack_size = 64*1024;
所以uc上下文的自定義棧底為0x612010,棧頂的位置在0x611ff8,在makecontext()后,自定義棧內部壓入了uc上下文的后繼上下文ucm和用于跳轉到后繼上下文的函數__start_context()地址。
當swapcontext執行后,進程切換到uc上下文執行,執行foo()函數,foo()執行結束之前,uc自定義棧的數據如下:
ucontext_gdb_2.png
可以看到foo()ret之前,自定義站棧頂指向0x611ff0,其內容是沒有swapcontext之前棧頂指針,這里不管它,因為foo()執行完之前會pop出該內容,然后棧頂指針就是指向0x611ff8,然后再執行ret指令。ret指令,會首先從棧頂pop出內容到RIP寄存器,進行下一條指令的執行:前面說了0x611ff8是__start_context()地址。
ucontext_gdb_3.png
進入__start_context的匯編代碼后,棧頂指針已經指向0x612000,如上圖,此時棧頂就是后繼上下文ucm的地址;然后將棧頂pop到rdi中,然后會通過調用setcontext,將ucm上限為恢復到進程的寄存器中,在call調用setcontext時,同時會壓入下一條指令的地址,就是0x7ffff7a5db6e,如下:
ucontext_gdb_4.png
這里我們就會發現一點:uc的自定義棧的數據已經完全被破壞,所以,當執行完后繼上下文ucm,然后在swapcontext()中,再次切換到foo()后,uc的自定義棧已經完全不是第一次使用的狀態,當再次進行后繼上下文執行時,core在了后繼上下文的回復過程中,因為此時的0x612000已經不在執行ucm,而是一個__start_context()指令地址。
uc上下文執行過程中,只有自定義棧會在運行時被修改,uc 的ucontext_t數據結構是不會發生改變的, 為了能夠讓該代碼達到預期:進行如下修改就好了。
1
2
3
4
5for (int i = 0; i < 100; i++)
{
swapcontext(&ucm, &uc);
makecontext(&uc, &foo, 0);
}
[參考]
總結
以上是生活随笔為你收集整理的linux ucontext 类型,协程:posix::ucontext用户级线程实现原理分析 | WalkerTalking的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ps4荒野大镖客2售价
- 下一篇: vivox27的灵感水冷散热是什么