有時候我們需要在C程序中操作字符串裏的字符,比如求字符串"你好\n"中有幾個漢字或字符,用strlen就不靈了,因爲strlen只看結尾的0字節而不管字符串裏存的是什麼,求出來的是字節數7。爲了在程序中操作Unicode字符,C語言定義了寬字符(Wide Character)類型wchar_t和一些庫函數。在字符常量或字符串字面值前面加一個L就表示寬字符常量或寬字符串,例如定義wchar_t c = L'你';,變量c的值就是漢字“你”的31位UCS編碼,而L"你好\n"就相當於{L'你', L'好', L'\n', 0},wcslen函數就可以取寬字符串中的字符個數。
看下面的程序:
#include <stdio.h>
#include <locale.h>
int main(void)
{
if (!setlocale(LC_CTYPE, ""))
{
fprintf(stderr, "Can't set the specified locale! " "Check LANG, LC_CTYPE, LC_ALL.\n");
return 1;
}
printf("%ls", L"你好\n");
return 0;
}
寬字符串L"你好\n"在源代碼中當然還是存成UTF-8編碼的,但編譯器會把它變成4個UCS編碼0x00004f60 0x0000597d 0x0000000a 0x00000000保存在目標文件中,按小端存儲就是60 4f 00 00 7d 59 00 00 0a 00 00 00 00 00 00 00,用od命令查看目標文件應該能找到這些字節。
$ gcc hihao.c $ od -tx1 a.outprintf的%ls轉換說明表示把後面的參數按寬字符串解釋,不是見到0字節就結束,而是見到UCS編碼爲0的字符才結束,但是要write到終端仍然需要以多字節編碼輸出,這樣終端驅動程序才能識別,所以printf在內部把寬字符串轉換成多字節字符串再write出去。事實上,C標準並沒有規定多字節字符必須以UTF-8編碼,也可以使用其它的多字節編碼,在運行時根據環境變量確定當前系統的編碼,所以在程序開頭需要調用setlocale獲取當前系統的編碼設置,如果當前系統是UTF-8的,printf就把UCS編碼轉換成UTF-8編碼的多字節字符串再write出去。一般來說,程序在做內部計算時通常以寬字符編碼,如果要存盤或者輸出給別的程序,或者通過網絡發給別的程序,則採用多字節編碼。
target ... : prerequisites ... command1 command2 ...
例如:
main: main.o stack.o maze.o gcc main.o stack.o maze.o -o main
main是這條規則的目標(Target),main.o、stack.o和maze.o是這條規則的條件(Prerequisite)。目標和條件之間的關係是:欲更新目標,必須首先更新它的所有條件;所有條件中只要有一個條件被更新了,目標也必須隨之被更新。所謂“更新”就是執行一遍規則中的命令列表,命令列表中的每條命令必須以一個Tab開頭,注意不能是空格,Makefile的格式不像C語言的縮進那麼隨意,對於Makefile中的每個以Tab開頭的命令,make會創建一個Shell進程去執行它。
[33] 只要符合本章所描述的語法的文件我們都叫它Makefile,而它的文件名則不一定是Makefile。事實上,執行make命令時,是按照GNUmakefile、makefile、Makefile的順序找到第一個存在的文件並執行它,不過還是建議使用Makefile做文件名。除了GNU make,有些UNIX系統的make命令不是GNU make,不會查找GNUmakefile這個文件名,如果你寫的Makefile包含GNU make的特殊語法,可以起名爲GNUmakefile,否則不建議用這個文件名
clean目標是一個約定俗成的名字,在所有軟件項目的Makefile中都表示清除編譯生成的文件,類似這樣的約定俗成的目標名字有:
-
all,執行主要的編譯工作,通常用作缺省目標。
-
install,執行編譯後的安裝工作,把可執行文件、配置文件、文檔等分別拷到不同的安裝目錄。
-
clean,刪除編譯生成的二進制文件。
特殊變量$@和$<,這兩個變量的特點是不需要給它們賦值,在不同的上下文中它們自動取不同的值。常用的特殊變量有:
-
$@,表示規則中的目標。
-
$<,表示規則中的第一個條件。
-
$?,表示規則中所有比目標新的條件,組成一個列表,以空格分隔。
- $^,表示規則中的所有條件,組成一個列表,以空格分隔。
make的隱含規則數據庫中用到了很多變量,有些變量沒有定義(例如CFLAGS),有些變量定義了缺省值(例如CC),我們寫Makefile時可以重新定義這些變量的值,也可以在缺省值的基礎上追加。以下列舉一些常用的變量,請讀者體會其中的規律。
靜態庫打包命令的名字,缺省值是ar。
ARFLAGS靜態庫打包命令的選項,缺省值是rv。
AS彙編器的名字,缺省值是as。
ASFLAGS彙編器的選項,沒有定義。
CCC編譯器的名字,缺省值是cc。
CFLAGSC編譯器的選項,沒有定義。
CXXC++編譯器的名字,缺省值是g++。
CXXFLAGSC++編譯器的選項,沒有定義。
CPPC預處理器的名字,缺省值是$(CC) -E。
CPPFLAGSC預處理器的選項,沒有定義。
LD鏈接器的名字,缺省值是ld。
LDFLAGS鏈接器的選項,沒有定義。
TARGET_ARCH和目標平臺相關的命令行選項,沒有定義。
OUTPUT_OPTION輸出的命令行選項,缺省值是-o $@。
LINK.o把.o文件鏈接在一起的命令行,缺省值是$(CC) $(LDFLAGS) $(TARGET_ARCH)。
LINK.c把.c文件鏈接在一起的命令行,缺省值是$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)。
LINK.cc把.cc文件(C++源文件)鏈接在一起的命令行,缺省值是$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)。
COMPILE.c編譯.c文件的命令行,缺省值是$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c。
COMPILE.cc編譯.cc文件的命令行,缺省值是$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c。
RM刪除命令的名字,缺省值是rm -f。
現在我們的Makefile寫成這樣:
all: main main: main.o stack.o maze.o gcc $^ -o $@ main.o: main.h stack.h maze.h stack.o: stack.h main.h maze.o: maze.h main.h clean: -rm main *.o .PHONY: clean按照慣例,用all做缺省目標。現在還有一點比較麻煩,在寫main.o、stack.o和maze.o這三個目標的規則時要查看源代碼,找出它們依賴於哪些頭文件,這很容易出錯,一是因爲有的頭文件包含在另一個頭文件中,在寫規則時很容易遺漏,二是如果以後修改源代碼改變了依賴關係,很可能忘記修改Makefile的規則。爲了解決這個問題,可以用gcc的-M選項自動生成目標文件和源文件的依賴關係:
$ gcc -M main.c main.o: main.c /usr/include/stdio.h /usr/include/features.h \ /usr/include/sys/cdefs.h /usr/include/bits/wordsize.h \ /usr/include/gnu/stubs.h /usr/include/gnu/stubs-32.h \ /usr/lib/gcc/i486-linux-gnu/4.3.2/include/stddef.h \ /usr/include/bits/types.h /usr/include/bits/typesizes.h \ /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \ /usr/lib/gcc/i486-linux-gnu/4.3.2/include/stdarg.h \ /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \ stack.h maze.h-M選項把stdio.h以及它所包含的系統頭文件也找出來了,如果我們不需要輸出系統頭文件的依賴關係,可以用-MM選項:
$ gcc -MM *.c main.o: main.c main.h stack.h maze.h maze.o: maze.c maze.h main.h stack.o: stack.c stack.h main.h接下來的問題是怎麼把這些規則包含到Makefile中,GNU make的官方手冊建議這樣寫:
all: main main: main.o stack.o maze.o gcc $^ -o $@ clean: -rm main *.o .PHONY: clean sources = main.c stack.c maze.c include $(sources:.c=.d) %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$sources變量包含我們要編譯的所有.c文件,$(sources:.c=.d)是一個變量替換語法,把sources變量中每一項的.c替換成.d,所以include這一句相當於:
include main.d stack.d maze.d類似於C語言的#include指示,這裏的include表示包含三個文件main.d、stack.d和maze.d,這三個文件也應該符合Makefile的語法。如果現在你的工作目錄是乾淨的,只有.c文件、.h文件和Makefile,運行make的結果是:
$ make Makefile:13: main.d: No such file or directory Makefile:13: stack.d: No such file or directory Makefile:13: maze.d: No such file or directory set -e; rm -f maze.d; \ cc -MM maze.c > maze.d.$$; \ sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \ rm -f maze.d.$$ set -e; rm -f stack.d; \ cc -MM stack.c > stack.d.$$; \ sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \ rm -f stack.d.$$ set -e; rm -f main.d; \ cc -MM main.c > main.d.$$; \ sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \ rm -f main.d.$$ cc -c -o main.o main.c cc -c -o stack.o stack.c cc -c -o maze.o maze.c gcc main.o stack.o maze.o -o main一開始找不到.d文件,所以make會報警告。但是make會把include的文件名也當作目標來嘗試更新,而這些目標適用模式規則%.d: %c,所以執行它的命令列表,比如生成maze.d的命令:
set -e; rm -f maze.d; \ cc -MM maze.c > maze.d.$$; \ sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \ rm -f maze.d.$$注意,雖然在Makefile中這個命令寫了四行,但其實是一條命令,make只創建一個Shell進程執行這條命令,這條命令分爲5個子命令,用;號隔開,並且爲了美觀,用續行符\拆成四行來寫。執行步驟爲:
-
set -e命令設置當前Shell進程爲這樣的狀態:如果它執行的任何一條命令的退出狀態非零則立刻終止,不再執行後續命令。
-
把原來的maze.d刪掉。
-
重新生成maze.c的依賴關係,保存成文件maze.d.1234(假設當前Shell進程的id是1234)。注意,在Makefile中$有特殊含義,如果要表示它的字面意思則需要寫兩個$,所以Makefile中的四個$傳給Shell變成兩個$,兩個$在Shell中表示當前進程的id,一般用它給臨時文件起名,以保證文件名唯一。
-
這個sed命令比較複雜,就不細講了,主要作用是查找替換。maze.d.1234的內容應該是maze.o: maze.c maze.h main.h,經過sed處理之後存爲maze.d,其內容是maze.o maze.d: maze.c maze.h main.h。
-
最後把臨時文件maze.d.1234刪掉。
不管是Makefile本身還是被它包含的文件,只要有一個文件在make過程中被更新了,make就會重新讀取整個Makefile以及被它包含的所有文件,現在main.d、stack.d和maze.d都生成了,就可以正常包含進來了(假如這時還沒有生成,make就要報錯而不是報警告了),相當於在Makefile中添了三條規則:
main.o main.d: main.c main.h stack.h maze.h maze.o maze.d: maze.c maze.h main.h stack.o stack.d: stack.c stack.h main.h如果我在main.c中加了一行#include "foo.h",那麼:
1、main.c的修改日期變了,根據規則main.o main.d: main.c main.h stack.h maze.h要重新生成main.o和main.d。生成main.o的規則有兩條:
main.o: main.c main.h stack.h maze.h %.o: %.c # commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<第一條是把規則main.o main.d: main.c main.h stack.h maze.h拆開寫得到的,第二條是隱含規則,因此執行cc命令重新編譯main.o。生成main.d的規則也有兩條:
main.d: main.c main.h stack.h maze.h %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$因此main.d的內容被更新爲main.o main.d: main.c main.h stack.h maze.h foo.h。
2、由於main.d被Makefile包含,main.d被更新又導致make重新讀取整個Makefile,把新的main.d包含進來,於是新的依賴關係生效了。
-n選項只打印要執行的命令,而不會真的執行命令,這個選項有助於我們檢查Makefile寫得是否正確,由於Makefile不是順序執行的,用這個選項可以先看看命令的執行順序,確認無誤了再真正執行命令。
-C選項可以切換到另一個目錄執行那個目錄下的Makefile,比如先退到上一級目錄再執行我們的Makefile(假設我們的源代碼都放在testmake目錄下):
$ cd .. $ make -C testmake make: Entering directory `/home/akaedu/testmake' cc -c -o main.o main.c cc -c -o stack.o stack.c cc -c -o maze.o maze.c gcc main.o stack.o maze.o -o main make: Leaving directory `/home/akaedu/testmake'一些規模較大的項目會把不同的模塊或子系統的源代碼放在不同的子目錄中,然後在每個子目錄下都寫一個該目錄的Makefile,然後在一個總的Makefile中用make -C命令執行每個子目錄下的Makefile。例如Linux內核源代碼根目錄下有Makefile,子目錄fs、net等也有各自的Makefile,二級子目錄fs/ramfs、net/ipv4等也有各自的Makefile。
在make命令行也可以用=或:=定義變量,如果這次編譯我想加調試選項-g,但我不想每次編譯都加-g選項,可以在命令行定義CFLAGS變量,而不必修改Makefile編譯完了再改回來:
$ make CFLAGS=-g cc -g -c -o main.o main.c cc -g -c -o stack.o stack.c cc -g -c -o maze.o maze.c gcc main.o stack.o maze.o -o main如果在Makefile中也定義了CFLAGS變量,則命令行的值覆蓋Makefile中的值。
自動生成 Makefile 的全過程詳解! automake/autoconf 入門
作爲Linux 下的程序開發人員,大家一定都遇到過Makefile ,用make 命令來編譯自己寫的程序確實是很方便。一般情況下,大家都是手工寫一個簡單Makefile ,如果要想寫出一個符合自由軟件慣例的Makefile 就不那麼容易了。
命令: 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
Makefile.amby Jian Lee
一般格式
對於可執行文件和靜態庫類型,如果只想編譯,不想安裝到系統中,可以用 noinst_PROGRAMS代替bin_PROGRAMS,noinst_LIBRARIES代替lib_LIBRARIES。 全局變量Makefile.am還提供了一些全局變量供所有的目標體使用 :
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
在編寫新程序時,首先遇到的障礙之一就是如何處理控制其行爲的命令行參數。這包括從命令行傳遞給您程序的 main() 函數的一個整數計數(通常名爲 argc)和一個指向字符串的指針數組(通常名爲 argv).可以採用兩種實質一樣的方式聲明標註 main() 函數,如清單 1 中所示。 清單 1. 聲明 main() 函數的兩種方式
第一種方式使用的是指向 char 指針數組,現在似乎很流行這種方式,比第二種方式(其指針指向多個指向 char 的指針)略微清楚一些。由於某些原因,我使用第二種方式的時間更多一些,這可能源於我在高中時艱難學習 C 指針的經歷。對於所有的用途和目的,這兩種方法都是一樣的,因此可以使用其中您自己最喜歡的方式。 當 C 運行時庫的程序啓動代碼調用您的 main() 時,已經對命令行進行了處理。argc 參數包含參數的計數值,而 argv 包含指向這些參數的指針數組。對於 C 運行時庫,arguments 是程序的名稱,程序名後的任何內容都應該使用空格加以分隔。 例如,如果使用參數 -v bar www.ibm.com 運行一個名爲 foo 程序,您的 argc 將設置爲 4,argv 的設置情況將如清單 2 中所示。 清單 2. argv 的內容
一個程序僅有一組命令行參數,因此我要將此信息存儲在記錄選項和設置的全局結構中。對程序有意義的要跟蹤的任何內容都可以記錄到此結構中,我將使用結構來幫助減少全局變量的數量。正如我在網絡服務設計文章(請參閱參考資料)所提到的,全局變量非常不適合用於線程化編程中,因此要謹慎使用。 示例代碼將演示一個假想的 doc2html 程序的命令行處理。該 doc2html 程序將某種類型的文檔轉換爲 HTML,具體由用戶指定的命令行選項控制。它支持以下選項:
您還將支持 -h 和 -?,以打印幫助消息來提示各個選項的用途。 getopt() 函數位於 unistd.h 系統頭文件中,其原型如清單 3 中所示: 清單 3. getopt() 原型
給定了命令參數的數量 (argc)、指向這些參數的數組 (argv) 和選項字符串 (optstring) 後,getopt() 將返回第一個選項,並設置一些全局變量。使用相同的參數再次調用該函數時,它將返回下一個選項,並設置相應的全局變量。如果不再有識別到的選項,將返回 -1,此任務就完成了。 getopt() 所設置的全局變量包括:
對於每個選項,選項字符串 (optstring) 中都包含一個對應的字符。具有參數的選項(如示例中的 -l 和 -o 選項)後面跟有一個 :字符。示例所使用的 optstring 爲 Il:o:vh?(前面提到,還要支持最後兩個用於打印程序的使用方法消息的選項)。 可以重複調用 getopt(),直到其返回 -1 爲止;任何剩下的命令行參數通常視爲文件名或程序相應的其他內容。 讓我們對 getopt_demo 項目的代碼進行一下深入分析;爲了方便起見,我在此處將此代碼拆分爲多個部分,但您可以在可下載源代碼部分獲得完整的代碼(請參見下載)。 在清單 4 中,可以看到系統演示程序所使用的系統頭文件;標準 stdio.h 提供標準 I/O 函數原型,stdlib.h 提供 EXIT_SUCCESS和EXIT_FAILURE,unistd.h 提供 getopt()。 清單 4. 系統頭文件
清單 5 顯示了我所創建的 globalArgs 結構,用於以合理的方式存儲命令行選項。由於這是個全局變量,程序中任何位置的代碼都可以訪問這些變量,以確定是否創建關鍵字索引、生成何種語言等等事項。最好讓 main() 函數外的代碼將此結構視爲一個常量、只讀存儲區,因爲程序的任何部分都可以依賴於其內容。 每個命令行選擇都有一個對應的選項,而其他變量用於存儲輸出文件名、指向輸入文件列表的指針和輸入文件數量。 清單 5. 全局參數存儲和選項字符串
選項字符串 optString 告知 getopt() 可以處理哪個選項以及哪個選項需要參數。如果在處期間遇到了其他選項,getopt() 將顯示一個錯誤消息,程序將在顯示了使用方法消息後退出。 下面的清單 6 包含一些從 main() 引用的用法消息函數和文檔轉換函數的小存根。可以對這些存根進行自由更改,以用於更爲有用的目的。 清單 6. 存根
最後,如清單 7 中所示,在 main() 函數中使用此結構。和優秀的開發人員一樣,您需要首先初始化 globalArgs 結構,然後纔開始處理命令行參數。在您的程序中,可以藉此設置在一定情況下合理的缺省值,以便在以後有更合適的缺省值時更方便地對其進行調整。 清單 7. 初始化
清單 8 中的 while 循環和 switch 語句是用於本程序的命令行處理的代碼部分。只要 getopt() 發現選項,switch 語句將確定找到的是哪個選項,將能在 globalArgs 結構中看到具體情況。當 getopt() 最終返回 -1 時,就完成了選項處理過程,剩下的都是您的輸入文件了。 清單 8. 使用 getopt() 處理 argc/argv
既然已經完成了參數和選項的收集工作,接下來就可以執行程序所設計的任何功能(在本例中是進行文檔轉換),然後退出(清單 9)。 清單 9. 開始工作
好,工作完成,非常漂亮。現在就可以不再往下讀了。不過,如果您希望程序符合 90 年代末期的標準並支持 GNU 應用程序中流行的長 選項,則請繼續關注下面的內容。 在 20 世紀 90 年代(如果沒有記錯的話),UNIX 應用程序開始支持長選項,即一對短橫線(而不是普通短 選項所使用的單個短橫線)、一個描述性選項名稱還可以包含一個使用等號連接到選項的參數。 幸運的是,可以通過使用 getopt_long() 向程序添加長選項支持。您可能已經猜到了,getopt_long() 是同時支持長選項和短選項的 getopt() 版本。 getopt_long() 函數還接受其他參數,其中一個是指向 struct option 對象數組的指針。此結構相當直接,如清單 10 中所示。 清單 10. getopt_long() 的選項
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_demo 幾乎與剛剛討論的 getopt_demo 代碼一樣,因此我將僅對更改的代碼進行說明。由於現在已經有了更大的靈活性,因此還將添加對 --randomize 選項(沒有對應的短選項)的支持。 getopt_long() 函數在 getopt.h 頭文件(而非 unistd.h)中,因此將需要將該頭文件包含進來(請參見清單 11)。我還包含了string.h,因爲將稍後使用 strcmp() 來幫助確定處理的是哪個長參數。 清單 11. 其他頭文件
您已經爲 --randomize 選項在 globalArgs 中添加了一個標誌(請參見清單 12),並創建了 longOpts 數組來存儲關於此程序支持的長選項的信息。除了 --randomize 外,所有的參數都與現有短選項對應(例如,--no-index 等同於 -I)。通過在選項結構中包含其短選項等效項,可以在不向程序添加任何其他代碼的情況下處理等效的長選項。 清單 12. 擴展後的參數
清單 13 將 getop() 調用更改爲了 getopt_long(),除了 getopt() 的參數外,它還接受 longOpts 數組和 int 指針 (longIndex)。當 getopt_long() 返回 0 時,longIndex 所指向的整數將設置爲當前找到的長選項的索引。 清單 13. 新的經改進的選項處理
我還添加了 0 的 case,以便處理任何不與現有短選項匹配的長選項。在此例中,只有一個長選項,但代碼仍然使用 strcmp() 來確保它是預期的那個選項。 這樣就全部搞定了;程序現在支持更爲詳細(對臨時用戶更加友好)的長選項。 UNIX 用戶始終依賴於命令行參數來修改程序的行爲,特別是那些設計作爲小工具集合 (UNIX 外殼環境)的一部分使用的實用工具更是如此。程序需要能夠快速處理各個選項和參數,且要求不會浪費開發人員的太多時間。畢竟,幾乎沒有程序設計爲僅處理命令行參數,開發人員更應該將精力放在程序所實際進行的工作上。 getopt() 函數是一個標準庫調用,可允許您使用直接的 while/switch 語句方便地逐個處理命令行參數和檢測選項(帶或不帶附加的參數)。與其類似的 getopt_long() 允許在幾乎不進行額外工作的情況下處理更具描述性的長選項,這非常受開發人員的歡迎。 既然已經知道了如何方便地處理命令行選項,現在就可以集中精力改進您的程序的命令行,可以添加長選項支持,或添加之前由於不想向程序添加額外的命令行選項處理而擱置的任何其他選項。
不要忘記在某處記錄您所有的選項和參數,並提供某種類型的內置幫助函數來爲健忘的用戶提供幫助。
Select在Socket編程中還是比較重要的,可是對於初學Socket的人來說都不太愛用Select寫程序,他們只是習慣寫諸如connect、accept、recv或recvfrom這樣的阻塞程序(所謂阻塞方式block,顧名思義,就是進程或是線程執行到這些函數時必須等待某個事件的發生,如果事件沒有發生,進程或線程就被阻塞,函數不能立即返回)。可是使用Select就可以完成非阻塞(所謂非阻塞方式non-block,就是進程或線程執行此函數時不必非要等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率較高)方式工作的程序,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。下面詳細介紹一下! 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模塊. |