Linux内核API完全参考手册(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-31 19:14:11

点击下载

作者:邱铁,周玉

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Linux内核API完全参考手册

Linux内核API完全参考手册试读:

前言

进入21世纪,IT技术以前所未有的速度向前发展。Linux作为源码开放的操作系统,在众多爱好者的共同努力下,不断成长并趋于完善。由于GNU计划所开发的各种组件和系统发行版所必备的软件可以运行于Linux内核之上,整个Linux内核符合通用公共许可证(General Public License,GNU),使得Linux在PC机、服务器以及嵌入式系统开发等领域得到了广泛应用。

编者在长期的Linux内核开发中发现,当前介绍内核API方面的书籍很少。目前市面上关于Linux内核编程开发方面的书可以分为三类:第一类,Linux内核分析,所分析的内核源码版本一般相对较早,而对于最新版本的内核源代码则很少提及;第二类,Linux编程,主要是以用户层面上的编程为主,一般涉及用户API;第三类,嵌入式Linux开发,相对于特定的硬件平台,只对所用到的特定内核API作简要说明。对于使用Linux内核进行编程开发,需要全面了解内核API,而目前市面上找不到一本能够全面介绍最新的Linux内核API的图书,这也正是本书写作的目的所在。

本书的编写工作从2009年6月开始,所有的内核API验证实例均基于最新的Linux内核源代码2.6.30版本,当时2.6.30内核刚刚发布。经过近15个月的源代码分析、编程实践与实例验证,对常用的内核API作了系统归纳,并编写了典型验证程序,做到了理论分析与实际编程的统一。本书分析的内核API模块包括:内核模块机制API、进程管理内核API、进程调度内核API、中断机制内核API、内存管理内核API、内核定时机制API、内核同步机制API、文件系统内核API和设备驱动及设备管理API。

在实例编写过程中,感谢陈潇参与了第8章部分实例的验证,开放源码项目组王伟、王亚坤和于龙等参与项目讨论并分析实例。另外,我们在编写过程中听取了同事、同行专家的意见和建议,并参阅了大量国内外文献的精华资料,特别是活跃在开放源码社区的Linux爱好者,向他们表示感谢。

由于Linux更新速度较快,再加上编者所具备知识的广度和深度所限,书中存在的错误与不当之处请各位同仁批评指正。对于书中的问题,读者可以发送到E-mail:openlinux2100@gmail.com,及时和作者交流,以便再版时更正与完善。编者2010年10月1日于大连

本书使用方法

Linux内核API分析必备知识

·介绍了内核分析与开发的基础知识。

检索方法1

·本书目录按照Linux功能模块大类分为9大模块;

·每个模块内部的内核API函数按照模块名称顺序排列。

检索方法2

·本书附录将所有常用内核API函数按照英文字母表顺序排列。第1章Linux内核API分析必备知识

从这里开始,我们来探索Linux内核2.6.30版本的内核API。内核API与用户API是具有本质区别的,因为它们所运行的系统模式是不同的。若要进行Linux内核源代码分析与内核API验证,需要具备一定的基础知识,掌握了这些基础知识后,才能在Linux内核源代码分析与内核API验证实例的理解中做到游刃有余。Linux内核编程注意事项

Linux可以运行在两种模式下:用户模式(user mode)和内核模式(kernel mode)。当我们编写一个普通程序时,有时会包含stdlib.h文件,也就是说我们使用了C标准库,这是典型的用户模式编程,在这种情况下,用户模式的应用程序要链接标准C库。在内核模式下不存在libc库,也就没有这些函数供我们调用。

此外,在内核模式下编程还存在一些限制:

·不能使用浮点运算。因为Linux内核在切换模式时不保存处理器的浮点状态。

·不要让内核程序进行长时间等待。Linux操作系统本身是抢占式的,但是内核是非抢占内核,就是说用户空间的程序可以抢占运行,但是内核空间程序不可以。

·尽可能保持代码的整洁性。内核调试不像调试应用程序那样方便,因此,在前期代码编写的过程中保持代码的整洁易懂,将大大方便后期的调试。

·在内核模式下编程,系统内的所有资源都是由内核来统一调配的,并且数量有限,因此申请资源用完后一定要进行释放,避免出现死锁情况。

·Linux内核API有很多配对使用,例如,文件引用计数有加操作,也会有相应的减操作。如果在实验中进行了“引用计数”加操作,函数执行后未进行减操作还原,那么可能会出现系统崩溃。

本书中的所有内核API验证实例都是在Linux内核模式下进行编程与验证的。本书中模块编译Makefile模板

在Linux 2.6内核中,模块的编译需要配置过的内核源代码;编译过程首先会到内核源码目录下读取顶层的Makefile文件,然后再返回模块源码所在目录;经过编译、链接后生成的内核模块文件的后缀为.ko。

2.6内核模块的Makefile模板:ifneq($(KERNELRELEASE),)mymodule-objs:=mymodule 1.o mymodule 2.o#依赖关系obj-m+=mymodule.o#编译、链接后将生成mymodule.o模块elsePWD:=$(shell pwd)KVER:=$(shell uname-r)KDIR:=/lib/modules/$(KVER)/buildall:$(MAKE)-C$(KDIR)M=$(PWD)#此处将再次调用makeclean:rm-rf *.o *.mod.c *.ko *.symvers *.order *.markers*~endif

当在命令行执行make命令时,将调用Makefile文件。KERNELRELEASE是在内核源码的顶层/usr/src/linux-2.6.30/Makefile文件中定义的一个变量,位置在第358行,如图1-1所示。在第一次读取执行此Makefile时,变量$(KERNELRELEASE)没有被设置,因此第一行ifneq的条件失败,从else后面开始执行,设置PWD、KVER和KDIR等变量。图 1-1 内核源码的顶层Makefile

当make到标号all时,-C$(KDIR)指明跳转到内核源码目录下读取那里的Makefile。M=$(PWD),表明返回到当前目录继续读入、执行当前的Makefile,也就是第二次调用make。这时的$(KERNELRELEASE)已被定义,因此语句ifneq成功,make将继续读取紧接在ifneq后面的内容。ifneq的内容为kbuild语法的语句,指明模块源码中各文件之间的依赖关系和要生成的目标模块名称。

语句"mymodule-objs:=mymodule 1.o mymodule2.o"表示mymoudule.o由mymodule1.o与mymodule2.o链接生成。语句"obj-m:=mymodule.o"表示编译链接后将生成mymodule.ko模块,这个文件就是要插入内核的模块文件。

如果make的目标是clean,直接执行clean标号后的操作,也就清除*.o *.mod.c *.ko *.symvers *.order *.markers*~这些文件操作。执行完clean后面的rm命令后,整个make工作就结束了。内核调试函数printk

本书中的模块代码中用到了内核调试函数printk(),在用户空间里我们经常使用C语言函数printf来向标准输出终端流打印信息。printk()是内核使用的函数,因为内核没有链接标准C函数库,其实printk()接口和printf()基本相似,printk()函数能够在终端一次最多显示大小为1024个字节的字符串。printk()函数执行时首先设法获取控制台信号量,然后将要输出的字符存储到控制台的日志缓冲区,再调用控制台驱动程序来刷新缓冲区。若printk()无法获得控制台信号量,则只能把要输出的字符存储到日志缓冲区,并依赖拥有控制台信号量的进程来刷新这个缓冲区。printk()函数会将数据存储到日志缓冲区,但是为了安全考虑,在这之前需要使用日志缓冲区锁,保证并发调用printk()的安全性。

内核模式下系统信息输出函数printk()与用户模式下的printf()函数在输出内容上也是有区别的,主要是两点:一是内核在切换模式时不保存处理器的浮点状态,因此printk()并不支持浮点数运算;二是printk()可以指定一个记录级别,内核根据这个级别来判断是否在终端上打印消息,而printf()函数则不需要。

printk()的语法格式为:printk(记录级别"格式化输出信息");

或者是采用编号形式:printk("<记录级别编号>格式化输出信息");

其中“记录级别”是在include/linux/kernel.h中的简单宏定义,其在内核源代码中的形式如图1-2所示的第91~98行。它们扩展开是如"<number>"这样的字符串,加入到printk()函数要打印的消息的前面。图 1-2 记录级别在内核源代码中的宏定义

内核用这个指定的记录等级和当前终端的记录等级console_loglevel进行比较,从而决定是否向终端打印输出。表1-1给出了所有记录等级、字符串编号及说明。

内核将最重要的记录等级KERN_EMERG定为“<0>”,将无关紧要的记录等级"KERN_DEBUG"定为“<7>”。

例如:printk("没有等级信息,则采用默认级别!\n");printk(KERN_INFO"内核提示信息\n");printk(KERN_DEBUG"内核调试信息\n");

如果没有特别指定一个记录等级,函数会选用默认的DEFAULT_MESSAGE_LOGLEVEL,现在默认等级是KERN_WARNING。由于这个默认值将来存在变化的可能性,所以还是应该为自定义的消息指定一个记录等级。

本章后面的实例中考虑到要让系统强制输出信息,因此使用了“<0>”级别,然后通过dmesg|tail或者是dmesg-c来查看系统输出信息。内核编译与定制

获得Linux内核与补丁

要编译Linux,首先是要获得Linux的内核源码(kernel source code)。最新的Linux官方源码可以从www.kernel.org或其映像站点获取,2.6.x版本一般放在/pub/linux/kernel/v2.6/,其在官方网站上的目录索引如图1-3所示。图 1-3 Linux内核网页目录索引

将下载的内核源代码放在Linux系统目录文件夹/usr/src/中。本书用以下命令下载最新2.6.30内核源码包。cd/usr/src/wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.30.tar.bz2

下载Linux 2.6.30内核补丁,其在官方网站上的目录索引如图1-4所示。图 1-4 Linux内核补丁网页目录索引

下载补丁的命令如下:cd/usr/src/wget http://www.kernel.org/pub/linux/kernel/v2.6/patch-2.6.30.bz2

准备编译需要的工具

要想顺利完成内核编译,首先要检查或安装必要的工具:

1)安装gcc、make等编译工具:apt-get install build-essential

