请稍侯

Linux的模块机制

Linux Kernel 是宏内核,和微内核相对的,它的所有内核内核功能的实现都在内核之中,所以相对于微内核实现, Linux Kernel 就是一个“巨无霸”。宏内核的好处是系统运行中性能相对好,不容易受到其他代码的影响,但是缺点不够灵活,如果 Linux Kernel 完全这样做的话,那么就会出现一种情况:哪怕添加一个很小的功能,都得把整个内核重新编译、更新,而且最终的内核将会十分巨大,甚至不能满足引导系统(如 uboot)对内核镜像大小的要求。为了解决这个问题, Linux 在宏内核的基础上采用了可加载模块机制,即将 Kernel 的绝大部分功能都和核心模块编译成一个独立的内核镜像,然后如果你要给内核添加你需要的功能,那么你可以按照 Kernel 的要求规范编写模块(module),在系统启动后将 module 加载到内核空间,这样既可以保证系统的高性能和可靠性,也可以保证系统必要的灵活性,方便的增加新功能,实际上很多需要运行在内核态的功能都是以可加载模块的形式实现的,比如很多驱动程序都是以 module 的形式使用的,除此之外,还有一种使用模块的原因:厂商不愿意开放源代码而通过可加载模块规避版权限制。

Linux 的模块机制可以简单的分为:

1. 构建模块

linux 的模块有专门的编写规范:入口(module_init)、出口(module_exit)和声明信息,最简单的模块就包括这些:

/*tm.c*/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ezio");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

static int __init tm_init(void){
   int result = 0;

   printk(KERN_INFO "Hello Module\n");
   return 0;
}


static void __exit tm_exit(void){
   printk(KERN_INFO "Bye Module\n");
}

module_init(tm_init);
module_exit(tm_exit);
/*Makefile */
obj-m += tm.o

KERNEL_PATH := ***

all:
	make -C $(KERNEL_PATH) M=$(PWD) modules
clean:
	make -C $(KERNEL_PATH) M=$(PWD) clean

其中 KERNEL_PATH 是你的内核文件的路径,编译模块依赖于你的目标内核的源文件。接下来执行 make 就可以编译模块了,最终会生成一个 tm.ko 文件,之后要做的就是 insmod tm.ko 加载我们编译好的这个模块了。注意,如果编译使用的内核源文件和目标内核不匹配的话,加载操作会失败的。

首先,模块使用的函数库和一般的应用程序不同,并没有使用标准库函数和系统库函数,而是用的内核函数和内核头文件,比如头文件引用的是 module.h ,打印使用了 printk() 而不是 printf() ;然后,模块的入口不像普通应用程序使用 main() ,而是 module_init 声明的函数,本处就是 tm_init(),而且模块还显式的声明了退出函数 module_exit ,本处就是 tm_exit(),如果模块不打算退出,则可以省略这一步;最后,要注意的是每个模块都需要声明自己采用的协议,内核模块一般都要使用 GPL 协议,这是因为内核本身采用了 GPL 开源协议,如果你的模块使用了内核的东西则也得采用 GPL 协议,否则是无法编译的 —— GPL 具有传染性。

编译模块需要注意两点:必须使用 Makefile 、必须指出内核位置并使用内核函数。

一个最简单的模块基本这样子,实际的模块要比这复杂的多,不过模块的基本元素都已经在这里包括了。

2. 加载模块

内核中和可加载模块相关的代码主要是:

kernel/module.c
kernel/kmod.c
arch/<arch>/kernel/module.c

其中 kernel 下的两个文件是和架构无关的核心代码,arch 下的 module.c 则是和 CPU 硬件实现相关的代码,比如地址布局、对齐等要素。

加载模块有两种途径:使用系统命令 insmod 加载和使用代码 request_module

2.1. insmod

insmod 加载模块的实现分为两部分:用户空间 insmod 命令的实现和内核空间加载模块的实现。

busybox 实现的 insmod 为例,首先 busybox 实现了 shell 命令 insmod

insmod .c

