字符设备驱动程序之一

本文详细介绍了简单字符设备驱动程序SCDDP的设计原理及实现过程,包括主设备号与次设备号的概念、设备编号的分配与释放、以及file_operations、file和inode等重要数据结构的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. scddp的设计

scddp,即"Simple Character Device Driver Program,简单的字符设备驱动程序"的缩写。scddp是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个设备。这个设备是由一个全局且持久的内存区域组成。"全局"是指,如果设备被多次打开,则打开它的所有文件描述符可共享该设备所包含的数据。"持久"是指,如果设备关闭后再打开,则其中的数据不会丢失。

 

2. 主设备号和次设备号

对字符设备的访问时通过文件系统内的设备名称进行的。那些名称被称为特殊文件、设备文件,或者称为文件系统树的节点,通常位于/dev目录下。字符设备驱动程序的设备文件可通过ls -l命令输出的第一列中的"c"来识别。块设备由字符"b"标识。

如果执行ls -l命令,则可在设备文件项的最后修改日期看到两个数(用逗号分隔),这个通常是文件的长度;而对于设备文件,这两个数就是相应设备的主设备号和次设备号。如下:

[wzhwho@local~]#ls -l /dev

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

crw-------     1        root     root    4,1  Apr 11 2009    tty1

crw-------     1        root     root    4,64  Oct 16 2009   ttyS0

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

主设备号标识对应的驱动程序,例如/dev/null和/dev/zero都由驱动程序1管理。次设备号由内核使用,用于正确确定设备文件所指的设备。

 

2.1. 设备编号的内部表达

在内核中,dev_t类型(在<linux/types.h>中定义)用来保存设备编号,包括主设备号和次设备号。在2.6.X的内核中,dev_t是一个32位的数,其中的12位用来表示主设备号,而其余的20位用来表示次设备号。如果,要获得dev_t的主设备号或次设备号,应使用:

MAJOR(dev_t dev);

MINOR(dev_t dev);

当然,如果需要将主设备号和此设备号转换成dev_t类型,则使用:

MKDV(int major, int minor);

 

2.2.分配和释放设备编号

在建立一个字符设备之前,需要获得一个或者多个设备编号。完成该工作的函数是register_chrdev_region,该函数在<linux/fs.h>中声明:

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

参数first是要分配的设备编号范围的起始值。first的次设备号经常被设置为0。参数count是所请求的连续设备编号的个数。而name是和该编号范围关联的设备名称,它将出现在/pr0c/devices和sysfs中。

在模块运行过程中,可使用下面的函数让内核来实现动态分配所需的主设备号。

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,

unsigned int count,char *name);

输出参数dev在成功完成调用后将保存已经分配范围的第一个编号。但是,不论采用哪种方式分配设备编号,都应该在不再使用它们的时候释放这些设备编号。内核提供的函数如下:

void unregister_chrdev_region(dev_t first, unsigned int first);

 

在内核源码树的Documentation/devices.txt文件中可以找到常见设备的清单。对于新的驱动程序应该使用动态分配的机制获取主设备号。但是动态分配的缺点是:由于分配的主设备号不能始终保持一致,所以无法预先建立设备节点。不过可以利用awk工具, 读取/proc/devices文件中信息。

Character devices:

1 mem

2 pty

3 ttyp

4 ttys

Block devices:

2 fd

8 sd

65 sd

66 sd

 

最后,分配主设备号的最佳方式:默认是动态分配,同时保留在加载甚至是编译时指定主设备号的余地。

 

3. 重要的数据结构

大部分驱动程序操作涉及到三个重要的内核数据结构:file_operations、file和inode。

 

3.1. 文件操作

file_operations结构就是连接驱动程序保留的设备编号。这个结构在<linux/fs.h>中定义,其中包含了一组函数指针。每个打开的文件(在内部由一个file结构表示)和一组函数(通过包含指向一个file_operations结构的f_op字段)关联。这些操作主要用来实现系统调用,命名为open、read等。我们可以认为文件是一个"对象",而操作它的函数是"方法"。

在通读file_operatios方法的清单,会发现许多参数包含__user字符串,它其实是一种形式的文档而已,表明该指针是一个用户空间地址,因此不能直接引用。下面仅是字符驱动程序常用到的成员。

