Linux下简单字符驱动设备架构及其见解--read()、write()、ioctl、show()、store()等函数实现

1,861 阅读14分钟

先上代码

内核驱动的mudev.c文件。

该驱动文件通过ioctl控制实现两种模式:

  1. write写入的字符串,read时倒序输出,同时echo 写进的字符串,cat正序输出。
  2. write写入的字符串,read时正序输出,同时echo 写进的字符串,cat倒序输出。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/ioctl.h>

 
//设备名称
#define MYCDEV_NAME "mycdev_simple"
//设备类名称
#define MYCDEV_CLASS "mycdev_class"
//设备数量
#define CDEV_NUM 1
//缓冲区大小
#define BUFFSIZE 1024

#define DEV_FIFO_TYPE 'k'
 //控制模式正序
#define POS_ORDER _IO(DEV_FIFO_TYPE,1)
//控制模式倒叙
#define REV_ORDER _IO(DEV_FIFO_TYPE,2)
 
//设备类
static struct class *mycdev_class;
//所有设备的数组
static struct cdev cdevs[CDEV_NUM];
static struct device devices[CDEV_NUM];
 
//设备号
static dev_t dev_num;
//正序倒序控制,默认false,read正序,cat倒序
static bool order_flag = false;
static char tmp_out[BUFFSIZE] = {0};
static char tmp_cat_out[BUFFSIZE] = {0};
 
static int strLength(const char *s);
void reverse(char* s);
static int mycdev_open(struct inode *inode, struct file *file);
static ssize_t mycdev_read(struct file *file, char __user *buff, size_t len , loff_t *pos);
static ssize_t mycdev_write(struct file *, const char __user *buff, size_t len, loff_t *pos);
static int mycdev_release(struct inode *inode, struct file *file);
static long mycdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

static ssize_t mycdev_show(struct device *dev, struct device_attribute *attr, char *buf);
static ssize_t mycdev_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t len);  
 
 
 //定义一个名字为my_device_test的设备属性文件
static DEVICE_ATTR(mycdev_simple, S_IWUSR|S_IRUSR, mycdev_show, mycdev_store);
static struct file_operations op_f={
	.read=mycdev_read,
	.write=mycdev_write,
	.open=mycdev_open,
	.release=mycdev_release,
	.unlocked_ioctl=mycdev_ioctl
};
 
//模块初始化函数
static int __init simple_chardev_init(void)
{
		int i,retVal;
    /*设备号*/
    dev_t curr_devnum; 
 
    /*请求内核分配设备号*/
    retVal=alloc_chrdev_region(&dev_num,0,CDEV_NUM,MYCDEV_CLASS);
    if(retVal!=0)
    {
    	printk("alloc_chrdev_region error %d\n",retVal);
    	return retVal;
    }
 
    //为字符设备创建设备类,在/sys/class中可见
    mycdev_class=class_create(THIS_MODULE,MYCDEV_CLASS);
    
    /*创建字符设备结构并注册到内核*/
    for(i=0;i<CDEV_NUM;i++)
    {
    	/*初始化字符设备*/
    	cdev_init(&cdevs[i],&op_f);
    	cdevs[i].owner=THIS_MODULE;
 
    	/*设置设备号*/
    	curr_devnum=MKDEV(MAJOR(dev_num),MINOR(dev_num)+i);
    	/*添加设备到内核*/
    	retVal=cdev_add(&cdevs[i],curr_devnum,1);
        printk("cdev_add retVal %d\n",retVal);
 
    	/*创建设备节点*/
    	devices[i]=*(device_create(mycdev_class,NULL,curr_devnum,NULL,MYCDEV_NAME "%d",i));
        printk("device_create %p\n",&devices[i]);
        //在mycdev_simple设备目录下创建一个mycdev_simple属性文件
        device_create_file(&devices[i], &dev_attr_mycdev_simple);
    }
	return retVal;
}
 
static void __exit simple_chardev_exit(void)
{
    dev_t _devnum;
    int i;
 
    for(i=0;i<CDEV_NUM;i++)
    {
        _devnum=MKDEV(MAJOR(dev_num),MINOR(dev_num)+i);
        device_destroy(mycdev_class, _devnum);
        cdev_del(&(cdevs[i]));
    }
    class_destroy(mycdev_class);
 
    unregister_chrdev_region(dev_num,CDEV_NUM);
    printk("simple_chardev_exit\n");
    return;
}
 
 
/*打开设备*/
static int mycdev_open(struct inode *inode, struct file *file)
{
	/*打印一条消息到内核日志*/
	printk("call mycdev_open\n");
	return 0;
}
 
