Ubuntu Linux---GNU libc库

来源:百度文库 编辑:神马文学网 时间:2024/04/28 04:08:03
除了libc和libm库,UNIX系统库没有其他标准的命名规范。Linux上的一些系统库可能和UNIX平台上库的名称不同,这就需要知道Linux上各库所包含和支持的功能。表3-2根据所支持的功能列举了一些Linux系统库。


    下面列出了GNU libc库所包含的库文件以及对应的描述(注释5):

  - ld.so,为使用了共享库的可执行程序提供的一个辅助程序;

  - libBrokenLocal.[a,so],Mozilla等应用程序用以解决被破坏的locale的库文件;

  - libSegFault.so,段错误信号处理器,它试图捕获段错误信号。

  - libanl.[a,so],异步的名称查询库。

  - libbsd-compat.a,在Linux上运行BSD程序时需要的库。

  - libc.[a,so],最主要的C库(常用的C函数的集合)。

  - libcrypt.[a,so],加密库。

  - libdl.[a,so],动态链接接口库。

  - libg.a,g++运行时库。

  - libieee.a,IEEE浮点运算库。

  - libm.[a,so],数学库。

  - libmcheck.a,包含启动时运行的代码。

  - libmemusage.so,memusage用来收集应用程序内存使用情况的库。

  - libnsl.a,网络服务库。

  - libnss_comkpat.so,libnss_dns.so, libnss_files.so, libnss_hesiod.so, libnss_nis.so, libnss_nisplus.so,NSS(Name Service Switch)库,包含解析主机名、用户名、组名、别名、服务、协议等的函数。

  - libpcprofile.so,包含一些跟踪统计代码行消耗CPU时间的概要分析(profiling)函数。

  - libpthread.[a,so],POSIX线程库。

  - libresolv.[a,so],包含为网络域名服务器创建、发送、解释网络包的函数。

  - librpcsvc.a,包含提供各种RPC服务的函数。

  - librt.[a,so],包含POSIX1.b实时扩展所定义的大部分接口函数。

  - libthread_db.so,包含开发多线程程序调试器的函数。

  - libutil.[a,so],包含常用的UNIX工具使用的“标准”函数。

  上面这些库大多位于/usr/lib目录,也有一些在/lib目录下,例如libSegFault.so.

3.1.1 glibc遵循的标准

 

  GNU glibc发布了一个描述其所遵循的标准的报告(注释6)。该报告同时也列出了GNU libc需要改进的地方。写作本书时,该报告显示GNU libc通过了FIPS POSIX90、POSIX96、UNIX98、ANSI、C89/99,和ISO9899标准的头文件一致性检查。所有主要Linux发行版的glibc也都遵循LSB规范。

  3.2 GNU科学库

  把进行高性能计算的应用程序移植到Linux上需要一个支持库,该库要与UNIX平台上的科学库非常匹配。Linux上类似的库叫GNU科学库(GNU Scientific Library,简写作GSL)。GSL是一系列数学运算例程的集合。这些例程是用C语言重新编写的,并且给编程人员提供了一个新式的API模式---允许编程人员为各高级语言编写包裹函数(wrapper)。这些源代码使用的是GPL发布许可。

  GNU科学库包含了数学运算领域的很多内容。表3-3列出了GNU科学库提供的例程。


    这些例程的用法在GSL手册(注释7)中有详尽的描述,包括函数的定义、示例程序,以及函数实现的算法所引用的论文。

  3.3 共享库

  我们所移植过的大多数应用程序都使用了共享库。然而,不同的操作系统在创建和命名共享库时却不尽相同。Linux上,共享库可以有不同的文件扩展名,例如,共享库可以以.so或.so.1.0结束。以.so.x.x(x为数字)结尾的共享库叫版本化库。第一个数字代表大版本号,第二个数字代表小版本号。有些情况下,共享库的扩展名还可以是.so.x.x.x(x为数字)的形式,这里最后一个数字代表发布号,并且是可选的。下面给出了共享库文件名的格式:

  (代码)p58 第11行,lib.so...

  大版本号、小版本号,以及发布号的变化反映了对共享库所作的不同类型的修改。下面是对增大大版本号、小版本号和发布号的一些指导:

  - 当对共享库提供的接口做了与以前版本不兼容的改变时,需要增大大版本号。这个大的改变意味着依赖该库先前大版本的应用程序需要作相应修改才能使用大版本更新后的库。

  - 当共享库增加了新的接口同时也保留了原来的接口时,增大小版本号。

  - 当作了与以前兼容的修改又没有增加新接口时,增大发布号。这通常是对一些实现做了改动以提高性能和扩展性。

  要在Linux上创建共享库,使用-shared编译参数;该参数告诉GNU ld创建一个共享库而不是应用程序。下面是这样一个例子:

  (代码)p58 最后一行 $ gcc –o libfoo.so –shared –fpic foo.c

