linux驱动
makefile写法:
ifneq ($*KERNELRELEASE),) obj-m :=hello.o else KERNELDIR?=/lib/modules/$(shell uname -r) /build PWD :=$(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif
初始化和关闭:
static int __init initialization_funciton(void) { * } module_init(initialization_function); static void __exit cleanup_function(void) { * } module_exit(cleanup_function);
__init意为在初始化时调用,调用完后会被清除出内存
__exit意为只在清除时调用,调用后即清除
参数传递:
static char *whom =”world”;
static int howmany=1;
module_param(howmany,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);
内核支持的模块参数类型如下:
bool、invbool、charp、int、long、uint、ulong、ushort
声明数组参数用以下形式:
module_param_array(name,type,num,perm);
perm为访问许可值,S_IRUGO为任何人可读,S_IWUSR为root可写。
主设备号和次设备号
通常而言,主设备号指示设备对应的驱动程序,次设备号用于正确确定设备文件所指的设备。内核中用dev_t类型保存设备号
获得主次设备号:
MAJOR(dev_t,dev);
MINOR(dev_t,dev);
生成dev_t类型:
MKDEV(int major,int minor);
分配和释放设备编号:
静态分配:
<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first是要分配的设备编号的起始值,count是要请求连续设备编号的个数,name是与范围相关联的名称。成功返回0,错误返回负的错误码。
动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
释放:
void unregister_chrdev_region(dev_t first, unsigned int count);
重要的数据结构:
设备号与驱动程序操作的关联:
file_operations,指向该结构的指针一般名叫fops,包含指向对文件各个操作的实现函数的指针。
有个owner字段需要被初始化为THIS_MODULE
file结构:
file结构代表一个打开的文件,系统中每一个打开的文件在内核空间都有一个对应file结构
需要用到的file字段:
mode_t |
f_mode |
文本模式,标识文件是可读可写,但不需要在驱动中检测 |
loff_t |
f_ops |
当前读写位置 |
unsigned int |
f_flags |
文件标志,定义在<linux/fcntl.h> |
Struct file_operations |
* f_op |
与文件相关的操作 |
void |
* private_data |
私有数据,必须在file结构被销毁前在relrease方法中释放内存 |
struct dentry |
* f_dentry |
文件对应的目录项结构 |
inode结构:
dev_t i_rdev; 对表示设备文件的inode结构,该字段包含了真正的设备编号
struct cdev *i_cdev 表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
有两个宏可用来从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
字符设备的注册:
内核使用struct cdev结构表示字符设备,定义在<linux/cdev.h>
分配和初始化cdev的两种方式:
获得独立的结构:
struct cdev *my_code=cdev_alloc();
my_cdev->ops=&my_fops;
或者嵌入到自己的设备:(不知道为什么,,,)
void cdev_init(struct cdev*cdev, struct file_operations *fops);
还有一个struct cdev的owner字段需要被初始化为THIS_MODULE
告知内核该结构的信息:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
num是设备对应的第一个设备号,count应该是和该设备关联的设备编号的数量。
在驱动程序还没有完全准备好处理设备上的操作时,不能调用cdev_add。
从系统中移除一个字符设备:
void cdev_del(struct cdev *dev);
open和release
open应该完成以下操作:
检查设备特定的错误
如果设备是首次打开,则对其进行初始化
如有必要,更新f_op指针
分配并填写置于filp-private_data里的数据结构。
open方法原型:
int (*open) (struct inode *inode, struct file *filep);
inode参数中i_cdev字段包含注册设备时的cdev的指针
struct cdev结构体在装在模块时被装如内存,全局只有一个。
个人理解:cdev结构体代表着字符设备,自定义的字符设备需要将cdev结构嵌入到自定义的结构体中,当需要访问这个结构体时,需要通过inode结构获得cdev指针,然后通过container_of宏获得自定义结构体的指针。然后将这个指针通过写入file结构的private_data传递给其他文件操作函数。
release应该完成以下操作:
释放由open分配的保存在private_data中的所有内容
在最后一次关闭操作时关闭设备。
并不是每个close方法都调用release方法,只在file中维护的计数器为0时,才调用release。
实例scull的内存结构:
用到定义在<linux/slab.h>的两个函数
void *kmalloc(size_t size, int flags);
void kfree(void *ptr); //将kmalloc返回的指针传给kfree,但传NULL是合法的
定义在<asm/access.h>的两个函数
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
这两个函数还会检测用户空间指针的有效性,遇到无效地址不拷贝或仅拷贝部分内容,返回值为还需拷贝的数量。如果确定不需要检测,可使用__copy_to_user和__copy_from_user。
read和write:
ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
ssize_t类型为“signed size type(有符号的尺寸类型)” loff_t为“long offset type(长偏移量类型)”,指明用户在文件中进行存取操作的位置。
出错时,read和write方法都返回一个负值,大于等于0的返回值告诉的调用程序成功传递输了多少个字节。如果在正确传输部分数据之后发生了错误,则返回值必须是成功传输的字节数,但这个错误只能在下一次函数调用时才会得到报告。这种实现惯例要求驱动程序必须记住错误的发生,这样才能在将来把错误状态返回给应用程序。
内核函数通过返回负值来表示错误,而且该返回值表明了错误的类型,但运行在用户空间的程序看到的始终是作为返回值的-1。为了找出出错的原因,用户空间的程序必须访问errno变量。用户空间这种行为源于POSIX标准,但该标准未对内核内部的操作做任何要求。
read方法:
返回值是等于read系统调用传递的count参数,则说明所请求的字节数成功完成了。
返回值小于count,则说明只传输了部分数据。需要程序继续读取,如果用fread函数,则会一直读
返回值为0,表示已经到文件尾
负值表示出现错误。
还一种情况是,现在没有数据,以后可能会有
write方法:
返回值等于count则完成传输
是正的但小于count,则完成部分
为0表示什么也没写入。但这不是错误,可能是没有理由返回一个错误码,例如阻塞
负值表示错误。错误码定义在<linux/errno.h>
readv和writev
向量操作,这些向量型函数具有一个结构体数组,每个结构包含一个指向缓冲区的指针和一个长度值。如果驱动程序没有提供用于处理向量操作的方法,readv和writev会通过read和write方法的多次调用实现。
原型:ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev)(struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
struct iovec
{
void __user *iov_base;
__kernel_size_t iov_len;
}
iovec应由用户程序创建。