当前位置: 首页 > 知识库问答 >
问题:

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

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

    编辑:

    我问的是用户级线程创建,它是调度,因为这里引用了多对一模型,在该模型中,许多用户级线程映射到一个内核级线程,线程管理由线程库在用户空间中完成。我只看到使用pthreads的引用,但不确定它是创建用户级线程还是内核级线程。

共有1个答案

马侯林
2023-03-14

这是顶部评论的序言。

您正在阅读的文档是通用的[不是linux专用的],而且有点过时。更重要的是,它使用了不同的术语。我相信,这就是混乱的根源。所以,继续读下去...

它所说的“用户级”线程就是我所说的[过时的]LWP线程。它所说的“内核级”线程就是Linux中所说的本机线程。在linux下,所谓的“内核”线程完全是其他的东西[见下文]。

所有这些都被内核调度器所知道的“本机”线程所取代。这种转变是在10-15年前完成的。

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

在本机线程下有一个LWP系统仍然是可能的,但是,现在,这是一个设计选择,而不是必须的。

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

如果它不创建内核级线程,那么内核线程是如何从userspace程序创建的呢?

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

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

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

(2)在此之后的所有内核都理解线程。没有线程主程序,但是,我们有pthreadsclonesyscall。现在,fork实现为cloneclonefork类似,但接受一些参数。值得注意的是,标志参数和child_stack参数。

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

在这种情况下,%RCX的“push”将是[使用AT&T汇编程序]:

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

并且,%RCX的“pop”是:

movq    0(%rbx),%rcx
addq    $8,%rbx
// push %ecx
    %rbx -= 8;
    0(%rbx) = %ecx;

// pop %ecx
    %ecx = 0(%rbx);
    %rbx += 8;
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_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和上面的汇编器技巧来创建一个大得多的堆栈。

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

  • 问题内容: 通过查看调度程序源代码(2.6.34,kernel / sched.c),我可以看到如何使用“可插拔”调度程序,并且相信可以理解要实现的接口。我还不了解的是如何将我的代码构建到内核中。至少,指向其他站点的指针将不胜感激。 现在,我在内核源代码树中为SCHED_FIFO,SCHED_RR和SCHED_NORMAL进行了搜索,因此,我真的在寻找一种更有洞察力的方式来查看它:- 编辑:作为某

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

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

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

  • 但没有。在应用程序中创建的dispatcher线程使我在优化dispatcher配置时束手无策。每次重新启动应用程序时,我都看到创建了不同数量的dispatcher线程(每次启动应用程序后,我都通过线程转储检查这一点)。 甚至线程数也不等于我在Parallelism-min中定义的线程数。由于这个低线程数,我的应用程序的处理速度非常慢。一查号码。通过下面的代码: GetRuntime().Avai