Chinaunix首页 | 论坛 | 博客
  • 博客访问: 83518
  • 博文数量: 11
  • 博客积分: 386
  • 博客等级: 一等列兵
  • 技术积分: 240
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-02 17:11
文章分类

全部博文(11)

文章存档

2012年(11)

我的朋友

分类: LINUX

2012-09-16 13:36:36

设备号

主设备号用于区分不同的驱动程序;

次设备号用于区分不同的设备(比如不同的分区);

设备号用dev_t来表示,它包括了主、次设备号,创建方法为:

MKDEV(int major, int minor);

注册设备号的方法为:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

手动分配设备号无法确保和现有系统中已分配的设备号不产生冲突,所以可以让系统自动分配:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

通常可以看到如下代码:

  1. if (scull_major) {
  2.     dev = MKDEV(scull_major, scull_minor);
  3.     result = register_chrdev_region(dev, scull_nr_devs, "scull");
  4. } else {
  5.     result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
  6.     scull_major = MAJOR(dev);
  7. }

  8. if (result < 0) {
  9.     printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
  10.     return result;
  11. }
     让内核自动分配设备号的话我们无法事先预知设备号,从而无法预先在/dev下创建设备节点,但是在驱动注册了设备之后,可以读取/proc/devices下的文件以获得内核分配给该设备的设备号,然后再创建设备节点。创建设备节点的命令是mknod

当打开某个设备节点(file结构体),内核会根据设备节点调用相应驱动程序。

 

file结构体

打开的file的操作方法用结构体struct file_operations来描述,其中部分有:

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 *);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

每一个打开的文件(不管是普通文件还是设备文件),内核中都用一个struct file来描述,它总是在一个文件被打开(open系统调用)的时候创建,相当于一个文件描述符,struct file结构体中包括了:

loff_t f_pos;

unsigned int f_flags;       // O_RDONLY, O_NONBLOCK

struct file_operations *f_op;//文件在open的时候,将其指向某个struct file_operations

struct dentry *f_dentry;    //通过filp->f_dentry->d_inode访问inode

一个文件可以被多个进程打开,每个进程都有这个文件的文件描述符(struct file),但是系统中只有一个这样的文件,文件本身的信息用structure inode来描述,其中只有少数几个与驱动编程有关:

struct inode{

          ...

          dev_t          i_rdev;

          umode_t        i_mode;

          struct file_operations *i_fop;

          union {

                  struct block_device *i_bdev;

                  struct cdev         *i_cdev;

          };

          ....

};

 

设备的注册

内核中用cdev来表示字符设备,用户一般定义自己的数据结构,并包含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      */

};

字符设备的注册指告诉内核系统中多了一个设备并且本设备(模块)支持该字符设备的操作:


  1. static void scull_setup_cdev(struct scull_dev *dev, int index)
  2. {
  3.     int err, devno = MKDEV(scull_major, scull_minor + index);
  4.     cdev_init(&dev->cdev, &scull_fops);
  5.     dev->cdev.owner = THIS_MODULE;
  6.     dev->cdev.ops = &scull_fops; //1.cdev的初始化
  7.     err = cdev_add (&dev->cdev, devno, 1); //2.cdev的注册,添加到驱动模型中
  8.     /* Fail gracefully if need be */
  9.     if (err)
  10.     printk(KERN_NOTICE "Error %d adding scull%d", err, index);
  11. }
     scull_setup_cdev一般在模块初始化或者probe通过的情况下调用,因为其中注册了一种新的字符设备,之后内核就知道系统中有这么一个新的字符设备了。然后通过某种方法(查看/proc或者udev),系统为该设备在/dev下面创建设备节点(文件),每一个设备节点就有了inode

udev的原理是设备注册时通过在/sys下创建类和设备(class_createdevice_create等操作),udev会周期的扫描/sys从而在/dev下会自动的建立相应的设备节点。

 

设备的打开与关闭

操作设备文件的第一步是open系统调用,它最终会调用到设备驱动中的open函数,其中主要完成设备初始化的工作,按需要更新filef_opsfile_operation的指针):

  1. int scull_open(struct inode *inode, struct file *filp)
  2. {
  3.     struct scull_dev *dev; /* device information */
  4.     dev = container_of(inode->i_cdev, struct scull_dev, cdev);
  5.     filp->private_data = dev; /* for other methods */

  6.     /* now trim to 0 the length of the device if open was write-only */
  7.     if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
  8.         scull_trim(dev); /* ignore errors */
  9.     }
  10.     return 0; /* success */
  11.     
  12. }//有的时候会为open计数
    在驱动开发中,文件读/写模式 filp->f_mode、标志filp->f_flags都是设备驱动关心的内容,而私有数据指针 filp->private_data在驱动中被广泛使用。大多被指向设备驱动自定义的用于描述设备的结构 体,filp->private_data在open中赋值。

