当对幸福的憧憬过于急切,那痛苦就在人的心灵深处升起。——加缪

本章的目的是编写一个完整的字符设备驱动。我们开发一个字符驱动是因为这一类适合大部分简单的硬件设备。字符驱动也比块驱动易于理解。本章的最终目的是编写一个模块化的字符驱动,但是我们不会在本章讨论模块化的事情。
贯穿本章,我们展示从一个真实设备驱动提取的代码片段:scull(Simple Character Utility for Loading Localities)。scull是一个字符驱动,操作一块内存区域好像它是一个设备。在本章,因为scull的这个怪特性,我们可互换地使用设备这个词和“scull使用的内存区”。

scull的优势在于它不依赖硬件。scull只是操作一些从内核分配的内存。任何人都可以编译和运行scull,并且scull在Linux运行的体系结构体中可移植。另一方面,这个设备除了演示内核与字符驱动的接口和允许用户运行一些测试之外,不做任何有用的事情。

3.1 scull的设计

编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制)。因为我们的“设备”是计算机内存的一部分,我们可以自由做我们想做的事情。它可以是一个顺序的或者随机存取的设备,一个或多个设备,等等。

为使scull作为一个模板来编写真实设备的真实驱动,我们将展示给你如何在计算机内存上实现几个设备的抽象,每个有不同的个性。

scull源码实现下面的设备,模块实现的每种设备都被引用做一种类型。

scull0到scull3
4个设备,每个由一个全局永久内存区组成。全局意味着如果设备被多次打开,设备中含有的数据由所有打开它的文件描述符共享。永久意味着如果设备关闭又重新打开,数据不会丢失。这个设备用起来有意思,因为它可以用惯常的命令来存取和测试,例如cpcat、以及I/O重定向。

scullpipe0 到scullpipe3
4个FIFO(先入先出)设备,行为像管道。一个进程度的内容来自另一个进程所写的。如果多个进程读同一个设备,它们竞争数据。scullpipe的内部将展示阻塞读写与非阻塞读写如何实现,而不必采取中断。尽管真实的驱动使用硬件中断来同步它们的设备,阻塞和非阻塞操作的主题是重要的,并且与中断处理是分开的。(在第十章涉及)

scullsingle
scullpriv
sculluid
scullwuid
这些设备与scull0相似,但是在什么时候允许打开上有一些限制。第一个(scullsingle)只允许一次一个进程使用驱动,而scullpriv对每个虚拟终端(或X中断会话)是私有的,因为每个控制台/终端上的进程有不同的内存区。sculluid和scullwuid可以多次打开,但是一次只能是一个用户;前者返回一个“设备忙”错误,如果另一个用户锁着设备,而后者实现阻塞打开。这些scull的变体可能看来混淆了策略和机制,但是它们值得看看,因为一些实际设备需要这类管理。

每个scull设备演示了驱动的不同特色,并且呈现了不同的难度。本章涉及scull0到scull3的内部;更高级的设备在第6章涉及。scullpipe在“一个阻塞I/O例子”一节中描述,其他的在“设备文件上的存取控制”中描述。

3.2 主次编号

字符设备通过文件系统中的名字。那些名字成为文件系统的特殊文件或者设备文件,或者文件系统的简单结点;惯例上它们位于/dev目录。字符驱动的特殊文件由使用ls -l的输出的第一列“c”标识。块设备也出现在/dev中,但是它们由“b”表示。本章集中在字符设备,但是下面的很多信息也使用于块设备。

如果你发出ls -l命令,你会看到在设备文件项中有2个数(由一个逗号分隔),在最后修改日期前面,这里通常是文件长度出现的地方。这些数字是给特殊设备的主次设备编号。下面的列表显示了一个典型系统上出现的几个设备。它们的主编号是1,4,7,10,而次编号是1,3,5,64,65和129。

crw-rw-rw- 1 root root 1,  3 Apr 11  2002 null
crw------- 1 root root 10, 1 Apr 11  2002 psaux
crw------- 1 root root 4,  1 Oct 28 03:04 tty1
crw-rw-rw- 1 root tty  4, 64 Apr 11  2002 ttys0
crw-rw---- 1 root uucp 4, 65 Apr 11  2002 ttyS1
crw--w---- 1 vcsa tty  7,  1 Apr 11  2002 vcs1
crw--w---- 1 vcsa tty  7,129 Apr 11  2002 vcsa1
crw-rw-rw- 1 root root 1,  5 Apr 11  2002 zero

传统上,主编号标识设备相连的驱动。例如,/dev/null/dev/zero都由驱动1来管理,而虚拟控制台和串口终端都由驱动4管理;同样vcs1和vcsa1设备都由驱动7管理。现代Linux内核允许多个驱动共享主编号,但是你看到的大部分设备仍然按照一个主编号一个驱动的原则来组织。
次编号被内核用来决定引用哪个设备。依据你的驱动是如何编写的(如同我们下面见到的),你可以从内核得到一个你的设备的直接指针,或者可以自己使用次编号作为本地设备数组的索引。不论哪个方法,内核自己几乎不知道次编号的任何事情,除了它们指向你的驱动实现的设备。

3.2.1 设备编号的内部表示

在内核中,dev_t类型(在<linux/types.h>中定义)用来持有设备编号——主次部分都包括。对于2.6.0内核,dev_t是32位的量,12位用作主编号,20位用作次编号。你的代码应当对于设备编号的内部组织从不做任何假设;相反,应当利用在<linux/kdev_t.h>中的一套宏定义获得一个dev_t的主编号或次编号,使用。

MAJOR(dev_t dev)
MINOR(dev_t dev)

