LINUX内核模块编程指南

合集下载

Linux设备驱动程序原理及框架-内核模块入门篇

Linux设备驱动程序原理及框架-内核模块入门篇

Linux设备驱动程序原理及框架-内核模块入门篇内核模块介绍应用层加载模块操作过程内核如何支持可安装模块内核提供的接口及作用模块实例内核模块内核模块介绍Linux采用的是整体式的内核结构,这种结构采用的是整体式的内核结构,采用的是整体式的内核结构的内核一般不能动态的增加新的功能。

为此,的内核一般不能动态的增加新的功能。

为此,Linux提供了一种全新的机制,叫(可安装) 提供了一种全新的机制,可安装) 提供了一种全新的机制模块” )。

利用这个机制“模块”(module)。

利用这个机制,可以)。

利用这个机制,根据需要,根据需要,在不必对内核重新编译链接的条件将可安装模块动态的插入运行中的内核,下,将可安装模块动态的插入运行中的内核,成为内核的一个有机组成部分;成为内核的一个有机组成部分;或者从内核移走已经安装的模块。

正是这种机制,走已经安装的模块。

正是这种机制,使得内核的内存映像保持最小,的内存映像保持最小,但却具有很大的灵活性和可扩充性。

和可扩充性。

内核模块内核模块介绍可安装模块是可以在系统运行时动态地安装和卸载的内核软件。

严格来说,卸载的内核软件。

严格来说,这种软件的作用并不限于设备驱动,并不限于设备驱动,例如有些文件系统就是以可安装模块的形式实现的。

但是,另一方面,可安装模块的形式实现的。

但是,另一方面,它主要用来实现设备驱动程序或者与设备驱动密切相关的部分(如文件系统等)。

密切相关的部分(如文件系统等)。

课程内容内核模块介绍应用层加载模块操作过程内核如何支持可安装模块内核提供的接口及作用模块实例内核模块应用层加载模块操作过程内核引导的过程中,会识别出所有已经安装的硬件设备,内核引导的过程中,会识别出所有已经安装的硬件设备,并且创建好该系统中的硬件设备的列表树:文件系统。

且创建好该系统中的硬件设备的列表树:/sys 文件系统。

(udev 服务就是通过读取该文件系统内容来创建必要的设备文件的。

)。

linux module的用法

linux module的用法

linux module的用法
Linux模块是一种可以动态加载到Linux内核中以扩展其功能的软件组件。

它们通常用于添加新的驱动程序、文件系统或其他内核功能。

下面我将从多个角度来介绍Linux模块的用法。

首先,要编写一个Linux模块,你需要具备一定的C语言编程知识。

一个基本的Linux模块包括初始化函数和清理函数。

初始化函数在模块加载时被调用,而清理函数在模块被卸载时被调用。

你需要使用特定的宏和数据结构来定义模块的初始化和清理函数,以及模块的许可证和作者信息。

其次,编译模块需要使用Linux内核源代码中的构建系统。

你需要确保已经安装了正确版本的内核头文件和构建工具。

然后,你可以编写一个Makefile来编译你的模块。

在Makefile中,你需要指定内核源代码的路径,并使用特定的命令来编译模块。

一旦你编译好了你的模块,你可以使用insmod命令将其加载到内核中。

加载模块后,你可以使用lsmod命令来查看已加载的模块列表。

你还可以使用modinfo命令来查看模块的信息,包括作者、描述和许可证等。

当你不再需要模块时,你可以使用rmmod命令将其从内核中卸载。

卸载模块后,你可以使用dmesg命令来查看内核日志,以确保
模块已经成功卸载。

总的来说,Linux模块的用法涉及到编写模块代码、编译模块、加载模块以及卸载模块等步骤。

掌握了这些基本的用法,你就可以
开始开发自己的Linux内核模块了。

希望这些信息能够帮助你更好
地理解Linux模块的用法。

linux模块编译

linux模块编译

linux 模块编译步骤(原)本文将直接了当的带你进入linux的模块编译。

当然在介绍的过程当中,我也会添加一些必要的注释,以便初学者能够看懂。

之所以要写这篇文章,主要是因为从书本上学的话,可能要花更长的时间才能学会整个过程,因为看书的话是一个学习过程,而我这篇文章更像是一个培训。

所以实践性和总结性更强。

通过本文你将会学到编译一个模块和模块makefile的基本知识。

以及加载(卸载)模块,查看系统消息的一些知识;声明:本文为初学者所写,如果你已经是一个linux模块编译高手,还请指正我文章中的错误和不足,谢谢第一步:准备源代码首先我们还是要来编写一个符合linux格式的模块文件,这样我们才能开始我们的模块编译。

假设我们有一个源文件mymod.c。

它的源码如下:mymodules.c1. #include <linux/module.h> /* 引入与模块相关的宏*/2. #include <linux/init.h> /* 引入module_init() module_exit()函数*/3. #include <linux/moduleparam.h> /* 引入module_param() */45. MODULE_AUTHOR("Yu Qiang");6. MODULE_LICENSE("GPL");78. static int nbr = 10;9. module_param(nbr, int, S_IRUGO);10.11. static int __init yuer_init(void)12.{13. int i;14. for(i=0; i<nbr; i++)15. {16. printk(KERN_ALERT "Hello, How are you. %d\n", i);17. }18. return 0;19.}20.21.static void __exit yuer_exit(void)22.{23. printk(KERN_ALERT"I come from yuer's module, I have been unlad.\n");24.}25.26. module_init(yuer_init);27. module_exit(yuer_exit);我们的源文件就准备的差不多了,这就是一个linux下的模块的基本结构。

LINUX内核模块编译步骤

LINUX内核模块编译步骤

LINUX内核模块编译步骤编译Linux内核模块主要包括以下步骤:1.获取源代码2.配置内核进入源代码目录并运行make menuconfig命令来配置内核。

该命令会打开一个文本菜单,其中包含许多内核选项。

在这里,你可以配置内核以适应特定的硬件要求和预期的功能。

你可以选择启用或禁用各种功能、设备驱动程序和文件系统等。

配置完成后,保存并退出。

3. 编译内核(make)运行make命令开始编译内核。

这将根据你在上一步中进行的配置生成相应的Makefile,然后开始编译内核。

编译的过程可能需要一些时间,请耐心等待。

4.安装模块编译完成后,运行make modules_install命令将编译好的模块安装到系统中。

这些模块被安装在/lib/modules/<kernel-version>/目录下。

5.安装内核运行make install命令来安装编译好的内核。

该命令会将内核映像文件(通常位于/arch/<architecture>/boot/目录下)复制到/boot目录,并更新系统引导加载程序(如GRUB)的配置文件。

6.更新GRUB配置文件运行update-grub命令来更新GRUB引导加载程序的配置文件。

这将确保新安装的内核在下次启动时可用。

7.重启系统安装完成后,通过重启系统来加载新的内核和模块。

在系统启动时,GRUB将显示一个菜单,你可以选择要启动的内核版本。

8.加载和卸载内核模块现在,你可以使用insmod命令来加载内核模块。

例如,运行insmod hello.ko命令来加载名为hello.ko的模块。

加载的模块位于/lib/modules/<kernel-version>/目录下。

如果你想卸载一个已加载的内核模块,可以使用rmmod命令。

例如,运行rmmod hello命令来卸载已加载的hello模块。

9.编写和编译模块代码要编写一个内核模块,你需要创建一个C文件,包含必要的模块代码。

Linux 汇编语言开发指南

Linux 汇编语言开发指南

二、Linux 汇编语法格式绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。

但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同:1.在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。

例如:2.在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。

例如:3.AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。

在Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。

例如:4.在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。

例如:5.在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上'*'作为前缀,而在 Intel 格式中则不需要。

6.远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为"ljump" 和 "lcall",而在 Intel 汇编格式中则为 "jmp far" 和 "call far",即:7.与之相应的远程返回指令则为:8.在 AT&T 汇编格式中,内存操作数的寻址方式是section:disp(base, index, scale)而在 Intel 汇编格式中,内存操作数的寻址方式为:section:[base + index*scale + disp]由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:disp + base + index * scale下面是一些内存操作数的例子:三、Hello World!真不知道打破这个传统会带来什么样的后果,但既然所有程序设计语言的第一个例子都是在屏幕上打印一个字符串 "Hello World!",那我们也以这种方式来开始介绍 Linux 下的汇编语言程序设计。