2)安装make menuconfig时必需的库文件:NCurses(libncurses5-dev或ncurses-devel),这是当make menuconfig时用作生成菜单窗口的程序库:apt-get install libncurses-devapt-get install kernel-package

3)安装Linux系统生成kernel-image的一些配置文件和工具:apt-get install fakerootapt-get install initramfs-tools,module-init-tools

4)在编译Linux内核时,通常还需要以下工具(这些工具一般是可选的):

·GNU C++Compiler(g++或gcc-c++)-编译make xconfig使用的Qt窗口时需要;

·Qt 3(qt-devel或qt3-devel)-make xconfig时用作Qt窗口的程序库;

·GTK+(gtk+-devel)-make gconfig时用作GTK+窗口的程序库;

·Glade(libglade2-devel)-要编译make gconfig时的GTK+窗口时需要。

在Ubuntu系统中,我们可以使用下面的命令来获得相关的软件包:apt-get updateapt-get install libncurses5-dev wget bzip2

解压内核

如果下载了GNU Zip格式的Linux核心源码压缩档(文件扩展名称为*.tar.gz),可以用指令“tar zxvf linux-版本编号.tar.gz”解压,例如:tar xzvf linux-2.6.30.tar.gz

如果下载了BZip2的Linux核心源码压缩档(文件扩展名称为*.tar.bz2),可以用指令“tar jxvf linux-版本编号.tar.bz2”解压,本书介绍的是bz2的压缩包,如下所示:tar xjvf linux-2.6.30.tar.bz2

