首页 文章

如何调度/创建用户级线程,以及如何创建内核级线程?

提问于
浏览
23

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

  • 谁在流程获得的时间片期间决定这些用户线程的调度,因为内核将其视为单个进程并且不知道线程,以及如何完成调度?

  • 如果pthreads创建用户级线程,如果需要,如何从用户空间程序创建内核级或OS线程?

  • 根据上面的链接,它说操作系统内核提供系统调用来创建和管理线程 . 那么 clone() 系统调用是否会创建内核级线程或用户级线程?

  • 如果它创建了一个内核级线程,那么 strace 的一个简单的pthreads program也会在执行时显示使用clone(),但是为什么它会被视为用户级线程呢?

  • 如果它没有创建内核级线程,那么如何从用户空间程序创建内核线程?

  • 根据链接,它说"It require a full thread control block (TCB) for each thread to maintain information about threads. As a result there is significant overhead and increased in kernel complexity.",所以在内核级线程中,只有堆是共享的,其余的都是线程的个体?

编辑:

我问的是用户级线程创建,以及它的调度,因为here,存在对多对一模型的引用,其中许多用户级线程被映射到一个内核级线程,并且线程管理由用户空间在线程完成图书馆 . 我一直只看到使用pthreads的引用,但不确定它是否创建了用户级或内核级线程 .

3 回答

  • 8

    这是前面的评论开头 .

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

    它所谓的"user-level"线程就是我所谓的[过时的] LWP线程 . 它所谓的"kernel-level"线程就是linux中的本机线程 . 在linux下,所谓的"kernel"线程完全是另一回事[见下文] .

    使用pthreads在用户空间中创建线程,内核不知道这一点并仅将其视为单个进程,而不知道内部有多少线程 .

    这是用户空间线程在 NPTL (本机posix线程库)之前完成的方式 . 这也是SunOS / Solaris称为 LWP 轻量级进程的原因 .

    有一个进程多路复用并创建了线程 . IIRC,它被称为线程主进程[或某些此类] . 内核没有意识到这一点 . 内核还没有理解或提供对线程的支持 .

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

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

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

    现在,通过上面的例子,我们有20个线程/进程,每个进程获得5%的CPU . 并且,上下文切换更快 .

    仍然可以在本机线程下拥有LWP系统,但是,现在,这是一种设计选择,而不是必需品 .

    此外,如果每个线程"cooperates",LWP工作得很好 . 也就是说,每个线程循环周期性地对"context switch"函数进行显式调用 . 它自愿放弃进程槽,以便另一个LWP可以运行 .

    但是, glibc 中的NPTL前实现也必须[强制]抢占LWP线程(即实现时间分割) . 我可以't remember the exact mechanism used, but, here'一个例子 . 线程主机必须设置警报,进入休眠状态,唤醒然后向活动线程发送信号 . 信号处理程序将影响上下文切换 . 这是混乱,丑陋,有点不可靠 .

    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 等 . 这些是内核线程,而不是程序/进程 .


    UPDATE:

    您提到内核不了解用户线程 .

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

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

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

    更多关于此...

    那么,用户级线程如何拥有单独的堆栈?

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

    在x86下,堆栈指针是 %rsp . x86具有 pushpop 指令 . 我们使用这些来保存和恢复: push %rcx 和[later] pop %rcx .

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

    在这种情况下, %rcx %rcx 将[使用AT&T汇编程序]:

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

    并且 %rcx%rcx 将是:

    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 系统调用接受 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]; 如果它希望这样做的话 .

    如果我们有一个不使用[任何类型]线程的普通程序,它可能希望"override"给出它的默认堆栈 .

    该进程可以决定使用 malloc 和上面的汇编程序技巧来创建一个更大的堆栈,如果它做了很大的递归函数 .

    在这里看到我的答案:What is the difference between user defined stack and built in stack in use of memory?

  • 1

    用户级线程通常是以一种或另一种形式的协同程序 . 在用户模式下切换执行流之间的上下文,没有内核参与 . 从内核POV,是一个线程 . 线程实际做的是在用户模式下控制,用户模式可以暂停,切换,恢复执行的逻辑流程(即协同程序) . 这一切都发生在为实际线程安排的量子期间 . 内核可以并且将毫不客气地中断实际线程(内核线程)并将处理器的控制权交给另一个线程 .

    用户模式协同程序需要协作式多任务处理 . 用户模式线程必须定期放弃对其他用户模式线程的控制(基本上执行将上下文更改为新用户模式线程,而内核线程没有注意到任何内容) . 通常情况是,当代码想要释放内核控制权时,代码知道的要好得多 . 编码不佳的协程可以窃取控制权并使所有其他协同程序饿死 .

    历史实现使用setcontext但现在已弃用 . Boost.context提供了替代品,但不是完全可移植的:

    Boost.Context是一个基础库,它在单个线程上提供一种协作式多任务处理 . 通过在当前线程中提供当前执行状态的抽象,包括堆栈(带有局部变量)和堆栈指针,所有寄存器和CPU标志以及指令指针,execution_context表示应用程序执行路径中的特定点 .

    毫不奇怪,Boost.coroutine基于Boost.context .

    Windows提供Fibers . .Net运行时具有Tasks和async / await .

  • 24

    LinuxThreads遵循所谓的“一对一”模型:每个线程实际上是内核中的一个独立进程 . 内核调度程序负责调度线程,就像它调度常规进程一样 . 线程是使用Linux clone()系统调用创建的,这是fork()的一般化,允许新进程共享父进程的内存空间,文件描述符和信号处理程序 .

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

相关问题