ini*_*nit 26 c c++ linux multithreading linux-kernel
如果这个问题很愚蠢,请道歉.我试图在网上找到答案很长一段时间,但不能,因此我在这里问.我学习线程,我一直在经历这个环节和这个Linux的管道工会议2013视频有关内核级和用户级线程,而据我了解,使用并行线程创建线程在用户空间,内核是不知道关于这一点,并将其视为一个单独的进程,不知道内部有多少线程.在这种情况下,
clone()系统调用是否会创建内核级线程或用户级线程?
strace一个简单的pthreads程序也会在执行时显示使用clone(),但是为什么它会被视为用户级线程呢?编辑:
我问的是用户级线程创建和它的调度,因为 这里有一个对多对一模型的引用,其中许多用户级线程被映射到一个内核级线程,并且线程管理在用户空间中由线程库.我一直只看到使用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)后,所有的内核做明白线程.有没有螺纹高手,但是,我们必须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,%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_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);
}
Run Code Online (Sandbox Code Playgroud)
内核只为进程或主线程分配其初始堆栈,通常是在高内存地址.所以,如果进程不使用线程,通常情况下,它只是使用了预分配堆栈.
但是,如果创建了一个线程,无论是LWP还是本机线程,启动进程/线程必须预先为所建议的线程分配区域malloc.旁注:使用malloc是正常的方式,但线程创建者可能只有一个庞大的全局内存池:char stack_area[MAXTASK][0x100000];如果它希望这样做.
如果我们有一个不使用[ 任何类型] 线程的普通程序,它可能希望"覆盖"它已经给出的默认堆栈.
malloc如果它正在执行一个非常大的递归函数,那个进程可以决定使用和上面的汇编器技巧来创建一个更大的堆栈.
请参阅我的答案:用户定义的堆栈和内置堆栈在使用内存时有什么区别?
用户级线程通常是以一种或另一种形式的协同程序.在用户模式下切换执行流之间的上下文,没有内核参与.从内核POV,是一个线程.线程实际做的是在用户模式下控制,用户模式可以暂停,切换,恢复执行的逻辑流程(即协同程序).这一切都发生在为实际线程安排的量子期间.内核可以并且将毫不客气地中断实际线程(内核线程)并将处理器的控制权交给另一个线程.
用户模式协同程序需要协作式多任务处理.用户模式线程必须定期放弃对其他用户模式线程的控制(基本上执行将上下文更改为新用户模式线程,而内核线程没有注意到任何内容).通常情况是,当代码想要释放内核控制权时,代码知道的要好得多.编码不佳的协程可以窃取控制权并使所有其他协同程序饿死.
使用的历史实现,setcontext但现在已弃用.Boost.context提供了替代它,但不是完全可移植的:
Boost.Context是一个基础库,它在单个线程上提供一种协作式多任务处理.通过在当前线程中提供当前执行状态的抽象,包括堆栈(带有局部变量)和堆栈指针,所有寄存器和CPU标志以及指令指针,execution_context表示应用程序执行路径中的特定点.
毫不奇怪,Boost.coroutine基于Boost.context.
Windows提供了Fibers..Net运行时具有Tasks和async/await.