簡體   English   中英

如何調度/創建用戶級線程,以及如何創建內核級線程?

[英]How are user-level threads scheduled/created, and how are kernel level threads created?

如果這個問題很愚蠢,請道歉。 我試圖在網上找到答案很長一段時間,但不能,因此我在這里問。 我正在學習線程,我一直在瀏覽這個鏈接這個關於內核級別和用戶級線程的Linux Plumbers Conference 2013視頻 ,據我所知,使用pthreads在用戶空間創建線程,內核不知道關於這一點,並將其視為一個單獨的進程,不知道內部有多少線程。 在這種情況下,

  • 誰決定在進程的時間片期間調度這些用戶線程,因為內核將其視為單個進程並且不知道線程,以及如何完成調度?
  • 如果pthreads創建用戶級線程,如果需要,如何從用戶空間程序創建內核級或OS線程?
  • 根據上面的鏈接,它說操作系統內核提供系統調用來創建和管理線程。 那么clone()系統調用是否會創建內核級線程或用戶級線程?
    • 如果它創建了一個內核級線程,那么一個簡單的pthreads程序的 strace也會在執行時顯示使用clone(),但是為什么它會被認為是用戶級線程呢?
    • 如果它沒有創建內核級線程,那么如何從用戶空間程序創建內核線程?
  • 根據鏈接,它說“它需要每個線程的完整線程控制塊(TCB)來維護有關線程的信息。因此會產生大量開銷並增加內核復雜性。”所以在內核級線程中,只有堆是共享的,其余的都是線程的個體?

編輯:

我問的是用戶級線程創建和它的調度,因為這里有一個對多對一模型的引用,其中許多用戶級線程被映射到一個內核級線程,並且線程管理在用戶空間中由線程庫。 我一直只看到使用pthreads的引用,但不確定它是否創建了用戶級或內核級線程。

這是前面的評論開頭。

您正在閱讀的文檔是通用的[不是特定於Linux的]並且有點過時。 而且,更重要的是,它使用了不同的術語。 也就是說,我相信,混亂的根源。 所以,請繼續閱讀......

它所謂的“用戶級”線程就是我所謂的[過時的] LWP線程。 它所謂的“內核級”線程就是linux中的本機線程。 在linux下,所謂的“內核”線程完全是另一回事[見下文]。

使用pthreads在用戶空間中創建線程,並且內核不知道這一點並僅將其視為單個進程,而不知道內部有多少線程。

這是用戶空間線程如何前完成NPTL (本地POSIX線程庫)。 這也是SunOS / Solaris稱為LWP輕量級進程的原因。

有一個進程多路復用並創建了線程。 IIRC,它被稱為線程主進程[或某些此類]。 內核沒有意識到這一點。 內核還沒有理解或提供對線程的支持。

但是,因為這些“輕量級”線程是由基於用戶空間的線程主機(又稱“輕量級進程調度程序”)[只是一個特殊的用戶程序/進程]中的代碼切換的,所以它們切換上下文非常慢。

此外,在“本機”線程出現之前,您可能有10個進程。 每個進程獲得10%的CPU。 如果其中一個進程是具有10個線程的LWP,則這些線程必須共享該10%,因此每個進程只有1%的CPU。

所有這一切都是由“原生”線程內核的調度知道的更換。 這種轉變是在10 - 15年前完成的。

現在,通過上面的例子,我們有20個線程/進程,每個進程獲得5%的CPU。 並且,上下文切換更快。

仍然可以在本機線程下擁有LWP系統,但是,現在,這是一種設計選擇,而不是必需品。

此外,如果每個線程“合作”,LWP工作得很好。 也就是說,每個線程循環周期性對“上下文切換”函數進行顯式調用。 自願放棄進程槽,以便另一個LWP可以運行。

然而, glibc的pre-NPTL實現也必須[強制]搶占LWP線程(即實現時間片)。 我不記得使用的確切機制,但是,這是一個例子。 線程主機必須設置警報,進入休眠狀態,喚醒然后向活動線程發送信號。 信號處理程序將影響上下文切換。 這是混亂,丑陋,有點不可靠。

Joachim提到pthread_create函數創建了一個內核線程

這在技術上是不正確的, 稱之為 內核線程。 pthread_create創建一個本機線程。 這在用戶空間中運行,並在與進程平等的基礎上爭奪時間片。 一旦創建,線程和進程之間幾乎沒有區別。

