【Linux系统】线程:线程库 / 线程栈 / 线程库源码阅读学习

news2025/2/8 6:17:37




在这里插入图片描述



一、线程库


1、线程库介绍:命名与设计

  • 命名:线程库通常根据其实现目的和平台特性进行命名。例如,POSIX标准定义了Pthreads(POSIX Threads),这是一个广泛使用的线程库规范,适用于多种操作系统。此外,还有像Windows下的Win32线程API,以及Java中的java.util.concurrent包等。

  • 设计:线程库的设计旨在隐藏底层操作系统细节,同时提供一个统一的接口来处理线程相关任务。良好的设计应该确保高效性、可移植性和易用性。例如,Pthreads提供了丰富的API集合,包括线程创建(pthread_create)、互斥锁(pthread_mutex_lock)等功能,使开发者能够专注于应用逻辑而非线程管理。



2、线程库封装


在这里插入图片描述


将LWP的操作方法和属性全部封装到线程库中,创造逻辑上的线程

Linux 中,为了模拟实现线程,使用了 LWP(轻量级进程)这一机制。因此,创建线程的过程实际上就是创建 LWP 的过程。然而,创建 LWP 的系统调用较为复杂,并且不能真正代表线程的概念。鉴于此,设计者们将 LWP 的创建以及相关操作封装成一系列与线程相关的操作,并将其集成到线程库中。

用户不仅需要利用线程库中的方法来创建和操作线程,有时还需要获取线程的相关属性,比如线程优先级、线程状态等信息。由于 Linux 线程的本质实际上是 LWP,这些属性被存储于系统底层的struct lwp 结构体中。值得注意的是,struct lwp 并不能完全反映概念上的线程,同时,获取struct lwp 的属性值也需要通过特定的LWP系统调用来完成。因此,设计者们进一步将struct lwp 封装为struct TCB(线程控制块),并放入线程库中。

这样一来,通过将LWP的操作方法和属性全部封装为用户概念上的“线程”,使得用户无需关心底层的具体实现细节,而只需知道Linux系统同样支持“真正”的线程概念。


通过将底层关于 LWP 的操作方法和属性全部封装到线程库中,用户仅需在程序中使用线程库提供的接口(如pthread_create()pthread_join()等)进行线程操作,无需直接与内核或操作系统交互。这种设计让用户程序可以专注于业务逻辑,而不必担心底层的实现细节变化。即使底层的线程实现方式发生变化(例如从 LWP 切换到其他线程模型),只要线程库的接口保持不变,应用程序就不需要做任何修改。此外,这种抽象层次的提升简化了编程过程,减少了出错的可能性。更重要的是,它提高了代码的跨平台兼容性,因为 POSIX 线程库提供了一套标准接口,使得代码可以在不同平台上运行。无论是在 Linux 中通过 LWP 实现的线程,还是在其他操作系统中采用不同的线程实现方式,只要遵循 POSIX 标准,线程库就能确保相同的接口在不同平台上正常工作,从而实现了用户软件层面与系统底层的解耦合,增加了代码的可移植性。



3、线程库的功能

  • 一般的库提供可调用方法和数据:

我们平时学习的静态库或动态链接库,在一般意义上可以被理解为一组预编译好的代码和数据,供开发者通过编程语言提供的接口查询并引用。这种库主要是为了提供可重用的功能和简化开发过程。

  • 线程库不仅提供可调用方法和数据,还具有管理线程的功能:

然而,“线程库”不仅仅是一个普通的库,它实际上是一组操作系统或运行时环境提供的系统级服务和工具,用于管理线程的创建、调度、同步等操作。因此,线程库不仅仅是提供一些方法供程序调用,它还涉及到实际的资源管理和状态跟踪。

  • 线程库可以存储和维护进程中所有线程的 TCB

    • 线程管理的本质:线程库负责管理进程内的线程生命周期,这包括创建新线程、销毁线程以及在线程间进行切换。为了实现这些功能,线程库需要访问并更新每个线程的状态信息,即TCB。因此,线程库内部必然有一个机制来存储和维护这些TCB。

    • 内存管理:线程库会在进程中分配特定的内存区域来存储TCB集合。这个内存区域是线程库的一部分,位于进程的地址空间中,使得同一进程中的所有线程都可以访问到这些信息。这样的设计保证了线程间的高效协作与通信。

    • 系统级支持:虽然从用户角度来看,线程库可能表现为一系列API的集合,但其实现往往依赖于底层操作系统提供的功能和服务。例如,操作系统会提供系统调用来支持线程的创建和调度,而线程库则是这些低级别服务的一个高级封装,方便程序员使用。同时,这也意味着线程库可以在一定程度上利用操作系统的资源管理能力来实现对TCB的存储和维护。

综上所述,线程库不仅仅是简单的函数和数据集合,它还包括了对线程状态进行管理和维护的能力,这是通过直接管理内存中的TCB集合来实现的。

  • 线程信息存储于 struct tcb 中,供系统和用户使用

用户使用 pthread_create 创建线程并执行线程对应的 func 函数,该函数的返回值以及线程退出的一些退出信息,其实都存在 struct tcb 中!这就是为什么 pthread_join 等函数可以直接获取线程退出信息,就是从 struct tcb 中获取的

因为进程中所有线程共享一个线程库,因此一个库内部,维护系统所有线程的 struct tcb 属性集合!


4、线程库加载的位置


线程ID:TCB的地址?

pthread_self() 函数获取的是调用该函数的线程的 线程标识符(Thread ID, TID),而不是逻辑工作组编号(LWP, Light Weight Process)。在 POSIX 线程(pthreads)的上下文中,这个线程标识符是一个类型为 pthread_t 的值,它唯一地标识了当前进程中的一个线程。

唯一性:每个线程在其所属的进程中都有一个唯一的 pthread_t 标识符。这意味着同一个进程中的两个线程不会具有相同的 pthread_t 值。

这个线程ID实际上是线程 TCB 在线程库中的地址位置!如何理解?看下文:



线程库加载于 mmap 区域

在共享区(mmap区域)中,加载着动态库:线程库

线程库中存储着多个描述线程的属性结构体 TCB,而 pthread_self 函数返回的线程标识符 TID,就是线程的属性结构体在库中的存储地址,因此未来想要获取某个线程的属性,只需获取到该线程属性结构体的地址,就能获取相关属性

数组形式:在线程库中,所有的 TCB按照数组形式存放,这样就完成了线程的再组织

创建一个线程,其实就要在该线程库中创建一个这样的结构体 TCB 用于描述线程,而 TCB 存储于线程库中被管理


在这里插入图片描述




二、线程栈

1、多线程使用栈易冲突

之前讲解过,因为同进程下的多线程共享该进程大部分的资源,包括进程地址空间,其中,

  • 代码区:每个线程通过获取自己线程执行函数的地址,拥有了自己的一块代码区。
  • 共享区:无需分割,所有线程直接共享使用一些动态库等资源。
  • 堆区:每个线程在自己的执行函数里面动态开辟堆区空间,自己开辟给自己使用,因为堆区底层是为申请空间的线程创建一个 vm_area_struct ,然后划分一块堆区空间给该线程。
  • 栈区:而栈区就有点特殊了,整个地址空间只有一个栈区,线程调用函数和创建局部变量等操作,都需要在栈区开辟函数栈帧空间,这样就可能会造成多个线程使用栈的冲突。


2、线程栈是开辟申请出来的


进程创建出来时,形成的第一个栈其实是主线程的栈

而调用 pthread_create 创建新线程,其实需要给新线程创建新的独立的栈

创建新的栈空间,就有点类似于堆区的动态申请开辟空间的逻辑。

因此,根据这个思路,创建新线程时,就在地址空间的栈区开辟一块栈空间给新线程,这就是新线程拥有的线程栈



再谈系统调用 clone


实际上,系统调用 fork / vfork 和线程库的 pthread_create 都封装了这个函数,创建新线程,为新线程分配栈空间就是从这里开始的:

在这里插入图片描述


3、线程局部存储 __thread

线程之间可以通过获取其他线程的栈数据的地址,通过该地址访问其他线程的栈数据,这句话说明一个道理,在同一进程地址空间中,所有线程栈之间的隔离并非绝对隔离,如果你能获取其他线程栈数据的地址,也是能够访问的,没有所谓的权限隔离层