int insmod_main(int argc UNUSED_PARAM, char **argv)
{
...

    rc = bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));
...
}

modutils/modutils.c

int FAST_FUNC bb_init_module(const char *filename, const char *options)
{
...
    image = try_to_mmap_module(filename, &image_size);
...
    init_module(image, image_size, options);
...
}       

modutils/modutils.c

# define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
# define delete_module(mod, flags) syscall(__NR_delete_module, mod, flags)

shell 命令 insmod 最终就是调用标准库函数的 syscall(__NR_init_module,...) 进入系统调用,注意,此时 busybox 要将模块的起始地址、长度、参数告诉给系统调用,否则系统调用是无法获取到模块文件的。到此加载模块的用户空间代码就执行完成了,接下来通过就要进入内核空间的系统调用 sys_init_module

Linux系统调用的定义所述,sys_init_module 的实现如下:

kernel/module.c

SYSCALL_DEFINE3(init_module, void __user *, umod,        
        unsigned long, len, const char __user *, uargs)  
{ 
    int err;   
    struct load_info info = { };   
  
    err = may_init_module();       
    if (err)   
        return err;     
  
    pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
  umod, len, uargs);      
  
    err = copy_module_from_user(umod, len, &info);       
    if (err)   
        return err;     
  
    return load_module(&info, uargs, 0); 
}

首先系统要检查是否允许加载该模块,然后从用户空间拷贝模块到内核空间(copy_module_from_user()),然后调用 load_module() 开始加载模块。

static int load_module(struct load_info *info, const char __user *uargs,
      int flags)     
{     
...    
    /* Figure out module layout, and allocate all the memory. */        
    mod = layout_and_allocate(info, flags);       
    if (IS_ERR(mod)) { 
        err = PTR_ERR(mod);   
        goto free_copy;
    } 
    /* Reserve our place in the list. */
    err = add_unformed_module(mod);     
    if (err)      
        goto free_module;      
       
...     
    err = check_module_license_and_versions(mod);  
    if (err)      
        goto free_unload;      
...    
    /* Fix up syms, so that st_value is a pointer to location. */      
    err = simplify_symbols(mod, info);
    if (err < 0)
        goto free_modinfo;   
     
    err = apply_relocations(mod, info); 
    if (err < 0)
        goto free_modinfo;   
     
    err = post_relocation(mod, info); 
    if (err < 0)
        goto free_modinfo;   
...   
    /* Link in to syfs. */     
    err = mod_sysfs_setup(mod, info, mod->kp, mod->num_kp);     
    if (err < 0)  
        goto bug_cleanup;  

    if (is_livepatch_module(mod)) {      
        err = copy_module_elf(mod, info);
        if (err < 0)                     
            goto sysfs_cleanup;          
    }                                       
... 
    return do_init_module(mod);
...     
}        

PS : 内核现在已经支持热补丁(livepatch),加载模块时会检查是否是热补丁,然后以执行对应的操作,如果对此感兴趣的话可以看看函数 is_livepatch_module()copy_module_elf()

load_module() 要干的工作包括为 module 分配内存,检查 module 是否已经加载再将 module 挂到全局的模块链表上,判断模块的版权和版本是否合规,处理模块的各种符号和进行地址重定位、将模块和 sysfs 关联起来,最后一步就是调用 do_init_module() 初始化模块。

static noinline int do_init_module(struct module *mod)
{
...

    /* Start the module */
    if (mod->init != NULL)
        ret = do_one_initcall(mod->init);
...
    /* Drop initial reference. */
    module_put(mod);
...
    /*
     * We want to free module_init, but be aware that kallsyms may be
     * walking this with preempt disabled.  In all the failure paths, we
     * call synchronize_sched(), but we don't want to slow down the success
     * path, so use actual RCU here.
     */
    call_rcu_sched(&freeinit->rcu, do_free_init);
...
}

do_init_module() 的主要工作就是调用模块的入口函数(module_init(...))进行模块初始化,最后再释放掉初始化函数。

2.2. request_module

request_module 也是一种加载模块的方法,直接在代码里面加载外部模块,它的定义如下:

kernel/kmod.c

#define request_module(mod...) __request_module(true, mod)

int __request_module(bool wait, const char *fmt, ...)
{
...
    if (!modprobe_path[0])
        return 0;

    va_start(args, fmt);
    ret = vsnprintf(module_name, MODULE_NAME_LEN, fmt, args);
    va_end(args);
    if (ret >= MODULE_NAME_LEN)
        return -ENAMETOOLONG;

    ...
    ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);

    atomic_dec(&kmod_concurrent);
    return ret;
}
EXPORT_SYMBOL(__request_module);

其中 modprobe_path 的定义如下:

char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";

它指出了我们要用到的用户空间 shell 命令 modprobe 的路径。

request_module() 首先检查 modprobe_path 是否正常,然后调用 call_modprobe() 加载模块。 call_modprobe() 的定义如下:

static int call_modprobe(char *module_name, int wait)
{
    struct subprocess_info *info;
    static char *envp[] = {
        "HOME=/",
        "TERM=linux",
        "PATH=/sbin:/usr/sbin:/bin:/usr/bin",
        NULL
    };

    char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
    if (!argv)
        goto out;

    module_name = kstrdup(module_name, GFP_KERNEL);
    if (!module_name)
        goto free_argv;

    argv[0] = modprobe_path;
    argv[1] = "-q";
    argv[2] = "--";
    argv[3] = module_name;  /* check free_modprobe_argv() */
    argv[4] = NULL;

    info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
 NULL, free_modprobe_argv, NULL);
    if (!info)
        goto free_module_name;

    return call_usermodehelper_exec(info, wait | UMH_KILLABLE);

free_module_name:
    kfree(module_name);
free_argv:
    kfree(argv);
out:
    return -ENOMEM;
}

call_modprobe() 的实现很简单,就是启动一个线程执行用户模式的 modprobe 命令加载模块。其中 envpargv 配置了命令运行环境、命令参数,函数 call_usermodehelper_setup() 构造了执行 modprobe 的命令结构体,call_usermodehelper_exec 用来执行命令。

比如,request_module("sound-slot-%i", unit>>4);表示让系统调用用户空间命令 /sbin/modprobe 加载名为 sound-slot-0.ko 模块。

到此加载模块的两种方法就解释完了,后者实际上就是调用了 modprobe 命令。

3. 卸载模块

命令 rmmod 是用来卸载销毁模块的。在 busybos 中 rmmod 的是这样实现的 :

int rmmod_main(int argc UNUSED_PARAM, char **argv)
{
...
    while (*argv) {
...
        if (bb_delete_module(modname, flags))
   bb_error_msg_and_die("can't unload '%s': %s",
modname, moderror(errno));
    }
...
}

int FAST_FUNC bb_delete_module(const char *module, unsigned int flags)
{
    errno = 0;
    delete_module(module, flags);
    return errno;
}

# define delete_module(mod, flags) syscall(__NR_delete_module, mod, flags)

执行了命令 rmmod 之后 busybox 最终会调用系统调用 __NR_delete_module 进入内核空间。

进入内核空间后,系统会调用删除模块的系统调用 sys_delete_module ,它的定义如下:

kernel/module.c

SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
		unsigned int, flags)
{
...

	mod = find_module(name);
...
	/* Doing init or already dying? */
	if (mod->state != MODULE_STATE_LIVE) {
		/* FIXME: if (force), slam module count damn the torpedoes */
		pr_debug("%s already dying\n", mod->name);
		ret = -EBUSY;
		goto out;
	}

	/* If it has an init func, it must have an exit func to unload */
	if (mod->init && !mod->exit) {
		forced = try_force_unload(flags);
		if (!forced) {
			/* This module can't be removed */
			ret = -EBUSY;
			goto out;
		}
	}

	/* Stop the machine so refcounts can't move and disable module. */
	ret = try_stop_module(mod, flags, &forced);
...
	if (mod->exit != NULL)
		mod->exit();
...

	free_module(mod);
...
}

