字符设备驱动程序

[ 795 查看 / 1 回复 ]

1.主设备号和次设备号
  linux下有一切设备皆文件的说法,不管这句话正确与否,但是起码说明了一个问题,大部分linux设备在应用层看来都是一些dev目录下的文件节点,事实也正是如此,那我们写的驱动程序怎么和dev目录下对应的文件节点连接呢?这就是设备号的作用了,驱动和应用通过主设备号连接,而次设备号则向驱动表明我当面是哪个设备正在被你使用,因为一个驱动程序可能会被n个设备拥有,(真羡慕驱动啊~~这在我们一夫一妻的原则是不允许的

  类型dev_t<linux/types.h>表明的设备号的类型(查看头文件知道其实就是一个u32类型),高12位表示主设备号,低20位表示次设备号,可以通过以下三个宏来获得主次设备号或者通过住次设备号产生一个dev_t类型的设备号。
  • #include<linux/kdev_t.h>
  • MAJOR(dev_t dev) //get major number
  • MINOR(dev_t dev) //get minor number
  • MKDEV(int major,int minor) //generate device number


实现很简单,一些移位操作,具体可以查看相关头文件。

  那么在linux驱动设计中,我们如何使用函数来获得设备编号呢?有两种方法,静态分配和动态分配
静态分配函数如下:
  • int register_chrdev_region(dev_t first,unsigned int count,char *name);
  • /*
  • **  first:设备号
  • **  count:所请求的设备编号的个数
  • **  name:和设备编号关联的名称,出现在/proc/devices/或者/sysfs下
  • **  返回值:成功返回0;失败返回负的错误码    */


这种静态分配的方式要求我们预先自己分配设备号first,而这种方式会使我们编写的驱动程序非常笨,兼容性不好,所以一般采用第二种方法动态分配设备号:
  • int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);

其中dev是一个指针,很明显是我们得到的由该函数返回的设备号,firstminor是指第一个次设备号,通常是0,其余两个参数和返回值都和静态分配函数一样。
最后我们要释放这些设备编号的话,会用到下面这个函数:
  • void unregister_chrdev_region(dev_t first,unsigned int count);

这个函数一般会在模块清除函数中被调用。
而对于书中提到的scull设备的设备号分配方式用下面的方式就很好理解了,定义scull_major为主设备号,0表示动态分配,代码如下:
  • if(scull_major){
  •   dev = MKDEV(scull_major,scull_minor);
  •   result = register_chrdev_region(dev,scull_nr_devs,"scull");
  • }else{
  •   result = alloc_chrdev_region(&dev,scull_minor,scull_nr_devs,"scull");
  •   scull_major = MAJOR(dev);
  • }
  • if(result < 0){
  •   printk(KERN_WARNING"scull: can't get major %d\n",scull_major);
  •   return result;
  • }

2.一些重要的数据结构
  2.1 struct file_operations
  这个结构在<linux/fs.h>中定义,这里我把这个重要的结构搬了过来:
  • /*
  • * NOTE:
  • * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
  • * can be called without the big kernel lock held in all filesystems.
  • */
  • 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 *, struct dentry *, 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 **);
  • };

全是一些函数指针,其中很熟悉的有read,write,open,close,ioctl,哈哈你猜对了,正好对应应用层的系统调用,可以说一个字符设备驱动程序大部分职责就是需要驱动工程师完成这里必要的函数指针的填充。对应书中说的scull设备这个初始化可以用以下方式完成:
  • struct file_operations scull_fops = {
  •       .owner = THIS_MODULE,
  •       .llseek = scull_llseek,
  •       .read = scull_read,
  •       .write = scull_write,
  •       .ioctl = scull_ioctl,
  •       .open = scull_open,
  •       .release = scull_release,
  • };

也就是说我们的scull设备支持lseek,read,write,ioctl,open,close系统调用。
  2.2 struct file
    这个结构也在<linux/fs.h>中定义,这个结构是一个内核结构,应用每打开一个文件,就对应这样一个结构,通常指向这样一个结构的指针定义为"filp"。
  • struct file {
  •         /*
  •         * fu_list becomes invalid after file_free is called and queued via
  •         * fu_rcuhead for RCU freeing
  •         */
  •         union {
  •                 struct list_head fu_list;
  •                 struct rcu_head fu_rcuhead;
  •         } f_u;
  •         struct path f_path;
  • #define f_dentry f_path.dentry
  • #define f_vfsmnt f_path.mnt
  •       const struct file_operations *f_op;
  •         atomic_long_t f_count;
  •         unsigned int f_flags;
  •         fmode_t f_mode;
  •         loff_t f_pos;
  •         struct fown_struct f_owner;
  •         const struct cred *f_cred;
  •         struct file_ra_state f_ra;
  •         u64 f_version;
  • #ifdef CONFIG_SECURITY
  •         void *f_security;
  • #endif
  •         /* needed for tty driver, and maybe others */
  •         void *private_data;
  • #ifdef CONFIG_EPOLL
  •         /* Used by fs/eventpoll.c to link all the hooks to this file */
  •         struct list_head f_ep_links;
  •         spinlock_t f_ep_lock;
  • #endif /* #ifdef CONFIG_EPOLL */
  •         struct address_space *f_mapping;
  • #ifdef CONFIG_DEBUG_WRITECOUNT
            unsigned long f_mnt_write_state;
    #endif
    };