代码引入

如下代码:定义一个全局变量 shared_var,创建两个新线程,两个新线程都能访问到同一个全局变量 shared_var,其中一个线程对该变量进行修改


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



int shared_var = 100;
void* run1(void* argv)
{
    std::string name = static_cast<char*>(argv);
    while(true)
    {
        printf("I am [%s], shared_var: %d, &shared_var: %p\n", name.c_str(), shared_var, &shared_var);
        sleep(1);
    }
}

void* run2(void* argv)
{
    std::string name = static_cast<char*>(argv);
    while(true)
    {
        printf("I am [%s], shared_var: %d, &shared_var: %p\n", name.c_str(), shared_var, &shared_var);
        shared_var++;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, run1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, run2, (void*)"thread-2");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    return 0;
}


运行结果如下:双线程访问的该变量的地址都一样,一个线程的修改也对其他线程的访问产生影响,这其实就是所谓的线程安全问题(因此这种被多线程共享访问的资源一般需要加上同步互斥的保护机制(这点之后的文章会讲解))

在这里插入图片描述



关键字 __thread :线程局部存储


关键字 __thread 的使用


此时,我们在该全局变量前面加上关键字:__thread

__thread int shared_var = 100;

运行结果如下:

在这里插入图片描述

可以看到一个线程对该变量的修改,影响不了另一个线程,并且两个线程访问的全局变量 shared_var 似乎不是同一个了,因为地址不同了



关键字 __thread 的作用

__thread 是GCC编译器的一个扩展特性,用于声明线程局部变量。使用 __thread 修饰的变量将在每个线程中拥有独立的实例。换句话说,每个线程都有自己的一份该变量的副本,这些副本是相互隔离的,一个线程对该变量的操作不会影响其他线程中的相同变量。

程序会在不同使用该相同变量的线程栈空间内部拷贝生成一个一摸一样的,当该线程使用该变量时 就会到自己的栈空间使用该变量,并不会影响到其他线程

实际上,每个线程在其启动时就会为这些 __thread 变量分配空间,并根据需要进行初始化。因此,每个线程看到的都是完全独立的变量,即使它们在源代码中看起来像是同一个变量。

这就是线程的局部存储!



不仅限于内置类型

一个常见的误解是 __thread 只能用于内置类型。实际上,它同样适用于自定义类型,如类或结构体。无论是简单的整数还是复杂的自定义对象,都可以使用 __thread 来确保每个线程访问的是自己的独立副本。这对于需要在线程间保持状态的应用场景非常有用,例如,当每个线程需要维护自己的配置或状态信息时。

对于结构体类型:也是可以用 __thread 修饰

struct B
{
    int _a = 100;
};

__thread struct B gb;
void* run1(void* argv)
{
    std::string name = static_cast<char*>(argv);

    while(true)
    {
        printf("I am [%s], gb._a: %d, &gb: %p\n", name.c_str(), gb._a, &gb);
        sleep(1);
    }
}
void* run2(void* argv)
{
    std::string name = static_cast<char*>(argv);
    while(true)
    {
        printf("I am [%s], gb._a: %d, &gb: %p\n", name.c_str(), gb._a, &gb);
        gb._a++;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, run1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, run2, (void*)"thread-2");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    return 0;
}

在这里插入图片描述



线程安全与同步

虽然 __thread 提供了线程之间的隔离,但这并不意味着使用它的代码就是完全线程安全的。如果涉及到对 __thread 变量进行非原子操作,比如同时读写复杂对象的不同部分,仍然可能需要额外的同步机制来保证数据一致性。因此,在实际应用中,开发者需要根据具体情况设计合理的同步策略,以确保程序的正确性。



三、线程库源码的阅读:从剖析线程

1、线程库

本文我们讲解 pthread_create 在线程库的实现,而不是讲解整个线程库



对于 pthread_create 函数

函数原型

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   			void *(*start_routine) (void *), void *arg);

