如果这个问题很愚蠢,请道歉。我试图在网上找到答案很长一段时间,但不能,因此我在这里问。我正在学习线程,我一直在通过this link和this Linux Plumbers Conference 2013 video关于内核级别和用户级线程,据我所知,使用pthreads在用户空间创建线程,内核是不知道这一点并仅将其视为单个进程,不知道内部有多少线程。在这种情况下,
clone()
系统调用是否会创建内核级线程或用户级线程?
strace
也会在执行时显示使用clone(),但为什么它会被视为用户级线程?编辑:
我询问用户级线程创建,并且它是调度因为here,存在对多对一模型的引用,其中许多用户级线程被映射到一个内核级线程,并且线程管理是由线程库在用户空间中完成。我一直只看到使用pthreads的引用,但不确定它是否创建了用户级或内核级线程。
答案 0 :(得分:27)
这是以热门评论开头的。
您正在阅读的文档是通用的[不是特定于Linux的]并且有点过时。而且,更重要的是,它使用了不同的术语。也就是说,我相信,混乱的根源。所以,请继续阅读...
它所谓的"用户级"线程是我称之为[过时的] LWP线程。它所谓的内核级别" thread是linux中所谓的 native 线程。在linux下,所谓的"内核"线程完全是另一回事[见下文]。
使用pthreads在用户空间中创建线程,并且内核不知道这一点并仅将其视为单个进程,而不知道内部有多少线程。
这是用户空间线程 在NPTL
(本机posix线程库)之前完成的方式。这也是SunOS / Solaris称为LWP
轻量级进程的原因。
有一个进程多路复用并创建了线程。 IIRC,它被称为线程主进程[或某些此类]。内核不意识到这一点。内核还没有 了解或提供对线程的支持。
但是,因为,这些"轻量级"线程由基于用户空间的线程主机(又称"轻量级进程调度程序")中的代码切换[只是一个特殊的用户程序/进程],它们切换上下文非常慢。
此外,在" native"的出现之前线程,你可能有10个进程。每个进程获得10%的CPU。如果其中一个进程是一个有10个线程的LWP,那么这些线程必须共享10%,因此每个进程只占CPU的1%。
所有这些都被" native"内核的调度程序 知道的线程。这种转变是在10 - 15年前完成的。
现在,通过上面的例子,我们有20个线程/进程,每个进程获得5%的CPU。并且,上下文切换要快得多。
仍然可以在本机线程下使用LWP系统,但是,现在,这是一种设计选择,而不是必需品。
此外,如果每个线程与#34;合作,LWP的效果很好。也就是说,每个线程循环周期性地对"上下文切换"进行显式调用。功能。它自愿放弃进程槽,以便另一个LWP可以运行。
然而,glibc
中的NPTL前实现也必须[强制]抢占LWP线程(即实现时间片)。我无法记住所使用的确切机制,但是,这是一个例子。线程主机必须设置警报,进入休眠状态,唤醒然后向活动线程发送信号。信号处理程序将影响上下文切换。这很混乱,丑陋,有点不可靠。
Joachim提到
pthread_create
函数创建了一个内核线程
<技术上] 调用 内核线程是不正确的。 pthread_create
创建原生主题。这在用户空间中运行,并在与进程平等的基础上争夺时间片。一旦创建,线程和进程之间几乎没有区别。
主要区别在于进程有自己唯一的地址空间。但是,线程是一个进程,它与属于同一线程组的其他进程/线程共享其地址空间。
如果它没有创建内核级线程,那么如何从用户空间程序创建内核线程?
内核线程不用户空间线程,NPTL,本机或其他。它们由内核通过kernel_thread
函数创建。它们作为内核的一部分运行,并且不与任何用户空间程序/进程/线程相关联。他们可以完全访问机器。设备,MMU等。内核线程以最高权限级别运行:响0。它们也在内核的地址空间中运行,不在任何用户进程/线程的地址空间。< / p>
用户空间程序/进程可能不创建内核线程。请记住,它使用pthread_create
创建本机线程,该线程调用clone
系统调用来执行此操作。
即使对于内核,线程也很有用。因此,它在各种线程中运行它的一些代码。您可以通过ps ax
查看这些主题。看,你会看到kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration
等。这些是内核线程和不是程序/进程。
<强>更新强>
您提到内核并不了解用户线程。
请记住,如上所述,有两个&#34; eras&#34;。
(1)在内核获得线程支持之前(大约2004年?)。这使用了线程主机(在这里,我将调用LWP调度程序)。内核只有fork
系统调用。
(2) 之后的所有内核都了解线程。有没有主线,但我们有pthreads
和clone
系统调用。现在,fork
已实现为clone
。 clone
与fork
类似,但需要一些参数。值得注意的是,flags
参数和child_stack
参数。
以下更多内容......
那么,用户级线程如何具有单独的堆栈?
没有什么&#34;魔法&#34;关于处理器堆栈。我将[主要]讨论限制在x86,但这适用于任何架构,甚至那些甚至没有堆栈寄存器的架构(例如1970年代的IBM大型机,例如IBM系统370)
在x86下,堆栈指针为%rsp
。 x86有push
和pop
条指令。我们使用这些来保存和恢复内容:push %rcx
和[稍后] pop %rcx
。
但是,假设x86 不有%rsp
或push/pop
指令?我们还能有堆叠吗?当然,通过约定。我们[作为程序员]同意(例如)%rbx
是堆栈指针。
在这种情况下,&#34;推&#34; %rcx
将[使用AT&amp; T汇编程序]:
subq $8,%rbx
movq %rcx,0(%rbx)
而且,&#34; pop&#34; %rcx
将是:
movq 0(%rbx),%rcx
addq $8,%rbx
为了更容易,我将切换到C&#34;伪代码&#34;。以下是上面的伪代码推送/弹出:
// 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 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或一个 native ,那么起始进程/线程必须使用{为该建议线程预先分配区域{1}}。 旁注:使用malloc
是正常的方法,但线程创建者可能只有一个大型全局内存池:malloc
如果它希望这样做。< / p>
如果我们有一个普通的程序不使用[em>任何类型]的线程,它可能希望&#34;覆盖&#34;已经给出了默认堆栈。
该进程可以决定使用char stack_area[MAXTASK][0x100000];
和上面的汇编程序技巧来创建一个更大的堆栈,如果它正在执行一个非常递归的函数。
请在此处查看我的回答:What is the difference between user defined stack and built in stack in use of memory?
答案 1 :(得分:8)
用户级线程通常是一种形式的协同程序。在用户模式下切换执行流之间的上下文,没有内核参与。从内核POV,是一个线程。实际所做的线程在用户模式下被控制,并且用户模式可以暂停,切换,恢复执行的逻辑流程(即协同程序)。这一切都发生在为实际线程安排的量子期间。内核可以并且将毫不客气地中断实际线程(内核线程)并将处理器的控制权交给另一个线程。
用户模式协同程序需要协作式多任务处理。用户模式线程必须定期放弃对其他用户模式线程的控制(基本上执行将 context 更改为新的用户模式线程,而内核线程没有注意到任何内容)。通常情况是,当希望释放内核控制时,代码知道更多。编码不佳的协程可以窃取控制权并使所有其他协同程序挨饿。
历史实施使用setcontext
但现在已弃用。 Boost.context提供了替代品,但不是完全可移植的:
Boost.Context是一个基础库,它在单个线程上提供一种协作式多任务处理。通过在当前线程中提供当前执行状态的抽象,包括堆栈(带有局部变量)和堆栈指针,所有寄存器和CPU标志以及指令指针,execution_context表示应用程序中的特定点。执行路径。
毫不奇怪,Boost.coroutine基于Boost.context。
Windows提供了Fibers。 .Net运行时有Tasks和async / await。
答案 2 :(得分:1)
LinuxThreads遵循所谓的&#34;一对一&#34; model:每个线程实际上是内核中的一个独立进程。内核调度程序负责调度线程,就像它调度常规进程一样。线程是使用Linux clone()系统调用创建的,这是fork()的一般化,允许新进程共享父进程的内存空间,文件描述符和信号处理程序。
来源 - 采访Xavier Leroy(创建LinuxThreads的人) http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html#K