【Linux编程】线程同步与互斥

【Linux编程】线程同步与互斥

一、线程互斥:解决资源竞争的核心

1.1 核心概念铺垫

多线程并发访问共享资源时,若缺乏保护机制,会导致数据一致性问题。先明确几个关键概念:

  • 共享资源:多个线程均可访问的数据或资源(如全局变量、硬件设备)。
  • 临界资源:需要被保护的共享资源(如售票系统中的剩余票数)。
  • 临界区:线程中访问临界资源的代码段。
  • 互斥:保证同一时刻只有一个线程进入临界区,是线程安全的基础。
  • 原子性:操作不可被中断,要么完整执行,要么不执行(如CPU的swap指令)。

1.2 为什么需要互斥?实战反例

先看一个无保护的售票系统代码,感受资源竞争带来的问题:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100; // 共享资源:剩余票数

void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
        if (ticket > 0) {
            usleep(1000); // 模拟业务处理延迟
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
        }
    }
}

int main(void) {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

模拟运行结果(异常)

复制代码
thread 4 sells ticket:100
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

问题分析

  1. if (ticket > 0) 判断后,线程可能被切换,导致多个线程同时进入临界区。
  2. ticket-- 并非原子操作,实际对应三条汇编指令(load→update→store),过程中可能被中断。

1.3 互斥量(mutex):Linux的锁机制

Linux提供互斥量(mutex)实现线程互斥,核心是通过锁机制保证临界区原子执行。

1.3.1 互斥量核心接口
操作 函数声明 说明
静态初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 简单场景使用,无需手动销毁
动态初始化 int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) attr传NULL表示默认属性
销毁 int pthread_mutex_destroy(pthread_mutex_t *mutex) 仅销毁动态初始化的互斥量,已加锁的互斥量不可销毁
加锁 int pthread_mutex_lock(pthread_mutex_t *mutex) 阻塞式加锁,若锁被占用则等待
解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex) 释放锁,唤醒等待的线程
1.3.2 改进版售票系统(互斥保护)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;
pthread_mutex_t mutex; // 定义互斥量

void *route(void *arg) {
    char *id = (char*)arg;
    while (1) {
        pthread_mutex_lock(&mutex); // 加锁
        if (ticket > 0) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex); // 解锁
        } else {
            pthread_mutex_unlock(&mutex); // 解锁(避免死锁)
            break;
        }
    }
    return nullptr;
}

int main(void) {
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL); // 动态初始化

    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    
    pthread_mutex_destroy(&mutex); // 销毁互斥量
    return 0;
}

运行结果(正常):票数从100递减到1,无负数,无重复售票。

1.4 互斥量实现原理

互斥量的原子性依赖CPU的特殊指令(如swap或exchange),核心伪代码逻辑:

asm 复制代码
lock:
    movb $0, %al
    xchgb %al, mutex  # 原子交换:寄存器与内存数据互换
    if (%al > 0) {
        挂起等待;
        goto lock;
    }
    return 0;

unlock:
    movb $1, mutex    # 释放锁
    唤醒等待线程;
    return 0;

1.5 C++封装:RAII风格锁

手动加锁解锁容易遗漏(如异常退出时),用RAII(资源获取即初始化)风格封装,自动管理锁的生命周期:

cpp 复制代码
// Lock.hpp
#pragma once
#include <pthread.h>

namespace LockModule {
    class Mutex {
    public:
        Mutex(const Mutex &) = delete; // 禁用拷贝
        Mutex &operator=(const Mutex &) = delete;

        Mutex() { pthread_mutex_init(&_mutex, nullptr); }
        ~Mutex() { pthread_mutex_destroy(&_mutex); }

        void Lock() { pthread_mutex_lock(&_mutex); }
        void Unlock() { pthread_mutex_unlock(&_mutex); }
        pthread_mutex_t *GetMutexOriginal() { return &_mutex; }

    private:
        pthread_mutex_t _mutex;
    };

    // RAII锁管理
    class LockGuard {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
        ~LockGuard() { _mutex.Unlock(); }

    private:
        Mutex &_mutex;
    };
}

使用封装后的锁

cpp 复制代码
#include "Lock.hpp"
using namespace LockModule;

int ticket = 1000;
Mutex mutex;

void *route(void *arg) {
    char *id = (char *)arg;
    while (1) {
        LockGuard lockguard(mutex); // 自动加锁
        if (ticket > 0) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
        }
    }
    return nullptr;
}

二、线程同步:实现有序协作

2.1 同步的核心意义

互斥解决了"资源竞争"问题,但未解决"执行顺序"问题。同步是在保证线程安全的前提下,让线程按特定顺序执行,避免饥饿问题。

  • 竞态条件:因线程执行时序不确定导致的程序异常。
  • 条件变量:实现线程同步的核心机制,允许线程等待某个条件满足后再执行。

2.2 条件变量核心接口

操作 函数声明 说明
初始化 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) attr传NULL表示默认属性
销毁 int pthread_cond_destroy(pthread_cond_t *cond) 销毁条件变量
等待 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 释放互斥量并等待,被唤醒后重新获取互斥量
唤醒单个线程 int pthread_cond_signal(pthread_cond_t *cond) 唤醒等待队列中的一个线程
唤醒所有线程 int pthread_cond_broadcast(pthread_cond_t *cond) 唤醒等待队列中的所有线程

2.3 条件变量使用场景:简单通知机制

cpp 复制代码
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *active(void *arg) {
    string name = static_cast<const char*>(arg);
    while (true) {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); // 等待通知
        cout << name << " 活动..." << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, active, (void*)"thread-1");
    pthread_create(&t2, NULL, active, (void*)"thread-2");

    sleep(3); // 确保线程已启动
    while (true) {
        pthread_cond_broadcast(&cond); // 唤醒所有线程
        sleep(1);
    }

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return 0;
}

运行结果:两个线程每隔1秒被唤醒并执行打印操作。

2.4 关键问题:为什么pthread_cond_wait需要互斥量?

  1. 条件依赖共享资源状态,互斥量保护共享资源的读写安全。
  2. 解锁和等待必须是原子操作,避免"信号丢失"(解锁后、等待前信号被发送)。
  3. pthread_cond_wait 内部逻辑:先释放互斥量→等待信号→被唤醒后重新获取互斥量。

2.5 条件变量使用规范

cpp 复制代码
// 等待条件(消费者)
pthread_mutex_lock(&mutex);
while (条件为假) { // 用while避免伪唤醒
    pthread_cond_wait(&cond, &mutex);
}
// 处理逻辑
pthread_mutex_unlock(&mutex);

// 发送信号(生产者)
pthread_mutex_lock(&mutex);
设置条件为真;
pthread_cond_signal(&cond); // 或broadcast
pthread_mutex_unlock(&mutex);

2.6 条件变量封装

cpp 复制代码
// Cond.hpp
#pragma once
#include <pthread.h>
#include "Lock.hpp"

namespace CondModule {
    using namespace LockModule;

    class Cond {
    public:
        Cond() { pthread_cond_init(&_cond, nullptr); }
        ~Cond() { pthread_cond_destroy(&_cond); }

        void Wait(Mutex &mutex) {
            pthread_cond_wait(&_cond, mutex.GetMutexOriginal());
        }
        void Notify() { pthread_cond_signal(&_cond); }
        void NotifyAll() { pthread_cond_broadcast(&_cond); }

    private:
        pthread_cond_t _cond;
    };
}

三、生产者消费者模型:同步与互斥的综合应用

3.1 模型核心思想(321原则)

  • 3种关系:生产者与生产者(互斥)、消费者与消费者(互斥)、生产者与消费者(同步+互斥)。
  • 2个角色:生产者(生产数据)、消费者(处理数据)。
  • 1个容器:阻塞队列(缓冲区),解耦生产者和消费者。

3.2 模型优势

  1. 解耦:生产者和消费者无需直接通信,通过队列间接交互。
  2. 支持并发:生产者和消费者可独立并发执行。
  3. 平衡负载:缓冲队列缓解生产者和消费者处理速度不匹配的问题。

3.3 基于阻塞队列的实现

3.3.1 阻塞队列封装(BlockQueue.hpp)
cpp 复制代码
#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__
#include <iostream>
#include <queue>
#include <pthread.h>

