首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
图文并茂VLAN详解,让你看一遍就理解VLAN
2
做了几年的网工也未必了解VLAN和VXLAN的区别,今天我来告诉你!
3
全网内容最全,质量最高的MPLS及MPLS VPN技术详解,瑞哥力荐!
4
Google BBR拥塞控制算法背后的数学解释 | 深度
5
QUIC 0-RTT实现简析及一种分布式的0-RTT实现方案
6
AGPS定位基本原理浅析
7
Golang语言情怀-第56期 Go 语言标准库翻译 crypto/cipher
8
神锁离线版插件端到端加密比HTTPS更安全
9
基于 TLS 1.3的微信安全通信协议 mmtls 介绍(上)
10
基于 TLS 1.3的微信安全通信协议 mmtls 介绍(下)
11
24位腾讯云专家精彩演讲,4万字《腾讯云技术实践精选集 2021》发布!(附合集下载)
12
浅谈VPC二三,秒懂秒透
13
提速 30%!腾讯TQUIC 网络传输协议
14
25 张图,一万字,拆解 Linux 网络包发送过程
15
nginx http模块数据存储结构
16
多机房多活,多机房平滑迁移架构方案全集(上+中+下)
17
小孩都看得懂的多臂老虎机和汤姆森采样
18
什么是 Go runtime.KeepAlive?
19
Rust 热点| Discord 为什么从 Go 切换到了 Rust
20
用户态 tcpdump 如何实现抓到内核网络包的?
21
一文读懂 | coredump文件是如何生成的
22
网工知识大扫盲——三层交换技术
23
聊聊 top 命令中的 CPU 使用率
24
什么是HDFS的纠删码
25
Linux ptrace 的实现
26
天天讲路由,那 Linux 路由到底咋实现的!?
27
Linux系统研究 - 操作系统是如何管理tcp连接的 (2)
28
一个有关tcp的非常有意思的问题
29
对上一篇文章中tcp问题的进一步思考
30
epoll和shutdown使用不当可能导致死循环
31
socket的SO_REUSEADDR参数全面分析
32
多进程可以监听同一端口吗
33
golang | 是返回struct还是返回struct的指针
34
​TCP 拥塞控制详解
35
C|网络|TCP-BBR拥塞控制剖析
36
如何使用 Go 语言写游戏服务器?
37
Nginx 日志 worker_connections are not enough while connecting to upstream
38
如何用九条命令在一分钟内检查Linux服务器性能?
39
服务器病了吗? Linux 服务器的那些性能参数指标
40
边缘计算比云计算强在哪里?终于有人讲明白了
41
详解边缘计算系统逻辑架构:云、边、端协同
42
边缘计算成为下一个爆发点,云计算巨头和CDN巨头谁会赢?
43
告知你不为人知的 UDP:连接性和负载均衡
44
为什么需要智能网卡?
45
从CDN到边缘计算,近水楼台是否先得月?
46
经典网络还是VPC,开发者作何选择?
47
Linux内核网络udp数据包发送(二)——UDP协议层分析
48
有没有人告诉你—写时拷贝的真相
49
[linux][network]net bridge技术分析
50
软硬件融合技术内幕 进阶篇 (1) —— 从小霸王到云计算

有没有人告诉你—写时拷贝的真相

作者简介:梁少华,QQ动漫后台开发,腾讯高级工程师。从事后台开发4年多,参与过QQ秀、手Q红点系统、手Q游戏公会、QQ动漫等项目,有丰富的后台架构经验,擅长海量服务设计

1. 什么是写时拷贝

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

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

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

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

2. 写时拷贝例子

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

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

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

3. 写时拷贝原理

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

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

图片摘自引文

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

4. stl源码分析

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

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

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

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

(4) 实际执行_M_refcopy

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

5. 写时拷贝是一把双刃剑

写时拷贝能减少不必要的内存操作,提高程序性能,但同时也是一把双刃剑,如果没按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)都会“被覆盖“了。那么该如何应对这种已经有问题的底层函数呢?可以强制触发写时拷贝,下面继续分析。

6. 强制触发写时拷贝

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

(1) 调用reserve函数

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

(2) 调用resize函数

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

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

(3) 调用[]操作符

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

(4) 调用char*参数版本assign

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

7. 相关参考资料

http://blog.csdn.net/haoel/article/details/24058

http://blog.csdn.net/haoel/article/details/24065

http://blog.csdn.net/haoel/article/details/24077

http://blogs.360.cn/360cloud/2012/11/26/linux-gcc-stl-string-in-depth/

下一篇
举报
领券