写时复制原理分析

本文介绍了写时复制页面保护机制,这是一种内存管理技术,能够帮助操作系统节省内存资源。当多个进程共享同一段内存时,仅在某个进程尝试修改内存内容时才会创建该内存的副本。此外,还详细说明了写时复制机制在调试器断点支持和POSIX子系统中的应用。
写时复制页面保护机制是一种优化,内存管理器利用它可以节约内存。

当进程为一个包含读/写页面的内存区对象映射了一份写时视图,而并非在映射该视图时创建一份进程私有的拷贝(Hewlett  Packard  OpenVMS操作系统就是这样做的)时,内存管理器将页面拷贝的动作推迟到页面被写入数据的时候。所有现代的UNIX系统也都使用了这项技术。如:2个进程正在共享3个页面,每个页面都被标记为写时复制,但是这2个进程都不打算修改页面上的任何数据。

如果这2个进程中任何一个线程对一个页面执行了写操作,则产生一个内存管理错误。内存管理器看到,此写操作作用在一个写时复制的页面上,所以,它不是将此错误报告为访问违例,而是在物理内存中分配一个新的读/写页面,并且把原始页面中的内容拷贝到新的页面中。同时也更新一下该进程中对应的页面映射信息,使它指向新的页面位置,然后解除异常,从而使得刚才产生错误的那条指令得以重新执行。这一次,写操作成功了。但是,新拷贝的页面现在对于执行写操作的那个进程来说是私有的,对于其它仍然在共享这一写时复制页面的进程来说,它是不可见的。每个往共享页面中写入数据的进程都将获得它自己的私有拷贝。

写时复制的一个应用是:在调试器中实现断点支持。例如:在默认情况下,代码页面在起始时都是只能执行的。然而,如果一个程序员在调试一个程序时设置了一个断点,则调试器必须在代码中加入一条断点指令。它是这样做的:首先将该页面的保护模式改变为PAGE_EXECUTE_READWRITE,然后改变指令流。因为代码页面是所映射的内存区的一部分,所以内存管理器为设置了断点的那个进程创建一份私有拷贝,同时其它进程仍然使用原先未经修改的代码页面。

写时复制是“延迟计算(lazy  evaluation)”这一计算技术(evaluation  technique)的一个例子,内存管理器广泛地使用了延迟计算的技术。延迟计算使得只有当绝对需要时才执行一个昂贵的操作--如果该操作从来也不需要的话,则它不会浪费任何一点时间。

POSIX  子系统利用写时复制来实现  fork 函数,当一个UNIX 应用程序调用fork 函数来创建另一个进程时,新进程所做的第一件事是调用exec 函数,用一个可执行程序来重新初始化它的地址空间。 在fork中,新进程不是拷贝整个地址空间,而是通过将页面标记为写时复制的方式,与父进程共享这些页面。如果子进程在这些页面中写入数据了,则生成一份进程私有的拷贝。如果没有写操作,则2个进程继续共享页面,不会执行拷贝动作。不管怎么样,内存管理器只拷贝一个进程试图要写入数据的那些页面,而不是整个地址空间。

