《Linux设备驱动程序》第三章 笔记

来源:互联网 发布:java radiobutton 选中 编辑:程序博客网 时间:2024/06/05 12:47
scull
scull是一个字符驱动,它不依赖硬件。只是操作一些从内核分配的内存。任何人都可以编译和运行scull,并且scull 在Linux运行的体系结构中可移植。另一方面,这个设备处理演示内核和字符驱动的接口和允许用户运行一些测试之外,不做任何有用的事情。

scull 的设计
scull 0 到scull3
4个设备,每个由一个全局永久的内存区组成。全局意味着如果设备被多次打开,设备中含有的数据由所有打开他的文件描述符共享。永久意味着如果设备关闭又重新打开,数据不会丢失。
scullpipe0 到 scullpipe3
4个FIFO 设备,行为像管道。一个进程读的内容来自另一个进程所写。如果多个进程读同一个设备,他们竞争数据。scullpipe 的内部将展示阻塞读写和非阻塞读写如何实现,而不必采取中断。
scullsingle scullpriv sculluid scullwuid
这些设备与scull0 相似,但是在什么时候允许打开上有些限制。第一个只允许一次一个进程使用驱动,而scullpriv 对每个虚拟终端(或者 X 终端会话)是私有的,因为每个控制台/终端上的进程有不同的内存区。sculluid 和 scullwuid 可以多次打开,但是一次只能是一个用户;前者返回一个”设备忙“错误,如果另一个用户锁着设备,而后者实现阻塞打开。这些scull的变体可能看来混淆了策略和机制。

主次编号
图片中逗号分隔的两个数为主次编号,前为主, 后为次
传统上,主编号标识设备相连的驱动。例如,/dev/null 和 /dev/zero 都由驱动 1 来管理,而虚拟控制台和串口终端都由驱动 4 管理;以此类推。现代Linux内核允许多个驱动共享主编号,但是你看到的大部分设备任然按照一个主编号一个驱动的原则来组织。
次编号被内核用来决定引用哪个设备。依据你的驱动是如何编写的,你可以从内核得到一个你的设备的直接指针,或者可以自己使用次编号作为本地设备数组的索引。不论哪个方法,内核自己几乎不知道次编号的任何事情,除了它们指向你的驱动实现的设备。
dev_t 类型(在<Linux/types.h>中定义)用来给设备编号。2.6.0 内核,dev_t 是32位的量,12位用做主编号,20位用作次编号。为获得一个 dev_t 的主或次编号,使用:
MAJOR(dev_t dev);
MINOR(dev_t dev);
如果有主次编号,需要将其转换为一个dev_t,使用:
MKDEV(int major, int minor);

分配和释放设备编号
获取一个或多个设备编号使用:
int register_chrdev_region(dev_t first, unsigned int count, char *name); <linux/fs.h>
first 是要分配的起始设备编号。first 的次编号部分常常是0。 count 是请求的连续设备编号的总数。name 是连接到这个编号范围的设备的名字;它会出现在/proc/devices 和 sysfs 中。
动态分配主编号:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
dev 是一个只输出的参数,他在函数成功完成时持有你的分配范围的第一个数。firstminor 是请求的第一个要用的次编号,通常为0,。count 和 name如上。

设备编号的释放:
void unregister_chrdev_region(dev_t first, unsigned int count);
上面函数分配设备编号给驱动使用,但它们不会告诉内核你实际上会对这些编号做啥。在用户空间程序能够存取这些设备号中一个之前,你的驱动需要连接它们到它的实现设备操作的内部函数上。

主编号的动态分配
两个选择:挑一个没有用的编号; 动态方式分配主编号。 如果只有你是驱动的唯一使用者,可以使用第一种方法; 但是驱动被广泛使用,就必须使用动态方式分配。
动态分配的缺点:无法提前创建设备节点,因为分配的主编号会变化。
#!/bin/sh
module="scull"
device="scull"
mode="664"
# invoke insmod with all arguments we got
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod ./$module.ko $* || exit 1
# remove stale nodes
rm -f /dev/${device}[0-3]
major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices)
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
# give appropriate group/permissions, and change the group.
# Not all distributions have staff, some have "wheel" instead.
group="staff"
grep -q '^staff:' /etc/group || group="wheel"
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
这个脚本可以通过重定义变量和调整mknod 行来适用于另外的驱动。这个脚本仅仅展示了创建4个设备,因为 4 是scull 源码中缺省的。 这个脚本必须由超级用户运行,因此新建的特殊文件由root 拥有。
获取主编号的代码:
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}

重要的数据结构
file_operations:
是一个字符驱动如何建立这个连接。它定义在<linux/fs.h>,是一个函数指针的集合。每个打开文件(内部用一个 file 结构来代表)与它自身的函数集合相关联(通过包含一个 f_op 的成员,它指向一个 file_operations 结构)。
在file_operations 方法中,有参数包含字串 __user。这种注解是一种文档形式,,注意,一个指针是一个不能被直接解引用的用户空间地址。对于正常的编译,__user 没有效果,但是它可被外部检查软件使用来找出对用户空间地址的错误使用。
struct module *owner: 第一个成员,不是一个操作;它是一个指向拥有这个结构的模块的指针。这个成员用来在它的操作还在被使用时阻止模块被卸载。几乎所有时间中,它被简单初始化为 THIS_MODULE,在<Linux/module.h> 中定义的宏。
ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t); : 初始化一个异步读 -- 可能在函数返回前不结束的读操作。如果这个方法是NULL, 所有的操作会由 read 代替进行(同步的)。
ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *); : 初始化设备上的一个异步写。
int (*readdir) (struct file *, void *, filldir_t); : 对于设备文件这个成员应当为NULL;它用来读取目录,并且仅对文件系统有用。
unsigned int (*poll) (struct file *, struct poll_table_struct *); : poll 方法是3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件系统描述符的读或写是否会阻塞。poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的,并且,可能提供给内核信息用来调用进程睡眠直到I/O 变为可能。如果一个驱动的poll 方法为NULL,设备假定为不阻塞的可读可写。
int (*ioctl) (struct inode *, strcut file *, unsigned int, unsigned long); : ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读也不是写)。另外,几个ioctl 命令被内核识别而不必引用 fops 表。如果设备不提供ioctl 方法,对于任何未事先定义的请求(-ENOTTY,“设备无这样的ioctl”),系统调用返回一个错误。
int (*mmap) (struct file *, struct vm_area_struct *); : mmap 用来请求将设备内存映射到进程的地址空间。如果这个方式 NULL, mmap 系统调用返回 -ENODEV。
int (*flush) (struct file *); : flush 操作在进程关闭它的设备文件描述符的拷贝时调用;它应当执行(并且等待)设备的任何未完成的操作。这个必须不要和用户查询请求的 fsync 操作混淆了。当前,flush 在很少驱动中使用;SCSI 磁带驱动使用它。例如,为确保所有写的数据在设备关闭前写到磁带上。如果flush 为NULL,内核简单的忽略用户应用程序的请求。
int (*fsync) (struct file *, struct dentry *, int ); : 这个方法是 fsync 系统调用的后端,用户调用来刷新任何挂着的数据。如果这个指针是 NULL, 系统调用返回 -EINVAL。
int (*aio_fsync) (struct kiocb *, int ); : fsync 方法的异步版本。
int (*fasync) (int, struct file I*, int ); : 这个操作用来通知设备它的 FASYNC 标志的改变。异步通知是一个高级的主题。这个成员可以是 NULL, 如果驱动不支持异步通知。
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
这些方法实现发散/汇聚读和写操作。应用程序偶尔需要做一个包含多个内存区的单个读或写操作;这些系统调用允许它们这样做而不必对数据进行额外拷贝。如果这些函数指针为 NULL ,read 和 write 方法被调用(可能多于一次)。
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *); :这个方法实现 sendfile 系统调用的读,使用最少的拷贝从一个文件描述符搬移数据到另一个。
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); : sendpage 是sendfile 的另一半;它由内核调用来发送数据,一次一页,到对应文件。设备驱动实际上不实现 sendpage。
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); : 这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段。这个任务通常由内存管理代码进行;这个方法存在为了使驱动能强制特殊设备可能有的任何对齐请求。大部分驱动可以置这个 方法为NULL。
int (*check_flags) (int); : 允许模块检查传递给 fnctl(F_SETFL...) 调用的标识。
int (*dir_notify) (struct file *, unsigned long); : 这个方法在应用程序使用 fnctl 来请求目录改变通知时调用。只对文件系统有用;驱动不需要实现 dir_notify。
scull 设备驱动只实现最重要的设备方法。它的 file_operations 结构初始化如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};

文件结构 struct file
file 与用户空间程序的 FILE 指针没有任何关系。
成员:
fmode_t f_mode;
文件模式,确定文件是可读还是可写(或者都是),通过位 FMODE_READ 和 FMODE_WRITE。
loff_t f_pos;
当前读写位置。
void *private_data;
open 系统调用用设置这个指针为NULL, 在为驱动调用 open 方法之前。你可自由使用这个成员或者忽略它;你可以使用这个成员来指向分配的数据,但是接着你必须记住在内核销毁文件结构之前,在 release 方法中释放那个内存。private_data是一个有用的资源,在系统调用间保留状态信息。
struct dentry *f_dentry;
关联到文件的目录入口结构。正常情况下不要关心这个结构,除了作为 file->f_dentry->d_inode 存取 inode 结构。

