Linux字符设备驱动模型

框架梳理

 

:重要结构体:file_operations结构体

struct file_operations { 
  struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES  
   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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作   
   ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作   
  int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL   
   unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入   
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令   
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl  
  long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替   
  int (*mmap) (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 *, struct dentry *, int datasync); //刷新待处理的数据   
  int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据   
  int (*fasync) (int, 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 **);   
};

结构体成员分析

1、struct module *owner

       第一个 file_operations 成员根本不是一个操作,它是一个指向拥有这个结构的模块的指针。

      这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。

2、loff_t (*llseek) (struct file * filp , loff_t p, int orig);

      (指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))

      llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.

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

3、ssize_t (*read) (struct file * filp, char __user * buffer, size_t    size , loff_t * p);

    (指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址),参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)

     这个函数用来从设备中获取数据。在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败。一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型). 

4、ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t   p);

      可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同 的,异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);初始化一个异步读 -- 可能在函数返回前不结束的读操作.如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答) 

5、ssize_t (*write) (struct file * filp, const char __user *   buffer, size_t count, loff_t * ppos);

     (参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)

     发送数据给设备.。如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数。

     (注:这个操作和上面的对文件进行读的操作均为阻塞操作) 

6、ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos);

      初始化设备上的一个异步写.参数类型同aio_read()函数; 

7、int (*readdir) (struct file * filp, void *, filldir_t);

      对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.

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

     (这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)

     这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。

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

    (这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果) 

9、int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

       (inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数.cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针.如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的.因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.)

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

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

        mmap 用来请求将设备内存映射到进程的地址空间。 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

       (如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)

11、int (*open) (struct inode * inode , struct file * filp ) ;

        (inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)

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

12、int (*flush) (struct file *);

        flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 

       它应当执行(并且等待)设备的任何未完成的操作.这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用;SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.

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

        release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:

       void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL. 

14、int(*synch)(struct file *,struct dentry *,int datasync);

       刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。 

15、int (*aio_fsync)(struct kiocb *, int);

        这是 fsync 方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束,这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘 

。。。。。。。。。。。。。

二:开发步骤

        1:模块的加载和卸载:

module_init(xxx_init);
//注册模块加载函数
module_exit(xxx_exit);
//注册模块卸载函数
        module_init(xxx_init); 用于向内核注册一个模块,使用insmod命令加载驱动,xxx_init函数开始被调用。
        module_exit(xxx_exit); //注册模块卸载函数,用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

        2:xxx_init函数:

1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函数具体内容 */
5 return 0;
6 }
7 
8 /* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函数具体内容 */
12 }

         3:字符设备的注册与注销

在Linux字符设备驱动中

static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
//注册一个字符设备
static inline void unregister_chrdev(unsigned int major, const char *name).
//卸载一个字符设备
       (1) register_chrdev 函数用于注册字符设备
        major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两
部分,关于设备号后面会详细讲解。
        name:设备名字,指向一串字符串。 fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
        unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
        major: 要注销的设备对应的主设备号。
        name: 要注销的设备对应的设备名。
        示例代码:
1 static struct file_operations test_fops;
2
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6 /* 入口函数具体内容 */
7 int retvalue = 0;
8
9 /* 注册字符设备驱动 */
10 retvalue = register_chrdev(200, "chrtest", &test_fops);
11 if(retvalue < 0){
12 /* 字符设备注册失败,自行处理 */
13 }
14 return 0;
15 }
16
17 /* 驱动出口函数 */
18 static void __exit xxx_exit(void)
19 {
20 /* 注销字符设备驱动 */
21 unregister_chrdev(200, "chrtest");
22 }
23
24 /* 将上面两个函数指定为驱动的入口和出口函数 */
25 module_init(xxx_init);
26 module_exit(xxx_exit);
    (2)cdev_init(&oled.cdev, &oled_ops);
            cdev_add(&oled.cdev, oled.devid, DEV_CNT);      cdev_del(&oled.cdev);  

        4:申请设备号

动态申请设备号:register_chrdev_region

int register_chrdev_region(dev_t from, unsigned count, const char *name);
/* 参数:
    dev_t from - 要申请的设备号(起始)
    unsigned count - 要申请的设备号数量
    const char *name - 设备名
   返回值:
    成功:0
    失败:负数(绝对值是错误码)*/

静态申请设备号:alloc_chrdev_region

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/* 参数:
    dev_t *dev - 用于保存分配到的第一个设备号(起始)
    unsigned baseminor - 起始次设备号
    unsigned count - 要分配设备号的数量
    const char *name - 设备名
   返回值:
    成功:0
    失败:负数(绝对值是错误码)*/

释放设备号:unregister_chrdev_region

void unregister_chrdev_region(dev_t from, unsigned count);
/* 参数:
    dev_t from - 要释放的第一个设备号(起始)
    unsigned count - 要释放的次设备号数量 */

5:自动创建设备节点

        在驱动用加入对udev 的支持主要做的就是:在驱动初始化的代码里调用class_create(...)为该设备创建一个class,再为每个设备调用device_create(...)创建对应的设备。内核中定义的struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。

1:class_create(...) 函数

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

owner:THIS_MODULE
name  : 名字

其中的函数__class_create(owner, name, &__key)为源代码

销毁函数:void class_destroy(struct class *cls)

void class_destroy(struct class *cls)
{
	if ((cls == NULL) || (IS_ERR(cls)))
		return;
 
	class_unregister(cls);
}

2:device_create(...) 函数

struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...)

功能:创建一个字符设备文件

参数:

      struct class *class  :类
      struct device *parent:NULL
     dev_t devt  :设备号
     void *drvdata  :null、
     const char *fmt  :名字

三:示例代码

hello_drv.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
 
static int major = 250;
static int minor=0;
static dev_t devno;
static struct class *cls;
static struct device *test_device;
 
static int hello_open (struct inode *inode, struct file *filep)
{
	printk("hello_open \n");
	return 0;
}
static struct file_operations hello_ops=
{
	.open = hello_open,
};
 
static int hello_init(void)
{
	int ret;	
	printk("hello_init \n");
 
 
	devno = MKDEV(major,minor);
	ret = register_chrdev(major,"hello",&hello_ops);
    //主设备号static int major = 250;次设备号static int minor=0;
 
	cls = class_create(THIS_MODULE, "myclass");
	if(IS_ERR(cls))
	{
		unregister_chrdev(major,"hello");
		return -EBUSY;
	}
	test_device = device_create(cls,NULL,devno,NULL,"hello");//mknod /dev/hello
	if(IS_ERR(test_device))
	{
		class_destroy(cls);
		unregister_chrdev(major,"hello");
		return -EBUSY;
	}	
	return 0;
}
static void hello_exit(void)
{
	device_destroy(cls,devno);
	class_destroy(cls);	
	unregister_chrdev(major,"hello");
	printk("hello_exit \n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

hello_app.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
 
 
main()
{
	int fd;
 
 
	fd = open("/dev/hello",O_RDWR);
	if(fd<0)
	{
		perror("open fail \n");
		return ;
	}
 
 
	close(fd);
}

makefile

ifneq  ($(KERNELRELEASE),)
obj-m:=hello.o
$(info "2nd")
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
	$(info "1st")
	make -C $(KDIR) M=$(PWD) modules
clean:
	rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
endif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值