Linux内核模块设计

来源:互联网 发布:python while循环语句 编辑:程序博客网 时间:2024/05/22 02:06

UNIT 1  Linux内核模块设计

 

 

 

 

 

 

1.  单体内核 VS 微内核

2.  Hello,kernel模块实例

3.  内核模块的Makefile

4.  模块的加载与测试

5.  内核符号表

6.  应用程序 VS Linux 内核模块

7.  其他

 

1.  单体内核 VS 微内核

当前流行的操作系统内核的设计方式中,一称为单体内核,又称为宏内核,也就是说内核本身只是一个程序,而内核所有的功能都是链接在这个程序的内部,如果某个进程要使用某个功能的话,将是进入内核空间,然后调用相对应的功能函数来实现的。一般我们所常见的 Unix系统都是采用单体内核的方式设计的,比如传统的UNIX, 现在大行其道的Linux,以及商业化非常成功的SUN Microsystem推出的Solaris

当一个操作系统内核作为一个程序的时候,有着许多的不足之处。我们都知道驱动程序是工作在内核中的,当前设计硬件的厂商多余牛毛,每一种硬件设备都必须配以驱动程序,而在单体内核的设计中,驱动程序又必须是和操作系统内核编译在一起的,硬件设备更新日新月异,每天都有新的硬件出现,每天都有更新版本的驱动产生,这样操作系统的发行商为了支持这些硬件每天都在编译新的操作系统内核,然后重新发布操作系统,这何其烦也?而且操作系统内核镜像文件会变的何其庞大?

其二,单体内核的操作系统镜像是作为一个程序运行的,并且是常驻在物理内存之中,当你安装并运行了这个操作系统后,你会发现你系统的物理资源都完全被你根本就没有,或者一辈子都适用不到的硬件设备的驱动程序占据着,而留给你的资源却寥寥无几,此时你有何感想?

微内核,恰恰不会吹灰之力就解决的上述的问题。

微内核是在上个世纪七八十年代提出来的一种操作系统的设计方式,微内核本身只提供最基本的操作系统的功能,比如进程调度与消息传递等,其他的功能由其独立的模块提供,每个独立的功能模块都可以是一个进程。当我们需要使用某个功能的时候,我们只需要在运行的操作系统里安装这个模块,并且运行对应服务,当这个功能不再需要的时候,我们可以停止这个服务,这样这个功能模块将不占据系统内存和处理器的资源,而不会破坏当前的系统正常运行,各功能模块与微内核之间是彼此独立的,这样即使功能模块崩溃的时候,系统也不至于挂机。

当一个进程如果需要使用某个功能,就会向微内核发一个消息请求,微内核转发这个请求到对应的功能模块的进程,然后把结果通过微内核再返回给请求进程。如果这个功能模块不存在,那么这个请求也将失败。此时我们可以在系统运行的情况下,安装这个功能模块,可能是一个驱动,也可能是一个服务进程。这样这个模块就可以为其他进程服务了。

    所以,微内核设计方式带来的优势:

ü  模块化的方式设计操作系统,模块的设计者只需要关注自己的功能模块。

ü  操作系统的更新时,除了微内核本身,可以动态的更新其他的功能模块

ü  在系统运行的时候,可以根据需要动态的使能/禁止对应的模块,以释放计算机的资源。

当前商业上使用较多的微内核方式设计的操作系统有风靡全球的Windows系统,用于图像处理的Mac OS系统,以及在实时嵌入式领域独领风骚的WindRiver VxWorks系统。

从上面的描述看来,模块化设计的微内核操作系统似乎是大势所趋,但是单体内核方式设计的Linux, Solaris却未在市场竞争中甘拜下风? 这是为何呢?

    深入研究,就会发现,其一,微内核在性能上有不可避免的弱点,因为一个进程需要使用某个功能模块的时候,需要通过消息机制来传输。这里涉及到消息传递进程切换的时间,这比函数调用的消耗大的多。其二, Unix的程序员向来都是天赋甚高的一类怪才,灵机一动,就会想到难道在单体内核中就不能实现类似于微内核的模块的方法吗?

    答案当然是肯定的,可以,这样我们既满足了动态的添加功能模块,删除功能模块,又可以利用函数调用带来性能上的优势。下面一节,我们就初窥Linux 内核模块的行径。

