在一些编译器中,STL中string采用了copy-on-write实现,这种情况会导致一些问题。
在我的工程中,首先是在Linux中编译项目,采用的是g 5.4,编译没有任何问题。当进行NDK的交叉编译的时候,由于NDK的toolchain中采用的是g 4.9,就出现了问题。问题的原因大概就是我在往一个string中写内容的时候,报访问非法内存的错误。这个string没有被显示的初始化,或者说采用的默认初始化。
这个问题的原因很奇怪,奇怪的地方在于一个用户进程声明的std::string
为什么无法写入呢?查阅资料发现,g 4.9中std::string
采用的copy-on-write实现,这就是问题所在了。
copy-on-write
opy-on-write(以下简称COW)是一种很重要的优化手段。它的核心思想是懒惰处理多个实体的资源请求,在多个实体之间共享某些资源,直到有实体需要对资源进行修改时,才真正为该实体分配私有的资源。
COW技术的一个经典应用在于Linux内核在进程fork时对进程地址空间的处理。由于fork产生的子进程需要一份和父进程内容相同但完全独立的地址空间,一种做法是将父进程的地址空间完全复制一份,另一种做法是将父进程地址空间中的页面标记为共享的(引用计数 1),使子进程与父进程共享地址空间,但当有一方需要对内存中某个页面进行修改时,重新分配一个新的页面(拷贝原内容),并使修改进程的虚拟地址重定向到新的页面上。
COW技术有哪些优点呢?
- 减少了分配(和复制)大量资源带来的瞬间延迟(注意仅仅是latency,但实际上该延迟被分摊到后续的操作中,其累积耗时很可能比一次统一处理的延迟要高,造成throughput下降是有可能的)
- 另一方面减少不必要的资源分配。(例如在fork的例子中,并不是所有的页面都需要复制,比如父进程的代码段(.code)和只读数据(.rodata)段,由于不允许修改,根本就无需复制。而如果fork后面紧跟exec的话,之前的地址空间都会废弃,花大力气的分配和复制只是徒劳无功。)
- 数据一致性:COW技术通过在写入操作时创建新的副本,确保了数据的一致性。因为每个进程或线程都拥有自己的副本,在进行修改时不会影响其他进程的数据。这样可以避免并发访问导致的数据不一致性问题。
COW的思想在资源管理上被广泛使用,甚至连STL中的std::string
的实现也要沾一下边,g 4.9中实现的std::string
便是COW的实现。
COW导致的问题
COW的核心思想就是lazy-copy。std::string
的lazy-copy行为只发生在两个string对象之间的拷贝构造,赋值和assign操作上,如果一个string由(const)char*
构造而来,则必然会分配内存和进行复制,因为string对象并不知道也无权控制char*
所指内存的生命周期。
但是就是赋值导致了我的copy-on-write问题,由于在赋值之后,另一端的string被释放了,导致我这个string指向的内存是悬空的,因此写入的时候才会发生非法内存访问的错误。这也是copy-on-write实现中比较常见的问题,引以为戒。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!