字符设备驱动程序

驱动程序的定义:使硬件工作的软件

 

驱动程序分类: 字符设备驱动(重点),网络接口驱动(重点),块设备驱动

 

字符设备:

按字节为最小单位来访问的设备。字符驱动则负责驱动字符设备,通常实现open,close,read,write系统调用

 

块设备:

Unix:只能一次传送一个或多个长度是512字节(或更大的2次幂的数)的整块数据。Linux:允许块设备传送热议数目的字节。

 

字符和块设备的区别:

字符设备和块设备的区别仅仅是驱动与内核的接口不同。访问顺序:块设备不能随机访问,字符设备可以随机访问。

 

网络接口:

负责发送和接收数据报文。

 

驱动程序安装:

1、模块方式; 2、直接编译进内核。

直接编译进内核:

修改:kconfig makefile

kconfig(在menuconfig的时候产生选项):

1、在设备文件的文件夹下面打开kconfig

2、添加:config CONFIG_FB_SPCA63XX

bool "gpe_dev"

makefile:

obj-$(CONFIG_FB_SPCA63XX) += spca63xxgpe.o

 

 

scull设计:

scull:区域装载的简单字符工具。

 

主设备号和次设备号:

对字符设备的访问是通过文件系统内的设备名称进行的。

通常位于/dev目录。

字符设备驱动程序的设备为假可通过ls -l命名输出的第一列“c”来识别。块设备“b”

 

主设备号:标识设备对应的驱动程序。

/dev/null /dev/zero

驱动程序1管理

虚拟控制台 串口终端

驱动程序4管理

vcsl和vcsal

驱动程序7管理

次设备号:由内核使用,用于正确确定设备文件所指的设备。次设备号用来指向驱动程序所实现的设备。

 

设备编号的内部表达

dev_t类型用来保存设备编号(主设备和次设备)

dev_t 32位的数,12位用来表示主设备号,其余20位次设备号。

获取主次设备号:

MAJOR(dev_t dev);

MINOR(dev_t dev);

将主设备号和次设备号转换成dev_t类型:

MKDEV(int major, int minor);

 

分配和释放设备编号

1、获得一个或多个设备编号

int register_chrdev_region(dev_t first, unsigned int count,

char *name);

first:要分配设备编号范围的起始值。通常是0。

count:请求连续设备编号的个数。

name:和该编号范围关联的设备名称,出现在:/proc/devices和sysfs中。

返回值: 成功0,失败:负数。

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,

unsigned int count, char *name);

dev:在成功完成调用后将保存已分配范围的第一个编号。

firstminor:要使用的被请求的第一个设备号。通常是0?。

 

2、释放设备编号

void unregister_chrdev_region(dev_t first, unsigned int count);

 

动态分配主设备号

对insmode的调用可替换为一个简单的脚本。该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号。然后创建对应的设备文件。

 

典型的/proc/devices文件如下所示:

 

Character devices:

 1   mem

 2   pty

 3   ttyp

 4   ttys

 6   lp

 7   vcs

 10  misc

 13  input

 14  sound

 21  sg

 180 usb

 

获取主设备号的代码:

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;

}

 

重要的数据结构

 

大部分几本的驱动操作设计到三个重要的内核结构:

file_operations、file和inode

 

文件操作(file_opertation):

结构定义在<linux/fs.h>,包含了一组函数指针。指向file_operation结构的指针称为fops。

 

struct module *owner

指向“拥有”该结构的模块指针。避免在模块的操作正在被使用的时候卸载该模块。

初始化为:THIS_MODULE,定义在<linux/module.h>中的一个宏。

 

loff_t (*llseek) (struct file *, loff_t, int);

用来修改文件的当前读写位置,并将新位置作为(正的)返回值。

struct file *:

loff_t: 长偏移量,即使在32位平台也要至少用64位的数据宽度。

 

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

从设备中读取数据。返回非负值表示成功读取的字节数(signed size)。

 

ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t *);

初始化一个异步的读取操作——即在函数返回之前可能不会完成的读取操作。

struct kiocb *

__user: 对通常的编译来讲,没有任何效果,但是可由外部检查软件试用,用来寻找对用户空间地址的错误试用。

 

ssize_t (*write) (struct kiocb *, char __user *, size_t, loff_t *);

向设备发送数据。如果返回负值,表示成功写入的字节数。

 

ssize_t (*aoi_write) (struct kiocb *, const char __user *, size_t, loff_t *);

初始化异步写入操作。

 

int (*readdir) (struct file *, void *, filldir_t);

它仅用于读取目录,只对文件系统有用。

 

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

poll方法是poll,epoll和select这单个系统调用的后端实现。这三个调用可以用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。poll返回一个位掩码,用来指出非阻塞的读取或写入是否可能。

struct poll_table_struct *

 

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

系统调用ioctl提供了一种执行设备特定命令的方法。

struct inode *:

 

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

mmap用于请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,那么mmap系统调用将返回-ENODEV.

struct vm_area_struct *:

 

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

打开设备文件,是对设备文件执行的第一个操作。

 

int (*flush) (struct file *);

对flush的操作的调用发生在进程关闭设备文件操作符副本的时候。它应该执行并等待设备上尚未完结的操作。仅仅用于少数几个驱动程序。

 

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

file结构被释放时,将调用这个操作。与open相仿,也可以将release设置为NULL。

 

int (*fsync) (struct file *, struct dentry *, int);

该方法是fsync系统调用的后端实现,用户调用它来刷新待处理的数据。

struct dentry *

 

int (*aoi_fsync) (struct kiocb *, int);

fsync方法的异步版本。

 

int (*fasync) (int, struct file *, int);

用来通知设备其FASYNC标志发生了变化。异步通知比较高级。如果设备不支持异步通知,该字段可以是NULL。

 

int (*lock) (struct file *, int, struct file_lock *);

lock方法用于实现文件的锁定,锁定是常规文件不可缺少的特性,但设备驱动程序几乎从来不实现这个方法。

 

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

用来实现分散/聚集型的读写操作。应用程序有时需要进行涉及多个内存区域的单次读或写操作,利用上面这些系统调用可以完成这类工作,而不必强加额外的数据拷贝操作。如果这些函数指针被设置为NULL,就会调用多次read 和 write方法。

const struct iovec *

 

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

这个方法实现sendfile系统调用的读取部分。sendfile系统调用以最小的复制操作将数据从一个文件描述符移动到另一个。设备驱动程序通常将sendfile设置为NULL;

read_actor_t, void *:

 

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

sendpage是sendfile文件调用的另一半,它由内核调用以将数据发送到对应的文件,每次一个数据页。设备驱动也不需要实现sendpage。

struct page *:

 

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,  

 unsigned long, unsigned long);

在进程的地址空间中找到一个合适的位置,以便将底层设备中的内存段映射到该位置。该方法可允许驱动程序强制满足特定的设备需要的任何对齐需求。

 

int (*check_flags)(int);

该方法允许模块检查传递给fcntl(F_SETFL…)调用的标志。

 

int (*dir_notify)(struct file *, unsigned long);

当应用程序使用fcntl来请求目录改变通知时,该方法将被调用。

 

scull设备驱动程序所实现的只是最重要的设备方法,它的file_operation结构被初始化为如下形式:

struct file_operation scull_fops = {

   .owner  = THIS_MODULE,

   .llseek = scull_llseek,

   .read   = scull_read,

   .write  = scull_write,

   .ioctl  = scull_ioctl,

   .open   = scull_open,

   .release = scull_release,

};

 

file结构:

在<linux/fs.h>中定义的struct file是设备驱动所使用的第二个最重要的数据结构。file结构代表一个打开的文件。它由内核在open的时候创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。在文件所有实例都被关闭后,内核会释放这个数据结构。

 

指向 struct file 的指针通常被称为:file或filp。

 

mode_t f_mode;

文件模式。它通过FMODE_READ和FMODE_WRITE位来标识文件是否可读或可写。

 

loff_t f_ops;

当前的读/写位置。loff_t是一个64位的数。不会直接对filp->f_ops进行操作。

 

unsigned int f_flags;

文件标志,如O_RDONLY, O_NONBLOCK和O_SYNC。为了检查用户请求的是否是非阻塞式的操作。

 

struct file_operations *f_op;

与文件相关的操作。内核在执行open操作的时候对这个指针赋值,以后要处理这些操作就读取这个指针。

 

void * private_data;

open系统调用在调用驱动程序的open 方法前将这个指针置为NULL。驱动程序可以将这个字段用于任何目的或者忽略这个字段。

 

struct dentry *f_dentry;

文件对应的目录项(dentry)结构。除了用file->f_dentry->d_inode的方式来访问索引节点结构之外。一般无需关心dentry结构。

 

inode结构:

内核用inode结构在内部表示文件。对于单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。

inode结构中包含了大量有关文件的信息。

 

dev_t i_rdev;

表示设备文件的inode结构,该字段包含了真正的设备编号。

 

struct cdev *i_cdev;

表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字包含了指向 struct cdev结构的指针

 

字符设备的注册:

内核内部使用 struct cdev 结构来表示字符设备。代码中应该包含<linux/cdev.h>

 

运行时获取一个独立的cdev结构:

1、 struct cdev *my_cdev = cdev_alloc();

    my_cdev->ops = &my_fops;

2、初始化已分配到的结构:

void cdev_init(struct cdev *cdev, struct file_operation *fops);

3、初始化所有者字段,被设置为THIS_MODULE.

4、通过调用告诉内核该结构的信息:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

dev:vdev结构。

num:该设备对应的第一个设备编号。

count:应该和该设备管理的设备编号的数量。通常取1.

如果它返回一个负的错误代码,则设备不会被添加到系统中。

要从系统中移除一个字符设备:

void cdev_del(struct cdev *dev);

 

Scull中的设备注册

scull内部,struct scull_dev结构来表示每个设备:

struct scull_dev{

   struct scull_qset *data;  /*指向第一个量子集的指针*/

   int quantum;              /*当前量子的大小*/

   int qset;                 /*当前数组的大小*/

   unsigned long size;       /*保存在其中的数据总量*/

   unsigned int access_key;  /*由sculluid和scullpriv使用*/

   struct semaphore sem;     /*互斥信号量*/

   struct cdev cdev;         /*字符设备结构*/

}

 

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

}

早期的注册方式:

 

int register_chrdev(unsigned int major, const char *name, struct file_operation *fops);

major:主设备号;

name:驱动程序名称(出现在/proc/devices中)

fops:默认的file_operation结构。

给指定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构。

 

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

 

open和release

 

open方法

open应该完成如下操作:

  • 检查设备特定的错误
  • 如果设备首次打开,则对其进行初始化
  • 如有必要,更新f_op指针
  • 分配并填写至于filp->private_data里面的数据结构。

 

1、确定要打开的设备:

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

inode参数在其i_cdev字段中包含了所需要的信息:cdev结构。而我们需要的是包含cdev结构的scull_dev结构。可以用宏来实现<linux/kernel.h> container_of:

 

2、用container_of来获取设备结构

container_of(pointer, container_type, container_field);

 

container_field字段的指针。包含在container_type类型的结构体中。然后返回该字段的结构指针。这个宏用来找到适当的设备结构:

struct scull_dev *dev;/*device information*/

dev = container_of(inode->i_cdev, struct scull_dev, cdev);

filp->private_data = dev; /*for other methods*/

找到scull_dev结构之后,scull将指针保存到了file结构的private_data字段中。

 

简化了的scull_open代码如下:

int scull_open(struct inode *inode, struct file *flip)

{

   struct scull_dev *dev;    /*device information*/

   dev = container_of(inode->i_cdev, struct scull_dev, cdev);

   filp->private_data = dev;     /*for other methods*/

 

   /*now trim to 0 the length of the device if open was write-only*/

   if((filp->f_flags & O_ACCMODE) == O_WRONLY){

      scull_trim(dev);    /*ignore errors*/

   }

   return 0;      /*success*/

}

 

release方法

完成如下操作:

  • 释放由open分配的、保存在filp->private_data中的所有内容。
  • 在最后一次关闭操作时关闭设备

 

int scull_release(strunct inode *inode, struct file *flip)

{

   return 0;

}

 

scull的内存使用

scull使用的内存区域这里也称为设备,其长度是可变的。

scull驱动程序引入了linux内核中用于内存管理的两个核心函数。定义在<linux/slab.h>中

void *kmalloc(size_t size, int flags);

void kfree(void *ptr);

kmalloc: 分配size个字节大小的内存;其返回值指向该内存的指针,分配失败返回NULL。

flags参数用来描述内存的分配方法,始终使用 (GFP_KERNEL)

kfree: 释放kmalloc分配产生的内存。

 

scull中,每个设备都是一个指针链表,每个指针都指向一个scull_qset结构。每一个这样的结构通过一个中间指针数组最多可引用4000 000个字节。

每一个内存区域称为一个量子,这个指针数组(或它的长度)称为量子集。

 

struct scull_qset{

   void **data;

   struct scull_qset *next;

}

 

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

      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;

   dev->quantum = scull_quantum;

   dev->qset = scull_qset;

   dev->data = NULL;

   return 0;

}

 

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

 

filp:文件指针。

count: 请求传输的数据长度。

buff:指向用户空间的缓冲区,这个缓冲区存放的数据。buff参数是用户空间的指针。内核代码不能直接引用,原因P67

offp:指向long long offset type对象的指针,这个对象指明用户在文件中进行存取操作的位置。

返回值:signed size type。

 

驱动程序必须访问用户空间的缓冲区以完成自己的工作。必须通过内核提供的专用函数完成。在<asm/uaccess.h>中定义。

 

scull的read 和 write代码要做的工作就是在用户地址空间和内核地址空间进行整段的数据拷贝。这种能力由下面的内核函数提供。

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

 

这两个函数作用并不限于内核空间和用户空间之间的拷贝数据。1、它们还检查用户空间的指针是否有效。如果无效,则不会进行拷贝。2、如果在拷贝过程中遇到无效地址,则仅仅会复制部分数据。 这两种情况下,返回值是:还需要拷贝的内存数量值(返回值不为0)。如果是这样,scull会返回-EFAULT。

 

出错时,read 和 write 方法都是返回一个负值。大于等于0的返回值告诉调用程序成功传输了多少字节。只有在下一次调用的时候才能够得到报告。

错误的时候只会返回-1.为了找到错误原因,用户空间的程序必须访问errno变量。

 

read方法,返回值:

如果返回值等于传递给read 系统调用的count参数,说明请求的字节数成功完成。

如果返回值是正数,但是小于count,说明部分数据传输成功。一般情况下,程序会重新读数据。

如果返回值是0,则表示已经到达了文件尾。

负值意味着发生错误,错误代码在<linux/errno.h>中定义。

 

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;   /*第一个链表项*/

   int quantum = dev->quantum, qset = dev->qset;

   int itemsize = quantum * qset;   /*该链表项中有多少字节*/

   int item, s_pos, q_pos, rest;

   ssize_t retval = 0;

 

   if(down_intteruptible(&dev->sem))

   return -ERESTARTSYS;

 

   if(*f_ops >= dev->size)

   goto out;

   if(*f_pos + count > dev->size)

   count = dev->size - *fpos;

 

   /*在量子集中寻找链表项、qset索引以及偏移量*/

   item = (long)*f_pos / itemsize;

   rest = (long)*f_pos % itemsize;

   s_pos = rest / quantum;

   q_pos = rest % quantum;

 

   /*沿该链表前行,直到正确的位置(在其他地方定义)*/

   dptr = scull_follow(dev, item);

 

   if(dptr == NULL || !dptr->data || !dptr->data[s_pos])

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

}

write方法,返回值:

如果返回值等于count、则完成所有请求数目的字节传送。

如果返回值是正的,但小于count,则只传输了部分数据。程序很可能再次试图写入数据。

如果值为0,意味着什么也没有写入。

负值意味着发生了错误,与read相同,有桥的错误码定义在<linux/errno.h>中。代码见:P72

 

readv 和 writeV

“向量”型的函数具有一个结构数组,每个结构包含一个指向缓冲区的指针和一个长度值。

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

iovec 结构定义在<linux/uio.h>中,形式如下:

struct iovec

{

   void __user *iov_base,

   __kernel_size_t iov_len;

};

每个iovec结构都描述了一个用于传输的数据块,起始位置在iov_base,长度是iov_len个字节。这些结构由应用程序创建,在内核调用驱动程序之前会把它们拷贝到内核空间。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值