/*读操作*/
static ssize_t mycdev_read(struct file *file, char __user *buff, size_t len , loff_t *pos)
{
    int count = BUFFSIZE, retVal = 0;
	//struct test_dev *dev = file->private_data;
    //const char *s1="from mycdev_simple!";
	printk("call mycdev_read\n");
	//count=strLength(s1);
    if(order_flag == true)
    {
		reverse(tmp_out);
    	retVal = copy_to_user(buff, tmp_out, len);
    	printk("read buff:%s \n", tmp_out);

    } else{
    	retVal=copy_to_user(buff, tmp_out, len);
    	printk("read buff:%s \n", tmp_out);
    }

    if(retVal==-EFAULT)
    {
        printk("mycdev_read error\n");
        return -EFAULT;
    }
	return count+1;
}
 
/*写操作*/
static ssize_t mycdev_write(struct file *file, const char __user *buff, size_t len, loff_t *pos)
{
	//char *buffer;
    int retVal;
	//buffer=NULL;
 
    printk("call mycdev_write\n");
    //buffer = (char*)kmalloc(len, GFP_KERNEL);
    /*if(buffer==NULL)
    {
    	printk("kmalloc error\n");
    	return -EFAULT;
    }
 */
    memset(tmp_out, 0, 1024);
    retVal=copy_from_user(tmp_out, buff, len);
    if(retVal==-EFAULT)
    {
        printk("mycdev_write error\n");
        return -EFAULT;
    }
    printk("recv %s\n",tmp_out);
    return len;
}
 
/*关闭文件*/
static int mycdev_release(struct inode *inode, struct file *file)
{
	printk("call mycdev_release\n");
	return 0;
}

/*控制函数 */
static long mycdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	switch(cmd){
		case POS_ORDER:
			order_flag = false;
			break;
		case REV_ORDER:
			order_flag = true;
			break;
		default:
			order_flag = false;
			break;
	}
	return 0;
}


/*cat命令时,将会调用该函数*/
static ssize_t mycdev_show(struct device *dev, struct device_attribute *attr, char *buf)        
{
	if(order_flag == false){

		reverse(tmp_cat_out);
	}
	printk("cat: %s   \n",tmp_cat_out);
    return sprintf(buf, "%s\n", tmp_cat_out);
}


 /*echo命令时,将会调用该函数*/
static ssize_t mycdev_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t len)       
{
    sprintf(tmp_cat_out, "%s", buf);
    return len;
}

/*计算字符串长度*/
static int strLength(const char *s)
{
	int len;
	len=0;
	while((*s)!=0)
	{
		s++;
		len++;
	}
	return len;
}

/*字符串取反
*/
 void reverse(char* s)
{
    // 获取字符串长度
    int len = 0;
    char* p = s;
    while (*p != 0)
    {
        len++;
        p++;
    }
    
    // 交换 ...
    int i = 0;
    char c;
    while (i <= len / 2 - 1)
    {
        c = *(s + i);
        *(s + i) = *(s + len - 1 - i);
        *(s + len - 1 - i) = c;
        i++;
    }
    p = null;
}
 
