如何安排/创建用户级线程,以及如何创建内核级线程?
抱歉,这个问题很愚蠢。我试图在网上找到答案已有一段时间,但找不到,因此我在这里提问。我正在学习线程,并且一直在浏览此链接以及有关内核级和用户级线程的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
,该线程调用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 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
和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 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
正在执行巨大的递归功能,则可以决定使用上述汇编程序的技巧来创建更大的堆栈。
以上是 如何安排/创建用户级线程,以及如何创建内核级线程? 的全部内容, 来源链接: utcz.com/qa/422600.html