编译内核
为什么编译驱动前要编译内核?
编译驱动的内核要和开发板上的内核一致。因为开发板出厂时预烧录了一个内核,但自己在 ubuntu 编译是使用的是自己的内核,二者不一致时会导致导入驱动模块时出现问题(如内核污染提示)。
内核编译的步骤
下面记录内核编译步骤是对应 IMX6ULL PRO 开发板平台,相关文档资料有百问网提供。
# IMX6ULL PRO 平台对应的编译zImage的步骤
cd /home/book/100ask_imx6ull-sdk/Linux-4.9.88
make mrproper
make zImage -j4
make dtbs
cp arch/arm/boot/zImage ~/nfs_rootfscp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs
编译后,会在对应路径下生成 zImage 内核文件和 dts 设备树文件。
内核文件:arch/arm/boot/zImage
设备树文件:arch/arm/boot/dts/100ask_imx6ull-14x14.dtb
编译及安装内核模块
# 在Linux源码路径下进行
make modules
make ARCH=arm INSTALL_MOD_PATH=/home/book/nfs_rootfs modules_install
安装内核及模块到开发板
# 在IMX6ULL PRO开发板进行
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
cp /mnt/zImage /boot
cp /mnt/100ask_imx6ull-14x14.dtb /boot
cp /mnt/lib/modules /lib -rfd
sync
编译驱动
编译驱动首先要确保编译驱动的 Makefile 文件中的 KERN_DIR 设置正确。一份编译驱动的 Makefile 参考文件如下。
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f hello_drv_test
obj-m += hello_drv.o
确保 Makefile 文件正确后,就可以执行 make all 命令编译驱动。编译后得到一个 hello_drv_test 测试文件、一个 hello_drv.o 模块文件。
安装驱动模块
驱动模块通过 insmod 命令来安装。
insmod # 在开发板上安装驱动模块
lsmod # 查看已安装的驱动模块
rmmod # 删除已安装的模块
insmod -f [xxx.ko] # 强制安装驱动模块
如果未按照前述要求去确保编译驱动的内核与开发板的内核相同,则执行 insmod 命令时会出现报错,提示内核已被污染。若要强制安装,可以使用上述强制安装命令。
驱动加载逻辑
驱动程序可以被直接编进内核中,也可以编译成 ko 文件后手动加载。这两种方式都会利用 module_init 和 module_exit 来初始化和注销驱动,但是两者的导入逻辑有所不同。
把驱动程序编进内核时,由于不会定义宏 MODULE,会选择与编译 ko 文件不同的分支来执行。这种情况下,会定义对应初始化和注销的两个函数指针,分别放在 .initcall6.init 段和 .exitcall6.exit 段中。内核启动时,就会去 .initcall6.init 段中取出指向该驱动的初始化函数的函数指针,执行这个函数指针指向的驱动的入口函数。若选择将驱动编进内核,那么驱动程序就只会被加载,而不会被卸载。所以,内核不会使用到 .exitcall6.exit 的段空间,这块空间会在内核启动后被释放。
编译为 ko 文件时,会使用宏 module_init(hello_init) 和 module_exit(hello_exit) 来将此驱动模块关联到模块相关命令。将这两个宏展开后,分别为 init_module 和 cleanup_module,分别在 insmod 和 rmmod 时被使用,通过调用 hello_init 和 hello_exit 来实现。
#ifndef MODULE
static initcall_t __initcall_hello_init6 __used \
__attribute__((__section__(".initcall6.init"))) = hello_init;
static exitcall_t __exitcall_hello_exit __used __section(.exitcall.exit) = hello_exi
t;
#else
static inline initcall_t __inittest(void) \
{ return hello_init; } \
int init_module(void) __attribute__((alias("hello_init")));
static inline exitcall_t __exittest(void) \
{ return hello_exit; } \
void cleanup_module(void) __attribute__((alias("hello_exit")));
#endif
内核使用 char_device_struct 来描述一种驱动程序,通过 chrdevs[256] 的指针数组来管理这些驱动程序,chrdevs 数组的每个成员都是一个链表头结点。实际上,chardevs 就是一个链表形式的哈希表。内核提供了 register_chrdev_region 函数来申请注册新的驱动程序(实际上注册函数还有 alloc_chrdev_region 和 register_chrdev)。注册驱动程序时,通过用户传递的主设备号和次设备号,查询对应区域是否被其他设备占用。若未被占用,则生成一个 char_device_struct 对象挂到 chrdevs 中对应的链表里。这就为字符驱动产生了一类新的驱动程序。
struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
}
内核使用 cdev 结构来描述具体的一个驱动设备,内核提供了 cdev_add 函数来加入新的驱动设备。在此函数中,cedv 将会被加入到 cdev_map 中,可以使用 dev (设备号)到 dev+count-1 来索引这个设备。
APP 打开一个字符设备节点时,则是根据该设备节点的主次设备号的计算结果,从 cdev_map 找到对应的 cdev,再从 cdev 中得到 file_operations 结构体,进行文件操作时则会调用 file_operations 下的各个函数接口去执行对应的操作。
标签:__,编译,init,内核,Linux,驱动,hello From: https://www.cnblogs.com/zenonx/p/17644242.html