module_init(simple_chardev_init);
module_exit(simple_chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxxx");
MODULE_DESCRIPTION("内核描述");

MakeFile文件

MakeFile文件解析详见(Linux驱动程序Makefile文件见解 - 掘金 (juejin.cn))

#  = 使用”=”进行赋值,变量的值是整个makefile中最后被指定的值。
# := 就表示直接赋值,赋予当前位置的值。
# ?= 表示如果该变量没有被赋值,则赋予等号后的值。
# += 表示将等号后面的值添加到前面的变量上

obj-m := mydev.o
#main-objs :=
#KERNELDIR ?= /lib/modules/$(shell uname -r)/build
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

.PHONY: install clean
all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules #ARCH=riscv CROSS_COMPILE=riscv64-buildroot-linux-gnu-

install:

clean:
	rm -rf *.o *~ core .depend *.mod .*.cmd *.ko *.mod.c .tmp_versions Module* modules*

用户端的驱动测试ioctl.h头文件

该部分的宏定义需要与内核驱动文件mydev.c的宏定义保持一致。

因为内核驱动文件mydev.c是写入到内核的,故不能引用该用户态的头文件。需要在驱动文件中重新定义。

#ifndef __IOCTL_H__
#define __IOCTL_H__

/*  定义设备类型,任意 char 型字符 */
#define DEV_FIFO_TYPE 'k'
/*
*   访问模式:
*   _IOC_NONE   不带参数操作,值为 0
*   _IOC_READ   带读参数,读数据操作,值为 1
*   _IOC_WRITE  带写参数,写数据操作,值为 2
*   _IOC_READ|_IOC_WRITE 带读写参数,读写数据操作
*/
/*
*   宏
*   _IO:       定义 不带参数的 ioctl 命令 
*   _IOR:      定义带 读参数的ioctl命令    (copy_to_user)
*   _IOW:      定义带 写参数的 ioctl 命令  (copy_from_user)
*   _IOWR:     定义带 读写参数的 ioctl 命令

/* 不带参数操作,常用于初始化 */
//#define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0)

//控制模式正序
#define POS_ORDER _IO(DEV_FIFO_TYPE,1)
//控制模式倒序
#define REV_ORDER _IO(DEV_FIFO_TYPE,2)

#endif

用户端的驱动测试mydev_app.c文件

带参数的main函数解析详见(C语言中 int main(int argc,char *argv[])的两个参数及其详细使用 )

下面测试文件需要输入 ./a.out rev (倒序)或者 ./a.out pos(正序)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include "ioctl.h"

int strLength(const char *s);

int main(int argc, char **argv) 
{
	int fd;
	int ret;
	char sysin[1024];
	char sysout[1024] = {0};
	char *filename = "/dev/mycdev_simple0";  //此处为驱动文件的路径地址
	
	if(argc < 2){
		printf("Usage:%s <rev or pos>\n",argv[0]);
		return -1;
	}
	
	/* 打开驱动文件 */
	fd = open(filename,O_RDWR);
	
	if(fd<0)
	{
		printf("Can't open file %s\r\n", filename);
		return fd;
	}
	
	printf("请输入字符串:");
	memset(sysin,0,1024);
	gets(sysin);

	write(fd, sysin, strLength(sysin)); // 写入输入的字符串
	if(strcmp(argv[1], "rev") == 0){
		ret = ioctl(fd,REV_ORDER); 
	}
	if(strcmp(argv[1], "pos") == 0){
		ret = ioctl(fd,POS_ORDER); 
	}
	
	read(fd,sysout,1024); //读数据

	printf("read = %s \n",sysout);

	/* 关闭设备 */
	ret = close(fd);
	if(ret < 0){
		printf("Can't close file %s\r\n", filename);
		return ret;
	}
	return 0;
}

int strLength(const char *s)
{
	int len;
	len=0;
	while((*s)!=0)
	{
		s++;
		len++;
	}
	return len;
}

测试程序与驱动文件是怎么联系的。

  • 通俗一点的理解

测试程序通过打开设备驱动文件,对文件进行打开、关闭、读写、控制等操作,然而设备驱动文件与驱动程序是根据主设备号进行关联的,因此驱动程序和用户测试程序产生了联系。而驱动程序的文件控制结构体定义了相应的操作调用了驱动文件的哪个操控函数。

static struct file_operations op_f={
	.read=mycdev_read,
	.write=mycdev_write,
	.open=mycdev_open,
	.release=mycdev_release,
	.unlocked_ioctl=mycdev_ioctl
};
  • 正经的解释

在Linux的世界里面一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。在这里我们字符设备为例,来看一下应用程序是如何和底层驱动程序关联起来的。必须知道的基础知识:

  • 1.在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。

  • 2.在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。

  • 3.在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。

struct cdev {
       struct kobject kobj;
       struct module *owner;
       const struct file_operations *ops;//接口函数集合
       struct list_head list;//内核链表
       dev_t dev;    //设备号
       unsigned int count;//次设备号个数
   };
  • 4.在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。

注意:

常常我们认为struct inode描述的是文件的静态信息,即这些信息很少会改变。而struct file描述的是动态信息,即在对文件的操作的时候,struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。

结构体关系如下图所示:

image.png

通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。

  • 1.当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个struct file结构体。

  • 2.根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个struct cdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。

  • 3.找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中。

  • 4.任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到strut file,然后再由struct file找到操作字符设备的函数接口了。

字符驱动设备mydev.c相关函数分析

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:
  初始化cdev结构体
参数:
  @cdev cdev结构体地址
  @fops 操作字符设备的函数接口地址
返回值:
  无
/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:
  添加一个字符设备到操作系统
参数:
  @p cdev结构体地址
  @dev 设备号
  @count 次设备号个数
返回值:
  成功返回0,失败返回错误码(负数)
/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */
#define class_create(owner, name)    \
({            \
  static struct lock_class_key __key;  \
  __class_create(owner, name, &__key);  \
})

参数:
  @owner  THIS_MODULE
  @name   类名字
返回值
  可以定义一个struct class的指针变量cls接受返回值,然后通过IS_ERR(cls)判断
  是否失败,如果成功这个宏返回0,失败返回非9值(可以通过PTR_ERR(cls)来获得
  失败返回的错误码)

read接口的实现

用户空间-->内核空间

字符设备的write接口定义如下:

ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
参数:
  filp:待操作的设备文件file结构体指针
  buf:待写入所读取数据的用户空间缓冲区指针
  count:待读取数据字节数
  f_pos:待读取数据文件位置,写入完成后根据实际写入字节数重新定位
返回:
  成功实际写入的字节数,失败返回负值

如果该操作为空,将使得write系统调用返回负EINVAL失败,正常返回实际写入的字节数。

用户空间向内核空间拷贝数据需要使用copy_from_user函数,该函数定义在arch/arm/include/asm/uaccess.h中。

static inline int copy_from_user(void *to, const void __user volatile *from,unsigned long n)
参数:
  to:目标地址(内核空间)
  from:源地址(用户空间)
  n:将要拷贝数据的字节数
返回:
  成功返回0,失败返回没有拷贝成功的数据字节数

还可以使用get_user宏:

int get_user(data, ptr);
参数:
  data:可以是字节、半字、字、双字类型的内核变量
  ptr:用户空间内存指针
返回:
  成功返回0,失败返回非0

write接口实现

内核空间-->用户空间

字符设备的read接口定义如下:

ssize_t (*read)(struct file *filp, char __user *buf, size_t  count, lofft *f_pos);
参数:
  filp: 待操作的设备文件file结构体指针
  buf:  待写入所读取数据的用户空间缓冲区指针
  count:待读取数据字节数
  f_pos:待读取数据文件位置,读取完成后根据实际读取字节数重新定位
  __user :是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址。

返回值:
  成功实际读取的字节数,失败返回负值

注意:如果该操作为空,将使得read系统调用返回负EINVAL失败,正常返回实际读取的字节数。

用户空间从内核空间读取数据需要使用copy_to_user函数:

 static inline int copy_to_user(void __user volatile *to, const void *from,unsigned long n)
参数:
  to:目标地址(用户空间)
  from:源地址(内核空间)
  n:将要拷贝数据的字节数
返回:
  成功返回0,失败返回没有拷贝成功的数据字节数

还可以使用put_user宏:

int put_user(data, prt)
参数:
  data:可以是字节、半字、字、双字类型的内核变量
  ptr:用户空间内存指针
返回:
  成功返回0, 失败返回非0

unlocked_ioctl接口实现

(1)为什么要实现xxx_ioctl ?

前面我们在驱动中已经实现了读写接口,通过这些接口我们可以完成对设备的读写。但是很多时候我们的应用层工程师除了要对设备进行读写数据之外,还希望可以对设备进行控制。例如:针对串口设备,驱动层除了需要提供对串口的读写之外,还需提供对串口波特率、奇偶校验位、终止位的设置,这些配置信息需要从应用层传递一些基本数据,仅仅是数据类型不同。

通过xxx_ioctl函数接口,可以提供对设备的控制能力,增加驱动程序的灵活性。

(2)如何实现xxx_ioctl函数接口?

增加xxx_ioctl函数接口,应用层可以通过ioctl系统调用,根据不同的命令来操作dev_fifo。

kernel 2.6.35 及之前的版本中struct file_operations 一共有3个ioctl :ioctl,unlocked_ioctl和compat_ioctl 现在只有unlocked_ioctl和compat_ioctl 了

在kernel 2.6.36 中已经完全删除了struct file_operations 中的ioctl 函数指针,取而代之的是unlocked_ioctl 。

· 2.6.36 之前的内核

long (ioctl) (struct inode node ,struct file* filp, unsigned int cmd,unsigned long arg)

· 2.6.36之后的内核

long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)

