Chinaunix首页 | 论坛 | 博客

分类: LINUX

2009-11-07 10:13:26

字符驱动基础
要访问一个字符设备,系统用户要调用一个合适的应用程序. 应用程序负责跟设备通话,但它需要得到合适驱动的身份信息. 驱动的联系详细信息通过/dev目录导出到用户空间.

从代码流程来看,字符驱动有如下内容:
1. 初始化程序,负责初始化设备并通过注册函数让驱动无缝融入到内核中.
2. 一系列入口访问点,如open(), read(), ioctl(), llseek()和write(), 它们对应于用户程序调用的跟/dev结点相关的I/O系统调用.
3. 中断程序, 后半部,定时器处理函数,助手内核线程和其他支持基础. 它们大多对用户程序透明.

从数据流程来看, 字符驱动拥有如下关键数据结构:
1. 一个per-device结构体.  驱动涉及到的信息仓库.
2. struct cdev, 内核对字符驱动的抽象. 该结构通常嵌入到per-device结构里面.
3. struct file_operations, 博爱含所有驱动入口点的地址.
4. struct file, 相关联的/dev结点信息.


驱动例子:系统CMOS

我们来实现一个字符驱动来访问系统CMOS. PC兼容机上的BIOS使用CMOS存储诸如启动选项, 引导顺序和系统时间的信息, 这些信息可以通过BIOS安装菜单配置. 我们的CMOS驱动例子可以像访问普通文件一样访问到两个PC CMOS区. 应用程序可以在/dev/cmos/0和/dev/cmos/1上操作, 并使用I/O系统调用访问2个区的数据. 因为BIOS指派CMOS区域以比特级别粒度访问, 所以驱动有比特级别访问能力. read()可以得到指定数目比特数据并能移动一定数目比特文件指针来读.

通过两个I/O地址来访问CMOS, 一个是索引寄存器,一个是数据寄存器,如下表所示. 你得指明索引寄存器的CMOS内存偏移量然后通过数据寄存器交互信息.
寄存器                                    名称描述
CMOS_BANK0_INDEX_PORT  在此寄存器中指定CMOS bank 0偏移量
CMOS_BANK0_DATA_PORT   从CMOS_BANK0_INDEX_PORT指定的地址读写数据.
CMOS_BANK1_INDEX_PORT  在此寄存器中指定CMOS bank 1偏移量
CMOS_BANK1_DATA_PORT   从CMOS_BANK1_INDEX_PORT指定的地址读写数据
每个驱动方法都有一个对应的系统调用, 现在逐一介绍.


驱动初始化

init()函数是注册机制的基石. 负责下列事情:
1. 请求分配主设备号
2. 为per-device结构体分配内存
3. 为字符设备cdev抽象连接入口点(open(), read()等等)
4. 关联主设备号到cdev.
5. 在/dev和/sys下创建结点.
6.  初始化硬件. 这和我们的简单的CMOS没有关系.
下面是init()函数代码:

#include

/* Per-device (per-bank) structure */
struct cmos_dev {
  unsigned short current_pointer; /* Current pointer within the
                                     bank */
  unsigned int size;              /* Size of the bank */
  int bank_number;                /* CMOS bank number */
  struct cdev cdev;               /* The cdev structure */
  char name[10];                  /* Name of I/O region */
  /* ... */                       /* Mutexes, spinlocks, wait
                                     queues, .. */
} *cmos_devp;

/* File operations structure. Defined in linux/fs.h */
static struct file_operations cmos_fops = {
  .owner    =   THIS_MODULE,      /* Owner */
  .open     =   cmos_open,        /* Open method */
  .release  =   cmos_release,     /* Release method */
  .read     =   cmos_read,        /* Read method */
  .write    =   cmos_write,       /* Write method */
  .llseek   =   cmos_llseek,      /* Seek method */
  .ioctl    =   cmos_ioctl,       /* Ioctl method */
};

static dev_t cmos_dev_number;   /* Allotted device number */
struct class *cmos_class;       /* Tie with the device model */

#define NUM_CMOS_BANKS          2
#define CMOS_BANK_SIZE          (0xFF*8)
#define DEVICE_NAME             "cmos"
#define CMOS_BANK0_INDEX_PORT   0x70
#define CMOS_BANK0_DATA_PORT    0x71
#define CMOS_BANK1_INDEX_PORT   0x72
#define CMOS_BANK1_DATA_PORT    0x73