template <typename T>
class BlockQueue {
private:
    bool IsFull() { return _block_queue.size() == _cap; }
    bool IsEmpty() { return _block_queue.empty(); }

public:
    BlockQueue(int cap) : _cap(cap), _productor_wait_num(0), _consumer_wait_num(0) {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_product_cond, nullptr);
        pthread_cond_init(&_consum_cond, nullptr);
    }

    ~BlockQueue() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_product_cond);
        pthread_cond_destroy(&_consum_cond);
    }

    // 生产者入队
    void Enqueue(T &in) {
        pthread_mutex_lock(&_mutex);
        while (IsFull()) { // 队列满则等待
            _productor_wait_num++;
            pthread_cond_wait(&_product_cond, &_mutex);
            _productor_wait_num--;
        }
        _block_queue.push(in);
        if (_consumer_wait_num > 0) {
            pthread_cond_signal(&_consum_cond); // 通知消费者
        }
        pthread_mutex_unlock(&_mutex);
    }

    // 消费者出队
    void Pop(T *out) {
        pthread_mutex_lock(&_mutex);
        while (IsEmpty()) { // 队列空则等待
            _consumer_wait_num++;
            pthread_cond_wait(&_consum_cond, &_mutex);
            _consumer_wait_num--;
        }
        *out = _block_queue.front();
        _block_queue.pop();
        if (_productor_wait_num > 0) {
            pthread_cond_signal(&_product_cond); // 通知生产者
        }
        pthread_mutex_unlock(&_mutex);
    }

private:
    std::queue<T> _block_queue;  // 存储数据的队列
    int _cap;                    // 队列容量上限
    pthread_mutex_t _mutex;      // 保护队列的互斥量
    pthread_cond_t _product_cond;// 生产者条件变量
    pthread_cond_t _consum_cond; // 消费者条件变量
    int _productor_wait_num;     // 等待的生产者数量
    int _consumer_wait_num;      // 等待的消费者数量
};
#endif
3.3.2 任务类型定义
cpp 复制代码
// 任务类型:支持任意可调用对象
using Task = std::function<void()>;

3.4 基于POSIX信号量的环形队列实现

信号量可简化同步逻辑,环形队列适合固定大小的缓冲区场景:

3.4.1 信号量封装(Sem.hpp)
cpp 复制代码
#pragma once
#include <semaphore.h>

class Sem {
public:
    Sem(int n) { sem_init(&_sem, 0, n); }
    ~Sem() { sem_destroy(&_sem); }

    void P() { sem_wait(&_sem); } // 等待(P操作:减1)
    void V() { sem_post(&_sem); } // 发布(V操作:加1)

private:
    sem_t _sem;
};
3.4.2 环形队列实现(RingQueue.hpp)
cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"

template<typename T>
class RingQueue {
private:
    void Lock(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); }
    void Unlock(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); }

public:
    RingQueue(int cap) : _ring_queue(cap), _cap(cap), _room_sem(cap), _data_sem(0),
                         _productor_step(0), _consumer_step(0) {
        pthread_mutex_init(&_productor_mutex, nullptr);
        pthread_mutex_init(&_consumer_mutex, nullptr);
    }

    ~RingQueue() {
        pthread_mutex_destroy(&_productor_mutex);
        pthread_mutex_destroy(&_consumer_mutex);
    }

    // 生产者入队
    void Enqueue(const T &in) {
        _room_sem.P(); // 等待空闲空间
        Lock(_productor_mutex);
        _ring_queue[_productor_step++] = in;
        _productor_step %= _cap; // 环形逻辑
        Unlock(_productor_mutex);
        _data_sem.V(); // 通知有新数据
    }

    // 消费者出队
    void Pop(T *out) {
        _data_sem.P(); // 等待数据
        Lock(_consumer_mutex);
        *out = _ring_queue[_consumer_step++];
        _consumer_step %= _cap; // 环形逻辑
        Unlock(_consumer_mutex);
        _room_sem.V(); // 通知有空闲空间
    }

private:
    std::vector<T> _ring_queue;       // 环形队列容器
    int _cap;                         // 容量上限
    int _productor_step;              // 生产者下标
    int _consumer_step;               // 消费者下标
    Sem _room_sem;                    // 空闲空间信号量
    Sem _data_sem;                    // 已用数据信号量
    pthread_mutex_t _productor_mutex; // 生产者互斥锁
    pthread_mutex_t _consumer_mutex;  // 消费者互斥锁
};

四、线程池设计:高效管理线程资源

4.1 线程池核心概念

线程池是预先创建一定数量的线程,等待处理任务的设计模式,优势如下:

  • 避免频繁创建销毁线程的开销。
  • 控制线程数量,防止资源耗尽。
  • 提高任务响应速度,线程可复用。

4.2 线程池核心组件

  1. 线程队列:存储预先创建的线程。
  2. 任务队列:存储待执行的任务。
  3. 互斥量:保护任务队列的线程安全。
  4. 条件变量:通知线程有新任务到达。
  5. 控制变量:标记线程池是否运行。