参数cmd: 通过应用函数ioctl传递下来的命令

先来看看应用层的ioctl和驱动层的xxx_ioctl对应关系:

image.png <1>应用层ioctl参数分析

int ioctl(int fd, int cmd, ...);
参数:
@fd:打开设备文件的时候获得文件描述符 
@ cmd:第二个参数:给驱动层传递的命令,需要注意的时候,驱动层的命令和应用层的命令一定要统一
@第三个参数: "..."在C语言中,很多时候都被理解成可变参数。
返回值
       成功:0
       失败:-1,同时设置errno

小贴士:

当我们通过ioctl调用驱动层xxx_ioctl的时候,有三种情况可供选择:

1: 不传递数据给xxx_ioctl 
2: 传递数据给xxx_ioctl,希望它最终能把数据写入设备(例如:设置串口的波特率)
3: 调用xxxx_ioctl希望获取设备的硬件参数(例如:获取当前串口设备的波特率)
这三种情况中,有些时候需要传递数据,有些时候不需要传递数据。在C语言中,是
无法实现函数重载的。那怎么办?用"..."来欺骗编译器了,"..."本来的意思是传
递多参数。在这里的意思是带一个参数还是不带参数。

参数可以传递整型值,也可以传递某块内存的地址,内核接口函数必须根据实际情况
提取对应的信息。

