BruceFan's Blog

Stay hungry, stay foolish

0%

Linux设备驱动(二)字符设备驱动

字符设备驱动结构

cdev结构体

Linux内核中cdev结构体描述一个字符设备。

1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj; // 内嵌的kobject对象
struct module *owner; // 所属模块
struct file_operations *ops; // 文件操作结构体
struct list_head list;
dev_t dev; // 设备号
unsigned int count;
}

dev_t定义了设备号,为32位,其中12位为主设备号,20位为次设备号。下面的宏可以获得主设备号和次设备号:

1
2
MAJOR(dev_t dev)
MINOR(dev_t dev)

使用下面的宏可以用主设备号和次设备号生成dev_t:

1
MKDEV(int major, int minor)

Linux内核提供了一组函数用于操作cdev结构体:

1
2
3
4
5
6
void cdev_init(struct cdev *, struct file_operations *); // 用于初始化cdev的成员,并建立cdev和file_operations之间的连接
struct cdev *cdev_alloc(void); // 用于动态申请一个cdev内存
void cdev_put(struct cdev *p);
// 用向系统添加和删除一个cdev,完成字符设备的注册和注销
int cdev_add(struct cdev *, dev_t, unsigned); // 通常在字符设备驱动模块加载函数中调用
void cdev_del(struct cdev *); // 字符设备驱动模块卸载函数中调用

分配和释放设备号

在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()alloc_chrdev_region()函数向系统申请设备号:

1
2
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况。

file_operations结构体

file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用。
代码清单 include/linux/fs.h: file_operations

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
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 (*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);
unsigned int (*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 *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

下面对file_operations结构体中的主要成员简要介绍:
llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write()函数向设备发送数据,成功时该函数返回写入的字节数。如果次函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。
unlocked_ioctl()提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。

字符设备驱动的组成

1.字符设备驱动模块加载与卸载函数
代码清单 加载与卸载函数模板

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
// 设备结构体
struct xxx_dev_t {
struct cdev cdev;
...
} xxx_dev;
// 设备驱动模块加载函数
static int __init xxx_init(void)
{
...
cdev_init(&xxx_dev.cdev, &xxx_fops); // 初始化cdev
xxx_dev.cdev.owner = THIS_MODULE;
// 获取字符设备号
if (xxx_major) {
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
} else {
alloc_chrdev_region(&xxx_dev_no, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); // 注册设备
...
}
// 设备驱动模块卸载函数
static void __exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); // 释放占用的设备号
cdev_del(&xxx_dev.cdev);
...
}

2.字符设备驱动的file_operations结构体中的成员函数
file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。
代码清单 字符设备驱动读、写、I/O控制函数模板

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
// 读设备
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...); // 内核空间到用户空间缓冲区的复制
...
}
// 写设备
ssize_t xxx_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
...
copy_from_user(..., buf, ...); // 用户空间缓冲区到内核空间的复制
...
}
// ioctl函数
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch (cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
// 不支持的命令
return -ENOTTY;
}
return 0;
}

由于用户空间不能直接访问内核空间的内存,因此借助了函数copy_from_user()完成用户空间缓冲区到内核空间的复制,copy_to_user()完成内核空间到用户空间缓冲区的复制。它们的原型如下:

1
2
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

完全复制成功返回值为0,如果复制失败,则返回负值。
读和写函数中的__user是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释功能。
在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员:

1
2
3
4
5
6
7
struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.read = xxx_read,
.write = xxx_write,
.unlocked_ioctl = xxx_ioctl,
...
};

上述xxx_fops在 dev_init(&xxx_dev.cdev, &xxx_fops) 语句中建立与cdev的连接。

globalmem虚拟设备驱动

在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4K)的内存空间,并在驱动中提供对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。

头文件、宏及设备结构体

代码清单 globalmem设备结构体和宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define GLOBALMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 230

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

struct globalmem_dev {
struct cdev cdev; // 字符设备
unsigned char mem[GLOBALMEM_SIZE]; // 使用的内存
};

struct globalmem_dev *globalmem_devp;

加载与卸载设备驱动

代码清单 globalmem设备驱动模块的加载与卸载函数

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
36
37
38
39
40
41
42
43
44
45
46
47
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
int err, devno = MKDEV(globalmem_major, index);

cdev_init(&dev->cdev, &globalmem_fops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

static int __init globalmem_init(void)
{
int ret;
dev_t devno = MKDEV(globalmem_major, 0);

if (globalmem_major)
ret = register_chrdev_region(devno, 1, "globalmem");
else {
ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
globalmem_major = MAJOR(devno);
}
if (ret < 0)
return ret;

globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL); // 申请了一份globalmem_dev结构体的内存并清0
if (!globalmem_devp) {
ret = -ENOMEM;
goto fail_malloc;
}

globalmem_setup_cdev(globalmem_devp, 0); // 完成cdev的初始化和添加
return 0;

fail_malloc:
unregister_chrdev_region(devno, 1);
return ret;
}
module_init(globalmem_init);

static void __exit globalmem_exit(void)
{
cdev_del(&globalmem_devp->cdev);
kfree(globalmem_devp);
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);

在cdev_init()函数中,与globalmem的cdev关联的file_operations结构体如下:
代码清单 globalmem设备驱动的文件操作结构体

1
2
3
4
5
6
7
8
9
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};

读写函数

globalmem设备驱动的读写函数主要是让设备结构体的mem[]数组与用户空间交互数据,并随着访问的字节数变更更新文件读写偏移位置。
代码清单 globalmem设备驱动的读写函数

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
36
37
38
39
40
41
42
43
44
45
static ssize_t globalmem_read(struct file *filp, char __user *buf, 
size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;

if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;

if (copy_to_user(buf, dev->mem + p, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
return ret;
}

static ssize_t globalmem_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;

if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;

if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}

seek函数

seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),假设globalmem支持从文件开头和当前位置的相对偏移。
代码清单 globalmem设备驱动的seek()函数

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
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig) {
case 0: // 从文件开头位置seek
if (offset < 0) {
ret = -EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: // 从文件当前位置开始seek
if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
if ((filp->f_pos + offset) < 0) {
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}

ioctl函数

globalmem设备驱动的ioctl()函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl()函数返回-EINVAL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static long globalmem_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data;

switch (cmd) {
case MEM_CLEAR:
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
break;

default:
return -EINVAL;
}
return 0;
}

使用文件私有数据

大多数Linux驱动遵循一个”潜规则”,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。对于globalmem驱动而言,私有数据的设置是在globalmem_open()中完成的。

1
2
3
4
5
static int globalmem_open(struct inode *inode, struct file *filp)
{
filp->private_data = globalmem_devp;
return 0;
}

完整代码下载

globalmem驱动在用户空间中的验证

在globalmem源代码目录中通过make命令编译globalmem驱动,运行:

1
2
3
4
5
6
7
8
9
10
$ sudo insmod globalmem.ko
$ lsmod
Module Size Used by
globalmem 1504 0
...
$ sudo mknod /dev/globalmem c 230 0 #创建设备节点,c表明是字符设备,230是主设备号,0是次设备号
$ sudo chmod 777 /dev/globalmem
$ echo "hello world" > /dev/globalmem
$ cat /dev/globalmem
hello world # 结果证明"hello world"字符串被正确地写入了globalmem字符设备

reference
《Linux设备驱动开发详解——基于最新的Linux 4.0内核》