Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3
Note

Work in progress

 

In Asterisk 12, we have relied on Asterisk's module architecture more than ever. While this has been good from a software architecture perspective, it has exposed some limitations of the Asterisk module loader. This page describes my ideas of what to do to improve the situation.

Table of Contents
printablefalse
stylenone
outlinetrue

Module Loader Problems

Dependent modules must be configured in modules.conf

At build time, Asterisk knows the inter-module dependencies necessary for building. Unfortunately, that information isn't kept at runtime. This means that if module A depends on module B, module B must be listed explicitly in modules.conf (well, unless you turn on autoloading). Some of these dependencies are not obvious, making life difficult for users who like to load with a minimal configuration.

Modules may load even if a dependent module fails to load

If module A depends on module B, and module B fails to load due to a configuration error, module A loads anyways. It's module A's responsibility to check error codes on the module B functions it calls, but we often fail to honor that responsibility.

Module dependencies don't bump refcounts

If module A depends on module B, the user is still allowed to explicitly unload module B. The next time module A calls a module B function, hilarity ensues.

Module loading bugs tend to be inconsistent

There are a number of things which can cause failures at load time, such as taking the address of a function from another module (this requires an eager symbol resolution, even when loaded with RTLD_LAZY. Depending on which module happens to get loaded first, sometimes it works, sometimes it fails). Module loading should be at least consistent so that if there are bugs, they are easier to reproduce on different systems. Plus, consistency is usually a good thing.

Proposed Solution

Inject dependencies into ast_module_info block

When menuconfig parses the MODULEINFO comment in a module's source file, it can take that dependency information and inject it bad into the module as a -D compiler predefine. I'm not sure what format would be best, but possibly just a colon/comma separated list of dependencies would be best.

Clean up dependency data

Module dependencies are currently specified as a mix of <use type=""> and <depend> tags, but the difference is undocumented (or the documentation is on display in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying 'Beware of the Leopard'). In order to properly inject dependencies, we'll need to know which dependencies are modules, and which are libraries.

Dependency information will need to be made consistent, especially using <use type="external"> for dependencies on external libraries.

Introduce module providers

There are some features within Asterisk which may have multiple providers/implementors. Media formats could be considered a special case of this, but sorcery providers would be another interesting use case. The MODULEINFO block will need to be updated to denote when a module is a provider for an interface, and when a module uses an interface. This will ensure that all of an interface's providers are loaded before users of that interface need it.

I'm thinking something like:

Code Block
/* A module using sorcery */
/*** MODULEINFO
        <provides>sorcery</provides>
        <support_level>core</support_level>
 ***/
 
/* A module using sorcery */
/*** MODULEINFO
        <use type="provider">sorcery</provides>
        <support_level>core</support_level>
 ***/
Info

My idea of injecting dependency data using menuconfig may not work out. I think it will, but if for some reason it doesn't, then dependency information will need to be duplicated in the AST_MODULE_INFO block. This is unfortunate, but we do that today with the .nonoptreq field, so meh.

 

Replace priority load order with a graph loader

Instead of ordering module loading by priority, modules would be loaded according to their specified dependencies. Here are some rough suggestions I have for the implementation.

  1. List modules in the lib/asterisk/modules directory
  2. Build module graph
    • Load module metadata lazily, to avoid loading metadata for a module you're not going to load
      • dlopen() with the RTLD_LAZY | RTLD_LOCAL flags, so that module load ordering doesn't matter
        • Normal inter-module function calls won't link, so module load ordering won't matter
        • If one module attempts to take the address of a function from another module, then dlopen() will always fail, consistently. Inter-module function pointers or global variable access must always be wrapped with a function.
        • Module functions won't be exported in the pre-initialized state
    • If autoload is enabled, then noload on a module implies noload for all modules that depend on it (and the modules that depend on them, recursively)
      • This does not apply to use types of optional or provider

      • If one of these modules that gets the implies noload is also specified in a load statement, exit with an error
    • If autoload is disabled, then load on a module implies load for all modules it depends on (and the modules that they depend on, recursively)
      • This does not apply to use types of optional or provider
      • If one of these moduels that gets the implies load is also specified in a noload statement, exit with an error

  3. Load all preload modules
    • Use the logic specified below
  4. Load the rest of the modules
    • Recursively load all of a module's dependencies that haven't been set to noload
      • If a dependency's load fails with a AST_MODULE_LOAD_DECLINE, also decline to load
      • Increment the module use count for the dependency
    • Reopen using dlopen() with RTLD_NOW | RTLD_LOCAL
      • Since we have loaded all dependencies, we can link functions now instead of lazily
      • We don't want to export functions until the module has been successfully initialized
    • Invoke the module's init function
      • If AST_MODULE_LOAD_FAILURE, exit with an error
      • if AST_MODULE_LOAD_DECLINE, return
    • If the modules is marked as AST_MODFLAG_GLOBAL_SYMBOLS, reopen with RTLD_NOW | RTLD_GLOBAL

Reopening modules

There's been a lot of mystery and confusion surrounding dlopen() and dlclose(). This is probably due to bugs in older systems we aren't likely to encounter any more. And, by experience, the work arounds put in place for buggy systems have caused Asterisk bugs on properly behaving systems.

When reopening the module, the docs (and observed behavior) tell me that we should first open the module, and then close the new handle. dlopen() and dlclose() refcounts the modules, and will change module flags on multiple dlopen(). This avoids unnecessary churn running load/unload code in the modules simply to change flags.

Code Block
int module_reopen(struct ast_module *module, int flags) {
  /* Assuming that ast_module has file and lib members */
  void *lib = dlopen(module->file, flags);
  if (!lib) {
    /* Print error from dlerror */
    return -1;
  }
  if (dlclose(module->lib) != 0) {
    /* Print error from dlerror */
    dlclose(lib);
    return -1;
  }
  module->lib = lib;
  return 0;
}

Loading/unloading at runtime

When a module is loaded at runtime, it should recursively load any modules it depends on (excluding optional and provider dependencies). It should also bump the use count for any module it depends on.

Conversely, when a module is unloaded it should recursively unload any modules that depend on it (excluding optional and provider dependencies). Unloading decrements the use count for any module it depends on.