目录

​什么是写时拷贝​

​写时拷贝原理​



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

【Linux】写时复制(CopyOnWrite)|写时拷贝|rcu_引用计数

典型例子:

在 Linux 系统中,调用 ​​fork​​ 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 ​​写时复制​​ 机制。

(也就是只有进程空间的某页内存的内容要发生变化时,才会将父进程的该页内存复制一份给子进程。)

这些共享页面标记为写时复制,这意味着如果任何一个进程写入共享页面,那么就创建共享页面的副本,写时复制如图 1 所示,图中分别反映了修改页面 C 的前与后。

【Linux】写时复制(CopyOnWrite)|写时拷贝|rcu_引用计数_02

写时拷贝原理

  写时拷贝技术实际上是运用了一个 “引用计数” 的概念来实现的。在开辟的空间中多维护四个字节来存储引用计数。

有两种方法:

①:多开辟四个字节(pCount)的空间,用来记录有多少个指针指向这片空间。

②:在开辟空间的头部预留四个字节的空间来记录有多少个指针指向这片空间。

  当我们多开辟一份空间时,让引用计数+1,如果有释放空间,那就让计数-1,但是此时不是真正的释放,是假释放,等到引用计数变为 0 时,才会真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。

linux 下的 fork() 就是用的写时拷贝技术,引用计数不光在 string 这里用到,还有智能指针 shared_ptr 也用到了引用计数来解决拷贝问题。

举个例子

string 的写时拷贝(维护一个指针):

class String
{
public:
//构造
String(const char* str)
:_str(new char[strlen(str) + 1])
,_pCount(new int(1))
{
strcpy(_str, str);
}

//拷贝构造
String(const String& s)
:_str(s._str)
,_pCount(s._pCount)
{
(*_pCount)++;
}

//赋值运算符重载
String& operator=(const String& s)
{
if(_str != s._str)
{
if(--(*_pCount) == 0)
{
delete[] _str;
delete _pCount;
}
_str = s._str;
_pCount = s._pCount;
(*_pCount)++;
}
return *this;
}

~String()
{
if(--(*_pCount) == 0)
{
delete[] _str;
delete _pCount;
}
}

char& operator[](size_t pos)
{
if(*_pCount > 1)
{
char* newstr = new char[strlen(_str) + 1];
strcpy(newstr, _str);
--(*_pCount);
_str = newstr;
_pCount = new int(1);
}
return _str[pos];
}

const char* c_str()
{
return _str;
}
private:
char* _str;
int* _pCount;
};

源码中的写法:在空间的头部维护四个字节的空间,记录引用的个数。放在头部维护效率能高一些,如果放在尾部维护的话,每次开辟新的空间都要讲这四个字节也向后挪动相应的位置,所以放在前面效率高点

class String
{
public:
//构造
String(const char* str)
:_str(new char[strlen(str) + 4 + 1])
{
_str += 4; //前四个字节放引用计数
strcpy(_str, str);
GetRefCount() = 1;
}

//拷贝构造
String(const String& s)
:_str(s._str)
{
GetRefCount()++;
}

//赋值运算符重载
String& operator=(const String& s)
{
if(_str != s._str)
{
if(--GetRefCount() == 0)
{
delete[] (_str - 4);
}
_str = s._str;
GetRefCount()++;
}
return *this;
}

~String()
{
if(--GetRefCount() == 0)
{
delete[] (_str - 4);
_str = nullptr;
}
}

char& operator[](size_t pos)
{
if(GetRefCount() > 1)
{
--GetRefCount();
char* newstr = new char[strlen(_str) + 4 + 1];
newstr += 4;
strcpy(newstr, _str);
_str = newstr;
GetRefCount() = 1;
}
return _str[pos];
}

int& GetRefCount()
{
return *((int*)(_str - 4)); //前四个字节为引用计数
}
private:
char* _str;
};

简介  

写入时复制是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的(transparently)。此作法的主要优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作是可以共享同一份资源。

应用

虚拟内存管理中的写时复制

一般把这种被共享访问的页面标记为只读。当一个task试图向内存中写入数据时,内存管理单元(MMU)抛出一个异常,内核处理该异常时为该task分配一份物理内存并复制数据到此内存,重新向MMU发出执行该task的写操作。

数据存储中的写时复制

[Linux]等的文件管理系统使用了写时复制策略。

[数据库]服务器也一般采用了写时复制策略,为用户提供一份snapshot。

软件应用中的写时复制

[C++标准程序库]中的[std::string]类,在C++98/C++03标准中是允许写时复制策略。但在[C++11]标准中为了提高并行性取消了这一策略。 GCC从版本5开始,std::string不再采用COW策略。


类似概念:rcu

用户态的rcu:urcu