unsigned char addrports[NUM_CMOS_BANKS] = {CMOS_BANK0_INDEX_PORT,
                                           CMOS_BANK1_INDEX_PORT,};

unsigned char dataports[NUM_CMOS_BANKS] = {CMOS_BANK0_DATA_PORT,
                                           CMOS_BANK1_DATA_PORT,};

/*
 * Driver Initialization
 */
int __init cmos_init(void)
{
  int i;

  /* Request dynamic allocation of a device major number */
  if (alloc_chrdev_region(&cmos_dev_number, 0,
                          NUM_CMOS_BANKS, DEVICE_NAME) < 0) {
    printk(KERN_DEBUG "Can't register device\n"); return -1;
  }

  /* Populate sysfs entries */
  cmos_class = class_create(THIS_MODULE, DEVICE_NAME);

  for (i=0; i    /* Allocate memory for the per-device structure */
    cmos_devp = kmalloc(sizeof(struct cmos_dev), GFP_KERNEL);
    if (!cmos_devp) {
      printk("Bad Kmalloc\n"); return 1;
    }

    /* Request I/O region */
    sprintf(cmos_devp->name, "cmos%d", i);
    if (!(request_region(addrports[i], 2, cmos_devp->name))) {
      printk("cmos: I/O port 0x%x is not free.\n", addrports[i]);
      return –EIO;
    }
    /* Fill in the bank number to correlate this device
       with the corresponding CMOS bank */
    cmos_devp->bank_number = i;

    /* Connect the file operations with the cdev */
    cdev_init(&cmos_devp->cdev, &cmos_fops);
    cmos_devp->cdev.owner = THIS_MODULE;

    /* Connect the major/minor number to the cdev */
    if (cdev_add(&cmos_devp->cdev, (dev_number + i), 1)) {
      printk("Bad cdev\n");
      return 1;
    }

    /* Send uevents to udev, so it'll create /dev nodes */
    class_device_create(cmos_class, NULL, (dev_number + i),
                        NULL, "cmos%d", i);
  }

  printk("CMOS Driver Initialized.\n");
  return 0;
}


/* Driver Exit */
void __exit cmos_cleanup(void)
{
  int i;

  /* Remove the cdev */
  cdev_del(&cmos_devp->cdev);

  /* Release the major number */
  unregister_chrdev_region(MAJOR(dev_number), NUM_CMOS_BANKS);

  /* Release I/O region */
  for (i=0; i    class_device_destroy(cmos_class, MKDEV(MAJOR(dev_number), i));
    release_region(addrports[i], 2);
  }
  /* Destroy cmos_class */
  class_destroy(cmos_class);
  return();
}

module_init(cmos_init);
module_exit(cmos_cleanup);
cmos_init()做的大部分事情都是通用的,所以删除了对CMOS数据结构的引用就可以作为开发其他字符驱动的模板了.
首先, cmos_init()调用alloc_chrdev_region()动态请求一个未使用的主设备号. 如果调用成功,dev_number就包含分配到的主设备号. alloc_chrdev_region()的第二个和第三个参数分布指定了起始次设备号和支持的次设备数目. 最后的参数设备名用来在/proc/devices下面识别CMOS:
bash> cat /proc/devices | grep cmos
253 cmos
253是为CMOS设备动态分配的主设备号. 在2.6内核以前动态设备结点分配不被支持,所以字符驱动调用register_chrdev()来静态请求指定的主设备号.

cmos_dev是早先提到的per-device数据结构. cmos_fops是file_operations结构,包含驱动入口点地址. cmos_fops还有个owner域设置为THIS_MODULE,即该驱动模块的地址. 知道结构所有者身份可允许内核卸下驱动的一些日常功能如打开释放设备时的用户计数.

如你看到的,内核使用一个cdev抽象在内部表示字符设备. 字符设备通常嵌入cdev在它们的per-device结构体中. 在我们的例子中, cdev在cmos_dev里面. cmos_init()在每个支持的次设备中循环为每个关联的per-device设备包括里面的cdev分配内存. cdev_init()关联文件操作(cmos_fops)到cdev, cdev_add()关联alloc_chrdev_region()分配到的主次设备后到cdev.

