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

时间:2016-08-27 19:49:33

标签: c++ c linux multithreading linux-kernel

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

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

编辑:

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

3 个答案:

答案 0 :(得分:27)

这是以热门评论开头的。

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

它所谓的"用户级"线程是我称之为[过时的] LWP线程。它所谓的内核级别" thread是linux中所谓的 native 线程。在linux下,所谓的"内核"线程完全是另一回事[见下文]。

  

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

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

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

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

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

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

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

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

此外,如果每个线程与#34;合作,LWP的效果很好。也就是说,每个线程循环周期性地对"上下文切换"进行显式调用。功能。它自愿放弃进程槽,以便另一个LWP可以运行。

然而,glibc中的NPTL前实现也必须[强制]抢占LWP线程(即实现时间片)。我无法记住所使用的确切机制,但是,这是一个例子。线程主机必须设置警报,进入休眠状态,唤醒然后向活动线程发送信号。信号处理程序将影响上下文切换。这很混乱,丑陋,有点不可靠。

  

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

<技术上] 调用 内核线程是不正确的。 pthread_create创建原生主题。这在用户空间中运行,并在与进程平等的基础上争夺时间片。一旦创建,线程和进程之间几乎没有区别。

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

  

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

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

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

即使对于内核,线程也很有用。因此,它在各种线程中运行它的一些代码。您可以通过ps ax查看这些主题。看,你会看到kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration等。这些是内核线程和不是程序/进程。

<强>更新

  

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

请记住,如上所述,有两个&#34; eras&#34;。

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

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

以下更多内容......

  

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

没有什么&#34;魔法&#34;关于处理器堆栈。我将[主要]讨论限制在x86,但这适用于任何架构,甚至那些甚至没有堆栈寄存器的架构(例如1970年代的IBM大型机,例如IBM系统370)

在x86下,堆栈指针为%rsp。 x86有pushpop条指令。我们使用这些来保存和恢复内容:push %rcx和[稍后] pop %rcx

但是,假设x86 %rsppush/pop指令?我们还能有堆叠吗?当然,通过约定。我们[作为程序员]同意(例如)%rbx是堆栈指针。

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

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

而且,&#34; pop&#34; %rcx将是:

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

为了更容易,我将切换到C&#34;伪代码&#34;。以下是上面的伪代码推送/弹出:

// 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或一个 native ,那么起始进程/线程必须使用{为该建议线程预先分配区域{1}}。 旁注:使用malloc是正常的方法,但线程创建者可能只有一个大型全局内存池:malloc如果它希望这样做。< / p>

如果我们有一个普通的程序使用[em>任何类型]的线程,它可能希望&#34;覆盖&#34;已经给出了默认堆栈。

该进程可以决定使用char stack_area[MAXTASK][0x100000];和上面的汇编程序技巧来创建一个更大的堆栈,如果它正在执行一个非常递归的函数。

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

答案 1 :(得分:8)

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

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

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

  

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

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

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

答案 2 :(得分:1)

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

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