<2>驱动层xxx_ioctl参数分析

long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);
参数:
@file:   vfs层为打开字符设备文件的进程创建的结构体,用于存放文件的动态信息 
@ cmd: 用户空间传递的命令,可以根据不同的命令做不同的事情
@第三个参数: 用户空间的数据,主要这个数据可能是一个地址值(用户空间传递的是一个地址),
也可能是一个数值,也可能没值
返回值
       成功:0
       失败:带错误码的负值

<3>如何确定cmd 的值。

通俗一点理解,利用设备类型,序列号,方向,数据尺寸四个关键字,可以确定一个32位的数字,然后跟读宏定义用这个数字作为参数cmd传入,还能很好的防止冲突。

该值主要用于区分命令的类型,虽然我只需要传递任意一个整型值即可,但是我们尽量按照内核规范要求,充分利用这32bite的空间,如果大家都没有规矩,又如何能成方圆?

现在我就来看看,在Linux 内核中这个cmd是如何设计的吧! 该值主要用于区分命令的类型,虽然我只需要传递任意一个整型值即可,但是我们尽量按照内核规范要求,充分利用这32bite的空间,如果大家都没有规矩,又如何能成方圆?

现在我就来看看,在Linux 内核中这个cmd是如何设计的吧!

image.png 具体含义如下:

  • 设备类型类型或叫幻数,代表一类设备,一般用一个字母或者1个8bit的数字。
  • 序列号代表这个设备的第几个命令。
  • 方向表示是由内核空间到用户空间,或是用户空间到内核空间,如:只读,只写,读写,其他
  • 数据尺寸表示需要读写的参数大小

通过Linux 系统给我们提供的宏,我们在设计命令的时候,只需要指定设备类型、命令序号,数据类型三个字段就可以了。

