linux驱动之字符设备

linux驱动之字符设备

linux驱动设备分类

linux驱动分为了三种驱动:

  • 字符设备:
    字符设备和应用程序之间是以字节进行进行数据交换的。在进行数据交换的时候数据是以一定顺序进行传输的,传输是实时的,过程中不存在数据缓存。绝大部分的设备都属于字符设备,比如led,key等驱动。
  • 块设备:
    块设备和字符设备相对应,块设备设备和应用之间的数据交互是以块的方式进行传输,块设备在传输数据过程中是存在数据缓存的,大容量存储设备一般都是块设备。
  • 网络设备:
    网络设备用于无线/有线网卡,网络设备没有设备文件。

设备号

主次设备号

创建设备的时候需要设备号,Linux中的设备号分为主设备号和次设备号。设备的主次设备号可以在/dev目录下查看,进入该目录执行ls -la,输出的每一行第一个字母为c的就是字符设备。

在这里插入图片描述

其中的7, 64分别是设备的主设备号和次设备号,vcsu是设备名字。
一个驱动的主设备号可以在/proc/devices中查看,执行cat /proc/devices,输出中"Character devices:"对应的是字符设备,"Block devices:"对应的是块设备,设备名字前面的数字就是主驱动号。

在这里插入图片描述

通常情况下,主设备号标识对应的驱动程序,主要用来区分不同种类的驱动,对于常用的一些驱动设备,Linux有约定俗成的主设备号,比如终端类的主设备号是4。次是设备号用来区分该驱动下的多个设备,一个驱动下面可以有多个设备。Linux 内核允许多个驱动共享一个主设备号,但更多的设备都遵循一个驱动对一个主设备号的原则。

设备号表示方式

设备号的类型为dev_t,该类型在<linux/types.h>中定义。
在内核版本2.6.0中,dev_t是32位,其中高12位用于表示主设备号,其余20位用于表示次设备号。
在这里插入图片描述
对于主次设备号,可以使用<linux/kdev_t.h>的宏来获取dev_t类型的主次设备号,也可以用其中的宏来生产一个dev_t

MAJOR(dev_t dev); //用于获取设备的主设备号
MINOR(dev_t dev); //用于获取设备的次设备号
MKDEV(int major, int minor); //根据所给的主次设备号生成dev_t

字符设备主次设备号分配和释放

内核中要创建一个设备,首先需要分配设备号,分配的设备号可以手动分配,也可以自动分配。
用于分配设备号的函数在<linux/fs.h>中。

静态分配
int register_chrdev_region(dev_t first, unsigned int count, char* name);
  • first:
    要分配的设备编号的范围起始值。
  • count:
    是要分配的设备号数量。
  • name:
    和该设备关联的名字。
  • 返回值:
    设备分配成功时,该函数返回0,失败时返回负的错误码。

调用示例:

int major,minor,dev_count;
dev_t dev;
char * name = "dev";
major = 500; 
minor = 0;
dev_count = 10;

dev = MKDEV(major,minor);
if(register_chrdev_region(dev, dev_count, name) < 0){
    printk(KERN_WARNING "%s: can't get major %d\n", name, major);
}

静态分配主设备号的时候要注意,主设备号不能和已存在于内核中的设备相同,相同的话,该函数调用就会失败。设备号分配成功后,就可以在/proc/devices中查看到分配的主设备号了。

动态分配
int alloc_chrdev_region(dev_t* dev, unsigned int firstminor, unsigned int count, char* name);
  • dev
    dev用于保存输出的设备号。
  • firstminor
    该参数是次设备号的分配范围起始值。
  • count
    该参数是要分配次设备号数量。
  • name
    和该设备关联的名字。
  • 返回值
    设备分配成功时,该函数返回0,失败时返回负的错误码。
    调用示例:
int major,minor,dev_count,err;
dev_t dev;
char * name = "dev";
minor = 0;
dev_count = 10;