fork()不创建新文件,它只增加file结构体中的引用计数。close系统调用仅在文件的引用计数减为0时调用驱动的release方法。

系统调用open打开一个字符设备的时候,根据设备号(inode->i_rdev), 通过kobj_lookup()cdev_map中查找对应的驱动程序,字符设备cdev默认的file_operations中定义了chrdev_open,在字符设备open操作的过程中,将inode所指向的file_operations替换成cdev所指向的file_operations

应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是 file_operations。由于字符设备的上层没有文件系统,所以的file_operations由设备驱动提供。

内核基本不关心次设备号,驱动程序才关心次设备号,次设备号的使用方法:


  1. int minor_open (struct inode *inode, struct file *filp){
  2.      switch (MINOR (inode->i_rdev)) {
  3.          case 1:
  4.               filp->f_op = &minor0_fops;
  5.               break;

  6.          case 2:
  7.               filp->f_op = &minor1_fops;
  8.               break;

  9.          default:
  10.               return -ENXIO;
  11.        }

  12.      if (filp->f_op && filp->f_op->open)
  13.          return filp->f_op->open (inode, filp); //运行次设备号配的open()函数
  14.      return 0;
  15. }

 设备的读写操作

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

其中的count是需要读写的字节数。返回值返回了实际读写的字节数,返回负数表示某种错误类型(如-EINTR-EFAULT等)。实际读写的字节数可能会小于count,多数情况下,应用程序会重复进行读写操作。

因为读写涉及了内核与用户两个空间:

出于安全性等因素的考虑(用户空间的数据在交换区、用户空间指针不合法等等情况),两个空间的数据不能直接拷贝,而需要通过下述方式:

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_from_user可能会导致调用的进程被挂起。

 

设备的IOCTL

ioctl用以处理非常规操作,比如光驱设备的弹出收拢、报错等等。ioctl系统调用的函数接口为:

int (*ioctl) (struct inode *inode, struct file *filp,unsigned int cmd, unsigned long arg);

每个ioctlcmd应该要做到系统唯一,cmd中包含了typenumberdirectionsize等信息,可以用过_IO_IOR_IOW_IOWR等宏来生成(详见LDD第六章)。

驱动中ioctl的实现一般是一个switch结构,每个switch的分支都代表一个cmd,在进入switch之前应判断cmd的有效性,若为非法的cmd,应返回给libclibc实现了系统调用)“-EINVAL”。

 

阻塞型读写(blocking I/O

很多情况下,设备的读写并不直接读写硬件,而是驱动程序在硬件与上层软件之间建立了一个缓冲区,读写等系统调用都是在这个缓冲区上进行的。如果在进行读操作的时候,发现读缓冲区上没有数据可读,可以将当前进程挂起(sleep);如果在进行写操作的时候,发现写操作缓冲区上数据已满(新的数据写不下),也可以将当前进程挂起。当driver检测到设备有数据可读或者有空间可写时,再唤醒进程。

特别地,对于写操作,若应用程序要确保数据写入设备,则在驱动层需要实现fsync系统调用。

     进程的挂起(sleep)在进程一篇中叙述过,被挂起的进程被移出活动进程组(不能被CPU调度,状态从TASK_RUNNINGTASK_INTERRUPTIBLEUNINTERRUPTIBLE),并加入到某一个事件的等待队列。当事件发生后,等待队列上的进程才会被唤醒。

进程在拥有spinlockseqlockRCU lock等的情况是不能够sleep的(会造成其他进程的忙等,甚至死锁)。获取semaphore的进程虽然能够sleep,但是也要意识到,这也使得其它要获得该semaphore的进程sleep了。另外,在禁用中断的情况下,也是不应该sleep的。

     进程的挂起与唤醒操作:

wait_event(queue, condition)

wait_event_interruptible(queue, condition)

wait_event_timeout(queue, condition, timeout)

wait_event_interruptible_timeout(queue, condition, timeout)

void wake_up(wait_queue_head_t *queue);

void wake_up_interruptible(wait_queue_head_t *queue);

在驱动中使用阻塞型读写操作的例子:


  1. static DECLARE_WAIT_QUEUE_HEAD(wq);
  2. static int flag = 0;

  3. ssize_t sleepy_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
  4. {
  5.     printk(KERN_DEBUG "process %i (%s) going to sleep\n",urrent->pid, current->comm);
  6.     wait_event_interruptible(wq, flag != 0);
  7.     //要wq的数据类型为wait_queue_head_t,一般放在设备结构体中!

  8.     flag = 0;
  9.     printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
  10.     return 0; /* EOF */
  11. }

  12. ssize_t sleepy_write (struct file *filp, const char __user *buf, size_t count,loff_t *pos)
  13. {
  14.     printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",current->pid, current->comm);
  15.     flag = 1;
  16.     wake_up_interruptible(&wq);
  17.     return count; /* succeed, to avoid retrial */
  18. }
     之前说过,当读缓冲区没有数据,或者写缓冲区没有空间的时候,读写操作都会被挂起,但是一旦读缓冲区有数据来临,或者写缓冲区有空间,发起读写操作的进程就会被唤醒,唤醒的工作一般在ISR中进行)。另外,对file(打开文件的文件描述符)可以设置O_NONBLOCK标志,这样的话,读写操作就不应该被挂起,若是遇到读写缓冲区应该被挂起的状况,则应返回“-EAGAIN”。以下是一个支持non-blocking的读例子:
  1. static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
  2. {
  3.     struct scull_pipe *dev = filp->private_data;
  4.     if (down_interruptible(&dev->sem))
  5.         return -ERESTARTSYS;//返回给VFS,说明被signal打断了

  6.     while (dev->rp == dev->wp) { /* nothing to read *///读缓冲区为空
  7.         up(&dev->sem); /* release the lock */
  8.         if (filp->f_flags & O_NONBLOCK) //设置了non-blocking
  9.             return -EAGAIN;

  10.         PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
  11.     
  12.         if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
  13.             return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
  14.     
  15.         /* otherwise loop, but first reacquire the lock */
  16.         if (down_interruptible(&dev->sem)) //说明被唤醒了!
  17.             return -ERESTARTSYS;
  18.     }

  19.     /* ok, data is there, return something */
  20.     if (dev->wp > dev->rp)
  21.         count = min(count, (size_t)(dev->wp - dev->rp));
  22.     else /* the write pointer has wrapped, return data up to dev->end */
  23.         count = min(count, (size_t)(dev->end - dev->rp));

  24.     if (copy_to_user(buf, dev->rp, count)) {
  25.         up (&dev->sem);
  26.         return -EFAULT;

  27.     }

  28.     dev->rp += count;
  29.     if (dev->rp == dev->end)
  30.         dev->rp = dev->buffer; /* wrapped */
  31.     
  32.     up (&dev->sem);
  33.     /* finally, awake any writers and return */

  34.     wake_up_interruptible(&dev->outq);
  35.     PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);

  36.     return count;
  37. }

 POLLSELECT

