Archive

Posts Tagged ‘c++’

强引用智能指针与弱引用智能指针

February 25th, 2015 No comments

c++代码中最麻烦的就是内存管理了, 还好有了RAII,这样至少可以解放一点我们的脑细胞,不需要时时刻刻注意释放内存或者避免多次释放内存。现在很多人都使用std::shared_ptr 当然了,如果你没有用到C++11,那么可以使用boost::shared_ptr. 不过我们当时使用的不是shared_ptr. 而是学习了ICE以后自己倒腾出来的一个intrusive_ptr,俺们管它叫做Pointer, 非常类似于boost::intrusive_ptr。不过我们当时还不知道boost有这个东西而已(也可能当时还没有这个玩意儿)。

侵入式智能指针和非侵入式智能指针的差别还是挺大的,我们用一幅图来说明一下。 
smart_pointer
这幅图的左边是侵入式智能指针,其引用计数放在了被指向的object的里面,intrusive_ptr指向这个object即可。而右边是非侵入式智能指针,引用计数被单独放在一块区域(其中还包括一个弱引用的计数)。non-intrusive_ptr(shared_ptr, weak_ptr)指向object和引用计数区域。这样一来,对于intrusive_ptr,我们还是可以直接使用raw pointer, 当然这样并不好,也可以再把一个raw pointer传递给一个intrusive_ptr, 其引用计数不会有被中断的情况。而non-intrusive_ptr就不能这样,raw pointer传递给他们以后引用计数只能重新开始计数。还有就是由于侵入式智能指针的引用计数和目标object实例在一块, 那么可以更加有效的利用CPU cache。 而非侵入式的智能指针因为有两个指针,就会稍弱一点。但是呢,侵入式智能指针相比非侵入式智能指针也有一个缺点,这就是我们要讲的弱引用智能指针。 
那么弱引用到底是什么?有什么用处呢? 我们知道智能指针的引入是为了减少内存泄漏的问题,顺带着把野指针,多次释放内存等问题一块解决了。但是有的时候只能指针也会有内存泄漏问题,循环引用–就是这种情况。当两个object互相持有对方的引用,那么他们的引用计数都不会减少到0,那么这两个object也都不会被释放。解决的办法也就是弱引用. 当两个object需要持有对方的时候,其中一方持有对方的弱引用,弱引用是不增加(强)引用计数的–即上图右中的reference count,但是会增加weak reference count. 举个例子,有A,B两个对象实例,其中A持有B的强引用refB, 但是B只持有A的弱引用refA. 当除B以外的object不再持有对A的引用的时候,由于弱引用不会增加引用计数,所以A的引用计数会降至0,此时A会被释放掉,同时B的引用计数也会减少。这样就可以有效的解决循环引用造成的内存泄漏问题。

现在我们来看看弱引用是如何实现的。如上图右边所示,非侵入式智能指针有两个指针。其中一个指向对象实例,我们命名为px; 另外一个指向一块专门用来存放引用计数的区域,我们为之命名为pn. pn所指向的区域( 就叫RefCountZone吧 )里面一共有两个变量,reference count和weak reference count, 他们分别记录强引用和弱引用的次数。当使用shared_ptr的时候,reference count就会加1,而weak reference count不会有变化。而使用weak_ptr的时候, weak reference count就会加1,但是reference count不会有变化。这样一来当所有的强引用都消失了, 那么reference count就会变成0,这个时候就可以删除px所指向的对象实例了。但是值得注意的是在这个时候pn所指向的区域不一定被删除。我们假设这个时候还有弱引用到这个区域,那么pn指向的区域是不会被删除的。这时候如果我们需要使用weak_ptr指向的object 实例的时候,首先需要调用weak_ptr::lock方法,该方法可以获得一个shared_ptr(但可能是空)。weak_ptr::lock首先检查pn->reference count是否为0,如果是0 的话,那么说明px指向的实例已经被删除,那么lock方法会失败,返回一个不指向任何实例的shared_ptr,如果pn->reference count不为0,那么说明px所指向的实例还存在,这个时候可以返回一个该实例的shared_ptr. 当所有的弱引用都消失的时候,也就是pn->weak reference count == 0的时候,同时pn->reference count也是0,那么说明已经没有任何对这个实例的引用了(不论是强引用还是弱引用),这是pn指向的区域就可以被安全删除了。 
从上面我们也可以看出弱引用很巧妙的利用了非侵入式智能指针的RefCountZone。同时利用了reference count和weak reference count来分别控制强引用和弱引用的个数。但是侵入式智能指针就不能很好的实现弱引用了,因为如果我们要实现弱引用,那么就需要一块区域来存放所引用的次数,而且这个块区域不能随着object instance的消亡而消亡,否者当强引用完全消失但还存在弱引用的时候由于object instance已经被删除导致弱引用计数无法被访问。如果我们也想像非侵入式智能指针 一样新开辟一块区域来存储ref count, 那么就变得和shared_ptr/weak_ptr一样,没有重新实现的必要了。所以如今在使用侵入式只能指针并且有循环引用风险的地方,我们会将其中一方持有对方的智能指针修改为原始指针或者是引用。

Debug Heap Corrupt

December 12th, 2014 No comments

c/c++代码对内存操作有着极强的控制力, 但是也很容易造成问题。 特别是内存访问越界这类问, 一旦出现了,在很多情况下是很难查找的。 如果软件中的某个模块因为单元测试没有完全覆盖(这个通常也很难)然后集成起来以后出来的这类问题就更加难以查找。 很多时候出现问题的地方根本就不是你看到的地方。 我们来举个例子, 这个也是我们遇到的问题的一个简化版本。 直接上代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <vector>

void corrupt( ) {
    char* p = (char*)malloc(1);
    memset(p,0x47,32);//fill some garbage to corrupt the heap
}

void victim( ) {
    std::vector<int> arr;
    for(int i = 0 ; i < 100; i ++ ) {
        arr.push_back(i);
    }
}

int main(int argc, char* argv[]){
    corrupt();
    victim();
    return 0;
}

上面的代码在corrupt函数中分配了一个字节的空间,接着调用memset向分配的地址写入32个0x47. 那么很明显,写越界了。 这样的操作会把heap的维护链表弄坏,导致以后再次进行内存分配释放的时候出现错误,也可能写入另外一块分配的内存区域。
在vc(visual studio 2005)下面编译运行以后在

        arr.push_back(i);

出现错误。 output窗口(调试模式下面)输出

First-chance exception at 0x7c922bb3 (ntdll.dll) in heap_corrupt.exe: 0xC0000005: Access violation reading location 0x003b6000.

很明显出现错误的地方并不是造成问题的地方, 这个简单的程序你可能很容易就可以看出问题出在什么位置。但是如果是一个很复杂的大型工程,那么很可能就没有那么容易了。如果没有其他的帮助工具的话估计要花很大的时间和精力来逐步收缩出问题的范围。
上面的代码在g++下面(需要增加一些include的头文件)编译运行以后也会显示是arr.push_back(i)这一行除了问题。
为了能有效的快速定位此类问题, 我们通常需要借助其他的工具。 在windows上面我们可以使用gflags来做到这一点。
使用如下命令,monitor heap的操作

gflags /p /enable heap_corrupt.exe /full

然后再来调试改程序,就会在memset(p,0x47,32);这一行发生异常。 那么我们就可以很快的检查出来是因为对p的写越界造成了这个问题。 但并不是有这个工具就万事OK了。如果我们把memset那一句改成memset(p,0x47,2);的话。那么程序在整个调试过程中是不会出错的,当然了,这个很可能是因为malloc(1)的时候得到的内存块的真是大小不是1而是某个最小的bucket的大小.
在linux上面呢, 我们可以使用AddressSanitizer这种神器来快速定位这个问题。因为我们的gcc版本是4.4.7的,还没有AddressSanitizer的支持,所以可以使用clang.

clang++ -g -fsanitize=address -o t test.cpp

然后直接运行程序就可以在输出上看到一堆东西

[hongquan.zhang@devbox Download]$ ./t
=================================================================
==1216==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000f00f at pc 0x48aaab bp 0x7fff4a1687d0 sp 0x7fff4a1687c8
WRITE of size 32 at 0x60200000f00f thread T0
    #0 0x48aaaa in corrupt() /home/hongquan.zhang/Download/test.cpp:10
    #1 0x48b0d0 in main /home/hongquan.zhang/Download/test.cpp:21
    #2 0x3f7201ecdc in __libc_start_main (/lib64/libc.so.6+0x3f7201ecdc)
    #3 0x48a84c in _start (/home/hongquan.zhang/Download/t+0x48a84c)

0x60200000f00f is located 30 bytes to the right of 1-byte region [0x60200000eff0,0x60200000eff1)
allocated by thread T0 here:
    #0 0x473131 in malloc /home/hongquan.zhang/Work/tools/llvm/projects/compiler-rt/lib/asan/asan_malloc_linux.cc:74
    #1 0x48a9cb in corrupt() /home/hongquan.zhang/Download/test.cpp:9
    #2 0x48b0d0 in main /home/hongquan.zhang/Download/test.cpp:21
    #3 0x3f7201ecdc in __libc_start_main (/lib64/libc.so.6+0x3f7201ecdc)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/hongquan.zhang/Download/test.cpp:10 corrupt()
Shadow bytes around the buggy address:
  0x0c047fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9df0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 01 fa
=>0x0c047fff9e00: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff9e50: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Contiguous container OOB:fc
  ASan internal:           fe
==1216==ABORTING

