【Linux学习】多线程——线程控制 | 线程TCB

news2024/11/24 14:27:51

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

线程控制 | 线程TCB

  • 🧰线程控制
    • 🎴线程创建
    • 🎴线程结束
    • 🎴线程等待
      • 线程返回值
      • 线程取消(线程结束的一种方式)
    • 🎴线程分离
  • 🧰C++多线程
  • 🧰线程库中的TCB
    • 🎴线程tid
    • 🎴线程局部存储(__thread)
  • 🧰总结

🧰线程控制

Linux内核中并不存在线程的概念,我们程序员是通过库来使用线程的,这个库是POSIX线程库,是由原生线程库提供的,它遵守POSIX标准,就像之前学过的System V标准一样。POSIX线程库有以下几个特点:

  1. 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
  2. 要使用这些函数库,要通过引入头文<pthread.h>。
  3. 链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

🎴线程创建

系统调用接口:

图

  • pthread_t* thread:输出型参数,将线程的tid值放入到我们外部创建好的pthread_t 类型的变量中。
  • 第二个参数:用来设置线程属性,一般情况下设置成nullptr,等用到的时候再详细讲解。
  • void* (*start_routine)(void *):函数指针,这是一个回调函数,该函数的内容就是新线程要执行的。
  • void* arg:回到函数的参数。
  • 返回值: 线程创建成功返回0,不成功返回错误码。

一般情况下,新线程的创建是不会失败的,万一失败了,也不会设置errno,因为errno是一个全局变量,某个线程改变了这个变量会对其他线程造成影响,所以直接将错误码返回即可。

  • 在编译的时候,必须指定线程库,使用-l pthread选项。

接下来用这个接口创建一批线程:

#define NUM 10

void* start_routine(void* args)
{
    sleep(1);
    string name = (char*)(args);
    while(1)
    {
        cout<<"new thread name: "<<name<<endl;
        sleep(1);
    }
}

int main()
{
    //创建一批线程
    for(size_t i = 0; i < NUM; ++i)
    {
        pthread_t tid;
        char buffer[64];
        snprintf(buffer,sizeof buffer,"thread %d",i+1);
        pthread_create(&tid,nullptr,start_routine,(void*)buffer);
    }

    while(1)
    {
        cout<<"----create success----"<<endl;
        sleep(1);
    }
    return 0;
}

创建10个线程,让它们同时运行,并且给每个线程编号,新线程死循环打印各自的线程名字,新线程在延时1秒后开始执行。

图
将上诉代码运行起来后,查看线程,可以看到一共有11个线程,其中1个主线程,10个新线程。

图
但是运行结果中,10个线程都是线程10,其他9个线程并没有出现,这是什么原因呢?

图

  • 新线程中首先要延时1秒钟,然后才开始执行代码,在它延时的过程中,主线程一直在跑。
  • 主线程中的名字缓冲区会被覆盖,最终只有"thread 10"。
  • 当10个新线程开始执行时,需要去缓冲区中拿数据(缓冲区所有线程共享),所以拿到的都是"thread 10"。

上面代码中,本喵故意给新线程先延时了一秒钟,让主线程先跑,去覆盖缓冲区,如果不延时也有可能会出现上诉情况。

  • 主线程和新线程到底谁先执行是不确定的,是由操作系统的调度器决定的。

即使不给新线程延时,也有可能是主线程先运行,在时间片结束之前,同样会完成数据覆盖,导致新线程从缓冲区中只能读到最终的数据。

所以说,上面的代码是有问题的,我们需要保证每个线程都有自己独一无二的缓冲区。

class ThreadData
{
public:
    pthread_t _tid;
    char _name[64];
};

创建一个类,这个类中包括线程的tid以及名字的缓冲区。

图

  • 每个线程都在堆区new一个对象,来存放该线程的tid以及名字,然后将这个对象的地址传给新线程。
  • 新线程通过主线程传过来的指针找到属于它的结构体对象,然后使用里面的数据。

图
此时10个线程就都能正常运行了,不存在缓冲区的覆盖问题了,因为一个线程有一个缓冲区。

🎴线程结束

  1. return nullptr结束线程

图
当新线程执行到return的时候,就会结束。

  • 在线程中加了计数值,5秒后跳出循环,执行return,结束线程。

图
当计数值到了以后,新线程全部结束,只剩下主线程在执行。

  1. pthread_exit()结束线程

POSIX线程库专门提供了一个接口来结束线程:

图

  • 参数:返回线程结束信息,当前阶段设置成nullptr即可。

调用该接口的线程会结束。

图
同样,当计数值到了以后,新线程会调用该接口,然后就只剩下主线程了,新线程全部结束了。

注意:

不能使用exit()来结束线程,因为exit系统调用是争对进程的,调用该接口会让整个进程都结束掉。

🎴线程等待

和进程一样,线程也是需要等待的,如果不等待会造成内存泄漏,也就是结束掉的线程PCB不会被回收(类似僵尸进程),但是我们看不到没有回收的现象。

系统调用:

图

  • pthread_t thread:要等待的线程tid。
  • void** retval:线程结束信息返回,这是一个输出型参数。
  • 返回值:等待成功返回0,等待失败返回错误码。

图
主线程中并没有延时,它执行的速度是很快的。在新线程中需要进行计数,所以执行速度会慢很多。

图

可以看到,主线程在执行到线程等待的时候,会阻塞等待,不再往下执行,直到所有线程都等待成功才会继续向下执行。

所以说,线程等待是阻塞式等待

线程返回值

线程等待和进程等待一样,主要有两个作用:

  • 获取线程退出信息。
  • 回收线程PCB资源,防止内存泄漏。

上面线程等待的代码中并没有获取线程退出的相关信息,那么该如何获取线程退出的相关信息呢?

图

  • 新线程在结束的时候会返回一个void*类型的指针。
  • 在pthread线程库中,有一个void类型的指针变量来接收从线程中返回的void指针。
  • 指针变量和指针是有区别的,指针变量会开辟空间,里面存放的是指针。
  • 指针就是地址,是数字,不会开辟空间。

如上图中代码所示,将整形数字10强转成void*类型,然后返回。

  • 现在面临的问题就是怎么从pthread线程库中拿到从线程中返回的void*指针。

图
在主线程的栈区中有一个void类型的指针变量,新线程中返回的void类型指针最终会放到这个ret中。

  • pthread线程库中有一个void** 类型的二级指针变量retval。
  • pthread_join()系统调用将主线程中void*类型的指针变量的地址传给了pthread线程库中的二级指针变量,此时主线程就和线程库建立了联系。
  • 将新线程中返回到线程库中的void*指针变量中的返回值,通过这种联系放到主线程中指针变量中----也就是 *retval = ret。

这样,我们就可以成功的获取到新线程退出时的返回信息了,桥梁就是pthread_join()系统调用。

pthread_join()系统调用中,之所以传的是二级指针,是为了在pthread库中能够找到主线程中一级指针变量 void * ret。

图
在线程等待时,传入ret的二级指针获取线程退出信息。

  • 由于Linux中void* ret是8个字节,接收到的线程退出信息10也是一个void*类型的。
  • 我们要想看到这个值,需要将它转换成整数,所以必须转成longlong类型,也是8个字节,如果转成int的话会有精度损失从而会报错。

图
可以看到,每个线程在退出时的退出信息都被主线程接收到了,由于所有线程的退出信息都是10,所以接收到的也都是10。

图
通过pthread_exit()同样可以将线程的退出信息返回到pthread的线程库中,然后再通过线程等待接口拿走这个退出信息。

  • 在结构体中增加一个线程编号信息,每创建成功一个线程都给它一个编号。
  • 新线程在退出的时候返回各自的编号。
  • 线程等待代码不变,和上面一样。

图

此时我们就成功获得了各个线程在退出时候返回的编号,也就是获得了线程的退出信息。

整数都可以返回,更别说一个真正的地址了,可以将要返回的信息放在数组中,然后返回数组地址

  • 在学习进程等待的时候,我们不仅可以获得进程的退出信息,还能获得进程的退出信号,但是在线程退出时就没有获得线程退出信号,这是为什么呢?
  • 因为信号是发给进程的,整个进程都会被退出,线程要退出信号也没有意义了。
  • 而且pthread_join默认是能够等待成功的,并不考虑异常的问题,异常是进程要考虑的事,线程不用考虑。

线程取消(线程结束的一种方式)

线程取消的接口:

图

  • 参数:要取消的线程tid。
  • 返回值:取消成功返回0,失败返回错误码。
  • 只有运行起来的线程才能被取消。

图
在主线程中,新线程被创建后,取消一半的线程,然后继续进行线程等待。

图
10个线程被创建后就会跑起来。

  • 前五个线程被取消了,线程等待直接成功,不用再阻塞,被取消的线程等待成功后的返回值是-1,并不是我们设定的线程编号。
  • 未被取消的后五个线程,仍然阻塞等待,等待成功后返回的是我们设定的线程编号。

所以说,如果一个线程是被取消结束的,它的退出码就是-1。它其实是一个宏定义:PTHREAD_CANCELED。

线程取消也是一种线程结束的方式,放在这里是为了能够通过线程等待看线程退出的退出码。

🎴线程分离

线程的tid也可以通过接口获得,就像获得pid一样,获取tid的接口:

图
这个接口也是POSIX线程库提供的,哪个线程调用该接口就会返回哪个线程的tid。

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

但是这样主线程就需要阻塞式等待线程的释放,主线程什么都干不了。能不能像进程那样不需要阻塞式等待(将SIGCHID信号设置为忽略),等新线程结束以后自动释放呢?

  • 尤其是不需要关心线程返回值的时候,join是一种负担。

当然可以,将需要自动释放的线程设置成分离状态,将线程设置成分离状态意味着不需要主线程再关心该线程的状态,它会自动释放。

  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

线程分离的接口:

图

  • 参数:要分离的线程tid。
  • 返回值:成功返回0,不成功返回错误码。
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。自己分离自己就需要使用接口获取到自己的tid。

线程分离后,如果主线程仍然等待该线程,就会等待失败,返回错误码

新线程中分离自己:

void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    size_t cnt = 5;
    pthread_detach(pthread_self());//线程分离
    while(cnt--)
    {
        cout<<"new thread name: "<<name<<", cnt: "<<cnt<<endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

int main()
{
    //创建新线程
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread one");
    cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;

    //线程等待
    int n = pthread_join(tid,nullptr);
    cout<<"error: "<<n<<"->"<<strerror(n)<<endl;

    return 0;
}

在新线程中分离线程。
图
不是说线程分离了再进行线程等待就会失败吗?怎么上面的运行结果仍然是等待成功呢?

  • 因为主线程先被调度,在新线程被创建但是没有执行的时候主线程就开始等待新线程了。

所以当新线程将自己分离以后,主线程已经处于等待状态了,它不认为新线程被分离,还会继续等待,而且可以等待成功。

图
可以让主线程延时一段时间,保证新线程先执行,也就是保证线程分离发生在线程等待之前。

图
可以看到,此时主线程在进行线程等待的时候就会失败,而且返回错误码。

在主线程中分离新线程:

最为稳妥的办法就是在主线程中分离新线程:

图
在主线程中分离新线程,任何进行线程等待,并且主线程在一直运行。

图

  • 主线程等待新线程失败后直接返回错误码,然后接着向下运行,并不会阻塞。

在新线程运行结束以后,自动回收其PCB资源,只剩下主线程在运行。

  • 一个线程一旦被分离就不用再管这个线程了,在它运行结束的时候系统会自动回收,不会造成内存泄漏。

🧰C++多线程

我们知道,C++也是可以多线程编程的,而且提供了多线程的库,而无论什么编程语言,什么库,在Linux系统上的多线程本质上都是对pthread原生线程库的封装

接下面本喵就模拟一下C++对线程库的封装,写一个小组件,同时也方便我们后面直接使用:

#define NUM 1024

class Thread;//前置声明

class Context//线程上下文
{
public:
    Context()
    :_this(nullptr)
    ,_args(nullptr)
    {}

    //成员变量
    Thread* _this;
    void* _args;
};

class Thread
{
public:
    //重命名函数对象
    typedef std::function<void*(void*)> func_t;
    
    //构造函数
    //传入新线程执行的函数,参数,以及新线程编号
    Thread(func_t func, void* args = nullptr, int number = 0)
        :_func(func)
        ,_args(args)
    {
        //格式化线程名
        char buffer[NUM];
        snprintf(buffer,sizeof (buffer),"thread-%d",number);
        _name = buffer;
        //创建线程
        Context* ctx = new Context();
        ctx->_this = this;
        ctx->_args = _args;
        int n = pthread_create(&_tid,nullptr,start_routine,ctx);
    }

    void* run(void* args)
    {
        return _func(args);
    }

    //由于调用成员函数有隐藏的this指针,所以使用static修饰
    //void* start_routine(this, void* args)
    static void* start_routine(void* args)
    {
        Context* ctx = static_cast<Context*>(args);//安全的类型转换
        void* ret = ctx->_this->run(ctx->_args);
        delete ctx;
        return ret;
    }

    //线程等待
    void join()
    {
        int n = pthread_join(_tid,nullptr);
        assert(n==0);
        (void)n;
    }
private:
    std::string _name;
    func_t _func;
    void* _args;
    pthread_t _tid;
};

tu

  • 在调用start_routine成员函数的时候,会隐藏一个this指针。
  • 而pthread_create中的函数指针只有一个形参,为了消除这个指针,用static修饰新线程调用的函数。

此时就面临一个新的问题,在static函数内,需要调用类内的成员函数run(),但是没有this指针无法调用。

  • 创建一个上下文类,里面放线程类的this指针,在static函数内通过这个指针来调用类内的成员函数run()。

测试代码:

void* thread_run(void* args)
{
    string work_type=static_cast<const char*>(args);
    while(1)
    {
        cout<<"新线程:"<<work_type<<endl;
        sleep(1);
    }
}

int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"thread1",1));
    unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"thread2",2));
    unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"thread3",3));

    thread1->join();
    thread2->join();
    thread3->join();


    return 0;
}

tu
可以看到,成功创建3个新线程,并且在不停运行。

这里仅是语言层面对线程库的封装。

🧰线程库中的TCB

🎴线程tid

前面多次见过线程的tid值,但是一直不知道它是什么,现在来揭开它的神秘面纱。

图
新线程和主线成都打印新线程的tid,并且主线程也打印自己的tid。

图

  • 主线程和新线程打印的新线程tid的值都是一样的。
  • 而且tid的值是一个地址。

图
我们知道,Linux内核中是没有线程概念的,也没有对应的TCB结构。

  • 用户创建线程时使用的是POSIX线程库提供的接口。
  • 线程库中会调用clone()系统调用接口,在内核中创建线程复用的PCB结构。
  • 这些轻量级进程共用一个进程地址空间。

系统中肯定不只一个线程存在,大量的线程势必要管理起来,管理的方式同样是先描述再组织。既然Linux内核中只有轻量级进程的PCB,那么描述线程的TCB结构就只能存在于线程库中

所以pthread线程库中就会维护很多TCB结构:

//伪代码
struct pthread
{
	//线程局部存储
	//线程栈
	//....
}

线程库中的TCB里,存放着线程的属性,这里的TCB被叫做用户级线程

  • Linux线程方案:用户级线程以及用户关心的线程属性在线程库中,内核提供线程执行流的调度。
  • Linux 用户级线程 : 内核轻量级进程= 1 :1

一个线程的所有属性描述是由两部组成的,一部分就是在pthread线程库中的用户级线程,另一部分就是Linux中的轻量级进程,它们俩的比例大约是1比1。

图

  • pthread线程库从磁盘上加载到内存中后,通过页表再将虚拟地址空间和物理地址映射起来。
  • 线程库最终是映射在虚拟地址空间中的共享区中的mmap区域。

既然线程库是映射在共享区的,那么线程库所维护的TCB结构也就一定在共享区。

图

如上图所示,将映射到共享区的动态线程库放大。

  • 线程库中存在多个TCB结构来描述线程。
  • 每个TCB的地址就是线程id。

线程tid的本质就是虚拟地址共享区中TCB结构体的地址

  • 线程的栈也在共享区中,而不在栈中。
  • 虚拟地址空间中的栈是主线程的栈,共享区中动态库中的栈是新线程的栈。

所以说,线程的栈结构是相互独立的,因为存在于不同的TCB中(主线程除外)

🎴线程局部存储(__thread)

在共享区线程库中的TCB里,有一个线程的局部存储属性,它是一个介于全局变量和局部变量之间线程特有的属性。

图
在主线程和新现在中同时打印全局变量g_val以及它的地址。

图
主线程和新线程打印的值都是一样的。

  • 说明主线程和新线程共用一个全局变量。

那如果此时新线程仍然想用这个变量名,但是又不想影响其他线程,也就是让这个全局变量独立出来,该怎么办呢?此时就可以使用线程的局部存储属性了。

图

  • 在全局变量g_val前面加__thread(两个下划线),此时这个全局变量就具有了局部存储的属性。

主线程和新线程同样打印这个全局变量,并且新线程将这个具有局部存储属性的全局变量不断加一。

图

  • 主线程和新线程打印出来的全局变量的地址不相同了,说明此时用的并不是同一个全局变量。
  • 新线程修改这个值,主线程不受影响。
  • 可以将全局变量或者static变量添加 __thread,设置位线程局部存储。
  • 此时每个线程的TCB中都会有一份该变量,相互独立,并不会互相影响。

🧰总结

有了进程的基础,线程有些地方可以进行类比,还是比较容易理解的。线程控制非常重要,而且在编程中经常使用到。

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

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

相关文章

华为eNSP综合实验学习记录

区域一&#xff1a;ISP 题目要求&#xff1a; 1、ISP区域&#xff0c;每台路由器需要指定一个环回口地址&#xff0c;所有设备之间互联使用/30网段&#xff0c;server1服务器区域是有10台服务器&#xff0c;使用10.1.1.0/24网段进行划分。LSW1是一台OLT设备&#xff0c;连接家…

谷歌账号注册流程全攻略

谷歌账号是获取谷歌各种服务的通行证&#xff0c;包括Gmail、Google Drive、Google Photos、Google Maps等。拥有一个谷歌账号可以让你的数字生活变得更加便捷。下面我们一起来看看谷歌账号的注册流程。 一、访问谷歌账号注册页面 首先&#xff0c;你需要访问谷歌账号注册页面…

【MYSQL】基础的增删查改

目录 1.create(新增) 2.retrieve(查询) 1.全列查询&#xff1a;*代表所有&#xff1b; 2.指定列查询&#xff1a; 3.取别名 4.去重--distinct 5.排序--order by 类型 asc 6.条件筛选--where 类型数值&#xff1b; 7.筛选null--is/not is 8.范围查找between and 9…

springboot第七章 结合Dubbo

实现Dubbo分布式框架&#xff0c;需要公共接口maven项目&#xff0c;需要服务提供者springboot项目&#xff0c;需要服务消费者springboot项目。 因为公共接口只有数据类和接口&#xff0c;后期提供者和消费者需要根据maven唯一坐标来导入公共接口项目的jar包&#xff0c;因此公…

Python简单教程(第01章---第04章)

Python简单教程 1. Python综述1.1 python是什么1.2 python的发展1.3 python的特点 2.Python3安装2.1 python3 和 python2的区别2.2 python3环境的安装&#xff08;以windows为例&#xff09;2.3 Anaconda 安装&#xff08;可选&#xff09; 3. Python基本语法3.1 编码3.2 标识符…

内网渗透之横向移动 委派-非约束委派约束委派资源委派

0x01 横向移动-非约束委派 原理&#xff1a; 机器A&#xff08;域控&#xff09;访问具有非约束委派权限的机器B的服务&#xff0c;会把当前认证用户&#xff08;域管用户&#xff09;的的TGT放在ST票据中&#xff0c;一起发送给机器B&#xff0c;机器B会把TGT存储在lsass进程…

tomcat乱码解决方案

2.将里面的java.util.logging.ConsoleHandler.encoding 的值改为GBK。如下图&#xff1a;

电影推荐算法

模型训练 下载数据集&#xff0c;解压到项目目录下的./ml-1m文件夹下。数据集分用户数据users.dat、电影数据movies.dat和评分数据ratings.dat。 ** 数据集分析 ** user.dat&#xff1a;分别有用户ID、性别、年龄、职业ID和邮编等字段。 数据集网站地址为http://files.group…

数智未来,因你而来 | 昇腾AI创新大赛2023全新启动

在5月6日-7日举行的昇腾AI开发者峰会2023上&#xff0c;昇腾AI创新大赛2023正式启动。大赛旨在鼓励全产业开发者基于昇腾AI技术和产品&#xff0c;打造软/硬件解决方案、探索模型算法&#xff0c;加速AI与行业融合&#xff0c;促进开发者能力提升。 会上&#xff0c;中国工程院…

【vite+vue3.2 项目性能优化实战】使用vite-plugin-cdn-import进行CDN加速优化项目体积

