线程池:线程池的实现 | 日志

在这里插入图片描述

🌈个人主页: 南桥几晴秋
🌈C++专栏: 南桥谈C++
🌈C语言专栏: C语言学习系列
🌈Linux学习专栏: 南桥谈Linux
🌈数据结构学习专栏: 数据结构杂谈
🌈数据库学习专栏: 南桥谈MySQL
🌈Qt学习专栏: 南桥谈Qt
🌈菜鸡代码练习: 练习随想记录
🌈git学习: 南桥谈Git

🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈
本科在读菜鸡一枚,指出问题及时改正

文章目录

  • 原理
  • 线程池实现
  • 日志
    • 获取当前时间函数接口
    • 启用类型设置
    • 输出到屏幕
    • 输出到文件
    • 选择输出方式
    • 创建日志消息
    • 完整代码
  • 携带日志的线程池设计

原理

在一个可执行程序内部存在多个线程和一个任务队列。如果任务队列里长时间没有任务,这些线程就会休眠,如果此时来了一个任务,那么线程就会被唤醒。像这种,提前创建好线程,需要的时候直接使用,我们称之为线程池。这种本质上就是一个生产消费模型。
在这里插入图片描述

线程池实现

//ThreadPool.hpp
#pragma once#include<iostream>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>
#include<functional>
#include"Thread.hpp"using namespace threadModel;static const int gdefaultnum=5;void test()
{while(true){std::cout<<"hello world"<<std::endl;sleep(1);}
}template<typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void WakeupAll(){pthread_cond_broadcast(&_cond);}void Sleep(){pthread_cond_wait(&_cond,&_mutex);}bool IsEmpty(){return _task_queue.empty();}void HandlerTask(const std::string& name)  // this{while (true){LockQueue();//如果队列为空(有任务)while(IsEmpty()&&_isrunning) //线程没有任务,但是在工作,继续休眠{_sleep_thread_num++;Sleep();_sleep_thread_num--;}if(IsEmpty()&&!_isrunning) // 任务是空的,并且线程退出工作{std::cout<<name<<" quit..."<<std::endl;UnlockQueue();break;}// 队列不为空,有任务 或者 队列被唤醒// 取任务T t=_task_queue.front();_task_queue.pop();UnlockQueue();// 此处任务已经不在任务队列中,任务已经被拿走,处理任务和临界资源是两码事t(); // 处理任务,不能不用也不能在临界区中处理std::cout<<name<<": "<<t.result()<<std::endl;}}public:ThreadPool(int thread_num=gdefaultnum):_thread_num(thread_num),_isrunning(false) //刚开始线程没有使用,_sleep_thread_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);}void Init(){func_t func=std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1);for(int i=0;i<_thread_num;i++){std::string threadname="thread-"+std::to_string(i+1);_threads.emplace_back(threadname,func);}}void Start(){_isrunning=true;for(auto& thread:_threads){thread.Start();}}void Stop(){LockQueue();_isrunning=false;WakeupAll();UnlockQueue();}void Equeue(const T &in){LockQueue();if(_isrunning){_task_queue.push(in);// 如果当前有线程在等待,需要唤醒if(_sleep_thread_num>0){Wakeup();}}UnlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _thread_num;std::vector<Thread> _threads;  // 管理多个线程std::queue<T> _task_queue; // 任务队列bool _isrunning; //当前线程是否在工作int _sleep_thread_num;   //计数器:休眠的线程个数pthread_mutex_t _mutex;pthread_cond_t _cond;
};

在这里插入图片描述

日志

日志是软件运行的记录信息,可以向显示器打印,也可以向文件中打印,日志必须有特定的格式:

  • [日志等级] [pid] [filename] [filenumber] [time] 日志内容(支持可变参数)

日志等级:DEBUG、INFO、WARNING、ERROR、FATAL(致命的)

// 日志等级
enum
{DEBUG=1,INFO,WARING,ERROR,FATAL
};
  • 日志消息:日志等级、id、文件名、行号、当前时间等