Linux内核模块开发(简单)

Linux内核模块开发(简单)

Linux内核模块开发(简单)Linux系统为应⽤程序提供了功能强⼤且容易扩展的API,但在某些情况下,这还远远不够。

与硬件交互或进⾏需要访问系统中特权信息的操作时,就需要⼀个内核模块。

Linux内核模块是⼀段编译后的⼆进制代码,直接插⼊Linux内核中,在 Ring 0(x86–64处理器中执⾏最低和受保护程度最低的执⾏环)上运⾏。

这⾥的代码完全不受检查,但是运⾏速度很快,可以访问系统中的所有内容。

Intel x86架构使⽤了4个级别来标明不同的特权级。

Ring 0实际就是内核态,拥有最⾼权限。

⽽⼀般应⽤程序处于Ring 3状态--⽤户态。

在Linux中,还存在Ring 1和Ring 2两个级别,⼀般归属驱动程序的级别。

在Windows平台没有Ring 1和Ring 2两个级别,只⽤Ring 0内核态和Ring 3⽤户态。

在权限约束上,⾼特权等级状态可以阅读低特权等级状态的数据,例如进程上下⽂、代码、数据等等,但反之则不可。

Ring 0最⾼可以读取Ring 0-3所有的内容,Ring 1可以读Ring 1-3的,Ring 2以此类推,Ring 3只能读⾃⼰的数据。

1. 为什么要开发内核模块编写Linux内核模块并不是因为内核太庞⼤⽽不敢修改。

直接修改内核源码会导致很多问题,例如:通过更改内核,你将⾯临数据丢失和系统损坏的风险。

内核代码没有常规Linux应⽤程序所拥有的安全防护机制,如果内核发⽣故障,将锁死整个系统。

更糟糕的是,当你修改内核并导致错误后,可能不会⽴即表现出来。

如果模块发⽣错误,在其加载时就锁定系统是最好的选择,如果不锁定,当你向模块中添加更多代码时,你将会⾯临失控循环和内存泄漏的风险,如果不⼩⼼,它们会随着计算机继续运⾏⽽持续增长,最终,关键的存储器结构甚⾄缓冲区都可能被覆盖。

编写内核模块时,基本是可以丢弃传统的应⽤程序开发范例。

除了加载和卸载模块之外,你还需要编写响应系统事件的代码(⽽不是按顺序模式执⾏的代码)。

Linux内核模块

Linux内核模块

⼯作模式⼯作性质层次权限影响竞态运⾏⽅式应⽤程序USR 模式策略性⽤户层低局部局部主动内核模块SVC 模式功能性内核层⾼全局全局被挡Linux 内核模块1、什么是内核模块?内核模块是Linux 提供的⼀种机制,允许在内核运⾏时动态加载进内核中,具有两个特点: 1)内核模块本⾝不编译⼊内核映像,有效控制缩减内核镜像⼤⼩ 2)内核模块⼀旦被加载,他就和内核中的其他部分完全⼀样2、为什么需要内核模块?如果在内核编译时把所有的功能都编译进去,就会导致内核很⼤,⽽且要往内核中添加或删除功能时必须重新编译内核⽐如在Ubuntu 在通⽤PC 平台上,预先⽆法知道需要什么设备,就不知道预先编译什么驱动。

