【Linux 应用开发 】Linux 跨进程读写锁全解析:原理、实现与实践

目录标题


在这里插入图片描述

第一章: 引言

1.1 读写锁的基本概念(Basic Concept of Read-Write Locks)

读写锁(Read-Write Locks)是一种同步机制,用于控制对共享资源的访问。在我们探索这个概念之前,不妨思考一下人类在团队工作中的协作方式。正如一个团队中的成员可能需要等待其他成员完成他们的任务才能继续自己的工作一样,多个进程或线程在访问共享资源时也需要相互协调和等待。

在技术层面,读写锁分为两种类型的锁:读锁(Read Lock)和写锁(Write Lock)。读锁是共享的,允许多个读者同时读取数据,类似于一个团队会议中,所有人都可以同时听取信息。相反,写锁是排他的,当一个写操作发生时,其他所有读写操作都必须等待,就像一个团队成员在会议上发表演讲时,其他人需要等待他完成才能发言。

当然可以。读写锁(Read-Write Lock),在 POSIX 线程(pthread)库中通常由 pthread_rwlock_t 类型表示,是一种用于控制对共享资源访问的同步机制。读写锁的主要特点是允许多个读操作同时进行,但写操作是互斥的。下面详细介绍读锁和写锁的规则:

读锁(共享锁)

  1. 允许多个读者:多个线程可以同时获得读锁。这意味着,如果一个共享资源被读锁锁定,其他请求读锁的线程也可以获得读锁并访问该资源。

  2. 读锁对写锁排他:当一个或多个读锁被持有时,写锁的请求会被阻塞,直到所有读锁都被释放。这防止了写操作在读操作进行时发生。

  3. 适用场景:读锁适用于读操作远多于写操作的场景,可以提高程序的并发性能。

写锁(排他锁)

  1. 只允许一个写者:写锁一次只能被一个线程持有。如果一个共享资源被写锁锁定,其他线程的写锁请求将被阻塞,直到当前写锁被释放。

  2. 写锁对读锁排他:当写锁被持有时,读锁的请求也会被阻塞,直到写锁被释放。这确保了写操作不会在有读操作的情况下进行。

  3. 写锁优先:为了防止写饥饿(写操作长时间等待),许多实现会在有写锁等待时,优先考虑写锁的请求,即使有新的读锁请求也会等待。

  4. 适用场景:写锁适用于需要修改共享资源的场景,确保了数据修改的一致性和完整性。

注意事项

  • 锁升级和降级:通常不允许直接从读锁升级为写锁,或者从写锁降级为读锁,因为这容易导致死锁。

  • 死锁风险:在使用读写锁时,需要特别注意避免死锁。例如,两个线程同时试图从读锁升级到写锁可能会导致死锁。

  • 公平性问题:读写锁的实现可能在公平性上有所不同,有的实现可能会偏向读锁,有的可能偏向写锁,这可能会导致“写饥饿”或“读饥饿”。

读写锁是一种强大的同步机制,可以在多线程程序中有效地管理对共享资源的访问,特别是在读多写少的场景中。然而,正确和高效地使用读写锁需要仔细的设计和对潜在问题的深入理解。

1.2 跨进程同步的重要性(Importance of Cross-Process Synchronization)

跨进程同步是计算机程序中的一个关键概念,它类似于在现实生活中,不同部门或团队之间为了共同的目标而进行的协作。每个进程就像是一个独立的团队,它们有自己的任务和资源,但在访问共享资源时,就需要协调一致的行动。

这种同步不仅关乎效率,更关乎安全和数据的完整性。想象一下,如果在一个工程项目中,不同的团队成员在没有任何协调的情况下同时操作同一部分,这将可能导致混乱甚至灾难性的后果。同样,在软件系统中,如果多个进程无序地访问和修改共享数据,那么数据损坏和不一致的情况就可能发生。

