執行緒切換函式schedule的實現
版權宣告:本文為博主原創,無版權,未經博主允許可以隨意轉載,無需註明出處,隨意修改或保持可作為原創!https://blog.csdn.net/dog250/article/details/89790086
繼續看昨晚的那個setjmp/longjmp實現的 “使用者態協作式多執行緒” (我還是不用 協程 這個詞了,這個詞太有文化,以至於會被皮鞋老闆認為我是在褻瀆協程)的demo:
// 基於標準的setjmp/longjmp實現! // 然而我不知道如何才能直接用 PTR_MANGLE 這個巨集,所以我使用自己實現內聯彙編版本! #include <stdio.h> #include <stdlib.h> #include <string.h> #include <setjmp.h> unsigned char *stack1, *stack2; struct task *tsk1, *tsk2; struct task { jmp_buf ctx; unsigned char *stack; unsigned int seq; char name[32]; }; unsigned long PTR_MANGLE(unsigned long var) { asm ("movq %1, %%rdx \n" "xor%%fs:0x30, %%rdx\n" "rol$0x11,%%rdx\n" "movq %%rdx, %0\t\n" : "=r" (var) :"0" (var)); return var; } unsigned long PTR_DEMANGLE(unsigned long var) { asm ("ror $0x11, %0\n" "xor %%fs:0x30, %0" : "=r" (var) : "0" (var)); return var; } void post_schedule(struct task *tsk) { printf("Previous task name:%s [sequence:%d]\n", tsk->name, tsk->seq); } void schedule(struct task *prev, struct task *next) { int ret; printf("Before task:%s switching to task:%s! current task sequence:%d\n", prev->name, next->name, prev->seq); ret = setjmp(prev->ctx); if (ret == 0) { longjmp(next->ctx, 2); } post_schedule(prev); } void func1() { int i = 1; while (i++) { printf("thread 1 :%d\n", i); sleep(1); if (i%3 == 0) { tsk1->seq = i; schedule(tsk1, tsk2); } } } void func2() { int i = 0xffff; while (i--) { printf("thread 2 :%d\n", i); sleep(1); if (i%3 == 0) { tsk2->seq = i; schedule(tsk2, tsk1); } } } #define JB_RBP1 #define JB_RSP6 #define JB_PC7 int main(int unused1, char **unused2) { int i, j; unsigned long *prip1, *prip2; unsigned long *pst1, *pst2, *pbp1, *pbp2; tsk1 = (struct task *)calloc(sizeof(struct task), 1); tsk2 = (struct task *)calloc(sizeof(struct task), 1); stack1 = (unsigned char *)malloc(4096); stack2 = (unsigned char *)malloc(4096); tsk1->stack = stack1 + 4000; tsk2->stack = stack2 + 4000; strncpy(&tsk1->name[0], "task1", 5); strncpy(&tsk2->name[0], "task2", 5); tsk1->seq = 0; tsk2->seq = 0; memset(&tsk1->ctx, 0, sizeof(jmp_buf)); memset(&tsk2->ctx, 0, sizeof(jmp_buf)); i =setjmp(tsk1->ctx); j =setjmp(tsk2->ctx); prip1 = ((unsigned long *)&(tsk1->ctx)) + JB_PC; prip2 = ((unsigned long *)&(tsk2->ctx)) + JB_PC; pst1 = ((unsigned long *)&(tsk1->ctx)) + JB_RSP; pst2 = ((unsigned long *)&(tsk2->ctx)) + JB_RSP; pbp1 = ((unsigned long *)&(tsk1->ctx)) + JB_RBP; pbp2 = ((unsigned long *)&(tsk2->ctx)) + JB_RBP; // 加密需要保護的指標值。 *prip1 = PTR_MANGLE(func1); *pst1 = *pbp1 = PTR_MANGLE(stack1+4000); *prip2 = PTR_MANGLE(func2); *pst2 = *pbp2 = PTR_MANGLE(stack2+4000); longjmp(tsk1->ctx, 2); }
關注一下 schedule 函式:
void post_schedule(struct task *tsk) { printf("Previous task name:%s [sequence:%d]\n", tsk->name, tsk->seq); } void schedule(struct task *prev, struct task *next) { int ret; printf("Before task:%s switching to task:%s! current task sequence:%d\n", prev->name, next->name, prev->seq); ret = setjmp(prev->ctx); if (ret == 0) { longjmp(next->ctx, 2); } post_schedule(prev); }
如果按照程式碼所述,當task1切換到task2的時候,需要這麼如下呼叫:
schedule(tsk1, tsk2);
按照 C語言常規的邏輯 順序執行程式碼,理所當然應該列印如下:
Before task:task1 switching to task:task2! current task sequence:6 Previous task name:task1 [sequence:6]
然而,事實卻是:
Before task:task1 switching to task:task2! current task sequence:6 Previous task name:task2 [sequence:65532]
問題出現了!
我在schedule函式的開頭和該函式的最後都是在引用 prev指標的欄位 為什麼結果看起來是不對的?
為什麼呢?C語言明明就是順序執行語句的呀!C語言同一個指標為什麼會被改變?如果在Before和After列印的位置列印prev的地址的話,你會發現地址都不一樣了!
其實答案很容易想到,答案就是 prev指標不是同一個prev指標!
如果你沒有看過Linux核心的switch_to巨集,沒有糾結過為什麼該巨集擁有三個引數,那麼以上的答案可能你馬上就能想到,然而,具有諷刺效果的是,看過了switch_to的分析,反而更加懵了,因為這些分析都太複雜了,事情根本就沒有那麼複雜!
原因就是 暫存器上下文在longjmp中全部切換了,然而C語言看不到這種切換! 換句話說, 組合語言可以對C語言實施降維打擊! (通過高維空間突然出現或者消失在低維空間!)
C語言抽象了程式的 業務邏輯 ,隱藏了底層的各種細節,C語言程式設計根本不需要理解什麼暫存器上下文,更不需要知道esp,rsp,r13,eax這些是幹什麼的。
所以說,如果說一段C程式碼中間插一段內聯彙編,那麼這段內聯彙編前後的C語句並不能保證是一定可以在邏輯層面上銜接的。
為了顯示在Before和After位置的列印處,暫存器上下文已經被切換,我們來看看堆疊的情況。
我們知道,區域性變數是在堆疊中儲存的,那麼我們在schedule函式的Before,After處分別列印一下區域性變數ret的地址值,來證明堆疊其實已經不是同一個了:
void schedule(struct task *prev, struct task *next) { int ret; printf("Before switch:[ret is at:%p]\n", &ret); ret = setjmp(prev->ctx); if (ret == 0) { longjmp(next->ctx, 2); } printf("After switch:[ret is at:%p]\n", &ret); }
結果如下:
Before switch:[ret is at:0x952184] After switch:[ret is at:0x953194]
那麼,如果在switch之後,我依然需要在暫存器上下文切換之前的prev指標怎麼辦呢?畢竟可能需要執行一些post事務之類的。
辦法就是用通用暫存器把prev指標給傳遞過去。既然要應對組合語言指令的降維打擊,當然需要組合語言操作C語言不可見的暫存器本身了。
哦,對了,這就是 為什麼Linux核心的switch_to需要3個引數的原因!
知道了要用內聯彙編,但是到底應該怎麼實現呢?也簡單!我們只需要看看setjmp/longjmp沒有touch到哪些暫存器,然後借用一下即可。這個還算清晰,我們從glibc裡就能看出:
https://code.woboq.org/userspace/glibc/sysdeps/x86_64/jmpbuf-offsets.h.html#define JB_RBX0 #define JB_RBP1 #define JB_R122 #define JB_R133 #define JB_R144 #define JB_R155 #define JB_RSP6 #define JB_PC7 #define JB_SIZE (8*8)
嗯,我們發現RCX沒有用到,那就用RCX唄,程式碼如下:
void schedule(struct task *prev, struct task *next) { int ret; printf("Before task:%s switching to task:%s! current task sequence:%d\n", prev->name, next->name, prev->seq); ret = setjmp(prev->ctx); if (ret == 0) { // 切換前,先將變數prev儲存在rcx中。 asm ("movq %0, %%rcx\n" : : "m" (prev)); longjmp(next->ctx, 2); } else { // 切換後,從rcx裡恢復到prev變數中。 asm ("movq %%rcx, %0\n" : "=c" (prev) :); } post_schedule(prev); }
看效果:
... Before task:task2 switching to task:task1! current task sequence:65532 Previous task name:task2 [sequence:65532] thread 1 :4 thread 1 :5 thread 1 :6 Before task:task1 switching to task:task2! current task sequence:6 Previous task name:task1 [sequence:6] thread 2 :65531 thread 2 :65530 thread 2 :65529 Before task:task2 switching to task:task1! current task sequence:65529 Previous task name:task2 [sequence:65529] ...
就是這個意思。
網上能搜到一大堆關於Linux核心task切換時switch_to巨集的文章,大致說的都是這個意思,不然一個巨集也沒啥好分析的,想學內聯彙編完全有更好的資源,根本沒有必要去分析什麼switch_to巨集。不是很多人覺得Linux程序排程相當高大上嗎?嗯,那是排程,那不是切換, 排程和切換不是一回事! 排程是判斷 讓誰執行 ,切換是 如何讓它執行。 想研究排程的,研究CFS就好。
本文描述的其實是一個相關語言層次的普遍現象:
-
低階語言的邏輯對高階語言不可見,高階語言沒有任何手段獲取這些邏輯。
比如暫存器這種,C/C++都不能操作,Python和PHP試試看? -
高階語言的特性對低階語言不可見,然而低階語言可以通過一些手段獲取。
比如類這個概念在彙編就不存在。
上面第一點很有意思,它會帶來本文描述的這種奇怪的結果,最終不得不借助於低階語言去搞定。在Java中也存在這種輔助的方案,比如Java自省到JVM,比如JNI等等。
說什麼XX是最好的語言,說什麼業務邏輯比底層技術重要,你要是隻學一個什麼高階語言,中介軟體,即便你精通業務邏輯怎麼實現,致力於需求分析和滿足需求,我敢說你連CPU怎麼工作的都不知道。
當然了,就算不穿西裝,骨子裡的西裝人士,也是不care什麼技術的,完全不在一個頻道的。我自己當然是關注底層技術和不關注業務邏輯的人,這點我還是敢於承認的。
浙江溫州皮鞋溼,下雨進水不會胖!