相反,如果你有主次编号,需要将其转换为一个dev_t使用:

MKDEV(int major, int minor)

注意,2.6内核能容纳有大量设备,而以前的内核版本限制在255个主编号和255个次编号。有人认为这么宽的范围在很长时间内是足够的,但是计算机领域被这个特性的错误假设搞乱了。因此,你应当希望dev_t的格式将来可能再次改变;但是,如果你仔细写你的驱动,这些变化不会是一个问题。

3.2.2分配和释放设备编号
在建立一个字符驱动时你的驱动需要做的第一件事是获取一个或多个设备编号来使用。为此目的的必要的函数是register_chrdev_region,在<linux/fs.h>中声明:

int register_chrdev_region(dev_t first, unsigned int count, char *name);

这里first是你要分配的起始设备编号。first的次编号部分常常是0,但是没有要求是那个效果。count是你请求的连续设备编号的总数。注意,如果count太大,你要求的范围可能溢出到下一个次编号;但是只要你要求的编号范围可用,一切都仍然会正确工作。最后,name是应当连接到这个编号范围的设备的名字;它会出现在/proc/devicessysfs中。

如同大部分内核函数,如果分配成功进行,resigister_chrdev_region的返回值是0,出错的情况下,返回一个负的错误码,你不能存取请求的区域。

如果你确实事先知道你需要哪个设备编号,register_chrdev_region工作得好。然而,你常常不会知道你的设备使用哪个主编号;在Linux内核开发社团中一直努力使用动态分配设备编号。内核会乐于动态为你分配一个主编号,但是你必须使用一个不同的函数来请求这个分配。

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, usigned int count, char *name);

使用这个函数,dev是一个只输出的参数,它在函数成功完成时持有你的分配范围的第一个数。firstminor应当是请求的第一个要用的次编号;它常常是0。countname参数如同给request_chrdev_region的一样。

不管你以任何方式分配你的设备编号,你应当在不使用它们的时候释放它。设备编号的释放使用:

void unregister_chrdev_region(dev_t first, unsigned int count);

调用unregister_chrdev_region的地方常常是你的模块的cleanup函数。
上面的函数分配设备编号给你的驱动使用,但是它们不告诉内核你实际上会对这些编号做什么。在用户空间程序能够存取这些设备号中一个之前,你的驱动需要连接它们到它的实现设备操作的内部函数上。我们将描述如何简短完成这个连接,但首先估计一些必要的枝节问题。

3.2.3 主编号的动态分配

一些主设备编号是静态分派给最普通的设备的。一个这些设备的列表在内核源码树的Documentation/deviecs.txt中。分配给你的新驱动使用一个已经分配的静态编号的机会很小,但是,并且新编号没在分配。因此,作为一个驱动编写者,你有一个选择:你可以简单地捡一个看起来没有用的编号,或者你以动态方式分配主编号。只要你是你的驱动的唯一用户就可以建一个编号用;一旦你的驱动更广泛的被使用了,一个随机捡来的主编号将导致冲突和麻烦。
因此,对于新驱动,我们强烈建议你使用动态分配来获取你的主设备编号,而不是随机选取一个当前空闲的编号。换句话说,你的驱动应当几乎肯定地使用alloc_chrdev_region,不是register_chrdev_region
动态分配的缺点是你无法提前创建设备节点,因为分配给你的模块的主编号会变化,对于驱动的正常使用,这不是问题,因为一旦编号分配了,你可以从/proc/devices中读取它。
为使用动态主编号来加载一个驱动,因此,可使用一个简单的脚本来代替调用insmod,在调用insmod后,读取/proc/devices来创建特殊文件。
一个典型的/proc/devices文件看来如下:

Character devices:
1 mem
2 pty
3 ttyp
4 ttyS
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
180 usbBlock devices:
2 fd
8 sd
11 sr
65 sd
66 sd

因此,加载一个已经安排了一个动态编号的模块的脚本,可以使用一个工具来编写,如awk,来从/proc/devices获取信息以创建/dev中的文件。

下面的脚本,snull_load,是scull发布的一部分。以模块发布的驱动的用户可以从系统的rc.local文件中调用这样一个脚本,或者在需要模块时手工调用它。

#!/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拥有。许可位缺省的是只有root有写权限,而任何人可以读。通常,一个设备节点需要一个不同的存取策略,因此在某些方面别人的存取权限必须改变。我们的脚本缺省是给一个用户组存取,但是你的需求可能不同。在第6章的“设备文件的存取控制”一节中,sculluid的代码演示了驱动如何能够强制它自己的对设备存取的授权。

还有一个scull_unload脚本来清理/dev目录并去除模块。

除了用一对脚本实现加载与卸载,还可编写一个init脚本,放在发布的脚本目录中。作为scull源码的一部分,这里提供了一个相当完整且可配置的init脚本示例,称为scull.init。(init脚本scull.init不在命令行中接收驱动选项,但是它支持一个配置文件,因为它被设计为在启动与关机时自动使用)该脚本接收传统的参数start,stop与restart,并且完成scull_loadscull_unload的角色。

如果反复创建与销毁/dev节点,听起来过分了,有一个有用的办法。如果你在加载和卸载单个驱动,你可以在你第一次使用你的脚本创建特殊文件之后,只使用rmmodinsmod,这样动态编号不是随机的。并且你每次都可以使用所选的同一个编号,如果你不加载任何别的动态模块。在开发中避免长脚本是有用的。但是这个技巧,显然不能扩展到一次多于一个驱动。

安排主编号最好的方式,我们认为是缺省使用动态分配,而留给自己加载时指定主编号的选项权,或者甚至在编译时。scull实现以这种方式工作;它使用一个全局变量scull_major来持有特定的编号(还有一个scull_minor给次编号)。这个变量初始化为SCULL_MAJOR,定义在scull.h。发布的源码中的SCULL_MAJOR的缺省值是0,意思是“使用动态分配”。用户可以接受缺省值或者选择一个特殊的主编号,或者在编译前修改宏定义或者在insmod命令行指定一个scull_load的命令行传递参数给insmod

这是我们用在scull的源码中获取主编号的代码:

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;
}

3.3 一些重要的数据结构

注册设备编号仅仅是驱动代码的诸多任务中的第一项。我们将很快看到其他重要的驱动组件,但在这之前需要一些其他准备。大部分的基础性的驱动操作包括3个重要的内核数据结构,成为file_operationsfileinode。需要对这些结构有基本了解才能够做大量有意思的事情。因此我们在探究基础性驱动操作的实现细节之前,快速地查看这三个内核数据结构。

3.3.1 文件操作

至此,我们已保留了一些设备编号来使用,但是还没有连接任何我们设备的操作到这些编号上。file_operations结构是一个字符驱动建立这种连接的结构,定义在<linux/fs.h>中,是一个函数指针的集合。每个打开文件(内部用一个file结构来代表,稍后我们会查看)与它自身的函数集合相关联(通过包含一个称为f_op的成员,它指向一个file_operations结构)。这些操作大部分负责实现系统调用,因此命名为open、read等等。我们可以认为文件是一个“对象”并且其上的函数操作称为它的“方法”,使用面向对象编程的术语类表示一个对象声明的用来操作对象的动作。这是我们在linux内核中看到的第一个面向对象编程的现象,后续章节中我们会看到更多。

传统上,一个file_operations结构或者其一个指针成为fops(或者它的一些变体)。结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作,或者对于不支持的操作留置为NULL。当指定为NULL指针时内核的确切的行为对于每个函数来说是不同的,如同本节后面的列表所示。

下面的列表介绍了一个应用程序能够在设备上调用的所有操作。我们已经试图保持列表简短,这样它可以作为一个参考,只是总结每个操作和在NULL指针使用时的缺省内核行为。

在你通读file_operations方法的列表时,你会注意到不少包含字串__user。这种注解是一种文档形式,注意,一个指针是一个不能被直接解引用的用户空间地址。对于正常的编译,__user没有效果,到那时它可被外部检查软件使用来找出对用户空间地址的错误使用。

本章剩下的部分,在描述一些其他重要数据结构后,解释了最重要的操作的角色并且给了提示、告诫和真实代码例子。我们推迟讨论更复杂的操作到后面章节,因为我们还不准备深入如内存管理、阻塞操作和异步通知。

struct module *owner

第一个file_operations成员根本不是一个操作,它是一个指向拥有这个结构的模块的指针。这个成员用来在它的操作还在被使用时阻止模块被卸载。几乎所有时间中,它被简单初始化为THIS_MODULE,一个在<linux/module.h>中定义的宏。

loff_t (*llseek) (struct file *, loff_t, int);

llseek方法用作改变文件的当前读写位置,并且新位置作为(正数)返回值。loff_t参数时以个long offset,并且在32位平台上也至少64位宽。错误由一个负返回值指示。如果这个函数指针是NULL,seek调用会以潜在地无法预知的方式修改file结构中的位置计数器(在"file"结构一节中描述)。

ssize_t (*read) (struct file *, char __user*, size_t, loff_t *);

用来从设备中获取数据,在这个位置的一个空指针导致read系统调用以-EINVAL (“Invalid argument”)失败。一个非负返回值代表了成功读取的字节数(返回值是一个"signed size"类型,常常是目标平台本地的整数类型)。

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t *);

初始化一个异步读,可能在函数返回前不结束读操作。如果这个方法是NULL,所有的操作会由read代替进行(同步的)。

ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);

发送数据给设备。如果NULL, -EINVAL返回给调用write系统调用的程序。如果非负,返回值带你表成功写的字节数。

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 *, struct file *, unsigned int, unsigned long);

ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读,也不是写)。另外,几个ioctl命令被内核识别而不必引用fops表。如果设备不提供ioctl方法,对于任何未事先定义的请求(-ENOTTY,“设备五这样的ioctl”),系统调用返回一个错误。

int (*mmap)(struct file *, struct vm_area_struct *);

mmap用来请求将设备内存映射到进程的地址空间。如果这个方法是NULL,mmap系统调用返回-ENODEV。

int (*open)(struct inode *, struct file *);

尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法,如果这个项时NULL,设备打开一直成功,但是你的驱动不会得到通知。

int (*flush)(struct file *);

flush操作在进程关闭它的设备文件描述符的拷贝时调用,它应当执行(并且等待)设备的任何未完成的操作。这个必须不要和用户查询请求的fsync操作混淆了。当前,flush在很少驱动中使用,SCSI磁带驱动使用它,例如,为了确保所有写的数据在设备关闭前写道磁带上。如果flush为NULL,内核简单地忽略用户应用程序的请求。

int (*release)(struct inode *, struct file *);

在文件结构被释放是应用这个操作。如同open,release也可以为NULL。

int (*fsync)(struct file *, struct dentry *, int);

这个方法时fsync系统调用的后端,用户调用来刷新任何挂着的数据。如果这个指针时NULL,系统调用返回-EINVAL。

int (*aio_fsync)(struct kiocb *, int);

这时fsync方法的异步版本。

int (*fasync)(int, struct file *, int);

这个操作用来通知设备它的FASYNC标志的改变。异步通知时一个高级的主题,在第6章中描述。这个成员可以是NULL,如果驱动不支持异步通知。

int (*lock)(struct file *, int, struct file_lock *);

lock方法用来实现文件加锁,加锁对常规文件时必不可少的特性,但是设备驱动几乎从不实现它。

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系统调用的读,是用最少的拷贝从一个文件描述符搬移数据到另一个。例如,它被一个需要发送文件内容到一个网络连接的web服务器使用。设备驱动常常使sendfile为NULL。

ssize_t (*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int);

sendpage是sendfile的另一半。它由内核调用来发送数据,一次一页,到对应的文件。设备驱动实际上不是先sendpage。

unsigned long (*get_unmmaped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中就。这个任务通常由内存管理代码进行,这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求。大部分驱动可以置这个方法为NULL。

int (*check_flags)(int);

这个方法允许模块检查传递给fnctl(F_SETGL…)调用的标志。

int (*dir_notify)(struct file *, unsigned long);

这个方法在应用层使用fcntl来请求目录改变通知时调用。只对文件系统有用,驱动不需要实现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,
};

这个声明使用标准的C标记式结构初始化语法。这个语法是首选的,因为它使驱动在结构定义的改变之间更加可移植,并且,有争议地,使代码更加紧凑和可读。标记式初始化允许结构成员重新排序,在某种情况下,真实的性能提高已经实现,通过安放经常使用的成员的指针在相同硬件高速存储行中。

3.3.2 文件结构

struct file,定义于 <linux/fs.h>是设备驱动中第二个最重要的数据结构。注意file与用户空间长须的FILE指针没有任何关系。一个FILE定义在C库中,从不出现在内核代码中。一个struct file, 另一方面,是一个内核结构,从不出现在用户程序中。

文件结构代表一个打开的文件。(它不特定给设备驱动,系统中每个打开的文件有一个挂念的struct file在内核空间)。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后的关闭。在文件的所有的实例都关闭后,内核释放这个数据结构。

在内核源码中,struct file的指针常常称为file或者filp(“file pointer”)。我们将一直称这个指针为filp以避免和结构自身混淆。因此file指的是结构,而filp是结构指针。

struct file的最重要成员在这展示,如同在前一节,第一次阅读可以跳过这个列表。但是,在本章后面,当我们面对一些真实C代码时,我们将更详细讨论这些成员。

mode_t f_mode;

文件模式确定文件是可读、可写或两者皆可,通过位FMODE_READFMODE_WRITE。你可能想在你的open或者ioctl函数中检查这个成员函数的读写许可,但是你不需要检查读写许可,因为内核在调用你的方法之前检查。当文件还没有为那种存取而打开时读或写的企图被拒绝,驱动甚至不知道这个情况。

loff_t f_pos;

当前读写位置。loff_t在所有平台都是64位(在gcc术语里是long long)。驱动可以读这个值,若它需要知道文件中的当前位置,正常地不应改变它;读与写应当使用它们作为最后参数而收到的指针来更新一个位置,代替直接作用于filp->f_pos。这个规则地一个例外是在llseek方法中,它的目的就是改变文件位置。

unsigned int f_flags;

这些是文件标志,例如O_RDONLY, O_NONBLOCKO_SYNC。驱动应当检查O_NONBLOCK标志类来看是否是请求非阻塞操作(我们在第一章的“阻塞和非阻塞操作”一节中讨论非阻塞I/O);其他标志很少使用。特别地,应当检查读/写许可,使用f_mode而不是f_flags。所有的标志在头文件<linux/fcntl.h>中定义。

struct file_operations *f_op;

和文件关联的操作。内核安排指针作为它的open实现的一部分,接着读取它当它需要分派任何的操作时。file->f_op中的值从不由内核保存为后面的引用,这意味着你可改变你的文件关联的文件操作,在你返回调用者之后新方法会起作用。例如,关联到主编号1(/dev/null/dev/zero等等)的open代码根据打开的次编号来代替filp->f_op中的操作。这个做法允许实现几种行为。在用一个主编号下而不必再每个系统调用中引入开销。替换文件操作的能力时面向对象编程的“方法重载”的内核对等体。

void *private_data

open系统调用设置这个指针为NULL,在为驱动调用open方法之前。你可自由使用这个成员或者忽略它,你可以使用这个成员来指向分配的数据,但是接着你必须记住在内核销毁文件结构之前,在release方法中释放那个内存。private_data是一个有用的资源,在系统调用间保留状态信息,我们大部分例子模块都使用它。

struct dentry *f_dentry;

关联到文件的目录入口(dentry)结构。设备驱动编写者正常地不需要关心dentry结构,除了作为filp->f_dentry->d_inode存取inode结构。

真实结构有多几个成员,但是它们对设备驱动没有用处。我们可以安全地忽略这些成员,因为驱动从不创建文件结构,它们真实存取别处创建的结构。

3.3.3 inode 结构

inode结构由内核在内部表示文件。因此,它和代表打开文件描述符的文件结构是不同的。可能有代表单个文件的多个打开打开描述符的许多文件结构,但是它们都指向一个单个inode结构。

inode结构包含大量关于文件的信息。作为一个通过用的规则,这个结构只有2个成员对于编写驱动代码有用:

dev_t i_rdev;

对于代表设备文件的节点,这个成员包含实际的设备编号。

struct cdev *i_cdev;

struct cdev是内核的内部结构,代表字符设备。这个成员包含一个指针,指向这个结构,当节点指的是一个字符设备文件时。

i_rdev类型在2.5开发系列中改变了,破坏了大量的驱动。作为一个鼓励更可移植编程的方法,内核开发者已经增加了2个宏,可用来从一个inode中获取主次编号:

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

为了不要被下一次改动抓住,应当使用这些宏代替直接操作i_cdev

注意,release不是每次进程调用close时都被调用。无论何时共享一个文件结构(例如,在一个fork或者dup之后),release不会调用直到所有的拷贝都关闭了。如果你需要在任一拷贝关闭时刷新挂着的数据,你应当实现flush方法。

是什么挂着的数据?待解决

3.4 字符设备注册

如我们提过的,内核在内部使用类型struct cdev的结构来代表字符设备。在内核调用你的设备操作前,你编写分配并注册一个或几个这些结构。为此,你的代码应当包含<linux/cdev.h>,这个结构和它的关联帮助函数定义在这里。

有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_operarions结构,struct cdev有一个拥有者成员,应当设置为THIS_MODULE。一旦cdev结构建立,最后的步骤是把它告诉内核,调用:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

这里,devcdev结构,num是这个设备相应的第一个设备号,count是应当关联到设备的设备号的数目。常常count是1,但是有多个设备号对应于一个特定的设备的情形。例如,设想SCSI磁带驱动,它允许用户空间来选择操作模式(例如密度),通过安排多个次编号给每一个物理设备。

在使用cdev_add是由几个重要事情要记住。第一个是这个调用可能失败。如果它返回一个负的错误码,你的设备没有增加到系统中,它几乎会一直成功,但是它的操作,除非你的驱动完全准备好处理设备上的操作,你不应当调用cdev_add。为从系统中去除一个字符设备,调用

void cdev_del(struct cdev *dev);

显然,你不应当在传递给cdev_del后存取cdev结构。

3.4.1 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 */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 struct */
};

我们在遇到结构的各个成员时讨论它们,但是现在,我们关注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 fracefully if need be */if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}

因为cdev结构嵌在struct scull_dev里面,cdev_init必须调用来进行那个结构的初始化。

3.4.2 老方法

如果你深入浏览2.6内核的大量驱动,你可能注意到有许多字符驱动不适用刚刚描述的cdev接口。你见到的是还未更新到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结构。使用这个接口的驱动必须准备好处理对所有256个次编号的open调用(不管让们是否对应真实设备),他们不能使用大于255的主或次编号。

如果你使用register_chrdev,从系统中去除你的设备的正确的函数是:

int unregister_chrdev(unsigned int major, const char *name);

majorname必须和传递给register_chrdev的相同,否则调用会失败。

3.5 open和release

到此我们已经快速浏览了这些成员,我们开始在真实的scull函数中使用它们。

3.5.1 open方法

open方法提供给驱动来做任何的初始化来准备后续的操作。再大部分驱动中,open应当进行下面的工作:
检查设备特定的错误(例如设备没准备好,或者类似的硬件错误)
如果它第一次打开,初始化设备
如果需要,更新f_op指针。
分配并填充要放进filp->private_data的任何数据结构

但是,事情的第一步常常是去顶打开哪个设备,记住open方法的原型是:

int (*open)(struct inode *inode, struct file *filp);

inode参数有我们需要的信息,以它的i_cdev成员的形式,里面包含我们之前建立的cdev结构。唯一的问题是通常我们不想要cdev结构本身,我们需要的是包含cdev结构的scull_dev。C语言使程序员玩弄各种技巧来做这种转换,但是,这种技巧编程是易出错的,并且导致别人难于阅读和理解代码。幸运的是,在这种情况下,内核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 */

一旦它找到scull_dev结构,scull在文件结构的private_data成员中存储一个它的指针,为以后更易存取。

识别打开的设备的另外的方法是查看存储在inode结构的次编号。如果你使用register_chrdev注册你的设备,你必须使用这个技术。确认使用iminorinode结构中获取次编号,并且确定它对应一个你的驱动真正准备好处理的设备。

scull_open的代码(稍微简化过)是:

int scull_open(struct inode *inode, struct file *filp) {struct scull_dev *dev; /* device information */dev = container_of(inode->i_cdev, struct scull_dev, cdev);filp->private_data = dev; /* for other methods *//* now trim to 0 the length of the device if open was write-only */if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {scull_trim(dev); /* ignore errors */}return 0; /* success */
}

代码看起来相当稀疏,因为在调用open时它没有做任何特别的设备处理。。它不需要,因为scull设备设计为全局的和永久的。特别地,没有如“在第一次打开时初始化设备”等动作,因为我们不为scull保持打开计数。

唯一在设备上的真实操作是当设备为写而打开时将它截取为长度为0。这样做是因为,在设计上,用一个短的文件覆盖一个scull设备导致一个短的设备数据区。这类似于为写而打开一个常规文件,将其截短为0。如果设备为读而打开,这个操作什么都不做。

在我们查看其他scull特性的代码时将看到一个真实的初始化如何起作用的。

3.5.2 release方法

release方法的角色是open的反面。又是你会发现方法的实现称为device_close,而不是device_release。任一方式,设备方法应当进行下面的任务:
释放open分配在filp->private_data中的任何东西
在最后的close关闭设备

scull的基本形式没有硬件去关闭,因此需要的代码是最少的:

int scull_release(struct inode *inode, struct file *filp) {return 0;
}

你可能想知道当一个设备文件关闭次数超过它被打开的次数会发生什么。毕竟,dupfork系统调用不调用open来创建打开文件的拷贝。每个拷贝接着在程序终止时被关闭。例如,大部分程序不打开它们的stdin文件(或设备),但是他们都以关闭它结束。当1个打开的设备文件已经真正被关闭时,驱动如何知道?

答案简单:
不是每个close系统调用引起调用release方法,只有真正释放设备数据结构的调用会调用这个方法。内核维持一个文件被使用多少次的计数。forkdup都不创建新文件(只有open这样);它们只递增正存在的结构中的计数。close系统调用仅在文件结构计数掉到0时执行release方法,这在结构被销毁时发生。release方法和close系统调用之间的这种关系保证了你的驱动一次open只看到一次release

注意,flush方法在每次应用程序调用close时都被调用。但是,很少驱动实现flush,因为常常在close时没有什么要做的,除非调用release

如你会想到的,前面的讨论即便是应用程序没有明显地关闭它打开的文件也适用:内核在进程exit时自动关闭了任何文件,通过在内部使用close系统调用。

3.6 scull的内存使用

在介绍读写操作前,我们最好看看如何以及为什么scull内存分配。

  • “如何”是需要全面理解代码本身
  • “为什么”演示了驱动编写者需要做的选择,尽管scull明确地不是典型设备。

本节只处理scull中的内存分配策略,不展示编写真正驱动需要的硬件管理技能。这些技能在第9章和第10章介绍。因此,你可跳过本章,如果你不感兴趣于理解面向内存的scull驱动的内部工作。

scull使用的内存区,也成为一个设备,长度可变。你写的越多,它增长越多;通过使用一个短文件覆盖设备来进行进行修整。

scull驱动引入2个核心函数来管理Linux内核中的内存。这些函数定义在<linux/slab.h>中,分别是:

void *kmalloc(size_t size, int flags);
void kfree(void *ptr);

kmalloc的调用试图分配size字节的内存;返回值使指向那个内存的指针或者如果分配失败为NULLflags参数用来描述内存应当如何分配;我们在第8章详细查看这些标志。对于现在,我们一直使用GFP_KERNEL。分配的内存应当用kfree来释放。你应当从不传递任何不是从kmalloc获得的东西给kfree,但是传递一个NULL指针给kfree是合法的。

kmalloc不是最有效的分配大内存区的方法(见第8章),所以挑选给scull的实现不是一个特别巧妙的。一个巧妙的源码实现可能更难阅读,而本节的目标是展示读和写,不是内存管理。因此,这里只是使用kmalloc和kfree而不依靠整页的分配,尽管这个方法会更有效。

在filp一边,我们不想限制“设备”区的大小。但由于理论上的与实践上的理由。理论上,给在被管理的数据项施加武断的限制总是个坏想法。实践上,scull可用来暂时地吃光你系统的内存,以便运行在低内存条件下的测试。运行这样的测试可能会帮助你理解系统的内部。你可以使用命令cp /dev/zero /dev/scull0 来用scull吃掉所有的真实RAM,并且你可以使用dd工具来选择拷贝多少数据给scull设备。

在scull,每个设备是一个指针链表,每个都指向一个scull_dev结构。每个这样的结构,缺省地,指向最多4兆字节,通过一个中间指针数组。发行代码使用一个1000个指针地数组指向每个4000字节的区域。我们称每个内存区域为一个量子,数组(或者它的长度)为一个量子集。一个scull设备和它的内存区如图所示:

选定的数字是这样,在scull中写单个一个字节消耗8000或12000KB内存:4000是量子,4000或8000是量子集(根据指针在目标平台上是用32位还是64位表示)。相反,如果你写入大量数据,链表的开销不是太坏。每4MB数据只有一个链表元素,设备的最大尺寸受限于计算机内存的大小。

为量子与量子集选择合适的值是一个策略问题,而不是机制,并且优化的值依赖于设备如何使用。因此,scull驱动不应当强制给量子与量子集使用任何特别的值。在scull中,用户可以掌管改变这些值,有几个途径:

  • 编译时间通过改变scull.h中的宏SCULL_QUANTUMSCULL_QSET
  • 在模块加载时设定整数值scull_quantumscull_qset
  • 使用ioctl在运行时改变当前值与缺省值。

使用宏定义和一个整数值来进行编译时和加载时的配置,是对于如何选择主编号的回忆。我们在驱动中任何与策略相关或专断的值上运用这个技术。

余下的唯一问题是如果选择缺省值。在这个特殊情况下,问题是找到最好的平衡,由填充了一半的量子和量子集导致内存浪费,如果量子和量子集小的情况下分配释放和指针连接引起开销。另外,kmalloc的内部设计应当考虑进去。(现在我们不追求这点,不过;kmalloc的内部在第8章探索。)缺省值的选择来自假设测试时可能有大量数据写进scull,尽管设备正常使用最可能只传送几KB数据。

我们已经见过内部代表我们设备的scull_dev结构。结构的quantumqset分别代表设备的量子与量子集大小。实际数据,但是,是由一个不同的结构跟踪,我们称为struct scull_qset

struct scull_qset {void **data;struct scull_qset *next;
}

下一个代码片段展示了实际中struct scull_devstruct scull_qset是如何被用来持有数据的。scull_trim函数负责释放整个数据区,由scull_open在文件为写而打开时调用。它简单地遍历列表并且释放它发现的任何量子与量子集。

int scull_trim(struct scull) {struct scull_qset *next, *dptr;int qset = dev->qset;int i;for (dptr = dev->data; dptr; dptr = next) {/* all the list items */if (dptr->data) {for (i = 0; i < qset; i++) {kfree(dptr->data[i]);}kfree(dptr->data);dptr->data = NULL;}next = dptr->next;kfree(dptr);}dev->size = 0;dev->quantum = scull_quantum;dev->qset = scull_qset;dev->data = NULL;return 0;
}

scull_trim也用在模块清理函数中,来归还scull使用的内存给系统。

3.7 读和写

读和写的方法都进行类似的任务,就是从和到应用程序代码拷贝数据。因此它们的原型相当相似,可以同事介绍它们:

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);

对于两个方法,filp是文件指针,count是请求的传输数据大小,buff参数指向持有被写入数据的缓存,或者放入新数据的空缓存。最后,offp是一个指针指向一个“long offset type”对象,它指出用户正在存取的文件位置。返回值是一个signed size type,它的使用在后面讨论。

让我们重复一下,read与write的buff参数是用户空间指针。因此,它不能被内核代码直接解引用。这个限制有几个理由:

  • 依赖于你的驱动运行的体系,以及内核被如何配置的,用户空间指针当运行于内核模式可能根本是无效的。可能没有那个地址的映射,或者它可能只想一些其他的随机数据。
  • 就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时这个内存可能没有在RAM中,试图直接应用用户空间内存可能产生一个页面错,这是内核代码不允许做的事情,结果可能是一个“oops”,导致进行系统调用的进程死亡。
  • 置疑中的指针由一个用户程序提供,它可能是错误的或者恶意的。如果你的驱动盲目地解引用一个用户提供的指针,它提供了一个打开的门路使用户空间程序存取或覆盖系统任何地方的内存。如果你不想负责你的用户的系统的安全危险,你就不能直接解引用用户空间指针。

显然,你的驱动必须能够存取用户空间缓存以完成它的工作。但是,为安全起见这个存取必须使用特殊的,内核提供的函数。我们介绍几个这样的函数(定义于<asm/uaccess.h>),剩下的在第一章“使用ioctl参数”一节中。它们使用一种特殊的、依赖体系的技巧来确保内核和用户空间的数据传输安全和正确。

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函数,必须加一点小心在从内核代码中从存取用户空间。寻址的用户也当前可能不在内存,虚拟内存子系统会是进程睡眠在这个页被传送到位时。例如,这发生在必须从交换空间获取页的时候。对于驱动编写者来说,最终结果是任何存取用户空间的函数必须是可重入的,必须能够和其他驱动函数并行执行,并且,特别的,必须在一个它能够合法地水面的位置。我们在第5章再回到这个主题。

这两个函数的角色不限于内核空间与用户空间之间来回拷贝数据,它们还检查用户空间指针是否有效。如果指针无效,不进行拷贝; 如果再拷贝中遇到一个无效地址,另一方面,只拷贝部分数据。在2种情况下,返回值时还要拷贝的数据量。scull代码查看这个错误返回,并且如果它不是0就返回-EFAULT给用户。

用户空间存取和无效用户空间指针的主题有些高级,在第6章讨论。如果无需检查用户空间指针,可以调用__copy_to_user__copy_from_user来代替。这是有用处的,例如,如果你确信你已经检查好了这些参数。但是要小心,如果不检查传递给这些函数的用户空间指针,那么可能造成内核崩溃或安全漏洞。

至于实际的设备方法,read方法的任务是从设备拷贝数据到用户空间,而write方法必须从用户空间拷贝数据到设备(使用copy_from_user)。每个read或write系统调用请求特定数目字节的传送,但是驱动可自由传送较少数据,对读和写这确切的规则稍微不同,在本章后面描述。

不管这些方法传送多少数据,他们通常应当更新*offp中的文件位置来表示在系统调用成功完成后当前的文件位置。内核接着在适当时候传播文件位置的改变到文件结构。pread和pwrite系统调用有不同的语义,他们从一个给定的文件偏移操作,并且不改变其他的系统调用看到的文件位置。这些调用传递一个指向用户提供的位置的指针,并且放弃你的驱动所作的改变。

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

尽管内核函数返回一个负数指示一个错误,这个数的值指出所发生的错误类型(如第2章介绍),用户空间运行的程序常常看到-1作为错误返回值。它们需要存取errno变量来找出发生了什么。用户空间的行为由POSIX标准来规定,但是这个标准没有规定内核内部如何操作。

3.7.1 read方法

read的返回值由调用的应用程序解释:

  • 如果这个值等于传递给read系统调用的count参数,请求的字节数已经被传送,这是最好的情况。
  • 如果是正数,但是小于count,只有部分数据被传送,这可能由于几个原因,依赖于设备。常常,应用程序重新试着读取,例如,如果你使用fread函数来读取,库函数重新发出系统调用直到请求的数据传送完成
  • 如果值为0,到达了文件末尾(没有读取数据)。
  • 一个负值表示有一个错误。这个值指出了什么错误,根据<linux/errno.h>,出错的典型返回值包括-EINTR(被打断的系统调用)或者-EFAULT(坏地址)

前面的列表中漏掉的是这种情况“没有数据,但是可能后来到达”。在这种情况下,read系统调用应当阻塞。我们将在第6章谈到阻塞

scull代码利用来这些规则。特别地,它利用了部分读规则。每个scull_read调用只处理单个数据量子,不实现一个循环来收集所有的数据;这使得代码更短更易读。如果程序确实需要更多数据,它重新调用。如果标准I/O库(例如,fread)用来读取设备,应用程序甚至不会注意到数据传送的量子化。

如果当前读取位置大于设备大小,scull的read方法返回0来表示没有可用的数据(换句话说,我们在文件尾),这个情况发生在如果进程A 在读设备,同时进程B打开它写,这样将设备截短为0。进程A突然发现自己过了文件尾,下一个读调用返回0。

这是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_post = rest / quantum;q_post = 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;
}

3.7.2 write方法

