线程基础概念

目录

背景知识

堆空间的划分

缺页中断

虚拟地址到物理地址的映射

线程是什么

线程是什么

理解线程

创建线程

再次理解线程

重新理解进程


背景知识

下面的三个问题实际是堆虚拟地址空间的再一次理解!

堆空间的划分

再虚拟地址空间的那一块,我们知识粗浅的说了一下进程地址空间。

进程地址空间分为用户级地址空间与内核级地址空间,用户级地址空间由低到搞分别是:

代码区、已初始化全局数据区、未初始化全局数据区、堆区、栈区、命令行参数与环境变量区。

而已初始化和未初始化全局数据区其实都是全局数据区,知识划分力度稍微细一点。

还有就是堆栈是相对而生的,堆栈中间还有一大段镂空,其中 中间的这一段是共享区,共享区里面一般放的都是加载的动态库,或者是共享内存。

这就是我们之前说了关于进程地址空间里面区域的内容。

那么我们现在还有一个问题,堆区是我们 malloc / new 出来的,那么我们只是向OS申请了空间,我们并没有给OS说我们什么时候使用完,我们申请后也只是有一个起始位置的地址,那么操作系统再释放的时候能知道我们当时申请了多少吗?

之前我们对堆区的划分是粗粒度的,实际上堆区还可以更细力度的划分,其实在操作系统中由一个结构体,专门管理堆空间的使用。

struct vm_area_struct
{
    unsigned long start;
    unsigned long end;
    struct page*  vm_area_struct, *  vm_area_struct;
}

其中就是这个结构体专门来管理堆空间的使用,而上面的字段是里面比较重要的字段,当然里面还有其他的字段。

而操作系统选择使用双链表的结构,将堆空间可以链接起来,而且里面也有 start 和 end 标记,所以其实可以对堆空间进行细粒度的划分。

缺页中断

我们知道,想要启动一个程序需要OS为该进程创建对应的内核数据结构,然后将代码和数据加载到内存,那么加载的时候是怎么加载,加载到哪里?一次加载多少呢?

这里我们就要说一下关于物理内存了,和编译后的可执行程序了(文件)。

其实物理内存并不是我们看到的一整块,实际上,物理内存是被划分为一块一块的,每一块的大小为4KB。

那么光有划分一定是不够的,而且操作系统知道哪些物理内存被使用过吗?所以OS一定需要将这些物理内存管理起来——先描述,在组织。

struct page
{
    unsigned long flag; // 32 位 可以用来表示 32 中状态,例如:是否被使用等...
    automatic_t count;  // 计数器 表示有多少个在使用该内存...
    ...
}

可以使用这个描述起来,里面还会有其他字段。

然后我们可以使用一个数组然后将该物理内存一个一个表示起来。

// 假设物理内存现在就是 4 G
// 然后被划分为 4KB 大小,所以最多可以被划分为 4G = 1024 * 1024 * 1024 * 4, 4KB = 1024 * 4
// 4G / 4KB = 1,048,576 块
​
int array[1,048,576];// 所以对于物理内存的管理,现在就变成了对数组的管理

其实每一块被划分号的物理内存就叫做 “页框”。

所以我们已经知道物理内存实际上是被划分的,而且也知道哪些物理内存被使用了,所以我们就可以加载到没被使用的物理内存上。

那么既然可执行程序实际上就是一个在磁盘上的文件,那么怎么加载呢?每一次加载多少?

既然物理内存被划分为了 4KB,那么可执行程序当然也一次性被加载 4KB 是最好的,所以在编译的时候,其实可执行程序也就被划分为了 4KB 大小。

所以可执行程序一次被加载也是按照 4KB 为单位。

而可执行程序被划分为 4KB 的单位,被称为 “页帧”。

页表

前面我们在谈进程地址空间的时候,一直说的是CPU用的是虚拟内存,也就是需要通过页表来映射。

       

在我们知道的页表中,至少有这三个字段,其中就是虚拟内存,物理内存的映射,还有就是 flag 标记位,看是否能访问,如果能访问的话,就正常访问,如果不可以的话,那么就发生缺页中断,如果这块内存是被置换出去了,但是可以访问,那么就需要将在磁盘上的数据先换入到内存中,然后访问。

