首页 > 代码库 > ldd3-2 构造和运行模块:Hello World模块笔记

ldd3-2 构造和运行模块:Hello World模块笔记

实验环境:

  1. 按照之前的搭建方法,已经在Ubuntu 5.04版本上构建了linux原始的2.6.10版本内核树;
  2. GCC是用的安装镜像自带的版本;
  3. 一切准备就绪后对虚拟机做了快照,防止内核损坏;
  4. 因为Ubuntu 5.04虚拟机下编程很麻烦,所以编码和调试都不在虚拟机下运行了:
    1. 编辑在windows下运行,然后把代码文件通过Xftp传输到虚拟机里:

      技术分享

    2. 调试的话通过Xshell:

      技术分享

  5. 笔记基本是按照书上小结的标题来的,每个标题能做实验的就做实验,理论性的就小结一下,不易过多记忆,因为没代码实践,理论也理解不了,比如并发等等。

??

先没有操作看完了这章,因为涉及了好多额外内容,所以信息量还是比较大,所以先整体看一遍,第二遍则写代码再看一遍。

??

编写DEMO代码:

Hello World模块,随便建个目录ldd3-trainning,然后写个程序hello.c:

技术分享

因为内核运行时是不依赖C库的,所以这里用了printk()

printk:处于内核的公用符号表中,待会到内核符号表那节再说吧

消息优先级:具有默认优先级的消息可能不会输出在控制台上,所以要显示地指定高优先级KERN_ALERT

static:因为这种函数在特定文件之外没有其他意义。因如果一个模块函数实在要对其他内核部分可见,则必须显式被导出。

??

编译模块:

如果想了解内核是如何构建的,阅读Documentation/kbuild目录下的文件。

----------------下面编写Makefile,一个Makefile可以管理多个实验代码,只要往里面添加就可以了

这里我拷贝随书代码的Makefile来解释:

技术分享

??

技术分享

obj-m表示 有一个模块hello.ko要从hello.o来构建,如果hello.o只由hello.c来生成则不需要写其他东西了,如果hello.o也是由file1.o file2.o生成,则要再写行:

module-objs := file1.o file2.o

??

好了,编译一下:

技术分享

报错了,缺少个void

技术分享

改正后继续编译

技术分享

还是没有生成模块目标文件.ko,看错误信息,它的意思是没有hellop.c然后终止了,把makefile文件里的删了看看:

技术分享

再次编译:

技术分享

好了,成功生成!在构造模块之后,就是将模块装入内核。先运行一下在分析:

首先切换到超级用户下:

技术分享

然后我们加载一下:

技术分享

为什么没有显示hello world,根据这个答案它说控制台console和终端terminal是有区别的,代码里通过提高消息优先级为KERN_ALEART让消息输出到console上,但是当前系统console的信息不会输出到terminal上,所以

Edit /proc/sys/kernel/printk to set up the lowest priority to print in console。

书上说到调试技术那节会详细讲,这里就不要深究打印了,内核消息的传递机制将在第4章中详细讨论。用dmesg来查看console信息

技术分享

然后卸载模块rmmod hello:

技术分享

或者:

技术分享

可见,编写模块并不困难,难点在于理解设备并最大化其性能。

??

insmode:

  1. 依赖于kernel/module.c中的一个系统调用,sys_init_module给模块分配内核内存(第8章vmalloc)
  2. 系统调用再将模块正文赋值到这个内存区域;
  3. 通过内核符号表解析模块中的内核引用,最后调用模块的初始化函数;
  4. insmod可以接受一些命令行选项,并且可以在模块链接到内核之前给模块中的整型或者字符串类型变量赋值;

modprobe: 也用来将模块装到内核中

  1. 考虑是否引用了一些当前内核不存在的符号,如果有,会在当前模块搜索路径中查找定义了这些符号的其他模块(需要导出符号表),如果找到了,会同时把这些模块也装载到内核;如果此时调用insmod,则在系统日志文件中记录 unresolved symbols;
  2. 补充:只能从标准的已经安装模块目录中搜索需要装入的模块;

lsmod:

  1. 提供其他模块是不是在使用某个特定模块
  2. 读取 /proc/modules 虚拟文件来获悉
  3. 已经装载模块的信息也可以在 /sys/module 下找到

??

核心模块和应用程序的对比

内核:内核和C库文件是两回事,C库是供应用程序使用的。和内核相关的任何内容都在我们安装并配置好的内核源码树的头文件中声明,大多数内核相关头文件保存在include/linux和include/asm目录中:

技术分享

上面其他子目录中保存的是和特定内核子系统相关的头文件。