3、内核模块和应⽤程序的区别4、内核模块的基本构成|——两个函数(⼀般需要)| |——模块初始化(加载)函数:当内核模块加载进内核的时候,做⼀些准备⼯作| |——模块卸载函数:回收、清理资源||——授权(许可证声明)(必须):Linux 内核受GPL (General Public License )授权约束|——模块参数(可选):模块被加载时可以被传递给它的值,本⾝对应模块内的全局变量|——模块导出符号(可选)|——模块信息说明(可选)5、模块加载(初始化)函数⼀般以 __init 标识声明函数命名规则 xxx_init xxx 设备名 init 功能名(初始化)函数形式:static ini __init xxx_init(void ){/* 初始化代码* 返回值: 成功:0 失败:负数,绝对值是错误码* 应⽤层得到的返回值是-1,错误码保存到errno (每个进程有⼀个); 标准化errno.h 已经明确定义linux/errno.h */}注册⽅式: module_init(x); x 为模块初始化函数的⾸地址 6、模块卸载函数⼀般以 __exit 标识声明函数命名规则 xxx_exit xxx 设备名 exit 功能名(卸载)static ini __exit xxx_exit(void ){/* 释放代码 */}注册⽅式: module_exit(x); x为模块卸载函数的⾸地址7、模块许可证声明MODULE_LICENSE(_license) //_license就是授权名称的字符串//"GPL" [GNU Public License v2 or later]//"GPL v2" [GNU Public License v2]//"GPL and additional rights" [GNU Public License v2 rights and more]//"Dual BSD/GPL" [GNU Public License v2 or BSD license choice]//"Dual MIT/GPL" [GNU Public License v2 or MIT license choice]//"Dual MPL/GPL" [GNU Public License v2 or Mozilla license choice]8、模块声明与描述在Linux内核模块中,我们可以⽤MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别来声明模块的作者、描述、版本、设备表和别名,例如:MODULE_AUTHOR(author);MODULE_DESCRIPTION(description);MODULE_VERSION(version_string);MODULE_DEVICE_TABLE(table_info);MODULE_ALIAS(alternate_name);对于USB、PCI等设备驱动,通常会创建⼀个MODULE_DEVICE_TABLE,表明该驱动模块⽀持的设备,如:/* 对应此驱动的设备列表 */static struct usb_device_id skel_table [ ] = {{USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) }, { } /* 表结束 */}};MODULE_DEVICE_TABLE (usb, skel_table);9、模块参数:在加载模块时,可以给模块传参头⽂件 linux/moduleparam.hA、传递普通变量module_param(name, type, perm);声明内核模块参数/*name - 接收参数的变量名type - 变量类型 Standard types are: byte, short, ushort, int, uint, long, ulong charp: a character pointer bool: a bool, values 0/1, y/n, Y/N. invbool: the above, only sense-reversed (N = true)perm - 权限 头⽂件 linux/stat.h #define S_IRWXUGO (S_IRWXU|S_IRWXG|S_IRWXO) #define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO) #define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH) #define S_IWUGO (S_IWUSR|S_IWGRP|S_IWOTH) #define S_IXUGO (S_IXUSR|S_IXGRP|S_IXOTH)*/范例:int i = 0;module_param(i, int, 0644);运⾏:# insmod xxx.ko i=10B、传递数组参数module_param_array(name, type, nump, perm)/*声明内核模块数组参数name - 数组名type - 数组成员类型nump – ⼀个指向保存数组长度的整型变量的指针perm - 权限*/范例:int arr[] = {1,2,3,4,5,6};int len=0;module_param(arr, int, &len, 0644);运⾏:# insmod xxx.ko arr=1,2,3,4,5C、传递字符串参数module_param_string(name, string, len, perm)/*声明内核模块字符串参数name - 字符串缓存的外部名(传⼊变量名)string - 字符串缓存的内部名nump - 数组的数量perm - 权限*/范例:char insidestr[] = "hello world";module_param(extstr, insidestr, szieof(insidestr), 0644);运⾏:# insmod xxx.ko extstr="hello"10、编译内核模块如果⼀个内核模块要加载到某个内核中运⾏,则这个模块必须使⽤编译该内核镜像的源码进⾏编译,否则运⾏时会出错A、头⽂件(语法问题)B、编译结果(最主要影响)编译时符号表(只在编译时使⽤)运⾏时内核符号表# cat /proc/kallsyms 运⾏时内核符号表C、编译系统⽰例Makefile:# 内核模块的Makefile(模块源码在内核源码外,且内核先编译)# 1、找内核的Makefile# 2、内核的Makefile找内核模块的Makeifle内核模块的Makeifle定义要编译对象ifneq ($(KERNELRELEASE),)#要编译对象表⽰把demo.c编译成demo.ko obj-m = demo.oelse#内核源码⽬录KERNELDIR := /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modulesendifclean: rm -rf .tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.cKERNELRELEASE 是在内核源码的顶层Makefile中定义的⼀个变量,在第⼀次读取执⾏此Makefile时,KERNELRELEASE没有被定义,所以make将读取执⾏else之后的内容。