删除模块的过程可以分为这么几步:

  1. 找到模块(find_module()
  2. 判断模块是否还运行并停止模块运行(mod->state != MODULE_STATE_LIVEtry_stop_module()
  3. 执行模块的退出函数(mod->exit()
  4. 释放模块占用的资源(free_module()

其中第四步释放模块资源需要释放的资源包括关联的文件项、相关的全局变量和链表、占用的内存等,调用了函数 free_module()

/* Free a module, remove from lists, etc. */     
static void free_module(struct module *mod)      
{      
    trace_module_free(mod);

    mod_sysfs_teardown(mod); 

    /* We leave it in list to prevent duplicate loads, but make sure     
    * that noone uses it while it's being deconstructed. */
    mutex_lock(&module_mutex);   
    mod->state = MODULE_STATE_UNFORMED; 
    mutex_unlock(&module_mutex); 

    /* Remove dynamic debug info */     
    ddebug_remove_module(mod->name);    

    /* Arch-specific cleanup. */ 
    module_arch_cleanup(mod);

    /* Module unload stuff */
    module_unload_free(mod); 

    /* Free any allocated parameters. */
    destroy_params(mod->kp, mod->num_kp);        

    if (is_livepatch_module(mod))
    free_module_elf(mod);

    /* Now we can delete it from the lists */    
    mutex_lock(&module_mutex);   
    /* Unlink carefully: kallsyms could be walking list. */ 
    list_del_rcu(&mod->list);
    mod_tree_remove(mod);  
    /* Remove this module from bug list, this uses list_del_rcu */
    module_bug_cleanup(mod); 
    /* Wait for RCU-sched synchronizing before releasing mod->list and buglist. */
    synchronize_sched();   
    mutex_unlock(&module_mutex); 

    /* This may be empty, but that's OK */       
    disable_ro_nx(&mod->init_layout);   
    module_arch_freeing_init(mod);      
    module_memfree(mod->init_layout.base);       
    kfree(mod->args);      
    percpu_modfree(mod);   

    /* Free lock-classes; relies on the preceding sync_rcu(). */  
    lockdep_free_key_range(mod->core_layout.base, mod->core_layout.size);

    /* Finally, free the core (containing the module structure) */
    disable_ro_nx(&mod->core_layout);   
    module_memfree(mod->core_layout.base);       

    #ifdef CONFIG_MPU 
    update_protections(current->mm);    
    #endif 
}  

4. 其它 module 相关的命令

和模块相关的除了定义模块、加载模块、删除模块外,还需要了解一些相关的命令(lsmodmodprobemodinfo和可执行文件格式 ELF

4.1. lsmod 和 modinfo

lsmod 可以列出系统已经加载的全部模块,modinfo 用来显示指定模块的信息,比如:

lsmod 列出了所有模块的名称、大小、使用状态、被那个系统模块使用这些信息。

➜  linux-master lsmod
Module                  Size  Used by
veth                   16384  0
vmw_vsock_vmci_transport    28672  0
vsock                  36864  1 vmw_vsock_vmci_transport
vmw_vmci               65536  1 vmw_vsock_vmci_transport
ctr                    16384  1
ccm                    20480  1
xt_addrtype            16384  2
br_netfilter           24576  0
ipt_MASQUERADE         16384  5
nf_nat_masquerade_ipv4    16384  1 ipt_MASQUERADE
iptable_nat            16384  1
nf_nat_ipv4            16384  1 iptable_nat
nf_nat                 24576  2 nf_nat_ipv4,nf_nat_masquerade_ipv4
nf_conntrack_ipv4      16384  3
nf_defrag_ipv4         16384  1 nf_conntrack_ipv4
xt_conntrack           16384  2
nf_conntrack          106496  5 nf_nat,nf_nat_ipv4,xt_conntrack,nf_nat_masquerade_ipv4,nf_conntrack_ipv4
ipt_REJECT             16384  2
nf_reject_ipv4         16384  1 ipt_REJECT
aufs                  217088  0
ebtable_filter         16384  0
ebtables               36864  1 ebtable_filter
ip6table_filter        16384  0
ip6_tables             28672  1 ip6table_filter
pci_stub               16384  1
vboxpci                24576  0
vboxnetadp             28672  0
vboxnetflt             28672  0
xt_CHECKSUM            16384  3

...

而 modinfo 则会给出指定模块(此处为 jffs2)的模块路径、版权、依赖、作者等信息。

➜  linux  modinfo jffs2
filename:       /lib/modules/3.13.0-52-generic/kernel/fs/jffs2/jffs2.ko
license:        GPL
author:Red Hat, Inc.
description:    The Journalling Flash File System, v2
alias: fs-jffs2
srcversion:     9480939EEC3AC916B2F529E
depends:        mtd
intree:Y
vermagic:       3.13.0-52-generic SMP mod_unload modversions 686
signer:Magrathea: Glacier signing key
sig_key:        E1:7C:1A:20:0E:70:82:2E:A7:1B:75:F9:A6:8F:D2:E2:5D:B8:9A:5A
sig_hashalgo:   sha512

以 busybox 实现的 lsmod 和 modinfo 为例。

  1. lsmod
int lsmod_main(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
{
...
    parser_t *parser = config_open("/proc/modules");
...
        while (config_read(parser, token, 4, 4, "# \t", PARSE_NORMAL & ~PARSE_GREEDY)) {
            // N.B. token[3] is either '-' (module is not used by others)
            // or comma-separated list ended by comma
            // so trimming the trailing char is just what we need!
            if (token[3][0])
                token[3][strlen(token[3]) - 1] = '\0';
...
            printf("%-19s %8s %2s %s\n", token[0], token[1], token[2], token[3]);
...
        }
    }
    if (ENABLE_FEATURE_CLEAN_UP)
        config_close(parser);
...
}

从代码可以看出,lsmod 只是读取系统文件 /proc/modules , 使用了 busybox 的三个内部函数 config_read/open/close,读取并解析模块信息文件然后打印出来即可。/proc/modules 的内容和 lsmod 打印出来的内容一致,只是在格式上有些许不同而已:

➜  linux-master cat /proc/modules 
veth 16384 0 - Live 0x0000000000000000
vmw_vsock_vmci_transport 28672 0 - Live 0x0000000000000000
vsock 36864 1 vmw_vsock_vmci_transport, Live 0x0000000000000000
vmw_vmci 65536 1 vmw_vsock_vmci_transport, Live 0x0000000000000000
ctr 16384 1 - Live 0x0000000000000000
ccm 20480 1 - Live 0x0000000000000000
xt_addrtype 16384 2 - Live 0x0000000000000000
br_netfilter 24576 0 - Live 0x0000000000000000
ipt_MASQUERADE 16384 5 - Live 0x0000000000000000
nf_nat_masquerade_ipv4 16384 1 ipt_MASQUERADE, Live 0x0000000000000000
iptable_nat 16384 1 - Live 0x0000000000000000
nf_nat_ipv4 16384 1 iptable_nat, Live 0x0000000000000000
nf_nat 24576 2 nf_nat_masquerade_ipv4,nf_nat_ipv4, Live 0x0000000000000000
nf_conntrack_ipv4 16384 3 - Live 0x0000000000000000
nf_defrag_ipv4 16384 1 nf_conntrack_ipv4, Live 0x0000000000000000
xt_conntrack 16384 2 - Live 0x0000000000000000
nf_conntrack 106496 5 nf_nat_masquerade_ipv4,nf_nat_ipv4,nf_nat,nf_conntrack_ipv4,xt_conntrack, Live 0x0000000000000000
ipt_REJECT 16384 2 - Live 0x0000000000000000
nf_reject_ipv4 16384 1 ipt_REJECT, Live 0x0000000000000000
aufs 217088 0 - Live 0x0000000000000000
ebtable_filter 16384 0 - Live 0x0000000000000000
ebtables 36864 1 ebtable_filter, Live 0x0000000000000000
ip6table_filter 16384 0 - Live 0x0000000000000000
ip6_tables 28672 1 ip6table_filter, Live 0x0000000000000000
pci_stub 16384 1 - Live 0x0000000000000000
vboxpci 24576 0 - Live 0x0000000000000000 (OE)
vboxnetadp 28672 0 - Live 0x0000000000000000 (OE)

...
  1. modinfo

modinfolsmod 一样也是直接使用模块的系统文件:

int modinfo_main(int argc UNUSED_PARAM, char **argv)
{
...

    uname(&uts);
    parser = config_open2(
        xasprintf("%s/%s/%s", CONFIG_DEFAULT_MODULES_DIR, uts.release, CONFIG_DEFAULT_DEPMOD_FILE),
        xfopen_for_read
        );

        while (config_read(parser, tokens, 2, 1, "# \t", PARSE_NORMAL)) {
            colon = last_char_is(tokens[0], ':');
            if (colon == NULL)
            continue;
            *colon = '\0';
            filename2modname(bb_basename(tokens[0]), name);
            for (i = 0; argv[i]; i++) {
               if (fnmatch(argv[i], name, 0) == 0) {
                modinfo(tokens[0], uts.release, &env);
                argv[i] = (char *) "";
            }
        }
    }
    if (ENABLE_FEATURE_CLEAN_UP)
    config_close(parser);

    for (i = 0; argv[i]; i++) {
        if (argv[i][0]) {
           modinfo(argv[i], uts.release, &env);
       }
   }

   return 0;
}

modinfo 直接调用 modinfo_main() ,读取模块的系统文件获取它的信息。首先在存放模块文件的路径下读取 modules.dep 找到模块依赖文件的具体路径,路径的获取是通过函数 xasprintf("%s/%s/%s", CONFIG_DEFAULT_MODULES_DIR, uts.release, CONFIG_DEFAULT_DEPMOD_FILE) 获取的,其中 CONFIG_DEFAULT_MODULES_DIRCONFIG_DEFAULT_DEPMOD_FILE 的定义如下:

CONFIG_DEFAULT_DEPMOD_FILE="modules.dep"
CONFIG_DEFAULT_MODULES_DIR="/lib/modules"

uts.release 则是通过 uname 获取的内核版本号,完整的路径就类似这样:

/lib/modules/4.4.0-21-generic/modules.dep

而这个文件的内容就是每个模块的 .ko 文件实际存放路径,比如:

➜  linux-master ccat /lib/modules/4.4.0-21-generic/modules.dep  | more
kernel/arch/x86/kernel/cpu/mcheck/mce-inject.ko:
kernel/arch/x86/kernel/msr.ko:
kernel/arch/x86/kernel/cpuid.ko:

得到模块文件的存放路径后就需要调用函数 modinfo() 打开模块文件获取模块的详细信息并打印出来:

static void modinfo(const char *path, const char *version,
   const struct modinfo_env *env)     
{    
    static const char *const shortcuts[] = {   
        "filename", 
        "license", 
        "author",  
        "description",
        "version",   
        "alias",
        "srcversion",
        "depends",
        "uts_release", 
        "vermagic", 
        "parm",  
        "firmware", 
    };       
    ...
    the_module = xmalloc_open_zipped_read_close(path, &len);
    if (!the_module) {   
        if (path[0] == '/')
        return;      
        /* Newer depmod puts relative paths in modules.dep */   
        path = xasprintf("%s/%s/%s", CONFIG_DEFAULT_MODULES_DIR, version, path);
        the_module = xmalloc_open_zipped_read_close(path, &len);
        free((char*)path); 
        if (!the_module) 
        return;      
    }
...  
}
  1. 小结

lsmod 和 modinfo 两个命令的实现其实就是直接读取 Linux 的系统文件来获取模块的信息,并不直接域内核函数或系统调用发生关系: lsmod 就是读取 /proc/modules 的内容,modinfo 的信息是从 /lib/modules 和模块文件获取的。

4.2. modprobe

modprobe 根据参数的不同,可以用来加载或删除模块,在 busybox 里它的实现如下:

int modprobe_main(int argc UNUSED_PARAM, char **argv)
{
...
	/* Goto modules location */
	xchdir(CONFIG_DEFAULT_MODULES_DIR);
	uname(&G.uts);
	xchdir(G.uts.release);
...

... 
	if (!argv[0]) {
		if (opt & OPT_REMOVE) {
			/* "modprobe -r" (w/o params).
			 * "If name is NULL, all unused modules marked
			 * autoclean will be removed".
			 */
			if (bb_delete_module(NULL, O_NONBLOCK | O_EXCL) != 0)
				bb_perror_nomsg_and_die();
		}
		return EXIT_SUCCESS;
	}

... 
	if (opt & (OPT_INSERT_ALL | OPT_REMOVE)) {
		/* Each argument is a module name */
		do {
			DBG("adding module %s", *argv);
			add_probe(*argv++);
		} while (*argv);
	} else {
		/* First argument is module name, rest are parameters */
		DBG("probing just module %s", *argv);
		add_probe(argv[0]);
		G.cmdline_mopts = parse_cmdline_module_options(argv, /*quote_spaces:*/ 1);
	}

	/* Happens if all requested modules are already loaded */
	if (G.probes == NULL)
		return EXIT_SUCCESS;

	read_config("/etc/modprobe.conf");
	read_config("/etc/modprobe.d");
	if (ENABLE_FEATURE_MODUTILS_SYMBOLS && G.need_symbols)
		read_config("modules.symbols");
	load_modules_dep();
	if (ENABLE_FEATURE_MODUTILS_ALIAS && G.num_unresolved_deps) {
		read_config("modules.alias");
		load_modules_dep();
	}

	rc = 0;
	while ((me = llist_pop(&G.probes)) != NULL) {
		if (me->realnames == NULL) {
			DBG("probing by module name");
			/* This is not an alias. Literal names are blacklisted
			 * only if '-b' is given.
			 */
			if (!(opt & OPT_BLACKLIST)
			 || !(me->flags & MODULE_FLAG_BLACKLISTED)
			) {
				rc |= do_modprobe(me);
			}
			continue;
		}

		/* Probe all real names for the alias */
		do {
			char *realname = llist_pop(&me->realnames);
			struct module_entry *m2;

			DBG("probing alias %s by realname %s", me->modname, realname);
			m2 = get_or_add_modentry(realname);
			if (!(m2->flags & MODULE_FLAG_BLACKLISTED)
			 && (!(m2->flags & MODULE_FLAG_LOADED)
			    || (opt & (OPT_REMOVE | OPT_SHOW_DEPS)))
			) {
//TODO: we can pass "me" as 2nd param to do_modprobe,
//and make do_modprobe emit more meaningful error messages
//with alias name included, not just module name alias resolves to.
				rc |= do_modprobe(m2);
			}
			free(realname);
		} while (me->realnames != NULL);
	}

	if (ENABLE_FEATURE_CLEAN_UP)
		moddb_free(&G.db);

	return (rc != 0);
}

static int do_modprobe(struct module_entry *m)
{
...

				rc = bb_delete_module(m2->modname, O_EXCL);
...

		rc = bb_init_module(fn, options);
...
	return rc;
}

modprobe 加载或者卸载模块最终都调用了函数 bb_delete_module()bb_init_module() ,实际上就是对 lsmodrmmod 的封装,以及增加了些额外的功能。

5. 总结

Linux 的模块机制大致就是这样的:按照规范编写模块,内核提供系统调用将模块的可执行文件从用户空间拷贝到内核空间,然后调用模块初始化函数进行初始化,并解析模块可执行文件(.ko 文件),用完之后再调用模块的卸载函数进行卸载。

实际上 Linux 的模块机制比上面介绍的还要复杂,要完成的工作并不限于上述的几个步骤。当系统将 .ko 文件拷贝到内核空间后,内核还需要按照模块可执行文件格式(也就是 ELF)解析模块,将必要的符号(即函数、变量)地址暴露给外部,这样系统的其它组件才能使用模块的功能。这些和其它细节本文暂不讨论。