inode 结构
inode 结构由内核在内部用来表示文件。
作为一个通用的规则,这个结构只有2个成员对于编写驱动代码有用:
dev_t i_rdev;
对于代表设备文件的结点,这个成员包含实际的设备编号。
struct cdev *i_cdev;
它是内核的内部结构,代表字符设备;这个成员包含一个指针,指向这个结构,当节点指的是一个字符设备文件时。
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
这两个宏可以用来从一个inode 中获取主次编号。

字符设备注册
内核在内部使用类型 struct cdev 的结构来代表字符设备。在内核调用你的设备操作前,你编写分配并注册一个或几个这些结构。
两种方法分配和初始化这些结构。
如果想在运行时获得一个独立的 cdev 结构:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
如果想将 cdev 结构嵌入一个你自己的设备特定的结构:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
struct cdev 有一个拥有者成员,应当设置为 THIS_MODULE。一旦 cdev 结构建立,最后的步骤是把它高速内核,调用:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
num 是这个设备响应的第一个设备号,count 是关联到设备的设备号的数目。常常count 是1。
在使用 cdev_add 几个重要事情:
1、调用可能失败,如果返回错误码,你的设备没有增加到系统中。
2、它几乎会一直成功,但是,带起了其它的点:cdev_add 一返回,你的设备就是“活的”并且内核可以调用它的操作。
3、除非你的驱动完全准备好处理设备上的操作,你不应当调用 cdev_add。
从系统去除一个字符设备:
void cdev_del(struct cdev *dev);

scull 中设备注册
在内部,scull使用一个struct scull_dev 类型的结构表示每个设备。这个结构定义为:
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
初始化struct cdev,如下:
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
int err, devno = MKDEV(scull_major, scull_minor + index);
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
因为 cdev 结构嵌在 struct scull_dev 里面,cdev_init 必须调用来进行那个结构的初
始化。

open 方法
大部分驱动中,open 应当进行的工作:
1、检查设备特定的错误(例如设备没准备好,或者类似的硬件错误)
2、如果它第一次打开,初始化设备
3、如果需要,更新 f_op 指针
4、分配并填充要放进去 filp->private_data 的任何数据。
open函数的原形:
int (*open)(struct inode *inode, struct file *filp);
inode 参数中有我们需要的 i_cdev 成员,即之前建立的 cdev 结构。但我们需要包含 cdev 结构的 scull_dev结构。在内核 hacker 中,container_of 宏,在<linux/kernel.h>中定义:
container_of(pointer, container_type, container_field);
这个宏使用一个指向 container_field 类型的成员指针,它在一个 container_type 类型的结构中,并且返回一个指针指向包含结构。在 scull_open,这个红涌来找到适当的设备结构:
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */

release 方法
release 方法进行的工作:
1、 释放 open 分配在 filp->private_data 中的任何东西
2、在最后的 close 关闭设备
scull 的基本形式没有硬件关闭,一次需要的代码是最少的:
int scull_release (struct inode *inode, struct file *filp)
{
return 0;
}
不是每个 close 系统调用都引起调用 release 方法。close 系统调用仅在文件结构计数掉到 0 时执行 release 方法。

scull 的内存使用
scull 使用的内存区,也称为一个设备,长度可变。写的越多,增长越多;使用一个短文件覆盖设备来进行修整。
scull 驱动引入2 个核心函数来管理 Linux 内核中的内存。这些函数,定义在<linux/slab.h>,是:
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
在 scull,每个设备是一个ie指针链表,每个都指向一个 sucll_dev 结构。每个这样的结构,缺省地,指向最多 4 兆字节,通过一个中间指针数组。发行代码使用一个 1000 个指针的数组指向每个 4000 字节的区域。我们称每个内存区域为一个量子,数组(或者他的长度)为一个量子集,一个scull 设备和它的内存区如图所示:(一个 scull设备)


读和写
read 和 write 方法的 Buff 参数使用户空间指针,因此,它不能被内核代码直接解引用。这个限制有几个理由:
1、依赖于你的驱动运行的体系,以及内核被如何配置的,用户空间指针当运行与内核模式可能根本是无效的。可能没有那个地址的映射,或者它可能指向一些其他的随机数据。
2、就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时这个内存可能没有在 RAM 中。试图直接引用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情。结果可能是一个“oops”,导致进行系统调用的进程死亡。
3、置疑中的指针由一个用户程序提供,它可能是错误的或者恶意的。如果你的驱动盲目地解引用一个用户提供的指针,它提供了一个打开的门路使用户空间程序存取或覆盖系统任何地方的内存。如果你不想负责你的用户的系统的安全危险,你就不能直接解引用用户空间指针。
scull 中的读写代码需要拷贝一整段数据到(或者从)用户地址空间。这个能力由下列内核函数提供,它们拷贝一个任意的字节数组,并且位于大部分读写实现的核心中。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
尽管这些函数表现像正常的 memcpy 函数,但必须小心在从内核代码中存取用户空间。寻址的用户可能当前不在内存,虚拟内存子系统会使进程睡眠在这个页被传送到位时。
两个函数还检查用户空间指针是否有效。如果指针无效,不进行拷贝;如果在拷贝中遇到一个无效地址,只拷贝部分数据。在第2情况下,返回值是还要拷贝的数量。scull 代码查看这个错误返回,并且如果它不是0 就返回 -EFAULT 给用户。
如果你不需要检查用户空间指针,可以调用 __copy_to_user 和 __copy_from_user 来代替。
下图,read 的参数表示了一个典型读实现是如何使用它的参数。

read 和 write 方法都在发生错误时返回一个负值。相反,大于或等于 0 的返回值告知调用程序有多少字节已经成功传送。如果一些数据成功传送接着发生错误,返回值必须是成功传送的字节数,错误不报告知道函数下一次调用。这要求你的驱动记住错误已经发生,以便它们可以在以后返回错误状态。

read 方法
read 的代码如下:(忽略down_interruptible 的调用并且现在为up)
ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;

if (*f_pos >= dev->size)
goto out;

if (*f_pos + count > dev->size)
count = dev->size - *f_pos;

/* find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */

/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count))
{
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

out:
up(&dev->sem);
return retval;
}

write 方法
write 的 scull 代码一次处理单个量子:
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* value used in "goto out" statements */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;

/* find listitem, qset index and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

/* follow the list up to the right position */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data)
{
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos])
{
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}

/* write only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count))
{
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

/* update the size */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return retval;
}

快速参考
本章介绍了下面符号和头文件. struct file_operations 和 struct file 中的成员的列
表这里不重复了.
#include <linux/types.h>
dev_t
dev_t 是用来在内核里代表设备号的类型.

int MAJOR(dev_t dev);
int MINOR(dev_t dev);
从设备编号中抽取主次编号的宏.

dev_t MKDEV(unsigned int major, unsigned int minor);
从主次编号来建立 dev_t 数据项的宏定义

#include <linux/fs.h>
"文件系统"头文件是编写设备驱动需要的头文件. 许多重要的函数和数据结构在此定义.

int register_chrdev_region(dev_t first, unsigned int count, char *name)
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char
*name)
void unregister_chrdev_region(dev_t first, unsigned int count);
允许驱动分配和释放设备编号的范围的函数. register_chrdev_region 应当用在事先知道需要的主编号时; 对于动态分配, 使用 alloc_chrdev_region 代替.

int register_chrdev(unsigned int major, const char *name, struct
file_operations *fops);
老的( 2.6 之前) 字符设备注册函数. 它在 2.6 内核中被模拟, 但是不应当给新代码使用. 如果主编号不是 0, 可以不变地用它; 否则一个动态编号被分配给这个设备.

int unregister_chrdev(unsigned int major, const char *name);
恢复一个由 register_chrdev 所作的注册的函数. major 和 name 字符串必须包含之前用来注册设备时同样的值.

struct file_operations;
struct file;
struct inode;
大部分设备驱动使用的 3 个重要数据结构. file_operations 结构持有一个字符驱动的方法; struct file 代表一个打开的文件, struct inode 代表磁盘上的一个文件
.
#include <linux/cdev.h>
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *dev, struct file_operations *fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);
cdev 结构管理的函数, 它代表内核中的字符设备.

#include <linux/kernel.h>
container_of(pointer, type, field);
一个传统宏定义, 可用来获取一个结构指针, 从它里面包含的某个其他结构的指针.

#include <asm/uaccess.h>
这个包含文件声明内核代码使用的函数来移动数据到和从用户空间.

unsigned long copy_from_user (void *to, const void *from, unsigned long count);
unsigned long copy_to_user (void *to, const void *from, unsigned long count);
在用户空间和内核空间拷贝数据.

阅读全文
0 0
原创粉丝点击