把源码包解压到/usr/src中,通过运行解压命令后,发现/usr/src中多了一个linux-2.6.30文件夹,如图1-5所示。图 1-5 内核解压后的文件夹

给内核打补丁

这一步在内核的编译过程中是可选的,如果你对内核有特殊的要求,可以将自己写的补丁打到内核中去。

对于本章中所下载的linux-2.6.30内核源码包在PC机上编译是不需要这一步骤的。如果读者有新的要求,可以写补丁包,例如,对当前的linux-2.6.30制作的补丁包(文件名为patch-2.6.30.bz2),可以使用patch命令给Linux内核源码打入补丁:cd linux-2.6.30bzcat../patch-2.6.30.bz2|patch-p1

由于我们是在PC机进行的Linux内核API验证,因此,这一步骤省略了。

设定编译选项

当编译Linux内核时,其中一个最重要的步骤就是定制新内核的配置选项,需要确定哪些是必选的,哪些是要编译成可加载模块(Loadable Modules)时进行动态加载,哪些不需要编译进新内核中。这些要根据使用的具体情况而定,定制的原则是:在满足功能需求的前提下,使新内核占用空间最少、耗费资源最少、运行速度最快。

在定制编译选项时,Linux系统提供了多个方法进行设定编译选项:

·config

·menuconfig

·xconfig

·gconfig

·oldconfig

也可以通过以下的命令来取得旧编译选项(注意,如果是初学者编译Linux内核,可以与旧的编译选项进行对比选择):cp/boot/config-`uname-r`.config

Make config为终端问答文字格式的编译选项,make menuconfig为菜单选项格式的配置界面如图1-6所示,在这里可以通过键盘来设置各个选项。图 1-6 make menuconfig编译选项界面

make xconfig为基于QT/Tcl的图形配置界面,如图1-7所示。make gconfig为基于GTK+的图形配置界面,如图1-8所示。这两个基于图形界面的编译选项均可通过鼠标操作编译选项,操作比较方便。图 1-7 make xconfig编译选项界面图 1-8 make gconfig编译选项界面

