Linux中的线程

1.线程的基本概念

2.线程和进程的区别

线程安全

*线程的同步

线程的调度

线程的通信编程思想之多线程与多进程(1)——以操作系统的角度述说线程与进程_阳光日志-CSDN博客_多线程和多进程编程线程是什么?要理解这个概念,须要先了解一下操作系统的一些相关概念。大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任https://sunlogging.blog.csdn.net/article/details/46595285

进程和线程之间有什么根本性的区别? - 知乎我画了 30 多张图,万字长文,一起来深入理解进程和线程!进程我们编写的代码只是一个存储在硬盘的静态文…https://www.zhihu.com/question/44087187/answer/2062919643


学习线程前需要知道的知识——并行和并发的区别


1. 并发(concurrency):在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。其中两种并发关系分别是同步和互斥。(并发是指同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上有多个进程被同时执行的效果--宏观上并行,针对单核处理器)

互斥:进程间相互排斥的使用临界资源的现象,就叫互斥。
同步(synchronous):进程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。进一步的说明:就是前一个进程的输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待。具有同步关系的一组并发进程相互发送的信息称为消息或事件。(彼此有依赖关系的调用不应该同时发生,而同步就是阻止那些“同时发生”的事情)
其中并发又有伪并发和真并发,伪并发是指单核处理器的并发,真并发是指多核处理器的并发。


2.并行(parallelism):在单处理器中多道程序设计系统中,进程被交替执行,表现出一种并发的外部特种;在多处理器系统中,进程不仅可以交替执行,而且可以重叠执行。在多处理器上的程序才可实现并行处理。从而可知,并行是针对多处理器而言的。并行是同时发生的多个并发事件,具有并发的含义,但并发不一定并行,也亦是说并发事件之间不一定要同一时刻发生。(同一时刻,有多条指令在多个处理器上同时执行--针对多核处理器)


一、线程简介

1.为什么使用线程?

我们举个例子,假设你要编写一个视频播放器软件,那么该软件功能的核心模块有三个:

  • 从视频文件当中读取数据;
  • 对读取的数据进行解压缩;
  • 把解压缩后的视频数据播放出来;

对于单进程的实现方式,我想大家都会是以下这个方式:

对于单进程的这种方式,存在以下问题:

  • 播放出来的画面和声音会不连贯,因为当 CPU 能力不够强的时候,Read 的时候可能进程就等在这了,这样就会导致等半天才进行数据解压和播放;
  • 各个函数之间不是并发执行,影响资源的使用效率;

那改进成多进程的方式:

对于多进程的这种方式,依然会存在问题:

  • 进程之间如何通信,共享数据?
  • 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;

那到底如何解决呢?需要有一种新的实体,满足以下特性:

  • 实体之间可以并发运行;
  • 实体之间共享相同的地址空间;

这个新的实体,就是线程( Thread ),线程之间可以并发运行且共享相同的地址空间。


2.线程的实现

线程是操作系统能够调度和执行的基本单位,在Linux内核中线程也被称之为轻量级进程。从定义中可以看出,线程它是操作系统的概念,在不同的操作系统中的实现是不同的  但是作为学习 我们还是先来了解一下 线程的概念

主要有三种线程的实现方式:

  • 用户线程(User Thread:在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
  • 内核线程(Kernel Thread:在内核中实现的线程,是由内核管理的线程;
  • 轻量级进程(LightWeight Process

那么,这还需要考虑一个问题,用户线程和内核线程的对应关系。


1)用户线程和内核线程的关系:

1.首先,第一种关系是多对一的关系,也就是多个用户线程对应同一个内核线程:

多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此相对一对一模型,多对一模型的线程切换速度要快许多;此外,多对一模型对用户线程的数量几乎无限制。但多对一模型也有两个缺点

  • 如果其中一个用户线程阻塞,那么其它所有线程都将无法执行,因为此时内核线程也随之阻塞了
  • 在多处理器系统上,处理器数量的增加对多对一模型的线程性能不会有明显的增加,因为所有的用户线程都映射到一个处理器上了。


 

2.第二种是一对一的关系,也就是一个用户线程对应一个内核线程(NPTL就是采用这种):

LinuxThreads与NPTL均采用一对一的线程模型 只不过实现形式有点不一样

内核负责每个线程的调度,可以调度到其他处理器上面。Linux 2.6默认使用NPTL线程库,一对一的线程模型

对于一对一模型来说,一个用户线程就唯一地对应一个内核线程(反过来不一定成立,一个内核线程不一定有对应的用户线程)。这样,如果CPU没有采用超线程技术(如四核四线程的计算机),一个用户线程就唯一地映射到一个物理CPU的线程,线程之间的并发是真正的并发。一对一模型使用户线程具有与内核线程一样的优点,一个线程因某种原因阻塞时其他线程的执行不受影响;此处,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。

但一对一模型也有两个缺点

  • 许多操作系统限制了内核线程的数量,因此一对一模型会使用户线程的数量受到限制
  • 许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。

3.三种是多对多的关系,也就是多个用户线程对应到多个内核线程

多对一线程模型是非常轻量的,问题在于多个用户线程对应到固定的一个内核线程。多对多线程模型解决了这一问题:m个用户线程对应到n个内核线程上,通常m>n。Linux由IBM主导的NGPT采用了多对多的线程模型,不过现在已废弃

多对多模型结合了一对一模型和多对一模型的优点,将多个用户线程映射到多个内核线程上。

优点:1.一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行;2.多对多模型对用户线程的数量没有限制;3.在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高。

缺点:

  • 实现复杂

在现在流行的操作系统中,大都采用多对多的模型。
 


2)用户线程,内核线程,轻量级进程简介

1)用户线程如何理解?存在什么优势和缺陷?

