NPTL分析之线程的创建

NPTL(NativePosix Thread Library)

NPTL包括pthread线程库以及配套的同步方法,我们这里暂时只讲pthread线程库的实现。

1. NPTL的起源

在NPTL之前,linux的线程库是LinuxThreads,该库部分实现了Posixthreads的规范。其主要特点是线程的调度在用户态完成,且由一个管理线程来调度。相应的,其缺点也来自于这种架构,导致管理线程带来了很大的任务切换的开销。另外一个问题是LinuxThreads对信号的处理不满足Posixthreads的规范。

NPTL解决了LinuxThreads的问题,因此,成为了Linux上默认的线程库。

2.NPTL的实现

提到NPTL与LinuxThreads的区别,很多人会说到1:1和M:N,即用户态线程与内核态进程的比例问题。NPTL的1:1在我看来是0.5:0.5,因为用户态看到的那个线程与内核态的进程本来就是同一个进程。下图是我对NPTL线程的理解:

上图中,红色线以上是用户态,下面是内核态。左边虚线框内是单线程的,右边是多线程的(2个线程)。

在Linux的眼里,不会区别进程和线程,在它眼里只有task_struct。task_struct是用来描述进程(线程)的结构体,其中会记录一切关于进程的信息。内核做任务调度的时候,仅仅是选择一个task_struct而已。这个task_struct是属于进程还是线程的,内核并不关心。正因为如此,多线程下的无差别调度才能保证(所以,如果你想拖慢别人程序的速度,你可以创建大量的线程)。既然内核中线程和进程都是task_struct,那在用户态中线程的特性是如何实现的?下面将就几个例子来说明线程的特性是如何实现的。

2.1 为什么线程间可以很容易地访问全局变量,但是进程间却不行?

我们在写多线程代码时,线程间共享某些数据非常简单:弄一个全局变量即可。但是写多进程的程序时,则必须另外创建一个共享内存才能共享数据。为什么会有这样的差异?

简单地说,是因为线程的实现时线程间共享了虚拟地址空间。一般来说,每个进程都有独立的虚拟地址空间,其中包括独立的页表。那么,在一个线程组中,每个线程看到的虚拟地址对应的物理地址都是一样的。而在两个进程中,同样的虚拟地址可能就对应不同的物理地址。这就导致线程间的数据共享和进程间有很大的差别,继而导致线程间的同步方式和进程间有较大差异。

2.2 线程在内核中的数据模型

首先,看下面这个图:

不管是通过fork产生的进程还是通过pthread_create产生的线程,其在内核中都对应着一个task_struct。linux_structure图中有两个task_struct,其中右边的task_struct是通过pthread_create产生的,因此左边的task_struct是threadgroup的leader。task_struct有很多字段,其中mm就是用来描述虚拟地址空间的。我们可以看到两个task_struct的mm指向了同一个mm_struct对象,也就是前面提到的线程间共享同一个虚拟地址空间。

不仅仅是虚拟地址空间,线程间还共享信号相关的数据。比如,线程间共享相同的信号处理函数,相同的pending信号(线程也有独立的pending信号,这些信号是通过pthread_kill发送的,而不是通过kill发送的)。从图中可以看到,signal、sighand和pending字段都指向相同的对象。既然共享信号相关的数据,那么就可以解释一个问题:多线程程序中,信号是由哪个线程处理的?答案很简单:由最先从内核态返回用户态的线程处理的。信号处理会作为一个专门的技术问题在以后的技术分享中讲解。

用户态中,多线程会作为一个进程看待,在内核中,会被抽象为“线程组”。线程组中的task_struct有不同的pid字段,但是会有相同的tgid(threadgroup id)字段,这个tgid作为用户态进程的pid给上层使用。所以,在一个进程下的每个线程中获取到的pid是相同的,用户态的pid与内核态的pid不是同一个东西,想获取线程在内核态的pid可以通过系统调用(gettid)来获取。线程组中的task_struct会通过一个链表(thread_group)链接起来,后面创建的线程的task_struct中的group_leader字段会指向leader。通过共享mm_struct、fs、signal相关的数据,再通过thread_group相关的设置,线程的概念基本就实现了。

2.3 线程的创建流程,从用户态到内核

   2.3.1 pthread_create

pthread_create是创建线程的入口,主要参数是一个函数指针,其指向的函数为线程创建成功后要执行的函数。由于线程组中的各个线程是可以并行运行在不同的CPU上的,所以各个线程必须得有自己独立的用户态栈和内核栈。Linux用户态栈的大小一般是8M,通过mmap在MemoryMapping Area中分配一块内存(不考虑用户态栈缓存)。在用户态也有一个数据结构用来描述线程(structpthread),该数据结构就放在用户态栈的最下面的位置,其中会存放线程创建成功后要执行的函数地址。

分配完structpthread和用户态栈之后(其实还有很多事情,不过都是些琐事),差不多就可以带着这些信息进入内核申请task_struct了。

   2.3.2 syscall入口

像创建新的进程这种资源管理工作基本上都要通过内核完成,从用户态进入内核态有好几种方式,其中系统调用是主动陷入内核的一种方式。从用户态进入内核态与从内核态返回用户态,两种路径都是有标准的,由于涉及到模式的转换,所以与处理器架构有很大的关系。一般来说,从用户态进入内核态时,都会把进程在用户态时的状态保存在内核栈中,这样完成了内核态的任务之后返回时可以恢复用户态的状态。各种寄存器信息基本上就保存在内核栈的顶部的structpt_regs里面,x86_64架构下pt_regs的字段如下:

这个结构体在内核很多地方会见到,在trace系统中有重要的作用,后面会以专题的形式讲一下pt_regs的故事。

