编译成模块的Linux驱动是怎么运行的?(系统调用)

前文提要

上一次是编译进内核的驱动,可知是通过接口将驱动入口函数放在特定的段中,
然后在内核启动时按照优先级被初始化。
上一次:https://www.cnblogs.com/Hynaya/p/20059807
这一次是编译成模块的驱动被内核运行的。

初识——insmod命令

加载内核的ko文件通常会使用insmod命令
以buildroot系统为例,insmod命令是busybox提供的
关于busybox系统:https://www.cnblogs.com/Hynaya/p/20071691
打开buildroot的源码,找到output,里面存放了busybox的源码
而busybox把命令放在了busybox源码的busybox/modutils/insmod.c
image

打开后可以看到,有两种方式:

  1. 设置一个句柄直接用open打开文件,用句柄调用ko文件的init
  2. 将ko文件映射到内存,映射成功后调用init,结束后释放内存。如果映射失败,尝试分配堆内存把ko文件读入到内存,成功后调用init,结束后释放内存。

在实际调用中,优先使用第一种finit_module 这个系统调用

finit_module(fd, options, 0)直接通过文件描述符加载内核模块,不需要用户态自己读取整个文件到内存,更高效、更安全。

如果内核不支持第一种,就使用第二种,读取模块文件到内存,再用 init_module 系统调用加载,结束后释放内存

init_module(image, image_size, options) 是传统的内核模块加载系统调用,它需要模块文件的内容在用户态内存中的起始地址和大小,所以必须先把模块文件读到内存里。

先用映射,后考虑用malloc是因为映射更高效和安全。1.不需要read到缓存,减少拷贝消耗2.不需要提前分配大内存,按需获取3.自动管理内存,比malloc的必须手动释放不容易出错4.代码简洁,不需要手动处理open和read之类的。

可以仿照insmod写一个自己的insmod命令,逻辑和busybox的一样,此处省略

深入——系统调用的流程

什么是系统调用

前面只看到了insmod如何加载ko文件,但是不知道ko文件是如何真正被系统执行的,因为两种方法背后都是通过系统调用加载了ko文件,那么系统调用是什么?
编程人员编程时,上层应用不能直接操作硬件,所以要利用系统调用接口来请求操作系统的服务,如访问硬件。即系统调用是操作系统提供给编程人员的接口。
因为涉及到硬件操作,系统调用是与cpu架构进行绑定的,与内核版本也有关系。

系统调用的流程

init_module 为例init_module 的原型为:

#define init_module(image,size,args) syscall(__NR_init_module,image,size,args)

syscall 函数原型,

long int syscall(long int sysno,...)

参数 sysno为系统调用号,每个系统有——个唯一的系统调用号来标识对应的函数。
...是可变参数,是系统调用所以带的参数。

作用:syscall根据系统调用号,调用相应的系统调用,并且还能传入函数所需要的参数
__NR_init_module 是在哪和函数绑定了呢?打开 include/uapi/asm-generic/unistd.h文件,找到以下代码:
image

可以找到,系统调用号105通过__SYSCALL(注意这个是大写的,和前文的不是同一个)绑定了内核里的系统函数sys_init_module。当使用syscall调用系统调用号105的时候,就实现了执行sys_init_module
有一个规律,在用户空间调用的xxx函数,对应系统调用里名为sys_xxx的函数。

系统调用号绑定的函数的定义

因为是系统内核源码,所以定义在内核源文件里,并且,它还是一个宏定义
sys_init_module函数定义在kernel/module.c文件中,这里就是调用内核加载驱动的服务实现
image

其中主要的实现是load_modul函数,加载ko文件的工作都是由它完成的,
其中具体实现非常专业且硬核,适合内核开发人员阅读
如果只是进行驱动开发,其中细节不必深究,它其中一些关键逻辑将在文末阐述

系统调用函数宏定义的展开

既然sys_init_module函数是一个宏定义
跳转到宏定义所在位置include/linux/syscalls.h中,得到:
image

这是一系列linux提供的,公用的宏定义
这里有很多不同的标号,不同的标号代表了不同参数的个数,对应参数的个数会对应特定宏定义,
最大的数字是6,也就是说最大可以带6个参数。
这里sys_init_module用到的是SYSCALL_DEFCALLDEFINE3,代表sys_init_module用了三个参数
注意,__VA_ARGS等价于一串宏定义,放入sys_init_module,经过一系列复杂的展开,最后给它起了一个别名__se_sys_Init_module。根据不同的内核和架构,这里展开的实现也不尽相同。

最终是使得sys_init_module传入SYSCALL_DEFCALLDEFINE3后,最后等价于__se_sys_Init_module函数

实践——尝试在内核中添加一个自己的系统调用

在内核源码中添加自己的服务,并编译进内核

和系统内置的一样,在定义自己的服务时也应该用宏定义,linux提供的宏定义如下,正如前文所说,根据参数数量的不同,所使用的也不同,并且目前linux支持系统调用最高是6个参数
image

定义一需要0个参数的helloworld.c

#include <linux/kernel.h>//内核
#include <linux/syscalls.h>//syscall
SYSCALL_DEFINE0(helloworld){
printk("This is helloworld syscall\n");
return 0;
}

注意:要在kcongfig里设置编译进内核,且写好对应的Makefile。
自定义的系统调用函数本身在linux内核源码里的路径没有要求(只要在kcongfig里设置好编译进内核路径一致),只要在应用层调用系统调用号,它就会被执行。

添加系统调用号

在Linux 源码kernel/include/uapi/asm-generic/unistd.h 文件中
仿照其他系统调用号,在最后面添加系统调用号435。(不要忘记在前面加sys_),将后面的依次顺延
image

编译并烧写内核到开发板

执行./bilud.h编译内核
烧写到开发板上

测试系统调用

回顾前文自己写的insmod,就可以理解为什么要加一个宏定义(设置马甲)
这个马甲也可以不加

#include <stdio.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#define __NR_helloworld 435
int main(int argc,char **argv){
syscall(__NR_helloworld);
return 0;

使用交叉编译器编译,并拷贝到开发板上
执行程序,成功打印This is helloworld syscall

实现——内核运行ko的流程

前文可知,系统调用时内核执行SYSCALL_DEFCALLDEFINE3
最后都会去执行load_modul
image

由这个流程可知,其中的调用关系,
最终是通过do_init_module下的do_one_initcall(mod->init)来执行驱动程序的入口函数
为什么是mod->init而不是我们自己在驱动代码里的定义呢?
打开include/linux/module.h 文件,找到以下代码
image

将init_module作为函数inifn的别名,init_module是驱动加载函数的统一别名,我们自己定义的名字最后都变成了init_module
打开驱动编译ko的中间文件文件名.mod.c可以看到定义了一个module类型的结构体并存放init函数
和load_modul里存放init的module结构体一样,最后就这样调用驱动的入口函数
而宏定义改变的只是入口函数的名字,不管init叫什么,都会改名成 init_module

总结

image

posted @ 2026-05-19 19:02  Hynaya  阅读(11)  评论(0)    收藏  举报