用户线程是基于用户态的线程管理库来实现的,那么线程控制块(Thread Control Block, TCB 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。

所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。

**也就是说 用户线程就是函数库实现(也就是说,不管你操作系统是不是支持线程的,我都可以在你上面用多线程编程)。 

也就是说就算操作系统内核不支持线程 但是用户线程相当于我使用一个库函数 来模拟的线程

用户级线程的模型,也就类似前面提到的多对一的关系,即多个用户线程对应同一个内核线程,如下图所示:

用户线程的优点

  • 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
  • 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;

用户线程的缺点

  • 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。
  • 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
  • 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;

以上,就是用户线程的优缺点了。


2)那内核线程如何理解?存在什么优势和缺陷?

内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。

内核线程的模型,也就类似前面提到的一对一的关系,即一个用户线程对应一个内核线程,如下图所示:

内核线程的优点

  • 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
  • 分配给线程,多线程的进程获得更多的 CPU 运行时间;

内核线程的缺点

  • 在支持内核线程的操作系统中,由内核来维护进程和线程的上下问信息,如 PCB 和 TCB;
  • 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;

以上,就是内核线的优缺点了。


3)最后的轻量级进程(LWP)如何理解?

轻量级进程(Light-weight process,LWP)。

它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。每一个进程有一个或多个LWPs,每个LWP由一个内核线程支持。这种模型实际上就是恐龙书上所提到的一对一线程模型。

由于每个LWP都与一个特定的内核线程关联,因此每个LWP都是一个独立的线程调度单元。即使有一个LWP在系统调用中阻塞,也不会影响整个进程的执行。

Linux内核上没有线程的概念,CPU的调度是以进程为单位的。一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持

轻量级进程具有局限性。首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user mode和kernel mode中切换。其次,每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。

另外,LWP 只能由内核管理并像普通进程一样被调度,Linux 内核是支持 LWP 的典型例子。

在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息一般来说,一个进程代表程序的一个实例,而 LWP 代表程序的执行线程,因为一个执行线程不像进程那样需要那么多状态信息,所以 LWP 也不带有这样的信息。

在 LWP 之上也是可以使用用户线程的,那么 LWP 与用户线程的对应关系就有三种:

  • 1 : 1,即一个 LWP 对应 一个用户线程;
  • N : 1,即一个 LWP 对应多个用户线程;
  • N : N,即多个 LMP 对应多个用户线程;

接下来针对上面这三种对应关系说明它们优缺点。先下图的 LWP 模型:

1 : 1 模式

一个线程对应到一个 LWP 再对应到一个内核线程,如上图的进程 4,属于此模型。

  • 优点:实现并行,当一个 LWP 阻塞,不会影响其他 LWP;
  • 缺点:每一个用户线程,就产生一个内核线程,创建线程的开销较大。

N : 1 模式

多个用户线程对应一个 LWP 再对应一个内核线程,如上图的进程 2,线程管理是在用户空间完成的,此模式中用户的线程对操作系统不可见。

  • 优点:用户线程要开几个都没问题,且上下文切换发生用户空间,切换的效率较高;
  • 缺点:一个用户线程如果阻塞了,则整个进程都将会阻塞,另外在多核 CPU 中,是没办法充分利用 CPU 的。

M : N 模式

根据前面的两个模型混搭一起,就形成 M:N 模型,该模型提供了两级控制,首先多个用户线程对应到多个 LWP,LWP 再一一对应到内核线程,如上图的进程 3。

  • 优点:综合了前两种优点,大部分的线程上下文发生在用户空间,且多个线程又可以充分利用多核 CPU 的资源。

组合模式

如上图的进程 5,此进程结合 1:1 模型和 M:N 模型。开发人员可以针对不同的应用特点调节内核线程的数目来达到物理并行性和逻辑并行性的最佳方案。



二.Linux中的线程

需要了解的东西:

简单的说:内核级就是操作系统内核支持,用户级就是函数库实现(也就是说,不管你操作系统是不是支持线程的,我都可以在你上面用多线程编程)。

