探秘写时拷贝的真相发布啦!

流風餘韻 流風餘韻     2022-12-11     427

关键词:


导读写时拷贝(copy-on-write, COW)就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝。
什么是写时拷贝

其实我们对写时拷贝并不陌生,Linux fork和STL string是比较典型的写时拷贝应用,本文只讨论STL string的写时拷贝。

string类的实现必然有个char*成员变量,用以存放string的内容,写时拷贝针对的对象就是这个char*成员变量。通过赋值或拷贝构造类操作,不管派生多少份string“副本”,每个“副本”的char*成员都是指向相同的地址,也就是共享同一块内存,直到某个“副本”执行string写操作时,才会触发写时拷贝,拷贝一份新的内存空间出来,然后在新空间上执行写操作。显然,那些只读的“副本”节省了内存分配的时间和空间。

听起来有点懵,对于没了解过写时拷贝的同学,会感觉完全颠覆平常对string的认知,下面我们来看一下实际例子。

写时拷贝例子

如上代码所示,调用拷贝构造函数生成str2,调用赋值操作符生成str3,那么str2与str3是否有分配内存空间来存储内容“abc”呢?

运行结果告诉我们,str1、str2与str3是共享内存空间的(char*成员指向相同的地址)。那么问题来了,对str1、str2或str3内容的修改是否会互相影响呢?答案是,只要遵守STL的约定来修改,是会触发写时拷贝的,不会互相影响(毕竟平时一直这样用也没有问题)。


可以看到,对str1重新复制,修改str3的值,都会触发写时拷贝,分配了新的空间。由于str1、str3都分配了新的空间,str2就可以继续使用原来的空间了。

写时拷贝原理

看了上面的例子,相信大家都已明白写时拷贝的表象了。但我们不能满足于现象,还要知道实现原理。应该很多同学都能猜到,string肯定是使用计数器来记录引用数,当有新的string对象共享内存块时,计数器+1,当有对象触发写时拷贝或析构时,计数器-1。

那么计数器存放在哪里呢?这是对象级别的计数器,由若干个对象共享,string类成员变量、静态变量或全局变量都不能满足要求。最合适的就是在堆里分配空间专门存储这个计数器,由第一个创建的对象分配并初始化计数器,其他对象按照约定引用计数器。我们知道string的内存空间就在堆上,那么直接在这块区上多分配一个空间来存储计数器是最方便的,所有共享这块内存的string对象都能访问计数器。事实上STL就是这么实现的,在string内存空间的最前面分配了空间存储计数器,如下图所示(图片摘自引文):

string的所有赋值、拷贝构造操作,计数器都会+1;修改string数据时,先判断计数器是否为0(0代表没有其他对象共享内存空间),为0则可以直接使用内存空间(如例子中的str2),否则触发写时拷贝,计数器-1,拷贝一份数据出来修改,并且新的内存计数器置0;string对象析构时,如果计数器为0则释放内存空间,否则计数器也要-1。

STL源码分析

我们稍微走读下STL源码,看看写时拷贝的实现,以赋值操作符为例(拷贝构造函数类似):

赋值操作符事实上是调用assign函数

_M_grab完成引用计数器更新,返回string数据内存地址

_M_rep返回Rep指针,Rep保存在string数据内存前面,所以使用-1下标索引。计数器_M_refcount就在Rep中。

实际执行_M_refcopy

引用计数器+1,返回数据内存地址(因为rep在数据前面,所以指针+1)

写时拷贝是一把双刃剑

写时拷贝能减少不必要的内存操作,提高程序性能,但同时也是一把双刃剑,如果没按STL约定使用string,可能会导致极其严重的bug,而且通常是很隐蔽的,因为一般不会把注意力放到一个赋值语句。

