Linux 设备驱动学习笔记 - 字符设备驱动篇 - Note.1[主次设备号]

本文深入探讨Linux字符设备驱动,讲解主次设备号的概念及其在内核中的表示,包括如何申请与注销设备号。此外,还介绍了file_operations结构、file结构和inode结构在设备驱动中的重要角色,以及如何定义和使用这些结构来实现基本的驱动操作。
摘要由CSDN通过智能技术生成

Linux 设备驱动学习笔记 - 字符设备驱动篇 - Note.1

  • LINUX DEVICE DRIVERS,3RD EDITION

一、字符驱动

【1】主次设备号

字符设备通过文件系统中的名字来存取. 那些名字称为文件系统的设备文件

规定它们位于 /dev 目录,通过ls -l 命令可以查看他们的详细信息,c 代表这是一个 char 字符设备,b 代表这是一个块设备,图片中的 1、10 、251代表了他们的主设备号,而后面的7、229 代表了该设备的次设备号,Linux 内核允许多个驱动共享主编号。

请添加图片描述

【2】设备编号的内部表示

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

利用在<linux/kdev_t.h>中的一套宏定义. 为获得一个 dev_t 的主或者次编号, 使用:

MAJOR(dev_t dev);

MINOR(dev_t dev);

如果你有主次编号, 需要将其转换为一个 dev_t, 可以使用:

MKDEV(int major, int minor);

请添加图片描述

【3】设备号的申请与注销

不算可控的申请方式

  • 第一种申请设备方法,但是内核会一次性帮你申请256个次设备号,这又显得很多余,数量并不算特别可控,所以我们有了下面的动态分配设备号。
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}

可控的申请设备号

  • 手动申请设备号,你需要提前知道你空闲的设备号,并填入该函数,这很明显不是特别友好,当你填入0时,他会默认帮你找到一个暂时没有用的设备号给你。
int register_chrdev_region(dev_t first, unsigned int count, char *name);

/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}
  • 动态为你分配一个主编号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);  

/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}
  • 使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. fisetminor 应当是请求的第一个要用的次编号; 它常常是 0. count 和 name 参数如同给 request_chrdev_region 的一样. 设备编号的释放使用:
void unregister_chrdev_region(dev_t first, unsigned int count); 

/**
 * unregister_chrdev_region() - return a range of device numbers
 * @from: the first in the range of numbers to unregister
 * @count: the number of device numbers to unregister
 *
 * This function will unregister a range of @count device numbers,
 * starting with @from.  The caller should normally be the one who
 * allocated those numbers in the first place...
 */
void unregister_chrdev_region(dev_t from, unsigned count)
{
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
}
  • 实际上我们发现,申请结构体中最终都是跳转到了 __register_chrdev_region 中
    /*
     * Register a single major with a specified minor range.
     *
     * If major == 0 this functions will dynamically allocate a major and return
     * its number.
     *
     * If major > 0 this function will attempt to reserve the passed range of
     * minors and will return zero on success.
     *
     * Returns a -ve errno on failure.
     */
    static struct char_device_struct *
    __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name);
【4】申请设备号的一般形式
if (module_major) { 
    dev = MKDEV(module_major, module_minor); 
    result = register_chrdev_region(dev, module_nr_devs, "module"); 
} 
else { 
    result = alloc_chrdev_region(&dev, module_minor, module_nr_devs, modulel"); 
    module_major = MAJOR(dev); 
} 
if (result < 0) { 
    printk(KERN_WARNING "module: can't get major %d\n", module_major); 
    return result; 
}

二、 三个关键数据结构

大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 称为 file_operations, file, 和 inode.

【1】file_operations 文件操作
  • 进入内核代码后会发现,平时再用户空间使用的那些接口,在内核空间中都能够找到对应的解释,这也是很有意思的一件事情

请添加图片描述

  • 该结构体中常用到的一些操作函数
struct module *owner  
第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
它被初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.  

loff_t (*llseek) (struct file *, loff_t, int);  
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器("file 结构" 一节中描述).  

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 
一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).  
    
ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);  
初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).  

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);  
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.  

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *); 
初始化设备上的一个异步写.  

unsigned int (*poll) (struct file *, struct poll_table_struct *);  
poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.  

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);  
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的ioctl"), 系统调用返回一个错误.  

int (*open) (struct inode *, struct file *);  
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如 
果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.  

//对应 close(),相对于 open() 操作
int (*release) (struct inode *, struct file *);  
在文件结构被释放时引用这个操作. 如同 open, release 可以为 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 方法被调用( 可能多于一次 ).  
  • 一般最简单的module 设备驱动只实现最重要的设备方法. 它的 file_operations 结构是如下初始化的:
struct file_operations my_module_fops = {  
    .owner = THIS_MODULE, 
    .read = module_read,  
    .write = modulel_write, 
    .ioctl = module_ioctl,  
    .open = modulel_open,  
    .release modulell_release,  
};  
【2】file文件结构
  • 结构体部分截取

请添加图片描述

struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构.

注意 file 与用户空间程序的 FILE 指针没有任何关系.

  • 一个 FILE 定义在 C 库中, 从不出现在内核代码中.

  • 一个 struct file, 另一方面, 是一个内核结构, 从不出现在用户程序中.

  • 文件结构代表一个打开的文件. (它不特定给设备驱动; 系统中每个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时创建, 并传递给在文件上操作的任何函数, 直到最后的关闭. 在文件的所有实例都关闭后, 内核释放这个数据结构. 在内核源码中, struct file 的指针常常称为 file 或者 filp(“file pointer”). 这个指针称为 filp 以避免和结构自身混淆. 因此, file 指的是结构, 而 filp 是结构指针.

  • 注意, release 不是每次进程调用 close 时都被调用. 无论何时共享一个文件结构 (例如, 在一个 fork 或 dup 之后),  release 不会调用直到所有的拷贝都关闭了. 在本结构体中也可以看到记录文件操作次数的成员变量。

  • 该结构体中常用到的一些操作函数

mode_t f_mode;  

文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.  

loff_t f_pos;  
当前读写位置. loff_t 在所有平台都是 64( 在 gcc 术语里是 long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它;读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.  

unsigned int f_flags;  
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应当检查读/写许可, 使用f_mode 而不是 f_flags. 所有的标志在头文件 <linux/fcntl.h> 中定义.  

struct file_operations *f_op;  
和文件关联的操作. 内核安排指针作为它的 open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.  

void *private_data;  
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.  

struct dentry *f_dentry;  
关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry  
结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.  
【3】inode 结构
  • 结构体部分截取

请添加图片描述

  • Linux系统中万物皆文件的思想,每个文件都对应有自己唯一的一个的文件编号,inode 结构由内核在内部用来表示文件。 因此,它和代表打开文件描述符的文件结构是不同的。可能有代表单个文件的多个打开描述符的许多文件结构,但是它们都指向一个单个 inode 结构。通过使用ls -i 指令我们可以查看,这也就是文件的 inode 号。

请添加图片描述

inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于编写驱动代码有用:  
dev_t i_rdev; 对于代表设备文件的节点, 这个成员包含实际的设备编号.  

struct cdev *i_cdev;  
|-->struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
    };

struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.  
内核开发者已经增加了 2 个宏, 可用来从一个 inode 中获取主次编号:  
unsigned int iminor(struct inode *inode);  
unsigned int imajor(struct inode *inode);
|-->static inline unsigned iminor(const struct inode *inode)
    {
        return MINOR(inode->i_rdev);
    }

    static inline unsigned imajor(const struct inode *inode)
    {
        return MAJOR(inode->i_rdev);
    }

三、一个简单的驱动程序框架

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值