class_create()为设备产生一个sysfs入口, class_device_create()产生两个uevent: cmos和cmos1.

设备驱动需要通过调用request_region()来对已经声明的范围内I/O桩地址进行操作.这个调正机制确保了其他对该区域的请求将一直失败直到通过release_region()来释放它. request_region()通常由I/O总线驱动如PCI和ISA调用, 来标记处理器上地址空间的卡上内存的所有权. cmos_init()调用request_region()来请求访问每个CMOS bank的I/O区域. request_region()的最后一个参数是/proc/ioport用的标识符, 如果你查看该文件可以看到它:
bash>  grep cmos /proc/ioports
0070-0071  :  cmos0
0072-0073  :  cmos1
这样就完成了注册过程, 最好cmos_init()打印一条信息表示进展顺利.


打开和释放
当应用程序打卡对应的设备结点时内核调用驱动的open()函数, 你可以这样做来触发cmos_open()的执行:
bash>cat /dev/cmos/0
应用程序关闭打开的设备时内核调用release()函数. 所以在cat读完CMOS bank 0的内容后关闭关联到/dev/cmos/0的文件描述符时,内核就调用cmos_release().

下面代码显示了cmos_open()和cmos_release()的实现. 有两点值得关注, 第一个是cmos_dev的提取. 作为参数传给cmos_open()的inode包含初始化时分配的cdev结构的地址. cdev嵌入到cmos_dev内部了. 为提取出cmos_dev结构体的地址, cmos_open()使用了内核助手函数container_of().

cmos_open()里面另外一个值得留意的操作是struct file里面的private_data域的使用方法. 你可以使用这个域作为占位符以方便与内部其他驱动函数关联信息. CMOS驱动使用该域存储cmos_dev的地址. 查看cmos_release()看下怎么用private_data得到对应CMOS bank的cmos_dev结构体上的处理函数的.

/*
 * Open CMOS bank
 */
int
cmos_open(struct inode *inode, struct file *file)
{
  struct cmos_dev *cmos_devp;

  /* Get the per-device structure that contains this cdev */
  cmos_devp = container_of(inode->i_cdev, struct cmos_dev, cdev);

  /* Easy access to cmos_devp from rest of the entry points */
  file->private_data = cmos_devp;

  /* Initialize some fields */
  cmos_devp->size = CMOS_BANK_SIZE;
  cmos_devp->current_pointer = 0;

  return 0;
}

/*
 * Release CMOS bank
 */
int
cmos_release(struct inode *inode, struct file *file)
{
  struct cmos_dev *cmos_devp = file->private_data;

  /* Reset file pointer */
  cmos_devp->current_pointer = 0;

  return 0;
}

交换数据

read()和write()是用户空间和设备之间负责交换数据的最基本字符驱动方法. read()/write()族的扩展包含其他方法: fsync(), aio_read(), aio_write()和mmap().
CMOS驱动对一个很简单的内存设备操作并不处理一些通常的字符设备的复杂方面:
1. CMOS数据访问程序无需睡眠等待设备I/O完成, 字符驱动的read()和write()函数都支持阻塞和非阻塞形式操作. 除非设备文件通过非阻塞(O_NONBLOCK)模式打开,read()和write()都允许调用进程进入睡眠知道对应的操作完成.
2. CMOS驱动完全是同步操作不用依赖中断.但一些驱动的数据访问方法依赖于收集数据的中断并且要通过诸如等待队列的数据结构同中断上下文代码通信.
 下面代码包含了CMOS驱动的read()和write()方法. 你不能在内核空间里面直接访问用户缓存,反之亦然. 所以cmos_read()使用copy_to_user()拷贝CMOS内存内容到用户空间, cmos_write()使用copy_from_user()做相反的事情. 因为copy_to_user()和copy_from_user()会进入睡眠,所以你不能在调用它们时持有自旋锁.
 /*
 * Read from a CMOS Bank at bit-level granularity
 */