那么STL的约定用法是怎样呢?可以概括为两点:一,使用string提供的写操作,包括操作符与成员函数修改内容,都能正常触发写时拷贝,不会有“坑”;二,c_str()与data()返回const char*指针,只用来读取数据,不要强制转成char*指针直接修改内存。写时拷贝惹的祸都是因第二点使用不当导致的,“有经验”的程序员喜欢直接操作内存,硬是把const指针改成非const,殊不知这样修改内存,string对象是不感知的,没有办法触发写时拷贝,后果就是所有共享同一内存的string对象内容都被篡改了。

所以,应该从来都不把c_str()与data()返回的指针转换成非const,从源头上杜绝写时拷贝惹的祸。

但是有时却不得不应付已弄脏的源头,比如底层库实现有问题,传string对象进去,里面却通过指针修改string内容,导致写时拷贝机制失效。举个列子:

假设有上面一个Decode函数(为了方便描述,str默认空间够大),通过指针操作把data的数据拷贝到str。如果只调用一次,通常不会有什么问题,但是如果多次调用Decode,并且把str结果保存下来,那就出大bug,看下面代码:

可以看到,每次调用Decode后,之前保存的结果(str1、str2)都会“被覆盖”了。那么该如何应对这种已经有问题的底层函数呢?可以强制触发写时拷贝,下面继续分析。

强制触发写时拷贝

下面这些方法都可以强制触发写时拷贝:

1. 调用reserve函数

注意:reserve一定是在赋值后调用,不然提前触发写时拷贝是没用的

2. 调用resize函数


注意:resize大小一定要跟原来不一样,不然string会认为无需重新分配空间,请看下面resize源码。

另外,resize也要在赋值后调用。

3. 调用[]操作符

string[]操作符返回char&,允许调用者修改数据,所以会触发写时拷贝。

4. 调用char*参数版本assign


还要重点提醒,string参数版本的assign等价于赋值,不会触发写时拷贝的。

本文转载自:http://www.linuxprobe.com/quest-copy-truth.html

免费提供最新Linux技术教程书籍,为开源技术爱好者努力做得更多更好,开源站点:http://www.linuxprobe.com/


c++深浅拷贝写时拷贝(代码片段)

...f1a;本章以string类为例介绍浅拷贝与深拷贝,引用计数写时拷贝作为了解内容,string类的模拟实现参考C++string类的模拟实现。文章目录1.浅拷贝2.深拷贝3.引用计数+写时拷贝1.浅拷贝浅拷贝:对于有申请空间的... 查看详情

写时拷贝技术

CopyOnWrite(COW):写时拷贝技术一、什么是写时拷贝技术:写时拷贝技术可以理解为“写的时候才去分配空间”,这实际上是一种拖延战术。举个栗子:650)this.width=650;"src="http://img.blog.csdn.net/20160829170427832?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkb... 查看详情

c++——浅拷贝深拷贝写时拷贝详解(代码片段)

C++——浅拷贝、深拷贝、写时拷贝详解浅拷贝与深拷贝解决浅拷贝的问题——引用计数写时拷贝浅拷贝与深拷贝用String类模拟用将“/0”拷贝进去:调用系统默认的拷贝构造函数,结果就是内容相同,地址相同... 查看详情

string类的写时拷贝

...当我们需要写的时候才去新开辟内存空间。这种方法就是写时拷贝。 在构造函数中开辟新的空间时多 查看详情

string类的实现写时拷贝浅析

...当我们需要写的时候才去新开辟内存空间。这种方法就是写时拷贝。这也是一种解决由于浅拷贝使多个对象共用一块内存地址,调用析构函数时导致一块内存被多次释放,导致程序奔溃的问题。这种方法同样需要用到引用计数:... 查看详情

写时拷贝(copyonwrite)(代码片段)

    适用于深拷贝提高效率的一种方法。    例如string类,在拷贝构造时,会要用到深拷贝。如果用浅拷贝,会导致两对象的指针指向同一块空间,导致对象析构时,导致同一块空间释放两次,程序奔... 查看详情

难道是“写时拷贝”?