输出的最开始就表明了出错的地方是corrupt函数,可以非常方便的让我们在第一时间发现真正制造问题的地方。而且即便你把memset调用改成memset(p,0x47,1)也依然有效。

上面的方法也许对你在遇到问题的时候有一些帮助,当然了最好还是在操作内存的时候小心一些。如果可以的话,尽量使用c++已经封装好的class。 如果不得已需要直接操作内存需要慎之又慎。

Categories: programming Tags: ,

C++抛exception会主动让出CPU吗?

September 10th, 2014 No comments

虽然我自己并不是很喜欢使用exception.但是在有的场合用exception比return code让代码更具可阅读性.一直以来,我都认为throw exception不会大幅度降低程序的性能, 但是最近线上发生的一件事情却把exception这个东西牵扯了进来.
事情很简单, 现场报说有的请求timeout(500ms超时). 查看log以后发现一部分代码可能造成这个问题,但是当时并无法肯定是哪一个调用造成的耗时长. 后来做了一系列的实验以后发现居然是一个查找部分的代码会很耗时(最长可达1s), 但是这个查找的代码的实现及其的简单.基本上就是这个样子:

void findXXX( ) {
    find in a std::map<std::string, std::string> instance;
    if not found {
        throw InvalidParameter();
    }
}

而当时的情况是, map中是没有数据的,也就是说一定会出发抛出异常. 开始我怎么都觉得throw exception会那么耗时.可是多次的测试下来却发现总是在这个点上可能耗时很长.那么无论如何这个抛异常看起来都有点问题.
于是我做了一个小程序,就是很简单的抛异常100w次.但是没有发现耗时很长的情况发生(至少和我预期是一样的). 那么为何线上程序总是在这个抛异常的地方出现问题呢? 于是对比了自己的模拟程序和线上程序,发现了一个很大的不同点,那就是线上程序是多线程的,而且线程相当多(500+),而我的测试程序是单线程的.那么有没有可能是抛异常会导致线程切换,最终引发耗时长呢?
于是我修改了一下测试程序,加入线程,线程run里面就是不断的++一个long long,过一会儿sleep 30ms. 我一共new了800个线程(我也是够猛的). 这次测试程序我做了两个版本,一个是抛异常的,另外一个是不抛异常,取而代之的是循环++10次,然后return的.
从这两个版本的测试结果来看,在同一个环境(CentOS5, 4core cpu, 16G mem)上测试10次,平均下来,抛异常的版本运行时间为26s,而不抛异常的版本运行时间为1.5s左右. 同时抛异常的版本中能监测到有很50+次调用函数耗时达1ms以上的.而不抛异常的版本平均只有2次不到.
基于这些测试, 我猜测很可能抛异常会引发context switch. 同时由于线程数量巨大,线程切换十分频繁. 最终造成这个简单的findXXX耗时很大.
当然了,这个只是我的猜测,没有证实. 希望以后可以继续看看是不是真的造成了主动的线程切换.

PS: google了好一阵, 还没有直接证据可以证明throw exception可能导致线程上下文切换. 以后有空倒是可以看看libstdc++的实现.

Categories: programming Tags: ,

printf like function

May 28th, 2013 No comments

在编写服务器端程序的代码的时候,打日志应该是一个必不可少的事情。使用c++流的方式是一个不错的选择,类型安全。但是总给人一种很不爽的感觉。因为好好的一段话被硬生生的分隔开了。例如:

logger.DEBUG<<"client ["<<client.ip<<":"<<client.port<<"] timed out due to no message in ["<<timeout_interval<<"]ms";

这样看起来远没有如下格式看起来舒服:


logger(DEBUG,"client [%s:%u] timed out due to no message in [%u]ms",client.ip.cstrt(),client.port,timeout_interval);

当然了,有利就有弊。如果用第二种方法的话,一不小心就会让程序崩溃。例如如果一个%s的地方被传入了一个int的值,或者传入了一个std::string但是没有加上.c_str().这个时候多会让你的程序死的很难看,严重的时候会让你的core dump的call stack看起来怪异无比,甚至无从下手。
不过还好的就是gcc下的有个extension可以帮我们在编译的时候检查传入的参数是否类型正确。方法如下:

#   define    PRINTFLIKE(m,n) __attribute__((format(printf,m,n)))

其中m是格式化字符串在参数列表中的位置,n是传入到格式化字符串的参数的其实位置


class Logger
{
...
void operator()( int level, const char* fmt, ... ) PRINTFLIKE(2,3)
...
};

这样编译器在编译的时候如果发现格式化字符串和传入参数的类型不匹配的时候就会出一个warning。就像下面这样,这会帮你挽回很多crash造成的损失的 …


t.cpp:6: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘long long int’

Categories: programming Tags:

非侵入式与侵入式 之 链表

May 26th, 2013 No comments