参数说明

  • pthread_t *thread: 一个指向 pthread_t 类型的指针,用于存储新创建线程的标识符。这个标识符可以用于引用新创建的线程。
  • const pthread_attr_t *attr: 指向线程属性对象的指针。可以设置为 NULL 使用默认属性。
  • void *(*start_routine) (void *): 新线程开始执行的函数的名称。这个函数接受一个 void* 类型的参数,并返回一个 void* 类型的结果。
  • void *arg: 传递给 start_routine 函数的参数。****

使用样例

pthread_create(&tid, NULL, func, (void*)"thread-1")


pthread_create 源码的完整展示


下面的图展示的是 原生线程库 nptl 的一部分(可以看到,线程库其实也是 glic 库的一部分!)

在下面的源码中,对于一些重要的地方我都用黄色记号标记起来了,后面我会一一讲解:点击跳转讲解部分


在这里插入图片描述




源码剖析

重点1:线程属性 pthread_attr

点击跳转阅读该结构的源码


在这里插入图片描述



阅读这里的源码,在源码程序中,会将 attr 传递给同类型的 iattr

const struct pthread_attr *iattr =(struct pthread_attr *)attr;

这个 attr 是从哪里来的?是用户使用函数 pthread_create 时,传递进来的!

在这里插入图片描述


用户可以传递一个实际的 attr,也可以传递 NULL


// 传递一个实际的 attr : 需要用户自定义
struct pthread_attr attr;
pthread_create(&tid, attr, func, (void*)"thread-1");


// 传递 NULL
pthread_create(&tid, NULL, func, (void*)"thread-1");

对于源码中的程序,当用户传递的 struct pthread_attr 类型数据为 NULL 时,底层的 struct pthread_attr 字段会赋值为一个默认值:

在这里插入图片描述



重点2:线程TCB

传说中的原生线程库中的用来描述线程的 tcb :结构 struct pthread ( 点击跳转阅读该 TCB 结构的源码讲解 )


在这里插入图片描述



因此,我们使用 pthread_create 函数创建线程,首先就给我们定义了一个线程TCB结构 struct pthread 的指针



重点3:ALLOCATE_STACK

源码中,程序向 传递一个线程属性 pthread_attr类型的 iattr 和 一个 struct pthread 的指针类型数据 &pd(这个就是刚刚讲解的线程TCB)

在这里插入图片描述



问题:而 ALLOCATE_STACK 这个有什么来头?

答:其实 ALLOCATE_STACK 可以帮我们申请栈空间,

( 点击跳转阅读对 空间申请 ALLOCATE_STACK的完整源码和源码讲解 )



重点4:设置线程执行函数和参数

就是把线程的执行函数的地址和参数设置给 struct pthread pd

在这里插入图片描述



重点5:线程 TCB 的ID值是虚拟地址

这里将 newthread 设置为 pd,pd 可是我们线程TCB结构 struct pthread 的虚拟地址呀!

我们函数 pthread_self 获取的线程 ID 实际上就是一个地址!!!!

也就是线程TCB结构 struct pthread 在线程库中的 虚拟地址位置!

在这里插入图片描述

在这里插入图片描述



重点6:检查线程是否分离

这个就顾名思义了
在这里插入图片描述



重点7:创建线程

当前面所有工作做好了,最后我们调用函数 create_thread 创建和启动一个线程

在这里插入图片描述

点击跳转阅读 create_pthread 的源码和逻辑



2、线程 TCB:struct pthread

在源码中,线程TCB是一种名为 struct pthread 的结构,该结构有 200多行代码,其中比较重要需要我们认识的字段我都标注好了,某些没怎么重要的地方因篇幅限制就截掉了,如下图


在这里插入图片描述



看下面这张图,线程TCB结构被管理组织在 mmap 区域的 线程库 中,其中一个TCB结构主要包含着三个部分:struct pthread 、线程局部存储、线程栈

  • struct pthread :这个结构实际上就是我们上面阅读的线程底层结构 struct pthread
  • 线程局部存储:记录一些线程内部的局部存储数据
  • 线程栈:这里并不是开辟了一块空间,而是记录着一个线程栈的指针,指向其他被开辟作为线程栈的空间

在这里插入图片描述




3、线程属性 pthread_attr


在这里插入图片描述



在线程属性结构中,定义了许多重要的字段,例如线程栈的地址和栈大小:

在这里插入图片描述



