【Linux学习】多线程——页表详解 | 线程概念 | 线程理解

news2024/10/6 15:31:09

🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!
图

线程概念

  • 🥞 页表详解
    • 🍞页目录和页表项
  • 🥞线程
    • 🍞轻量级进程
    • 🍞线程引入Linux
  • 🥞看见线程
    • 🍞原生线程库
    • 🍞LWP和PID
  • 🥞线程的公有资源和私有资源
  • 🥞线程的优缺点
  • 🥞总结

🥞 页表详解

我们在之前一直都提到页表,知道它的作用是将虚拟地址映射到物理地址,但是它具体怎么映射的,它的结构是什么样的,并没有提及过。

char* str = "hello world";
*str = 'H';

上诉代码,会在运行时报错,原因是str指向的地址在字符常量区,字符常量区的内容是不允许用户去修改的。

代码在运行起来以后,操作系统是怎么知道用户在修改字符常量区的呢?

图
如上图所示的页表示意图,页表中不仅右虚拟地址和物理地址的映射关系,还有是否命中,RWX权限,U/K权限等等内容。

  • U/K权限:U表示用户(user),K表示内核(kernal)。
  • RWX权限:当前身份(用户或者内核)对当前地址的读,写执行权限。
  • 上面代码在对srt指向的地址写内容时,先会经过页表映射到物理地址。
  • 但是在页表中发现这是一个写操作,并且该地址是不允许被写的,此时MMU就发送信号,导致程序报错。
  • 虚拟地址,物理地址以及属性所在的一行,称为条目

图
仍然是这张图,需要将这张图分解进行讲解。

物理内存空间划分:

tu

以32位系统为例,它的物理内存理论上有4GB大小,但是这4GB又被分成了多块小空间。

  • 被分割的小空间,每一块大小是4KB,被叫做页框

物理内存中会又很多个页框,并且这些页框也需要操作系统管理起来,采用的方式同样是先描述,再组织

通过一个结构体来描述页框

struct_Page
{
	//内存属性--4KB
}

代码形式如上所示,每一个页框都会有这样一个结构体对象,将多个结构体对象放在一个数组中:

struct_Page mem[];

此时就完成了先描述再组织的过程。操作系统中有专门的内存管理算法,叫做伙伴系统,有兴趣的小伙伴可以自行查阅资料了解。

可执行文件:

我们写好的代码会经过编译器的处理形成二进制可执行文件放在磁盘中,在运行的时候加载到内存中。

  • 编译器在处理源文件生成的二进制可执行文件,同样是以4KB为单位的,这4KB的数据块被叫做页桢

这一切都是设计好的,所以可执行程序在加载到内存中的时候是以4KB为单位的,正好一个页帧来填充一个页框。当页框被填充了以后,就会创建对应的struct_Page结构体对象,并且放在数组中,让操作系统来管理。

🍞页目录和页表项

再回到页表,我们知道,每个进程对应的虚拟地址空间大小都是4GB的,也就是有232个地址,如果每个虚拟地址在页表中都对应着一个物理地址:

图
那么页表就会有232行,每一行都是一个条目,每个条目中不仅有物理地址,虚拟地址,还有其他属性,假设一个条目的大小是10B,那么光页表就有10*232=40GB,已经超过了物理内存的大小,所以页表肯定不是这样的。

  • 实际上,页表是由页目录和页表项组成的。

在32位机器上,地址的大小是4G字节,也就是有32个比特位:

图
随便写了一个地址,如上图所示,一个32个比特位。

  • 将32个比特位分为10个比特位,10个比特位,12个比特位,共3组。

图

  • 之前的的页表 = 页目录 + 页表项,如上图所示。
  • 32个比特位的高10位,作为页目录的下标,如上图所示的0000 0000,通过这个下标0可以访问到页目录中的第一个条目。
  • 页目录中存放的是页表项的地址,可以通过下标找到对应的页表项。