glibc在用户态对clone封装了一层,名为ARCH_CLONE。其中会把线程创建成功后要执行的函数地址以及参数压入用户态栈,然后再调用系统调用clone,这样系统调用clone返回后再从栈中取出线程函数以及对应的参数,继而开始执行线程函数。

   2.3.3 clone

系统调用(syscall)clone用于创建当前进程的一个副本,fork就是调用的clone。我们这里是要创建线程,那么对clone的用法当然与fork有所区别了。区别主要在于clone的参数clone_flags的设置:

创建线程调用的clone:

创建进程调用的clone:

可以看到创建线程时使用的clone_flags中多了CLONE_VM、CLONE_FS、CLONE_FILES和CLONE_SIGNAL(CLONE_SIGHAND| CLONE_THREAD),这些是实现Posixthread规范的关键。这些参数体现在linux_structure图中就是其中的mm、fs、files、signal、sighand和pending字段指向相同的对象。正因为这些数据的共享,线程间的数据共享和同步比在进程间简单得多。

fork创建的进程是当前进程的儿子,pthread_create创建的进程则相当于当前进程的兄弟。CLONE_THREAD这个标记使得创建的进程加入当前的线程组。

clone的过程主要包括task_struct的分配以及相应的资源的拷贝,比较有条理,可以自行查看相关的代码,重点关注内核栈的建立以及内核栈与task_struct的关联关系的创建。

2.4 总结

线程在Linux内核中只是一个逻辑上的概念,并没有某个数据结构来区分进程或者线程,“线程”是一个轻量级进程,它与其它进程共享了虚拟地址空间,这一特性直接导致多线程和多进程的天壤之别。本文仅仅讲了NPTL中线程库部分(其中的线程创建部分),线程间的同步(futex)以后再讲。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-29 18:23:04

NPTL分析之线程的创建的相关文章

Linux 线程实现机制分析 Linux 线程实现机制分析 Linux 线程模型的比较:LinuxThreads 和 NPTL

Linux 线程实现机制分析 Linux 线程实现机制分析  Linux 线程模型的比较:LinuxThreads 和 NPTL http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/ 自从多线程编程的概念出现在 Linux 中以来,Linux 多线应用的发展总是与两个问题脱不开干系:兼容性.效率.本文从线程模型入手,通过分析目前 Linux 平台上最流行的 LinuxThreads 线程库的实现及其不足,描述了 Linux 社区是

UDT协议实现分析——UDT Socket的创建

UDT API的用法 在分析 连接的建立过程 之前,先来看一下UDT API的用法.在UDT网络中,通常要有一个UDT Server监听在某台机器的某个UDP端口上,等待客户端的连接:有一个或多个客户端连接UDT Server:UDT Server接收到来自客户端的连接请求后,创建另外一个单独的UDT Socket用于与该客户端进行通信. 先来看一下UDT Server的简单的实现,UDT的开发者已经提供了一些demo程序可供参考,位于app/目录下. #include <unistd.h>

浅析Linux线程的创建

本文首先使用了接口pthread_create创建一个线程,并用strace命令追踪了接口pthread_create创建线程的步骤以及涉及到的系统调用,然后讨论了Linux中线程与进程关系,最后概述了为了实现POSIX线程,Linux内核所做的修改. 使用pthread_create创建线程 在Linux下可以使用pthread_create来创建线程,该接口声明如下: #include <pthread.h> int pthread_create(phtread_t *thread, co

线程的创建,等待与终止

一.线程的创建 基础知识 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成. 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源. 一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行.由于线程之间的相互制约,致使线程在运行中呈现出间断性

猫猫学iOS(四十九)多线程网络之线程的创建NSThreand

猫猫分享,必须精品 原创文章,欢迎转载.转载请注明:翟乃玉的博客 地址:http://blog.csdn.net/u013357243?viewmode=contents 一:NSThread的基本使用 1:创建和启动线程 一个NSThread对象就代表一条线程 创建.启动线程 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(sel) object:nil]; [thread start];

在子线程中创建Handler和looper并与主线程进行交互

分析完上面那篇文章,基本理解了handler的实现原理,乘热打铁,这里我们利用handler原理,在子线程中创建一个handler和looper 可能很多面试时候问道,子线程中能不能new一个handler ? 答案是可以的,但是因为主线程系统默认在ActivityThread中已将帮我们创建好一个looper和MessagQueue,我们不需要手动去创建 (手动创建会出错,因为一个线程中默认只运行一个looper和MessageQueue,具体见ThreadLocal代码原理), 而子线程中没

线程的创建和控制

线程的定义:线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成.另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源.一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行.由于线程之间的相互制约,致使线程在运行中呈现出间断性.线程也有就

Memcached源码分析之线程模型

作者:Calix 一)模型分析 memcached到底是如何处理我们的网络连接的? memcached通过epoll(使用libevent,下面具体再讲)实现异步的服务器,但仍然使用多线程,主要有两种线程,分别是“主线程”和“worker线程”,一个主线程,多个worker线程. 主线程负责监听网络连接,并且accept连接.当监听到连接时,accept后,连接成功,把相应的client fd丢给其中一个worker线程.worker线程接收主线程丢过来的client fd,加入到自己的epol

Dalvik虚拟机进程和线程的创建过程分析

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8923484 我们知道,在Android系统中,Dalvik虚拟机是运行Linux内核之上的.如果我们把Dalvik虚拟机看作是一台机器,那么它也有进程 和线程的概念.事实上,我们的确是可以在Java代码中创建进程和线程,也就是Dalvik虚拟机进程和线程.那么,这些Dalvik虚拟机所创建的进程 和线程与其宿主Linux内核的进程和线程有什么关