主要區別在於進程有自己唯一的地址空間。 但是,線程是與其他進程/線程共享其地址空間的進程,這些進程/線程是同一線程組的一部分。

如果它沒有創建內核級線程,那么如何從用戶空間程序創建內核線程?

內核線程不是用戶空間線程,NPTL,本機或其他。 它們由內核通過kernel_thread函數創建。 它們作為內核的一部分運行,並且與任何用戶空間程序/進程/線程相關聯。 他們可以完全訪問機器。 設備,MMU等。內核線程以最高權限級別運行:ring 0.它們也在內核的地址空間中運行,而不是在任何用戶進程/線程的地址空間中運行。

用戶空間程序/進程可能不會創建內核線程。 請記住,它使用pthread_create創建一個本機線程,它會調用clone系統調用來執行此操作。

即使對於內核,線程也很有用。 因此,它在各種線程中運行它的一些代碼。 你可以通過ps ax來看到這些線程。 看,你會看到kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration等。這些是內核線程,而不是程序/進程。


更新:

您提到內核不了解用戶線程。

請記住,如上所述,有兩個“時代”。

(1)在內核獲得線程支持之前(大約2004年?)。 這使用了線程主機(在這里,我將稱之為LWP調度程序)。 內核只有fork系統調用。

(2)后,其確實了解所有線程的內核。 沒有線程主,但是,我們有pthreadsclone系統調用。 現在, fork被實現為clone clone類似於fork但需要一些參數。 值得注意的是, flags參數和child_stack參數。

更多關於此...

那么,用戶級線程如何擁有單獨的堆棧呢?

關於處理器堆棧沒有任何“魔力”。 我將討論主要限於x86,但這適用於任何架構,甚至那些甚至沒有堆棧寄存器的架構(例如1970年代的IBM大型機,例如IBM System 370)

在x86下,堆棧指針是%rsp x86有pushpop指令。 我們使用這些來保存和恢復: push %rcx和[更晚] pop %rcx

但是,假設86 沒有 %rsppush/pop指令? 我們還能有堆疊嗎? 當然, 按照慣例 我們[作為程序員]同意(例如) %rbx是堆棧指針。

在這種情況下, %rcx的“推送”將是[使用AT&T匯編程序]:

subq    $8,%rbx
movq    %rcx,0(%rbx)

並且, %rcx的“pop”將是:

movq    0(%rbx),%rcx
addq    $8,%rbx

為了更容易,我將切換到C“偽代碼”。 以下是上面的push / pop in偽代碼:

// push %ecx
    %rbx -= 8;
    0(%rbx) = %ecx;

// pop %ecx
    %ecx = 0(%rbx);
    %rbx += 8;

要創建線程,LWP調度程序必須使用malloc創建堆棧區域。 然后它必須將此指針保存在每個線程的結構中,然后啟動子LWP。 實際代碼有點棘手,假設我們有一個(例如) LWP_create函數,類似於pthread_create

typedef void * (*LWP_func)(void *);

// per-thread control
typedef struct tsk tsk_t;
struct tsk {
    tsk_t *tsk_next;                    //
    tsk_t *tsk_prev;                    //
    void *tsk_stack;                    // stack base
    u64 tsk_regsave[16];
};

// list of tasks
typedef struct tsklist tsklist_t;
struct tsklist {
    tsk_t *tsk_next;                    //
    tsk_t *tsk_prev;                    //
};

tsklist_t tsklist;                      // list of tasks

tsk_t *tskcur;                          // current thread

// LWP_switch -- switch from one task to another
void
LWP_switch(tsk_t *to)
{

    // NOTE: we use (i.e.) burn register values as we do our work. in a real
    // implementation, we'd have to push/pop these in a special way. so, just
    // pretend that we do that ...

    // save all registers into tskcur->tsk_regsave
    tskcur->tsk_regsave[RAX] = %rax;
    // ...

    tskcur = to;

    // restore most registers from tskcur->tsk_regsave
    %rax = tskcur->tsk_regsave[RAX];
    // ...

    // set stack pointer to new task's stack
    %rsp = tskcur->tsk_regsave[RSP];

    // set resume address for task
    push(%rsp,tskcur->tsk_regsave[RIP]);

    // issue "ret" instruction
    ret();
}

