本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/cplusplus-virtual-base-class/ 在C++的某些应用环境中,如果有多个派生类同时继承同一个基类,但只要想基类成员的一份拷贝,这样可以把对基类的继承声明为虚拟(virtual)的,以达到需要的目的,用 virtual 限定的继承即为虚继承,对应的基类即为虚基类,没有则为非虚基类。在派生类产生的对象中,同名的虚基类只产生一个虚基类对象,非虚基类则有各自的对象,需要注意虚基类和抽象类的区分。 另外特别需要注意的是一个基类即可以被用作虚基类,也可以被用作非虚基类,这纯粹看是否是虚继承(是否有 virtual 限定)。虚基类子对象是由最远派生类(也就是建立对象时所指定的类)的构造函数调用虚基类的构造函数进行初始化。如果一个派生类继承了多个基类,其中既有虚基类,也有非虚基类,则虚基类构造函数优先于非虚基类的构造函数,而不管实际书写顺序,而且如果该派生类的构造函数初始化列表中同时有对虚基类和非虚基类构造函数的调用,同样也是虚基类优先。 有关虚基类的其它说明请参考其它书籍咯,下面是测试的代码: 比较简单,Base2 和 Base3 分别派生自 Base,而且都是虚继承,Base4 同时派生自 Base2 和 Base3,但 Base3 前有 virtual,表示 Base4 对 Base3 是虚继承,Base4 对 Base2 是非虚继承。另外代码中比较特殊的一点是 Base 基类中去掉了默认构造函数,改为一个 int 参数的构造函数(第 8 行),因此 Base2 和 Base3 的构造函数的初始化列表中都需要增加对基类 Base 构造函数的调用,同样 Base4 需要调用 Base、Base2 和 Base3。 主程序中只是生成一个 Base4 的对象,上述代码的运行输出为: init Base 0x22ff34, value:22 init Base3 init Base2 destory Base 0x22ff34, value:22 由上面的输出可以验证虚基类的概念: 由于 Base2 和 Base3 都是对 Base 的虚继承,虽然 Base4 中分别调用了 Base2 和 Base3 的构造函数,但虚继承就表示基类 Base 只有一份拷贝,Base 只需要构造一次。另外由于 Base4 对 Base2 是非虚继承,对 Base3 是虚继承,尽管 Base2 看起来在前面调用,但由于 Base3 对 Base4 是虚基类,它的构造函数就需要先于 Base2 对调用,因此 ”init Base3“ 在 ”init Base2“ 前面。 接着对代码稍作改动,将 Base2 和 Base3 都改为非虚继承,Base4 中去掉对 Base 基类构造函数的调用: 第22行:class Base2 : public Base 第31行:class Base3 : public Base 第43行: Base4(int val) : Base2(val), Base3(val) 编译运行输出,结果为: init Base 0x22ff38, value:22 init Base3 init Base 0x22ff34, value:22 init Base2 destory Base 0x22ff34, value:22 destory Base 0x22ff38, value:22 可以看到由于 Base2 和 Base3 都不是非虚继承,Base 就被构造了两次。 在上面的基础上再做个小改动,把 Base4 对 Base3 的派生也改为非虚继承: 第40行:class Base4 : public Base2, public Base3 再次运行输出,结果即为: init Base 0x22ff30, value:22 init Base2 init Base 0x22ff34, value:22 init Base3 destory Base 0x22ff34, value:22 destory Base 0x22ff30, value:22 由于 Base4 对 Base2 和 Base3 都不是虚继承了,对 Base2 和 Base3 的构造函数调用就按实际顺序了。 这样虚基类的概念就算是比较清楚咯。 ^_^
Category: Technology
vector的push_back拷贝构造和空间占用分析
本文同步自:https://zohead.com/archives/vector-push-back-space-copy/ 这两天在实际程序中使用 STL 的 vector push_back 类对象时出现问题,偶尔发现 vector 在 push_back 时的调用类对象的拷贝构造函数和析构函数有点特别,简单做下分析。 程序代码: 功能很简单,main 中定义一个 sss 类对象和对应的 vector,然后在循环中改类成员的值,并依次 push_back 到 vector 中,类的构造函数、析构函数、拷贝构造函数中都加了对应的打印输出。循环运行了5次,往 vector 中增加了5个类成员。 实际运行输出如下: ---init sss 0x22ff20, value:11 ---copy 0x22ff20 to 0x5d2a58 size: 1, capacity: 1 ---copy 0x5d2a58 to 0x5d2ad8 ---copy 0x22ff20 to 0x5d2adc ---destory sss 0x5d2a58, value:12 size: 2, capacity: 2 ---copy 0x5d2ad8 to 0x5d2ae8 ---copy 0x5d2adc to 0x5d2aec ---copy 0x22ff20 to 0x5d2af0 ---destory sss 0x5d2ad8, value:12 ---destory sss 0x5d2adc, value:13 size: 3, capacity: 4 ---copy 0x22ff20 to 0x5d2af4 size: 4, capacity: 4 ---copy 0x5d2ae8 to 0x5d2b00 ---copy 0x5d2aec to 0x5d2b04 ---copy 0x5d2af0 to 0x5d2b08 ---copy 0x5d2af4 to 0x5d2b0c ---copy 0x22ff20 to 0x5d2b10 ---destory sss 0x5d2ae8, value:12 ---destory sss 0x5d2aec, value:13 ---destory sss 0x5d2af0, value:14 ---destory sss 0x5d2af4, value:15 size: 5, capacity: 8 ---destory sss 0x5d2b00, value:12 ---destory sss 0x5d2b04, value:13 ---destory sss 0x5d2b08, value:14 ---destory sss 0x5d2b0c, value:15 ---destory sss 0x5d2b10, value:16 ---destory sss 0x22ff20, value:16 结果分析: vector 每次调用 push_back 时都会拷贝一个新的参数指定的 sss 类对象,这会调用 sss 的拷贝构造函数,第一次的 copy 正常,而且 vector 的实际容量也由 0 变为 1。 第二次调用 push_back,通过输出会发现调用了两次拷贝构造函数,一次析构函数,原来 vector 此时判断容量不够,将容量扩大为原来的两倍,变为 2,并将原来的元素再次拷贝一份存放到新的内存空间,然后拷贝新加的类对象,最后再释放原来的元素。 第三次调用 push_back 时,vector 自动扩大为4,因此拷贝构造函数调用了3次,析构函数调用了2次,程序最终退出了时就析构了 5 次加本身的 sss 类对象一共 6 次。 参考: 由此看来,vector 的 push_back 在发现空间不足时自动将空间以 2 的指数增长:0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 … 查找资料后得知,如此设计的主要目的是为了尽可能的减小时间复杂度;如果每次都按实际的大小来增加 vector 的空间,会造成时间复杂度很高,降低 push_back 的速度。 另外关于 push_back 为什么会执行拷贝构造函数,push_back 的原型为: void push_back(const _Ty& _Val) 参数是以引用方式传递,按说不会拷贝,但 push_back 实际实现中判断空间不足时是调用 insert 函数添加元素: void push_back(const _Ty& _Val) { // insert element at end if (size() < capacity()) #if _HAS_ITERATOR_DEBUGGING { // room at end, construct it there _Orphan_range(_Mylast, _Mylast); _Mylast = _Ufill(_Mylast, 1, _Val); } #else /* _HAS_ITERATOR_DEBUGGING */ _Mylast = _Ufill(_Mylast, 1, _Val); #endif /* _HAS_ITERATOR_DEBUGGING */ else insert(end(), _Val); } 更新: 2012-05-10: 近期在 Visual Studio 2010 中发现 vector 的实际空间增加顺序为:1 - 2 - 3 - 4 […]
解决socket在execl之后被自动继承的问题
本文同步自:https://zohead.com/archives/close-socket-after-execl/ Linux下的进程在创建子进程(fork)后,子进程会自动继承父进程的文件描述符,这种机制在没有 execl 运行时一般都没有问题,而且也比较方便在子进程中直接使用父进程中打开的文件描述符,但如果子进程通过 execl 类函数运行新的程序时,这些继承的文件描述符却没有被关闭,导致 execl 之后的程序中也有父进程中文件描述符的拷贝,有些情况就会出现错误。 由于 socket 也在这些文件描述符范围内,本文简单说下解决 socket 被自动继承的几种方法。 首先是测试程序,头文件就没列出来了。 父进程和子进程分别 sleep 10秒钟,方便外部查看打开的文件描述符,编译运行程序,使用 lsof 命令就可以看到子进程在 execl 之后的 sleep 进程中也打开了父进程的 socket 描述符(UDP连接),这不是我们想要的效果滴: [root@localhost ~]# lsof -c sleep COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sleep 5750 root cwd DIR 8,1 4096 8145 /root sleep 5750 root rtd DIR 8,1 4096 2 / sleep 5750 root txt REG 8,1 27880 456104 /bin/sleep sleep 5750 root mem REG 8,1 155696 301801 /lib64/ld-2.12.so sleep 5750 root mem REG 8,1 1912928 301802 /lib64/libc-2.12.so sleep 5750 root mem REG 8,1 99158752 391655 /usr/lib/locale/locale-archive sleep 5750 root 0u CHR 136,1 0t0 4 /dev/pts/1 sleep 5750 root 1u CHR 136,1 0t0 4 /dev/pts/1 sleep 5750 root 2u CHR 136,1 0t0 4 /dev/pts/1 sleep 5750 root 3u IPv4 15022 0t0 UDP *:5555 解决办法有如下几种: 1、比较死板的方法,在子进程 fork 之后关闭所有的 socket 描述符,在 execl 之前加上: int fd = 0, fdtablesize = 0; for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) { // close all socket handle inherited from parent process if (isfdtype(fd, S_IFSOCK)) { close(fd); } } 这种处理比较彻底,即使子进程没有 execl 也会关闭继承的 socket 句柄,如果要关闭从父进程继承的所有句柄,去掉 isfdtype 判断即可。 2、使用 fcntl 为父进程 socket 设置 O_CLOEXEC 标志,表示此 socket 在 execl 之前自动关闭(注意并非在 fork 之后),在 socket 创建完成之后加上: ret = fcntl(sock, F_GETFD); ret = fcntl(sock, F_SETFD, ret | FD_CLOEXEC); 3、Linux 2.6.27 之后创建 socket 时开始支持 SOCK_CLOEXEC 参数,与上面的效果相似,只要改创建 socket 的地方: sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP); 另外 Linux 2.6.27 之后创建 socket 另外支持 SOCK_NONBLOCK 参数,如果设置此参数,socket 自动创建为非阻塞式。 以上三种办法均在 RHEL 6.1 64位系统上编译测试通过,有任何问题欢迎交流,玩的开心 ^_^
rsync在 Linux/cygwin/msys 环境下的备份性能对比
本文博客链接:https://zohead.com/archives/rsync-performance-linux-cygwin-msys/ rsync是一个开源免费的文件同步和备份工具,可用于本地备份,本地与远程服务器之间的备份,可以实现增量和差异备份,而且由于比较好的算法,在文件备份速度上也相对其它一些文件备份工具有明显的优势。 但 rsync 一直以来没有 Windows 下的原生客户端,都是基于 cygwin 环境实现,实际备份性能会受一些影响,近日看到 rsync 的 基于 MSYS 的 Win32 原生客户端已经被 port 出来,故简单做下性能对比测试。 测试环境: rsync服务器为 RHEL5 Linux 64bit,8个SATA盘的RAID0做下层存储,采用单千兆网络和千兆交换机 rsync客户端为:RHEL5 Linux 64bit,Windows 2003 Enterprise 32bit 测试时 rsync 均通过匿名方式访问,不经过SSH做用户验证,由于考虑到测试的 rsync 客户端的系统盘速度有瓶颈,客户端文件读写都通过内存文件系统来实现(Linux 上使用 tmpfs,Windows 上使用 ImDisk 模拟内存盘)。 使用同样的客户端主板分别在 Linux 和 Windows 内存中产生 1.5GB 的测试文件,然后通过 rsync 客户端进行备份到服务器(写操作)和从服务器上恢复(读操作)的操作。 备份命令示例: rsync -hv x.dat 192.168.1.125::rsync0/ 测试软件列表: 标准 Linux rsync 客户端(RHEL 5 系统自带) cygwin rsync 客户端 MSYS rsync 客户端 http://sourceforge.net/projects/mingw/files/MSYS/Extension/rsync/ RsyncWin32 客户端 http://sourceforge.net/projects/rsyncwin32/ 测试结果: 测试软件 写性能(MB/s) 读性能(MB/s) Linux rsync 105.27 105.28 cygwin rsync 76.22 64.49 MSYS rsync 7.98 8.14 RsyncWin32 76.72(出现错误) 38.50(出现错误) 从测试结果看,由于 rsync 本身面向类 Linux 环境开发,在 Linux 系统中有着非常好的性能,cygwin rsync 与 Linux 相比有一定差距,但实际使用中还是比较稳定的,而 MSYS rsync 还处于测试阶段,虽然没有出现备份错误,但在千兆网络环境下性能非常差,RsyncWin32 则相对而言问题比较多,备份过程中甚至会出现备份错误。 综上看来,目前在 Windows 上使用 cygwin rsync 做备份客户端仍然算是比较好的解决方案,MSYS rsync 的问题可以啥时候有空再看看咯。
Linux下出现 pam_get_item 的原因分析
本文博客链接:https://zohead.com/archives/linux-pam-get-item-error-library/ 近日在将一个 RHEL6 上的服务程序移植到老的 Fedora Core 2 Linux 上时出现了一些问题,该程序的功能为预先加载几个动态库,这几个动态库中再需要根据不同的需求做 PAM 用户验证操作,程序在 RHEL6 上编译和使用都没有问题,换到 Fedora Core 2 环境之后,编译时一切正常,但在运行时出现比较奇怪的 pam_get_item 错误: pam: PAM [dlerror: /lib/security/../../lib/security/pam_env.so: undefined symbol: pam_get_item] pam: PAM [dlerror: /lib/security/../../lib/security/pam_unix.so: undefined symbol: pam_get_item] pam: PAM [dlerror: /lib/security/../../lib/security/pam_succeed_if.so: undefined symbol: pam_get_item] … 程序所加载的动态库中使用的 PAM 配置文件 /etc/pam.d/system-auth 里就用到了 pam_env.so、pam_unix.so、pam_succeed_if.so 等几个 PAM 模块。 但实际系统中还有很多服务都使用 /etc/pam.d/system-auth 这个 PAM 配置来进行用户验证,都能正常工作,因此做下简单分析。 由于此服务程序太复杂,就打定写个简单的模拟程序来进行分析。 1、动态库部分(checkpam.c): PAM 验证的实现,调用 PAM 函数进行验证,具体 PAM 函数的调用就不详细写出了。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <security/pam_appl.h> int check_pam(const char * user, const char * pass) { int ret = -1; pam_start("system-auth", user, …, …); … // 实际 PAM 代码省略,成功返回0,失败返回1 // … pam_end(…); return ret; } 和实际服务程序一致,通过 system-auth PAM 配置进行验证,编译生成动态库: cc -c checkpam.c cc -shared -fPIC -lpam -o libcheckpam.so checkpam.o ldd libcheckpam.so,查看此动态库的依赖列表: [root@linux root]# ldd libcheckpam.so linux-gate.so.1 => (0x00c67000) libpam.so.0 => /lib/libpam.so.0 (0x0025b000) libc.so.6 => /lib/tls/libc.so.6 (0x0041d000) libdl.so.2 => /lib/libdl.so.2 (0x00153000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00876000) 2、主程序部分(pamtest.c): 加载动态库,调用 PAM 进行用户验证,比较丑陋,只需能验证,HOHO。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> int main(int argc, char ** argv) { int ret = 0; void * lpso = NULL; int (* lpfunc)(const char *, const char *) = NULL; lpso = dlopen("./libcheckpam.so", RTLD_LAZY); if (lpso == NULL) { printf("Library load failed: %s.\n", dlerror()); return 1; } lpfunc = dlsym(lpso, "check_pam"); if (lpfunc == NULL) { printf("Load symbol failed: %s.\n", dlerror()); dlclose(lpso); return 2; } ret = lpfunc(argv[1], argv[2], NULL); dlclose(lpso); printf("pam ret: %d.\n", ret); return ret; } 功能很简单,dlopen 加载刚才生成的 libcheckpam.so,然后 dlsym 找到 check_pam 函数,接着调用函数进行用户验证。 编译程序: cc -o pamtest pamtest.c -ldl ldd pamtest,查看程序的依赖列表,可以看到此时已不需要 pam 库,和实际使用的服务程序的实现方式一致了: [root@linux root]# ldd pamtest linux-gate.so.1 => (0x00631000) libdl.so.2 => /lib/libdl.so.2 (0x009ac000) libc.so.6 => /lib/tls/libc.so.6 (0x0088f000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00876000) […]
Linux advisory and mandatory lock with fcntl
本文博客链接:https://zohead.com/archives/linux-fcntl-advisory-mandatory-lock/ 近日小温下APUE,发现Linux下的 fcntl 实现强制锁的功能好像都没试验过,简单做个测试。 首先用 fcntl 实现建议锁(Advisory locking),比较简单,贴个最简单的代码: 关键的几句已高亮显示,用 F_SETLKW 等待写的锁,很好测试,分别开一个终端运行一次就可以测试出来。 备注: Linux 同时实现了 POSIX 的 fcntl 锁函数,BSD 的 flock 函数,SVR4 的 lockf 函数,这些默认都是建议锁。 这个简单的实现只是建议锁,需要每个对文件的操作的进程都遵循同样的锁操作才能起作用,这些进程称为合作进程(Cooperative processes),但实际使用时会有例外的情况,如果另外开一个终端窗口直接写 test.file 文件,会发现可以直接写,因为这种写没有锁操作,不是合作进程,这种情况就需要用强制锁(Mandatory locking)了。 强制锁的实际代码和上面的完全一样,不过在 Linux 上需要做一些改动: 1、挂载文件系统时需要加 mand 参数在文件系统上启用强制锁支持,比较新的 Linux kernel 里已经基本在所有文件系统上都实现了; 2、去掉程序的组执行权限; 3、增加程序的设置组ID权限。 第二项和第三项在普通情况下实际上是自相矛盾的,所以 Linux 就用这种特殊情况就表示启用强制锁(Mandatory locking)了。 备注: 1、使用 BSD 的 flock 函数时不能使用强制锁,见 Linux kernel 源码下 mandatory-locking.txt 中的说明: Mandatory locks can only be applied via the fcntl()/lockf() locking interface - in other words the System V/POSIX interface. BSD style locks using flock() never result in a mandatory lock. 2、如果一个文件被某进程强制锁住,另一个进程通过 creat 函数或者 open 时加 O_TRUNC 参数,都会导致函数调用失败; 3、强制锁在不同的 UNIX 及 类UNIX 系统中实现不同,第二个备注中的 O_TRUNC 参数便是 Linux kernel 和 HP-UX 不同的地方; 4、tmpfs、nfs 也支持强制锁,可以用这两个文件系统方便测试; 5、删除文件(unlink操作)再重新创建文件,会导致强制锁失效; 6、Linux kernel 的强制锁的实现并不完全可靠(感觉白写了,哈哈,实际情况还要做处理),见 man fcntl 的说明。 强制锁的实际操作: mount -t tmpfs -o size=10m,mand tmpfs /mnt cp test /mnt cd /mnt chmod g-x test chmod g+s test 运行 test 程序,同时开另一个终端,直接用 echo 读写 test 文件,会发现强制锁已经起作用。
Thunderbird(Firefox)病毒扫描原理分析及禁用方法
本文博客链接:https://zohead.com/archives/thunderbird-virus-scan-analyse-disable/ 笔者现在浏览网页和电子邮件全部都用 Mozilla 系的 Firefox 和 Thunderbird 了,这俩软件算是我的 Windows 系统中启动之后运行时间最长的两个应用程序,都非常好用,Thunderbird 虽然没有 Firefox 那么受欢迎,也算比 Outlook 好用很多(仅仅个人意见 ^_^),同时都支持扩展和附加组件。 最近发现有一点需要吐槽的是 Thunderbird 和 Firefox 默认下载附件或者下载东西完成之后都需要运行杀毒软件进行病毒扫描,而且附件比较大时还相当缓慢,对于我这种不想装杀毒软件的人或者受不了速度影响的人会有点感官影响,HOHO,寻办法禁用之。 1、Firefox 先说说比较简单的 Firefox 下禁用病毒扫描的方法(可以直接 百度、GG 到): 地址栏中输入“about:config”,忽略坑爹的 “保修提示”,在过滤器中中输入“browser.download.manager.scanWhenDone”,通常找到的第一个就是要改的配置,双击将值改为 false,随便下个软件,马上就可以看到效果。 2、老版本 Thunderbird 接下来 Thunderbird 的禁用方法就没有直接的了,老版本的 Thunderbird 似乎也可以在 “about:config” 设置同样的值,但新版本的 “about:config” 中已经没有这个配置,注意 Thunderbird 的 ”about:config“ 不是直接输入了,需要打开首选项,进入 “高级” 选项卡,点 ”常规“,然后打开 ”配置编辑器“ 就是想要的效果。 如果直接要新版本的修改方法,直接忽略分析部分直接跳到最后。 3、Thunderbird 扫描原理分析 无奈新版本的 Thunderbird 在搜索了 N 次之后找不到解决办法,祭出查源代码大法(一般都比较好使,哈哈),不想下那么大的源码包,直接在 Mozilla 官方网站上找 Thunderbird 的在线版本库,找到了 Mozilla Cross-Reference 做参考: http://mxr.mozilla.org/ 进入 Comm. Central 仓库: http://mxr.mozilla.org/comm-central/ 搜索 virus scan 之类的就能看到下面几个代码段,稍加分析,大略知道 Thunderbird 和 Firefox 的病毒扫描原理,简单分析下。 首先在 nsDownloadScanner.cpp 下找到扫描的初始化部分: 下面是同一文件中的比较重要的 IsAESAvailable 的实现: 对是否支持扫描的判断很简单,判断 IAttachmentExecute COM组件是否可用,此组件在 Windows XP SP2 之后的系统中就自带,另外如果在更老的系统中则使用 IOfficeAntiVirus COM组件。 下面是下载管理器 nsDownloadManager.cpp 的处理: 可以看到第12行处会启动 nsDownloadScanner 进行下载附件的扫描处理。 4、组策略处理方法(不完美) 经过粗略了解之后便是解决方法咯,首先看能否对 Thunderbird 禁用 IAttachmentExecute COM组件。 有关 IAttachmentExecute COM组件的配置见这篇文章通过组策略设置“附件管理器”,可以参考这篇文章对整个系统的附件扫描机制做设置: http://edu.kafan.cn/html/hips/11386.html 简单流程: 开始 - 运行,输入:gpedit.msc,打开组策略编辑器,定位到 用户配置 - 管理模板 - Windows组件 - 附件管理器,截图如下,看下应该就能明白 可以看到这个设置是全局,是一种解决方法,但会对其它需要扫描的程序(例如:Outlook)也会造成影响。 5、完美禁用 Thunderbird 病毒扫描 再经过搜寻源代码之后终于找到只改变 Thunderbird 扫描病毒配置的方法,原来 Thunderbird 可能出于安全的考虑将 ”browser.download.manager.scanWhenDone“ 配置从 ”about:config“ 配置编辑器中移除掉了,但这个配置功能并没有真正去掉,我们还有直接修改 Thunderbird 安装目录的配置脚本的方法,进入 Thunderbird 安装目录的 defaults\pref 下,打开 channel-prefs.js 文件编辑,增加扫描的配置。 例如我的配置脚本的路径为:C:\Program Files\Mozilla Thunderbird\defaults\pref\channel-prefs.js,打开该文件,增加一行: pref("browser.download.manager.scanWhenDone", false); 即可禁用下载附件时的病毒扫描,需要再启用时恢复即可,贴图: oops,写累了,以上只是粗略的分析,如果有任何错误或者更好的修改方法请PM或评论。 ^_^