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

道歉,如果这个问题是愚蠢的。 我试图在网上find一个相当长的答案,但不能,所以我在这里问。 我正在学习线程,而且我一直在浏览这个链接和这个关于内核级和用户级线程的Linux Plumbers Conference 2013video ,并且据我所知,使用pthreads在用户空间创build线程,内核不知道关于这个,并且只将它视为一个单独的进程,不知道里面有多less个线程。 在这种情况下,

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

编辑:

我在询问用户级线程的创build,因为这里有一个对许多对一个模型的引用,许多用户级线程映射到一个内核级线程,线程pipe理在用户空间由线程库。 我只看到使用pthreads的引用,但不确定它是否创build用户级别或内核级别的线程。

Solutions Collecting From Web of "如何调度/创build用户级线程,以及如何创build内核级线程?"

这由前面的评论开始。

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

它所谓的“用户级”线程就是我称之为[过时的] LWP线程。 它所谓的“内核级”线程就是在linux中所谓的本地线程。 在linux下,所谓的“内核”线程是完全不同的东西[见下文]。

使用pthreads在用户空间中创建线程,而内核并不知道这一点,只将它视为一个单独的进程,不知道里面有多少线程。

这是用户空间线程在NPTL (本地posix线程库)之前完成的。 这也是SunOS / Solaris所称的LWP轻量级过程。

有一个进程复用自己并创建线程。 IIRC,它被称为线程主进程[或一些这样的]。 内核没有意识到这一点。 内核还不了解或提供对线程的支持。

但是,因为这些“轻量级”的线程在基于用户空间的线程主机(又称“轻量级进程调度器”)(仅仅是一个特殊的用户程序/进程)中被代码切换,所以他们切换上下文非常缓慢。

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

所有这些都被内核的调度程序知道的“本地”线程所取代。 这个转换是在10 – 15年前完成的。

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

在原生线程下仍然可以有一个LWP系统,但现在,这是一个设计选择,而不是必要的。

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

但是, glibc的NPTL实现前也必须[强行]抢占LWP线程(即实现时间片)。 我不记得确切的机制,但是,这里是一个例子。 线程主机必须设置一个闹钟,进入睡眠状态,唤醒,然后发送活动线程一个信号。 信号处理程序会影响上下文切换。 这是混乱,丑陋,有些不可靠。

Joachim提到pthread_create函数会创建一个内核线程

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

主要的区别是一个进程有它自己独特的地址空间。 然而,一个线程是一个进程,它与同一线程组中的其他进程/线程共享其地址空间。

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

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

用户空间程序/进程可能不会创建内核线程。 请记住,它使用pthread_create创建一个本地线程,该线程将调用clone系统调用来执行此操作。

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


更新:

你提到内核不知道用户线程。

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

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

(2)所有的内核知道线程。 没有线程主,但是,我们有pthreadsclone系统调用。 现在, fork被实现为cloneclone类似于fork但需要一些参数。 值得注意的是,一个flags参数和一个child_stack参数。

更多关于这个下面…

那么,用户级别的线程怎么可能拥有单独的堆栈呢?

处理器堆栈没有什么“魔力”。 我将讨论[大部分]限于x86,但是这将适用于任何架构,即使那些甚至没有堆栈寄存器的架构(例如IBM的大型机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 (ie) 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和上面的汇编语言来创建一个更大的堆栈,如果它正在做一个巨大的递归函数的话。

在这里看到我的答案: 用户定义的堆栈和使用内存的内置堆栈有什么区别?

用户级别的线程通常是协程,以一种或另一种形式。 在用户模式下的执行流之间切换上下文,而不涉及内核。 从内核POV来看,都是一个线程。 线程实际上做的是在用户模式下进行控制,用户模式可以挂起,切换,恢复执行的逻辑流程(即协程)。 这一切都发生在为实际线程计划的量子期间。 内核可以,而且会毫不客气地中断实际的线程(内核线程),并将处理器的控制权交给另一个线程。

用户模式协同程序需要协作式多任务处理。 用户模式线程必须定期将控制权交给其他用户模式线程(基本上,执行将上下文更改为新的用户模式线程,而内核线程从未注意到任何事情)。 通常发生的事情是,当代码想要释放内核的控制权时,代码会更好地了解它们。 一个编码不好的协程可能会窃取所有其他协程。

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

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

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

Windows提供了Fibers 。 .Net运行时有任务和异步/等待。

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

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