LV10-02-字符设备驱动-02-基本驱动函数实现

本文主要是字符设备驱动基本函数的实现的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

前边那些笔记其实已经可以完成一个最基本的字符设备驱动框架了,但是我们还缺少自己编写的驱动函数,这一部分主要介绍几个重要驱动函数的实现。

一、两个常用结构体

1. struct inode

该结构体是内核中记录文件元信息的结构体,它定义在linux源码的include/linux/fs.h中,这里只列我们可能会关注的成员:

1
2
3
4
5
6
7
8
struct inode
{
// ... ...
dev_t i_rdev; /* 设备号 */
// ... ...
struct cdev *i_cdev; /* 如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象 */
//... ...
}
  • 内核中每个struct inode对象对应着一个实际文件,一对一。
  • open()打开一个文件时,如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建。
  • 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)。

2. struct file

该结构体是读写文件内容过程中用到的一些控制性数据组合而成的对象,可以称之为文件操作引擎或者叫文件操控器,它定义在linux源码的include/linux/fs.h中,这里只列我们可能会关注的成员:

1
2
3
4
5
6
7
8
9
10
11
12
struct file
{
//... ...
mode_t f_mode; /* 不同用户的操作权限,驱动一般不用 */
loff_t f_pos; /* position 数据位置指示器,需要控制数据开始读写位置的设备有用 */
unsigned int f_flags; /* open时的第二个参数flags存放在此,驱动中常用 */
struct file_operations *f_op; /* open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用 */
void *private_data; /* 本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据 */
struct dentry *f_dentry; /* 驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode */
int refcnt; /* 引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象 */
//... ...
};
  • open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作。
  • open同一个文件多次,每次open都会创建一个该类型的对象。
  • 文件描述符数组中存放的地址指向该类型的对象。
  • 每个文件描述符都对应一个struct file对象的地址。

该结构体存在于设备的操作函数集的每个函数参数中,我们会经常使用该结构体的private_data成员来存放我们的设备的私有数据,以完成参数的传递。

二、几个函数

1. copy_to_user()

1.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep copy_to_user -r -n ~/5linux/linux-3.14/include

经过查找,会发现该函数定义在linux源码的include/asm-generic/uaccess.h目录中,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
/* 需包含的头文件 */
#include <asm/uaccess.h>

/* 函数声明 */
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_fault();
if (access_ok(VERIFY_WRITE, to, n))
return __copy_to_user(to, from, n);
else
return n;
}

【函数说明】该函数用于内核空间的数据复制到用户空间。

【函数参数】

  • tovoid __user *类型,目标地址(用户空间)。
  • fromvoid *类型,源地址(内核空间)。
  • nunsigned long类型,要拷贝数据的字节数。

【返回值】long类型,如果复制成功,返回值为 0,如果复制失败则返回负数 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
/* 需要包含的头文件 */
#include <asm/uaccess.h>
/* 至少应该有的语句 */
char __user *pbuf /* 用户空间数据 */
int second; /* 内核空间数据 */
ret = copy_to_user(pbuf, &second, sizeof(int));
if (ret)
{
printk("copy to user failed\n");
return -1;
}

【注意事项】

(1)该函数定义在linux源码的include/asm-generic/uaccess.h中,但是在包含头文件的时候注意使用asm/uaccess.h,这个吧,我也不清楚是为什么,这样编译的时候使用make ARCH=arm不报错,但是直接编译x86平台的模块的话就会报错,可能是内核版本问题,我们是要在ARM平台上跑驱动,所以就按照asm/uaccess.h就可以啦。

1.2 使用实例

暂无

2. copy_from_user()

2.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep copy_from_user -r -n ~/5linux/linux-3.14/include

经过查找,会发现该函数定义在linux源码的include/asm-generic/uaccess.h目录中,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
/* 需包含的头文件 */
#include <asm/uaccess.h>

/* 函数声明 */
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
{
might_fault();
if (access_ok(VERIFY_READ, from, n))
return __copy_from_user(to, from, n);
else
return n;
}

【函数说明】该函数用于将用户空间的数据拷贝到内核空间。

【函数参数】

  • tovoid *类型,目标地址(内核空间)。
  • fromvoid __user *类型,源地址(用户空间)。
  • nunsigned long类型,要拷贝数据的字节数。

【返回值】long类型,如果复制成功,返回值为 0,如果复制失败则返回负数 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
/* 需要包含的头文件 */
#include <asm/uaccess.h>
/* 至少应该有的语句 */
char __user *pbuf /* 用户空间数据 */
char mydev_buf[100]; /* 内核空间数据 */
ret = copy_from_user(mydev_buf, pbuf, size);
if (ret)
{
printk("copy from user buf failed!\n");
return -1;
}

【注意事项】

(1)该函数定义在linux源码的include/asm-generic/uaccess.h中,但是在包含头文件的时候注意使用asm/uaccess.h,这个吧,我也不清楚是为什么,这样编译的时候使用make ARCH=arm不报错,但是直接编译x86平台的模块的话就会报错,可能是内核版本问题,我们是要在ARM平台上跑驱动,所以就按照asm/uaccess.h就可以啦。