【练习】

(1)Linux是从Minix演进过来的,Minix采用的是那种设计方式?

(2)有没有微内核设计的类Unix系统? 如果有,其名称是什么?

2.  Hello,kernel模块实例

在学习C语言的时候我们一开篇会学习hello,world的程序,我相信大家都觉得及其简单,以至于我重复写下面的程序,大家都觉得是多余的:

#include <stdio.h>

int main()

{

    printf(“hello,world/n”);

    return 0;

}

请同学们思考两个问题:

ü  为什么我们必须写一个main()函数?内核的C程序需要main吗?

ü  在这里#include <stdio.h>是为了让我们使用printf(), 实际上他们都是C语言库的函数,他们能够在内核程序中使用吗?

我们先回答这两个问题, C语言的应用程序必须要有一个main()函数,因为它是应用程序的入口,至于为什么非要是这样个入口,我们只有一个答案: 规定的,强制性的C应用程序有应用程序的规定, 作为内核模块有内核模块的规定,所以我们在写内核模块框架的时候,记住这是规定就可以了。

至于第二个问题比较重要:应用程序可以调用C语言标准库的函数,而内核程序将是绝对不可以的,如果大家还记得我们说fopen,是依赖于open的系统调用,而系统调用是有内核导出的话,那么如果我们能够在内核程序中使用标准函数库,那么就转入了到底是鸡生蛋,还是蛋生鸡的怪圈。

下面的程序就是Linux内核模块的标准的框架(请大家在初次学习的时候看老师是如何写这段代码的)

#include <linux/module.h>

#include <linux/init.h>

#include <linux/kernel.h>          //使用printk,需要包含此文件

MODULE_LICENSE(“Dual BSD/GPL”);

MODULE_AUTHOR(“stephanxu@eetek”);

MODULE_DESCRIPTION(“the first kernel module”)

static int __init hello_init(void)

{

    return 0;

}

static void __exit hello_exit(void)

{

}

module_init(hello_init);

module_exit(hello_exit);

-----------------------------------------------------------------------

这就是一个hello内核模块的框架,如果我们要实现打印出hello,kernel 我们只需要在修改hello_init:

static int __init hello_init(void)

{

    printk(“hello,kernel/n”);

    return 0;

}

模块的框架包含下面四个部分:

(1)     模块在加载的时候需要执行的module_init(function),以及在module_init()中指定的function,模块在卸载的时候执行的module_exit(function)以及在module_exit()中定义的function.如果声明使用module_exit(),那么此模块将不具备动态卸载功能。

(2)     需要定义module_init()调用的初始化函数,以及在module_exit()中使用的清理函数。只有当初始化函数返回非负值(因为在内核中,负值表示操作失败),内核模块才能被正确的加载,否则模块加载失败。而清理函数返回void类型。一般情况下,初始化函数是在模块加载的时候用来申请资源,而清理函数是在模块卸载的时候用来释放资源,有点类似于C++中的constructordeconstructor.

(3)     头文件, 对于内核模块来讲,必须要使用<linux/module.h><linux/init.h>。需要特别注意的是,这里面使用了<>来包含头文件,但很明显这两个头文件都不会是标准函数的头文件,因为,正如前面所说,内核模块不能引用标准函数库的函数。这里的头文件实际上来自于Linux的内核源代码路径下的$(KERNELSRC)/include目录。

(4)     MODULE_XXX表示的相关内容,这些都是对当前内核模块的描述,虽然不是必须的,但是一般情况下,还是请你们填上几项,特别是模块的许可问题。 当然也让你有扬名立万的机会,同时你也该负有责任。你对模块有更详尽的描述将对你以后调试错误是有帮助的。Modinfo可以让你更快的识别模块,如果有需要,请参考LDD(<<Linux 设备驱动程序>>,以后均简称为LDD)中有关更多的MODULE_XXX的宏描述。

3.  内核模块的Makefile

若要编译hello的内核模块,最简单的方法就是要编写如下内容的Makefile(若对Makefile比较陌生,请参考Linux开发基础讲义)

-----------------------------------------------------------------------

ifeq ($(KERNELRELEASE),)

KERNELDIR ?=/usr/src/kernels/2.6.18-8.el5xen-i686   #指向你的内核代码目录

