自动生成 Makefile 的全过程详解! automake/autoconf 入门
作为Linux 下的程序开发人员,大家一定都遇到过Makefile ,用make 命令来编译自己写的程序确实是很方便。一般情况下,大家都是手工写一个简单Makefile ,如果要想写出一个符合自由软件惯例的Makefile 就不那么容易了。
在本文中,将给大家介绍如何使用autoconf 和automake 两个工具来帮助我们自动地生成符合自由软件惯例的Makefile ,这样就可以象常 见的GNU 程序一样,只要使用“./configure” ,“make” ,“make instal” 就可以把程序安装到Linux 系统中去了。这将特别适合想做开放源代码软件的程序开发人员,又或如果你只是自己写些小的Toy 程序,那么这 个文章对你也会有很大的帮助。
一、Makefile 介绍
Makefile 是用于自动编译和链接的,一个工程有很多文件组成,每一个文件的改变都会导致工程的重新链接,但是不是所有的文件都需要重新编译,Makefile 中纪录有文件的信息,在make 时会决定在链接的时候需要重新编译哪些文件。
Makefile 的宗旨就是:让编译器知道要编译一个文件需要依赖其他的哪些文件。当那些依赖文件有了改变,编译器会自动的发现最终的生成文件已经过时,而重新编译相应的模块。
Makefile 的基本结构不是很复杂,但当一个程序开发人员开始写Makefile 时,经常会怀疑自己写的是否符合惯例,而且自己写的 Makefile 经常和自己的开发环境相关联,当系统环境变量或路径发生了变化后,Makefile 可能还要跟着修改。这样就造成了手工书写 Makefile 的诸多问题,automake 恰好能很好地帮助我们解决这些问题。
使用automake ,程序开发人员只需要写一些 简单的含有预定义宏的文件,由autoconf 根据一个宏文件生成configure ,由automake 根据另一个宏文件生成Makefile.in , 再使用configure 依据Makefile.in 来生成一个符合惯例的Makefile 。下面我们将详细介绍Makefile 的automake 生成 方法。
二、使用的环境
本文所提到的程序是基于Linux 发行版本:Fedora Core release 1 ,它包含了我们要用到的autoconf ,automake 。
三、从helloworld 入手
我们从大家最常使用的例子程序helloworld 开始。
下面的过程如果简单地说来就是:
新建三个文件:
helloworld.c
configure.in
Makefile.am
然后执行:
aclocal; autoconf; automake --add-missing; ./configure; make; ./helloworld
就可以看到Makefile 被产生出来,而且可以将helloworld.c 编译通过。
很简单吧,几条命令就可以做出一个符合惯例的Makefile ,感觉如何呀。
现在开始介绍详细的过程:
1 、建目录
在你的工作目录下建一个helloworld 目录,我们用它来存放helloworld 程序及相关文件,如在/home/my/build 下:
$ mkdir helloword
$ cd helloworld
2 、 helloworld.c
然后用你自己最喜欢的编辑器写一个hellowrold.c 文件,如命令:vi helloworld.c 。使用下面的代码作为helloworld.c 的内容。
int main(int argc, char** argv)
{
printf("Hello, Linux World!\n");
return 0;
}
完成后保存退出。
现在在helloworld 目录下就应该有一个你自己写的helloworld.c 了。
3 、生成configure
我们使用autoscan 命令来帮助我们根据目录下的源代码生成一个configure.in 的模板文件。
命令:
$ autoscan
$ ls
configure.scan helloworld.c
执行后在hellowrold 目录下会生成一个文件:configure.scan ,我们可以拿它作为configure.in 的蓝本。
现在将configure.scan 改名为configure.in ,并且编辑它,按下面的内容修改,去掉无关的语句:
============================configure.in 内容开始=========================================
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_INIT(helloworld.c)
AM_INIT_AUTOMAKE(helloworld, 1.0)
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_OUTPUT(Makefile)
============================configure.in 内容结束=========================================
然后执行命令aclocal 和autoconf ,分别会产生aclocal.m4 及configure 两个文件:
$ aclocal
$ls
aclocal.m4 configure.in helloworld.c
$ autoconf
$ ls
aclocal.m4 autom4te.cache configure configure.in helloworld.c
大家可以看到configure.in 内容是一些宏定义,这些宏经autoconf 处理后会变成检查系统特性、环境变量、软件必须的参数的shell 脚本。
autoconf 是用来生成自动配置软件源代码脚本(configure )的工具。configure 脚本能独立于autoconf 运行,且在运行的过程中,不需要用户的干预。
要生成configure 文件,你必须告诉autoconf 如何找到你所用的宏。方式是使用aclocal 程序来生成你的aclocal.m4 。
aclocal 根据configure.in 文件的内容,自动生成aclocal.m4 文件。aclocal 是一个perl 脚本程序,它的定义是:“aclocal - create aclocal.m4 by scanning configure.ac” 。
autoconf 从configure.in 这个列举编译软件时所需要各种参数的模板文件中创建configure 。
autoconf 需要GNU m4 宏处理器来处理aclocal.m4 ,生成configure 脚本。
m4 是一个宏处理器。将输入拷贝到输出,同时将宏展开。宏可以是内嵌的,也可以是用户定义的。除了可以展开宏,m4 还有一些内建的函数,用来引用文件,执行命令,整数运算,文本操作,循环等。m4 既可以作为编译器的前端,也可以单独作为一个宏处理器。
4 、新建Makefile.am
新建Makefile.am 文件,命令:
$ vi Makefile.am
内容如下:
AUTOMAKE_OPTIONS=foreign
bin_PROGRAMS=helloworld
helloworld_SOURCES=helloworld.c
automake 会根据你写的Makefile.am 来自动生成Makefile.in 。
Makefile.am 中定义的宏和目标, 会指导automake 生成指定的代码。例如,宏bin_PROGRAMS 将导致编译和连接的目标被生成。
5 、运行automake
命令:
$ automake --add-missing
configure.in: installing `./install-sh'
configure.in: installing `./mkinstalldirs'
configure.in: installing `./missing'
Makefile.am: installing `./depcomp'
automake 会根据Makefile.am 文件产生一些文件,包含最重要的Makefile.in 。
6 、执行configure 生成Makefile
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking for gcc... gcc
checking for C compiler default output... a.out
checking whether the C compiler works... yes
checking whether we are cross compiling... no
checking for suffix of executables...
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ANSI C... none needed
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
configure: creating ./config.status
config.status: creating Makefile
config.status: executing depfiles commands
$ ls -l Makefile
-rw-rw-r-- 1 yutao yutao 15035 Oct 15 10:40 Makefile
你可以看到,此时Makefile 已经产生出来了。
7 、使用Makefile 编译代码
$ make
if gcc -DPACKAGE_NAME="" -DPACKAGE_TARNAME="" -DPACKAGE_VERSION="" -
DPACKAGE_STRING="" -DPACKAGE_BUGREPORT="" -DPACKAGE="helloworld" -DVERSION="1.0"
-I. -I. -g -O2 -MT helloworld.o -MD -MP -MF ".deps/helloworld.Tpo" \
-c -o helloworld.o `test -f 'helloworld.c' || echo './'`helloworld.c; \
then mv -f ".deps/helloworld.Tpo" ".deps/helloworld.Po"; \
else rm -f ".deps/helloworld.Tpo"; exit 1; \
fi
gcc -g -O2 -o helloworld helloworld.o
运行helloworld
$ ./helloworld
Hello, Linux World!
这样helloworld 就编译出来了,你如果按上面的步骤来做的话,应该也会很容易地编译出正确的helloworld 文件。你还可以试着使用一些其 他的make 命令,如make clean ,make install ,make dist ,看看它们会给你什么样的效果。感觉如何?自己也能写出这么专业的Makefile ,老板一定会对你刮目相看。
四、深入浅出
针对上面提到的各个命令,我们再做些详细的介绍。
1 、 autoscan
autoscan 是用来扫描源代码目录生成configure.scan 文件的。autoscan 可以用目录名做为参数,但如果你不使用参数的话,那么 autoscan 将认为使用的是当前目录。autoscan 将扫描你所指定目录中的源文件,并创建configure.scan 文件。
2 、 configure.scan
configure.scan 包含了系统配置的基本选项,里面都是一些宏定义。我们需要将它改名为configure.in
3 、 aclocal
aclocal 是一个perl 脚本程序。aclocal 根据configure.in 文件的内容,自动生成aclocal.m4 文件。aclocal 的定义是:“aclocal - create aclocal.m4 by scanning configure.ac” 。
4 、 autoconf
autoconf 是用来产生configure 文件的。configure 是一个脚本,它能设置源程序来适应各种不同的操作系统平台,并且根据不同的系统来产生合适的Makefile ,从而可以使你的源代码能在不同的操作系统平台上被编译出来。
configure.in 文件的内容是一些宏,这些宏经过autoconf 处理后会变成检查系统特性、环境变量、软件必须的参数的shell 脚本。configure.in 文件中的宏的顺序并没有规定,但是你必须在所有宏的最前面和最后面分别加上AC_INIT 宏和AC_OUTPUT 宏。
在configure.ini 中:
# 号表示注释,这个宏后面的内容将被忽略。
AC_INIT(FILE)
这个宏用来检查源代码所在的路径。
AM_INIT_AUTOMAKE(PACKAGE, VERSION)
这个宏是必须的,它描述了我们将要生成的软件包的名字及其版本号:PACKAGE 是软件包的名字,VERSION 是版本号。当你使用make dist 命令时,它会给你生成一个类似helloworld-1.0.tar.gz 的软件发行包,其中就有对应的软件包的名字和版本号。
AC_PROG_CC
这个宏将检查系统所用的C 编译器。
AC_OUTPUT(FILE)
这个宏是我们要输出的Makefile 的名字。
我们在使用automake 时,实际上还需要用到其他的一些宏,但我们可以用aclocal 来帮我们自动产生。执行aclocal 后我们会得到aclocal.m4 文件。
产生了configure.in 和aclocal.m4 两个宏文件后,我们就可以使用autoconf 来产生configure 文件了。
5 、 Makefile.am
Makefile.am 是用来生成Makefile.in 的,需要你手工书写。Makefile.am 中定义了一些内容:
AUTOMAKE_OPTIONS
这个是automake 的选项。在执行automake 时,它会检查目录下是否存在标准GNU 软件包中应具备的各种文件,例如AUTHORS 、ChangeLog 、NEWS 等文件。我们将其设置成foreign 时,automake 会改用一般软件包的标准来检查。
bin_PROGRAMS
这个是指定我们所要产生的可执行文件的文件名。如果你要产生多个可执行文件,那么在各个名字间用空格隔开。
helloworld_SOURCES
这个是指定产生“helloworld” 时所需要的源代码。如果它用到了多个源文件,那么请使用空格符号将它们隔开。比如需要 helloworld.h ,helloworld.c 那么请写成helloworld_SOURCES= helloworld.h helloworld.c 。
如果你在bin_PROGRAMS 定义了多个可执行文件,则对应每个可执行文件都要定义相对的filename_SOURCES 。
6 、 automake
我们使用automake --add-missing 来产生Makefile.in 。
选项--add-missing 的定义是“add missing standard files to package” ,它会让automake 加入一个标准的软件包所必须的一些文件。
我们用automake 产生出来的Makefile.in 文件是符合GNU Makefile 惯例的,接下来我们只要执行configure 这个shell 脚本就可以产生合适的 Makefile文件了。
7 、 Makefile
在符合GNU Makefiel 惯例的Makefile 中,包含了一些基本的预先定义的操作:
make
根据Makefile 编译源代码,连接,生成目标文件,可执行文件。
make clean
清除上次的make 命令所产生的object 文件(后缀为“.o” 的文件)及可执行文件。
make install
将编译成功的可执行文件安装到系统目录中,一般为/usr/local/bin 目录。
make dist
产生发布软件包文件(即distribution package )。这个命令将会将可执行文件及相关文件打包成一个tar.gz 压缩的文件用来作为发布软件的软件包。
它会在当前目录下生成一个名字类似“PACKAGE-VERSION.tar.gz” 的文件。PACKAGE 和VERSION ,是我们在configure.in 中定义的AM_INIT_AUTOMAKE(PACKAGE, VERSION) 。
make distcheck
生成发布软件包并对其进行测试检查,以确定发布包的正确性。这个操作将自动把压缩包文件解开,然后执行configure 命令,并且执行make ,来确认编译不出现错误,最后提示你软件包已经准备好,可以发布了。
===============================================
helloworld-1.0.tar.gz is ready for distribution
===============================================
make distclean
类似make clean ,但同时也将configure 生成的文件全部删除掉,包括Makefile 。
五、结束语
通过上面的介绍,你应该可以很容易地生成一个你自己的符合GNU 惯例的Makefile 文件及对应的项目文件。
如果你想写出更复杂的且符合惯例的Makefile ,你可以参考一些开放代码的项目中的configure.in 和Makefile.am 文件,比如:嵌入式数据库sqlite ,单元测试cppunit 。
http://jianlee.ylinux.org/Computer/C/makefile-am.html#sec4
一般格式 全局变量 automake 安装路径 示例
一般格式
文件类型 |
书写格式 |
可执行文件 |
bin_PROGRAMES = foo |
foo_SOURCES = xxxx.c |
foo_LDADD = |
foo_LDFLAGS = |
foo_DEPENDENCIES = |
静态库 |
lib_LIBRARIES = libfoo.a |
foo_a_SOURCES = |
foo_a_LDADD = |
foo_a_LIBADD = |
foo_a_LDFALGS = |
头文件 |
include_HEADERS = foo.h |
数据文件 |
data_DATA = data1 data2 |
对于可执行文件和静态库类型,如果只想编译,不想安装到系统中,可以用 noinst_PROGRAMS代替bin_PROGRAMS,noinst_LIBRARIES代替lib_LIBRARIES。
全局变量
Makefile.am还提供了一些全局变量供所有的目标体使用 :
变量 |
含义 |
INCLUDES |
比如链接时所需要的头文件 |
LDADD |
比如链接时所需要的库文件 |
LDFLAGS |
比如链接时所需要的库文件选项标志 |
EXTRA_DIST |
源程序和一些默认的文件将自动打入 .tar.gz 包,其他文件若要进入 .tar.gz 包可以使用这种方法,如配置文件,数据文件等。 |
SUBDIRS |
处理本目录前要递归处理哪些子目录 |
automake 安装路径
automake设置了默认的安装路径:
标准安装路径默认安装路径为 : $(prefix) = /usr/local 可以通过 ./configure --prefix=<new_path> 的方法来覆盖。 其它的预定义目录还包括 : bindir = $(prefix)/bin, libdir = $(prefix)/lib, datadir = $(prefix)/share, sysconfdir = $(prefix)/etc 等等。定义一个新的安装路径
比如test, 可定义
testdir = $(prefix)/test,
然后 test_DATA =test1 test2,则 test1,test2 会作为数据文件安装到 $(prefix)/test 目录下。
示例
我们首先需要在工程顶层目录下创建一个 Makefile.am 来指明包 含的子目录:
SUBDIRS=src/lib src/ModuleA/apple/shell src/ModuleA/apple/core CURRENTPATH=$(shell /bin/pwd) INCLUDES=-I$(CURRENTPATH)/src/include -I$(CURRENTPATH)/src/ModuleA/apple/include export INCLUDES
由于每个源文件都会用到相同的头文件,所以我们在最顶层的Makefile.am中包含 了编译源文件时所用到的头文件,并导出。
我们将 lib 目录下的 swap.c 文件编译成 libswap.a 文件,被 apple/shell/apple.c 文件调用,那么lib目录下的 Makefile.am 如下所示:
noinst_LIBRARIES=libswap.a libswap_a_SOURCES=swap.c INCLUDES=-I$(top_srcdir)/src/includ
这里使用 noinst_LIBRARIES , 是因为如果只想编译,而不想安装到系统中,就 用 noinst_LIBRARIES 代替 bin_LIBRARIES ,对于可执行文件就用 noinst_PROGRAMS 代替 bin_PROGRAMS 。对于安装的情况,库将会安装到 $(prefix)/lib 目录下,可执行文件将会安装到 ${prefix}/bin 。如果想安装该 库,则 Makefile.am 示例如下:
bin_LIBRARIES=libswap.a libswap_a_SOURCES=swap.c INCLUDES=-I$(top_srcdir)/src/include swapincludedir=$(includedir)/swap swapinclude_HEADERS=$(top_srcdir)/src/include/swap.h
最后两行的意思是将 swap.h 安装到 ${prefix}/include/swap 目录下。
接下来,对于可执行文件类型的情况,我们将讨论如何写 Makefile.am . 对于编 译apple/core目录下的文件,我们写成的Makefile.am如下所示:
noinst_PROGRAMS=test test_SOURCES=test.c test_LDADD=$(top_srcdir)/src/ModuleA/apple/shell/apple.o $(top_srcdir)/src/lib/libswap.a test_LDFLAGS=-D_GNU_SOURCE DEFS+=-D_GNU_SOURCE #LIBS=-lpthread
由于我们的 test.c 文件在链接时,需要 apple.o 和 libswap.a 文件,所以我 们需要在 test_LDADD 中包含这两个文件。对于 Linux 下的信号量/读写锁文件 进行编译,需要在编译选项中指明 -D_GNU_SOURCE 。所以在 test_LDFLAGS 中指 明。而 test_LDFLAGS 只是链接时的选项,编译时同样需要指明该选项,所以需 要 DEFS 来指明编译选项,由于 DEFS 已经有初始值,所以这里用 += 的形式指 明。从这里可以看出,Makefile.am 中的语法与 Makefile 的语法一致,也可以 采用条件表达式。如果你的程序还包含其他的库,除了用 AC_CHECK_LIB 宏来指 明外,还可以用LIBS来指明。
如果你只想编译某一个文件,那么 Makefile.am 如何写呢?这个文件也很简单, 写法跟可执行文件的差不多,如下例所示:
noinst_PROGRAMS=apple apple_SOURCES=apple.c DEFS+=-D_GNU_SOURCE
我们这里只是欺骗 automake ,假装要生成apple文件,让它为我们生成依赖关系 和执行命令。所以当你运行完 automake 命令后,然后修改 apple/shell/ 下的 Makefile.in 文件,直接将LINK语句删除,即:
clean-noinstPROGRAMS: -test -z "$(noinst_PROGRAMS)" || rm -f $(noinst_PROGRAMS) apple$(EXEEXT): $(apple_OBJECTS) $(apple_DEPENDENCIES) @rm -f apple$(EXEEXT) #$(LINK) $(apple_LDFLAGS) $(apple_OBJECTS) $(apple_LDADD) $(LIBS)
http://www.ibm.com/developerworks/cn/aix/library/au-unix-getopt.ht
命令行 getopt
在编写新程序时,首先遇到的障碍之一就是如何处理控制其行为的命令行参数。这包括从命令行传递给您程序的 main() 函数的一个整数计数(通常名为 argc)和一个指向字符串的指针数组(通常名为 argv).可以采用两种实质一样的方式声明标注 main() 函数,如清单 1 中所示。
清单 1. 声明 main() 函数的两种方式
int main( int argc, char *argv[] ); int main( int argc, char **argv ); |
第一种方式使用的是指向 char 指针数组,现在似乎很流行这种方式,比第二种方式(其指针指向多个指向 char 的指针)略微清楚一些。由于某些原因,我使用第二种方式的时间更多一些,这可能源于我在高中时艰难学习 C 指针的经历。对于所有的用途和目的,这两种方法都是一样的,因此可以使用其中您自己最喜欢的方式。
当 C 运行时库的程序启动代码调用您的 main() 时,已经对命令行进行了处理。argc 参数包含参数的计数值,而 argv 包含指向这些参数的指针数组。对于 C 运行时库,arguments 是程序的名称,程序名后的任何内容都应该使用空格加以分隔。
例如,如果使用参数 -v bar www.ibm.com 运行一个名为 foo 程序,您的 argc 将设置为 4,argv 的设置情况将如清单 2 中所示。
清单 2. argv 的内容
argv[0] - foo argv[1] - -v argv[2] - bar argv[3] - www.ibm.com |
一个程序仅有一组命令行参数,因此我要将此信息存储在记录选项和设置的全局结构中。对程序有意义的要跟踪的任何内容都可以记录到此结构中,我将使用结构来帮助减少全局变量的数量。正如我在网络服务设计文章(请参阅参考资料)所提到的,全局变量非常不适合用于线程化编程中,因此要谨慎使用。
示例代码将演示一个假想的 doc2html 程序的命令行处理。该 doc2html 程序将某种类型的文档转换为 HTML,具体由用户指定的命令行选项控制。它支持以下选项:
- -I——不创建关键字索引。
- -l lang——转换为使用语言代码 lang 指定的语言。
- -o outfile.html——将经过转换的文档写入到 outfile.html,而不是打印到标准输出。
- -v——进行转换时提供详细信息;可以多次指定,以提高诊断级别。
- 将使用其他文件名称来作为输入文档。
您还将支持 -h 和 -?,以打印帮助消息来提示各个选项的用途。
简单命令行处理: getopt()
getopt() 函数位于 unistd.h 系统头文件中,其原型如清单 3 中所示:
清单 3. getopt() 原型
int getopt( int argc, char *const argv[], const char *optstring ); |
给定了命令参数的数量 (argc)、指向这些参数的数组 (argv) 和选项字符串 (optstring) 后,getopt() 将返回第一个选项,并设置一些全局变量。使用相同的参数再次调用该函数时,它将返回下一个选项,并设置相应的全局变量。如果不再有识别到的选项,将返回 -1,此任务就完成了。
getopt() 所设置的全局变量包括:
- optarg——指向当前选项参数(如果有)的指针。
- optind——再次调用 getopt() 时的下一个 argv 指针的索引。
- optopt——最后一个已知选项。
对于每个选项,选项字符串 (optstring) 中都包含一个对应的字符。具有参数的选项(如示例中的 -l 和 -o 选项)后面跟有一个 :字符。示例所使用的 optstring 为 Il:o:vh?(前面提到,还要支持最后两个用于打印程序的使用方法消息的选项)。
可以重复调用 getopt(),直到其返回 -1 为止;任何剩下的命令行参数通常视为文件名或程序相应的其他内容。
getopt() 的使用
让我们对 getopt_demo 项目的代码进行一下深入分析;为了方便起见,我在此处将此代码拆分为多个部分,但您可以在可下载源代码部分获得完整的代码(请参见下载)。
在清单 4 中,可以看到系统演示程序所使用的系统头文件;标准 stdio.h 提供标准 I/O 函数原型,stdlib.h 提供 EXIT_SUCCESS和EXIT_FAILURE,unistd.h 提供 getopt()。
清单 4. 系统头文件
#include <stdio.h> #include <stdlib.h> #include <unistd.h> |
清单 5 显示了我所创建的 globalArgs 结构,用于以合理的方式存储命令行选项。由于这是个全局变量,程序中任何位置的代码都可以访问这些变量,以确定是否创建关键字索引、生成何种语言等等事项。最好让 main() 函数外的代码将此结构视为一个常量、只读存储区,因为程序的任何部分都可以依赖于其内容。
每个命令行选择都有一个对应的选项,而其他变量用于存储输出文件名、指向输入文件列表的指针和输入文件数量。
清单 5. 全局参数存储和选项字符串
struct globalArgs_t { int noIndex; /* -I option */ char *langCode; /* -l option */ const char *outFileName; /* -o option */ FILE *outFile; int verbosity; /* -v option */ char **inputFiles; /* input files */ int numInputFiles; /* # of input files */ } globalArgs; static const char *optString = "Il:o:vh?"; |
选项字符串 optString 告知 getopt() 可以处理哪个选项以及哪个选项需要参数。如果在处期间遇到了其他选项,getopt() 将显示一个错误消息,程序将在显示了使用方法消息后退出。
下面的清单 6 包含一些从 main() 引用的用法消息函数和文档转换函数的小存根。可以对这些存根进行自由更改,以用于更为有用的目的。
清单 6. 存根
void display_usage( void ) { puts( "doc2html - convert documents to HTML" ); /* ... */ exit( EXIT_FAILURE ); } void convert_document( void ) { /* ... */ } |
最后,如清单 7 中所示,在 main() 函数中使用此结构。和优秀的开发人员一样,您需要首先初始化 globalArgs 结构,然后才开始处理命令行参数。在您的程序中,可以借此设置在一定情况下合理的缺省值,以便在以后有更合适的缺省值时更方便地对其进行调整。
清单 7. 初始化
int main( int argc, char *argv[] ) { int opt = 0; /* Initialize globalArgs before we get to work. */ globalArgs.noIndex = 0; /* false */ globalArgs.langCode = NULL; globalArgs.outFileName = NULL; globalArgs.outFile = NULL; globalArgs.verbosity = 0; globalArgs.inputFiles = NULL; globalArgs.numInputFiles = 0; |
清单 8 中的 while 循环和 switch 语句是用于本程序的命令行处理的代码部分。只要 getopt() 发现选项,switch 语句将确定找到的是哪个选项,将能在 globalArgs 结构中看到具体情况。当 getopt() 最终返回 -1 时,就完成了选项处理过程,剩下的都是您的输入文件了。
清单 8. 使用 getopt() 处理 argc/argv
opt = getopt( argc, argv, optString ); while( opt != -1 ) { switch( opt ) { case 'I': globalArgs.noIndex = 1; /* true */ break; case 'l': globalArgs.langCode = optarg; break; case 'o': globalArgs.outFileName = optarg; break; case 'v': globalArgs.verbosity++; break; case 'h': /* fall-through is intentional */ case '?': display_usage(); break; default: /* You won't actually get here. */ break; } opt = getopt( argc, argv, optString ); } globalArgs.inputFiles = argv + optind; globalArgs.numInputFiles = argc - optind; |
既然已经完成了参数和选项的收集工作,接下来就可以执行程序所设计的任何功能(在本例中是进行文档转换),然后退出(清单 9)。
清单 9. 开始工作
convert_document(); return EXIT_SUCCESS; } |
好,工作完成,非常漂亮。现在就可以不再往下读了。不过,如果您希望程序符合 90 年代末期的标准并支持 GNU 应用程序中流行的长 选项,则请继续关注下面的内容。
复杂命令行处理: getopt_long()
在 20 世纪 90 年代(如果没有记错的话),UNIX 应用程序开始支持长选项,即一对短横线(而不是普通短 选项所使用的单个短横线)、一个描述性选项名称还可以包含一个使用等号连接到选项的参数。
幸运的是,可以通过使用 getopt_long() 向程序添加长选项支持。您可能已经猜到了,getopt_long() 是同时支持长选项和短选项的 getopt() 版本。
getopt_long() 函数还接受其他参数,其中一个是指向 struct option 对象数组的指针。此结构相当直接,如清单 10 中所示。
清单 10. getopt_long() 的选项
struct option { char *name; int has_arg; int *flag; int val; }; |
name 成员是指向长选项名称(带两个短横线)的指针。has_arg 成员设置为 no_argument、optional_argument, 或required_argument(均在 getopt.h 中定义)之一,以指示选项是否具有参数。如果 flag 成员未设置为 NULL,在处理期间遇到此选项时,会使用 val 成员的值填充它所指向的 int 值。如果 flag 成员为 NULL,在 getopt_long() 遇到此选项时,将返回 val中的值;通过将 val 设置为选项的 short 参数,可以在不添加任何其他代码的情况下使用 getopt_long()——处理 while loop和 switch 的现有 getopt() 将自动处理此选项。
这已经变得更为灵活了,因为各个选项现在可以具有可选参数了。更重要的是,仅需要进行很少的工作,就可以方便地放入现有代码中。
让我们看看如何使用 getopt_long() 来对示例程序进行更改(getopt_long_demo 项目可从下载部分获得)。
使用 getopt_long()
由于 getopt_long_demo 几乎与刚刚讨论的 getopt_demo 代码一样,因此我将仅对更改的代码进行说明。由于现在已经有了更大的灵活性,因此还将添加对 --randomize 选项(没有对应的短选项)的支持。
getopt_long() 函数在 getopt.h 头文件(而非 unistd.h)中,因此将需要将该头文件包含进来(请参见清单 11)。我还包含了string.h,因为将稍后使用 strcmp() 来帮助确定处理的是哪个长参数。
清单 11. 其他头文件
#include <getopt.h> #include <string.h> |
您已经为 --randomize 选项在 globalArgs 中添加了一个标志(请参见清单 12),并创建了 longOpts 数组来存储关于此程序支持的长选项的信息。除了 --randomize 外,所有的参数都与现有短选项对应(例如,--no-index 等同于 -I)。通过在选项结构中包含其短选项等效项,可以在不向程序添加任何其他代码的情况下处理等效的长选项。
清单 12. 扩展后的参数
struct globalArgs_t { int noIndex; /* -I option */ char *langCode; /* -l option */ const char *outFileName; /* -o option */ FILE *outFile; int verbosity; /* -v option */ char **inputFiles; /* input files */ int numInputFiles; /* # of input files */ int randomized; /* --randomize option */ } globalArgs; static const char *optString = "Il:o:vh?"; static const struct option longOpts[] = { { "no-index", no_argument, NULL, 'I' }, { "language", required_argument, NULL, 'l' }, { "output", required_argument, NULL, 'o' }, { "verbose", no_argument, NULL, 'v' }, { "randomize", no_argument, NULL, 0 }, { "help", no_argument, NULL, 'h' }, { NULL, no_argument, NULL, 0 } }; |
清单 13 将 getop() 调用更改为了 getopt_long(),除了 getopt() 的参数外,它还接受 longOpts 数组和 int 指针 (longIndex)。当 getopt_long() 返回 0 时,longIndex 所指向的整数将设置为当前找到的长选项的索引。
清单 13. 新的经改进的选项处理
opt = getopt_long( argc, argv, optString, longOpts, &longIndex ); while( opt != -1 ) { switch( opt ) { case 'I': globalArgs.noIndex = 1; /* true */ break; case 'l': globalArgs.langCode = optarg; break; case 'o': globalArgs.outFileName = optarg; break; case 'v': globalArgs.verbosity++; break; case 'h': /* fall-through is intentional */ case '?': display_usage(); break; case 0: /* long option without a short arg */ if( strcmp( "randomize", longOpts[longIndex].name ) == 0 ) { globalArgs.randomized = 1; } break; default: /* You won't actually get here. */ break; } opt = getopt_long( argc, argv, optString, longOpts, amp;longIndex ); } |
我还添加了 0 的 case,以便处理任何不与现有短选项匹配的长选项。在此例中,只有一个长选项,但代码仍然使用 strcmp() 来确保它是预期的那个选项。
这样就全部搞定了;程序现在支持更为详细(对临时用户更加友好)的长选项。
总结
UNIX 用户始终依赖于命令行参数来修改程序的行为,特别是那些设计作为小工具集合 (UNIX 外壳环境)的一部分使用的实用工具更是如此。程序需要能够快速处理各个选项和参数,且要求不会浪费开发人员的太多时间。毕竟,几乎没有程序设计为仅处理命令行参数,开发人员更应该将精力放在程序所实际进行的工作上。
getopt() 函数是一个标准库调用,可允许您使用直接的 while/switch 语句方便地逐个处理命令行参数和检测选项(带或不带附加的参数)。与其类似的 getopt_long() 允许在几乎不进行额外工作的情况下处理更具描述性的长选项,这非常受开发人员的欢迎。
既然已经知道了如何方便地处理命令行选项,现在就可以集中精力改进您的程序的命令行,可以添加长选项支持,或添加之前由于不想向程序添加额外的命令行选项处理而搁置的任何其他选项。
不要忘记在某处记录您所有的选项和参数,并提供某种类型的内置帮助函数来为健忘的用户提供帮助。
http://blog.csdn.net/zi_jin/article/details/4214359
linux中select()函数分析
Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。下面详细介绍一下!
Select的函数格式(我所说的是Unix系统下的伯克利socket编程,和windows下的有区别,一会儿说明):
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
先说明两个结构体:
第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。
第二,struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。
具体解释select的参数:
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
返回值:
负值:select错误 正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件
在有了select后可以写出像样的网络程序来!举个简单的例子,就是从网络上接受数据写入一个文件中。
例子:
main()
{
int sock;
FILE *fp;
struct fd_set fds;
struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
char buffer[256]={0}; //256字节的接收缓冲区
/* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
sock=socket(...);
bind(...);
fp=fopen(...); */
while(1)
{
FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,&fds); //添加描述符
FD_SET(fp,&fds); //同上
maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1
switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
{
case -1: exit(-1);break; //select错误,退出程序
case 0:break; //再次轮询
default:
if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
{
recvfrom(sock,buffer,256,.....);//接受网络数据
if(FD_ISSET(fp,&fds)) //测试文件是否可写
fwrite(fp,buffer...);//写入文件
buffer清空;
}// end if break;
}// end switch
}//end while
}//end main
参考资料:http://cuijinbird.blogchina.com/cuijinbird/1921117.html
Part 2:
select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,
每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他
文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,
当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执
行了select()的进程哪一Socket或文件可读,下面具体解释:
#include <sys/types.h>
#include <sys/times.h>
#include <sys/select.h>
int select(nfds, readfds, writefds, exceptfds, timeout)
int nfds;
fd_set *readfds, *writefds, *exceptfds;
struct timeval *timeout;
ndfs:select监视的文件句柄数,视进程中打开的文件数而定,一般设为呢要监视各文件
中的最大文件号加一。
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。(见/usr/sys/select.h,
可精确至百万分之一秒!)
当readfds或writefds中映象的文件可读或可写或超时,本次select()
就结束返回。程序员利用一组系统提供的宏在select()结束时便可判
断哪一文件可读或可写。对Socket编程特别有用的就是readfds。
几只相关的宏解释如下:
FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset):检查fdset联系的文件句柄fd是否
可读写,>0表示可读写。
(关于fd_set及相关宏的定义见/usr/include/sys/types.h)
这样,你的socket只需在有东东读的时候才读入,大致如下:
...
int sockfd;
fd_set fdR;
struct timeval timeout = ..;
...
for(;;) {
FD_ZERO(&fdR);
FD_SET(sockfd, &fdR);
switch (select(sockfd + 1, &fdR, NULL, &timeout)) {
case -1:
error handled by u;
case 0:
timeout hanled by u;
default:
if (FD_ISSET(sockfd)) {
now u read or recv something;
/* if sockfd is father and
server socket, u can now
accept() */
}
}
}
所以一个FD_ISSET(sockfd)就相当通知了sockfd可读。
至于struct timeval在此的功能,请man select。不同的timeval设置
使使select()表现出超时结束、无超时阻塞和轮询三种特性。由于
timeval可精确至百万分之一秒,所以Windows的SetTimer()根本不算
什么。你可以用select()做一个超级时钟。
FD_ACCEPT的实现?依然如上,因为客户方socket请求连接时,会发送
连接请求报文,此时select()当然会结束,FD_ISSET(sockfd)当然大
于零,因为有报文可读嘛!至于这方面的应用,主要在于服务方的父
Socket,你若不喜欢主动accept(),可改为如上机制来accept()。
至于FD_CLOSE的实现及处理,颇费了一堆cpu处理时间,未完待续。
--
讨论关于利用select()检测对方Socket关闭的问题:
仍然是本地Socket有东东可读,因为对方Socket关闭时,会发一个关闭连接
通知报文,会马上被select()检测到的。关于TCP的连接(三次握手)和关
闭(二次握手)机制,敬请参考有关TCP/IP的书籍。
不知是什么原因,UNIX好象没有提供通知进程关于Socket或Pipe对方关闭的
信号,也可能是cpu所知有限。总之,当对方关闭,一执行recv()或read(),
马上回返回-1,此时全局变量errno的值是115,相应的sys_errlist[errno]
为"Connect refused"(请参考/usr/include/sys/errno.h)。所以,在上
篇的for(;;)...select()程序块中,当有东西可读时,一定要检查recv()或
read()的返回值,返回-1时要作出关断本地Socket的处理,否则select()会
一直认为有东西读,其结果曾几令cpu伤心欲断针脚。不信你可以试试:不检
查recv()返回结果,且将收到的东东(实际没收到)写至标准输出...
在有名管道的编程中也有类似问题出现。具体处理详见拙作:发布一个有用
的Socket客户方原码。
至于主动写Socket时对方突然关闭的处理则可以简单地捕捉信号SIGPIPE并作
出相应关断本地Socket等等的处理。SIGPIPE的解释是:写入无读者方的管道。
在此不作赘述,请详man signal。
以上是cpu在作tcp/ip数据传输实验积累的经验,若有错漏,请狂炮击之。
唉,昨天在hacker区被一帮孙子轰得差点儿没短路。ren cpu(奔腾的心) z80
补充关于select在异步(非阻塞)connect中的应用,刚开始搞socket编程的时候
我一直都用阻塞式的connect,非阻塞connect的问题是由于当时搞proxy scan
而提出的呵呵
通过在网上与网友们的交流及查找相关FAQ,总算知道了怎么解决这一问题.同样
用select可以很好地解决这一问题.大致过程是这样的:
1.将打开的socket设为非阻塞的,可以用fcntl(socket, F_SETFL, O_NDELAY)完
成(有的系统用FNEDLAY也可).
2.发connect调用,这时返回-1,但是errno被设为EINPROGRESS,意即connect仍旧
在进行还没有完成.
3.将打开的socket设进被监视的可写(注意不是可读)文件集合用select进行监视,
如果可写,用 getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int));
来得到error的值,如果为零,则connect成功.
在许多unix版本的proxyscan程序你都可以看到类似的过程,另外在solaris精华
区->编程技巧中有一个通用的带超时参数的connect模块.