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_operation 是 fops,下面是应用层系统调用与驱动层 fops 的调用关系。

file_operations 的 open 和 release 接口的第一个参数是 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 异步通知
~