// 日志消息
class logmessage
{public:std::string _level; // 日志等级pid_t _id; std::string _filename; // 文件名int _filenumber;  // 行号std::string _cur_time;std::string _message_info;};

获取当前时间函数接口

std::string GetCurTime()
{time_t now=time(nullptr);struct tm* cur_time=localtime(&now);char buffer[128];snprintf(buffer,sizeof(buffer),"%d-%02d-%02d %02d:%02d:%02d",cur_time->tm_year+1900,cur_time->tm_mon+1,cur_time->tm_mday,cur_time->tm_hour,cur_time->tm_min,cur_time->tm_sec);return std::string(buffer);
}
  • time(nullptr) 返回当前的时间(从 1970 年 1 月 1 日到现在的秒数),并将其赋值给 now 变量。time_t 是表示时间点的类型。
  • localtime(&now) now 转换为当地时间,并返回一个指向 tm 结构的指针。tm 结构包含了年、月、日、时、分、秒等信息。
    在这里插入图片描述

启用类型设置

void Enable(int type)
{_type=type;
}

Enable 函数用于设置日志输出类型,可以选择输出到屏幕或文件。

输出到屏幕

void FlushLogToSCreen(const logmessage& lg)
{printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());
}

FlushLogToSCreen 函数将日志信息格式化并输出到控制台。使用 printf 格式化字符串。

输出到文件

void FlushLogToFile(const logmessage& lg)
{std::ofstream out(_logfile,std::ios::app); // 追加打印if(!out.is_open())return;char logtxt[2048];snprintf(logtxt,sizeof(logtxt),"[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());out.write(logtxt,strlen(logtxt));out.close();
}

FlushLogToFile 函数将日志信息写入指定的文件。以追加模式打开文件,并在打开失败时返回。
使用snprintf 格式化日志信息,然后将其写入文件。

选择输出方式

void FlushLog(const logmessage& lg)
{switch(_type){case SCREEN_TYPE:FlushLogToSCreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}
}

创建日志消息

void logMessage(std::string filename,int filenumber,int level,const char* format,...)
{logmessage lg;lg._level=LevelToString(level);lg._id=getpid();lg._filename=filename;lg._filenumber=filenumber;lg._cur_time=GetCurTime();va_list ap;va_start(ap,format);char log_info[1024];vsnprintf(log_info,sizeof(log_info),format,ap);va_end(ap);lg._message_info=log_info;FlushLog(lg);
}
  • logMessage 函数用于创建一条新的日志消息。它接受文件名、文件编号、日志级别和格式化字符串作为参数。
  • 使用可变参数处理(va_list)来处理格式化字符串。
  • 将生成的日志信息存储在 lg 对象中,并调用 FlushLog 函数进行输出。
  • va_list ap;声明了一个 va_list 类型的变量 ap,它用于存储可变参数列表。在 C 语言中,va_list 是一个用于遍历不定数量参数的类型。
  • va_start(ap, format);va_start 宏初始化 ap 以指向函数参数列表中的第一个可变参数。这里的 format 是最后一个固定参数,va_start 会从它的下一个参数开始读取可变参数。因此,ap 现在可以用来访问 format 之后的所有参数。
  • va_end(ap)用于清理va_list 变量 ap。在读取完可变参数后,调用 va_end 是良好的实践,它可以释放与 ap 相关的资源(如果有的话)。

完整代码

#pragma once#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<unistd.h>
#include<stdarg.h>
#include<ctime>
#include<fstream>
#include<pthread.h>
#include<cstdio>
#include"LockGuard.hpp"using std::cin;
using std::cout;
using std::endl;namespace log_ns
{// 日志等级enum{DEBUG=1,INFO,WARING,ERROR,FATAL};// 日志消息class logmessage{public:std::string _level; // 日志等级pid_t _id; std::string _filename; // 文件名int _filenumber;  // 行号std::string _cur_time;std::string _message_info;};pthread_mutex_t glock=PTHREAD_MUTEX_INITIALIZER; // 定义一个全局的锁std::string LevelToString(int level){LockGuard lockguard(&glock);switch(level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARING:return "WARING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetCurTime(){time_t now=time(nullptr);struct tm* cur_time=localtime(&now);char buffer[128];snprintf(buffer,sizeof(buffer),"%d-%02d-%02d %02d:%02d:%02d",cur_time->tm_year+1900,cur_time->tm_mon+1,cur_time->tm_mday,cur_time->tm_hour,cur_time->tm_min,cur_time->tm_sec);return std::string(buffer);}#define SCREEN_TYPE 1#define FILE_TYPE 2const std::string glogfile="./log.txt";// 日志class Log{public:Log(const std::string& logfeile=glogfile):_logfile(logfeile),_type(SCREEN_TYPE){}void Enable(int type){_type=type;}void FlushLogToSCreen(const logmessage& lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage& lg){std::ofstream out(_logfile,std::ios::app); // 追加打印if(!out.is_open())return;char logtxt[2048];snprintf(logtxt,sizeof(logtxt),"[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());out.write(logtxt,strlen(logtxt));out.close();}void FlushLog(const logmessage& lg){switch(_type){case SCREEN_TYPE:FlushLogToSCreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}void logMessage(std::string filename,int filenumber,int level,const char* format,...){logmessage lg;lg._level=LevelToString(level);lg._id=getpid();lg._filename=filename;lg._filenumber=filenumber;lg._cur_time=GetCurTime();va_list ap;va_start(ap,format);char log_info[1024];vsnprintf(log_info,sizeof(log_info),format,ap);va_end(ap);lg._message_info=log_info;//cout<<lg._message_info<<endl;// 打印日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(Level,Format,...) do{lg.logMessage(__FILE__,__LINE__,Level,Format,##__VA_ARGS__);}while(0)#define EnableScreen() do{lg.Enable(SCREEN_TYPE);}while(0)#define EnableFile() do{lg.Enable(FILE_TYPE);}while(0)};

在这里插入图片描述

携带日志的线程池设计

//ThreadPool.hpp
#pragma once#include<iostream>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>
#include<functional>
#include"Thread.hpp"
#include"Log.hpp"using namespace threadModel;
using namespace log_ns;static const int gdefaultnum=5;void test()
{while(true){std::cout<<"hello world"<<std::endl;sleep(1);}
}template<typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void WakeupAll(){pthread_cond_broadcast(&_cond);}void Sleep(){pthread_cond_wait(&_cond,&_mutex);}bool IsEmpty(){return _task_queue.empty();}void HandlerTask(const std::string& name)  // this{while (true){LockQueue();//如果队列为空(有任务)while(IsEmpty()&&_isrunning) //线程没有任务,但是在工作,继续休眠{_sleep_thread_num++;LOG(INFO,"%s thread sleep begin!\n",name.c_str());Sleep();LOG(INFO,"%s thread wakeup!\n",name.c_str());_sleep_thread_num--;}if(IsEmpty()&&!_isrunning) // 任务是空的,并且线程退出工作{UnlockQueue();LOG(INFO,"%s quit\n",name.c_str());break;}// 队列不为空,有任务 或者 队列被唤醒// 取任务T t=_task_queue.front();_task_queue.pop();UnlockQueue();// 此处任务已经不在任务队列中,任务已经被拿走,处理任务和临界资源是两码事t(); // 处理任务,不能不用也不能在临界区中处理LOG(DEBUG,"hander task done, task is: \n%s",t.result().c_str());}}public:ThreadPool(int thread_num=gdefaultnum):_thread_num(thread_num),_isrunning(false) //刚开始线程没有使用,_sleep_thread_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);}void Init(){func_t func=std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1);for(int i=0;i<_thread_num;i++){std::string threadname="thread-"+std::to_string(i+1);_threads.emplace_back(threadname,func);LOG(DEBUG,"construct thread %s done, init success.\n",threadname.c_str());}}void Start(){_isrunning=true;for(auto& thread:_threads){LOG(DEBUG,"Start thread %s done.\n",thread.Name().c_str());thread.Start();}}void Stop(){LockQueue();_isrunning=false;WakeupAll();UnlockQueue();LOG(INFO,"Thread Pool Stop Success!\n");}void Equeue(const T &in){LockQueue();if(_isrunning){_task_queue.push(in);// 如果当前有线程在等待,需要唤醒if(_sleep_thread_num>0){Wakeup();}}UnlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _thread_num;std::vector<Thread> _threads;  // 管理多个线程std::queue<T> _task_queue; // 任务队列bool _isrunning; //当前线程是否在工作int _sleep_thread_num;   //计数器:休眠的线程个数pthread_mutex_t _mutex;pthread_cond_t _cond;
};
//Main.cc
#include"ThreadPool.hpp"
#include"Task.hpp"
#include"Log.hpp"
#include<memory>using std::cin;
using std::cout;
using std::endl;
using namespace log_ns;int main()
{EnableScreen();//std::unique_ptr<ThreadPool> tp=std::make_unique<>();  //构建一个ThreadPool对象ThreadPool<Task> *tp=new ThreadPool<Task>();tp->Init();tp->Start();int cnt=10;while (cnt){// 不断地向线程池推送任务sleep(1);Task t(1,1);tp->Equeue(t);LOG(INFO,"equeue a task, %s\n",t.debug().c_str());sleep(1);}tp->Stop();LOG(INFO,"thread pool stop!\n");sleep(10);return 0;
}
// Thread.hpp
#pragma once
#include <pthread.h>
#include <iostream>
#include <string>
#include<functional>namespace threadModel
{// 线程执行的方法//typedef void (*func_t)(ThreadData* td);using func_t=std::function<void(const std::string&)>;class Thread{public:void Excute(){_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func) : _name(name), _func(func){}// void *ThreadRoutine(void* args)实际上参数里面还有一个Thread* thisstatic void *ThreadRoutine(void *args) // 加上static后,参数里面就没有Thread* this{Thread *self = static_cast<Thread *>(args); // 获得当前对象self->Excute();return nullptr;}bool Start(){int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);if (n != 0)return false;return true;}std::string Status(){if (_isrunning)return "running";elsereturn "sleep";}void Stop(){if (_isrunning){pthread_cancel(_tid);_isrunning = false;}}void Join(){pthread_join(_tid, nullptr);}std::string Name(){return _name;}~Thread(){Stop();}private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程执行的回调函数};
}
//Task.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>class Task
{public:Task(){}Task(int x,int y):_x(x),_y(y){}void Excute(){_result=_x+_y;}void operator()(){Excute();}std::string debug(){std::string msg=std::to_string(_x)+"+"+std::to_string(_y)+"=?";return msg;}std::string result(){std::string msg=std::to_string(_x)+"+"+std::to_string(_y)+"="+std::to_string(_result);return msg;}private:int _x;int _y;int _result;
};

