当前位置: 首页 > 面试题库 >

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

汪阳飇
2023-03-14
问题内容

抱歉,这个问题很愚蠢。我试图在网上找到答案已有一段时间,但找不到,因此我在这里提问。我正在学习线程,并且一直在浏览此链接以及有关内核级和用户级线程的2013年Linux
Plumbers Conference
2013视频

,据我了解,使用pthreads在用户空间中创建线程,而内核并不知道关于此问题,并且仅将其视为单个进程,而不知道内部有多少个线程。在这种情况下,

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

编辑:

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


问题答案:

开头是最重要的评论。

您正在阅读的文档是通用的(不是特定于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正在执行巨大的递归功能,则可以决定使用上述汇编程序的技巧来创建更大的堆栈。



 类似资料:
  • 在进程获得的时间间隔内,谁决定这些用户线程的调度,因为内核将其视为单个进程,并不知道线程,调度是如何完成的? 如果pthreads创建用户级线程,如果需要,如何从用户空间程序创建内核级或OS线程? 根据上面的链接,它说操作系统内核提供系统调用来创建和管理线程。那么,系统调用是创建内核级线程还是用户级线程呢? 如果它创建了一个内核级线程,那么简单pthreads程序的也会显示在执行时使用clone(

  • 本文向大家介绍如何创建线程池 ?相关面试题,主要包含被问及如何创建线程池 ?时的应答技巧和注意事项,需要的朋友参考一下 在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。 为什么呢? 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而

  • 创建并执行内核线程 建立进程控制块(proc.c中的alloc_proc函数)后,现在就可以通过进程控制块来创建具体的进程/线程了。首先,考虑最简单的内核线程,它通常只是内核中的一小段代码或者函数,没有自己的“专属”空间。这是由于在uCore OS启动后,已经对整个内核内存空间进行了管理,通过设置页表建立了内核虚拟空间(即boot_cr3指向的二级页表描述的空间)。所以uCore OS内核中的所有

  • 问题内容: 我是Java技术的新手。我知道在Java中只有两种创建方式 扩展线程类 实施可运行接口 因此,这只是两种创建方法。但是,当我们使用主JVM启动程序时,它启动了一个main 。我认为甚至JVM也必须遵循创建主要方法的规则,以创建主线程JVM必须扩展Thread类或实现。 我尽了最大的努力,但是不知道JVM是如何创建这个主要对象的。当我完全遍历主类()时,我知道这是负责主线程的类。但是在G

  • 我正在寻找关于ThreadLocal的以下使用的验证。 我有一个服务,比如说在一组进程上运行,比如系统中的。哪个将在必须以相同的方式识别中的所有进程。 为此,我应该将一个ID值(比如String类型)写入一个新线程的线程本地存储(TLS),然后在需要时读取该值。因此,调用的第一个线程的ID将是识别它们的ID。当第一个线程启动另一个线程时,它将进入这个新线程的TLS并写入这个ID。从那里开始,这个链

  • 创建第 1 个内核线程 initproc 第0个内核线程主要工作是完成内核中各个子系统的初始化,然后就通过执行cpu_idle函数开始过退休生活了。所以uCore接下来还需创建其他进程来完成各种工作,但idleproc内核子线程自己不想做,于是就通过调用kernel_thread函数创建了一个内核线程init_main。在实验四中,这个子内核线程的工作就是输出一些字符串,然后就返回了(参看init