err = alloc_chrdev_region(&dev, minor, dev_count, name);
if(err < 0){
    printk(KERN_WARNING "%s: can't get major\n", name);
    return err;
}

major = MAJOR(dev);

动态创建设备号后,可以在/proc/devices中查看到该设备动态注册的主设备号。可以通过指令cat /proc/devices | grep 设备名字来查看。

释放设备号

在加载驱动时要分配设备号,分配的设备号在卸载设备时需要归还给内核。

    void unregister_chrdev_region(dev_t dev, unsigned int count);
  • dev
    该参数为需要释放的设备号
  • count
    该参数为需要释放的设备数量
    调用示例:
    unregister_chrdev_region(MKDEV(major, minor), dev_count);

和字符设备相关的重要数据结构以及函数

cdev

//内核版本:5.4.0-1071-raspi
struct cdev {
	struct kobject kobj; 
	struct module *owner;//所属模块
	const struct file_operations *ops;//文件操作
	struct list_head list;
	dev_t dev;//设备号
	unsigned int count;
} __randomize_layout;

cdev表示了一个字符设备,其中最重要的是成员是ops,我们字符设备所能实现的功能都需要依靠这个这个。用户在调用相关函数时最终会通过这个ops指向我们所实现的各种函数中。其中的owner表示了模块的所属,一般都初始化为宏THIS_MODULE
初始化一个字符设备和注销字符设备时会用到如下几种函数:

//初始化一个cdev结构体,并和file_operations绑定
void cdev_init(struct cdev *, const struct file_operations *);
//分配一个cdev结构体
struct cdev *cdev_alloc(void);

void cdev_put(struct cdev *p);
//添加一个字符设备到内核,在添加之前,第二个参数设备号需要已经被注册过,第三个参数是分配的范围,以所给的设备号,初始几个设备。
int cdev_add(struct cdev *, dev_t, unsigned);
//cdev_del必须和cdev_add配合使用,cdev_del用于删除一个字符设备。在卸载驱动时要删除已经添加了的字符设备。
void cdev_del(struct cdev *);

一般初始化一个cdev的方式。

struct file_operations dev_ops{
	//省略...
}
struct cdev cdev;
dev_t devnum;//需要提前注册

void dev_init(){
	int err;
	&cdev= cdev_alloc();
	cdev_init(&cdev,&dev_ops);
	cdev->owner = THIS_MODULE;
	err = cdev_add(&dev->cdev, devnum, 1);
}

struct file_operations

//内核版本:5.4.0-1071-raspi
struct file_operations {
	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 (*iopoll)(struct kiocb *kiocb, bool spin);
	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 *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	//。。。省略
};

该结构用来表明设备的文件操作,用户应用的文件操作(open,write,read,close等操作)最终会通过调用该结构中的函数指针来调用我们自己定义相关操作函数。设备不支持的调用可以设置为NULL;
该结构中的struct module *owner;是指向拥有该结构体模块的指针,一般该成员会被初始化为<linux/module.h>中的宏THIS_MODULE
其中常用的操作有open,write,read,llseek,unlocked_ioctl等。

open
int (*open) (struct inode *, struct file *);

open函数中的参数inode表示一个具体的文件节点,其中对我们有用的参数有i_cdev指针和i_rdev,i_rdev表示实际设备编号,当inode指向一个字符设备时,i_cdev就是这个字符设备。其中参数file是文件描述符,用于表示一个文件的信息。可以通过file中的f_flags判断当前打开的方式。

struct xxx_dev{
	struct cdev cdev;
	char *data;
};
static int xxx_open(struct inode *inode, struct file *filp){
	struct xxx_dev *dev;
	/*
	通过container_of宏获取dev指针,该宏可以通过某个成员来获取首地址,这里通过cdev获取相对于i_cdev时的xxx_dev首地址。
	*/
	dev = container_of(inode->i_cdev, struct xxx_dev, cdev);

	//将dev指针存放到filp的private_data中,这样以便于后续我们在其他函数中获取到该结构体。
	filp->private_data = dev;

	//...
}
write
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

