Chinaunix首页 | 论坛 | 博客
  • 博客访问: 723743
  • 博文数量: 66
  • 博客积分: 2418
  • 博客等级: 大尉
  • 技术积分: 1659
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-28 10:04
个人简介

keep moving

文章分类

全部博文(66)

文章存档

2015年(2)

2014年(6)

2013年(7)

2011年(7)

2010年(42)

2009年(2)

分类: 嵌入式

2010-01-06 21:37:37

6字符设备的注册

在内核中,使用struct cdev)来表示字符设备。在内核调用你的设备操作前你必须分配并注册一个或多个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_addcdev添加(注册)到内核 :

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_operationsregister_chrdev为给定的主设备号注册次设备号0 - 255, 并且为每一个次设备号建立一个缺省的cdev结构。使用register_chrdev注册字符设备的驱动必须准备好处理对所有256个次设备号的open调用不管它们是否对应真实设备 ), 并且不能使用大于 255 的主或次设备号.

如果你使用register_chrdev注册字符设备,则应使用unregister_chrdev来卸载你的设备。

int unregister_chrdev(unsigned int major, const char *name);

majorname必须和传递给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中,使用核心函数kmallockfree)来管理linux内核的内存空间。

void *kmalloc(size_t size, int flags); 

void kfree(void *ptr);

kmalloclinux内核空间分配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用于释放整个数据区(量子集链表),类似于清零

 

int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset; /* 获取量子集中量子的个数 */
    int i;

    for (dptr = dev->data; dptr; dptr = next) /*遍历量子集链表*/
    { /* all the list items */
        if (dptr->data) 
        {
            for (i = 0; i < qset; i++) /*遍历量子集*/
                kfree(dptr->data[i]);/*释放当前量子的空间*/
            kfree(dptr->data);/*释放当前量子集的空间*/
            dptr->data = NULL;
         }
        next = dptr->next;
        kfree(dptr);/*释放当前链表节点*/
    }

    dev->size = 0;/*当前scull_dev所存的数据大小为0*/
    dev->quantum = scull_quantum;/*初始化量子大小*/
    dev->qset = scull_qset;/*初始化量子集大小*/
    dev->data = NULL;/*初始化量子集链表为NULL*/
    return 0;
}

 

8、open
    open提供驱动为后续的设备操作做任何初始化工作的能力. 在大部分驱动中, open 应当进行下面的工作:

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(  )宏将cdev转换成scull_dev

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)。

scullopen方法:

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_releaserelease要完成以下两个任务:

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函数中什么也没有。

int scull_release(struct inode *inode, struct file *filp)
{
    return 0;
}

 

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 系统调用应当阻塞(我们将在第 章介绍阻塞)

read方法的 buff 参数是用户空间指针因此它不能被内核代码直接解引用。但是为了完成read任务,你的驱动必须能够存取用户空间为安全起见,存取用户空间必须使用内核提供的特殊函数copy_to_usercopy_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_usercopy_from_user)在检查用户空间指针是否有效后,调用了__copy_to_user__copy_from_user)。

read 方法的任务是从设备拷贝数据到用户空间(使用 copy_to_user), 而 write 方法必须从用户空间拷贝数据到设备(使用 copy_from_user). 每个readwrite)系统调用请求一个特定数目字节的传送但是驱动可自由传送较少数据。不管驱动究竟传送多少数据在系统调用成功完成后,应当更新当前文件读写位置*offp。如果在数据传输过程发生error了(有一部分数据已经被传输), 返回值应当是已经成功传输了多少字节,而错误在下次调用readwrite)时返回。

scullread方法:

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)。

scullwrite方法类似read方法,这里不多说了。

 

11readvwritev

readvwritevreadwrite的矢量版本。它们使用一个结构体(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字节的数据。

 

 测试程序:

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    char buffer1[]="hello,lingd!";
    char buffer2[10];
    int fd_scull;
    int ret = 0;
    int size = sizeof(buffer1) / sizeof(char) - 1;

     /*以只写方式打开设备*/
    fd_scull = open("/dev/scull0", O_WRONLY);    
    /*向设备发送数据,若一次发送不完,则分多次发送*/
    while (size > 0)
    {
        ret = write(fd_scull, buffer1 + ret, size);
        if (ret < 0)
        {
            perror("write error");
            exit(1);
        }

        printf("write size: %d\n", ret);
        size = size - ret;
    }

    close(fd_scull);

     /*以只读方式打开设备*/
    fd_scull = open("/dev/scull0", O_RDONLY);
    /*从设备读取数据,若一次无法读取完,则分多次读取*/
    size = sizeof(buffer1) / sizeof(char) - 1;
    while (size > 0)
    {
        ret = read(fd_scull, buffer2, size);
        if (ret < 0)
        {
            perror("read error");    
            exit(1);        
        }

        buffer2[ret] = '\0';
        printf("read: %s\n", buffer2);
        size = size - ret;        
    }

    close(fd_scull);
    return 0;
}

scull和test编译后,下载到开发板,测试结果:

/tmp/lingd/ldd/scull # ./scull_load    #量子大小为默认的4000
scullsingle registered at fd00008
sculluid registered at fd00009
scullwuid registered at fd0000a
sullpriv registered at fd0000b
/tmp/lingd/ldd/scull # lsmod
Module Size Used by
scull 15448 0
/tmp/lingd/ldd/scull # cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  6 lp
  7 vcs
 10 misc
 13 input
 14 sound
 29 fb
 81 video4linux
 90 mtd
 99 ppdev
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
253 scull
253 scullp
253 sculla
254 usb_endpoint
                                                                                                           
Block devices:
  1 ramdisk
  7 loop
 31 mtdblock
179 mmc
180 ub
/tmp/lingd/ldd/scull # ./test
write size: 12
read: hello,
/tmp/lingd/ldd/scull # ./scull_unload
/tmp/lingd/ldd/scull # lsmod
Module Size Used by
/tmp/lingd/ldd/scull # ./scull_load scull_quantum=6  #量子大小设置为6
scullsingle registered at fd00008
sculluid registered at fd00009
scullwuid registered at fd0000a
sullpriv registered at fd0000b
/tmp/lingd/ldd/scull # ./test
write size: 6
write size: 6
read: hello,
read: lingd!
/tmp/lingd/ldd/scull # ./scull_load scull_quantum=5
scullsingle registered at fd00008
sculluid registered at fd00009
scullwuid registered at fd0000a
sullpriv registered at fd0000b
/tmp/lingd/ldd/scull # ./test
write size: 5
write size: 5
write size: 2
read: hello
read: ,ling
read: ,l


阅读(1298) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~