分类:
2011-09-11 11:17:03
6、字符设备的注册
在内核中,使用struct cdev(
有2种方法可以分配和初始化一个cdev结构. 如果你想在运行时获得一个独立的cdev 结构, 你可以使用下面这种方法
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
但是, 有时你会想将 cdev 结构嵌套到你定义的特定设备结构体中(scull就这样做了)。在这种情况下, 你应当使用下面的方法来初始化你已经分配的结构。
void cdev_init(struct cdev *cdev, struct file_operations *fops);
不管你使用以上哪种方法,在cdev中有个owner成员必须初始化为THIS_MODULE。
分配和初始化cdev 结构后, 就是调用cdev_add将cdev添加(注册)到内核 :
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
说明:
dev为要添加的cdev(字符设备)
num 是这个设备响应的第一个设备编号
count 是与该设备相关联的设备编号的数目. 常常 count 是 1, 但也有多个设备编号对应一个特定设备的情形. 例如, 设想 SCSI 磁带驱动, 它允许用户空间来选择操作模式(例如密度), 通过安排多个次设备号给每一个物理设备.
注意:
1、在绝大多数情况下,cdev_add调用都会成功,但也可能会失败。cdev_add调用失败时返回一个负数error number,并且你的设备不会被添加到内核中。
2、cdev_add成功返回时,你的设备就是可用的了,内核可以调用其设备操作。因此,在你的设备还没准备好处理设备操作前,不要调用cdev_add
通过cdev_del将字符设备从系统中卸载。
void cdev_del(struct cdev *dev);
注意:在2.6内核以前,注册字符设备使用如下方法:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major 是主设备号, name 是驱动名称(出现在/proc/devices中), fops 是缺省的 file_operations。register_chrdev为给定的主设备号注册次设备号0 - 255, 并且为每一个次设备号建立一个缺省的cdev结构。使用register_chrdev注册字符设备的驱动必须准备好处理对所有256个次设备号的open调用( 不管它们是否对应真实设备 ), 并且不能使用大于 255 的主或次设备号.
如果你使用register_chrdev注册字符设备,则应使用unregister_chrdev来卸载你的设备。
int unregister_chrdev(unsigned int major, const char *name);
major和name必须和传递给register_chrdev的相同, 否则unregister_chrdev会失败.
以下是scull的初始化代码(sucll_dev之前已经分配空间):
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
7、scull的内存使用
在scull中,使用核心函数kmalloc和kfree(
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
kmalloc在linux内核空间分配size字节的内存;返回值是指向那个内存的指针或者如果分配失败为NULL. flags 参数用来描述内存应当如何分配(详见第8章)。
scull模型:
/*
* Representation of scull quantum sets.
* 一个链表项(一个scull量子集)
*data中每一行代表一个量子
*/
struct scull_qset {
void **data;
struct scull_qset *next; /* 下一个链表节点(链表项) */
};
/* scull设备结构(包含了基本的cdev字符设备结构) */
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set (量子集链表)*/
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here (保存在其中的数据总量)*/
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
scull_trim用于释放整个数据区(量子集链表),类似于清零
|
1、检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误)
2、在设备第一次打开时, 初始化设备
3、如果有必要, 更新 f_op 指针.
4、分配并填充要置于filp->private_data里的任何数据结构
open的原型:
int (*open)(struct inode *inode, struct file *filp);
inode参数中包含有我们需要的信息——cdev(它的 i_cdev 成员)。唯一的问题是通常我们想要的不是 cdev 本身, 而是包含 cdev 的 scull_dev 结构。在这种情况下, 可以使用container_of(
container_of(pointer, container_type, container_field);
这个宏将一个container_field类型的指针pointer转换成一个container_type类型的指针(container_type结构体中包含了一个container_field类型的成员指针)。在 scull_open, 这个宏用来获取scull_dev。然后,将scull_dev保存在filp->private_data(为以后更方便访问scull_dev)。
scull中open方法:
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */
/*
*获取scull_dev并将它保存在filp->private_data 中,
*以方便其它函数使用scull_dev
*/
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* 如果以只写方式打开设备,则先调用scull_trim清空设备数据区 */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
{
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
scull_trim(dev); /* ignore errors */
up(&dev->sem);
}
return 0; /* success */
}
9、release
release提供释放内存,关闭设备的能力。有时release方法会命名为Device_close而不是Device_release。release要完成以下两个任务:
1、释放open分配在 filp->private_data 中的任何东西
2、在最后一次close操作时关闭设备
并不是每个 close 系统调用引起 release 方法的调用. 只有真正释放设备数据结构的close会调用release(因此得名)。内核维护一个引用计数器来记录file结构被引用多少次. fork 和 dup 都不创建新file(只有 open 会创建新file); 它们只是递增该引用计数器. close 系统调用仅在引用计数器为0时执行 release 方法。 release 方法和 close 系统调用之间的这种关系保证了你的驱动一次 open 对应一次 release.
注意:flush 方法在每次应用程序调用 close 时都被调用. 但是, 很少驱动实现 flush, 因为常常在 close 时没有什么要做, 除非调用 release.
因为scull设备是一个全局的永久内存区,不需要释放filp->private_data中的空间(在驱动卸载时才释放),也没有设备可关闭,所以其release函数中什么也没有。
|
10、read
read从设备中获取数据,其原型为:
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
说明:
filp 是设备文件指针,
count 是请求传输的数据大小.
buff 参数指向用户空间用来存储数据的缓存区
offp 是一个指向"long offset type"对象的指针, 表示当前用户正在读写的文件位置.
返回值:
1、返回值等于传递给 read 系统调用的 count 参数, 表示请求的字节数已经被传送. 这是最好的情况.
2、如果返回值是正数, 但是小于 count, 表示只有部分数据被传送. 这可能由于几个原因, 依赖于设备. 常常, 应用程序重新试着读取. 例如, 如果你使用 fread 函数来读取, 库函数重新发出系统调用直到请求的数据传送完成.
3、如果返回值为 0, 到达了文件末尾(没有读取数据).
4、返回值是一个负值error number,表示数据传送发生错误。出错的典型返回值包括 -EINTR( 被打断系统调用) 或者 -EFAULT( 坏地址 ).
注意:没有数据可以读取, 但是迟点会有数据达到。在这种情况下, read 系统调用应当阻塞(我们将在第 6 章介绍阻塞)
read方法的 buff 参数是用户空间指针. 因此, 它不能被内核代码直接解引用。但是为了完成read任务,你的驱动必须能够存取用户空间. 为安全起见,存取用户空间必须使用内核提供的特殊函数copy_to_user和copy_from_user(
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);
这两个函数不仅存取数据到用户空间,它还检查用户空间指针是否有效. 如果指针无效, 不进行拷贝; 如果在拷贝过程中遇到一个无效地址, 也就是说, 只拷贝部分数据. 在这2种情况下, 其返回值表示还要存取的数据量.
如果你不需要检查用户空间指针, 你可以调用 __copy_to_user和__copy_from_user 来代替。通过源码可知,copy_to_user(copy_from_user)在检查用户空间指针是否有效后,调用了__copy_to_user(__copy_from_user)。
read 方法的任务是从设备拷贝数据到用户空间(使用 copy_to_user), 而 write 方法必须从用户空间拷贝数据到设备(使用 copy_from_user). 每个read(write)系统调用请求一个特定数目字节的传送, 但是驱动可自由传送较少数据。不管驱动究竟传送多少数据, 在系统调用成功完成后,应当更新当前文件读写位置*offp。如果在数据传输过程发生error了(有一部分数据已经被传输), 返回值应当是已经成功传输了多少字节,而错误在下次调用read(write)时返回。
scull中read方法:
ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (*f_pos >= dev->size) /*文件当前读写位置已达(或超出)文件尾*/
goto out;
if (*f_pos + count > dev->size) /*在当前位置所要读的数目超过文件尾了*/
count = dev->size - *f_pos; /*实际可读到的数据量*/
/*确定当前文件读写位置在哪个量子集的哪个量子里,即dptr->data[s_pos] + q_pos*/
item = (long)*f_pos / itemsize;/*确定当前文件读写位置在哪个链表节点下(量子集)*/
rest = (long)*f_pos % itemsize;/*在这个链表节点(量子集)的偏移量*/
s_pos = rest / quantum; /*在这个节点里**data这个指针数组的第几行(量子)*/
q_pos = rest % quantum;/*在这个量子里的偏移量*/
/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item); /*找到这个量子集*/
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */
/*以一个量子为单位传,简化了代码*/
if (count > quantum - q_pos)
count = quantum - q_pos;
/*关键一步,将数据拷给用户空间*/
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count))
{
retval = -EFAULT;
goto out;
}
*f_pos += count; //当前文件读写位置
retval = count;
out:
up(&dev->sem);
return retval;
}
11、write
write向设备发送数据,与read相对,但与read有很多相似的地方。其原型为:
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
说明:
filp 是设备文件指针,
count 是请求传输的数据大小.
buff 参数指向用户空间要发送的数据
offp 是一个指向"long offset type"对象的指针, 表示当前用户正在读写的文件位置.
返回值:
1、返回值等于传递给 write 系统调用的 count 参数, 表示请求的字节数已经被传送. 这是最好的情况.
2、如果返回值是正数, 但是小于 count, 表示只有部分数据被传送.
3、如果返回值为 0, 表示没有数据被传送
4、返回值是一个负值error number。
同样,write方法的 buff 参数也是用户空间指针。它不能被内核代码直接解引用。但可以通过copy_from_user将数据从用户空间拷贝到设备(copy_from_user详情见read)。
scull的write方法类似read方法,这里不多说了。
11、readv和writev
readv和writev是read和write的矢量版本。它们使用一个结构体(structure)数组, 该结构体包含两个成员:缓存区指针和长度值。readv方法轮流读取指定数量的数据到每个缓存;相反, writev 将每个缓存的数据收集到一起后,发送给设备(单个写操作)。
如果你的驱动不支持矢量操作, readv 和 writev 通过多次调用你的 read 和 write 方法来实现. 但是,在许多情况下,直接实现 readv 和 writev 能获得更大的效率.
矢量操作的原型是:
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);
filp为设备文件指针
ppos 是文件的当前读写位置
iov是一个iovec结构体数组,数组每一元素描述了一块要传送的数据。这个结构数组由应用程序创建,并且内核在调用驱动程序之前,将它复制到内核空间。
count为数组iov的大小
iovec 结构, 定义于
struct iovec
{
void __user *iov_base; __kernel_size_t iov_len;
};
iovec描述了一块起始于iov_base (在用户空间)、长度为iov_len字节的数据。
|
scull和test编译后,下载到开发板,测试结果:
|