<Linux> 初识线程

目录

前言:

一、什么是线程

(一)基本概念

(二)线程理解

(三)线程与进程的关系

(四)简单实用线程

(五)重谈虚拟地址空间

1. 页表的大小

2. 内存与磁盘的交互

3. 局部性原理

4. 页表映射原理

5. 扩展:动态内存管理

(六)小结

二、线程优缺点

(一)优点

(二)缺点

(三)线程用途


前言:

将一份代码成功编译后,可以得到一个可执行程序,程序运行后,操作系统会生成对应数据结构(比如 PCB)对其进行管理及分配资源,并且相关代码和数据被 加载 到内存中,准备工作做完之后,我们就可以得到一个运行中的程序,简称为 进程。对于操作系统来说,光有 进程 的概念是无法满足高效运行的需求的,因此需要一种执行粒度更细、调度成本更低的执行流,而这就是线程。

Windows11中的线程:

一、什么是线程

(一)基本概念

教材观点:

  1. 线程就是一个执行分支、执行粒度比进程更细、调度成本更低
  2. 线程就是进程内部的一个执行流

内核观点:

  • 进程是承担系统资源分配的基本实体,而线程是 CPU 调度的基本单位

线程是对以往进程概念的补充完善,正确理解线程概念是一件十分重要的事。

(二)线程理解

注意:以下理解是站在 Linux 系统的角度对于线程的理解,不同的操作系统具体实现方式略有差异。

理解 线程 之前需要先简单回顾一下 进程

  • 程序运行后,操作系统为其创建对应的 PCB 数据结构,然后生成虚拟地址空间、分配内存资源,相关的代码和数据会被 load 到内存中,并通过页表建立映射关系。

进程之间是相互独立

即使是 父子进程,他们也有各自的 虚拟地址空间、映射关系、代码和数据(可能共享部分数据,出现修改行为时引发 写时拷贝机制)

如果我们想要创建 其他进程 执行任务,那么 虚拟地址空间、映射关系、代码和数据 这几样东西是必不可少的,想象一下:如果只有进程的概念,并且同时存在几百个进程,那么操作系统调度就会变得十分臃肿。

  • 操作系统在调度进程时,需要频繁保存上下文数据、创建的虚拟地址空间及建立映射关系

为了避免这种繁琐的操作,引入了 线程 的概念,所谓线程就是:额外创建一个 task_struct 结构,该 task_struct 同样指向当前的虚拟地址空间,并且不需要建立映射关系及加载代码和数据,如此一来,我们就会发现我们新创建的task_struct 好像也是一个"进程",更是一个独立的执行流,其实这就是创建一个线程。操作系统只需要针对一个 task_struct 结构即可完成调度,成本非常低。

为什么线程其执行粒度比进程更细?

现在一个进程有多个task_struct ,都指向同一个地址空间。把地址空间中例如代码区的代码划分成多个部分,每一个 task_struct 在未来执行时,都执行同一个地址空间的不同部分的代码。这样一个进程中就存在了多个执行流(线程)。因此线程是一个执行分支,这样每个线程就能更精确的执行完成任务。

为什么调度线程的成本比进程更低?

CPU 内部包括:运算器、控制器、寄存器、MMU、硬件级缓存(cache),其中 硬件级缓存 cache 又称为 高速缓存,遵循计算机设计的基本原则:局部性原理会预先加载 部分用户可能访问的数据,并在接下来的执行中有很大的概率命中这些数据,从而提高整机的效率。如果需要切换进程,会导致 高速缓存 中的数据无法使用(进程具有独立性),需重新开始 预加载,这是非常浪费时间的(对于 CPU 来说);但切换线程就不一样了,由于进程没变,所以缓存内容也不变,切换线程时,所需要的数据的也不会发生改变,这就意味值 高数缓存 中的数据可以继续使用,不需重新加载,并且可以接着 预加载 下一波数据。

注:高速缓存中预加载的是公共数据,并非线程的私有数据

进程processtask_struct 称为 PCB线程(thread)task_struct 则称为 TCB

