栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

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

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

开头是最重要的评论。

您正在阅读的文档是通用的(不是特定于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
,该线程调用
clone
syscall来这样做。

线程对于做事情很有用,即使对于内核也是如此。因此,它在各种线程中运行一些代码。您可以通过查看这些线程

ps ax
。看,您将看到
kthreadd,ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration
,等等。这些是内核线程,而 不是
程序/进程。


更新:

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

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

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

fork
系统调用。

(2)之后的所有 确实 了解线程的内核。有 没有
螺纹高手,但是,我们必须

pthreads
clone
系统调用。现在,
fork
实现为
clone
clone
类似于
fork
但带有一些论点。值得注意的是,一个
flags
论点和一个
child_stack
论点。

下面的更多内容…

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

关于处理器堆栈,没有任何“魔术”。我将讨论[主要]限于x86,但这将适用于任何体系结构,甚至没有栈寄存器的体系结构(例如1970年代的IBM大型机,例如IBM
System 370)。

在x86下,堆栈指针为

%rsp
。x86具有
push
pop
说明。我们使用它们来保存和恢复内容:
push %rcx
和[稍后]
pop%rcx

但是,假设86并 没有 拥有

%rsp
push/pop
说明?我们还能叠吗?当然, 按照惯例
。我们(作为程序员)同意(例如)
%rbx
是堆栈指针。

在这种情况下,

%rcx
将使用[AT&T汇编程序] 进行“推送” :

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

并且,“流行”为

%rcx

movq    0(%rbx),%rcxaddq    $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 controltypedef 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 taskstypedef struct tsklist tsklist_t;struct tsklist {    tsk_t *tsk_next;         //    tsk_t *tsk_prev;         //};tsklist_t tsklist;// list of taskstsk_t *tskcur;    // current thread// LWP_switch -- switch from one task to anothervoidLWP_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 LWPtsk_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 LWPvoidLWP_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
clone
,但是 仍然 必须创建新线程的堆栈。该内核并 没有
创建/分配堆栈一个新的线程。该
clone
系统调用接受
child_stack
的说法。因此,
pthread_create
必须为新线程分配一个堆栈,并将其传递给
clone

// pthread_create -- start a new native threadtsk_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 LWPvoidpthread_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
是正常的方法,但是线程创建者可能只是拥有大量的全局内存:
charstack_area[MAXTASK][0x100000];
如果它希望那样做。

如果我们有一个 使用[ 任何 类型的线程] 的普通程序,则可能希望“覆盖”已提供的默认堆栈。

如果该过程

malloc
正在执行巨大的递归功能,则可以决定使用上述汇编程序的技巧来创建更大的堆栈。



转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/380274.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号