好了,那么,我们首先明白一件事:不管Linux还是什么OS,都可以多线程编程的,怎么多线程编程呢?程序员要创建一个线程,当然需要使用xxx函数,这个函数如果是操作系统本身就提供的系统函数,当然没问题,操作系统创建的线程,自然是内核级的了。

如果操作系统没有提供“创建线程”的函数(比如Linux 2.4及以前的版本,因为Linux刚诞生那时候,还没有“线程”的概念,能处理多“进程”就不错了),当然你程序员也没办法在操作系统上创建线程。所以,Linux 2.4内核中不知道什么是“线程”,只有一个“task_struct”的数据结构,就是进程。那么,后来随着科学技术的发展,大家提出线程的概念,而且,线程有时候的确是个好东西,于是,我们希望Linux能加入“多线程”编程。

要修改一个操作系统,那是很复杂的事情,特别是当操作系统越来越庞大的时候。怎么才能让Linux支持“多线程”呢?

首先,最简单的,就是不去动操作系统的“内核”,而是写一个函数库来“模拟”线程。也就是说,我用C写一个函数,比如 create_thread,这个函数最终在Linux的内核里还是去调用了创建“进程”的函数去创建了一个进程(因为OS没变嘛,没有线程这个东西)。 如果有人要多线程编程,那么你就调用 这个create_thread 去创建线程吧,好了,这个线程,就是用库函数创建的线程,就是所谓的“用户级线程”了。等等,这是神马意思?赤裸裸的欺骗?也不是。

为什么不是?因为别人给你提供了这个线程函数,你创建了“线程”,那么,你的线程(虽然本质上还是进程)就有了“线程”的一些“特征”,比如可以共享变量啊什么的,咦?那怎么做到的?当然有一套机制,反正人家给你做好了,你用就行了。

这种欺骗自然是不“完美”的,有线程的“一些”特征,但不能完全符合理论上的“线程”的概念(POSIX的要求),比如,这种多线程不能被分配到多核上,用户创建的N个线程,对于着内核里面其实就一个“进程”,导致调度啊,管理啊麻烦.....

为什么要采用这种“模拟”的方式呢?改内核不是一天两天的事情,先将就用着吧。内核慢慢来改。

怎么干改内核这个艰苦卓越的工作?Linux是开源、免费的,谁愿意来干这个活?有两家公司参与了对LinuxThreads的改进(向他们致敬):IBM启动的NGTP(Next Generation POSIX Threads)项目,以及红帽Redhat公司的NPTL(Native POSIX Thread Library),IBM那个项目,在2003年因为种种原因放弃了,大家都转到NPTL这个项目来了。

最终,当然是成功了,在Linux 2.6的内核版本中,这个NPTL项目怎么做的呢?并不是在Linux内核中加了一个“线程”,仍然和原来一样,进程 只不过使用了轻量级进程(其实,进程线程就是个概念,对于计算机,只要能高效的实现这个概念就行,程序员能用就OK,管它究竟怎么实现的),不过,用的clone实现的轻量级进程,内核又增加了若干机制来保证线程的表现和POSIX相同,最关键的一点,用户调用pthread库创建的一个用户线程,会在内核创建一个“线程”(轻量级进程LWP),这就是所谓的1:1模型。所以,Linux下,是有“内核级”线程(轻量级进程)的,网上很多说Linux是用户级线程,都是不完整的,说的Linux很早以前的版本(现在Linux已经是4.X的版本了)。


Linux使用的线程库

使用的线程库也就是前面我们学习用户线程里面提到的 通过函数库创建用户线程 中的函数库

  Linux提供两个线程库,Linux Threads 和新的原生的POSIX线程库(NPTL),linux threads在某些情况下仍然使用,但现在的发行版已经切换到NPTL,并且大部分应用已经不在加载linux threads,NPTL更轻量,更高效,也会有那些linux threads遇到的问题。

NPTL是一个1×1的线程模型,即一个线程对于一个操作系统的调度进程,优点是非常简单。

1)LinuxThread

LinuxThreads是用户空间的线程库,所采用的是线程-进程1对1模型将线程的调度等同于进程的调度,调度交由内核完成,而线程的创建、同步、销毁由核外线程库完成(LinuxThtreads已绑定到 GLIBC中发行)。

在LinuxThreads中,由专门的一个管理线程处理所有的线程管理工作。当进程第一次调用pthread_create()创建线程时就会先 创建(clone())并启动管理线程。后续进程pthread_create()创建线程时,都是管理线程作为pthread_create()的调用 者的子线程,通过调用clone()来创建用户线程,并记录轻量级进程号和线程id的映射关系,因此,用户线程其实是管理线程的子线程。

LinuxThreads只支持调度范围为PTHREAD_SCOPE_SYSTEM的调度,默认的调度策略是SCHED_OTHER。

用户线程调度策略也可修改成SCHED_FIFO或SCHED_RR方式,这两种方式支持优先级为0-99,而SCHED_OTHER只支持0。

  • SCHED_OTHER 分时调度策略,
  • SCHED_FIFO   实时调度策略,先到先服务
  • SCHED_RR     实时调度策略,时间片轮转