10个比特位,意味着页目录的下标范围是0~1023,最多是210也就是1KB个条目,大大减少了对内存的消耗。

  • 32个比特位的中间10位,作为页表项的下标,同样可以访问页表项中的条目。
  • 页表项中存放的是物理内存中页框的起始地址,可以通过下标找到物理内存中对应的页框。

同样,一个页表项最多有1KB个条目,指向1KB个页框。

  • 32个比特位中的低12位,作为偏移量,在物理内存中页框的起始地址基础上进行偏移,此时就可以得到具体数据在内存中的地址。
  • 这也是为什么页框和页帧的大小设置为4KB的原因,因为最低的12个比特位是212=4KB,偏移量最大就是4KB。
  • 32位虚拟地址->物理地址的映射过程:
  • 根据高10位下标找到页目录中对应页表项的地址
  • 再根据中间10位下标找到页表相中对应页框的物理地址
  • 再根据低12位偏移量进行偏移找到具体的物理地址。

页目录和页表项同样是采用先描述再组织的方式被操作系统管理起来的,每创建一个进程就会有一个页目录,只有在目录中存在的页表项才会被建立

采用这种方式,大大减少了对内存的消耗。

如何看到地址空间和页表:

  • 地址空间是进程看到的资源窗口,每个进程都认为自己有4GB的资源。
  • 页表决定进程真正拥有的资源。
  • 合理对地址空间和页表进行资源划分,就可以对一个进程所有的资源进行分类。

🥞线程

  • 线程:是进程中的一个执行流。

先将概念抛出,那么这句话到底是什么意思呢?

回忆一下,之前我们对进程的定义是:内核数据结构 + 进程对应的代码和数据。

图
如上图所示,此时我们创建了多个“子进程”。

  • 新创建的“子进程”中的mm_struct* mm都指向父进程的虚拟地址空间。
  • 也就是说,所有“子进程”和父进程共用一块虚拟地址空间。

和父进程共用一块虚拟地址空间的“子进程”,就叫做线程

此时开始,我们就将带引号的"子进程",叫做线程。

  • 线程的作用:执行进程中的一部分代码。

线程在执行进程的部分代码时,有点像父子进程执行同一份代码中不同部分,区别在于线程和父进程使用的是同一份虚拟地址空间,而父子进程使用的是两个独立的虚拟地址空间。

🍞轻量级进程

从图中可以看到,每个线程都也有一个task_struct结构体对象,用来描述线程的属性(id,状态,优先级,上下文,栈等等)。那么线程要不要被操作系系统管理起来呢?

答案是要的,而且采用的方式同样是先描述再组织,描述线程的task_struct结构体被叫做TCB–线程控制块,是英文Thread Contral Block的首字母。

描述好了以后同样像PCB一样,需要用链表组织起来进行管理,并也和PCB一样,有自己的管理算法。

  • 但是,TCB中的属性和PCB几乎一样,管理TCB的数据结构和算法也和PCB的一样。

此时不仅会导致代码上的冗余,而且还会增加系统的开销,所以Linux并不是使用TCB管理线程的,因为这种方式比较复杂,维护起来不方便,而且运行也不是很稳定。

  • Linux中,线程是直接复用PCB的数据结构和管理方法。
  • 所以在Liux操作系统中,进程和线程的描述结构体都是task_struct。

图

  • 站在CPU的角度,它只关注task_struct。

CPU是一个被动的硬件,给它什么它就执行什么,所以它并不会区分当前执行的task_struct是一个进程还是一个线程,在它看来,都是进程

之前VS现在:

  • 之前:CPU执行的task_struct是一个进程
  • 现在:CPU执行的task_struct是一个执行流

所以说,今天CPU处理的task_struct <= 之前task_struct的含义。

  • 站在内核的角度,称今天学习的task_struct为轻量级进程

