【sylar-webserver】重构日志系统

news2025/4/22 9:23:14

文章目录

  • 主要工作
  • 流程图
  • FiberCondition
  • Buffer
  • BufferManager
  • LogEvent 序列化 & 反序列化
  • Logger
  • RotatingFileLogAppender

主要工作

  1. 实现, LogEvent 序列化和反序列化 (使用序列化是为了更标准,如果转成最终的日志格式再存储(确实会快点,但是对于缓冲区的利用就不够了)
  2. 优化,使用 LoggerBuild 建造者 实现和管理日志器;
  3. 双缓冲区设计,使用条件变量等同步技术实现日志异步处理器。支持定时检查缓冲区 和 生产者缓冲区 唤醒;
  4. 使用 WorkerManager 管理多个调度器;
  5. 增加 循环日志写入,按时间分片的 LoggerAppender
  6. 支持上传下载和展示功能,支持备份重要日志; (待实现网络库后实现)
  7. 性能测试,2h4g 服务器下,异步日志器每秒输出 130MB 日志器。

流程图

在这里插入图片描述

FiberCondition

仿照 std::condition_variable

存在的问题,由于是 notify_one 是把 fiber 重新添加调度,并且调度策略是先来先服务。
所以,存在日志时序性错误问题。(解决方法,后期修改调度策略,增加优先级。)⭐

class FiberCondition{
public:
    using MutexType = Spinlock;

    void wait(MutexType::Lock& lock);

    template <typename Predicate>
    void wait(MutexType::Lock& lock, Predicate pred){
        while(!pred()){
            wait(lock);
        }
    }

    void notify_one();
    void notify_all();

private:
    void printWaiters() const;

private:
    MutexType m_mutex;
    std::list<std::pair<Scheduler*, Fiber::ptr>> m_waiters;
};
void FiberCondition::wait(MutexType::Lock& lock){
    SYLAR_ASSERT(Scheduler::GetThis());
    {
        MutexType::Lock lock(m_mutex);
        m_waiters.push_back(std::make_pair(Scheduler::GetThis(), Fiber::GetThis()));
        printWaiters();
    }
    lock.unlock();
    Fiber::GetThis()->yield();
    lock.lock();
}

void FiberCondition::notify_one(){
    MutexType::Lock lock(m_mutex);
    if (!m_waiters.empty()) {
        auto next = m_waiters.front();
        m_waiters.pop_front();
        next.first->schedule(next.second);
    }
}

void FiberCondition::notify_all() {
    MutexType::Lock lock(m_mutex);
    for (auto& waiter : m_waiters) {
        waiter.first->schedule(waiter.second);
    }
    m_waiters.clear();
}

Buffer

class Buffer {
    public:
        using ptr = std::shared_ptr<Buffer>;
        using MutexType = Spinlock;

        Buffer(size_t buffer_size);
        Buffer(size_t buffer_size, size_t threshold, size_t linear_growth);
        
        void push(const char* data, size_t len);
        void push(const std::string& str);
        char* readBegin(int len);
        bool isEmpty();
        void swap(Buffer& buf);
        size_t writeableSize();
        size_t readableSize() const;
        const char* Begin() const;
        void moveWritePos(int len);
        void moveReadPos(int len);
        void Reset();

    protected:
        void ToBeEnough(size_t len);

    private:
        MutexType m_mutex;
        size_t m_buffer_size;
        size_t m_threshold;
        size_t m_linear_growth;
        std::vector<char> m_buffer;
        size_t m_write_pos = 0;
        size_t m_read_pos = 0;
};

BufferManager

重点操作:

BufferManager::BufferManager(const functor& cb, 			// 消费者缓冲区写入的回调函数 ⭐
                            AsyncType::Type asyncType,
                            size_t buffer_size,
                            size_t threshold,
                            size_t linear_growth,
                            size_t swap_time,
                            IOManager* iom)
    :   m_stop(false),
        m_swap_status(false),
        m_asyncType(asyncType),
        m_buffer_productor(std::make_shared<Buffer>(buffer_size, threshold, linear_growth)),
        m_buffer_consumer(std::make_shared<Buffer>(buffer_size, threshold, linear_growth)),
        m_callback(cb),
        m_swap_time(swap_time)
{
    assert(iom != nullptr);

    iom->schedule(std::bind(&BufferManager::ThreadEntry, this));
    m_timer = iom->addTimer(m_swap_time, std::bind(&BufferManager::TimerThreadEntry, this), true);
}

// 写入线程,生产者
void BufferManager::push(const char* data, size_t len) {
    MutexType::Lock lock(m_mutex);

    if(m_asyncType == AsyncType::ASYNC_SAFE){
        if (len > m_buffer_productor->writeableSize()) {
            SYLAR_LOG_DEBUG(g_logger) << "notify consumer";
            m_cond_consumer.notify_one();
        }
        m_cond_producer.wait(lock, [&](){
            return (m_stop || (len <= m_buffer_productor->writeableSize()));
        });
    }
        
    if(m_stop){
        throw std::runtime_error("BufferManager is stopped");
    }
    m_buffer_productor->push(data, len);
}

// 使用Timer,按照频率访问缓冲区
// 如果生产者没有就退出
void BufferManager::TimerThreadEntry(){
    {
        MutexType::Lock lock(m_mutex);

        if ((!m_buffer_productor->isEmpty() && m_buffer_consumer->isEmpty()) || m_stop) {
            swap_buffers();
        
            if(m_asyncType == AsyncType::ASYNC_SAFE){
                m_cond_producer.notify_all();
            }
        }else{
            return;
        }
    }
    {
        MutexType::Lock lock(m_swap_mutex);
        m_callback(m_buffer_consumer);
        m_buffer_consumer->Reset();
    }
}

void BufferManager::ThreadEntry() {
    while(true){
        {
            MutexType::Lock lock(m_mutex);
            SYLAR_LOG_DEBUG(g_logger) << "ThreadEntry started.";
            m_cond_consumer.wait(lock, [&](){
                return m_stop || (!m_buffer_productor->isEmpty() && m_buffer_consumer->isEmpty());
            });

            swap_buffers();

            if(m_asyncType == AsyncType::ASYNC_SAFE){
                m_cond_consumer.notify_all();
            }
        }
        {
            MutexType::Lock lock(m_swap_mutex);
            m_callback(m_buffer_consumer);
            m_buffer_consumer->Reset();
            if(m_stop && m_buffer_productor->isEmpty()) return;
        }
    }
}

LogEvent 序列化 & 反序列化

#pragma pack(push, 1)
struct LogMeta {
    uint64_t timestamp;    // 时间戳
    uint32_t threadId;     // 线程ID
    uint32_t fiberId;      // 协程ID
    int32_t line;          // 行号
    uint32_t elapse;
    LogLevel::Level level; // 日志级别
    uint16_t fileLen;      // 文件名长度
    uint32_t threadNameLen;// 线程名长度
    uint32_t msgLen;       // 消息内容长度
};
#pragma pack(pop)
		Buffer::ptr LogEvent::serialize() const {
            LogMeta meta{ // ⭐
                .timestamp = m_time,
                .threadId = m_threadId,
                .fiberId = m_fiberId,
                .line = m_line,
                .elapse = m_elapse,
                .level = m_level,
                .fileLen = static_cast<uint16_t>(m_file.size()),
                .threadNameLen = static_cast<uint16_t>(m_threadName.size()),
                .msgLen = static_cast<uint32_t>(m_ss.str().size())
            };

            const size_t total_need = sizeof(LogMeta) + meta.fileLen + meta.threadNameLen + meta.msgLen;
            auto buffer = std::make_shared<Buffer>(total_need); // 使用 shared_ptr 管理内存

            // 序列化元数据
            buffer->push(reinterpret_cast<const char*>(&meta), sizeof(meta));

            // 序列化变长数据(包含终止符)
            buffer->push(m_file.c_str(), meta.fileLen);
            buffer->push(m_threadName.c_str(), meta.threadNameLen);
            buffer->push(m_ss.str().c_str(), meta.msgLen);

            return buffer; // 返回 shared_ptr
        }

		// 每次调用,解析一个LogEvent
        static LogEvent::ptr LogEvent::deserialize(Buffer& buffer) {
            if(buffer.readableSize() < sizeof(LogMeta)) {
                return nullptr;
            }
            LogMeta meta;
            memcpy(&meta, buffer.Begin(), sizeof(LogMeta));

            const size_t total_need = sizeof(LogMeta) + meta.fileLen + meta.threadNameLen + meta.msgLen;
            if(buffer.readableSize() < total_need){
                return nullptr;
            }

            // 4. 提取各字段数据(使用临时指针操作)
            const char* data_ptr = buffer.Begin() + sizeof(LogMeta);
            
            // 文件名处理
            std::string file(data_ptr, meta.fileLen);
            data_ptr += meta.fileLen;

            // 线程名
            std::string thread_name(data_ptr, meta.threadNameLen);
            data_ptr += meta.threadNameLen;

            // 消息内容处理
            std::string message(data_ptr, meta.msgLen);

            // 5. 统一移动读指针(原子操作保证数据一致性)
            buffer.moveReadPos(total_need);

            // 6. 构建日志事件对象
            auto event = std::make_shared<LogEvent>(
                std::move(file),
                meta.line,
                meta.elapse,
                meta.threadId,
                std::move(thread_name),
                meta.fiberId,
                meta.timestamp,
                meta.level
            );

            event->getSS() << message;

            return event;
        }

Logger

重构时,出现的问题:Logger 对 BufferManager 的依赖,并且 BufferManger 也依赖 IOMgr调度器。
简单说,就说 全局静态变量的初始化顺序问题
解决方法:Logger默认构造的时候,不提供BufferParams,就使用同步方式创建。
导入 yaml 配置后,重置 logger,再创建 异步日志器

		Logger(
            const std::string name, 
            LogLevel::Level level,
            std::vector<LogAppender::ptr>& appenders, 
            const BufferParams& bufParams
        )   :m_name(name), 
            m_level(level), 
            m_appenders(appenders.begin(), appenders.end())
        {
            if(bufParams.isValid()){
                m_bufMgr = std::make_shared<BufferManager>(
                    std::bind(&Logger::realLog, this, std::placeholders::_1), bufParams);
            }else{
                m_bufMgr = nullptr;
            }
        }
        
		// 由 iom_log 写入真正的文件。
        void realLog(Buffer::ptr buffer) {
            MutexType::Lock lock(m_log_mutex);      // 强制 只能 一个线程写入。

            if (!buffer) {
                std::cerr << "realLog: invalid buffer pointer" << std::endl;
                return;
            }
            
            std::vector<LogEvent::ptr> events;
            while (true) {  // 解析 buffer
                LogEvent::ptr event = LogEvent::deserialize(*buffer);
                // 理论上 buffer 里是多个Event的数据,不存在处理失败。
                
                if (event) {
                    events.push_back(event);
                } else {
                    if (buffer->readableSize() == 0) { // 读完了
                        break;
                    } else {
                        // 处理失败但数据未读完(说明发生严重错误)
                        std::cout << "Log deserialization error, remaining data: " << buffer->readableSize() << std::endl;
                        break;
                    }
                }
            }
            auto self = shared_from_this();
            for (auto& appender : m_appenders) {
                appender->log(self, events);
            }
        }

		// 写入缓冲区
        // 多个线程的 写日志,写入缓存区
        void log(LogEvent::ptr event){
            if(event->getLevel() >= m_level){
                if(m_bufMgr != nullptr){
                    // MutexType::Lock lock(m_mutex);   当协程阻塞,这个锁就一直没释放。搞半天,给我整的怀疑人生了。
                    Buffer::ptr buf = event->serialize();
                    m_bufMgr->push(buf);
                }else{
                    // 如果没有配置iom,直接同步输出日志
                    auto self = shared_from_this();
                    for(auto& appender : m_appenders) {
                        appender->log(self, event);
                    }
                }
            }
        }

RotatingFileLogAppender

FileLogAppeneder 改为使用 FILE 库函数

支持功能:

  1. max_size,限制单个日志文件大小(按照时间片创建文件名)
  2. m_maxFile = 0,无限增加日志
  3. m_maxFile > 0,限制日志文件的个数。当超过,从第一个文件循环写入。
class RotatingFileLogAppender : public LogAppender{
public:
    typedef std::shared_ptr<RotatingFileLogAppender> ptr;
    RotatingFileLogAppender(const std::string& filename, 
        LogLevel::Level level, 
        LogFormatter::ptr formatter,
        size_t max_size,
        size_t max_file = 0,  // 默认是无限增加
        FlushRule::Rule flush_rule = FlushRule::Rule::FFLUSH  // 默认是普通日志 
    );
    ~RotatingFileLogAppender(){
        if(m_curFile){
            fclose(m_curFile);
            m_curFile = NULL;
        }
    }
    std::string toYamlString();
    void log(std::shared_ptr<Logger> logger, LogEvent::ptr event) override;
    void log(std::shared_ptr<Logger> logger, std::vector<LogEvent::ptr> events) override;     

private:
    void initLogFile(size_t len = 0);
    /**
     * 判断是否写的下,如果写的下就 ss<<str,缓存
     * 如果写不写了,就把 ss 缓存一次性写入。重置ss 
     */
    bool checkLogFile(const std::string& str);
    std::string createFilename();
private:
    std::string m_filename;
    FILE* m_curFile = NULL;
    std::vector<std::string> m_fileNames;
    size_t m_maxSize;
    size_t m_maxFile;
    FlushRule::Rule m_flushRule;
    size_t m_curFilePos = 0;
    size_t m_curFileIndex = 0;
    Buffer m_buffer; 
};
void RotatingFileLogAppender::initLogFile(size_t len){
    if(m_curFile == NULL || (m_curFilePos + len) > m_maxSize){
        // 写不下了,保证日志的完整性,直接新建文件。
        if(m_curFile != NULL){
            fflush(m_curFile);
            fclose(m_curFile);
            
            if(m_maxFile == 0){
                // 无限增加日志文件
                m_curFileIndex++;
            }else{
                m_curFileIndex = (m_curFileIndex + 1) % m_maxFile;

                if(!m_fileNames[m_curFileIndex].empty()){    // 说明 循环到 已有的文件了。
                    std::string newfilename = createFilename();
                    if(rename(m_fileNames[m_curFileIndex].c_str(), newfilename.c_str()) != 0){  // 文件 改新名字
                        perror("rename failed");
                    }   
                    m_fileNames[m_curFileIndex] = newfilename;
                    m_curFile = fopen(newfilename.c_str(), "r+b");
                    fseek(m_curFile, 0, SEEK_SET);   // 从头 覆盖,不考虑 日志文件名了。默认最后一个文件可能会存在过往的日志信息。
                    m_curFilePos = 0;
                    return;
                }
            }
        }
        std::string filename = createFilename();
        m_fileNames[m_curFileIndex] = filename;
        m_curFile = fopen(filename.c_str(), "ab");
        if(m_curFile==NULL){
            std::cout <<__FILE__<<__LINE__<<"open file failed"<< std::endl;
            perror(NULL);
        }
        m_curFilePos = 0;
        return;
    }
}

std::string RotatingFileLogAppender::createFilename() {
    time_t now = time(nullptr);
    struct tm tm;
    localtime_r(&now, &tm);
    char time_buf[64];
    strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", &tm);
    return m_filename + "_" + time_buf + "_" + std::to_string(m_curFileIndex) + ".log";
}

void RotatingFileLogAppender::log(std::shared_ptr<Logger> logger, LogEvent::ptr event){
    MutexType::Lock lock(m_mutex);
    if(event->getLevel() >= m_level){
        std::string data = m_formatter->format(logger , event);
        initLogFile(data.size());
        fwrite(data.c_str(), 1, data.size() , m_curFile);

        if(ferror(m_curFile)){
            std::cout <<__FILE__<<__LINE__<<"write log file failed"<< std::endl;
            perror(NULL);
        }
        m_curFilePos += data.size();
        if(m_flushRule == FlushRule::Rule::FFLUSH){
            if(fflush(m_curFile)==EOF){  // 刚好最后一个日志把文件写满了。
                std::cout <<__FILE__<<__LINE__<<"fflush file failed"<< std::endl;
                perror(NULL);
            }
        }else if(m_flushRule == FlushRule::Rule::FSYNC){
            fflush(m_curFile);
            fsync(fileno(m_curFile));
        }
    }
}



bool RotatingFileLogAppender::checkLogFile(const std::string& data){
    if(m_curFile == NULL || (m_curFilePos + data.size()) > m_maxSize){
        // 写不下了,保证日志的完整性,直接新建文件。
        if(m_curFile != NULL){
            // 把 ss 缓存一次性写入
            fwrite(m_buffer.Begin(), 1, m_buffer.readableSize(), m_curFile);
            m_buffer.Reset();

            // 判断错误信息
            if(ferror(m_curFile)){
                std::cout <<__FILE__<<__LINE__<<"write log file failed"<< std::endl;
                perror(NULL);
            }
            if(m_flushRule == FlushRule::Rule::FFLUSH){
                if(fflush(m_curFile)==EOF){  // 刚好最后一个日志把文件写满了。
                    std::cout <<__FILE__<<__LINE__<<"fflush file failed"<< std::endl;
                    perror(NULL);
                }
            }else if(m_flushRule == FlushRule::Rule::FSYNC){
                fflush(m_curFile);
                fsync(fileno(m_curFile));
            }
            
            fclose(m_curFile);
            
            if(m_maxFile == 0){
                // 无限增加日志文件
                m_curFileIndex++;
            }else{
                m_curFileIndex = (m_curFileIndex + 1) % m_maxFile;

                if(!m_fileNames[m_curFileIndex].empty()){    // 说明 循环到 已有的文件了。
                    std::string newfilename = createFilename();
                    if(rename(m_fileNames[m_curFileIndex].c_str(), newfilename.c_str()) != 0){  // 文件 改新名字
                        std::cout << "rename failed" << std::endl;
                        perror(NULL);
                    }
                    m_fileNames[m_curFileIndex] = newfilename;
                    m_curFile = fopen(newfilename.c_str(), "r+b");
                    if (m_curFile == NULL) {
                        std::cout << __FILE__ << __LINE__ << "open file failed" << std::endl;
                        perror(NULL);
                    }

                    // 从头 覆盖,不考虑 日志文件名了。默认最后一个文件可能会存在过往的日志信息。
                    fseek(m_curFile, 0, SEEK_SET); 
                    m_curFilePos = 0;

                    // 模拟写入文件,实际上写入缓存。
                    m_buffer.push(data);
                    m_curFilePos += data.size();

                    return true;
                }
            }
        }
        
        // m_curFile为空,创建新文件 
        std::string filename = createFilename();
        if(m_maxFile > 0){
            m_fileNames[m_curFileIndex] = filename;    // 只有限制最大文件数,记录文件名
        }
        m_curFile = fopen(filename.c_str(), "ab");
        if(m_curFile==NULL){
            std::cout <<__FILE__<<__LINE__<<"open file failed"<< std::endl;
            perror(NULL);
        }
        m_curFilePos = 0;
    }

    // 模拟写入文件,实际上写入缓存。
    m_buffer.push(data);
    m_curFilePos += data.size();
    return false;
}

void RotatingFileLogAppender::log(std::shared_ptr<Logger> logger, std::vector<LogEvent::ptr> events){
    // 这个时候,m_curFilePos 转变成对 m_buffer 写入数据的 pos 长度。 ⭐
    MutexType::Lock lock(m_mutex);
    for(auto& event : events){
        if(event->getLevel() >= m_level){
            std::string data = m_formatter->format(logger , event);
            checkLogFile(data);
        }
    }

    // 最后再次,把缓存里的写入
    if(m_buffer.readableSize() > 0 && m_curFile != NULL){
        fwrite(m_buffer.Begin(), 1, m_buffer.readableSize(), m_curFile);
        m_buffer.Reset();

        // 判断错误信息
        if(ferror(m_curFile)){
            std::cout <<__FILE__<<__LINE__<<"write log file failed"<< std::endl;
            perror(NULL);
        }
        if(m_flushRule == FlushRule::Rule::FFLUSH){
            if(fflush(m_curFile)==EOF){  // 刚好最后一个日志把文件写满了。
                std::cout <<__FILE__<<__LINE__<<"fflush file failed"<< std::endl;
                perror(NULL);
            }
        }else if(m_flushRule == FlushRule::Rule::FSYNC){
            fflush(m_curFile);
            fsync(fileno(m_curFile));
        }
    }
}     

剩下的就是:
works,多个调度器的的管理~
对Config监听函数的修改,补充BufferParams
分离works.yml和log.yml,分别导入。提前导入works.yml 保证 调度器创建完成。
详见 代码 https://github.com/star-cs/webserver

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

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

相关文章

树莓派超全系列教程文档--(38)config.txt视频配置

config.txt视频配置 视频选项HDMI模式树莓派4-系列的HDMI树莓派5-系列的HDMI 复合视频模式enable_tvout LCD显示器和触摸屏ignore_lcddisable_touchscreen 通用显示选项disable_fw_kms_setup 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 视频选…

线性DP:最短编辑距离

Dp 状态表示 f&#xff08;i&#xff0c;j&#xff09; 集合所有将A[1~i]变成B[1~j]的操作方式属性min 状态计算 &#xff08;划分&#xff09; 增f(i,j)f(i,j-1)1//A[i]元素要增加&#xff0c;说明A前i位置与B前j-1相同删f(i,j)f(i-1,j)1//A[i]元素要删除&#xff0c;说明A前i…

STM32——新建工程并使用寄存器以及库函数进行点灯

本文是根据江协科技提供的教学视频所写&#xff0c;旨在便于日后复习&#xff0c;同时供学习嵌入式的朋友们参考&#xff0c;文中涉及到的所有资料也均来源于江协科技&#xff08;资料下载&#xff09;。 新建工程并使用寄存器以及库函数进行点灯操作 新建工程步骤1.建立工程2.…

java集合框架day1————集合体系介绍

在进入正文之前&#xff0c;我们先来思考一下之前学过的数组有什么缺点&#xff1f; <1>长度开始时必须指定&#xff0c;而且一旦指定&#xff0c;不能更改 <2>保存的必须为同一类型的元素 <3>使用数组进行增加/删除元素的代码比较麻烦 为了方便读者理解&…

百度热力图数据获取,原理,处理及论文应用18

目录 0、数据简介0、示例数据1、百度热力图数据日期如何选择1.1、其他实验数据的时间1.2、看日历天气 2、百度热力图几天够研究&#xff1f;部分文章统计3、数据原理3.1 Bd09mc即百度墨卡托投影坐标系200单位的距离是可以自己设置的吗&#xff1f;3.2 csv文件字段说明3.3 ** 这…

【身份证扫描件识别表格】如何识别大量身份证扫描件将内容导出保存到Excel表格,一次性处理多张身份证图片导出Excel表格,基于WPF和腾讯云的实现方案

基于WPF和腾讯云的身份证扫描件批量处理方案 适用场景 本方案适用于需要批量处理大量身份证扫描件的场景,例如: 企业人事部门批量录入新员工身份信息银行或金融机构办理批量开户业务教育机构收集学生身份信息政府部门进行人口信息统计酒店、医院等需要实名登记的场所这些场景…

基于语义网络表示的不确定性推理

前文我们已经了解了: 1.不确定与非单调推理的基本概念:不确定与非单调推理的基本概念-CSDN博客 2.不确定与非单调推理的概率方法:不确定与非单调推理的概率方法-CSDN博客 3.不确定与非单调推理的可信度方法:不确定与非单调推理的可信度方法-CSDN博客 4.不确定与非单调推…

ICMAN防水触摸芯片 - 复杂环境下精准交互,提升触控体验

▍核心优势 ◆ 超强抗干扰能力 ◆ 工业级设计&#xff0c;一致性和稳定性好 ▍提供场景化解决方案 【智能厨电矩阵】抽油烟机档位调节 | 电磁炉火力触控 | 洗碗机模式切换 【卫浴设备方案】淋浴房雾化玻璃控制 | 智能马桶触控面板 | 浴缸水位感应 【工业控制应用】仪器仪…

【java实现+4种变体完整例子】排序算法中【希尔排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格

以下是希尔排序的详细解析&#xff0c;包含基础实现、常见变体的完整代码示例&#xff0c;以及各变体的对比表格&#xff1a; 一、希尔排序基础实现 原理 希尔排序是插入排序的改进版本&#xff0c;通过分步缩小增量间隔&#xff0c;将数组分成多个子序列进行插入排序&#…

matlab 处理海洋数据并画图的工具包--ocean_data_tools

matlab 处理海洋数据并画图的工具包–ocean_data_tools matlab 处理海洋数据并画图的工具包–ocean_data_tools ocean_data_tools 简化了提取、格式化和可视化免费可用的海洋学数据的过程。虽然可以在线访问大量海洋学数据&#xff0c;但由于获取这些数据并将其格式化为可用数据…

MCP:AI时代的“万能插座”,开启大模型无限可能

摘要&#xff1a;Model Context Protocol&#xff08;MCP&#xff09;由Anthropic在2024年底开源&#xff0c;旨在统一大模型与外部工具、数据源的通信标准。采用客户端-服务器架构&#xff0c;基于JSON-RPC 2.0协议&#xff0c;支持stdio、SSE、Streamable HTTP等多种通信方式…

静态网页的开发

文章目录 基于 idea 开发静态网页添加web框架前端配置服务器并启动服务资源名字不是 index 静态网页 流转 基于 idea 开发静态网页 添加web框架 方法1 方法2 前端 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8&quo…

【CPU】结合RISC-V CPU架构回答中断系统的7个问题(个人草稿)

结合RISC-V CPU架构对中断系统七个关键问题的详细解析&#xff0c;按照由浅入深的结构进行说明&#xff1a; 一、中断请求机制&#xff08;问题①&#xff09; 硬件基础&#xff1a; RISC-V通过CLINT&#xff08;Core Local Interrupter&#xff09;和PLIC&#xff08;Platfor…

uCOS3实时操作系统(任务切换和任务API函数)

文章目录 任务切换任务API函数 任务切换 C/OS-III 将 PendSV 的中断优先级配置为最低的中断优先级&#xff0c;这么一来&#xff0c; PendSV 异常的中断服务函数就会在其他所有中断处理完成后才被执行。C/OS-III 就是将任务切换的过程放到 PendSV 异常的中断服务函数中处理的。…

科学养生指南:解锁健康生活新方式

在快节奏的现代生活中&#xff0c;健康养生已成为人们关注的焦点。科学合理的养生方式&#xff0c;能帮助我们增强体质、预防疾病&#xff0c;享受更优质的生活。​ 饮食是健康养生的基石。遵循 “均衡饮食” 原则&#xff0c;每日饮食需包含谷类、蔬菜水果、优质蛋白质和健康…

第十四届蓝桥杯 2023 C/C++组 有奖问答

目录 题目&#xff1a; 题目描述&#xff1a; 题目链接&#xff1a; 思路&#xff1a; 核心思路&#xff1a; 思路详解&#xff1a; 代码&#xff1a; 代码详解&#xff1a; 题目&#xff1a; 题目描述&#xff1a; 题目链接&#xff1a; 蓝桥云课 有奖问答 思路&…

transformer注意力机制

单头注意力机制 import torch import torch.nn.functional as Fdef scaled_dot_product_attention(Q, K, V):# Q: (batch_size, seq_len, d_k)# K: (batch_size, seq_len, d_k)# V: (batch_size, seq_len, d_v)batch_size: 一次输入的句子数。 seq_len: 每个句子的词数。 d_mo…

QT 5.15 程序打包

说明&#xff1a; windeployqt 是 Qt 提供的一个工具&#xff0c;用于自动收集并复制运行 Qt 应用程序所需的动态链接库&#xff08;.dll 文件&#xff09;及其他资源&#xff08;如插件、QML 模块等&#xff09;到可执行文件所在的目录。这样你就可以将应用程序和这些依赖项一…

【路由交换方向IE认证】BGP选路原则之AS-Path属性

文章目录 一、路由器BGP路由的处理过程控制平面和转发平面选路工具 二、BGP的选路顺序选路的前提选路顺序 三、AS-Path属性选路原则AS-Path属性特性AS-Path管进还是管出呢&#xff1f;使用AS-Path对进本AS的路由进行选路验证AS-Path不接收带本AS号的路由 四、BGP邻居建立配置 一…

Linux系统下docker 安装 redis

docker安装最新版的redis 一、docker拉取最新版redis镜像 拉取镜像若没有指定版本&#xff0c;代表拉取最新版本 二、查询redis镜像 三、挂载配置文件 在docker容器内修改redis配置文件不方便&#xff0c;所以挂载配置文件&#xff0c;这样可以在外边修改redis配置 3.1 创建…