高级Linux程序设计第二章:编写良好的Linux软件
1、同运行环境交互
1.1、命令行
-
当一个程序从shell启动的时候,其参数列表包括程序名称及所有的命令行参数
% ls -s / 其参数列表包含三项: 第一项是程序名称ls,第二项和第三项分别是两个命令行参数,-s和/ |
-
main函数可以通过argc和argv两个参数来访问命令行参数列表:argc是命令行参数的个数,argv是命令行参数字符串指针所组成的数组
#include <stdio.h> int main (int argc, char* argv[]) { printf (“The name of this program is ‘%s’.\n”, argv[0]); printf (“This program was invoked with %d arguments.\n”, argc - 1); /* Were any command-line arguments specified? */ if (argc > 1) { /* Yes, print them. */ int i; printf (“The arguments are:\n”); for (i = 1; i < argc; ++i) printf ("%s\n", argv[i]); } return 0; } |
-
命令行参数分两类:选项和参数
-
选项有两种格式:
-
短格式:包括一个连字符和一个字母。短格式选项有利于用户的尽快输入。
-
长格式:包括两个连字符,及一个有小写字母,大写字母及连字符组成的选项名称。长格式选项更易记易读。
-
短格式 |
长格式 |
目的 |
-h |
--help |
显示用法概要 |
-o filename |
--output filename |
指定输出文件名 |
-v |
--verbose |
打印详细信息 |
-
函数getopt_long可以解析短格式和长格式的选项
-
Include <getopt.h>.
-
提供两个数据结构
-
第一个数据结构一是一个字符串,包含所有的短格式选项,每个选项一个字母。需要跟一个参数的选项后面有一个冒号。char* const short_options = “ho:v”;
-
第二个数据结构是一个option结构体组成的数组。数组中的每一项对应一个长选项,包括四个域:
-
第一个域是长选项的名称
-
第二个域表示此选项是否需要跟一个参数
-
第三个域是NULL
-
第四个域是一个字符,是此长选项对应的短选项
-
数组的最后一项为四个域都是0的项
-
-
const struct option long_options[] = { { “help”, 0, NULL, ‘h’ }, { “output”, 1, NULL, ‘o’ }, { “verbose”, 0, NULL, ‘v’ }, { NULL, 0, NULL, 0 } }; |
-
getopt_long函数的参数依次为argc, argv, 短选项字符串,长选项数组
-
每次getopt_long被调用时,其解析一个选项,返回相应的短选项。当没有选项的时候返回-1
-
应该在循环中调用getopt_long,然后用switch根据返回的短选项进行相应的处理
-
当getopt_long遇到一个非法的选项时,其打印出错误信息,并返回'?'
-
当一个选项需要跟一个参数的时候,有一个全局变量optarg指向此参数字符串
-
当getopt_long完成解析所有的选项后,有一个全局变量optind包含第一个非选项参数在argv中的偏移量
#include <getopt.h> #include <stdio.h> #include <stdlib.h> /* The name of this program. */ const char* program_name; /* Prints usage information for this program to STREAM (typically stdout or stderr), and exit the program with EXIT_CODE. Does not return. */ void print_usage (FILE* stream, int exit_code) { fprintf (stream, “Usage: %s options [ inputfile ... ]\n”, program_name); fprintf (stream, “ -h --help Display this usage information.\n” “ -o --output filename Write output to file.\n” “ -v --verbose Print verbose messages.\n”); exit (exit_code); } /* Main program entry point. ARGC contains number of argument list elements; ARGV is an array of pointers to them. */ int main (int argc, char* argv[]) { int next_option; /* A string listing valid short options letters. */ const char* const short_options = “ho:v”; /* An array describing valid long options. */ const struct option long_options[] = { { “help”, 0, NULL, ‘h’ }, { “output”, 1, NULL, ‘o’ }, { “verbose”, 0, NULL, ‘v’ }, { NULL, 0, NULL, 0 } /* Required at end of array. */ }; /* The name of the file to receive program output, or NULL for standard output. */ const char* output_filename = NULL; /* Whether to display verbose messages. */ int verbose = 0; /* Remember the name of the program, to incorporate in messages. The name is stored in argv[0]. */ program_name = argv[0]; do { next_option = getopt_long (argc, argv, short_options, long_options, NULL); switch (next_option) { case ‘h’: /* -h or --help */ /* User has requested usage information. Print it to standard output, and exit with exit code zero (normal termination). */ print_usage (stdout, 0); case ‘o’: /* -o or --output */ /* This option takes an argument, the name of the output file. */ output_filename = optarg; break; case ‘v’: /* -v or --verbose */ verbose = 1; break; case ‘?’: /* The user specified an invalid option. */ /* Print usage information to standard error, and exit with exit code one (indicating abnormal termination). */ print_usage (stderr, 1); case -1: /* Done with options. */ break; default: /* Something else: unexpected. */ abort (); } } while (next_option != -1); /* Done with options. OPTIND points to first nonoption argument. For demonstration purposes, print them if the verbose option was specified. */ if (verbose) { int i; for (i = optind; i < argc; ++i) printf (“Argument: %s\n”, argv[i]); } /* The main program goes here. */ return 0; } |
1.2、标准输入输出
-
C标准库提供三个流:
-
标准输入流:stdin
-
标准输出流:stdout
-
标准错误流:stderr
-
-
三个流出了用C标准库访问外,还可以直接通过Unix API直接对文件描述符进行操作
-
stdin的文件描述符为0,stdout为1,stderr为2
-
标准输出流和标准错误流都可以重定向到一个文件或者一个管道(pipe)
% program > output_file.txt 2>&1 % program 2>&1 | filter 2<&1表示文件描述符2也即stderr将合并到文件描述符1也即stdout中。 欲重定向stdout和stderr到一个文件,2>&1必须跟在一个文件后面。 欲重定向stdout和stderr到一个管道,2>&1必须在一个管道前面。 |
-
stdout有缓冲,此缓存可以通过fflush(stdout)显式的写出。
-
stderr没有缓冲。
1.3、程序退出码
-
当一个程序结束的时候,其在退出码中返回程序的退出状态。
-
退出码是一个整形数。
-
退出码为0表示程序成功退出,退出码非零表示程序因错误而退出。
-
最近运行的程序的退出码可以通过一个特殊的shell变量$?得到。
% ls / bin coda etc lib misc nfs proc sbin usr boot dev home lost+found mnt opt root tmp var % echo $? 0 % ls bogusfile ls: bogusfile: No such file or directory % echo $? 1 |
1.4、环境变量
-
环境变量是一组变量及其值的组合。
-
常用的环境变量:
-
USER保存用户名
-
HOME保存主目录的路径
-
PATH保存一个以冒号分隔的一系列目录的列表。Linux将从这些目录中寻找运行的命令。
-
-
printenv命令可以打印所有的环境变量。
-
在程序中,可以用getenv函数得到环境变量的值。
-
可以用函数setenv和unsetenv来设置和清除环境变量。
-
可以通过一个特殊的全局变量environ来得到所有的环境变量。
#include <stdio.h> /* The ENVIRON variable contains the environment. */ extern char** environ; int main () { char** var; for (var = environ; *var != NULL; ++var) printf (“%s\n”, *var); return 0; } |
-
当一个程序运行起来的时候,其继承了启动程序的环境变量。
-
环境变量多用于给程序传递配置信息。
1.5、使用临时文件
-
临时文件存储在/tmp文件夹中。
-
不同的进程应该使用不同的临时文件。
-
没有授权的用户不能通过改变或者替换临时文件来改变程序运行的行为。
-
临时文件名应该以不可预测的方式生成。
-
mkstemp函数
-
根据一个文件名模板创建一个独一无二的文件名。
-
其创建的文件应被设置了除当前用户外其他用户无法访问的权限。
-
其以可读可写的方式打开临时文件。
-
被mkstemp的临时文件不会被自动删除。
-
如果一个临时文件仅仅在一个程序内部使用,而不会被其他程序使用,则应该在mkstemp生成临时文件后,对临时文件进行unlink,从而程序退出时临时文件会被删除。
-
#include <stdlib.h> #include <unistd.h> /* A handle for a temporary file created with write_temp_file. In this implementation, it’s just a file descriptor. */ typedef int temp_file_handle; /* Writes LENGTH bytes from BUFFER into a temporary file. The temporary file is immediately unlinked. Returns a handle to the temporary file. */ temp_file_handle write_temp_file (char* buffer, size_t length) { /* Create the filename and file. The XXXXXX will be replaced with characters that make the filename unique. */ char temp_filename[] = “/tmp/temp_file.XXXXXX”; int fd = mkstemp (temp_filename); /* Unlink the file immediately, so that it will be removed when the file descriptor is closed. */ unlink (temp_filename); /* Write the number of bytes to the file first. */ write (fd, &length, sizeof (length)); /* Now write the data itself. */ write (fd, buffer, length); /* Use the file descriptor as the handle for the temporary file. */ return fd; } /* Reads the contents of a temporary file TEMP_FILE created with write_temp_file. The return value is a newly allocated buffer of those contents, which the caller must deallocate with free. LENGTH is set to the size of the contents, in bytes. The temporary file is removed. */ char* read_temp_file (temp_file_handle temp_file, size_t* length) { char* buffer; /* The TEMP_FILE handle is a file descriptor to the temporary file. */ int fd = temp_file; /* Rewind to the beginning of the file. */ lseek (fd, 0, SEEK_SET); /* Read the size of the data in the temporary file. */ read (fd, length, sizeof (*length)); /* Allocate a buffer and read the data. */ buffer = (char*) malloc (*length); read (fd, buffer, *length); /* Close the file descriptor, which will cause the temporary file to go away. */ close (fd); return buffer; } |
-
如果不需要把临时文件传给另一个程序,则可以用tmpfile函数
-
其创建和打开一个临时文件
-
返回一个指向文件的指针
-
临时文件已经被unlink了,当文件指针被关闭的时候(fclose)或者程序结束的时候,临时文件会自动删除。
-
2、编写及使用库
2.1、静态链接库(Archives)
-
静态链接库仅仅将一系列对象文件(.obj)归档为一个文件。
-
当链接器遇到一个静态链接库的时候,其将需要的对象文件提取出来,并将它链接到程序文件中。
-
静态链接库可用ar命令创建。
% ar cr libtest.a test1.o test2.o |
-
当链接静态链接库的时候,静态链接库的指定顺序是很重要的。
-
当链接器依次解析命令行中指定的静态链接库时,其首先在已经解析过的对象文件中寻找所有被引用了,但是没有定义的标识符,然后在静态链接库中将包含这些标识符的对象文件抽取出来,链接到程序文件中。
2.2、动态链接库(Shared Libraries)
-
当一个动态链接库被链接到一个程序文件中的时候,最后的程序文件并不包括动态链接库中的代码,而是仅仅包括对动态链接库的引用。
-
动态链接库可被多个程序共享。
-
动态链接库不仅仅是一组对象文件的简单归档,而是多个对象文件的重新组合。
-
当一个程序链接一个动态链接库的时候,其或者包括动态链接库的所有代码,或者不包括动态链接库的任何代码,而不像静态链接库一样包括程序需要的那一部分。
-
当创建一个动态链接库德时候,需要用-fPIC参数编译对象文件。
% gcc -c -fPIC test1.c |
-
可以将多个对象文件合并成一个动态链接库。
% gcc -shared -fPIC -o libtest.so test1.o test2.o |
-
链接一个动态链接库和链接一个静态链接库一样。
% gcc -o app app.o -L. –ltest |
-
由于链接的时候没有指定.so还是.a,链接器优先选择动态链接库,除非显式指定选择静态链接库。
% gcc -static -o app app.o -L. –ltest |
-
ldd命令显示链接到程序的所有动态链接库。
-
当链接一个动态链接库到一个程序的时候,链接器并不保存动态链接库的全路径,而仅仅是动态链接库的名称。
-
当程序运行的时候,系统首先寻找动态链接库,然后加载它。
-
默认情况下,系统在/lib和/usr/lib文件夹下寻找动态链接库。
-
当链接动态链接库到程序的时候,如果指定-Wl,-rpath参数,则系统会在/usr/local/lib下寻找动态链接库。
-
设定LD_LIBRARY_PATH环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库。
Create test1.cvoid f1() { printf("Test 1 fucntion 1"); } gcc -c -fPIC test1.c -rw-rw-r-- 1 liuchao liuchao 1124 Jul 31 19:51 test1.o Create test2.cvoid f2() { printf("Test 2 function 2"); } gcc -c -fPIC test2.c -rw-rw-r-- 1 liuchao liuchao 1124 Jul 31 19:52 test2.o Create libstatictest.aar cr libstatictest.a test1.o test2.o -rw-rw-r-- 1 liuchao liuchao 2508 Jul 31 19:53 libstatictest.a Create libdynamictest.sogcc -shared -fPIC -o libdynamictest.so test1.o test2.o -rwxrwxr-x 1 liuchao liuchao 4573 Jul 31 19:54 libdynamictest.so Create app.cint main(int argc, char* argv[]) { f2(); } gcc -c app.c -rw-rw-r-- 1 liuchao liuchao 772 Jul 31 19:55 app.o Create staticappgcc -o staticapp app.o -L. –lstatictest -rwxrwxr-x 1 liuchao liuchao 4861 Jul 31 19:56 staticapp Create dynamicappgcc -o dynamicapp app.o -L. -ldynamictest Run staticapp./staticapp Test 2 function 2 Run dynamicapp./dynamicapp ./dynamicapp: error while loading shared libraries: libdynamictest.so: cannot open shared object file: No such file or directory Ldd staticappldd staticapp linux-gate.so.1 => (0x0060d000) libc.so.6 => /lib/libc.so.6 (0x007a3000) /lib/ld-linux.so.2 (0x00786000) Ldd dynamicappldd dynamicapp linux-gate.so.1 => (0x00861000) libdynamictest.so => not found libc.so.6 => /lib/libc.so.6 (0x00111000) /lib/ld-linux.so.2 (0x00786000) export LD_LIBRARY_PATH=.export LD_LIBRARY_PATH=. echo $LD_LIBRARY_PATH . ./dynamicapp Test 2 function 2 |
2.3、库依赖
-
一个库会经常依赖另一个库:例如libtiff库包含读写TIFF图片文件的函数,其依赖libjpeg库和libz库。
-
创建使用libtiff库的程序:
#include <stdio.h> #include <tiffio.h> int main (int argc, char** argv) { TIFF* tiff; tiff = TIFFOpen (argv[1], “r”); TIFFClose (tiff); return 0; } |
-
编译程序,链接libtiff
% gcc -o tifftest tifftest.c –ltiff |
-
链接器选择动态链接库libtiff。由于libtiff依赖libjpeg和libz,所以他们也被链接进来。
% ldd tifftest libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4001d000) libc.so.6 => /lib/libc.so.6 (0x40060000) libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x40155000) libz.so.1 => /usr/lib/libz.so.1 (0x40174000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) |
-
如果是静态链接库,其依赖的库不会被链接进来。
% gcc -static -o tifftest tifftest.c -ltiff /usr/bin/../lib/libtiff.a(tif_jpeg.o): In function ‘TIFFjpeg_error_exit’: tif_jpeg.o(.text+0x2a): undefined reference to ‘jpeg_abort’ /usr/bin/../lib/libtiff.a(tif_jpeg.o): In function ‘TIFFjpeg_create_compress’: tif_jpeg.o(.text+0x8d): undefined reference to ‘jpeg_std_error’ tif_jpeg.o(.text+0xcf): undefined reference to ‘jpeg_CreateCompress’ ... |
-
如果欲链接静态链接库及其依赖的静态链接库,必须显式指定。
% gcc -static -o tifftest tifftest.c -ltiff -ljpeg –lz |
-
如果两个静态链接库是相互依赖的,则应该在命令行上指定两次。
% gcc -static -o app app.o -lfoo -lbar –lfoo 例如程序app中的函数void a()在libfoo.a的A.o中实现,A.o中的函数void b()在libbar.a的B.o中实现,B.o中的函数void c()在libfoo.a的C.o中实现。则链接器的工作过程如下:
|
2.4、动态加载和卸载库
-
调用dlopen函数可以加载动态链接库libtest.so
dlopen (“libtest.so”, RTLD_LAZY) |
-
要使用动态加载函数,要include<dlfcn.h>,并且链接的时候要指定-ldl参数来链接libdl库。
-
dlopen的返回值是一个void *的指针,作为访问动态链接库的句柄。
-
可以讲句柄传给函数dlsym来得到加载的动态链接库中的某个函数的地址。
void* handle = dlopen (“libtest.so”, RTLD_LAZY); void (*test)() = dlsym (handle, “my_function”); (*test)(); dlclose (handle); |
-
dlopen和dlsym失败的时候返回NULL,可以用函数dlerror得到可描述失败原因的信息。
-
dlclose函数卸载动态链接库。