3.4 库版本化

 

  在共享库和应用程序之间维护二进制级的兼容性或ABI是很重要的。共享库的ABI是应用程序依赖的运行时接口;如果每次发布时共享库的ABI都与以前的兼容,那么在其中某一个版本的共享库上编译的应用程序不需要任何改动就可以在后续版本上运行。库版本化就是Linux以及同期的其他操作系统实现二进制兼容性的方法。

  我们以前移植过的一些应用程序需要库版本化的支持。各UNIX平台也都实现了库版本化,但实现的方法不尽相同。Linux提供了两种不同的技术来实现库版本化:外部库版本化和符号版本化。

  3.4.1 外部库版本化

  链接过程中,链接器(ld)会查找以.so结尾的共享库文件。以.so结尾的库文件叫链接器名称,这是由他们在Linux上的使用方式决定的。当编译一个依赖某一共享库的应用程序时,仅仅是该共享库的soname(不是共享库的文件名)作为依赖关系被记录在应用程序的二进制代码中。运行时链接器就是使用共享库的soname来查找和装载该库的。共享库的soname只包含有大版本号(例如,libfoo.so.1)

  当修改后的共享库与以前版本不兼容时,新的共享库必须有一个新的外部版本名称。也就是说,该库的soname必须改变。这些不兼容的修改包括:删除一个符号,去掉某函数的一个参数,改变了某函数的语义属性以致与以前的定义不再一致并且与老版本二进制不兼容等等。我们来看下面的例子。(见pdf附件 341.pdf)

   3.4.2 符号版本化

  就像前面所提到的,当对共享库所作的修改能够向前兼容时,我们只增大小版本号。这种修改包括增加一些新的接口同时又不改变已有的接口。但是,即使只做这种小版本的修改,也会出现一个很重要的问题:一个在某一小版本的共享库上编译的应用程序并不一定能够在以前小版本的库上运行。这是因为该应用程序可能使用了新增加的、以前小版本的库中没有的接口。为了解决这个问题,引入了符号版本化。符号版本化允许共享库记录下每个小版本都新增了什么内容。

  在Linux上,GNU ld可以使用-version-script连接器选项来创建符号版本化的共享库。编译器选项-Wl,--version-script=mapfile告诉链接器哪些符号要从生成的共享库中输出出来。每个符号分属global(被输出)和local(不被输出)两类中的一种。来看下面的例子。foo.c包含一个函数foo1,该文件用来创建1.1版本的共享库。(见附件 示例代码.pdf)

  可以看到,这次main只引用了版本化库的LX_1.1。

  GNU ld还允许在定义符号的源文件中把符号绑定到某一版本中,而不仅仅是在脚本文件中指定。另外,GNU ld还允许同一函数的多个版本出现在同一个共享库中。更多详细信息,请参考GNU ld手册(注释13)和Ulrich Drepper的文章“How to Write Shared Libraries”。

  从2.1版本开始,glibc就已经实现了符号版本化。符号版本化同时也是LSB规范1.2及更高版本的一部分。

  3.5 动态链接器(运行时链接器)

  Linux动态链接器(/lib/ld.so.1或/lib64/ld64.so.1)查找和装载应用程序所需的共享库,准备应用程序的运行,然后运行应用程序。除非编译时为ld指明-static选项,否则Linux二进制程序都是动态链接的。

  在所有现代UNIX操作系统上,都有一些环境变量可以影响动态链接器的运行。例如AIX上的环境变量LIBPATH可以改变动态链接器的搜索路径。以下环境变量可以影响到Linux上动态链接器的运行:

  - LD_LIBRARY_PATH,以冒号分开的目录列表,运行时会在这些目录中查找需要的库。

  - LD_PRELOAD,以空格分开的库列表,这些库会在其他所有库之前装载。这常常用来有选择的覆盖某些共享库中的函数。

  - LD_BIND_NOW,如果该环境变量设置成非空字符串,动态链接器会在程序启动时解析所有符号,而不是首次引用时才解析符号(也就是常说的“延迟绑定”)。这在使用调试器时非常有用。

  - LD_TRACE_LOADED_OBJECTS,如果该环境变量设置成非空字符串,程序会列出它所依赖的共享库,就像运行ldd命令一样,而不是正常的执行。

  Linux动态链接器采用广度优先(breadth first)的方式解决库的依赖关系。也就是说,首先是可执行程序所依赖的库按照动态节(dynamic section)列出的顺序被装载进来,然后是“第一个被依赖的库”所依赖的库按照同样的方法装载进来,以此类推,直到所有的依赖关系都被解决。

  在命令行运行下面的命令,会得到更多关于Linux动态链接器的信息:

  (代码)(P69第最后一行)

  $ info ld.so