make oldconfig命令只选择新编译选项。一般情况下,当编译Linux内核时,执行make menuconfig弹出对话框,可以对内核的编译选项进行新的设定,并生成一个.config文件,make的时侯就是根据.config的设定值进行编译内核。如果再重新执行make menuconfig时,内核编译选项又重新回到了以前的默认值。当我们设定了内核编译选项之后,执行make oldconfig命令就可以保存前面设定好的内核编译配置选项。当下次再执行make menuconfig命令时,出现的设定就是前一次的设定内容。

注意 如果是在PC机下,没有特殊的要求,初学者编译内核,可以先选择“默认”的编译选项。一旦出现问题,便可以对照旧的编译配置文件,逐步查找并解决问题。

编译与安装内核

首先用make mrproper命令清除所有旧的配置和旧的编译目标等文件:cd/usr/src/linux-2.6.30make mrproper

接着执行命令make来编译内核,在默认情况下,make是一个顺序执行的工具。它按次序调用底层编译器来编译C/C++源文件。在某些情况下,有的源文件不需以其他源文件为基础即可编译,这时可以使用-j选项调用make来完成并行编译操作。make指令格式如下:make-jn

n代表同时编译的进程,可以加快编译速度,n由用户计算机的配置与性能决定,当前的典型值为10。make编译内核过程如图1-9所示。图 1-9 make编译内核过程

经过以上编译内核操作,将会在目录arch/x86/boot下生成名为"bzImage"的文件(如图1-10所示)即编译出来的新内核。为方便管理,需要把它移动至目录/boot中,并改名为“vmlinuz-核心版本”。为保存编译选项方便日后参考,同时也要把.config复制至/boot中,并改名为“config-核心版本”。我们可以通过输入命令make install来完成这些步骤:make install图 1-10 生成bzImage

接下来执行命令:make modules

进行编译模块,如图1-11所示。图 1-11 编译模块

最后执行命令:make modules_install

make modules_install是将内核模块安装到/lib/modules中,其执行过程如图1-12所示。图 1-12 内核模块的安装过程

创建initramfs

为了在initramfs中添加指定kernel的驱动模块,内核模块2.6.30还需要创建initramfs的kernel版本号。如果是给当前kernel制作initramfs,可以使用uname-r查看当前的版本号。mkinitramfs会把/lib/modules/${kernel_version}/目录下一些启动时需要使用的模块添加到initramfs中。本实例中通过执行以下命令来实现:mkinitramfs-o/boot/initrd.img-2.6.30/lib/modules/2.6.30

设置grub

在/boot/grub文件夹的menu.list中添加项,具体请参考menu.list原来的grub引导项,如图1-13所示。其中黑色部分是添加内容,其中第161行:uuid 2c683e9a-ec59-471b-8bf8-27af7d56ec21图 1-13 添加grub引导项

注意 这一串数据根据不同的机器可能不同。

启动选项

重新启动系统后,进入启动选项目录,如图1-14所示,其中Ubuntu 9.04,kernel 2.6.30就是新加入的启动选项。图 1-14 运行新内核之前的启动选项目录

图1-15为运行新内核启动之后uname显示的内核版本,可以看出新内核已经正常工作了。图 1-15 运行新内核之后温馨提示

至此,我们成功地在ubuntu9.04上安装了新的Linux-2.6.30内核,内核源代码放在了/usr/src/linux-2.6.30目录下,可用ls命令查看Linux-2.6.30文件夹下的信息,如图1-16所示。图 1-16 查看linux-2.6.30文件夹下的信息

接下来,我们开始分模块对Linux-2.6.30内核API进行分析与实例验证。

主要模块如下:

·Linux内核模块机制API

·Linux进程管理内核API

·Linux进程调度内核API

·Linux中断机制内核API

·Linux内存管理内核API

·Linux内核定时机制API

·Linux内核同步机制API

·Linux文件系统内核API

·Linux设备驱动与设备管理API

相信这些Linux内核API的分析与验证,一定会对内核编程开发人员和广大的Linux爱好者起到很好的指导与借鉴作用。参考文献

[1]Linux内核裁剪的具体过程和方法[J/OL].http://blog.csdn.net.

[2]邱铁,于玉龙,徐子川.Linux应用与开发典型实例精讲[M].北京:清华大学出版社,2010.

[3]Linux 2.6.26.x内核编译配置选项简介[J/OL].http://www.diybl.com/course/6_system/linux/Linuxjs/2007926/74020.html.

[4]Linux下PCI设备驱动开发详解[J/OL].http://lifenobody.ycool.com/post.867888.html.

[5]从2.4到2.6:Linux内核可装载模块机制的改变对设备驱动的影响[J/OL].http://hi.baidu.com/litaosmile/blog/item/74b952a15451db8d46106465.html.

[6]内核调试(一)[J/OL].http://www.lupaworld.com/bbs/viewthread.php?tid=36973.

