注    册
密 码 忘记密码
保存密码         取消

个人资料

昵称: boldeagle
姓名:
性别:
生日: 1980-1-1
星座:
学历:
院校:
行业:
头衔:
位置: 中国-北京-西城区
家乡: 其它-北京-西城区
个人标签:
个人简介:
!
座右铭:
自信并快乐着

详细资料..

公告

暂无公告

统计

统计中,请等候...

统计中,请等候...

我的工具

我的广告

日志

libtool的工作原理(转)

分类:编程点滴

libtool 是一个通用库支持脚本,将使用动态库的复杂性隐藏在统一、可移植的接口中;使用libtool的标准方法,可以在不同平台上创建并调用动态库。可以认为libtool是gcc的一个抽象,其包装了gcc(或者其他的编译器),用户无需知道细节,只要告诉libtool需要编译哪些库即可,libtool将处理库的依赖等细节。libtool只与后缀名为lo、la为的libtool文件打交道。

libtool主要的一个作用是在编译大型软件的过程中解决了库的依赖问题;将繁重的库依赖关系的维护工作承担下来,从而释放了程序员的人力资源。libtool提供统一的接口,隐藏了不同平台间库的名称的差异等细节,生成一个抽象的后缀名为la高层库libxx.la(其实是个文本文件),并将该库对其它库的依赖关系,都写在该la的文件中。该文件中的dependency_libs记录该库依赖的所有库(其中有些是以.la文件的形式加入的);libdir则指出了库的安装位置;library_names记录了共享库的名字;old_library记录了静态库的名字。

当编译过程到link阶段的时候,如果有下面的命令:

$libtool --mode=link gcc -o myprog -rpath /usr/lib –L/usr/lib –la

libtool会到/usr/lib路径下去寻找liba.la,然后从中读取实际的共享库的名字(library_names中记录了该名字,比如liba.so)和路径(lib_dir中记录了,比如libdir=’/usr/lib’),返回诸如/usr/lib/liba.so的参数给激发出的gcc命令行。

如果liba.so依赖于库/usr/lib/libb.so,则在liba.la中将会有dependency_libs=’-L/usr/lib -lb’或者dependency_libs=’/usr/lib/libb.la’的行,如果是前者,其将直接把“-L/usr/lib –lb”当作参数传给gcc命令行;如果是后者,libtool将从/usr/lib/libb.la中读取实际的libb.so的库名称和路径,然后组合成参数“/usr/lib/libb.so”传递给gcc命令行。

当要生成的文件是诸如libmylib.la的时候,比如:

$libtool --mode=link gcc -o libmylib.la -rpath /usr/lib –L/usr/lib –la

其依赖的库的搜索基本类似,只是在这个时候会根据相应的规则生成相应的共享库和静态库。

注意:libtool在链接的时候只会涉及到后缀名为la的libtool文件;实际的库文件名称和库安装路径以及依赖关系是从该文件中读取的。

2 为何使用 -Wl,--rpath-link -Wl,DIR?
使用libtool解决编译问题看上去没什么问题:库的名称、路径、依赖都得到了很好的解决。但下结论不要那么着急,一个显而易见的问题就是:并不是所有的库都是用libtool编译的。

比如上面那个例子,

$libtool --mode=link gcc -o myprog -rpath /usr/lib –L/usr/lib –la

如果liba.so不是使用libtool工具生成的,则libtool此时根本找不到liba.la文件(不存在该文件)。这种情况下,libtool只会把“–L/usr/lib –la”当作参数传递给gcc命令行。

考虑以下情况:要从myprog.o文件编译生成myprog,其依赖于库liba.so(使用libtool生成),liba.so又依赖于libb.so(libb.so的生成不使用libtool),而且由于某种原因,a对b的依赖并没有写入到liba.la中,那么如果用以下命令编译:

$libtool --mode=link gcc -o myprog -rpath /usr/lib –L/usr/lib –la

激发出的gcc命令行类似于下面:

  gcc –o myprog /usr/lib/liba.so 

由于liba.so依赖于libb.so(这种依赖可以用readelf读liba.so的ELF文件看到),而上面的命令行中,并没有出现libb.so,于是,可能会出现问题。

  说“可能”,是因为如果在本地编译的情况下,gcc在命令行中找不到一个库(比如上面的liba.so)依赖的其它库(比如libb.so),链接器会按照某种策略到某些路径下面去寻找需要的共享库:

1. 所有由'-rpath-link'选项指定的搜索路径.

2. 所有由'-rpath'指定的搜索路径. '-rpath'跟'-rpath_link'的不同之处在于,由'-rpath'指定的路径被包含在可执行文件中,并在运行时使用, 而'-rpath-link'选项仅仅在连接时起作用. 

3. 在一个ELF系统中, 如果'-rpath'和'rpath-link'选项没有被使用, 会搜索环境变量'LD_RUN_PATH'的内容.它也只对本地连接器起作用.

4. 在SunOS上, '-rpath'选项不使用, 只搜索所有由'-L'指定的目录.

5. 对于一个本地连接器,环境变量'LD_LIBRARY_PATH'的内容被搜索.

6. 对于一个本地ELF连接器,共享库中的`DT_RUNPATH'和`DT_RPATH'操作符会被需要它的共享库搜索. 如果'DT_RUNPATH'存在了, 那'DT_RPATH'就会被忽略.

7. 缺省目录, 常规的,如'/lib'和'/usr/lib'.

8. 对于ELF系统上的本地连接器, 如果文件'/etc/ld.so.conf'存在, 这个文件中有的目录会被搜索.

从以上可以看出,在使用本地工具链进行本地编译情况下,只要库存在于某个位置,gcc总能通过如上策略找到需要的共享库。但在交叉编译下,上述八种策略,可以使用的仅仅有两个:-rpath-link,-rpath。这两个选项在上述八种策略当中优先级最高,当指定这两个选项时,如果链接需要的共享库找不到,链接器会优先到这两个选项指定的路径下去搜索需要的共享库。通过上面的描述可以看到:-rpath指定的路径将被写到可执行文件中;-rpath-link则不会;我们当然不希望交叉编译情况下使用的路径信息被写进最终的可执行文件,所以我们选择使用选项-rpath-link。

  gcc的选项“-Wl,--rpath-link –Wl,DIR”会把-rpath-link选项及路径信息传递给链接器。回到上面那个例子,如果命令行中没有出现libb.so,但gcc指定了“-Wl,--rpath-link –Wl,DIR”,则链接器找不到libb.so的时候,会首先到后面-rpath-link指定的路径去寻找其依赖的库。此处我们使用的编译命令的示例是使用unicore平台的工具链。

$ unicore32-linux-gcc –o myprog /usr/lib/liba.so

-Wl,--rpath-link -Wl,/home/UNITY_float/install/usr/lib

这样,编译器会首先到“/home/UNITY_float/install/usr/lib”下面去搜索libb.so

  libtool如何把选项“-Wl,--rpath-link –Wl,DIR”传递给gcc?libtool中有一个变量“hardcode_libdir_flag_spec”,该变量本来是传递“-rpath”选项的,但我们可以修改它,添加我们需要的路径,传递给unicore32-linux-gcc。

  “hardcode_libdir_flag_spec”原来的定义如下:

hardcode_libdir_flag_spec="${wl}--rpath ${wl}$libdir"

我们修改后的定义如下:

hardcode_libdir_flag_spec="${wl}—rpath-link ${wl}$libdir

-Wl,--rpath-link -Wl,/home/UNITY_float/install/usr/lib

  -Wl,--rpath-link -Wl,/home/UNITY_float/install/usr/X11R6/lib "

这样,当libtool在“--mode=link”的模式下,就会把选项“-Wl,--rpath-link –Wl,DIR”传递给gcc编译器了。
libtool 是一个通用库支持脚本,将使用动态库的复杂性隐藏在统一、可移植的接口中;使用libtool的标准方法,可以在不同平台上创建并调用动态库。可以认为libtool是gcc的一个抽象,其包装了gcc(或者其他的编译器),用户无需知道细节,只要告诉libtool需要编译哪些库即可,libtool将处理库的依赖等细节。libtool只与后缀名为lo、la为的libtool文件打交道。

libtool主要的一个作用是在编译大型软件的过程中解决了库的依赖问题;将繁重的库依赖关系的维护工作承担下来,从而释放了程序员的人力资源。libtool提供统一的接口,隐藏了不同平台间库的名称的差异等细节,生成一个抽象的后缀名为la高层库libxx.la(其实是个文本文件),并将该库对其它库的依赖关系,都写在该la的文件中。该文件中的dependency_libs记录该库依赖的所有库(其中有些是以.la文件的形式加入的);libdir则指出了库的安装位置;library_names记录了共享库的名字;old_library记录了静态库的名字。

当编译过程到link阶段的时候,如果有下面的命令:

$libtool --mode=link gcc -o myprog -rpath /usr/lib –L/usr/lib –la

libtool会到/usr/lib路径下去寻找liba.la,然后从中读取实际的共享库的名字(library_names中记录了该名字,比如liba.so)和路径(lib_dir中记录了,比如libdir=’/usr/lib’),返回诸如/usr/lib/liba.so的参数给激发出的gcc命令行。

如果liba.so依赖于库/usr/lib/libb.so,则在liba.la中将会有dependency_libs=’-L/usr/lib -lb’或者dependency_libs=’/usr/lib/libb.la’的行,如果是前者,其将直接把“-L/usr/lib –lb”当作参数传给gcc命令行;如果是后者,libtool将从/usr/lib/libb.la中读取实际的libb.so的库名称和路径,然后组合成参数“/usr/lib/libb.so”传递给gcc命令行。

当要生成的文件是诸如libmylib.la的时候,比如:

$libtool --mode=link gcc -o libmylib.la -rpath /usr/lib –L/usr/lib –la

其依赖的库的搜索基本类似,只是在这个时候会根据相应的规则生成相应的共享库和静态库。

注意:libtool在链接的时候只会涉及到后缀名为la的libtool文件;实际的库文件名称和库安装路径以及依赖关系是从该文件中读取的。

2 为何使用 -Wl,--rpath-link -Wl,DIR?
使用libtool解决编译问题看上去没什么问题:库的名称、路径、依赖都得到了很好的解决。但下结论不要那么着急,一个显而易见的问题就是:并不是所有的库都是用libtool编译的。

比如上面那个例子,

$libtool --mode=link gcc -o myprog -rpath /usr/lib –L/usr/lib –la

如果liba.so不是使用libtool工具生成的,则libtool此时根本找不到liba.la文件(不存在该文件)。这种情况下,libtool只会把“–L/usr/lib –la”当作参数传递给gcc命令行。

考虑以下情况:要从myprog.o文件编译生成myprog,其依赖于库liba.so(使用libtool生成),liba.so又依赖于libb.so(libb.so的生成不使用libtool),而且由于某种原因,a对b的依赖并没有写入到liba.la中,那么如果用以下命令编译:

$libtool --mode=link gcc -o myprog -rpath /usr/lib –L/usr/lib –la

激发出的gcc命令行类似于下面:

  gcc –o myprog /usr/lib/liba.so 

由于liba.so依赖于libb.so(这种依赖可以用readelf读liba.so的ELF文件看到),而上面的命令行中,并没有出现libb.so,于是,可能会出现问题。

  说“可能”,是因为如果在本地编译的情况下,gcc在命令行中找不到一个库(比如上面的liba.so)依赖的其它库(比如libb.so),链接器会按照某种策略到某些路径下面去寻找需要的共享库:

1. 所有由'-rpath-link'选项指定的搜索路径.

2. 所有由'-rpath'指定的搜索路径. '-rpath'跟'-rpath_link'的不同之处在于,由'-rpath'指定的路径被包含在可执行文件中,并在运行时使用, 而'-rpath-link'选项仅仅在连接时起作用. 

3. 在一个ELF系统中, 如果'-rpath'和'rpath-link'选项没有被使用, 会搜索环境变量'LD_RUN_PATH'的内容.它也只对本地连接器起作用.

4. 在SunOS上, '-rpath'选项不使用, 只搜索所有由'-L'指定的目录.

5. 对于一个本地连接器,环境变量'LD_LIBRARY_PATH'的内容被搜索.

6. 对于一个本地ELF连接器,共享库中的`DT_RUNPATH'和`DT_RPATH'操作符会被需要它的共享库搜索. 如果'DT_RUNPATH'存在了, 那'DT_RPATH'就会被忽略.

7. 缺省目录, 常规的,如'/lib'和'/usr/lib'.

8. 对于ELF系统上的本地连接器, 如果文件'/etc/ld.so.conf'存在, 这个文件中有的目录会被搜索.

从以上可以看出,在使用本地工具链进行本地编译情况下,只要库存在于某个位置,gcc总能通过如上策略找到需要的共享库。但在交叉编译下,上述八种策略,可以使用的仅仅有两个:-rpath-link,-rpath。这两个选项在上述八种策略当中优先级最高,当指定这两个选项时,如果链接需要的共享库找不到,链接器会优先到这两个选项指定的路径下去搜索需要的共享库。通过上面的描述可以看到:-rpath指定的路径将被写到可执行文件中;-rpath-link则不会;我们当然不希望交叉编译情况下使用的路径信息被写进最终的可执行文件,所以我们选择使用选项-rpath-link。

  gcc的选项“-Wl,--rpath-link –Wl,DIR”会把-rpath-link选项及路径信息传递给链接器。回到上面那个例子,如果命令行中没有出现libb.so,但gcc指定了“-Wl,--rpath-link –Wl,DIR”,则链接器找不到libb.so的时候,会首先到后面-rpath-link指定的路径去寻找其依赖的库。此处我们使用的编译命令的示例是使用unicore平台的工具链。

$ unicore32-linux-gcc –o myprog /usr/lib/liba.so

-Wl,--rpath-link -Wl,/home/UNITY_float/install/usr/lib

这样,编译器会首先到“/home/UNITY_float/install/usr/lib”下面去搜索libb.so

  libtool如何把选项“-Wl,--rpath-link –Wl,DIR”传递给gcc?libtool中有一个变量“hardcode_libdir_flag_spec”,该变量本来是传递“-rpath”选项的,但我们可以修改它,添加我们需要的路径,传递给unicore32-linux-gcc。

  “hardcode_libdir_flag_spec”原来的定义如下:

hardcode_libdir_flag_spec="${wl}--rpath ${wl}$libdir"

我们修改后的定义如下:

hardcode_libdir_flag_spec="${wl}—rpath-link ${wl}$libdir

-Wl,--rpath-link -Wl,/home/UNITY_float/install/usr/lib

  -Wl,--rpath-link -Wl,/home/UNITY_float/install/usr/X11R6/lib "

这样,当libtool在“--mode=link”的模式下,就会把选项“-Wl,--rpath-link –Wl,DIR”传递给gcc编译器了。

 

作者:刘军涛 系所:微处理器研发中心 日期:2006-1-6

cc 编译命令的基本用法

分类:编程点滴

生成一个可执行的文件通常需要经过以下几个步骤:

 

 

 

预处理你的源代码,去掉注释,以及其他技巧性的工作就像在 C 中展开宏。

 

检查代码的语法看你是否遵守了这个语言的规则。如果没有,编译器会给出 警告。

 

把源代码转换为汇编语言 ── 和机器代码很相似, 但是在一定情况下我们仍然可以理解。 [1]

 

把汇编语言转换为机器语言──是的,我们在说位元和字节,就是10

 

检查你是否准确地使用了函数和全局变量类似的东西。例如,如果你调用了一个不存在的函数,编译器就会给出警告。

 

如果你是从多个源代码文件编译,就要学会如何把这些文件组合到一起。

 

把产生出来的东西用系统的运行装载器装入内存并运行。

 

最后,把可执行文件写入文件系统。

 

  编译 这个词的意思通常指 1 4 步──其他的 步骤叫做 连接。有时侯第一步叫做 预处理 。第三和第四步叫做 汇编

 

  幸运的是,几乎所有这些细节都是隐藏的,因为 cc 只是一个前端。它根据正确的参数调用程序来处理代码。只要输入

 

% cc foobar.c

 

  就会把 foobar.c 通过以上的步骤编译出来。如果你有多个文件要编译,只要输入

 

% cc foo.c bar.c

 

  注意,语法检查就是──纯粹的检查语法。而不会检测你可能犯的任何逻辑错误。比如无限循环,或者是你想用一元排序却使用了冒泡排序。 [2]

 

  cc 有很多选项,在帮助手册中都可以找到。这里列出了一些最重要的选项,并且有例子。

 

-o filename

输出的文件名。如果你不使用这个选项,cc为产生 出一个叫 a.out 的执行文件。 [3]

 

 

% cc foobar.c

可执行文件是 a.out

% cc -o foobar foobar.c 可执行文件是 foobar

 

 

-c

仅仅编译文件,不会连接。如果你只想检查你写的测试程序的语法的话,这个选项非常有用。或者你会使用 Makefile

 

 

% cc -c foobar.c

 

 

这会产生一个 目标文件 (不可执行) 叫做 foobar.o。这个文件可以和其他的目标文件连接在一起构成一个可执行文件。

 

-g

产生一个可调试的可执行文件。编译器会在可执行文件中植入一些信息,这些信息能够把源文件中的行数和被调用的函数联系起来。在你一步一步调试程序的时候,调试器能够使用这些信息来显示源代码。这是 非常 有用的;缺点就是被植入的信息让程序变得更大。通常情况下,开 发一个程序的时候我们经常使用 -g,但是我们在编译一个 release 版本” 的程序的时候,如果程序工作得让人满意了,我 们就不使用 -g 编译。

 

 

% cc -g foobar.c

 

 

这会产生一个可调试版本的程序。 [4]

 

-O

产生一个优化版本的可执行文件。编译器会使用一些聪明的技巧产生出比普通编译产生的文件执行更快的可执行文件。可以在 -O 加上数字来使用更高级的优化。但是这样做经常会暴露出编译器的优化器中的一些 错误。例如,2.1.0 版本的 FreeBSD 中的 cc 在某些情况 下使用了 -O2 的话,会产生出错误的代码。

 

优化通常只在编译一个 release 版本的时候才被打开。

 

 

% cc -O -o foobar foobar.c

 

这会产生一个优化版本的 foobar

 

-O -O1指定1级优化

 

-O2 指定2级优化

 

-O3 指定3级优化

 

-O0指定不优化

 

$cc -c O3 -O0 hello.c

 

当出现多个优化时,以最后一个为准!!

 

-I

 

可指定查找include文件的其他位置.例如,如果有些include文件位于比较特殊的地方,比如/usr/local/include,就可以增加此选项如下:

 

$cc -c -I/usr/local/include -I/opt/include hello.c

 

 

此时目录搜索会按给出的次序进行.

 

 

 

 

-E

 

这个选项是相对标准的,它允许修改命令行以使编译程序把预先处理的C文件发到标准输出,而不实际编译代码.在查看C预处理伪指令和C宏时,这是很有用的.可能的编译输出可重新定向到一个文件,然后用编辑程序来分析:

 

 

 

 

 

 

$cc -c -E hello.c >cpp.out

 

此命令使include文件和程序被预先处理并重定向到文件cpp.out.以后可以用编辑程序或者分页命令分析这个文件,并确定最终的C语言代码看起来如何.

 

-D

 

允许从编译程序命令行定义宏符号

 

 

 

一共有两种情况:一种是用-DMACRO,相当于在程序中使用#define MACRO,另一种是用-DMACRO=A,相当于

 

程序中的#define MACRO A.如对下面这代码:

 

 

 

#ifdefine DEBUG

 

printf("debug messagen");

 

#endif

 

编译时可加上-DDEBUG参数,执行程序则打印出编译信息

 

  下面的三个参数会迫使 cc 检查你的代码是否符合一些国际标准,经常被我们叫做 ANSI 标准,虽然严格的来说它是一个 ISO 标准。

 

-Wall

打开所有 cc 的作者认为值得注意的警告。不要只看这个选项的名字,它并没有打开所有 cc 能够注意到的所有警告。

 

-ansi

关闭大多数,但并不是所有,cc 提供的非 ANSI C 特性。不要只看选项的名字,它并不严格保证你的代码会兼容标准。

 

-pedantic

关闭 所有 cc 的非 ANSI C 特性。

 

  没有这些选项,cc 能允许你按照标准使用一些非标准的扩展。有一些扩展非常有用,但不能与其他编译器兼容──实际上,这个标准的主要目的之一就是允许我们写出可以在任何系统上的由任何编译器编译的代码。这就叫做 可移植代码

 

  通常来说,你应该让你的代码尽可能的可以移植。否则你就不得不完全重写你的代码以便能够在其他地方运行之──而且谁知道几年后你是否还会用它?

 

 

% cc -Wall -ansi -pedantic -o foobar foobar.c

 

  这会在检查 foobar.c对标准的兼容性以后产生一个 foobar 可执行文件。

 

-Ldirname

指定连接库的搜索目录,-l(小写L)指定连接库的名字

 

$gcc main.o -L/usr/lib -lqt -o hello

 

指定连接库的搜索目录,-l(小写L)指定连接库的名字

 

上面的命令把目标文件main.o与库qt相连接,连接时会到/usr/lib查找这个库文件.也就是说-L-l一般要成对出现.

 

 

 

-llibrary

在连接的时候指定一个函数库。

 

最常见的情况就是当你编译一个使用了一些 C 中的数学函数的时候。不像大多数其他的平台,这些函数都不在 C 的标准库里面。你必须告诉编译器加 上这些库。

 

这个规则就是,如果库的名字叫做 libsomething.a,你就必 须给 cc 这样的选项 -lsomething。例如,数学库 叫做 libm.a,因此你给 cc 的选 项就是 -lm。一般情况下,我们要把这个选项放到命令行的 最后。

 

 

% cc -o foobar foobar.c -lm

 

 

这个会把数学函数库连接到 foobar 里面。

 

如果你要编译 C 代码,你需要 -lg ,或者 -lstdc 如果你使用的是 FreeBSD 2.2 或者更高版本,来 连接 C 库。或者,你可以运行 c 而不是 cc 来编译 C 代码。在 FreeBSD 上, c 也可以通过运行 g 来唤醒。

 

 

% cc -o foobar foobar.cc

-lg 对于 FreeBSD 2.1.6 或者更低的版本

% cc -o foobar foobar.cc -lstdc

FreeBSD 2.2 或者更高的版本

% c -o foobar foobar.cc

 

 

两种情况都会从 C 源文件 foobar.cc产生一个 可执行文件 foobar。注意,在 UNIX® 系统中,C 文件的传统后缀是 .C.cxx .cc,而不是 MS-DOS® 类型的 .cpp (这个后缀已经被用到了其他的地方) gcc 根据这个约定来确定应该使用何种类型的编译器来编译源文件。但是,这个限制不再起作用了,因此现在你可以自由的使用 .cpp 这个后缀来命名你的 C 源文件!

 

常见 cc 问题

1. 我尝试写一个程序,其中使用了 sin() 这个函数。但是我却得到了如下的错误。这个错误是什么意思?

2. 好的,我写了一个简单的程序,练习使用 -lm。也 就是计算 2.1 6 次方。

3. 那么我怎么才能改正这个错误?

4. 我编译了一个文件叫 foobar.c 但是我没有找 到叫 foobar 的执行文件。这个文件到哪里去了?

5. 好的,我有一个执行文件 foobar,我用命令 ls 可以看见,但是在命令行我输入 foobar 却得到提示说没有这个文件。为什么找不到呢?

6. 我的可执行文件叫做 test,但是我运行之后却 什么也没发生。到底怎么了?

7. 我编译了一个程序,开始看起来运行得不错。但是后来调试了,说什么 core dumped”。这个是什么意思?

8. 挺不错,但现在我该怎么办呢?

9. 我的程序把 core dump 以后,说有一个什么 segmentation fault”。这是什么?

10. 有时候当我得到一个 core dump,提示说 bus error”。我的 UNIX 教材里面说这意味这硬件错误,但是计算机看起来运行很正常。这是真的吗?

11. 如果我可以让 core dump 在需要的时候产生,那就真的很不错。我能 这样做吗,或者我得等直到发生一个错误?

 

 

1. 我尝试写一个程序,其中使用了 sin() 这个函数。但是我却得到了如下的错误。这个错误是什么意思?

 

/var/tmp/cc0143941.o: Undefined symbol `_sin' referenced from text segment

 

 

 

当使用像 sin() 这样的数学函数的时候,你必 须告诉 cc 把数学函数库给连接进来,就像这样:

 

 

% cc -o foobar foobar.c -lm

 

 

 

 

2. 好的,我写了一个简单的程序,练习使用 -lm。也 就是计算 2.1 6 次方。

 

 

#include <stdio.h>

 

int main() {

float f;

 

f = pow(2.1, 6);

printf("2.1 ^ 6 = %fn", f);

return 0;

}

 

 

然后我编译:

 

 

% cc temp.c -lm

 

 

动态创建二维数组(转)

分类:编程点滴

http://blog.chinaitlab.com/user1/338633/archives/2006/46994.html

动态创建二维数组
  最近,经常看到在论坛的VC板块看到有人问有关于动态创建二维数组的人,自己准备写一篇出来。
  记得自己刚学习C的时候,就需要学习数组,不过在学校里,我最多就是学到了二维数组,并且自己使用的也绝大部分是一维数组。后来,快毕业的时候,程序中需要一个使用动态的二维数组,那时候没有老师可以请教,自己摸索着,竟然被我弄出来了一段代码,可以动态创建二维数组(这段代码有可能是不正确的,希望行家不要取笑,只需要提出意见就可以了):

#i nclude <stdio.h>

#i nclude <iostream.h>
 

const int ROW = 10;

const int COL = 12;

 

int main()

{

  int iRow=0, iCol=0;

  //创建数组

       cout<<"Create array"<<endl;

  int **ppint = new int*[ROW];

  for(iRow=0;iRow<ROW;iRow++){

    ppint[iRow] = new int[COL];

  }

  //进行计算

  cout<<"TODO sth."<<endl;

  for(iRow=0;iRow<ROW;iRow++){

    for(iCol=0;iCol<COL;iCol++){

      ppint[iRow][iCol] = iRow+iCol;

    }

  }

  //输出

  cout<<"use the array"<<endl;

  for(iRow=0;iRow<ROW;iRow++){

    for(iCol=0;iCol<COL;iCol++){

      cout<<"["<<iRow<<"]["<<iCol<<"]"<<ppint[iRow][iCol]<<";";

    }

    cout<<endl;

  }

  //删除

  cout<<endl<<"Now, delete the array"<<endl;

  for(iRow=0;iRow<ROW;iRow++){

    delete[] ppint[iRow];

  }

  delete[] ppint;
  ppint = NULL;


  //

  cout<<"Press return to exit......."<<endl;

  getchar();

  return 0;

}

  上面的代码,演示了怎样创建、使用以及删除一个二维数组。这段代码虽然是我刚刚写的,但是早已成型于2002年底。我不知道这段代码创建一个二维数组有什么错误,但是一直到现在我仍在使用。

------------------------------------------------------------------------------

C/C++有一个模板库,支持动态创建二维数组与删除二维数组,再后来,又在网上看到有人提供了一个宏来创建二维数组。
  关于那个创建与删除二维数组的模板,这下子我忘记了,自己写了一个类似的(如果写错了,还是那句话:只需要贴出正确的就可以了,少量的批评也行,就是不要骂人了)

 

#i nclude <stdio.h>

#i nclude <iostream.h>

 

template <class T>

T** CreateMatrix(int nRow, int nCol)

{

  T **ppT = new T*[nRow];

  for(int iRow = 0;iRow<nRow;iRow++){

    ppT[iRow] = new T[nCol];

  }

  return ppT;

}

 

template <class T>

void DeleteMatrix(T ***pppT, int nRow)

{

  for(int iRow=0; iRow<nRow; iRow++){

    delete[] (*pppT)[iRow];

  }

  delete[] (*pppT);

  (*pppT) = NULL;

}

 

const int ROW = 10;

const int COL = 12;

 

int main()

{

  int iRow=0, iCol=0;

  //创建数组

       cout<<"Create array"<<endl;

  int **ppint = CreateMatrix<int>(ROW, COL);

  //进行计算

  cout<<"TODO sth."<<endl;

  for(iRow=0;iRow<ROW;iRow++){

    for(iCol=0;iCol<COL;iCol++){

      ppint[iRow][iCol] = iRow+iCol;

    }

  }

  //输出

  cout<<"use the array"<<endl;

  for(iRow=0;iRow<ROW;iRow++){

    for(iCol=0;iCol<COL;iCol++){

      cout<<"["<<iRow<<"]["<<iCol<<"]"<<ppint[iRow][iCol]<<";";

    }

    cout<<endl;

  }

  //删除

  cout<<endl<<"Now, delete the array"<<endl;

  DeleteMatrix<int>(&ppint, ROW);

  //

  cout<<"Pres return to exit......."<<endl;

  getchar();

  return 0;

}
  经过我的测试,输出的结果是一样的。

------------------------------------------------------------------------------

  至于那个使用宏来生成二维数组,个人不推荐,所以这里也就不贴出来了(主要是自己手头没有,而自己又懒得不愿意写)。

------------------------------------------------------------------------------

题外话:自己本来想测试一下这段代码是否正确的(虽然在实际使用中是正确的),想把内存中的内容给分析一下,但是发现自己是在使看不懂二维数组在内存中的表示。如果有谁能够分析验证一下,hengai先在这里谢过了!

posted on 2005-07-09 12:32 hengai 阅读(920) 评论(21)  编辑 收藏

评论
# 沙发
大哥,你搞得这个字体怎么这么难看

,da
2005-07-09 12:47 | hzero
# re: 动态创建二维数组
很好!

但这并不是 二维数组 ,在某些语言中你一定看过 int[2,3] 和 int[2][3] 的区别,在C/C++中 int [2][3] 是 二维数组,而
int* a[2] = { new int[3], new int[3] } 还只是一维数组,区别在于二维数组的每一维都是一个数组,而不是指向一个数组的指针。

在位本质上,多维数组和一维数组是一致的,你可以重载operator[]来用一维数组模拟多维数组。
2005-07-09 13:02 | 周星星
# to 周星星
谢谢!
的确,这不算是二维数组,但是几乎可以满足二维数组的要求,也可以当作二维数组来使用。
二维数组,其实在内存中是连续的一段地址,而我这个实现的,并不是一个连续的内存段。
2005-07-09 13:34 | hengai
# to hengai:
《Effective C++ 2/e》中说:别做和内建类型不一致的行为。
对于内建二维数组,比如int a[2][3],fwrite( a, sizeof(a), f )就写入文件,fread( a, sizeof(a), f )就读入文件。
不是说你的实现有错误,而是说它可能会成为后来错误的伏笔。
2005-07-09 13:40 | 周星星
# to 周星星
的确,按照我这样的实现方法,在写入文件时会发生错误。但是要考虑到什么场合使用什么样的方法。在我的要求中,对于二维数组,一般不会要求写入到文件,即使有写入到文件的地方,自己也会注意。
不过我可能说得不完善,至少需要提醒使用这段代码的人,在写入文件(或者按照内存偏移来取内容的人)会出错。

再次谢谢你提出的问题
2005-07-09 13:46 | hengai
# re: 动态创建二维数组
只要是用二维(行列)来读写存储体都是二维数组, 只不过在实现上分连续型的和邻接型的罢了.

但我觉得连续型的存取效率比邻接的高, 因为它是定列宽的. 而邻接比连续的灵活.
2005-07-09 13:54 | anonymous
# re: 动态创建二维数组
我认为都差不多吧,都是对内存进行操作。
不过“邻接比连续的灵活”有一定的道理,因为你有时候分配一块巨大的内存时出现失败的可能性比申请多块小内存的可能性大(不知道这句话是否正确)。
不过,如果是连续型的,我就可以通过下标的累加来访问下一个元素。比如有个数组 a[10][10],我可以通过 (&a[0][9])++来访问a[1][0](代码可能有误),这又体现出来了更多的灵活性
2005-07-09 14:02 | hengai
# re: 动态创建二维数组
底层寻址应该是不同的, 连续的应该是间接寻址, 邻接的应该是基址或变址的寻址方式.  定列宽的好处就是减少一个寄存器, 把它变为立即数.
2005-07-09 14:17 | anonymous
# re: 动态创建二维数组
随便写几笔:

template <class T>
class Array
{
public:
Array(int nRow, int nCol):m_nRow(nRow),m_nCol(nCol)
{
m_pData = new T[nRow*nCol];
}
~Array()
{
delete [] m_pData;
}

T * operator [] (int n)
{
return m_pData + n * m_nCol;
}

int Size() const
{
return sizeof(T) * m_nRow * m_nCol;
}

T &GetAt(int nRow,int nCol)
{
return m_pData[nRow * m_nCol + nCol];
}
private:
int m_nRow;
int m_nCol;
T * m_pData;
};
2005-07-09 14:40 | Panic
# re: 动态创建二维数组
Painc的Array实现一拷贝或赋值就OVER了. 
:-P
2005-07-09 14:47 | anonymous
# re: anonymous
那你重载一下拷贝构造和赋值运算符吧:P
2005-07-09 14:56 | pAnic
# to Panic
呵呵,我现在也在写一个能够实现连续地址的二维数组内,刚刚已经请教完了关于 [][] 操作符的重载问题
你代码中,使用GetAt(int nRow,int nCol)来获得,毕竟没有直接使用[][]方便,以及习惯。
顺便说一下,文中提到的那个宏,我偶然找到了

#define matrix_allocate(matrix, width, height, TYPE) {
matrix = new TYPE* [height];
for(int _i = 0; _i < height; _i++)
matrix[_i] = new TYPE[width];
}
#define matrix_delete(matrix, width, height){
for(int _i = 0; _i < height; _i++)
delete [] matrix[_i];
delete [] matrix;
matrix = 0;
}
不过我保证正确性
2005-07-09 14:59 | hengai
# re: hengai
我那个实现也可以用[][]来操作啊,你没看明白:P
2005-07-09 15:03 | pAnic
# to pAnic
你说的是
T * operator [] (int n)
{
return m_pData + n * m_nCol;
}

正确的做法不应该是这样的。你可以看看
http://www.vckbase.com/bbs/viewtopic.asp?pg=4&id=1618233
是怎么实现的。
2005-07-09 15:37 | hengai
# re: 动态创建二维数组
他以T * 来作为最后一维也是对的, 其实没什么正确不正确, 只是不同实现方式而已.
class Arr{
public:
    Arr& operator[](int);
    operator int&();
};

也是一种实现. 
:-P
2005-07-09 18:17 | anonymous
# 因为个人C++中不太喜欢动态二维数组
我想你实现动态二维数组也是一种奇怪的方法,因为就实现而已,自己也还能没有问题,所以急下来看评论。
第一个评论看到了,所以急于过来说上几句了。
1.平凡的new小块内存,效率低效,还会导致内存碎片。
2.建议你重载operator[][]用来模拟二维数组的功能
3.建议实现体内可以考虑STL容器,而不要不停的new(当然,如果你觉得自己能够做的更好,自己做也无妨)
2005-07-10 10:40 | 清风雨
# 就另一个评论
如果你想杜绝 = 和 copy构造,可以不妨考虑private一下。
这样,就不好=和copy崩溃了。

或者实现自己意义上的=和copy,不过由于数组的=是指针地址copy,所以还是趋向建议private,以保持一直性、降低学习难度(个人是极度反对不一直的东西到处都是,每个都要特殊处理的)。
2005-07-10 10:45 | 清风雨
# re: to pAnic
你肯定晕死了。^_^

搂主兄弟,他是告诉你方法,而不是形式。你把GetAt换成operator[][]就一样了。
2005-07-10 10:51 | 清风雨
# to 清风雨
我仅仅实现的就是一个模拟的二维数组,关于那些类似于 = 的操作符,我不需要重载。昨天本来想自己实现一个能够动态生成的二维数组类,结果因为自己的水平确实不行,一个晚上都没有实现。我的想法是:假设要求的二维数组为ROW*COL,那么我先在内存中分配一个 new T[ROW*COL];也就是说使用线形的方式存储一个二维数组。
记得以前看到过pAnic兄(好像是他),回答一个人的问题“关于 foo(char p[][])与 foo(char **p) 有什么不同),昨天晚上自己也始了一下,个人认为,这就是传递一个二维数组与双重指针,如果我是使用 char p[10][12]那么只能是以 foo(char p[][])这种形式传递,如果这个 char p[][] 是按照我上面贴子所生成的方法,那么就需要使用 foo(char **p),两者之间绝对不能混用。
另外,我昨天晚上就是在重载[][]操作符上碰到了问题,我知道,如果我使用 GetAt 函数,可以很容易的解决这个问题,但是我需要的就是按照习惯,因为按照使用习惯,人们一般都是使用 p[row][col]来访问二维数组而不是使用 GetAt(row, col)来访问。

另外,我有一点说错了,这里自我批评:“我以为多次new一段小内存的效率会高于一次性new大段的内存”,这句话是错的。以前看过一篇文章,说“如果需要频繁的分配内存,可以先使用HeapAlloc来一大段的内存,然后在从这里划分”。再次自我批评一下。

2005-07-10 14:15 | hengai
# re: 动态创建二维数组
为什么不用boost 的多维数组? 或者看看它的实现方式?
2005-07-11 10:03 | blueskyzsz
# to 清风雨
就是不想用那么高级的东西,所以才写这些的,要不然我直接使用 vctor< vctor<T> > 了。

QT中的事件机制(转)

分类:编程点滴

原文Another Look at Events 
作者: Jasmin Blanchette  译:清源游民 gameogre@gmail.com

什么是自发事件?哪些类型的事件可以被propagated 或compressed? posting and sending 事件之间有何不同?什么时候应该调用 accept() 或是ignore() ? 如果这些问题你还不是很了解,那么继续看下去。

事件起源:

基于事件如何被产生与分发,可以把事件分为三类:
* Spontaneous 事件,由窗口系统产生,它们被放到系统队列中,通过事件循环逐个处理。
* Posted 事件,由Qt或是应用程序产生,它们被Qt组成队列,再通过事件循环处理。
* Sent  事件,由Qt或是应用程序产生,但它们被直接发送到目标对象。
当我们在main()函数的末尾调用QApplication::exec()时,程序进入了Qt的事件循环,大概来讲,事件循环如下面所示:
while (!exit_was_called)
{
  while(!posted_event_queue_is_empty)
       {
         process_next_posted_event();
       }
  while(!spontaneous_event_queue_is_empty)
      {
         process_next_spontaneous_event();
      }
  while(!posted_event_queue_is_empty)
      {
        process_next_posted_event();
      }
}
首先,事件循环处理所有的posted事件,直到队列空。然后再处理所有的spontaneous事件,最后它处理所有的因为处理spontaneous事件而产生的posted事件。send 事件并不在事件循环内处理,它们都直接被发送到了目标对象。现在看一下实践中的paint 事件是如何工作的。当一个widget第一次可见,或是被遮挡后再次变为可见,
窗口系统产生一个(spontaneous) paint事件,要求程序重画widget,事件循环最终从事件队列中捡选这个事件并把它分发到那个需要重画的widget。
并不是所有的paint事件都是由窗口系统产生的。当你调用QWidget::update()去强行重画widget,这个widget会post 一个paint 事件给自己。这个paint事件被放入队列,最终被事件循环分发之。
假如你很不耐烦,等不及事件循环去重画一个widget, 理论上,你应该直接调用paintEvent()强制进行立即的重画。但实际上这不总是可行的,因为paintEvent()函数是protected的(很可能访问不了)。它也绕开了任何存在的事件过滤器。因为这些原因,Qt提供了一个机制,直接sending事件而不是posting 。
QWidget::repaint()就使用了这个机制来强制进行立即重画。
posting 相对于sending的一个优势是,它给了Qt一个压缩(compress)事件的机会。假如你在一个widget上连续地调用update() 十次,因update()而产生的这十个事件,将会自动地被合并为一个单独的事件,但是QPaintEvents事件附带的区域信息也合并了。可压缩的事件类型包括:paint,move,resize,layout hint,language change。
最后要注意,你可以在任何时候调用QApplication::sendPostedEvent(),强制Qt产生一个对象的posted事件。

人工合成的事件

QT应用程序可以产生他们自己的事件,或是预定义类型,或是自定义类型。 这可以通过创建QEvent类或它的
子类的实例,并且调用QApplication:postEvent()或QApplication::sendEvent()来实现。
这两个函数需要一个 QObject* 与一个QEvent * 作为参数,假如你调用postEvent(),你必须用 new 操作符来创建事件对象,Qt会它被处理后帮你删除它。假如你用sendEvent(), 你应该在栈上来创建事件。下面举两个例子:
一是posting 事件:
QApplication::postEvent(mainWin, new QKeyEvent(QEvent::KeyPress,Key_X,'X',0));
二是sending 事件:
    QKeyEvent event(QEvent::KeyPress, Key_X, 'X', 0);
    QApplication::sendEvent(mainWin, &event);
Qt应用程序很少直接调用postEvent()或是sendEvnet(),因为大多数事件会在必要时被Qt或是窗口系统自动产生
。在大多数的情况下,当你想发送一个事件时,Qt已经为了准备好了一个更高级的函数来为你服务。(例如
update()与repaint())。

定制事件类型

qt允许你创建自己的事件类型,这在多线程的程序中尤其有用。在单线程的程序也相当有用,它可以作为
对象间的一种通讯机制。为什么你应该用事件而不是其他的标准函数调用,或信号、槽的主要原因是:事件既可用于同步也可用于异步(依赖于你是调用sendEvent()或是postEvents()),函数调用或是槽调用总是同步的。事件的另外一个好处是它可以被过滤。
演示如何post一个定制事件的代码片段:
const QEvent::Type MyEvent = (QEvent::Type)1234;
  ...
QApplication::postEvent(obj, new QCustomEvent(MyEvent));
事件必须是QCustomEvent类型(或子类)的。构造函数的参数是事件的类型,1024以下被Qt保留。其他可被程序使用。为处理定制事件类型,要重新实现customEvent()函数:
void MyLineEdit::customEvent(QCustomEvent *event)
    {
        if (event->type() == MyEvent) {
            myEvent();
        } else {
            QLineEdit::customEvent(event);
        }
    }
QcustomEvent类有一个void *的成员,可用于特定的目的。你也可以子类化QCustomEvent,加上别的成员,但是你也需要在customEvent()中转换QCustomeEvent到你特有的类型。

事件处理与过滤

Qt中的事件可以在五个不同的层次上被处理
1,重新实现一个特定的事件handler
 QObjectQWidget提供了许多特定的事件handlers,分别对应于不同的事件类型。(如paintEvent()对应paint事件)
2,重新实现QObject::event()
 event()函数是所有对象事件的入口,QObject和QWidget中缺省的实现是简单地把事件推入特定的事件handlers。
3,在QObject安装上事件过滤器
  事件过滤器是一个对象,它接收别的对象的事件,在这些事件到达指定目标之间。
4,在aApp上安装一个事件过滤器,它会监视程序中发送到所有对象的所有事件
5,重新实现QApplication:notify(),Qt的事件循环与sendEvent()调用这个函数来分发事件,通过重写它,你可以在别人之前看到事件。

一些事件类型可以被传递。这意味着假如目标对象不处理一个事件,Qt会试着寻找另外的事件接收者。用新的目标来调用QApplication::notify()。举例来讲,key事件是传递的,假如拥有焦点的Widget不处理特定键,Qt会分发相同的事件给父widget,然后是父亲的父亲,直到最顶层widget。

接受或是忽略?

可被传递的事件有一个accept()函数和一个ignore()函数,你可以用它们来告诉Qt,你“接收”或是
“忽略”这个事件。假如事件handler调用accept(),这个事件将不会再被传递。假如事件handler调用
ignore(),Qt会试着查找另外的事件接收者。
像大多数的开发者一样,你可能不会被调用accept()或是ignore()所烦恼。缺省情况下是“接收”,在
QWidget中的缺省实现是调用ignore(),假如你希望接收事件,你需要做的是重新实现事件handler,避免
调用QWidget的实现。假如你想“忽略”事件,只需简单地传递它到QWidget的实现。下面的代码演示了这一点:
void MyFancyWidget::keyPressEvent(QKeyEvent *event)
    {
        if (event->key() == Key_Escape) {
            doEscape();
        } else {
            QWidget::keyPressEvent(event);
        }
    }
在上面的例子里,假如用户按了"ESC"键,我们会调用doEscape()并且事件被“接收”了(这是缺省的情况),
事件不会被传递到父widget,假如用户按了别的键,则调用QWidget的缺省实现。
void QWidget::keyPressEvent(QKeyEvent *event)
    {
        event->ignore();
    }
应该感谢ignore(),事件会被传递到父widget中去。
讨论到目前为至,我们都假设基类是QWidget,然而,同样的规则也可以应用到别的层次中,只要用QWidget
代替基类即可。举例来说:
 void MyFancyLineEdit::keyPressEvent(QKeyEvent *event)
    {
        if (event->key() == Key_SysReq) {
            doSystemRequest();
        } else {
            QLineEdit::keyPressEvent(event);
        }
    }
由于某些原因,你会在event()中处理事件,而不是在特定的handler中,如keyPressEvent(),这个过程会有些不同。event()会返回一个布尔值,来告诉调用者是否事件被accept或ignore,(true表示accept),从event()中调用accept()或是ignore()是没有意义的。“Accept”标记是event()与特定事件handler之间的一种通讯机制。而从event()返回的布尔值却是用来与QApplication:notify()通讯的。在QWidgetk中缺省的event()实现是转换“Accept”标记为一个布尔值,如下所示:
bool QWidget::event(QEvent *event)
    {
        switch (e->type()) {
        case QEvent::KeyPress:
            keyPressEvent((QKeyEvent *)event);
            if (!((QKeyEvent *)event)->isAccepted())
                return false;
            break;
        case QEvent::KeyRelease:
            keyReleaseEvent((QKeyEvent *)event);
            if (!((QKeyEvent *)event)->isAccepted())
                return false;
            break;
            ...
        }
        return true;
    }

到现在为至,我们所说的内容不仅仅适用于key事件,也适用于mouse,wheel,tablet,context menu等事件
Close事件有点不同,调用QCloseEvent:ignore()取消了关闭操作,而accept()告诉Qt继续执行正常的关闭操作。为了避免混乱,最好是在closeEvent()的新实现中明确地进行accept()与ignore()的调用:
 void MainWindow::closeEvent(QCloseEvent *event)
    {
        if (userReallyWantsToQuit()) {
            event->accept();
        } else {
            event->ignore();
        }
    }


正确使用#include和前置声明(转)

分类:编程点滴

作者:SpriteLW      来源:csdn 
差不多一年时间没用过C++写过程序了,由于工作的需要,我又回到了C++的阵形。在工作的过程中遇到了很多麻烦,当我往工程里加一个类,而且那个类又与工程里的类相关,如有那个类型的成员变量。情况如下
//////A.h///////////
class A
{
.......
};
////////B.h//////////
class B:A
{
....
A member;
}
结果,编译就会出错,说找不到类形A。解决的办法是在B.h里#include “A.h”。但是有时候不用#include “A.h”,只要在classB:A前加class A;就可以了。更严重的是不但要#include “A.h”,还要class A;。
起初觉得没问题,因为这样搞来搞去总会编译通过的,而且不会让程序变大,因为有#ifndef...#endif和#pragma once控制。直到有一次,我需要那些常量放到一个文件中“const.h”,然后include到其它需要它的类中,结果怎么也编译不成功(因为文件多了,而且每个文件都这样互相include,把我也蒙了)
直到今天终于从《Effective C++》里找到原理。现向大家分享一下,首先我以下面这个类结构作例子。(先不管我为什么不加一个Woman,为什么Man就有child,我只是作例子解说,绝没有性别歧视。
代码如下:
////////////main.h//////////////
#include "stdafx.h"
#include "man.h"
int main(){
    Man m;
    return 0;
}
////////////Person.h/////////////
#pragma once
class Person
{
public:
    Person(void);
    ~Person(void);
};
////////Person.cpp///////////
#include "StdAfx.h"
#include ".person.h"
Person::Person(void){
}
Person::~Person(void){
}
/////////Man.h///////////
#pragma once
#include "person.h"
class Man : public Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person child;
};
/////////////Man.cpp//////////////
#include "StdAfx.h"
#include ".man.h"
Man::Man(void){
}
Man::~Man(void){
}
 
上述代码,编译运行一切正常。现在我作以下修改:
/////////Man.h///////////
#pragma once
//#include "person.h"        //去掉
class Man : public Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person child;
};
/////////Man.h///////////
#pragma once
//#include "person.h"   //去掉
class Person;           //加入
class Man:public Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person child;
};
error C2504: “Person” : 未定义基类
error C2504: “Person” : 未定义基类
/////////Man.h///////////
#pragma once
//#include "person.h"   //去掉
class Person;           //加入
class Man:public Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person *child;      //改为指针
};
/////////Man.h///////////
#pragma once
//#include "person.h"   //去掉
class Person;           //加入
class Man               //去掉:Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person *child;      //改为指针
};
error C2504: “Person” : 未定义基类
编译通过
要讲解上面的代码还要一些预备知备,看下面代码:
int main()
{
    int x;
    Person p;//用C++时编译不通过;
}
当编译器看到x定义式时,它们知道必须配置足够的空间以放置一个int。没问题,每个编译器都知道int有多大。然而当编译器看到p的定义式时,虽然它们也知道必须配置足够空间以放置一个Person,但一个Person对象有多大呢?编译器获得这项信息的唯一办法就是询问class定义式。然而class的定义式可以合法地不列出实现细节(如:
只写出class Person;)那么编译器又如何知道该配置多少空间呢?
    Java等语言对此问题的解法是,当程序定义出一个对象时,只配置足够空间给一个“指向该对象的指针”使用,如
public Person;
public static void main(String[] args)
{
    Person p;
}
    对于C++就如下那样:
class Person;
int main()
{
    Person *p;//编译器当要配置一个指针大小的空间的指针给p就可以了。
    //Person &p2; 这个理论上也可以,但references object必须“言之有物”
    return 0;
}
    看回刚才那段代码为什么“Person p;//用C++时编译不通过;”呢?因为它要调用Person constructor。那就是Person的实现细节。
   
    现在可以解说上面的表格了,我的目的是 去掉#include Person.h并加入class Person; 所以要做有:
1.     Person child改为Person *child。因为child也是Man的成员,Man的大小与Child相关,而child不是内部类型,它的大小编译器不知道。
2.     :public Person去掉。因为Man继承Person,所以编译器也要知道Person是怎样实现的,那样才能构造出正确的Man来(为了编译成功,我忍痛割爱了)。
 
同时我也要对原码作一下解释:
/////////Man.h///////////
#pragma once
#include "person.h"
class Man : public Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person child;
};
    这里#include “person.h”不但包含了Person的定义,也包含了Person的实现细节,所以是编译成功的。
 
结论
1.     当不需要调用类的实现时,包括constructor,copy constructor,assignment operator,member function,甚至是address-of operator时,就不用#include,只要forward declaration就可以了。
2.       当要用到类的上面那些“方法”时,就要#include
 
扩充
    为了加深认识,我分享遇到的另一情况。
////////////Person.h/////////////
#pragma once
class Person
{
public:
    Person(void);
    ~Person(void);
    virtual void addChild(Person p) = 0;//将Person变为抽象类
};
/////////Man.h///////////
#pragma once
//#include "person.h"   //去掉
class Person;           //加入
class Man               //去掉:Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person *child;
    void addChild(Person p);//相应地在Man.cpp中加上这个空函数
};
 
error C2259: “Person” : 不能实例化抽象类
/////////Man.h///////////
#pragma once
#include "person.h"    //加回来
class Person;      //加不加入也没所谓
class Man               //去掉:Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person *child;
    void addChild(Person p);//相应地在Man.cpp中加上这个空函数
};
/////////Man.h///////////
#pragma once
#include "person.h"    //加回来
class Person;      //加不加入也没所谓
class Man               //去掉:Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person *child;
    void addChild(Person *p);//将形参变为Person*
};
error C2259: “Person” : 不能实例化抽象类
编译成功
/////////Man.h///////////
#pragma once
#include "person.h"    //加回来
class Person;      //加不加入也没所谓
class Man               //去掉:Person
{
public:
    Man(void);
    ~Man(void);
private:
    Person *child;
    void addChild(Person &p);//将形参变为Person&
};
 
编译成功
 
       为什么出现不能实例化抽象类?我并没有实例化过它。
       这是参数的传递问题。当一个变量传给函数时,我们说是实参传给形参(pass-by-value),形参是通过copy constructor建立的,所以就是实例化了一个抽象类。而pass-by-reference和传指针就没问题了。(全文完)
 
参考资料:
候捷:《Effective C++》

使用 GDB 调试多进程程序

分类:UNIX/LINUX

http://www.ibm.com/developerworks/cn/linux/l-cn-gdbmp/index.html

文档选项
'); //--> >
将此页作为电子邮件发送

将此页作为电子邮件发送

将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项



级别: 中级

田 强 (tianq@cn.ibm.com), 软件工程师, IBM中国软件开发中心

2007 年 7 月 30 日

GDB 是 linux 系统上常用的调试工具,本文介绍了使用 GDB 调试多进程程序的几种方法,并对各种方法进行比较。

GDB 是 linux 系统上常用的 c/c++ 调试工具,功能十分强大。对于较为复杂的系统,比如多进程系统,如何使用 GDB 调试呢?考虑下面这个三进程系统:


进程
进程

Proc2 是 Proc1 的子进程,Proc3 又是 Proc2 的子进程。如何使用 GDB 调试 proc2 或者 proc3 呢?

实际上,GDB 没有对多进程程序调试提供直接支持。例如,使用GDB调试某个进程,如果该进程fork了子进程,GDB会继续调试该进程,子进程会不受干扰地运行下去。如果你事先在子进程代码里设定了断点,子进程会收到SIGTRAP信号并终止。那么该如何调试子进程呢?其实我们可以利用GDB的特点或者其他一些辅助手段来达到目的。此外,GDB 也在较新内核上加入一些多进程调试支持。

接下来我们详细介绍几种方法,分别是 follow-fork-mode 方法,attach 子进程方法和 GDB wrapper 方法。

follow-fork-mode

在2.5.60版Linux内核及以后,GDB对使用fork/vfork创建子进程的程序提供了follow-fork-mode选项来支持多进程调试。

follow-fork-mode的用法为:

set follow-fork-mode [parent|child]

  • parent: fork之后继续调试父进程,子进程不受影响。
  • child: fork之后调试子进程,父进程不受影响。

因此如果需要调试子进程,在启动gdb后:

(gdb) set follow-fork-mode child

并在子进程代码设置断点。

此外还有detach-on-fork参数,指示GDB在fork之后是否断开(detach)某个进程的调试,或者都交由GDB控制:

set detach-on-fork [on|off]

  • on: 断开调试follow-fork-mode指定的进程。
  • off: gdb将控制父进程和子进程。follow-fork-mode指定的进程将被调试,另一个进程置于暂停(suspended)状态。

注意,最好使用GDB 6.6或以上版本,如果你使用的是GDB6.4,就只有follow-fork-mode模式。

follow-fork-mode/detach-on-fork的使用还是比较简单的,但由于其系统内核/gdb版本限制,我们只能在符合要求的系统上才能使用。而且,由于follow-fork-mode的调试必然是从父进程开始的,对于fork多次,以至于出现孙进程或曾孙进程的系统,例如上图3进程系统,调试起来并不方便。

Attach子进程

众所周知,GDB有附着(attach)到正在运行的进程的功能,即attach <pid>命令。因此我们可以利用该命令attach到子进程然后进行调试。

例如我们要调试某个进程RIM_Oracle_Agent.9i,首先得到该进程的pid

[root@tivf09 tianq]# ps -ef|grep RIM_Oracle_Agent.9i
nobody    6722  6721  0 05:57 ?        00:00:00 RIM_Oracle_Agent.9i
root      7541 27816  0 06:10 pts/3    00:00:00 grep -i rim_oracle_agent.9i

通过pstree可以看到,这是一个三进程系统,oserv是RIM_Oracle_prog的父进程,RIM_Oracle_prog又是RIM_Oracle_Agent.9i的父进程。

[root@tivf09 root]# pstree -H 6722


通过 pstree 察看进程
通过 pstree 察看进程

启动GDB,attach到该进程


用 GDB 连接进程
用 GDB 连接进程

现在就可以调试了。一个新的问题是,子进程一直在运行,attach上去后都不知道运行到哪里了。有没有办法解决呢?

一个办法是,在要调试的子进程初始代码中,比如main函数开始处,加入一段特殊代码,使子进程在某个条件成立时便循环睡眠等待,attach到进程后在该代码段后设上断点,再把成立的条件取消,使代码可以继续执行下去。

至于这段代码所采用的条件,看你的偏好了。比如我们可以检查一个指定的环境变量的值,或者检查一个特定的文件存不存在。以文件为例,其形式可以如下:

void debug_wait(char *tag_file)
{
    while(1)
    {
        if (tag_file存在)
            睡眠一段时间;
        else
            break;
    }
}

当attach到进程后,在该段代码之后设上断点,再把该文件删除就OK了。当然你也可以采用其他的条件或形式,只要这个条件可以设置/检测即可。

Attach进程方法还是很方便的,它能够应付各种各样复杂的进程系统,比如孙子/曾孙进程,比如守护进程(daemon process),唯一需要的就是加入一小段代码。

GDB wrapper

很多时候,父进程 fork 出子进程,子进程会紧接着调用 exec族函数来执行新的代码。对于这种情况,我们也可以使用gdb wrapper 方法。它的优点是不用添加额外代码。

其基本原理是以gdb调用待执行代码作为一个新的整体来被exec函数执行,使得待执行代码始终处于gdb的控制中,这样我们自然能够调试该子进程代码。

还是上面那个例子,RIM_Oracle_prog fork出子进程后将紧接着执行RIM_Oracle_Agent.9i的二进制代码文件。我们将该文件重命名为RIM_Oracle_Agent.9i.binary,并新建一个名为RIM_Oracle_Agent.9i的shell脚本文件,其内容如下:

[root@tivf09 bin]# mv RIM_Oracle_Agent.9i RIM_Oracle_Agent.9i.binary
[root@tivf09 bin]# cat RIM_Oracle_Agent.9i
#!/bin/sh
gdb RIM_Oracle_Agent.binary

当fork的子进程执行名为RIM_Oracle_Agent.9i的文件时,gdb会被首先启动,使得要调试的代码处于gdb控制之下。

新的问题来了。子进程是在gdb的控制下了,但还是不能调试:如何与gdb交互呢?我们必须以某种方式启动gdb,以便能在某个窗口/终端与gdb交互。具体来说,可以使用xterm生成这个窗口。

xterm是X window系统下的模拟终端程序。比如我们在Linux桌面环境GNOME中敲入xterm命令:


xterm
xterm

就会跳出一个终端窗口:


终端
终端

如果你是在一台远程linux服务器上调试,那么可以使用VNC(Virtual Network Computing) viewer从本地机器连接到服务器上使用xterm。在此之前,需要在你的本地机器上安装VNC viewer,在服务器上安装并启动VNC server。大多数linux发行版都预装了vnc-server软件包,所以我们可以直接运行vncserver命令。注意,第一次运行vncserver时会提示输入密码,用作VNC viewer从客户端连接时的密码。可以在VNC server机器上使用vncpasswd命令修改密码。

[root@tivf09 root]# vncserver 
New 'tivf09:1 (root)' desktop is tivf09:1
Starting applications specified in /root/.vnc/xstartup
Log file is /root/.vnc/tivf09:1.log
[root@tivf09 root]#
[root@tivf09 root]# ps -ef|grep -i vnc
root     19609     1  0 Jun05 ?        00:08:46 Xvnc :1 -desktop tivf09:1 (root) 
  -httpd /usr/share/vnc/classes -auth /root/.Xauthority -geometry 1024x768 
  -depth 16 -rfbwait 30000 -rfbauth /root/.vnc/passwd -rfbport 5901 -pn
root     19627     1  0 Jun05 ?        00:00:00 vncconfig -iconic
root     12714 10599  0 01:23 pts/0    00:00:00 grep -i vnc
[root@tivf09 root]#

Vncserver是一个Perl脚本,用来启动Xvnc(X VNC server)。X client应用,比如xterm,VNC viewer都是和它通信的。如上所示,我们可以使用的DISPLAY值为tivf09:1。现在就可以从本地机器使用VNC viewer连接过去:


VNC viewer:输入服务器
VNC viewer:输入服务器

输入密码:


VNC viewer:输入密码
VNC viewer:输入密码

登录成功,界面和服务器本地桌面上一样:


VNC viewer
VNC viewer

下面我们来修改RIM_Oracle_Agent.9i脚本,使它看起来像下面这样:

#!/bin/sh
export DISPLAY=tivf09:1.0; xterm -e gdb RIM_Oracle_Agent.binary

如果你的程序在exec的时候还传入了参数,可以改成:

#!/bin/sh
export DISPLAY=tivf09:1.0; xterm -e gdb --args RIM_Oracle_Agent.binary $@ 

最后加上执行权限

[root@tivf09 bin]# chmod 755 RIM_Oracle_Agent.9i

现在就可以调试了。运行启动子进程的程序:

[root@tivf09 root]# wrimtest -l 9i_linux
Resource Type  : RIM
Resource Label : 9i_linux
Host Name      : tivf09
User Name      : mdstatus
Vendor         : Oracle
Database       : rim
Database Home  : /data/oracle9i/920
Server ID      : rim
Instance Home  : 
Instance Name  : 
Opening Regular Session...

程序停住了。从VNC viewer中可以看到,一个新的gdb xterm窗口在服务器端打开了


gdb xterm 窗口
gdb xterm窗口

[root@tivf09 root]# ps -ef|grep gdb
nobody   24312 24311  0 04:30 ?        00:00:00 xterm -e gdb RIM_Oracle_Agent.binary
nobody   24314 24312  0 04:30 pts/2    00:00:00 gdb RIM_Oracle_Agent.binary
root     24326 10599  0 04:30 pts/0    00:00:00 grep gdb

运行的正是要调试的程序。设置好断点,开始调试吧!

注意,下面的错误一般是权限的问题,使用 xhost 命令来修改权限:


xterm 错误
xterm 错误

[root@tivf09 bin]# export DISPLAY=tivf09:1.0
[root@tivf09 bin]# xhost +
access control disabled, clients can connect from any host

xhost + 禁止了访问控制,从任何机器都可以连接过来。考虑到安全问题,你也可以使用xhost + <你的机器名>。

小结

上述三种方法各有特点和优劣,因此适应于不同的场合和环境:

  • follow-fork-mode方法:方便易用,对系统内核和GDB版本有限制,适合于较为简单的多进程系统
  • attach子进程方法:灵活强大,但需要添加额外代码,适合于各种复杂情况,特别是守护进程
  • GDB wrapper方法:专用于fork+exec模式,不用添加额外代码,但需要X环境支持(xterm/VNC)。



 

参考资料



 

关于作者

田强,中国软件开发中心 Tivoli 部门软件工程师,负责 IBM 产品TMF(Tivoli Management Framework)的维护和客户支持工作,热爱 Linux。

[精彩] 关于dup2()的疑问

分类:UNIX/LINUX

原文链接:http://bbs.chinaunix.net/viewthread.php?tid=254008

#include<stdio.h>;
#include<unistd.h>;
int main(int argc, char *argv[])
{
        int fd;
        FILE  *fp;

        //to open a log file
        if((fp=fopen("/var/log/ftp","w"))==NULL)
        {
                printf("fopen error n");
                exit(1);
        }

        fd=fileno(fp);
        if(dup2(fd,STDOUT_FILENO)==-1){

                fprintf(stderr,"Redirect Standard Out error");
                exit(1);
        }
        printf("have a testn");
        fclose(fp);
        printf("The end!n");
}
[color=red][/color]
为什么关掉文件后,还能把The end写进去?运行结果如下:
[root@localhost bxf]# ./a.out
[root@localhost bxf]# cat /var/log/ftp
have a test
The end!



 kj501 回复于:2004-02-05 18:32:46

fp是指向打开的/var/log/ftp,fclose(fp)也只是关闭这个文件。并没有关闭stdout。所以printf()仍然可以向stdout输出信息。由于stdout已经重定向为/var/log/ftp,信息自然输出到了/var/log/ftp中。


 flyingbxf 回复于:2004-02-16 13:09:44

再问一个问题:我把想要的信息重定向到文件后,便于进行分析判断下一步的操作。等将重要信息截取完后,我如果再想把输出重定向回来的话,应该用哪一个函数呢?不知道不退出程序的话,能不能重定向回来?


 lenovo 回复于:2004-02-16 13:45:35

你再close(fd)试试。


 flyingbxf 回复于:2004-02-16 15:46:02

:em09: 看见你我就感觉有戏了。马上去测试 :mrgreen:


 flyingbxf 回复于:2004-02-16 17:06:16

可惜。还是不行。


 kj501 回复于:2004-02-16 17:23:49

引用:原帖由 "flyingbxf"]再问一个问题:我把想要的信息重定向到文件后,便于进行分析判断下一步的操作。等将重要信息截取完后,我如果再想把输出重定向回来的话,应该用哪一个函数呢?不知道不退出程序的话,能不能重定向回来?
 发表:


如果要把输出重定向回来,就应该在重定向前先保存stdout,然后在重定向完成之后,再把保存的stdout用dup2()恢复回来。


 flyingbxf 回复于:2004-02-16 20:13:57

引用:原帖由 "kj501" 发表:

如果要把输出重定向回来,就应该在重定向前先保存stdout,然后在重定向完成之后,再把保存的stdout用dup2()恢复回来。


怎么个保存法?压栈?还是用另外的指针指向STDOUT?


 henngy 回复于:2004-02-17 16:03:51

用unlink试试


 flyingbxf 回复于:2004-02-17 16:37:06

unlink("/var/log/ftp");与 rm 命令 rm  /var/log/ftp 都是删除文件,其具体实现有什么区别吗?


 forest077 回复于:2004-02-17 18:31:38

dup2我没有用过,不过我知道freopen重定向输出到文件后如何恢复到终端上,不知和这个一样不一样。


 flyingbxf 回复于:2004-02-17 20:03:28

引用:原帖由 "forest077"]dup2我没有用过,不过我知道freopen重定向输出到文件后如何恢复到终端上,不知和这个一样不一样。
 发表:


freopen怎么恢复??用fclose()吗?


 kj501 回复于:2004-02-17 21:24:40

引用:原帖由 "flyingbxf" 发表:

怎么个保存法?压栈?还是用另外的指针指向STDOUT?

 
int saved_fd ;
saved_fd = STDOUT_FILENO; /* 保存标准输出 */

dup2(saved_fd, STDOUT_FILENO); /* 恢复标准输出 */


 forest077 回复于:2004-02-17 21:44:12

freopen重定向后的恢复,基本原理是获得当前的tty,然后把输出流定向到当前tty即可,实现起来很简单,两三句话即可。范例如下:
//把stdout定向到文件aaa
fpout=freopen("aaa","w",stdout);
//现在printf会输出到文件aaa里面
fp=popen("tty","r");
fgets(str,sizeof(str),fp);
str[strlen(str)-1]=0;//当前tty保存在str里面了
fclose(fp);
//把fpout重定向到当前tty
freopen(str,"w",fpout);
//现在printf输出到当前终端
不知道这个功能适合不适合你。


 forest077 回复于:2004-02-17 21:45:11

kj501的方式我要试一试,如果可以,比我的方便多了。


 converse 回复于:2004-02-17 23:04:12

kj501的方法我试过了不行呀


 win_hate 回复于:2004-02-17 23:05:09

引用:原帖由 "flyingbxf"]alhost bxf 发表:
#为什么关掉文件后,还能把The end写进去?运行结果如下:
[root@localhost bxf]# ./a.out
[root@localhost bxf]# cat /var/log/ftp
have a test
The end!



这是因为你 dup2 后, stdout 和 fp 都指向文件 /var/log/ftp, 关掉其中一个后,另一个仍然可写。


 win_hate 回复于:2004-02-17 23:08:32

引用:原帖由 "kj501" 发表:

int saved_fd ;
saved_fd = STDOUT_FILENO; /* 保存标准输出 */

dup2(saved_fd, STDOUT_FILENO); /* 恢复标准输出 */



你写错了吧?这几行程序一般相当于:

int saved_fd;
save_fd = 2;
dup2 (2, 2);

这怎么行?


 win_hate 回复于:2004-02-17 23:12:18

这个估计行:


int sfd;
sfd = dup (STDOUT_FILENO);  /* save */
....

dup2 (sfd, STDOUT_FILENO); /* restore */




 converse 回复于:2004-02-18 17:45:09

win_hate,你好!
如果我这么写:

int main(void)
{
        FILE *fp;
        int fd, id;
                                                                                
        id = dup(STDOUT_FILENO);
                                                                                
        fp = fopen("install.log", "w");
        if (fp == NULL) {
                printf("read error.n");
                return 0;
        }
        fd = fileno(fp);
        dup2(fd, STDOUT_FILENO);
        printf("testn");
        fclose(fp);
        printf("hellon");
                                                                                
        dup2(id, STDOUT_FILENO);
        printf("worldn");
                                                                                
        return 0;
}

将在终端打印出
test
hello
world

如果我要做到在打开的文件install.log中写入
test
hello
而在终端输出
world
应该怎么作呢?

另外,在学习UNIX编程时我看的是APUE,感觉光看书中的例子还不够呀,那些例子功能比较单一,大多为了讲述一组函数的功能的,有没有那本书将大部分函数功能都用上,实现一个比较大的程序呢


 lenovo 回复于:2004-02-18 17:53:30

你的要求很奇怪哦。
那样的话你用fprintf()吧。
也没这么麻烦了。


 win_hate 回复于:2004-02-18 18:29:20

引用:原帖由 "converse" 发表:
 dup2(id, STDOUT_FILENO);
printf("worldn");

return 0; 



在 dup2 (id, STDOUT_FILENO); 之前加入


fflush (stdout);


就能达到你要求的效果了。该死的缓冲.....

对另一个问题, 我认为只有写过 ''真正的程序'' 后才能融会贯通。如果没有参加实际项目的机会,可以多看看代码。


 converse 回复于:2004-02-18 21:55:22

我明白了,fflush函数把原来的test和hello强行写入打开的文件中,然后由于stdut被重新定位到屏幕上所以就在屏幕上输出world了,对吧??

我现在比较的郁闷,虽然找了一个作LINUX的公司,不过作的东西好象和我想像的有分别,暂时还用不上APUE里的知识,缺少了在实际中的锻炼学起来自然慢的多,我看了一下APUE后面有几个实际的例子,不过好象是数据库的,然后是打印机的,我这些都没有学过呀,不知道看过这本书的朋友感觉怎么样呢?

个人觉得APUE对于初学者来说太难了,很多基础的概念一带而过,还是lenovo推荐过的《UNIX程序设计教程》不错,把这两本书结合在一起看学的很快,就是可惜的是没有实践的锻炼,郁闷呀。。。。。。。。。


 lenovo 回复于:2004-02-18 22:35:39

引用:原帖由 "converse" 发表:
我明白了,fflush函数把原来的test和hello强行写入打开的文件中,然后由于stdut被重新定位到屏幕上所以就在屏幕上输出world了,对吧??

我现在比较的郁闷,虽然找了一个作LINUX的公司,不过作的东西好象和我想像..........


我看APUE看的也很慢,现在看到进程高级通信那里了,
时间也不是很充足。不过感觉用处还是很大的,
很多不明白的地方,那本书都说了。


 forest077 回复于:2004-02-19 09:45:19

说来惭愧,虽然我一直向别人大力推荐APUE,但是自己从来没有从头到尾看过一遍。我把它当作一本开发手册,用到时再去翻翻,呵呵。


 flyingbxf 回复于:2004-02-19 11:01:36

[quote="win_hate"]
在 dup2 (id, STDOUT_FILENO); 之前加入 

代码: 

[color=blue]fflush (stdout); [/color]
 

quote]

我没有加fflush (stdout); 也没有出现问题啊!为什么呢?我是在linux下运行的。


 win_hate 回复于:2004-02-19 11:41:07

引用:原帖由 "flyingbxf" 发表:

 

quote]

我没有加fflush (stdout); 也没有出现问题啊!为什么呢?我是在linux下运行的。



我记得你是做嵌入式开发的,可能是环境不同。你换个台式机试一试可能就有这个问题。


 huahua0459 回复于:2004-02-19 18:32:59

dup2首先close标准输出,然后把标准输出的文件描述符1传到fd所代表的结构中?
close(fd)其实是close了一个没有用的数(3),fclose关闭的也是这个数字(3),可以通过fileno看到。所以还可以写道这个文件中。如果关闭了close(1),则不行了。
看来FILE和文件描述符最好不一起使用。不然一定要当心啊。


 kj501 回复于:2004-02-19 20:20:35

引用:原帖由 "win_hate" 发表:


你写错了吧?这几行程序一般相当于:

int saved_fd;
save_fd = 2;
dup2 (2, 2);

这怎么行?


说错了,stdout应该是1,2是stderr。


 kj501 回复于:2004-02-19 20:42:05

引用:原帖由 "kj501" 发表:

int saved_fd ;
saved_fd = STDOUT_FILENO; /* 保存标准输出 */

dup2(saved_fd, STDOUT_FILENO); /* 恢复标准输出 */


我来检讨一下,由于我对dup2的使用在概念有理解错误,我给出的例子是行不通的。
经过试验,我发现dup2(int oldfd, int newfd)其中的newfd必须是一个实际打开文件的文件描述符,不能只是一个不指向任何文件的整数。
这是我作试验的代码:

#include<unistd.h>;
#include<stdlib.h>;
#include<fcntl.h>;
#include<sys/types.h>;
#include<sys/stat.h>;

int main()
{
int sfd,testfd;

testfd = open("temp",O_CREAT | O_RDWR | O_APPEND);

if (-1 == testfd) {
printf("open file error.n");
exit(1);
}

/* 先复制一个真实的文件描述符 */
sfd = dup(testfd);

/* 保存标准输出 */
if (-1 == dup2(STDOUT_FILENO,sfd) ) {
printf("can't save fd n");
exit(1);
}

/* 重定向 */
if (-1 == dup2(testfd,STDOUT_FILENO) ) {
printf("can't redirect fd errorn");
exit(1);
}

/* 此时向stdout写入应该输出到文件 */
write(STDOUT_FILENO,"filen",5);

/* 恢复stdout */
if (-1 != dup2(sfd,STDOUT_FILENO) ) {
printf("recover fd ok n");

/* 恢复后,写入stdout应该向屏幕输出 */
write(STDOUT_FILENO,"stdoutn",7);
}
}

如果把其中的这一行注释掉:

sfd = dup(testfd);

dup2函数执行时肯定会失败。
但从代码中也可以看出,我前面提出的先保存文件描述符,然后在重定向之后再恢复回来的思路是行得能的。
有不当之处,还请大家多多指教!


 win_hate 回复于:2004-02-19 20:48:40

引用:原帖由 "kj501" 发表:

说错了,stdout应该是1,2是stderr。



说得对,我写错了, 应该是1


 win_hate 回复于:2004-02-19 21:16:51

引用:原帖由 "kj501" 发表:
dup2函数执行时肯定会失败。
但从代码中也可以看出,我前面提出的先保存文件描述符,然后在重定向之后再恢复回来的思路是行得能的。
有不当之处,还请大家多多指教!



你对 dup2 的理解仍然有误, dup2 (a, b) 把 a 复制到 指定的“数字”(文件描述)上, 如果 b 是已打开的文件描述符, 先把 b 关掉(取消关联)。

你可以试一试, 把

sfd = dup(testfd); 


注释掉,然后,很重要的一点,把 sfd 初始化为一个整数, 不要太大。我估计程序不会出错。

你试过后,请把结果告诉我。谢谢。


 flyingbxf 回复于:2004-02-20 09:20:16

引用:原帖由 "win_hate" 发表:


我记得你是做嵌入式开发的,可能是环境不同。你换个台式机试一试可能就有这个问题。


我写错了,我的使用环境是uclinux,我在台式机上redhat8.0的环境下试了,不加fflush(stdout)果然全都输出到屏幕上了,文件里面没有写进去.不过我不明白能不能写进文件关缓冲什么事 呢?


 converse 回复于:2004-02-20 14:36:36

我来谈谈我的看法吧,我对这个问题还有一些疑问的

                                                                                                                                             #include <stdio.h>;
#include <unistd.h>;
                                                                                                                                              
int main(void)
{
        FILE *fp;
        int fd, id;
                                                                                                                                              
        id = dup(STDOUT_FILENO);//这时id和STDOUT_FILENO一起指向stdout
        if ((fp = fopen("install.log", "w")) == NULL) {
                perror("read error.n");
                exit(-1);
        }
        fd = fileno(fp);//得到fp的文件描述符
        dup2(fd, STDOUT_FILENO);//STDOUT_FILENO和stdout流断开,             //STDOUT_FILENO指向fd的对应文件,这样使输出到达fd的文件
                                //fd对应的是已经打开的文件install.log,//而stdout在这里指的是终端,现在任何的输入都输入到
                                //install.log中
        printf("hellon");
                                                                                                                                              
        fflush(stdout);//刷新stdout上的缓冲区,这样前面的hello写入文件//install.log中
        dup2(id, STDOUT_FILENO);
        printf("worldn");
        fclose(fp);
                                                                                                                                              
        return 0;
}



以上的注释基本表达了我对这几个函数运用的了解,不过这里对fflush(stdout)的运用还有一点疑问,fflush(stream)刷新的是流stream上的缓冲,是不是可以理解为不论文件描述符STDOUT_FILENO定位到了哪个文件,stdout都保存有未写入文件的缓冲区的内容呢?按理来说,文件描述符比流更加底层才对呀


 converse 回复于:2004-02-20 14:39:21

倒,我的代码怎么在这里变得这么乱呀


 win_hate 回复于:2004-02-20 22:32:39

引用:原帖由 "converse" 发表:

fflush(stdout);//刷新stdout上的缓冲区,这样前面的hello写入文件 install.log中
        dup2(id, STDOUT_FILENO);
        printf("worldn");
        fclose(fp); 



引用:原帖由 "converse" 发表:

不过这里对fflush(stdout)的运用还有一点疑问,fflush(stream)刷新的是流 stream上的缓冲,是不是可以理解为不论文件描述符STDOUT_FILENO定位到了哪个文件,stdout都保存有未写入文件的缓冲区的内容呢?按理来说,文件描述符比流更加底层才对呀



呵呵, 确实如此。 事实上,文件指针,比如说fp, 是根据fileno(fp)来对应文件的。你看上面的代码,STDOUT_FILENO 已经对应到 install.log,所以,当fflush(stdout)后, 缓冲内容写入文件 install.log。如果不fflush,执行
dup2 (id, STDOUT_FILENO), 这时 STDOUT_FILENO又对应回屏幕了, 然后缓冲中的内容和后来的数据都输到屏幕上了。


 kj501 回复于:2004-02-21 10:56:37

引用:原帖由 "win_hate" 发表:

注释掉,然后,很重要的一点,把 sfd 初始化为一个整数, 不要太大。我估计程序不会出错。

你试过后,请把结果告诉我。谢谢。


你的说法一点不错,我将

sfd = dup(testfd);

注释掉后,再将sfd初始化为0~1023之间的任何数,都可以执行成功。看来问题出在这一句:

dup2(STDOUT_FILENO,sfd);

我用的系统是linux(内核2.4.21),由于linux内核限制最大打开的文件总数为1024(这一点可能改进了,原来很多书上说的是最大只能打开256个文件),因此,dup2在执行时肯定会进行文件描述符的合法性检查,如果大于1023,肯定导致失败。sfd没有初始化时的值是由系统任意给出的(将sfd没有初始化的值打印出来,结果是1073817472),已经超出了1023的限制,从而导致dup2()执行失败。这才是问题的真正原因。其实在man文档中对错误原因说得很清楚:“oldfd  isn't  an  open  file  descriptor, or newfd is out of the allowed range for file descriptors.” 只是我自己没有认真看。
经过这次讨论,感觉自己对dup2()的理解加深不少,APUE上对dup2()的介绍对于全面理解这两个函数的使用并不充分。以后man文档一定要仔细看。
最后,非常感谢win_hate,希望以后能和你多多交流。:)


 converse 回复于:2004-02-21 11:22:35

这样针对一组函数功能的讨论真的可以提高很快,我也受益匪浅呀

建议斑竹加为精华


 win_hate 回复于:2004-02-21 12:50:17

引用:原帖由 "kj501"]最后,非常感谢win_hate,希望以后能和你多多交流。
 发表:



kj501 兄客气了, 这里我常来,期待与大家交流,向各位学习。


 forest077 回复于:2004-02-21 16:02:53

我有一个不解的地方,就是freopen实现的功能也是把一个流重定向到另一个流去,它和dup、dup2的底层实现有什么不一样吗?


 win_hate 回复于:2004-02-21 23:05:43

引用:原帖由 "forest077"]我有一个不解的地方,就是freopen实现的功能也是把一个流重定向到另一个流去,它和dup、dup2的底层实现有什么不一样吗?
 发表:



freopen 并不是把一个流定向到另一个流,而是把一个流对应到一个新的文件:

fd1 = dup (fd);   fd1, fd 对应同一个文件, 但不能指定 fd1 的值。

dup2 (fd, fd1);  fd1, fd 对应同一个文件,但可以指定 fd1的值,根据 kj501提供的资料, fd1 不能大于 1024

freopen ("xxx", "w", fp);  关掉 fp 对应的文件, 并打开文件 xxx, 指针 fp 的值不变。

在前两种情况中, 都有两个文件描述符出现, 但在第三种情况中, 只有一个文件指针。

使用 freopen (..., fp) 时, 原来 fp 对应的缓存写入, 关掉 fileno(fp), 寻找一个最小未用的文件描述符, 在其上打开新文件。也就是说, fp 值不变, 其指向的 FILE 结构仍然使用, 但 fileno (fp) 则有可能改变。


 Wangwen 回复于:2004-02-21 23:42:35

引用:原帖由 "win_hate" 发表:


呵呵, 确实如此。 事实上,文件指针,比如说fp, 是根据fileno(fp)来对应文件的。你看上面的代码,STDOUT_FILENO 已经对应到 install.log,所以,当fflush(stdout)后, 缓冲内容写入文件 install.log。如果不fflus..........


可以理解为 dup2首先close标准输出,然后把标准输出的文件描述符STDOUT_FILENO 传到fd所代表的fp文件指针结构中

STDOUT_FILENO 指向install.log的文件表

文件指针stdout的文件描述符为STDOUT_FILENO

所以fflush(stdout)后, 缓冲内容写入文件 install.log


 win_hate 回复于:2004-02-22 08:48:06

引用:原帖由 "Wangwen" 发表:

可以理解为 dup2首先close标准输出,然后把标准输出的文件描述符STDOUT_FILENO 传到fd所代表的fp文件指针结构中

STDOUT_FILENO 指向install.log的文件表

文件指针stdout的文件描述符为STDOUT_FILENO

所以f..........



说法有误,dup2 是低级 I/O 操作函数, 不会直接与 fp 指向的文件结构打交道。


 flyingbxf 回复于:2004-02-23 09:24:54

引用:原帖由 "converse" 发表:

fflush(stream)刷新的是流stream上的缓冲,是不是可以理解为不论文件描述符STDOUT_FILENO定位到了哪个文件,stdout都保存有未写入文件的缓冲区的内容呢?........


不用fflush(stream)就写不进文件,但为什么不用fflush(stream)就能输出到屏幕上呢?


 flyingbxf 回复于:2004-02-23 09:29:40

还有就是  dup2(STDOUT_FILENO,sfd); 和 
 dup2(sfd,STDOUT_FILENO); 起的作用是一样的?都是将两个文件描述符指向同一个文件是吗?


 Wangwen 回复于:2004-02-23 12:08:53

可以理解为 dup2首先close标准输出,

将STDOUT_FILENO 指向install.log的文件表 

文件指针stdout的文件描述符为STDOUT_FILENO 

所以fflush(stdout)后, 缓冲内容写入文件 install.log

使用fprintf(fp,"hellon");fflush(fp);仍然可以将hello写入install.log

dup2(id, STDOUT_FILENO)使STDOUT_FILENO指向标准输出

printf("worldn");写入终端


 Wangwen 回复于:2004-02-23 12:11:24

引用:原帖由 "flyingbxf" 发表:

不用fflush(stream)就写不进文件,但为什么不用fflush(stream)就能输出到屏幕上呢?


程序结束是 执行了fflush(stream)


 win_hate 回复于:2004-02-23 14:14:13

引用:原帖由 "Wangwen" 发表:
可以理解为 dup2首先close标准输出,

将STDOUT_FILENO 指向install.log的文件表

文件指针stdout的文件描述符为STDOUT_FILENO

所以fflush(stdout)后, 缓冲内容写入文件 install.log

使用fprintf(fp,"hellon");fflush(fp);仍然可以将hello写入install.log

dup2(id, STDOUT_FILENO)使STDOUT_FILENO指向标准输出

printf("worldn");写入终端



Great!


 Wangwen 回复于:2004-02-23 14:25:49

xiexie


 converse 回复于:2004-02-23 20:10:51

茅塞顿开,不错不错,强烈建议加精


 declare 回复于:2006-07-07 16:50:01

#include<stdio.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
        int fd;
        FILE  *fp;

        //to open a log file
        if((fp=fopen("nihao","w"))==NULL)
        {
                printf("fopen error n");
                exit(1);
        }

        fd=fileno(fp);
        if(dup2(fd,STDOUT_FILENO)==-1){
                fprintf(stderr,"Redirect Standard Out error");
                exit(1);
        }
        printf("have a testn");
        fclose(fp);
        printf("The end!n");
}

  该程序屏幕没有输出,只是输出到文件了

Visual C 线程同步技术剖析 (转)

分类:编程点滴

摘要: 多线程同步技术是计算机软件开发的重要技术,本文对多线程的各种同步技术的原理和实现进行了初步探讨。

  关键词: VC++6.0; 线程同步;临界区;事件;互斥;信号量;  正文

  使线程同步

  在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。

   如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个 全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修 改后的。

  为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。

  线程同步是一个非常大的话题,包括方方面面的内容。从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。

  内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。

  临界区

   临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线 程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操 作共享资源的目的。

  临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用 EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的 CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中 的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。


图1 使用临界区保持线程同步

   下面通过一段代码展示了临界区在保护多线程访问的共享资源中的作用。通过两个线程来分别对全局变量g_cArray[10]进行写入操作,用临界区结构 对象g_cs来保持线程的同步,并在开启线程前对其进行初始化。为了使实验效果更加明显,体现出临界区的作用,在线程函数对共享资源g_cArray [10]的写入时,以Sleep()函数延迟1毫秒,使其他线程同其抢占CPU的可能性增大。如果不使用临界区对其进行保护,则共享资源数据将被破坏(参 见图1(a)所示计算结果),而使用临界区对线程保持同步后则可以得到正确的结果(参见图1(b)所示计算结果)。代码实现清单附下:

// 临界区结构对象
CRITICAL_SECTION g_cs;
// 共享资源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
 // 进入临界区
 EnterCriticalSection(&g_cs);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 离开临界区
 LeaveCriticalSection(&g_cs);
 return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
 // 进入临界区
 EnterCriticalSection(&g_cs);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 离开临界区
 LeaveCriticalSection(&g_cs);
 return 0;
}
……
void CSample08View::OnCriticalSection()
{
 // 初始化临界区
 InitializeCriticalSection(&g_cs);
 // 启动线程
 AfxBeginThread(ThreadProc10, NULL);
 AfxBeginThread(ThreadProc11, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

   在使用临界区时,一般不允许其运行时间过长,只要进入临界区的线程还没有离开,其他所有试图进入此临界区的线程都会被挂起而进入到等待状态,并会在一定 程度上影响。程序的运行性能。尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界区。如果进入了临界区却一直没有释放,同样也会引 起其他线程的长时间等待。换句话说,在执行了EnterCriticalSection()语句进入临界区后无论发生什么,必须确保与之匹配的 LeaveCriticalSection()都能够被执行到。可以通过添加结构化异常处理代码来确保LeaveCriticalSection()语句 的执行。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

  MFC为临界区提供有一个 CCriticalSection类,使用该类进行线程同步处理是非常简单的,只需在线程函数中用CCriticalSection类成员函数Lock ()和UnLock()标定出被保护代码片段即可。对于上述代码,可通过CCriticalSection类将其改写如下:

// MFC临界区类对象
CCriticalSection g_clsCriticalSection;
// 共享资源
char g_cArray[10];
UINT ThreadProc20(LPVOID pParam)
{
 // 进入临界区
 g_clsCriticalSection.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 离开临界区
 g_clsCriticalSection.Unlock();
 return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
 // 进入临界区
 g_clsCriticalSection.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 离开临界区
 g_clsCriticalSection.Unlock();
 return 0;
}
……
void CSample08View::OnCriticalSectionMfc()
{
 // 启动线程
 AfxBeginThread(ThreadProc20, NULL);
 AfxBeginThread(ThreadProc21, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  管理事件内核对象

  在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。对于前面那段使用临界区保持线程同步的代码可用事件对象的线程同步方法改写如下:

// 事件句柄
HANDLE hEvent = NULL;
// 共享资源
char g_cArray[10];
……
UINT ThreadProc12(LPVOID pParam)
{
 // 等待事件置位
 WaitForSingleObject(hEvent, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 处理完成后即将事件对象置位
 SetEvent(hEvent);
 return 0;
}
UINT ThreadProc13(LPVOID pParam)
{
 // 等待事件置位
 WaitForSingleObject(hEvent, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 处理完成后即将事件对象置位
 SetEvent(hEvent);
 return 0;
}
……
void CSample08View::OnEvent()
{
 // 创建事件
 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 // 事件置位
 SetEvent(hEvent);
 // 启动线程
 AfxBeginThread(ThreadProc12, NULL);
 AfxBeginThread(ThreadProc13, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

   在创建线程前,首先创建一个可以自动复位的事件内核对象hEvent,而线程函数则通过WaitForSingleObject()等待函数无限等待 hEvent的置位,只有在事件置位时WaitForSingleObject()才会返回,被保护的代码将得以执行。对于以自动复位方式创建的事件对 象,在其置位后一被WaitForSingleObject()等待到就会立即复位,也就是说在执行ThreadProc12()中的受保护代码时,事件 对象已经是复位状态的,这时即使有ThreadProc13()对CPU的抢占,也会由于WaitForSingleObject()没有hEvent的 置位而不能继续执行,也就没有可能破坏受保护的共享资源。在ThreadProc12()中的处理完成后可以通过SetEvent()对hEvent的置 位而允许ThreadProc13()对共享资源g_cArray的处理。这里SetEvent()所起的作用可以看作是对某项特定任务完成的通知。

  使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。可以通过OpenEvent()函数获取得到,其函数原型为:

HANDLE OpenEvent(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 指向事件对象名的指针
);

   如果事件对象已创建(在创建事件时需要指定事件名),函数将返回指定事件的句柄。对于那些在创建事件时没有指定事件名的事件内核对象,可以通过使用内核 对象的继承性或是调用DuplicateHandle()函数来调用CreateEvent()以获得对指定事件对象的访问权。在获取到访问权后所进行的 同步操作与在同一个进程中所进行的线程同步操作是一样的。

  如果需要在一个线程中等待多个事件,则用 WaitForMultipleObjects()来等待。WaitForMultipleObjects()与WaitForSingleObject ()类似,同时监视位于句柄数组中的所有句柄。这些被监视对象的句柄享有平等的优先权,任何一个句柄都不可能比其他句柄具有更高的优先权。 WaitForMultipleObjects()的函数原型为:

DWORD WaitForMultipleObjects(
 DWORD nCount, // 等待句柄数
 CONST HANDLE *lpHandles, // 句柄数组首地址
 BOOL fWaitAll, // 等待标志
 DWORD dwMilliseconds // 等待时间间隔
);

   参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对 象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。 dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回 WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某个值,则说明所有指定对象的状态均 为已通知状态(当fWaitAll为TRUE时)或是用以减去WAIT_OBJECT_0而得到发生通知的对象的索引(当fWaitAll为FALSE 时)。如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知, 且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用以减去WAIT_OBJECT_0表示一个等待正常结束的互斥对象的 索引(当fWaitAll为FALSE时)。 下面给出的代码主要展示了对WaitForMultipleObjects()函数的使用。通过对两个事件内核对象的等待来控制线程任务的执行与中途退 出:

// 存放事件句柄的数组
HANDLE hEvents[2];
UINT ThreadProc14(LPVOID pParam)
{
 // 等待开启事件
 DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
 // 如果开启事件到达则线程开始执行任务
 if (dwRet1 == WAIT_OBJECT_0)
 {
  AfxMessageBox("线程开始工作!");
  while (true)
  {
   for (int i = 0; i < 10000; i++);
   // 在任务处理过程中等待结束事件
   DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0);
   // 如果结束事件置位则立即终止任务的执行
   if (dwRet2 == WAIT_OBJECT_0 + 1)
    break;
  }
 }
 AfxMessageBox("线程退出!");
 return 0;
}
……
void CSample08View::OnStartEvent()
{
 // 创建线程
 for (int i = 0; i < 2; i++)
  hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
  // 开启线程
  AfxBeginThread(ThreadProc14, NULL);
  // 设置事件0(开启事件)
  SetEvent(hEvents[0]);
}
void CSample08View::OnEndevent()
{
 // 设置事件1(结束事件)
 SetEvent(hEvents[1]);
}

   MFC为事件相关处理也提供了一个CEvent类,共包含有除构造函数外的4个成员函数PulseEvent()、ResetEvent()、 SetEvent()和UnLock()。在功能上分别相当与Win32 API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函数。而构造函数则履行了原 CreateEvent()函数创建事件对象的职责,其函数原型为:

CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );

  按照此缺省设置将创建一个自动复位、初始状态为复位状态的没有名字的事件对象。封装后的CEvent类使用起来更加方便,图2即展示了CEvent类对A、B两线程的同步过程:


图2 CEvent类对线程的同步过程示意

   B线程在执行到CEvent类成员函数Lock()时将会发生阻塞,而A线程此时则可以在没有B线程干扰的情况下对共享资源进行处理,并在处理完成后通 过成员函数SetEvent()向B发出事件,使其被释放,得以对A先前已处理完毕的共享资源进行操作。可见,使用CEvent类对线程的同步方法与通过 API函数进行线程同步的处理方法是基本一致的。前面的API处理代码可用CEvent类将其改写为:

// MFC事件类对象
CEvent g_clsEvent;
UINT ThreadProc22(LPVOID pParam)
{
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 事件置位
 g_clsEvent.SetEvent();
 return 0;
}
UINT ThreadProc23(LPVOID pParam)
{
 // 等待事件
 g_clsEvent.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 return 0;
}
……
void CSample08View::OnEventMfc()
{
 // 启动线程
 AfxBeginThread(ThreadProc22, NULL);
 AfxBeginThread(ThreadProc23, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}
  信号量内核对象

   信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源 的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置 为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数 减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应 在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。


图3 使用信号量对象控制资源

   下面结合图例3来演示信号量对象对资源的控制。在图3中,以箭头和白色箭头表示共享资源所允许的最大资源计数和当前可用资源计数。初始如图(a)所示, 最大资源计数和当前可用资源计数均为4,此后每增加一个对资源进行访问的线程(用黑色箭头表示)当前资源计数就会相应减1,图(b)即表示的在3个线程对 共享资源进行访问时的状态。当进入线程数达到4个时,将如图(c)所示,此时已达到最大资源计数,而当前可用资源计数也已减到0,其他线程无法对共享资源 进行访问。在当前占有资源的线程处理完毕而退出后,将会释放出空间,图(d)已有两个线程退出对资源的占有,当前可用计数为2,可以再允许2个线程进入到 对资源的处理。可以看出,信号量是通过计数来对线程访问资源进行控制的,而实际上信号量确实也被称作Dijkstra计数器。

  使用信 号量内核对象进行线程同步主要会用到CreateSemaphore()、OpenSemaphore()、ReleaseSemaphore()、 WaitForSingleObject()和WaitForMultipleObjects()等函数。其中,CreateSemaphore()用来 创建一个信号量内核对象,其函数原型为:

HANDLE CreateSemaphore(
 LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针
 LONG lInitialCount, // 初始计数
 LONG lMaximumCount, // 最大计数
 LPCTSTR lpName // 对象名指针
);

   参数lMaximumCount是一个有符号32位值,定义了允许的最大资源计数,最大取值不能超过4294967295。lpName参数可以为创建 的信号量定义一个名字,由于其创建的是一个内核对象,因此在其他进程中可以通过该名字而得到此信号量。OpenSemaphore()函数即可用来根据信 号量名打开在其他进程中创建的信号量,函数原型如下:

HANDLE OpenSemaphore(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 信号量名
);

   在线程离开对共享资源的处理时,必须通过ReleaseSemaphore()来增加当前可用资源计数。否则将会出现当前正在处理共享资源的实际线程数 并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。ReleaseSemaphore()的函数原型为:

BOOL ReleaseSemaphore(
 HANDLE hSemaphore, // 信号量句柄
 LONG lReleaseCount, // 计数递增数量
 LPLONG lpPreviousCount // 先前计数
);

   该函数将lReleaseCount中的值添加给信号量的当前资源计数,一般将lReleaseCount设置为1,如果需要也可以设置其他的值。 WaitForSingleObject()和WaitForMultipleObjects()主要用在试图进入共享资源的线程函数入口处,主要用来判 断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。

  信号量 的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以 为没一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面 进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。下面给出的示例 代码即展示了类似的处理过程:

// 信号量对象句柄
HANDLE hSemaphore;
UINT ThreadProc15(LPVOID pParam)
{
 // 试图进入信号量关口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 线程任务处理
 AfxMessageBox("线程一正在执行!");
 // 释放信号量计数
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
UINT ThreadProc16(LPVOID pParam)
{
 // 试图进入信号量关口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 线程任务处理
 AfxMessageBox("线程二正在执行!");
 // 释放信号量计数
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
UINT ThreadProc17(LPVOID pParam)
{
 // 试图进入信号量关口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 线程任务处理
 AfxMessageBox("线程三正在执行!");
 // 释放信号量计数
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
……
void CSample08View::OnSemaphore()
{
 // 创建信号量对象
 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
 // 开启线程
 AfxBeginThread(ThreadProc15, NULL);
 AfxBeginThread(ThreadProc16, NULL);
 AfxBeginThread(ThreadProc17, NULL);
}


图4 开始进入的两个线程


图5 线程二退出后线程三才得以进入

   上述代码在开启线程前首先创建了一个初始计数和最大资源计数均为2的信号量对象hSemaphore。即在同一时刻只允许2个线程进入由 hSemaphore保护的共享资源。随后开启的三个线程均试图访问此共享资源,在前两个线程试图访问共享资源时,由于hSemaphore的当前可用资 源计数分别为2和1,此时的hSemaphore是可以得到通知的,也就是说位于线程入口处的WaitForSingleObject()将立即返回,而 在前两个线程进入到保护区域后,hSemaphore的当前资源计数减少到0,hSemaphore将不再得到通知, WaitForSingleObject()将线程挂起。直到此前进入到保护区的线程退出后才能得以进入。图4和图5为上述代脉的运行结果。从实验结果可 以看出,信号量始终保持了同一时刻不超过2个线程的进入。

  在MFC中,通过CSemaphore类对信号量作了表述。该类只具有一个构造函数,可以构造一个信号量对象,并对初始资源计数、最大资源计数、对象名和安全属性等进行初始化,其原型如下:

CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );

   在构造了CSemaphore类对象后,任何一个访问受保护共享资源的线程都必须通过CSemaphore从父类CSyncObject类继承得到的 Lock()和UnLock()成员函数来访问或释放CSemaphore对象。与前面介绍的几种通过MFC类保持线程同步的方法类似,通过 CSemaphore类也可以将前面的线程同步代码进行改写,这两种使用信号量的线程同步方法无论是在实现原理上还是从实现结果上都是完全一致的。下面给 出经MFC改写后的信号量线程同步代码:

// MFC信号量类对象
CSemaphore g_clsSemaphore(2, 2);
UINT ThreadProc24(LPVOID pParam)
{
 // 试图进入信号量关口
 g_clsSemaphore.Lock();
 // 线程任务处理
 AfxMessageBox("线程一正在执行!");
 // 释放信号量计数
 g_clsSemaphore.Unlock();
 return 0;
}
UINT ThreadProc25(LPVOID pParam)
{
 // 试图进入信号量关口
 g_clsSemaphore.Lock();
 // 线程任务处理
 AfxMessageBox("线程二正在执行!");
 // 释放信号量计数
 g_clsSemaphore.Unlock();
 return 0;
}
UINT ThreadProc26(LPVOID pParam)
{
 // 试图进入信号量关口
 g_clsSemaphore.Lock();
 // 线程任务处理
 AfxMessageBox("线程三正在执行!");
 // 释放信号量计数
 g_clsSemaphore.Unlock();
 return 0;
}
……
void CSample08View::OnSemaphoreMfc()
{
 // 开启线程
 AfxBeginThread(ThreadProc24, NULL);
 AfxBeginThread(ThreadProc25, NULL);
 AfxBeginThread(ThreadProc26, NULL);
}
  互斥内核对象

   互斥(Mutex)是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。同临界区有些类似,只有拥有互斥对象的线程才具有访问资 源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交 出,以便其他线程在获得后得以访问资源。与其他几种内核对象不同,互斥对象在操作系统中拥有特殊代码,并由操作系统来管理,操作系统甚至还允许其进行一些 其他内核对象所不能进行的非常规操作。为便于理解,可参照图6给出的互斥内核对象的工作模型:


图6 使用互斥内核对象对共享资源的保护

   图(a)中的箭头为要访问资源(矩形框)的线程,但只有第二个线程拥有互斥对象(黑点)并得以进入到共享资源,而其他线程则会被排斥在外(如图(b)所 示)。当此线程处理完共享资源并准备离开此区域时将把其所拥有的互斥对象交出(如图(c)所示),其他任何一个试图访问此资源的线程都有机会得到此互斥对 象。

  以互斥内核对象来保持线程同步可能用到的函数主要有CreateMutex()、OpenMutex()、 ReleaseMutex()、WaitForSingleObject()和WaitForMultipleObjects()等。在使用互斥对象前, 首先要通过CreateMutex()或OpenMutex()创建或打开一个互斥对象。CreateMutex()函数原型为:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性指针
 BOOL bInitialOwner, // 初始拥有者
 LPCTSTR lpName // 互斥对象名
);

   参数bInitialOwner主要用来控制互斥对象的初始状态。一般多将其设置为FALSE,以表明互斥对象在创建时并没有为任何线程所占有。如果在 创建互斥对象时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex()函数得到此互斥对象的句柄。OpenMutex()函数 原型为:

HANDLE OpenMutex(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 互斥对象名
);

  当目前对资源具有访问权的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex()函数来释放其拥有的互斥对象,其函数原型为:

BOOL ReleaseMutex(HANDLE hMutex);

   其唯一的参数hMutex为待释放的互斥对象句柄。至于WaitForSingleObject()和WaitForMultipleObjects ()等待函数在互斥对象保持线程同步中所起的作用与在其他内核对象中的作用是基本一致的,也是等待互斥内核对象的通知。但是这里需要特别指出的是:在互斥 对象通知引起调用等待函数返回时,等待函数的返回值不再是通常的WAIT_OBJECT_0(对于WaitForSingleObject()函数)或是 在WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1之间的一个值(对于WaitForMultipleObjects()函 数),而是将返回一个WAIT_ABANDONED_0(对于WaitForSingleObject()函数)或是在WAIT_ABANDONED_0 到WAIT_ABANDONED_0+nCount-1之间的一个值(对于WaitForMultipleObjects()函数)。以此来表明线程正在 等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止。除此之外,使用互斥对象的方法在等待线程的可调度性上同使用其他几种内核 对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时失去可调度性,而使用互斥的方法却可以在等待的同时仍具 有可调度性,这也正是互斥对象所能完成的非常规操作之一。

  在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可 以确保任何线程在处理此内存块时都对其拥有可靠的独占访问权。下面给出的示例代码即通过互斥内核对象hMutex对共享内存快g_cArray[]进行线 程的独占访问保护。下面给出实现代码清单:

// 互斥对象
HANDLE hMutex = NULL;
char g_cArray[10];
UINT ThreadProc18(LPVOID pParam)
{
 // 等待互斥对象通知
 WaitForSingleObject(hMutex, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 释放互斥对象
 ReleaseMutex(hMutex);
 return 0;
}
UINT ThreadProc19(LPVOID pParam)
{
 // 等待互斥对象通知
 WaitForSingleObject(hMutex, INFINITE);
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 释放互斥对象
 ReleaseMutex(hMutex);
 return 0;
}
……
void CSample08View::OnMutex()
{
 // 创建互斥对象
 hMutex = CreateMutex(NULL, FALSE, NULL);
 // 启动线程
 AfxBeginThread(ThreadProc18, NULL);
 AfxBeginThread(ThreadProc19, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

   互斥对象在MFC中通过CMutex类进行表述。使用CMutex类的方法非常简单,在构造CMutex类对象的同时可以指明待查询的互斥对象的名字, 在构造函数返回后即可访问此互斥变量。CMutex类也是只含有构造函数这唯一的成员函数,当完成对互斥对象保护资源的访问后,可通过调用从父类 CSyncObject继承的UnLock()函数完成对互斥对象的释放。CMutex类构造函数原型为:

CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );

  该类的适用范围和实现原理与API方式创建的互斥内核对象是完全类似的,但要简洁的多,下面给出就是对前面的示例代码经CMutex类改写后的程序实现清单:

// MFC互斥类对象
CMutex g_clsMutex(FALSE, NULL);
UINT ThreadProc27(LPVOID pParam)
{
 // 等待互斥对象通知
 g_clsMutex.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 释放互斥对象
 g_clsMutex.Unlock();
 return 0;
}
UINT ThreadProc28(LPVOID pParam)
{
 // 等待互斥对象通知
 g_clsMutex.Lock();
 // 对共享资源进行写入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 释放互斥对象
 g_clsMutex.Unlock();
 return 0;
}
……
void CSample08View::OnMutexMfc()
{
 // 启动线程
 AfxBeginThread(ThreadProc27, NULL);
 AfxBeginThread(ThreadProc28, NULL);
 // 等待计算完毕
 Sleep(300);
 // 报告计算结果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  小结

  线程的使用使程序处理更够更加灵活,而这种灵活同样也会带来各种不确定性的可能。尤其是在多个线程对同一公共变量进行访问时。虽然未使用线程同步的程序代码在逻辑上或许没有什么问题,但为了确保程序的正确、可靠运行,必须在适当的场合采取线程同步措施。

编写跨平台的进程内Event事件驱动(转)

分类:编程点滴

嗯.开头之前呢.我先把我对Event机制简单说一下.
   Event也就是事件,典型的就是Windows的消息,我们写Windows程序时就会经常碰到这种消息:
  SendMessage(HWND.....)以及经典的switch(),通过事件机制,进程内部的通信就变得轻松了,相比之下.unix就没有这么方便了.虽然有msgget以及msgsnd之类的消息队列函数,但在win32可没有这类的函数哦.
   所以呢.如果想写一些跨平台的程序,经常就要借助第三方的库比如QT或wxWindow之类的,不过我可不太喜欢一个程序就带一个很大的库,如果是gui还好.如果是命令行的...嘿嘿.你就要装x还有gtk等等,编译几个小时没有问题的. 所以我们就写一个简单的进程内的事件驱动实现,省得带这么大的库.
  
   进程内的事件驱动主要是三部分,一是定义事件,二是准备接收事件.三是响应事件.事件的执行顺序就是按照fifo的原则了,结构当然是选用简单的栈队列了.
  typedef struct tagQInn QInn;
  typedef struct tagInn Inn;
  typedef struct tagQInnVtbl {
   void (*Push)(QInn *self,void *data);
   void *(*Pop)(QInn *self);
   void (*Release)(QInn *self);
  }QInnVtbl;
  struct tagInn
  {
   void *data;
   struct tagInn *pNext;
  };
  struct tagQInn
  {
   Inn *pHead;
   Inn *pTail;
   QInnVtbl *lpVtbl;
  };
  // 这种结构可以在chamoro.tar.gz中找到.
  就提供两个函数.pop以及push,这样就可以实现事件的响应了
  另外,我们的进程内事件就叫QEvent吧.我们先看看结构
  typedef struct tagQEvent QEvent;
  typedef struct QEventVtbl {
   int (*EventStart) (QEvent *self);
   int (*AddEvent) (QEvent *self ,int event,void (*eventfuc)(void *));
   int (*SendEvent) (QEvent *self,int event,void *param);
   int (*EventRun) (void *param);
   int (*Release) (QEvent *self);
  }QEventVtbl;
  //原始的事件列表
  typedef struct tagQEventUnit {
   int m_nEventID;
   void (*m_pEvenFunc)(void *);
  } QEventUnit;
  //在队列中的事件
  typedef struct tagQEventDeed {
   int m_nEvent;
   void *m_pParam;
  } QEventDeed;
  struct tagQEvent {
   QThread *m_threadEvent;
   pthread_mutex_t *m_mutexEvent;
   pthread_cond_t *m_condEvent;
   pthread_mutex_t *m_mutexUnit;
   QInn *m_innEvent; //当前的事件列表
   QAutoList *m_listUnit; //原生的事件,根据事件列表提供的eventid取出事件执行
   QEventVtbl *lpVtbl;
  };
  主要是四个函数,我们在后面慢慢的讲
   定义事件一般是事先定义好事件的ID以及响应事件执行的函数,就叫AddEvent吧.把事件的ID比如EN_HELLO先定义好.
   #define EN_HELLO 110
   因为需要把事先定义好事件保存成一个原始的待响应列表,以便有事件到达时可以方便的找出事件,因为我们需要把待响应事件保存起来(具体的QList实现,可以看我另一个作品Chamoro里的QList.我这里就不重复了)
   看看AddEvent的实现
   QEventUnit *unt = NULL;
   assert(self);
   assert(eventfuc);
   unt = (QEventUnit *)malloc(sizeof(QEventUnit));
   if(unt == NULL)
   {
   return -1;
   }
   pthread_mutex_lock(self->m_mutexUnit);
  
   unt->m_nEventID = event;
   unt->m_pEvenFunc = eventfuc;
   self->m_listUnit->list.lpVtbl->Add(&self->m_listUnit->list,unt);
   pthread_mutex_unlock(self->m_mutexUnit);
  
   这里我们讲一下eventfuc这个参数,eventfuc的原型在 int (*AddEvent) (QEvent *self ,int event,void (*eventfuc)(void *));中的void (*eventfuc)(void *),这是一个指针函数,所以我们的事件响应函数的声明一般的方法是
   void OnHello(void *param);
   待响应事件建立完后就这可以StartEvent了.就是我们可以接收和响应事件了.
   我们看看StartEvent的实现:
   assert(self);
   self->m_threadEvent->lpVtbl->Create(self->m_threadEvent,self->lpVtbl->EventRun,self);
   就一句话,之所以需要用StartEvent来手工启动接收线程,就是因为我们在还没有AddEvent之前,是不会有事件响应的,所以我们可以灵活一点.手工来启动要不要接收事件.(QThread的实现代码我后面附上)
   StartEvent时就会启动一个线程,这个线程就是self->lpVtbl->EventRun,也就是线程运行的主体,这个函数检查有没有新的事件,有新的事件就调用相应的响应函数.来完成事件驱动.
   讲EventRun之前,我们还是看看int (*SendEvent) (QEvent *self,int event,void *param);这个函数吧.如果你做过Win32开发,你一定对SendMessage不陌生.呵呵.很熟悉吧.SendEvent的第二个函数就是事件的ID,第三个是参数,如果你多个的参数,你最好用结构来传送.再看看SendEvent的实现吧,代码面前,没有秘密. :)
   QEventDeed *deed = NULL;
   assert(self);
   deed = (QEventDeed *)malloc(sizeof(QEventDeed));
   if(deed == NULL)
   {
   return -1;
   }
   pthread_mutex_lock(self->m_mutexEvent);
   deed->m_nEvent = event;
   deed->m_pParam = param;
   self->m_innEvent->lpVtbl->Push(self->m_innEvent,deed);
   pthread_cond_signal(self->m_condEvent);
   pthread_mutex_unlock(self->m_mutexEvent);
   哈哈,是不是很简单?就一个Push,先生成一个声明,再加到栈中.就这么简单. :) 当然了,还是需要锁的.看到 pthread_cond_signal(self->m_condEvent);这句了吧.等一下我们马上就会知道了,源加完事件.当然要通知主进程,有事件了啦.不要睡了 :P
   OK.马上看EventRun
   QEvent *self = (QEvent *)param;
   QEventUnit *unit;
   QEventDeed *deed;
  #ifdef WIN32
  // DWORD ThreadID;
  #endif
   assert(self);
   while(TRUE)
   {
   pthread_mutex_lock(self->m_mutexEvent);
   // 如果没有新的事件,就等待新事件
   if (!self->m_innEvent->pHead)
   {
   pthread_cond_wait(self->m_condEvent,self->m_mutexEvent);
   pthread_mutex_unlock(self->m_mutexEvent);
   continue;
   }
   else
   {
   pthread_mutex_lock(self->m_mutexUnit);
   // 如果有新的事件,查找事件相对应的event
   deed = (QEventDeed *)self->m_innEvent->lpVtbl->Pop(self->m_innEvent);
   //找出事件相对应的id
   self->m_listUnit->list.lpVtbl->MoveToHead(&self->m_listUnit->list);
   unit = (QEventUnit *)self->m_listUnit->list.lpVtbl->GetData(&self->m_listUnit->list);
   while(unit)
   {
   if(unit->m_nEventID == deed->m_nEvent )
   {
   pthread_mutex_unlock(self->m_mutexEvent);
   pthread_mutex_unlock(self->m_mutexUnit);
   //创建线程还是直接执行回调函数
   unit->m_pEvenFunc(deed->m_pParam);
   pthread_mutex_lock(self->m_mutexEvent);
   pthread_mutex_lock(self->m_mutexUnit);
   /*
  #ifdef WIN32
  _beginthreadex(0, 0,(unsigned (__stdcall *)(void*))unit->m_pEvenFunc, deed->m_pParam, 0, &ThreadID);
  #else
  pthread_create( NULL, NULL,unit->m_pEvenFunc, deed->m_pParam);
  #endif
   */
  
  break;
   }
   self->m_listUnit->list.lpVtbl->MoveNext(&self->m_listUnit->list);
   unit = (QEventUnit *)self->m_listUnit->list.lpVtbl->GetData(&self->m_listUnit->list);
   }
   free(deed);deed = NULL;
   pthread_mutex_unlock(self->m_mutexUnit);
   }
   pthread_mutex_unlock(self->m_mutexEvent);
   }
   return 0;
   嗯.好像我的注释已经写得很清楚了.不过有一点,也就是我用/**/注释的那一部分.
   //创建线程还是直接执行回调函数
   这点也是我最拿不好了.一般执行回调函数的时候,就会阻塞的,那就会影响到其他函数了,如果用线程的话.代价又会太高,
   因为我的水平有限.暂时没有想出好的解决方法.如果你有好的方法,你告诉我一下吧.
   OK.我们就介绍到这里,不过我们还是要看看如何调用 :)
  #include <QEvent.h>
  #define EN_HELLO 100
  void OnHello(void *param)
  {
   char *str = (char *) param;
   if(str == NULL)
   return ;
   printf("OnHello[%s]n",str);
  }
  
  int main(int argc,char *argv[])
  {
   int i = 10;
   QEvent *event = MallocQEvent();
  event->lpVtbl->AddEvent(event,EN_HELLO,OnHello);
  event->lpVtbl->EventStart(event);
  event->lpVtbl->SendEvent(event,EN_HELLO,"This is Hello Event");
  Sleep(1000);
   event->lpVtbl->SendEvent(event,EN_HELLO,"测试中");
   event->lpVtbl->SendEvent(event,EN_HELLO,"测试完");
  
  while(i<=1)
  {
   Sleep(1000);
   i--;
  }
  //其实程序写到这里,还有一个大大的问题,就是为什么要用while?呵呵.因为我们的事件驱动需要长期的运行,这一段就是WIN sdk 里的那一段,
  // Main message loop:
  while (GetMessage(&msg, NULL, 0, 0))
  {
  if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
  {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
  }
  }
  只不过,我们不是检测有没有消息,而且等着别人来告诉你,你要执行什么事了. :)
  OK.未了不要忘记这句哦.
  event->lpVtbl->Release(event);
  
  }
  写完了.这是我第一次写这样的文章,可能写得不是很清楚.如果你们没有看明白的,我等下把代码上传之后.你们看看代码就可以了.另外,QThread我已经进行了封装了. pthread_mutex_unlock 之类的函数我也封装了,不过是封装了win32,原样还是用unix的. :)
  Bemusedgod

CreateProcess

分类:编程点滴

http://baike.baidu.com/view/697167.htm

说明:
WIN32API函数CreateProcess用来创建一个新的进程和它的主线程,这个新进程运行指定的可执行文件。

函数原型:
BOOL CreateProcess
(
    LPCTSTR lpApplicationName,       
    LPTSTR lpCommandLine,       
    LPSECURITY_ATTRIBUTES lpProcessAttributes。
    LPSECURITY_ATTRIBUTES lpThreadAttributes,       
    BOOL bInheritHandles,       
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,       
    LPCTSTR lpCurrentDirectory,       
    LPSTARTUPINFO lpStartupInfo,       
    LPPROCESS_INFORMATION lpProcessInformation
);

参数:
lpApplicationName:指向一个NULL结尾的、用来指定可执行模块的字符串。
  这个字符串可以使可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径。
  这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数的最前面并由空格符与后面的字符分开。
  这个被指定的模块可以是一个Win32应用程序。如果适当的子系统在当前计算机上可用的话,它也可以是其他类型的模块(如MS-DOS 或 OS/2)。
  在Windows NT中,如果可执行模块是一个16位的应用程序,那么这个参数应该被设置为NULL并且因该在lpCommandLine参数中指定可执行模块的名称。16位的应用程序是以DOS虚拟机或Win32上的Windows(WOW) 为进程的方式运行。

lpCommandLine:指向一个NULL结尾的、用来指定要运行的命令行。
  这个参数可以为空,那么函数将使用参数指定的字符串当作要运行的程序的命令行。
  如果lpApplicationName和lpCommandLine参数都不为空,那么lpApplicationName参数指定将要被运行的模块,lpCommandLine参数指定将被运行的模块的命令行。新运行的进程可以使用GetCommandLine函数获得整个命令行。C语言程序可以使用argc和argv参数。
  如果lpApplicationName参数为空,那么这个字符串中的第一个被空格分隔的要素指定可执行模块名。如果文件名不包含扩展名,那么.exe将被假定为默认的扩展名。如果文件名以一个点(.)结尾且没有扩展名,或文件名中包含路径,.exe将不会被加到后面。如果文件名中不包含路径,Windows将按照如下顺序寻找这个可执行文件:
    1.当前应用程序的目录。
    2.父进程的目录。
    3.Windows 95:Windows系统目录,可以使用GetSystemDirectory函数获得。
      Windows NT:32位Windows系统目录。可以使用GetSystemDirectory函数获得,目录名是SYSTEM32。
    4.在Windows NT中:16位Windows系统目录。不可以使用Win32函数获得这个目录,但是它会被搜索,目录名是SYSTEM。
    5.Windows目录。可以使用GetWindowsDirectory函数获得这个目录。
    6.列在PATH环境变量中的目录。
  如果被创建的进程是一个以MS-DOS或16位Windows为基础的应用程序,lpCommandLine参数应该是一个以可执行文件的文件名作为第一个要素的绝对路径,因为这样做可以使32位Windows程序工作的很好,这样设置lpCommandLine参数是最强壮的。

lpProcessAttributes:指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpProcessAttributes参数为空(NULL),那么句柄不能被继承。
  在Windows NT中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员指定了新进程的安全描述符,如果参数为空,新进程使用默认的安全描述符。
  在Windows95中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员被忽略。

lpThreadAttributes:指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpThreadAttributes参数为空(NULL),那么句柄不能被继承。
  在Windows NT中,SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员指定了主线程的安全描述符,如果参数为空,主线程使用默认的安全描述符。
  在Windows95中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员被忽略。

bInheritHandles:指示新进程是否从调用进程处继承了句柄。如果参数的值为真,调用进程中的每一个可继承的打开句柄都将被子进程继承。被继承的句柄与原进程拥有完全相同的值和访问权限。

dwCreationFlags:指定附加的、用来控制优先类和进程的创建的标志。以下的创建标志可以以除下面列出的方式外的任何方式组合后指定。

值:CREATE_DEFAULT_ERROR_MODE
含义:新的进程不继承调用进程的错误模式。CreateProcess函数赋予新进程当前的默认错误模式作为替代。应用程序可以调用SetErrorMode函数设置当前的默认错误模式。
这个标志对于那些运行在没有硬件错误环境下的多线程外壳程序是十分有用的。
对于CreateProcess函数,默认的行为是为新进程继承调用者的错误模式。设置这个标志以改变默认的处理方式。

值:CREATE_NEW_CONSOLE
含义:新的进程将使用一个新的控制台,而不是继承父进程的控制台。这个标志不能与DETACHED_PROCESS标志一起使用。

值:CREATE_NEW_PROCESS_GROUP
含义:新进程将使一个进程树的根进程。进程树种的全部进程都是根进程的子进程。新进程树的用户标识符与这个进程的标识符是相同的,由lpProcessInformation参数返回。进程树经常使用GenerateConsoleCtrlEvent函数允许发送CTRL+C或CTRL+BREAK信号到一组控制台进程。

值:CREATE_SEPARATE_WOW_VDM
含义:(只适用于Windows NT)这个标志只有当运行一个16位的Windows应用程序时才是有效的。如果被设置,新进程将会在一个私有的虚拟DOS机(VDM)中运行。另外,默认情况下所有的16位Windows应用程序都会在同一个共享的VDM中以线程的方式运行。单独运行一个16位程序的优点是一个应用程序的崩溃只会结束这一个VDM的运行;其他那些在不同VDM中运行的程序会继续正常的运行。同样的,在不同VDM中运行的16位Windows应用程序拥有不同的输入队列,这意味着如果一个程序暂时失去响应,在独立的VDM中的应用程序能够继续获得输入。

值:CREATE_SHARED_WOW_VDM
含义:(只适用于Windows NT)这个标志只有当运行一个16位的Windows应用程序时才是有效的。如果WIN.INI中的Windows段的DefaultSeparateVDM选项被设置为真,这个标识使得CreateProcess函数越过这个选项并在共享的虚拟DOS机中运行新进程。

值:CREATE_SUSPENDED
含义:新进程的主线程会以暂停的状态被创建,直到调用ResumeThread函数被调用时才运行。

值:CREATE_UNICODE_ENVIRONMENT
含义:如果被设置,由lpEnvironment参数指定的环境块使用Unicode字符,如果为空,环境块使用ANSI字符。

值:DEBUG_PROCESS
含义:如果这个标志被设置,调用进程将被当作一个调试程序,并且新进程会被当作被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。
如果你使用这个标志创建进程,只有调用进程(调用CreateProcess函数的进程)可以调用WaitForDebugEvent函数。

值:DEBUG_ONLY_THIS_PROCESS
含义:如果此标志没有被设置且调用进程正在被调试,新进程将成为调试调用进程的调试器的另一个调试对象。如果调用进程没有被调试,有关调试的行为就不会产生。

值:DETACHED_PROCESS
含义:对于控制台进程,新进程没有访问父进程控制台的权限。新进程可以通过AllocConsole函数自己创建一个新的控制台。这个标志不可以与CREATE_NEW_CONSOLE标志一起使用。

dwCreationFlags参数还用来控制新进程的优先类,优先类用来决定此进程的线程调度的优先级。如果下面的优先级类标志都没有被指定,那么默认的优先类是NORMAL_PRIORITY_CLASS,除非被创建的进程是IDLE_PRIORITY_CLASS。在这种情况下子进程的默认优先类是IDLE_PRIORITY_CLASS
可以下面的标志中的一个:

优先级:HIGH_PRIORITY_CLASS       
含义:指示这个进程将执行时间临界的任务,所以它必须被立即运行以保证正确。这个优先级的程序优先于正常优先级或空闲优先级的程序。一个例子是Windows任务列表,为了保证当用户调用时可以立刻响应,放弃了对系统负荷的考虑。确保在使用高优先级时应该足够谨慎,因为一个高优先级的CPU关联应用程序可以占用几乎全部的CPU可用时间。

优先级:IDLE_PRIORITY_CLASS       
含义:指示这个进程的线程只有在系统空闲时才会运行并且可以被任何高优先级的任务打断。例如屏幕保护程序。空闲优先级会被子进程继承。

优先级:NORMAL_PRIORITY_CLASS       
含义:指示这个进程没有特殊的任务调度要求。

优先级:REALTIME_PRIORITY_CLASS       
含义:指示这个进程拥有可用的最高优先级。一个拥有实时优先级的进程的线程可以打断所有其他进程线程的执行,包括正在执行重要任务的系统进程。例如,一个执行时间稍长一点的实时进程可能导致磁盘缓存不足或鼠标反映迟钝。

lpEnvironment:指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。
  一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的。每个字符串都是name=value的形式。
  因为相等标志被当作分隔符,所以它不能被环境变量当作变量名。
  与其使用应用程序提供的环境块,不如直接把这个参数设为空,系统驱动器上的当前目录信息不会被自动传递给新创建的进程。对于这个情况的探讨和如何处理,请参见注释一节。
  环境块可以包含Unicode或ANSI字符。如果lpEnvironment指向的环境块包含Unicode字符,那么dwCreationFlags字段的CREATE_UNICODE_ENVIRONMENT标志将被设置。如果块包含ANSI字符,该标志将被清空。
  请注意一个ANSI环境块是由两个零字节结束的:一个是字符串的结尾,另一个用来结束这个快。一个Unicode环境块石油四个零字节结束的:两个代表字符串结束,另两个用来结束块。

lpCurrentDirectory:指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录。这个选项是一个需要启动启动应用程序并指定它们的驱动器和工作目录的外壳程序的主要条件。

lpStartupInfo:指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。

lpProcessInformation:指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。

返回值:
如果函数执行成功,返回非零值。

如果函数执行失败,返回零,可以使用GetLastError函数获得错误的附加信息。

注释:
  CreateProcess函数用来运行一个新程序。WinExec和LoadModule函数依旧可用,但是它们同样通过调用CreateProcess函数实现。

  另外CreateProcess函数除了创建一个进程,还创建一个线程对象。这个线程将连同一个已初始化了的堆栈一起被创建,堆栈的大小由可执行文件的文件头中的描述决定。线程由文件头处开始执行。

  新进程和新线程的句柄被以全局访问权限创建。对于这两个句柄中的任一个,如果没有安全描述符,那么这个句柄就可以在任何需要句柄类型作为参数的函数中被使用。当提供安全描述符时,在接下来的时候当句柄被使用时,总是会先进行访问权限的检查,如果访问权限检查拒绝访问,请求的进程将不能使用这个句柄访问这个进程。

  这个进程会被分配给一个32位的进程标识符。直到进程中止这个标识符都是有效的。它可以被用来标识这个进程,或在OpenProcess函数中被指定以打开这个进程的句柄。进程中被初始化了的线程一样会被分配一个32位的线程标识符。这个标识符直到县城中止都是有效的且可以用来在系统中唯一标识这个线程。这些标识符在PROCESS_INFORMATION结构体中返回。

  当在lpApplicationName或lpCommandLine参数中指定应用程序名时,应用程序名中是否包含扩展名都不会影响运行,只有一种情况例外:一个以.com为扩展名的MS-DOS程序或Windows程序必须包含.com扩展名。

  调用进程可以通过WaitForInputIdle函数来等待新进程完成它的初始化并等待用户输入。这对于父进程和子进程之间的同步是极其有用的,因为CreateProcess函数不会等待新进程完成它的初始化工作。举例来说,在试图与新进程关联的窗口之前,进程应该先调用WaitForInputIdle

  首选的结束一个进程的方式是调用ExitProcess函数,因为这个函数通知这个进程的所有动态链接库(DLLs)程序已进入结束状态。其他的结束进程的方法不会通知关联的动态链接库。注意当一个进程调用ExitProcess时,这个进程的其他县城没有机会运行其他任何代码(包括关联动态链接库的终止代码)。

  ExitProcess, ExitThread, CreateThread, CreateRemoteThread,当一个进程启动时(调用了CreateProcess的结果)是在进程中序列化进行的。在一段地址空间中,同一时间内这些事件中只有一个可以发生。这意味着下面的限制将保留:
  *在进程启动和DLL初始化阶段,新的线程可以被创建,但是直到进程的DLL初始化完成前它们都不能开始运行。
  *在DLL初始化或卸下例程中进程中只能有一个线程。
  *直到所有的线程都完成DLL初始化或卸下后,ExitProcess函数才返回。

  在进程中的所有线程都终止且进程所有的句柄和它们的线程被通过调用CloseHandle函数终止前,进程会留在系统中。进程和主线程的句柄都必须通过调用CloseHandle函数关闭。如果不再需要这些句柄,最好在创建进程后立刻关闭它们。

当进程中最后一个线程终止时,下列的事件发生:
  *所有由进程打开的对象都会关闭。
  *进程的终止状态(由GetExitCodeProcess函数返回)从它的初始值STILL_ACTIVE变为最后一个结束的线程的结束状态。
  *主线程的线程对象被设置为标志状态,供其他等待这个对象的线程使用。
  *进程对象被设置为标志状态,供其他等待这个对象的线程使用。

假设当前在C盘上的目录是MSVCMFC且有一个环境变量叫做C:,它的值是C:MSVCMFC,就像前面lpEnvironment中提到过的那样,这样的系统驱动器上的目录信息在CreateProcess函数的lpEnvironment参数不为空时不会被自动传递到新进程里。一个应用程序必须手动地把当前目录信息传递到新的进程中。为了这样做,应用程序必须直接创建环境字符串,并把它们按字母顺序排列(因为Windows NT和Windows 95使用一种简略的环境变量),并把它们放进lpEnvironment中指定的环境块中。类似的,他们要找到环境块的开头,又要重复一次前面提到的环境块的排序。

一种获得驱动器X的当前目录变量的方法是调用GetFullPathName("x:",..)。这避免了一个应用程序必须去扫描环境块。如果返回的绝对路径是X:,就不需要把这个值当作一个环境数据去传递了,因为根目录是驱动器X上的新进程的默认当前目录。


CreateProcess函数返回的句柄对于进程对象具有PROCESS_ALL_ACCESS的访问权限。

由lpcurrentDirectory参数指定的当前目录室子进程对象的当前目录。lpCommandLine参数指定的第二个项目是父进程的当前目录。

对于Windows NT,当一个进程在指定了CREATE_NEW_PROCESS_GROUP的情况下被创建时,一个对于SetConsoleCtrlHandler(NULL,True)的调用被用在新的进程上,这意味着对新进程来说CTRL+C是无效的。这使得上层的外科程序可以自己处理CTRL+C信息并有选择的把这些信号传递给子进程。CTRL+BREAK依旧有效,并可被用来中断进程/进程树的执行。

参见
AllocConsole, CloseHandle, CreateRemoteThread, CreateThread, ExitProcess, ExitThread, GenerateConsoleCtrlEvent, GetCommandLine, GetEnvironmentStrings, GetExitCodeProcess, GetFullPathName, GetStartupInfo, GetSystemDirectory, GetWindowsDirectory, LoadModule, OpenProcess, PROCESS_INFORMATION, ResumeThread, SECURITY_ATTRIBUTES, SetConsoleCtrlHandler, SetErrorMode, STARTUPINFO, TerminateProcess, WaitForInputIdle, WaitForDebugEvent, WinExec

熟悉Tortoise SVN 客户端 基本用法

分类:编程点滴

1. export 和check out
  export 下载源代码
  用法:
  1、新建一个空的文件夹,右键点击它,可以看到TortoiseSVN菜单以及上面的SVN Checkout。
  2、不用管这个Checkout,我们选择TortoiseSVN菜单下的Export...,接着它会让你输入url。
  3、比如输入【迷宫探宝】的SVN地址是:http://game-rts-framework.googlecode.com/svn/trunk/
  4、其他选项不需要更改,Omit externals不要勾选,HEAD Revision选中表示最新的代码版本,接着点击OK即可将代码导出到这个目录中:)
  check out 意思签出,虽然和Export的效果一样是把代码从服务器下载到本地,但是Checkout有验证的功能,Checkout到某处的代码,将会被TortoiseSVN监视,里面的文件可以享受各种SVN的服务。
  
  2 .每次提交代码需要注意哪些问题
  如果你更新了目录中的文件,提交代码需要用到commit功能,commit的功能不仅仅是上传,他会和服务器上面的文件进行对比,假如你更新了某个文件而服务器上面也有人更新了这个文件,并且是在你checkout之后做的更新,那么它会尝试将你的更新和他人的更新进行融合(merge),假如自动merge不成功,那么报告conflict,你必须自己来手动merge,也就是把你的更新和别人的更新无冲突的写在一起。
  commit的时候,最好填写Log信息,这样保证别人可以看到你的更新究竟做了写什么。这就相当于上传文件并且说明自己做了那些修改,多人合作的时候log非常重要。
  TortoiseSVN的commit只会上传原先checkout然后又被修改了的文件,假如你新加入了某些文件,需要右键点击文件选择Add,然后文件上面会出现一个加号,在下次commit的时候它就会被upload并且被标记为绿色对勾。没有绿色对勾的文件不会被commit。
  假如你需要给带有绿色对勾文件改名或者移动它的位置,请不要使用windows的功能,右键点击它们,TortoiseSVN都有相应的操作。想象这些文件已经不在是你本地的东西,你的一举一动都必须让Tortoise知道。
  假如修改了某个文件但是你后悔了,可以右键点击它选择Revert,它将变回上次checkout时候的情况。或者Revert整个工程到任意一个从前的版本.
  下面描述在使用Commit时的几个注意点:
  -------------如有多个文件需要同时提交,同时文件在不同的目录下,必须找到这些文件的最短目录上点击Commit,TortoiseSVN会搜索被点击目录以及该目录下所有的文件,并将修改变动的文件罗列在列表中。
  -------------仔细查看列表中的文件,确定哪些文件时需要更新的,如果不需要更新某个已经变化了的文件,只需要在该文件上点击右键,选择还原操作;选择需要新增的文件,不要将临时文件添加到版本库中。
  -------------如遇到文件冲突(冲突:要提交的文件已被其他人改动并提交到版本库中)要启用解决冲突功能。
  3. 如何保持本地版本和服务器版本同步
  使用update来同步本地和服务器上的代码。同样是右键选择SVN update,所有的更改就会从服务器端传到你的硬盘。注意,假如别人删除了某个文件,那么更新之后你在本地的也会被删除。
  如果本地的代码已经被修改,和commit一样会先进行merge,不成功的话就会报告conflict
  4 如何在同一个在一个工程的各个分支或者主干之间切换
  使用tortoise SVN-->switch
  在URL中输入branch或trunk的url地址
  5.如何比较两个版本之间的差别
  
  本地更改
  如果你想看到你的本地副本有哪些更加,只用在资源管理器中右键菜单下选TortoiseSVN→ 比较差异。
  与另外一个分支/标签之间的差异
  如果你想查看主干程序(假如你在分支上开发)有哪些修改或者是某一分支(假如你在主干上开发)有哪些修改,你可以使用右键菜单。在你点击文件的同时按住Shift键,然后选择TortoiseSVN→ URL比较。在弹出的对话框中,将特别显示将与你本地版本做比较的版本的URL地址。
  你还可以使用版本库浏览器,选择两个目录树比较,也许是两个标记,或者是分支/标记和最新版本。邮件菜单允许你使用比较版本来比较它们。阅读第 5.9.2 节 “比较文件夹”以便获得更多信息。
  与历史版本的比较差异
  如果你想查看某一特定版本与本地拷贝之间的差异,使用显示日志对话框,选择要比较的版本,然后选择在右键菜单中选与本地拷贝比较差异
  两个历史版本的比较
  如果你要查看任意已提交的两个历史版本之间的差异,在版本日志对话框中选择你要比较的两个版本(一般使用 Ctrl-更改),然后在右键菜单中选比较版本差异
  如果你在文件夹的版本日志中这样做,就会出现一个比较版本对话框,显示此文件夹的文件修改列表。阅读第 5.9.2 节 “比较文件夹”以便获得更多信息。
  提交所有修改
  如果你要在一个视窗中查看某一版本的所有更改,你可以使用统一显示所有比较 (GNU 片段整理)。它将显示所有修改中的部分内容。它很难显示一个全面清晰的比较,但是会将所有更改都集中显示出来。在版本日志对话框中选择某一版本,然后在右键菜单中选择统一显示所有比较。
  文件差异
  如果你要查看两个不同文件之间的差异,你可以直接在资源管理器中选择这两个文件(一般使用 Ctrl-modifier),然后右键菜单中选TortoiseSVN→ 比较差异。
  WC文件/文件夹与URL之间的比较差异
  如果你要查看你本地拷贝中的任一文件与版本库中任一文件之间差异,
  谴责信息之间的比较差异
  如果你要查看的不仅是比较差异而且包括修改该版本的作者,版本号和日期,你可以在版本日志对话框中综合比较差异和谴责信息。这里有更多详细介绍第 5.20.2 节 “追溯不同点”。
  比较文件夹差异
  TortoiseSVN 自带的内置工具不支持查看多级目录之间的差异,但你可以使用支持该功能的外置工具来替代。在这里 第 5.9.4 节 “其他的比较/合并工具”我们可以介绍一些我们使用过的工具。
  6.提交代码时怎样知道自己改了哪些文件,别人改了哪些文件
  7. 如何知道某个文件的某一行是谁在哪个版本修改的
  
  8. 如何为一个SVN主工程建立分支或tag
  创建分支使用步骤:
  1、选择你要产生分支的文件,点击鼠标右键,选择[分支/标记...]
  2、在[至URL(T)]输入框中将文件重命名为你的分支文件名,输入便于区分的日志信息,点击确认。
  3、在SVN仓库中会复制一个你所指定的文件,文件名称就是你所命名的,但是在你的本地目录上看不到新建的分支文件名,要使你的文件更新作用到你的分支上,你必须选择文件,点击鼠标右键,选择[切换...],选择你重命名的文件,点击确定即可。这样你的本地文件就和分支文件关联上了,不要奇怪,这时本地目录上看到的文件名仍然为旧的文件名。
  经验小结:
  1、如果操作的文件之前还未提交,而你又想把文件提交到新的分支上,记得一定要选择切换
  2、SVN分支的管理实际上就是把不同的分支用不同的文件保存,因此你在取得新版本的时候会发现,不同分支的最新文件也会被获取下来。
  创建tag操作,相当于把当前的代码版本复制一份到其他地方,然后以这个地方为出发点进行新的开发,与原来位置的版本互不干扰。
  对于branches、tags、trunk这三个目录,并不是subversion必需的,而是被总结的一种良好的团队开发习惯,其使用方法为:
  1、开发者提交所有的新特性到主干。 每日的修改提交到/trunk:新特性,bug修正和其他。
  2、这个主干被拷贝到“发布”分支。 当小组认为软件已经做好发布的准备(如,版本1.0)然后/trunk会被拷贝到/branches/1.0。
  3、项目组继续并行工作,一个小组开始对分支进行严酷的测试,同时另一个小组在/trunk继续新的工作(如,准备2.0),如果一个bug在任何一个位置被发现,错误修正需要来回运送。然而这个过程有时候也会结束,例如分支已经为发布前的最终测试“停滞”了。
  4、分支已经作了标签并且发布,当测试结束,/branches/1.0作为引用快照已经拷贝到/tags/1.0.0,这个标签被打包发布给客户。
  5、分支多次维护。当继续在/trunk上为版本2.0工作,bug修正继续从/trunk运送到/branches/1.0,如果积累了足够的bug修正,管理部门决定发布1.0.1版本:拷贝/branches/1.0到/tags/1.0.1,标签被打包发布。
  一般建立最初的repository时,就建好这三个目录,把所有代码放入/trunk中,如:要将project1目录下的代码导入repository,project1的结构就是:project1/branches,project1/tags,project1/trunk,project1/trunk/food.c,project1/trunk/egg.pc……,然后将project1目录导入repository,建立最初的资料库。然后export回project1,作为本地工作目录。

VC 的工程文件说明

分类:编程点滴

http://blog.chinaunix.net/u/28371/showart.php?id=397460

dsw, aps, clw, plg这些文件都可以删除。只保留 H,C,CPP,DSP,RC,剩余文件去除只读属性,其余全部删除。然后打开DSP 有提示选 YES,就可以了

*.dsp(DeveloperStudio Project):是VC++的工程配置文件,比如说你的工程包含哪个文件,你的编译选项是什么等等,编译的时候是按照.dsp的配置来的。
*.dsw(DeveloperStudio Workspace):是工作区文件,用来配置工程文件的。它可以指向一个或多个.dsp文件。
*.clw:是 ClassWizard信息文件,实际上是INI文件的格式,有兴趣可以研究一下.有时候ClassWizard出问题,手工修改CLW文件可以解决.如果此文件不存在的话,每次用ClassWizard的时候绘提示你是否重建。
*.opt:工程关于开发环境的参数文件,如工具条位置等信息。
*.aps:(AppStudio File),资源辅助文件,二进制格式,一般不用去管他。
*.rc:资源文件。在应用程序中经常要使用一些位图、菜单之类的资源, VC中以rc为扩展名的文件称为资源文件, 其中包含了应用程序中用到的所有的windows资源, 要指出的一点是rc文件可以直接在VC集成环境中以可视化的方法进行编辑和修改。
*.plg:是编译信息文件,编译时的error和warning信息文件(实际上是一个html文件,一般用处不大),在Tools->Options里面有个选项可以控制这个文件的生成。
*.hpj:(Help Project)是生成帮助文件的工程,用microsfot Help Compiler可以处理。
*.mdp:(Microsoft DevStudio Project)是旧版本的项目文件,如果要打开此文件的话,会提示你是否转换成新的DSP格式。
*.bsc:是用于浏览项目信息的,如果用Source Brower的话就必须有这个文件。如果不用这个功能的话,可以在Project Options里面去掉Generate Browse Info File,可以加快编译速度。
*.map:是执行文件的映像信息纪录文件,除非对系统底层非常熟悉,这个文件一般用不着。
*.pch:(Pre-Compiled File)是预编译文件,可以加快编译速度,但是文件非常大。
*.pdb:(Program Database)记录了程序有关的一些数据和调试信息,在调试的时候可能有用。
*.exp:只有在编译DLL的时候才会生成,记录了DLL文件中的一些信息。一般也没什么用。
*.ncb:无编译浏览文件(no compile browser)。当自动完成功能出问题时可以删除此文件,build后会自动生成。
*.c:源代码文件,按C语言用法编译处理。
*.cpp:源代码文件,按C++语法编译处理。
*.h是头文件,一般用作声明和全局定义。
*.sln:在开发环境中使用的解决方案文件。它将一个或多个项目的所有元素组织到单个的解决方案中。此文件存储在父项目目录中.解决方案文件,他是一个或多个.proj(项目文件)的集合。
*.vcproj 是vc的工程项目文件 
 

.vcproj .sln 分别是VC2002以上工程文件和解决方案文件
.dsp .dsw 分别是VC6的工程文件和工程组文件

将VC7工程转换回VC6工程,换言之,就是将.sln/.vcproj这两个文件转换到.dsw/.dsp文件。

vc6打开dsp或dsw,vc2003,2005打开sln或vcproj

VC6打开*.dsw文件,单击工具栏上的"!",然后你的文件夹里多了一个DEBUG文件夹里面有.EXE文件

在vc7里面打开vc6的工程时,它会提示你是否转换成vc7的格式,转换后就可以直接使用了。转换后的工程,vc6就不认识了。

  我使用vc.net 2003打开vc6的工程,提示要升级,我也选了yes   to   all  
  但是其实vc.net只是读取vc6的.dsw和.dsp中的信息并添加了.sln和.vcproj  
  用vc.net打开过的vc6工程   dsw和dsp都没有改变  
  所以用vc.net打开过的vc6工程   虽然已经被“升级”其实dsw和dsp并没有被改动  
  再打开时就不用dsw而用sln了   这点做得很好   只是从vc6的工程文件中读信息   并不改  
  照样可以用vc6打开原来的dsw和dsp

vcproj文件格式

分类:编程点滴

http://hi.baidu.com/izouying/blog/item/eed1a01f05e1baf7e1fe0b6e.html

vcproj文件格式

  

上回说到了sln文件格式,每个sln都包含了一个到多个工程文件,c++工程文件的文件扩展名为vcproj,这回说一下vcproj的格式。

vcproj是一个标准的xml文件。因此以下就以节点顺序描述。

根节点是VisualStudioProject,属性中包含了工程的全局信息,常见的信息有:

属性

含义

说明

ProjectType

工程类型

默认值是Visual C++

Version

版本

默认值是7.10

Name

工程名称

ProjectGUID

工程的GUID

Keyword

工程关键字

默认值是Win32Proj

SccProjectName

SccAuxPath

SccLocalPath

SccProvider

SourceSafe信息

默认值是SAK

后面的Scc**属性,标志了此工程在SourceSafe中。手动将一个工程从SourceSafe中删除时,只要删除这四个属性就行。当它们的默认值是SAK时,可以在工程文件的同一个目录下找到一个文件mssccprj.scc,这里面包含了工程在SourceSafe中的信息,比如:

SCC = This is a Source Code Control file

[PS.vcproj]

SCC_Aux_Path = "\code-servercode$"

SCC_Project_Name = "$/project/PS", IQIBAAAA

根节点下有三个子节点比较重要。Platforms很简单,表示平台内容,通常就是“Win32”;Configurations是编译和链接的配置信息;Files下包括的是工程中的文件信息。以下主要谈谈ConfigurationsFiles

Configurations包含了工程编译和链接等配置信息,其子节点是Configuration,由用户设定的编译类型决定,默认有DebugRelease两个子节点。Configuration的属性如下:

属性

含义

说明

Name

编译选项名称

Debug下通常为Debug|Win32

OutputDirectory

目标文件输出路径

默认为Debug

IntermediateDirectory

编译信息输出路径

默认为Debug

ConfigurationType

工程类型

1表示exe程序文件,2表示dll动态库文件,3表示lib静态库文件

UseOfMFC

表示是否使用MFC

0表示不使用MFC1表示静态链接MFC2表示动态链接MFC

CharacterSet

表示编码类型

1表示Unicode2表示Ansi

Configuration的子节点全是Tool,每个子节点都有一个属性Name表示节点含义。这里面有两个子节点比较有用,一个是VCCLCompilerTool,表示编译信息,一个是VCLinkerTool,表示链接信息,其它的子节点用的不多。

VCCLCompilerTool的常用属性如下:

属性

含义

说明

Optimization

优化选项

可以为01234

PreprocessorDefinitions

预定义标记

通常都是WIN32 _WINDOWS _DEBUG

MinimalRebuild

是否使用最小编译

设置为TRUE能节约编译时间

BasicRuntimeChecks

运行时检测,包括栈和未初始化变量等

默认为3

RuntimeLibrary

程序运行时

选择多(单)线程,(非)调试,DLLEXE)类型

TreatWChar_tAsBuiltInType

是否将wchar_t当作内置类型

如果为FALSEwchar_t被认为是unsigned short类型

ForceConformanceInForLoopScope

iffor循环中声明的变量的作用范围是否在循环内

7.1默认为FALSE8.0默认为TRUE

UsePrecompiledHeader

预定义头文件设置

0表示不使用,1表示创建预定义头文件,2表示自动创建,3表示使用预定义头文件

PrecompiledHeaderThrough

预定义头文件名

通常都是stdafx.h,可以随意指定

PrecompiledHeaderFile

预编译信息文件名

默认为$(IntDir)/$(TargetName).pch

WarningLevel

警告级别

4

Detect64BitPortabilityProblems

检测是否兼容64位程序

FALSE

DebugInformationFormat

调试信息格式

Debug下通常设置为4Release下可以设置为3

VCLinkerTool的常用属性如下:

属性

含义

说明

AdditionalDependencies

依赖lib文件

OutputFile

输出的目标文件

默认$(OutDir)/$(ProjectName).exe

LinkIncremental

增量编译

2

AdditionalLibraryDirectories

依赖lib的位置

GenerateDebugInformation

是否生成调试信息

通常都为TRUE

ProgramDatabaseFile

调试信息文件名称

$(IntDir)/$(ProjectName).pdb

SubSystem

子系统

1为控制台,2Windows程序

ImportLibrary

导入的lib文件

默认$(IntDir)/$(ProjectName).lib

Files下包括的是工程中的文件信息,由FilterFile组成,Filter表示目录,File表示文件。每个cpp还可以包含一个子节点FileConfiguration,这个子节点表示此cpp文件编译时与全局编译选项不一致的内容,通常情况下除了预编译头文件外这不是必须的,预编译头则必须指定它的UsePrecompiledHeader信息为1,也就是由它来创建预定义头文件。

qt在vs2005下的编译安装

分类:默认栏目

http://www.diybl.com/course/3_program/c++/cppjs/20071018/78073_2.html

QT有商业版和免费开源的,但是针对Windows下trolltech 没有提供免费的编译好的二进制库,同时在linux下自带的版本都比较低,fedora6的还使用的是3.*的,所以,自己编译下QT还是有必要的。在linux下面,和其他的一样./configure,make ,make install一路下来就可以了,之后在home的.bashrc中设置一些QT环境变量即可。比较简单。而在windows下安装比较麻烦,尤其对习惯于等待windows把所有东西都准备的很好的windows用户,自己编译库是不习惯的,尤其是QT这样需要配置很多东西的。但是安装完成之后,仔细想下也没有什么了,QT库和其他的Boost,ACE等都一样,就是C++的Dll形式的库而已,我们的工作就是编译出一大堆Dll以及exe工具,仅此而已,如果你编译过简单的dll,估计QT的编译也不是很难理解了。
所以,这里简单的翻译下我安装过程中参考的文档。我的环境是32位的笔记本,Windows XP sp2, VS2005, QT 4.2.3,这里需要注意的是,VS2005没有打补丁,否则编译出错,我没有去试图解决,因为我的没有补丁 :-)
原文参考这里:http://www.qtnode.net/wiki/Qt4_with_Visual_Studio

1. 下载代码:到官方网站http://www.trolltech.com/developer/downloads/qt/windows下载windows下的opensource的压缩包,或者来这里http://www.qtnode.net/wiki/Download_Qt下载名字类似qt-win-opensource-src-4.2.3.zip。然后下载编译VS的QT库的补丁http://downloads.sourceforge.net/qtwin/acs-4.2.3-patch1.zip,解压这两个包到一个目录,如C:Qt4.2.3

2.配置环境
配置VS的vsvars32.bat,一般在C:Program FilesMicrosoft Visual Studio 8Common7Tools下面在PATH的头部添加QT所在目录,如C:Qt4.2.3,在INCLUDE上添加C:Qt4.2.3include,LIB中添加C:Qt4.2.3lib,具体的目录是你刚才解压的目录,打开这个批处理文件,看着原来怎么写的你就学着写好了。俺的类似如下:
@set PATH=C:Qt4.2.3;C:Program FilesMicrosoft Visual Studio 8Common7IDE;。。。%PATH%
@set INCLUDE=C:Qt4.2.3include;C:Program FilesMicrosoft Visual Studio 8VCATLMFCINCLUDE;。。。%INCLUDE%
@set LIB=C:Qt4.2.3lib;C:Program FilesMicrosoft Visual Studio 8VCATLMFCLIB;。。%LIB%
打开一个cmd命令行窗口,把这个文件托过来执行,这样,你打开的cmd就具有上面设置的环境变量了,但是这些变量只针对你当前的cmd,不会更改本机配置,所以,不要关闭cmd窗口,执行下nmake /?看看能不能找到nmake命令

3.打补丁
执行刚才解压acs-4.2.2-patch1.zip之后的那个installpatch42.bat,直接托过来运行就好了。
C:Qt4.2.3> installpatch42.bat

4.配置QT安装,QT需要一些,定位到QT的目录下面,执行C:Qt4.2.3> qconfigure.bat msvc.net -release -no-stl上面的选项根据你自己需要来写啊,比如你要debug版,就加个-debug选项,想要STL就把后面的去掉。第一个参数表示你编译出来的为那个VS版本使用,msvc 对应Visual Studio 6.0,msvc.net对应2003,我的2005就用 msvc2005了。如果你不怕一会编译时间太长,你就干脆直接输入C:Qt4.2.3> qconfigure.bat msvc2005会输出一些配置信息,默认情况会编译很多东西。看好了,是不是你想要的,没有问题,就同意好了

5.编译
运行nmake
C:Qt4.2.3> nmake
慢慢等吧,或者让它自己在那跑,你自己继续工作。

6.配置环境,
我的电脑>属性>高级>环境变量>用户变量里面设置几个变量:
PATH中增加C:Qt4.2.3bin(如果没有就创建),
创建QMAKESPEC值为 win32-msvc2005,这个值还是根据你要生成的Vs版本,6.0 使用win32-msvc, 2003 使用win32-msvc.net,

2005就是 win32-msvc2005   
创建QTDIR值为 C:Qt4.2.3
如果你机器上安装多个版本的QT,就通过这三个环境变量来切换了,我同时有C:Qt4.0.0,那么就把那些前缀都换成C:Qt4.0.0,就使用4.0了。最后为了使的你刚才修改的环境变量生效,重启cmd,可以通过C:> qmake -v来看你使用的QT版本。

7.整个程序测试下hello.cpp
#include <QApplication>
#include <QLabel>

int main(int argc, char **argv) {
  QApplication app(argc, argv);
  QLabel *label = new QLabel("Hello World!");
 
  label->show();

  return app.exec();
}

执行
C:> qmake -project -t vcapp -o projectname.pro
C:> qmake
在目录下面会生成projectname.vcproj,有这个就可以使用VS打开了,剩下的和普通的C++程序一样编译,运行,调试好了。

以上基本上可以开发简单的程序了,如果需要使用opengl等其他的,自己google下吧。有米的人也可以直接购买QT的商业版,可以直接集成到VS里面去,有向导等东西。我使用Qt4.0的还是比较爽的。
自己有时间好好看看examples里面的例子,开发的时候也可以作为参考,很好的资料。
几个链接大家逛逛
http://www.trolltech.com
http://www.qtnode.net/wiki/Main_Page
http://www.qtopia.org.cn/phpBB2/

select poll使用

分类:UNIX/LINUX

http://blog.csdn.net/dreamtofly/archive/2007/04/12/1561586.aspx

2.1. 如何管理多个连接?
“我想同时监控一个以上的文件描述符(fd)/连接(connection)/流(stream),应该怎么办?” 

使用 select() 或 poll() 函数。 

注 意:select() 在BSD中被引入,而poll()是SysV STREAM流控制的产物。因此,这里就有了平台移植上的考虑:纯粹的BSD系统可 能仍然缺少poll(),而早一些的SVR3系统中可能没有select(),尽管在SVR4中将其加入。目前两者都是POSIX. 1g标准,(译者 注:因此在Linux上两者都存在) 

select()和poll()本质上来讲做的是同一件事,只是完成的方法不一样。两者都通过检验一组文件描述符来检测是否有特定的时间将在上面发生并在一定的时间内等待其发生。 

[重要事项:无论select()还是poll()都不对普通文件起很大效用,它们着重用于套接口(socket)、管道(pipe)、伪终端(pty)、终端设备(tty)和其他一些字符设备,但是这些操作都是系统相关(system-dependent)的。] 

2.1.1. 我如何使用select()函数?
select()函数的接口主要是建立在一种叫'fd_set'类型的基础上。它('fd_set') 是一组文件描述符(fd)的集合。由于fd_set类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量: 

    fd_set set;
    FD_ZERO(&set);       /* 将set清零 */
    FD_SET(fd, &set);    /* 将fd加入set */
    FD_CLR(fd, &set);    /* 将fd从set中清除 */
    FD_ISSET(fd, &set);  /* 如果fd在set中则真 */
      
在 过去,一个fd_set通常只能包含少于等于32个文件描述符,因为fd_set其实只用了一个int的比特矢量来实现,在大多数情况下,检查 fd_set能包括任意值的文件描述符是系统的责任,但确定你的fd_set到底能放多少有时你应该检查/修改宏FD_SETSIZE的值。*这个值是系 统相关的*,同时检查你的系统中的select() 的man手册。有一些系统对多于1024个文件描述符的支持有问题。[译者注: Linux就是这样 的系统!你会发现sizeof(fd_set)的结果是128(*8 = FD_SETSIZE=1024) 尽管很少你会遇到这种情况。] 

select的基本接口十分简单: 

    int select(int nfds, fd_set *readset, fd_set *writeset,
               fd_set *exceptset, struct timeval *timeout);
      
其中: 

nfds     
     需要检查的文件描述符个数,数值应该比是三组fd_set中最大数
     更大,而不是实际文件描述符的总数。
readset    
     用来检查可读性的一组文件描述符。
writeset
     用来检查可写性的一组文件描述符。
exceptset
     用来检查意外状态的文件描述符。(注:错误并不是意外状态)
timeout
     NULL指针代表无限等待,否则是指向timeval结构的指针,代表最
     长等待时间。(如果其中tv_sec和tv_usec都等于0, 则文件描述符
     的状态不被影响,但函数并不挂起)
      
函数将返回响应操作的对应操作文件描述符的总数,且三组数据均在恰当位置被修改,只有响应操作的那一些没有修改。接着应该用FD_ISSET宏来查找返回的文件描述符组。 

这里是一个简单的测试单个文件描述符可读性的例子: 

     int isready(int fd)
     {
         int rc;
         fd_set fds;
         struct timeval tv;
    
         FD_ZERO(&fds);
         FD_SET(fd,&fds);
         tv.tv_sec = tv.tv_usec = 0;
    
 rc = select(fd+1, &fds, NULL, NULL, &tv);
         if (rc < 0)
           return -1;
    
         return FD_ISSET(fd,&fds) ? 1 : 0;
     }
      
当然如果我们把NULL指针作为fd_set传入的话,这就表示我们对这种操作的发生不感兴趣,但select() 还是会等待直到其发生或者超过等待时间。 

[译 者注:在Linux中,timeout指的是程序在非sleep状态中度过的时间,而不是实际上过去的时间,这就会引起和非Linux平台移植上的时间不 等问题。移植问题还包括在System V风格中select()在函数退出前会把timeout设为未定义的 NULL状态,而在BSD中则不是这样, Linux在这点上遵从System V,因此在重复利用timeout指针问题上也应该注意。] 

2.1.2. 我如何使用poll()?
poll ()接受一个指向结构'struct pollfd'列表的指针,其中包括了你想测试的文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前 的结构在调用后将被填写并在事件发生后返回。在SVR4(可能更早的一些版本)中的 "poll.h"文件中包含了用于确定事件的一些宏定义。事件的等待 时间精确到毫秒 (但令人困惑的是等待时间的类型却是int),当等待时间为0时,poll()函数立即返回,-1则使poll()一直挂起直到一个指定 事件发生。下面是pollfd的结构。 

     struct pollfd {
         int fd;        /* 文件描述符 */
         short events;  /* 等待的事件 */
         short revents; /* 实际发生了的事件 */
     };
      
于select()十分相似,当返回正值时,代表满足响应事件的文件描述符的个数,如果返回0则代表在规定事件内没有事件发生。如发现返回为负则应该立即查看 errno,因为这代表有错误发生。 

如果没有事件发生,revents会被清空,所以你不必多此一举。 

这里是一个例子 

   /* 检测两个文件描述符,分别为一般数据和高优先数据。如果事件发生
      则用相关描述符和优先度调用函数handler(),无时间限制等待,直到
      错误发生或描述符挂起。*/
   
   #include <stdlib.h>
   #include <stdio.h>
  
   #include <sys/types.h>
   #include <stropts.h>
   #include <poll.h>
  
   #include <unistd.h>
   #include <errno.h>
   #include <string.h>
  
   #define NORMAL_DATA 1
   #define HIPRI_DATA 2
  
   int poll_two_normal(int fd1,int fd2)
   {
       struct pollfd poll_list[2];
       int retval;
  
       poll_list[0].fd = fd1;
       poll_list[1].fd = fd2;
       poll_list[0].events = POLLIN|POLLPRI;
       poll_list[1].events = POLLIN|POLLPRI;
  
       while(1)
       {
           retval = poll(poll_list,(unsigned long)2,-1);
           /* retval 总是大于0或为-1,因为我们在阻塞中工作 */
  
           if(retval < 0)
           {
               fprintf(stderr,"poll错误: %sn",strerror(errno));
               return -1;
           }
    
           if(((poll_list[0].revents&POLLHUP) == POLLHUP) ||
              ((poll_list[0].revents&POLLERR) == POLLERR) ||
              ((poll_list[0].revents&POLLNVAL) == POLLNVAL) ||
              ((poll_list[1].revents&POLLHUP) == POLLHUP) ||
              ((poll_list[1].revents&POLLERR) == POLLERR) ||
              ((poll_list[1].revents&POLLNVAL) == POLLNVAL))
             return 0;
  
           if((poll_list[0].revents&POLLIN) == POLLIN)
             handle(poll_list[0].fd,NORMAL_DATA);
           if((poll_list[0].revents&POLLPRI) == POLLPRI)
             handle(poll_list[0].fd,HIPRI_DATA);
           if((poll_list[1].revents&POLLIN) == POLLIN)
             handle(poll_list[1].fd,NORMAL_DATA);
           if((poll_list[1].revents&POLLPRI) == POLLPRI)
             handle(poll_list[1].fd,HIPRI_DATA);
       }
   }
      
2.1.3. 我是否可以同时使用SysV IPC和select()/poll()?
*不能。* (除非在AIX上,因为它用一个无比奇怪的方法来实现这种组合) 

一般来说,同时使用select()或poll()和SysV 消息队列会带来许多麻烦。SysV IPC的对象并不是用文件描述符来处理的,所以它们不能被传递给select()和 poll()。这里有几种解决方法,其粗暴程度各不相同: 


完全放弃使用SysV IPC。 :-)

用fork(),然后让子进程来处理SysV IPC,然后用管道或套接口和父进程 说话。父进程则使用select()。 

同上,但让子进程用select(),然后和父亲用消息队列交流。 

安排进程发送消息给你,在发送消息后再发送一个信号。*警告*:要做好 这个并不简单,非常容易写出会丢失消息或引起死锁的程序。 

另外还有其他方法。 

ACE中Connector-Acceptor架构

分类:编程点滴

http://blog.csdn.net/riding/archive/2006/02/16/600009.aspx

ACE作为通讯方面的开源架构,不但用c++实现,而且用JAVA实作的架构已经可以使用了,由此看来掌握ACE成为每个开发通讯程序的程序员的必备技能。

ACE的库分为4个层次:

    OS适配该层将ACE的较高层和与OS机制相关联的平台特有的依赖屏蔽开来。

l         OO包装层 封装并增强在像Win32UNIX这样的现代操作系统上可用的并发、进程间通信(IPC)、以及虚拟内存机制。应用可以通过有选择地继承、聚合(aggregating)、和/或实例化ACE包装类属来合并和编写这些组件

l         框架    包括反应器,服务配置器,流。

l         ACE 的通讯模式包括接受器-连接器,前摄器两种主要的通讯模式。

前摄器理解可以理解为象WindwsOverlapper形式的一种利用操作系统的挂钩进行快速异步处理IO通讯的一种方式。它在某种程度上类似于一种软中断。用户只负责编写并注册相应的挂钩, 操作系统负责j监测事件发生,并调用相应的挂钩。

接受器-连接器模式是我们经常使用的通讯模式。相对于连接器,接收器模式是服务器处理程序经常重复编写的救世主。程序员在编写服务器处理程序时,无论是采用异步通讯还是阻塞通讯,单个线程还是多个线程,都可以采用接收器方式,由此可见接受器-连接器模式的强大。

接受器-接器模式的服端用接收器,客使用器(当然可以采用其他方式接到采用接收器的服上),相器,接受器了服程的复杂度,使程序从大量重的工作中解脱出来,并且写出成熟定的务处程序,对比以前只有少数具有丰富的通程序经验的人才能写出健壮的服务处程序(如web),ACE的接收器可以称之改写的巨人。接受器模式是ACE中最耀,是通程序史上的分水他的足以使我震惊。

ACE文档方面,尽管有马维Douglas C.SchmidtHuston写的C++络编程》1,卷2ACE术论文集》,《ACE程序员教程》《ACE应用实例》,但是ACE的接收器不是一件容易的事情。原因也许归于开源项目的一个通病--文档比较生僻难懂,或者不全面。所以开源项目领悟的最好方法是结合文档读源代码。

    接收器主要有ACE_Acceptor, ACE_Svc_Handler, ACE_Reactor 3个主要类组成。ACE_Reactor是分发器(Dispatcher), ACE_Acceptor 创建出ACE_Svc_Handler.处理顺序是:

1.ACE_Acceptoropen将自身帮定到ACE_Reactor上,并向其注册:当在PEER_ADDR上发生ACCEPT事件,调用handle_input成员挂钩函数。

2.主程序调用ACE_Reactorhandle_events()时,检测到ACCEPT,调用ACE_Acceptorhandle_input()。在Handle_Input中继续调用虚函数make_svc_handle()构造出ACE_Svc_Handler类(可以新建,则每客户一个Handler,也可使用单例,则多个客户共用一个服务处理器)。接着调用accept_ svc_handle(),将具体的参数传给ACE_Svc_Handler。最后调用active_ svc_handle(),一般调用ACE_Svc_Handleropen函数。在Open函数中注册反应器事件,如必要调用active()创建出线程。

   我们把创建接收器的线程称为主线程,把运行Ace_Reactorhandle_events()的线程取名为事件分发线程。把运行ACE_Svc_Handlersvc()的线程叫做服务线程。这些线程根据实现不同会有以下几种组合。

l         主线程,事件分发线程, 服务线程 三者合一

ACE_Svc_Handleropen函数中不调用active(),则服务不创建新的线程。

 主线程,事件分发线程合一,服务线程运行

z在ACE_Svc_Handleropen函数中调用active(),则服务线程创建,线程运行ACE_Svc_Handlersvc()

l         主线程运行,事件分发线程和服务线程合一。

后叙述

l         主线程,事件分发线程,服务线程都运行

ACE_Svc_Handleropen函数中调用active()

另创建一个线程,循环运行ACE_Reactorhandle_events() 或者run_event_loop();

l         主线程, 服务线程合一,  事件分发线程运行。

另创建一个线程,循环运行ACE_Reactorhandle_events() 或者run_event_loop();

ACE_Svc_Handleropen函数中不调用active()

 

三个线程都运行时,下表显示类的成员函数被线程调用的关系.3个线程都运行时,缺省状态下ACE_AcceptorACE_Svc_Handler使用一个反应器,而反应器监测事件到达时,调用ACE_Event_Handler类(ACE_AcceptorACE_Svc_Handler 都继承了ACE_Event_Handler)的handle_*钩子函数。因此,handle_*函数实际上运行在事件分发线程上。一般ACE_Svc_Handlerhandle_input钩子函数中读取数据,也就是说读数据在事件分发线程内执行了,这样读处理没有并行化,可能ACE的设计者认为操作系统的socket尽管有多个,实际的IO处理比如读写是串行的,因此缺省的Connector-Acceptor架构被设计成这样。Svc()运行在服务线程内,对读取的数据进行处理,并用peer()返回的Stream发送结果,这样一来,事件分发线程和服务线程会公用一个Handler,所以应当采用互斥方式访问IO

传统的Soket服务编程一般是主线程在监听soket等待一个客户端的连接,然后产生一个用于和客户端通讯的数据soket,同时创建一个线程或进程,该线程用此socket从客户端接收数据,进行数据处理,然后发送结果到客户端。这就是每客户每处理的通讯服务程序方式。如何用Connector-Acceptor架构实现这一过程呢? 如上表格所示,ACE_Svc_Handlerhandle_input()应当从事件分发线程线程移到服务线程运行,因此,实现多线程同步IO的方法是将用于ACE_Acceptor的反应器和ACE_Svc_Handler的反应器分开,并且做到每个数据Soket有一个反应器,这样就可以同步访问,这种方式就是前面列出的组合的第3种组合,不过事件分发线程被分为两部分,一部分是反应器分发监听socket的事件函数即run_reactor_event_loop()函数。另一部分是反应器分发数据socket的事件函数即run_reactor_event_loop()函数。具体实现步骤如下:

1. ACE_Svc_Handler open函数中, 重新为此 ACE_Svc_Handler实例分配一个新的反应器,并用此实例作为参数 注册读写,关闭事件。

2. 接着调用active(),会创建出线程。

3. ACE_Svc_Handlersvc()中循环调用此反应器的handle_events()run_reactor_event_loop()函数。

4. handle_input函数中读取数据,然后进行数据分析。发送写消息到反应器。会调用handle_outpu().

5. handle_outpu()发送结果到客户端。

经过这样改写,就可以用Connector-Acceptor架构实现每客户每处理的通讯方式。

ACE的功能强大,配置灵活多样,以至于很多新手在实现一个程序时面对很多实现方式,一时不能决定究竟如何选择。而现有的关于ACE的书本和文档的例子都很不完整,这些都为初学ACE的程序员增加了难度。我的经验是尽量优先使用模式,然后框架,最后是类层次的复用。所以使用ACE,一定先选择Connector-AcceptorProactor,只有当这两者不能满足要求时再考虑使用其它的类。

    最后感激参与开源项目开发的前辈和同仁,也感恩他们创造出ACE这样的品。

组播通信(转)

分类:默认栏目

摘要:
  
    本文试图成为学习TCP/IP网络组播技术的入门材料。文中介绍了组播通信的概念及原理,以及用于组播应用编程的Linux API的详细资料。为了使读者更加完整的了解Linux 组播的整体概念,文中对实现该技术的核心函数也做了介绍。在文章的最后给出了一个简单的C语言套接字编程例子,说明如何创建组播应用程序。
  
    一、导言
  
    在网络中,主机间可以用三种不同的地址进行通信:
  
    单播地址(unicast):即在子网中主机的唯一地址(接口)。如IP地址:192.168.100.9MAC地址:80:C0:F6:A0:4A:B1
  
    广播地址:这种类型的地址用来向子网内的所有主机(接口)发送数据。如广播IP地址是192.168.100.255MAC广播地址:FF:FF:FF:FF:FF
  
    组播地址:通过该地址向子网内的多个主机即主机群(接口)发送数据。
  
    如果只是向子网内的部分主机发送报文,组播地址就很有用处了;在需要向多个主机发送多媒体信息(如实时音频、视频)的情况下,考虑到其所需的带宽,分别向每一客户端主机发送数据并不是个好办法,如果发送主机与某些接收端的客户主机不在子网之内,采用广播方式也不是一个好的解决方案。
  
    二、组播地址
  
    大家知道,IP地址空间被划分为ABC三类。第四类即D类地址被保留用做组播地址。在第四版的IP协议(IPv4)中,从224.0.0.0239.255.255.255间的所有IP地址都属于D类地址。
  
    组播地址中最重要的是第24位到27位间的这四位,对应到十进制是224239,其它28位保留用做组播的组标识,如下图所示:
   
  图1 组播地址示意图
  
    IPv4的组播地址在网络层要转换成网络物理地址。对一个单播的网络地址,通过ARP协议可以获取与IP地址对应的物理地址。但在组播方式下ARP协议无法完成类似功能,必须得用其它的方法获取物理地址。在下面列出的RFC文档中提出了完成这个转换过程的方法:
  
  RFC1112Multicast IPv4 to Ethernet physical address correspondence
  RFC1390
Correspondence to FDDI
  RFC1469
Correspondence to Token-Ring networks
  

    在最大的以太网地址范围内,转换过程是这样的:将以太网地址的前24位最固定为01:00:5E,这几位是重要的标志位。紧接着的一位固定为0,其它23位用IPv4组播地址中的低23位来填充。该转换过程如下图所示:
  
  图2 地址转换示意图
  
    例如,组播地址为224.0.0.5其以太网物理地址为01:00:5E:00:00:05
  
    还有一些特殊的IPv4组播地址:
  
    224.0.0.1:标识子网中的所有主机。同一个子网中具有组播功能的主机都是这个组的成员。
  
    224.0.0.2:该地址用来标识网络中每个具有组播功有的路由器。
  
    224.0.0.0----224.0.0.255范围内的地址被分配给了低层次的协议。向这些范围内的地址发送数据包,有组播功能的路由器将不会为其提供路由。
  
    239.0.0.0----239.255.255.255间的地址分配用做管理用途。这些地址被分配给局部的每一个组织,但不可以分配到组织外部,组织内的路由器不向在组织外的地址提供路由。
  
    除了上面列出的部分组播地址外,还有许多的组播地址。在最新版本的RFC文档“Assinged Numbers”中有完整的介绍。
  
    下面的表中列出了全部的组播地址空间,同时还列出了相应的地址段的常用名称及其TTLIP包的存活时间)。在IPv4组播方式下,TTL有双重意义:正如大家所知的,TTL原本用来控制数据包在网络中的存活时间,防止由于路由器配置错误导致出现数据包传播的死循环;在组播方式下,它还代表了数据包的活动范围,如:数据包在网络中能够传送多远?这样就可以基于数据包的分类来定义其传送范围。
  
    范围 TTL 地址区间 描述
  
    节点(Node) 0 只能向本机发送的数据包,不能向网络中的其它接口传送
  
    链路(Link) 1 224.0.0.0-224.0.0.255 只能在发送主机所在的一个子网内的传送,不会通过路由器转发。
  
    部门 32 239.255.0.0-239.255.255.255 只在整个组织下的一个部门内(Department) 传送
  
    组织 64 239.192.0.0--239.195.255.255 在整个组织内传送(Organization)
  

    全局(Global)255 224.0.1.0--238.255.255.255 没有限制,可全局范围内传送
  
    三、组播的工作过程
  
    在局域网内,主机的网络接口将到目的主机的数据包发送到高层,这些数据包中的目的地址是物理接口地址或广播地址。
  
    如果主机已经加入到一个组播组中,主机的网络接口就会识别出发送到该组成员的数据包。
  
    因此,如果主机接口的物理地址为80:C0:F6:A0:4A:B1,其加入的组播组为224.0.1.10,则发送给主机的数据包中的目的地址必是下面三种类型之一:
  
    接口地址:80:C0:F6:A0:4A:B1
  

    广播地址:FF:FF:FF:FF:FF:FF:FF:FF
  

    组播地址:01:00:5E:00:01:0A
  

    广域网中,路由器必须支持组播路由。当主机中运行的进程加入到某个组播组中时,主机向子网中的所有组播路由器发送IGMPInternet分组管理协议)报文,告诉路由器凡是发送到这个组播组的组播报文都必须发送到本地的子网中,这样主机的进程就可以接收到报文了。子网中的路由器再通知其它的路由器,这些路由器就知道该将组播报文转发到哪些子网中去。
  
    子网中的路由器也向224.0.0.1发送一个IGMP报文(224.0.0.1代表组中的全部主机),要求组中的主机提供组的相关信息。组中的主机收到这个报文后,都各将计数器的值设为随机值,当计数器递减为0时再向路由器发送应答。这样就防止了组中所有的主机同时向路由器发送应答,造成网络拥塞。主机向组播地址发送一个报文做为对路由器的应答,组中的其它主机一旦看到这个应答报文,就不再发送应答报文了,因为组中的主机向路由器提供的都是相同的信息,所以子网路由器只需得到组中一个主机提供的信息就可以了。
  
    如果组中的主机都退出了,路由器就收不到应答,因此路由器认为该组目前没有主机加入,遂停止到该子网报文的路由。IGMPv2的解决方案是:组中的主机在退出时向224.0.0.2 发送报文通知组播路由器。
  
    四、应用编程接口(API
  
    如果你有套接字编程的经验,就会发现,对组播选项所进行的操作只需五个新的套接字操作。函数setsockopt()getsockopt()用来建立和读取这五个选项的值。下表中列出了组播的可选项,并列出其数据类型和描述:
  
    IPv4 选项 数据类型 描 述
  
    IP_ADD_MEMBERSHIP struct ip_mreq 加入到组播组中
  
    IP_ROP_MEMBERSHIP struct ip_mreq 从组播组中退出
  
    IP_MULTICAST_IF struct ip_mreq 指定提交组播报文的接口
  
    IP_MULTICAST_TTL u_char 指定提交组播报文的TTL
  

    IP_MULTICAST_LOOP u_char 使组播报文环路有效或无效
  
    在头文件中定义了ip_mreq结构:
  
  struct ip_mreq {
  
struct in_addr imr_multiaddr; /* IP multicast address of group */
  
struct in_addr imr_interface; /* local IP address of interface */
  
};
  

    在头文件中组播选项的值为:
  
  #define IP_MULTICAST_IF 32
  
#define IP_MULTICAST_TTL 33
  
#define IP_MULTICAST_LOOP 34
  
#define IP_ADD_MEMBERSHIP 35
  
#define IP_DROP_MEMBERSHIP 36
  
IP_ADD_MEMBERSHIP
  

    若进程要加入到一个组播组中,用soketsetsockopt()函数发送该选项。该选项类型是ip_mreq结构,它的第一个字段imr_multiaddr指定了组播组的地址,第二个字段imr_interface指定了接口的IPv4地址。
  
    IP_DROP_MEMBERSHIP
  

    该选项用来从某个组播组中退出。数据结构ip_mreq的使用方法与上面相同。
  
    IP_MULTICAST_IF
  

    该选项可以修改网络接口,在结构ip_mreq中定义新的接口。
  
    IP_MULTICAST_TTL
  

    设置组播报文的数据包的TTL(生存时间)。默认值是1,表示数据包只能在本地的子网中传送。
  
    IP_MULTICAST_LOOP
  

    组播组中的成员自己也会收到它向本组发送的报文。这个选项用于选择是否激活这种状态。
  
    五、一个组播通信的例子
  
    下面给出一个简单的例子实现文中阐述的思想:由一个进程向一个组播组发送报文,组播组中的相关进程接收报文,并将报文显示到屏幕上。
  
    下面的代码实现了一个服务进程,它将标准输入接口输入的信息全部发送到组播组224.0.1.1

>  

。你会发现,将信息发送到组播组不需要特别的操作,只要设置好组播组的目的地址就足够了。若在开发过程中,LoopbackTTL这两个选项的默认值不适合应用程序,可以加以调整。
  
    服务程序
  
    将标准输入端口的输入发送到组播组224.0.1.1
  
  #include
  
#include
  
#include
  
#include
  
#include
  
#include
  
#define MAXBUF 256
  
#define PUERTO 5000
  
#define GRUPO "224.0.1.1"
  
int main(void) {
  
int s;
  
struct sockaddr_in srv;
  
char buf[MAXBUF];
  
bzero(&srv, sizeof(srv));
  
srv.sin_family = AF_INET;
  
srv.sin_port = htons(PUERTO);
  
if (inet_aton(GRUPO, &srv.sin_addr) < 0) {
  
perror("inet_aton");
  
return 1;
  
}
  
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
  
perror("socket");
  
return 1;
  
}
  
while (fgets(buf, MAXBUF, stdin)) {
  
if (sendto(s, buf, strlen(buf), 0,
  
(struct sockaddr *)&srv, sizeof(srv)) < 0) {
  
perror("recvfrom");
  
} else {
  
fprintf(stdout, "Enviado a %s: %s
  
", GRUPO, buf);
  
}
  
}
  
}
  

    客户端程序
  
    下面的代码是客户端程序,它负责接收由服务程序发送到组播组中的信息,将收到的报文在标准输出设备中显示。程序中唯一与接收UDP报文过程不同是它设置了IP_ADD_MEMBERSHIP选项。
  
  #include
  
#include
  
#include
  
#include
  
#include
  
#define MAXBUF 256
  
#define PUERTO 5000
  
#define GRUPO "224.0.1.1"
  
int main(void) {
  
int s, n, r;
  
struct sockaddr_in srv, cli;
  
struct ip_mreq mreq;
  
char buf[MAXBUF];
  
bzero(&srv, sizeof(srv));
  
srv.sin_family = AF_INET;
  
srv.sin_port = htons(PUERTO);
  
if (inet_aton(GRUPO, &srv.sin_addr) < 0) {
  
perror("inet_aton");
  
return 1;
  
}
  
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
  
perror("socket");
  
return 1;
  
}
  
if (bind(s, (struct sockaddr *)&srv, sizeof(srv)) < 0) {
  
perror("bind");
  
return 1;
  
}
  
if (inet_aton(GRUPO, &mreq.imr_multiaddr) < 0) {
  
perror("inet_aton");
  
return 1;
  
}
  
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
  
if (setsockopt(s,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))
  
< 0) {
  
perror("setsockopt");
  
return 1;
  
}
  
n = sizeof(cli);
  
while (1) {
  
if ((r = recvfrom(s, buf, MAXBUF, 0, (struct sockaddr *)
  
&cli, &n)) < 0) {
  
perror("recvfrom");
  
} else {
  
buf[r] = 0;
  
fprintf(stdout, "Mensaje desde %s: %s
  
",
  
inet_ntoa(cli.sin_addr), buf);
  
}
  
}
  
}
  

    六、内核与组播
  
    在上面的例子中我们看到:如果一个进程要加入到组播组中,就要使用setsockopt()函数在IP层设置IP_ADD_MEMBERSHIP
  

  在/usr/src/linux/net/ipv4/ip_sockglue.c文件中可以找见该函数的源代码。其中设置IP_ADD_MEMBERSHIPIP_DROP_MEMBERSHIP的部分代码如下:
  
  struct ip_mreqn mreq;
  
if (optlen < sizeof(struct ip_mreq))
  
return -EINVAL;
  
if (optlen >= sizeof(struct ip_mreqn)) {
  
if(copy_from_user(&mreq,optval,sizeof(mreq)))
  
return -EFAULT;
  
} else {
  
memset(&mreq, 0, sizeof(mreq));
  
if (copy_from_user(&mreq,optval,sizeof(struct ip_mreq)))
  
return -EFAULT;
  
}
  
if (optname == IP_ADD_MEMBERSHIP)
  
return ip_mc_join_group(sk,&mreq);
  
else
  
return ip_mc_leave_group(sk,&mreq);
  

    程序一开始先检查输入参数ip_mreq结构的长度是否正确,并将其从用户区复制到内核区。在得到参数的值后,接着调用ip_mc_join_group()加入到组播组或调用ip_mc_leave_group()退出组播组。
  
  在/usr/src/linux/net/ipv4/igmp.c中可以找到这些函数的代码。加入组播组的源程序代码如下:
  
  int ip_mc_join_group(struct sock *sk , struct ip_mreqn *imr)
  
{
  
int err;
  
u32 addr = imr->imr_multiaddr.s_addr;
  
struct ip_mc_socklist, *iml, *i;
  
struct in_device *in_dev;
  
int count = 0;
  

    在开始部分用MULTICAST宏检查组的地址,确认其在保留的组播组地址范围内。只要检查IP地址的第一部分是不是224就可以确认地址是否有效:
  
  if (!MULTICAST(addr))
  
return -EINVAL;
  
rtnl_shlock();
  

    检查完组播地址后,接着就要设置网络接口了。如果不能通过接口索引获得网络接口(如在IPv6下),在这种情况下可以调用函数ip_mc_find_dev()获取网络接口。在本文中不存在这个问题,因为我们的工作都是在IPv4下进行的。若地址的格式是INADDR_ANY,内核就依照路由表的定义,按照组地址在路由表中查找网络接口。
  
  if (!imr->imr_ifindex)
  
in_dev = ip_mc_find_dev(imr);
  
else
  
in_dev = inetdev_by_index(imr->imr_ifindex);
  
if (!in_dev) {
  
iml = NULL;
  
err = -ENODEV;
  
goto done;
  
}
  

    接着给ip_mc_socklist结构分配内存,然后比较套接字的每个组地址和接口。只要发现了一个匹配项就跳出该函数,因为有一个匹配项就可以了。若网络接口地址不是INADDR_ANY,相应的计数器值就要增加。
  
  iml = (struct ip_mc_socklist *)sock_kmalloc(sk, sizeof(*iml),
  
GFP_KERNEL);
  
err = -EADDRINUSE;
  
for (i=sk->ip_mc_list; i; i=i->next) {
  
if (memcmp(&i->multi, imr, sizeof(*imr)) == 0) {
  
/* New style additions are reference counted */
  
if (imr->imr_address.s_addr == 0) {
  
i->count++;
  
err = 0;
  
}
  
goto done;
  
}
  
count++;
  
}
  
err = -ENOBUFS;
  
if (iml == NULL' 'count >= sysctl_igmp_max_memberships)
  
goto done;
  

    到这里,就可以用新创建的套接字与组播组建立链接了,这时还必须创建一个新的记录,记录下属于该套接字的组的列表。首先还是要预先分配内存,然后只要给相关结构中的几个字段赋值,就完成了这个操作:
  
  memcpy(&iml->multi,imr, sizeof(*imr));
  
iml->next = sk->ip_mc_list;
  iml->

count = 1;
  sk->ip_mc_list = iml;
  
ip_mc_inc_group(in_dev,addr);
  
iml = NULL;
  
err = 0;
  
done:
  
rtnl_shunlock();
  
if (iml)
  
sock_kfree_s(sk, iml, sizeof(*iml));
  
return err;
  
}
  

    用函数ip_mc_leave_group()从一个组播组中退出,它的工作过程比前面的函数要来得简单。首先在套接字记录中查找组播组及接口地址,找到后,将调用这个接口地址的进程数的值递减,若该值为0,就删除该计数器,因为与组播组相关的进程至少要有一个。
  
  int ip_mc_leave_group(struct sock *sk, struct ip_mreqn *imr)
  
{
  
struct ip_mc_socklist *iml, **imlp;
  
for (imlp=&sk->ip_mc_list;(iml=*imlp)!=NULL; imlp=&iml->next) {
  
if (iml->multi.imr_multiaddr.s_addr==imr->imr_multiaddr.s_addr
  
&& iml->multi.imr_address.s_addr==imr->imr_address.s_addr &&
  
(!imr->imr_ifindex' 'iml->multi.imr_ifindex==imr->imr_ifindex)) {
  
struct in_device *in_dev;
  
if (--iml->count)
  
return 0;
  
*imlp = iml->next;
  
synchronize_bh();
  
in_dev = inetdev_by_index(iml->multi.imr_ifindex);
  
if (in_dev)
  
ip_mc_dec_group(in_dev, imr->imr_multiaddr.s_addr);
  
sock_kfree_s(sk, iml, sizeof(*iml));
  
return 0;
  
}
  
}
  
return -EADDRNOTAVAIL;
  
}
  

    其它的组播选项都很简单,只要给当前套接字内的字段赋值就可以了,赋值的过程由ip_setsockopt()函数完成。

 

消息映射的实现 (转)

分类:编程点滴

  1. Windows消息概述
  2.  
  3. Windows应用程序的输入由Windows系统以消息的形式发送给应用程序的窗口。这些窗口通过窗口过程来接收和处理消息,然后把控制返还给Windows。
  4.  
  5. 消息的分类

     

     

  6. 队列消息和非队列消息

     

    从消息的发送途径上看,消息分两种:队列消息和非队列消息。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。

    这里,对消息队列阐述如下:

    Windows维护一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。

    鼠标、键盘事件由鼠标或键盘驱动程序转换成输入消息并把消息放进系统消息队列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次从系统消息队列移走一个消息,确定它是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进窗口创建线程的线程消息队列。线程消息队列接收送给该线程所创建窗口的消息。线程从消息队列取出消息,通过Windows把它送给适当的窗口过程来处理。

    除了键盘、鼠标消息以外,队列消息还有WM_PAINT、WM_TIMER和WM_QUIT。

    这些队列消息以外的绝大多数消息是非队列消息。

     

  7. 系统消息和应用程序消息

     

从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。

系统消息ID的范围是从0到WM_USER-1,或0X80000到0XBFFFF;应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID。

         

      1. 消息结构和消息处理

         

     

  1. 消息的结构

     

    为了从消息队列获取消息信息,需要使用MSG结构。例如,::GetMessage函数(从消息队列得到消息并从队列中移走)和::PeekMessage函数(从消息队列得到消息但是可以不移走)都使用了该结构来保存获得的消息信息。

    MSG结构的定义如下:

    typedef struct tagMSG { // msg

    HWND hwnd;

    UINT message;

    WPARAM wParam;

    LPARAM lParam;

    DWORD time;

    POINT pt;

    } MSG;

    该结构包括了六个成员,用来描述消息的有关属性:

    接收消息的窗口句柄、消息标识(ID)、第一个消息参数、第二个消息参数、消息产生的时间、消息产生时鼠标的位置。

     

  2. 应用程序通过窗口过程来处理消息

     

    如前所述,每个“窗口类”都要登记一个如下形式的窗口过程:

    LRESULT CALLBACK MainWndProc (

    HWND hwnd,// 窗口句柄

    UINT msg,// 消息标识

    WPARAM wParam,//消息参数1

    LPARAM lParam//消息参数2

    )

    应用程序通过窗口过程来处理消息:非队列消息由Windows直接送给目的窗口的窗口过程,队列消息由::DispatchMessage等派发给目的窗口的窗口过程。窗口过程被调用时,接受四个参数:

    a window handle(窗口句柄);

    a message identifier(消息标识);

    two 32-bit values called message parameters(两个32位的消息参数);

    需要的话,窗口过程用::GetMessageTime获取消息产生的时间,用::GetMessagePos获取消息产生时鼠标光标所在的位置。

    在窗口过程里,用switch/case分支处理语句来识别和处理消息。

     

  3. 应用程序通过消息循环来获得对消息的处理

     

    每个GDI应用程序在主窗口创建之后,都会进入消息循环,接受用户输入、解释和处理消息。

    消息循环的结构如下:

    while (GetMessage(&msg, (HWND) NULL, 0, 0)) {//从消息队列得到消息

    if (hwndDlgModeless == (HWND) NULL ||

    !IsDialogMessage(hwndDlgModeless, &msg) &&

    !TranslateAccelerator(hwndMain, haccel, &msg)) {

    TranslateMessage(&msg);

    DispatchMessage(&msg); //发送消息

    }

    }

    消息循环从消息队列中得到消息,如果不是快捷键消息或者对话框消息,就进行消息转换和派发,让目的窗口的窗口过程来处理。

    当得到消息WM_QUIT,或者::GetMessage出错时,退出消息循环。

     

  4. MFC消息处理

     

使用MFC框架编程时,消息发送和处理的本质也如上所述。但是,有一点需要强调的是,所有的MFC窗口都使用同一窗口过程,程序员不必去设计和实现自己的窗口过程,而是通过MFC提供的一套消息映射机制来处理消息。因此,MFC简化了程序员编程时处理消息的复杂性。

所谓消息映射,简单地讲,就是让程序员指定要某个MFC类(有消息处理能力的类)处理某个消息。MFC提供了工具ClassWizard来帮助实现消息映射,在处理消息的类中添加一些有关消息映射的内容和处理消息的成员函数。程序员将完成消息处理函数,实现所希望的消息处理能力。

如果派生类要覆盖基类的消息处理函数,就用ClassWizard在派生类中添加一个消息映射条目,用同样的原型定义一个函数,然后实现该函数。这个函数覆盖派生类的任何基类的同名处理函数。

 

下面几节将分析MFC的消息机制的实现原理和消息处理的过程。为此,首先要分析ClassWizard实现消息映射的内幕,然后讨论MFC的窗口过程,分析MFC窗口过程是如何实现消息处理的。

       

    1. 消息映射的定义和实现

       

         

      1. MFC处理的三类消息

         

根据处理函数和处理过程的不同,MFC主要处理三类消息:

     

  • Windows消息,前缀以“WM_”打头,WM_COMMAND例外。Windows消息直接送给MFC窗口过程处理,窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。

     

     

  • 控制通知消息,是控制子窗口送给父窗口的WM_COMMAND通知消息。窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。

     

需要指出的是,Win32使用新的WM_NOFITY来处理复杂的通知消息。WM_COMMAND类型的通知消息仅仅能传递一个控制窗口句柄(lparam)、控制窗ID和通知代码(wparam)。WM_NOTIFY能传递任意复杂的信息。

     

  • 命令消息,这是来自菜单、工具条按钮、加速键等用户接口对象的WM_COMMAND通知消息,属于应用程序自己定义的消息。通过消息映射机制,MFC框架把命令按一定的路径分发给多种类型的对象(具备消息处理能力)处理,如文档、窗口、应用程序、文档模板等对象。能处理消息映射的类必须从CCmdTarget类派生。

     

在讨论了消息的分类之后,应该是讨论各类消息如何处理的时候了。但是,要知道怎么处理消息,首先要知道如何映射消息。

         

      1. MFC消息映射的实现方法

         

        MFC使用ClassWizard帮助实现消息映射,它在源码中添加一些消息映射的内容,并声明和实现消息处理函数。现在来分析这些被添加的内容。

        在类的定义(头文件)里,它增加了消息处理函数声明,并添加一行声明消息映射的宏DECLARE_MESSAGE_MAP。

        在类的实现(实现文件)里,实现消息处理函数,并使用IMPLEMENT_MESSAGE_MAP宏实现消息映射。一般情况下,这些声明和实现是由MFC的ClassWizard自动来维护的。看一个例子:

        在AppWizard产生的应用程序类的源码中,应用程序类的定义(头文件)包含了类似如下的代码:

        //{{AFX_MSG(CTttApp)

        afx_msg void OnAppAbout();

        //}}AFX_MSG

        DECLARE_MESSAGE_MAP()

         

        应用程序类的实现文件中包含了类似如下的代码:

        BEGIN_MESSAGE_MAP(CTApp, CWinApp)

        //{{AFX_MSG_MAP(CTttApp)

        ON_COMMAND(ID_APP_ABOUT, OnAppAbout)

        //}}AFX_MSG_MAP

        END_MESSAGE_MAP()

         

        头文件里是消息映射和消息处理函数的声明,实现文件里是消息映射的实现和消息处理函数的实现。它表示让应用程序对象处理命令消息ID_APP_ABOUT,消息处理函数是OnAppAbout。

        为什么这样做之后就完成了一个消息映射?这些声明和实现到底作了些什么呢?接着,将讨论这些问题。

         

      2. 在声明与实现的内部

         

     

  1. DECLARE_MESSAGE_MAP宏:

     

    首先,看DECLARE_MESSAGE_MAP宏的内容:

    #ifdef _AFXDLL

    #define DECLARE_MESSAGE_MAP()

    private:

    static const AFX_MSGMAP_ENTRY _messageEntries[];

    protected:

    static AFX_DATA const AFX_MSGMAP messageMap;

    static const AFX_MSGMAP* PASCAL _GetBaseMessageMap();

    virtual const AFX_MSGMAP* GetMessageMap() const;

     

    #else

    #define DECLARE_MESSAGE_MAP()

    private:

    static const AFX_MSGMAP_ENTRY _messageEntries[];

    protected:

    static AFX_DATA const AFX_MSGMAP messageMap;

    virtual const AFX_MSGMAP* GetMessageMap() const;

     

    #endif

    DECLARE_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。

     

  2. BEGIN_MESSAE_MAP宏

     

    然后,看BEGIN_MESSAE_MAP宏的内容:

    #ifdef _AFXDLL

    #define BEGIN_MESSAGE_MAP(theClass, baseClass)

    const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap()

    { return &baseClass::messageMap; }

    const AFX_MSGMAP* theClass::GetMessageMap() const

    { return &theClass::messageMap; }

    AFX_DATADEF const AFX_MSGMAP theClass::messageMap =

    { &theClass::_GetBaseMessageMap, &theClass::_messageEntries[0] };

    const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =

    {

     

    #else

    #define BEGIN_MESSAGE_MAP(theClass, baseClass)

    const AFX_MSGMAP* theClass::GetMessageMap() const

    { return &theClass::messageMap; }

    AFX_DATADEF const AFX_MSGMAP theClass::messageMap =

    { &baseClass::messageMap, &theClass::_messageEntries[0] };

    const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =

    {

     

    #endif

     

    #define END_MESSAGE_MAP()

    {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }

    };

    对应地,BEGIN_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。END_MESSAGE_MAP相对简单,就只有一种定义。

     

  3. ON_COMMAND宏

     

最后,看ON_COMMAND宏的内容:

#define ON_COMMAND(id, memberFxn)

{

WM_COMMAND,

CN_COMMAND,

(WORD)id,

(WORD)id,

AfxSig_vv,

(AFX_PMSG)memberFxn

};

           

        1. 消息映射声明的解释

           

在清楚了有关宏的定义之后,现在来分析它们的作用和功能。

消息映射声明的实质是给所在类添加几个静态成员变量和静态或虚拟函数,当然它们是与消息映射相关的变量和函数。

     

  1. 成员变量

     

有两个成员变量被添加,第一个是_messageEntries,第二个是messageMap。

     

  • 第一个成员变量的声明:

     

AFX_MSGMAP_ENTRY _messageEntries[]

这是一个AFX_MSGMAP_ENTRY 类型的数组变量,是一个静态成员变量,用来容纳类的消息映射条目。一个消息映射条目可以用AFX_MSGMAP_ENTRY结构来描述。

AFX_MSGMAP_ENTRY结构的定义如下:

struct AFX_MSGMAP_ENTRY

{

//Windows消息ID

UINT nMessage;

//控制消息的通知码

UINT nCode;

//Windows Control的ID

UINT nID;

//如果是一定范围的消息被映射,则nLastID指定其范围

UINT nLastID;

 

UINT nSig;//消息的动作标识

//响应消息时应执行的函数(routine to call (or special value))

AFX_PMSG pfn;

};

从上述结构可以看出,每条映射有两部分的内容:第一部分是关于消息ID的,包括前四个域;第二部分是关于消息对应的执行函数,包括后两个域。

在上述结构的六个域中,pfn是一个指向CCmdTarger成员函数的指针。函数指针的类型定义如下:

typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

当使用一条或者多条消息映射条目初始化消息映射数组时,各种不同类型的消息函数都被转换成这样的类型:不接收参数,也不返回参数的类型。因为所有可以有消息映射的类都是从CCmdTarge派生的,所以可以实现这样的转换。

nSig是一个标识变量,用来标识不同原型的消息处理函数,每一个不同原型的消息处理函数对应一个不同的nSig。在消息分发时,MFC内部根据nSig把消息派发给对应的成员函数处理,实际上,就是根据nSig的值把pfn还原成相应类型的消息处理函数并执行它。

 

     

  • 第二个成员变量的声明

     

AFX_MSGMAP messageMap;

这是一个AFX_MSGMAP类型的静态成员变量,从其类型名称和变量名称可以猜出,它是一个包含了消息映射信息的变量。的确,它把消息映射的信息(消息映射数组)和相关函数打包在一起,也就是说,得到了一个消息处理类的该变量,就得到了它全部的消息映射数据和功能。AFX_MSGMAP结构的定义如下:

struct AFX_MSGMAP

{

//得到基类的消息映射入口地址的数据或者函数

#ifdef _AFXDLL

//pfnGetBaseMap指向_GetBaseMessageMap函数

const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();

#else

//pBaseMap保存基类消息映射入口_messageEntries的地址

const AFX_MSGMAP* pBaseMap;

#endif

//lpEntries保存消息映射入口_messageEntries的地址

const AFX_MSGMAP_ENTRY* lpEntries;

};

从上面的定义可以看出,通过messageMap可以得到类的消息映射数组_messageEntries和函数_GetBaseMessageMap的地址(不使用MFC DLL时,是基类消息映射数组的地址)。

 

     

  1. 成员函数

     

     

  • _GetBaseMessageMap()

     

用来得到基类消息映射的函数。

 

     

  • GetMessageMap()

     

用来得到自身消息映射的函数。

           

        1. 消息映射实现的解释

           

消息映射实现的实质是初始化声明中定义的静态成员函数_messageEntries和messageMap,实现所声明的静态或虚拟函数GetMessageMap、_GetBaseMessageMap。

这样,在进入WinMain函数之前,每个可以响应消息的MFC类都生成了一个消息映射表,程序运行时通过查询该表判断是否需要响应某条消息。

     

  1. 对消息映射入口表(消息映射数组)的初始化

     

    如前所述,消息映射数组的元素是消息映射条目,条目的格式符合结构AFX_MESSAGE_ENTRY的描述。所以,要初始化消息映射数组,就必须使用符合该格式的数据来填充:如果指定当前类处理某个消息,则把和该消息有关的信息(四个)和消息处理函数的地址及原型组合成为一个消息映射条目,加入到消息映射数组中。

    显然,这是一个繁琐的工作。为了简化操作,MFC根据消息的不同和消息处理方式的不同,把消息映射划分成若干类别,每一类的消息映射至少有一个共性:消息处理函数的原型相同。对每一类消息映射,MFC定义了一个宏来简化初始化消息数组的工作。例如,前文提到的ON_COMMAND宏用来映射命令消息,只要指定命令ID和消息处理函数即可,因为对这类命令消息映射条目,其他四个属性都是固定的。ON_COMMAND宏的初始化内容如下:

    {WM_COMMAND,

    CN_COMMAND,

    (WORD)ID_APP_ABOUT,

    (WORD)ID_APP_ABOUT,

    AfxSig_vv,

    (AFX_PMSG)OnAppAbout

    }

    这个消息映射条目的含义是:消息ID是ID_APP_ABOUT,OnAppAbout被转换成AFX_PMSG指针类型,AfxSig_vv是MFC预定义的枚举变量,用来标识OnAppAbout的函数类型为参数空(Void)、返回空(Void)。

    在消息映射数组的最后,是宏END_MESSAGE_MAP的内容,它标识消息处理类的消息映射条目的终止。

     

  2. 对messageMap的初始化

     

    如前所述,messageMap的类型是AFX_MESSMAP。

    经过初始化,域lpEntries保存了消息映射数组_messageEntries的地址;如果动态链接到MFC DLL,则pfnGetBaseMap保存了_GetBaseMessageMap成员函数的地址;否则pBaseMap保存了基类的消息映射数组的地址。

     

  3. 对函数的实现

     

_GetBaseMessageMap()

它返回基类的成员变量messagMap(当使用MFC DLL时),使用该函数得到基类消息映射入口表。

 

GetMessageMap():

它返回成员变量messageMap,使用该函数得到自身消息映射入口表。

 

顺便说一下,消息映射类的基类CCmdTarget也实现了上述和消息映射相关的函数,不过,它的消息映射数组是空的。

 

既然消息映射宏方便了消息映射的实现,那么有必要详细的讨论消息映射宏。下一节,介绍消息映射宏的分类、用法和用途。

         

      1. 消息映射宏的种类

         

为了简化程序员的工作,MFC定义了一系列的消息映射宏和像AfxSig_vv这样的枚举变量,以及标准消息处理函数,并且具体地实现这些函数。这里主要讨论消息映射宏,常用的分为以下几类。

     

  1. 用于Windows消息的宏,前缀为“ON_WM_”。

     

    这样的宏不带参数,因为它对应的消息和消息处理函数的函数名称、函数原型是确定的。MFC提供了这类消息处理函数的定义和缺省实现。每个这样的宏处理不同的Windows消息。

    例如:宏ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函数,消息映射条目的第一个成员nMessage指定为要处理的Windows消息的ID,第二个成员nCode指定为0。

     

  2. 用于命令消息的宏ON_COMMAND

     

这类宏带有参数,需要通过参数指定命令ID和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0)。消息处理函数的原型是void (void),不带参数,不返回值。

除了单条命令消息的映射,还有把一定范围的命令消息映射到一个消息处理函数的映射宏ON_COMMAND_RANGE。这类宏带有参数,需要指定命令ID的范围和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0),第三个成员nID和第四个成员nLastID指定了映射消息的起止范围。消息处理函数的原型是void (UINT),有一个UINT类型的参数,表示要处理的命令消息ID,不返回值。

(3)用于控制通知消息的宏

这类宏可能带有三个参数,如ON_CONTROL,就需要指定控制窗口ID,通知码和消息处理函数;也可能带有两个参数,如具体处理特定通知消息的宏ON_BN_CLICKED、ON_LBN_DBLCLK、ON_CBN_EDITCHANGE等,需要指定控制窗口ID和消息处理函数。

控制通知消息也被映射到WM_COMMAND上,也就是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (void),没有参数,不返回值。

还有一类宏处理通知消息ON_NOTIFY,它类似于ON_CONTROL,但是控制通知消息被映射到WM_NOTIFY。消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (NMHDR*, LRESULT*),参数1是NMHDR指针,参数2是LRESULT指针,用于返回结果,但函数不返回值。

对应地,还有把一定范围的控制子窗口的某个通知消息映射到一个消息处理函数的映射宏,这类宏包括ON__CONTROL_RANGE和ON_NOTIFY_RANGE。这类宏带有参数,需要指定控制子窗口ID的范围和通知消息,以及消息处理函数。

对于ON__CONTROL_RANGE,是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID等于指定了控制窗口ID的范围。消息处理函数的原型是void (UINT),参数表示要处理的通知消息是哪个ID的控制子窗口发送的,函数不返回值。

对于ON__NOTIFY_RANGE,消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID指定了控制窗口ID的范围。消息处理函数的原型是void (UINT, NMHDR*, LRESULT*),参数1表示要处理的通知消息是哪个ID的控制子窗口发送的,参数2是NMHDR指针,参数3是LRESULT指针,用于返回结果,但函数不返回值。

(4)用于用户界面接口状态更新的ON_UPDATE_COMMAND_UI宏

这类宏被映射到消息WM_COMMND上,带有两个参数,需要指定用户接口对象ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID都指定为用户接口对象ID。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,不返回值。

对应地,有更新一定ID范围的用户接口对象的宏ON_UPDATE_COMMAND_UI_RANGE,此宏带有三个参数,用于指定用户接口对象ID的范围和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID用于指定用户接口对象ID的范围。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,函数不返回值。之所以不用当前用户接口对象ID作为参数,是因为CCmdUI对象包含了有关信息。

(5)用于其他消息的宏

例如用于用户定义消息的ON_MESSAGE。这类宏带有参数,需要指定消息ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为消息ID,第二个成员nCode被指定为0,第三个成员nID和第四个成员也是0。消息处理的原型是LRESULT (WPARAM, LPARAM),参数1和参数2是消息参数wParam和lParam,返回LRESULT类型的值。

(6)扩展消息映射宏

很多普通消息映射宏都有对应的扩展消息映射宏,例如:ON_COMMAND对应的ON_COMMAND_EX,ON_ONTIFY对应的ON_ONTIFY_EX,等等。扩展宏除了具有普通宏的功能,还有特别的用途。关于扩展宏的具体讨论和分析,见4.4.3.2节。

作为一个总结,下表列出了这些常用的消息映射宏。

表4-1 常用的消息映射宏

消息映射宏

用途

ON_COMMAND

把command message映射到相应的函数

ON_CONTROL

把control notification message映射到相应的函数。MFC根据不同的控制消息,在此基础上定义了更具体的宏,这样用户在使用时就不需要指定通知代码ID,如ON_BN_CLICKED。

ON_MESSAGE

把user-defined message.映射到相应的函数

ON_REGISTERED_MESSAGE

把registered user-defined message映射到相应的函数,实际上nMessage等于0x0C000,nSig等于宏的消息参数。nSig的真实值为Afxsig_lwl。

ON_UPDATE_COMMAND_UI

把user interface user update command message映射到相应的函数上。

ON_COMMAND_RANGE

把一定范围内的command IDs 映射到相应的函数上

ON_UPDATE_COMMAND_UI_RANGE

把一定范围内的user interface user update command message映射到相应的函数上

ON_CONTROL_RANGE

把一定范围内的control notification message映射到相应的函数上

 

 

在表4-1中,宏ON_REGISTERED_MESSAGE的定义如下:

#define ON_REGISTERED_MESSAGE(nMessageVariable, memberFxn)

{ 0xC000, 0, 0, 0,

(UINT)(UINT*)(&nMessageVariable),

/*implied 'AfxSig_lwl'*/

(AFX_PMSG)(AFX_PMSGW)(LRESULT

(AFX_MSG_CALL CWnd::*)

(WPARAM, LPARAM))&memberFxn }

从上面的定义可以看出,实际上,该消息被映射到WM_COMMAND(0XC000),指定的registered消息ID存放在nSig域内,nSig的值在这样的映射条目下隐含地定为AfxSig_lwl。由于ID和正常的nSig域存放的值范围不同,所以MFC可以判断出是否是registered消息映射条目。如果是,则使用AfxSig_lwl把消息处理函数转换成参数1为Word、参数2为long、返回值为long的类型。

在介绍完了消息映射的内幕之后,应该讨论消息处理过程了。由于CCmdTarge的特殊性和重要性,在4.3节先对其作一个大略的介绍。

       

    1. CcmdTarget类

       

除了CObject类外,还有一个非常重要的类CCmdTarget。所有响应消息或事件的类都从它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。

CCmdTarget类是MFC处理命令消息的基础、核心。MFC为该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,而且,很大一部分是针对OLE设计的。在OLE应用中,CCmdTarget是MFC处理模块状态的重要环节,它起到了传递模块状态的作用:其构造函数获取当前模块状态,并保存在成员变量m_pModuleState里头。关于模块状态,在后面章节讲述。

CCmdTarget有两个与消息映射有密切关系的成员函数:DispatchCmdMsg和OnCmdMsg。

     

  1. 静态成员函数DispatchCmdMsg

     

    CCmdTarget的静态成员函数DispatchCmdMsg,用来分发Windows消息。此函数是MFC内部使用的,其原型如下:

    static BOOL DispatchCmdMsg(

    CCmdTarget* pTarget,

    UINT nID,

    int nCode,

    AFX_PMSG pfn,

    void* pExtra,

    UINT nSig,

    AFX_CMDHANDLERINFO* pHandlerInfo)

     

    关于此函数将在4.4.3.2章节命令消息的处理中作更详细的描述。

     

  2. 虚拟函数OnCmdMsg

     

CCmdTarget的虚拟函数OnCmdMsg,用来传递和发送消息、更新用户界面对象的状态,其原型如下:

OnCmdMsg(

UINT nID,

int nCode,

void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

框架的命令消息传递机制主要是通过该函数来实现的。其参数描述参见4.3.3.2章节DispacthCMdMessage的参数描述。

在本书中,命令目标指希望或者可能处理消息的对象;命令目标类指命令目标的类。

CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数(标准Windows消息不会送到这里处理)。

这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配指定的消息映射条目。匹配标准:命令消息ID相同,控制通知代码相同。因为GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。

如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数;

如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为止;

如果最后没有找到,则返回FASLE。

 

每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命令目标的OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时,要调用基类的被覆盖的OnCmdMsg。

在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。具体实现见后续章节。

必要的话,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令目标中。

本节对CCmdTarget的两个成员函数作一些讨论,是为了对MFC的消息处理有一个大致印象。后面4.4.3.2节和4.4.3.3节将作进一步的讨论。

       

    1. MFC窗口过程

       

      前文曾经提到,所有的消息都送给窗口过程处理,MFC的所有窗口都使用同一窗口过程,消息或者直接由窗口过程调用相应的消息处理函数处理,或者按MFC命令消息派发路径送给指定的命令目标处理。

      那么,MFC的窗口过程是什么?怎么处理标准Windows消息?怎么实现命令消息的派发?这些都将是下文要回答的问题。

         

      1. MFC窗口过程的指定

         

        从前面的讨论可知,每一个“窗口类”都有自己的窗口过程。正常情况下使用该“窗口类”创建的窗口都使用它的窗口过程。

        MFC的窗口对象在创建HWND窗口时,也使用了已经注册的“窗口类”,这些“窗口类”或者使用应用程序提供的窗口过程,或者使用Windows提供的窗口过程(例如Windows控制窗口、对话框等)。那么,为什么说MFC创建的所有HWND窗口使用同一个窗口过程呢?

        在MFC中,的确所有的窗口都使用同一个窗口过程:AfxWndProc或AfxWndProcBase(如果定义了_AFXDLL)。它们的原型如下:

        LRESULT CALLBACK

        AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

         

        LRESULT CALLBACK

        AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

        这两个函数的原型都如4.1.