一尘不染

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

linux

抱歉,这个问题很愚蠢。我试图在网上找到答案已有一段时间,但找不到,因此我在这里提问。我正在学习线程,并且一直在浏览此链接以及有关内核级和用户级线程的2013年Linux
Plumbers Conference
2013视频
,据我了解,使用pthreads在用户空间中创建线程,而内核并不知道关于此问题,并且仅将其视为单个进程,而不知道内部有多少个线程。在这种情况下,

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

编辑:

我问的是用户级线程的创建和调度,因为
这里引用了“多对一模型”,其中许多用户级线程映射到一个内核级线程,线程管理由用户空间完成。线程库。我一直只看到有关使用pthread的参考,但是不确定它是否创建了用户级或内核级线程。


阅读 644

收藏
2020-06-02

共1个答案

一尘不染

开头是最重要的评论。

您正在阅读的文档是通用的(不是特定于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可以运行。

但是,NPTL之前的实现glibc还必须[强制]抢占LWP线程(即,实现时间分段)。我不记得所使用的确切机制,但这是一个示例。线程主控器必须设置一个警报,进入睡眠状态,醒来,然后向活动线程发送信号。信号处理程序将影响上下文切换。这是混乱的,丑陋的并且有点不可靠。

Joachim提到的pthread_create函数创建内核线程

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

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

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

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

用户空间程序/进程可能 无法 创建内核线程。记住,它使用创建一个 本机
线程pthread_create,该线程调用clonesyscall来这样做。

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


更新:

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

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

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

(2)之后的所有 确实 了解线程的内核。有 没有
螺纹高手,但是,我们必须pthreadsclone系统调用。现在,fork实现为cloneclone类似于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

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

为了简化操作,我将切换到C“伪代码”。以下是上述伪代码中的push / pop:

// 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正在执行巨大的递归功能,则可以决定使用上述汇编程序的技巧来创建更大的堆栈。

2020-06-02