#KERNELDIR ?=/home/eetek/linux-2.6.29-eetek

PWD := $(shell pwd)

all:

    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules      #编译内核模块的指令

clean:                                               #删除生成的所有文件

    rm -rf modules.* *.o *~ core .depend .*.cmd *.ko

rm –rf *.mod.c .tmp_versions Module*

.PHONY: modules modules_install clean

else

    obj-m := hello.o                                 #你要编译的模块名称

endif

-----------------------------------------------------------------------

对于上述的Makefile需要特别说明如下几点:

(1)     $(MAKE) -C $(KERNELDIR) M=$(PWD) modules       #编译内核模块的指令

ü  理解这条指令,首先需要理解make命令选项-C的作用,-C是指Change Directory

ü  MAKEMakefile中的宏变量,和KERNELDIR相似,要引用这个宏变量的值需要使用$符号。在这里实际上就是指向make程序。注:此时应该写为$(MAKE),而不能写成$MAKE,请思考为什么。

ü  M=$(PWD) 是指要编译的内核模块的源程序在那个目录下,$(PWD)指与Makefile在同一目录下,也就是当前目录。(pwd命令的作用是:print work directory).modules指明要按照内核模块的方式编译。

结合起来理解,就是进入$(KERNELDIR)的目录,并且执行make命令,如果没有M=$(PWD) modulesmake –C $(KERNELDIR)将是编译Linux内核映像的指令。M=$(PWD) modules也就是按照$(KERNELDIR)内核模块的方式编译$(PWD)目录下的程序。modules实际上是$(KERNELDIR)目录下的Makefile的一个Target入口.

    既然是为$(KERNELDIR)编译内核模块,所以必须要求$(KERNELDIR)这个内核必须正确配置,并且得到编译的,以为编译模块的过程需要使用内核配置的相关信息,比如处理器的类型等。至于如何配置Linux内核,请参考Linux系统移植过程中的相关实验。

(2)     obj-m = hello.o       #你要编译的模块名称

这里必须和$(PWD)目录下C程序文件的名称相对应,如果C程序文件为world.c,那么必须修改成obj-m = world.o.

    和很多C语言程序一样,一个内核模块也可能有多个C程序文件,那么如何修改Makefile让其编译成一个内核模块呢?此时,需要将obj-m = hello.o换成如下代码:

    obj-m = hello.o

    hello-objs = hello1.o, hello2.o

(3)     cleanMakefile的入口,其目的是为了清除所有生成的文件。

正确的配置和编译了$(KERNELDIR)的内核,就可以编译hello的内核模块了,你将会在当前的目录中看到一个hello.ko的内核模块,下面我们看看如何想内核中动态的加载和删除。

4.  模块的加载和测试

Linux内核模块是为了解决单体内核的不足而提出来的一种技术,所以,它必须要能够动态的加载和删除。当我们获得hello.ko的内核模块之后,我们就可像内核中加载了,但是在加载之前,我们必须注意一个非常重要的问题:

我们解释Makefile的时候说明,我们的内核模块是为$(KERNELDIR)这个内核服务的,所以我们不能将编译好的内核模块胡乱的加载到一个运行的内核里面,否则就会出现牛头不对马嘴的笑话。这也是我们众多学员容易犯下的错误之一,经常拿为虚拟机里RHEL5编译的内核模块往ARM开发板上加载,或者反之。所以,请大家在试验前一定要用file hello.ko查看一下这个内核模块是ARM版本的还是X86版本。同样的, 即使是同样一个版本,如果对内核的配置不一样,也可能会导致不能加载的情况,所以,如果出现加载不进的情况,请首先检查内核模块的file属性,然后检查是否是内核配置不匹配倒置的,总之,运行的内核一定要和编译内核模块的时候指定的内核是一致的。

加载内核使用的命令是insmod,例如

#insmod hello.ko

只有Linuxroot用户才有权限向内核中添加模块,请大家思考一下为什么?

    如果没有出现出错信息,就表明加载成功,可以通过lsmod查看当前运行的内核中加载的模块情况。属性分别表示模块的大小,以及被几个模块使用,分别是那些模块。

 

Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