write函数中的file和open中的file是一样的,该函数的第二个参数表示用户空间的地址,第三个参数是要写入的数据长度,第四个参数是当前的位置,这个位置相当于world文档中的光标,这个光标告诉我们当前的位置在哪里。用户将数据写入内核会涉及到用户空间数据和内核空间数据的交互,从用户空间将数据拷贝到内核空间会用到copy_from_user,与之相对的是copy_to_user,内核地址和用户空间地址是不一样的,所以同样的地址所对应的东西是不一样的,该函数可以实现将用户空间的地址的数据拷贝到内核空间中。

//这两个函数会检查用户空间地址是否有效,无效时会返回-EFAULT,若不能完全拷贝则会返回剩下未拷贝的长度,正常执行完则返回0。
copy_from_user(void __user *to, const void *from, unsigned long count);
copy_to_user(void __user *to, const void *from, unsigned long count);

//这两个函数不会进行用户空间合法性检查,如果不能保证地址的合法性,这可能会引起是系统崩溃。
__copy_from_user(void __user *to, const void *from, unsigned long count);
__copy_to_user(void __user *to, const void *from, unsigned long count);

//可以手动调用下面的函数检查地址是否合法
access_ok(type, addr, size);
//其中type定义
#define VERIFY_READ 0
#define VERIFY_WRITE 1

//对于简单的一些数据类型(int, char, long等),可以使用get_user和put_user
get_user(x,addr);
put_user(x,addr);

//与之相对应的是不做检查版本的
__get_user(x,addr);
__put_user(x,addr);

一般的write函数定义如下:

static ssize_t hello_write(struct file *file, const char __user *buf,
                           size_t count, loff_t *f_pos) {
	//获取我们之前存放的指针。
	int err=0;				
	struct xxx_dev *dev = file->private_data;

	//...

	//拷贝数据
	err=copy_from_user(dev->date+*f_pos, buf, count);
	
	//...

	//更新f_pos
	*f_ops+=count;

	return err;
}

read

一般read函数定义如下:

static ssize_t  xxx_write (struct file *file, const char __user *buf, size_t count, loff_t *f_ops){
	int err=0;
	struct xxx_dev *dev = file->private_data;

	//...

	//将数据从内核空间拷贝到用户空间
	err=copy_to_user(buf,dev->date+*f_ops,count);

	//...
	*f_ops+=count;

	return err;
}

llseek

loff_t llseek(struct file *filp, loff_t off, int whence) 

在正常的读写过程中f_ops会一直不断的发生变化,这时候用户如果想要调整f_ops的位置可以通过llsek来进行调整,该函数返回非负值为当前f_ops位置,负值表明函数调用失败。
其中参数off表示文件光标移动数值,可以是正值,也可以是负值。whence表示光标移动参考位置,有三种参考位置(当前位置,文件开头,文件末尾)。
一般llseek定义如下:

static loff_t xxx_llseek(struct file *filp, loff_t off, int whence) {
  struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
  loff_t newpos;
  switch (whence) {
    case SEEK_SET://文件开头位置
      newpos = off;
      break;
    case SEEK_CUR://文件当前位置
      newpos = filp->f_pos + off;
      break;
    case SEEK_END://文件末尾
      newpos = dev->max_size + off;
      break;
    default:
      return -EINVAL;
  }
  if (newpos < 0) return -EINVAL;
  filp->f_pos = newpos;
  return newpos;
}

ioctl