我们可以通过虚拟地址空间 + 页表的方式对进程进行资源划分,让不同的“轻量级进程”同时执行不同部分的代码,所以单个“轻量级进程”的执行粒度,一定要比之前的进程细。

  • Linux内核中并没有线程的概念,线程是用进程PCB来模拟的。

由于Linux中,线程也是使用的PCB结构,是一种轻量化的进程,所以在Linux内核中并不存在线程的概念,也不存在线程的结构。

  • 站在CPU的角度,每一个PCB都被称为轻量级进程。

CPU并不关注它处理的task_struct是属于进程还是线程,在它看来,都是进程。

  • Linux线程是CPU调度的基本单位。

CPU每次都是调度一个task_struct结构体,而这些PCB都是轻量级进程,有可能是属于进程,也有可能是属于线程,即使是属于进程,也可以看作是一个线程,因为无论是进程还是线程,都是一个个的执行流,CPU每次调度的都是一个执行流。

  • 进程的重新定义:进程是承担分配系统资源的基本实体。

每创建一个进程,都会创建一个PCB,一个虚拟地址空间,一个页表,一块物理空间,而线程是属于这个进程中的执行流,它使用的是这个进程的资源。

所以此时的进程就包括因为创建它而产生的一系列开销(PCB,虚拟地址空间,页表,物理空间),这些都是属于这个进程的。

  • 当这个进程中的某个线程申请新资源的时候,也是以该进程的名义去申请,而不是也这个线程的名义。

讲到这里,是不是觉得和之前学习的进程概念有冲突了?其实是自洽的。

  • 之前我们学习的进程,每个进程只有一个执行流。
  • 而现在每个进程中有多个执行流,每个执行流都是一个线程。

一个进程内可以有多个执行流,这些执行流都共用一个虚拟地址,一个页表。

  • 最初的进程执行流被叫做主线程
  • 之后创建的执行流被叫做新线程

主线程和新线程都属于一个进程,都是一体的,就像一个家庭中,有不同的成员,他们的工作是不同的,但是目的都是一样的–为了这个家好。

同样,多个线程同时工作的目的也是相同的–为了完成这个进程的任务。

🍞线程引入Linux

通过上面介绍,我们知道,在Linux内核中是不存在线程这一个概念的,因为没有TCB数据结构以及管理算法,而我们所说的线程,都是在宏观层面,代指所有操作系统。

  • Linux操作系统中也没有提供创建线程的系统调用。
  • 无论是宏观操作系统,还是用户(程序员)都只认线程的概念,但是Linux内核中并没有线程的概念。

我们(程序员)在编程的时候,仍然会使用线程的概念,那么我们在创建线程的时候,Linux内核中是怎么创建出轻量级进程的呢?

图

  • 我们在创建进程的时候,会调用一个线程库,库中再通过一些系统调用创建出轻量级进程。

这样一来,程序员创建线程,Linux中创建轻量级进程,双方的要求就都满足了。

这个线程库是所有Linux操作系统必须自带的,所以也叫做原生线程库。

🥞看见线程

下面我们来看看线程的样子,创建线程使用到的库函数接口是:

图

  • pthread_t* thread:线程标识符tid,是一个输出型参数。
  • const pthread_attr_t* attr:线程属性,当前阶段一律设成nullptr。
  • void* (*start_routine)(void *):是一个函数指针,线程执行的就是该函数中的代码。
  • void* arg:是上面函数指针指向函数的形参。
  • 返回值:线程创建成功返回0。

TU

  • 主线程执行的任务是在一个死循环中,新线程执行的任务也在一个死循环中。

如果只有一个执行流的话,程序会陷入一个死循环中,另一个死循环就不会再执行,而我们创建新线程就是为了让新线程和主线程同时执行两个不同的死循环。

图
将上诉代码进行编译的时候,我们发现报错了,编译器不认识pthread_create函数。

  • 我们创建新线程只能通过原生线程库去创建,此时编译器找不到原生线程库。