可能当前的系统里面动态加载的模块过多,此时你可以用lsmod |grep hello来查找。

    rmmod这个命令可以让我们从内核中动态的卸载模块,如下:

    #rmmod hello      #注意这里不需要ko,而且可在任何目录下操作,2.6支持带有ko

    如果我们在模块的初始化函数和清除函数中有打印信息的话,可能会在控制台上显示出来,或者通过dmesg可以查看到自开机以来所有的缓冲的打印信息。

    那么在insmodrmmod的时候到底发生了什么呢?

    这里实际上包含两个重要的过程,我们知道内核模块是kernel object,也就是说它仅仅是目标代码,没有经过链接。所以在insmod的第一个过程是解析内核模块外部符号。这类似于一个链接的过程,如果有不能解析的符号,就会报unsolved symbol,此时模块加载会失败。

    外部符号被正确的解析后,就开始加载内核模块,就会调用在module_init()中声明的初始化函数,只有当初始化函数返回非负值,模块才能正确的被加载。

    对于rmmod也有两个过程, 首先检查是否有模块依赖于此模块,也就是说只有当lsmod查看到此模块used一栏的值为0时,模块才能被安全的卸载。卸载的时候调用module_exit()中声明的清除函数,释放占有的资源。

 

5.  内核符号表

上一节我们提到,模块在加载的会进行连接操作,来解析内核模块的外部符号,比如前面例中的printk,对于hello模块来说就是一个外部的符号,如果能够被正确的解析,那么将没什么问题。对于printk是可以被正确的解析的,但是如果,我们把printk换成另一个函数,比如print_hello(),而我们有假设print_hello是一个外部函数,又会如何呢?代码示例如下:

-----------------------------------------------------------------------

#include <linux/module.h>

#include <linux/init.h>

#include <linux/kernel.h>          //使用printk,需要包含此文件

MODULE_LICENSE(“Dual BSD/GPL”);

MODULE_AUTHOR(“stephanxu@eetek”);

MODULE_DESCRIPTION(“the first kernel module”)

extern void print_hello(void);  //声明需要使用此模块外,内核中的函数

static int __init hello_init(void)

{

    print_hello();

    return 0;

}

static void __exit hello_exit(void)

{

}

module_init(hello_init);

module_exit(hello_exit);

-----------------------------------------------------------------------

使用与刚才相同的Makefile,编译,看看结果如何?

Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

你会看到,这里生成了hello.ko,但是提出了一个警告,我们试着加载这个模块:

 

Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

我们会得到一个加载的错误,这条信息告诉我们在加载hello.ko模块的时候,有一个Sysmbol不能被解析,分析一下和前面程序的区别,这里不能解析的当然只有print_hello().

大家可能已经知道,因为Linux内核符号表中没有提供print_hello()的符号,所以不能被解析,解决问题的办法就是向Linux内核符号表中添加print_hello()的符号,怎么才能做的到呢?

