字符设备驱动(2):globalmem虚拟设备实例描述

文章介绍了Linux内核中字符设备驱动的开发,以一个虚拟的globalmem设备为例,讲解了如何创建设备结构体、加载与卸载驱动、实现读写函数、seek函数以及ioctl函数。globalmem驱动提供了一片内存空间供用户空间读写,并通过file_operations结构体定义了相应的操作函数。
摘要由CSDN通过智能技术生成

前言

从本章开始,后续的数章都将基于虚拟的globalmem设备进行字符设备驱动的讲解。globalmem意味着“全局内存”,在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用获取或设置这片内存的内容。

实际上,这个虚拟的globalmem设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。

本章将给出globalmem设备驱动的雏形,而后续章节会在这个雏形的基础上添加并发与同步控制等复杂功能。

1-头文件、宏及设备结构体

在globalmem字符设备驱动中,应包含它要使用的头文件,并定义globalmem设备结构体及相关宏。

 1#include <linux/module.h>
 2#include <linux/fs.h>
 3#include <linux/init.h>
 4#include <linux/cdev.h>
 5#include <linux/slab.h>
 6#include <linux/uaccess.h>
 7
 8#define GLOBALMEM_SIZE   0x1000
 9#define MEM_CLEAR 0x1
10#define GLOBALMEM_MAJOR 230
11
12static int globalmem_major = GLOBALMEM_MAJOR;
13module_param(globalmem_major, int, S_IRUGO);
14
15struct globalmem_dev {
16 struct cdev cdev;
17 unsigned char mem[GLOBALMEM_SIZE];
18};
19
20struct globalmem_dev *globalmem_devp;

从第15~18行代码可以看出,定义的globalmem_dev设备结构体包含了对应于globalmem字符设备的cdev使用的内存mem[GLOBALMEM_SIZE]

当然,程序中并不一定要把mem[GLOBALMEM_SIZE]和cdev包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。

2-加载与卸载设备驱动

globalmem设备驱动的模块加载和卸载函数遵循代码清单6.5的类似模板,其实现的工作与代码清单6.5完全一致,如代码清单6.9所示。

 1static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
 2 {
 3  int err, devno = MKDEV(globalmem_major, index);
 4
 5  cdev_init(&dev->cdev, &globalmem_fops);
 6  dev->cdev.owner = THIS_MODULE;
 7  err = cdev_add(&dev->cdev, devno, 1);
 8  if (err)
 9       printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
10 }
11
12static int __init globalmem_init(void)
13{
14  int ret;
15  dev_t devno = MKDEV(globalmem_major, 0);
16
17  if (globalmem_major)
18       ret = register_chrdev_region(devno, 1, "globalmem");
19  else {
20       ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
21       globalmem_major = MAJOR(devno);
22  }
23  if (ret < 0)
24      return ret;
25
26  globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
27  if (!globalmem_devp) {
28       ret = -ENOMEM;
29       goto fail_malloc;
30  }
31
32  globalmem_setup_cdev(globalmem_devp, 0);
33  return 0;
34
35  fail_malloc:
36  unregister_chrdev_region(devno, 1);
37  return ret;
38 }
39module_init(globalmem_init);
  • 第1~10行的globalmem_setup_cdev()函数完成cdev的初始化和添加,
  • 17~22行完成了设备号的申请,
  • 第26行调用kzalloc()申请了一份globalmem_dev结构体的内存并清0。

在cdev_init()函数中,与globalmem的cdev关联的file_operations结构体如代码清单6.10所示。

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

3-读写函数

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

 1static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
 2              loff_t * ppos)
 3{
 4 unsigned long p = *ppos;
 5 unsigned int count = size;
 6 int ret = 0;
 7 struct globalmem_dev *dev = filp->private_data;
 8
 9 if (p >= GLOBALMEM_SIZE)
10      return 0;
11 if (count > GLOBALMEM_SIZE - p)
12     count = GLOBALMEM_SIZE - p;
13
14 if (copy_to_user(buf, dev->mem + p, count)) {
15     ret = -EFAULT;
16 } else {
17     *ppos += count;
18     ret = count;
19
20     printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
21 }
22
23 return ret;
24}

*ppos是要读的位置相对于文件开头的偏移,如果该偏移大于或等于GLOBALMEM_SIZE,意味着已经到达文件末尾,所以返回0(EOF)。

 1static ssize_t globalmem_write(struct file *filp, const char __user * buf,
 2                size_t size, loff_t * ppos)
 3{
 4 unsigned long p = *ppos;
 5 unsigned int count = size;
 6 int ret = 0;
 7 struct globalmem_dev *dev = filp->private_data;
 8
 9 if (p >= GLOBALMEM_SIZE)
10     return 0;
11 if (count > GLOBALMEM_SIZE - p)
12     count = GLOBALMEM_SIZE - p;
13
14 if (copy_from_user(dev->mem + p, buf, count))
15     ret = -EFAULT;
16 else {
17     *ppos += count;
18     ret = count;
19
20     printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
21 }
22
23 return ret;
24}

4-seek函数

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

在定位的时候,应该检查用户请求的合法性,若不合法,函数返回-EINVAL,合法时更新文件的当前位置并返回该位置,如代码清单6.13所示。

 1static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
 2 {
 3  loff_t ret = 0;
 4  switch (orig) {
 5  case 0: /* 从文件开头位置seek */
 6       if (offset< 0) {
 7            ret = -EINVAL;
 8            break;
 9       }
10       if ((unsigned int)offset > GLOBALMEM_SIZE) {
11            ret = -EINVAL;
12            break;
13       }
14       filp->f_pos = (unsigned int)offset;
15       ret = filp->f_pos;
16       break;
17  case 1: /* 从文件当前位置开始seek */
18       if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
19            ret = -EINVAL;
20            break;
21       }
22       if ((filp->f_pos + offset) < 0) {
23            ret = -EINVAL;
24            break;
25       }
26       filp->f_pos += offset;
27       ret = filp->f_pos;
28       break;
29  default:
30       ret = -EINVAL;
31       break;
32  }
33  return ret;
34}

5-ioctl函数

1.globalmem设备驱动的ioctl()函数

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

 1static long globalmem_ioctl(struct file *filp, unsigned int cmd,
 2                unsigned long arg)
 3{
 4  struct globalmem_dev *dev = filp->private_data;
 5
 6  switch (cmd) {
 7  case MEM_CLEAR:
 8       memset(dev->mem, 0, GLOBALMEM_SIZE);
 9       printk(KERN_INFO "globalmem is set to zero\n");
10       break;
11
12  default:
13       return -EINVAL;
14  }
15
16  return 0;
17 }

在上述程序中,MEM_CLEAR被宏定义为0x01,实际上这并不是一种值得推荐的方法,简单地对命令定义为0x0、0x1、0x2等类似值会导致不同的设备驱动拥有相同的命令号。

如果设备A、B都支持0x0、0x1、0x2这样的命令,就会造成命令码的污染。因此,Linux内核推荐采用一套统一的ioctl()命令生成方式。

2.ioctl()命令

Linux建议以如图6.2所示的方式定义ioctl()的命令。

设备类型(8)+序列号(8)+方向(2)+数据尺寸(13/14)

命令码的设备类型字段为一个“幻数”,可以是0~0xff的值,内核中的ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”,新设备驱动定义“幻数”的时候要避免与其冲突。

命令码的序列号也是8位宽。

命令码的方向字段为2位,该字段表示数据传送的方向,可能的值是_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。

命令码的数据长度字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或者14位。

内核还定义了_IO()、_IOR()、_IOW()和_IOWR()这4个宏来辅助生成命令,这4个宏的通用定义如代码清单6.15所示。

 1#define _IO(type,nr)         _IOC(_IOC_NONE,(type),(nr),0)
 2#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),\
 3                                 (_IOC_TYPECHECK(size)))
 4#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),\
 5                                 (_IOC_TYPECHECK(size)))
 6#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),  \
 7                                 (_IOC_TYPECHECK(size)))
 8/* _IO、_IOR等使用的_IOC宏*/
 9#define _IOC(dir,type,nr,size) \
10     (((dir)  << _IOC_DIRSHIFT) | \
11     ((type) << _IOC_TYPESHIFT) | \
12     ((nr)   << _IOC_NRSHIFT) | \
13     ((size) << _IOC_SIZESHIFT))

由此可见,这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)、size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。由于globalmem的MEM_CLEAR命令不涉及数据传输,所以它可以定义为:

#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)

3.预定义命令

内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会作为预定义命令被内核处理而不是被设备驱动处理,下面列举一些常用的预定义命令。

  • FIOCLEX:即File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。
  • FIONCLEX:即File IOctl Not Close on Exec,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。
  • FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。
  • FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在filp->f_flags中的O_NONBLOCK标志。

FIOCLEX、FIONCLEX、FIOQSIZE和FIONBIO这些宏定义在内核的include/uapi/asm-generic/ioctls.h文件中。

6-使用文件私有数据

6.3.1~6.3.5 节给出的代码完整地实现了预期的globalmem雏形,代码清单6.11的第7行,代码清单6.12的第7行,代码清单6.14的第4行,都使用了struct globalmem_dev*dev=filp->private_data获取globalmem_dev的实例指针。