《Linux高级系统编程》教学教案

《Linux高级系统编程》教学教案

《Linux高级系统编程》教学教案一、教学目标1. 让学生掌握Linux系统编程的基本概念和原理。

2. 培养学生熟练使用Linux系统编程API的能力。

3. 使学生了解Linux系统编程的高级主题和技巧。

4. 培养学生解决实际问题的能力,提高他们在Linux环境下的软件开发水平。

二、教学内容1. Linux系统编程概述讲解Linux系统编程的基本概念、特点和优势。

2. 文件I/O操作介绍Linux文件I/O模型,讲解文件的打开、关闭、读写、同步等操作。

3. 进程管理讲解Linux进程的概念、创建、终止、进程间通信等知识。

4. 线程管理介绍Linux线程的基本概念、创建、同步、互斥等知识。

5. 高级I/O操作讲解Linux高级I/O操作,如异步I/O、直接I/O、内存映射I/O等。

三、教学方法1. 讲授法:讲解基本概念、原理和知识点。

2. 案例教学法:通过实际案例让学生掌握编程技巧和方法。

3. 实验教学法:安排实验课程,让学生亲自动手实践,提高实际操作能力。

四、教学环境1. 教室环境:投影仪、计算机、网络等。

2. 实验环境:装有Linux操作系统的计算机、网络等。

五、教学评估1. 课堂问答:检查学生对课堂知识的理解和掌握程度。

2. 实验报告:评估学生在实验过程中的动手能力和解决问题能力。

3. 课程作业:检查学生对课程知识点的综合运用能力。

4. 期末考试:全面评估学生对本门课程的掌握程度。

六、信号处理1. 信号基本概念讲解信号的定义、作用和信号处理的基本方法。

2. 信号处理函数介绍Linux信号处理函数,如signal(), rse(), sigaction()等。

3. 信号在进程和线程中的处理讲解信号在进程和线程中的传播和处理机制。

七、同步与互斥1. 同步与互斥基本概念讲解同步与互斥的概念、作用和应用场景。

2. 互斥锁介绍Linux互斥锁的使用,如pthread_mutex_lock(), pthread_mutex_unlock()等。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

第1章Hello, World如果第一个程序员是一个山顶洞人,它在山洞壁(第一台计算机)上凿出的第一个程序应该是用羚羊图案构成的一个字符串“Hello, Wo r l d”。

罗马的编程教科书也应该是以程序“S a l u t, M u n d i”开始的。

我不知道如果打破这个传统会带来什么后果,至少我还没有勇气去做第一个吃螃蟹的人。

内核模块至少必须有两个函数:i n i t_m o d u l e和c l e a n u p_m o d u l e。

第一个函数是在把模块插入内核时调用的;第二个函数则在删除该模块时调用。

一般来说,i n i t_m o d u l e可以为内核的某些东西注册一个处理程序,或者也可以用自身的代码来取代某个内核函数(通常是先干点别的什么事,然后再调用原来的函数)。

函数c l e a n u p_m o d u l e的任务是清除掉i n i t_m o d u l e所做的一切,这样,这个模块就可以安全地卸载了。

1.1 内核模块的Makefiles 文件内核模块并不是一个独立的可执行文件,而是一个对象文件,在运行时内核模块被链接到内核中。

因此,应该使用- c 命令参数来编译它们。

还有一点需要注意,在编译所有内核模块时,都将需要定义好某些特定的符号。

• _ _KERNEL_ _—这个符号告诉头文件:这个程序代码将在内核模式下运行,而不要作为用户进程的一部分来执行。

• MODULE —这个符号告诉头文件向内核模块提供正确的定义。

• L I N U X —从技术的角度讲,这个符号不是必需的。

然而,如果程序员想要编写一个重要的内核模块,而且这个内核模块需要在多个操作系统上编译,在这种情况下,程序员将会很高兴自己定义了L I N U X 这个符号。

这样一来,在那些依赖于操作系统的部分,这个符号就可以提供条件编译了。

还有其它的一些符号,是否包含它们要取决于在编译内核时使用了哪些命令参数。

如果用户不太清楚内核是怎样编译的,可以查看文件/ u s r /i n c l u d e /l i n u x /c o n f i g .h 。

• _ _SMP_ _—对称多处理。