因此,跨进程的读写锁不仅是一项技术挑战,更是对于协调和合作能力的考验。它们在保证数据安全和提高系统效率方面发挥着至关重要的作用,就像一个高效运作的团队一样,既能确保每个成员的工作顺利进行,又能保持整个团队的协调和统一。

在下一章中,我们将深入探讨读写锁的工作原理,并通过代码示例展示它们在实际编程中的应用。这不仅是对技术细节的讲解,更是对协调和合作这一普遍原则的应用展示。

第二章: 读写锁的工作原理

2.1 读写锁的定义(Definition of Read-Write Locks)

读写锁(Read-Write Locks)是一种同步机制,用来控制对共享资源的访问,特别是在数据读取操作远多于写入操作的场景中。这种锁机制的设计思想,源于生活中的一个简单现象:在阅读图书馆的书籍时,人们可以共享阅读同一本书,但如果有人需要对书籍进行编辑,那么这本书在编辑过程中就不能被其他人阅读或编辑。

在技术层面,读写锁提供了两种类型的锁:

  • 读锁(Read Lock): 允许多个线程同时进行读操作。
  • 写锁(Write Lock): 仅允许一个线程进行写操作,同时阻止任何读操作。

2.2 读写锁与互斥锁的比较(Comparison with Mutexes)

读写锁和互斥锁(Mutexes)都是用于管理对共享资源的访问,但它们在行为和使用场景上有显著的差异。下面是一个简单的对比:

特点/锁类型 读写锁 互斥锁
并发读取 允许多个线程同时读取 不允许
写操作排他性 在写操作时,阻止任何其他读写操作 任何时候只允许一个线程访问资源
使用场景 读多写少的应用场景 读写操作差异不大的应用场景

示例代码

// 互斥锁示例
pthread_mutex_t mutex;
pthread_mutex_lock(&mutex);
// 执行操作...
pthread_mutex_unlock(&mutex);

// 读写锁示例
pthread_rwlock_t rwlock;
pthread_rwlock_rdlock(&rwlock); // 读锁
// 执行读操作...
pthread_rwlock_unlock(&rwlock);

pthread_rwlock_wrlock(&rwlock); // 写锁
// 执行写操作...
pthread_rwlock_unlock(&rwlock);

通过这个比较,我们可以看出,读写锁在处理读多写少的情况时更高效,因为它允许多个读操作并行进行,而不是像互斥锁那样,即使是读操作也需要排队等候。

在接下来的章节中,我们将深入探讨如何在跨进程环境中实现和使用读写锁,以及如何通过代码示例将这些理论应用于实践中。通过这些内容,读者不仅能够了解读写锁的技术细节,还能领会到在多线程编程中协调与合作的重要性。

第三章: 跨进程读写锁的实现

3.1 共享内存概述(Overview of Shared Memory)

共享内存(Shared Memory)是一种在多个进程间共享和传递数据的有效方式。它就像一个公共的数据存储区域,任何知道这个区域存在的进程都可以访问和修改其中的数据。这类似于一个家庭的公共留言板,家庭成员可以在上面写下信息,供其他成员阅读和回复。

在技术上,共享内存是一段可以被多个进程访问的物理内存。它是跨进程通信(Inter-Process Communication, IPC)中最快的一种方式,因为它避免了数据的复制,直接在进程间进行数据交换。

3.2 在共享内存中创建读写锁(Creating Read-Write Locks in Shared Memory)

要在共享内存中实现跨进程的读写锁,关键是将锁本身放置在共享内存区域中。这样,所有映射了该共享内存的进程都可以访问和操作这个锁。

实现步骤

  1. 创建共享内存区域
    使用系统调用(如 shm_open 在 POSIX 系统中)创建共享内存。

  2. 映射共享内存到进程空间
    使用 mmap 将共享内存映射到进程的地址空间中。

  3. 在共享内存中初始化读写锁
    使用 pthread_rwlock_init 并设置锁属性为 PTHREAD_PROCESS_SHARED,这使得锁可以在多个进程间共享。

示例代码

int fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
ftruncate(fd, sizeof(pthread_rwlock_t));
pthread_rwlock_t* rwlock = mmap(NULL, sizeof(pthread_rwlock_t), 
                                PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_rwlock_init(rwlock, &attr);

3.3 设置锁属性为 PTHREAD_PROCESS_SHARED(Setting the PTHREAD_PROCESS_SHARED Attribute)

为了使读写锁在多个进程间工作,锁的属性需要被设置为 PTHREAD_PROCESS_SHARED。这个属性告诉操作系统,锁将被多个进程共享,而不是仅限于创建它的那个进程。

在实际应用中,这就像为一个工作团队制定规则,确保团队成员在共享资源时遵循相同的协议。这种协作方式不仅提高了资源利用效率,还保持了团队工作的和谐。

通过本章的学习,读者应该能够理解如何在共享内存中实现跨进程的读写锁,并理解设置合适的锁属性对于确保多个进程能够正确同步访问共享资源的重要性。下一章将讨论如何操作这些锁以及在实际编程中应用这些概念。

第四章: 读写锁的操作流程

4.1 获取和释放读锁(Acquiring and Releasing Read Locks)

读锁允许多个线程或进程同时读取共享数据,但阻止任何写入操作。这类似于在一个办公室中,许多人可以同时查阅公共文件,但如果有人需要更新这些文件,则必须等待所有的查阅活动结束。

获取读锁的步骤

  1. 调用 pthread_rwlock_rdlock:
    使用 pthread_rwlock_rdlock 函数请求读锁。

  2. 执行读取操作:
    在锁定期间,执行对共享资源的读取操作。

  3. 释放读锁:
    使用 pthread_rwlock_unlock 函数释放锁。

示例代码

pthread_rwlock_rdlock(rwlock);
// 执行读取操作...
pthread_rwlock_unlock(rwlock);

4.2 获取和释放写锁(Acquiring and Releasing Write Locks)

写锁提供独占访问权,确保在修改共享资源时,没有其他读写操作发生。这就像在一个办公环境中,进行文件的大规模更新,需要暂时禁止其他人访问或修改文件,以保持数据的一致性和完整性。

获取写锁的步骤

  1. 调用 pthread_rwlock_wrlock:
    使用 pthread_rwlock_wrlock 函数请求写锁。

  2. 执行写入操作:
    在锁定期间,执行对共享资源的修改操作。

  3. 释放写锁:
    使用 pthread_rwlock_unlock 函数释放锁。

示例代码

pthread_rwlock_wrlock(rwlock);
// 执行写入操作...
pthread_rwlock_unlock(rwlock);

通过本章的介绍,读者应该能够理解如何在跨进程环境中正确操作读写锁。这种能力类似于在一个多部门协作的大型项目中,能够理解和遵守各种规则和流程,以确保项目的顺利进行。在下一章中,我们将探讨读写锁在实际应用场景中的使用,特别是在需要高效并发处理的环境中。

第五章: 跨进程读写锁的应用场景

5.1 保证数据一致性(Ensuring Data Consistency)

在多进程环境中,数据一致性是一个核心关注点。跨进程的读写锁在这方面发挥着重要作用。它们保证当一个进程正在写入数据时,其他进程不能读取或写入相同的数据。这就好比在一场接力赛中,只有当一位选手完成他的部分并传递接力棒后,下一位选手才能开始奔跑。

实际应用示例

假设我们有一个共享的数据结构,比如一个简单的字符串,我们将展示如何在不同进程中安全地读取和修改这个字符串。

#include <fcntl.h>
#include <sys/mman.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

// 定义共享内存中的结构
struct SharedData {
    
    
    pthread_rwlock_t rwlock;
    char data[100];  // 示例数据
};

int main(int argc, char *argv[]) {
    
    
    // 创建或打开共享内存
    int fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
    ftruncate(fd, sizeof(SharedData));

    // 映射共享内存
    SharedData* sharedData = static_cast<SharedData*>(mmap(NULL, sizeof(SharedData), 
                                                           PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));

    // 初始化读写锁
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_rwlock_init(&sharedData->rwlock, &attr);

    // 示例操作
    if (argc > 1 && strcmp(argv[1], "write") == 0) {
    
    
        // 获取写锁
        pthread_rwlock_wrlock(&sharedData->rwlock);
        // 修改数据
        strncpy(sharedData->data, "Hello from writer", sizeof(sharedData->data));
        pthread_rwlock_unlock(&sharedData->rwlock);
        std::cout << "Data written." << std::endl;
    } else {
    
    
        // 获取读锁
        pthread_rwlock_rdlock(&sharedData->rwlock);
        // 读取数据
        std::cout << "Data read: " << sharedData->data << std::endl;
        pthread_rwlock_unlock(&sharedData->rwlock);
    }

    // 清理
    pthread_rwlock_destroy(&sharedData->rwlock);
    munmap(sharedData, sizeof(SharedData));
    close(fd);

    return 0;
}

使用说明

  1. 运行程序时,可以通过命令行参数指定是读操作还是写操作。例如,使用 ./program write 来写入数据,使用 ./program 来读取数据。

  2. 这个示例中的共享内存名为 "/my_shared_memory"。在运行示例之前,确保没有其他程序使用相同的共享内存名。

  3. 示例中的数据仅为一个字符串。在实际应用中,这可以是任何类型的共享数据。

  4. 请注意,共享内存和锁的初始化应该只在第一次创建共享内存时进行。在实际应用中,可能需要额外的逻辑来处理这种情况。

此示例展示了如何在多个进程之间使用读写锁来保护对共享内存中数据的访问,确保数据在读取和写入时的一致性。

5.2 提高并发性能(Enhancing Concurrent Performance)

读写锁特别适用于读操作远多于写操作的场景。在这种情况下,读写锁允许多个读操作同时进行,从而大大提高了系统的并发性能。这就像在一个图书馆里,虽然每个人都可以自由地阅读书籍,但当图书管理员需要更新某本书的内容时,读者们需要暂时等待。

下面,我将提供一个具体的Linux C++代码示例,演示如何在多线程环境中使用读写锁来提高并发读取性能。这个示例将模拟一个简单的场景,其中多个线程同时读取共享数据,而另外一些线程负责写入数据。

示例代码

首先,你需要包含必要的头文件,并定义共享的数据结构和读写锁:

#include <pthread.h>
#include <iostream>
#include <vector>
#include <unistd.h>

pthread_rwlock_t rwlock;

// 假设的共享数据
int shared_data = 0;

然后,创建读操作和写操作的函数:

void* read_function(void* arg) {
    
    
    while (true) {
    
    
        pthread_rwlock_rdlock(&rwlock); // 获取读锁
        std::cout << "Read Thread ID: " << pthread_self() << " - Shared Data: " << shared_data << std::endl;
        pthread_rwlock_unlock(&rwlock); // 释放读锁
        sleep(1); // 模拟读取操作的耗时
    }
    return NULL;
}

void* write_function(void* arg) {
    
    
    while (true) {
    
    
        pthread_rwlock_wrlock(&rwlock); // 获取写锁
        ++shared_data;
        std::cout << "Write Thread ID: " << pthread_self() << " - Updated Data: " << shared_data << std::endl;
        pthread_rwlock_unlock(&rwlock); // 释放写锁
        sleep(2); // 模拟写入操作的耗时
    }
    return NULL;
}

最后,创建多个线程来执行读写操作,并初始化读写锁:

int main() {
    
    
    pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁

    const int NUM_THREADS = 5;
    pthread_t read_threads[NUM_THREADS];
    pthread_t write_thread;

    // 创建读线程
    for (int i = 0; i < NUM_THREADS; ++i) {
    
    
        pthread_create(&read_threads[i], NULL, read_function, NULL);
    }

    // 创建写线程
    pthread_create(&write_thread, NULL, write_function, NULL);

    // 等待线程结束 (在实际应用中你可能会有更优雅的退出方式)
    for (int i = 0; i < NUM_THREADS; ++i) {
    
    
        pthread_join(read_threads[i], NULL);
    }
    pthread_join(write_thread, NULL);

    pthread_rwlock_destroy(&rwlock); // 销毁读写锁
    return 0;
}

注意事项

  • 这个示例只是为了演示读写锁的用法,在实际应用中,你可能需要更复杂的逻辑来处理线程的创建、同步和终止。
  • 在使用读写锁时,要确保正确地管理锁的获取和释放,避免死锁和资源泄露。
  • 示例中的 sleep 调用是为了模拟实际操作中的耗时,实际应用中应根据实际情况进行调整。

通过这个示例,你可以看到在多线程环境下,读写锁是如何用来同时提高读操作的并发性能,同时确保写操作的独占性和数据的一致性。

5.3 读写锁在消息传递中的应用(Application in Message Passing)

跨进程的读写锁也可以用于消息传递系统,确保消息的完整性和一致性。这类似于在一个忙碌的邮局中,工作人员需要在处理大量邮件的同时,保证每封邮件都能准确无误地送达。

我们将通过一个具体的示例来展示如何在Linux环境下使用C++实现跨进程读写锁来同步消息传递。此示例将模拟一个简单的消息队列,其中一个进程写入消息,而另一个进程读取消息。

代码示例

首先,我们需要定义一个共享内存结构,用于存放读写锁和消息数据:

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <iostream>
#include <vector>

#define SHM_NAME "/my_shared_memory"
#define MAX_MESSAGE_SIZE 1024

struct SharedMemory {
    
    
    pthread_rwlock_t rwlock;
    char message[MAX_MESSAGE_SIZE];
};

void initializeSharedMemory(int shm_fd, SharedMemory*& shared_memory) {
    
    
    ftruncate(shm_fd, sizeof(SharedMemory));
    shared_memory = (SharedMemory*)mmap(NULL, sizeof(SharedMemory), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);

    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_rwlock_init(&shared_memory->rwlock, &attr);
}

void writeMessage(SharedMemory* shared_memory, const std::string& message) {
    
    
    pthread_rwlock_wrlock(&shared_memory->rwlock);
    strncpy(shared_memory->message, message.c_str(), MAX_MESSAGE_SIZE);
    pthread_rwlock_unlock(&shared_memory->rwlock);
}

std::string readMessage(SharedMemory* shared_memory) {
    
    
    pthread_rwlock_rdlock(&shared_memory->rwlock);
    std::string message(shared_memory->message);
    pthread_rwlock_unlock(&shared_memory->rwlock);
    return message;
}

int main() {
    
    
    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    SharedMemory* shared_memory = nullptr;

    initializeSharedMemory(shm_fd, shared_memory);

    // 示例:写入消息
    writeMessage(shared_memory, "Hello, World!");

    // 示例:读取消息
    std::string message = readMessage(shared_memory);
    std::cout << "Received message: " << message << std::endl;

    // 清理资源
    pthread_rwlock_destroy(&shared_memory->rwlock);
    munmap(shared_memory, sizeof(SharedMemory));
    close(shm_fd);
    shm_unlink(SHM_NAME);

    return 0;
}

解析

  • 我们定义了一个 SharedMemory 结构,包含一个 pthread_rwlock_t 类型的读写锁和一个存储消息的字符数组。
  • initializeSharedMemory 函数用于初始化共享内存和读写锁。
  • writeMessage 函数演示了如何获取写锁,写入消息到共享内存,然后释放锁。
  • readMessage 函数演示了如何获取读锁,从共享内存中读取消息,然后释放锁。
  • main 函数中,我们创建共享内存,写入一条消息,然后读取这条消息。

这个示例提供了一个基本框架,展示了在Linux下使用C++进行跨进程读写锁同步的方法。当然,实际应用中可能需要更复杂的错误处理和同步逻辑。

第六章: 面临的挑战与解决方案

6.1 避免死锁(Avoiding Deadlocks)

在使用读写锁时,死锁是一个常见的挑战。死锁发生在多个进程或线程互相等待对方释放锁的情况下,导致系统陷入停滞。这类似于城市交通中的四车道交叉口,如果每辆车都等待对面的车先行,那么交通就会陷入僵局。

解决方案

  • 锁获取顺序: 确保所有进程或线程以相同的顺序获取锁。
  • 锁超时: 使用带有超时的锁获取方法,如 pthread_rwlock_timedrdlockpthread_rwlock_timedwrlock
  • 资源分配策略: 采用预防策略,比如按需分配资源,避免一次性请求所有资源。

6.2 处理锁的重复初始化问题(Handling Repeated Initialization of Locks)

在共享内存环境中,锁的重复初始化可能导致不可预测的行为。这就像在建筑一个大楼时,反复更改建筑的基础,可能导致结构不稳定。

解决方案

  • 状态标记: 在共享内存中使用一个标记来指示锁是否已经被初始化。
  • 进程间协调: 通过进程间通信确保只有一个进程负责初始化锁。

6.3 写饥饿问题及其预防(Write Starvation and Its Prevention)

在读多写少的场景中,写操作可能会因为持续的读操作而长时间等待,这被称为写饥饿。这类似于一个讨论会议,如果持续有人发言,那么想要进行演讲的嘉宾可能永远没有机会。

解决方案

  • 公平的锁策略: 使用更公平的锁策略,确保写锁请求不会被长时间忽视。
  • 读锁的有限持有时间: 限制读锁的持有时间,给予写操作更多的机会。

通过本章的学习,读者应该能够理解在使用读写锁时可能遇到的挑战,以及如何通过合理的设计和策略来避免这些问题。这些挑战和解决方案不仅适用于软件开发,也适用于日常生活中的许多情况,反映了在复杂系统中实现协调和效率的普遍原则。在下一章中,我们将总结本文的内容,并对读写锁的应用进行深入的思考。

第七章: 结论

7.1 读写锁的优势和局限性(Advantages and Limitations of Read-Write Locks)

读写锁作为一种强大的同步机制,它的优势在于提高了读多写少场景下的并发性能,允许多个读操作同时进行,而写操作则保持了对资源的独占访问。这类似于一个团队在处理多个并行任务时的高效协作,其中某些任务需要集中注意力,而其他任务则可以同时进行。

然而,读写锁也有其局限性。在写锁频繁请求的场景下,它们可能导致性能瓶颈或写饥饿。这就像一个团队在需要频繁做出关键决策时,过多的讨论可能会拖慢决策过程。

7.2 未来的发展趋势(Future Trends)

随着并发编程的不断发展,读写锁的概念和实现也在不断进化。未来可能会有更高效的同步机制出现,以更好地处理复杂的并发场景。同时,软件工程的不断进步也可能带来更智能的锁管理策略,自动优化锁的使用,减少开发者的负担。

在探索未来趋势的同时,我们也应该记住,技术的发展总是为了更好地服务于人类的需求。在这个过程中,理解基本原则和概念的重要性是不变的。读写锁的原理,以及它在多线程和多进程编程中的应用,是这些原则的完美体现。

通过本文的学习,我们不仅深入了解了跨进程读写锁的技术细节,还领会到了在多线程环境中协调与合作的重要性。这些知识和经验对于任何希望在并发编程领域取得成功的开发者来说都是宝贵的财富。


在这篇博客中,我们从读写锁的基本概念出发,深入探讨了它们在跨进程环境中的实现和应用,分析了使用读写锁时可能面临的挑战及其解决方案,并最终对读写锁的优势、局限性以及未来趋势进行了总结。希望本文能为您在并发编程的旅程中提供指导和启发。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_21438461/article/details/135361103