daemon平滑升级

March 15th, 2014 No comments

对于socket server来说,平滑升级对于用户体验是一个很好的加分。这样可以不对用户操作造成任何影响。而平滑升级的最直接最核心的应该就是不在升级的过程中关掉listen端口,也不断掉当前的连接,同时还可以接受新的连接,所有的已被接受的请求必须要完整的执行完毕,不能因为升级步骤而终止.

为了到达以上几点,一般来说可以如下来达到平滑升级的目的。
1. 分区域升级。 首先在总控部分,禁掉某些部分server接受新请求的能力。然后等待这些server的当前请求全部执行完毕以后开始升级这些server上的服务。采用这种方法逐步升级所有的server
2. 采用类似nginx的平滑升级的方法。

第一种方法对于那些直接面对客户端的server是行不通的,因为一旦停掉一会儿,客户端就无法正常连接了。还有就是即便是分布式的后台加上一个proxy, 也可能会因为某些特定的请求很难在短时间内完成而无法升级(例如,一个广播流的频道请求),对于这些特定的就需要在指定的维护窗口时间强行升级服务了。
第二种方法看起来比较的容易接受。google了一下nginx的平滑升级的办法。其主要使用到了如下几点技术:

1. 未设置FD_CLOEXEC 属性的文件描述符在fork以后会被child process继承,一般来说除了listen socket和log以外的fd都会设置这个属性
2. 把需要在child process里面使用的文件描述符通过env或者命令行参数在调用execv的时候传入新的process
3. 在启动child process之前,停止accept新的请求。
4. parent process一直服务到所有的请求执行完毕以后正常退出。
5. child process在收到传入的文件描述符(通常是用于listen的那个socket的fd)以后使用这个fd来做accept操作。

这个方法解决了socket连接的平滑升级,但是对于log的问题,我还不是特别肯定。如果parent process和child process都是用同一个log文件的描述符,那么必然会造成打印出的日志是混乱的。当然如果你的日志写入是严格按照一个信息块的方式写入的话(也就是不是几个字节几个自己的写入),而vfs_write又是对inode加锁的,那么虽然会有parent process和child process的日志混杂在一起,但是每一条日志内容本身应该是有完整信息的。(这个还需要核实)。

在这里有相关的测试代码

 

Written with StackEdit.

Categories: programming Tags:

program killed by signal 4

March 4th, 2014 No comments

一个跑在linux上的程序crash了,coredump的文件名显示是被signal 4干掉的。哎呀,第一次见到signal 4. 赶紧kill -l看了一下,原来4是指SIGILL,就是invalid instruction. 一般来说我会认为这个东西是stack buffer overflow。 对我们自己的程序来说最可能出错的地方就是打印日志的时候参数类型和format里面指定的类型不一致。特别是fmt=”%s”但是传入了一个sd::string。但是我记得以前这种问题貌似都不会是SIGILL,要么SIGABORT要么SIGSEGV。那么这次是咋回事呢。

上gdb,同时把symbol文件放上去了。看了一下callstack。



(gdb) bt

0x00000000004ab9cc in waitForResponse (this=0x7f12e80804d8, cseq=2)

0x0000000000525920 in SsServiceImpl::doCommit (ss=..., ctx=..., )

0x0000000000570ecd in SsStreamCommitRequest::run (this=0x7f125c001330)

0x00007f12f51318ca in SlaveThread::run (this=0x7f12e80260d0)

0x00007f12f5120fc2 in NativeThread::_execute (thread=0x7f12e80260d0)

0x00007f12f2dba851 in start_thread () from /lib64/libpthread.so.0

0x00007f12f30b767d in clone () from /lib64/libc.so.6

其中waitForResponse里面主要是等待一个信号量。检查其代码没有发现啥问题,而且该函数内部没有任何日志打印的调用。

在老半天没有进展的境况下,我尝试着看了一下反汇编的结果



(gdb) set disassembly-flavor intel

(gdb) disas

...

   0x00000000004ab9ca <+256>:   leave

   0x00000000004ab9cb <+257>:   ret

=> 0x00000000004ab9cc <+258>:   ud2a

   0x00000000004ab9ce <+260>:   mov    ebx,edx

   0x00000000004ab9d0 <+262>:   mov    r12,rax

   0x00000000004ab9d3 <+265>:   lea    rax,[rbp-0x40]

箭头所指的地方就是crash的点,但是,但是ud2a是个神马玩意儿,对汇编不甚了解啊。

还是求助于google大神吧。

一阵瞎找以后看到了这篇文章里面解释了gcc在遇到format里面的参数类型和传入参数类型不匹配的时候报了warning,但是同时可能会产生出ud2a之类的代码,让程序在运行时挂掉。

例如:



struct A {

    int a,b,c,d;

};

A a;

log.info("What a stupid error [%p]",a);

呵呵,我们的代码中就犯了这样的错误。

修改以后让同事们多加注意代码问题以及build时候的warning.



t.c:9:2: warning: format ‘%p’ expects argument of type ‘void *’, but argument 2 has type ‘struct AA’ [-Wformat=]

  log.info("What a stupid error [%p]\n",a);

  ^

Written with StackEdit.

Categories: Uncategorized Tags:

go的测试程序,与预期不符合。不知道是哪里理解不正确了

March 1st, 2014 No comments

在学习go的时候,按照[go语言编程]第94页的例子写了一个测试程序,如下

package main
import  (
    "fmt"
)
func Count( ch chan int, value int ) {
    ch <- value
    fmt.Println("Counting")
}
func main( ) {
    chs :=make( []chan int, 10 )
    for i := 0; i < 10; i++ {
        chs[i] = make(chan int) //请注意这一行
        go Count(chs[i],i)
    }
    var value int
    for _, ch := range(chs) {
        value = <-ch
        fmt.Printf("got value %d\n",value)
    }
}


但是运行的时候发现结果与我预期的不一样。本来我认为这个程序会打印10行Counting, 但实际上只有1行Counting出现。
于是我把标示那一行改成

chs[i] = make(chan int,1)

就这样,运行的时候就可以打印出10行Counting来了。是在不明白这个到底是咋回事啊。
继续修改有标示的那一行发现,如果make( chan int, 0 )或者make( chan int)那么就只会打印一行Counting.如果make的第二个参数>= 1,那么就会如预期一般打印10行Counting.
这让我更加糊涂了,按照书上所讲。make( chan int, x)的方式是建立一个有缓冲区且大小为x的channel。而且测试程序始终能够输出如下:

got value 0
got value 1
got value 2
got value 3
got value 4
got value 5
got value 6
got value 7
got value 8
got value 9

 

那么至少代表建立的10个 go routine是开始执行了的。看起来当make( chan int, 0)的时候只有一个Count的go routine在向channel写入数据以后被切换回去继续执行了,而剩下的Count的go routine都没有得到执行机会。不过这个不能解释为何make( chan int, x) x >=1 的时候所有的go routine就都机会执行完毕了。

嗯,我的go的版本是:

$ go version
go version go1.2 linux/amd64

Written with StackEdit.

Categories: programming Tags: ,

为何多线程程序占用这么多内存(linux)(续)

January 26th, 2014 2 comments

上一篇文章指出了在Centos6.4 x86_64下面多线程程序会相当占用内存资源。经过一番google和代码查看。终于知道了原来是glibc的malloc在这里捣鬼。请看developerworks该文章指出在glibc 2.10以上的版本会有这个问题,我的glibc版本是2.12

lrwxrwxrwx 1 root root 12 Oct 21 21:29 /lib64/libc.so.6 -> libc-2.12.so

glibc为了分配内存的性能的问题,使用了很多叫做arena的memory pool,缺省配置在64bit下面是每一个arena为64M,一个进程可以最多有 cores * 8个arena。假设你的机器是4核的,那么最多可以有4 * 8 = 32个arena,也就是使用32 * 64 = 2048M内存。 当然你也可以通过设置环境变量来改变arena的数量.例如export MALLOC_ARENA_MAX=1
hadoop推荐把这个值设置为4。当然了,既然是多核的机器,而arena的引进是为了解决多线程内存分配竞争的问题,那么设置为cpu核的数量估计也是一个不错的选择。设置这个值以后最好能对你的程序做一下压力测试,用以看看改变arena的数量是否会对程序的性能有影响。

后记:如果你打算在程序代码中来设置这个东西,那么可以调用mallopt(M_ARENA_MAX, xxx)来实现,不过很奇怪的是我在centos6.4上面居然看不到mallopt的man page,最后实在我的ubuntu 12.04的虚拟机上看到这个函数解释,而且里面并没有M_ARENA_MAX这个宏的解释。最后我实在glibc2.12的malloc/malloc.c的mALLOPt函数实现中才看到M_ARENA_MAX的。

Categories: programming Tags: , ,

记一个subclass gevent.Greenlet遇到的问题

January 22nd, 2014 No comments

打算子类化gevent.Greenlet,大致样子如下:


from gevent import Greenlet
class Runner( Greenlet ):
  def __init__(self):
      Greenlet.__init__(self)
  def _run( self ):
      self.run() # invoke the real run

class Downloader( Runner ):
  def __init__(self):
      Runner.__init__(self)
  def run(self):
      print "processing ..."

d = Downloader()
d.start()
d.join()

结果一运行就出错了,

processing ...
Traceback (most recent call last):
  File "test.py", line 30, in <module>
    d.join()
  File "/usr/local/lib/python2.7/dist-packages/gevent/greenlet.py", line 290, in join
    result = self.parent.switch()
  File "/usr/local/lib/python2.7/dist-packages/gevent/hub.py", line 331, in switch
    return greenlet.switch(self)
gevent.hub.LoopExit: This operation would block forever

一时间没有搞懂是怎么回事,于是google了一下,但是看起来别人的问题和我这个不相关。
于是看了一下greenlet.py里面的Greenlet的时间,结果发现一个问题Greenlet里面有一个run函数。所以我这里的Downloader申明run函数以后覆盖了Greenlet本身的run,结果就出现了你看到的这个错误。 所以解决方法也很简单,就是不要用run这个名字。

Categories: programming Tags: ,

为何多线程程序占用这么多内存(linux)

January 16th, 2014 No comments

我们的应用基本都是多线程的程序,程序启动以后如果在client发起命令之前内存占用量并不大,大概在几百兆左右,但是经过一段时间的运行呢,有的进程会占用到3~4G的内存。但是继续运行下去的话也不会继续增加了。由于增长到最高点以后内存使用量没有明显的大幅攀升,所以一直也没有特别的关注这个问题。最近呢,老大要看看这个问题究竟是咋回事,于是呢,有了本文。
一开始,我认为是程序逻辑分配了这么多内存。为了证明我的观点,我首先使用了ltrace。
ltrace -f -e “malloc,realloc,calloc,free” -p pid > trace 2>&1
上面是利用ltrace来检测程序对于malloc,realloc,calloc,free的调用情况。加上-f参数是因为是多线程的程序。当然,为了查看方便,我将上述命令的结果重定向到了一个文件。
当程序运行到了一个内存高点,例如3G的时候,取出trace文件,这个时候并没有让程序退出(否则可能会有很多free的调用)。trace文件大致是这个样子的:

[pid 21161] malloc(9) = 0x1b55a40
[pid 21161] malloc(9) = 0x1b58410
[pid 21161] free(0x1b55a40) =  <void>
于是,写了一个很简单的python脚本来分析程序的内存分配释放情况。最后得到的答案出乎我的意料。程序从启动稳定在300M左右到内存最高点期间的内存分配而没有释放的总量只有200k+,远小于1M。于是乎我怀疑自己统计错了。有重复做了几遍相同的操作,得到的答案都是一样的。

在多次使用ltrace无果以后,我开始怀疑程序内部是不是用到mmap之类的调用,于是使用strace对mmap,brk等系统函数的检测:
strace -f -e “brk,mmap,munmap” -p pid > trace 2>&1
测试方法同上,运行到内存高点以后查看strace文件。文件内容大致如下:
brk(0) = 0x6fd000
mmap(NULL, 4096, PROTREAD|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7f77f9853000
mmap(NULL, 82462, PROTREAD, MAPPRIVATE, 3, 0) = 0x7f77f9604000
我检查了一下trace文件也没有发现大量内存mmap动作,即便是brk动作引起的内存增长也不大。
于是乎我开始没有方向了 …
后来,我开始减少thread的数量开始测试,在测试的时候偶然发现一个很奇怪的现象。那就是如果进程创建了一个线程并且该线程运行以后内存使用量就会激增。我写了一个简单的程序,大致内容如下:

void* thread_run( void* ) {
    cout << "thread running,    VM SIZE:" << memusage() << " MB" << endl;
}

int main()
{
    cout << "process running,   VM SIZE:" << memusage() << " MB" << endl;

    pthread_t th;
    pthread_create(&th, 0, thread_run, 0);
    cout << "thread created,    VM SIZE:" << memusage() << " MB" << endl;

    pthread_join( th, 0 );

    cout << "after join thread, VM SIZE:" << memusage() << " MB" << endl;

    return 0;
}

以上调用的memusage()是用来获取当前进程的virtual memory usage的函数。
在我的机器上(CENTOS 6.4,X86-64)上运行结果如下:
process running, VM SIZE:13 MB
thread created, VM SIZE:23 MB
thread running, VM SIZE:87 MB
after join thread, VM SIZE:87 MB
也就是说,程序启动以后用了13M内存,创建了一个线程以后内存使用量增加了10M(这是因为64bit上缺省的线程stack size是10M,这个可以在创建线程的时候指定)。此时线程应该还没有运行,因为当前的主线程还没有被切换掉。当线程运行起来了以后,整个process的内存使用量立马又增加了64M。而且这个增加的64M内存在线程被join以后也没有被回收。

很神奇吧,反正我是没有反应过来是咋回事。接下来,让程序停在threadrun函数里面,然后使用pmap查看进程的内存情况。pmap -x的输出情况如下:


12346: ./test
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 8 8 8 r-x-- test
0000000000602000 4 4 4 rw--- test
000000000104e000 132 8 8 rw--- [ anon ]
0000003f71800000 128 104 0 r-x-- ld-2.12.so
0000003f71a1f000 4 4 4 r---- ld-2.12.so
0000003f71a20000 4 4 4 rw--- ld-2.12.so
0000003f71a21000 4 4 4 rw--- [ anon ]
0000003f72000000 1576 324 0 r-x-- libc-2.12.so
0000003f7218a000 2044 0 0 ----- libc-2.12.so
0000003f72389000 16 16 8 r---- libc-2.12.so
0000003f7238d000 4 4 4 rw--- libc-2.12.so
0000003f7238e000 20 16 16 rw--- [ anon ]
0000003f72400000 92 68 0 r-x-- libpthread-2.12.so
0000003f72417000 2048 0 0 ----- libpthread-2.12.so
0000003f72617000 4 4 4 r---- libpthread-2.12.so
0000003f72618000 4 4 4 rw--- libpthread-2.12.so
0000003f72619000 16 4 4 rw--- [ anon ]
0000003f72c00000 524 20 0 r-x-- libm-2.12.so
0000003f72c83000 2044 0 0 ----- libm-2.12.so
0000003f72e82000 4 4 4 r---- libm-2.12.so
0000003f72e83000 4 4 4 rw--- libm-2.12.so
0000003f7f000000 88 16 0 r-x-- libgccs-4.4.7-20120601.so.1
0000003f7f016000 2044 0 0 ----- libgccs-4.4.7-20120601.so.1
0000003f7f215000 4 4 4 rw--- libgccs-4.4.7-20120601.so.1
0000003f7fc00000 928 528 0 r-x-- libstdc++.so.6.0.13
0000003f7fce8000 2048 0 0 ----- libstdc++.so.6.0.13
0000003f7fee8000 28 28 28 r---- libstdc++.so.6.0.13
0000003f7feef000 8 8 8 rw--- libstdc++.so.6.0.13
0000003f7fef1000 84 12 12 rw--- [ anon ]
00007f7e4c000000 132 8 8 rw--- [ anon ]
00007f7e4c021000 65404 0 0 ----- [ anon ]
00007f7e50c2c000 4 0 0 ----- [ anon ]
00007f7e50c2d000 10260 28 28 rw--- [ anon ]
00007f7e51645000 12 8 8 rw--- [ anon ]
00007fffbb182000 84 12 12 rw--- [ stack ]
00007fffbb1ff000 4 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
total kB 89820 1260 188

请注意65404这一行,种种迹象表明,这个再加上它上面那一行(在这里是132)就是增加的那个64M)。后来增加thread的数量,就会有新增thread数量相应的65404的内存块。(后来发现可能不全是65404,我看到过65400,但是加上该行之上的哪一行数据都是64M)
而且我们看,65404这一行里面的该内存区域的权限,全部turn off,也就是该内存是不可读,不可写,不可执行。实在是不明白这个内存块有啥用处。

还有一点,就是估计到了一定thread的数量以后,增加量就不是64M了。我测试的时候发现创建了一个100个thread,所有的线程都不退出的话进程的内存总量是2999M。当然了如果线程一个跑完了再创建另外一个的话那么只有开始这个64M的增长量了。

不过到这里,我们已经知道了这个多出来64M内存并不是我们的程序本身的行为(当然我现在还并不知道到底是什么地方产生出来的)。也许这个ntpl的行为,也许这个是内核的行为,现在我还不清楚而已。这个留作以后的作业吧。

后记:
我今天对测试程序做了一个strace,得到了如下结果:
strace -f -e “brk,mmap,munmap,mprotect” test
brk(0) = 0x228f000
mmap(NULL, 4096, PROTREAD|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e30000
mmap(NULL, 82462, PROTREAD, MAPPRIVATE, 3, 0) = 0x7fab81e1b000
mmap(0x3f72000000, 3745960, PROTREAD|PROTEXEC, MAPPRIVATE|MAPDENYWRITE, 3, 0) = 0x3f72000000
mprotect(0x3f7218a000, 2093056, PROTNONE) = 0
mmap(0x3f72389000, 20480, PROT
READ|PROTWRITE, MAPPRIVATE|MAPFIXED|MAPDENYWRITE, 3, 0×189000) = 0x3f72389000
mmap(0x3f7238e000, 18600, PROTREAD|PROTWRITE, MAPPRIVATE|MAPFIXED|MAPANONYMOUS, -1, 0) = 0x3f7238e000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e1a000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e19000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e18000
mprotect(0x3f72389000, 16384, PROT
READ) = 0
mprotect(0x3f71a1f000, 4096, PROTREAD) = 0
munmap(0x7fab81e1b000, 82462) = 0
brk(0) = 0x228f000
brk(0x22b0000) = 0x22b0000
mmap(NULL, 99158576, PROT
READ, MAP_PRIVATE, 3, 0) = 0x7fab7bf87000

从上面没有看出应用程序有调用分配出64M左右的空间且权限是PROTNONE,或者更改某个约64M大小的区域的权限到PROTNONE的请求。难道这个6神秘的64M真的来自内核?看来这个问题只能说 “未完,待续 …”

Categories: programming Tags: , ,

putty无密码登陆linux box

January 16th, 2014 No comments

工作的时候很多时间都是使用putty登陆到linux机器进行操作,由于需要每次登陆都要输入密码,这多多少少有点烦人,况且又在局域网内部。所以自然想到了不输入密码使用putty登陆ssh server。
网上的方法很多,基本上都是一致的。
首先,使用putty的keygen工具puttygen.exe生成公钥和密钥。点击Generate然后在空白区域瞎乱动鼠标就行了。在生成公钥和密钥的时候我选择的类型是ssh-2 RSA,key长度是1024bits.生成以后保存private key文件。保存private key文件的时候不要输入密码。其实这个文件里面既有公钥又有密钥。当然你还可以单独保存一个公钥文件。
然后将上一步保存的公钥文件上传到你需要登陆的机器的用户目录下的~/.ssh目录。假设你的公钥文件叫做rsa_pub.现在使用命令
ssh-keygen -i -f rsa_
pub > rsa.pub
接着保存rsa.pub到authorized_keys文件就可以了。
cat rsa.pub >> authorized_
keys
然后启动putty,找到你登陆目标机器的那个保存的session,如果没有的话那么现在新建一个。找到connection->ssh->auth
在右边的Private key file authentication里面输入在第一步生成的private key的全路径。
照理,这个时候你使用putty登陆该linux机器的时候就无需输入密码了。但是我在使用的时候发现无法自动登陆成功。
putty上显示 “server refused our key”,于是按照askubuntu上的方法,解决了。(虽然我使用的linux server是centos的,但是大体上在这件事情上没有太大差别)该方法大致如下:
修改.ssh目录权限为700,也就是当前用户可读,写,执行
修改authorizedkeys的权限是600,也就是当前用户可读,写
修改/etc/ssh/sshdconfig,是下面这一句生效,也就是去掉行首的’#’
AuthorizedKeysFile .ssh/authorized_
keys
然后重启sshd
service sshd restart
就可以了。

其实linux下面需要无密码登陆到其他机器的做法也是一样的,首先用ssh-keygen生成一个公钥密钥对,然后上传公钥到目标机器的目标用户目录,后面的工作和上面一样的了。
ssh-keygen -t rsa.
同样的,在生成文件的时候不要输入密码

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: