抱歉,这个问题很愚蠢。我试图在网上找到答案已有一段时间,但找不到,因此我在这里提问。我正在学习线程,并且一直在浏览此链接以及有关内核级和用户级线程的2013年Linux Plumbers Conference 2013视频,据我了解,使用pthreads在用户空间中创建线程,而内核并不知道关于此问题,并且仅将其视为单个进程,而不知道内部有多少个线程。在这种情况下,
clone()
strace
编辑:
我问的是用户级线程的创建和调度,因为 这里引用了“多对一模型”,其中许多用户级线程映射到一个内核级线程,线程管理由用户空间完成。线程库。我一直只看到有关使用pthread的参考,但是不确定它是否创建了用户级或内核级线程。
开头是最重要的评论。
您正在阅读的文档是通用的(不是特定于Linux的),并且有些过时了。而且,更重要的是,它使用了不同的术语。我认为,这就是造成混乱的根源。所以,请继续阅读…
它所谓的“用户级”线程就是我所说的[过时] LWP线程。它所谓的“内核级” 线程在Linux中称为 本机 线程。在linux下,所谓的“内核”线程完全是另一种东西[见下文]。
使用pthreads在用户空间中创建线程,内核不知道这一点,并且仅将其视为单个进程,而不知道内部有多少个线程。
这是用户空间线程如何 进行 之前完成NPTL(本地POSIX线程库)。这也是SunOS / Solaris所谓的LWP轻量级过程。
NPTL
LWP
有一个进程可以自我复用并创建线程。IIRC,它被称为线程主进程(或某些此类)。内核 不 知道这一点。内核 尚不 了解线程或不提供对线程的支持。
但是,因为这些“轻量级”线程是通过基于用户空间的线程主控器(又称“轻量级进程调度程序”)中的代码进行切换的(只是一个特殊的用户程序/进程),所以切换上下文的速度非常慢。
同样,在“本机”线程出现之前,您可能有10个进程。每个进程获得10%的CPU。如果进程之一是具有10个线程的LWP,则这些线程必须共享10%的线程,因此每个线程仅获得1%的CPU。
所有这一切都换成了“原生”线程内核的调度 是 知道的。这项转换是在10到15年前完成的。
现在,在上面的示例中,我们有20个线程/进程,每个线程/进程获得5%的CPU。并且,上下文切换要快得多。
在本地线程下仍然可以使用LWP系统,但是,这是设计选择,而不是必须的。
此外,如果每个线程“协作”,LWP的效果很好。也就是说,每个线程循环都定期对“上下文切换”函数进行 显式 调用。它会 自动 放弃进程插槽,以便另一个LWP可以运行。
但是,NPTL之前的实现glibc还必须[强制]抢占LWP线程(即,实现时间分段)。我不记得所使用的确切机制,但这是一个示例。线程主控器必须设置一个警报,进入睡眠状态,醒来,然后向活动线程发送信号。信号处理程序将影响上下文切换。这是混乱的,丑陋的并且有点不可靠。
glibc
Joachim提到的pthread_create函数创建内核线程
pthread_create
从技术上来说, 将 其称为 内核 线程是不正确的。pthread_create创建一个 本机 线程。它在用户空间中运行,并在与进程平等的基础上争夺时间片。创建后,线程和进程之间几乎没有什么区别。
主要区别在于,进程具有其自己的唯一地址空间。但是,线程是与同一线程组中的其他进程/线程共享其地址空间的进程。
如果它没有创建内核级线程,那么如何从用户空间程序创建内核线程?
内核线程 不是 用户空间线程,NPTL,本机线程或其他。它们是由内核通过kernel_thread函数创建的。它们作为内核的一部分运行,并且 不 与任何用户空间程序/进程/线程关联。他们具有对计算机的完全访问权限。设备,MMU等。内核线程以最高特权级别运行:ring0。它们还运行在内核的地址空间中,而 不是 在任何用户进程/线程的地址空间中。
kernel_thread
用户空间程序/进程可能 无法 创建内核线程。记住,它使用创建一个 本机 线程pthread_create,该线程调用clonesyscall来这样做。
clone
线程对于做事情很有用,即使对于内核也是如此。因此,它在各种线程中运行一些代码。您可以通过查看这些线程ps ax。看,您将看到kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration,等等。这些是内核线程,而 不是 程序/进程。
ps ax
kthreadd, ksoftirqd, kworker, rcu_sched, rcu_bh, watchdog, migration
更新:
您提到内核不了解用户线程。
请记住,如上所述,有两个“时代”。
(1)在内核获得线程支持之前(大约在2004年?)。这使用了线程主机(在这里,我将其称为LWP调度程序)。内核只有fork系统调用。
fork
(2)之后的所有 确实 了解线程的内核。有 没有 螺纹高手,但是,我们必须pthreads和clone系统调用。现在,fork实现为clone。clone类似于fork但带有一些论点。值得注意的是,一个flags论点和一个child_stack论点。
pthreads
flags
child_stack
下面的更多内容…
那么,用户级线程如何可能具有单独的堆栈?
关于处理器堆栈,没有任何“魔术”。我将讨论[主要]限于x86,但这将适用于任何体系结构,甚至没有栈寄存器的体系结构(例如1970年代的IBM大型机,例如IBM System 370)。
在x86下,堆栈指针为%rsp。x86具有push和pop说明。我们使用它们来保存和恢复内容:push %rcx和[稍后] pop %rcx。
%rsp
push
pop
push %rcx
pop %rcx
但是,假设86并 没有 拥有%rsp或push/pop说明?我们还能叠吗?当然, 按照惯例 。我们(作为程序员)同意(例如)%rbx是堆栈指针。
push/pop
%rbx
在这种情况下,%rcx将使用[AT&T汇编程序] 进行“推送” :
%rcx
subq $8,%rbx movq %rcx,0(%rbx)
并且,“流行”为%rcx:
movq 0(%rbx),%rcx addq $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:
malloc
LWP_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一个 本地 一个,起始进程/线程必须预先分配的区域为所提出的螺纹带malloc。 旁注: 使用malloc是正常的方法,但是线程创建者可能只是拥有大量的全局内存:char stack_area[MAXTASK][0x100000];如果它希望那样做。
char stack_area[MAXTASK][0x100000];
如果我们有一个 不 使用[ 任何 类型的线程] 的普通程序,则可能希望“覆盖”已提供的默认堆栈。
如果该过程malloc正在执行巨大的递归功能,则可以决定使用上述汇编程序的技巧来创建更大的堆栈。