3.5.1 编程接口

 

  Linux提供了一套API来动态装载库。下面列出了这些API:

  - dlopen,打开一个库,并为使用该库做些准备。

  - dlsym,在打开的库中查找符号的值。

  - dlclose,关闭库。

  - dlerror,返回一个描述最后一次调用dlopen、dlsym,或dlclose的错误信息的字符串。

  C语言用户需要包含头文件dlfcn.h才能使用上述API。glibc还增加了两个POSIX标准中没有的API:

  - dladdr,从函数指针解析符号名称和所在的文件。

  - dlvsym,与dlsym类似,只是多了一个版本字符串参数。

  在Linux上,使用动态链接的应用程序需要和库libdl.so一起链接,也就是使用选项-ldl。但是,编译时不需要和动态装载的库一起链接。程序3-1是一个在Linux上使用dl*例程的简单示例。

  (代码)(P70-73)

  编译该程序:

  (代码)(P73第5行)

  $ make

  运行程序:

  (代码)(P73第15行)

  $ ./main

  用ldd命令检查可执行程序:

  $ ldd ./main

  (代码)(P73第19行)

  可以看到,可执行程序main没有引用动态装载的库。

  3.5.2 延迟重定位(Lazy Relocation)

  延迟重定位/装载是一个允许符号只在需要时才重定位的特性。这常在各UNIX系统上解析函数调用时用到。当一个和共享库一起链接的应用程序几乎不会用到该共享库中的函数时,该特性被证明是非常有用的。这种情况下,只有库中的函数被应用程序调用时,共享库才会被装载,否则不会装载,因此会节约一些系统资源。但是如果把环境变量LD_BIND_NOW设置成一个非空值,所有的重定位操作都会在程序启动时进行。也可以在链接器命令行通过使用-z now链接器选项使延迟绑定对某个特定的共享库失效。需要注意的是,除非重新链接该共享库,否则对该共享库的这种设置会一直有效。

  3.5.3 初始化(initializing)和终止化(finalizing)函数

  有时候,以前的代码可能用到了两个特殊的函数:_init和_fini。_init和_fini函数用在装载和卸载某个模块(注释14)时分别控制该模块的构造器和析构器(或构造函数和析构函数)。他们的C语言原型如下:

  (代码)(P74第8行)

  void _init(void);

  void _fini(void);

  当一个库通过dlopen()动态打开或以共享库的形式打开时,如果_init在该库中存在且被输出出来,则_init函数会被调用(注释15)。如果一个库通过dlclose()动态关闭或因为没有应用程序引用其符号而被卸载时,_fini函数会在库卸载前被调用。当使用你自己的_init和_fini函数时,需要注意不要与系统启动文件一起链接。可以使用GCC选项-nostartfiles做到这一点。

但是,使用上面的函数或GCC的-nostartfiles选项并不是很好的习惯,因为这可能会产生一些意外的结果。相反,库应该使用__attribute__((constructor))和__attribute__((destructor))函数属性来输出它的构造函数和析构函数。如下所示:

 

  (代码)(P74第21行)

  void __attribute__((constructor)) x_init(void)

  void __attribute__((destructor)) x_fini(void)

  构造函数会在dlopen()返回前或库被装载时(注释16)调用。析构函数会在这样几种情况下被调用:dlclose()返回前,或main()返回后,或装载库过程中exit()被调用时。

  3.6 系统调用

  系统调用是用户程序请求内核为调用线程或进程提供具体服务的接口。因为UNIX平台上的一些系统调用是与操作系统密切相关的,因此在Linux上可能不存在类似的系统调用。这种情况下,就需要在Linux上实现一个包裹函数(wrapper)。

  Linux上系统调用的列表位于/usr/include/asm/unistd.h中。本书的附录部分还对Linux和UNIX系统(如Solaris,HP-UX等)进行了并列比较。

  3.7 大页面支持

  大页面的应用主要是用来提高应用程序的性能,该类应用程序需要分配大块内存并且频繁访问该内存。性能的提高主要是通过减少地址转换缓冲器 (Translation Lookaside Buffer,简写作TLB,一块虚拟地址到物理地址转换的缓冲区)的未命中次数来实现的。当TLB能够映射更大的虚拟内存范围时,即可减少TLB的未命中次数。因为大部分现代的体系结构支持多种页面大小,上述方法也就可以实现了。例如,Intel 32位架构支持4KB和4MB(PAE模式时为2MB)的页面;Itanium支持多种页面大小:4K,8K,64K,256K,1M,4M,16M和256M;SUN UltraSPARC支持8K,64K,512K和4M的页面;64位PowerPC(ppc64)支持4K,64K,16M和64G的页面。本节内容将告诉应用程序开发人员如何使用Linux内核提供的大页面支持功能。

  Linux 2.6内核包含有内建的对hugetlbpage(Linux社区称呼大页面的专用术语)的支持。内核配置成支持hugetlbpage时,命令cat /proc/meminfo的输出会显示出关于hugetlbpage的信息,如下例:

  (代码)(P75倒数第8行)

  HugePages_Total : 20

  HugePages_Free : 20

  Hugepagesize: 16384 KB

  一种类型为hugetlbfs的文件系统也应该会出现在/proc/filesystems中。在用户空间的应用程序能够使用hugetlbpage支持前,管理员应该先在内核中分配这些大页面。/proc/sys/vm/nr_hugepages的内容显示的是内核中当前配置的大页面的个数。如果要在系统上配置10个大页面,可以用下面的命令:

  (代码)(P76第1行)

  echo 10 > /proc/sys/vm/nr_hugepages

  只有当系统中存在足够的连续物理内存时,分配请求才会成功;只有存在足够多的能够转回到正常内存池的空闲大页面时,释放请求才会成功。用作hugetlbpage的页面在内核中作为保留页面而不能用作其他用途。

  应用程序开发人员有两种方法可以使用hugetlbpage支持:

  1. 系统V共享内存系统调用(shmget,shmat)

  2. mmap系统调用

  同一个应用程序也可以两者都使用。

  下面的示例程序中,我们给出了如何使用上述系统调用来获得hugetlbpage支持。这些程序来源于/usr/src/linux/Documentation/vm/hugetlbpage.txt。

  示例3-2中,应用程序使用系统V共享内存系统调用来申请由大页面保留的256M内存。shmget系统调用使用SHM_HUGETLB标志告诉内核申请的是大页面。

  (代码)(P76-78)

  对ia86架构,内核为大页面保留了一个特定的内存区域。也就是说,调用进程必须指定某一个固定的地址。但对i386,x86_64,和ppc64不需要一个固定的地址。

  你也可能需要把每个共享内存段的最大大小增大到256MB。这可以用下面的命令实现:

  (代码)(P78第26行)

  echo 268435456 > /proc/sys/kernel/shmmax

  还需要关注的另一个限制是/proc/sys/kernel/shmall,它显示的是系统中可以创建的共享内存的总页数。

  mmap system call

  这种情况下,需要管理员首先挂载一个hugetlbfs类型的文件系统,然后在该挂载点上创建的所有文件都保存在大页面上。

  (代码)(P78第33行)

  mount none /mnt/huge –t hugetlbfs –o uid=1000,gid=100

  上述命令在目录/mnt/huge上挂载一个hugetlbfs类型的文件系统,并把该文件系统的根目录的所有者和组分别设置成1000和100。程序3-3给出了一个使用mmap系统调用申请由大页面保留的256MB内存的示例。

  (代码)(P79-80)

  注意,对hugetlbfs文件系统上的文件,read和write系统调用是不支持的。通常的chown,chgrp和chmod(如果有权限的话)可以用来改变hugetlbfs文件系统上文件的属性。