// LWP_create -- start a new LWP
tsk_t *
LWP_create(LWP_func start_routine,void *arg)
{
    tsk_t *tsknew;

    // get per-thread struct for new task
    tsknew = calloc(1,sizeof(tsk_t));
    append_to_tsklist(tsknew);

    // get new task's stack
    tsknew->tsk_stack = malloc(0x100000)
    tsknew->tsk_regsave[RSP] = tsknew->tsk_stack;

    // give task its argument
    tsknew->tsk_regsave[RDI] = arg;

    // switch to new task
    LWP_switch(tsknew);

    return tsknew;
}

// LWP_destroy -- destroy an LWP
void
LWP_destroy(tsk_t *tsk)
{

    // free the task's stack
    free(tsk->tsk_stack);

    remove_from_tsklist(tsk);

    // free per-thread struct for dead task
    free(tsk);
}

使用了解線程的內核,我們使用pthread_createclone ,但我們仍然需要創建新線程的堆棧。 內核不會創建/分配堆棧一個新的線程。 clone syscall接受child_stack參數。 因此, pthread_create必須為新線程分配一個堆棧並將其傳遞給clone

// pthread_create -- start a new native thread
tsk_t *
pthread_create(LWP_func start_routine,void *arg)
{
    tsk_t *tsknew;

    // get per-thread struct for new task
    tsknew = calloc(1,sizeof(tsk_t));
    append_to_tsklist(tsknew);

    // get new task's stack
    tsknew->tsk_stack = malloc(0x100000)

    // start up thread
    clone(start_routine,tsknew->tsk_stack,CLONE_THREAD,arg);

    return tsknew;
}

// pthread_join -- destroy an LWP
void
pthread_join(tsk_t *tsk)
{

    // wait for thread to die ...

    // free the task's stack
    free(tsk->tsk_stack);

    remove_from_tsklist(tsk);

    // free per-thread struct for dead task
    free(tsk);
}

內核只為進程或主線程分配其初始堆棧,通常是在高內存地址。 所以,如果在過程中使用線程,通常情況下,它只是使用了預分配堆棧。

但是,如果一個線程被創建, 無論是一個或LWP 天然一個,起始進程/線程必須為所提出的螺紋與預分配的區域malloc 旁注:使用malloc是正常的方法,但線程創建者可能只有一個龐大的全局內存池: char stack_area[MAXTASK][0x100000]; 如果它希望這樣做的話。

如果我們有一個使用[ 任何類型]線程的普通程序,它可能希望“覆蓋”它已經給出的默認堆棧。

該進程可以決定使用malloc和上面的匯編程序技巧來創建一個更大的堆棧,如果它正在執行一個非常遞歸的函數。

請參閱我的答案: 用戶定義的堆棧和內置堆棧在使用內存時有什么區別?

用戶級線程通常是以一種或另一種形式的協同程序。 用戶模式下切換執行流之間的上下文,沒有內核參與。 從內核POV,是一個線程。 線程實際做的是在用戶模式下控制,用戶模式可以暫停,切換,恢復執行的邏輯流程(即協同程序)。 這一切都發生在為實際線程安排的量子期間。 內核可以並且將毫不客氣地中斷實際線程(內核線程)並將處理器的控制權交給另一個線程。

用戶模式協同程序需要協作式多任務處理。 用戶模式線程必須定期放棄對其他用戶模式線程的控制(基本上執行將上下文更改為新用戶模式線程,而內核線程沒有注意到任何內容)。 通常情況是,當代碼想要釋放內核控制權時,代碼知道的要好得多。 編碼不佳的協程可以竊取控制權並使所有其他協同程序餓死。

歷史實現使用了setcontext但現在已棄用。 Boost.context提供了替代它,但不是完全可移植的:

Boost.Context是一個基礎庫,它在單個線程上提供一種協作式多任務處理。 通過在當前線程中提供當前執行狀態的抽象,包括堆棧(帶有局部變量)和堆棧指針,所有寄存器和CPU標志以及指令指針,execution_context表示應用程序執行路徑中的特定點。

毫不奇怪, Boost.coroutine基於Boost.context。

Windows提供了Fibers .Net運行時具有Tasks和async / await。

LinuxThreads遵循所謂的“一對一”模型:每個線程實際上是內核中的一個獨立進程。 內核調度程序負責調度線程,就像它調度常規進程一樣。 線程是使用Linux clone()系統調用創建的,這是fork()的一般化,允許新進程共享父進程的內存空間,文件描述符和信號處理程序。

來源 - 采訪Xavier Leroy(創建LinuxThreads的人) http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html#K

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM