gpt4 book ai didi

c++ - 用户级线程是如何调度/创建的,内核级线程是如何创建的?

转载 作者:行者123 更新时间:2023-12-01 22:40:45 26 4
gpt4 key购买 nike

如果这个问题很愚蠢,请道歉。我试图在网上找到答案已经有一段时间了,但不能,因此我在这里问。我正在学习线程,我一直在经历 this linkthis Linux Plumbers Conference 2013 video关于内核级和用户级线程,据我所知,使用 pthreads 在用户空间中创建线程,内核不知道这一点,只将其视为单个进程,不知道内部有多少线程。在这种情况下,

  • 谁在进程获得的时间片内决定这些用户线程的调度,因为内核将其视为单个进程并且不知道线程,调度是如何完成的?
  • 如果 pthread 创建用户级线程,如果需要,如何从用户空间程序创建内核级或操作系统线程?
  • 根据上面的链接,它说操作系统内核提供系统调用来创建和管理线程。 clone() 也是如此系统调用创建内核级线程或用户级线程?
  • 如果它创建了内核级线程,则 strace一个简单的 pthreads program还显示了在执行时使用 clone(),但是为什么它会被视为用户级线程?
  • 如果它不创建内核级线程,那么如何从用户空间程序创建内核线程?
  • 根据链接,它说“每个线程都需要一个完整的线程控制块(TCB)来维护有关线程的信息。因此,开销很大并且内核复杂性增加。”所以在内核级线程中,只有堆是共享的,其余的都是线程独立的?

  • 编辑:

    我问的是用户级线程创建,它是调度因为 here,有一个对多对一模型的引用,其中许多用户级线程映射到一个内核级线程,线程管理在用户空间由线程库完成。我一直只看到使用 pthreads 的引用,但不确定它是创建用户级线程还是内核级线程。

    最佳答案

    这是由最高评论开始的。

    您正在阅读的文档是通用的 [不是特定于 linux 的] 并且有点过时。而且,更重要的是,它使用了不同的术语。我相信,这就是困惑的根源。所以,请继续阅读...

    它所谓的“用户级”线程就是我所说的 [过时] LWP 线程。它所谓的“内核级”线程就是Linux中所谓的 native 线程。在 linux 下,所谓的“内核”线程完全是另一回事 [见下文]。

    using pthreads create threads in the userspace, and the kernel is not aware about this and view it as a single process only, unaware of how many threads are inside.



    这是在 NPTL 之前完成用户空间线程的方式( native posix 线程库)。这也是 SunOS/Solaris 所说的 LWP轻量级工艺。

    有一个进程可以复用自身并创建线程。 IIRC,它被称为线程主进程[或类似的]。内核不知道这一点。内核尚未理解或提供对线程的支持。

    但是,因为这些“轻量级”线程是由基于用户空间的线程主(又名“轻量级进程调度程序”)[只是一个特殊的用户程序/进程]中的代码切换的,所以它们切换上下文的速度非常慢。

    此外,在“ native ”线程出现之前,您可能有 10 个进程。每个进程获得 10% 的 CPU。如果其中一个进程是具有 10 个线程的 LWP,则这些线程必须共享 10%,因此每个线程只能获得 1% 的 CPU。

    所有这些都被内核调度程序知道的“ native ”线程所取代。这种转变是在 10-15 年前完成的。

    现在,在上面的例子中,我们有 20 个线程/进程,每个线程/进程获得 5% 的 CPU。而且,上下文切换要快得多。

    仍然可以在 native 线程下使用 LWP 系统,但现在,这是一种设计选择,而不是必需品。

    此外,如果每个线程“合作”,LWP 效果很好。也就是说,每个线程循环都会周期性地显式调用“上下文切换”函数。它自愿放弃进程槽,以便另一个 LWP 可以运行。

    但是, glibc 中的 pre-NPTL 实现还必须[强行]抢占 LWP 线程(即实现时间切片)。我不记得使用的确切机制,但是,这里有一个例子。线程主必须设置闹钟,进入休眠状态,唤醒然后向事件线程发送信号。信号处理程序会影响上下文切换。这是凌乱的,丑陋的,有点不可靠。

    Joachim mentioned pthread_create function creates a kernel thread



    将其称为内核线程 [技术上] 是不正确的。 pthread_create创建一个本地线程。它在用户空间中运行,并在与进程平等的基础上争夺时间片。一旦创建,线程和进程之间几乎没有区别。

    主要区别在于进程有自己唯一的地址空间。然而,线程是与属于同一线程组的其他进程/线程共享其地址空间的进程。

    If it doesn't create a kernel level thread, then how are kernel threads created from userspace programs?



    内核线程不是用户空间线程、NPTL、 native 线程或其他线程。它们由内核通过 kernel_thread 创建。功能。它们作为内核的一部分运行,不与任何用户空间程序/进程/线程相关联。他们可以完全访问机器。设备、MMU 等 内核线程运行在最高权限级别:ring 0。它们也在内核的地址空间中运行,而不是在任何用户进程/线程的地址空间中运行。

    用户空间程序/进程可能不会创建内核线程。请记住,它使用 pthread_create 创建了一个本地线程。 ,它调用 clone系统调用这样做。

    线程对于做事情很有用,即使对于内核也是如此。所以,它在不同的线程中运行它的一些代码。您可以通过执行 ps ax 来查看这些线程。 .看你就会看到 kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration等。这些是内核线程,而不是程序/进程。

    更新:

    You mentioned that kernel doesn't know about user threads.



    请记住,如上所述,有两个“时代”。

    (1) 在内核获得线程支持之前(大约 2004 年?)。这使用了线程主控(在这里,我将其称为 LWP 调度程序)。内核只有 fork系统调用。

    (2) 之后所有理解线程的内核。没有线程大师,但是,我们有 pthreadsclone系统调用。现在, fork实现为 clone . clone类似于 fork但需要一些论据。值得注意的是, flags参数和 child_stack争论。

    更多关于这下面...

    then, how is it possible for user level threads to have individual stacks?



    处理器堆栈没有任何“魔法”。我将讨论 [主要] 限于 x86,但这适用于任何架构,甚至那些甚至没有堆栈寄存器的架构(例如 1970 年代的 IBM 大型机,例如 IBM System 370)

    x86下,栈指针为 %rsp . x86 有 pushpop指示。我们用这些来保存和恢复东西: push %rcx和 [稍后] pop %rcx .

    但是,假设 x86 没有 %rsppush/pop指示?我们还能有一个堆栈吗?当然,按照惯例。我们 [作为程序员] 同意(例如) %rbx是堆栈指针。

    在这种情况下,“推” %rcx将是 [使用 AT&T 汇编程序]:
    subq    $8,%rbx
    movq %rcx,0(%rbx)

    而且, %rcx 的“流行”将是:
    movq    0(%rbx),%rcx
    addq $8,%rbx

    为方便起见,我将切换到 C 语言“伪代码”。以下是伪代码中的上述推送/弹出:
    // 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系统调用接受 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以及上面的汇编技巧,如果它正在执行一个巨大的递归函数,它就会创建一个更大的堆栈。

    在此处查看我的回答: What is the difference between user defined stack and built in stack in use of memory?

    关于c++ - 用户级线程是如何调度/创建的,内核级线程是如何创建的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39185134/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com