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应由用户程序创建。