4、空间申请 ALLOCATE_STACK

完整源码


在这里插入图片描述




源码讲解

在这里插入图片描述



这里先使用 get_cached_stack ,在线程库的缓存空间中申请空间

如果申请失败,再使用 mmap(...) ,这个会在物理内存上申请一块空间,然后将这块空间映射到线程库的存储位置中

可以简单理解 mmap(...) 类似 malloc (可以提一嘴: mmap(...) 实际上就是 malloc 的底层实现!)

在这里插入图片描述



将刚刚申请的内存空间地址 mem 记录在 pd->stackblock 中, pd->stackblock 不就是前面讲解过存储在线程结构struct pthread 中,线程栈的地址指针吗

在这里插入图片描述



最后将 pd 赋值给 *pdp

在这里插入图片描述



可以看到 pd 是函数内部设置的局部变量,pdp 是传递进来的一个 struct pthread ** 类型的输出型参数!

在这里插入图片描述



再看回该函数的调用处:

这里 &pd 就是函数参数 struct pthread **pdp ,通过这个函数,将一个 struct pthread 类型的数据设置完成

在这里插入图片描述



5、创建线程 create_pthread

完整源码


在这里插入图片描述




源码讲解

在这里插入图片描述

调用 do_clone 函数,如下:



6、函数 do_clone

完整源码


在这里插入图片描述




源码讲解

执行特性体系结构下的 clone 函数,这个 ARCH 的意思是体系结构,这个不应该,我们继续跳转看一下 ARCH_CLONE 是何方神圣:

在这里插入图片描述


有没有发现,从头到尾,从 struct pthread* pd 的创建,到空间开辟,到各种属性完善,到传递到 ARCH_CLONE 函数,最后传入系统底层的系统调用 clone ,都是以 struct pthread 的指针形式传入,这个意思是至始至终我们都在使用和修改一个 struct pthread 线程TCB结构,这样做的好处是:线程的状态,空间申请情况…. 的实时状态可以被更新,我们用户上层可以获取该线程 TCB结构,进而了解一个线程的各种属性!!

在这里插入图片描述



7、体系结构下的 clone 函数 ARCH_CLONE

完整源码

在这里插入图片描述



源码讲解

认真的看一下该函数下的这些汇编指令:

这里第 27 行的指令是获取 系统调用clone 的系统调用号

第 32 行的指令 syscall ,使我们陷入内核!!!!

在这里插入图片描述

一切都连起来了,这个就是一种软件中断,通过调用系统调用,通过固定的软中断号 syscall 触发中断,陷入内核中执行中断例程,执行对应的系统调用!!!



四、再谈线程栈

虽然 Linux 将线程和进程不加区分的统一到了 task_struct,但是对待其地址空间的 stack 还是有些区别的。

  • 对于 Linux 进程或者说主线程,简单理解就是 main 函数的栈空间,在 fork 的时候,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯一可以访问未映射页而不一定会发生段错误——超出扩充上限才报。

  • 然而对于主线程生成的子线程而言,其 stack 将不再是向下生长的,而是事先固定下来的。线程栈一般是调用 glibc/uclibcpthread 接口 pthread_create 创建的线程,在文件映射区(或称之为共享区)。其中使用 mmap 统调用,这个可以从 glibc 的 nptl/allocatestack.c 中的 allocate_stack 函数中看到:

在这里插入图片描述

此调用中的 size 参数的获取很是复杂,你可以手工传入 stack 的大小,也可以使用默认的,**一般而言就是默认的 8M。**这些都不重要,重要的是,这种 stack 不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。在 glibc 中通过 mmap 得到了 stack 之后,底层将调用 sys_clone 系统调用:

在这里插入图片描述


因此,对于子线程的 stack,它其实是在进程的地址空间中 map 出来的一块内存区域,原则上是线程私有的,但是同一个进程的所有线程生成的时候,是会浅拷贝生成者的 task_struct 的很多字段,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

因为只要你能获取我栈数据的地址,你就能访问我的“私有”栈.

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

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

相关文章

深度剖析 Redis:缓存穿透、击穿与雪崩问题及实战解决方案