pollBSD)、selectsystem V)、epolllinux)是一些系统调用,它们可以检查对一组文件描述符(struct file)的操作是否会挂起。之所以需要它们是因为,有时候应用程序需要操作多个文件,如果在一个文件上因为读写而挂起是不行的,所以应用程序在读写文件之前先检查一下文件的状态(可读或可写),然后再用non-blocking的方式去读写文件

     pollselectepoll需要驱动程序的支持,驱动程序中有“poll”系统调用的接口:

  1. static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
  2. {
  3.     struct scull_pipe *dev = filp->private_data;
  4.     unsigned int mask = 0;

  5.     /*
  6.      * The buffer is circular; it is considered full
  7.      * if "wp" is right behind "rp" and empty if the
  8.      * two are equal.
  9.      */

  10.     down(&dev->sem);
  11.     poll_wait(filp, &dev->inq, wait);
  12.     poll_wait(filp, &dev->outq, wait);

  13.     if (dev->rp != dev->wp) //判断读写指针
  14.         mask |= POLLIN | POLLRDNORM; /* readable */
  15.     if (spacefree(dev))
  16.         mask |= POLLOUT | POLLWRNORM; /* writable */

  17.     up(&dev->sem);
  18.     return mask;
  19. }
     poll()做的事情有:

1.对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应等待队列添加到poll_table,并不阻塞

2.返回表示是否能对设备进行无阻塞、写访问的掩码

非阻塞读写应用程序片段:

  1.  fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
  2.  if (fd != - 1){
  3.   while (1){
  4.    FD_ZERO(&rfds);//将指定的文件描述符集清空
  5.    FD_SET(fd, &rfds);//将fd加入文件描述符集合
  6.    tv.tv_sec = 5;
  7.    tv.tv_usec = 0;

  8.    select(fd + 1, &rfds, NULL, NULL, &tv);//返回值记录了发生状态变化的fd的集合
  9.  
  10.    if (FD_ISSET(fd, &rfds))//判断某个文件描述符是否被置位{
  11.     read(fd, &num, sizeof(int));
  12.     printf("The globalvar is %d\n", num);

  13.     if (num == 0){
  14.      close(fd);
  15.      break;
  16.     }
  17.    }
  18.    else
  19.     printf("No data within 5 seconds.\n");
  20.   }
 

LDD的第六章还介绍了fcntl系统调用,这样应用程序可以通过signal异步获得驱动的通知,而不用通过poll等方式。

LDD的第六章还介绍了驱动设备的访问控制,比如打开状态、用户权限等。

 

参考:

Linux Device Drivers

Linux Kernel Development

http://blog.csdn.net/hfyinsdu/article/details/6563497

http://blog.csdn.net/engerled/article/details/6254289

 

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