收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
内核模块的另外一个困难,是内核失效对于整个系统或者对于当前进程常常是致命的,而在应用程序的开发过程中,缺段(segment fault)并不会造成什么危害,我们可以利用调试器轻松地跟踪到出错的地方。所以在内核模块编程的过程中,必须特别的小心。
好了,下面我们可以具体地看一看内核模块机制究竟是怎么实现的。
2.2 内核符号表
首先,我们来了解一下内核符号表这个概念。内核符号表是一个用来存放所有模块可以访问的那些符号以及相应地址的特殊的表。模块的连接就是将模块插入到内核的过程。模块所声明的任何全局符号都成为内核符号表的一部分。内核模块根据系统符号表从内核空间中获取符号的地址,从而确保在内核空间中正确地运行。
这是一个公开的符号表,我们可以从文件/proc/kallsyms中以文本的方式读取。在这个文件中存放数据地格式如下:
内存地址 属性 符号名称 【所属模块】
在模块编程中,可以利用符号名称从这个文件中检索出该符号在内存中的地址,然后直接对该地址内存访问从而获得内核数据。对于通过内核模块方式导出的符号,会包含第四列“所属模块”,用来标志这个符号所属的模块名称;而对于从内核中释放出的符号就不存在这一列的数据了。
内核符号表处于内核代码段的_ksymtab部分,其开始地址和结束地址是由C编译器所产生的两个符号来指定:__start___ksymtab和__stop___ksymtab。
2.3 模块依赖
内核符号表记录了所有模块可以访问的符号及相应地址。一个内核模块被装入后,它所声明的符号就会被记录到这个表里,而这些符号当然就可能会被其他模块所引用。这就引出了模块依赖这个问题。
一个模块A引用另一个模块B所导出的符号,我们就说模块B被模块A引用,或者说模块A装载到模块B的上面。如果要链接模块A,必须先要链接模块B。否则,模块B所导出的那些符号的引用就不可能被链接到模块A中。这种模块间的相互关系就叫做模块依赖。
2.4 内核代码分析
内核模块机制的源代码实现,来自于Richard Henderson的贡献。2002年后,由Rusty Russell重写。较新版本的Linux内核,采用后者。
2.4.1 数据结构
跟模块有关的数据结构存放在include/linux/module.h中,当然,首推struct module,
include/linux/module.h
232 struct module
233 {
234 enum module_state state;
235
236 /* Member of list of modules */
237 struct list_head list;
238
239 /* Unique handle for this module */
240 char name[MODULE_NAME_LEN];
241
242 /* Sysfs stuff. */
243 struct module_kobject mkobj;
244 struct module_param_attrs *param_attrs;
245 const char *version;
246 const char *srcversion;
247
248 /* Exported symbols */
249 const struct kernel_symbol *syms;
250 unsigned int num_syms;
251 const unsigned long *crcs;
252
253 /* GPL-only exported symbols. */
254 const struct kernel_symbol *gpl_syms;
255 unsigned int num_gpl_syms;
256 const unsigned long *gpl_crcs;
257
258 /* Exception table */
259 unsigned int num_exentries;
260 const struct exception_table_entry *extable;
261
262 /* Startup function. */
263 int (*init)(void);
264
265 /* If this is non-NULL, vfree after init() returns */
266 void *module_init;
267
268 /* Here is the actual code + data, vfree'd on unload. */
269 void *module_core;
270
271 /* Here are the sizes of the init and core sections */
272 unsigned long init_size, core_size;
273
274 /* The size of the executable code in each section. */
275 unsigned long init_text_size, core_text_size;
276
277 /* Arch-specific module values */
278 struct mod_arch_specific arch;
279
280 /* Am I unsafe to unload? */
281 int unsafe;
282
283 /* Am I GPL-compatible */
284 int license_gplok;
285
286 /* Am I gpg signed */
287 int gpgsig_ok;
288
289 #ifdef CONFIG_MODULE_UNLOAD
290 /* Reference counts */
291 struct module_ref ref[NR_CPUS];
292
293 /* What modules depend on me? */
294 struct list_head modules_which_use_me;
295
296 /* Who is waiting for us to be unloaded */
297 struct task_struct *waiter;
298
299 /* Destruction function. */
300 void (*exit)(void);
301 #endif
302
303 #ifdef CONFIG_KALLSYMS
304 /* We keep the symbol and string tables for kallsyms. */
305 Elf_Sym *symtab;
306 unsigned long num_symtab;
307 char *strtab;
308
309 /* Section attributes */
310 struct module_sect_attrs *sect_attrs;
311 #endif
312
313 /* Per-cpu data. */
314 void *percpu;
315
316 /* The command line arguments (may be mangled). People like
317 keeping pointers to this stuff */
318 char *args;
319 };
在内核中,每一个内核模块信息都由这样的一个module对象来描述。所有的module对象通过list链接在一起。链表的第一个元素由static LIST_HEAD(modules)建立,见kernel/module.c第65行。如果阅读include/linux/list.h里面的LIST_HEAD宏定义,你很快会明白,modules变量是struct list_head类型结构,结构内部的next指针和prev指针,初始化时都指向modules本身。对modules链表的操作,受module_mutex和modlist_lock保护。
下面就模块结构中一些重要的域做一些说明。
234 state表示module当前的状态,可使用的宏定义有:
MODULE_STATE_LIVE
MODULE_STATE_COMING
MODULE_STATE_GOING
240 name数组保存module对象的名称。
244 param_attrs指向module可传递的参数名称,及其属性
248-251 module中可供内核或其它模块引用的符号表。num_syms表示该模块定义的内核模块符号的个数,syms就指向符号表。
- 300 init和exit 是两个函数指针,其中init函数在初始化模块的时候调用;exit是在删除模块的时候调用的。
294 struct list_head modules_which_use_me,指向一个链表,链表中的模块均依靠当前模块。
在介绍了module{}数据结构后,也许你还是觉得似懂非懂,那是因为其中有很多概念和相关的数据结构你还不了解。例如kernel_symbol{} (见include/linux/module.h)
struct kernel_symbol
{
unsigned long value;
const char *name;
};
这个结构用来保存目标代码中的内核符号。在编译的时候,编译器将该模块中定义的内核符号写入到文件中,在读取文件装入模块的时候通过这个数据结构将其中包含的符号信息读入。
value定义了内核符号的入口地址
name指向内核符号的名称
2.4.2 实现函数
接下来,我们要研究一下源代码中的几个重要的函数。正如前段所述,操作系统初始化时,static LIST_HEAD(modules)已经建立了一个空链表。之后,每装入一个内核模块,则创建一个module结构,并把它链接到modules链表中。
我们知道,从操作系统内核角度说,它提供用户的服务,都通过系统调用这个唯一的界面实现。那么,有关内核模块的服务又是怎么做的呢?请参看arch/i386/kernel/syscall_table.S,2.6.15版本的内核,通过系统调用init_module装入内核模块,通过系统调用delete_module卸载内核模块,没有其它途径。这下,代码阅读变得简单了。
kernel/module.c
1931 asmlinkage long
1932 sys_init_module(void __user *umod,
1933 unsigned long len,
1934 const char __user *uargs)
1935 {
1936 struct module *mod;
1937 int ret = 0;
1938
1939 /* Must have permission */
1940 if (!capable(CAP_SYS_MODULE))
1941 return -EPERM;
1942
1943 /* Only one module load at a time, please */
1944 if (down_interruptible(&module_mutex) != 0)
1945 return -EINTR;
1946
1947 /* Do all the hard work */
1948 mod = load_module(umod, len, uargs);
1949 if (IS_ERR(mod)) {
1950 up(&module_mutex);
1951 return PTR_ERR(mod);
1952 }
1953
1954 /* Now sew it into the lists. They won't access us, since
1955 strong_try_module_get() will fail. */
1956 stop_machine_run(__link_module, mod, NR_CPUS);
1957
1958 /* Drop lock so they can recurse */
1959 up(&module_mutex);
1960
1961 down(¬ify_mutex);
1962 notifier_call_chain(&module_notify_list, MODULE_STATE_COMING, mod);
1963 up(¬ify_mutex);
1964
1965 /* Start the module */
1966 if (mod->init != NULL)
1967 ret = mod->init();
1968 if (ret < 0) {
1969 /* Init routine failed: abort. Try to protect us from
1970 buggy refcounters. */
1971 mod->state = MODULE_STATE_GOING;
1972 synchronize_sched();
1973 if (mod->unsafe)
1974 printk(KERN_ERR "%s: module is now stuck!\n",
1975 mod->name);
1976 else {
1977 module_put(mod);
1978 down(&module_mutex);
1979 free_module(mod);
1980 up(&module_mutex);
1981 }
1982 return ret;
1983 }
1984
1985 /* Now it's a first class citizen! */
1986 down(&module_mutex);
1987 mod->state = MODULE_STATE_LIVE;
1988 /* Drop initial reference. */
1989 module_put(mod);
1990 module_free(mod, mod->module_init);
1991 mod->module_init = NULL;
1992 mod->init_size = 0;
1993 mod->init_text_size = 0;
1994 up(&module_mutex);
1995
1996 return 0;
1997 }
函数sys_init_module()是系统调用init_module( )的实现。入口参数umod指向用户空间中该内核模块image所在的位置。image以ELF的可执行文件格式保存,image的最前部是elf_ehdr类型结构,长度由len指示。uargs指向来自用户空间的参数。系统调用init_module( )的语法原型为:
long sys_init_module(void *umod, unsigned long len, const char *uargs);
1940-1941 调用capable( )函数验证是否有权限装入内核模块。
1944-1945 在并发运行环境里,仍然需保证,每次最多只有一个module准备装入。这通过down_interruptible(&module_mutex)实现。
1948-1952 调用load_module()函数,将指定的内核模块读入内核空间。这包括申请内核空间,装配全程量符号表,赋值__ksymtab、__ksymtab_gpl、__param等变量,检验内核模块版本号,复制用户参数,确认modules链表中没有重复的模块,模块状态设置为MODULE_STATE_COMING,设置license信息,等等。
1956 将这个内核模块插入至modules链表的前部,也即将modules指向这个内核模块的module结构。
1966-1983 执行内核模块的初始化函数,也就是表6-1所述的入口函数。
1987 将内核模块的状态设为MODULE_STATE_LIVE。从此,内核模块装入成功。
/kernel/module.c
573 asmlinkage long
574 sys_delete_module(const char __user *name_user, unsigned int flags)
575 {
576 struct module *mod;
577 char name[MODULE_NAME_LEN];
578 int ret, forced = 0;
579
580 if (!capable(CAP_SYS_MODULE))
581 return -EPERM;
582
583 if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
584 return -EFAULT;
585 name[MODULE_NAME_LEN-1] = '\0';
586
587 if (down_interruptible(&module_mutex) != 0)
588 return -EINTR;
589
590 mod = find_module(name);
591 if (!mod) {
592 ret = -ENOENT;
593 goto out;
594 }
595
596 if (!list_empty(&mod->modules_which_use_me)) {
597 /* Other modules depend on us: get rid of them first. */
598 ret = -EWOULDBLOCK;
599 goto out;
600 }
601
602 /* Doing init or already dying? */
603 if (mod->state != MODULE_STATE_LIVE) {
604 /* FIXME: if (force), slam module count and wake up
605 waiter --RR */
606 DEBUGP("%s already dying\n", mod->name);
607 ret = -EBUSY;
608 goto out;
609 }
610
611 /* If it has an init func, it must have an exit func to unload */
612 if ((mod->init != NULL && mod->exit == NULL)
613 || mod->unsafe) {
614 forced = try_force_unload(flags);
615 if (!forced) {
616 /* This module can't be removed */
617 ret = -EBUSY;
618 goto out;
619 }
620 }
621
622 /* Set this up before setting mod->state */
623 mod->waiter = current;
624
625 /* Stop the machine so refcounts can't move and disable module. */
626 ret = try_stop_module(mod, flags, &forced);
627 if (ret != 0)
628 goto out;
629
630 /* Never wait if forced. */
631 if (!forced && module_refcount(mod) != 0)
632 wait_for_zero_refcount(mod);
633
634 /* Final destruction now noone is using it. */
635 if (mod->exit != NULL) {
636 up(&module_mutex);
637 mod->exit();
638 down(&module_mutex);
639 }
640 free_module(mod);
641
642 out:
643 up(&module_mutex);
644 return ret;
645 }
函数sys_delete_module()是系统调用delete_module()的实现。调用这个函数的作用是删除一个系统已经加载的内核模块。入口参数name_user是要删除的模块的名称。
580-581 调用capable( )函数,验证是否有权限操作内核模块。
583-585 取得该模块的名称
590-594 从modules链表中,找到该模块
597-599 如果存在其它内核模块,它们依赖该模块,那么,不能删除。
635-638 执行内核模块的exit函数,也就是表6-1所述的出口函数。
640 释放module结构占用的内核空间。
源代码的内容就看到这里。kernel/module.c文件里还有一些其他的函数,如果你有兴趣可以自己尝试着分析一下,对于模块机制的实现会有更深的理解。
- 使用内核模块
3.1 模块的加载
系统调用当然是将内核模块插入到内核的可行方法。但是毕竟太底层了。此外,Linux环境里还有两种方法可达到此目的。一种方法稍微自动一些,可以做到需要时自动装入,不需要时自动卸载。这种方法需要执行modprobe程序。我们待一会介绍modprobe。
另一种是用insmod命令,手工装入内核模块。在前面分析helloworld例子的时候,我们提到过insmod的作用就是将需要插入的模块以目标代码的形式插入到内核中。注意,只有超级用户才能使用这个命令。insmod的格式是:
insmod [path]modulename.ko
insmod其实是一个modutils模块实用程序,当我们以超级用户的身份使用这个命令的时候,这个程序完成下面一系列工作:
- 从命令行中读入要链接的模块名,通常是扩展名为“.ko”,elf格式的目标文件。
- 确定模块对象代码所在文件的位置。通常这个文件都是在lib/modules的某个子目录中。
- 计算存放模块代码、模块名和module对象所需要的内存大小。
- 在用户空间中分配一个内存区,把module对象、模块名以及为正在运行的内核所重定位的模块代码拷贝到这个内存里。其中,module对象中的init域指向这个模块的入口函数重新分配到的地址;exit域指向出口函数所重新分配的地址。
- 调用init_module(),向它传递上面所创建的用户态的内存区的地址,其实现过程我们已经详细分析过了。
- 释放用户态内存, 整个过程结束。
3.2 模块的卸载
要卸载一个内核模块使用rmmod命令。rmmod程序将已经插入内核的模块从内核中移出,rmmod会自动运行在内核模块自己定义的出口函数。它的格式是:
rmmod [path]modulename
当然,它最终还是通过delete_module()系统调用实现的。
3.3 模块实用程序modutils
Linux内核模块机制提供的系统调用大多数都是为modutils程序使用的。可以说,是Linux的内核模块机制和modutils两者的结合提供了模块的编程接口。modutils(modutils-x.y.z.tar.gz)可以在任何获得内核源代码的地方获得, 选择最高级别的patchlevel x.y.z等于或者小于当前的内核版本,安装后在/sbin目录下就会有insmod、rmmod、ksyms、lsmod、modprobe等等实用程序。当然,通常我们在加载Linux内核的时候,modutils已经被装入了。
- lsmod的使用
调用lsmod 程序将显示当前系统中正在使用的模块信息。 实际上这个程序的功能就是读取/proc文件系统中的文件/proc/modules中的信息。所以这个命令和cat /proc/modules等价。它的格式就是:
lsmod
- ksyms
显示内核符号和模块符号表的信息,可以读取/proc/kallsyms文件。
- modprobe的使用
modprobe是由modutils提供的根据模块之间的依赖性自动插入模块的程序。前面讲到的按需装入的模块加载方法会调用这个程序来实现按需装入的功能。举例来讲,如果模块A依赖模块B,而模块B并没有加载到内核里,当系统请求加载模块A时,modprobe程序会自动将模块B加载到内核。
与insmod类似,modprobe程序也是链接在命令行中指定的一个模块,但它还可以递归地链接指定模块所引用到的其他模块。从实现上讲,modprobe只是检查模块依赖关系,真正的加载的工作还是由insmod来实现的。那么,它又是怎么知道模块间的依赖关系的呢? 简单的讲,modprobe通过另一个modutils程序depmod来了解这种依赖关系。而depmod是通过查找内核中所有的模块并把它们之间的依赖关系写入/lib/modules/2.6.15-1.2054_FC5目录下,一个名为modules.dep的文件。
- kmod的实现
在以前版本的内核中,模块机制的自动装入通过一个用户进程kerneld来实现,内核通过IPC和内核通信,向kerneld发送需要装载的模块的信息,然后kerneld调用modprobe程序将这个模块装载。 但是在最近版本的内核中,使用另外一种方法kmod来实现这个功能。kmod与kerneld比较,最大的不同在于它是一个运行在内核空间的进程,它可以在内核空间直接调用modprobe,大大简化了整个流程。
- 实例
为了便于更直观地认识内核模块的功能,下面用实例来说明模块单元是怎样与系统内核交互的。
4.1 内核模块的make文件
首先我们来看一看模块程序的make文件应该怎么写。自2.6版本之后,Linux对内核模块的相关规范,有很大变动。例如,所有模块的扩张名,都从“.o”改为“.ko”。详细信息,可参看Documentation/kbuild/makefiles.txt。针对内核模块而编辑Makefile,可参看Documentation/kbuild/modules.txt
我们练习“helloworld.ko”时,曾经用过简单的Makefile:
TARGET = helloworld
KDIR = /usr/src/linux
PWD = $(shell pwd)
obj-m += $(TARGET).o
default:
make -C $(KDIR) M=$(PWD) modules
$(KDIR)表示源代码最高层目录的位置。
“obj-m += ( T A R G E T ) . o ”告诉 k b u i l d ,希望将 (TARGET).o”告诉kbuild,希望将 (TARGET).o”告诉kbuild,希望将(TARGET),也就是helloworld,编译成内核模块。
“M=$(PWD)”表示生成的模块文件都将在当前目录下。
4.2 多文件内核模块的 make文件
现在,我们把问题引申一下,对于多文件的内核模块该如何编译呢?同样以“Hello,world”为例,我们需要做以下事情:
在所有的源文件中,只有一个文件增加一行#define __NO_VERSION__。这是因为module.h一般包括全局变量kernel_version的定义,该全局变量包含模块编译的内核版本信息。如果你需要version.h,你需要自己把它包含进去,因为定义了 __NO_VERSION__后module.h就不会包含version.h。
下面给出多文件的内核模块的范例。
Start.c
/* start.c
*
* "Hello, world" –内核模块版本
* 这个文件仅包括启动模块例程
*/
/* 必要的头文件 */
/* 内核模块中的标准 */
#include <linux/kernel.h> /*我们在做内核的工作 */
#include <linux/module.h>
/*初始化模块 */
int init_module()
{
printk("Hello, world!\n");
/* 如果我们返回一个非零值, 那就意味着
* init_module 初始化失败并且内核模块
* 不能加载 */
return 0;
}
stop.c
/* stop.c
*"Hello, world" -内核模块版本
*这个文件仅包括关闭模块例程
*/
/*必要的头文件 */
/*内核模块中的标准 */
#include <linux/kernel.h> /*我们在做内核的工作 */
#define __NO_VERSION__
#include <linux/module.h>
#include <linux/version.h> /* 不被module.h包括,因为__NO_VERSION__ */
/* Cleanup - 撤消 init_module 所做的任何事情*/
void cleanup_module()
{
printk("Bye!\n");
}
/*结束*/
这一次,helloworld内核模块包含了两个源文件,“start.c”和“stop.c”。再来看看对于多文件内核模块,该怎么写Makefile文件
Makefile
TARGET = helloworld
KDIR = /usr/src/linux
PWD = $(shell pwd)
obj-m += $(TARGET).o
$(TARGET)-y := start.o stop.o
default:
make -C $(KDIR) M=$(PWD) modules
相比前面,只增加一行:
$(TARGET)-y := start.o stop.o
收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
做内核的工作 /
#define NO_VERSION
#include <linux/module.h>
#include <linux/version.h> / 不被module.h包括,因为__NO_VERSION__ */
/* Cleanup - 撤消 init_module 所做的任何事情*/
void cleanup_module()
{
printk(“Bye!\n”);
}
/结束/
这一次,helloworld内核模块包含了两个源文件,“start.c”和“stop.c”。再来看看对于多文件内核模块,该怎么写Makefile文件
Makefile
TARGET = helloworld
KDIR = /usr/src/linux
PWD = $(shell pwd)
obj-m += $(TARGET).o
$(TARGET)-y := start.o stop.o
default:
make -C ( K D I R ) M = (KDIR) M= (KDIR)M=(PWD) modules
相比前面,只增加一行:
$(TARGET)-y := start.o stop.o
**收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。**
[外链图片转存中...(img-prpguJzE-1715787949536)]
[外链图片转存中...(img-hSdFJPwl-1715787949536)]
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618679757)**
**需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人**
**都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**