图

  • 使用-l选项指定原生线程库pthread,之前在动态静态库的时候详细介绍过如果指定动态库。

图
此时我们再次编译以后,就能够正常运行了。

  • 新线程和主线程在同时运行,并没有陷入某一个死循环中。

🍞原生线程库

图
查看可执行程序的链接属性。可以看到是动态链接,链接的库是原生线程库,如上图绿色框中所示。

根据线程库的路径去查看该路径下的所有文件,可以看到还有静态库,我们使用的线程库是一个软链接文件,它所链接的库才是真正的原生线程库。

图
在创建新线程的时候,传递的最后一个参数作为新线程执行函数的形参,如上图所示。

图
可以看到,新线程中打印的name内容正式在主线程中创建新线程时传过来的字符串。

🍞LWP和PID

图

  • 主线程和新线程在同时运行,此时存在两个执行流。

但是在查看该进程的时候,发现mythread进程只有一个,pid,ppid等值也只有一个。

  • 这也证明,线程是进程中的一个执行流,线程属于进程的一部分。

图

  • 给mythread进程发送9号信号,主线程和新线程都结束了。
  • 所有信号针对的都是进程,而线程属于进程。
  • 当一个进程结束以后,它的所有资源都会被回收,所以线程也就不存在了。

那我们想看到线程该怎么办呢?

图

  • 使用指令ps -aL来查看线程。L必须大小
  • 此时名字为mythread的线程有两个,它们的PID值相同,LWP不同。
  • PID:进程标识符
  • LWP:轻量级进程表示符,LWP是英文Light Weight Process的首字母。
  • 可以看到,第一个线程的LWP和PID是一样的,这个线程就被叫做主线程
  • 第二个线程的LWP和PID不一样,这个线程就被叫做新线程

那么CPU在调度PCB的时候,根据的是LWP呢还是PID呢?

  • CPU在调度PCB的时候是根据LWP为标识符表示一个特点的执行流的。

因为CPU调度的都是轻量级进程,而每个轻量级进程也就线程的根本区别就在于LWP不同,但是不同线程的PID却有可能相同。

  • 我们之前学习的进程,它只有一个执行流,也就是主线程,所以它的PID和LWP是相同的,即PID = LWP,我们使用哪个都无所谓。
  • 而现在我们学习了线程,就不能再只使用PID了,而是使用LWP。

从这里可以再次看出线程是属于进程的一部分

线程和进程的关系如下图:

图

一个框表示一个进程,一条波浪线表示一个线程。

🥞线程的公有资源和私有资源

公有资源:

所有线程都共享一个虚拟地址空间,一个页表,所以进程中的绝大部分资源都是所有线程共享的,先来看看共享的情况:

图

写了一个函数,分别在主线程和新线程中调用这个函数。

图
可以看到,主线程和新线程都可以调用这个函数。

  • 该进程中只有一份虚拟地址空间,该函数放在代码段中。
  • 所有线程共享一个代码段。

图

  • 创建一个全局变量,在主线程和新线程中都打印,并且使用后置++。
  • 再将全局变量的地址在主线程和新线程中打印出来。

图

  • 新线程和主线程看到的全局变量是一个,当任意一个线程改变这个变量的值时,都会影响另一个线程使用这个值。
  • 主线程和新线程中,全局变量的地址是相同的,说明它们使用的是同一个全局变量。

根据上面现象以及分析,可以知道,数据段也是被所有线程共享的。

进程中的绝大部分资源都是和所有线程共享的

私有资源:

因为所有线程都共享一个虚拟地址空间以及页表,线程之间有私有资源吗?答案肯定是有的。

  1. PCB属性私有

所有线程都有各自的PCB,所以PCB中的属性肯定是私有的,属于各自线程。

  1. 上下文数据私有

CPU在调度PCB的时候,采用轮转时间片的方式,当一个线程被换下时,该线程的上下文一定是私有的,防止被其他线程修改而导致恢复上下文的时候出现错误。

  1. 栈结构私有

