1、第一个驱动程序hello_drv
admin
2024-02-09 11:42:49
0

在裸机上面开发可以直接操作硬件,而在linux等操作系统上开发却无法直接操作硬件,必须编写驱动程序,通过驱动程序操作硬件。

应用程序如何通过驱动来控制硬件?

应用程序通过对设备文件调用系统接口(如read、write、open等)以调用设备文件对应驱动的对应函数,当然前提是该驱动中有对应的函数可供调用,如果没有,则会出错。

如何编写驱动?怎样创建设备文件?

接下来就让我们一起来学习如何 编写一个简单的驱动 hello_drv。

就像我们编写的第一个C语言程序一样

#include "stdio.h"int main(void) {printf("hello world!\n");return 0;
}

驱动程序也是有一个简单的框架的,一个最简单的驱动程序的框架如下:

#include int init_module(void) {return 0;
}void cleanup_module(void) {}MODULE_LICENSE("GPL");

刚开始时我们可以不用弄清除为什么这么写,先写,以后慢慢的就懂了(当然自己要尽力的去弄懂,实在不懂的可以先放一放)。

有了第一个驱动程序后我们应该去编译它,驱动程序是基于内核开发的,所以我们在驱动中包含的头文件都是内核文件,我们的编译自然也不能像以前一样gcc,我们必须要指定内核的路径,这样才能成功编译。这里我直接提供一个Makefile


#后面的hello_drv是你要生成的模块的名字,可自行修改
obj-m := hello_drv.o 
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)
all:$(MAKE) -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:rm *.korm *.o

其中hello_drv.o是目标文件的名字,可自行修改。

执行make后就能看到目录下多了很多文件,其中有一个hello_drv.ko就是我们需要的kernel object文件。

看到这个文件就说明我们的驱动模块编译成功了,接下来就是装载模块了。使用sudo insmode hello_drv.ko装载模块,使用lsmod查看所有已经装载的模块,仔细找找就能发现我们装载的模块。使用sudo rmmod hello_drv(注意后面不需要加.ko)可以卸载模块。

上面的驱动程序是最简单的驱动程序,什么都没干,什么现象都没有,下面我们开始尝试在上面的代码中添加一些东西

#include int init_module(void) {printk("hello drv");return 0;
}void cleanup_module(void) {}MODULE_LICENSE("GPL");

跟上面相比这里制作了轻微的修改---------输出一个hello drv,不过值得注意的是并不是使用printf函数,而是使用printk也就是print kernel函数。将这个输出语句放在init_module中意味着模块加载的时候会输出hello drv,因为init_module函数是模块被加载时被调用的函数。但是这个输出并不会直接输出到控制台,我们必须通过dmesg命令才能看到输出的内容。

如果有人按照上面的步骤尝试了,但是并没有看到hello drv的信息,而是看到如下的信息:

hello_drv: loading out-of-tree module taints kernel.
hello_drv: module verification failed: signature and/or required key missing - tainting kernel

你只需要卸载模块,然后重新装载一次就行了(注意期间不能重启哦),每次重启后第一次模块装载都会出现如上的字样。

关于驱动的入口函数名和出口函数名

驱动有固定的入口函数和出口函数名,但是难道每次都必须用固定的函数名吗?其实不是的,我们可以用自己喜欢的任意函数名字当作驱动入口或者出口函数的名字。只需要使用module_init()和module_exit()这个宏进行说明就行。例如你可以尝试一下编译下面的模块。

#include int hello_init(void) {printk("hello drv init\n");return 0;
}void hello_exit(void) {printk("hello drv exit\n");}MODULE_LICENSE("GPL");module_init(hello_init);module_exit(hello_exit);

虽然函数名变了,但是模块仍然可以在装载和卸载的时候调用该函数。

int hello_init(void) 与 int __init hello_init(void)的区别是加了__init后该函数不会加载进内存中,因为只有在加载的时候调用一次。

void hello_exit(void) 与 void __exit hello_exit(void),如果一个模块被编进内核,那么该模块是无法卸载的,所以在一般的模块中__exit没有实际意义,但是对于被编进内核中的模块而言__exit会使出口函数不会加载进内存中。

