转自:http://blog.csdn.net/u012927281/article/details/51602898
今天偶然谈起了进程的相关概念,发现其中有许多不清晰的地方,现就以上的概念做一些研究,所参考的资料全部来自于网络,所以对于其中不正确的地方,欢迎大家给我指正,让我能够对以上概念更加清晰。
好,首先理清以上几个概念,先来看“内核线程”。此处要申明的一点是,从我所看到的资料来看(《Linux内核设计与实现》、《深入理解Linux内核》以及网上的诸多文章),只有“内核线程”的概念,不存在所谓的“内核进程”。此处对于内核线程的具体定义我并没有找到,不过可以从它的功能出发,窥探一二。
关于内核线程的作用主要参考自以下文章:http://blog.csdn.net/kickxxx/article/details/9211381
内核线程的作用主要有:
- 周期性的将dirty内存页同步到磁盘设备上。 比如 bpflush线程周期性的把dirty数据写回磁盘
- 内存页很少的情况下,把内存page 交换到磁盘空间。 比如kswapd,系统会为每一个NUMA创建一个kswapd进程,但是在非NUMA系统上,则仅有一个kswapd
- 管理延时动作
- 实现文件系统的事物日志
主要包括两种类型的内核线程:
- 线程按周期性间隔运行,检测特定资源的使用,在用量超出或者低于预置的限制时采取行动
- 在线程启动后则一直等待,直到内核线程请求执行某一特定的操作。
同时内核线程是由内核自己创建的线程,也叫做守护线程(deamon)。在终端上用命令”ps -Al”列出的所有进程中,名字以k开关以d结尾的往往都是内核线程,比如kthreadd、kswapd。
既然是内核线程是一个单独的概念,那么与用户线程之间一定存在某些区别,主要区别参考自以下文章:http://www.2ndmoon.net/weblog/?p=603
内核线程与用户线程的相同点是:
- 都由do_fork()创建,每个线程都有独立的task_struct和内核栈;
- 都参与调度,内核线程也有优先级,会被调度器平等地换入换出
不同之处在于:
- 内核线程只工作在内核态中;而用户线程则既可以运行在内核态(执行系统调用时),也可以运行在用户态;
- 内核线程没有用户空间,所以对于一个内核线程来说,它的0~3G的内存空间是空白的,它的current->mm是空的,与内核使用同一张页表;而用户线程则可以看到完整的0~4G内存空间。
在Linux内核启动的最后阶段,系统会创建两个内核线程,一个是init,一个是kthreadd。其中init线程的作用是运行文件系统上的一系列”init”脚本,并启动shell进程,所以init线程称得上是系统中所有用户进程的祖先,它的pid是1。kthreadd线程是内核的守护线程,在内核正常工作时,它永远不退出,是一个死循环,它的pid是2。
通过上述不同之处的对比可以发现,内核线程没有用户内存空间,与之相对的是用户进程(注意,此处不是用户线程,用户线程内存空间与用户进程空间之间存在的一定差异,具体差异可以参考我之前的博文),用户进程同时具备内核空间与用户空间,在进行系统调用时用户进程会由用户内存空间陷入内核内存空间。之所以此处采用内核线程与用户进程(而非用户线程)进行对比,是由于内核线程(kernel thread)是“独立运行在内核空间的标准进程”(以上这句话摘自《linux内核设计与实现》),因此从功能上看内核线程一方面具有进程的概念特点——具有独立功能的程序关于某个数据集合上的一次执行活动,是系统进行资源分配的单位,同时内核线程又具有线程的概念特点——进程内的一个可调度实体。
好了通过以上分析基本可以分清“内核线程(kernel thread)”、“用户进程”、“用户线程”这几个基本概念。这里要补充的一点是以上几个概念全部是在逻辑层面上的,从实现的角度来看linux内核本身并不支持线程这一概念,linux 将所有的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程(概念上应该是线程)共享某些资源的进程(概念上应该是线程)。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。关于这一点可以通过系统调用clone的实现来验证,无论是fork、vfork、kthread_create最后都是要调用do_fork,而do_fork就是根据不同的函数参数,对一个进程所需的资源进行分配。在linux2.6之前,内核并不支持线程的概念,仅通过轻量级进程(lightweight process)模拟线程,一个用户线程对应一个内核线程(内核轻量级进程),这种模型最大的特点是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库(LinuxThread)函数完成的。但这个问题还存在很多的问题,具体问题请见http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/index.html 这篇文章中的第6小节。在linux2.6之后,为了完全兼容posix标准,linux2.6首先对内核进行了改进,引入了线程组的概念(仍然用轻量级进程表示线程),有了这个概念就可以将一组线程组织称为一个进程,如此通过这个改变,linux内核正式支持多线程特性。以上是逻辑上的改变,在实现上主要的改变就是在task_struct中加入tgid字段,这个字段就是用于表示线程组id的字段。在用户线程库方面,也使用NPTL代替LinuxThread。不同调度模型上仍然采用“1对1”模型,关于线程的调度模型,接下来会详细分析。
对于linux内核中对于多线程支持的变化历史请见以下这篇blog:http://blog.sina.com.cn/s/blog_631d3a630102uwn2.html
关于LinuxThread与NPTL的对比请见这篇文章:http://blog.csdn.net/ixidof/article/details/24579879
对于2.4内核中线程的实现,请见这篇文章http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/index.html
接下来来看看内核线程与用户线程之间的对应关系。首先从原理上出发,对内核线程与用户线程之间的对应关系进行分析。
现在在支持多线程的操作系统中一般采用三种调度模型,分别是“一对一模型”、“多对一模型”、“多对多模型”。
以上内容参考自下述两篇blog
http://www.cnblogs.com/zhaoyl/p/3620204.html
1)“一对一模型”
一对一模型中,每个用户线程都对应各自的内核调度实体。内核会对每个线程进行调度,可以调度到其他处理器上面。当然由内核来调度的结果就是:线程的每次操作会在用户态和内核态切换。另外,内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。但该模型的实用性还是高于多对一的线程模型。LinuxThread与NPTL都是采用这种模型。
在linux中通过LWP(lightweight process)作为线程概念的支持,轻量级线程(LWP)是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。每一个进程有一个或多个LWPs,每个LWP由一个内核线程支持。这种模型实际上就是恐龙书上所提到的一对一线程模型。在这种实现的操作系统中,LWP就是用户线程。
由于每个LWP都与一个特定的内核线程关联,因此每个LWP都是一个独立的线程调度单元。即使有一个LWP在系统调用中阻塞,也不会影响整个进程的执行。
轻量级进程具有局限性。首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user mode和kernel mode中切换。其次,每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。图也是盗的。
2)“多对一模型”
多对一线程模型中,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。用户态线程的很多操作对内核来说都是透明的,因为不需要内核来接管,这意味不需要内核态和用户态频繁切换。线程的创建、调度、同步处理速度非常快。当然线程的一些其他操作还是要经过内核,如IO读写。这样导致了一个问题:当多线程并发执行时,如果其中一个线程执行IO操作时,内核接管这个操作,如果IO阻塞,用户态的其他线程都会被阻塞,因为这些线程都对应同一个内核调度实体。在多处理器机器上,内核不知道用户态有这些线程,无法把它们调度到其他处理器,也无法通过优先级来调度。这对线程的使用是没有意义的!
3)“多对多模型”
用户线程库还是完全建立在用户空间中,因此用户线程的操作还是很廉价,因此可以建立任意多需要的用户线程。操作系统提供了 LWP 作为用户线程和内核线程之间的桥梁。 LWP 还是和前面提到的一样,具有内核线程支持,是内核的调度单元,并且用户线程的系统调用要通过 LWP ,因此进程中某个用户线程的阻塞不会影响整个进程的执行。用户线程库将建立的用户线程关联到 LWP 上, LWP 与用户线程的数量不一定一致。当内核调度到某个 LWP 上时,此时与该 LWP 关联的用户线程就被执行。