虚拟地址到物理地址的映射

既然页表是虚拟内存到物理内存的映射,那么以 32 位操作系统来说,地址总线一共有 32 位,就是全 0 到全 1 ,那么一共下来就是有 2 ^ 32 次方个地址。

那么以上面的页表结构来说的话,至少也需要9字节来表示一行,那么又因为一共有2 ^ 32 位地址,然后再乘 9 字节,那么是需要很多内存才可以表示一张页表,但是每个进程都会有一张页表,那么物理内存一定是存储不下的,所以那应该如何存储呢?

0000 0000 00  00 0000 0000   0000 0000 0000

其实 32 位操作系统和 64 位操作系统的虚拟内存到物理内存的映射原理都是相同的,所以我们还是以 32 位为例:

一次性将2 ^ 32 次方的地址存入显然是不太现实的,而操作系统也确实没有这么干。

实际上,操作系统是将前 10 个比特位映射,将前 10 个比特位映射的页表叫做 ”一级页表“, 在将后面 10 个比特位继续映射形成“二级页表”,而一级页表的 v 就是二级页表,那么还剩下 12 个比特位,还需要映射吗?

实际上就不需要映射了,这时候,我们就可以通过一个地址的前 10 个比特位就可以找到 二级页表,然后再通过 后10个比特位找到对应的 二级页表,由于物理内存是以 4KB 划分的,而后面 12 个比特位刚好是 4KB,所以当找到对应的 二级页表的时候,就可以通过最后的 12 个比特位找到对应的地址。

线程是什么

线程是什么

线程是什么,再很多操作系统的书上往往总结下来可以用一句话来概括:

线程就是轻量级进程,一般是再进程内运行的,是CPU调度的基本单位。

上面就是线程的内核中的一般被总结的。

但是这样其实是说不清楚线程是什么的,再 Linux 下,我们还是需要从进程的调度去看待线程。

理解线程

那么什么是线程呢?

之前我们再学习进程的时候,我们一直再说,进程实际上就是内核的数据结构加代码和数据。

然后我们继续学习进程,我们就学习的是里面有哪些数据结构,里面的这些数据结构分别管理的是哪些模块。

例如:mm_struct 表示的是进程的地址空间、struct file_struct 里面就是关于文件的内容等等...

而且再前面,我们说的是CPU调度的基本单位就是PCB,那么我们说的有没有问题呢?

其实在前面,我们说的一直都是单执行流的进程。

现在有一个问题,当我们在创建一个进程的时候,那么我们会做什么呢?

首先创建一个进程,操作系统会为给进程创建对应的内核数据结构,然后将进程的代码和数据加载到内存即可,而内核数据结构里面有些什么呢?

包括:PCB、进程地址空间、文件相关内容、虚拟到物理转化的页表等...

那么创建一个进程的时候当然也需要将这些东西也一同创建,所以创建一个进程的开县还是很大的。

而且每个进程都有自己的页表,进程地址空间,然后每个进程都指向自己的进程地址空间,这样就可以实现每个进程独立性,那么假设我们现在只创建进程的PCB,但是不创建那些页表,进程地址空间等等呢?

我们就是让新创建的进程使用老进程的进程地址空间,还有页表那会怎么样呢?

那么我们只创建 PCB 不创建其他的数据结构,然后让他们的PCB 共用一个进程地址空间和页表

 

那么此时,CPU 还是调度的是 PCB ,但是这些PCB 所指向的进程地址空间以及用的页表都是相同的,那么如果我们让这些PCB可以执行不同部分的代码呢?

所以我们其实就可以将这些 PCB 理解为线程,而这些线程的创建也只需要创建一个 PCB 就可以了,并不需要创建进程的其他数据结构,也不需要向系统在申请资源。

所以通过上面的藐视,我们也就理解了为什么 Linux 的线程被称为轻量级进程了。

那么在 Linux 中有真实的线程吗?没有!