不同线程各自的临时变量一定是私有的,而临时变量存放在栈结构中,所有栈也是私有的。

理解是能理解,但是都是同一块虚拟地址空间,怎么就让不同线程的栈结构私有了呢?这就涉及到了原生线程库的实现,本喵大致说一下:

图
系统调用clone是用来创建子进程的,这里的子进程是轻量级进程,也就是没有独立的虚拟地址空间。

  • clone中有一个参数,如上图中绿色框中所示,该参数就是用来自定这个子进程的栈空间的。

所以我们在使用pthread_create创建新线程的时候,底层会调用clone,并且会指定属于该线程的私有栈结构。

🥞线程的优缺点

优点:

与线程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 进程切换:PCB切换 + 上下文切换 + 虚拟地址空间切换 + 页表切换
  • 线程切换:PCB切换 + 上下文切换

可以看到,线程切换比进程切换少了两项。 除此之外,线程切换时cache不用太更新。

  • cache:硬件缓冲区,其实就是我们所说的高速缓存。
  • 它存在于CPU中,速度只是比CPU慢一点,但是比内存快很多。

图

  • cache会根据局部性原理从内存中拿数据,尤其是使用频率高的热点数据会一直放在cache中。
  • CPU在使用数据时,不是直接去内存中拿,而是先去cache中拿,如果不命中,也就是不存在,cache就会将CPU所需要的数据从内存中缓存到cache中。
  • 进程间切换:不仅上面提到的四项内容需要切换,而且cache中的内容也需要重新缓存。
  • 线程间却换:切换PCB和上下文,但是cache中缓存的数据不需要切换。

所以线程都共用一个虚拟地址空间和一个页表,而cache中的内容也是根据虚拟地址和页表缓存进来的,所以不同进程之间是可以共用的。

这样一来,大大节省了cache从内存中缓存数据的时间,并且也节省了操作系统的大量工作。

当然还有很多其他的优点,比如:

  • 创建一个新线程的代价要比创建一个新进程小得多,因为线程不会创建新的虚拟地址空间和页表。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
  • 计算密集型应用:主要体现在CPU的高频工作,如加密,解密,算法等。
  • I/O密集型应用:主要体现在和外设的交互上,如访问磁盘,显示器,网卡等。

上面很多线程的优点,进程也是拥有的。

缺点:

健壮性或者鲁棒性较差:

图

  • 在新线程中,对空指针指向的地址进行写入。
  • 我们知道,这个操作在运行时肯定会出错。

图

  • 新线程中发送端错误异常,收到了11号信号SIGSEGV。
  • 但是不仅新线程结束了,主线程也结束了。

线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。而多进程就不存在,一个进程的退出并不会影响另一个进程。

除此之外,线程还有一些其他的缺点:

  • 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销(线程切换),而可用的资源不变。

  • 缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高
  • 一般情况下,CPU有几个核就创建几个线程。
  • 核:只的是一个CPU中的运算器个数,即使是多核,而控制器也是一个CPU只有一个。

🥞总结

在这篇文章中,一定要明白线程是什么,它和进程的区别。并且要知道线程是站在宏观操作系统而言的概念,而具体到Linux操作系统中是没有线程这一个概念的,也没有线程对应的数据结构和系统调用。概念上的线程和内核中的轻量级进程是通过线程库建立的联系。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023/4/18往日题目总结

搜索路径状态记录 1076. 迷宫问题 - AcWing题库 //以最简单的迷宫问题为例&#xff0c;如何记录走迷宫的路径&#xff0c;其实只需要记录一下状态即可 //也就是记录一下这个点是从哪个点来的&#xff0c;最后从终点开始输出即可&#xff08;此时输出的是逆序&#xff09; #in…

PHP+python+nodejs+springboot+vue 大学生提问论坛系统