SCHED_OTHER是普通进程的,后两个是实时进程的(一般的进程都是普通进程,系统中出现实时进程的机会很少)。SCHED_FIFO、 SCHED_RR优先级高于所有SCHED_OTHER的进程,所以只要他们能够运行,在他们运行完之前,所有SCHED_OTHER的进程的都没有得到 执行的机会。


    在实现LinuxThread之前,系统内核并没有提供任何对线程的支持,实现LinuxThread时也并没有针对其做任何的改动,所以LinuxThread只能使用现有的系统调用来创建一些用户接口来尽量模仿POSIX定义的API的语义,这也就是导致了pthread之外的系统调用接口表现出来的行为跟POSIX的线程标准不一致,如最简单的在同一个进程里的不同线程里调用getpid()的结果不一致,具体原因后面详细说明。

创建线程

    LinuxThread使用的是1 * 1模型,即每一个用户态线程都有一个内核的管理实体跟其对应,这个内核对应的管理实体就是进程,又称LWP(轻量级进程)。这里先说一下,系统调用clone(),大家熟知的fork()函数就是调用clone()来实现父进程拷贝的从而创建一个新进程的。系统调用clone()里有一个flag参数,这个参数有很多的标志位指定了克隆时需要拷贝的东西,其中标志位CLONE_VM就是定义拷贝时是否使用相同的内存空间。fork()调用clone()时没有设置CLONE_VM,所以在内核看来就是产生了两个拥有不同内存空间的进程。而pthread_create()里调用clone()时设置了CLONE_VM,所以在内核看来就产生了两个拥有相同内存空间的进程所以用户态创建一个新线程,内核态就对应生成一个新进程。

    从上面就可以得到问题的答案了,为什么在同一个进程里面不同线程getpid()得到的结果不一样其他很多在pthreads(7) - Linux manual page里提到的不兼容特性都可以根据这一段的论述得到答案。

同步互斥

    内核没有提供任何对线程的支持,当然也就没有可供线程同步互斥使用的系统原语,但POSIX的线程标准里要求了诸多的互斥同步接口,怎么办呢?LinuxThread使用信号来模拟同步互斥,比如互斥锁,大致过程我猜如下:新建互斥锁的时候,在内核里把所有的进程mask掉一个特定信号,然后再kill()发出一个信号,等某个线程执行锁定时,就用sigwait()查看是否有发出的信号,如果没有就等待,有则返回,相当于锁定。解锁时就再kill()发出这个信号。那么LinuxThread使用的是哪几个信号来模拟这个同步互斥的呢?有的文档说是SIGUSR1和SIGUSR2,也有的说是某几个实时信号,具体可以看对应线程库的开发手册。必须知道你所使用的线程库内部使用哪几个信号,因为如果你的多线程程序里也使用了这几个信号的话,就会导致线程API工作混乱。

    从行分析就可以得出,LinuxThrea的同步互斥是用信号模拟完成的,所以效率不高且可能影响原有进程的信号处理,确实是个很大的缺陷。

信号处理

    LinuxThread的信号处理的行为可以说跟POSIX的标准是完全不一致的。因为信号的投递过程是发生在内核的,而每个线程在内核都是对应一个个单独的进程(不理解请看LinuxThread的创建线程一节),所以没有内核支持,所以当你对一个进程发送一个信号后,只有拥有这个进程号的进程才有反应,而属于这个进程的线程因为拥有不同的进程号而无法做出响应,从而LinuxThread无法做到跟POSIX定义的行为一致。

线程管理

    这里不得不说到LinuxThread的一个特性,当你创建第一个线程时,也就会自动创建一个管理线程,这个过程对用户是透明的。所以如果你还在使用LinuxThread线程库,当你创建一个线程后ps的结果会是有三个相同的进程而不是两个。这个管理线程的主要作用是管理线程的创建与终止,所以如果你把这个管理线程kill掉后,当你的某个线程退出后就会出现Zombie进程。另外,因为线程的创建与终止都要通过这个管理线程,在一个频繁创建与终止线程的程序这个线程很可能成为性能的瓶颈。

2)NPTL

NPTL使用了跟LinuxThread相同的办法,在内核里面线程仍然被当作是一个进程,并且仍然使用了clone()系统调用(在NPTL库里调用)。但是,NPTL需要内核级的特殊支持来实现,比如需要挂起然后再唤醒线程的线程同步原语futex.

NPTL也是一个1*1的线程库就是说,当你在使用pthread_create()调用创建一个用户线程后,在内核里就相应创建了一个调度实体(也就是轻量级进程LWP),在linux里就是一个新进程,这个方法最大可能的简化了线程的实现。

    因为没有内核支持的LinuxThread的线程实现的诸多缺陷,所以要想实现完全跟POSIX线程标准兼容的线程库,重写线程库是必然的,内核的修改也势在必行。有关NPTL实现也从线程创建,同步互斥及信号处理及线程管理几个方面来说明。

创建线程

    NPTL同样使用的是1 * 1模型,但此时对应内核的管理结构不再是LWP了。为了管理进程有进程组的概念,那内核要管理线程提出线程组的概念就是很自然的了。Linux内核只是在原来的进程管理结构(task_struct结构体)新增了一个TGIP的字段,如下图。当一个线程的PID等于TGID时,这个线程就是线程组长,其PID也就是这个线程组的进程号。线程组内的所有线程的TGID字段都指向线程组长的PID,当你使用getpid返回的都是TGID字段,而线程号返回的就是PID字段。那么NPTL下线程又是如何创建线程的呢?同样是使用clone()系统调度,不过新的clone()调用的flag参数新增了一个标志位CLONE_THREAD,当这个标志位设置的时候新创建的行为就是创建一个线程,内核内部初始管理结构时把TGID指向调用者的PID,原来的PID位置填新线程号(也就是以前的进程号)。

    从上,LinuxThread因为在内核是一个LWP而产生的跟POSIX标准不兼容的错误都消除了。

同步与互斥

    从LinuxThread中的线程同步与互斥中可看到使用信号来模拟的缺点,所以内核增加一个新的互斥同步原语futex(fast usesapace locking system call),意为快速用户空间系统锁。因为进程内的所有线程都使用了相同的内存空间,所以这个锁可以保存在用户空间。这样对这个锁的操作不需要每次都切换到内核态,从而大大加快了存取的速度。NPTL提供的线程同步互斥机制都建立在futex上,所以无论在效率上还是咋对程序的外部影响上都比LinuxThread的方式有了很大的改进。具体futex的描述可以man futex。

信号处理

    此时因为同一个进程内的线程都属于同一个进程,所以信号处理跟POSIX标准完全统一。当你发送一个SIGSTP信号给进程,这个进程的所有线程都会停止。因为所有线程内用同样的内存空间,所以对一个signal的handler都是一样的,但不同的线程有不同的管理结构所以不同的线程可以有不同的mask。后面这一段对LinuxThread也成立。

管理线程

    线程创建与结束的管理都由内核负责了,由LinuxThread的管理线程机制引出的问题已不复存在了。当然系统调度上仍是一个单独的线程而不是多个线程组成一个进程为整体进行调度的。这跟POSIX的标准还是稍有不同,不过这一缺点看起来无伤大雅。 


Linux中的线程概念

Linux内核中是没有线程这个概念的,而是轻量级进程的概念:LWP。一般我们所说的线程概念是C库当中的概念,也就是我们前面学的使用库函数模拟的线程 用户线程。

线程它是一种概念;操作系统的概念,在不同的操作系统中的实现是不同的

线程是操作系统能够调度和执行的基本单位

对于Linux操作系统而言,它对Thread的实现方式比较特殊。在Linux内核中(注意是内核中,并不是用户态),其实是没有线程的概念的,它把所有的线程当做标准的进程来实现,也就是说Linux内核,并没有为线程提供任何特殊的调度语义,也没有为线程实现特定的数据结构。取而代之的是,线程的概念只是一个与其他进程共享某些资源的进程。每一个线程拥有一个唯一的task_struct结构,Linux内核它仅仅把线程当做一个正常的进程,或者说是轻量级进程,LWP(Lightweight processes)。

对于其他的操作系统而言,比如windows,线程相对于进程,只是一个提供了更加轻量、快速执行单元的抽象概念。对于Linux而言,线程只是进程间共享资源的一种方式,非常轻量。举个简单例子,假设有一个进程包含了N个线程。对于那些显示支持线程的操作系统而言,应该是存在一个进程描述符,依次轮流指向N个线程。这个进程描述符指明共享资源,包括内存空间和打开的文件,然后线程描述它们自己独享的资源。相反的是在Linux中,只有N个进程,因此有N个task_struct数据结构,只是这些数据结构的某些资源项是共享的。

这里再总结一下:Linux线程是进程资源共享的一种方式,而其他操作系统,线程则是一种实现轻量、快速执行单元的抽象概念或者实体。这里再深入的理解一下,Linux中的线程和进程的区别。这也是诸多面试题中,最常见的一个。

linux下没有真正意义的线程,因为linux下没有给线程设计专有的结构体,它的线程是用进程模拟的,而它是由多个进程共享一块地址空间而模拟得到的。可以说,Linux系统内核并不认识线程,所有的任务执行都是以进程的形式存在的

  • 从Linux内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的。

线程切换只能在内核态完成,如果当前用户处于用户态,则必然引起用户态与内核态的切换


 线程是怎样描述的?

什么是主线程和工作线程?

一个进程可以包含多个线程 ,主线程可以看做是进程的化身 而工作线程 顾名思义就是工作的 也就是去执行我们指定的函数 也就是我们使用线程库函数pthread_create()创建的用户线程 

而所谓主线程与所属进程实际上是同一个task_struct,也能被CPU调度,因此主线程也是CPU调度的基本单位。

tgid(也就是所属进程的PID)相同的所有线程组成了概念上的“进程”,只有主线程在创建时会实际分配资源,其他线程(也既工作线程)通过浅拷贝共享主线程的资源。结合前面介绍的普通线程与轻量级进程,实现“进程是资源分配的基本单位”

上图中的tid其实指的是使用线程库创建的每个用户线程在内核对应的轻量级进程(LWP)的进程标识符PID  而用户线程的id就和线程库函数的实现有关了我们可以使用pthread_self()这个线程库函数来获取用户线程id.

每个用户线程实际上在内核中对应一个轻量级进程(LWP)也就是一个task_struct,工作线程拷贝主线程的task_struct,然后共用主线程的mm_struct。线程ID是在用task_struct中pid描述的,而task_struct中tgid是线程组ID,表示线程属于该线程组,对于主线程(也就是包含许多线程的进程)而言,其pid和tgid是相同的,我们一般看到的进程ID就是tgid。

即:

在这里插入图片描述

  

线程是CPU调度的基本单位、一个进程下可能有多个线程

linux加入了线程组的概念,让原有“进程”对应线程,“线程组”对应进程,实现“一个进程下可能有多个线程”:

  • 操作系统中存在多个进程组
  • 一个进程组下有多个进程(1:n)
  • 一个进程对应一个线程组(1:1)
  • 一个线程组下有多个线程(1:n)

task_struct(进程控制块PCB)中,使用pgid标的进程组,tgid标的线程组,pid标的进程或线程。假设目前有一个进程组,则上述概念对应如下:

  • 进程组中有一个主进程(父进程),pid等于进程组的pgid;进程组下的其他进程都是父进程的子进程,pid不等于pgid
  • 每个进程对应一个线程组,进程的pid等于线程组tgid。
  • 线程组中有一个“主线程”(勉强称为“主线程”,为的是与主进程对应;语义上绝不能称为“父线程”),pid等于该线程组的tgid线程组下的其他线程都是与主线程平级,pid不等于tgid

因此,调用getpgid返回pgid,调用getpid应返回tgid,调用gettid应返回pid(这个pid是内核中的轻量化进程(LWP)的pid)。使用的时候不要糊涂。

也就是说:

线程:pid  :线程所属的进程的PID

          tgid  :线程组PID, 也就是线程所属的进程的PID

          tid    :其实就是 用户线程在内核对应的轻量级进程(LWP)的PID。注意要和线程库                        函数pthread_self()获取的用户线程的id区别开来

进程:pid  :该进程的PID

          pgid  :进程所属的进程组的ID

         

      


为什么要有多线程?


举个生活中的例子, 这就好比去银行办理业务。 到达银行后, 首先取一个号码, 然后坐下来安心等待。 这时候你一定希望, 办理业务的窗口越多越好。 如果把整个营业大厅当成一个进程的话, 那么每一个窗口就是一个工作线程。

Linux中的线程的资源


Linux中一个进程中的多个线程:

1)共享以下资源:

  • 共享同一个进程的部分虚拟地址空间(共享区)
  •     执行的命令
  •     静态数据(例如全局变量等)
  •     打开文件的文件描述符
  •     信号处理函数
  •     当前工作目录
  •     用户ID(UID)
  •     用户组ID(GID)

2)每个线程私有的资源有:

  •     线程标识符(简称线程号,TID)
  •     程序计数器(PC)与相关寄存器
  •     堆栈区(局部变量、函数返回地址等)
  •     错误号errno
  •     信号掩码与优先级
  •     执行状态与属性

PS:如果进程退出了 则这个进程的所有线程都会退出  

 所以就有:

因为我们使用线程库pthread_create()函数创建用户线程 内核会对应创建一个轻量化进程(LWP)也就是我们说的“内核线程” 所以这是需要时间的 即使它可能很短

又因为如果进程退出 那么这个进程所有的线程都会退出 所以我们在使用线程库创建线程去执行函数时要确保进程不会退出 不然创建的线程还没执行完函数就因为进程的退出而退出了


多线程如何避免调用栈混乱的问题?

工作线程和主线程共用一个mm_struct,如果都向栈中压栈,必然会导致调用栈出错。

实际上工作线程压栈是压了共享区,该共享区包含了许多线程独有的资源。如图:

在这里插入图片描述

每一个线程,默认在共享区中占有的空间为8M,可以使用ulimit -s修改。


线程带来的优势

  • 线程会共享内存地址空间。
  • 创建线程花费的时间要少于创建进程花费的时间。
  • 终止线程花费的时间要少于终止进程花费的时间。
  • 线程之间上下文切换的开销, 要小于进程之间的上下文切换。
  • 线程之间数据的共享比进程之间的共享要简单。
  • 充分利用多处理器的可并行数量。(线程会提高运行效率,但当线程多到一定程度后,可能会导致效率下降,因为会有线程调度切换。)
     

