目录
1.什么是写时复制
首先,我们从Linux系统父子进程讲起,也就是fork()函数,在Linux系统下使用fork ()函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork 返回,各自继续往下运行,但是原进程的fork 返回值与子进程的fork 返回值不同,在原进程中,fork 返回子进程的pid,而在子进程中,fork 返回0,如果fork 返回负值,表示创建子进程失败。
也就是说初始时子进程和父进程拥有的是相同的物理空间(内存地址),但当其中有进程进行写操作时,系统会为修改程序段的进程重新分配物理空间。那么为什么这么做呢?因为在读的时候,进程共享的数据都是不变的,因此不必要为每个进程重新分配内存空间,而且如果这样做,很可能在多任务下,内存空间提前耗尽,而且分配内存空间也需要耗费一定的时间,因此为了避免不必要的内存分配,优化性能。
2.写时复制的原理
在Linux系统下,在调用fork()函数后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,无需改动。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
可以从图a看出,当调用fork()函数时,进程1,进程2所共享的内存时一样的,但当进程1修改数据时,会对数据进行拷贝然后重新分配物理空间,如图b,进程1修改页C,在内存中创建了一份页C的副本。
3.C++中写时复制的实现
在C++中,String的实现就是通过写时复制来实现的,一共有两种实现方式,一种是开辟两个空间,一个char* _str和一个int* _refCountPtr, _str用来指向字符串的地址,refCountPtr则指向引用计数的地址。另一种则是在char* _str的初始位置开辟一个地址空间,前面4个字节用来存放refCount,剩下的为字符串的空间。
一.开辟两个空间的写时复制
1._str存放字符串, _refCountPtr存放引用计数
2.在复制构造时,先(* _refCountPtr)++,然后直接把字符串的指针赋给新的String
3.在释放String时,先对计数器(* _refCountPtr)--,再判断是否为0,若为0,则直接释放 _str和 _refCountPtr的地址
4.如果对字符串进行修改即写时,就要进行深拷贝,先判断引用计数是否为1,若大于1则进行步骤3,然后进行深拷贝,同时深拷贝后的字符串引用计数加1。
二.开辟一个空间的写时复制
1.开辟一个空间,前4个字节用来引用计数,剩下的为字符串_str的空间
2.处理方式与第一种下同,这里展示第二种写时复制的代码,如下
class String
{
class Char
{
public:
Char(size_t idx, String &s)
: _idx(idx)
, _s(s)
{}
char &operator=(const char &ch);
friend std::ostream &operator<<(std::ostream &os, const Char &rhs);
private:
size_t _idx;
String &_s;
};
public:
String();
String(const char* pstr);
String(const String &rhs);
String &operator = (const String &rhs);
~String();
const char &operator[](size_t idx) const;
Char operator[](size_t idx);
size_t size();
const char* c_str() const;
size_t refcount() const;
friend std::ostream &operator<<(std::ostream &os, const String &rhs);
friend std::ostream &operator<<(std::ostream &os, const Char &rhs);
private:
void initRefcount();
void increaseRefcount();
void decreaseRefcount();
void release();
char* _pstr;
};
size_t String::refcount() const
{
return ((int*)(_pstr - 4))[0];
}//返回引用计数
void String::initRefcount()
{
((int*)(_pstr - 4))[0] = 1;
}//初始化引用计数
void String::increaseRefcount()
{
++((int*)(_pstr - 4))[0];
}//引用计数+1
void String::decreaseRefcount()
{
--((int*)(_pstr - 4))[0];
}//引用计数-1
void String::release()
{
decreaseRefcount();
if(refcount() == 0)
{
delete[] (_pstr - 4);
}
cout << "release()" << endl;
}//引用计数为0时释放空间
String::String()
: _pstr(new char[5]())
{
cout << "String()" << endl;
_pstr += 4;
initRefcount();
}//初始化,无参构造函数
String::String(const char* pstr)
: _pstr(new char[strlen(pstr) + 5]())//结尾'\0'占一个字节-> 4 + str + '\0'
{
// cout << "String(const char*)" << endl;
_pstr += 4;
initRefcount();
strcpy(_pstr, pstr);
}//带参构造函数
String::String(const String &rhs)
: _pstr(rhs._pstr)
{
increaseRefcount();
}//复制构造函数
String &String::operator=(const String &rhs)
{
//cout << "operator = " << endl;
if(this != &rhs)
{
release();
_pstr = rhs._pstr;
increaseRefcount();
}
return (*this);
}//赋值运算符重载
String::~String()
{
cout << "~String()" << endl;
release();
}//析构函数
const char &String::operator[](size_t idx) const
{
//cout << "const char &String::operator[]" << endl;
return _pstr[idx];
}//下标运算符重载
size_t String::size()
{
return strlen(_pstr);
}//获取字符串长度
const char* String:: c_str() const
{
return _pstr;
}//字符串首地址
std::ostream &operator<<(std::ostream &os, const String &rhs)
{
os << rhs._pstr;
return os;
}//输出流重载
String::Char String::operator[](size_t idx)
{
cout << "嵌套类处理" << endl;
return Char(idx, *this);
}//下标运算符重载时下标传入嵌套类返回处理结果
char &String::Char::operator=(const char &ch)
{
cout << "写操作" << endl;
if(_s.refcount() > 1)
{
_s.decreaseRefcount();//引用计数减1
char* p = new char[_s.size() + 5]();
strcpy(p + 4,_s._pstr);//深拷贝
_s._pstr = p + 4;//指向新的地址
_s.initRefcount();//引用计数初始化为1
}
_s._pstr[_idx] = ch;
return _s._pstr[_idx];
}//如果是写操作
std::ostream &operator<<(std::ostream &os, const String::Char &rhs)
{
cout << "读操作" << endl;
os << rhs._s._pstr[rhs._idx];
return os;
}//如果是读操作
int main()
{
String s1("HelloWorld!");
String s2(s1);
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s1's refcount = " << s1.refcount() << endl;
cout << "s2's refcount = " << s2.refcount() << endl;
printf("s1 add = %p\n", s1.c_str());
printf("s2 add = %p\n", s2.c_str());
cout << endl;
String s3("WorkHard!");
cout << "s3 = " << s3 << endl;
cout << "after execute s3 = s1" << endl;
s3 = s1;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s3 = " << s3 << endl;
cout << "s1's refcount = " << s1.refcount() << endl;
cout << "s2's refcount = " << s2.refcount() << endl;
cout << "s3's refcount = " << s3.refcount() << endl;
printf("s1 add = %p\n", s1.c_str());
printf("s2 add = %p\n", s2.c_str());
printf("s3 add = %p\n", s3.c_str());
s3[0] = 'L';
cout << endl << "after execute Writing s3[0] = 'L'" << endl;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s3 = " << s3 << endl;
cout << "s1's refcount = " << s1.refcount() << endl;
cout << "s2's refcount = " << s2.refcount() << endl;
cout << "s3's refcount = " << s3.refcount() << endl;
printf("s1 add = %p\n", s1.c_str());
printf("s2 add = %p\n", s2.c_str());
printf("s3 add = %p\n", s3.c_str());
cout << endl << "after execute Reading s2[0] = " << s2[0] << endl;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s3 = " << s3 << endl;
cout << "s1's refcount = " << s1.refcount() << endl;
cout << "s2's refcount = " << s2.refcount() << endl;
cout << "s3's refcount = " << s3.refcount() << endl;
printf("s1 add = %p\n", s1.c_str());
printf("s2 add = %p\n", s2.c_str());
printf("s3 add = %p\n", s3.c_str());
return 0;
运行结果如下:
从结果中可以看到,当s3进行修改后,s1,s2引用计数变为2,而s3引用计数变为1,达到了写时复制的效果。
4.Java中的写时复制
在Java中,典型的写时复制就是并发工具包下的CopyOnWriteArrayList,它是线程安全读无需操作,但写会创建底层数组的新副本。当然相似的容器还有CopyOnWriteSet。这里对CopyOnWriteArrayList的源码分析一下。
可以看到set,move,add操作都是需要加锁,并且写时将原数组拷贝到新的数组上进行相应的操作,结束后,容器指向新的数组副本。
//set操作
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;//修改,长度不变
Object[] newElements = Arrays.copyOf(elements, len);//深拷贝数组
newElements[index] = element;//下标赋值
setArray(newElements);//指向新的数组
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();//解锁
}
}
//add操作
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//原数组长度加1
newElements[len] = e;//新数组末尾添加元素
setArray(newElements);//指向新的数组
return true;
} finally {
lock.unlock();//解锁
}
}
//remove
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);//拿到值
int numMoved = len - index - 1;
if (numMoved == 0)//是删除的末尾值,则将len-1个数值拷贝到数组上
setArray(Arrays.copyOf(elements, len - 1));//数组长度减1
else {
Object[] newElements = new Object[len - 1];
//分两次copy
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);//copy index前的数组 和index后的数组到新的数组
setArray(newElements);//指向新的数组
}
return oldValue;
} finally {
lock.unlock();
}
}
再来看看CopyOnWriteArrayList的get也就是读操作
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];//直接去下标
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);//直接取下标
}
可以看到CopyOnWriteArrayList的写操作都会加上lock,保证同一个时间写操作只有一个,而读操作则不需要加锁,直接取下标进行返回
5.总结
在Java, C++,Linux系统中CopyOnWrite都能看到其应用。因为能减少不必要的资源分配,从而减少复制时带来的时间延迟和资源消耗。
参考:
http://c.biancheng.net/view/1272.html 写时复制技术(详解版)
https://blog.csdn.net/qq_32131499/article/details/94561780 写时复制(Copy On Write)
https://blog.csdn.net/m0_37956168/article/details/74898047 写时拷贝的两种方案