📄 x571.html
字号:
<HTML
><HEAD
><TITLE
>Character Device Drivers</TITLE
><META
NAME="GENERATOR"
CONTENT="Microsoft FrontPage 4.0"><LINK
REL="HOME"
TITLE="The Linux Kernel Module Programming Guide"
HREF="index.html"><LINK
REL="UP"
TITLE="Character Device Files"
HREF="c569.html"><LINK
REL="PREVIOUS"
TITLE="Character Device Files"
HREF="c569.html"><LINK
REL="NEXT"
TITLE="The /proc File System"
HREF="c714.html"></HEAD
><BODY
CLASS="SECT1"
BGCOLOR="#FFFFFF"
TEXT="#000000"
LINK="#0000FF"
VLINK="#840084"
ALINK="#0000FF"
><DIV
CLASS="NAVHEADER"
><TABLE
SUMMARY="Header navigation table"
WIDTH="100%"
BORDER="0"
CELLPADDING="0"
CELLSPACING="0"
><TR
><TH
COLSPAN="3"
ALIGN="center"
>Linux内核驱动模块编程指南 (内核版本2.2, 2.4)</TH
></TR
><TR
><TD
WIDTH="10%"
ALIGN="left"
VALIGN="bottom"
><A
HREF="c569.html"
ACCESSKEY="P"
>返回</A
></TD
><TD
WIDTH="80%"
ALIGN="center"
VALIGN="bottom"
>第四章. 字符设备文件</TD
><TD
WIDTH="10%"
ALIGN="right"
VALIGN="bottom"
><A
HREF="c714.html"
ACCESSKEY="N"
>继续</A
></TD
></TR
></TABLE
><HR
ALIGN="LEFT"
WIDTH="100%"></DIV
><DIV
CLASS="SECT1"
><H1
CLASS="SECT1"
><A
NAME="AEN571"
></A
>4.1. 字符设备文件</H1
><DIV
CLASS="SECT2"
><H2
CLASS="SECT2"
><A
NAME="AEN576"
></A
>4.1.1. 关于<SPAN
CLASS="TYPE"
>file_operations</SPAN
>结构体</H2
><P
>结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。</P
><P
>举个例子,每个字符设备需要定义一个用来读取设备数据的函数。结构体file_operations中存储着内核模块中执行这项操作的函数的地址。一下是该结构体在内核2.4.2中看起来的样子:</P
><TABLE
BORDER="1"
BGCOLOR="#E0E0E0"
WIDTH="100%"
><TR
><TD
><FONT
COLOR="#000000"
><PRE
CLASS="SCREEN"
> struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_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 *);
};
</PRE
></FONT
></TD
></TR
></TABLE
><P
>驱动内核模块是不需要实现每个函数的。像视频卡的驱动就不需要从目录的结构中读取数据。那么,相对应的file_operations重的项就为NULL。</P
><P
>gcc还有一个方便使用这种结构体的扩展。你会在较现代的驱动内核模块中见到。新的使用这种结构体的方式如下:</P
><TABLE
BORDER="1"
BGCOLOR="#E0E0E0"
WIDTH="100%"
><TR
><TD
><FONT
COLOR="#000000"
><PRE
CLASS="SCREEN"
> struct file_operations fops = {
read: device_read,
write: device_write,
open: device_open,
release: device_release
};
</PRE
></FONT
></TD
></TR
></TABLE
><P
>同样也有C99语法的使用该结构体的方法,并且它比GNU扩展更受推荐。我是用的版本为2.95的gcc同样也支持C99语法。为了方便那些想移植你的代码的人,你最好使用这种语法。它将提高代码的兼容性:</P
><TABLE
BORDER="1"
BGCOLOR="#E0E0E0"
WIDTH="100%"
><TR
><TD
><FONT
COLOR="#000000"
><PRE
CLASS="SCREEN"
> struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
</PRE
></FONT
></TD
></TR
></TABLE
><P
>这种语法很清晰,你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。</P
><P
>指向结构体file_operations的指针通常命名为fops。</P
></DIV
><DIV
CLASS="SECT2"
><H2
CLASS="SECT2"
><A
NAME="AEN601"
></A
>4.1.2. 关于<SPAN
CLASS="TYPE"
>file结构体</SPAN
> </H2
><P
>每一个设备文件都代表着内核中的一个file结构体。该结构体在头文件<TT
CLASS="FILENAME"
>linux/fs.h</TT
>定义。注意,file结构体是内核空间的结构体,这意味着它不会在用户程序的代码中出现。它绝对不是在glibc中定义的FILE。FILE自己也从不在内核空间的函数中出现。它的名字确实挺让人迷惑的。它代表着一个抽象的打开的文件,但不是那种在磁盘上用结构体inode表示的文件。</P
><P
>指向结构体 <TT
CLASS="VARNAME"
>file </TT
>的指针通常命名为 <TT
CLASS="FUNCTION"
>filp</TT
>。 你同样可以看到 <TT
CLASS="VARNAME"
>struct file file</TT
>的表达方式。但不要被它诱惑。</P
><P
> </P
><P
>去看看结构体 <TT
CLASS="FUNCTION"
>file</TT
> 的定义。 大部分的函数入口,像结构体
<TT
CLASS="FUNCTION"
> dentry</TT
>
并没有被设备驱动模块使用,你大可忽略它们。这是因为设备驱动模块并不自己直接填充结构体 <TT
CLASS="VARNAME"
>file</TT
> ; 它们只是使用在别处建立的结构体 <TT
CLASS="VARNAME"
>file</TT
> 中的数据。</P
></DIV
><DIV
CLASS="SECT2"
><H2
CLASS="SECT2"
><A
NAME="AEN623"
></A
>4.1.3. 注册一个设备</H2
><P
>如同先前讨论的,字符设备通常通过在路径
<TT
CLASS="FILENAME"
>/dev </TT
><A
NAME="AEN632"
HREF="#FTN.AEN632"
><SPAN
CLASS="footnote"
>[1]</SPAN
></A
>
设备文件访问。主设备号告诉你哪些驱动模块是用来操纵哪些硬件设备的。从设备号是驱动模块自己使用来区别它操纵的不同设备,当此驱动模块操纵不只一个设备时。
</P
><P
>将内核驱动模块加载入内核意味着要向内核注册自己。这个工作是和驱动模块获得主设备号时初始化一同进行的。你可以使用头文件<TT
CLASS="FILENAME"
> linux/fs.h </TT
>中的函数 <TT
CLASS="FUNCTION"
>register_chrdev</TT
> 来实现。
</P
><TABLE
BORDER="1"
BGCOLOR="#E0E0E0"
WIDTH="100%"
><TR
><TD
><FONT
COLOR="#000000"
><PRE
CLASS="SCREEN"
> int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops);
</PRE
></FONT
></TD
></TR
></TABLE
><P
> </P
><P
>其中 <TT
CLASS="VARNAME"
>unsigned int major</TT
> 是你申请的主设备号,<TT
CLASS="VARNAME"
>const char
*name</TT
> 时将要在文件 <TT
CLASS="FILENAME"
>/proc/devices</TT
> 出现的设备的名字,<TT
CLASS="VARNAME"
>struct
file_operations *fops</TT
> 是指向你的驱动模块的 <TT
CLASS="VARNAME"
>file_operations</TT
>
表的指针。负的返回值意味着注册失败。注意注册并不需要提供从设备号。内核本身并不在意从设备号。 </P
><P
>现在的问题是你如何申请到一个没有被使用的主设备号?最简单的方法是查看文件 <TT
CLASS="FILENAME"
>Documentation/devices.txt</TT
>
并从中挑选一个没有被使用的。这不是一劳永逸的方法因为你无法得知该主设备号在将来会被占用。最终的方法是让内核为你动态分配一个。</P
><P
>如果你向函数 <TT
CLASS="FUNCTION"
>register_chrdev</TT
> 传递为0的主设备号,那么返回的就是动态分配的主设备号。副作用就是既然你无法得知主设备号,你就无法预先建立一个设备文件。
有多种解决方法。第一种方法是新注册的驱动模块会输出自己新分配到的主设备号,所以我们可以手工建立需要的设备文件。第二种是利用文件
<TT
CLASS="FILENAME"
>/proc/devices</TT
>
新注册的驱动模块的入口,要么手工建立设备文件,要么编一个脚本去自动读取该文件并且生成设备文件。第三种是在我们的模块中,当注册成功时,使用
<TT
CLASS="FUNCTION"
>mknod</TT
> 系统调用建立设备文件并且调用 rm
删除该设备文件在驱动模块调用函数
<TT
CLASS="FUNCTION"
>cleanup_module</TT
> 前。</P
></DIV
><DIV
CLASS="SECT2"
><H2
CLASS="SECT2"
><A
NAME="AEN653"
></A
>4.1.4. 注销一个设备</H2
><P
>即使时root也不能允许随意卸载内核模块。当一个进程已经打开一个设备文件时我们卸载了该设备文件使用的内核模块,我们此时再对该文件的访问将会导致对已卸载的内核模块代码内存区的访问。幸运的话我们最多获得一个讨厌的错误警告。如果此时已经在该内存区加载了另一个模块,倒霉的你将会在内核中跳转执行意料外的代码。结果是无法预料的,而且多半是不那么令人愉快的。</P
><P
>平常,当你不允许某项操作时,你会得到该操作返回的错误值(一般为一负的值)。但对于无返回值的函数 <TT
CLASS="FUNCTION"
>cleanup_module</TT
>
这是不可能的。然而,却有一个计数器跟踪着有多少进程正在使用该模块。你可以通过查看文件 <TT
CLASS="FILENAME"
>/proc/modules</TT
>
的第三列来获取这些信息。如果该值非零,则卸载就会失败。你不需要在你模块中的函数 <TT
CLASS="FUNCTION"
>cleanup_module</TT
> 中检查该计数器,因为该项检查由头文件<TT
CLASS="FILENAME"
>linux/module.c </TT
>中定义的系统调用 <TT
CLASS="FUNCTION"
>sys_delete_module</TT
>
完成。你也不应该直接对该计数器进行操作。你应该使用在文件
<TT
CLASS="FILENAME"
>linux/modules.h</TT
> 定义的宏来增加,减小和读取该计数器:</P
><P
></P
><UL
><LI
><P
><TT
CLASS="VARNAME"
>MOD_INC_USE_COUNT</TT
>: Increment the use count.</P
></LI
><LI
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -