Linux操作系统之线程

为什么要使用线程/h2>
  • 考虑,进程是对一个正在运行的应用程序的抽象,例如我们电脑中正在运行的Goland和IDEA两个编译器对应两个不同的进程。而考虑,线程是进程内部的一个执行分支,一个执行流,它共享进程的地址空间,文件,数据,代码等。有时我们会遇到很难解决的问题,例如Goland中,我从键盘输入字符和编译器中对语法进行检查,如果只有一个的话,那么我们必须把字符全部输入到编译器里面去了之后,再开始进行语法检查,这样带给用户的体验感是很差的,因此我们需要一个执行的操作。也就是引入了多线程的概念,,与多进程不同的是,Goland和IDEA是两个不同的进程,需要实现和模拟某种并行的场景,但是Goland打开的文件,代码,数据等和IDEA是不同的,如果使用多进程的方式来进行某一个编译器内部的沟通的话,那么每次就要替换掉很多数据,替换程序地址空间,替换页表,刷新内存高速缓存,遇到缺页中断等问题。因此我们需要的是在一个进程内部模拟并发的场景,也就是线程做的事情。通俗点儿来讲,进程复杂软件与软件之间的并行,线程负责同一个软件内部不同功能的并行

  • 因为线程是进程内部的一个执行流,因此它比进程更加轻量级,创建,切换和销毁非常容易。比一个进程要块10-100倍。

  • 如果是CPU密集型操作的话,线程并不会很有优势,因为**CPU已经很快(巨快无比)**了,如果让多个线程去执行一个CPU密集型任务的话,那么创建线程,调度线程等开销可能比单线程运行更慢了,这也是为什么不会有人拿多线程去计算1-100的和的原因。但是,如果这个任务真的特别庞大,导致CPU也无法轻松解决的话,用多线程也未尝不可。例如:加密,大数据等。多线程主要的优势在于IO密集型操作的任务。例如,每一个语句都要插入数据库,进行了大量的IO操作。为什么多线程适合IO密集型操作呢为多线程可以让IO(磁盘IO和网络IO)等待的时间进行重合。如图,我们来了解一下一个IO发生了什么:

    Linux操作系统之线程

    我们在文章里面已经学习过了进程的相关概念,不难理解,虚线框里面的所有东西全部统一称之为,那么此时,线程就是所谓的一个而已。我们创建一个线程就是照着PCB的模板创建一个PCB而已,程序地址空间,页表,代码,数据等我们不需要去创建,只需要创建一个结构体,可以看到线程比进程轻量级和迅速多少了。当然,线程内部有自己的,,,,以便于区分其他线程(task_struct),而其他的东西全部都是共享一个进程的。

    CPU在调度的适合以task_struct为单位,至于是线程还是进程,它不关心,也无法区分,CPU只认PCB,并且机械的,迅速的把里面的东西进行执行。所以我们得出一个结论CPU调度的基本单位是task_struct,你可以理解成线程,我们不可以说task_struct是进程,只能说它是线程,原因是进程是一个很大的概念,虚线框内一整块内容全部都是进程。

    对于Linux操作系统的这种设计思想。和Windows不同的是,Linux没有专门的去描述和组织线程专用的数据结构,不需要为它设计任何算法和维护程序,只需要在意task_struct是如何调度的,这也是Linux操作系统非常的原因。

    既然线程是由tast_struct模拟的,也就是说对于Linux操作系统来说,实际上是不存在线程这个概念的,他们只认识task_struct,线程只是从用户的角度去理解的。因此,Linux操作系统的系统调用接口不会提供直接创建线程,调度线程,销毁线程的接口,它只提供了如何操作task_struct的相关接口,这对程序猿的要求非常高!很困难。因此Linux系统程序猿给我们在的层面封装了一套接口,也就是大名鼎鼎的

    等等接口,这些接口实际上是用户层的函数包,Linux操作系统对于这些函数的存在是不可知的,它甚至不知道有这些接口的存在。

    所以我们用上述函数包在用户的级别创建出来的线程叫做用户级别线程。

    用户级别线程和内核级别线程

    有两个方法来实现线程包

    • 把线程包放在用户的空间,内核对线程包的存在毫无感知
    • 内核级别线程

    我们刚才介绍的东西就是用户级别的线程。

    用户级别的线程在进程内部会记录一张线程表,内核级别的线程在内核会记录一张线程表。

    Linux操作系统之线程

    当然,这个图是不严谨的,但是为了知识的理解,暂时画成这个样子,后面会做详细说明。

    • 用户态的线程通过把线程的相关数据存到内核态的线程中去,然后内核态的线程作为真正的调度单位被CPU调度。

    因此操作系统这样的设计方案充分利用了用户态线程和内核态线程的优点。

    用户态线程的优点:

    • 最然而易见的优点就是用户态的线程是一个函数包,它可以用在不支持多线程的操作系统中,可移植性很强
    • 用户级别的线程的创建,创建,销毁,切换等都是本地的方法,是本地,用户态的过程,因此不涉及到内核态与用户态的切换,不需要上下文切换(上下文切换是内核在CPU上对线程或者进程进行切换),也不需要刷新内存高速缓存
    • 并且由于它是用户自己定义函数包,所以可以允许每一个进程有自己定制的调度算法。例如有一个垃圾回收线程不需要担心线程会在不合适的时刻停止。

    用户线程的缺点:

    • 假如多个用户级别线程对应一个内核级别线程,而因为内核级别线程才是CPU调度的基本单位,而内核根本不知道用户线程的存在,所以当因为某些情况,例如IO,或者缺页中断等,用户态线程发生阻塞的时候,其实也就是内核的线程正在被阻塞,其余所有的用户级别线程就只能等这这个线程执行完毕之后才可以切换。因为操作系统一方面不知道你的存在,无法在你被阻塞的时候调度其他线程,另一方面你是用户级别的,没有内核级别的权限。
    • 线程一旦被执行,其他线程就无法运行,除非第一个线程主动放弃CPU,因为你没有权限让CPU调度你,用户级别而已。
    • 你可以知道,一个内核级别线程对应了很多用户级别线程,所以时间片分配的比较少,执行很慢
    • 最大的争议就是,多线程最关键的意义本来就是IO密集型操作,也就是在别人阻塞的时候让其他任务填充等待时间,而用户级别的线程一旦阻塞,会影响整个进程,这让多线程存在的意义得到了质疑。

    内核级别的优点:

    • 当你阻塞的时候,操作系统可以根据情况选择另外一个线程运行
    • 时间片分配比较多

    内核级别的缺点:

    • 在不支持多线程的操作系统里面是不支持内核级别线程的
    • 创建,调度,销毁线程需要用到系统调用,由用户态切换到内核态,代价很大,因此操作系统会采用的方式,当线程被撤销的时候,没有真正的被销毁,只是标记成了不可运行的。但是内核数据结构没有被影响,一旦有需要,会重新启动。

    线程模型

    针对上述的场景,操作系统采用的方法是多对多:

    Linux操作系统之线程

    声明,此图片摘自《小林coding》

    • 这样并发性很高,因为每一个用户线程都对应一个内核线程,也就是说当一个用户级别线程被阻塞的时候,其他的完全不影响,可以实现并发性,并发性很强
    • 缺点是内核线程太多,系统开销太大

    多对一线程模型:

    Linux操作系统之线程

    PID是我们用户级别的线程的编号,LWP是轻量级线程的编号。可以看到此时PID == LWP。

    那么什么是轻量级线程LWP呢/p>

    轻量级进程(Light-weight process,LWP)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持,而且 LWP 是由内核管理并像普通进程一样被调度

    划重点:轻量级进程和内核线程是一一对应的。

    Linux操作系统之线程

    mmap区域里面会映射动态库的地址,好让线程得以使用,mmap里面的pthread_t pid其实就是调用的返回值,这个返回值是一个地址,根据这个地址,我们可以在mmap区域离找到线程的相关信息,里面的struct pthread里面就记录了LWP的编号。

    线程带来的问题

    线程是多个执行流的,所以会出现相互竞争资源的问题,也就是线程安全问题。接下来的文章我们会重点讲解多执行流的安全问题

    文章知识点与官方知识档案匹配,可进一步学习相关知识CS入门技能树Linux入门初识Linux24962 人正在系统学习中

    来源:胡桃姓胡,蝴蝶也姓胡

    声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年9月25日
下一篇 2022年9月25日

相关推荐