/* 定义设备类型,任意 char 型字符 / #define DEV_FIFO_TYPE 'k' /

访问模式:

  • _IOC_NONE 不带参数操作,值为 0
  • _IOC_READ 带读参数,读数据操作,值为 1
  • _IOC_WRITE 带写参数,写数据操作,值为 2
  • _IOC_READ|_IOC_WRITE 带读写参数,读写数据操作

  • _IO: 定义 不带参数的 ioctl 命令
  • _IOR: 定义带 读参数的ioctl命令 (copy_to_user)
  • _IOW: 定义带 写参数的 ioctl 命令 (copy_from_user)
  • _IOWR: 定义带 读写参数的 ioctl 命令

//控制模式正序 #define POS_ORDER _IO(DEV\_FIFO_TYPE,1)

//控制模式倒序 #define REV_ORDER _IO(DEV_FIFO_TYPE,2)

<4> 如何检查命令?

可以通过宏_IOC_TYPE(nr)来判断应用程序传下来的命令type是否正确;

可以通过宏_IOC_DIR(nr)来得到命令是读还是写,然后再通过宏access_ok(type,addr,size)来判断用户层传递的内存地址是否合法。

使用方法如下:

  if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
    pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
    return-ENOTTY;
  }
  if(_IOC_DIR(cmd)&_IOC_READ)
    ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
  else if( _IOC_DIR(cmd)&_IOC_WRITE )
    ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
  if(ret){
    pr_err("bad   access %ld.\n",ret);
    return-EFAULT;
  }

创建设备节点的方法和DEVICE_ATTR

使用DEVICE_ATTR宏,可以定义一个struct device_attribute设备属性,使用函数device_create_file便可以在设备目录下创建具有show和store方法的节点。能方便的进行调试。 DEVICE_ATTR的定义:

#define DEVICE_ATTR(_name, _mode, _show, _store) 
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

而__ATTR宏的定义在include/linux/sysfs.h文件中,如下:

#define __ATTR(_name, _mode, _show, _store) {               
    attr = {.name = __stringify(_name),             
    .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },        
    .show    = _show,                      
    .store    = _store,                       
}

那么struct device_attribute的定义又是怎么样的呢?该结构体的定义在include /linux/device.h,其定义如下:

struct  device_attribute {
    struct attribute    attr;
    ssize_t (*show)(struct device *dev,  struct device_attribute *attr,char `*buf);
    ssize_t (*store)(struct device *dev,  struct device_attribute *attr,
    const char *buf, size_t count);
};

而其中的struct attribute的定义在include/linux/device.h中,如下:

struct attribute {
    const char        *name;
    umode_t            mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    bool            ignore_lockdep:1;
    struct lock_class_key    *key;
    struct lock_class_key    skey;
#endif
};

总结一下: DEVICE_ATTR(_name, _mode, _show, _store) 等价于:

struct device_attribute dev_attr_##_name = {
    .attr = {.name = __stringify(_name),
             .mode = VERIFY_OCTAL_PERMISSIONS(_mode)},
    .show = _show,
    .store = _store,
}

其中:.show和.store的类型定义如下:

show函数的详细描述:cat时会调用show函数

ssize_t (*show)(struct device *dev, struct device_attribute *attr,char *buf);
  •  入参buf是需要我们填充的string即我们cat属性节点时要显示的内容;
  • 函数的返回值是我们填充buf的长度,且长度应当小于一个页面的大小(4096字节);
  • 其他参数一般不用关心。

例如:

//cat命令时,将会调用该函数
static ssize_t show_my_device(struct device *dev,struct device_attribute *attr, char *buf) 
{
    return sprintf(buf, "%s\n", mybuf);
}

store函数的详细描述:

ssize_t (*store)(struct device *dev, struct device_attribute *attr,const char *buf, size_t count);
  •  入参buf是用户传入的字符串,即echo到属性节点的内容;
  • 入参count是buf中字符串的长度。
  • 函数的返回值通常返回count即可。
  • 其他参数一般不用关心。

例如:

//echo命令时,将会调用该函数
static ssize_t store_my_device(struct device *dev,struct device_attribute *attr,const char *buf, size_t count) 
{
    sprintf(mybuf, "%s", buf);
    return count;
}

定义好了.show和.store函数,那么就可以使用DEVICE_ATTR了。定义一个名字为mycdev_simple的设备属性文件

static DEVICE_ATTR(mycdev_simple, S_IWUSR|S_IRUSR, mycdev_show, mycdev_store);

device attribute添加到sysfs中

//在mycdev_simple设备目录下创建一个mycdev_simple属性文件
device_create_file(&devices[i], &dev_attr_mycdev_simple);

&devices[i] :设备节点

&dev_attr_mycdev_simple: dev_attr为固定的,后面跟设备名称

测试步骤:

1. make编译

在mydev.c目录下执行make

2. 加载.ko文件

在mydev.c目录下执行sudo insmod mydev.ko

3.gcc编译测试文件

在mydev_app.c目录下执行gcc -o app mydev_app.c

4 运行测试文件

main()函数带有参数rev或者pos

.app rev.app pos

详细命令参考:linux字符设备驱动实验_Lazy_Goat的博客-CSDN博客