stl中的std::list想必是一个使用率不低的container, 在这里我们称std::list为非侵入式链表,std::list的用户数据并不拥有其在链表中的位置信息。但是当想要删除已被插入的用户数据的时候,就没有那么方便了。因为使用std::list的时候,链表相关的信息是由std::list来管理的,被插入道std::list的用户数据并不知道自己的位置信息,也就是并不知道prev和next指向的是什么地方。所以如果你想把插入的数据从链表上取下,那么首先你需要找到你的数据在std::list中的位置信息,然后通过操作prev和next指针来达到目的。所以为了删除这个数据,你必须要首先找到这个数据在list中的位置,然后调用list::erase来删除这个用户数据。而查找这个动作的时间复杂度为O(n/2)。那么当你需要频繁插入删除数据并且你需要查找才知道数据所在位置的时候,std::list或许并不是一个特别好的选择。(当然你还有其他选择,那就是记录下你的数据插入到list中的iterator,这样的话就可以略过查找这一步了,不过这个不在讨论范围内。)

这个时候也许你就需要侵入式的链表了,我们先来看一下boost中侵入式链表intrusive list.当然了如果直接使用boost::intrusive::list的erase方法的话,那么和std::list之间没有什么差别。所以我们需要使用auto_unlinkmode.代码如下:

#include <iostream>
#include <boost/intrusive/list.hpp>
using namespace boost::intrusive;
typedef list_base_hook<link_mode<auto_unlink> > auto_unlink_hook;
class MyData: public auto_unlink_hook
{
private:
    int data;
public:
    MyData( int i ):data(i){}
    void show( ) const
    {
        std::cout<<data<<std::endl;
    }
};
int main()
{
    typedef list<MyData,constant_time_size<false> > DataList;
    DataList dl;
    MyData d1(1);   MyData d2(2);   MyData d3(3);
    dl.push_front(d1);  dl.push_front(d2);  dl.push_front(d3);
    d2.unlink();
    DataList::const_iterator it = dl.begin();
    for( ; it != dl.end(); it ++ )
        it->show();
    return 0;
}

可以看到当d2调用unlink以后它自己就从dl中删除了引用。以下是unlink的代码:


static node_ptr unlink(const node_ptr &this_node)
{
  node_ptr next(NodeTraits::get_next(this_node));
  if(next){
     node_ptr prev(NodeTraits::get_previous(this_node));
     NodeTraits::set_next(prev, next);
     NodeTraits::set_previous(next, prev);
     return next;
  }
  else{
     return this_node;
  }
}

由于用户数据记录了自己在list中的位置,所以删除的时候就很方便,也就是略过了查找这一步了。

Categories: programming Tags:

记录一个关于C++里面delete[]的事情

April 30th, 2013 No comments

在网上看到一篇“C++的数组不支持多态”?,写的是在使用delete[] 删除一个数组的时候的虚析构函数的调用问题。
大致故事是这样的,首先见如下代码。

include < iostream >

using namespace std;
class Base
{
public:
virtual ~Base() { cout<<“Base”< int a;
};
class Derived : public Base
{
public:
virtual ~Derived() { cout <<“Derived”< int a;
int b;
int c;
};
void test( Base* b)
{
delete[] b;
}
int main(int argc, char* argv[])
{
cout<<“size(Base)”<virtualarray.exe
size(Base)4 size(Derived)16
Derived
Base
Derived
Base
Derived
Base
Derived
Base
Derived
Base
Derived
Base
Derived
Base
Derived
Base
Derived
Base
Derived
Base

而在g++下面程序会崩溃掉。
~/Downloads$ ./test
size(Base)8 size(Derived)20
Segmentation fault (core dumped)

但是在clang++下面的表现让人不是很明白,居然是调用了析构函数,但是只是调用了基类的析构函数
~/Downloads$ ./test
size(Base)8 size(Derived)20
Base
Base
Base
Base
Base
Base
Base
Base
Base
Base

VC一向都比较喜欢帮助大家,所以看起来VC知道你的真正意图是想以sizeof(Derived)为步长去做析构,所以可以跑出这样的结果
在g++下面程序会crash这个很正常,按照delete[] base这一句要表达的是在base所指向的地址上开始析构一个数组,数组的大小记录在base-4的地址上,在这里是10个, 接下来就按照sizeof(Base)为步长,不断的查找虚表并且调用相应的析构函数。但是很不幸base实际上指向的是Derived实例化出来的数组,真正的步长应该是sizeof(Derived).所以上面查找析构函数的地址就肯定是错的。那么崩溃也就在所难免了。 这个是真实的反应出了这个C++语句的本身含义。
而在clang++下面,从输出结果来看,其查找步长似乎是正确的,但是为何调用的是Base的析构而不是Derived的析构则实在是让人不解。

Categories: programming Tags: