以下内容只是记录的要点,详细看参考资料:
一、内核模块的编写:
1、内核模块的代码编写没有外部的函数库可以用,只能使用内核导出的函数;这点于应用程序是有区别的,应用程序习惯于使用外部的库函数,在编译的时候将程序与库函数链接在一起。比如说:内核模块中不能使用printf(),而只能使用printk()函数。
2、内核模块至少包含两个函数:模块加载函数、卸载函数;
内核版本2.3.13以前,使用init_module()和cleanup_module作为模块的初始和结束函数; 在内核Linux 2.4中,你可以为你的模块的“开始”和“结束”函数起任意的名字了。它们不再必须使用 init_module()和cleanup_module()的名字。这可以通过宏 module_init()和module_exit()实现。这些宏在头文件kernel../include/linux/init.h定义。唯一需要注意的地方是函数必须在宏的使用前定义,否则会有编译 错误。
3、内核模块包含头文件及宏说明:
#include:它定义了模块的 API、类型和宏(MODULE_LICENSE、MODULE_AUTHOR等等),所有的内核模块都必须包含这个头文件。
#include:使用内核信息优先级时要包含这个文件,一般在使用printk函数时使用到优先级信息。
#include头文件:module_init、module_exit等宏定义。
#include: 在对/proc文件系统的文件目录进行操作时使用到的函数等在这个文件中定义,比如:create_proc_entry()在/proc下创建文件。
接下来解释一下常用宏定义:
MODULE_LICENSE
定义了这个模块使用的许可证。此处,我们定义的是 GPL,从而防止会污染到内核。
二、内核模块的编译(2.6系列内核):
编译模块的时候,你可以将模块放在代码树中,用make modules来编译你的模块;你也可以将模块相关文件目录放在代码树以外的位置,用如下命令来编译模块:
make -C path/to/kernel/source M=$PWD modules
参数说明: -C 指定代码树的位置;M=$PWD或者M=`PWD`告诉kbuild回到当前目录执行build操作。
参考资料: 2.6内核Makefile简单语法与应用
http://blog.chinaunix.net/u/24474/showart_237820.html
在我的机子上运行的内核版本是2.6.22-14-generic,编译内核模块mymodule的步骤如下
内核模块的编译:
1)写内核模块源文件(任意目录), 如mymodule.c
2)在模块源文件目录编写Makefile,内容:obj-m+=mymodule.o
3) 在终端执行: make -C /usr/src/linux版本目录 SUBDIRS=$PWD modules
网上搜到的命令是:
make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules
其中:linux-`uname -r`应该根据自己的实际情况进行调整,比如我的就是linux-headers-2.6.22-14-generic,也就是linux-headers-'uname -r'。
说明:SUBDIRS=..和 M=..是一样的,
疑点:对kbuild的理解?
2.6内核引入了kbuild,将外部的内核模块的编译和内核源码树的编译统一起来了。
三、插入模块:
$insmod ./mymodule.ko
查看信息:demsg | tail -5 , 并且 cat /proc/modules 可看到插入的模块:mymodule。
卸载模块:
$rmmod ./mymodule.ko
参考资料:2.6内核模块的编写框架和编译方法
http://hi.baidu.com/skie/blog/item/87ab59a9b45b46fc1f17a208.html
四、 模块的安装
安装模块。模块的默认安装目录是 /lib/modules/<内核-版本>
当你需要将模块安装到非默认位置的时候,你可以用INSTALL_MOD_PATH 指定一个前缀,如:
make INSTALL_MOD_PATH=/foo modules_install
模块将被安装到 /foo/lib/modules目录下。
四、内核模块的运行:
内核模块运行在内核空间,而应用程序在用户空间。应用程序的运行会形成新的进程,而内核模块一般不会。每当应用程序执行系统调用时,linux执行模式从用户空间切换到内核空间。
五、几个可以用来开发有用LKM(可加载内核模块)的内核API:
要使用/proc文件系统,首先一定要包含procfs的头文件:include(这些函数是在此文件中声明的),其次利用procfs提供的如下API函数。
首先值得一提的是:结构体proc_dir_entry,他的部分成员如下:
struct proc_dir_entry {
const char *name; // virtual file name
mode_t mode; // mode permissions
uid_t uid; // File's user id
gid_t gid; // File's group id
struct inode_operations *proc_iops; // Inode operations functions
struct file_operations *proc_fops; // File operations functions
struct proc_dir_entry *parent; // Parent directory
...
read_proc_t *read_proc; // /proc read function
write_proc_t *write_proc; // /proc write function
void *data; // Pointer to private data
atomic_t count; // use count
...
};
为了创建可读可写的proc文件并指定该proc文件的写操作函数,必须设置下面那些创建proc文件的函数返回指针指向的struct proc_dir_entry结构的write_proc字段,并指定该proc文件的访问权限有写权限。
我们会使用 read_proc
和 write_proc
命令来插入对这个虚拟文件进行读写的函数。
读者可以通过cat和echo等文件操作函数来查看和设置这些proc文件。
- create_proc_entry:
1)原型:
struct proc_dir_entry *create_proc_entry( const char *name, mode_t mode,struct proc_dir_entry *parent );
2)作用:该函数用于在/proc文件系统中创建一个虚拟文件(proc条目)。
3)参数:
name: 给出要创建的文件名称;
mode:给出建立的该proc文件的访问权限;
parent:指定建立的proc文件所在的目录;
如果要在/proc根目录下建立proc文件,parent应当为NULL,否则他应当为proc_mkdir,也就是说我们也可以在自己创建的目录下面再创建虚拟文件.(详细解释见remove_proc_entry).
4)返回struct proc_dir_entry结构的指针,我们可以使用这个返回的指针来配置这个虚拟文件的其他参数,比如对该文件执行读操作时应调用的函数。
若返回NULL,说明create出错。
- remove_proc_entry:
1)原型:
void remove_proc_entry( const char *name, struct proc_dir_entry *parent );
2)作用:用于删除上面函数创建的proc文件。
3)参数:
name:给出要删除的proc文件的名称;
parent:指定创建的proc文件所在的目录。
parent参数可以是NULL(指/proc根目录),也可以取其他值,这取决于我们创建时将这个文件放到了什么地方。可以使用的其他一些值 proc_dir_entry:proc_root_fs、proc_net、proc_bus、proc_root_driver等。这些取值在linux/proc_fs.h中引入,如下:
extern struct proc_dir_entry proc_root; (/proc目录)
extern struct proc_dir_entry *proc_root_fs; (/proc/fs目录)
extern struct proc_dir_entry *proc_net; (/proc/net)
extern struct proc_dir_entry *proc_bus; (/proc/bus)
extern struct proc_dir_entry *proc_root_driver; (/proc/driver)
extern struct proc_dir_entry *proc_root_kcore; (/proc/kcore)
注意:上面的定义中,proc_root和其他变量的类型不同,其他的都是指针,在使用时要注意。利用这些位置值,我们可在/proc文件系统的根目录(/proc)及其内部的目录下(如/proc/net、/proc/bus等)建立文件。除此之外,我们还可以用自己创建的目录(proc_mkdir)。
删除目录和文件都是使用这一个函数。
proc_mkdir:
1)原型:
struct proc_dir_entry *proc_mkdir(const char * name, struct proc_dir _entry *parent)
2)作用:该函数用于创建一个proc目录。
3)参数:
name:指定要创建的目录的名称;
parent:该proc目录所在的目录。
- proc_mkdir_mode:
1)原型:
extern struct proc_dir_entry *proc_mkdir_mode(const char *name,
mode_t mode, struct proc_dir_entry *parent)
2)作用:该函数用于以一定的模式创建proc目录。
3)参数:
name:指定要创建的目录的名称;
mode:给出了建立该proc目录的访问权限;
parent:该proc目录所在的目录。
- proc_symlink:
1)原型:
truct proc_dir_entry *proc_symlink(const char * name,struct proc_ dir_entry * parent, const char * dest)
2)作用:该函数用于建立一个proc文件的符号连接。
3)参数:
name:给出要建立链接的proc文件的名称;
parent:指定符号链接所在的目录;
dest:指定链接到的proc文件名称。
- creat_proc_read_entry:
1)原型:
struct proc_dir_entry *create_proc_read_entry(const char *name,mode_t mode, struct proc_dir_entry *base,read_proc_t *read_proc, void * data)
2)作用:该函数用于建立一个规则的只读proc文件
3)参数:
name: 给出要创建的文件名称;
mode:给出了建立该proc文件的访问权限;
base:指定建立proc文件所在的目录(指定方式同上);
read_proc:给出读取该文件的操作函数;
data:为该proc文件的专用数据,它将保存在该proc文件对应的struct file结构中的
private_data字段中。
- creat_proc_info_entry:
1)原型:
struct proc_dir_entry *create_proc_info_entry(const char *name,mode_t mode, struct proc_dir_entry *base, get_info_t *get_info)
2)作用:该函数用于创建一个info型的proc文件。
3)参数:
name: 给出要创建的proc文件名称;
mode:给出了建立该proc文件的访问权限;
base:指定建立proc文件所在的目录(指定方式同上);
get_info:指定该proc文件的get_info操作函数。实际上get_info等同于read_proc,如果proc文件没有定义read_proc,对该文件的read操作将由
get_info取代,因此它在功能上非常类似于函数creat_proc_read_entry.
- proc_net_creat:
1)原型:
struct proc_dir_entry *proc_net_create(const char *name, mode_t mode, get_info_t *get_info)
2)作用:该函数用于在/proc/net目录下创建一个proc文件。
3)参数:
name:给出了要创建的proc文件名称;
mode:给出了建立该proc文件的访问权限;
get_info:指定该proc文件的get_info操作函数。
- proc_net_fops_create:
1)原型:
struct proc_dir_entry *proc_net_fops_create(const char *name, mode_t mode, struct file_operations *fops)
2)作用:该函数也用于在/proc/net目录下创建一个proc文件,但是他同时指定了对该文件的文件操作函数。 - proc_net_remove:
1)原型:void proc_net_remove(const char *name)
2)作用:该函数用于删除前面两个函数在/proc/net目录下创建的文件。
3)参数:
name:指定要删除的proc文件;
回调函数:
我们可以使用write_proc函数向/proc中写入一项。此函数原型:
int mod_write( struct file *filp, const char __user *buff,
unsigned long len, void *data );
参数: filp
参数实际上是一个打开文件结构(我们可以忽略这个参数);
buff
参数是存放在缓冲区的要写入的字符串数据;
len
参数定义了在 buff
中有多少数据要被写入;
data
参数是一个指向私有数据的指针(proc_dir_entry中)。
注意:缓冲区buff地址实际上是一个用户空间的缓冲区,因此我们不能直接读取它。Linux 提供了一组 API 来在用户空间和内核空间之间移动数据,我们使用了 copy_from_user
函数来维护用户空间的数据。
读回调函数:
我们可以使用read_proc函数从一个/proc项中读取数据(从内核空间到用户空间)。此函数原型:
int mod_read( char *page, char **start, off_t off, int count, int
*eof, void *data );
参数:page
参数是这些数据写入到的位置;
count
定义了可以写入的最大字符数;
在返回多页数据(通常一页是 4KB)时,我们需要使用 start
和 off
参数;
当所有数据全部写入之后,就需要设置 eof
(文件结束参数);
与 write
类似,data
表示的也是私有数据。
注意:此处提供的page缓冲区在内核空间中,因此我们可以直接读出,而不用调用copy_to_user。
- 其他有用的/proc函数:
对于只需要一个read函数的简单/proc项来说,可以使用create_proc_read_entry,这会创建一个/proc项,并在一个调用中对read_proc函数进行初始化。
其他有用的/proc函数:
copy_to_user/* Copy buffer to user-space from kernel-space */
copy_from_user/* Copy buffer to kernel-space from user-space */
vmalloc/* Allocate a 'virtually' contiguous block of memory */
vfree/* Free a vmalloc'd block of memory */
EXPORT_SYMBOL( symbol )/* Export a symbol to the kernel (make it visible to the kernel)*/
EXPORT_SYMTAB* Export all symbols in a file to the kernel (declare before module.h) */
5、参考文献:The Linux Kernel Module Programming Guide
* 每一个内核模块都需要包含linux/module.h;当需要使用printk()记录级别的宏扩展KERN_ALERT等时,需要包含linux/kernel.h。
*printk()函数中,当指定的优先级低于consle_loglevel时,信息会直接打印到终端上(不是在Xwindows下的虚拟终端下,而是
真正的字符界面下);如果同时syslogd和klogd都在运行,信息也同时添加在文件/var/log/messages中,不管是否显示在控制台
上。
说明:printk()函数不是设计用来同用户交互的,而是为内核提供日志功能,记录内核信息或给出警告。
注意,内核的输出进到了内核回环缓冲区中,而不是打印到 stdout
上,这是因为 stdout
是进程特有的环境。要查看内核回环缓冲区中的消息,可以使用 dmesg
工具(或者通过 /proc 本身使用 cat /proc/kmsg
命令)。下面指令 给出了 dmesg
显示的最后几条消息:dmesg | tail -5.
* 一种称为kbuild的新方法被引入,现在外部的可加载内核模块的编译的方法已经同内核编译统一起来。 想了解更多的编 译非内核代码树中的模块,请参考帮助文件linux/Documentation/kbuild/modules.txt。
* 2.6的内核现在引入一种新的内核模块命名规范:内核模块现在使用.ko的文件后缀(代替 以往的.o后缀),这样内核模块就可以同普通的目标文件区别开。更详细的文档请参考 linux/Documentation/kbuild/makefiles.txt。
* 内核模块证书 :
在2.4或更新的内核中,一种识别代码是否在GPL许可下发布的机制被引入, 因此人们可以在使用非公开的源代码产品时得到警告。
利用宏 MODULE_LICENSE(),即当你设置在GPL证书下发布你的代码时, 你可以取消这些警告。这种证书机制在头文件linux/module.h 实现 。
类似的,宏MODULE_DESCRIPTION()用来描述模块的用途。
宏MODULE_AUTHOR()用来声明模块的作者。
宏MODULE_SUPPORTED_DEVICE() 声明模块支持的设备。
这些宏都在头文件linux/module.h定义, 并且内核本身并不使用这些宏。它们只是用来提供识别信息,可用工具程序像objdump查看。
*
版本印戳作为一个静态的字符串存在于内核模块中,以 vermagic:。 版本信息是在连接阶段从文件init/vermagic.o中获得的。 查看版本印戳和其它在模块中的一些字符信息,可以使用下面的命令 modinfo module.ko(模块名).
*
模块是在insmod加 载时才连接的目标文件。那些要用到的函数(如:printk)的符号链接是内核自己提供的。 也就是说, 你可以在内核模块中使用的函数只能来自内核本身。如果你对内核提供了哪些函数符号 链接感兴趣,看一看文件/proc/kallsyms。
文件/proc/kallsyms保存着内核知道的所有的符号,你可以访问它们, 因为它们是内核代码空间的一部分。
*
库函数和系统调用的区别: 库函数是高层的,完全运行在用户空间, 为程序员提供调用真正的在幕后 完成实际事务的系统调用的更方便的接口。系统调用在内核 态运行并且由内核自己提供。
一般库函数在用户态执行。 库函数调用一个或几个系统调用,而这些系统调用为库函数完成工作,但是在超级状态。 一旦系统调用完成工作后系统调用就返回同时程序也返回用户态。
* 宏 __init 和 __exit 可以使函数在运行完成后自动回收(限模块中),__initdata用于变量,
举例:
#include //需要包含的头文件
static int ntest __initdata = 3;
static int __init test_init(void) {...}
static void __exit test_exit(void) {...}
module_init(test_init); //申明放在实现函数后
module_exit(test_exit);
参考资料:
[3]linux操作系统内核实习
[5]讲解的Makefile语法部分
&
[6]《《Linux内核模块编程指南》jnux内核模块编程指南》(四)
[7]Linux操作系统下 内核模块开发详细解析(简单易懂)
[8]Linux内核/模块开发常见问题集(FAQ)
[9]5.3 基于Linux操作系统的底层驱动技术(对procfs提供的一些API接口函数有详细讲解)
[10]
Linux操作系统下 内核模块开发详细解析(一些基础知识)
http://dev.21tx.com/2007/10/23/11093.html
阅读(2123) | 评论(0) | 转发(0) |