ssize_t
cmos_read(struct file *file, char *buf,
          size_t count, loff_t *ppos)
{
  struct cmos_dev *cmos_devp = file->private_data;
  char data[CMOS_BANK_SIZE];
  unsigned char mask;
  int xferred = 0, i = 0, l, zero_out;
  int start_byte = cmos_devp->current_pointer/8;
  int start_bit  = cmos_devp->current_pointer%8;

  if (cmos_devp->current_pointer >= cmos_devp->size) {
    return 0; /*EOF*/
  }

  /* Adjust count if it edges past the end of the CMOS bank */
  if (cmos_devp->current_pointer + count > cmos_devp->size) {
    count = cmos_devp->size - cmos_devp->current_pointer;
  }

  /* Get the specified number of bits from the CMOS */
  while (xferred < count) {
    data[i] = port_data_in(start_byte, cmos_devp->bank_number)
              >> start_bit;
    xferred += (8 - start_bit);
    if ((start_bit) && (count + start_bit > 8)) {
      data[i] |= (port_data_in (start_byte + 1,
                  cmos_devp->bank_number) << (8 - start_bit));
      xferred += start_bit;
    }
    start_byte++;
    i++;
  }
  if (xferred > count) {
    /* Zero out (xferred-count) bits from the MSB
       of the last data byte */
    zero_out = xferred - count;
    mask = 1 << (8 - zero_out);
    for (l=0; l < zero_out; l++) {
      data[i-1] &= ~mask; mask <<= 1;
    }
    xferred = count;
  }

  if (!xferred) return -EIO;

  /* Copy the read bits to the user buffer */
  if (copy_to_user(buf, (void *)data, ((xferred/8)+1)) != 0) {
    return -EIO;
  }

  /* Increment the file pointer by the number of xferred bits */
  cmos_devp->current_pointer += xferred;
  return xferred; /* Number of bits read */
}


/*
 * Write to a CMOS bank at bit-level granularity. 'count' holds the
 * number of bits to be written.
 */
ssize_t
cmos_write(struct file *file, const char *buf,
           size_t count, loff_t *ppos)
{
  struct cmos_dev *cmos_devp = file->private_data;
  int xferred = 0, i = 0, l, end_l, start_l;
  char *kbuf, tmp_kbuf;
  unsigned char tmp_data = 0, mask;
  int start_byte = cmos_devp->current_pointer/8;
  int start_bit  = cmos_devp->current_pointer%8;

  if (cmos_devp->current_pointer >= cmos_devp->size) {
    return 0; /* EOF */
  }
  /*
 Adjust count if it edges past the end of the CMOS bank */
  if (cmos_devp->current_pointer + count > cmos_devp->size) {
    count = cmos_devp->size - cmos_devp->current_pointer;
  }

  kbuf = kmalloc((count/8)+1,GFP_KERNEL);
  if (kbuf==NULL)
    return -ENOMEM;

  /* Get the bits from the user buffer */
  if (copy_from_user(kbuf,buf,(count/8)+1)) {
    kfree(kbuf);
    return -EFAULT;
  }

  /* Write the specified number of bits to the CMOS bank */
  while (xferred < count) {
    tmp_data = port_data_in(start_byte, cmos_devp->bank_number);
    mask = 1 << start_bit;
    end_l = 8;
    if ((count-xferred) < (8 - start_bit)) {
      end_l = (count - xferred) + start_bit;
    }

    for (l = start_bit; l < end_l; l++) {
      tmp_data &= ~mask; mask <<= 1;
    }
    tmp_kbuf = kbuf[i];
    mask = 1 << end_l;
    for (l = end_l; l < 8; l++) {
      tmp_kbuf &= ~mask;
      mask <<= 1;
    }

    port_data_out(start_byte,
                  tmp_data |(tmp_kbuf << start_bit),
                  cmos_devp->bank_number);
    xferred += (end_l - start_bit);

    if ((xferred < count) && (start_bit) &&
        (count + start_bit > 8)) {
      tmp_data = port_data_in(start_byte+1,
                              cmos_devp->bank_number);
      start_l = ((start_bit + count) % 8);
      mask = 1 << start_l;
      for (l=0; l < start_l; l++) {
        mask >>= 1;
        tmp_data &= ~mask;
      }
      port_data_out((start_byte+1),
                    tmp_data |(kbuf[i] >> (8 - start_bit)),
                    cmos_devp->bank_number);
      xferred += start_l;
    }

    start_byte++;
    i++;
  }

  if (!xferred) return -EIO;

  /* Push the offset pointer forward */
  cmos_devp->current_pointer += xferred;
  return xferred; /* Return the number of written bits */
}