如果编译内核的目的是为了支持对称多处理,在编译时就需要定义这个符号(即使内核只是在一个C P U 上运行也需要定义它)。

当然,如果用户使用对称多处理,那么还需要完成其它一些任务(参见第1 2章)。

• C O N F I G _M O D V E R S I O N S —如果C O N F I G _M O D V E R S I O N S 可用,那么在编译内核模块时就需要定义它,并且包含头文件/ u s r /i n c l u d e /l i n u x /m o d v e r s i o n s .h 。

还可以用代码自身来完成这个任务。

完成了以上这些任务以后,剩下唯一要做的事就是切换到根用户下(你不是以r o o t 身份编译内核模块的吧?别玩什么惊险动作哟!),然后根据自己的需要插入或删除h e l l o 模块。

在执行完i n s m o d 命令以后,可以看到新的内核模块在/ p r o c /m o d u l e s 中。

顺便提一下,M a k e f i l e 建议用户不要从X 执行i n s m o d 命令的原因在于,当内核有个消息需要使用p r i n t k 命令打印出来时,内核会把该消息发送给控制台。

当用户没有使用X 时,该消息146第二部分Linux 内核模块编程指南将发送到用户正在使用的虚拟终端(用户可以用A l t-F<n>来选择当前终端),然后用户就可以看到这个消息了。

而另一方面,当用户使用X时,存在两种可能性。

一种情况是用户用命令xterm -C打开了一个控制台,这时输出将被发送到那个控制台;另一种情况是用户没有打开控制台,这时输出将送往虚拟终端7—被X所“覆盖”的一个虚拟终端。

当用户的内核不太稳定时,没有使用X的用户更有可能取得调试信息。

如果没有使用X,p r i n t k将直接从内核把调试消息发送到控制台。

而另一方面,在X中p r i n t k的消息将被送给一个用户模式的进程(xterm -C)。

当那个进程获得C P U时间时,它将把该消息传送给X服务器进程。

然后,当X服务器获得C P U时间时,它将显示该消息—但是一个不稳定的内核通常意味着系统将要崩溃或者重新启动,所以用户不希望推迟错误信息显示的时间,因为该信息可能会向用户解释什么地方出了问题,如果显示的时刻晚于系统崩溃或重启的时刻,用户将会错过这个重要的信息。

1.2 多重文件内核模块有时候在多个源文件间划分内核模块是很有意义的。

这时用户需要完成下面三件任务:1) 除了一个源文件以外,在其它所有源文件中加入一行#define _ _ NO_VERSION_ _。

这点很重要,因为m o d u l e.h中通常会包含有k e r n e l_v e r s i o n的定义( k e r n e l_v e r s i o n是一个全局变量,它表明该模块是为哪个内核版本所编译的)。

如果用户需要v e r s i o n.h文件,那么用户必须自己把它包含在源文件中,因为在定义了_ _NO_VERSION_ _的情况下,m o d u l e.h是不会为用户完成这个任务的。

2) 像平常一样编译所有的源文件。

3) 把所有的对象文件组合进一个文件中。

在x 86下,可以使用命令:ld -m elf_i386 -r -o〈模块名称〉.o (第一个源文件).o (第二个源文件) .o来完成这个任务。

下面是这种内核模块的一个例子。

148第二部分Linux 内核模块编程指南第2章字符设备文件我们现在就可以吹牛说自己是内核程序员了。

虽然我们所写的内核模块还什么也干不了,但我们仍然为自己感到骄傲,简直可以称得上趾高气扬。

但是,有时候在某种程度上我们也会感到缺少点什么,简单的模块并不是太有趣。

内核模块主要通过两种方法与进程打交道。

一种方法是通过设备文件(例如在目录/ d e v中的文件),另一种方法是使用p r o c文件系统。

既然编写内核模块的主要原因之一就是支持某些类型的硬件设备,那么就让我们从设备文件开始吧。

设备文件最初的用途是使进程与内核中的设备驱动程序通信,并且通过设备驱动程序再与物理设备(调制解调器、终端等等)通信。

下面我们要讲述实现这一任务的方法。

每个设备驱动程序都被赋予一个主编号,主要用于负责某几种类型的硬件。

