本章的目的是编写一个完整的字符设备驱动,我们开发一个字符驱动是因为这一类适合大部分简单硬件设备,字符驱动也比块驱动易于理解。
贯穿本章,我们展示从一个真实设备驱动提取的代码片段: scull( Simple Character Utility for Loading Localities):区域装载的简单字符工具。scull 是一个字符驱动,操作一块内存区域好像它是一个设备,在本章,因为 scull 的这个特殊之处, 我们可互换地使用“设备”这个词和"scull 使用的内存区"。
scull 的优势在于它不依赖硬件,scull 只是操作一些从内核分配的内存。
编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制)。scull 源码实现下面的设备. 模块实现的每种设备都被引用做一种类型。
本章涉及 scull0 到 scull3 的内部结构
对字符设备的访问是通过文件系统内的设备名称进行的,那些名称被称为文件系统的特殊文件、设备文件,或者简单称之为文件系统树的节点;它们通常位于 /dev 目录。字符驱动的特殊文件由使用 ls -l 的输出的第一列的"c"标识,块设备也出现在 /dev 中,但是它们由"b"标识。
在Linux系统上输入:ls -l /dev 观察输出。我们会发现如下面所示的文件的详细信息:
总用量 0
crw-r--r-- 1 root root 10, 235 5月 9 21:44 autofs
drwxr-xr-x 2 root root 560 5月 28 22:53 block
drwxr-xr-x 2 root root 80 5月 13 14:15 bsg
crw------- 1 root root 10, 234 5月 9 21:44 btrfs-control
drwxr-xr-x 3 root root 60 5月 9 21:43 bus
lrwxrwxrwx 1 root root 3 5月 13 14:08 cdrom -> sr0
drwxr-xr-x 2 root root 3900 5月 29 11:07 char
crw--w---- 1 root tty 5, 1 5月 9 21:44 console
lrwxrwxrwx 1 root root 11 5月 9 21:43 core -> /proc/kcore
drwxr-xr-x 6 root root 120 5月 9 21:44 cpu
crw------- 1 root root 10, 124 5月 9 21:44 cpu_dma_latency
crw------- 1 root root 10, 203 5月 9 21:44 cuse
drwxr-xr-x 8 root root 160 5月 9 21:44 disk
drwxr-xr-x 2 root root 60 5月 9 21:43 dma_heap
crw-rw----+ 1 root audio 14, 9 5月 9 21:44 dmmidi
drwxr-xr-x 3 root root 100 5月 9 21:44 dri
crw------- 1 root root 10, 126 5月 9 21:44 ecryptfs
在设备文件项中有 2 个数(由一个逗号分隔)在最后修改日期前面,这里通常是文件长度出现的地方,这些数字是给特殊设备的主次设备编号,上面的列表显示了一个典型系统上出现的几个设备,它们的主编号是 5、10、14, 而次编号是 1, 3, 9…。
主编号标识设备相连的驱动,例如 /dev/null 和 /dev/zero 都由驱动 1 来管理,而虚拟控制台和串口终端都由驱动 4 管理;次编号被内核用来决定引用哪个设备,依据你的驱动是如何编写的,你可以从内核得到一个你的设备的直接指针, 或者可以自己使用次编号作为本地设备数组的索引。
例如我们要操作某个设备,首先,我们要知道设备在/dev下的设备文件名。这个设备文件提供主设备号以及次设备号。然后内核通过设备文件提供的主设备找到设备驱动程序(操作设备由驱动程序实现)。最后通过主设备号和次设备构成的设备号找到正确的设备。有了操作的对象(设备)和操作的方法(驱动程序)那就可以完成了我们的要求。
一个驱动程序可以操作多个设备,所以不同的设备可以具有相同的主设备号。
因为我们在添加设备到内核的时候我们是关联设备号的,不同的设备可以具有相同的主设备号,那不同的次设备号和相同的主设备号结合就可以构成不同的设备号了,就标识了不同的设备了。
在上面主设备和次设备的介绍中我们提到设备编号,真正能标识不同的设备的是设备编号,每一个设备有一个唯一的设备编号。
在内核中,用 dev_t 类型来保存设备编号,它是一个32位的数,其中前12位用来表示主设备号,后20位用来表示次设备号。这个类型在
设备号由主设备号和次设备号构成。内核提供三个宏来实现这三个东西的转换。分别是:
MKDEV(int major, int minor) //将主次设备号转换成 dev_t 类型
MAJOR(dev_t dev) //获得dev_t dev中的主设备号
MINOR(dev_t dev)。 //获得dev_t dev中的次设备号
这三个宏在
内核是通过设备编号找到设备的,理所当然地要建立一个字符设备那必须要获得字符设备编号。要建立多少个字符设备就要得到多少个字符设备编号。
完成这一工作有两种方式,一种是静态获取,一种是动态获取,分别由:register_chrdev_region() 和 alloc_chrdev_region() 这两个函数实现。成功调用申请设备编号的函数后,在系统的 /proc/devices 下就会包含设备以及设备主设备号的信息。函数在
静态获取设备编号:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
返回: 0 成功分配.返回:负数 分配失败.
first 是你要分配的设备编号的起始范围值. first 的次编号部分常常是 0, 但是没有特别要求.
count 是你请求的连续设备编号的总数. 注意, 如果 count 太大, 那么所请求的范围可能和下一个主设备号重叠, 但是只要请求的编号范围可用, 一切都仍然会正确工作.。
name 是应当连接到这个编号范围的设备的名字;
动态获取设备编号:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
dev 是一个只用于输出的参数, 在成功完成调用后将保存已分配范围的第一个编号. fisetminor 是要使用的被请求的第一个次设备号,它常常是 0.
count 和 name 参数如同给 request_chrdev_region 函数的一样。
成功调用申请设备编号的函数后,在系统的 /proc/devices下就会包含设备以及设备主设备号的信息。
释放设备编号:
void unregister_chrdev_region(dev_t first, unsigned int count);
这三个函数在
尽量使用动态分配的方法,这样就能在加载甚至编译模块的时候设定主设备号,大大优于静态分配。
动态分配的缺点:由于分配的主设备号不能始终保持一致,所以无法在分配设备号之前创造设备节点。 但一旦设备号被分配之后, 你可以通过 /proc/devices 来读取它。
典型的 /proc/devices 文件如下所示:
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
14 sound/midi
14 sound/dmmidi
21 sg
29 fb
89 i2c
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
202 cpu/msr
203 cpu/cpuid
204 ttyMAX
226 drm
238 ipmidev
239 hidraw
240 ttyDBC
241 vfio
242 wwan_port
243 bsg
244 watchdog
245 remoteproc
246 ptp
247 pps
248 rtc
249 dma_heap
250 dax
251 dimmctl
252 ndctl
253 tpm
254 gpiochip
Block devices:
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
253 device-mapper
254 mdp
259 blkext
分配主设备号最佳的方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地,scull 的实现采用了这种方式工作;它使用一个全局变量,scull_major,用来保存所选择的设备号(也有一个用于次设备号的 scull_minor 变量)。这个变量初始化为 SCULL_MAJOR,定义在 scull.h 中,发布的源码中的 SCULL_MAJOR 的缺省值是 0,意思是"使用动态分配"。用户可以使用这个默认值或者选择某个特定的主设备号,而且既可以在编译前修改宏定义,也可以通过 insmod 命令行指定一个值给 scull_major。
下面是 scull.c 中用来获取主设备号的代码:
int result, i;
dev_t dev = 0; //设备编号
/*
*申请分配设备编号,根据scull_major的值是否为0,分别采用静态分配设备编号(register_chrdev_region)
*或动态分配设备编号(alloc_chrdev_region)的方法。scull_major代表主设备号,它的值是怎么确定的呢?
*scull_init_module函数中,如果用户没有通过命令行参数给scull_major赋任意大于0的值,
*则会采用alloc_chrdev_region动态分配设备编号。如果用户给scull_major赋了一个大于0值,
*则采用register_chrdev_region静态申请设备编号。(因此这里默认动态分配)
*/
if (scull_major) { //scull_major在头文件中定义为0
dev = MKDEV(scull_major, scull_minor); //将主次设备号转换成dev_t类型
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;
}
大部分的基础性的驱动操作包括 3 个重要的内核数据结构,称为 file_operations,file 和 inode。
我们已经为自己保留了一些设备编号来使用,但未将任何程序操作连接到这些编号上。 file_operation 结构是一个字符驱动如何建立这个连接,这个结构,定义在
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,
};
在你通读 file_operations 方法的列表时,你会注意到不少参数包含字串 __user,这种注解是一种文档形式而已,表明指针是一个用户空间地址,因此不能被直接引用。对于正常的编译,__user 没有任何效果,但是它可被外部检查软件使用来找出对用户空间地址的错误使用
struct file,定义于
在内核源码中,指向 struct file 的指针常常称为 file 或者 filp(“file pointer”),我们将一直称这个指针为 filp 以避免和结构自身混淆,因此,file 指的是结构本身,而 filp 是结构指针。
struct file 的重要成员:
内核使用 inode 结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即 file 文件结构)是不同的,我们可以使用多个file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些 file 文件结构全部都必须只能指向一个 inode 结构体。
inode 结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:
此外,内核也提供了两个宏可以从 inode 结点中获取主次设备号,宏的原型如下:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
内核在内部使用类型 struct cdev 的结构来代表字符设备,在内核调用你的设备操作前,你编写分配并注册一个或几个这些结构,为此,你的代码应当包含
有 2 种方法来分配和初始化一个这些结构,如果你想在运行时获得一个独立的 cdev 结构,你可以为此使用这样的代码:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
但是,偶尔你会想将 cdev 结构嵌入一个你自己的设备特定的结构;scull 这样做了,在这种情况下,你应当初始化你已经分配的结构,使用:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
任一方法,有一个其他的 struct cdev 成员你需要初始化,像 file_operations 结构,
struct cdev 有一个拥有者成员,应当设置为 THIS_MODULE。一旦 cdev 结构建立,最后的步骤是把它告诉内核,调用:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
这里,dev 是 cdev 结构,num 是这个设备响应的第一个设备号,count 是应当关联到设备的设备号的数目,常常 count 是 1,但是有多个设备号对应于一个特定的设备的情形。
在使用 cdev_add 是有几个重要事情要记住,第一个是这个调用可能失败,如果它返回一个负的错误码,你的设备没有增加到系统中。 cdev_add 一返回,你的设备就是"活的"并且内核可以调用它的操作,因此,在驱动程序还没有完全准备好处理设备上的操作时,就不能调用 cdev_add。
从系统去除一个字符设备,调用:
void cdev_del(struct cdev *dev);
在将 cdev 结构传递到 cdev_del 函数之后,就不应再访问 cdev结构了。
在内部,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 */
};
我们在遇到它们时讨论结构中的各个成员,但是现在我们关注于 cdev,我们的设备与内核接口的 struct cdev,这个结构必须初始化并且如上所述添加到系统中,处理这个任务的 scull 代码是:
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 来执行该结构的初始化。
没有更新到 2.6 内核接口的老代码,注册一个字符设备的经典方法是使用:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
这里,major 是设备的主编号,name 是驱动的名称(出现在 /proc/devices),fops 是缺省的 file_operations 结构。一个对 register_chrdev 的调用将为给定的主编号注册 0 - 255 作为次编号,并且为每一个建立一个缺省的 cdev 结构。
如果使用 register_chrdev,从系统中去除你的设备的正确的函数是:
int unregister_chrdev(unsigned int major, const char *name);
major 和 name 必须和传递给 register_chrdev 的相同,否则调用会失败。
在大部分驱动中,open 应当进行下面的工作:
open 方法的原型是:
int (*open)(struct inode *inode, struct file *filp);
其中的 inode 参数在其 i_cdev 字段中包含了我们所需要的信息,即我们先前设置的 cdev 结构。唯一的问题是,我们通常不需要 cdev 结构本身,而是希望得到包含 cdev 结构的 scull_dev 结构,可以通过定义在
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 */
一旦它找到 scull_dev 结构,scull 在文件结构的 private_data 成员中存储一个它的指针,为以后更易存取。
scull_open 的代码(稍微简化过)是:
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /*获取设备信息 */
dev = container_of(inode->i_cdev, struct scull_dev, cdev); //调用container_of宏,通过cdev成员得到包含该cdev的scull_dev结构
filp->private_data = dev; /*将得到的scull_dev结构保存在filp->private_data中,因为open结束后,后面的read,write等操作使用同一个filp变量,它们即可以从filp->private_data中直接取出scull_dev结构体来使用。*/
/*如果scull设备文件是以只写的方式打开,则要调用scull_trim将scull设备清空。up(&dev->sem)是进行加锁解锁操作,进行互斥。*/
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
scull_trim(dev);
up(&dev->sem);
}
return 0; /* 打开成功 */
}
release 方法的作用正好与 open 相反,这个设备方法都应该完成下面的任务;
scull 的基本形式没有需要关闭的硬件,因此所需的代码量最少:
/*
*这个函数直接返回0。因为scull设备是内存设备,关闭设备时也没有什么需要特别
*理的,所以这个函数比较简单。
*/
int scull_release(struct inode *inode, struct file *filp)
{
return 0;
}
你可能想知道当一个设备文件关闭次数超过它被打开的次数会发生什么? 答案很简单: 不是每个 close 系统调用引起调用 release 方法,只有真正释放设备数据结构的调用会调用这个方法。
scull 使用的内存区,也称为一个设备,长度可变,你写的越多,它增长越多;用更短的文件以覆盖方式写设备时则会变短。
scull 驱动引入 2 个核心函数来管理 Linux 内核中的内存,这些函数定义在
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
对 kmalloc 的调用试图分配 size 字节的内存,返回值是指向那个内存的指针或者如果分配失败为 NULL,flags 参数用来描述内存应当如何分配,对于现在,我们一直使用 GFP_KERNEL。分配的内存应当用 kfree 来释放,我们不应该将非 kmalloc 返回的指针传递给 kfree,但是,将 NULL指针传递给 kfree 是合法的。
在 scull 中,每个设备都是一个指针链表,其中每个指针都指向一个 scull_qset 结构,每个这样的结构,默认的, 通过一个中间指针数组最多指向 4 兆字节,发行代码使用了一个 1000 个指针的数组,每个指针指向一个 4000 字节的区域,我们称每个内存区域为一个量子,而这个指针数组(或者它的长度)称为量子集,scull 设备和它的内存区如图所示:
选择的参数使得向 scull 中写入一个字节消耗 8000 或 12000 个字节的内存:每个量子占用 4000 个字节,而一个量子集占用 4000 或 8000 个字节(根据指针在目标平台上是用 32 位还是 64 位表示)。
为量子和量子集选择合适的值是一个策略问题而非机制问题,在 scull 中,用户可以掌管改变这些值,有几个途径:编译时间通过改变 scull.h 中的宏 SCULL_QUANTUM 和 SCULL_QSET,在模块加载时设定整数 scull_quantum 和 scull_qset,或者使用 ioctl 在运行时改变当前值和缺省值。
我们已经看到内部表示 scull 设备的 scull_dev 结构,该结构的 quantum 和 qset 字段分别代表设备的量子和量子集大小,但是,实际的数据是由另外的结构跟踪,我们称为 struct scull_qset:
struct scull_qset {
void **data;
struct scull_qset *next;
};
下面代码片段展示了实际中 struct scull_dev 和 struct scull_qset 是如何被用来持有数据的,sucll_trim 函数负责释放整个数据区,由 scull_open 在文件为写而打开时调用。它简单地遍历链表,并且释放它发现的任何量子和量子集:
int scull_trim(struct scull_dev *dev)
{
struct scull_qset *next, *dptr;
int qset = dev->qset; /* "dev" is not-null */
int i;
for (dptr = dev->data; dptr; dptr = next) { /* dev->data指向第一个量子集scull_qset,所以这个for循环每次循环处理一个scull_qset。 */
if (dptr->data) {
for (i = 0; i < qset; i++) //这个for循环循环1000次,因为每个量子集有1000个量子。
kfree(dptr->data[i]); //每次kfree释放一个量子的内存空间。
kfree(dptr->data); //释放量子集数组占用的内存空间。
dptr->data = NULL; //将指针重新初始化为NULL。防止野指针。
}
next = dptr->next; //next指向下一个量子集。
kfree(dptr); //释放scull_qset占用的内存空间。
}
/*
*恢复初始状态。
*/
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->data = NULL;
return 0;
}
scull_trim 也用在模块清理函数中,来归还 scull 使用的内存给系统。
read 和 write 方法都进行类似的任务,就是拷贝数据到应用程序空间和反过来到应用程序中拷贝数据,因此它们的原型相当相似:
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
对于 2 个方法:
需要再次强调的是,read 和 write 方法的 buff 参数是用户空间指针,因此,它不能被内核代码直接解引用。
scull 中的 read 和 write 代码要做的工作就是在用户地址空间和内核地址空间之间进行整段数据的拷贝,这个能力由下列内核函数提供的,它们拷贝一个任意的字节序列,这也是大多数 read 和 write 方法实现的核心部分:
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);
这 2 个函数的作用不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效,如果指针无效,就不会进行拷贝;另一方面,如果在拷贝中遇到一个无效地址,则只会复制部分数据。
scull_read
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;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
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;
item = (long)*f_pos / itemsize; // item代表要读的数据起始点在哪个scull_qset中
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum; //s_pos代表要读的数据起始点在哪个量子中,q_pos代表要读的数据的起始点在量子的具体哪个位置
dptr = scull_follow(dev, item); /*调用scull_follow函数,这个函数的第二个参数代表要读的数据在哪个scull_qset中,该函数的作用是返回item指定的scull_qset。如果scull_qset不存在,还要分配内存空间,创建指定的scull_qset。*/
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos]) //如果指定的scull_qset不存在,或者量子指针数组不存在,或者量子不存在,都退出。
goto out;
/* 设置scull_read一次最多只能读一个量子 */
if (count > quantum - q_pos)
count = quantum - q_pos;
/*调用copy_to_user(buf, dptr->data[s_pos] + q_pos, count)函数,将数据拷贝到用户空间。*/
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count; //读取完成后,新的文件指针位置向前移动count个字节
retval = count;
out:
up(&dev->sem);
return retval; //返回读取到的字节数,即count。
}
read 的返回值的解释:
scull_write
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;
if (down_interruptible(&dev->sem)) //获得互斥锁
return -ERESTARTSYS;
item = (long)*f_pos / itemsize; // item代表要写的数据起始点在哪个scull_qset中
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum; //s_pos代表要写的数据起始点在哪个量子中,q_pos代表要写的数据的起始点在量子的具体哪个位置
/*调用scull_follow函数,这个函数的第二个参数代表要读的数据在哪个scull_qset中,
该函数的作用是返回item指定的scull_qset。*/
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;
}
/* 限定一次最多只能写满一个量子。 */
if (count > quantum - q_pos)
count = quantum - q_pos;
/*调用copy_from_user,将用户数据写到量子中。*/
if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count; //将文件指针后移count字节。
retval = count; //设置返回值为count,即写入字节数。
/* 更新文件大小。 */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return retval;
}
与read类似,write也能传送少于要求的数据,根据返回值规则:
unix 系统很早就支持了两个可选的系统调用:readv 和 writev。这些“向量”型的函数具有一个结构数组,每个结构包含一个指向缓冲区的指针和一个长度值,readv 调用可用于将指定数量的数据依次读入每个缓冲区。writev 则是把各个缓冲区的内容搜集起来,并将它们在一次写入操作中进行输出。
向量操作的原型:
ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
这里,filp 和 ppos 参数与 read 和 write 的相同,iovec 结构,定义于
struct iovec
{
void __user *iov_base; __kernel_size_t iov_len;
};
每个 iovec 描述了一块要传送的数据;它开始于 iov_base (在用户空间)并且有 iov_len字节长,count 参数告诉有多少 iovec 结构,这些结构由应用程序创建,但是内核在调用驱动之前拷贝它们到内核空间。
前面了解了字符设备程序的一些基本操作方法,但是要使用这些方法的话,还需要给创建出来的设备分配设备节点才行,而创建设备节点又有两种方式:
手动创建设备节点的话,顾名思义,是在命令行敲命令来给新创建的设备分配设备节点,命令为:
mkmod 设备名称 主设备号 次设备号
创建完设备节点后,就可以通过open、read、write方法来对设备进行操作了。
自动创建设备节点,就是在代码里调用device_create()或device_register()或device_add()方法来创建设备节点,这三个方法位于
如果想尝试比较简单的字符驱动实现,可以查看我的另一篇驱动开发之 hello 驱动程序实现的文章:
荔枝派Zero(全志V3S)驱动开发之hello驱动程序
我的qq:2442391036,欢迎交流!