4.3 线程池实现(ThreadPool.hpp)

结合日志系统和单例模式,实现生产级线程池:

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <memory>
#include <pthread.h>
#include <functional>
#include "Log.hpp"
#include "Lock.hpp"
#include "Cond.hpp"

using namespace ThreadModule;
using namespace CondModule;
using namespace LockModule;
using namespace LogModule;

const static int gdefaultthreadnum = 10;

template <typename T>
class ThreadPool {
private:
    ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum),
                                                    _waitnum(0), _isrunning(false) {
        LOG(LogLevel::INFO) << "ThreadPool Construct()";
    }

    // 禁用拷贝
    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
    ThreadPool(const ThreadPool<T> &) = delete;

    // 线程执行函数
    void HandlerTask() {
        std::string name = GetThreadNameFromNptl();
        LOG(LogLevel::INFO) << name << " is running...";
        while (true) {
            _mutex.Lock();
            // 等待任务或线程池停止
            while (_task_queue.empty() && _isrunning) {
                _waitnum++;
                _cond.Wait(_mutex);
                _waitnum--;
            }
            // 线程池停止且任务队列为空,退出线程
            if (_task_queue.empty() && !_isrunning) {
                _mutex.Unlock();
                break;
            }
            // 取出任务并执行
            T t = _task_queue.front();
            _task_queue.pop();
            _mutex.Unlock();

            LOG(LogLevel::DEBUG) << name << " get a task";
            t(); // 执行任务
        }
    }

public:
    // 单例模式:双重检查锁定
    static ThreadPool<T> *GetInstance() {
        if (nullptr == _instance) {
            LockGuard lockguard(_lock);
            if (nullptr == _instance) {
                _instance = new ThreadPool<T>();
                _instance->InitThreadPool();
                _instance->Start();
                LOG(LogLevel::DEBUG) << "创建线程池单例";
            }
        }
        LOG(LogLevel::DEBUG) << "获取线程池单例";
        return _instance;
    }

    // 初始化线程池:创建线程(不启动)
    void InitThreadPool() {
        for (int num = 0; num < _threadnum; num++) {
            _threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this));
            LOG(LogLevel::INFO) << "init thread " << _threads.back().Name() << " done";
        }
    }

    // 启动所有线程
    void Start() {
        _isrunning = true;
        for (auto &thread : _threads) {
            thread.Start();
            LOG(LogLevel::INFO) << "start thread " << thread.Name() << " done";
        }
    }

    // 停止线程池
    void Stop() {
        _mutex.Lock();
        _isrunning = false;
        _cond.NotifyAll(); // 唤醒所有等待线程
        _mutex.Unlock();
        LOG(LogLevel::DEBUG) << "线程池退出中...";
    }

    // 等待所有线程退出
    void Wait() {
        for (auto &thread : _threads) {
            thread.Join();
            LOG(LogLevel::INFO) << thread.Name() << " 退出...";
        }
    }

    // 任务入队
    bool Enqueue(const T &t) {
        bool ret = false;
        _mutex.Lock();
        if (_isrunning) {
            _task_queue.push(t);
            if (_waitnum > 0) {
                _cond.Notify(); // 唤醒等待线程
            }
            LOG(LogLevel::DEBUG) << "任务入队列成功";
            ret = true;
        }
        _mutex.Unlock();
        return ret;
    }

private:
    int _threadnum;                  // 线程数量
    std::vector<Thread> _threads;    // 线程队列
    std::queue<T> _task_queue;       // 任务队列
    Mutex _mutex;                    // 保护队列的互斥量
    Cond _cond;                      // 通知条件变量
    int _waitnum;                    // 等待任务的线程数
    bool _isrunning;                 // 线程池运行状态
    static ThreadPool<T> *_instance; // 单例实例
    static Mutex _lock;              // 单例锁
};

// 静态成员初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template <typename T>
Mutex ThreadPool<T>::_lock;

4.4 线程池测试代码

cpp 复制代码
#include <iostream>
#include <functional>
#include <unistd.h>
#include "ThreadPool.hpp"

using task_t = std::function<void()>;

void DownLoad() {
    std::cout << "this is a task" << std::endl;
}

