linux设备驱动三(字符设备驱动)

主设备号和次设备号

  • 主设备号标示设备对应的驱动程序,次设备号用来确定设备文件指向的设备。
  • 现代linux允许多个驱动程序共享主设备号,但是大多数设备仍延用一个主设备号对应一个驱动程序

设备号内部表达

  • 2.6版本中以后的主设备12,次设备20,兼容性的做法是使用宏,而不对设备号的组织进行任何假定
  • MAJOR(dev_t dev) MINOR(dev_t dev) MKDEV(int major, int minor)
    分配和释放设备号
  • 如果count非常大,则所请求的范围可能和下一个主设备号重叠,但只要我们所请求的编号范围是可用的,则不会有什么问题。疑问:这句话该怎么理解, 指定了主设备号和次设备号,count很大的时候会分配到下一个主设备号上吗?请求范围可用是指区域会跨两个主设备号?
  • 设备号在使用结束之后需要释放。
  • 设备号分配完成,但是并没有和内部函数关联。

动态分配主设备号

  • Documentation/devices.txt中记录了系统已经静态分配的主设备号。
  • 将一个已经分配的静态编号用于新设备驱动的机会是非常小的。
  • 在使用未分配的设备号时有两种方式,一种是随机指定未分配的设备号,另外一种是动态分配。
  • 随机指定:当一个设备驱动被广泛使用时,随机指定也会造成冲突和麻烦。
  • 动态分配:主设备号不能始终保持一致,因此不能预先创建节点。但是可以在/proc/devices中通过name来查找。
  • 为了加载一个使用动态主设备号的设备驱动程序,对insmod的调用可替换为简单的脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号后创建对应的设备文件。疑问:一般调用register_device之后不是会在dev路径下生成设备吗,为什么要自己创建?他的意思是不是只注册设备驱动程序,而不注册设备,通过指定主设备号来创建设备,该设备再由driver来处理?
  • 在第一次分配主设备号之后,如果没有其他模块并发的情况下rmmod之后再insmod,仍然可以分配到相同的主设备号。
  • 最佳方法是可以通过insmod参数来指定主设备号,不指定时默认为0,才用动态分配。这样可以避免反复创建和删除设备文件。

一些重要的数据结构

大部分基本的驱动程序操作涉及到三个重要的内部数据结构,file_operations、file和inode
file_operations的第一个字段是一个模块指针,内核通过该字段来避免在模块的操作正在进行的时候卸载模块。
unsigned int (*poll) (struct file *, struct poll_table_struct );
poll,额破裂了,select三个系统调用的后端实现,用来查询某个或者多个文件描述符上的读写操作是否会阻塞,poll返回一个位掩码,用来支出非阻塞读写是否可能,如果将pool定义为NULL,则设备被认既及可读也可写,并且不会阻塞。
int (ioctl)(struct inoode, struct file
, unsigned int, unsigend long);执行设备特定命令的方法,内核还能识别一部分,而不必调用fops中的ioctl,如果设备不支持ioctl,则对于内核未定义的ioctl系统调用将返回错误-ENOTTY 设备无此ioctl命令。
int (*mmap)(struct file *, struct vm_area_struct *);将设备内存映射到进程地址空间,没有实现则返回错误-ENODEV
int (*open)(struct inode *, struct file ); 设置为NULL,设备代开操作永远成功,但系统不会通知驱动程序。
int (release)(struct inode, struct file
); file结构被释放时将调用该操作
int (*flush)(struct file *);发生在进程关闭设备文件描述符时,等待设备上尚未完成的操作。和fsync操作不同。如果flush为NULL,内核忽略请求。
int (fsync)(struct file , struct dentry , int);fsync系统调用的后端实现,刷新待处理的数据。设置为NULL,系统调用返回-EINVAL。
int (aio_fsync)(struct kiocb, int);fsync异步实现
int (fasync)(int, struct file , int) 通知设备其FASYNC标志发生了变化。异步通知。不支持可以设置为NULL
int (lock)(struct file , int, struct file_lock)文件锁定,设备不会实现该方法
ssize (readv)(struct file, const struct iovec
, unsigned long, loff_t
)
ssize (writev)(struct file , const struct iovec, unsigned long, loff_t); 设置为NULL,可能多次调用read和write方法
ssize (sendfile)(struct file, loff_t
, size_t, read_actor_t, void
) sendfile系统调用实现,用以最小的复制操作将数据从一个文件描述符移动到另一个。web服务器调用它把文件发送到网络。驱动通常设置为NULL
ssize (sendpage)(struct file, struct page
, int, size_t ,loff_t
, int);sendpage是sendfile系统调用的另外一半,由内核调用将数据发送到对应文件,每次一页,驱动通常设置为NULL
unsigned long (get_unmapped_area)(struct file, unsigned long, unsigned long, unsigend long, unsigned long);进程地址空间中找到一个合适的位置,吧底层设备中的内存段映射到该位置。通常由内存管理代码完成,但该方法存在可允许驱动程序强制满足特定设备需要的任何对其需求。大部分驱动程序可设置为NULL
int (*check_flags)(int) 该方法允许模块检查传递给fcntl(F_SETFL…)调用的标志
int (dir_notify) (struct file, unsigned long);当应用程序使用fcntl来请求目录改变通知时,该方法将被调用。驱动程序不必实现
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,

}
这种初始化方法是推荐的,紧凑易读,允许对结构成员进行重新排列,某些场合,把频繁访问成员放在相同硬件缓存行上,将大大提高性能。

file结构,驱动程序从不自己填写file结构,而只是对别处创建的file结构进行访问

inode结构中 驱动程序有用的两个字段
dev_t i_rdev; 设备文件对应的设备编号
struct cdev *i_cdev inode指向一个字符设备文件时使用,通过宏来访问设备号

字符设备注册

使用单独的cdev_alloc函数,则不需要使用cdev_init初始化,如果不是单独申请,而是嵌入到其他结构中,则使用cdev_init初始化cdev结构
所有初始化完成后再调用cdev_add,cdev_add返回之后设备即被激活了,避免并发访问
cdev_del后不能再访问cdev对象
通常cdev_add的count参数为1,也有特例,比如scsi磁盘驱动,通过每个物理设备的多个次设备号来允许用户空间选择不同使用模式(比如密度)

早期方法:

int register_chrdev(unsigned int major, const char* name, struct file_operations fios);
major是主设备号,name是驱动程序的名称。register_chrdev将为指定的主设备号注册0-255作为此设备号,并为每个设备监理一个对应的cdev结构。使用这一接口的驱动程序必须能够处理所有256个次设备号上的open调用。
移除调用int unregister_chrdev(unsigned int major, const char
name);major和name必须一致,疑问,每次移除驱动上的所有255个设备?

open和release

  • open方法:
    检查设备特定错误
    如果设备首次打开,进行初始化
    如有必要,更新f_op
    分配并填写置于filp->private_data里的数据结构

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

注意:flush方法hi在应用程序每次调用close时会被调用,不过很少驱动程序会实现flush,因为在close时并没有什么事情去做,除非releae被调用。
内核在进程退出的时候,通过在内部使用close系统调用来关闭所有相关文件。

scull的内存使用

  • NULL指针传递给kfree是合法的
  • 通过cp /dev/zero /dev/scull0用光所有的系统RAM
  • 驱动程序中仍和不确定的或与策略相关的值,都可以使用编译期间和加载截断进行配置。

内核不能直接引用用户空间内存地址

  • 原因:架构和内核配置的不同,在内核模式中运行时,用户空间的指针可能是无效。该地址可能根本无法映射到内核空间,或者指向某些随机数。
    用户空间内存是分页,可能被换出了,根本不在RAM中,
    用户程序可能存在缺陷或者是个恶意程序,可能允许随意访问内核资源或者覆盖系统中的内存,危及系统安全

  • 函数:unsigned long copy_to_user
    unsigned long copy_from_user

    使用时可以使用这两个函数来检查指针是否有效,无效就不会拷贝
    拷贝过程遇到无效地址则仅仅复制部分数据,
    在这两种情况下,返回值是还需要拷贝的数值,通常在使用时,如果不为0,返回给用户-EFAULT错误。

    确认不需要内核检查用户空间指针(其他地方已经检查过了),可以使用函数:
    __copy_to_user 和 __copy_from_user

  • 注意:内核中访问用户空间的内存,可能将该进程转入睡眠状态,
    所以访问用户空间的任何函数必须是可重入的,
    必须能够和其他驱动程序函数并发执行,
    必须处于能够合法休眠的状态。
    read和write会将文件位置的变化传回file结构,pread和pwrite系统调用不会修改文件位置。
    strace监控应用程序调用的系统调用以及它们的返回值。

总结:

完成一个字符设备的简单过程可以描述如下:

  • 注册设备号,指定register_chrdev_region或者动态分配alloc_chrdev_region
  • 创建设备对象cdev cdev_alloc
  • 初始化设备 cdev_init,是否单独调用时可选的,设置其他值owner,操作函数表ops等
  • cdev_add,把创建的设备及它对应的设备号告知内核

需要注意

  • 设置的操作函数表ops中每个函数的具体语义要认识准确
  • 用户空间的地址不能随便使用,要有限制及理解清楚限制条件
  • 注册设备使用的早起方法应该避免使用,后续可能从内核中移除
  • 设备注册过程中可能发生的并发访问
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值