核心模块和应用程序的差别:

  1. 应用程序就是从头到尾执行一个单个任务;

    而模块却是和内核说:"我在这儿,我初始化好了,我能做这些工作。"

  2. 应用程序退出时可以不管资源的释放或者其他的清除工作,

    但是模块的退出函数必须仔细撤销初始化函数所做的一切,否则,在系统重新引导之前某些东东就会残留在系统里;

  3. 可卸载模块化驱动程序编程当中最鲜明的特色,有助于缩短模块的开发周期;
  4. 模块仅仅被链接到内核,而不存在任何可链接的函数库;
  5. 应用程序开发过程中的段错误是无害的,并且总是可以使用调试器跟踪到源代码中的问题所在,

    而在一个内核错误即使不影响整个系统,也至少会杀死当前进程。(什么意思?不懂)

  6. 运行模式(内核空间+用户空间)模块化代码在内核空间中运行,用于扩展内核的功能。通常来讲,一个驱动程序要执行先前讲述过的两类任务:模块中某些函数作为系统调用的一部分执行,其他负责中断处理。
  7. 内核中的并发:的确,大部分应用程序出了多线程外都是顺序执行的。但是,内核代码是被被动调用的,所以自然很多人会索取资源:同一时刻,可能会有许多事情正在发生。这里的情况还是很重要的,等以后编程时在理解。
  8. 其他一些细节:
    1. 内核具有非常小的栈,所以声明大的自动变量不是一个好主意,如果我们需要打的结构,应该调用时动态分配;

    ??

当前进程

内核执行的大多数操作还是和某个特定的进程相关。

获得当前进程:访问定义在 include/asm-i386/current.h 中的全局项current

技术分享

Jump To Definition可以看到task_struct定义在linux/sched.h,可以在window菜单下看到路径:

技术分享

current指针指向当前正在运行的进程。在open等系统调用执行过程中,当前进程指的是调用这些系统调用的进程。我们来查看当前进程所执行文件的基本名称和PID看看,还在hello.c里面添加:

技术分享

编译运行:

技术分享

查看控制台:

技术分享

看到了当前进程所执行的程序文件的基本名称base name:

技术分享

??

来看下如何在模块中使用函数指针和函数调用的,从而为运行中的内核增加新的功能

我的理解是通过赋值内核的函数指针,从而把我们实现的函数体让内核通过函数指针实现函数调用:下图使用visio绘制,书上也有

技术分享

??

内核符号表(cat /proc/kallsyms | grep hello):

公共内核符号表(所有的全局内核项目:函数和变量的地址):insmod使用公共内核符号表来解析模块中未定义的符号。

模块层叠技术:

模块符号的可见性:模块符号需要显式导出EXPORT SYMBOL(hello_symbol);

实验验证:

技术分享

编译出错:

技术分享

错误说元素错了,修改下:

技术分享

编译还是报错:

技术分享

难道符号只能是函数名或者变量名吗?于是我改成了:

技术分享

编译后,依然报相同的错误,那我改成:

技术分享

编译出错:

技术分享

没辙了,不能瞎猜了,到stackoverflow上搜索EXPORT_SYMBOL,搜到一个问题:那我自己定义一个函数,然后导出看看:

技术分享

编译出错提示:

技术分享

好像说fun没有声明,好吧,把fun定义放到导出之前:

技术分享

这次终于编译通过了:

技术分享

看来只能导出我自己定义的函数啊!还必须在导出之前声明。等等,我加个static看看:

技术分享

还是可以通过,看来是模块初始化函数和模块退出函数不可以导出啊!不对,我把导出放到后面试试:

技术分享

好吧,成功了!初始化函数和模块退出函数也可以导出没有区别!不知道为什么文件里的全局变量不可以导出为符号?

我们可以在其他模块中声明外部函数:extern int fun(void); 来为调用它做准备。

加载模块,查看内核公共符号表:

技术分享

把全局变量hello_symbol设置为static在编译了看看:

技术分享

技术分享

好了这次hello_symbol还是有的啊,不过这次注意到前面那个符号d是小写,凡是显式导出的就是大写,不显式导出的就是小写,总之都存在,我们再导出这个全局变量看看:

技术分享

编译加载后看看:

技术分享

看到了,d变成大写的D了,确实啊!

对了,书上说符号EXPORT_SYMBOL(hello_symbol);会被扩展为一个特殊变量的声明,而该变量将在模块可执行文件的特殊部分(一个"ELF段")中保存,在装载时,内核通过这个段来寻找模块导出的变量:readelf -a hello.ko | grep hello_symbol

技术分享

然后取消导出看看:

技术分享

看到确实被扩展成了一个特殊变量 __crc__hello_symbol,其他几个我就不认识了。

??

【预备知识】

内核环境

内核代码

头文件:

include/linux/module.h

include/linux/init.h

??

以下声明习惯放在源文件的最后:

MODULE_LICENSE("GPL");

MODULE_AUTHOR();

MODULE_DESCRIPTION();

MODULE_VERSION();

MODULE_ALIAS(); ch11

MODULE_DEVICE_TABLE(); ch12

??

【初始化和关闭】

初始化函数

__init

软件抽象:也是可注册的设备

模块装载器

设备类型

设施:一个设备的多个功能

注册:注册多个设施完成一个设备的注册

内核符号表

注册模块提供的设施

【清除函数】

【模块装载竞争】

【模块参数】

??

ldd3-2 构造和运行模块:Hello World模块笔记