int main() {
    ENABLE_CONSOLE_LOG_STRATEGY(); // 启用控制台日志

    int cnt = 10;
    while (cnt--) {
        ThreadPool<task_t>::GetInstance()->Enqueue(DownLoad);
        sleep(1);
    }

    ThreadPool<task_t>::GetInstance()->Stop();
    sleep(5);
    ThreadPool<task_t>::GetInstance()->Wait();

    return 0;
}

编译命令g++ main.cc -std=c++17 -lpthread

运行结果:线程池创建10个线程,每隔1秒接收一个任务并执行,最后正常退出。

五、线程安全与可重入

5.1 核心定义

  • 线程安全:多个线程并发访问时,程序执行结果一致,无数据竞争。
  • 可重入:同一函数被多个执行流(线程或信号)重复调用时,结果正确。

5.2 常见场景对比

特性 线程安全 可重入
核心关注 多线程访问共享资源的安全性 函数本身的设计健壮性
关系 可重入函数一定是线程安全的 线程安全函数不一定是可重入的
常见不安全情况 未保护的共享变量、静态变量读写 调用malloc/free、静态变量操作
常见安全情况 仅访问局部变量、原子操作 无全局/静态变量、不调用不可重入函数

5.3 实战建议

  1. 避免在函数中使用全局变量和静态变量。
  2. 若必须使用共享资源,需通过互斥量保护。
  3. 可重入函数优先(如仅使用栈空间、参数传递数据)。

六、死锁与避免:多锁场景的坑

6.1 死锁的四个必要条件

  1. 互斥条件:资源只能被一个线程占用。
  2. 请求与保持条件:线程持有资源时,请求其他资源并阻塞。
  3. 不剥夺条件:资源不能被强行剥夺。
  4. 循环等待条件:线程间形成资源等待循环。

6.2 避免死锁的关键方法

  1. 破坏循环等待:统一加锁顺序(如按资源地址排序)。
  2. 资源一次性分配:申请所有需要的资源后再执行。
  3. 超时机制:加锁时设置超时,超时后释放已持有资源。
  4. 避免锁未释放:使用RAII风格锁,确保异常时也能解锁。

示例:统一加锁顺序避免死锁

cpp 复制代码
// 错误:加锁顺序不一致
// 线程A:lock(mtx1) → lock(mtx2)
// 线程B:lock(mtx2) → lock(mtx1)

// 正确:按固定顺序加锁
void func() {
    pthread_mutex_lock(&mtx1); // 先加锁1
    pthread_mutex_lock(&mtx2); // 再加锁2
    // 业务逻辑
    pthread_mutex_unlock(&mtx2);
    pthread_mutex_unlock(&mtx1);
}

七、总结与进阶方向

本文从线程互斥、同步的基础概念出发,逐步深入到生产者消费者模型、线程池设计等实战场景,覆盖了Linux线程编程的核心知识点。关键总结:

  1. 互斥用mutex,解决资源竞争;同步用条件变量/信号量,解决执行顺序。
  2. 生产者消费者模型是同步与互斥的经典应用,解耦线程间交互。
  3. 线程池通过复用线程提高效率,适合高并发短任务场景。
  4. 线程安全和可重入是编写可靠多线程程序的基础。
  5. 死锁避免的核心是破坏四个必要条件中的任意一个。

进阶学习方向

  1. 深入学习红黑树、哈希表等数据结构,理解STL容器的线程安全性。
  2. 研究高级同步机制:读写锁、自旋锁、屏障(barrier)。
  3. 探索分布式场景下的同步方案:分布式锁(如Redis实现)。
  4. 性能优化:锁粒度控制、无锁编程(CAS操作)。
相关推荐
任聪聪2 小时前
Centos平替系统RockyLinux详细安装教程
linux·运维·centos
zjj5873 小时前
ubuntu虚拟内存
linux·运维·ubuntu
阿林学习计算机3 小时前
C++11特性
c++
Elias不吃糖3 小时前
NebulaChat:C++ 高并发聊天室服务端
开发语言·c++·redis·sql·项目文档
帅中的小灰灰3 小时前
C++编程策略设计模式
开发语言·c++·设计模式
*翊墨*3 小时前
达梦数据库Linux安装
linux·数据库·excel
h***38184 小时前
SQL 注入漏洞原理以及修复方法
网络·数据库·sql
瑶总迷弟4 小时前
在centos上基于kubeadm部署单master的k8s集群
linux·kubernetes·centos
是小胡嘛4 小时前
华为云CentOS系统中运行http服务器无响应
linux·服务器·c++·http·centos·华为云