进一步理解驱动程序

先看一个程序

#include 
#include static unsigned int major = 0;static const struct file_operations hello_fops = {.owner = THIS_MODULE,
};int hello_init(void) {printk("hello drv init\n");major = register_chrdev(0, "hello_drv", &hello_fops);return 0;
}void hello_exit(void) {printk("hello drv exit\n");unregister_chrdev(major, "hello_drv");
}MODULE_LICENSE("GPL");module_init(hello_init);module_exit(hello_exit);

相比于上个程序,这个程序多了一个结构体的定义以及两个函数的调用,先说结构体,file_operations结构体中包含了对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 *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iopoll)(struct kiocb *kiocb, bool spin);int (*iterate) (struct file *, struct dir_context *);int (*iterate_shared) (struct file *, struct dir_context *);__poll_t (*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);int (*mmap) (struct file *, struct vm_area_struct *);unsigned long mmap_supported_flags;int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);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_MMUunsigned (*mmap_capabilities)(struct file *);
#endifssize_t (*copy_file_range)(struct file *, loff_t, struct file *,loff_t, size_t, unsigned int);loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,struct file *file_out, loff_t pos_out,loff_t len, unsigned int remap_flags);int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

除了一个owner之外清一色的都是函数指针,我们可以让函数指针指向对应的函数,这样当我们对一个设备文件进行相应操作的时候就会调用对应驱动中对应函数指针指向的函数。

在我们的函数中我们没有让函数指针指向任何函数,只是一个简单的.owner = THIS_MODULE,这里顺便说一个.owner是指结构体中名为owner的成员,通过这种C99后新出现的语法就能单独对结构体中的某个属性进行定义,非常方便。

register_chrdev函数用于注册一个字符设备,它的第一个参数是想要注册的主设备号(如果传入0则表示让系统分配),第二个参数是设备的名字,第三个参数是一个file_operations结构体。通过注册一个字符设备,我们就可以用cat /proc/devices查看设备了,仔细找找,一定能找到注册的设备号,unregister_chrdev自然就是取消字符设备的注册了。

光注册设备还不行,我们必须还得创建节点,在shell下使用sudo mknod /dev/hello_drv c 240 0创建一个字符设备节点。之后就能对该设备节点进行open、read、write等操作了。

在创建了一个设备节点之后,我们可以编写一段测试代码来对设备节点进行操作,测试代码如下:

#include 
#include 
#include 
#include 
#include int main() {int fd = open("/dev/hello_drv", O_RDWR);if(fd == -1) {printf("open error\n");return -1;}else {printf("open success\n");int ret = write(fd, "hello", 5);if(ret == -1) {printf("write error\n");return -1;}else {printf("write success\n");}}printf("end\n");
}

调用该测试代码,你就会发现open成功但是write失败了,如果你连open都没有成功的话那么一定是你没有sudo。其实由此我们也可以知道会提供一个默认的open函数而不会提供默认的write函数。那不妨让我们自己写一个write函数来看看会有什么效果吧。

在驱动代码中添加一个write函数,然后再次调用测试代码。添加write函数后的驱动代码如下:

#include 
#include static ssize_t hello_write (struct file *, const char __user *, size_t, loff_t *);static unsigned int major = 0;static const struct file_operations hello_fops = {.owner = THIS_MODULE,.write = hello_write,
};int hello_init(void) {printk("hello drv init\n");major = register_chrdev(0, "hello_drv", &hello_fops);return 0;
}void hello_exit(void) {printk("hello drv exit\n");unregister_chrdev(major, "hello_drv");
}ssize_t hello_write (struct file *FILE, const char __user *buf, size_t len, loff_t * off) {return 0;
}MODULE_LICENSE("GPL");module_init(hello_init);module_exit(hello_exit);

此时就会发现write成功了,其实就是当我们对设备文件调用write函数的时候,会先去检测字符设备对应的file_operations结构体中是否有对write函数的定义,如果有,则调用该write函数,如果没有则返回一个错误。当然对于该结构体中的其他函数也是一样的了。

编写有意义的驱动程序

前面的步骤让我们对驱动的理解更深一步了,但是单单只是在write函数中返回一个 0 并不是我们想要的结果,我们希望能通过write函数往文件中写数据,并且能通过read函数将数据读出来。这样才算是实现了一个有意义的驱动程序,这也是我们本阶段的目标。

在此之前,需要先介绍几个函数:

  • static inline int copy_to_user(void __user volatile *to, const void *from, unsigned long n)

    copy_to_user函数有三个参数,其中第一个参数是目标缓冲区的地址,第二个参数是源缓冲区的地址,第三个参数是要拷贝的长度。从函数名我们可以知道这个函数是把数据拷贝到用户区中去,从哪里拷贝到用户区呢 ? 当然是从内核区了。

  • static inline int copy_from_user(void *to, const void __user volatile *from, unsigned long n)

    copy_from_user也是三个参数,其中第一个参数是目标缓冲区的地址,第二个参数是源缓冲区的地址,第三个参数是要拷贝的长度。从函数名我们可以知道这个函数是从用户区中拷贝数据,拷贝到哪儿呢?当然是内核区啦!

学会这两个函数之后我们就可以简单的完成我们上面的需求了,驱动代码和测试代码如下:

#include 
#include 
#include static ssize_t hello_write (struct file *, const char __user *, size_t, loff_t *);
ssize_t hello_read (struct file *, char __user *, size_t, loff_t *);static unsigned int major = 0;
static char kernel_buf[10];static const struct file_operations hello_fops = {.owner = THIS_MODULE,.write = hello_write,.read = hello_read,
};int hello_init(void) {printk("hello drv init\n");major = register_chrdev(0, "hello_drv", &hello_fops);return 0;
}void hello_exit(void) {printk("hello drv exit\n");unregister_chrdev(major, "hello_drv");
}ssize_t hello_write (struct file *FILE, const char __user *buf, size_t len, loff_t * off) {if(len > 10) return -1;copy_from_user( kernel_buf, buf, len);return len;
}ssize_t hello_read (struct file *FILE, char __user * buf, size_t len, loff_t *off) {copy_to_user(buf, kernel_buf, len);return len;
}MODULE_LICENSE("GPL");module_init(hello_init);module_exit(hello_exit);
#include 
#include 
#include 
#include 
#include 
#include int main(int argc, char *argv[]) {if(argc < 2) {printf("格式错误\n");return -1;}int fd = open("/dev/hello_drv", O_RDWR);if(fd == -1) {printf("open error\n");return -1;}else {printf("open success\n");if(!strcmp(argv[1], "-w")) {int ret = write(fd, argv[2], strlen(argv[2]));printf("成功写入 %d 个字节\n",ret);}else if(!strcmp(argv[1], "-r")) {char buf[11];int ret = read(fd, buf, 5);buf[ret] = '\0';printf("读到的数据为 %s\n",buf);}else {printf("未定义的操作\n");return -1;}}printf("end\n");
}

虽然上面的代码有很多不完善的地方,但是确实可以观测到我们想要的现象。

luoxin@luoxin-virtual-machine:/mnt/hgfs/linux_driver/hello_drv$ sudo ./hello_drv_test -w 12345
open success
成功写入 5 个字节
end
luoxin@luoxin-virtual-machine:/mnt/hgfs/linux_driver/hello_drv$ sudo ./hello_drv_test -r
open success
读到的数据为 12345
end
luoxin@luoxin-virtual-machine:/mnt/hgfs/linux_driver/hello_drv$ sudo ./hello_drv_test -w hello
open success
成功写入 5 个字节
end
luoxin@luoxin-virtual-machine:/mnt/hgfs/linux_driver/hello_drv$ sudo ./hello_drv_test -r
open success
读到的数据为 hello
end
  1. 构造一个file_operation结构体
  2. 将结构体放入chrdevs数组中(需要先确定主设备号)

