对于CopyOnWrite(写时复制)的一点研究

本文深入探讨了写时复制(CopyOnWrite)的概念,从Linux系统的fork()函数出发,解释了其原理,然后详细介绍了C++中String类的两种实现方式,以及Java中CopyOnWriteArrayList的实现。通过实例展示了写时复制如何在多线程环境下提高效率,减少不必要的资源分配。
摘要由CSDN通过智能技术生成

 

目录

对于CopyOnWrite(写时复制)的一点研究

 

1.什么是写时复制

2.写时复制的原理

3.C++中写时复制的实现

一.开辟两个空间的写时复制

二.开辟一个空间的写时复制

4.Java中的写时复制

5.总结


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就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。

è¿ç¨1ä¿®æ¹é¡µé¢Cåå

可以从图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 写时拷贝的两种方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值