实际上,大多数Linux驱动遵循一个“潜规则”,那就是将文件的私有数据private_data指向设备结构体,再用read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。私有数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux的面向对象的设计思想。对于globalmem驱动而言,私有数据的设置是在globalmem_open()中完成的,如代码清单6.16所示。

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

为了让读者建立字符设备驱动的全貌视图,代码清单6.17列出了完整的使用文件私有数据的globalmem的设备驱动,本程序位于本书配套虚拟机代码的/kernel/drivers/globalmem/ch6目录下。

  1/*
  2 * a simple char device driver: globalmem without mutex
  3 *
  4 * Copyright (C) 2014 Barry Song  (baohua@kernel.org)
  5 *
  6 * Licensed under GPLv2 or later.
  7 */
  8
  9#include <linux/module.h>
 10#include <linux/fs.h>
 11#include <linux/init.h>
 12#include <linux/cdev.h>
 13#include <linux/slab.h>
 14#include <linux/uaccess.h>
 15
 16#define GLOBALMEM_SIZE   0x1000
 17#define MEM_CLEAR 0x1
 18#define GLOBALMEM_MAJOR 230
 19
 20static int globalmem_major = GLOBALMEM_MAJOR;
 21module_param(globalmem_major, int, S_IRUGO);
 22
 23struct globalmem_dev {
 24   struct cdev cdev;
 25   unsigned char mem[GLOBALMEM_SIZE];
 26};
 27
 28struct globalmem_dev *globalmem_devp;
 29
 30static int globalmem_open(struct inode *inode, struct file *filp)
 31{
 32   filp->private_data = globalmem_devp;
 33   return 0;
 34}
 35
 36static int globalmem_release(struct inode *inode, struct file *filp)
 37{
 38   return 0;
 39}
 40
 41static long globalmem_ioctl(struct file *filp, unsigned int cmd,
 42               unsigned long arg)
 43{
 44 struct globalmem_dev *dev = filp->private_data;
 45
 46 switch (cmd) {
 47 case MEM_CLEAR:
 48      memset(dev->mem, 0, GLOBALMEM_SIZE);
 49      printk(KERN_INFO "globalmem is set to zero\n");
 50      break;
 51
 52 default:
 53      return -EINVAL;
 54 }
 55
 56 return 0;
 57 }
 58
 59static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
 60                 loff_t * ppos)
 61{
 62 unsigned long p = *ppos;
 63 unsigned int count = size;
 64 int ret = 0;
 65 struct globalmem_dev *dev = filp->private_data;
 66
 67 if (p >= GLOBALMEM_SIZE)
 68      return 0;
 69 if (count > GLOBALMEM_SIZE - p)
 70      count = GLOBALMEM_SIZE - p;
 71
 72 if (copy_to_user(buf, dev->mem + p, count)) {
 73      ret = -EFAULT;
 74 } else {
 75      *ppos += count;
 76      ret = count;
 77
 78      printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
 79 }
 80
 81 return ret;
 82}
 83
 84static ssize_t globalmem_write(struct file *filp, const char __user * buf,
 85                  size_t size, loff_t * ppos)
 86{
 87 unsigned long p = *ppos;
 88 unsigned int count = size;
 89 int ret = 0;
 90 struct globalmem_dev *dev = filp->private_data;
 91
 92 if (p >= GLOBALMEM_SIZE)
 93      return 0;
 94 if (count > GLOBALMEM_SIZE - p)
 95      count = GLOBALMEM_SIZE - p;
 96
 97 if (copy_from_user(dev->mem + p, buf, count))
 98      ret = -EFAULT;
 99 else {
100      *ppos += count;
101      ret = count;
102
103      printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
104 }
105
106 return ret;
107 }
108
109static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
110 {
111 loff_t ret = 0;
112 switch (orig) {
113 case 0:
114      if (offset < 0) {
115           ret = -EINVAL;
116           break;
117      }
118      if ((unsigned int)offset > GLOBALMEM_SIZE) {
119           ret = -EINVAL;
120           break;
121      }
122      filp->f_pos = (unsigned int)offset;
123      ret = filp->f_pos;
124      break;
125 case 1:
126      if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
127           ret = -EINVAL;
128           break;
129      }
130      if ((filp->f_pos + offset) < 0) {
131           ret = -EINVAL;
132           break;
133      }
134      filp->f_pos += offset;
135      ret = filp->f_pos;
136      break;
137 default:
138      ret = -EINVAL;
139      break;
140 }
141 return ret;
142}
143
144static const struct file_operations globalmem_fops = {
145 .owner = THIS_MODULE,
146 .llseek = globalmem_llseek,
147 .read = globalmem_read,
148 .write = globalmem_write,
149 .unlocked_ioctl = globalmem_ioctl,
150 .open = globalmem_open,
151 .release = globalmem_release,
152};
153
154static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
155{
156 int err, devno = MKDEV(globalmem_major, index);
157
158 cdev_init(&dev->cdev, &globalmem_fops);
159 dev->cdev.owner = THIS_MODULE;
160 err = cdev_add(&dev->cdev, devno, 1);
161 if (err)
162      printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
163}
164
165static int __init globalmem_init(void)
166{
167 int ret;
168 dev_t devno = MKDEV(globalmem_major, 0);
169
170 if (globalmem_major)
171      ret = register_chrdev_region(devno, 1, "globalmem");
172 else {
173      ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
174      globalmem_major = MAJOR(devno);
175 }
176 if (ret < 0)
177      return ret;
178
179 globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
180 if (!globalmem_devp) {
181      ret = -ENOMEM;
182      goto fail_malloc;
183 }
184
185 globalmem_setup_cdev(globalmem_devp, 0);
186 return 0;
187
188 fail_malloc:
189 unregister_chrdev_region(devno, 1);
190 return ret;
191}
192module_init(globalmem_init);
193
194static void __exit globalmem_exit(void)
195{
196 cdev_del(&globalmem_devp->cdev);
197 kfree(globalmem_devp);
198 unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
199}
200module_exit(globalmem_exit);
201
202MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
203MODULE_LICENSE("GPL v2"); 