在Linux中由于进程和线程基本是一样的,所以 linux 中采用了复用,使用进程的 PCB 来描述线程,随意在 Linux 中实际上是没有线程的,用进程来代替。

但是当CPU调度的时候,还是以PCB来调度的。

创建线程

上面既然我们说了一下关于什么是线程,并且浅浅的理解了一下线程,而且我们也知道线程是CPU调度的基本单位,下面我们来实操一下,也就是创建线程,然后让他们分别做一些事情。

NAME
       pthread_create - create a new thread
​
SYNOPSIS
       #include <pthread.h>
​
       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
  • 该函数就是创建一个线程的函数。

  • 说这个函数之前,我们先说一下,由于Linux中没有提供正真意义上的线程,所以也是没有线程的一系列接口的,只有关于轻量级进程的一系列接口,但是由于由于我们不懂轻量级进程,所以就有人写了一批接口,专门就是为了线程写的。

  • 下面我们看第一个参数,第一个参数是一个 pthread_t 的指针,这个是一个输出型参数,我们将一个 pthread_t 的对像传进去,就会将线程的 id 返回。

  • 第二个参数是设置线程属性的,如果使用默认属性传NULL即可。

  • 第三个参数是一个函数指针的类型,其中返回值是 void* 参数也是 void*,这个参数就是创建的这个线程后让它去执行那个函数。

  • 最后一个是一个void* 的指针,而这个参数就是在创建线程的时候就会将这个参数传入到回调函数的参数中。

下面我们就创建线程,然后我们让线程执行回调函数:

void* handler(void* args)
{
  string str((char*)args);
  // 这里也循环打印pid,如果这些线程打印的pid相同的话,说明是同一个进程里面的
  for(int i = 0; ; ++i)
  {
    cout << str << " pid: " << getpid() << endl << endl;  
    sleep(1);
  }
}
​
​
void test1()
{
  // 线程 id,创建的时候会返回新线程的 id
  pthread_t tid;
  // 这里循环创建线程
  for(int i = 0; i < 5; ++i)
  {
    string str = "thread ";
    str += to_string(i);
    pthread_create(&tid, nullptr, handler, (char*)str.c_str());
    sleep(1);// sleep 是为了缓解传参的一个问题
  }
  // 主线程循环打印,自己的pid
  while(true)
  {
    cout << "我是主线程 pid:  " << getpid() << endl;
    sleep(3);
  }
}

这里我们看上面的这段代码,循环的创建线程,然后让主线程和新线程都打印pid,查看打印的pid是否相同。

这里其实再g++编译的时候,是有一个问题的,如果我们什么都不带的话,那么是编译不了的,为什么呢?

还是我们前面说的那个原因,因为Linux中没有线程,所以没有关于线程的库,而只有关于轻量级进程的库,但是其他人写了一个关于线程的库,但是i这个库并不属于语言,也不属于系统,所以需要我们指定要 link 哪一个库,所以我们需要带选项。

下面看一下 makefile 怎么写的:

thread:mythread.cc #依赖
        g++ -o thread mythread.cc -std=c++11 -lpthread #依赖方法
.PHONY:clean #伪目标
clean: #clean 没有依赖
        rm -rf thread #依赖方法

上面应该带 -lpthread,表示要 link 的库为 pthread 库。

下面看一下结果:

[lxy@hecs-348468 thread]$ ./thread 
thread 0 pid: 11358
​
thread 1 pid: 11358
​
thread 0 pid: 11358
​
thread 0 pid: 11358
​
thread 1 pid: 11358
​
thread 2 pid: 11358
​
thread 0 pid: 11358
​
thread 1 pid: 11358
​
thread 2 pid: 11358
​
thread 3 pid: 11358
​
thread 0 pid: 11358
​
thread 1 pid: 11358
​
thread 2 pid: 11358
​
thread 3 pid: 11358
​
thread 4 pid: 11358
​
我是主线程 pid:  11358

这里看到所有的线程打印的pid都是相同的,所以我们也知道它们是同一个进程。

我们也可以使用命令来查看他们的id:

ps -aL #查看所有的线程

我们下面启动上面的程序,然后查看一下线程的id:

[lxy@hecs-348468 thread]$ ps -aL | head -1 && ps -aL | grep thread
  PID   LWP TTY          TIME CMD
11364 11364 pts/1    00:00:00 thread
11364 11365 pts/1    00:00:00 thread
11364 11366 pts/1    00:00:00 thread
11364 11367 pts/1    00:00:00 thread
11364 11374 pts/1    00:00:00 thread
11364 11375 pts/1    00:00:00 thread

这里发现他们的 pid 是一样的,其中第一个线程的 pid 和 lwp(Lightweight Process) 是相同的,所以第一个线程是主线程。

再次理解线程

通过上面的试验,我们也能看出一下线程与进程的不同,我们现在重新理解一下进程。

我们现在知道,线程是CPU的基本调度单位,那么我们现在知道,同一个进程里面的线程的pid是相同的,所以我们CPU使用的是lwp还是pid呢?

我们现在知道,我们一定使用的是 lwp,因为同一个进程里面的 pid是相同的。

那么关于线程的内核级理解(线程也是轻量级进程,线程运行于进程的内部,线程是cpu调度的基本单位)。

我们可以再一次理解一下这句话:

线程是轻量级进程:是因为创建线程只需要创建PCB,不需要创建进程的其他资源,而且使用的资源也是来自于进程申请的,所以说线程是轻量级进程。

线程运行于进程内部:同一个进程内部的线程使用的是同一个进程地址空间,还有页表等,所以同一个进程里面的线程看到的资源是相同的也就是同一个进程里面的线程都运行于这个进程的地址空间中,所以线程运行于进程内部。

线程是CPU调度的基本单位:通过上面的试验,我们也能发现,CPU确实是调度的是线程,而并非进程。

那么为什么说线程的切换,或者说创建比进程创建要简单呢?

如果是同一个进程的话,那么所有的线程看到的进程地址空间是相同的,但是当CPU切换进程的时候,需要将进程的上下文都切换,而CPU运行一个线程需要它代码和数据,还有就是进程的地址空间以及页表,但是线程切换不需要切换地址空间和页表这些,实际上CPU寄存器上的内容切换其实并不是多么的困难,其实CPU上是有缓存的,但是当我们切换进程的时候,我们是需要将缓存数据也全部切换的,但是切换线程的时候,由于是同一个进程,又根据局部性原理,即使线程切换了,但是代码和数据还有很大一部分是相同的,所以缓存里面的数据是不需要切换的,但是进程切换的话,那么代码和数据一定是不一样的,所以一定需要切换CPU中的缓存,而缓存的切换就不是那么的简单的。

同一个进程里面的线程看到的是同一个进程地址空间,那么也就是说同一个进程中的像代码,以及全局数据等都是所有线程所共享的,那么线程有没有私有的数据呢?

一定是有的,为什么?

因为每个线程的运行起来是不同的,所以每个线程一定需要自己的上下文数据(一组寄存器),包括运行到那行代码等这些数据都是每个线程所私有的,那么还有什么是私有的呢?还有就是线程的私有栈结构里面的数据也是私有的,其实还有线程的id也是私有的,当然假设一个线程出错了,那么当然是需要有错误返回码的,那么如果错误返回码是共享的,那么就是有问题的,所以每个线程的错误码也是私有的,当然还有就是每个线程的重要性是不同的,所以线程的优先级也是不同的,所以线程的优先级也是私有的。

线程的共享数据有哪些呢?

线程看到的进程地址空间是相同的,那么也就是说进程地址空间里面的很多数据都是共享的。

其中有堆的数据是共享的,还有就是全局的数据也当然是共享的,还有静态变量,以及线程打开的文件描述符也是共享的。

重新理解进程

前面我们说的进程就是内核数据结构加代码和数据,但是我们现在还需要重新理解一下。

现在我们所说的进程还是代码加数据,但是里面可能不止一个PCB,里面可能有多个执行流,当然我们之前说的也不是错的,我们之前说的都是一个执行流的进程,当然多个执行流可能就包含一个执行流,所以现在我们所说的进程就是下面这样的。

