Linux学习笔记---018
- Linux之线程池、进程池知识储备
Linux之线程池、进程池知识储备
前言:
前篇开始进行了解学习Linux线程、进程、多线程、多进程等相关内容,接下来学习关于Linux线程池、进程池等知识,深入地了解这个强大的开源操作系统。
/知识点汇总/
1、线程池
线程池是一种基于池化技术 的并发框架,它预先创建并管理一组线程,用于执行提交给它的任务。
线程池中的线程可以重复利用,减少了线程的创建和销毁开销,提高了系统的响应速度和吞吐量。
1.1、池化技术
池化技术(Pooling Technique)是一种广泛应用于软件开发、系统设计以及深度学习中的资源管理策略。其核心理念是通过复用预先创建和初始化的资源,来提升系统性能、效率和稳定性。
1.1.1、定义与原理
池化技术指的是在系统运行前或运行时,预先分配并维护一组资源(如线程、数据库连接、内存块等),这些资源被组织成一个"池"。当系统需要这些资源时,不是每次都重新创建新的实例,而是从池中获取一个已存在的实例进行使用。使用完毕后,实例会被归还到池中,以便后续的请求可以重复利用。
1.1.2、优点
1.资源节约:
减少资源创建与销毁的开销。例如,创建新线程、打开数据库连接等操作可能涉及耗时的系统调用,而池化技术通过复用已有资源来避免这些开销。
2.提高响应速度:通过预先初始化好的资源和重用现有资源,避免了每次请求时的初始化延迟,从而提高了系统的响应速度。
3.控制资源使用量:限制资源的最大使用数量,防止资源耗尽,确保系统的稳定运行。
4.增强系统性能和稳定性:通过管理资源的生命周期(监控),可以及时调整资源分配,满足系统需求,并更好地处理资源泄露等问题。
1.1.3、应用场景
1.线程池: 管理一组可用的线程,以高效地执行并发任务。线程池减少了线程的创建和销毁开销,提高了系统对并发请求的响应能力。
2.数据库连接池: 管理数据库连接资源,通过复用连接来减少连接创建和销毁的开销,提高系统对数据库资源的访问性能。
3.内存池: 将一块连续的内存分割成多个固定大小的对象块,通过对这些对象进行复用,减少内存分配与释放的频率,提高内存使用效率。
4.连接池: 不仅限于数据库连接,还包括如Redis连接池、HTTP连接池等,用于管理各种网络连接资源。
5.对象池: 管理一组可复用的对象实例,以减少对象的创建和销毁开销,提高系统的性能和资源利用率。
6.缓存池: 将常用的计算结果、数据或资源存储在内存中,以加快对这些数据的访问速度。
1.2、线程池的特点与优势
1.资源共享: 线程池中的线程共享相同的地址空间和资源,使得线程间的通信和同步变得容易。
2.减少开销: 线程的创建和销毁开销相对较小,线程池通过重用线程减少了这些开销。
3.提高响应速度: 线程池可以快速地响应大量的并发请求,因为它可以立即分配一个空闲线程来处理新任务。
4.易于管理: 线程池提供了统一的接口来管理线程的生命周期和任务执行,简化了并发编程的复杂性。
1.3、线程池的适用场景
1.IO密集型任务: 如Web服务器处理HTTP请求,数据库操作等。这些任务大部分时间都在等待IO操作完成,线程池可以充分利用CPU资源来并发处理这些任务。
2.高并发场景: 当需要处理大量并发请求时,线程池可以快速地分配线程来处理这些请求,提高系统的响应速度和吞吐量。
3.计算密集型任务(但非极端情况): 虽然线程池在处理计算密集型任务时可能不如进程池高效(因为线程间的切换开销和共享资源竞争),但在非极端情况下,线程池仍然可以胜任。
1.4、线程池的实现
ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <queue>
#include <functional>
#include "Thread.hpp"
using namespace ThreadMoudle;
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()
{
//强关联,绑定,且placeholders自动推导name参数
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;
};
Task.hpp
cpp
#pragma once
/
//方法三:function
/**/
#include <iodtream>
#include <functional>
using Task_t = std::function<void()>;//函数对象:返回值为void,参数为空的函数类型对象
//等价
//typedef std::function<void()> Task_t;
void Download()
{
std::cout << "我是一个下载任务!" << std::endl;
}
/
//方法二:类
/*
class Task
{
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;
};
*/
/**/
#pragma once
#include<iostream>
#include<functional>
// typedef std::function<void()> task_t;
// using task_t = std::function<void()>;
// void Download()
// {
// std::cout << "我是一个下载的任务" << std::endl;
// }
// 要做加法
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;
};
cpp
#include "ThreadPool.hpp"
#include "Task.hpp"
//#include <memory>
int main()
{
//std::unique_ptr<ThreadPool> tp = std::make_unique<ThreadPool>();//C++14 智能指针
//ThreadPool<int>* tp = new ThreadPool<int>();
ThreadPool<Task>* tp = new ThreadPool<Task>();
tp->Init();
tp->Start();
int cnt = 10;
while (cnt--)
{
//不断的向线程池推送任务
sleep(1);
Task t(1, 1);
tp->Equeue(t);
sleep(1);
std::cout << "cnt" << cnt-- << std::endl;
}
tp->Stop();
std::cout << "thread pool stop" << std::endl;
sleep(10);
return 0;
}
2、进程池
2.1、定义和基本概念
进程池与线程池类似,但它管理的是一组进程而不是线程。
每个进程都有自己独立的地址空间和资源,因此进程间的通信和同步相对复杂,但提供了更好的隔离性和安全性。
2.2、进程池的特点与优势
1.高隔离性:
每个进程都有自己独立的地址空间和资源,因此进程间的隔离性较好,一个进程的崩溃不会影响其他进程。
2.安全性:由于进程间的隔离性,进程池可以提供更高的安全性,防止恶意代码或错误操作对其他进程造成影响。
3.适用性强:对于需要独立运行的任务或需要较高隔离性的场景,进程池是更好
的选择。
2.3、进程池的适用场景:
**1.CPU密集型任务: **
当任务主要是计算密集型时,进程池可以提供更好的性能,因为每个进程都有自己独立的CPU资源,减少了线程间的切换开销和共享资源竞争。
**2.高隔离性需求: **
当任务之间需要较高的隔离性时,如避免一个任务的崩溃影响其他任务时,进程池是更好的选择。
3.安全性要求高的场景:如处理敏感数据或执行高风险操作时,进程池可以提供更高的安全性保障。
3、单例模式
Linux单例模式是一种常用的软件设计模式,**旨在确保一个类在系统中只有一个实例,并提供一个全局访问点来获取这个实例。**单例模式可以分为饿汉式和懒汉式两种实现方式,它们的主要区别在于实例的创建时机。
3.1、"饿汉"
1.特点:
系统一运行就立即创建实例,实例的创建是线程安全的。
实例在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。
2.实现方式:
饿汉式单例模式通常通过静态初始化块或直接在静态变量初始化时创建实例 。这种方式保证了单例的唯一性 ,并且由于是静态初始化,因此也是线程安全的。
cpp
class Singleton {
private:
static Singleton instance; // 静态实例,类加载时就初始化
// 私有化构造函数和析构函数,防止外部创建和销毁实例
Singleton() {}
~Singleton() {}
public:
// 提供一个静态方法返回实例
static Singleton& GetInstance() {
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
// 静态实例初始化
Singleton Singleton::instance;
注意:
在C++中,如果全局对象之间有依赖关系,可能会导致构造顺序问题。此外,由于实例在类加载时就创建,可能会浪费资源,尤其是当实例很大且长时间不被使用时。
3.2、"懒汉"
1.特点:
实例在第一次被使用时创建 ,即"延时加载"。
在多线程环境下,需要加锁来保证线程安全,否则可能创建多个实例。
2.实现方式:
懒汉式单例模式在第一次调用GetInstance方法时创建实例。为了实现线程安全,可以使用互斥锁(如std::mutex)来保护实例的创建过程。
cpp
#include <mutex>
class Singleton {
private:
static Singleton* instance; // 静态指针,指向类的唯一实例
static std::mutex mtx; // 互斥锁
// 私有化构造函数和析构函数
Singleton() {}
~Singleton() {}
public:
// 提供一个静态方法返回实例
static Singleton* GetInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) { // 双重检查锁定
instance = new Singleton();
}
}
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
// 静态成员初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
注意:
懒汉式单例模式在多线程环境下需要加锁来保证线程安全,但加锁会带来一定的性能开销。此外,懒汉式单例模式在第一次使用实例时才创建,因此可以提高程序启动速度,并减少不必要的资源浪费。但是,如果实例创建过程较为复杂或耗时较长,可能会影响程序的首次响应时间。
4、死锁问题
死锁是指在一组进程中,各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态称为死锁,即: 长时间代码不被推进 。
Linux死锁问题是一个在多进程或多线程环境中常见的复杂问题,它指的是两个或多个进程(或线程)在执行过程中,因互相持有对方所需的资源而又都等待对方释放资源,导致它们都无法继续执行下去的一种僵局状态。
4.1、死锁的定义
在Linux操作系统中,死锁是指一组进程(或线程)中的每一个进程(或线程)都在等待仅由该组进程中的其他进程(或线程)才能引发的事件,从而导致它们都无法继续执行。这种情况发生时,进程(或线程)将无法进行下去,无法释放资源,也无法获取需要的资源,进而导致系统无法继续运行。
4.2、死锁的产生条件
死锁通常发生在多进程或多线程环境中,当满足以下四个条件时,就可能发生死锁:
1.互斥条件: 一个资源只能被一个进程(线程)访问,即资源独占。
2.占有且等待: 进程(线程)在占有一个资源时,可以请求其他资源。
3.不可剥夺条件: 一个资源只能由其持有者释放,不能强行剥夺。
4.循环等待条件: 多个进程(线程)之间形成一种循环等待资源的关系,每个进程(线程)等待下一个进程(线程)所持有的资源。
4.3、死锁的原因
Linux中产生死锁的原因主要有以下几种:
1.竞争不可抢占资源: 如共享文件时引起死锁,当两个或多个进程都试图访问对方已占用的资源时,就可能发生死锁。
2.竞争可消耗资源: 如消息传递中的死锁,当进程间通过消息传递进行通信时,如果它们都以特定的顺序发送和接收消息,且都先等待接收消息而不发送,就可能发生死锁。
3.进程推进顺序不当: 进程在运行过程中,请求和释放资源的顺序不当,也可能导致死锁。
4.4、死锁的解决方式
为了避免和解决死锁问题,可以采取以下几种方法:
1.预防死锁: 通过设置某些限制条件,以破坏产生死锁的四个必要条件中的一个或几个,来防止发生死锁。例如,通过资源预分配、资源有序性等方法来降低死锁的可能性。
2.避免死锁: 在资源的动态分配过程中,使用某种方法去防止系统进入不安全状态,从而避免了死锁的发生。例如,使用银行家算法来动态检查资源分配的安全性。
3.检测死锁: 允许系统运行过程中发生死锁,但通过系统所设置的检测机构,及时检测出死锁的发生,并精确地确定与死锁有关的进程和资源。
4.解除死锁: 一旦检测到死锁,就采取适当措施从系统中消除死锁。常用的方法是撤销或者挂起一些进程,以便于释放出一些资源,再将它分配给已经处于阻塞的进程,使其转换为就绪状态可以继续运行。
4.5、小结
死锁的四个条件:
1.互斥条件:一个资源每次只能被一个执行流使用
2.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺;
4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
避免死锁:
1.破坏死锁的四个必要条件
2.加锁顺序一致
3.避免锁未释放的场景
4.资源一次性分配
5、 可重入 VS 线程安全
5.1、可重入
可重入性通常指的是一个函数或程序段在被多个线程(或单个线程多次)调用时,能够正确地执行而不会产生不可预测的结果,即使这些调用是同时进行的(并发执行)。
在C/C++等语言中,一个函数如果满足以下条件,则可以被认为是可重入的:
1.不修改全局变量:或者至少是以一种线程安全的方式修改全局变量。
2.不依赖于静态局部变量:因为静态局部变量在函数的不同调用之间保持其值。
3.不调用任何不可重入的函数。
值得注意的是: 可重入性通常关注的是函数内部的状态管理,而不直接涉及多线程间的同步问题。
5.2、线程安全
线程安全指的是一个函数或程序段在多个线程同时执行时,能够正确地执行而不会产生不可预测的结果。
线程安全通常需要确保:
1.对共享资源的访问是同步的:
这通常通过互斥锁(mutexes)、信号量(semaphores)或其他同步机制来实现。
2.避免了竞态条件:即多个线程尝试同时修改同一资源时可能导致的问题。
3.正确处理了所有可能的并发访问情况。
线程安全的概念比可重入更广泛,因为它涉及到多线程之间的交互和同步。一个线程安全的函数或程序段可能包括不可重入的函数,但通过适当的同步机制来保证整体的线程安全性。
5.3、关系与区别
1.可重入是线程安全的一个必要条件: 一个线程安全的函数或程序段必须是可重入的,但可重入的函数或程序段不一定是线程安全的,特别是当它们访问共享资源时。
2.关注点不同: 可重入性主要关注函数内部的状态管理,而线程安全性则关注多线程之间的交互和同步。
小结:
1.线程安全:
多个线程并发同一段代码时,不会出现不同的结果。
2. 重入:指对同一个函数被不同的执行流调用,当前一个流程还没完,就有其他的执行流再次进入,我们称之为重入。且一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
3.结论:如果一个函数是可重入的 那么,多线程调用这个可重入函数时,多线程也是安全的 ;
反过来多线程能调用可重入。那么整个代码就是安全的。
线程安全不一定是可重入的,可重入函数则一定是线程安全的。
6、volatile关键字
C++中的volatile关键字是一个类型修饰符,用于告诉编译器该变量的值可能会在程序的控制之外被改变。这通常用于多线程编程、硬件访问(如内存映射的I/O寄存器)或中断服务例程中,其中变量的值可能会因为外部事件(如硬件中断或另一个线程的执行)而突然改变。
6.1、volatile关键字的作用
1.防止编译器过度优化:
编译器在编译代码时,会尝试通过优化来提高程序的执行效率。对于它认为在两次读取之间不会改变的变量,编译器可能会将其值缓存在寄存器中,而不是每次都从内存中读取。然而,如果这样的变量被volatile修饰,编译器就会知道这个变量的值可能会突然改变,因此它不会对这个变量进行这样的优化。
2.保证内存可见性:
在多线程环境中,一个线程对volatile变量的修改对其他线程是可见的。然而,需要注意的是,volatile并不保证操作的原子性,也不提供任何形式的同步机制(如互斥锁)。
6.2、volatile样例
下面是一个简单的C++多线程示例,其中使用了volatile关键字来确保一个线程能够检测到另一个线程对共享变量的修改。但是,请注意,这个示例并不是最佳实践,因为volatile并不足以保证线程安全。在实际应用中,应该使用更高级的同步机制,如互斥锁(std::mutex)或原子操作(std::atomic)。
cpp
#include <iostream>
#include <thread>
#include <chrono>
volatile bool running = true; // 使用volatile修饰共享变量
void threadFunction() {
while (running) {
// 执行一些操作
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
}
std::cout << "Thread has stopped." << std::endl;
}
int main() {
std::thread t(threadFunction);
// 做一些工作
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟主线程的工作
running = false; // 通知线程停止
t.join(); // 等待线程结束
return 0;
}
在这个例子中,running变量被声明为volatile,以确保当主线程将其设置为false时,这个修改对threadFunction中的线程是可见的。然而,由于volatile不保证操作的原子性,如果running的读取和写入不是原子的(在大多数现代处理器上,单个字节的读写通常是原子的,但这不是由C++标准保证的),那么仍然可能会遇到竞态条件。
此外,即使running的读取和写入是原子的,volatile也不足以防止指令重排序等更复杂的并发问题。因此,在需要确保线程安全的情况下,应该使用更强大的同步机制。
注意:在C++11及更高版本中,推荐使用std::atomic来代替volatile进行线程间的同步和通信 。std::atomic提供了必要的原子性和内存顺序保证,以安全地在多线程环境中使用共享变量。
volatile主要用于与硬件直接交互的场景,或者当你确信编译器优化会导致问题时。在大多数多线程编程场景中,应该优先考虑使用std::atomic、std::mutex、std::condition_variable等同步机制。
7、日志的重要性
Linux日志是Linux系统中非常重要的组成部分,它们记录了系统的运行情况、错误信息、用户活动等重要信息 ,对于系统管理和故障排查具有不可替代的作用。
7.1、日志文件概述
Linux系统中的日志文件通常存储在/var/log目录下,该目录下包含了多种不同类型的日志文件,用于记录不同类型的信息。例如,/var/log/messages记录了系统的各种事件和错误信息,/var/log/secure记录了用户的登录、认证和授权信息,/var/log/httpd/access_log和/var/log/httpd/error_log则分别记录了Apache
HTTP服务器的访问日志和错误日志等。
7.2、常见日志文件及作用
1.系统日志
name | Value |
---|---|
/var/log/messages | 记录了系统的各种事件和错误信息,包括系统启动、登录、网络连接、内核错误等 |
/var/log/dmesg | 记录了系统引导过程中的信息,包括硬件检测、内核启动等 |
/var/log/boot.log | 记录了系统在引导过程中发生的事件,与/var/log/dmesg类似,但可能包含更详细的启动信息 |
2.用户日志
name | Value |
---|---|
/var/log/secure | 记录了用户的登录、认证和授权信息,包括成功和失败的登录尝试、sudo或su命令的使用等 |
/var/log/lastlog | 记录了每个用户最后一次成功登录的信息,如登录时间、IP地址等 |
/var/log/wtmp | 记录了用户登录、注销、系统启动等信息,可以使用last命令查看信息 |
/var/log/btmp | 记录了失败的登录尝试信息,可以使用lastb命令查看 |
3.程序日志
应用程序通常会将日志信息写入到/var/log目录下的特定文件中,文件名和位置取决于应用程序的配置。例如,Apache HTTP服务器的访问日志和错误日志分别存储在/var/log/httpd/access_log和/var/log/httpd/error_log中。
其他常见的程序日志包括Nginx的/var/log/nginx/access.log和/var/log/nginx/error.log,MySQL的/var/log/mysql/error.log等。
4.安全日志
name | Value |
---|---|
/var/log/audit/audit.log | 记录了安全事件、安全漏洞、攻击尝试等信息,需要启用审计服务(auditd)才能生成此日志文件 |
7.3、日志管理工具
Linux系统提供了多种日志管理工具,用于帮助用户查看、分析和管理日志文件。
常用的工具有:
name | Value |
---|---|
tail | 用于查看文件的末尾内容,特别是与-f选项结合使用时,可以实时监控日志文件的更新 |
cat | 用于显示整个文件的内容,虽然对于大型日志文件来说可能不太实用,但在处理小型文件或需要查看整个文件内容时非常有用 |
grep | 用于在文件中搜索包含特定模式的行,是日志分析中最常用的命令之一 |
less | 提供了一个分页查看文件内容的界面,允许用户向前和向后浏览文件,搜索文本等 |
awk | 一个强大的文本处理工具,可以用于格式化文本文件和提取数据 |
sed | 一个流编辑器,用于对文本文件进行基本的文本转换 |
7.4、日志的查看与分析
在Linux系统中,查看和分析日志文件的常用方法包括:
1.使用tail、cat、grep等命令直接查看日志文件的内容。
2.使用less或more命令分页查看大型日志文件的内容。
3.使用awk、sed等文本处理工具对日志文件进行格式化、提取和转换操作。
4.结合使用多个命令和管道符(|),实现复杂的日志分析和过滤操作。
7.5、简易日志的实现
获取时间的函数:
cpp
int gettimeofday(struct timeval* tv,struct timezone* tz);
int settimeofday(const struct timeval* tv,const struct timezone* tz);
cpp
time_t time(time_t* tloc);//time_t时间戳
转换时间戳为年月日制:
cpp
struct tm* loacltime(const time_t* timep);
处理可变参数常用:vsnprintf
cpp
#include <stdarg.h>
int vsnprintf(char* str,size_t size,const char* format,va_list ap);
补充:
实参传递到形参,是把形参入栈,从有向左入栈的。
比如可变参数的样例。
va_list ap;就是可变参数指针
va_start(ap,num);初始化,num是距离可变参数左边最近的参数
...
ca_end(ap);释放
Log.hpp程序实现
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"
namespace log_ns
{
enum
{
DEBUG = 1,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LevelToString(int level)//日志等级
{
switch (level)
{
case DEBUG:
return "DEBUG";
case INFO:
return "INFO";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
std::string GetCurrTime()//获取时间
{
time_t now = time(nullptr);
struct tm* curr_time = localtime(&now);//转时间制
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
curr_time->tm_year + 1900,
curr_time->tm_mon + 1,
curr_time->tm_mday,
curr_time->tm_hour,
curr_time->tm_min,
curr_time->tm_sec);
return buffer;
}
class logmessage
{
public:
std::string _level;
pid_t _id;
std::string _filename;
int _filenumber;
std::string _curr_time;
std::string _message_info;
};
#define SCREEN_TYPE 1
#define FILE_TYPE 2
const std::string glogfile = "./log.txt";
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
// log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
class Log
{
public:
Log(const std::string& logfile = glogfile) : _logfile(logfile), _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._curr_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._curr_time.c_str(),
lg._message_info.c_str());
out.write(logtxt, strlen(logtxt));//写入
out.close();
}
void FlushLog(const logmessage& lg)
{
// 加过滤逻辑 --- TODO,筛选等级,宏参/条件编译等处理
LockGuard lockguard(&glock);//加锁兼容多线程
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();//pid
lg._filename = filename;//文件名
lg._filenumber = filenumber;//行号
lg._curr_time = GetCurrTime();//时间
va_list ap;//可变参数处理
va_start(ap, format);//可变参数初始化
char log_info[1024];
vsnprintf(log_info, sizeof(log_info), format, ap);//可变参数格式化转为字符串放入指定log_info中
va_end(ap);//销毁
lg._message_info = log_info;
// 打印出来日志
FlushLog(lg);
}
~Log()
{
}
private:
int _type;//打印目标的类型,1.屏幕 2.文件
std::string _logfile;
};
Log lg;
//优化,日志输出格式,VA_ARGS()/__VA_ARGS__属于C99语法,作用是让宏支持可变参数,
//并在此之前加两个##表示兼容没有可变参数的情况。自动去掉前面的逗号。匹配只需要的参数即可
#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)
};
7.6、日志的维护与优化
由于日志文件可能会不断增长并占用大量磁盘空间,因此需要对它们进行定期的维护和优化。
常用的方法包括:
1.使用logrotate工具定期压缩、归档和删除旧的日志文件。
2.配置日志文件的轮转策略,如按天、周或月进行轮转。
3.设置日志文件的大小限制,当文件达到指定大小时自动进行轮转。
4.清理不必要的日志文件,以释放磁盘空间。