[7]printk及控制台的日志级别[J/OL].http://hi.baidu.com/zhangtqqq/blog/item/2ec67f45a4fd162ecffca305.html.

[8]printk函数[J/OL].http://saviee.blogbus.com/logs/1752163.html.

[9]corbet.Porting device drivers to the 2.6 kernel[J/OL].http://lwn.net/Articles/driver-porting.

[10]倪继利.Linux安全体系分析与编程[M].北京:电子工业出版社,2007.

[11]KARIM YAGHMOUR,JON MASTERS,GILAD BEN-YOSSEF,PHILIPP GERUM.构建嵌入式Linux系统[M].2版:影印版.南京:东南大学出版社,2009.第2章Linux内核模块机制API函数:__module_address()

文件包含:#include <linux/module.h>

函数定义:

函数在内核源码中的位置:linux-2.6.30/kernel/module.c

函数定义格式:struct module*__module_address(unsigned long addr)

函数功能描述:

函数__module_address()主要用于根据给定的一个内存地址addr,获得该内存地址所在的模块。

输入参数说明:

addr:其值表示内存地址。

返回参数说明:

如果内存地址addr在某一模块的地址空间中,则返回指向该模块的结构体指针,否则返回NULL。其中关于结构体struct module的定义见本章中关于find_module()函数的分析。

实例解析:

编写测试文件:__module_address.c

头文件及全局变量声明如下:#include <linux/module.h>#include <linux/init.h>MODULE_LICENSE("GPL");static int __init __module_address_init(void);static void __exit __module_address_exit(void);

