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

ini*_*nit 26 c c++ linux multithreading linux-kernel

如果这个问题很愚蠢,请道歉.我试图在网上找到答案很长一段时间,但不能,因此我在这里问.我学习线程,我一直在经历这个环节这个Linux的管道工会议2013视频有关内核级和用户级线程,而据我了解,使用并行线程创建线程在用户空间,内核是不知道关于这一点,并将其视为一个单独的进程,不知道内部有多少线程.在这种情况下,

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

编辑:

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

Cra*_*tey 28

这是前面的评论开头.

您正在阅读的文档是通用的[不是特定于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,在最高特权级别运行等内核线程:0环,还内核的地址空间,并运行没有任何用户进程/线程的地址空间.

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

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


更新:

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

请记住,如上所述,有两个"时代".

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

(2)后,所有的内核明白线程.有没有螺纹高手,但是,我们必须pthreadsclone系统调用.现在,fork实现为clone.clone类似于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)
Run Code Online (Sandbox Code Playgroud)

并且,"流行" %rcx将是:

movq    0(%rbx),%rcx
addq    $8,%rbx
Run Code Online (Sandbox Code Playgroud)

为了更容易,我将切换到C"伪代码".以下是上面的push/pop in伪代码:

// push %ecx
    %rbx -= 8;
    0(%rbx) = %ecx;

// pop %ecx
    %ecx = 0(%rbx);
    %rbx += 8;
Run Code Online (Sandbox Code Playgroud)

要创建线程,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);
}
Run Code Online (Sandbox Code Playgroud)

使用了解线程的内核,我们使用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);
}
Run Code Online (Sandbox Code Playgroud)

内核只为进程或主线程分配其初始堆栈,通常是在高内存地址.所以,如果进程使用线程,通常情况下,它只是使用了预分配堆栈.

但是,如果创建了一个线程,无论是LWP还是本机线程,启动进程/线程必须预先为所建议的线程分配区域malloc.旁注:使用malloc是正常的方式,但线程创建者可能只有一个庞大的全局内存池:char stack_area[MAXTASK][0x100000];如果它希望这样做.

如果我们有一个使用[ 任何类型] 线程的普通程序,它可能希望"覆盖"它已经给出的默认堆栈.

malloc如果它正在执行一个非常大的递归函数,那个进程可以决定使用和上面的汇编器技巧来创建一个更大的堆栈.

请参阅我的答案:用户定义的堆栈和内置堆栈在使用内存时有什么区别?

  • 一个小疑问.您提到内核不了解用户线程.那么,用户级线程如何拥有单独的堆栈呢? (2认同)

Rem*_*anu 8

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

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

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

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

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

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