Linux驱动开发(一)--字符驱动

Linux驱动开发–字符驱动

2022/1/06:更新
写驱动之前的目的导向:

编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制).因为我们的"设备"是计算机内存的一部分, 我们可自由做我们想做的事情. 它可以是一个顺序的或者随机存取的设备, 一个或多个设备, 等等.

一、scull源码

1.1 、主次编号

dev_t是32位量,其中12位用作主编号,20位用作次编号

获取主次编号
MAJOR(dev_t dev)
MINOR(dev_t dev)
将主次编号转换为dev_t
MKDEV(int major, int minor)

1.2、分配和释放设备编号

建立字符驱动时第一件事:获取一个或多个设备编号来使用。

<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
	first 是你要分配的起始设备编号. first 的次编号部分常常是 0, 但是没有要求是那个效果. 
	count 是你请求的连续设备编号的总数. 注意, 如果 count 太大, 你要求的范围可能溢出到下一个次编号; 但是只要你要求的编号
范围可用, 一切都仍然会正确工作.
	name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中.
	返回值 0 成功,失败返回错误码

动态分配设备编号:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
	dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. 
	fisetminor 应当是请求的第一个要用的次编号; 它常常是 0.
	count 和 name 与register_chrdev_region一致
	
设备编号的释放:
void unregister_chrdev_region(dev_t first, unsigned int count);

1.3、file_operations 结构体注解

struct file_operations {
	struct module *owner;
	指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
	
	loff_t (*llseek) (struct file *, loff_t, int);
	llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器。
	
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
	
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
	
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
	
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	打开设备,若NULL,则驱动得不到通知
	
	int (*flush) (struct file *, fl_owner_t id);
	flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.
	
	int (*release) (struct inode *, struct file *);
	在文件结构被释放时引用这个操作.
	
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
	
	int (*fasync) (int, struct file *, int);
	这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知
	
	int (*lock) (struct file *, int, struct file_lock *);
	lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.
	
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

scull 设备驱动只实现最重要的设备方法. 它的 file_operations 结构是如下初始化的:

struct file_operations scull_fops = { 
 		.owner = THIS_MODULE, 
 		.llseek = scull_llseek, 
 		.read = scull_read, 
 		.write = scull_write, 
 		.ioctl = scull_ioctl, 
 		.open = scull_open, 
 		.release = scull_release, 
 }; 

1.4、文件结构

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

	/*
	 * Protects f_ep_links, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	enum rw_hint		f_write_hint;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	文件标志,如O_NONBLOCK此类
	
	fmode_t			f_mode;
	文件模式确定文件是可读的或者是可写的(或者都是), 通过位FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数
中检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企
图被拒绝, 驱动甚至不知道这个情况.
	
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	当前读写位置. loff_t 在所有平台都是 64( 在 gcc 术语里是 long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.
	
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

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

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct list_head	f_ep_links;
	struct list_head	f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

1、5、inode结构

该结构用于在内核表示文件,和代表打开文件描述符的文件结构是不同的. 可能有代表单个文件的多个打开描述符的许多文件结构, 但

是它们都指向一个单个 inode 结构.编写驱动代码需要注意的两个结构:

dev_t i_rdev; 
	对于代表设备文件的节点, 这个成员包含实际的设备编号. 
struct cdev *i_cdev; 
	struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.
        
获取主次编号:
	unsigned int iminor(struct inode *inode); 
	unsigned int imajor(struct inode *inode);
使用这些宏代替直接操作 i_rdev

二、字符设备的注册

内核使用struct cdev来代表字符设备,包含头文件<linux/cdev.h>

两种分配和初始化字符设备结构体的方法
    struct cdev *my_cdev = cdev_alloc();
	my_cdev->ops = &my_fops;
初始化结构:
    void cdev_init(struct cdev *cdev, struct file_operations *fops);
拥有者成员--THIS_MODULE,当结构建立时候,则需向内核进行添加
    int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
		dev 是 cdev 结构
		num 是这个设备响应的第一个设备号
		count 是应当关联到设备的设备号的数目. 常常 count 是 1, 但是有多个设备号对应于一个特定的设备的情形.
若调用失败,返回错误码,设备也不能被添加到系统中。
删除字符设备:
	void cdev_del(struct cdev *cdev);
传入后的cdev将不能再被存取

2.1、scull中的设备注册

struct scull_dev { 
 	struct scull_qset *data; /* Pointer to first quantum set */ 
 	int quantum; /* the current quantum size */ 
 	int qset; /* the current array size */ 
 	unsigned long size; /* amount of data stored here */ 
 	unsigned int access_key; /* used by sculluid and scullpriv */ 
 	struct semaphore sem; /* mutual exclusion semaphore */ 
 	struct cdev cdev; /* Char device structure */ 
};

scull设备的初始化

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); // 添加设备,devno为设备响应的第一个设备号
	/* Fail gracefully if need be */ 
	if (err) 
	printk(KERN_NOTICE "Error %d adding scull%d", err, index); 
}

2.6内核之前的注册字符设备的经典方法

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
	@major : 主编号
	@name : 驱动名字(出现在/proc/devices)
	@fops : 缺省结构
与之对应的取出字符设备函数:
int unregister_chrdev(unsigned int major, const char *name);
此处参数与注册时相同

2.2、open方法

open方法提供给驱动来进行后续初始化操作,大部分驱动中应完成的工作如下:

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

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

​ • 如果需要, 更新 f_op 指针.

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

int (*open)(struct inode *inode, struct file *filp);
container_of(pointer, container_type, container_field); // <linux/kernel.h>
这个宏使用一个指向 container_field 类型的成员的指针, 它在一个 container_type 类型的结构中, 并且返回一个指针指向包含结构. 在 
scull_open, 这个宏用来找到适当的设备结构:
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 在文件结构的 private_data 成员中存储一个它的指针, 为以后更易存取.
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值