linux驱动程序入门2

linux驱动程序入门

  • [1. file_operation结构](#1. file_operation结构)
  • [1.2 使用register_chrdev注册字符设备](#1.2 使用register_chrdev注册字符设备)
  • [1.3 使用cdev_add注册字符设备](#1.3 使用cdev_add注册字符设备)
  • [1.4 字符设备的读写](#1.4 字符设备的读写)
  • [1.5 IOCTL接口](#1.5 IOCTL接口)
  • [1.6 seek接口](#1.6 seek接口)
  • [1.7 poll接口](#1.7 poll接口)
  • [1.8 异步通知](#1.8 异步通知)

1. file_operation结构

这个结构是提供给 VFS(虚拟文件系统)的文件接口,是 Linux 驱动开发中最核心、最常用的数据结构之一。它定义了一组函数指针,将用户空间对文件的操作(如打开、读写、定位、释放等)与驱动程序的具体实现关联起来。当用户进程对设备文件执行系统调用时,VFS 会通过该结构找到对应的驱动函数并调用。它的结构定义如下:

c 复制代码
struct file_operation{
struct module *owner;														//模块所有者
loff_t(*llseek) (struct file*, loff_t, int);							//寻找文件读写位置
ssize_t(*read)(struct file*, char __user*, size_t, loff_t *);			//读
ssize_t(*write)(struct file*, const char __user*, size_t, loff_t *);	//写
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*);			// 查询可读性
unsigned int (*poll)(struct file*, struct poll_table_struct *);		// 轮询,检查设备是否可读写
long (*unlocked_ioctl)(struct file*, unsigned int , unsigned long);			// 未加锁的控制接口

// 64位系统上处理32位的ioctl调用
long (*compat_ioctl)(struct file *, unsigned int , unsigned long);			
int(*mmp)(struct file *, struct vm_area_struct *);	// 内存映射
int (*open)(struct inode *, struct file *);		// 打开设备
int (*flush)(struct file *, fl_owner_t id);		// 执行未完成的操作
int (*release)(struct inode *, struct file *);		// 释放
int (*fsync)(struct file *, loff_t, loff_t, int datasync);	// 刷新待处理的数据
int (*aio_fsync)(struct kiocb *, int datasync);	// 异步刷新待处理的数据
int (*fasync)(struct file *, int);	// 通知设备FASYNC标志
int(*lock)(struct file *, int, struct file_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 **);	// 设置租约
long(*fallocate)(struct file *, int, loff_t, loff_t);	// 预分配文件空间
void(*show_fdinfo)(struct seq_file *, struct file *);	// 显示文件描述符信息
};

#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);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);

在内核中,file 结构代表一个打开的文件,file 在执行 file_operation 中的 open 操作时创建。假设驱动程序定义的 file_operationfops,下面是应用层系统调用与驱动层 fops 的调用关系。

file_operationsopenrelease 接口的第一个参数是 inode 结构。该结构被内核用来表示一个文件节点,也就是一个具体的文件或目录。

inode 结构同样定义了一组操作接口,用于描述对文件节点本身的操作(如创建、删除、重命名、查找子项等),其结构定义如下:

c 复制代码
struct inode_operations {
	struct dentry * (*lookup) (struct inode *dir, struct dentry *dentry, unsigned int flags);
	// 在目录中查找子项,返回对应的 dentry

	const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);
	// 获取符号链接指向的路径

	int (*permission) (struct inode *, int);
	// 检查是否允许对 inode 执行指定操作

	struct posix_acl * (*get_acl) (struct inode *, int, bool);
	// 获取文件的 POSIX ACL 访问控制列表

	int (*readlink) (struct dentry *, char __user *, int);
	// 读取符号链接的内容

	int (*create) (struct inode *dir, struct dentry *, umode_t, bool);
	// 在目录中创建新文件

	int (*link) (struct dentry *, struct inode *, struct dentry *);
	// 创建硬链接

	int (*unlink) (struct inode *, struct dentry *);
	// 删除文件

	int (*symlink) (struct inode *, struct dentry *, const char *);
	// 创建符号链接

	int (*mkdir) (struct inode *, struct dentry *, umode_t);
	// 创建目录

	int (*rmdir) (struct inode *, struct dentry *);
	// 删除目录

	int (*mknod) (struct inode *, struct dentry *, umode_t, dev_t);
	// 创建特殊文件(设备节点、管道等)

	int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *, unsigned int);
	// 重命名文件或目录

	int (*setattr) (struct dentry *, struct iattr *);
	// 修改文件属性(如权限、大小、时间戳)

	int (*getattr) (struct inode *dir, struct dentry *, struct kstat *);
	// 获取文件属性

	int (*setxattr) (struct dentry *, const char *, const void *, size_t, int);
	// 设置扩展属性

	ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
	// 获取扩展属性

	ssize_t (*listxattr) (struct dentry *, char *, size_t);
	// 列出所有扩展属性

	int (*removexattr) (struct dentry *, const char *);
	// 删除扩展属性

	int (*tmpfile) (struct inode *, struct dentry *, umode_t);
	// 创建临时文件

	int (*set_acl) (struct inode *, struct posix_acl *, int);
	// 设置 POSIX ACL

	int (*fileattr_get) (struct dentry *, struct fileattr *);
	// 获取文件属性标志

	int (*fileattr_set) (struct dentry *, struct fileattr *);
	// 设置文件属性标志

	int (*update_time) (struct inode *, struct timespec64 *, int);
	// 更新文件时间戳

	int (*atomic_open) (struct inode *, struct dentry *, struct file *, unsigned open_flag, umode_t create_mode);
	// 原子地打开并创建文件

	int (*fiemap) (struct inode *, struct fiemap_extent_info *, u64 start, u64 len);
	// 获取文件物理块映射信息
};

1.2 使用register_chrdev注册字符设备

注册字符设备可以使用register_chrdev函数。

c 复制代码
int register_chrdev(unsingned int major, const char *name, struct file_operation **fops);

register_chrdev函数的major参数如果等于0,则表示采用系统动态分配的主设备号。

注销字符设备可以使用unregister_chrdev函数。

c 复制代码
int unregister_chrdev(unsingned int major, const char *name);

字符设备模块使用insmod加载,加载后在/dev/下使用mkmod命令建立相应的文件节点。

c 复制代码
[root@]$ insmod demo.ko
[root@]$ kmnod /dev/fgj c 224 0
[root@]$ ./test   # 这是应用层可执行程序

1.3 使用cdev_add注册字符设备

register_chrdev函数调用了cdev_add函数。在linux内核中的字符设备用cdev结构来描述,其定义如下:

c 复制代码
struct cdev {
	struct kobject kobj;
	struct module *owner;		//所属模块
	const struct file_operation *ops;	//文件操作结构
	dev_t dev;	//设备号,int类型,高12位为主设备号,低20位为次设备号
	unsigned int count;
}

下面对cdev结构进行操作:

c 复制代码
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *, const struct file_operations *);	//初始化cdev的file_operation
void cdev_put(struct cdev *p);	
// 注册设备,通常发生在驱动模块的加载函数中
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
// 注销设备,通常发生在驱动模块的卸载函数中
void cdev_del(struct cdev *p); 

使用cdev_add注册字符设备前应先调用register_chrdev_region或alloc_chrdev_region分配设备号

  • register_chrdev_region用于指定设备号的情况
  • alloc_chrdev_region用于动态申请设备号
c 复制代码
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count ,const char *name);

register_chrdev_region函数申请从from开始的count个主设备号,alloc_chrdev_region申请一个动态主设备号。

baseminor 是起始次设备号,count 是次设备号的数量。

注销设备号cdev_del后使用unregister_chrdev_region:

c 复制代码
void unregister_chrdev_region(dev_t from, unsigned count);

1.4 字符设备的读写

在应用程序看来,字符设备只是一个设备文件,应用程序可以对硬件设备进行操作。操作都在file_operation结构中的接口。比如read对应file_operation->read,write对应file_operation->write。

从内核空间向用户空间复制数据使用copy_to_user函数:

c 复制代码
unsigned long copy_to_user(void __user * to,const void *from, unsigned long n);

从用户空间向内核空间复制数据使用copy_from_user函数:

c 复制代码
unsigned long copy_from_user(void * to,const void *from, unsigned long n);

这两个函数在include/asm/uaccess.h。

此外,内核空间和用户空间可进行单值交互

c 复制代码
put_user(x,p)	// 向用户空间指针p传值x
get_user(x,p)	// 从用户空间指针p读值x

当一个指针指向用户空间时,必须确保指向的用户地址是合法的,且对应页面已经映射。可以使用access_ok函数检测。type参数有两个选项:VERIFY_READ,VERIFY_WRITE。

c 复制代码
int access_ok(int type, const void *addr, unsigned long size);

1.5 IOCTL接口

IOCTL接口用来进行IO控制,应用程序通过IOCTL接口向设备发送命令、参数设置等信息,file_operation结构中对应的IOCTL接口函数原型如下:

c 复制代码
long (*unlocked_ioctl)(struct file *file, unsigned int cmd, unsigned long arg);

cmd是命令类型,arg是参数。

c 复制代码
#define COMMAND1 0x11
#define COMMAND2 0x12
// 内核IOCTL接口
int sample_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg){
	switch(cmd)
	{
		case COMMAND1 :
			memset(demoBuffer, 0x31, 256);
			break;
		case COMMAND2 :
			memset(demoBuffer, 0x32, 256);
			break;
		default :
			return -EFAULT;
			break;
	}
	return 0;
}

struct file_operation simple_fops = {
	.owner = THIS_MODULE,
	.unlocked_ioctl = simple_ioctl,
	.open = simple_open, 
	.release = simple_release,
}
c 复制代码
// 应用程序
void main(void)
{
	int fd;
	int i;
	char data[256];
	int retval;
	fd = open("/dev/fgj", O_RDWR);
	if(fd == -1)
	{
		perror("error open\n");
		exit(-1);
	}
	printf("open /dev/dgj successfully\n");
	// 应用层IOCTL控制
	retval = ioctl(fd, COMMAND1, 0);
	if(retval == -1)
	{
		perror("ioctl error\n");
		exit(-1);
	}
	printf("send command1 successfully\n");
	retval = ioctl(fd, COMMAND2, 0);
	if(retval == -1)
	{
		perror("ioctl error\n");
		exit(-1);
	}
	printf("send command1 successfully\n");
	close(fd);
}

运行结果如下:

c 复制代码
[root@] $ insmod demo.ko
[root@] $ mknod /dev/fgj c 224 0
[root@] $ ./test

unlocked_ioctl中的cmd参数不能随意定义,否则会发生冲突。

c 复制代码
#define FIBMAP _IO(0x00, 1)	//访问bmap
#define FIGETBSZ _IO(0x00, 2)	//访问bmap块大小
#define FIFREEZE _IO('X', 119, int)	//冻结
#define FITHAW _IO('X', 120, int)	//解冻
#define FITRIM	_IOWR('X', 121, struct fstrim_range)	//剪裁
#define FICLONE	_IOW(0x94, 9, int)
#define FICLONERANGE _IOW(0x94, 13, struct file_clone_range)
#define FIDEDUPERANGE _IOW(0x94, 54, struct file_dedupe_range)

实际上IOCTL接口的cmd参数每个位都有特殊的含义。

Bit0~7:NR

Bit8~15:type 类型

Bit16~29:size arg变量传送数据大小

Bit30~31: dir 区别读写

用于创建IOCTL接口命令的宏包括:

c 复制代码
#define _IO(type, nr)	
#define _IOR(type, nr, size)
#define _IOW(type, nr, size)
#define _IOWR(type, nr, size)
#define _IOR_BAD(type, nr, size)
#define _IOW_BAD(type, nr, size)
#define _IOWR_BAD(type, nr, size)

用于解码IOCTL命令的宏包括

c 复制代码
#define _IOC_DIR(nr)
#define _IOC_TYPE(nr)
#define _IOC_NR(nr)
#define _IOC_SIZE(nr)

1.6 seek接口

字符设备只能按顺序读写,上次操作结束位置就是当前读写位置,seek接口用来对设备读写位置进行重定位,file_operation结构体中对应的seek接口如下;

c 复制代码
loff_t(*llseek)(struct file *filp, loff_t off, int whence)

off是偏移量,whence参数指起点位置。

1.7 poll接口

如果设备被设置成阻塞式操作,即当设备执行 I/O 操作时,如果不能获得数据,将阻塞,直到获得数据。应用层可以使用select函数查询设备当前的状态,可以得知能否对设备进行非阻塞的访问。

使用select函数要在设备驱动程序中添加file_operation->poll接口支持。

c 复制代码
static unsigned int my_poll(struct file *file, struct poll_table_struct *wait) {
	unsigned int mask = 0;
	pol_wait(file, &outq, wait);	//把当前进程添加到等待列表
	if(0 != bta->read_count)	//如果有数据
		mask |= (POLLIN | POLLRDNORM);
	return mask;
}

应用层多路IO选择函数select原型如下:

c 复制代码
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • readfds:被select函数监视的读的文件描述符集合
  • writefds:select函数监视的写的文件描述符集合
  • exceptfds:select函数监视的异常处理的文件描述符集合
  • numfds: 需要监视的号码最高的文件描述符加1
  • timeout:指向timeval结构类型的指针,超时时间
    有两种情况会返回,一种是监视设备中有一些设备可读可写或发生异常,另一种是超时。

文件描述符常用函数接口如下:

c 复制代码
FD_ZERO(fd_set *set)		//清除一个文件描述符集 
FD_SET(int fd, fd_set *set)		//将文件描述符fd加入文件描述符集中 
FD_CLR(int fd, fd_set *set)		//将文件描述符fd从文件描述符集中清除
FD_INSET(int fd, fd_set *set)	//判断文件描述符fd是否被置位 

poll 驱动程序代码:

c 复制代码
unsigned int simple_poll(struct file *file, poll_table *pt)
{
	unsigned int mask = POLLIN | POLLRDNORM;
	poll_wait(file, &read_queeu, pt);
	return mask;
}

struct file_operation simple_fops = {
	.owner = THIS_MODULE,
	.unlocked_ioctl = simple_ioctl,
	.open = simple_open, 
	.release = simple_release,
}

应用层代码:

c 复制代码
int fd;
void readthread(void *arg)	//读数据线程
{
	char data[256];
	fd_set rfds;
	fd_set wfds;
	int retval = 0;
	while(1) {
			FD_ZERO(&rfds);
			FD_SET(fd, &rfds);
			select(fd+1, &rfds, &wfds, NULL, NULL );
			if(FD_ISSET(fd, &rfds) {
				retval = read(fd, data, 3);
				if(retval == -1)
				{
					perror("read error\n");
					exit(-1);
				}
				data[retval] = 0;
				printf("read successfully:%s\n", data);
			}
	}
	return (void*) 0;
}

void main() {
	int i;
	int retval;
   fd = open("/dev/fgj", O_RDWR); 
   if(fd == -1) 
   {
   	perror("error open\n"); 
     exit(-1); 
   }
   printf("open /dev/fgj successfully\n");
	pthread_t tid;
	pthread_create(^tid, NULL, readthread, NULL);	//创建读线程
	while(1) {
		retval = write(fd, "fgj", 3);
		if(retval == -1) 
   	{
   		perror("write error\n"); 
     	exit(-1); 
   	}
	}
	close(fd); 
}

1.8 异步通知

~

相关推荐
ShineWinsu1 小时前
对于MySQL:内置函数的解析
linux·数据库·c++·mysql·面试·函数·查询
万物智能信息科技2 小时前
RK3568 的多路显示移植—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
linux·开发语言·华为·开源·harmonyos
IT菜鸟程2 小时前
Ubuntu 下 apt 版 Nginx 升级为源码编译版本完整操作指南(1.24.0 → 1.30.4 实战)
linux·nginx·ubuntu
新时代牛马2 小时前
Linux 磁盘管理:分区、文件系统、挂载与扩容
linux·运维·服务器
weixin_402486342 小时前
VS Code Codex/Claude Extension 无法安装 / 无法正常打开
linux·服务器·vscode
奋斗的阿狸_19863 小时前
ch32v003j4m6+ws2812b方案2:规划引脚定义
linux
Mr-Apple3 小时前
系统找不到指定的文件-CreateInstance/CreateVm/HCS/ERROR_FILE_NOT_FOUND
linux·windows·ubuntu·wsl
布裘3 小时前
【银河麒麟】服务器系统开机黑屏故障排查与修复
linux·运维·服务器·银河麒麟·无法启动
Zenova EdgeOS3 小时前
Linux iotop:工业边缘 I/O 高级分析实战
linux·运维·服务器