Linux Cは純粋なユーザー状態のプリエンプト式マルチスレッドを実現します!

45231 ワード

温州の靴工場のボス、靴しかありません!
2010年の古い出来事:純ユーザー空間占有式マルチスレッドの設計:https://blog.csdn.net/dog250/article/details/5735512
純粋なユーザ空間のプリエンプトマルチスレッドライブラリは実は面倒なことです!
ええ、9年前のことです.当時は純粋なユーザ状態のマルチスレッドを作りたかったが,最終的には優雅な方法は見つからなかった.
メーデーの休み前の土曜日は休みで、家で子供を連れて、一つの方法を考えました.今夜は本文に記す.
純粋なユーザ状態のマルチスレッドの実装を探すには、それは多すぎますが、ほとんどが非プリエンプトです.いずれも何らかのコンセンサス形式であり,命令フロー自身がCPUを放棄し,実行権を他の実行フローに自発的に渡す必要がある.なぜ純粋なユーザ状態プリエンプトマルチスレッドを研究する人は少ないのでしょうか.原因は次の通りだと思います.
  • このようなマルチスレッドは、Solarisのcall upメカニズムのようなオペレーティングシステムカーネル自体が提供するメカニズムに大きく依存し、各システムが必ずしも同じ類似のメカニズムを提供するとは限らず、必ずしも提供するとは限らない.
  • 現在のスレッドライブラリは非常に多く、Linuxもとっくに純粋なカーネルマルチスレッドを実現しており、このユーザ状態のマルチスレッドを実現することは意味がない.
  • 純粋なユーザ状態スレッドが実現されても、各スレッドはオペレーティングシステムに制約されます.例えば、1つのスレッドがオペレーティングシステムのカーネルでブロックされている場合、他のスレッドもそれに従う必要があります.
  • 一定の正規化された性能評価システムがなく、性能オーバーヘッドを測定するのは難しい.これはOSカーネルベースのスレッドとは異なり、後者は厳格なソフト・ハードウェア・インタフェースprofileを有するためである.
  • この事をするのは仕事に役立たないで、ただ自分で游ぶだけに适して、しかし谁が大丈夫にこれを游ぶことができますか?

  • 私も知っています.このことをするのは意味がありませんが、何が意味ですか.
    直接に言えば、このことをやり終えて、少なくともLinuxカーネルの信号処理に対する理解が深まったのではないでしょうか.あるいは1万歩下がって、少なくとも、このことの練習を通じて、sigcontextとsigframeの構造体を理解しましたね.
    まだ意味がないと思う?じゃ、スプレーしてください.
    プリエンプトマルチスレッドスケジューリングとは,スレッド自体に依存してCPUを放棄して実行権を別のスレッドに渡すのではなく,外部アクティブ干渉モードのスケジューリングメカニズムにより,スケジューリングが必要な時点で現在のスレッドの実行権を強引に剥奪し,ポリシーに基づいて別のスレッドを選択して実行することである.
    優雅な案が見つからなかったのは、2つのことを同時にできる場所が見つからなかったからです.
  • 現在のスレッドの実行を中断し、スケジューリングポリシーに従ってスケジューリングおよび切り替えを実施するためにhandlerに進む.
  • このhandlerでプロセスのレジスタコンテキストを変更し、現在のスレッドの実行権を他のスレッドに渡す.

  • まず、上記第1の点は、信号で行うことができ、例えばalarm関数で、時間分割中断を実現することができる.しかし,割り込み処理関数ではレジスタを修正する方法が見つからず,setjmp/longjmpを用いることを考えたが失敗し,最終的にPTRACE機構を用いて極めて醜く粗いことを実現した.
    9年前の文章の中で、冒頭で純ユーザー空間のプリエンプトマルチスレッドライブラリは実は面倒なことだと言いました!確かに面倒だと思うのは、上の難題が解決していないからだ.
    当時は確かに術業が不精だったのだろう.その後数年、Linuxカーネル信号処理に関するものをあまり見たことがありません.
    土曜日はちょうどちょうど乳を飲んで寝てしまった後、私は一人で波に出られなくて、突然またこの問題を思い出しました.私は優雅な案を探すことを誓った.結局9年が過ぎたので、自分の内功はその時よりずっと強いと思います.
    確かに、この案も本当に手当たり次第だ.
    Linuxプロセスは、ストリームがユーザ状態に戻る前に信号を処理するときに、信号処理関数を呼び出すことを知っています.この信号処理関数はユーザ状態に定義されているので、Linuxプロセスはこのhandler関数を実行するために、自分でユーザ状態スタックをsetupする必要があります.
    このメカニズムは、レジスタを修正する機会を与えてくれた.
    9年前、ユーザ状態のレジスタコンテキストは、ユーザ状態が完全に戻る前にカーネルスタックに保存され、変更できないと思っていました.しかし、実際には、信号処理関数を実行すると、カーネルはプロセスカーネルスタック上のレジスタコンテキストsigcontextをユーザ状態のスタックにコピーし、sigreturnシステム呼び出しをリターンアドレスとして押し込み、信号処理関数が完了するとsigreturnは自動的にカーネルに陥り、ユーザ状態のsigcontextをカーネルスタックにコピーし、信号処理を徹底的に完了し、プロセスのレジスタコンテキストを復元します.
    すなわち、信号処理関数が実行されると、現在のスタック上でレジスタコンテキストを見つけることができ、スタック上でsigcontext構造体を探すだけでよい.この場合、私たちはそれを修正し、これらの修正されたレジスタコンテキストは、信号処理が完了してカーネルに戻ると、カーネルスタック上のレジスタコンテキストを更新し、私たちの目的を達成します.
    では、まず信号処理関数を書いて、信号処理関数が実行されるとき、スタックに何があるかを見てみましょう.
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int i, j, k = 0;
    unsigned char *stack_buffer;
    unsigned long *p;
    
    void sig_start(int signo)
    {
    	unsigned long a = 0x1234567811223344;
    
    	p = (unsigned char *)&a;
    	stack_buffer = (unsigned char *)&a;
    
    	//     8     ,       
    	printf("----begin stack----
    "
    ); for (i = 0; i < 32; i++) { for (j = 0; j < 8; j++) { printf(" %.2x", stack_buffer[k]); k++; } printf("
    "
    ); } printf("----end stack----
    "
    ); if (signo = SIGINT) signal(SIGINT, NULL); if (signo = SIGHUP) signal(SIGHUP, NULL); return; } int main() { printf("process id is %d %p %p
    "
    ,getpid(), main, wait_start); signal(SIGINT, sig_start); signal(SIGHUP, sig_start); for (;;); }

    これを実行して、Ctrl-Cを押してSIGINT信号を与え、印刷されたスタックの内容を見てみましょう.
    [root@localhost ~]# ./a.out
    process id is 3036  0x4007a1 0x40068d
    ^C----begin stack----
     44 33 22 11 78 56 34 12 #            a
     98 7b 98 fa f8 7f 00 00
     10 11 92 b5 fc 7f 00 00
     80 02 3d fa f8 7f 00 00 #                ,  sigreturn 
     01 00 00 00 00 00 00 00 #      ,       rt_sigframe    
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     02 00 00 00 01 00 00 00
     00 00 00 00 00 00 00 00
     60 10 92 b5 fc 7f 00 00
     10 0f 92 b5 fc 7f 00 00
     08 00 00 00 00 00 00 00
     06 02 00 00 00 00 00 00
     a0 05 40 00 00 00 00 00
     f0 11 92 b5 fc 7f 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     01 00 00 00 00 00 00 00
     70 0e 92 b5 fc 7f 00 00 #               ,0x7ffc...
     10 11 92 b5 fc 7f 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     8d 03 3d fa f8 7f 00 00
     10 11 92 b5 fc 7f 00 00
     e3 07 40 00 00 00 00 00
     02 02 00 00 00 00 00 00
     33 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
     00 00 00 00 00 00 00 00
    ----end stack----
    ^C
    [root@localhost ~]#
    

    私はx 86_64プラットフォームで行われた実験なので、x 86を見てみましょう.64のrt_sigframe構造体、arch/x 86/include/asm/sigframeに位置する.h:
    #ifdef CONFIG_X86_64
    
    struct rt_sigframe {
        char __user *pretcode;
        struct ucontext uc;
        struct siginfo info;
        /* fp state follows here */
    };
    ...
    /*     ,  rt_sigframe       */
    // include/uapi/asm-generic/ucontext.h
    struct ucontext {
        unsigned long     uc_flags;
        struct ucontext  *uc_link;
        stack_t       uc_stack;
        struct sigcontext uc_mcontext;  //            !
        sigset_t      uc_sigmask;   /* mask last for extensibility */
    };
    

    オフセット位置を計算すると、ちょうどpretcodeフィールドの58バイトにあります.すなわち、信号処理関数のpretcodeオフセットを見つけさえすれば、58=40バイトを加えるとsigcontext構造体になり、この構造体にはすべてレジスタがあります.
    struct sigcontext {
        __u64 r8;
        __u64 r9;
        __u64 r10;
        __u64 r11;
        __u64 r12;
        __u64 r13;
        __u64 r14;
        __u64 r15;
        __u64 rdi;
        __u64 rsi;
        __u64 rbp;
        __u64 rbx;
        __u64 rdx;
        __u64 rax;
        __u64 rcx;
        __u64 rsp;
        __u64 rip;
        __u64 eflags;       /* RFLAGS */
        __u16 cs;
        __u16 gs;
        __u16 fs;
        __u16 __pad0;
        __u64 err;
        __u64 trapno;
        __u64 oldmask;
        __u64 cr2;
        struct _fpstate *fpstate;   /* zero when no FPU context */
    #ifdef __ILP32__
        __u32 __fpstate_pad;
    #endif
        __u64 reserved1[8];
    };
    

    我々のいわゆる純粋なユーザ状態スレッドスケジューリングは,信号処理関数においてsave/restoreが上述した構造体であればよいが,上述した構造体の位置は,どこにあるか知っている.
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    //      demo,  4096   stack   。
    #define STACK_SIZE		4096
    /* 
     *     72?
     *                    ,  pretcode     32   。
     *   32+40=72!
     */
    #define CONTEXT_OFFSET	72
    // rip          a   。  rip sigcontext     16
    #define PC_OFFSET		200
    
    int wait_start()
    {
    	for (;;) {
    		sleep(1000);
    	}
    }
    
    //   1     
    void thread1()
    {
    	int a = 1, ret = 0;
    	char buf[64];
    	int fd = open("./file", O_RDWR);
    	for (;;) {
    		//   1           。
    		snprintf(buf, 32, "user thread 1 stack: %p  value:%d
    "
    , &a, a++); ret = write(fd, buf, 32); printf("write buffer to file:%s size=%d
    "
    , buf, ret); sleep(1); } } // 2 void thread2() { int a = 2; for (;;) { // 2 。 printf("tcp user cong 2 stack: %p value:%d
    "
    , &a, a++); sleep(1); } } unsigned char *buf; int start = 0; struct sigcontext context[2]; struct sigcontext *curr_con; unsigned long pc[2]; int idx = 0; unsigned char *stack1, *stack2; // SIGINT , 。 void sig_start(int dunno) { unsigned long a = 0, *p; if (start == 0) { // // sigcontext rip, rip , thread1 p = (unsigned long*)((unsigned char *)&a + PC_OFFSET); *p = pc[0]; // sigcontext p = (unsigned long *)((unsigned char *)&a + CONTEXT_OFFSET); curr_con = (struct sigcontext *)p; // 。 curr_con->rsp = curr_con->rbp = (unsigned long)((unsigned char *)stack1 + STACK_SIZE); start++; } else if (start == 1) { // // 1 sigcontext, , schedule 2 。 p = (unsigned long *)((unsigned char *)&a + CONTEXT_OFFSET); curr_con = (struct sigcontext *)p; memcpy((void *)&context[0], (const void *)curr_con, sizeof(struct sigcontext)); // sigcontext rip , 1 p = (unsigned long *)((char*)&a + PC_OFFSET); idx = 1; *p = pc[1]; p = (unsigned long *)((unsigned char *)&a + CONTEXT_OFFSET); curr_con = (struct sigcontext *)p; // 。 curr_con->rsp = curr_con->rbp = (unsigned long)((unsigned char *)stack2 + STACK_SIZE); start++; // , 。 alarm(2); signal(SIGINT, NULL); } return; } void sig_schedule(int unused) { unsigned long a = 0; unsigned char *p; // p = (unsigned char *)((unsigned char *)&a + CONTEXT_OFFSET); curr_con = (struct sigcontext *)p; memcpy((void *)&context[idx%2], curr_con, sizeof(struct sigcontext)); // , 。 idx++; memcpy(curr_con, (void *)&context[idx%2], sizeof(struct sigcontext)); // 2 alarm(2); return; } int main() { printf("process id is %d %p %p
    "
    ,getpid(), thread1, thread2); // stack 。 // , stack , 。 stack1 = (unsigned char *)calloc(1, 4096); stack2 = (unsigned char *)calloc(1, 4096); signal(SIGINT, sig_start); signal(SIGALRM, sig_schedule); pc[0] = (unsigned long)thread1; pc[1] = (unsigned long)thread2; wait_start(); }

    効果は次のとおりです.
    [root@localhost ~]# ./a.out
    process id is 2994  0x4007cd 0x400869
    0x1191010 0x1192020
    ^Cwrite buffer to file:user thread 1 stack: 0x1191ffc  value:1
      size=32
    ^Ctcp user cong 2 stack: 0x1193014  value:2
    tcp user cong 2 stack: 0x1193014  value:3
    write buffer to file:user thread 1 stack: 0x1191ffc  value:2
      size=32
    write buffer to file:user thread 1 stack: 0x1191ffc  value:3
      size=32
    tcp user cong 2 stack: 0x1193014  value:4
    tcp user cong 2 stack: 0x1193014  value:5
    write buffer to file:user thread 1 stack: 0x1191ffc  value:4
      size=32
    write buffer to file:user thread 1 stack: 0x1191ffc  value:5
      size=32
    tcp user cong 2 stack: 0x1193014  value:6
    ^C
    

    2つのスレッドが完全に交互に実行されていることがわかります.
    最初の例は少し複雑ですが、簡単なものに変えましょう.
    void thread1()
    {
        int i = 1;
        while (1) {
            printf("I am thread:%d
    "
    , i); sleep(1); } } void thread2() { int i = 2; while (1) { printf("I am thread:%d
    "
    , i); sleep(1); } }

    効果は次のとおりです.
    [root@localhost ~]# ./a.out
    process id is 3085  0x4006fd 0x40072c
    0x11f3010 0x11f4020
    ^CI am thread:1 # Ctrl-C     1
    ^CI am thread:2 # Ctrl-C     2
    I am thread:2
    I am thread:1
    I am thread:1
    I am thread:2
    I am thread:2
    I am thread:1
    I am thread:1
    I am thread:2
    I am thread:2
    I am thread:1
    I am thread:1
    I am thread:2
    I am thread:2
    I am thread:1
    I am thread:1
    ^C
    

    この例はずっとはっきりしている.
    以上の純ユーザ状態マルチスレッド設計では,オペレーティングシステムプロセスレベル以外のデータ構造を用いてスレッドコンテキストを格納していないが,これが純ユーザ状態の意味である.すべてのスレッドコンテキストおよびスレッドスケジューリングに関連するデータ構造が個別のプロセスアドレス空間に格納されていることを示した.
    言い換えれば、単独のプロセスの外で、このマルチスレッドの存在を意識している人はいません!このユーザ状態のマルチスレッドを収容するプロセスコンテナは、LinuxカーネルがLinuxプロセスで行ったように、スレッドハードウェアコンテキストのsave/restoreを完了した仮想マシンインスタンスです.
    私は、私のこの純粋なユーザ状態プリエンプトマルチスレッドは、信号処理メカニズムを使用していると言った.
    「純」というユーザー状態ではないかと聞かれることがあります.何で信号を使うの?信号はカーネルメカニズムではありませんか?
    はい、信号はカーネルメカニズムですが、ここでの注目点は、信号がカーネルメカニズムであるかどうかではなく、「プリエンプトスケジューリングを実施するために第三者が必要だ」ということです.なぜ「サードパーティ」が必要なのでしょうか.
    プリエンプトマルチスレッドはコラボレーションマルチスレッドではないため、スレッド自身がスケジューリング決定に参加しない以上、サードパーティが決定する必要があります.信号を使用するのはただ1つの方法で、私はLinuxシステムでこのユーザー状態のマルチスレッドをするため、信号はちょうど需要を満たすことができます.もちろん、信号を使わなくてもいいです.等価なメカニズムを見つけることができればいいです.
    【注意⚠️: 信号メカニズムを用いてプリエンプトするコストは確かに少し大きいが、これは実現可能な方法にすぎず、唯一の方法ではない.また、このようなプリエンプトスケジューリングは完全にコラボレーションスケジューリング、例えばコラボレーションスケジューリングと協働して動作することができ、何らかのプリエンプトをしなければならないことが発生した後にのみ、信号プリエンプトを実施することができる.
    OSカーネルスケジューリングもサードパーティのクロック中断に依存しているのではないでしょうか.クロック結晶振動はコアの一部ではなく、ハードウェアである.
    この時4月29日午前2時13分、パソコンを閉じて、あと数時間寝るかもしれません.
    浙江温州の靴は湿っていて、雨が降って水に入って太らない!