在这里插入图片描述

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

spark计算引擎-架构和应用

一Spark 定义&#xff1a;Spark 是一个开源的分布式计算系统&#xff0c;它提供了一个快速且通用的集群计算平台。Spark 被设计用来处理大规模数据集&#xff0c;并且支持多种数据处理任务&#xff0c;包括批处理、交互式查询、机器学习、图形处理和流处理。 核心架构&#x…

【Linux】Linux环境基础开发工具使用

Linux开发工具 Linux编辑器-vim使用 1. vim的基本概念 vim的三种模式&#xff0c;分别是命令模式&#xff08;command mode&#xff09;、插入模式&#xff08;Insert mode&#xff09;和底行模式&#xff08;last line mode&#xff09;。 正常/普通/命令模式&#xff1a; …

如果您忘记了 Apple ID 和密码,按照指南可重新进入您的设备

即使您的 iPhone 或 iPad 由于各种原因被锁定或禁用&#xff0c;也可以使用 iTunes、“查找我的”、Apple 支持和 iCloud 解锁您的设备。但是&#xff0c;此过程需要您的 Apple ID 和密码来验证所有权并移除激活锁。如果您忘记了 Apple ID 和密码&#xff0c;请按照我们的指南重…

基于大数据技术的宠物商品信息比价及推荐系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

【算法竞赛】算法复杂度

计算的资源是有限的,竞赛题会限制代码所使用的计算资源。 计算资源有两种:计算时间和存储空间。与此对应的有时间复杂度和空间复杂度,时间复杂度衡量计算的次数,空间复杂度衡量需要的存储空间。 编程竞赛的题目在逻辑、数学、算法上有不同的难度:简单的题目,可以一眼看懂;复 …

针对考研的C语言学习(定制化快速掌握重点5)

顺序表 特点&#xff1a; 写代码主要就是增删改查&#xff01;&#xff01;&#xff01; 写代码的边界性非常重要以及考研插入和删除的位置都是从1开始&#xff0c;而数组下标是从0开始 【注】下标和位置的关系 线性表最重要的是插入和删除会涉及边界问题以及判断是否合法 …

SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器

SpringMVC九大内置组件之HandlerMapping处理器映射器-AbstractHandlerMethodMapping类以及子类RequestMappingHandlerMapping如何将Controller修饰的注解类以及类下被注解RequestMapping修饰的方法存储到处理器映射器中。 从RequestMappingHandlerMapping寻找: AbstractHandle…

C语言VS实用调试技巧

文章目录 一、什么是bug?二、什么是调试&#xff1f;三、Debug和Release四、VS调试快捷键4.1环境准备4.2调试快捷键 五、监视和内存观察5.1监视5.2内存 六、调试举例七、编程常见错误归类7.1编译型错误7.2链接型错误7.3运行时错误 一、什么是bug? &#x1f34e;bug本意是 “…

前端——切换轮播图

学完前端js小知识后&#xff0c;动手操作的一个简单图片轮播图。 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"keywords" content"关键词信息"><meta name"des…

MATLAB绘图基础9:多变量图形绘制

参考书&#xff1a;《 M A T L A B {\rm MATLAB} MATLAB与学术图表绘制》(关东升)。 9.多变量图形绘制 9.1 气泡图 气泡图用于展示三个或更多变量变量之间的关系&#xff0c;气泡图的组成要素&#xff1a; 横轴( X {\rm X} X轴)&#xff1a;表示数据集中的一个变量&#xff0c…