模块加载函数:int a_module(){return 0;}int __init __module_address_init(void){struct module * ret;//用于接收测试函数返回值unsigned long addr=(unsigned long)a_module;//得到内核符号a_module的地址/*调用__module_address()函数之前,必须禁止中断,以防止模块再执行*操作期间被释放*/preempt_disable();//禁止抢占ret=__module_address(addr);preempt_enable();//允许抢占/*如果返回不为空,则输出该模块的信息*/if(ret!=NULL){printk("<0>ret->name:%s\n",ret->name);//输出模块名printk("<0>ret->state:%d\n",ret->state);//输出模块状态printk("<0>ret->core_size:%d\n",ret->core_size);//输出模块core段所占空间大小printk("<0>refs of%s is%d\n",ret->name,module_refcount(ret));输出模块引用计数}else{rintk("<0>__module_address return NULL!\n");}return 0;}

模块退出函数:void __exit __module_address_exit(void){printk("<0>module exit ok!\n");}

模块加载、退出函数调用:module_init(__module_address_init);module_exit(__module_address_exit);

实例运行结果及分析:

首先编译模块,执行命令insmod __module_address.ko插入模块,然后执行命令dmesg-c,将会出现如图2-1所示的结果。图 2-1 插入__module_address模块后系统输出信息

结果分析:

在该测试程序中,首先令函数__module_address()的参数addr取值为函数a_module()的入口地址,显然addr所表示的内存地址在待加载模块__module_address中。

然后调用__module_address()函数,为了防止模块被释放,需要禁止抢占,宏preempt_disable()和preempt_enable()分别用来实现禁止内核抢占和允许内核抢占。如果查找到内存地址addr所属的模块,则输出该模块的name、state等信息。由图2-1中的输出信息可知,ret->name恰为"__module_address",ret->state为1(即表示该模块处于正在被加载的MODULE_STATE_COMING状态,见下面关于枚举类型module_state的说明),ret->core_size为1396字节,然后调用module_refcount()得到该模块的引用计数为1。

分析中涉及了枚举类型module_state,它定义了模块的三种状态,其具体定义请参见本章中关于函数find_module()的分析。函数module_refcount()的功能是得到模块的引用计数,具体请见本章中该函数的分析。函数:__module_ref_addr()

文件包含:#include <linux/module.h>

函数定义:

函数在内核源码中的位置:linux-2.6.30/include/linux/module.h

函数定义格式:static inline local_t*__module_ref_addr(struct module * mod,int cpu)

函数功能描述:

该函数的功能是获得模块mod的引用计数所在的内存地址单元,从而通过改变该地址单元的内容,实现对其引用计数的改变。

输入参数说明:

mod:指向模块结构体的指针,模块结构体module包含模块的名称、状态、所属的模块链表等,关于结构体module的定义请参见本章关于内核函数find_module()的分析。

cpu:cpu ID号。

返回参数说明:

结构体local_t的定义在内核文件linux-2.6.30/arch/x86/include/asc/local.h中(笔者的系统是基于x86体系结构的),其定义如下:typedef struct{atomic_long_t a;}local_t;

其中atomic_long_t相当于一个长整型类型。

函数返回值为local_t类型的指针,该指针指向的内容即为参数mod所表示模块的引用计数。

实例解析:

编写测试文件:__module_ref_addr.c

头文件及全局变量声明如下:#include <linux/module.h>#include <linux/init.h>MODULE_LICENSE("GPL");static int __init __module_ref_addr_init(void);static void __exit __module_ref_addr_exit(void);

模块加载函数:int __init __module_ref_addr_init(void){local_t * addr;unsigned int cpu=get_cpu();//获取当前cpu ID/*addr为指向当前模块引用计数的指针*/addr=__module_ref_addr(THIS_MODULE,cpu);printk("<0>addr:%lx\n",(unsigned long)addr);printk("<0>originally,\n");//输出初始时当前模块的引用计数printk("<0>refs of this module is:%d\n",module_refcount(THIS_MODULE));local_inc(addr);//实现将addr所指向的内容加1printk("<0>after calling local_inc,\n");printk("<0>refs of this module is:%d\n",module_refcount(THIS_MODULE));local_dec(addr);//实现将addr所指向的内容减1printk("<0>after calling local_dec,\n");printk("<0>refs of this module is:%d\n",module_refcount(THIS_MODULE));put_cpu();//允许抢占preempt_enable()return 0;}

模块退出函数:void __exit __module_ref_addr_exit(void){printk("<0>module exit ok!\n");}

模块加载、退出函数调用:module_init(__module_ref_addr_init);module_exit(__module_ref_addr_exit);

实例运行结果及分析:

首先编译模块,执行命令insmod __module_ref_addr.ko插入模块,然后执行命令dmesg-c,将会出现如图2-2所示的结果。图 2-2 插入__module_ref_addr模块后系统输出信息

结果分析:

在该测试程序中,用到了宏get_cpu(),put_cpu(),local_inc(),local_dec(),它们的定义如下:

其中get_cpu()是禁止抢占,并且得到当前的cpu ID号,put_cpu()则是允许抢占。为了防止操作期间模块被释放,这里添加禁止抢占操作,并在操作完成后允许抢占。

local_inc(l)则是将l所指向的内存单元的内容加1,local_dec(l)是将l所指向的内存单元的内容减1。

将指向当前模块的指针THIS_MODULE和当前cpu ID作为参数传递给函数__module_ref_addr()得到模块THIS_MODULE的引用计数指针addr。初始时,先通过调用module_refcount()函数输出当前模块的引用计数(见本章中关于该函数的分析),由输出信息可知为1,然后通过操作local_inc(addr)将引用计数的值加1,由图2-2显示的信息可知,引用计数增加1后变为2,最后再调用宏local_dec(addr),将引用计数减1。

由上面的分析可知,函数__module_ref_addr()返回的指针addr指向了当前模块引用计数所在的内存单元。函数:__module_text_address()

文件包含:#include <linux/module.h>

函数定义:

函数在内核源码中的位置:linux-2.6.30/kernel/module.c

函数定义格式:struct module*__module_text_address(unsigned long addr)

函数功能描述:

该函数的功能是获得一个模块指针,但必须满足条件:addr所表示的内存地址落在该模块的代码段中。

输入参数说明:

addr:表示内存地址。

返回参数说明:

返回值是一个struct module类型的指针,如果内存地址addr在某一模块的代码段内,则返回指向该模块的指针,如果addr不在模块的地址空间内或者不在代码段内,则返回NULL。

关于结构体struct module的定义,请参见本章中对find_module()函数的分析。

实例解析:

编写测试文件:__module_text_address.c

头文件及全局变量声明如下:#include <linux/module.h>#include <linux/init.h>MODULE_LICENSE("GPL");static int __init __module_text_address_init(void);static void __exit __module_text_address_exit(void);int fun_a(){return 0;}static int var_b=4;

模块加载函数:int __init __module_text_address_init(void){unsigned long addr=(unsigned long)fun_a;//addr为函数fun_a的入口地址struct module * ret;preempt_disable();//禁止抢占ret=__module_text_address(addr);preempt_enable();//允许抢占/*如果查找成功,则输出该模块的信息*/printk("<0>it's about fun_a:\n");if(ret!=NULL){printk("<0>ret->name:%s\n",ret->name);//输出模块名printk("<0>ret->state:%d\n",ret->state);//输出模块状态/*输出模块core段所占空间大小*/printk("<0>ret->core_size:%d\n",ret->core_size);}else{printk("<0>fun_a is not in text area!\n");}addr=(unsigned long)&var_b;//addr为静态全局变量var_b的地址preempt_disable();ret=__module_text_address(addr);preempt_enable();/*如果查找成功,则输出该模块的信息*/printk("<0>\nit's about var_b:\n");if(ret!=NULL){printk("<0>ret->name:%s\n",ret->name);//输出模块名printk("<0>ret->state:%d\n",ret->state);//输出模块状态printk("<0>ret->core_size:%d\n",ret->core_size);//输出模块core段所占空间大小}else{printk("<0>var_b is not in text area!\n");}return 0;}

模块退出函数:void __exit __module_text_address_exit(void){printk("<0>module exit ok!\n");}

模块加载、退出函数调用:module_init(__module_text_address_init);module_exit(__module_text_address_exit);

实例运行结果及分析:

首先编译模块,执行命令insmod __module_text_address.ko插入模块,然后执行命令dmesg-c,将会出现如图2-3所示的结果。图 2-3 插入__module_text_address模块后系统输出信息

结果分析:

在该测试程序中,定义了一个函数fun_a(),其存在于程序空间的代码段中;定义了一个全局静态变量var_b,其将位于程序空间的数据段中。

程序示例中调用__module_text_address()时,为了防止模块被释放,需要禁止抢占,宏preempt_disable()和宏preempt_enable()分别用来实现禁止内核抢占和允许内核抢占。

首先,将fun_a()的入口地址作为实参传递给__module_text_address()函数,该函数判断所给的实参地址是否位于某一模块代码段中,并且返回相应的模块指针。从输出信息可知,ret不为空,并且ret->name为"__module_text_address",ret->state为1(即该模块处于表示正在被加载的MODULE_STATE_COMING状态,关于模块状态见本章中find_module()函数的分析),ret->core_size为1512字节。以上说明fun_a()的入口地址确实位于某一模块的代码段,而且该模块为当前正被加载的模块__module_text_address。

然后,将全局静态变量var_b的地址作为实参传递给__module_text_address()函数,从输出信息可知,由输出语句为"var_b is not in text area!"可知ret为空。这说明var_b所在的内存单元并不在模块的代码段,因为var_b是全局静态变量,它存在于数据段中。

函数fun_a()和变量var_b在模块插入到内核后,作为内核符号存在,在虚拟文件系统proc的kallsyms文件中有对它们相关的描述,如图2-4所示。图 2-4 fun_a和var_b在kallsyms文件中的相关信息

在图2-4中显示的两行中,关于fun_a的显示,第二列为"t",它表示符号fun_a位于代码段;关于var_b的显示,第二列为"d",它表示var_b位于数据段。这与上面的测试结果是一致的。函数:__print_symbol()

文件包含:#include <linux/kallsyms.h>

函数定义:

函数在内核源码中的位置:linux-2.6.30/kernel/kallsyms.c

函数定义格式:void __print_symbol(const char * fmt,unsigned long address)

函数功能描述:

该函数的功能与sprint_symbol()的函数功能相似(见本章中sprint_symbol()函数的分析),实际上,__print_symbol()函数的实现中调用了函数sprint_symbol()。

该函数根据一个内存中的地址address查找一个内核符号,并将该符号的基本信息(例如符号名name)、它在内核符号表中的偏移offset和大小size、所属的模块名(如果有的话)等信息以格式化串fmt的形式输出。而sprint_symbol()函数则是将这些信息放到文本缓冲区buffer中。

输入参数说明:

fmt:输出内核符号基本信息所依据的格式串,由于符号信息是连接成一个字符串的(见本章中sprint_symbol()函数的分析),因此fmt格式化串中一般包含一个"%s"。

address:内核符号中的某一地址,为输入型参数。

返回参数说明:

该函数无返回值。

实例解析:

编写测试文件:__print_symbol.c

头文件及全局变量声明如下:#include <linux/module.h>#include <linux/init.h>#include <linux/kallsyms.h>MODULE_LICENSE("GPL");static int __init __print_symbol_init(void);static void __exit __print_symbol_exit(void);//符号a_symbolint a_symbol(){return 1;}EXPORT_SYMBOL(a_symbol);

模块加载函数:int __init __print_symbol_init(void){char * fmt;//格式化字符串unsigned long address;//表示符号地址char * name;//模块名字struct module * fmodule=NULL;//指向一个模块的指针address=(unsigned long)__builtin_return_address(0);//当前函数的返回地址fmt="it's the first part,\n%s";__print_symbol(fmt,address);printk("<0>\n\n");name="vboxvideo";fmodule=find_module(name);//查找模块名为"vboxvideo"的模块if(fmodule!=NULL){printk("<0>fmodule->name:%s\n",fmodule->name);/*将模块的内存起始地址赋值给address*/address=(unsigned long)fmodule->module_core;fmt="it's the second part,\n%s";__print_symbol(fmt,address);}printk("<0>\n\n");/*将当前模块中符号a_symbol的地址加上偏移量5赋值给address*/address=(unsigned long)a_symbol+5;fmt="it's the third part,\n%s";__print_symbol(fmt,address);return 0;}

模块退出函数:void __exit __print_symbol_exit(void){printk("<0>module exit ok!\n");}

模块加载、退出函数调用:module_init(__print_symbol_init);module_exit(__print_symbol_exit);

实例运行结果及分析:

首先编译模块,执行命令insmod __print_symbol.ko插入模块,然后执行命令dmesg-c,将会出现如图2-5所示的结果。图 2-5 插入__print_symbol模块后系统输出信息

结果分析:

测试程序中调用了find_module()内核函数,它的功能是根据所给的模块名字来获得模块描述符指针的,关于其详细说明见本章中该函数的分析。

关于图2-5所示输出信息内容的说明请参考本章sprint_symbol()函数的分析。

格式化串赋值如fmt="it's the first part,\n%s",其类似于C语言中的printf()函数的第一个参数。调用__print_symbol(fmt,address),根据address查找特定符号的基本信息,先把信息存放到文本缓冲区buffer中,然后以格式化串fmt的形式输出。函数:__symbol_get()

文件包含:#include <linux/module.h>

函数定义:

函数在内核源码中的位置:linux-2.6.30/kernel/module.c

函数定义格式:void*__symbol_get(const char * symbol)

函数功能描述:

该函数的功能是根据给定的内核符号名symbol,获得该符号的内存地址。

输入参数说明:

symbol:字符串常量,代表内核符号名。

返回参数说明:

返回一个void类型指针,其值代表内核符号symbol的地址。如果不存在内核符号symbol,则返回NULL。

实例解析:

编写测试文件:__symbol_get.c

头文件及全局变量声明如下:#include <linux/module.h>#include <linux/init.h>MODULE_LICENSE("GPL");static int __init __symbol_get_init(void);static void __exit __symbol_get_exit(void);

模块加载函数:int __init __symbol_get_init(void){const char *symbol_name;void * addr;symbol_name="symbol_A";//内核符号名为"symbol_A"addr=__symbol_get(symbol_name);if(addr!=NULL)printk("<0>the address of%s is:%lx\n",symbol_name,(unsigned long)addr);elseprintk("<0>%s isn't found\n",symbol_name);symbol_name="symbol_0";//内核符号名为"symbol_0"addr=__symbol_get(symbol_name);if(addr!=NULL)printk("<0>the address of%s is:%lx\n",symbol_name,(unsigned long)addr);elseprintk("<0>%s isn't found\n",symbol_name);return 0;}

模块退出函数:void __exit __symbol_get_exit(void){printk("<0>module exit ok!\n");}

模块加载、退出函数调用:module_init(__symbol_get_init);module_exit(__symbol_get_exit);

实例运行结果及分析:

首先编译模块,执行命令insmod __symbol_get.ko插入模块,然后执行命令dmesg-c,会出现如图2-6所示的结果。图 2-6 插入__symbol_get模块后系统输出信息

结果分析:

在Linux内核中,内核空间中的每一个函数和变量都会有对应的符号,这部分符号也可称作内核符号,内核使用变量和函数的地址(指针)来访问对应的变量和函数。内核符号表提供了变量和函数符号名到地址的映射。

在测试程序中试图通过__symbol_get()函数获得内核符号"symbol_A"和"symbol_0"的内存地址,由图2-6显示的信息可知,内核符号"symbol_A"的内存地址为0xe3072000,而"symbol_0"则未找到相应的内存地址,这是因为它在内核符号表中不存在。

虚拟文件系统proc的kallsyms列出了对所有内核符号的相关描述,这里输出关于内核符号"symbol_A"和"symbol_0"的相关信息,如图2-7所示。图 2-7 symbol_A和symbol_0在kallsyms文件中的相关信息

从图2-7中第6行可以看到在kallsyms文件中存在内核符号"symbol_A",且其对应的内存地址为0xe3072000(第一列),而未找到关于内核符号"symbol_0"的信息,说明该内核符号不存在,所以通过__symbol_get()函数找不到其内存地址,即addr=NULL。函数:__symbol_put()

文件包含:#include <linux/module.h>

函数定义:

函数在内核源码中的位置:linux-2.6.30/kernel/module.c

函数定义格式:void __symbol_put(const char * symbol)

函数功能描述:

该函数的功能是根据给定的内核符号名symbol,找到其所在的内核模块,并将该模块的引用计数减1。

输入参数说明:

symbol:字符串常量,代表内核符号名。

返回参数说明:

该函数无返回值。

实例解析:

编写测试文件:__symbol_put.c

头文件及全局变量声明如下:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载