一、定义
再看正文之前我要先指出一下几点:
1.Linux中没有真正的线程,但windows中确实有线程
2.Linux中没有的线程是由进程来模拟实现的(又叫做:轻量级进程)
3.所以在Linux中(在CPU角度看)进程被叫做轻量级进程(LWP)
因为Linux下没有真正的线程,只有所谓的用户级线程,线程在CPU地址空间内运行
关于进程(PCB)、轻量级进程(LWP)、线程(TCB)、用户线程、内核线程的定义,在现代操作系统中linux培训学校,进程支持多线程。进程是分配资源(资源管理)的最小单元;而线程是调度资源(程序执行)的最小单元。一个进程的组成实体可以分为两大部份:线程集合和资源集合。进程中的线程是动态的对象;代表了进程指令的执行。
a、线程的概念
我们晓得,进程在各自独立的空间中运行,进程之间共享数据须要用mmap或则进程间通讯机制(IPC)怎样在一个进程空间中执行多个线程,有些情况须要在一个进程中同时执行多个控制流程,这时侯线程就派上了用场,
线程的创建:
1intpthread_create(pthread_t*thread,NULL,void*(*start_routine)(void*),void*arg);
线程共享以下进程的资源和环境:
1.**文件描述符表**(重点)
2.每种讯号的处理方法(SIG_IGN、SIG_DFL或则自定义的讯号处理函数)
3.当前工作目录
4.用户id和组id
线程有自己的私有数据:
1. 线程id
2. **上下文信息,包括各种寄存器的值、程序计数器和栈指针**(重点)
3. **栈空间(临时变量存储在栈空间中)**(重点)
4. errno变量
5. 信号屏蔽字
6. 调度优先级
Linux上线程坐落libpthread共享库中,因而在编译时要加上-lpthread选项(-l:指明所链接的库)
b、进程与线程的联系与区别
1. 线程是在进程内部运行的执行分支
2. 线程是为了资源共享(共享地址空间),进程是为了资源独占(私有地址空间)
3. Linux下没有真正的线程,它是利用进程来代替实现的
4. 进程是分配资源(资源管理)的最小单元;而线程是调度资源(程序执行)的最小单元
5. 线程与线程之间是独立的
二、传统进程的缺点
现实中有好多须要并发处理的任务,如数据库的服务器端、网络服务器、大容量估算等。一个任务是一个进程,传统的UNIX进程是单线程(执行流)的,单线程意味着程序必须是次序执行,单个任务不能并发;既在一个时刻只能运行在一个处理器上,因而不能充分借助多处理器框架的计算机。假若采用多进程的方式,即把一个任务用多个进程解决,则有如下问题:
a.fork一个子进程的消耗是很大的,fork是一个高昂的系统调用,虽然使用现代的写时复制(copy-on-write)技术。
b.各个进程拥有自己独立的地址空间,进程间的协作须要复杂的IPC(进程间通讯)技术,如***消息队列、共享显存、信号量***等。
a、多线程的异同点
线程:虽然可以先简单理解成cpu的一个执行流,指令序列。多支持多线程的程序(进程)可以取得真正的并行(parallelism),且因为共享进程的代码和全局数据,故线程间的通讯是便捷的。它的缺点也是因为线程共享进程的地址空间,因而可能会造成竞争,因而对某一块有多个线程要访问的数据须要一些同步技术。
b、轻量级进程LWP
既然叫做轻量级进程,可见其本质依然是进程,与普通进程相比,LWP与其它进程共享所有(或大部份)逻辑地址空间和系统资源,一个进程可以创建多个LWP,这样它们共享大部份资源;LWP有它自己的进程标示符,并和其他进程有着姐弟关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的。LWP由内核管理并像普通进程一样被调度。Linux内核是支持LWP的典型事例。Linux内核在2.0.x版本就早已实现了轻量进程,应用程序可以通过一个统一的clone()系统调用插口,用不同的参数指定创建轻量进程还是普通进程,通过参数决定子进程和父进程共享的资源种类和数目,这样就有了轻重之分。在内核中,clone()调用经过参数传递和解释后会调用do_fork()linux查看进程的线程数,这个核内函数同时也是fork()、vfork()系统调用的最终实现。
在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息,而这也是它之所以被称为轻量级的诱因。
由于LWP之间共享它们的大部份资源,所以它在个别应用程序就不适用了;这个时侯就要使用多个普通的进程了。诸如,为了防止显存泄露(aprocesscanbereplacedbyanotherone)和实现特权分隔(processescanrununderothercredentialsandhaveotherpermissions)。
c、用户线程
这儿的用户线程指的是完全构建在用户空间的线程库,用户线程的构建,同步,销毁,调度完全在用户空间完成,不须要内核的帮助。因而这些线程的操作是非常快速的且低消耗的。
上图是最初的一个用户线程模型,从中可以看出,进程中包含线程,用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不晓得用户线程的存在。用户线程之间的调度由在用户空间实现的线程库实现。
这些模型对应着恐龙书中提及的多对一线程模型,其缺点是一个用户线程假如阻塞在系统调用中,则整个进程都将会阻塞。
d、内核线程
用户态线程和内核态线程;主要的分辨就是“谁来管理”线程,用户态是用户管理,内核态是内核管理(但肯定要提供一些API,比如创建)。
用户线程与内核线程优劣势:
1)可移植性:由于ULT完全在用户态实现线程,因而也就和具体的内核没有哪些关系,可移植性方面ULT略胜一筹;
2)可扩充性:ULT是由用户控制的,因而扩充也就容易;相反,KLT扩充就很不容易,基本上只能受制于具体的操作系统内核;
3)性能:因为ULT的线程是在用户态,对应的内核部份还是一个进程,因而ULT就没有办法借助多处理器的优势,而KLT就可以通过调度将线程分布在多个处理上运行,这样KLT的性能高得多;另外,一个ULT的线程阻塞,所有的线程都阻塞,而KLT一个线程阻塞不会影响其它线程。
4)编程复杂度:ULT的所有管理工作都要由用户来完成,而KLT仅仅须要调用API插口,因而ULT要比KLT复杂的多。
小结
虽然最初根本没有线程的概念,只有进程,一个任务一个进程一个执行流,多任务处理机就是多进程。后来提出线程的概念,并且要怎么去实现,这儿就有好多种实现方式了,可以想到两种实现方式,一种就是前面所说的用户线程的方式,其侧重点点上文以阐述;再有就是用轻量级进程去模拟,即我们可以把LWP看成是一个线程。就由于这个促使线程和进程的概念混淆了,有人说系统调度单位是进程,又有人说是线程,虽然系统调度的单位仍然就没有改变,只是后来部份线程和进程的界限模糊了,起码上文中的用户线程绝对不是调度对象,LWP模拟的线程却是调度对象。
另附:
Linux线程发展
这个对于理解Linux多线程很有帮助,可惜《深入理解Linux内核》这本书只字未提,根本没讲Linux多线程的机理,起码我没搞懂。
仍然以来,linux内核并没有线程的概念.每一个执行实体都是一个task_struct结构,一般称之为进程.Linux内核在2.0.x版本就早已实现了轻量进程,应用程序可以通过一个统一的clone()系统调用插口,用不同的参数指定创建轻量进程还是普通进程。在内核中中文linux操作系统,clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork()、vfork()系统调用的最终实现。后来为了引入多线程,Linux2.0~2.4实现的是也称LinuxThreads的多线程形式,到了2.6,基本上都是NPTL的方法了。下边我们分别介绍。
a、模型一:LinuxThreads
注:以下内容主要参考“杨沙洲(mailto:?subject=Linux线程实现机制剖析&cc=)国防科技学院计算机大学”的“Linux线程实现机制剖析”。
linux2.6曾经,pthread线程库对应的实现是一个名叫linuxthreads的lib.这些实现本质上是一种LWP的实现方法,即通过轻量级进程来模拟线程,内核并不晓得有线程这个概念,在内核看来,都是进程。
Linux采用的“一对一”的线程模型,即一个LWP对应一个线程。这个模型最大的用处是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库函数完成的。
linux上的线程就是基于轻量级进程,由用户态的pthread库实现的.使用pthread之后,在用户看来,每一个task_struct就对应一个线程,而一组线程以及它们所共同引用的一组资源就是一个进程.并且,一组线程并不仅仅是引用同一组资源就够了,它们还必须被视为一个整体.
对此,POSIX标准提出了如下要求:
1:查看进程列表的时侯,相关的一组task_struct应该被诠释为列表中的一个节点;
2:发送给这个"进程"的讯号(对应kill系统调用),将被对应的这一组task_struct所共享,而且被其中的任意一个"线程"处理;
3:发送给某个"线程"的讯号(对应pthread_kill),将只被对应的一个task_struct接收,而且由它自己来处理;
4:当"进程"被停止或继续时(对应SIGSTOP/SIGCONT讯号),对应的这一组task_struct状态将改变;
5:当"进程"收到一个致命讯号(例如因为段错误收到SIGSEGV讯号),对应的这一组task_struct将全部退出;
6:等等(以上可能不够全);
在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时侯才会创建并启动管理线程。之后管理线程再来创建用户恳求的线程。也就是说,用户在调用pthread_create后,先是创建了管理线程,再由管理线程创建了用户的线程。
linuxthreads借助上面提及的轻量级进程来实现线程,并且对于POSIX提出的这些要求,linuxthreads不仅第5点以外,都没有实现(实际上是无能为力):
1,倘若运行了A程序,A程序创建了10个线程,这么在shell下执行ps命令时将见到11个A进程,而不是1个(注意,也不是10个,下边会解释);
2,不管是kill还是pthread_kill,讯号只能被一个对应的线程所接收;
3,SIGSTOP/SIGCONT讯号只对一个线程起作用;
还好linuxthreads实现了第5点,我觉得这一点是最重要的.假如某个线程”挂”了,整个进程还在若无其事地运行着,可能会出现好多的不一致状态.进程将不是一个整体,而线程也不能称为线程.其实这也是为何linuxthreads其实与POSIX的要求差别甚远,却还能存在,而且还被使用了好几年的诱因吧~
然而,linuxthreads为了实现这个”第5点”,还是付出了好多代价,但是创造了linuxthreads本身的一大性能困局.
接出来要谈谈,为何A程序创建了10个线程,并且ps时却会出现11个A进程了.由于linuxthreads手动创建了一个管理线程.前面提及的”第5点”就是靠管理线程来实现的.
当程序开始运行时,并没有管理线程存在(由于虽然程序早已链接了pthread库,而且未必会使用多线程).
程序第一次调用pthread_create时,linuxthreads发觉管理线程不存在,于是创建这个管理线程.这个管理线程是进程中的第一个线程(主线程)的女儿.
之后在pthread_create中,会通过pipe向管理线程发送一个命令,告诉它创建线程.即是说,除主线程外,所有的线程都是由管理线程来创建的,管理线程是它们的母亲.
于是,当任何一个子线程退出时,管理线程将收到SIGUSER1讯号(这是在通过clone创建子线程时指定的).管理线程在对应的sig_handler中会判定子线程是否正常退出,倘若不是,则杀害所有线程,之后自尽.
这么,主线程如何办呢?主线程是管理线程的儿子,其退出时并不会给管理线程发讯号.于是,在管理线程的主循环中通过getppid检测父进程的ID号,倘若ID号是1,说明丈夫早已退出,并把自己托管给了init进程(1号进程).这时侯,管理线程也会杀掉所有子线程,之后自尽.
可见,线程的创建与销毁都是通过管理线程来完成的,于是管理线程就成了linuxthreads的一个性能困局.
创建与销毁须要一次进程间通讯,一次上下文切换以后才会被管理线程执行,但是多个恳求会被管理线程串行地执行.
这些通过LWP的方法来模拟线程的实现看上去还是比较巧妙的,但也存在一些比较严重的问题:
1)线程ID和进程ID的问题
根据POSIX的定义,同一进程的所有的线程应当共享同一个进程和父进程ID,而Linux的这些LWP方法似乎不能满足这一点。
2)讯号处理问题
异步讯号是以进程为单位分发的,而Linux的线程本质上每位都是一个进程,且没有进程组的概念,所以个别缺省讯号无法做到对所有线程有效,比如SIGSTOP和SIGCONT,就难以将整个进程挂起,而只能将某个线程挂起。
3)线程总量问题
LinuxThreads将每位进程的线程最大数量定义为1024,但实际上这个数值还遭到整个系统的总进程数限制,这又是因为线程虽然是核心进程。
4)管理线程问题
管理线程容易成为困局,这是这些结构的弊病;同时,管理线程又负责用户线程的清除工作,为此,虽然管理线程早已屏蔽了大部份的讯号,但一旦管理线程死亡,用户线程就不得不手工清除了linux查看进程的线程数,但是用户线程并不晓得管理线程的状态,以后的线程创建等恳请将无人处理。
5)同步问题
LinuxThreads中的线程同步很大程度上是构建在讯号基础上的,这些通过内核复杂的讯号处理机制的同步方法,效率仍然是个问题。
6)其他POSIX兼容性问题
Linux中好多系统调用,根据语义都是与进程相关的,例如nice、setuid、setrlimit等,在目前的LinuxThreads中,这种调用都仅仅影响调用者线程。
7)实时性问题
线程的引入有一定的实时性考虑,但LinuxThreads暂时不支持,例如调度选项,目前还没有实现。除了LinuxThreads这般,标准的Linux在实时性上考虑都甚少
b、模型二:NPTL
到了linux2.6,glibc中有了一种新的pthread线程库–NPTL(NativePOSIXThreadingLibrary).
本质上来说,NPTL还是一个LWP的实现机制,但相对原有LinuxThreads来说,做了好多的改进。下边我们看一下NPTL怎么解决原有LinuxThreads实现机制的缺陷
NPTL实现了上面提及的POSIX的全部5点要求.并且,实际上,与其说是NPTL实现了,不如说是linux内核实现了.
在linux2.6中,内核有了线程组的概念,task_struct结构中降低了一个tgid(threadgroupid)数组.
假如这个task是一个”主线程”,则它的tgid等于pid,否则tgid等于进程的pid(即主线程的pid).
在clone系统调用中,传递CLONE_THREAD参数就可以把新进程的tgid设置为父进程的tgid(否则新进程的tgid会设为其自身的pid).
类似的XXid在task_struct中还有两个:task->signal->pgid保存进程组的打头进程的pid、task->signal->session保存会话打头进程的pid。通过这两个id来关联进程组和会话。
有了tgid,内核或相关的shell程序就晓得某个tast_struct是代表一个进程还是代表一个线程,也就晓得在哪些时侯该诠释它们,哪些时侯不该诠释(例如在ps的时侯,线程就不要诠释了).
而getpid(获取进程ID)系统调用返回的也是tast_struct中的tgid,而tast_struct中的pid则由gettid系统调用来返回.
在执行ps命令的时侯不显露子线程,也是有一些问题的。例如程序a.out运行时,创建了一个线程。假定主线程的pid是10001、子线程是10002(它们的tgid都是10001)。这时假若你kill10002,是可以把10001和10002这两个线程一起杀害的,虽然执行ps命令的时侯根本看不到10002这个进程。假如你不晓得linux线程背后的故事,肯定会认为遇见灵异风波了。
为了应付”发送给进程的讯号”和”发送给线程的讯号”,task_struct上面维护了两套signal_pending,一套是线程组共享的,一套是线程独有的.
通过kill发送的讯号被置于线程组共享的signal_pending中,可以由任意一个线程来处理;通过pthread_kill发送的讯号(pthread_kill是pthread库的插口,对应的系统调用中tkill)被置于线程独有的signal_pending中,只能由本线程来处理.
当线程停止/继续,或则是收到一个致命讯号时,内核会将处理动作施加到整个线程组中.
NGPT
前面提及的两种线程库使用的都是内核级线程(每位线程都对应内核中的一个调度实体),这些模型称为1:1模型(1个线程对应1个内核级线程);
而NGPT则准备实现M:N模型(M个线程对应N个内核级线程),也就是说若干个线程可能是在同一个执行实体上实现的.
线程库须要在一个内核提供的执行实体上具象出若干个执行实体,并实现它们之间的调度.这样被具象下来的执行实体称为用户级线程.
大体上,这可以通过为每位用户级线程分配一个栈,之后通过longjmp的方法进行上下文切换.(百度一下”setjmp/longjmp”,你就晓得.)
而且实际上要处理的细节问题十分之多.目前的NGPT似乎并没有实现所有预期的功能,但是暂时也不打算去实现.
用户级线程的切换似乎要比内核级线程的切换快一些,后者可能只是一个简单的长跳转,而前者则须要保存/装载寄存器,步入之后退出内核态.(进程切换则还须要切换地址空间等.)
而用户级线程则不能享受多处理器,由于多个用户级线程对应到一个内核级线程上,一个内核级线程在同一时刻只能运行在一个处理器上.
不过,M:N的线程模型虽然提供了这样一种手段,可以让不须要并行执行的线程运行在一个内核级线程对应的若干个用户级线程上,可以节约它们的切换开支.貌似一些类UNIX系统(如Solaris)早已实现了比较成熟的M:N线程模型,其性能比起linux的线程还是有着一定的优势.