驱动模块操作命令
代码语言:javascript复制# 列出当前内核中已经安装的模块(list module)
lsmod
# 安装模块(install module)
insmod xxx.ko
# 打印模块的自带信息(module information)
modinfo xxx.ko
# 卸载模块(remove module)
rmmod xxx
# 查看驱动加载信息
dmesg
# 查看设备号
cat /proc/devices
# 创建设备文件
# 格式(c表示字符设备驱动、b表示块设备驱动):
mknod /dev/xxx c或者b major minor
查看设备文件信息
代码语言:javascript复制ls -l /dev/xxx
最简单的模块
module_test.c
代码语言:javascript复制#include <linux/module.h>
#include <linux/init.h>
static int __init chrdev_init(void)
{
printk(KERN_INFO "moudue_test chrdev initrn");
return 0;
}
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "module_test chrdev exitrn");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ifan Tsai");
__init__宏:被修饰的函数会被链接器链接放入.init.text段中(本来默认情况下函数是被放入.text段中)。对内核而言是一种暗示,表示该函数仅在初始化期间使用,内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
__exit__宏:被修饰的函数仅用于模块卸载,链接器会将其放入特殊的ELF段。如果模块被直接内嵌到内核中,或内核的配置不允许卸载模块,则被修饰的函数将被简单的丢弃。
prink函数:模块在被加载到内核后,它能调用的函数仅仅是由内核导出的那些函数。KERN_INFO是printk的打印级别,其实只是一个字符串(如<1>)。操作系统的命令行中也会有一个打印级别的设置(值为0-7),当前操作系统中执行printk的时候会去对比printk中的打印级别和操作系统命令行中设置的打印级别,小于命令行设置级别的信息会被打印出来,大于的会被拦截。
module_init宏:该宏声明的函数会在模块被装载到内核中调用。
module_exit宏:该宏声明的函数会在模块被卸载时调用。
MODULE_LICENSE宏:指定该代码所使用的许可证协议。
MODULE_AUTHOR:描述模块作者。
附:可在模块中包含的其他描述信息还有:MODULE_DESCRIPTION(模块的简短描述)、MODULE_VERSION(代码修订号)、MODULE_ALIAS(模块别名)等等
Makefile
代码语言:javascript复制# ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录
KERN_DIR = ~/Embedded/kernel
obj-m = module_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
# 将编译好的模块复制到开发板的rootfs下
cp *.ko -f ~/Embedded/nfs/rootfs/root/driver
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
驱动模块的Makefile是通过make -C
进入到内核源码树下借用内核源码的编译体系(调用内核源码下的Makefile)来完成驱动模块的编译链接。
内核和应用之间的数据传递
copy_from_user
从用户空间拷贝数据到内核空间
代码语言:javascript复制/* asm/uaccess.h */
static inline unsigned long __must_check copy_from_user(void* to, const void __user *from, unsigned long n)
返回值:成功返回0, 失败返回剩下的未成功复制的字节数
copy_to_user
从内核空间拷贝数据到用户空间
代码语言:javascript复制/* asm/uaccess.h */
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
返回值:成功返回0, 失败返回剩下的未成功复制的字节数
file_operations结构体
代码语言:javascript复制/* 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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *, int datasync);
int (*aio_fsync) (struct kiocb *, 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 **);
};
该结构体成员除了owner,其他皆为函数指针。每个设备驱动都需要一个该结构体类型的变量,在应用层的系统编程中open、read、write、close等API最终调用的就是该结构体内函数指针的指向。该结构体第一个成员owner指针指向拥有该结构体模块的指针,这个成员几乎所有情况下都被初始化为THIS_MODULE,相当于c 中的this指针。
字符设备驱动老接口
register_chrdev
向内核注册字符设备驱动
代码语言:javascript复制/* linux/fs.h */
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
unsigned int major:主设备号(1 - 255),传递0则由内核自动分配一个合适的空闲的主设备号
**const char *name**:设备名称
**const struct file_operations *fops**:结构体指针, file_operations里封装了一堆字符驱动操作的函数指针
返回值:成功返回主设备号, 失败返回一个负值
内核中有一个数组要来存储注册的字符设备驱动,register_chrdev注册的字符设备驱动的信息主要就存在这个数组的相应位置,该数组的下标就是主设备号,可以通过查看/proc/devices文件来确定已经注册了 的字符设备驱动和块设备驱动以及相应的编号
代码语言:javascript复制cat /proc/devices
unregister_chrdev
从内核卸载字符设备驱动
代码语言:javascript复制/* linux/fs.h */
static inline void unregister_chrdev(unsigned int major, const char *name)
unsigned int major:主设备号(1 - 255),传递0则由内核自动分配一个合适的空闲的主设备号
**const char *name**:设备名称
字符设备驱动新接口
主次设备号
在老接口中使用register_chrdev函数来注册字符设备驱动,但是设备号只有major(主设备号),而新接口中提供了minor(次设备号),内核中通过dev_t类型描述设备号,其实质是unsigned int类型,其中高12位为设备号,低20位为次设备号。内核并提供了三个宏函数对dev_t的操作。
代码语言:javascript复制/* linux/kdev_t.h */
// 传入两个参数生成设备号(dev_t),分别为主设备号和次设备号
MKDEV(ma, mi)
// 传入设备号(dev_t),返回主设备号
MAJOR(dev)
// 传入设备号(dev_t),返回次设备号
MINOR(dev)
register_chrdev_region
事先知道要使用的主次设备号,需要cat /proc/devices查看未使用的设备号,则使用该接口来分配主次设备号。
代码语言:javascript复制/* linux/fs.h */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
from: 起始设备号(主次)
count: 请求分配的连续设备的数量
name: 设备名称
返回值: 成功返回0, 失败返回负值
alloc_chrdev_region
动态分配主次设备号
代码语言:javascript复制/* linux/fs.h */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:传出参数,传入需要自动分配的设备号变量
baseminor: 起始次设备号
count: 请求分配的连续设备的数量
name: 设备名称
返回值: 成功返回0, 失败返回负值
unregister_chrdev_region
注销主次设备号
代码语言:javascript复制/* linux/fs.h */
void unregister_chrdev_region(dev_t from, unsigned count)
from: 起始设备号(主次)
count: 注销的连续设备的数量
struct cdev
内核使用cdev结构体来描述字符设备,在新接口中必须使用该结构体和file_operations结构体一起来描述一个字符设备驱动。
代码语言:javascript复制/* linux/cdev.h */
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; // fops
struct list_head list; // 内核链表
dev_t dev; // 设备号 (主设备号 次设备号)
unsigned int count; // 文件引用计数
};
cdev_alloc
为cdev指针分配内存
代码语言:javascript复制/* linux/cdev.h */
struct cdev *cdev_alloc(void)
cdev_init
初始化cdev结构体,将cdev与file_operations绑定起来
代码语言:javascript复制/* linux/cdev.h */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);
这两句代码在cdev_alloc中已经做了,所以这个函数可以用cdev->ops = fops;
代替
cdev_add
注册字符设备驱动
代码语言:javascript复制/* linux/cdev.h */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
p:cdev结构体变量的指针
dev: 起始设备号(主次)
count: 请求注册的连续设备的数量
返回值:成功返回0, 失败返回负值
cdev_del
注销字符设备驱动,并释放用cdev_alloc分配的内存
代码语言:javascript复制/* linux/cdev.h */
void cdev_del(struct cdev *p)
本文作者: Ifan Tsai (菜菜)
本文链接: https://cloud.tencent.com/developer/article/2164585
版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!