在各学校的教学过程中&#xff0c;大学生提问论坛是一项非常重要的事情。随着计算机多媒体技术的发展和网络的普及。 本文首先介绍了大学生提问论坛的发展背景与发展现状&#xff0c;然后遵循软件常规开发流程&#xff0c;首先针对系统选取适用的语言和开发平台&#xff0c;根…

微信小程序的生命周期函数有哪些,以及执行过程?

目录 1、应用的生命周期 2、页面的生命周期 3、组件的生命周期 4、执行顺序 小程序中&#xff0c;生命周期主要分成了三部分&#xff1a; ① 应用的生命周期 ② 页面的生命周期 ③ 组件的生命周期 1、应用的生命周期 在 app.js 里面调用&#xff0c;通过…

【hello Linux】理解文件系统

目录 创建文件的过程&#xff1a; 删除文件的过程&#xff1a; 创建目录的过程&#xff1a; 查看inode编号&#xff1a; 硬链接 软链接 Linux&#x1f337; 我们知道文件所有数据 文件内容 文件属性信息&#xff1b; 未打开的文件是被存放到磁盘/固态硬盘中的&#xff1b; …

0201概述-网关Gateway-微服务架构

文章目录 1 前言2 项目引入3 术语4 工作原理5 配置示例5.1 简洁配置5.2 展开配置 6 Predicate7 GatewayFilter7.1 StripPrefix GatewayFilter7.2 RequestRateLimiter GatewayFilter① pom 依赖② 配置按照请求IP 的限流 6 Global Filters7 网关超时配置7.① 配置全局路由超时时…

用java 实现二叉树创建

二叉树是数据结构中的一个重要的概念&#xff0c;二叉树的概念最早由 Linus Torvalds在1958年提出。他给出了一个树形数据结构&#xff0c;可以用来存储二叉树。每个节点的左子树和右子树都是空&#xff0c;中间层是子树。在一个给定的空间中&#xff0c;每一个节点都有两个左右…

Adobe认证证书含金量

当今数字时代&#xff0c;Adobe软件已经成为了许多人工作和创造的必备工具。为了证明自己在使用Adobe软件方面的专业能力&#xff0c;许多人选择参加Adobe认证考试并获取Adobe认证证书。 那么&#xff0c;这些证书的含金量究竟如何呢&#xff1f; 首先&#xff0c;需要指出的…

C++:Article:链接器(二):符号决议

链接器 1. C源文件都有些什么1.1 . 目标文件里有什么 2. 符号表 Symbol table2.1. 符号表的位置2.2. 符号的决议2.3. 符号决议过程 3. 实例说明3.1. 意外出现3.2 总结排查 在上篇文章中&#xff0c;我们介绍了 链接器基本概念&#xff0c;我们知道所有的应用程序否是连接器将所…

基于Python长时间序列遥感数据处理及在全球变化、物候提取、植被变绿与固碳分析、生物量估算与趋势分析

植被是陆地生态系统中最重要的组分之一&#xff0c;也是对气候变化最敏感的组分&#xff0c;其在全球变化过程中起着重要作用&#xff0c;能够指示自然环境中的大气、水、土壤等成分的变化&#xff0c;其年际和季节性变化可以作为地球气候变化的重要指标。此外&#xff0c;由于…

Cesium基础教程

一、概述 Cesium是国外一个基于javascript的地图引擎&#xff0c;支持3D、2D、2.5D形式的展示&#xff0c;可以自行绘制图形、高亮区域&#xff0c;并提供良好的触摸支持&#xff0c;并支持大多数的浏览器和移动端。 Cesium 是一个跨平台、跨浏览器的展示三维地球和地图的 ja…

【PWN刷题__ret2text】[CISCN 2019华北]PWN1

ret2text~ 前言 依旧是简单的ret2text 一、checksec查看 No canary found 没有开启栈溢出保护 二、IDA反汇编 双击进入func() 发现后门函数system("cat/flag")&#xff1b;根据语义&#xff0c;函数提供了修改v1&#xff0c;判断v2是否等于11.28125&#xff0c;如…