线程带来的缺点

  1. 健壮性降低:多个线程之中, 只要有一个线程不够健壮存在bug(如访问了非法地址引发的段错误) , 就会导致进程内的所有线程一起完蛋。
  2. 线程模型作为一种并发的编程模型, 效率并没有想象的那么高, 会出现复杂度高、 易出错、 难以测试和定位的问题。

注意

1.并不是只有主线程才能创建线程, 被创建出来的线程同样可以创建线程。

2.不存在类似于fork函数那样的父子关系, 大家都归属于同一个线程组, 进程ID都相等, group_leader都指向主线程, 而且各有各的线程ID。

 通过group_leader指针, 每个线程都能找到主线程。 主线程存在一个链表头,后面创建的每一个线程都会链入到该双向链表中。

3.并非只有主线程才能调用pthread_join连接其他线程, 同一线程组内的任意线程都可以对某线程执行pthread_join函数。

4.并非只有主线程才能调用pthread_detach函数, 其实任意线程都可以对同一线程组内的线程执行分离操作。

线程的对等关系:

在这里插入图片描述


为什么Linux中的线程相比进程能减少开销?

Linux中的线程相比进程能减少开销,体现在

  • 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
  • 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
  • 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
  • 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;

所以,线程比进程不管是时间效率,还是空间效率都要高。


Linux中的线程与进程的区别


    一个进程可以拥有多个线程,每个线程共享该进程内的系统资源。由于线程共享进程的内存空间,因此任何线程对内存内数据的操作都可能对其他线程产生影响,因此多线程的同步与互斥机制是十分重要的。

    线程本身只占用少量的系统资源,其内存空间也只拥有堆栈区与线程控制块(Thread Control Block,简称TCB),因此对线程的调度需要的系统开销会小得多,能够更高效地提高任务的并发度。

简单总结,Linux中进程与线程的区别主要在以下几点:

    1)线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

    2)地址空间与系统资源:进程间的地址空间与系统资源互相独立,互不干扰;同一进程内各线程共享地址空间与系统资源。一个进程内的线程对其他进程是不可见(私有)的。

    3)通信手段:由于进程间互相独立,因此进程间通信必须借助某些手段。进程间通信手段主要有管道、信号、共享内存、SystemV等;而线程共享进程的资源与空间,因此同一个进程的线程间可以直接读写进程的数据段(例如全局变量等)进行通信,不过需要使用同步与互斥机制保证数据一致性。

    4)调度与切换:进程占用系统资源较多,因此切换进程时开销较大;而线程占用系统资源较小,因此切换进程时开销较小。

进程是资源分配的基本单位、线程共享进程的资源

普通进程需要深拷贝虚拟内存、文件描述符、信号处理等;而轻量级进程之所以“轻量”,是因为其只需要浅拷贝虚拟内存等大部分信息,多个轻量级进程共享一个进程的资源。

线程是CPU调度的基本单位、一个进程下可能有多个线程

linux加入了线程组的概念,让原有“进程”对应线程,“线程组”对应进程,实现“一个进程下可能有多个线程”:

  • 操作系统中存在多个进程组
  • 一个进程组下有多个进程(1:n)
  • 一个进程对应一个线程组(1:1)
  • 一个线程组下有多个线程(1:n)

task_struct(进程控制块PCB)中,使用pgid标的进程组,tgid标的线程组,pid标的进程或线程。假设目前有一个进程组,则上述概念对应如下:

  • 进程组中有一个主进程(父进程),pid等于进程组的pgid;进程组下的其他进程都是父进程的子进程,pid不等于pgid
  • 每个进程对应一个线程组,进程的pid等于线程组tgid。
  • 线程组中有一个“主线程”(勉强称为“主线程”,为的是与主进程对应;语义上绝不能称为“父线程”),tid等于该线程组的tgid线程组下的其他线程都是与主线程平级,tid不等于tgid

因此,线程 调用getpgid返回pgid(也就是线程所属的进程所属的进程组id),调用getpid应返回tgid(也就是所属进程的id),调用gettid应返回pid(这个pid是内核中的轻量化进程(LWP)的pid)。使用的时候不要糊涂。

进程下除主线程外的其他线程是CPU调度的基本单位,这很好理解。而所谓主线程与所属进程实际上是同一个task_struct,也能被CPU调度,因此主线程也是CPU调度的基本单位。

tgid相同的所有线程组成了概念上的“进程”,只有主线程在创建时会实际分配资源,其他线程通过浅拷贝共享主线程的资源。结合前面介绍的普通线程与轻量级进程,实现“进程是资源分配的基本单位”。

举个栗子

  • 存在3个进程组111、112、113
  • 进程组111下有1个父进程111,单独分配资源
  • 进程111下有1个线程111,共享进程111的资源
  • 进程组112下有1个父进程112,单独分配资源
  • 进程112下有2个线程112、113,共享进程112的资源
  • 进程组113下有1个父进程113,1个子进程115,各自单独分配资源
  • 进程113下有2个线程113、114,共享进程113的资源
  • 进程115下有3个线程115、116、117,共享进程115的资源

小结

现在再来理解linux中的进程与线程就容易多了:

  • 进程是一个逻辑上的概念,用于管理资源,对应task_struct中的资源
  • 每个进程至少有一个线程,用于具体的执行,对应task_struct中的任务调度信息
  • 以task_struct中的pid区分线程,tgid区分进程,pgid区分进程组


Linux中的线程的上下文切换

在前面我们知道了,线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位

所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。

对于线程和进程,我们可以这么理解:

  • 当进程只有一个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;

另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

Linux中的线程 上下文切换的是什么?

这还得看线程是不是属于同一个进程:

  • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;

所以,线程的上下文切换相比进程,开销要小很多。


 


用户态进程/线程的创建 fork/vfork/pthread_create_Peter的专栏-CSDN博客forkfork 函数创建子进程成功后,父进程返回子进程的 pid,子进程返回0。具体描述如下:fork返回值为-1, 代表创建子进程失败fork返回值为0,代表子进程创建成功,这个分支是...https://peter.blog.csdn.net/article/details/118004707

上面这篇博客介绍了用户态创建进程和线程的方式,以及各个方式的特点。关于其底层的实现本质,我们后面会详细讲解。这里先提供一下三者之间的关系,可见三者最终都会调用 do_fork 实现。

在Linux中使用fork创建进程,使用pthread_create创建线程。两个系统调用最终都都调用了do_dork,而do_dork完成了task_struct结构体的复制,并将新的进程加入内核调度。这也印证了Linux中的线程其实就是轻量级的进程 。内核态没有进程线程的概念,内核中只认 task_struct 结构,只要是 task_struct 结构就可以参与调度


二.Linux中的线程编程——线程的创建、控制与删除

对于线程来说,线程编程主要考虑两部分工作:第一部分是线程的创建、控制与删除;第二部分是线程的同步与互斥。二者都可以使用NPTL线程库来实现。

在Linux系统中,多线程编程是通过第三方的线程库NPTL实现的。

/**********NPTL简介******************/

本地POSIX线程库(New POSIX Thread Library,简称NPTL)是早期Linux系统内Threads模型的改进,它可以让Linux内核高效运行使用POSIX风格编写的线程程序。有测试证明,使用NPTL启动10万个线程大概只需2秒时间,而未使用NPTL则需要15分钟。

NPTL最先发布在RedHat9.0版本中(2003年),老式POSIX线程库的效率太低,因此从这个版本开始,NPTL开始取代老式Linux线程库。

NPTL有以下特性:

    采用1:1线程模型

    显著提高运行效率

    信号处理效率更高

使用NPTL线程库,需要添加头文件#include<pthread.h>,并且在编译时添加线程库-lpthread

/**********NPTL简介end***************/

//在使用NPTL线程库编程相关函数时,需要额外注意pthread_t类型,该数据类型是线程独有的数据类型,专门用于表示线程标识符,不能使用int类型代替。如果需要输出pthread_t类型数据,使用格式控制符%u(不过可能会出现warning)。

**使用NPTL线程库编程操作的对象基本上都是用户线程

线程中使用到的数据类型:

    改变互斥量属性,条件变量属性,线程属性的步骤是类似的可以类比  

1、*创建线程函数pthread_create()

pthread_create()_DSMGUOGUO的博客-CSDN博客多线程编程C语言使用pthread_create()函数完成多线程的创建,pthread_create()函数共有四个参数。这四个参数分别为:1. pthread_t *第一个 参数负责向调用者传递子线程的线程号2. const pthread_attr_t *第二这个参数负责控制线程的各种属性,这也是线程在创建的时候,最为复杂的一个参数。下面是这个结构体的定义:线程

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux的多线程实际上是通过进程来模拟实现的。在Linux,多个线程是通过共享父进程的资源来实现的,而不是像其他操作系统那样拥有自己独立的线程管理模块。因此,在Linux所谓的“线程”其实是通过克隆父进程的资源而形成的“线程”。这也是为什么在Linux所说的“线程”概念需要加上引号的原因。 对于Linux线程,需要使用线程库来进行管理。具体来说,Linux线程ID(pthread_t类型)实质上是进程地址空间上的一个地址。因此,要管理这些线程,需要在线程库进行描述和组织。 由于Linux没有真正意义上的线程,因此线程的管理和调度都是由线程库来完成的。线程库负责创建线程、终止线程、调度线程、切换线程,以及为线程分配资源、释放资源和回收资源等任务。需要注意的是,线程的具体实现取决于Linux的实现,目前Linux使用的是NPTL(Native POSIX Thread Library)。 总结来说,Linux的多线程是通过进程来模拟实现的,线程共享父进程的资源。线程的管理和调度由线程库完成。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Linux —— 多线程](https://blog.csdn.net/sjsjnsjnn/article/details/126062127)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值