Linux驱动学习之hello模块的实现

目录

 

内核模块简介

内核模块编写

源码分析

Makefile文件

hello模块的具体实现

ARM端调试

ARM端调试遇到问题


  • 内核模块简介

Linux 内核模块是一种可以被内核动态加载和卸载的可执行程序,通过内核模块可以扩展内核的功能,通常内核模块被用于设备驱动、文件系统等。如果没有内核模块,需要向内核添加功能就需要修改代码、重新编译内核、安装新内核等步骤,不仅繁琐,而且容易出错,不易于调试。

Linux内核是一个整体结构,可以把内核想象成一个巨大的程序,各种功能结合在一起。当修改和添加新功能的时候,需要重新生成内核,效率极低。为了弥补整体式内核的缺点,Linux内核的开发者设计了内核模块机制。从代码角度看, 内核模块是一组可以完成某种功能的函数集合。从执行角度看,内核模块是一个已经编译但是没有连接的程序。

对于内核来说 ,模块包括了在运行时可以连接的代码。模块的代码可以被连接带内核,作为内核的一部分,因此被称为内核模块。从用户角度看,内核模块是一个外挂组件,在需要的时可以被删除。内核模块给开发者提供了动态扩充内核功能的途径。

内核模块是一个应用程序,但是与普通的应用程序有所不同,区别在于:

  1. 运行环境不同。内核模块运行在内核空间,可以访问系统的几乎所有的软硬件资源,因此在编程内核模块时要格外注意,一点小错误就会导致系统崩溃;

  2. 功能定位不同。普通应用程序是为了完成某个特定目标,功能定位明确;内核模块是为了其他的内核模块以及应用程序服务的,通常提供的是通用的功能;

  3. 函数调用方式不同。内核模块只是调用内核提供的函数,访问其他的函数会导致运行异常;普通应用程序可以调用自身以外的函数,只要正确连接就行了;

  4. 内核模块使用的是物理内存,这点与应用程序不同。应用程序使用虚拟内存,有一个巨大的地址空间, 在应用程序中可以分配大块的内存。内核模块可以使用的内存非常小,在编内核模块代码的时候要注意内存的分配和使用。

  • 内核模块编写

#include 
#include 
#include 

static __init int hello_init(void)
{
        printk(KERN_ALERT "Hello, This is my first driver program!\n");
        return 0;
}

