Linux从0到1——进程池

news2024/11/25 2:32:59

Linux从0到1——进程池

  • 1. 进程池的概念
  • 2. 进程池实现思路
  • 3. 进程池的代码实现
    • 3.1 创建管道,创建子进程
    • 3.2 封装任务
    • 3.3 Work接口
    • 3.4 发送任务
    • 3.5 回收资源,关闭管道(重点)
    • 3.6 改造CreatChannels接口
  • 4. 完整代码


1. 进程池的概念


1. 池化技术:

  • 在古代,有一种建筑叫粮仓,用来储存粮食。人们一般会先把大量的粮食囤积在粮仓中,等到需要用粮食时,再从粮仓中一点一点的拿。相当于是先把粮食提前准备好,你需要的时候直接去拿就行了,不然的话每次用粮食前还要先去田里收割粮食。

2. 内存池:

  • 计算机中有很多的设计理念,都用到了池化技术。如内存池,当一个进程需要申请空间时,OS会一次性开辟好大量的空间,进程直接去使用这些空间即可。而不是每一次进程申请空间时,操作系统都要做开辟空间的动作,这样太低效了。

3. 进程池:

  • 如果我们每启动一个任务,就创建一个进程来完成这个任务,这样未免有些低效。我们可以将多个进程一次性开辟好,等任务来的时候,直接丢给开辟好的进程即可,省去了多次创建进程的时间开销。

2. 进程池实现思路


在这里插入图片描述

  • 一次性开辟多个管道和多个子进程,让每一个管道和子进程一一对应。

  • 父进程每次向管道中送入4字节int类型的数据,表示需要执行任务的任务码code,一旦管道中被放入了数据,意味着这个管道对应的子进程被激活,开始执行任务码对应的任务。


3. 进程池的代码实现


3.1 创建管道,创建子进程


1. 描述管道:

  • 为了管理我们创建的管道,需要为其创建对应的结构体来描述它,然后用一定的数据结构组织起来。
const int num = 5; // 最大管道数
int number = 0;    // 管道编号

class channel
{
public:
    channel(int fd, pid_t id)
        : ctrlfd(fd), workerid(id)
    {
        name = "channel-" + std::to_string(++number);
    }

public:
    int ctrlfd;         // 管道写端
    pid_t workerid;     // 对应的子进程pid
    std::string name;   // 管道名
};
  • 结构体中,需要记录管道的写端,以便将来父进程关闭。
  • 还需要记录对应的子进程pid,以便子进程退出后,wait处理子进程僵尸状态。

2. 创建管道,创建进程:

  • 介绍一下C++中规范的传参形式:
    • 输入型参数:const &
    • 输出型参数:*
    • 输入输出型参数:&
// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&

void CreatChannels(std::vector<channel> *c)
{
    // bug version

    for (int i = 0; i < num; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向通信信道
        if (id == 0)
        {
            // child
            close(pipefd[1]); // 关闭写端
            // TODO
            dup2(pipefd[0], 0); // 重定向
            Work();
            exit(0);    // 会自动关闭自己打开的所有fd
        }

        // father
        close(pipefd[0]); // 关闭读端
        c->push_back(channel(pipefd[1], id));
    }
}

int main()
{
    std::vector<channel> channels;
    // 创建信道,创建进程
    CreatChannels(&channels);

    // ...

    return 0;
}
  • Work函数为将来子进程执行任务时要完成的模块。重定向操作执行后,每一个子进程中,fd为0的位置就指向读端。
  • 每建立一条管道,就将它放入数组中。这样,对管道的管理就变为了对数组的管理。

上面这种创建管道和进程的方式有一个很深层次的bug,我们到后面说。


3.2 封装任务


Task.hpp头文件:

#pragma once

#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>

typedef std::function<void()> task_t;

// 三个任务
void Download()
{
    std::cout << "我是一个下载任务" << " 处理者: " << getpid() << std::endl;
}

void PrintLog()
{
    std::cout << "我是一个打印日志的任务" << " 处理者: " << getpid() << std::endl;
}

void PushVideoStream()
{
    std::cout << "我是一个推送视频流的任务" << " 处理者: " << getpid() << std::endl;
}


class Init
{
public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_push_videostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;
public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);

        srand(time(nullptr) ^ getpid());    // ^getpid(),让数据更有随机性
    }

    // 检查任务码是否安全
    bool CheckSafe(int code)
    {
        if (code >= 0 && code < tasks.size()) return true;
        else return false;
    }

	// 执行任务
    void RunTask(int code)
    {
        tasks[code]();
    }

	// 选择任务
    int SelectTask()
    {
        // 返回随机任务码
        return rand() % tasks.size();
    }

	// 返回任务码对应的任务
    std::string ToDesc(const int &code)
    {
        switch(code)
        {
            case g_download_code:
                return "Download";
            case g_printlog_code:
                return "PrintLog";
            case g_push_videostream_code:
                return "PushVideoStream";
            default:
                return "Unknow";
        }
        return "";
    }
};

Init init;  // 定义对象
  • 定义了三条任务,即这三条任务对应的任务码。
  • 定义了一个Init类来封装这些任务,以及提供对应的接口。
  • 这里我们采取随机选择任务的设计,实际应用中会有所不同。三个任务也只是象征性的模拟一下,并不是具体的下载,打印日志,推送视频流任务。

3.3 Work接口


不断从管道中读取任务码,执行对应的任务。直到写端关闭。

void Work()
{
    while (true)
    {
        int code = 0;   // 任务码
        ssize_t n = read(0, &code, sizeof(code));
        if (n == sizeof(code))
        {
            if (!init.CheckSafe(code))
                continue;
            init.RunTask(code);
        }
        else if (n == 0)
        {
            // n == 0 写端退出
            break;
        }
        else
        {
            // do nothing
        }
    }
    
    std::cout << "child quit" << std::endl;
}

3.4 发送任务


  • 设计了标签g_always_loop来控制是否一直发送任务。true为一直发送,false为不是一直发送。传false要配合参数num,标明发送次数。
  • 轮巡式的向管道中发送信息。
const bool g_always_loop = true;    // 是否一直执行

void SendCommand(const std::vector<channel> &channels, bool flag, int num = -1)
{
    int pos = 0;
    while (true)
    {
        // 1. 选择任务
        int command = init.SelectTask();
        // 2. 选择信道(进程)
        const auto &c = channels[pos++];
        pos %= channels.size();

        // debug
        std::cout << "send command " << init.ToDesc(command) << "[" << command << "]" 
                  << " in" << c.name << "worker is: " << c.workerid << std::endl;

        // 3. 发送任务
        write(c.ctrlfd, &command, sizeof(command));
        // 4. 判断是否要退出
        if (!flag)
        {
            num--;
            if (num <= 0) break;
        }

        sleep(1);
    }

    std::cout << "SendCommand done..." << std::endl;
}

int main()
{
    std::vector<channel> channels;
    // 创建信道,创建进程
    CreatChannels(&channels);

    // 开始发送任务
    SendCommand(channels, !g_always_loop, 10);

	// ...

    return 0;
}

3.5 回收资源,关闭管道(重点)


1. 版本1:

  • 这种版本为,先将写端全部关闭,再回收子进程,自动关闭读端,循环两次。该版本不会出bug
  • 原理是,写端关闭后,Work模块中read会读到0,子进程就会执行到exit退出。
void ReleaseChannels(const std::vector<channel> &channels)
{
    // version1
    for (const auto &c : channels)
    {
        close(c.ctrlfd);
    }

    for (const auto &c : channels)
    {
        pid_t rid = waitpid(c.workerid, nullptr, 0);
        if (rid == c.workerid)
        {
            std::cout << "wait child: " << c.workerid << " success" << std::endl;
        }
    }
}

int main()
{
    std::vector<channel> channels;
    // 创建信道,创建进程
    CreatChannels(&channels);

    // 开始发送任务
    SendCommand(channels, !g_always_loop, 10);

    // 回收资源,想让子进程退出,并且释放管道资源,只要关闭写端即可(写端关闭后,子进程自动退出)
    ReleaseChannels(channels);

    return 0;
}

2. 会出现bug的版本:

  • 关闭一个写端,就关闭对应的读端,循环一次。
  • 这种版本会出现bug,现象就是会在发送任务完成时卡死。
void ReleaseChannels(const std::vector<channel> &channels)
{
    // bug version
    for (const auto &c : channels)
    {
        close(c.ctrlfd);
        pid_t rid = waitpid(c.workerid, nullptr, 0);
        if (rid == c.workerid)
        {
            std::cout << "wait child: " << c.workerid << " success" << std::endl;
        }
    }
}

在这里插入图片描述

3. 分析bug出现的原因:

在这里插入图片描述

  • 这个bug要追溯到创建管道的模块。首先,如果以我们之前的方式创建管道和进程,那么在创建第二个管道时,子进程拷贝父进程struct files_struct结构体,将上一个管道的写端也拷贝下来了。此时,第一个管道就会有两个写端。
  • 以此类推,在创建第三个管道时,子进程又将父进程的struct files_struct结构体继承下来了。第一个管道就会有三个写端,第二个管道会有两个写端。
  • 此时如果从第一个管道开始关闭管道,回收子进程,就会出现,第一个管道只关闭了一个写端,read无法读取到0,子进程在read处阻塞的情况。
  • 版本1之所以不会出现bug,是因为,最后一个管道只有一个写端,将父进程对应的写端一次性全部关闭后,最后一个进程就没有写端了,最后一个子进程就会退出。最后一个子进程退出了,倒数第二个管道也就没有写端了,倒数第二个子进程也要跟着退出。从后向前,管道和子进程资源依次释放。

2. 版本2:

  • 根据上面分析的bug出现原因,我们只需要从后向前关闭管道即可。
void ReleaseChannels(const std::vector<channel> &channels)
{
    // version2
    int num = channels.size() - 1;
    for (; num >= 0; num--)
    {
        close(channels[num].ctrlfd);
        pid_t rid = waitpid(channels[num].workerid, nullptr, 0);
        if (rid == channels[num].workerid)
        {
            std::cout << "wait child: " << channels[num].workerid << " success" << std::endl;
        }
    }
}

在这里插入图片描述


3.6 改造CreatChannels接口


通过3.5的讲解,我们是可以从后向前关闭管道,来解决卡死的问题。但是,这毕竟不是问题的本质,我们期望的管道是单向通信的,也就是只有一个写端和一个读端,出现一个管道多个写端的情况,不是我们想看到的。

所以,我们可以在创建管道时,提前记录一下子进程都从父进程继承下来了哪些写端,然后让子进程将这些写端关闭。

void CreatChannels(std::vector<channel> *c)
{
    std::vector<int> old;
    for (int i = 0; i < num; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向通信信道
        if (id == 0)
        {
            // child

            // 关闭从父进程继承下来的写端
            if (!old.empty())
            {
                for (auto fd : old)
                {
                    close(fd);
                }
                // debug
                PrintFd(old);
            }

            close(pipefd[1]); // 关闭写端
            // TODO
            dup2(pipefd[0], 0); // 重定向
            Work();
            exit(0);    // 会自动关闭自己打开的所有fd
        }

        // father
        close(pipefd[0]); // 关闭读端
        c->push_back(channel(pipefd[1], id));
        old.push_back(pipefd[1]);
    }
}
  • 这样一来,问题就解决了,回收资源时使用3.5中的bug版本也没有问题。可以说,那个bug版本才是逻辑最恰当的版本。

4. 完整代码


1. Task.hpp头文件:

#pragma once

#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>

typedef std::function<void()> task_t;

void Download()
{
    std::cout << "我是一个下载任务" << " 处理者: " << getpid() << std::endl;
}

void PrintLog()
{
    std::cout << "我是一个打印日志的任务" << " 处理者: " << getpid() << std::endl;
}

void PushVideoStream()
{
    std::cout << "我是一个推送视频流的任务" << " 处理者: " << getpid() << std::endl;
}


class Init
{
public:
    // 任务码
    const static int g_download_code = 0;
    const static int g_printlog_code = 1;
    const static int g_push_videostream_code = 2;
    // 任务集合
    std::vector<task_t> tasks;
public:
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(PrintLog);
        tasks.push_back(PushVideoStream);

        srand(time(nullptr) ^ getpid());    // ^getpid(),让数据更有随机性
    }

    // 检查任务码是否安全
    bool CheckSafe(int code)
    {
        if (code >= 0 && code < tasks.size()) return true;
        else return false;
    }

    // 执行任务
    void RunTask(int code)
    {
        tasks[code]();
    }

    int SelectTask()
    {
        // 返回随机任务码
        return rand() % tasks.size();
    }

    // 返回任务码对应的具体任务
    std::string ToDesc(const int &code)
    {
        switch(code)
        {
            case g_download_code:
                return "Download";
            case g_printlog_code:
                return "PrintLog";
            case g_push_videostream_code:
                return "PushVideoStream";
            default:
                return "Unknow";
        }
        return "";
    }
};

Init init;  // 定义对象

2. ProcessPool.cc文件:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"

const int num = 5; // 最大管道数
int number = 0;    // 管道编号

class channel
{
public:
    channel(int fd, pid_t id)
        : ctrlfd(fd), workerid(id)
    {
        name = "channel-" + std::to_string(++number);
    }

public:
    int ctrlfd;         // 管道写端
    pid_t workerid;     // 对应的子进程pid
    std::string name;   // 管道名
};

void Work()
{
    while (true)
    {
        int code = 0;   // 任务码
        ssize_t n = read(0, &code, sizeof(code));
        if (n == sizeof(code))
        {
            if (!init.CheckSafe(code))
                continue;
            init.RunTask(code);
        }
        else if (n == 0)
        {
            // n == 0 写端退出
            break;
        }
        else
        {
            // do nothing
        }
    }
    std::cout << "child quit" << std::endl;
}

// 打印都关闭了哪些从父进程继承下来的写端
void PrintFd(const std::vector<int> &fds)
{
    std::cout << getpid() << " close: ";
    for (auto fd : fds)
    {
        std::cout << fd << " ";
    }
    std::cout << std::endl;
}

// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&

void CreatChannels(std::vector<channel> *c)
{
    std::vector<int> old;
    for (int i = 0; i < num; i++)
    {
        // 1. 定义并创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 2. 创建进程
        pid_t id = fork();
        assert(id != -1);

        // 3. 构建单向通信信道
        if (id == 0)
        {
            // child

            // 关闭从父进程继承下来的写端
            if (!old.empty())
            {
                for (auto fd : old)
                {
                    close(fd);
                }
                // debug
                PrintFd(old);
            }

            close(pipefd[1]); // 关闭写端
            // TODO
            dup2(pipefd[0], 0); // 重定向
            Work();
            exit(0);    // 会自动关闭自己打开的所有fd
        }

        // father
        close(pipefd[0]); // 关闭读端
        c->push_back(channel(pipefd[1], id));
        old.push_back(pipefd[1]);
    }

    // bug version

    // for (int i = 0; i < num; i++)
    // {
    //     // 1. 定义并创建管道
    //     int pipefd[2];
    //     int n = pipe(pipefd);
    //     assert(n == 0);
    //     (void)n;

    //     // 2. 创建进程
    //     pid_t id = fork();
    //     assert(id != -1);

    //     // 3. 构建单向通信信道
    //     if (id == 0)
    //     {
    //         // child
    //         close(pipefd[1]); // 关闭写端
    //         // TODO
    //         dup2(pipefd[0], 0); // 重定向
    //         Work();
    //         exit(0);    // 会自动关闭自己打开的所有fd
    //     }

    //     // father
    //     close(pipefd[0]); // 关闭读端
    //     c->push_back(channel(pipefd[1], id));
    // }
}

void PrintfDebug(const std::vector<channel> &c)
{
    for (const auto &channel : c)
    {
        std::cout << channel.name << ", " << channel.ctrlfd << ", " << channel.workerid << std::endl;
    }
}

const bool g_always_loop = true;    // 是否一直执行

void SendCommand(const std::vector<channel> &channels, bool flag, int num = -1)
{
    int pos = 0;
    while (true)
    {
        // 1. 选择任务
        int command = init.SelectTask();
        // 2. 选择信道(进程)
        const auto &c = channels[pos++];
        pos %= channels.size();

        // debug
        std::cout << "send command " << init.ToDesc(command) << "[" << command << "]" 
                  << " in" << c.name << "worker is: " << c.workerid << std::endl;

        // 3. 发送任务
        write(c.ctrlfd, &command, sizeof(command));
        // 4. 判断是否要退出
        if (!flag)
        {
            num--;
            if (num <= 0) break;
        }

        sleep(1);
    }

    std::cout << "SendCommand done..." << std::endl;
}

void ReleaseChannels(const std::vector<channel> &channels)
{
    // version2
    // int num = channels.size() - 1;
    // for (; num >= 0; num--)
    // {
    //     close(channels[num].ctrlfd);
    //     pid_t rid = waitpid(channels[num].workerid, nullptr, 0);
    //     if (rid == channels[num].workerid)
    //     {
    //         std::cout << "wait child: " << channels[num].workerid << " success" << std::endl;
    //     }
    // }

    // version1
    // for (const auto &c : channels)
    // {
    //     close(c.ctrlfd);
    // }

    // for (const auto &c : channels)
    // {
    //     pid_t rid = waitpid(c.workerid, nullptr, 0);
    //     if (rid == c.workerid)
    //     {
    //         std::cout << "wait child: " << c.workerid << " success" << std::endl;
    //     }
    // }

    // bug version
    for (const auto &c : channels)
    {
        close(c.ctrlfd);
        pid_t rid = waitpid(c.workerid, nullptr, 0);
        if (rid == c.workerid)
        {
            std::cout << "wait child: " << c.workerid << " success" << std::endl;
        }
    }
}

int main()
{
    std::vector<channel> channels;
    // 创建信道,创建进程
    CreatChannels(&channels);

    // 开始发送任务
    SendCommand(channels, !g_always_loop, 10);
    
    // PrintfDebug(channels);
    // sleep(10);

    // 回收资源,想让子进程退出,并且释放管道资源,只要关闭写端即可(写端关闭后,子进程自动退出)
    ReleaseChannels(channels);

    return 0;
}

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

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

相关文章

数据结构面试-核心概念-问题理解

目录 1.数据结构及其分类 2.时间复杂度与空间复杂度及大O表示法 3.循环队列及其判断队空和队满的方法 4.栈与队列在计算机系统中的应用 5.串的模式匹配算法 6.线索二叉树、二叉搜索树、平衡二叉树 7.哈夫曼树与哈夫曼编码 8.DFS与BFS 9.最小生成树及其构建算法 10.最短…

谭晓生解读:AI如何重塑网络安全的未来?

导语 | 当前&#xff0c;对网络安全而言&#xff0c;每一次新的信息技术浪潮都蕴含着巨大机会&#xff0c;同时也意味着巨大的挑战。大模型的发展&#xff0c;是带来了更大的AI安全风险&#xff0c;还是将赋能提升网络安全呢&#xff1f;网络安全正在迎来一场重大范式转移&…

【网络编程】TCP通信基础模型实现

tcpSer.c #include <myhead.h> #define SER_IP "192.168.119.143" // 设置IP地址 #define SER_PORT 6666 // 设置端口号 int main(int argc, const char *argv[]) {// 1.创建socketint serfd socket(AF_INET, SOCK_STREAM, 0);// 参数1表示ipv4// 参数2表…

基于redis的zset实现排行榜

文章目录 &#x1f31e; Sun Frame&#xff1a;SpringBoot 的轻量级开发框架&#xff08;个人开源项目推荐&#xff09;&#x1f31f; 亮点功能&#x1f4e6; spring cloud模块概览常用工具 &#x1f517; 更多信息1.sun-club-subject集成redis1.sun-club-domain引入依赖2.sun-…

Linux 内核源码分析---内核 Netlink 套接字

linux 内核一直存在的一个严重问题就是内核态和用户态的交互的问题&#xff0c;对于这个问题内核大佬们一直在研究各种方法&#xff0c;想让内核和用户态交互能够安全高效的进行。如系统调用&#xff0c;proc&#xff0c;sysfs 等内存文件系统&#xff0c;但是这些方式一般都比…

从今年的计算机视觉比赛看风向

记第一次参加CV比赛的经历-长三角&#xff08;芜湖&#xff09;人工智能视觉算法大赛-CSDN博客 去年参赛的记录里说了&#xff1a; 最近&#xff0c;同样的由芜湖举办的比赛又上线了&#xff0c;果然&#xff1a; 2023年是这些赛题&#xff0c;典型的CV&#xff1a; 今年变成…

如何高效记录并整理编程学习笔记?一个好的笔记软件往往可以达到事半功倍的学习效果 φ(* ̄0 ̄)

在编程学习的旅程中&#xff0c;良好的笔记习惯不仅是知识积累的基石&#xff0c;更是提升学习效率、巩固学习成果的关键。选择合适的笔记工具&#xff0c;并掌握其高效使用方法&#xff0c;对于每一位编程学习者而言都至关重要。本文将从笔记工具的选择角度出发&#xff0c;探…

Linux 中断机制(一)之中断和异常

目录 一、什么是中断1、概述2、中断的分类 二、中断和异常1、中断和异常2、中断的上下部3、异常4、APIC5、中断描述符表 三、软件实现 一、什么是中断 1、概述 中断&#xff08;interrupt&#xff09;是指在 CPU 正常运行期间&#xff0c; 由外部或内部事件引起的一种机制。 …

Miracast ——随时随地在Wi-Fi®设备上分享高清内容

Miracast 是一种无线显示技术&#xff0c;由 Wi-Fi 联盟开发&#xff0c;允许设备之间通过无线方式分享多媒体内容。 Wi-Fi CERTIFIED Miracast™支持在Miracast设备之间无缝显示多媒体内容。Miracast使用户能够通过无线连接在Wi-Fi设备之间分享多媒体内容&#xff0c;包括高分…

六西格玛绿带培训对企业有什么帮助?

六西格玛&#xff0c;这一源自摩托罗拉、风靡全球的管理哲学和方法论&#xff0c;以其严谨的数据分析、持续改进的流程优化理念&#xff0c;帮助无数企业实现了从“好”到“卓越”的跨越。而六西格玛绿带&#xff0c;作为这一体系中的中坚力量&#xff0c;是连接高层管理者与一…

Linux--C语言之分支结构

文章目录 一、分支结构&#xff08;一&#xff09;概念&#xff08;二&#xff09;条件构建1.关系表达式&#xff1a;2.逻辑表达式&#xff1a;3.常量/变量&#xff1a;值是否非0&#xff0c;取值&#xff08;0|1&#xff09; &#xff08;三&#xff09;选择结构的形式1.单分支…

QT容器组

目录 容器组 Group BoX&#xff08;组&#xff09; Scroll Area&#xff08;组滑动&#xff09; Tool Box&#xff08;分页显示&#xff09; Tab Widget&#xff08;也是分页显示&#xff09; Stacked widget&#xff08;也是分页&#xff09; Frame&#xff08;就一个框…

无字母数字webshell之命令执行

文章目录 无字母数字的webshell构造技巧php7下简单解决问题php5下解决问题glob开始操作 无字母数字的webshell构造技巧 <?php if(isset($_GET[code])){$code $_GET[code];if(strlen($code)>35){die("Long.");}if(preg_match("/[A-Za-z0-9_$]/",$c…

应对FingerprintJS反爬:Selenium的破解策略与技术详解

目录 引言 FingerprintJS技术概述 技术原理 应用场景 应对策略 高级解决方案 代码实现与案例分析 去除webdriver特征 使用Undetected_chromedriver 案例分析&#xff1a;爬取目标网站数据 结论 引言 在现代互联网环境中&#xff0c;网站反爬技术日益成熟&#xff0…

分布式知识总结(基本概念)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 基本概念 吞吐量 指系统在单位时间能够处理多少个请求 QPS 每秒…

【mars3d】加载pbf矢量瓦片的最佳方案介绍

矢量瓦片的目前最佳方案&#xff1a; 目前示例中提供了不同的矢量瓦片的加载方案 但是加载矢量瓦片pbf的最佳方案&#xff1a; 使用 TileServer GL 开源地图服务工具&#xff1a;https://github.com/maptiler/tileserver-gl &#xff0c; 它利用 MapLibre GL Native 进行服务…

day34——TCP和UDP的基础通信

一、网络通信之套接字 1.1 套接字通信原理 1.2 socket函数介绍 #include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);功能&#xff1a;为通信创建一个端点&#xff0c;并返回该端点的文件描述…

Llama 3.1中文微调数据集已上线,超大模型一键部署

7 月的 AI 圈真是卷完小模型卷大模型&#xff0c;精彩不停&#xff01;大多数同学都能体验 GPT-4o、Mistral-Nemo 这样的小模型&#xff0c;但 Llama-3.1-405B 和 Mistral-Large-2 这样的超大模型让很多小伙伴犯了难。 别担心&#xff01;hyper.ai 官网在教程板块为大家提供了…

从AGV到立库,物流自动化的更迭与未来

AGV叉车 随着柔性制造系统的广泛应用&#xff0c;小批量、多批次的生产需求不断增强&#xff0c;“订单导向”生产已经成为趋势。这也让越来越多的企业认识到&#xff0c;产线的智能设备导入只是第一步&#xff0c;要想达到生产效率的最优解&#xff0c;物流系统的再优化必须提…

【redis的大key问题】

在使用 Redis 的过程中&#xff0c;如果未能及时发现并处理 Big keys&#xff08;下文称为“大Key”&#xff09;&#xff0c;可能会导致服务性能下降、用户体验变差&#xff0c;甚至引发大面积故障。 本文将介绍大Key产生的原因、其可能引发的问题及如何快速找出大Key并将其优…