上述代码的行数与代码清单1.3已经不能相比了,除了代码清单1.3中的硬件操作函数仍然需要外,代码清单1.4中还包含了大量暂时陌生的元素,如结构体file_operations、cdev,Linux内核模块声明用的MODULE_AUTHOR、MODULE_LICENSE、module_init、module_exit,以及用于字符设备注册、分配和注销的函数register_chrdev_region()、alloc_chrdev_region()、unregister_chrdev_region()等。我们也不能理解为什么驱动中要包含light_init()、light_cleanup()、light_read()、light_write()等函数。

此时,我们只需要有一个感性认识,那就是,上述暂时陌生的元素都是Linux内核为字符设备定义的,以实现驱动与内核接口而定义的。 Linux对各类设备的驱动都定义了类似的数据结构和函数。

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

在globalmem的源代码目录通过“make”命令编译globalmem的驱动,得到globalmem.ko文件。运行

baohua@baohua-VirtualBox:~/develop/training/kernel/drivers/globalmem/ch6$ sudo
    insmod globalmem.ko

命令加载模块,通过“lnsmod”命令,发现globalmem模块已被加载。再通过“cat/proc/devices”命令查看,发现多出了主设备号为230的“globalmem”字符设备驱动:

$ cat /proc/devices
Character devices:
  1mem
  4/dev/vc/0
  4tty
  4ttyS
  5/dev/tty
  5/dev/console
  5/dev/ptmx
  7vcs
 10misc
 13input
 14sound
 21sg
 29fb
116alsa
128ptm
136pts
180usb
189usb_device
202cpu/msr
203cpu/cpuid
226drm
230globalmem
249hidraw
250usbmon
251bsg
252ptp
253pps
254rtc

接下来,通过命令

#mknod /dev/globalmem c 230 0

创建“/dev/globalmem”设备节点,并通过“echo’hello world’>/dev/globalmem”命令和“cat/dev/globalmem”命令分别验证设备的写和读,结果证明“hello world”字符串被正确地写入了globalmem字符设备:

# echo "hello world" > /dev/globalmem
# cat /dev/globalmem
hello world

如果启用了sysfs文件系统,将发现多出了/sys/module/globalmem目录,该目录下的树形结构为:

.
├── coresize
├── holders
├── initsize
├── initstate
├── notes
├── parameters
│ └── globalmem_major
├── refcnt
├── sections
│ └── __param
├── taint└── uevent

refcnt记录了globalmem模块的引用计数sections下包含的几个文件则给出了globalmem所包含的BSS、数据段和代码段等的地址及其他信息。

对于代码清单6.18给出的支持N个globalmem设备的驱动,在加载模块后需创建多个设备节点,

如运行mknod/dev/globalmem0c 2300使得/dev/globalmem0对应主设备号为globalmem_major、次设备号为0的设备,

运行mknod/dev/globalmem1c 2301使得/dev/globalmem1对应主设备号为globalmem_major、次设备号为1的设备。

分别读写/dev/globalmem0和/dev/globalmem1,发现都读写到了正确的对应的设备。

小结

字符设备是3大类设备(字符设备、块设备和网络设备)中的一类,其驱动程序完成的主要工作是

  • 初始化、添加和删除cdev结构体,
  • 申请和释放设备号,
  • 以及填充file_operations结构体中的操作函数,实现file_operations结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。

内容来自:Linux设备驱动开发详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TrustZone_Hcoco

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值