入口函数:每个驱动都有一个入口函数和一个出口函数,入口函数和出口函数都是固定的,但是我们可以通过module_init()这个宏来指定函数成为入口函数。如果你不想使用module_init(),那么你必须指定入口函数名字为int init_module(void)

相关内容

热门资讯

电视安卓系统哪个品牌好,哪家品... 你有没有想过,家里的电视是不是该升级换代了呢?现在市面上电视品牌琳琅满目,各种操作系统也是让人眼花缭...
安卓会员管理系统怎么用,提升服... 你有没有想过,手机里那些你爱不释手的APP,背后其实有个强大的会员管理系统在默默支持呢?没错,就是那...
安卓系统软件使用技巧,解锁软件... 你有没有发现,用安卓手机的时候,总有一些小技巧能让你玩得更溜?别小看了这些小细节,它们可是能让你的手...
安卓系统提示音替换 你知道吗?手机里那个时不时响起的提示音,有时候真的能让人心情大好,有时候又让人抓狂不已。今天,就让我...
安卓开机不了系统更新 手机突然开不了机,系统更新还卡在那里,这可真是让人头疼的问题啊!你是不是也遇到了这种情况?别急,今天...
安卓系统中微信视频,安卓系统下... 你有没有发现,现在用手机聊天,视频通话简直成了标配!尤其是咱们安卓系统的小伙伴们,微信视频功能更是用...
安卓系统是服务器,服务器端的智... 你知道吗?在科技的世界里,安卓系统可是个超级明星呢!它不仅仅是个手机操作系统,竟然还能成为服务器的得...
pc电脑安卓系统下载软件,轻松... 你有没有想过,你的PC电脑上安装了安卓系统,是不是瞬间觉得世界都大不一样了呢?没错,就是那种“一机在...
电影院购票系统安卓,便捷观影新... 你有没有想过,在繁忙的生活中,一部好电影就像是一剂强心针,能瞬间让你放松心情?而我今天要和你分享的,...
安卓系统可以写程序? 你有没有想过,安卓系统竟然也能写程序呢?没错,你没听错!这个我们日常使用的智能手机操作系统,竟然有着...
安卓系统架构书籍推荐,权威书籍... 你有没有想过,想要深入了解安卓系统架构,却不知道从何下手?别急,今天我就要给你推荐几本超级实用的书籍...
安卓系统看到的炸弹,技术解析与... 安卓系统看到的炸弹——揭秘手机中的隐形威胁在数字化时代,智能手机已经成为我们生活中不可或缺的一部分。...
鸿蒙系统有安卓文件,畅享多平台... 你知道吗?最近在科技圈里,有个大新闻可是闹得沸沸扬扬的,那就是鸿蒙系统竟然有了安卓文件!是不是觉得有...
宝马安卓车机系统切换,驾驭未来... 你有没有发现,现在的汽车越来越智能了?尤其是那些豪华品牌,比如宝马,它们的内饰里那个大屏幕,简直就像...
p30退回安卓系统 你有没有听说最近P30的用户们都在忙活一件大事?没错,就是他们的手机要退回安卓系统啦!这可不是一个简...
oppoa57安卓原生系统,原... 你有没有发现,最近OPPO A57这款手机在安卓原生系统上的表现真是让人眼前一亮呢?今天,就让我带你...
安卓系统输入法联想,安卓系统输... 你有没有发现,手机上的输入法真的是个神奇的小助手呢?尤其是安卓系统的输入法,简直就是智能生活的点睛之...
怎么进入安卓刷机系统,安卓刷机... 亲爱的手机控们,你是否曾对安卓手机的刷机系统充满好奇?想要解锁手机潜能,体验全新的系统魅力?别急,今...
安卓系统程序有病毒 你知道吗?在这个数字化时代,手机已经成了我们生活中不可或缺的好伙伴。但是,你知道吗?即使是安卓系统,...
奥迪中控安卓系统下载,畅享智能... 你有没有发现,现在汽车的中控系统越来越智能了?尤其是奥迪这种豪华品牌,他们的中控系统简直就是科技与艺...