这整个才算我们所说的进程,我们之前说的进程里面只有一个PCB,也就是只有一个执行流。

当然这里我们自己的理解,下面我们看一下内核级的理解。

再内核里面是这样说进程的:进程是系统资源分配的基本实例。

为什么这样说呢?

因为我们再创建进程的时候需要创建对应的内核数据结构,还需要为进程分配所需的资源。

而线程使用的就是进程申请号的资源,所以当我们学习到线程的时候,我们也就知道了,线程是CPU调度的基本单位,而进程就是系统资源分配的基本实例。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/123075.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL事务篇】多版本并发控制(MVCC)

多版本并发控制(MVCC) 文章目录 多版本并发控制(MVCC)1. 概述2. 快照读与当前读2.1 快照读2.2 当前读 3. MVCC实现原理之ReadView3.1 ReadView概述3.2 设计思路3.3 ReadView的规则3.4 MVCC整体操作流程 4. 举例说明4.1 READ COMMITTED隔离级别下4.2 REPEATABLE READ隔离级别下 …

软件测试|selenium执行js脚本

JavaScript是运行在客户端&#xff08;浏览器&#xff09;和服务器端的脚本语言&#xff0c;允许将静态网页转换为交互式网页。可以通过 Python Selenium WebDriver 执行 JavaScript 语句&#xff0c;在Web页面中进行js交互。那么js能做的事&#xff0c;Selenium应该大部分也能…

Spring的注入

目录 一、Spring的概念 二、各种数据类型的注入 &#xff08;1&#xff09;studentService &#xff08;2&#xff09;applicationContext.xml&#xff08;Sring核心配置文件&#xff09; &#xff08;3&#xff09;测试 三、注入null或者empty类型的数据 &#xff08;1…

ESP-IDF-V5.1.1使用websocket

IDF Component Registry (espressif.com) 在windows系统中&#xff0c;在项目目录下使用命令 idf.py add-dependency "espressif/esp_websocket_client^1.1.0"

【小黑送书—第四期】>>用“价值”的视角来看安全:《构建新型网络形态下的网络空间安全体系》

经过30多年的发展&#xff0c;安全已经深入到信息化的方方面面&#xff0c;形成了一个庞大的产业和复杂的理论、技术和产品体系。 因此&#xff0c;需要站在网络空间的高度看待安全与网络的关系&#xff0c;站在安全产业的高度看待安全厂商与客户的关系&#xff0c;站在企业的高…

软件测试|测试方法论—边界值

边界值分析法是一种很实用的黑盒测试用例方法&#xff0c;它具有很强的发现故障的能力。边界值分析法也是作为对等价类划分法的补充&#xff0c;测试用例来自等价类的边界。 这个方法其实是在测试实践当中发现&#xff0c;Bug 往往出现在定义域或值域的边界上&#xff0c;而不…

目标跟踪(DeepSORT)

本文首先将介绍在目标跟踪任务中常用的匈牙利算法&#xff08;Hungarian Algorithm&#xff09;和卡尔曼滤波&#xff08;Kalman Filter&#xff09;&#xff0c;然后介绍经典算法DeepSORT的工作流程以及对相关源码进行解析。 目前主流的目标跟踪算法都是基于Tracking-by-Detec…

【离散数学】图论

图 无向图 <V,E>有序二元组&#xff0c;代表一个无向图G V是顶点的集合&#xff0c;元素为顶点&#xff1b;称为顶点集 E是边的集合&#xff0c;元素为无向边&#xff1b;称为边集合 有向图 <V,E>有序二元组&#xff0c;代表一个有向图G V是顶点的集合&#x…

07-MySQL-进阶-锁InnoDB引擎MySQL管理

涉及资料 链接&#xff1a;https://pan.baidu.com/s/1M1oXN_pH3RGADx90ZFbfLQ?pwdCoke 提取码&#xff1a;Coke 一、锁 ①&#xff1a;概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中&#xff0c;除传统的计算资源&#xff08;CPU、RAM、I/O&#xf…