从今天开始,无论是 进程 还是 线程,都可以称为 执行流线程 从属于 进程当进程中只有一个线程时,我们可以粗粒度的称当前进程为一个单独的执行流;当进程中有多个线程时,则称当前进程为多执行流,其中每一个执行流就是一个个的线程。

执行流的调度由操作系统负责CPU 只负责根据 task_struct 结构进行计算

  • 若下一个待调度的执行流为一个单独的进程,操作系统仍需创建 PCB 及 虚拟地址空间、建立映射关系、加载代码和数据。
  • 但如果下一个待调度的执行流为一个线程,操作系统只需要创建一个 TCB,并将其指向已有的虚拟地址空间即可。

(三)线程与进程的关系

进程是承担系统资源分配的实体,比如 程序运行必备的:虚拟地址空间、页表映射关系、相关数据和代码 这些都是存储在 进程 中的,也就是我们历史学习中 进程 的基本概念。

线程CPU 运行的基本单位,程序运行时,CPU 只认识 task_struct 结构,并不关心你是线程还是进程,不过,线程包含于进程中,一个 进程可以只有一个线程,也可以有很多线程,当只有一个 线程时,通常将其称为 进程,但对于CPU 来说,这个进程本质上仍然是线程;因为 CPU 只认 task_struct 结构,并且 PCBTCB 都属于 task_struct ,所以才说线程是CPU 运行的基本单位。

总结:进程是由操作系统将程序运行所需地址空间、映射关系、代码和数据打包后的资源包,而 线程/轻量级线程/执行流 则是利用资源完成任务的基本单位。

我们之前学习的进程概念是不完整的,引入线程之后,可以对进程有一个更加全面的认识。

通常将程序启动,比如 main 函数中的这个线程称为 主线程,其他线程则称为 次线程:

实际上 进程 = PCB TCB + 虚拟地址空间 + 映射关系 + 代码和数据,这才是一个完整的概念

以后谈及进程时,就要想到 一批执行流+可支配的资源

