C++基于多设计模式下的同步异步日志系统day3
C++基于多设计模式下的同步&异步日志系统day3
📟作者主页:慢热的陕西人
🌴专栏链接:C++基于多设计模式下的同步&异步日志系统
📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言
主要内容日志项目的输出格式化类的设计和日志落地(LogSink)类设计(简单工厂模式)

文章目录
- C++基于多设计模式下的同步&异步日志系统day3
- 1.代码设计
- 1.1日志输出格式化类设计
- 1.2日志落地(LogSink)类设计(简单工厂模式)
1.代码设计
1.1日志输出格式化类设计
⽇志格式化(Formatter)类主要负责格式化⽇志消息。其主要包含以下内容
-
pattern成员:保存⽇志输出的格式字符串。
◦ %d⽇期
◦ %T缩进
◦ %t线程id
◦ %p⽇志级别
◦ %c⽇志器名称
◦ %f⽂件名
◦ %l⾏号
◦ %m⽇志消息
◦ %n换⾏ -
std::vectorFormatItem::ptritems成员:⽤于按序保存格式化字符串对应的⼦格式化对象
FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
MsgFormatItem:表⽰要从LogMsg中取出有效⽇志数据LevelFormatItem:表⽰要从LogMsg中取出⽇志等级NameFormatItem:表⽰要从LogMsg中取出⽇志器名称ThreadFormatItem:表⽰要从LogMsg中取出线程IDTimeFormatItem:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化CFileFormatItem:表⽰要从LogMsg中取出源码所在⽂件名CLineFormatItem:表⽰要从LogMsg中取出源码所在⾏号TabFormatItem:表⽰⼀个制表符缩进NLineFormatItem:表⽰⼀个换⾏OtherFormatItem:表⽰⾮格式化的原始字符串
⽰例:“[%d{%H:%M:%S}]%m%n”
pattern = "[%d{%H:%M:%S}] %m%n"
items = {
{OtherFormatItem(), "["},
{TimeFormatItem(), "%H:%M:%S"},
{OtherFormatItem(), "]"},
{MsgFormatItem (), ""},
{NLineFormatItem (), ""}
}
LogMsg msg = {
size_t _line = 22;
size_t _ctime = 12345678;
std::thread::id _tid = 0x12345678;
std::string _name = "logger";
std::string _file = "main.cpp";
std::string _payload = "创建套接字失败";
LogLevel::value _level = ERROR;
};
格式化的过程其实就是按次序从Msg中取出需要的数据进⾏字符串的连接的过程。
最终组织出来的格式化消息:“[22:32:54]创建套接字失败\n”
代码实现:
#ifndef __M_FMT_H__
#define __M_FMT_H__#include"message.hpp"
#include<time.h>
#include<vector>
#include<cassert>
#include<sstream>namespace xupt
{//抽象格式化子类基类class FormatItem{public:using ptr = std::shared_ptr<FormatItem>;virtual void format(std::ostream& out, const LogMsg &msg) = 0; };//派生格式化子类项---消息,等级,时间,文件名,行号,线程ID,日志器名,制表符,换行,其他class MsgFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << msg._payload;}};class LevelFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << LogLevel::toString(msg._level);}};class TimeFormatItem : public FormatItem{public:TimeFormatItem(const std::string &fmt = "%H:%M:%S"):_time_fmt(fmt){}virtual void format(std:: ostream& out, const LogMsg &msg){struct tm t;localtime_r(&msg._ctime, &t);char tmp[128];strftime(tmp, 127, _time_fmt.c_str(), &t);out << tmp;}private:std::string _time_fmt;}; class FileFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << msg._file;}}; class LineFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << msg._line;}}; class ThreadFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << msg._tid;}}; class LoggerFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << msg._logger;}}; class TabFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << "\t";}}; class NewLineFormatItem : public FormatItem{public:virtual void format(std:: ostream& out, const LogMsg &msg){out << "\n";}}; class OtherFormatItem : public FormatItem{public:OtherFormatItem(const std::string str):_str(str){}virtual void format(std:: ostream& out, const LogMsg &msg){out << _str;}private:std::string _str;}; class Formatter{public:Formatter(const std::string & pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):_pattern(pattern){assert(parsePattern()); //如果解析失败直接跳断言}//对msg进行格式化void format(std::ostream& out ,const LogMsg & Msg){for(auto& item : _items){item->format(out, Msg);}}std::string format(const LogMsg & Msg){std::stringstream ss;format(ss, Msg);return ss.str();}private://对格式化规则字符串进行解析bool parsePattern(){//1.对格式化规则字符串进行解析//abcd[%d{%H:%M:%S}][%p]%T%m%nstd::vector<std::pair<std::string, std::string>> fmt_order;size_t pos = 0;std::string key, val; while(pos < _pattern.size()){//1.处理原始字符串,如果不是%,那么就是原始字符if(_pattern[pos] != '%'){val.push_back(_pattern[pos++]);continue;}//%的情况//1.双%的情况if(pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back('%'); pos += 2; continue;}//能走到这里,代表%后是一个格式化字符串,这时候代表原始字符串处理完毕if(val.empty() == false){fmt_order.push_back(std::make_pair("", val));val.clear();}//2.%后是格式化字符的情况pos += 1;//这一步之后,pos指向格式化字符位置if(pos == _pattern.size()){std::cout << "%后没有对应的格式化字符" << std::endl;return false;}key = _pattern[pos];//2.{}的情况pos += 1;if(_pattern[pos] == '{'){pos += 1; //这时候pos指向子规则的起始位置while(pos < _pattern.size() && _pattern[pos] != '}'){val.push_back(_pattern[pos++]);}//走到了末尾跳出了循环,则代表没有遇到},代表格式是错误的if(pos == _pattern.size()) //没有找到}{std::cout << "子规则{}匹配失败" << std::endl;return false;}pos += 1;}fmt_order.push_back(std::make_pair(key, val));key.clear();val.clear();}//2.根据解析得到的数据初始化子项数组成员for(auto &it : fmt_order){_items.push_back(createItem(it.first,it.second));}return true;}//根据不同的格式化字符创建不同的格式化子项对象FormatItem::ptr createItem(const std::string & key, const std::string &val){if(key == "d") return std::make_shared<TimeFormatItem>(val);if(key == "t") return std::make_shared<ThreadFormatItem>();if(key == "c") return std::make_shared<LoggerFormatItem>();if(key == "f") return std::make_shared<FileFormatItem>();if(key == "l") return std::make_shared<LineFormatItem>();if(key == "p") return std::make_shared<LevelFormatItem>();if(key == "T") return std::make_shared<TabFormatItem>();if(key == "m") return std::make_shared<MsgFormatItem>();if(key == "n") return std::make_shared<NewLineFormatItem>();if(key.empty()) return std::make_shared<OtherFormatItem>(val);std::cout << "没有对应的格式化字符串:%" << key << std::endl;abort();return FormatItem::ptr();}private:std::string _pattern; //格式化规则字符串std::vector<FormatItem::ptr> _items; };}#endif
1.2日志落地(LogSink)类设计(简单工厂模式)
⽇志落地类主要负责落地⽇志消息到⽬的地
它主要包括以下内容:
- Formatter⽇志格式化器:主要是负责格式化⽇志消息。
- mutex互斥锁:保证多线程⽇志落地过程中的线程安全,避免出现交叉输出的情况。这个类⽀持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出⽬标,可以增加⼀个类继承⾃该类并重写log⽅法实现具体的落地⽇志逻辑。
目前实现了三个不同方向上的日志落地:
-
标准输出:
StdoutSink -
固定文件:
FileSink -
滚动文件:
RollSinkBySize◦ 滚动⽇志⽂件输出的必要性:
• 由于机器磁盘空间有限,我们不可能⼀直⽆限地向⼀个⽂件中增加数据
• 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨⼤,也不利于查找我们需要的信息
• 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。对于那些过期的⽇志,⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过期⽇志
◦ ⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:⽂件⼤⼩和时间。我们可以选择:
- ⽇志⽂件在⼤于1GB的时候会更换新的⽂件
- 每天定点滚动⼀个⽇志⽂件
本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件
/* 日志落地模块的实现1.抽象落地基类2.派生落地子类(根据不同的落地方向)3.使用工厂模式进行使用和生产的分离 */#ifndef __M_SIK_H__ #define __M_SIK_H__#include"util.hpp" #include<memory> #include<fstream> #include<cassert> #include<sstream>namespace xupt {class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char* data, size_t len) = 0; }; //落地方向:标准输出class StdoutSink : public LogSink{public:virtual void log(const char* data, size_t len){std::cout.write(data, len);}};//落地方向:指定文件class FileSink : public LogSink{public:FileSink(const std::string &pathname):_pathname(pathname){//1.创建日志文件所在的目录util::File::CreateDirectory(util::File::path(_pathname));//2.创建并打开日志文件_ofs.open(_pathname, std::ios::binary | std::ios::app); //以二进制追加的方式写入//断言以下是否打开成功assert(_ofs.is_open());}//写入到文件virtual void log(const char* data, size_t len){_ofs.write(data, len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs;};//落地方向:滚动文件(以大小进行滚动)class RollSinkBySize : public LogSink{public://构造函数,传入文件名,文件大小,并管理文件输出句柄RollSinkBySize(const std::string &basename, size_t max_size):_basename(basename), _max_size(max_size),_cur_size(0),_name_count(0){std::string pathname = createNewFile();//1.创建日志文件所在的目录 util::File::CreateDirectory(util::File::path(pathname));//2.创建并打开日志文件_ofs.open(pathname, std::ios::binary | std::ios::app); //以二进制追加的方式写入//断言以下是否打开成功assert(_ofs.is_open()); }//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件virtual void log(const char* data, size_t len){if(_cur_size >= _max_size){//先关闭之前的文件_ofs.close();//创建新的文件std::string pathname = createNewFile();_ofs.open(pathname,std::ios::binary | std::ios::app);//断言以下是否打开成功assert(_ofs.is_open()); //置零当前文件大小_cur_size = 0;}//写入?没有更改_cur_size_ofs.write(data, len);//断言以下是否成功写入assert(_ofs.good()); _cur_size += len; }private://当文件大小达到限制的时候,用于切换文件使用(创建新的文件名,并不能创建文件) std::string createNewFile(){//获取系统时间time_t t = util::Date::now();struct tm lt;localtime_r(&t, <);//创建一个字符串流std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << "-";filename << _name_count++;filename << ".log";return filename.str();}private:size_t _name_count ; //用于区别文件名的std::string _basename; //存储日志信息的文件, 命名规则我们一般是加一个固定的头,尾部用时间来区分std::ofstream _ofs; //维护一个我们的文件输出句柄size_t _max_size; //单个文件的最大大小size_t _cur_size; //当前文件的大小,算是一个优化,这样我们就不用去频繁查看文件属性,从而降低效率};/*拓展一个以时间作为文件滚动切换类型的日志落地模块 */ class RollSinkByTime : public LogSink{public:enum class TimeGap{GAP_SECOND,GAP_MINUTE,GAP_HOUR,GAP_DAY,};public:RollSinkByTime(const std::string &basename, TimeGap gap_type):_basename(basename){switch(gap_type) {case TimeGap::GAP_SECOND : _gap_size = 1; break;case TimeGap::GAP_MINUTE : _gap_size = 60; break;case TimeGap::GAP_HOUR : _gap_size = 3600; break;case TimeGap::GAP_DAY : _gap_size = 3600 * 24; break;}_cur_gap = _gap_size == 1 ? util::Date::now() : util::Date::now() % _gap_size; //初始化当前的时间片util::File::CreateDirectory(util::File::path(_basename)); //1.创建日志文件所在的目录 std::string filename = createNewFile(); //2.创建文件名,并且打开文件_ofs.open(filename,std::ios::binary | std::ios::app);assert(_ofs.is_open()); }//写入到文件,判断当前的时间段,是否是当前文件的时间段,不是则切换文件virtual void log(const char* data, size_t len){//获取系统时间time_t cur = util::Date::now(); if(cur % _gap_size != _cur_gap){_ofs.close();std::string filename = createNewFile(); _ofs.open(filename,std::ios::binary | std::ios::app);assert(_ofs.is_open()); }//写入文件_ofs.write(data, len);assert(_ofs.good());}private://当时间片达到限制的时候,用于切换文件使用,(创建新的文件名,并不能创建文件) std::string createNewFile(){//获取系统时间time_t t = util::Date::now();struct tm lt;localtime_r(&t, <);//创建一个字符串流std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << "-";filename << ".log";return filename.str();}private:std::string _basename;std::ofstream _ofs;size_t _cur_gap;size_t _gap_size;};class SinkFactory{public:template<typename SinkType, typename ...Args>static LogSink::ptr create(Args && ...args) //右值引用,保持原来类型的属性,用于后来的完美转发{return std::make_shared<SinkType>(std::forward<Args>(args)...);}}; }#endif
到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

相关文章:
C++基于多设计模式下的同步异步日志系统day3
C基于多设计模式下的同步&异步日志系统day3 📟作者主页:慢热的陕西人 🌴专栏链接:C基于多设计模式下的同步&异步日志系统 📣欢迎各位大佬👍点赞🔥关注🚓收藏,&am…...
Cypher语句查询neo4j数据库教程
文章目录 Cypher介绍执行Cypher语句查询总结 Cypher介绍 NodeMatcher和RelationshipMatcher能够表达的匹配条件相对简单,更加复杂的查询还是需要用Cypher语句来表达。 Py2neo本身支持执行Cypher语句的执行,可以将复杂的查询写成Cypher语句,…...
【ESP32 IDF快速入门】点亮第一个LED灯与流水灯
文章目录 前言一、有哪些工作模式?1.1 GPIO的详细介绍1.2 GPIO的内部框图输入模式输出部分 二、GPIO操作函数2.1 GPIO 汇总2.2 GPIO操作函数gpio_config配置引脚reset 引脚函数设置引脚电平选中对应引脚设置引脚的方向 2.3 点亮第一个灯 三、流水灯总结 前言 ESP32…...
再见,Visual Basic——曾经风靡一时的编程语言
2020年3月,微软团队宣布了对Visual Basic(VB)的“终审判决”:不再进行开发或增加新功能。这意味着曾经风光无限的VB正式退出了历史舞台。 VB是微软推出的首款可视化编程软件,自1991年问世以来,便受到了广大…...
【C++精简版回顾】18.文件操作
1.文件操作头文件 2.操作文件所用到的函数 1.文件io 1.头文件 #include<fstream> 2.打开文件 (1)函数名 文件对象.open (2)函数参数 /* ios::out 可读 ios::in 可…...
【解决方案】ArcGIS Engine二次开发时,运行后出现“正尝试在 OS 加载程序锁内执行托管代码。不要尝试在 DllMain...”
我们在做ArcGIS Engine二次开发时,特别是新手,安装好了开发环境,满怀信心的准备将按照教程搭建好的框架在Visual Studio中进行运行。点击运行后,却出现了“正尝试在 OS 加载程序锁内执行托管代码。不要尝试在 DllMain 或映像初始化…...
新项目,Linux上一键安装MySQL,Redis,Nacos,Minio
大家好,我是 jonssonyan 分享一个我的一个开源项目,这是一个在 Linux 平台上一键安装各种软件的脚本项目,脚本使用 Shell 语言编写,后续还会增加更多软件的一键安装,代码在 GitHub 上全部开源的,开源地址如…...
Rust 从 PyTorch 到 Burn
一、性能轮盘赌 机器码相同,但放置在不同的地址上,性能可能截然不同。 作为软件开发人员,我们经常假设特定代码的性能仅由代码本身和运行它的硬件决定。这种假设让我们在优化代码以获得更好性能时感到有控制力。虽然在大多数情况下这种假设…...
Swin-Transformer网络代码实现
还是参考导师级别博主霹雳吧啦Wz的个人空间-霹雳吧啦Wz个人主页-哔哩哔哩视频 博主写的博客Swin-Transformer网络结构详解_swin transformer-CSDN博客 视频理论讲解12.1 Swin-Transformer网络结构详解_哔哩哔哩_bilibili pytorch实现12.2 使用Pytorch搭建Swin-Transformer网…...
Java ZooKeeper-RocketMQ 面试题
Java ZooKeeper-RocketMQ 面试题 前言1、谈谈你对ZooKeeper的理解 ?2、Zookeeper的工作原理(Zab协议)3、谈谈你对分布式锁的理解,以及分布式锁的实现?4、 zookeeper 是如何保证事务的顺序一致性的?5、 zook…...
css制作瀑布流布局
CSS制作瀑布流布局的步骤如下: HTML结构:使用无序列表ul和列表项li来创建网格布局。 <ul class"grid"><li><img src"image1.jpg"></li><li><img src"image2.jpg"></li><li…...
Redis 的哨兵模式配置
1.配置 vim sentinel.conf# mymaster 给主机起的名字 # 192.168.205.128 主机的ip地址 # 6379 端口号 # 2 当几个哨兵发现主观宕机,则判定为客观宕机。 原则上是大于一半。比如三个哨兵,则设置为 2 sentinel monitor mymaster 192.168.205.128 63…...
基于单片机的继电器参数测试系统设计
摘要:由于原有测试系统在参数设置上过于单一,在对继电器测试过程中需要多次进行器件连接,导致对其测试准确度下降,会造成继电器的二次损伤,基于单片机研究继电器参数测试系统的设计方法。在硬件设计上,构建二级模式集散控制框架,利用单片机进行数据采集和处理,满足实时…...
unity 数学 空间四个点是否在同一个平面
问题:已知三维空间中四点A、B、C、D,如何知道四个点是否在同一个平面呢 首先我们知道三点确定一个平面,所以可以由上面四个点其中任意三点组成一个平面p(A,B,C),另外一个点和三个任意点的形成线࿰…...
数据卷dockerfile
目录 一、数据卷 1. 简介 2. 数据卷和数据卷容器 1. 数据卷: 2. 数据卷容器: 二、自定义镜像 1. 作用 2. 自定义centos 3. 自定义tomcat8 一、数据卷 1. 简介 数据卷是一个可供一个或多个容器使用的特殊目录,它将主机操作系统目录直…...
AOP的介绍与使用
文章目录 AOP的概念AOP术语AOP的实现AspectJSpring AOP Spring AOP原理JDK动态代理CGLib动态代理 SpringAOP代码编写规则自定义切面自定义切点自定义通知在通知中获取当前请求代码实例 一些选择题 AOP的概念 • Aspect Oriented Programing,即面向方面(…...
金融行业专题|期货超融合架构转型与场景探索合集(2023版)
更新内容: 更新 SmartX 超融合在期货行业的覆盖范围、部署规模与应用场景。新增 CTP 主席系统实践与评测、容器云资源池等场景实践。更多超融合金融核心生产业务场景实践,欢迎下载阅读电子书《SmartX 金融核心生产业务场景探索文章合集》。 面对不断变…...
08 yum和git
什么是软件包 安装软件,一个通常的办法就是下载程序的源代码进行编译。这种太麻烦,于是一些人把常用软件编译好,做成软件包放在服务器上,通过包管理器可以很方便的得到这个软件包安装,就好比手机上的应用商店 yum&am…...
JMeter元件和采样器一览
Apache JMeter是一个强大的开源负载测试工具,用于性能和功能测试。JMeter提供了丰富的元件和采样器,使得它能够模拟复杂的测试场景和高并发的用户请求。以下是JMeter中常用的一些元件和采样器的介绍和讲解: 测试计划元件 测试计划࿰…...
BF算法的优化之SPFA算法
介绍 全称Shortest Path Faster Algorithm. 优化思想: 1.由int path[maxn]定义的记录最短距离的容器,只有在path[i]value<path[j]时才会更新,它们两者的值相等时path的值仍保持不变。由此优化容器,选择用一个队列来替path数…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
Web中间件--tomcat学习
Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机,它可以执行Java字节码。Java虚拟机是Java平台的一部分,Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...
Linux中《基础IO》详细介绍
目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改,实现简单cat命令 输出信息到显示器,你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...
轻量级Docker管理工具Docker Switchboard
简介 什么是 Docker Switchboard ? Docker Switchboard 是一个轻量级的 Web 应用程序,用于管理 Docker 容器。它提供了一个干净、用户友好的界面来启动、停止和监控主机上运行的容器,使其成为本地开发、家庭实验室或小型服务器设置的理想选择…...