这个结构中也有几个很重要的成员:
  mode_t f_mode;文件读写模式,通过FMODE_READ和FMODE_WRITE来标识,不过内核已经帮驱动程序完成了读写权限的检查,所以驱动程序可以不用做额外的判断。
  loff_t f_pos;64位数,表示当前的读写位置,read/write可以通过改变最后一个参数 loff_t *的位置来更新此域,而不是直接调用filp->f_pos来改变,但一个例外是llseek系统调用,因为该方法本身就是为了改变此域值,所以可以直接改变此域。
  unsigned int f_flags;文件标志,最常用的O_NONBLOCK,应用层请求可以是非阻塞式的操作,驱动程序一般需要检查O_NONBLOCK,注意该域和f_mode的区别。
  sturct file_operations *f_op;这个域的作用是方便主设备号相同的设备在open时修改file_operations对应的操作,类似于c++中的重载,比如主设备号为1的(dev/null和dev/zero)需要根据各自的次设备号在打开操作的时候替换filp->f_op。这样的好处是不会增加系统调用的负担。
  void *private_data;这个域一般用来保存我们自定义的设备结构,方便各个方法之间获取设备结构。
  2.3 struct inode
  内核在内核用inode表示文件,一个文件被多次打开对应多个file结构,但是他们都指向一个inode结构,indoe结构和打开的文件是一一对应的(相信它也很羡慕驱动~~)。
  两个域很重要:
  dev_t i_rdev; 看类型都应该猜出来吧,没错,就是我们的设备号。
  struct cdev *i_cdev;这是一个新的结构体,在内核中,每种设备都会有一种结构体对应,像我们这的字符设备就对应struct cdev这种结构体,像以后涉及到的网络设备,usb设备都会有一个专门的结构体来描述这种对应的设备。
  值得注意的是如果我们要从inode中获得设备号,为了编写代码移植性强的代码,应该使用下面的宏获得:
  unsigned int iminor(struct inode *inode);
  unsigned int imajor(struct inode *inode);
3.字符设备的注册
  在分析之前,先看看书中提到的scull_dev设备结构:
  • struct scull_dev{
  •   struct scull_qset *data;
  •   int quantnum;
  •   int qset;
  •   unsigned long size;
  •   unsigned int acess_key;
  •   struct semaphore sem;
  •   struct cdev cdev;
  • };

  这里先看最后一个字段,内嵌了一个字符设备cdev结构,其余的域我们后面分析read/write的时候再详细讲解。
  3.1分配和初始化一个cdev结构有两种方式:
  3.1.1获得一个独立的cdev结构:
  • struct cdev *my_dev = cdev_alloc();
  • my_cdev->ops = &my_fops;

  3.1.2将cdev结构嵌入到自己的设备结构当中,我们可以采用下面的API进行初始化:
  • void cdev_init(struct cdev *cdev,struct file_operations *fops);

  上面两种方式都只分配了cdev结构以及初始化了fops,还有一个cdev结构的owner域必须初始化:
  dev->cdev.owner = THIS_MODULE;
  来看看书中scull设备的初始化方法:
  • static void scull_setup_dev(struct scull_dev *dev,int index)
  • {
  •   int err,devno = NKDEV(scull_major,scull_minor+index);
  •   cdev_init(&dev->cdev,&scull_fops);
  •   dev->cdev.ops = &scull_fops;
  •   err = cdev_add(&dev->cdev,devno,1);
  •   if(err)
  •     printk(KERN_NOTICE"Error %d adding scull%d",err,index);
  • }

由于cdev内嵌在设备结构scull_dev中,所以使用第二种方法初始化。
4.open/release
  4.1 open方法
  先看看一个通用的字符设备驱动程序open方法应该做的工作:
  * 检查设备特定的错误(如设备未就绪)
  * 如果设备是首次打开,应该进行初始化
  * 如有必要更新f_op指针(前面讲到有时候需要转换f_ops的操作,以提高系统调用响应时间)
  * 分配并填写置于filp->private_data里的数据结构,一般用于保存我们的设备结构体,如下代码所示:
  • struct scull_dev *dev;  // device pointer
  • dev = container_of(inode->i_cdev,struct scull_dev,cdev);
  • filp->private_data = dev;  //keep device infomation

有一个陌生的宏函数container_of,其实这个宏函数的作用就是从一个结构体域的成员信息获得该结构体的地址信息,内核提供了很方便的宏函数让我们使用,即使我们用c也能实现类似的技巧,但是最好还是用内核提供的API。该宏函数的定义在<linux/kernel.h>中定义,有兴趣的朋友可以去看看究竟。
再看看scull设备的open操作是如何做的:
  • int scull_open(struct inode *inode,struct file *filp)
  • {
  •   struct scull_dev *dev;
  •   dev = container_of(inode->i_cdev,struct scull_dev,cdev);
  •   filp->private_data = dev;
  •   if((filp->f_flags & O_ACCMODE)==O_WRONLY)
  •     scull_trim(dev);
  •   return 0;
  • }