static __exit void hello_exit(void)
{
        printk(KERN_ALERT "Good Luck, see you!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("TJincheng ");
MODULE_DESCRIPTION("Linux Kernel hello module (c)");
MODULE_LICENSE("Dual BSD/GPL");
  • 源码分析

1、加载和卸载

从内核模块的动态加载特性可以看出,内核模块至少支持加载和卸载两种操作,因此一个内核模块至少包括加载和卸载两个函数。

module_init(hello_init);  //模块加载
module_exit(hello_exit);  //模块卸载

2、头文件

先看看头文件, 下面这两个头文件是模块的必备头文件

#include     //包含了init初始化函数和exit清理函数
#include   //包含了大量加载模块需要的函数和符号的定义

然后是包含内核打印函数printk的头文件

#include  //中包含了内核打印函数 printk函数 等

 

3、宏定义

__init、__exit(__双下划线)等宏是在linux-3.0/include/linux/init.h 中:

#define __init          __section(.init.text) __cold notrace
#define __exit          __section(.exit.text) __exitused __cold notrace
#define module_init(x)  __initcall(x);
#define module_exit(x)  __exitcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall("6",fn,6)
typedef int (*initcall_t)(void);
#define __define_initcall(level,fn,id) 
static initcall_t __initcall_##fn##id __used 
__attribute__((__section__(".initcall" level ".init"))) = fn

static __init int hello_init(void) 宏就会被展开为

static __section(.init.text) __cold notrace inthello_init(void)

static __exit int hello_exit(void) 宏就会被展开为

__section(.exit.text) __exitused __cold notrace int hello_exit

这里的 __section 为gcc的链接选项,他表示把该函数链接到Linux内核映像文件的相应段中,这样hello_init将会被链接进.init.text段中,而hello_exit将会被链接进.exit.text段中。被链接进这两个段中的函数代码在调用完成之后,内核将会自动释放他们所占用的内存资源。因为这些函数只需要初始化或退出一次,所以hello_init()和hello_exit()函数最好在前面加上__init 和 __exit。

module_init(hello_init) 宏就会被展开为:

static int (*initcall_t)(void) __initcall_hello_init6 __used __attribute__((__section__(".initcall" "6" ".init"))) = hello_init;

这段代码也就是定义了一个叫 __initcall_hello_init6的函数指针,他指向 hello_init 这个函数, gcc的链接选项 attribute 和 section 将该指针变量链接到linux内核映像的 .initcall 段中。linux系统在启动时,完成CPU和板级初始化之后,就会从该段中读入所有的模块初始化函数就执行。每一个Linux内核模块都需要使用module_init()和module_exit()宏来修饰,这样系统启动时才能自动调用并初始化他们。

4、printk函数简介

printk 函数在 Linux 内核中定义并且对模块可用,它与标准 C 库函数 printf 的行为相似。内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助. 模块能够调用 printk 是因为在insmod 加载了它之后, 模块被链接到内核并且可存取内核的公用符号. 字串 KERN_ALERT 是消息的优先级。printk支持分级别打印调试,这些级别定义在linux-3.0/include/linux/printk.h文件中:

#define KERN_EMERG "<0>"   /* system is unusable 紧急事件,用于系统奔溃时发出提示信息 */
#define KERN_ALERT "<1>"   /* action must be taken immediately 报告消息,提示用户必须立即采取措施 */
#define KERN_CRIT "<2>"    /* critical conditions 临界条件,在发生严重软硬件操作失败时提示 */
#define KERN_ERR "<3>"     /* error conditions 错误条件,硬件出错时打印的消息*/
#define KERN_WARNING "<4>" /* warning conditions 警告条件,对潜在问题的警告信息*/
#define KERN_NOTICE "<5>"  /* normal but significant condition 公告信息 */
#define KERN_INFO "<6>"    /* informational  提示信息,通常用于打印启动过程或者某个硬件的状态*/
#define KERN_DEBUG "<7>"   /* debug-level messages  调试消息  */

Linux内核中printk()的语句是否打印到串口终端上,与u-boot里的bootargs参数中的loglelve=7 相关,只有低于loglevel级别的信息才会打印到控制终端上,否则不会在控制终端上输出。这时我们只能通过dmesg命令查看。 linux下的dmesg命令的可以查看linux内核所有的打印信息,它们记录在/var/log/messages系统日志文件中。linux内核中的打印信息很多,我们可以使用 dmesg -c命令清除之前的打印信息。

 

5、模块相关信息的声明

声明可以写在模块的任何地方(但必须在函数外面),但是惯例是写在模块最后。

MODULE_AUTHOR("xxx");          //作者信息
MODULE_DESCRIPTION("xxxxxxx")  //简单描述
MODULE_LICENSE("xxxxx");       //指定它的代码使用哪个许可

其他的声明方式

MODULE_VERSION       //声明版本
MODULE_ALIAS         // 别名
MODULE_DEVICE_TABLE  // 来告知用户空间, 模块支持那些设备
  • Makefile文件

1、PC端Makefile

KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

obj-m := hello.o

modules:
        $(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
        @make clear
clear:
        @rm -f *.o *.cmd *.mod.c
        @rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
        @rm -f .*ko.cmd .*.o.cmd .*.o.d
        @rm -f *.unsigned
clean:
        @rm -f hello.ko

2、ARM端Makefile

LINUX_SRC = ${shell pwd}/../linux/linux-3.0/
CROSS_COMPILE=/opt/xtools/arm920t/bin/arm-linux-
INST_PATH=/tftp

PWD := $(shell pwd)

EXTRA_CFLAGS+=-DMODULE

obj-m += hello.o

modules:
	@echo ${LINUX_SRC}
	@make -C $(LINUX_SRC) M=$(PWD) modules
	@make clear

uninstall:
	rm -f ${INST_PATH}/*.ko

install: uninstall
	cp -af *.ko ${INST_PATH}

clear:
	@rm -f *.o *.cmd *.mod.c
	@rm -rf  *~ core .depend  .tmp_versions Module.symvers modules.order -f
	@rm -f .*ko.cmd .*.o.cmd .*.o.d

clean: clear
	@rm -f  *.ko
  • 注意:
  1. LINUX_SRC 应该指定开发板所运行的Linux内核源码的路径,并且这个linux内核源码必须make menuconfig并且make过的,因为Linux内核的一个模块可能依赖另外一个模块,如果另外一个没有编译则会出现问题。所以Linux内核必须编译过,这样才能确认这种依赖关系;
  2. 交叉编译器必须使用 CROSS_COMPILE 变量指定;
  3. 如果编译Linux内核需要其它的一些编译选项,那可以使用 EXTRA_CFLAGS 参数来指定;
  4. obj-m += hello.o 该行告诉Makefile要将 hello.c 源码编译生成内核模块文件 hello.ko ;
  5. clear 目标将编译linux内核过着产生的一些临时文件全部删掉;
  • hello模块的具体实现

1、make先编译源文件

make

Linux驱动学习之hello模块的实现_第1张图片

 2、加载模块

sudo insmod kernel_hello.ko

3、查看已加载模块

lsmod | grep ker*

Linux驱动学习之hello模块的实现_第2张图片

4、查看内核打印消息

sudo dmesg -c

Linux驱动学习之hello模块的实现_第3张图片

5、模块卸载

sudo rmmod kernel_hello

Linux驱动学习之hello模块的实现_第4张图片

6、PC端调试出现问题–PC端内核版本不匹配

大家可以参考我之前写的博客:博客链接

 

  • ARM端调试

我们通过tftpd32小程序(注意关PC防火墙和pc端的IP地址与ARM板子的Ip地址在同一网段内)将PC端使用ARM的模块传到开发板上:

tftp -gr hello.ko 192.168.1.2    //通过tftp获取自己在虚拟机编译好的hello模块

1、在ARM板上挂载模块:

 insmod hello.ko 

2、查看模块:

lsmod 

3、删除模块:

 rmmod hello.ko

Linux驱动学习之hello模块的实现_第5张图片

 

  • ARM端调试遇到问题

  1. tftp传输失败,会创建空文件, 如果这时候将其加入内核, 会报: short read 错误, 我们可以通过du -sh hello.ko命令来查看大小
  2. tftp传输失败有很多原因, 主要原因是: 客户端是否和服务器在同一网段内,防火墙是否关闭???

Linux驱动学习之hello模块的实现_第6张图片

你可能感兴趣的:(Linux驱动,内核,linux,嵌入式,tftp,hello)