带徒实训项目实战讲义分享:ApiFirst文档对比功能页面开发2

前一篇&#xff1a;带徒实训项目实战讲义分享&#xff1a;ApiFirst文档对比功能页面开发 亲爱的学员朋友们好&#xff0c;本小节跟小卷一起来学习用thymeleaf模板技术来渲染数据模型到表格中&#xff0c;通过本小节的学习&#xff0c;你会真正将thymeleaf模板技术应用到实处&a…

Qt获取本机Mac地址、Ip地址

一、简述 今天给大家分享一个获取本机IP地址和Mac地址的方法&#xff0c;经过多次测试&#xff0c;台式机、笔记本等多个设备&#xff0c;暂时没有发现问题。 由于很多时候本地安装了虚拟机、蓝牙、无线网卡或者其他设备等&#xff0c;会有多个Mac地址&#xff0c;所以需要进…

汽车信息安全 -- 存到HSM中的密钥还需包裹吗?

目录 1.车规芯片的ROM_KEY 2.密钥加密与包裹 3.瑞萨RZ\T2M的密钥导入 4.小结 在车控类ECU中&#xff0c;我们通常把主控芯片MCU中的HSM以及HSM固件统一看做整个系统安全架构的信任根。 所以大家默认在HSM内部存储的数据等都是可信的&#xff0c;例如CycurHSM方案中使用HSM…

ControlGAN:Controllable Text-to-Image Generation

1 研究目的 当前的生成网络通常是不可控的&#xff0c;这意味着如果用户更改句子的某些单词&#xff0c;合成图像将与原始文本生成的合成图像显着不同&#xff1b;当给定的文本描述&#xff08;例如颜色&#xff09;发生变化时&#xff0c;鸟类的相应视觉属性被修改&#xff0c…

easyexcel常见问题分析

文章目录 一、读取数字多了很多小数位的精度问题 一、读取数字多了很多小数位的精度问题 浮点型转成BigDecimal的时候会出现精度问题&#xff0c;例如 这儿设置的实体类对象类型是String&#xff0c;默认用到的是StringNumberConverter转换器 2.1.4 版本 public class Strin…

【无人机设计与技术】四旋翼无人机的建模

摘要 本项目的目标是通过 Simulink 建模和仿真&#xff0c;研究四旋翼无人机的建模、姿态控制、定点位置控制及航点规划功能。无人机建模包含了动力单元模型、控制效率模型和刚体模型&#xff0c;并运用这些模型实现了姿态控制和位置控制。姿态控制为无人机的平稳飞行提供基础…

计算机视觉实战项目4(图像分类+目标检测+目标跟踪+姿态识别+车道线识别+车牌识别+无人机检测+A*路径规划+单目测距与测速+行人车辆计数等)

往期热门项目回顾&#xff1a; 计算机视觉项目大集合 改进的yolo目标检测-测距测速 路径规划算法 图像去雨去雾目标检测测距项目 交通标志识别项目 yolo系列-重磅yolov9界面-最新的yolo 姿态识别-3d姿态识别 深度学习小白学习路线 AI健身教练-引体向上-俯卧撑计数…

在VMware虚拟机上部署polardb

免密登录到我们的虚拟机之后&#xff0c;要在虚拟机上部署polardb数据库&#xff0c;首先第一步要先克隆源码&#xff1a; 为了进SSH协议进行传输源码需要先进行下面的步骤&#xff1a; 将宿主机上的私钥文件复制到虚拟机上 scp "C:\Users\waitw\.ssh\id_rsa" ann…

ThinkPHP发送邮件教程:从配置到发送指南!

ThinkPHP发送邮件功能实现策略&#xff1f;Thinkphp如何发邮件&#xff1f; ThinkPHP作为一个流行的PHP框架&#xff0c;提供了强大的邮件发送功能&#xff0c;使得开发者可以轻松地在应用中集成邮件发送功能。AokSend将详细介绍如何在ThinkPHP中配置和发送邮件。 ThinkPHP发…

【Linux-基础IO】如何理解Linux下一切皆文件磁盘的介绍

目录 如何理解Linux系统上一切皆文件 1.物理角度认识磁盘 2.对磁盘的存储进行逻辑抽象 磁盘寻址 3.磁盘中的寄存器 如何理解Linux系统上一切皆文件 计算机中包含大量外设&#xff0c;操作系统想要管理好这些外设&#xff0c;就必须对这些外设进行先描述再组织&#xff0c…