2.2 使用实例

暂无

3. container_of()

3.1 函数说明

我们使用以下命令查询一下函数所在头文件:

1
grep "#define container_of" -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 需包含的头文件 */
#include <linux/kernel.h>

/* 函数声明 */
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

/* offsetof 定义在linux内核源码的 include/linux/stddef.h 中*/
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif

【函数说明】该函数用于通过结构体某个成员的首地址获取整个结构体变量的首地址,该函数其实是一个宏,在网上看到它被称之为内核第一宏,也不知真假😂,关于原理,这里我就不写了,可以看这里,我自己觉得说的挺清晰的Linux内核宏Container_Of的详细解释_Linux_脚本之家 (jb51.net)

【函数参数】

  • ptr:结构体内某成员member的地址。
  • type:要求首地址的结构体的类型。
  • member:结构体内的某成员名。

【返回值】目前只知道返回的是要求的结构体的首地址,我们一般会用强制类型转换转换为我们需要的地址类型。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
/* 需要包含的头文件 */
#include <linux/kernel.h>
/* 至少应该有的语句 */
struct _my_dev
{
/* 字符设备对象 */
struct cdev mydev; /* 定义一个字符设备对象设备 */
};
struct _my_dev g_my_dev; /* 定义一个新字符设备 */

struct _my_dev *p;
p = container_of(&g_my_dev->mydev, struct _my_dev, mydev)

【注意事项】none

3.2 使用实例

暂无

三、基本驱动函数实现

1. 打开与关闭设备

1.1 myopen()

当我们使用字符设备的时候首先肯定需要打开设备文件,当我们的应用程序调用open()函数的时候,相应的驱动中的myopen()便会被调用。我们自己编写的myopen()函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @Function : mydev_open
* @brief : 打开设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象,设备文件, file 结构体有个叫做 private_data 的成员变量。一般在 open 的时候将 private_data 指向设备属性结构体。
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .open
*/
int myopen(struct inode *pnode, struct file *pfile)
{
printk("myopen is called!\n");
return 0;
}

1.2 myclose()

该驱动函数对应应用程序中的close()函数,我们自己编写的myclose()函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @Function : myclose
* @brief : 关闭设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象,设备文件, file 结构体有个叫做 private_data 的成员变量。一般在 open 的时候将 private_data 指向设备属性结构体。
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .close
*/
int myclose(struct inode *pnode, struct file *pfile)
{
printk("mydev_close is called!\n");
return 0;
}

2. 读写设备操作

2.1 myread()

该驱动函数对应应用程序中的open()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Function : myread
* @brief : 读设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次read对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :char __user * 类型,指向用户空间一块内存, 就是返回给用户空间的数据缓冲区,用来保存读到的数据。
* @param count :size_t 类型,用户期望读取的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功读取的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .read
*/
ssize_t myread(struct file *pfile, char __user *pbuf, size_t count, loff_t *ppos)
{
/* 读取数据 */
/* 会用到 copy_to_user 函数,将数据从内核空间拷贝到用户空间 */
return count;
}

2.2 mywrite()

该驱动函数对应应用程序中的write()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Function : mywrite
* @brief : 写设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次write对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :char __user * 类型,指向用户空间一块内存,用来保存被写的数据。
* @param count :size_t 类型,用户期望写入的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功写入的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .write
*/
ssize_t mywrite(struct file *pfile, const char __user *pbuf, size_t count, loff_t *ppos)
{
/* 读取数据 */
/* 会用到 copy_from_user 函数,将数据从用户空间拷贝到内核空间 */
return size;
}

3. 设备控制操作

3.1 myioctl()

该驱动函数对应应用程序中的ioctl()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @Function : mydev_ioctl
* @brief : 设备属性获取
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次ioctl对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param cmd :unsigned int 类型,用来表示做的是哪一个操作。
* @param arg :unsigned long 类型,和 cmd 配合用的参数。
* @return : 成功返回0,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .unlocked_ioctl
*/
long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int __user *pret = (int *)arg; // 要获取的数据是int类型,所以这里强制类型转换
switch(cmd)
{
case MYCHAR_IOCTL_GET_MAXLEN:
if(copy_to_user(pret, &maxlen, sizeof(int)))
{
printk("copy_to_user maxlen failed!\n");
return -1;
}
break;
case MYCHAR_IOCTL_GET_CURLEN:
if(copy_to_user(pret, &curlen, sizeof(int)))
{
printk("copy_to_user curlen failed!\n");
return -1;
}
break;
default:
printk("The cmd is unknown!\n");
return -1;
}
return 0;
}
1
2
3
4
5
6
#include <asm/ioctl.h>

#define MY_CHAR_MAGIC 'k'
/* 要获取的数据是int类型,所以这里的size参数可以写为 int * 以表示我们要读取的数据类型 */
#define MYCHAR_IOCTL_GET_MAXLEN _IOR(MY_CHAR_MAGIC, 1, int *)
#define MYCHAR_IOCTL_GET_CURLEN _IOR(MY_CHAR_MAGIC, 2, int *)

【注意事项】arg可以用于进行参数的传递,可以进行强制类型转换,以表示不同类型的数据,并不单纯的就是unsigned long

3.2 cmd

3.2.1 命令格式

linux中这样定义一个命令:

image-20220901151314143
  • dirdirection):ioctl命令访问模式(属性数据传输方向),占据2bit,可以为 _IOC_NONE_IOC_READ_IOC_WRITE_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据。
  • typedevice type):设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”(也就是说来源没有依据),可以为任意char型字符,例如 'a''b''c' 等等,其主要作用是使ioctl命令有唯一的设备标识。
  • nrnumber):命令编号或者叫序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl命令,通常从 0 开始编号递增。
  • size:涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数。

3.2.2 相关函数

linux内核为我们提供了几个宏来帮助我们定义命令,这些宏定义在linux源码的这个文件中:

1
include/uapi/asm-generic/ioctl.h

我们可以使用以下命令看来查询定义这些宏的头文件:

1
2
3
4
grep "#define _IO" -r -n ~/5linux/linux-3.14/include
grep "#define _IOR" -r -n ~/5linux/linux-3.14/include
grep "#define _IOW" -r -n ~/5linux/linux-3.14/include
grep "#define _IOWR" -r -n ~/5linux/linux-3.14/include

会发现这些宏定义都出现在include/uapi/asm-generic/ioctl.h文件中,我们打开它,可以看到如下信息(我自己添加了注释,只写了几个自己用到的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... ...
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
// ... ...
/* used to create numbers */
//定义不带参数的 ioctl 命令
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
//定义带读参数的ioctl命令(copy_to_user) size为类型名
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
//定义带写参数的 ioctl 命令(copy_from_user) size为类型名
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
//定义带读写参数的 ioctl 命令 size为类型名
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

// ... ...

【注意事项】我们使用的时候需要加上asm/ioctl.h这个头文件。

4. 设备属性结构体

一般来说,我们创建一个字符设备驱动,这个字符设备需要有这些属性:主设备号、次设备号、设备号、设备数量,字符设备结构对象、读写缓冲区等等,随着功能的增加,设备驱动的属性也会增加,我们全部用全局变量的话,有些不是太好管理,这个时候我们可以自定义一个结构体来表示在我们的这个字符设备驱动的各个属性。如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define BUF_LEN 100                   /* 数据读写缓冲区大小 */
/* 新字符设备的属性 */
struct _my_dev
{
/* 1.设备号与设备数量 */
int major; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095 */
int minor; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
int dev_num; /* 申请的设备数量 */
/* 2.字符设备对象 */
struct cdev mydev; /* 定义一个字符设备结构体对象(主要用于绑定操作函数集和向内核添加设备) */
/* 设备读写相关成员变量 */
char mydev_buf[BUF_LEN]; /* 读写缓冲区 */
int curlen; /* 读写缓冲区中当前数据字节数 */
/* 3.自动创建设备节点相关成员变量 */
struct class *class; /* 类 */
struct device *device; /* 设备 /dev/dev_name */
};
struct _my_dev g_my_dev; /* 定义一个新字符设备 */

这样的话也方便了我们参数的传递以及对整个驱动程序的属性管理。

5. 设置文件私有数据

我们在驱动的各个函数之间进行参数传递的时候,使用的的是struct _my_dev这个结构体中的各个成员,我们不能在每个驱动函数中都使用全局变量吧,这样读写很容易出现问题。

经过观察会发现,struct file_operations结构体中所有的函数指针都带有struct file这个结构体参数,于是,我们便可以将包含了设备驱动各个属性的结构体变量传给struct file结构体的private_data 成员。这个工作在myopen()函数中完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Function : mydev_open
* @brief : 打开设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象,设备文件, file 结构体有个叫做 private_data 的成员变量。一般在 open 的时候将 private_data 指向设备属性结构体。
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .open
*/
int myopen(struct inode *pnode, struct file *pfile)
{
/* 获取 struct _my_dev g_my_dev 地址,也就是将包含字符设备的各个属性参数的结构体变量传给 private_data 成员*/
pfile->private_data = (void *)(container_of(pnode->i_cdev, struct _my_dev, mydev));
printk("myopen is called!\n");
return 0;
}

之后我们在mywrite()myread()myclose() 等我们自己实现的驱动函数中直接读取private_data即可得到设备结构体。 例如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Function : myread
* @brief : 读设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次read对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :char __user * 类型,指向用户空间一块内存, 就是返回给用户空间的数据缓冲区,用来保存读到的数据。
* @param count :size_t 类型,用户期望读取的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功读取的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .read
*/
ssize_t myread(struct file *pfile, char __user *pbuf, size_t count, loff_t *ppos)
{
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
/* 读取数据 */
/* 会用到 copy_to_user 函数,将数据从内核空间拷贝到用户空间 */
return count;
}