因为内核此时已经在运行中,所以,我们不能只能修改内核,让内核导出print_hello这个符号到内核符号表中,这里有一个替代的方法:我们可以编写一个新的内核模块,让这个内核模块导出print_hello()到内核符号表中,代码如下(print_hello.c:

-----------------------------------------------------------------------

#include <linux/module.h>

#include <linux/init.h>

#include <linux/kernel.h>          //使用printk,需要包含此文件

MODULE_LICENSE(“Dual BSD/GPL”);

MODULE_AUTHOR(“stephanxu@eetek”);

MODULE_DESCRIPTION(“the first kernel module”)

void print_hello(void)                 //定义print_hello函数

{

    Printk(“hello,world/n”);

}

static int __init print_hello_init(void)

{

    return 0;

}

static void __exit print_hello_exit(void)

{

}

EXPORT_SYMBOL(print_hello);            //导出符号到内核符号表中

module_init(print_hello_init);

module_exit(print_hello_exit);

-----------------------------------------------------------------------

print_hello.c编写Makefile,并编译生成print_hello.ko.而后加载print_hello.ko, ,再尔后加载hello.ko,你将会发现,原来的Unknown Symbol in Module不见了,操作过程如下:

 

Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

此时,我们说hello.ko的模块依赖于print_hello.ko, 通过lsmod就可以看出:

 

Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

print_hello.ko模块被hello.ko模块使用,所以如果要想卸载print_hello.ko模块,必须先卸载hello模块,这和对正在使用的文件进行删除的道理是一样的,错误信息如下:

Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

    从上可以看出,如果希望想内核符号表导出一个符号,必须使用EXPORT_SYMBOL()的宏,否则这个符号将仅仅只在模块内部可。此宏有两个版本:

EXPORT_SYMBOL():

EXPORT_SYMBOL_GPL():

他们的区别在于:前一宏定义的任一个使得给定的符号在模块外可用. _GPL 版本的宏定义只能使符号对 GPL 许可的模块可用.

符号必须在模块文件的全局部分输出, 在任何函数之外, 因为宏定义扩展成一个特殊用

途的并被期望是全局存取的变量的声明. 这个变量存储于模块的一个特殊的可执行部分( 一个

"ELF " ), 内核用这个部分在加载时找到模块输出的变量. ( 感兴趣的读者可以看 <linux/module.h> 获知详情, 尽管并不需要这些细节使东西动起来. )

 

 

 

 

 

6.  应用程序 VS Linux内核模块

 

不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说, 模块初始化函数的任务是为以后调用模块的函数做准备; 好像是模块说,"我在这里, 这是我能做的"。模块的退出函数( 例子里是 hello_exit )就在模块被卸载时调用. 它好像告诉内核,"我不再在那里了,不要要求我做任何事了"。这种编程的方法类似于事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是. 另外一个主要的不同, 在事件驱动的应用程序和内核代码之间, 是退出函数: 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.

 偶然地, 卸载模块的能力是你将最欣赏的模块化的其中一个特色, 因为它有助于减少开发时间; 你可测试你的新驱动的连续的版本, 而不用每次经历漫长的关机/重启周期.

作为一个程序员, 你知道一个应用程序可以调用它没有定义的函数: 连接阶段使用合适的函数库解决了外部引用. printf 是一个这种可调用的函数并且在 libc 里面定义. 一个模块, 在另一方面, 只连接到内核, 它能够调用的唯一的函数是内核输出的那些; 没有库来连接. hello.c 中使用的 printk 函数, 例如, 是在内核中定义的 printf 版本并且输出给模块. 它表现类似于原始的函数, 只有几个小的不同, 首要的一个是缺乏浮点的支持.

 

用户空间和内核空间

A module runs in kernel space, whereas applications run in user space. This concept is at the base of operating systems theory. 一个模块在内核空间运行, 而应用程序在用户空间运行. 这个概念是操作系统理论的基础.

操作系统的角色, 实际上, 是给程序提供一个一致的计算机硬件的视角. 另外, 操作系统必须承担程序的独立操作和保护对于非授权的资源存取. 这一不平凡的任务只有 CPU 增强系统软件对应用程序的保护才有可能. 每种现代处理器都能够加强这种行为. 选中的方法是 CPU 自己实现不同的操作形态(或者级别). 这些级别有不同的角色, 一些操作在低些级别中不允许; 程序代码只能通过有限的几个门从一种级别切换到另一个. Unix 系统设计成利用了这种硬件特性, 使用了两个这样的级别. 所有当今的处理器至少有两个保护级别, 并且某些, 例如 x86 家族, 有更多级别; 当几个级别存在时, 使用最高和最低级别. Unix , 内核在最高级运行( 也称之为超级模式 ), 这里任何事情都允许, 而应用程序在最低级运行(所谓的用户模式), 这里处理器控制了对硬件的直接存取以及对内存的非法存取.

我们常常提到运行模式作为内核空间和用户空间. 这些术语不仅包含存在于这两个模式中不同特权级别, 还包含有这样的事实, 即每个模式有它自己的内存映射 -- 它自己的地址空间.

Unix 从用户空间转换执行到内核空间, 无论何时一个应用程序发出一个系统调用或者被硬件中断挂起时. 执行系统调用的内核代码在进程的上下文中工作 -- 它代表调用进程并且可以存取该进程的地址空间. 换句话说, 处理中断的代码对进程来说是异步的, 不和任何特别的进程有关.

模块的角色是扩展内核的功能; 模块化的代码在内核空间运行. 经常地一个驱动进行之前提到的两种任务: 模块中一些的函数作为系统调用的一部分执行, 一些负责中断处理.

 

 

内核的并发

 

内核编程与传统应用程序编程方式很大不同的是并发问题. 大部分应用程序, 多线程的应用程序是一个明显的例外, 典型地是顺序运行的, 从头至尾, 不必要担心其他事情会发生而改变它们的环境. 内核代码没有运行在这样的简单世界中, 即便最简单的内核模块必须在这样的概念下编写, 很多事情可能马上发生.

内核编程中有几个并发的来源. 自然的, Linux 系统运行多个进程, 在同一时间, 不止一个进程能够试图使用你的驱动. 大部分设备能够中断处理器; 中断处理异步运行, 并且可能在你的驱动试图做其他事情的同一时间被调用. 几个软件抽象( 例如内核定时器, 7 章介绍 )也异步运行. 而且, 当然,Linux 可以在对称多处理器系统( SMP )上运行, 结果是你的驱动可能在多个 CPU 上并发执行. 最后, 2.6, 内核代码已经是可抢占的了; 这个变化使得即便是单处理器会有许多与多处理器系统同样的并发问题.

结果, Linux 内核代码, 包括驱动代码, 必须是可重入的 -- 它必须能够同时在多个上下文中运行. 数据结构必须小心设计以保持多个执行线程分开, 并且代码必须小心存取共享数据, 避免数据的破坏. 编写处理并发和避免竞争情况( 一个不幸的执行顺序导致不希望的行为的情形 )的代码需要仔细考虑并可能是微妙的. 正确的并发管理在编写正确的内核代码时是必须的。

 

当前进程

 

尽管内核模块不象应用程序一样顺序执行, 内核做的大部分动作是代表一个特定进程的. 内核代码可以引用当前进程, 通过存取全局项 current, 它在 <asm/current.h> 中定义, 它产生一个指针指向结构 task_struct, <linux/sched.h> 定义. current 指针指向当前在运行的进程. 在一个系统调用执行期间, 例如 open 或者 read, 当前进程是发出调用的进程. 内核代码可以通过使用 current 来使用进程特定的信息。

际上, current 不真正地是一个全局变量. 支持 SMP 系统的需要强迫内核开发者去开发一种机制, 在相关的 CPU 上来找到当前进程. 这种机制也必须快速, 因为对 current 的引用非常频繁地发生. 结果就是一个依赖体系的机制, 常常, 隐藏了一个指向 task_struct 的指针在内核堆栈内. 实现的细节对别的内核子系统保持隐藏, 一个设备驱动可以只包含 <linux/sched.h> 并且引用当前进程. 例如, 下面的语句打印了当前进程的进程 ID 和命令名称, 通过存取结构 task_struct 中的某些字段.

printk(KERN_INFO "The process is /"%s/" (pid %i)/n",

current->comm, current->pid);

存于 current->comm 的命令名称是由当前进程执行的程序文件的基本名称( 截短到 15 个字符, 如果需要 ).

 

几个别的细节

内核编程与用户空间编程在许多方面不同. 我们将在本书的过程中指出它们, 但是有几个基础性的问题, 尽管没有保证它们自己有一节内容, 也值得一提. 因此, 当你深入内核时, 下面的事项应当牢记.

应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一个非常小的堆栈; 它可能小到一个, 4096 字节的页. 你的函数必须与这个内核空间调用链共享这个堆栈. 因此, 声明一个巨大的自动变量从来就不是一个好主意; 如果你需要大的结构, 你应当在调用时间内动态分配.

常常, 当你查看内核 API , 你会遇到以双下划线(__)开始的函数名. 这样标志的函数名通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调用这个函数, 确信你知道你在做什么."

内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存和恢复浮点处理器的状态 -- 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮点, 额外的负担不值得.

 

7.  其他

模块参数

模块也可以向应用程序那样传递参数,我们都知道应用程序的参数传递是通过main()函数的两个参数argc,argv[][]来完成的,argc是参数的个数,所有的参数都保存在argv[][]中。对于内核模块来说,稍有不同:

(1)     虽然和main函数一样,必须声明可传递的参数,但是声明方式不同,模块参数的传递需要借助于宏定义:module_param(name,type,perm);其中name就是参数的名字,而且必须在使用之前进行声明,并且最好赋有默认值;type可以取后面列表中的值,但必须与name的类型声明相同,perm在后面有相应解释。下面是对前面的hello模块的修改为hello_ext.c:

-----------------------------------------------------------------------

#include <linux/module.h>

#include <linux/init.h>

#include <linux/moduleparam.h>

#include <linux/kernel.h>          //使用printk,需要包含此文件

MODULE_LICENSE(“Dual BSD/GPL”);

MODULE_AUTHOR(“stephanxu@eetek”);

MODULE_DESCRIPTION(“the first kernel module”)

static char *greeting = “hello,world”; //greeting默认情况下是”hello,world”

static int howmany = 1;                  //默认打印一次  

 

//设置howmanygreeting作为模块的参数可以在加载时传递进来

module_param(howmany,int,S_IRUGO);    module_param(greeting,charp,S_IRUGO); 

static int __init hello_ext_init(void)

{

    for(int i=0;i<howmany;i++)

       printk(“%s/n”,greeting);

    return 0;

}

static void __exit hello_ext_exit(void)

{

}

module_init(hello_ext_init);

module_exit(hello_ext_exit);

-----------------------------------------------------------------------

为其模块修改Makefile(仅仅将其obj-m=hello.o改称hello_ext.o),然后编译,生成hello_ext.ko模块。

(2)     在加载模块的时候,我们可以还是使用insmod命令添加内核模块,我们可以在这里传递零个,或者多个模块的参数,对于我们未传递的模块参数,模块会使用默认在模块代码中定义的默认值;比如我们可以如下操作:      

       Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

通过dmesg察看输出的内容:

         Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

 

如果把greeting=”welcome”传递进去的话,可以如下操作,并得到结果:       

       Linux驱动程序设计之一---- Linux内核模块设计 - 御风亭 - 专业,诚信,责任

模块参数支持许多类型,如下表:

 bool

invbool

一个布尔型( true 或者 false)(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.

charp

一个字符指针值. 内存为用户提供的字串分配, 指针因此设置.

int

long

short

uint

ulong

ushort

基本的变长整型值. u 开头的是无符号值.

 

 

还可以传递数组参数, 用逗号间隔的列表提供的值, 模块加载者也支持. 声明一个数组参数, 使用:

module_param_array(name,type,num,perm);

这里 name 是你的数组的名子(也是参数名), type 是数组元素的类型, num 是一个整型变量, perm 是通常的权限值. 如果数组参数在加载时设置, num 被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值.

如果你确实需要一个没有出现在上面列表中的类型, 在模块代码里有钩子会允许你来定义它们; 任何使用它们的细节见 moduleparam.h. 所有的模块参数应当给定一个缺省值; insmod 只在用户明确告知它的时候才改变这些值. 模块可检查明显的参数, 通过对应它们的缺省值检查这些参数.

最后的 module_param 字段是一个权限值; 你应当使用 <linux/stat.h> 中定义的值. 这个值控制谁可以存取这些模块参数在 sysfs 中的表示. 如果 perm 被设为 0, 就根本没有 sysfs . 否则, 它出现在 /sys/module[5] 下面, 带有给定的权限. 使用 S_IRUGO 作为参数可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR 允许 root 来改变参数. 注意, 如果一个参数被 sysfs 修改, 你的模块看到的参数值也改变了, 但是你的模块没有任何其他的通知. 你应当不要使模块参数可写, 除非你准备好检测这个改变并且因而作出反应.

 

【思考】 请思考模块的参数传递和应用程序的参数传递的异同?

 

实验(1)

(1)   编写hello模块,并能为其编写Makefile

 

 

(2)   编写hello模块,此hello模块会使用一个函数print_hello;所以需要再编写一个print_hello模块,完成print_hello函数,并且导出。

 

 

3)编写一个hello_ext模块,可以在加载的时候传递参数给hello_ext模块。

 

4)在ARM开发板做测试,ARM使用NFS作为其根文件系统。

【总结】

1.  单体内核和微内核的概念,工作原理的异同,各有什么优劣势。

2.  Linux内核模块的基本框架,以及如何编写Linux 2.6的内核模块的Makefile

3.  模块的加载,删除操作,以及内核符号表的作用

4.  内核模块和应用程序的比较

原创粉丝点击