# Linux 驱动开发入门 站点:答源(https://www.dandroid.cn/) 原文:https://www.dandroid.cn/archives/20555 Canonical:https://www.dandroid.cn/archives/20555 作者:admin 发布日期:2023-09-15 最后更新:2023-09-15 17:38 分类:Android嵌入式 / Android系统开发 内容类型:post 说明:本页是原文的纯文本镜像,供大模型与检索器完整读取;不含导航、广告、付费框。引用请以上方「原文」链接为准。 ## 正文 1. 编写一个简单的 Linux 内核模块 首先,我们需要理解什么是内核模块?简单来说,内核模块是一段 "固定格式" 的代码,像一个“插件”一样,linux 内核可以动态的加载并执行这段代码,也可以把这段代码编译进内核,在内核启动的时候来执行这段代码。 下面我们写一个简单的 linux 模块: 准备工作: sudo apt install build-essential linux-headers-`uname -r` 创建我们的项目目录: mkdir HelloModule cd HelloModule 在 HelloModule 中添加 hello_module.c #include #include /** * __init 是一个宏,表示 hello_init 是一个初始化函数,会放到编译后目标文件的初始化段中 */ static int __init hello_init(void) { //printk 是内核中的日志打印函数 printk("Hello world!\n"); return 0; } /** * __exit 是一个宏,表示 hello_exit 是一个初始化函数,会放到编译后目标文件的初始化段中 */ static void __exit hello_exit(void) { printk("hello exit\n"); } /** * hello_init 是当前模块的启动函数 */ module_init(hello_init); /* * hello_exit 是当前模块的退出函数 */ module_exit(hello_exit); 2. 将模块编译进内核 把 hello_module 移动到内核的 /drivers/char 目录: cp hello_module.c kernel目录/drivers/char 接下来我们修改 /drivers/char/Kconfig 文件,使得我们的 hello 模块,能出现在内核的编译选择中。 在 /drivers/char 中的 Kconfig 文件中添加: config HELLO_MODULE bool "hello module support" default y 然后在 /drivers/char 下的 Makefile 文件中添加: obj-$(CONFIG_HELLO_MODULE) += hello_module.o 当在 make menuconfig 编译菜单中选中了 hello module support, CONFIG_HELLO_MODULE 的值是 y,没有选中值是 m(我们定义的默认值是 y): obj-y += hello_module.o 的意思是将 hello_module.o 编译进内核 obj-m += hello_module.o 的意思是文件 hello_module.o 作为"模块"进行编译,不会编译到内核,但是会生成一个独立的 "test.ko" 文件 最后配置内核: cp ./arch/x86/configs/x86_64_ranchu_defconfig .config make menuconfig 进入 Device Drivers 选项: 进入 Character devices 这里就可以看见我们刚才添加的选项,默认是选上的。 然后执行编译: #执行之前的编译脚本 sh build.sh 启动模拟器: source build/envsetup.sh lunch aosp_x86_64-eng emulator -kernel ~/kernel/goldfish/arch/x86_64/boot/bzImage 查看开机信息: # dmesg 用于显示开机信息 adb shell dmesg 3. linux 中文件的读写 驱动是干什么的?在驱动的相关书籍上,网络上你能看到很多专业的定义。我们暂时不关心这些专业的说法,仅从功能的角度来说,驱动程序使得应用程序可以访问硬件。 那应用是如何访问硬件的?linux 中一切皆文件,访问硬件就是对文件的读写操作。比如 led 灯对应的文件是 /etc/led, 读写这个文件就能操作 led 灯。 接下来的问题就是,linux 中如何读写文件? linux中文件读写相关的主要 api: //打开文件 int open(const char *pathname, int flags, mode_t mode); //从文件中读数据 ssize_t read(int fd, void *buf, size_t count); //向文件中写数据 ssize_t write(int fd, const void *buf, size_t count); //专用于设备输入输出操作 int ioctl(int fd, unsigned long request, ...); //关闭文件的读写,回收资源 int close(int fd); 函数的具体用法不是本文的重点,有兴趣的同学可以学习 Linux程序设计 的第二章。工作中忘了,可以通过 man 命名查看具体用法。 下面来看一下 open 函数: //该函数用于打开文件 int open(const char *pathname, int flags, mode_t mode); 当打开一个文件的时候,会返回一个 int 值,一般称这个返回值为句柄或者 handle,在内核中,句柄是一个数组的索引(index),数组的成员是 struct file : struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; //关注1 /* * Protects f_ep_links, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; enum rw_hint f_write_hint; atomic_long_t f_count; unsigned int f_flags; //关注2 fmode_t f_mode; //关注3 struct mutex f_pos_lock; loff_t f_pos; //关注4 struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct list_head f_ep_links; struct list_head f_tfile_llink; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; } __randomize_layout __attribute__((aligned(4))); struct file 的结构有点复杂,入门阶段主要关注代码中标注的四个关注点。 在内核中,有一个 struct file 的数组,当调用 open 函数打开一个文件的时候,内核就会构建一个 struct file,并添加到这个数组中,返回 struct file 在数组中的 index 给用户态程序,这个值就是 open 函数的返回值。 根据文件的命名,容易猜出:使用 open 打开文件时,传入的 flags、mode 等参数会被记录在内核中,具体如下图所示: struct file 有一个成员为 file_operations: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //关注点1 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //关注点2 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long);//关注点3 int (*mmap) (struct file *, struct vm_area_struct *);//关注点4 int (*open) (struct inode *, struct file *);//关注点5 int (*flush) (struct file *, fl_owner_t id);//关注点6 int (*release) (struct inode *, struct file *);//关注点7 int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64); ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64); } __randomize_layout; 内部主要是一些函数指针,我们主要关注常用的几个函数: ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); int (*mmap) (struct file *, struct vm_area_struct *); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); 这些函数都是由相应硬件驱动程序提供。 至此,文件读写的大致流程就出来了: app 调用 open read 等系统调用函数 内核构建相应的 struct file,并添加进数组,返回 index 给 app 调用驱动程序 file_operations 指针提供的 open read 等函数,完成实际的硬件操作 4. hello 驱动的编写 驱动就是一个模块,在模块的基础上添加驱动框架和硬件操作的部分就可以完成驱动程序的编写了。下面我们写一个 hello 驱动,这个驱动只是简单的在用户态和内核态之间拷贝数据,没有实际的硬件操作,仅用于流程的展示。编写驱动的步骤如下: 确定主设备号,也可以让内核分配 (设备号就是硬件的一个编号) 定义自己的 file_operations 结构体 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体 定义 init 函数,在 init 函数中调用 register_chrdev 注册函数 定义 exit 函数,在 exit 函数中调用 unregister_chrdev 卸载函数 其他完善:提供设备信息,自动创建设备节点:class_create, device_create 在 ~/Project 目录下创建如下的目录结构: HelloDriver ├── build_driver.sh ├── build_driver_test.sh ├── CMakeLists.txt ├── hello_drv.c ├── hello_drv_test.c └── Makefile 其中 hello_drv.c: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 1. 确定主设备号 */ static int major = 0; static char kernel_buf[1024]; static struct class *hello_class; #define MIN(a, b) (a < b ? a : b) /* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */ static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { int err; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); err = copy_to_user(buf, kernel_buf, MIN(1024, size)); return MIN(1024, size); } static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { int err; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); err = copy_from_user(kernel_buf, buf, MIN(1024, size)); return MIN(1024, size); } static int hello_drv_open (struct inode *node, struct file *file) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } static int hello_drv_close (struct inode *node, struct file *file) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } /* 2. 定义自己的file_operations结构体 */ static struct file_operations hello_drv = { .owner = THIS_MODULE, .open = hello_drv_open, .read = hello_drv_read, .write = hello_drv_write, .release = hello_drv_close, }; /* 4. 把file_operations结构体告诉内核:注册驱动程序 */ /* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */ static int __init hello_init(void) { int err; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */ //提供设备信息,自动创建设备节点。 hello_class = class_create(THIS_MODULE, "hello_class"); err = PTR_ERR(hello_class); if (IS_ERR(hello_class)) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); unregister_chrdev(major, "hello"); return -1; } device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */ //到这里我们就可以通过 /dev/hello 文件来访问我们的驱动程序了。 return 0; } /* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */ static void __exit hello_exit(void) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); device_destroy(hello_class, MKDEV(major, 0)); class_destroy(hello_class); unregister_chrdev(major, "hello"); } /* 7. 其他完善:提供设备信息,自动创建设备节点 */ module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); 可以看出,除了定义 read write 等函数,其他都是一些模板代码。 接着我们再写一个测试程序: hello_drv_test.c #include #include #include #include #include #include /* * ./hello_drv_test -w abc * ./hello_drv_test -r */ int main(int argc, char **argv) { int fd; char buf[1024]; int len; /* 1. 判断参数 */ if (argc < 2) { printf("Usage: %s -w \n", argv[0]); printf(" %s -r\n", argv[0]); return -1; } /* 2. 打开文件 */ fd = open("/dev/hello", O_RDWR); if (fd == -1) { printf("can not open file /dev/hello\n"); return -1; } /* 3. 写文件或读文件 */ if ((0 == strcmp(argv[1], "-w")) && (argc == 3)) { len = strlen(argv[2]) + 1; len = len < 1024 ? len : 1024; write(fd, argv[2], len); } else { len = read(fd, buf, 1024); buf[1023] = '\0'; printf("APP read : %s\n", buf); } close(fd); return 0; } 我们可以模块章节介绍的方法将驱动编译进内核。也可以直接编写 makefile 来编译驱动模块,然后通过命令行加载和卸载驱动程序。这里介绍第二种方法: 创建 Makefile 文件: KERN_DIR = /home/zzh0838/kernel/goldfish all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order rm -f hello_drv_test obj-m += hello_drv.o -C 选项的作用是指将当前工作目录转移到你所指定的位置。“M=”选项的作用是,当用户需要以某个内核为基础编译一个外部模块的话,需要在 make modules 命令中加入“M=dir”,程序会自动到你所指定的 dir 目录中查找模块源码,将其编译,生成 KO 文件。 编写编译驱动的脚本 build_driver.sh: #!/bin/bash export ARCH=x86_64 export SUBARCH=x86_64 export CROSS_COMPILE=x86_64-linux-android- export PATH=~/aosp/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9/bin:$PATH make 执行 ./build_driver.sh,编译出 hello_drv.ko,接下来启动模拟器,把 ko 文件上传到模拟器: cd aosp目录 source build/envsetup.sh lunch aosp_x86_64-eng emulator -kernel ~/kernel/goldfish/arch/x86_64/boot/bzImage cd hellodriver # 使用 adb 上传 ko 文件 adb push hello_drv.ko /data/local/tmp # 进入模拟器的 shell 环境 adb shell cd /data/local/tmp #加载模块,加载完成后,/dev 目录下就会有一个 hello 文件 insmod hello_drv.ko ls /dev/hello -l 通过 cmake 的方式来编译测试程序: 编写 CMakeLists.txt: cmake_minimum_required(VERSION 3.0) project(test) add_executable(${PROJECT_NAME} hello_drv_test.c) 编写编译脚本 build_driver_test.sh: export ANDROID_NDK=你的ndk安装目录 rm -r build mkdir build && cd build # CMake的内置支持 # cmake -DCMAKE_SYSTEM_NAME=Android \ # -DCMAKE_SYSTEM_VERSION=29 \ # -DCMAKE_ANDROID_ARCH_ABI=x86_64 \ # -DANDROID_NDK=$ANDROID_NDK \ # -DCMAKE_ANDROID_STL_TYPE=c++_shared \ # .. # 工具链文件支持 cmake \ -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=x86_64 \ -DANDROID_PLATFORM=android-29 \ -DANDROID_STL=c++_shared \ .. cmake --build . 编译程序并上传模拟器: # 编译 sh ./build_driver_test.sh # 打开模拟器,流程略 # 上传可执行文件 adb push build/test /data/local/tmp # 进入到模拟器 shell adb shell # 执行程序 cd /data/local/tmp # 加载驱动程序 insmod hello_drv.ko ./test -w "nihao" ./test -r 执行程序的结果如下所示: 参考资料 如何编写一个简单的 Linux 内核模块 如何将内核模块编译进linux内核 奔跑吧 Linux内核 入门篇 Linux设备驱动开发详解 《嵌入式Linux应用开发完全手册 第二版》 韦东山 《Linux程序设计》 Neil Matthew Richard Stones --- 由 答源 DAN 底座 1.4.1 生成 · 2026-11-11T03:21:17+00:00