可以在/ p r o c/d e v i c e s中找到驱动程序以及它们对应的主编号的列表。

由设备驱动程序管理的每个物理设备都被赋予一个从编号。

这些设备中的每一个,不管是否真正安装在计算机系统上,都将对应一个特殊的文件,该文件称为设备文件,所有的设备文件都包含在目录/ d e v中。

例如,如果执行命令ls -l /dev/hd[ab]*,用户将可以看到与一个计算机相连接的所有的I D E硬件分区。

注意,所有的这些硬盘分区都使用同一个主编号:3,但是从编号却各不相同。

需要强调的是,这里假设用户使用的是P C体系结构。

我并不知道在其它体系结构上运行的L i n u x的设备是怎么样的。

在安装了系统以后,所有的设备文件都由命令m k n o d创建出来。

从技术的角度上讲,并没有什么特别的原因一定要把这些设备文件放在目录/ d e v中,这只不过是一个有用的传统习惯而已。

如果读者创建设备文件的目的只不过是为了试试看,就像本章的练习一样,那么把该设备文件放置在编译内核模块的目录中可能会更有意义一些。

设备一般分为两种类型:字符设备和块设备。

它们的区别在于块设备具有一个请求缓冲区,所以块设备可以选择按照何种顺序来响应这些请求。

这对于存储设备来说是很重要的。

在存储设备中,读或写相邻的扇区速度要快一些,而读写相互之间离得较远的扇区则要慢得多。

另一个区别在于块设备只能以成块的形式接收输入和返回输出(块的大小根据设备类型的变化而有所不同),而字符设备则可以随心所欲地使用任意数目的字节。

当前大多数设备都是字符设备,因为它们既不需要某种形式的缓冲,也不需要按照固定的块大小来进行操作。

如果想知道某个设备文件对应的是块设备还是字符设备,用户可以执行命令ls -l,查看一下该命令的输出中的第一个字符,如果第一个字符是“b”,则对应的是块设备;如果是“c”,则对应的是字符设备。

模块分为两个独立的部分:模块部分和设备驱动程序部分。

前者用于注册设备。

函数i n i t_m o d u l e调用m o d u l e_r e g i s t e r_c h r d e v,把该设备驱动程序加入到内核的字符设备驱动程序表中,它还会返回供驱动程序所使用的主编号。

函数c l e a n u p_m o d u l e则取消该设备的注册。

注册某设备和取消它的注册是这两个函数最基本的功能。

内核中的东西并不是按照它们自己的意愿主动开始运行的,就像进程一样,而是由进程通过系统调用来调用,或者由硬件设备通过中断来调用,或者由内核的其它部分调用(只需调用特定的函数),它们才会执行。

因此,如果用户往内核中加入了代码,就必须把它作为某种特定类型事件的处理程序进行注册;而在删除这些代码时,用户必须取消它的注册。

设备驱动程序一般是由四个d e v i c e_<a c t i o n>函数所组成的,如果用户需要处理具有对应主编号的设备文件,就可以调用这四个函数。

通过f i l e_o p e r a t i o n s结构F o p s内核可以知道调用哪些函数。

因为该结构的值是在注册设备时给定的,它包含了指向这四个函数的指针。

在这里我们还需要记住的一点是:无论如何不能乱删内核模块。

原因在于如果设备文件是由进程打开的,而我们删去了该内核模块,那么使用该文件就将导致对正确的函数(读/写)原来所处的存储位置的调用。

如果我们走运,那里没有装入什么其它的代码,那我们至多得到一些难看的错误信息,而如果我们不走运,在原来的同一位置已经装入了另一个内核模块,这就意味着跳转到了内核中另一个函数的中间,这样做的后果是不堪设想的,起码不会是令人愉快的。

一般来说,如果用户不愿意让某件事发生,可以让执行这件事的函数返回一个错误代码(一个负数)。

而对c l e a n u p_m o d u l e来说这是不可能的,因为它是一个v o i d函数。

一旦调用了c l e a n u p_m o d u l e,这个模块就死了。

然而,还有一个计数器记录了有多少个其它的内核模块正在使用该内核模块,这个计数器称为引用计数器(就是位于文件/ p r o c/m o d u l e s信息行中的最后那个数值)。

相关文档
最新文档