struct module *owner

      第一个file_operations字段并不是操作,而是一个指向"拥有"该结构的模块的指针。几乎所有情况下,该成员都会被初始化为THIS_MODULE(在<linux/module.h>中定义)。

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

      方法llseek用来修改文件的当前读写位置,并将新位置作为返回值返回。参数loff_t是一个"长偏移量",即使32位平台上也至少占用64位的数据宽度。出错时返回一个负的返回值。如果这个函数指针式NULL,对seek的调用将会以不可预期的方式修改file结构中的位置计数器。

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

      用来从设备中读取数据。该函数指针被设置为NULL值时,将导致read系统调用出错并返回-EINVAL("Invalid argument, 非法参数")。函数返回非负值表示成功读取的字节数。

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

      向设备中发送数据。该函数指针被设置为NULL值时,将导致write系统调用出错并返回-EINVAL。函数返回非负值表示成功写入的字节数。

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

      poll方法是poll、epoll和select这三个系统调用的后端实现。这三个系统调用可用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。poll方法应该返回一个位掩码,用来指出非阻塞的读写或写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到I/O变为可能时的信息。如果驱动程序将poll设置为NULL,则设备会被认为既可读也可写,并且不会被阻塞。

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

     系统调用ioctl提供了一种执行设备特殊命令的方法(如格式化磁盘)。如果设备不提供ioctl入口点,则对于任何内核未预先定义的请求,ioctl系统调用会返回错误(-ENOTTY,"no such ioctl for device,该设备无此ioctl命令")。

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

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

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

     如果该方法被置为NULL,设备的打开操作永远成功,但系统不会通知驱动程序。

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

     当file结构被释放,就调用该方法。

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

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

     这两个方法用来实现分散/聚集型的读写操作。如果函数指针被设置为NULL值时,就会调用read和write方法多次。

scddp的file_operations结构被初始化为如下形式:

struct file_operations scddp_fops = {

    .owner = THIS_MODULE,

    .llseek =    scddp_llseek,

    .read =      scddp_read,

    .write =     scddp_write,

    .ioctl  =    scddp_ioctl,

    .open =     scddp_open,

    .release =    scddp_release,

};

这个声明采用了标准C的标记化结构初始化语法,因为他时驱动程序在结构定义发生变化时更具可移植性,并使得代码更加紧凑且易读。标记化的初始化方法允许对结构成员进行重新排列。

 

3.2. file结构

在<linux/fs.h>中定义的struct file是设备驱动程序所使用的第二个重要结构。file结构与用户空间程序中的FILE没有任何关联。FIlE在C库中定义且不会出现在内核代码中;struct file是一个内核结构,不会出现在用户程序中。file结构代表一个打开的文件。它是由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。在文件的所有实例都被关闭后,内核会释放该结构。它重要的成员如下:

mode_t   f_mode;

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

loff_t   f_ops;

     当前的读/写位置。loff_t是一个64位的数。read/write会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对filp->f_ops进行操作。这一规则的一个例外就是llseek方法,该方法的目的本身就是为了修改文件位置。

unsigned int f_flags;

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

struct file_operations *f_op;

      与文件相关的操作。内核在执行open操作时对这个指针赋值,以后需要处理这些操作时就读取这个指针。filep->f_op中的值决不会保存,也就是说,可以在任何需要的时候修改文件的关联操作,这个在面向对象编程技术叫做"方法重载"。

void * privat_data;

      open系统调用在调用驱动去、程序的open方法前将这个指针置为NULL。

struct dentry *f_dentry;

      文件对应的目录项(dentry)结构。除了用filep->f_dentry->d_inode的方式来访问索引节点结构之外,驱动程序的开发者无需关系dentry结构。

 

3.3. inode结构

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

结构inode包含大量有关文件的信息,以下两个字段对编写驱动程序有用:

dev_t i_rdev;

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

struct cdev *i_cedv;

      是表示字符设备的内核的内部结构。

为了增强可移植性,内核开发者增加了两个新的宏,用来从一个inode中获取主设备号和次设备号。

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值