《LINUX设备驱动程序》第3章(字符驱动)学习笔记

第  3 章  字符驱动

主次编号:

使用命令ls –l /dev,下面的列表显示了一个典型系统上出现的几个设备. 它们的主

编号是 1, 4, 7, 和 10, 而次编号是 1, 3, 5, 64, 65, 和 129.  

 crw-rw-rw- 1 root  root 1,  3 Apr 11  2002 null 

 crw------- 1 root  root 10, 1 Apr 11  2002 psaux 

 crw------- 1 root  root 4,  1 Oct 28 03:04 tty1 

 crw-rw-rw- 1 root  tty  4, 64 Apr 11  2002 ttys0 

 crw-rw---- 1 root  uucp 4, 65 Apr 11  2002 ttyS1 

 crw--w---- 1 vcsa  tty  7,  1 Apr 11  2002 vcs1 

 crw--w---- 1 vcsa  tty  7,129 Apr 11  2002 vcsa1 

 crw-rw-rw- 1 root  root 1,  5 Apr 11  2002 zero

主编号标识设备相连的驱动. 例如, /dev/null和 /dev/zero 都由驱动 1 来管理, 而虚拟控制台和串口终端都由驱动 4 管理; 同样, vcs1 和 vcsa1 设备都由驱动 7 管理.

 

设备编号的内部表达:

dev_t 类型(在 <linux/types.h>中定义)用来保存设备编号(主次部分都包括).对于 2.6.0 内核, dev_t 32 位的量, 12 位用作主编号, 20 位用作次编号.

代码中应当利用在 <linux/kdev_t.h>中的一套宏定义. 使用:

MAJOR(dev_t dev);  //从dev_t中读取主设备号返回

MINOR(dev_t dev); //从dev_t中读取次设备号返回

MKDEV(int major, int minor); //将主次编号转换为一个 dev_t类型返回

 

分配和释放设备编号:

驱动程序中,首先应获取一个或多个设备编号来使用. 为此目的的必要的函数是 register_chrdev_region, 在 <linux/fs.h>中声明:

int register_chrdev_region(dev_tfirst, unsigned int count, char *name);

这里, first 是你要分配的起始设备编号.first 的次编号经常被置为 0, 但是没有要求是必须是0, count 是你请求的连续设备编号的总数. name 是应当连接到这个编号范围的设备的名子(它会出现在 /proc/devices 和 sysfs 中).分配成功, register_chrdev_region 的返回值是 0. 出错的情况下, 返回一个负的错误码.

         预先知道主设备号的情况下,使用register_chrdev_region,否则你必须使用一个不同的函数来请求自动分配设备号.  

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

使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. fisetminor 应当是请求的第一个要用的次编号; 它常常是 0. count 和 name 参数如同给 request_chrdev_region 的一样.

无论使用什么方式分配的设备编号, 你应当在不再使用它们时释放它. 设备编号的释放通常在模块卸载时使用:  

voidunregister_chrdev_region(dev_t first, unsigned int count);  

 

主编号的动态分配:

通常驱动程序使用动态分配来获取你的主设备编号, 而不是随机选取一个当前空闲的编号. 换句话说, 你的驱动应当几乎肯定地使用alloc_chrdev_region, 不是 register_chrdev_region.

动态分配的缺点:是你无法提前创建设备节点,因为分配给你的模块的主编号会变化. 对于驱动的正常使用, 这不是问题, 因为一旦编号分配了, 你可从 /proc/devices 中读取它.

为使用动态主编号来加载一个驱动, 可使用一个简单的脚本来代替调用 insmod, 在调用 insmod 后, 读取 /proc/devices 来创建特殊文件.可以使用一个工具来编写一个脚本, 如 awk , 来从 /proc/devices 获取设备号信息以创建 /dev 中的文件,

脚本如下:

#!/bin/sh

module="scull"

device="scull"

mode="664"

 

# 使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定模块位置,

# 这是因为新的modutils默认不会在当前目录中查找模块

/sbin/insmod ./$module.ko $* || exit 1

 

# 删除原有节点

rm -f /dev/${device}[0-3]

 

major=$(awk "\\$2==\"$module\" {print \\$1}"/proc/devices) 

mknod /dev/${device}0 c $major 0

mknod /dev/${device}1 c $major 1

mknod /dev/${device}2 c $major 2

mknod /dev/${device}3 c $major 3

 

# 给定适当的组属性及许可,并修改属组。

# 并非所有发行版本都具有staff组,有些有wheel组

group="staff"

grep -q '^staff:' /etc/group || group="wheel"

 

chgrp $group /dev/${device}[0-3]

chmod $mode /dev/${device}[0-3]

这个脚本同样适用于其他驱动程序,只需要从新定义变量并调整mknod那几行语句即可

scull 的源码中获取主编号的代码:  

if (scull_major) { //是否预先知道主设备号

 dev = MKDEV(scull_major,scull_minor); .//获取dev_t类型设备信息

 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_operation 结构是一个字符驱动用来把设备操作与分配的设备编号建立连接. 这个结构, 定义在 <linux/fs.h> , 是一个函数指针的集合.

structfile_operations {

struct module *owner;

//一个指向拥有这个结构的模块的指针.通常它被简单初始化为THIS_MODULE,

//THIS_MODULE是一个在<linux/module.h>中定义的宏.

loff_t (*llseek) (structfile *, loff_t, int);

//该方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.

//loff_t 参数是一个"longoffset", 并且就算在 32位平台上也至少 64 位宽.

ssize_t (*read) (structfile *filp, char *buff, size_t count, loff_t *offp);

     //用来从设备中获取数据. 在这个位置的一个空指针导致read 系统调用

//以-EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数.

ssize_t (*write) (structfile *filp, const char *buff, size_t count, loff_t *offp);

//发送数据给设备. 如果 NULL,-EINVAL 返回给调用 write 系统调用的程序.

//如果非负, 返回值代表成功写的字节数.

int (*readdir) (structfile *, void *, filldir_t);

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

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

//ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个

//磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引

//用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求

//(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.

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

//mmap 用来请求将设备内存映射到进程的地址空间.

//如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

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

//尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的

//方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.

int (*flush) (struct file*);

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

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

int (*fasync) (int, structfile *, int);

int (*check_media_change)(kdev_t dev);

int (*revalidate) (kdev_tdev);

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

};

每个设备的驱动程序不一定要实现其中所有的函数操作,若不需要定义实现时,则只需将其设为NULL即可。

文件结构:

struct file代表一个打开的文件. 定义于<linux/fs.h>, 是设备驱动中第二个最重要的数据结构. 注意 file 与用户空间程序的 FILE 指针没有任何关系. 一个 FILE 定义在 C 库中, 从不出现在内核代码中. struct file是一个内核结构, 从不出现在用户程序中.

structfile {

mode_t f_mode;

//标识文件是否可读或可写,FMODE_READ或FMODE_WRITE

dev_t f_rdev;

//用于/dev/tty

off_t f_pos;

//当前文件位移

unsigned short f_flags;

//文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC

unsigned short f_count;

//打开的文件数目

unsigned short f_reada;

struct inode *f_inode;

//指向inode的结构指针

struct file_operations*f_op;

//文件索引指针

};

inode 结构:

inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的. 可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构.

inode 结构只有 2 个成员对于编写驱动代码有用:

dev_ti_rdev;

//对于代表设备文件的节点, 这个成员包含实际的设备编号.

structcdev *i_cdev;

//struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针,

//指向这个结构, 当节点指的是一个字符设备文件时.

内核开发者已经增加了 2 个宏, 可用来从一个 inode 中获取主次编号:  

unsigned int iminor(structinode *inode);

unsigned intimajor(struct inode *inode);

 

字符设备注册:

内核在内部使用类型 struct cdev的结构来代表字符设备. 在内核调用你的设备操作前, 你编写分配并注册一个或几个这些结构. 这个结构和它的关联帮助函数定义在<linux/cdev.h>.

初始化字符设备(两种方式):

(1)如果你想在运行时获得一个独立的cdev 结构, 你可以为此使用这样的代码:  

struct cdev *my_cdev = cdev_alloc();

my_cdev->ops = &my_fops;

(2)如果你想将 cdev 结构嵌入一个你自己的设备特定的结构(如scull 驱动). 在这种情况下, 你应当初始化你已经分配的结构, 使用:

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

如同file_operations 结构, struct cdev 也有一个拥有者成员, 应当设置为 THIS_MODULE.

         my_cdev->owner = THIS_MODULE;

把初始化完成后的cdev告诉内核:  

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

//dev 是 cdev 结构, num 是这个设备对应的第一个设备号, count 是应当和该设备

//关联的设备编号的数目. 通常 count 取 1

移除一个字符设备:

voidcdev_del(struct cdev *dev);

//在将cdev结构传递给cdev_del 后,就不能访问 cdev 结构了.

 

open 和release:

open方法提供给驱动来做任何的初始化来准备后续的操作. 在大部分驱动中,

open 应当进行下面的工作:

  检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误

  如果它第一次打开, 初始化设备

  如果需要, 更新 f_op 指针.

  分配并填充要放进filp->private_data 的任何数据结构

open方法的原型是:  

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

//inode 参数的 i_cdev 字段中包含我们需要的信息,

//里面包含我们之前建立的 cdev 结构.

唯一的问题是通常我们不想要 cdev 结构本身, 我们需要的是包含 cdev结构的 scull_dev 结构. 庆幸的是,内核开发者已经为我们实现了这个技巧, 以container_of 宏的形式, 在 <linux/kernel.h> 中定义:  

container_of(pointer,container_type, container_field);

         这个宏需要一个指向container_field 类型的成员的指针, 它在一个container_type 类型的结构中, 然后返回向包含该字段的结构指针. 在 scull_open, 这个宏用来找到适当的设备结构:

         structscull_dev *dev; /* device information */ 

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

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

一旦它找到 scull_dev 结构, scull 在文件结构的private_data 成员中存储一个它的指针, 为以后更易存取.

release 方法的角色是 open 的反面. 有时你会发现方法的实现称为device_close, 而不是 device_release. 任一方式, 设备方法应当进行下面的任务:

  释放 open 分配在filp->private_data 中的任何东西

  在最后的 close 关闭设备

 

读和写:

读和写在#include <linux/fs.h>中定义,函数原型如下:

ssize_t (*read) (structfile *filp, char *buff, size_t count, loff_t *offp)

ssize_t (*write) (structfile *filp, const char *buff, size_t count, loff_t *offp)

//filp:文件指针

//buff:指向用户缓冲区

//count:传入的数据长度

//offp:用户在文件中的位置

//成功:写入的数据长度

虽然这个过程看起来很简单,但是内核空间地址和应用空间地址是有很大区别的,其中之一就是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以就不能使用诸如memcpy 之类的函数来完成这样的操作。

用户空间和内核空间的数据交换:

通常使用使用copy_to_user 或copy_from_user函数(定义于 <asm/uaccess.h>)来实现用户空间和内核空间的数据的交换。

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

//To:数据目的缓冲区

//From:数据源缓冲区

//count:数据长度

//成功:写入的数据长度

//失败:-EFAULT

要注意,这两个函数不仅实现了用户空间和内核空间的数据转换,而且还会检查用户空

间指针的有效性。如果指针无效,那么就不进行复制。

 

 

 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值