一、缓存基本使用逻辑 在应用程序中&#xff0c;为了提高数据访问效率&#xff0c;常常会使用缓存。一般的缓存使用逻辑是&#xff1a;根据 key 去 Redis 查询是否有数据&#xff0c;如果命中就直接返回缓存中的数据&#xff1b;如果缓存不存在&#xff0c;则查询数据库&#…

如何使用el-table的多选框

对el-table再次封装&#xff0c;使得功能更加强大&#xff01; 本人在使用el-table时&#xff0c;因为用到分页&#xff0c;导致上一页勾选的数据在再次返回时&#xff0c;没有选中&#xff0c;故在原有el-table组件的基础之上再次进行了封装。 1.首先让某些不需要勾选的列表进…

【工具变量】上市公司企业渐进式创新程度及渐进式创新锁定数据(1991-2023年)

测算方式&#xff1a; 参考顶刊《经济研究》孙雅慧&#xff08;2024&#xff09;老师的做法&#xff0c;用当期创新和往期创新的内容重叠度作为衡量渐进式创新程度的合理指标。通过搜集海量专利摘要&#xff0c;测算当前专利申请和既有专利的内容相似度&#xff0c;反映企业在…

LM Studio 部署本地大语言模型

一、下载安装 1.搜索&#xff1a;lm studio LM Studio - Discover, download, and run local LLMs 2.下载 3.安装 4.更改成中文 二、下载模型(软件内下载) 1.选择使用代理&#xff0c;否则无法下载 2.更改模型下载目录 默认下载位置 C:\Users\用户名\.lmstudio\models 3.搜…

嵌入式工程师面试经验分享与案例解析

嵌入式工程师岗位受到众多求职者的关注。面试流程严格&#xff0c;技术要求全面&#xff0c;涵盖C/C编程、数据结构与算法、操作系统、嵌入式系统开发、硬件驱动等多个方向。本文将结合真实案例&#xff0c;深入剖析嵌入式工程师的面试流程、常见问题及应对策略&#xff0c;帮助…

英特尔至强服务器CPU销量创14年新低,AMD取得进展

过去几年是英特尔56年历史上最艰难的时期之一。该公司在晶圆代工、消费级处理器和服务器芯片等各个领域都面临困境。随着英特尔重组其晶圆代工业务&#xff0c;新的分析显示其服务器业务的现状和未来前景不容乐观。 英特尔最近发布的10-K文件显示&#xff1a;“数据中心和人工…

判断您的Mac当前使用的是Zsh还是Bash:echo $SHELL、echo $0

要判断您的Mac当前使用的是Zsh还是Bash&#xff0c;可以使用以下方法&#xff1a; 查看默认Shell: 打开“终端”应用程序&#xff0c;然后输入以下命令&#xff1a; echo $SHELL这将显示当前默认使用的Shell。例如&#xff0c;如果输出是/bin/zsh&#xff0c;则说明您使用的是Z…

使用Springboot实现MQTT通信

目录 一、MQ协议 MQTT 特点 MQTT 工作原理 MQTT 主要应用场景 MQTT 配置与注意事项 二、MQTT服务器搭建 三、参考案例 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种基于发布/订阅模型的轻量级消息传输协议&#xff0c;常用于物联网&#xff…

植物大战僵尸融合版(电脑/安卓)

《植物大战僵尸融合版》是一款由B站UP主“蓝飘飘fly”制作的同人策略塔防游戏&#xff0c;基于经典《植物大战僵尸》玩法&#xff0c;加入了独特的植物融合系统。 出于方便&#xff0c;软件是便携版&#xff0c;解压后双击即可畅玩。 游戏主页依旧是植物大战僵尸经典界面。右下…

02DevOps基础环境准备

准备两台Linux的操作系统&#xff0c;最简单的方式就是在本机上使用虚拟机搭建两个操作系统&#xff08;实际生产环境是两台服务器&#xff0c;虚拟机的方式用于学习使用&#xff09; 我搭建的两台服务器的ip分别是192.168.1.10、192.168.1.11 192.168.1.10服务器用于安装doc…

苍穹外卖-day12(工作台、数据导出)