前言:1#if023其实,现在我要做的这件事情,是有个前提的,4有一天晚上,我和一个朋友讨论一个相关技术的问题,5(因为我也不是很懂,我不确定我的观点是正确的,所以才是讨论),6我们聊到了,Windows的映射机制,7我们模... 查看详情

写时拷贝cow(copy-on-write)

   写时拷贝技术是通过"引用计数"实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到... 查看详情

c++——浅拷贝深拷贝写时拷贝详解(代码片段)

C++——浅拷贝、深拷贝、写时拷贝详解浅拷贝与深拷贝解决浅拷贝的问题——引用计数写时拷贝浅拷贝与深拷贝用String类模拟用将“/0”拷贝进去:调用系统默认的拷贝构造函数,结果就是内容相同,地址相同... 查看详情

c++——浅拷贝深拷贝写时拷贝详解(代码片段)

C++——浅拷贝、深拷贝、写时拷贝详解浅拷贝与深拷贝解决浅拷贝的问题——引用计数写时拷贝浅拷贝与深拷贝用String类模拟用将“/0”拷贝进去:调用系统默认的拷贝构造函数,结果就是内容相同,地址相同... 查看详情

宋宝华:谈一谈linux写时拷贝(cow)的安全漏洞(代码片段)

写时拷贝的原理我们没什么好赘述的,就是当P1fork出来P2后,P1和P2会以只读的形式共享page,直到P1或者P2写这个page的内容,才发生pagefault导致写的进程得到一份新的数据拷贝。 下面的代码演示了它的效果:int... 查看详情

string类的写时拷贝与引用计数(代码片段)

由于浅拷贝使多个对象共用一块内存地址,调用析构函数时导致一块内存被多次释放,导致程序奔溃。实现string类的时候通常显示的定义拷贝构造函数和运算符重载函数。由于释放内存空间,开辟内存空间时花费时间... 查看详情

深拷贝&浅拷贝&引用计数&写时拷贝

(1).浅拷贝:classString{public:String(constchar*str=""):_str(newchar[strlen(str)+1]){strcpy(_str,str);}~String(){if(NULL!=_str){delete[]_str;_str=NULL;}}private:char*_str;};intmain(){Strings1("hello");Str 查看详情

并发容器之写时拷贝的list和set

对于一个对象来说,我们为了保证它的并发性,通常会选择使用声明式加锁方式交由我们的Java虚拟机来完成自动的加锁和释放锁的操作,例如我们的synchronized。也会选择使用显式锁机制来主动的控制加锁和释放锁的操作,例如... 查看详情

c语言如何获得变量的物理地址以及简单的写时拷贝测试

基本的思路:linux下的/proc/self是对自身进程映射的文件夹,里面的pagemap允许查看到每个虚拟页映射到的物理页。#include<stdio.h>#include<unistd.h>#include<inttypes.h>intptr_tMytop(uintptr_tvaddr)FILE*pagemap;intptr_ 查看详情

ognl探秘之二:数据乐园中的演员

既然Ognl是数据的乐园,这里面必然有各种演员在里面扮演自己的角色,这场戏才热闹精彩,里面有三种类型的演员。一种演员比较随便,叫字符串,没什么结构,就是些点点链接的字符串,这个点点代表了什么呢?那就是第二... 查看详情

string类的写时拷贝与引用计数(代码片段)

...当我们需要写的时候才去新开辟内存空间。这种方法就是写时拷贝。在构造函数中开辟新的空间时多开辟4个字节的空间,用来存放引用计数器,记录这快空间的引用次数。#include<iostream>#include<stdlib.h>usingnamespacest... 查看详情

宋宝华:谈一谈linux写时拷贝(cow)的安全漏洞(代码片段)

写时拷贝的原理我们没什么好赘述的,就是当P1fork出来P2后,P1和P2会以只读的形式共享page,直到P1或者P2写这个page的内容,才发生pagefault导致写的进程得到一份新的数据拷贝。 下面的代码演示了它的效果:int... 查看详情