在 Linux 中,认为 PCB TCB 的共同点太多了,于是直接复用了 PCB 的设计思想和调度策略,在进行 线程管理 时,完全可以复用 进程管理 的解决方案(代码和结构),这可以大大减少系统调度时的开销,做到 小而美,因此 Linux 中实际是没有真正的 线程 概念的有的只是复用 PCB 设计思想的 TCB。在这种设计思想下,线程 注定不会过于庞大,因此 Linux 中的 线程 又可以称为 轻量级进程(LWP轻量级进程 足够简单,且 易于维护、效率更高、安全性更强,可以使得 Linux 系统不间断的运行程序,不会轻易 崩溃。

与 一切皆文件一样,这种设计思想注定 Linux 会成为一款 卓越 的操作系统

别的系统采用的是其他方案,比如 Windows 使用的是真线程方案,为 TCB 额外设计了一逻辑,这就导致操作系统在同时面临 PCB TCB 时需要进行识别后切换成不同的处理手段,存在不同的逻辑容易增加系统运行不稳定的风险,这就导致 Windows 无法做到长时间运行,需要通过重启来重置风险
此时我的电脑中同时存在几百个进程和几千个真线程,可想而知操作系统的负担有多大:

(四)简单实用线程

接下来简单使用一下 pthread 线程原生库中的线程相关函数(只是简单使用,不涉及其他操作)

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadHandler1(void *args)
{
    while (true)
    {
        cout << "我是次线程1,我正在运行..." << endl;
        sleep(1);
    }
}

void *threadHandler2(void *args)
{
    while (true)
    {
        cout << "我是次线程2,我正在运行..." << endl;
        sleep(1);
    }
}

void *threadHandler3(void *args)
{
    while (true)
    {
        cout << "我是次线程3,我正在运行..." << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t t1, t2, t3; // 创建三个线程
    pthread_create(&t1, NULL, threadHandler1, NULL);
    pthread_create(&t2, NULL, threadHandler2, NULL);
    pthread_create(&t3, NULL, threadHandler3, NULL);

    // 主线程运行
    while (true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }

    return 0;
}

编译程序时,需要带上 -lpthread 指明使用 线程原生库:

g++ -o $@ $^ -std=c++11 -lpthread

结果:主线程+三个次线程同时在运行

至于为什么打印结果会有点不符合预期,这就涉及到 加锁 相关问题了,后面再解决

使用指令查看当前系统中正在运行的 线程 信息:

ps -aL | head -1 && ps -aL | grep mythread | grep -v grep

可以看到此时有 四个线程

  • 细节1:四个线程的 PID 都是 24786
  • 细节2:四个线程的 LWP 各不相同
  • 细节3:第一个线程的 PID 和 LWP 是一样的

其中,第一个线程就是 主线程,也就是我们之前一直很熟悉的 进程,因为它的 PID 和 LWP 是一样的,所以只需要关心 PID 也行。

操作系统如何判断调度时,是切换为 线程 还是切换为 进程 ?

  • 将待切换的执行流 PID 与当前执行流的 PID 进行比对,如果相同,说明接下来要切换的是 线程,否则切换的就是 进程。
  • 操作系统只需要找到 LWP 与 PID 相同的线程,即可轻松锁定主线程。

线程是进程的一部分,给其中任何一个线程发送信号,都会影响到其他线程,进而影响到整个进程。

(五)重谈虚拟地址空间

注:当前部分是拓展,与线程没有很大的关系,但是一个比较重要的知识点

1. 页表的大小

页表 是用来将 虚拟地址 和 物理地址 之间建立映射关系的,页表难道真的只是简单一 一存储映射吗?除此之外,页表 中还存在 其他属性 字段

在 32 位系统中,存在 2^32 个地址(一个内存单元大小是 1byte),虚拟地址空间中的每一个地址依次为 [0, 2^32 − 1] 0x00000000 - 0xFFFFFFFF,也就是我们常说的 4 GB 虚拟内存空间。

假设极端情况:每个地址都在页表中建立了映射关系,其中页表的每一列大小都是 4 字节,那么页表的大小就是 2^32 * 4 * 3 * 1byte = 48GB,这就意味着悲观情况下页表已经干掉 48GB 的内存了,但现在电脑普遍都只有 16GB 内存,更何况是几十年前的电脑。

所以说页表绝对不是采用这种单纯 地址->地址 的映射方案。

2. 内存与磁盘的交互

操作系统从 磁盘 中读取数据时,一次读取大量数据 比 多次读取少量数据 要快的多,因为 磁盘 是外设,每一次读取都必然伴随着寻址等机械运动(机械硬盘),无论是对于 内存 还是 CPU ,这都是非常慢的,为了尽可能提高效率,操作系统选择一次 IO 大量数据的方式读取数据。

通常 IO 的数据以  为基本单位,在文件系统中,一个  的大小为 4KB(一个块由8个扇区组成,单个扇区大小为 512Byte),所以OS在和磁盘这样的设备进行IO交互的时候,就不能按照字节为单位的而是要按照为单位。即使我们一次只想获取一个字节,操作系统最低也会 IO 一个 数据块4KB)。

4KB 这个大小很关键

  • 文件系统/编译器:文件存储时,需要以 4KB 为单位进行存储
  • 操作系统/内存:读取文件或进行内存管理时,也是以 4KB 为单位的

为了让内存与与磁盘更高效地进行IO,操作系统对内存也进行了按管理划分,OS将内存划分成一个个页框,其中每个页框可以存储的数据的大小为4KB,这4KB被称为 (Page)的数据。

为了将内存中的 Page 进行管理,需要 先描述,在组织,构建 struct page 结构体,用于描述 Page 的各种属性,比如是否为脏数据、是否已经被占用了,因为存在很多 Page,所以需要将这些 struct page 结构进行管理,使用的就是 数组(天然有下标) struct page mem[N],其中 N 表示当前内存中的 页 Page 数量。

struct page
{
	int status; // 基础字段:状态
	// 注意:这个结构不能设计的太复杂了,因为稍微大一点内存就爆了,所以里面的属性非常少
};

struct page mem[N]; // 管理 page 结构体的数组

假设我们的内存为 4GB,那么等分为 4KB 的 页 Page,可以得到约 100w 个 页 Page,其中 struct page 结构体不会设计的很大,大小是 字节 级别的,也就是说 struct page mem[100w] 占用的总大小不过 4~5MB,对于偌大的内存来说可以忽略不计。

内存管理的本质:

  • 申请:无非就是寻找 mem 数组中一块未被使用的足量空间,将对应的 Page 属性设置为已被申请,并返回起始地址(足量空间页框的起始地址)。
  • 使用:将磁盘中的指定的 4KB 大小数据块存储至内存中对应的 页 Page 中。
  • 释放:将 页 Page 属性设置为可用状态。

3. 局部性原理

重新视 4KB,为什么内存与磁盘交互的基本单位是 块(4KB

这里就要提一下 局部性原理 了:

局部性原理的特征

  • 现代计算机预加载的理论基础
  • 允许我们提前加载正在访问数据的 相邻或者附加的数据(数据预加载)

局部性原理 的核心在于 预加载,如果没有 局部性原理,那么我们可能今天都用不上电脑,因为如果没有这个原则,那么内存在于磁盘交互时,只能做到用户需要什么,就申请什么,这会直接拉低 CPU 的速度,而速度极快的 磁盘 又非常贵。

而 局部性原理 有效避免了这个问题:用户访问数据时,操作系统不仅会加载用想要访问的数据,同时还会加载当前数据的临近数据,如此一来就可以做到用户访问下一份数据时,不必再次 IO,尽量减少 IO 的次数。

  • 合理性:用户访问的数据大多都是具有一定连续性的,比如用户访问 668 号数据,那么他下一次想访问的数据大概是 669 及以后,因此可以提前加载669及以后的数据。

配合上 4KB 的块大小,可以使得每次 IO 足量的数据,并且有可能会多出,起到 预加载 的效果

所以现在就可以回答为什么是 4KB 

  1. IO 的基本单位,内核系统/文件系统 都对其提供了支持
  2. 利于通过 局部性原理 预测数据的命中情况,尽可能提高效率

总结:IO 的基本单位是 4KB ,内存实际上被划分成了很多个 4KB 的小块,并存在相应的数据结构对其进行管理。

4. 页表映射原理

显然,页表 绝对不可能动辄几十个 GB,实际在根据 虚拟地址 进行寻址时,页表 也有自己的设计逻辑。

虚拟地址(32 位操作系统) 大小也就是 32 比特位,大概也就是 4Byte,通常将一个 虚拟地址 分割为三份:10、10、12

  • 10:虚拟地址中的前 10 个比特位,用于寻址 页表2
  • 10:虚拟地址中间的 10 个比特位,用于寻找 页框起始地址
  • 12:虚拟地址中的后 12 个比特位,用于定位 具体地址(偏移量)

所以,实际上在通过 页表 进行寻址时,需要用到 两个页表(为了方便演示,仅包含一组 kv 关系):

注:“页表2” 中的 20 表示内存中的下标,即 页框地址

通常将 “页表1” 称为 页目录,“页表2” 称为 页表项

  • 页目录:使用 10 个比特位定位 页表项
  • 页表项:使用 10 个比特位定位 页框地址
  • 偏移量:使用 12 个比特位,在 页 Page 中进行任意地址的寻址

 所以即使是每个 物理地址 都被寻址的的极端情况下,页表 总大小不过为:(2^10 + 2^10) * (2^10 + 2^20),大约也就需要 4Mb 大小,即可映射至每一个 物理内存,但实际上 物理内存 并不会被时刻占满,大多数情况下都是使用一部分,因此实际页表大小不过 几十字节。

像这种 页框起始地址+偏移量 的方式称为 基地址+偏移量,是一种运用十分广泛的思想,比如所谓的 类型(int、double、char…)都是通过 类型的起始地址+类型的大小 来标识该变量大小的也就是说我们只需要 获得变量的起始地址,即可自由进行偏移操作(如果偏移过度了,就是越界),这也就解释了为什么取地址只会取到 起始地址。

总结:得益于 划分+偏移 的思想,使得页表的大小可以变得很小。

5. 扩展:动态内存管理

实际上,我们在进行 动态内存管理(malloc/new 申请堆空间时,操作系统 并没有立即在物理内存中申请空间(因为你申请了可能不会立马使用),而是 先在 虚拟地址 中进行申请(成本很低),当我们实际使用该空间时,操作系统 再去 填充相应的页表信息+申请具体的物理内存。

像这种操作系统赌博式的行为我们已经不是第一次见了,比如之前的 写时拷贝,就是在赌你不会修改,这样做的好处就是可以 最大化提高效率,对于内存来说,这种使用时再申请的行为会引发 缺页中断。

具体原理: 

当用户 动态申请内存 时,操作系统只会在虚拟地址中申请,具体表现为 返回一块未被使用的空间起始地址,用户实际使用这块空间时,遵循 查页表、寻址物理内存 的原则,实际进行 查页表 操作时,发现 页表项 没有记录此地址的映射关系,于是就会引发 缺页中断,发出对应的 中断信号陷入内核态,通过 中断控制器 识别 中断信号 后做出相应的动作,比如这里的动作是:填充页表信息、申请物理内存 ;把 物理内存 准备好后,用户就可以进行正常使用了,整个过程非常快,对于用户来说几乎无感知。

同理,在进行 磁盘文件读取 时,也存在 缺页中断 行为,毕竟你打开文件了,并不是立即进行读写操作的。

诸如这种 硬件级的中断行为 我们已经在 以往 信号产生 章节 中学过了,即:从键盘按下的那一刻,发出硬件中断信号,中断控制器识别为 键盘 发出的信号后,去 中断向量表 中查找执行方法,也就是 键盘 的读取方法。

所以操作系统根本不需要关系 硬件 是什么样子,只需要关心对方是否发出了 信号(请求),并作出相应的 动作(执行方法) 即可,很好的实现了 解耦。

对于 内存 的具体情况,诸如:是否命中、是否被占用、对应的 RWX 权限 需要额外的空间对其进行描述,而 页表 中的 其他属性 列就包含了这些信息:

对 内存 进行操作时,势必要进行 虚拟地址到物理地址 之间的转换,而 MMU 机制 + 页表信息 可以判断 当前操作 是否合法,如果不合法会报错。

注:UK 权限用于区分当前是用户级页表,还是内核级页表

比如这段代码:

char *ps = "good morning";
*ps = 'A'; // 此时程序会报错(需要赋值为字符,否则无法编译)

 结合 页表、信号 等知识,解释整个报错逻辑:

  • "good morning" 属于字符常量存储在字符常量区中,其中的权限为 R
  • char *ps 属于一个指针变量,指向字符常量的起始地址
  • 当我们进行 *ps = "A" 操作时,首先会将字符常量的地址转换为物理地址,在转换过程中,MMU 机制发现该内存权限仅为 R,但 *ps 操作需要 W 权限,于是 MMU 引发异常 -> 操作系统识别到异常,将该异常转换为 信号 -> 并把 信号 发给出现问题的 进程 -> 信号暂时被保存 -> 在 内核态转为用户态 的过程中,进行 信号处理 -> 最终结果是终止进程,也就是报错。

程序运行后,就会报错:

(六)小结

所以目前 地址空间 的所有组成部分我们都已经打通了,再次回顾这种设计时,会发现 用户压根不知道、也不需要知道虚拟地址空间之后发生的事,只需要正常使用就好了,当引发异常操作时,操作系统能在 查页表 阶段就进行拦截,而不是等到真正影响到 物理内存 时才报错。

所谓的 虚拟地址空间 就是在进行设计时添加的一层 软件层,它解决了 多进程时的物理内存访问问题、也解决了物理内存的保护问题,同时还为用户提供了一个简单的虚拟地址空间,做到了 虚拟与物理 的 完美解耦:

这种设计思想就是计算机界著名的 所有问题都可以通过添加一层 软件层 解决,这种思想早在几十年前就已经得到了运用。

这种分层结构不仅适用于 操作系统,还适用于 网络,比如大名鼎鼎的 OSI 七层网络模型。

二、线程优缺点

Linux 中没有 真线程,有的只是复刻进程代码和管理逻辑 轻量级线程(LWP

线程 有以下概念:

  • 在一个程序中的一个执行路线就叫做 线程(Thread),或者说 线程 是一个进程内部的控制程序
  • 每一个进程都至少包含一个 主线程
  • 线程 在进程内部执行,本质上仍然是在进程地址空间内运行
  • Linux 系统中,CPU 看到的 线程TCB 比传统的 进程PCB 更加轻量化
  • 透过进程地址空间,可以看到进程的大部分资源,将资源合理分配给每个执行流,就形成了 线程执行流

(一)优点

线程 最大的优点就是 轻巧、灵活,更容易进行调度

  • 创建一个线程的代价比创建一个进程的代价要小得多
  • 调度线程比调度进程要容易得多
  • 线程占用的系统资源远小于进程
  • 可以充分利用多处理器的并行数量(进程也可以)
  • 在等待慢速 IO 操作时,程序可以执行其他任务(比如看剧软件中的 “边下边看” 功能)
  • 对于计算密集型应用,可以将计算分解到多个线程中实现(比如 压缩/解压 时涉及大量计算)
  • 对于 IO密集型应用,为了提高性能,将 IO操作重叠,线程可以同时等待资源,进行 高效IO(比如 文件/网络 的大量 IO 需要,可以通过 多路转接 技术,提高效率)

线程 的合理使用可以提高效率,但 线程不是越多越好,而是 合适 最好,让每一个线程都能参与到计算中。

(二)缺点

线程 也是有缺点的:
1、性能损失:当线程数量过多时,频繁的线程调度所造成的消耗会导致 计算密集型应用 无法专心计算,从而造成性能损失。

2、 健壮性降低:在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

在下面这个程序中,次线程4 出现异常后,会导致整个进程运行异常,进而终止进程

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadHandler1(void *args)
{
    while (true)
    {
        cout << "我是次线程1,我正在运行..." << endl;
        sleep(1);
    }
}

void *threadHandler4(void *args)
{
    while (true)
    {
        sleep(5); // 等其他线程先跑一会
        cout << "我是次线程4,我正在运行..." << endl;
        char *ps = "good morning";
        *ps = 'A';
    }
}

int main()
{
    pthread_t t1, t4; // 创建两个线程
    pthread_create(&t1, NULL, threadHandler1, NULL);
    pthread_create(&t4, NULL, threadHandler4, NULL);

    // 主线程运行
    while (true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }

    return 0;
}

结果一轮到次线程4运行,因为触发异常,从而整个进程就直接终止了: 

为什么 单个线程 引发的错误需要让 整个进程 来承担?

  • 站在技术角度:完全可以让其自行承担,但这不合理
  • 系统角度:线程是进程的执行分支,线程出问题了,进程也不应该继续运行(比如一颗老鼠屎坏了一锅汤)
  • 信号角度:线程出现异常后,MMU 识别到异常 -> 操作系统将异常转换为信号 -> 发送信号给指定

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

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int g_val = 0;

void *threadHandler1(void *args)
{
    while (true)
    {
        printf("我是次线程1,我正在运行... &g_val: %p  g_val: %d\n", &g_val, g_val);
        sleep(1);
    }
}

void *threadHandler2(void *args)
{
    while (true)
    {
        printf("我是次线程2,我正在运行... &g_val: %p  g_val: %d\n", &g_val, g_val);
        g_val++; // 次线程2 每次都需改这个全局变量
        sleep(1);
    }
}

int main()
{
    pthread_t t1, t2; // 创建两个线程
    pthread_create(&t1, NULL, threadHandler1, NULL);
    pthread_create(&t2, NULL, threadHandler2, NULL);

    // 主线程运行
    while (true)
    {
        printf("我是主线程,我正在运行... &g_val: %p  g_val: %d\n", &g_val, g_val);
        sleep(1);
    }

    return 0;
}

结果:无论是主线程还是次线程,当其中的一个线程出现修改行为时,其他线程也会同步更改 

多个线程访问同时访问一个资源,不加以保护的话,势必会造成影响,当然这都是后话了(加锁相关内容)。

4、编程难度提高,编写与调试一个多线程程序需要考虑许多问题,诸如 加锁、同步、互斥 的等,面对多个执行流时,调试也是非常困难的。

(三)线程用途

合理的使用 多线程,可以提高 CPU 计算密集型程序的效率

合理的使用 多线程,可以提高 IO 密集型程序中用户的体验(具体表现为用户可以一边下载,一边做其他事情)

三、

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

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

相关文章

ARMv8/ARMv9架构入门到精通-学习方法

目录 1、学习ARM基础知识2、学习ARM异常(中断)3、学习MMU4、学习Cache5、学习Trustzone和安全架构6、学习ARM架构和各类IP推荐 本文转自 周贺贺&#xff0c;baron&#xff0c;代码改变世界ctw&#xff0c;Arm精选&#xff0c; 资深安全架构专家&#xff0c;11年手机安全/SOC底层…

WorldView卫星遥感影像数据/米级分辨率遥感影像

目前世界上最常用的高分辨率卫星影像莫过于WORLDVIEW系列了&#xff0c;在卫星遥感圈内可谓大名鼎鼎&#xff0c;不仅具有超高的分辨率还具有其他高分辨卫星所不具有的8波段&#xff0c;风光无限。在分辨率方面目前只有WORLDVIEW3和WORLDVIEW4能够达到0.3米的分辨率&#xff0c…

【神经网络与深度学习】LSTM(Long Short-Term Memory)神经网络模型

概述 LSTM&#xff08;Long Short-Term Memory&#xff09;是一种特殊的循环神经网络&#xff08;RNN&#xff09;结构&#xff0c;通常被用于处理和学习时间序列数据。因此&#xff0c;LSTM属于深度学习领域中的一种神经网络模型。 在深度学习中&#xff0c;LSTM被广泛应用于…

站库分离技术--反向代理技术-雷池云WAF-给自己搭建一个安全点的网站

文章目录 概要整体架构流程技术名词解释技术细节ssh-ubuntu服务器docker-映射-链接-通信nginx反代mysql设置数据库新密码 小结我的mysql映射目录我的wordpress映射目录 成果展示 概要 新买了一个云服务器&#xff0c;想搭建一个站库分离的wordpress为主的网站&#xff0c;采用d…

数据结构:图及相关算法讲解

图 1.图的基本概念2. 图的存储结构2.1邻接矩阵2.2邻接表2.3两种实现的比较 3.图的遍历3.1 图的广度优先遍历3.2 图的深度优先遍历 4.最小生成树4.1 Kruskal算法4.2 Prim算法4.3 两个算法比较 5.最短路径5.1两个抽象存储5.2单源最短路径--Dijkstra算法5.3单源最短路径--Bellman-…

CentOS 7安装MySQL及常见问题与解决方案(含JDBC示例与错误处理)

引言 MySQL是一个流行的开源关系型数据库管理系统&#xff0c;广泛应用于各种业务场景。在CentOS 7上安装MySQL后&#xff0c;我们通常需要使用JDBC&#xff08;Java Database Connectivity&#xff09;连接MySQL进行后端操作。 目录 引言 CentOS 7安装MySQL 使用JDBC连接My…

AI入门笔记(四)

深度学习是人工智能的一种实现方法。本文我将学习到的关于深度学习的代表卷积神经网络的数学结构分享给大家。 深度学习是重叠了很多层的隐藏层&#xff08;中间层&#xff09;的神经网络。我们以一个例题为例。 建立一个卷积神经网络&#xff0c;用来识别通过 66 像素的图像读…

基于VSCode安装Node.js开发环境

根据官网介绍&#xff0c;Node.js 是一个免费的、开源的、跨平台的JavaScript实时运行环境&#xff0c;允许开发人员在浏览器之外编写命令行工具和服务器端脚本. Node.js框架由于是采用JavaScript语法进行调用的&#xff0c;因此Node.js环境除了用来编写调试Node.js代码&#…

mybatis基础操作(三)

动态sql 通过动态sql实现多条件查询&#xff0c;这里以查询为例&#xff0c;实现动态sql的书写。 创建members表 创建表并插入数据&#xff1a; create table members (member_id int (11),member_nick varchar (60),member_gender char (15),member_age int (11),member_c…

视图【MySQL】

文章目录 概念操作视图创建视图查询视图更新视图删除视图 视图规则和限制 概念 MySQL 中的视图&#xff08;View&#xff09;是一个虚拟表&#xff0c;其内容由查询定义。视图本身不包含数据&#xff0c;这些数据是从一个或多个实际表中派生出来的&#xff0c;通过执行视图定义…

简单了解TCP/IP四层模型

什么是计算机网络&#xff1f; 计算机网络我们可以理解为一个巨大的城市地图&#xff0c;我们想从A地前往B地&#xff0c;其中要走的路、要避开的问题都交给计算机网络解决&#xff0c;直到我们可以正常的到达目的地&#xff0c;那么我们会把其中的过程抽象成一个网络模型&…

练习01-登录注册(简单)

一、用户登录/注册实现 综合前面学的知识来实现简单的注册登录功能 1.准备工作 注册登录页面 数据库&#xff0c;数据表 mybatis 坐标引入&#xff0c;MySQL驱动 配置 映射文件 用户实体类 Servlet代码 2.页面 不想手写的可以看博主IT黄大大【带源码】 【炫酷登录界…

吴恩达机器学习-可选实验室:可选实验:使用逻辑回归进行分类(Classification using Logistic Regression)

在本实验中&#xff0c;您将对比回归和分类。 import numpy as np %matplotlib widget import matplotlib.pyplot as plt from lab_utils_common import dlc, plot_data from plt_one_addpt_onclick import plt_one_addpt_onclick plt.style.use(./deeplearning.mplstyle)jupy…

机器学习——PPO补充

On-policy vs Off-policy 今天跟环境互动&#xff0c;并学习是on-policy 只是在旁边看&#xff0c;就是Off-policy 从p中选q个重要的&#xff0c;需要加一个weight p(x)/q(x) p和q不能相差太多 采样数太少导致分布差很多&#xff0c;导致weight发生变化 On-Policy -&g…

STM32F103 CubeMX ADC 驱动 PS2游戏摇杆控制杆传感器模块

STM32F103 CubeMX ADC 驱动 PS2游戏摇杆控制杆传感器模块 1. 工程配置1.1 配置debug口1.2 配置时钟1.3 配置ADC1.4 配置串口1.5 配置时钟1.6 生成工程 2. 代码编写2.1 串口代码2.2 ADC读取数据的代码 1. 工程配置 1.1 配置debug口 1.2 配置时钟 1.3 配置ADC 1.4 配置串口 1.5 …

小迪安全37WEB 攻防-通用漏洞XSS 跨站权限维持钓鱼捆绑浏览器漏洞

#XSS跨站系列内容:1. XSS跨站-原理&分类&手法 XSS跨站-探针&利用&审计XSS跨站另类攻击手法利用 XSS跨站-防御修复&绕过策略 #知识点&#xff1a; 1、XSS 跨站-另类攻击手法分类 2、XSS 跨站-权限维持&钓鱼&浏览器等 1、原理 指攻击者利用…

JavaWeb-Maven基础

Maven是专门用于管理和构建Java项目的工具&#xff0c;是 Apache 下的一个纯 Java 开发的开源项目&#xff0c;基于项目对象模型&#xff08;POM&#xff09;概念。先来学习一下Maven基础&#xff0c;等后面学完开发框架后再学Maven高级&#xff0c;这次的内容如下 一、概述 …

leetcode 热题 100_搜索二维矩阵

题解一&#xff1a; 二叉搜索树&#xff1a;从矩阵右上角观察&#xff0c;结构类似二叉搜索树&#xff0c;因此可以用类似的解法来做。具体做法是双指针从右上角开始&#xff0c;向左下角逐步搜索&#xff0c;如果当前值比目标值大&#xff0c;则向下移动&#xff0c;如果当前值…

体系班第十三节

1判断完全二叉树递归做法 有四种情况&#xff1a;1 左树完全&#xff0c;右数满&#xff0c;且左高为右高加一 2左满 &#xff0c;右满&#xff0c;左高为右高加一 3左满&#xff0c;右完全&#xff0c;左右高相等 4左右均满且高相等 #include<iostream> #include&l…

封装方法3

上一篇处理了单元格返回值改写 这一篇处理剩余普通方法返回值改写 已经给了Object的返回值&#xff0c;需要回调 //返回结果是22个单元格的值&#xff0c;怎么给调用方 Object value getCellValue(cell);没有给调用方的情况 value值内容是什么 处理ecxel-22个单元值的返回结…