1. 程式人生 > >使用ucontext 來手動切換上下文

使用ucontext 來手動切換上下文

簡介

使用ucontext.h裡面的函式和資料結構,可以讓使用者實現手動來切換函式的執行流,**libtask-輕量級協程庫**就是使用ucontext.h來實現協程的。

主要的資料結構

主要的資料結構是:ucontext_t:
typedef struct ucontext {
    unsigned
long int uc_flags; struct ucontext *uc_link; stack_t uc_stack; mcontext_t uc_mcontext; __sigset_t uc_sigmask; struct _libc_fpstate __fpregs_mem; } ucontext_t;

其中有幾個成員是我們需要注意的:

成員 說明
uc_link 這個成員指向了當前函式返回之後繼續執行的上下文
uc_sigmask 這個成員是跟訊號相關的
uc_stack 這個成員是當前上下文所使用的棧
uc_mcontext 這個成員包括了一些暫存器變數

Warning:如果uc_link指標為NULL,那麼當該上下文結束之後,執行該上下文的執行緒會退出

我們主要會使用下面4個函式來對上下文進行操作:

成員 說明
getcontext(ucontext_t *ucp) 獲取當前執行的上下文
setcontext(const ucontext_t *ucp) 恢復ucp指向的上下文
makecontext(ucontext_t *ucp, void(*func)(void), int argc, …) 設定該上下文到函式func,並且設定上下文開始執行時的引數和引數個數
swapcontext(ucontext_t *oucp, const ucontext_t *ucp) 切換到ucp上下文執行,並且把當前上下文儲存在oucp裡面

所以一般我們的呼叫順序是:
1.getcontext:先得到當前的上下文。
2.makecontext:設定該上下文到函式func。
3.setcontext or swapcontext:切換到指定的上下文開始執行。

測試程式碼

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <ucontext.h>
#include <string.h>
#include <errno.h>

static ucontext_t mainContext, sumContext, multiContext;

#define STACKSIZE 1 << 13

int add(int a, int b) {
    printf("Calling the add function.\n");
    int n;
    if ((n = swapcontext(&sumContext, &multiContext)) < 0)  {
        perror("The Swap Context Error");
    }
    printf("The add result is : %d.\n", a + b);
    return a + b;
}

int mul(int a, int b) {
    printf("Calling the mul function.\n");
    int n;
    if ((n = swapcontext(&multiContext, &sumContext)) < 0)  {
        perror("The Swap Context Error");
    }
    printf("The multi result is : %d.\n", a * b);
    return a * b;
}

int main(int argc, char **argv) {
    int n;
    char firstStack[STACKSIZE];
    char secondStack[STACKSIZE];
    memset(firstStack, 0, STACKSIZE);
    memset(secondStack, 0, STACKSIZE);
    if ((n = getcontext(&sumContext)) < 0) {
        perror("Get Context Error");
        return -1;
    }
    sumContext.uc_stack.ss_sp = firstStack;
    sumContext.uc_stack.ss_size = STACKSIZE;
    sumContext.uc_link = &mainContext;
    if ((n = getcontext(&multiContext)) < 0) {
        perror("Get Context Error");
        return -1;
    }
    multiContext.uc_stack.ss_sp = secondStack;
    multiContext.uc_stack.ss_size = STACKSIZE;
    multiContext.uc_link = &sumContext;

    makecontext(&sumContext, (void(*)())add, 2, 4, 5);
    makecontext(&multiContext, (void(*)())mul, 2, 3, 4);
    if ((n = swapcontext(&mainContext, &multiContext)) < 0) {
        perror("The Swap Context Error");
    }
    return 0;
}

上面執行完之後的結果是:
這裡寫圖片描述
上面的程式碼會如下執行:
1.主函式先執行,並且設定sumContext, multiContext兩個上下文的棧和上下文的開始執行位置。
2.暫停主函式的執行,把主函式的上下文儲存在mainContext裡面,然後切換到addContext去執行,因為addContext的開始執行點為mul函式,所以會從mul函式的第一條指令開始往下執行。
3.mul函式裡面又會進行切換,切換到sumContext上下文開始執行,並且儲存當前執行的上下文到multiContext裡面。
4.在add函式裡面再次進行切換,切換到multiContext上下文,因為在第三步的時候,已經把相關的上下文儲存在了multiContext裡面,所以切換的時候會繼續mul函式中斷之後的下一條指令開始執行。
5.multiContext返回,multiContext的uc_link欄位指向的是sumContext,所以sumContext繼續執行。
6.sumContext返回,sumContext的uc_link欄位指向的是主函式的Context,所以主函式繼續執行。
7.主函式return。

相關連結

在libtask庫裡面,就是使用了ucontext裡面的函式和結構體來對作業系統的任務進行再一次的抽象,抽象成為routine,並且自己實現了一個排程器來對routine進行排程。
具體的程式碼可以在:傳送門下載。
具體的實現檔案為:task.h task.c taskimpl.h fd.c