write像read,可以传送少于要求的数据,根据返回值的下列规则:

  • 如果值等于count,要求的字节数已被传送。
  • 如果为正数,但是小于count,只有部分数据被传送,程序最可能重试写入剩下的数据。
  • 如果值为0,什么没有写。这个结果不是一个错误,没有理由返回一个错误码。再一次,标准库重试写调用。我们将在第6章查看这种情况的确切含义,那里介绍了阻塞。
  • 一个负值表示发生一个错误;如同对于读,有效的错误值是定义于<linux/errno.h>中。

不幸的是,仍然可能有发出错误消息的不当行为程序,它在进行了部分传送时中止。这是因为一些程序员习惯看写调用要么完全失败要么完全成功,这实际上是大部分时间的情况,应当也被设备支持。scull实现的这个限制可以修改,但是我们不想使代码不必要地复杂。

write的scull代码一次处理单个量子,如read方法做的:

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_pocs = 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;
}

3.7.3 readv与writev

Unix系统已经长时间支持名为readv和writev的2个系统调用。这些read和write的“矢量”版本使用一个结构数据。

Linux设备驱动——第三章字符驱动相关推荐

  1. Linux 设备驱动程序(三)

    系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核(一) 深入理解 Linux 内核(二) Linux 设备驱动程序(一) Linux 设备驱动程序(二) Linux 设备驱动程序( ...

  2. Linux设备驱动程序 第三版 读书笔记(一)

    Linux设备驱动程序 第三版 读书笔记(一) Bob Zhang 2017.08.25 编写基本的Hello World模块 #include <linux/init.h> #inclu ...

  3. linux2.6驱动学习笔记之字符驱动

    1.字符驱动组成 1.1字符驱动的模块加载与卸载 //设备结构体模板 struct xxx_dev_t { struct cdev cdev; ...... }xxx_dev; 在字符驱动模块加载函数 ...

  4. LINUX设备驱动程序第三版.pdf,linux设备驱动程序第3版.pdf免费下载链接,学习资源下载

    LINUX设备驱动程序第三版.pdf免费下载链接(.pdf书籍的优点是便于直接在电脑中保存有电脑就可以阅读,如果觉得这本书给你提供到了很大的帮助,可以去书店补一本纸质版) 资源保存在腾讯微云上,下载不 ...

  5. Linux 驱动开发 三:字符设备驱动框架

    一.参考 (3条消息) Linux 字符设备驱动结构(一)-- cdev 结构体.设备号相关知识解析_知秋一叶-CSDN博客 (3条消息) linux设备驱动框架_不忘初心-CSDN博客_linux设 ...

  6. 上海lin上海linux培训ux,lin教材ux字符驱动设备-学习笔记(最新实例).pdf

    - 1 - 字符驱动开发学习笔记 Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过 这些函数使得Windows 的设备操作犹如文件一般.在应用程序看来,硬件设备只 是一个设备文件, ...

  7. linux驱动开发 | 第一个字符驱动

    一.驱动框架编写 1.编写驱动文件 打开linux内核,全局搜索module_init函数. linux内核中已经有了很多厂家写好的驱动模块,所以我们完全可以参考他们的代码. #include < ...

  8. ESP32设备驱动-ADXL345三轴加速计驱动

    ADXL345三轴加速计驱动 文章目录 ADXL345三轴加速计驱动 1.ADXL345介绍 2.硬件准备 3.软件准备 4.驱动实现 1.ADXL345介绍 ADXL345 是一款小型.薄型.低功耗 ...

  9. 【正点原子Linux连载】第三章 RV1126开发环境搭建 摘自【正点原子】ATK-DLRV1126系统开发手册

    1)实验平台:正点原子RV1126 Linux开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=692176265749 3)全套实验源码+手册+视频 ...

最新文章

  1. 数字下变频和数字上变频
  2. EeePC1000hg安装archlinux20121201和openbox
  3. 使用curl获取Location:重定向后url
  4. nginx服务器绑定域名和设置根目录的方法
  5. IT 拉呱室 | 论我遇到的最刺激的bug【长期福利站】
  6. 计算机台式右上角三个灯作用,键盘右上角的三个灯是什么?有什么用
  7. JMETER Beanshell
  8. 用友凭证打印故障解决
  9. vue对table的某一行的数据进行编辑,删除,查看详情操作
  10. ping丢包故障处理
  11. C++-OpenCV(1)-连通区域
  12. 01 复杂度分析(上):时间、空间复杂度讲解
  13. java mail类_Java Mail工具类
  14. Jetson Nano系列教程3-生死看淡,不服就干之GPIO
  15. 双向可控硅晶片光耦 (TLP160J TLP260J TLP525G) 基本原理及应用实例
  16. 低调,中国的FPGA到底有多强?!
  17. 刀片服务器改台式电脑_一种刀片服务器机箱的制作方法
  18. 怎样学好英语?(多年英语学习经验总结)
  19. 恒太照明在北交所上市:募资规模缩水三成,第三季度收入下滑
  20. 计算机网络思科平台第四章测验答案

热门文章

  1. excel隔行显示不同颜色
  2. 手机翻译html工具,1分钟教你用手机实时翻译,自带翻译功能就是强大,各牌手机均可...
  3. 如何做口碑营销?企业实施网络口碑营销的技巧
  4. 测试用例编写练习(二)
  5. matlab分析具体问题论文,关于Matlab论文范文写作 Matlab在化探异常解释评价中应用相关论文写作资料...
  6. 利用Python+OpenCV对图像加密/解密
  7. VS没有CUDA模块问题
  8. RGB与HSV颜色空间转换
  9. CISP证书价值​NISP证书价值|CISP和NISP含金量如何
  10. redis maser-salve