这里有个scull_trim函数是当应用层以写方式打开文件时,长度截为0。其余的应该很好理解了吧?
  4.2 release方法
  先看看release方法应该要实现的功能:
  * 释放由open分配的,保存在filp->private_data中的所有的内容。
  * 在最后一次关闭操作时关闭设备。
  由于scull设备不对应实际的物理设备,所以scull设备的release函数很简单:
  • int scull_release(struct inode *inode,struct file *filp)
  • {
  •   return 0;
  • }

书中还提到一个很有意思的问题,当关闭一个设备文件的次数大于打开它的次数,系统会发生什么情况呢?
比如,dup和fork系统调用会不在调用open的情况下创建已打开文件的副本,但是每一个副本都会在程序终止时关闭。再如,大多数程序从不打开stdin文件,但是都会在终止时关闭它,那么驱动程序如何才能知道一个打开的文件要真正调用release呢?
答案:并不是每个close系统调用都会引起release的调用,只有当内核维护的file结构的count为0时,才会去调用release,所有前面的dup之类的系统调用只是会增加file结构的count计数。
5.scull设备的内存使用
  先看定义的scull_qset结构:
  struct scull_qset {
    void **data;
    struct scull_qset *next;
  };
  在看看内核中分配内存和释放内存的API,如下:
  • #include<linux/slab.h>
  • void *kmalloc(size_t size,int flags);
  • void kfree(void *ptr);

kmalloc试图分配size个字节的内存,返回值指向该内存的指针,失败返回NULL。flags参数描述内存的分配方法,包括GFP_KERNEL.GFP_ATOMIC等,再后面第八章中会详细讲解。等不及了可以买本书来翻翻哈~,我已经翻过了~~~
得到的内存可以用kfree释放,参数为kmalloc返回得到的指针,注意书中说将NULL指针传递给kfree是合法的。
  在讲解read/write之前需要先知道书中提到的scull设备的内存分配策略。其实一个图就能很清晰的显示它的内存策略了,如下图:

从图可以看出,一个设备其实就是一个指针链表,每个指针指向一个scull_qset结构,这个结构通过一个指针数组可以拿到4M字节的内存区域,如果定义指针数组大小1000,每个指针指向一个4000字节的buffer的话。
6.read/write
  直接上驱动方法中read,write的函数原型:
  • ssize_t read(struct file *filp,char __user *buff,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 int类型,size_t表示unsigned int,loff_t表示long long类型,__user表示用户空间的指针,基于一些内核设计原因,用户空间指针不能直接被内核空间使用,必须使用内核提供的API进行数据的拷贝
其中两种方式是:
  • 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);

我想大家根据名字也可以猜出各参数的意义了吧,这里就不说明了。有什么不懂的尽管留言。
  下面说明read方法在一个字符设备驱动中应该做的事情:
  * 如果返回值等于传递给read系统调用的count参数,说明请求成功
  * 如果返回值是正的,但是小于count,说明部分数据请求成功。这种情况因设备的不同可能有很多原因,大部分情况,程序会重新读取数据,例如,如果用fread函数读取数据,这个库函数就会不断调用系统调用read,直到所有的请求数据传输完毕
  * 如果返回值是0,表示达到了文件尾。
  * 负值意味着发生了错误,该值表明了发生了什么错误,错误码在<linux/errno.h>中定义,比如-EINTR(系统调用中断),-EFAULT(无效地址)。
  * 现在还没有数据,以后可能有,这种情况需要阻塞read系统调用(阻塞:即让出处理器,程序进入休眠状态)
  下面说明write方法在一个字符设备驱动中应该做的事情:
  * 如果返回值等于count,则完成了应用的请求。
  * 如果返回值是正的,但小于count,则只传输了部分数据,程序很可能会再次试图写余下的数据。
  * 如果值为0,意味着什么也没写入。
  * 负值意味着发生了错误。
7.测试驱动程序
  还是在开发板上进行测试,建立好环境后,可以用cp,dd,输入输出重定向来测试。
测试例:ls -l > /dev/scull0
        cp test.txt /dev/scull0
[img=77,5]chrome://livemargins/skin/monitor-background-horizontal.png[/img]        [img]chrome://livemargins/skin/monitor-background-vertical.png[/img]        [img]chrome://livemargins/skin/monitor-play-button.png[/img][img=77,5]chrome://livemargins/skin/monitor-background-horizontal.png[/img]        [img]chrome://livemargins/skin/monitor-background-vertical.png[/img]        [img]chrome://livemargins/skin/monitor-play-button.png[/img][img=77,5]chrome://livemargins/skin/monitor-background-horizontal.png[/img]        [img]chrome://livemargins/skin/monitor-background-vertical.png[/img]        [img]chrome://livemargins/skin/monitor-play-button.png[/img]
最后编辑asdfjljsdfklj 最后编辑于 2011-08-17 17:18:53
TOP
凌阳教育嵌入式培训

这个以前学过,现在都忘记了,呵呵
TOP