想了解写时复制错误的比率,查看性能计数器:Memory: Write  Copies/Sec   。
<think>我们使用复制(Copy-On-Write)技术来实现线程安全的容器。复制的核心思想是:当需要修改容器内容,不直接修改原容器,而是先复制一份副本,在副本上进行修改,修改完成后再将容器的指针(或引用)指向新的副本。这样,读取线程始终访问旧的容器,不会受到入线程修改的影响,从而实现无锁读取。 具体步骤: 1. 容器内部维护一个指向实际数据结构的指针(或智能指针)。 2. 读取操作:直接读取指针指向的数据(因为指针本身是原子的,读取指针是安全的),读取过程中数据不会被修改(因为入操作在副本上进行)。 3. 入操作: a. 复制当前数据(深拷贝)得到一个新副本。 b. 在新副本上进行修改(如添加元素)。 c. 使用原子操作将内部指针指向新的副本(此操作是原子的,因此线程安全)。 4. 旧数据的回收:由于读取操作可能还在使用旧数据,因此需要谨慎管理旧数据的生命周期。我们可以使用引用计数(如std::shared_ptr)来确保当所有读取线程都不再使用旧数据才释放。 优点: - 读取操作完全无锁,因此读取性能非常高。 - 入操作不会阻塞读取操作(读取操作始终访问旧数据,入操作在副本上进行)。 缺点: - 入操作需要复制整个容器,因此入性能较低,特别是容器较大。 - 内存占用可能较高,因为会产生完整副本。 适用场景:读多少,且容器规模不是特别大的情况。 下面我们用C++实现一个线程安全的复制(Copy-On-Write)容器,以std::vector为基础: ```cpp #include <vector> #include <memory> #include <mutex> #include <atomic> #include <iostream> #include <thread> template <typename T> class CopyOnWriteVector { private: // 使用shared_ptr管理vector,以便引用计数 std::shared_ptr<std::vector<T>> data_ptr; // 入操作需要互斥,因为可能有多个入线程同触发复制(但通常操作少,所以竞争小) mutable std::mutex write_mutex; public: CopyOnWriteVector() : data_ptr(std::make_shared<std::vector<T>>()) {} // 读取操作:返回当前数据的共享指针(读取线程可以安全使用该指针指向的数据) std::shared_ptr<std::vector<T>> get_data() const { // 原子操作,读取指针(不需要锁) return std::atomic_load(&data_ptr); } // 入操作:添加一个元素 void push_back(const T& value) { // 入操作需要互斥,防止多个入线程同修改 std::lock_guard<std::mutex> lock(write_mutex); // 复制当前数据(注意:这里复制的是指针指向的vector,而不是指针本身) std::shared_ptr<std::vector<T>> new_data = std::make_shared<std::vector<T>>(*data_ptr); // 在副本上修改 new_data->push_back(value); // 原子地更新指针(将原指针替换为新的指针) std::atomic_store(&data_ptr, new_data); } // 其他入操作(如pop_back, clear等)类似,先复制再修改,最后更新指针 }; // 测试函数 void test_copy_on_write() { CopyOnWriteVector<int> cow_vec; // 入线程 auto writer = [&]() { for (int i = 0; i < 100; ++i) { cow_vec.push_back(i); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } }; // 读取线程 auto reader = [&](int id) { for (int i = 0; i < 50; ++i) { // 获取当前数据的快照 std::shared_ptr<std::vector<int>> data_snapshot = cow_vec.get_data(); std::cout << "Reader " << id << ": ["; for (const auto& item : *data_snapshot) { std::cout << item << " "; } std::cout << "]" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } }; std::thread t1(writer); std::thread t2(reader, 1); std::thread t3(reader, 2); t1.join(); t2.join(); t3.join(); } int main() { test_copy_on_write(); return 0; } ``` ### 关键点解释: 1. **读取操作**:通过`get_data()`方法,我们使用`std::atomic_load`来原子地读取`shared_ptr`,这样我们获得的是当前数据指针的一个副本(会增加引用计数)。读取线程可以安全地遍历这个指针指向的vector,因为入操作不会修改这个vector(只修改副本),所以不会出现迭代器失效等问题。 2. **入操作**:在`push_back`中,我们先加锁(防止多个入线程同复制),然后复制整个vector(深拷贝),在副本上修改,最后使用`std::atomic_store`原子地更新数据指针。这样,后续的读取操作将看到新的数据。 3. **内存管理**:使用`std::shared_ptr`管理vector,当没有读取线程再持有旧数据的指针,旧数据会被自动释放。 4. **性能**:读取操作非常快(无锁,仅原子操作)。入操作由于需要复制整个容器,所以代价较高。因此,这种容器适合读多少,且数据量不是特别大的场景。 ### 注意事项: - 如果入操作非常频繁,或者容器非常大,复制整个容器会带来较大的性能开销和内存占用。此,可以考虑其他数据结构(如无锁队列)或优化复制策略(如使用持久化数据结构,只复制修改的部分)。 - 该实现只保证了容器结构的安全,如果容器中存储的是指针,那么指针指向的内容需要用户自行保证线程安全。 ### 扩展:优化入操作 如果入操作频繁,我们可以考虑批量入,或者使用更高效的数据结构(如链表,这样复制只需要复制指针)。另外,也可以考虑使用引用计数的复制(当需要修改,如果引用计数为1,说明没有其他线程在读取,可以直接修改而不复制)。 ### 相关问题 1. 复制容器在入操作频繁如何优化性能? 2. 除了复制,还有哪些无锁数据结构适用于多读多场景? 3. 复制容器在内存管理方面需要注意哪些问题?</think>### 复制(COW)实现线程安全数据结构 复制(Copy-On-Write)是一种高效的线程安全技术,核心思想是**在修改数据创建副本**,确保读取线程始终访问稳定的数据快照。这种方法特别适合读多少的场景,能实现无锁读取,同保证数据一致性[^1]。 #### 实现原理 1. **读取操作**:直接访问当前数据指针,无需加锁 2. **入操作**: - 创建数据副本 - 在副本上执行修改 - 原子操作切换数据指针 3. **内存管理**:使用智能指针自动回收旧数据 #### C++实现示例 ```cpp #include <memory> #include <vector> #include <atomic> #include <thread> #include <iostream> template <typename T> class CowVector { private: // 使用shared_ptr管理数据,支持引用计数 std::shared_ptr<std::vector<T>> data_ptr; // 入操作的互斥锁(仅保护入过程) std::mutex write_mutex; public: CowVector() : data_ptr(std::make_shared<std::vector<T>>()) {} // 读取操作:无锁访问 std::shared_ptr<std::vector<T>> get_data() const { return std::atomic_load(&data_ptr); // 原子操作获取指针 } // 入操作:复制 void push_back(const T& value) { std::lock_guard<std::mutex> lock(write_mutex); // 保护复制过程 // 1. 创建副本 auto new_data = std::make_shared<std::vector<T>>(*data_ptr); // 2. 修改副本 new_data->push_back(value); // 3. 原子切换指针 std::atomic_store(&data_ptr, new_data); } // 批量入 template <typename Iter> void append(Iter begin, Iter end) { std::lock_guard<std::mutex> lock(write_mutex); auto new_data = std::make_shared<std::vector<T>>(*data_ptr); new_data->insert(new_data->end(), begin, end); std::atomic_store(&data_ptr, new_data); } }; // 测试函数 void test_cow_vector() { CowVector<int> cow_vec; const int READERS = 5; const int WRITERS = 2; // 入线程函数 auto writer = [&](int id) { for (int i = 0; i < 100; ++i) { cow_vec.push_back(id * 1000 + i); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } }; // 读取线程函数 auto reader = [&](int id) { for (int i = 0; i < 50; ++i) { // 无锁获取数据快照 auto data_snapshot = cow_vec.get_data(); std::cout << "Reader " << id << " snapshot: "; for (const auto& val : *data_snapshot) { std::cout << val << " "; } std::cout << "\n"; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } }; std::vector<std::thread> threads; // 创建入线程 for (int i = 0; i < WRITERS; ++i) { threads.emplace_back(writer, i); } // 创建读取线程 for (int i = 0; i < READERS; ++i) { threads.emplace_back(reader, i); } // 等待所有线程 for (auto& t : threads) t.join(); } int main() { test_cow_vector(); return 0; } ``` ### 关键技术解析 1. **原子指针操作**: ```cpp std::atomic_load(&data_ptr); // 安全获取指针 std::atomic_store(&data_ptr, new_data); // 原子更新指针 ``` 确保指针更新是原子的,防止读取线程看到中间状态[^2]。 2. **智能指针管理**: ```cpp std::shared_ptr<std::vector<T>> data_ptr; ``` 自动处理内存回收,当没有读取线程使用旧数据自动释放。 3. **优化**: - 批量入操作减少复制次数 - 细粒度锁仅保护复制过程(约10-20行代码) 4. **无锁读取**: ```cpp auto data_snapshot = cow_vec.get_data(); ``` 读取操作完全无锁,性能接近普通vector[^1]。 ### 性能特点 | 操作类型 | 间复杂度 | 线程安全机制 | |---------|-----------|------------| | 读取 | $O(1)$ | 原子指针操作 | | 单次入 | $O(n)$ | 复制 + 互斥锁 | | 批量入 | $O(n+m)$ | 单次复制完成多操作 | ### 适用场景 1. 配置信息等读多少的数据 2. 实数据监控系统 3. 高频读取的缓存系统 4. 需要数据快照的分析场景 > **注意事项**:当入频率超过读取频率或数据量极大,COW可能导致内存压力,此可考虑[无锁队列](https://en.cppreference.com/w/cpp/atomic/atomic)等替代方案[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值