Git的安装以及它的介绍

目录 一. Git简介 分布式特点 优缺点 Git 与 SVN 区别 二. Git安装 三. Git常用命令 四. Git的文件状态 1.文件状态 2.工作区域 一. Git简介 Git 是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理。 也是Linus Torvalds…

ChatGPT 的 Text Completion

该章节我们来学习一下 “Text Completion” &#xff0c;也就是 “文本完成” 。“Text Completion” 并不是一种模型&#xff0c;而是指模型能够根据上下文自动完成缺失的文本部分&#xff0c;生成完整的文本。 ⭐ Text Completion 的介绍 Text Completion 也称为文本自动补全…

Kafka入门

kafka无疑是当今互联网公司使用最广泛的分布式实时消息流系统&#xff0c;它的高吞吐量&#xff0c;高可靠等特点为并发下的大批量实时请求处理提供了可靠保障。很多同学在项目中都用到过kafka&#xff0c;但是对kafka的设计原理以及处理机制并不是十分清楚。为了知其然知其所以…

2023年眼镜行业分析(京东眼镜销量数据分析):市场规模同比增长26%,消费需求持续释放

随着我国经济的不断发展&#xff0c;电子产品不断普及&#xff0c;低龄及老龄人口的用眼场景不断增多&#xff0c;不同年龄阶段的人群有不同的视力问题&#xff0c;因此&#xff0c;视力问题人口基数也随之不断加大&#xff0c;由此佩戴眼镜的人群也不断增多。 同时&#xff0c…

2023 全栈工程师 Node.Js 服务器端 web 框架 Express.js 详细教程(更新中)

Express 框架概述 Express 是一个基于 Node.js 平台的快速、开放、极简的Web开发框架。它本身仅仅提供了 web 开发的基础功能&#xff0c;但是通过中间件的方式集成了外部插件来处理HTTP请求&#xff0c;例如 body-parser 用于解析 HTTP 请求体&#xff0c;compression 用于压…

微前端qiankun嵌入vue项目后iconfont显示方块

个人项目地址&#xff1a; SubTopH前端开发个人站 &#xff08;自己开发的前端功能和UI组件&#xff0c;一些有趣的小功能&#xff0c;感兴趣的伙伴可以访问&#xff0c;欢迎提出更好的想法&#xff0c;私信沟通&#xff0c;网站属于静态页面&#xff09; SubTopH前端开发个人…

【Linux】第十四站:进程优先级

文章目录 一、Linux内核怎么设计各种结构二、进程优先级1.基本概念2.是什么3.为什么要有优先级4.批量化注释操作5.查看优先级6.PRI and NI 三、位图与优先级 一、Linux内核怎么设计各种结构 我们前面所写的数据结构都是比较单纯的。 而linux中就比较复杂了&#xff0c;同一个…

MATLAB|热力日历图

目录 日历图介绍&#xff1a; 热力日历图的特点&#xff1a; 应用场景&#xff1a; 绘图工具箱 属性 (Properties) 构造函数 (Constructor) 公共方法 (Methods) 私有方法 (Private Methods) 使用方法 日历图介绍&#xff1a; 热力日历图是一种数据可视化形式&#xf…

Vue中的 配置项 setup

setup 是 Vue3 中的一个全新的配置项&#xff0c;值为一个函数。 setup 是所有 Composition API&#xff08;组合式API&#xff09;的入口&#xff0c;是 Vue3 语法的基础。 组件中所用到的数据、方法、计算属性等&#xff0c;都需要配置在 setup 中。 setup 会在 beforeCre…

公众号开发实践:用PHP实现通过接口自定义微信公众号菜单

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

解决在表格数据行赋值给表单,会出现表单输入框无法输入的情况

1 直接赋值属性的方法 会出现表单输入框无法输入的情况 handleFixUpdate(row){this.resetForm("formFixUpdate");console.log(this.formFixUpdate)this.formFixUpdate.repairId row.repairIdthis.formFixUpdate.itemId row.itemIdthis.formFixUpdate.repairMan …