【倒计时4天】金融服务用户体验专场沙龙开启预约

易观&#xff1a;数智化浪潮下&#xff0c;金融业务用户运营从线下向全渠道延伸。用户体验已成为⾦融机构打造差异化的重要抓手。此外&#xff0c;随着⽤户体验的全⾯数字化和精细化&#xff0c;⽤户体验贯穿于整个客户旅程中&#xff0c;做好用户体验管理&#xff0c;体现差异…

okio篇 1.Buffer

总览: Okio的两个基本概念&#xff1a;Source和Sink。Source对标基础io库中的InputStream&#xff0c;负责读数据。Sink对标OutputStream&#xff0c;负责写数据。 Source和Sink的内部实现&#xff0c;都是一个Buffer。Buffer从字面意思理解就是一个缓冲区&#xff0c;跟Buff…

『pyqt5 从0基础开始项目实战』12. 实现多线程循环检测数据的开始和停止(保姆级图文)

目录 最终效果导包和框架代码 main.py避免重复执行开始与停止事件表格更新事件 scheduler.py初始化开始线程停止线程在线程列表中删除线程TaskThread新建线程StopThread停止线程完整代码main.pythreads.pyscheduler.py 总结 欢迎关注 『pyqt5 从0基础开始项目实战』 专栏&#…

C. Anadi and Domino(思维 + 枚举)

Problem - C - Codeforces 阿纳迪有一副多米诺骨牌。每张多米诺骨牌都有两个部分&#xff0c;每个部分都包含一些小点。对于每一个a和b&#xff0c;如1≤a≤b≤6&#xff0c;1≤a≤b≤6&#xff0c;正好有一块多米诺骨牌的一半是a点&#xff0c;另一半是b点。这组多米诺骨牌正好…

手动实现 Tomcat 底层机制+ 自己设Servlet

目录 手动实现 Tomcat 底层机制 自己设Servlet 完成小案例 运行效果 此项目用maven至于怎么配置在下一篇文章 创建cal.html CalServlet.java web.xml WebUtils 问题: Tomcat 整体架构分析 测试分析&#xff1a; 抓包情况 手动实现 Tomcat 底层机制 自己设计 Serv…

十个高质量工具网站推荐,AI自动抠图换背景,任意背景自动融合

AI 背景更换是一种利用生成式人工智能创建新图像背景的软件工具。与传统方法需要移除原有的背景并更换新的不同&#xff0c;AI背景生成器使用先进的算法生成与前景完美融合的全新背景。这项技术彻底改变了图像编辑的方式&#xff0c;为设计提供了更多的创造自由和灵活性。 特点…

数据结构--B树、B+树

数据结构--B树、B树 1. 什么是B树2.建立B树的要求3.什么是B树4.Mysql里面为什么使用B树作为索引结构&#xff1f; 1. 什么是B树 B树是一种数据结构&#xff0c;用于在硬盘或其他非易失性存储介质上快速存储和访问大量数据。它是一种平衡树&#xff0c;其每个节点可以存储多个键…

zabbix SNMP traps 监控案例

目标 根据H3C网络设备 发送 SNMP trap 信息进行网络端口的告警。 具体过程 继上次配置的trap 方式进行监控一个案例。 其中log数据中的内容是&#xff1a; 20230330.163810 ZBXTRAP 192.168.21.148 UDP: [192.168.21.148]:52289->[172.18.18.2]:1162 DISMAN-EVENT-MIB::…

Keil5软件安装方法(兼容stm32与c51方法)

目录 一、下载软件包 二、安装软件 1、安装C51v960a.exe (1&#xff09;右键以管理员权限运行程序 &#xff08;2&#xff09;开始安装软件 &#xff08;3&#xff09;勾选协议 &#xff08;4&#xff09;选择安装路径 &#xff08;5&#xff09;填写名字与邮箱 &#xff0…