/*
 * Read data from specified CMOS bank
 */
unsigned char
port_data_in(unsigned char offset, int bank)
{
  unsigned char data;

  if (unlikely(bank >= NUM_CMOS_BANKS)) {
    printk("Unknown CMOS Bank\n");
    return 0;
  } else {
    outb(offset, addrports[bank]); /* Read a byte */
    data = inb(dataports[bank]);
  }
  return data;


}
/*
 * Write data to specified CMOS bank
 */
void
port_data_out(unsigned char offset, unsigned char data,
                int bank)
{
  if (unlikely(bank >= NUM_CMOS_BANKS)) {
    printk("Unknown CMOS Bank\n");
    return;
  } else {
    outb(offset, addrports[bank]); /* Output a byte */
    outb(data, dataports[bank]);
  }
  return;
}

正如你早先看到的那样, 访问CMOS内存是通过对一对I/O地址操作的. 要从一个I/O地址读不同大小的数据,内核提供了处理器相关的一系列函数: in[b|w|l|sb|sl](). 同样,out[b|w|l|sb|sl]()用来写I/O区域. 上面代码中的port_data_in()和port_data_out()使用了inb()和oub()作数据传输.
如果字符驱动write()函数成功返回, 这暗示了驱动呈现了应用程序传递数据到底层的职责,但并不能保证数据成功写到设备中. 如果应用程序需要这个保证,它可以调用fsync()系统调用. 对应的fsync()驱动函数确保应用程序数据从驱动缓冲区中被冲洗到设备中. CMOS驱动并不需要fsync()函数因为在这种情况下驱动写和设备写是一个意思.
如果应用程序有数据驻留在多个缓冲区并且需要发送到设备,它可以请求多次驱动写.但由于一下原因这并不高效:
1. 多个系统调用和相关的上下文切换的开销.
2. 驱动是最亲密了解设备的, 所以它能在有效的从不同缓冲区收集数据并分发给驱动方面做出一些更聪明的举动.
由此,linux和其他Unix支持read()和write()的向量版本. Linux 字符框架曾经提供了两个专门函数执行向量操作:readv()和writev(). 从2.6.19内核发布起, 这两个函数折叠到通用AIO(Linux异步I/O)层中了. 但是Linux AIO是个宽泛话题,我们集中关注AIO提供的同步向量功能.

向量驱动函数的原型定义如下:
ssize_t aio_read(struct kiocb *iocb, const struct iovec *vector,
                 unsigned long count, loff_t offset);
ssize_t aio_write(struct kiocb *iocb, const struct iovec *vector,
                  unsigned long count, loff_t offset);
aio_read()/aio_write()的第一个参数描述了AIO操作,第二个参数是个iovec数组,最后的向量函数使用的一个关键数据结构,它包含了持有数据的缓冲区的地址和长度.  include/linux/uio.h可以看到iovec的定义, drivers/net/tun.c有向量字符驱动函数的例子.

另外一个数据访问函数是mmap(),它关联设备内存到用户虚拟内存. 应用程序可以调用对应的系统调用也可以调用mmap()来直接对返回的内存区域进行操作以达到访问设备中内存效果. 不是很多的驱动实现了mmap(),所以这里并不深入探求了. 只看一下drivers/char/mem.c,这是一个mmap()实现的例子. 我们的CMOS驱动没有实现mmap().

Seek
内核使用一个内部指针追踪当前文件的访问位置,应用程序使用lseek()系统调用来请求这个内部文件指针的重定位. 通过使用lseek()你可以复位文件指针到文件内的任何偏移位置. 字符驱动中类似lseek()的就是llseek()函数, cmos_llseek()实现了这个函数.

CMOS里面的内部文件指针以比特为单位移动而不是字节.如果从CMOS驱动中读一字节数据,文件指针就会移动8位. cmos_llseek()也根据CMOS bank的大小实现了文件结束语义.

