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.cobj-m := driver.o
else KERNELDIR ?= /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)
endifdefault:$(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINC=$(PWD)/../include modulesclean: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

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

linux驱动之字符设备相关推荐

  1. 【linux驱动之字符设备驱动基础】

    linux驱动之字符设备驱动基础 文章目录 linux驱动之字符设备驱动基础 前言 一.开启驱动学习之路 二.驱动预备知识 三.什么是驱动? 3.1 驱动概念 3.2 linux 体系架构 3.3 模 ...

  2. Linux驱动之字符设备驱动

    系列文章目录 第一章 Linux入门之驱动框架 第二章 Linux驱动之字符设备驱动 文章目录 系列文章目录 前言 一.认识字符设备驱动 1.基本概念 2.基本概念 二.字符设备旧框架 1.注册和注销 ...

  3. linux用户空间flash驱动,全面掌握Linux驱动框架——字符设备驱动、I2C驱动、总线设备驱动、NAND FLASH驱动...

    原标题:全面掌握Linux驱动框架--字符设备驱动.I2C驱动.总线设备驱动.NAND FLASH驱动 字符设备驱动 哈~ 这几天都在发图,通过这种方式,我们希望能帮大家梳理学过的知识,全局的掌握Li ...

  4. 嵌入式linux驱动之———字符设备驱动(一)

    一.简介: 在Linux内核驱动中,字符设备是最基本的设备驱动.字符设备是能够像字节流(比如文件)一样被访问的设备,就是说对它的读写是以子为单位的.比如串口在进行收发数据时就是一个字节一个字节进行的. ...

  5. 【Linux驱动】字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 1.字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面 ...

  6. Linux驱动之 字符设备 ioctl接口使用

    字符设备ioctl接口使用: Linux驱动编写除了对设备进行读写数据之外,通常还希望可以对设备进行控制. 从应用层传递一些命令参数,并在驱动层实现相应设备操作,这时候就用到了 ioctl函数: 应用 ...

  7. linux驱动学习——字符设备号

    字符设备号本质就是一个32位的无符号整型值.高12位为主设备号:低20位为次设备号. 查看设备号 cat /proc/devices 4.1.构造设备号 源码路径: include/linux/kde ...

  8. Linux驱动笔记-字符设备,块设备,网络设备

      在Linux设备驱动开发中,粗略的将设备分为三种类型:字符设备,块设备和网络设备. 1.字符设备:指能够像字节流串行顺序依次进行访问的设备,对它的读写是以字节为单位.字符设备的上层没有磁盘文件系统 ...

  9. linux 驱动开发 --- 字符设备与混杂设备区别

    2019独角兽企业重金招聘Python工程师标准>>> 一.主设备号的生成方式不同 1.所有的混杂设备都被分配一个主设备号10,次设备号系统自动生成 2.字符设备,的主设备号,开发驱 ...

最新文章

  1. mysql+字符串后8位_字符的一字节8位问题
  2. python爬虫有什么用处-Python爬虫的作用与地位(附爬虫技术路线图)
  3. ethercat主站控制软件TwinCAT的安装
  4. Scala中那些令人头痛的符号
  5. 十分钟计算机说课稿,足球十分钟说课稿范文(精选3篇)
  6. springmvc使用requestmapping无法访问控制类_研究人员称人类使用的新烟碱类杀虫剂让蜜蜂无法入睡...
  7. 30天提升技术人的写作力-第二十三天
  8. Web Hacking 101 中文版 十二、开放重定向漏洞
  9. php 快速排序函数,PHP实现快速排序算法的三种方法
  10. bzoj 2987: Earthquake(类欧几里得)
  11. Springboot -- 由于jar版本不匹配遇到的问题
  12. php数组由哪三部分构成,数据结构研究的主要内容有哪三部分
  13. ResultSet大数据量导致内存溢出
  14. Android之本地数据存储(SQLite数据库)
  15. pandas统计个数
  16. 设置电脑的背景颜色为保护色
  17. 使用32驱动1602液晶屏
  18. C语言编程>第十七周 ⑤ 请补充fun函数,该函数的功能是:用来求出数组的最小元素在数组中的下标并存放在k所指的存储单元。
  19. 【Flocking算法】海王的鱼塘是怎样炼成的
  20. python 打印99乘法口诀

热门文章

  1. Sporadic IOException: Failed to persist config
  2. iPad越狱搭建java环境_win7+virtualbox安装Mac os搭建完美越狱环境
  3. python画图怎样写文字_python画图系列之个性化显示x轴区段文字的实例
  4. SqlServer 并发事务:死锁跟踪(一)简单测试
  5. golang学习笔记(基础篇)
  6. 一个完整的直播App功能分析
  7. 单机安装 hadoop 环境(Hadoop伪分布式安装)
  8. 处理Oracle数据库服务安全漏洞的几种方法
  9. Window Git配置
  10. 如何在mac上播放iphone音频