本文主要介绍线程的相关概念与基础控制
什么是线程&&线程的相关知识
按照书本上的定义,线程就是进程内部的一个执行分支,而线程是cpu调度的基本单位。
如果直接按照书本上的定义理解,线程这个概念是比较模糊的,而且我们也没法看出其与进程的具体区别。下面以Linux操作系统为例,解释一下线程。
一般在单进程的程序中,我们一般是让正文区的代码依次串行运行,也就是从上到下依次运行(我们的代码是由一个一个函数组成的,我们运行函数的过程是从上至下的)。但是,这种方式的运行效率会比较低,所以我们就想让操作正文段的代码拆结多个部分,并行运行。就比如,我们需要在代码中执行四个函数,如果这四个函数没有必然的先后调用关系,我们就可以考虑把这四个函数分成两份,并发执行这两份代码,这样可以提高我们运行效率。前面我们使用多进程来实现类似的功能(实际上还是有点区别,这里暂时先这样认为)。不过多进程实现该功能还是有点缺陷的,因为使用多进程就需要创建task_struct、进程地址空间、页表等等一系列相关的数据结构。所以就有了线程的存在。
实际我们需要的就是多条执行流去执行一段代码,为了节约时间和空间成本,我们就可以创建多个task_struct,然后让这些task_struct指向同一份地址空间。同时,我们需要将地址空间中的正文区分成若干份,让每个task_struct 指向的正文区代码都是原来代码的一部分。而这种方式创建的进程就称为线程。
什么我们要这样设计linux的线程呢?
在OS存在很多的进程,进程中可能包含很多的执行流,也就是进程。那么OS要不要对这些线程进行管理呢?当然需要,为了方便对线程的管理,我们就需要用结构体对各个线程进行描述管理。我们一般称该结构体位struct TCB。线程作为进程的一个执行分支,肯定比进程多得多。如果对线程再单独设计调度算法和相关的数据结构,就会造成系统变得更加复杂,这无疑增加了系统的维护成本。为了避免上述缺点,linux的设计者采用了上面提到的线程创建方式,即复用进程的创建代码。这种方案可行,也是因为线程在很多方面其实和进程是非常相似的。当然,也有别的操作系统为线程创建了一套单独的运行体系,比如windows.
通过下面两个图,我们可以重新认识一下进程。以前我们说的进程都是只有一个线程的进程,而我们今天所说的进程,包含多个线程,也就是多个执行流。从内核的角度来看,进程就是承担分配系统资源的实体。
相关的调度问题
在linux操作系统中,由于我们使用的是创建新的task_struct,共享地址空间的方式来建立线程。所以对于cpu而言,进程和线程并没有区别,cpu只需要根据task_struct对相应进程或线程进行调度即可。所以在linux中,是没有线程的概念的,我们统一称为轻量级进程。
如何对多执行流的代码进行划分?
前面我们提到,线程作为进程的执行流,只会占有一部分的正文区代码。下面介绍一下执行流代码的划分,在此之前,我们需要简单了解一些地址空间的相关知识。
首先我们需要了解一个概念,那就是页帧或页框(这里两者之间的概念还是有点不同的,不过这里就不做区分了)。
一般情况下,物理内存都是以4kb为单位进行划分的,有些操作系统可以自行设定。为了管理这些数据和空间,我们就需要对其进行描述组织。我们这里用struct Page结构体对其进行描述,其中就包含了这个页帧的属性。比如使用状态,最后修改时间等等。不过这里我们重点关注的是使用状态,这里一般OS都会使用宏(整数)来标定其使用的状态。在4GB的内存中,我们可以将物理内存分成1048576份(1024*1024*1024 / 1024 /4) 。然后创建一个1048576大小的数组num,类型为 struct Page。这样我们对内存的管理就变成了对num数组的增删查改。struct Page的大小不会太大,num这个数组不会占据内存太多空间。
下面介绍一下页表
页表的映射方式其实并不是像我们画的键值映射,如果我们采用键值来存储映射关系,每条指令都需要至少十几个字节进行存储,那么所需的内存将会是一个非常大的数字,这根本就不现实。其实页表分为页目录和页表。
以上图为例,我们将32位机器下的虚拟地址的存储字节分为三段。第一段10字节,页目录,这个页目录用于查询该指令所在页表,大小为1024;第二段10字节,用于查询该指令对应的物理内存在该张页表中的存储位置,大小为1024;第三段12字节,第二段中查询的物理内存地址为页帧的首地址,我们只是访问其中一段数据,那我们就需要通过加偏移量的方式来进行访问。而偏移量的大小正好等于4kb的大小,这也完美地覆盖了页帧内的所有地址,确保用户能访问所有的数据。
回到开始的问题,我们想让不同线程看到不同的代码段,本质上就是让不同的线程看到各自的页表即可。
以上就是所有内容,如果不对之处,还望各位大佬指正,谢谢!!!