对于驱动,常见的操作只能满足部分需求,有时候一些其他操作只能痛过ioctl函数来进行操作,对于自定义操作,linux驱动提供了ioctl来支持自定义命令。
旧版内核的file_operations中存在ioctl,unlocked_ioctl,compat_ioctl。在2.6.36过后内核中就只有后面两种了。如果实现的驱动是64位的,则必须实现compat_ioctl,当内核是64位的时候,用户空间如果有32位的应用调用驱动,则会调用compat_ioctl,如果用户空间是64位的应用则调用unlocked_ioctl,如果内核和用户都是32位的则调用unlocked_ioctl,如果用户空间是32位,调用64位的驱动时没有实现compat_ioctl,则会返回错误:Not a typewriter。
对于自定义的命令,内核提供一组宏来辅助生成命令:

  • _IO(type,nr,size)
  • _IOR(type,nr,size)
  • _IOW(type,nr,size)
  • _IOWR(type,nr,size)

cmd的大小一般为32位,其中分为4个域。其中设备类型(魔数)用于区分内核中不同的驱动ioctl,设备类型占8位。序列号占8位,序列号用于区分命令序号。数据大小占14/13位,还有一个用于表述方向的占两位。上面的四个宏,带W的表示可以写,带R的表示可以读取,什么都不带的则表明该命令不涉及数据传输。其中type所代表的魔数必须要独一无二。
一般的ioctl定义如下:

#define IO_READ_NOW_SIZE _IOR('h', 'a', size_t *)
#define IO_SET_NOW_SIZE _IOW('h', 'b', size_t *)

static long xxx_unlocked_ioctl(struct file *filp, unsigned int cmd,
                                 unsigned long date) {
  int ret = 0;
  size_t set_size = 0;
  struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
  switch (cmd) {
    case IO_READ_NOW_SIZE: {
		//这里传递数据是用户空间的指针,调用put_user将数据写入用户空间
      if (put_user(dev->max_size, (size_t *)date)) {
        ret = -EFAULT;
        goto out;
      };
    } break;
    case IO_SET_NOW_SIZE: {
		//这里传递的是用户空间的指针,调用get_user获取用户空间的数据
      if (get_user(set_size, (size_t *)date)) {
        ret = -EFAULT;
        goto out;
      }
		dev->max_size = set_size;
    	filp->f_pos = 0;
    } break;
    default:
      printk(KERN_WARNING "not find now cmd");
      break;
  }
out:
  return ret;
}

其他函数

对于一般的字符驱动来说,都会实现上面几种方法,其他的一些方法则对于不同设备来说有不同的支持,需要的时候可以自行查阅文档来实现相关需求。

字符设备创建中的必要步骤

  • 首先需要注册设备号,只有注册了设备号才能向内核添加cdev设备。
  • 定义并实现文件相关操作,一般该变量都会命名为fops。
  • 初始化cdev,绑定fops并且添加到内核。
  • 在驱动卸载时一定要删除cdev和注销设备号,以及释放自己申请的内存。

用于编译的makefile

ifneq ($(KERNELRELEASE),)
# driver为驱动名字,对应于driver.c
	obj-m := driver.o 
else 
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)
endif


default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINC=$(PWD)/../include modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

装载驱动

编译好驱动后,可以insmod来装载驱动。

# 装载驱动
sudo insmod ./driver.ko

# 卸载驱动
sudo rmmod ./driver.ko

装载完驱动后,这个时候离访问自己的驱动还差一步,虽然装载了驱动,但我们并没有一个节点来访问驱动,这个时候可以调用mknod命令在dev目录下创建一个节点用于访问我们的驱动。

sudo mknod /dev/driver c <这个地方填主设备号> <这个地方填次设备号>
# mknod命令中的c表示创建的节点为字符设备,这个命令还能用于创建其他类型的设备,这个命令会在dev下面创建一个节点。

sudo chmod 777 /dev/driver 
# 更改文件权限,以便于普通用户正常访问。

# 在卸载驱动前需要删除节点
sudo rm -rf /dev/driver

对于设备的装载和卸载,我们可以自己写一个脚本来自动执行,这样可以避免每次麻烦的操作。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值