引言
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在 Linux内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进 Linux 内核中,当然也可以不编译进 Linux内核中,具体看自己的需求。
模块安装与卸载
在我们调试开发阶段,将模块编译成.ko文件,那么如何将这个文件加载进系统呢?
insmod xxx.ko
从系统中移除,当我们在.ko文件目录的时候可以带上.ko后缀,当我们不在目录中的时候要用第二种方法,第二种方法是通用的,建议任何时候使用第二种,不会出错。
rmmod xxx.ko
/* rmmod xxx */
但这时候问题来了,假设我们2.ko调用了1.ko中的函数,产生了依赖关系,那么我们需要先加载1.ko再加载2.ko,那么有没有一种方法可以一起加载呢?
modprobe 2.ko
使用modprobe 命令就可以把2.ko依赖的模块一起加载进来了,注意modprobe 命令默认会去
/lib/modules/目录中查找模块,一般自己制作的根文件系统是没有这个目录的,需要自己创建。同样modprobe 也可以用来卸载,但通常不建议这么做,一般卸载使用rmmod,因为modprobe 会递归卸载,假设1.ko正在被别的进程占用,那么就会出错。
modprobe -r 2.ko
内核驱动操作结构体
那么什么是内核操作结构体呢?我们看下图可知,当应用调用了open函数,最终会进入驱动程序的open函数,那么这个函数在哪里指定呢?就是指定内核操作结构体中成员。
在include\linux\fs.h中文件下有如下定义,这就是内核驱动操作结构体。
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 (*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);
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 *);
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);
};
- owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
- llseek函数用于修改文件当前的读写位置。
- read 函数用于读取设备文件。
- write 函数用于向设备文件写入(发送)数据。
- poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
- unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
- compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32
位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。 - mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD
显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。 - open 函数用于打开设备文件。
- release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
- fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
- aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
注册驱动模块加载与卸载函数
通过如下这两个函数就可以完成
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
那么模块加载函数和模块卸载函数的原型是什么呢?如下所示
/* insomod xxx 会进入这个函数 */
static int __init temp_drv_init(void){
return 0;
}
/* rmomod xxx 会进入这个函数 */
static void __exit temp_drv_exit(void){
}
驱动加入许可证信息
由于Linux基于GPL协议开源,根据GPL协议的传染性,要求你的驱动必须遵守GPL协议,所以这也就是很多厂家把驱动写的很简单,核心放在应用层的原因,如果不加这个,编译是不通过的。
注意,没有分号!
**
MODULE_LICENSE(“GPL”) //添加模块 LICENSE 信息
MODULE_AUTHOR(“xxx”) //添加模块作者信息
**
定义内核驱动操作结构体,添加一个操作函数
先定义这么一个结构体,填入操作函数名字
static struct file_operations temp_drv_opr = {
.owner = THIS_MODULE,
.open = temp_drv_open,
};
那么这个操作函数格式是什么呢,看一下原型
对应添加我们的open函数
static int temp_drv_open (struct inode *node, struct file *filep){
return 0;
}
字符设备注册与注销
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
- major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,写0的话会自动分配一个未占用的主设备号。
- name:设备名字,指向一串字符串。
- fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
- major:要注销的设备对应的主设备号。
- name:要注销的设备对应的设备名。
编写模板
#define DRV_NAME "temp_drv"
static int major;
static int temp_drv_open (struct inode *node, struct file *filep){
return 0;
}
static ssize_t temp_drv_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
return 0;
}
static ssize_t temp_drv_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt){
return 0;
}
static int temp_drv_close(struct inode *inode, struct file *filp){
return 0;
}
static struct file_operations temp_drv_opr = {
.owner = THIS_MODULE,
.open = temp_drv_open,
.release = temp_drv_close,
.read = temp_drv_read,
.write = temp_drv_write,
};
/* insomod xxx 会进入这个函数 */
static int __init temp_drv_init(void){
/* 参数1写0的话 系统自动分配主设备号 */
major = register_chrdev(0,DRV_NAME,&temp_drv_opr);
if(major < 0){
printk("%s:error init\n",DRV_NAME);
}
return 0;
}
/* rmomod xxx 会进入这个函数 */
static void __exit temp_drv_exit(void){
unregister_chrdev(major, DRV_NAME);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(temp_drv_init);
module_exit(temp_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jianglin");