工作台Apache POI导出运营数据Excel报表 功能实现&#xff1a;工作台、数据导出 工作台效果图&#xff1a; 数据导出效果图&#xff1a; 在数据统计页面点击数据导出&#xff1a;生成Excel报表 1. 工作台 1.1 需求分析和设计 1.1.1 产品原型 工作台是系统运营的数据看板&…

详解享元模式

引言 在计算机中&#xff0c;内存是非常宝贵的资源&#xff0c;而程序中可能会有大量相似或相同的对象&#xff0c;它们的存在浪费了许多空间。而享元模式通过共享这些对象&#xff0c;从而解决这种问题的。 1.概念 享元模式(Flyweight Pattern)&#xff1a;运用共享技术有效地…

openEuler22.03LTS系统升级docker至26.1.4以支持启用ip6tables功能

本文记录了openEuler22.03LTS将docker升级由18.09.0升级至26.1.4的过程&#xff08;当前docker最新版本为27.5.1&#xff0c;生产环境为保障稳定性&#xff0c;选择升级到上一个大版本26的最新小版本&#xff09;。 一、现有环境 1、系统版本 [rootlocalhost opt]# cat /etc…

< OS 有关 > Ubuntu 版本升级 实践 24.04 -> 24.10, 安装 .NET

原因&#xff1a; 想安装 .NET 9 去编译 GitHut 项目&#xff0c;这回用不熟悉的 Ubuntu来做&#xff0c;不知道怎么拐去给 Ubuntu 升级&#xff0c;看到现在版本是 24.10 但不是 LTS 版本&#xff0c;记录下升级过程。 一、实践过程&#xff1a; 1. 查看当前版本 命令1: l…

某咨询大数据解决方案介绍(32页PPT)

本文档介绍了一个大数据平台解决方案&#xff0c;旨在解决企业当前面临的数据问题&#xff0c;包括数据定义缺失、重复采集和存储、数据不完整以及缺乏可靠决策依据等。通过引入大数据技术&#xff0c;该方案强调从被动的IT支撑向主动的数据核心服务转型&#xff0c;以实现科学…

matlab simulink 汽车四分之一模型主动被动悬架-LQR

1、内容简介 略 matlab simulink 可以交流、咨询、答疑 124- 2、内容说明 略汽车悬架系统由弹性元件、导向元件和减振器组成,是车身与车轴之间连接的所有组合体零件的总称,也是车架(或承载式车身)与车桥(或车轮)之间一切力传递装置的总称,其主要功能是使车轮与地面有很好的…

从零开始:OpenCV 图像处理快速入门教程

文章大纲 第1章 OpenCV 概述 1.1 OpenCV的模块与功能  1.2 OpenCV的发展 1.3 OpenCV的应用 第2章 基本数据类型 2.1 cv::Vec类 2.2 cv&#xff1a;&#xff1a;Point类 2.3 cv&#xff1a;&#xff1a;Rng类 2.4 cv&#xff1a;&#xff1a;Size类 2.5 cv&#xff1a;&…

强化学习笔记6——异同策略、AC、等其他模型总结

异步两种方法&#xff1a;1&#xff1a;经验回放 2&#xff1a;数据动作非同时产生 举例QLearning为什么是异策略&#xff1f; 生成动作时e的概率从Q表选&#xff0c;1-e概况随机。 更新策略时&#xff0c;贪心策略选择Q_max作为动作。 策略优化两种主要方法&#xff1a;基于梯…

Linux提权--passwd提权

passwd​ 命令用于更改用户密码。在 Linux 系统中&#xff0c;普通用户可以通过 passwd​ 更改自己的密码&#xff0c;但如果攻击者能够以某种方式执行 passwd​ 命令更改 root 用户的密码&#xff0c;他们就能获取 root 权限。 1.常见的 passwd 提权方法 SUID 设置&#xff1…

一、本地部署安装 DeepSeek 并训练本地知识库,并调用对话框进行问答

本地部署安装 DeepSeek 1、硬件环境 操作系统&#xff1a;Windows10 内存&#xff1a;16G 显卡&#xff1a;NIVIDIA GeForce RTX 2060 6G 2、安装步骤 &#xff08;1&#xff09;安装 Ollama 访问Ollama 官网&#xff0c;点击 “Download for Windows” 下载安装程序。下载…