CDN&#xff08;Content Delivery Network&#xff09;即内容分发网络&#xff0c;是一种通过在全球范围内分布式部署服务器来加速网络内容传输的技术。CDN加速的原理是&#xff0c;当用户请求访问某个资源时&#xff0c;CDN会根据用户的地理位置和网络状况&#xff0c;自动选择…

【内置函数】——高级编程——如桃花来

目录索引 1. hasattr()&#xff1a;2. getattr()&#xff1a;有&#xff1a;没有则报错&#xff1a; 3. setattr()&#xff1a;4. delattr():5. issubclass():6. isinstance():判断前面是不是属于后面的类型&#xff1a;判断前面是不是属于后面的类型之一&#xff1a;判断前面是…

百度网盘密码数据兼容处理

文章目录 一、问题描述二、代码实现1. 配置类2. 数据库配置3. config 配置类4. AOP 通知类5. 数据层6. 业务层7. 实体类 三、测试及结果 一、问题描述 需求&#xff1a; 对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。 分析&#xff1a; ① 在业务方法执行之前对所…

目标检测YOLO(V1、V2、V3)入门

one-stage和two-stage的区别 YOLO常见的指标 YOLO V1 yolo v1架构解读 yolo v1损失函数 NMS非极大值抑制 yolo v1版本的问题 重叠在一起的物体不好分类&#xff0c;只是一个单分类问题多标签问题&#xff08;狗和哈士奇&#xff09; v2和v1的区别 v2网络结构 batch normaliz…

【iOS】---pthread,NSThread

在iOS中多线程开发有四种方式&#xff0c;在之前我们浅浅了解了一下GCD&#xff0c;这期来看看pthread和NSThread pehread pthread简介 pthread 是一套通用的多线程的 API&#xff0c;可以在Unix / Linux / Windows 等系统跨平台使用&#xff0c;使用 C 语言编写&#xff0c;…

Redis持久化之RDB高频问题

1、RDB是如何应用的&#xff1f; 因为记录的是操作命令&#xff0c;而不是实际的数据&#xff0c;所以&#xff0c;用 AOF 方法进行故障恢复的时候&#xff0c;需要逐一把操作日志都执行一遍。如果操作日志非常多&#xff0c;Redis 就会恢复得很缓慢&#xff0c;影响到正常使用…

ApacheBench网站压力测试

ApacheBench &#xff08;简称ab&#xff09;是一个指令列程式&#xff0c;可用于网站压力测试&#xff0c;亦可用于发起CC攻击&#xff0c;请不要滥用哦。ApacheBench &#xff08;简称ab&#xff09;是一个指令列程式&#xff0c;可用于网站压力测试。如果已经安装过Apache&a…

Linux基本指令----上

Linux基本指令----上 ls指令pwd指令cd指令touch指令mkdir指令rmdir指令&&rm指令man指令cp指令mv指令cat指令echo指令more指令less指令head指令tail指令结语 ls指令 语法&#xff1a; ls [选项] [目录或文件] 功能&#xff1a; 对于目录&#xff0c;该命令列出该目录下…

Spring Boot集成ShardingSphere实现按月数据分片及创建自定义分片算法 | Spring Cloud 44

一、前言 在前面我们通过以下章节对数据分片有了基础的了解&#xff1a; Spring Boot集成ShardingSphere实现数据分片&#xff08;一&#xff09; | Spring Cloud 40 Spring Boot集成ShardingSphere实现数据分片&#xff08;二&#xff09; | Spring Cloud 41 Spring Boot集…

【Windows】高效的本地文件搜索工具《Everything》

&#x1f433;好用高效的本地文件搜索工具《Everything》 &#x1f9ca;一、什么是Everything&#x1f9ca;二、为什么选择Everything&#x1f9ca;三、下载Everything&#x1f9ca;四、Everything为什么高效 &#x1f9ca;一、什么是Everything Everything是一个运行于Window…

单片机GD32F303RCT6 (Macos环境)开发 (五)—— IAP代码架构工程

IAP代码架构工程 1、IAP一般分两个工程&#xff0c;一个Bootloader工程&#xff0c;一个Application工程。 这两个工程的差异后面会讲。 IAP架构工程的好处在于产品上线以后&#xff0c;想要升级的话&#xff0c;不用借助烧录器&#xff0c;就可以完成产品的软件升级。 2、Boo…