lseek()系统调用支持的三种命令:
1. SEEK_SET 设置文件指针到提供的固定偏移位置.
2. SEEK_CUR 计算相对于当前位置的偏移.
3. SEEK_END 计算相对文件末尾的偏移.这个技巧常用于创建大文件,CMOS驱动不支持SEEK_END。

下面为cmos_llseek()代码: 

 1 /*
 2  * Seek to a bit offset within a CMOS bank
 3  */
 4 static loff_t
 5 cmos_llseek(struct file *file, loff_t offset,
 6             int orig)
 7 {
 8   struct cmos_dev *cmos_devp = file->private_data;
 9
10   switch (orig) {
11     case 0: /* SEEK_SET */
12       if (offset >= cmos_devp->size) {
13         return -EINVAL;
14       }
15       cmos_devp->current_pointer = offset; /* Bit Offset */
16       break;
17
18     case 1: /* SEEK_CURR */
19       if ((cmos_devp->current_pointer + offset) >=
20            cmos_devp->size) {
21         return -EINVAL;
22       }
23       cmos_devp->current_pointer = offset; /* Bit Offset */
24       break;
25
26     case 2: /* SEEK_END - Not supported */
27       return -EINVAL;
28
29     default:
30       return -EINVAL;
31   }
32
33   return(cmos_devp->current_pointer);
34 }
35

控制
另外一个字符驱动函数是I/O控制(ioctl), 该函数用于接受和实现请求特定设备的应用程序命令. 由于CMOS内存被BIOS用来存储关键信息如引导设备顺序, 通常通过CRC算法来保护自己. 未来检测数据冲突,CMOS驱动支持两种ioctl命令:
1. 调整校验和, 用来在CMOS内容修改后重新计算CRC, 计算后的检验和存储在CMOS bank1上的一个预先指定的偏移里面.
2. 验证校验和, 用来检查CMOS内容是否完好, 这通常通过比较当前内容的CRC和以前存储的CRC做到.
当应用程序希望请求检查校验和操作时就通过ioctl()系统调用发送这些命令到驱动底层. 查看cmos_ioctl()代码, 它实现了CMOS驱动的ioctl函数, adjust_cmos_crc(int bank, unsigned short seed)实现了标准CRC算法并未列出:
  
 1
#define CMOS_ADJUST_CHECKSUM 1
 2 #define CMOS_VERIFY_CHECKSUM 2
 3
 4 #define CMOS_BANK1_CRC_OFFSET 0x1E
 5
 6 /*
 7  * Ioctls to adjust and verify CRC16s.
 8  */
 9 static int
10 cmos_ioctl(struct inode *inode, struct file *file,
11            unsigned int cmd, unsigned long arg)
12 {
13   unsigned short crc = 0;
14   unsigned char buf;
15
16   switch (cmd) {
17     case CMOS_ADJUST_CHECKSUM:
18       /* Calculate the CRC of bank0 using a seed of 0 */
19       crc = adjust_cmos_crc(0, 0);
20
21       /* Seed bank1 with CRC of bank0 */
22       crc = adjust_cmos_crc(1, crc);
23
24       /* Store calculated CRC */
25       port_data_out(CMOS_BANK1_CRC_OFFSET,
26                     (unsigned char)(crc & 0xFF), 1);
27       port_data_out((CMOS_BANK1_CRC_OFFSET + 1),
28                     (unsigned char) (crc >> 8), 1);
29       break;
30
31     case CMOS_VERIFY_CHECKSUM:
32      /* Calculate the CRC of bank0 using a seed of 0 */
33       crc = adjust_cmos_crc(0, 0);
34
35      /* Seed bank1 with CRC of bank0 */
36      crc = adjust_cmos_crc(1, crc);
37
38      /* Compare the calculated CRC with the stored CRC */
39      buf = port_data_in(CMOS_BANK1_CRC_OFFSET, 1);
40      if (buf != (unsigned char) (crc & 0xFF)) return -EINVAL;
41
42      buf = port_data_in((CMOS_BANK1_CRC_OFFSET+1), 1);
43      if (buf != (unsigned char)(crc >> 8)) return -EINVAL;
44      break;
45      default:
46        return -EIO;
47   }
48
49   return 0;
50 }
阅读(959) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~