Ceph 的命令行参数处理

ceph 不管是客户端 lib 库、命令行,还是服务端守护进程都支持大量的配置参数,与大多数软件一致,ceph 的配置同样支持多个级别的配置:

  • 代码内置的默认配置值
  • 环境变量
  • 配置文件
  • 命令行参数

librados 提供了如下的 C/C++ 接口支持参数配置:

rados_conf_read_file / Rados::conf_read_file
rados_conf_parse_argv / Rados::conf_parse_argv
rados_conf_parse_argv_remainder / Rados::conf_parse_argv_remainder
rados_conf_parse_env / Rados::conf_parse_env
rados_conf_set / Rados::conf_set
rados_conf_get / Rados::conf_get

其实现主要调用了如下一些接口:

argv_to_vec
env_to_vec
conf->parse_config_files
conf->parse_argv
conf->set_val
conf->get_val

其中 confmd_config_t * 实例。

服务端(mon/mgr/osd/mds)的配置参数处理流程如下:

  1. 调用 argv_to_vec 将 main 函数的命令行参数转成 vector<const char*> 的形式;
  2. 调用 env_to_vecCEPH_ARGS 环境变量转成 vector<const char*> 的形式,并与 argv_to_vec 得到的 vector 合并;
  3. 调用 global_init -> global_pre_init 解析参数:
void global_pre_init(std::vector < const char * > *alt_def_args,
             std::vector < const char* >& args,
             uint32_t module_type, code_environment_t code_env,
             int flags)
{
  std::string conf_file_list;
  std::string cluster = "";
  // 调用 ceph_argparse_early_args 得到集群名字、id、配置文件路径,注意其中的 module_type 参数指定了 entity type,因此不应该
  // 再通过 --name / -n 参数进行覆盖;
  CephInitParameters iparams = ceph_argparse_early_args(args, module_type,
                            &cluster, &conf_file_list);

  CephContext *cct = common_preinit(iparams, code_env, flags);

  cct->_conf->cluster = cluster;
  md_config_t *conf = cct->_conf;

  // 调用 conf->parse_argv 解析 alt_def_args 指定的参数,alt_def_args 与 args 类似,但 args 是用户指定的参数,而 alt_def_args 是
  // 进程实现代码设置的一些默认值;
  if (alt_def_args)
    conf->parse_argv(*alt_def_args);  // alternative default args

  // 调用 conf->parse_config_files 解析配置文件(可以是使用 ;,= \t 等字符分隔的一个列表),如果用户没有使用 --conf / -c 指定配置
  // 文件,则会尝试读取 CEPH_CONF 环境变量,如果环境变量也没有定义,则会使用默认的配置文件;
  int ret = conf->parse_config_files(c_str_or_null(conf_file_list),
                     &cerr, flags);

  // 调用 conf->parse_env 读取 CEPH_KEYRING 环境变量定义的 keyring 路径,注意不要被它的名字所迷惑,它就解析这么一个环境
  // 变量,真正的环境变量(即  CEPH_ARGS)定义的参数在前面通过 env_to_vec 已经与 argv_to_vec 得到的命令行参数合并至 args 了;
  conf->parse_env(); // environment variables override

  // 调用 conf->parse_argv 解析合并后的参数 args;
  conf->parse_argv(args); // argv override
}

显然,除了通过调用 ceph_argparse_early_args 得到集群名字、id、配置文件路径,与 librados 客户端的接口类似,具体的配置参数都传递给内部的 md_config_t *conf 实例进行处理。

参数的处理依赖于 ceph_argparse 模块(common/ceph_argparse.cc)中定义的辅助函数,主要的辅助函数如下:

  1. argv_to_vec,将命令行参数(int argc, const char **argv)转成 vector<const char*> 的形式,由于它假设第一个参数是可执行文件的路径,在处理时会跳过第一个参数:
args.insert(args.end(), argv + 1, argv + argc);

由于 librados 接口 rados_conf_parse_argv / Rados::conf_parse_argv 在内部也调用了这个辅助函数,因此需要特别注意这个特殊处理;

  1. env_to_vec,将指定的环境变量(如果不指定则使用默认的 CEPH_ARGS 环境变量)以空格作为分隔符转成 vector<const char*> 的形式,同时它还会与传递进来的 vector<const char*> (通常是 argv_to_vec 的处理结果)进行合并,且 env_to_vec 在合并的时候会考虑对 -- 进行特殊处理:
args.clear();
args.insert(args.end(), options.begin(), options.end());
args.insert(args.end(), env_options.begin(), env_options.end());
if (dashdash)
  args.push_back("--");
args.insert(args.end(), arguments.begin(), arguments.end());
args.insert(args.end(), env_arguments.begin(), env_arguments.end());

形成类似如下的排序:options -> env_options -> arguments -> env_arguments

注意在命令行中指定 ceph 的环境命令时需要使用单引号,如:

export CEPH_ARGS='log_file=/var/log/ceph/$cluster-$name.$pid.$cctid.log admin_socket=$run_dir/clients/$cluster-$name.$pid.$cctid.asok'

因为在命令行中双引号中的 $ 会被自动展开,导致得到的结果与预期完全不一致;

  1. ceph_argparse_early_args,遍历传入的 vector<const char*> 参数,查找参数中是否指定了 --version / -v / --conf / -c / --cluster / -i / --id / --user / --name / -n / --show-args 等 11 个选项:
for (std::vector<const char*>::iterator i = args.begin(); i != args.end(); ) {
  if (strcmp(*i, "--") == 0) {
    break;
  } else if (ceph_argparse_flag(args, i, "--version", "-v", (char*)NULL)) {
    cout << pretty_version_to_str() << std::endl;
    _exit(0);
  } else if (ceph_argparse_witharg(args, i, &val, "--conf", "-c", (char*)NULL)) {
    *conf_file_list = val;
  }
  ...
  else {
    // ignore
    ++i;
  }
}

--version / -v,打印版本信息并退出;

--show-args,打印命令行以及环境变量指定的参数并退出;

--conf / -c,指定配置文件路径;

--cluster,指定集群名字;

-i,指定 entity id,用于非 librados 客户端,即通常用于服务端守护进程(mon/mgr/osd/mds);

--id / --user,指定 entity id,用于 librados 客户端;

--name / -n,指定 entity type 和 id,由于与 -i / --id 一样都会设置 id,因此如果同时指定的话,后面的参数设置会覆盖前面的参数设置,由于对于守护进程也可以指定 --name / -n 参数,因此会覆盖进程实际的 entity type(此处存在 bug,对于守护进程不应该指定 --name / -n);

其内部又调用了 ceph_argparse_xxx 等更底层的辅助函数;

  1. ceph_argparse_flag,假定命令行是类似 --flag / -f 这样的 flag 选项,将用户传递的命令行与指定的选项进行对比,如果返回 true 则表明命令行与指定 flag 选项匹配:
// 指定进行对比的选项可以同时指定长选项和短选项,所以使用可变参数
bool ceph_argparse_flag(std::vector<const char*> &args,
    std::vector<const char*>::iterator &i, ...)
{
  // 命令行
  const char *first = *i;
  char tmp[strlen(first)+1];
  // 将命令行中的中划线转成下划线
  dashes_to_underscores(first, tmp);
  first = tmp;

  va_list ap;
  va_start(ap, i);
  // 遍历可变参数
  while (1) {
    // 得到指定进行对比的选项
    const char *a = va_arg(ap, char*);
    if (a == NULL) { // 最后一个可变参数是 NULL,因此 while 循环能够正常结束
      va_end(ap);
      return false;
    }

    // 将指定的选项中的中划线转成下划线,实际上不需要,因为 ceph 支持的选项都是使用的下划线
    char a2[strlen(a)+1];
    dashes_to_underscores(a, a2);

    if (strcmp(a2, first) == 0) { // 命令行与指定的选项匹配
      i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 flag 选项:--flag / -f
      va_end(ap);
      return true;
    }
  }
}
  1. ceph_argparse_binary_flag,假定命令行是类似 --flag=true / -f=0 这样的带真值的 flag 选项,或者类似 --flag / -f 这样的 flag 选项,将用户传递的命令行与指定的选项进行对比,如果返回 true 则表明命令行与指定选项匹配,并设置 flag 的真值,其主要处理流程通过调用 va_ceph_argparse_binary_flag 实现:
// 指定进行对比的选项可以同时指定长选项和短选项,所以使用可变参数,注意 va_start(ap, oss) / va_end(ap) 都在外层函数 ceph_argparse_binary_flag 中被调用
static bool va_ceph_argparse_binary_flag(std::vector<const char*> &args,
    std::vector<const char*>::iterator &i, int *ret, std::ostream *oss,
    va_list ap) {
  // 命令行
  const char *first = *i;
  char tmp[strlen(first) + 1];
  // 将命令行中的中划线转成下划线
  dashes_to_underscores(first, tmp);
  first = tmp;

  // 遍历可变参数,注意 va_start(ap, oss) 在外层函数 ceph_argparse_binary_flag 中被调用
  // does this argument match any of the possibilities?
  while (1) {
    // 得到指定进行对比的选项
    const char *a = va_arg(ap, char*);
    if (a == NULL) // 最后一个可变参数是 NULL,因此 while 循环能够正常结束,注意 va_end(ap) 在外层函数 ceph_argparse_binary_flag 中被调用
      return false;

    // 将指定的选项中的中划线转成下划线,实际上不需要,因为 ceph 支持的选项都是使用的下划线
    int strlen_a = strlen(a);
    char a2[strlen_a + 1];
    dashes_to_underscores(a, a2);

    if (strncmp(a2, first, strlen(a2)) == 0) { // 命令行与指定的选项匹配
      if (first[strlen_a] == '=') {
        i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 flag 选项: --flag=true / --flag=1 / -f=true / -f=1 / --flag=false / --flag=0 / -f=false / -f=0
        const char *val = first + strlen_a + 1;
        if ((strcmp(val, "true") == 0) || (strcmp(val, "1") == 0)) { // 命令行为 --flag=true / --flag=1 / -f=true / -f=1 的形式
          *ret = 1;
          return true;
        } else if ((strcmp(val, "false") == 0) || (strcmp(val, "0") == 0)) { // 命令行为 --flag=false / --flag=0 / -f=false / -f=0 的形式
          *ret = 0;
          return true;
        }
        *ret = -EINVAL;
        return true;
      } else if (first[strlen_a] == '\0') { // 命令行为 --flag / -f 的形式
        i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 flag 选项: --flag / -f
        *ret = 1;
        return true;
      }
    }
  }
}
  1. ceph_argparse_witharg,假定命令行是类似 --option=arg / -o=arg / --option arg / -o arg 这样的这样的带参数的 option 选项,将用户传递的命令行与指定的选项进行对比,如果返回 true 则表明命令行与指定选项匹配,并设置 arg 的值,由于参数 arg 的类型有差异,因此针对字符串类型与数值型有不同的错误处理,但其内部主要处理流程一致,都是通过调用 va_ceph_argparse_witharg 实现:
// 指定进行对比的选项可以同时指定长选项和短选项,所以使用可变参数,注意 va_start(ap, oss) / va_end(ap) 都在外层函数 ceph_argparse_witharg 中被调用
static int va_ceph_argparse_witharg(std::vector<const char*> &args,
    std::vector<const char*>::iterator &i, std::string *ret, std::ostream &oss,
    va_list ap) {
  // 命令行
  const char *first = *i;
  char tmp[strlen(first) + 1];
  // 将命令行中的中划线转成下划线
  dashes_to_underscores(first, tmp);
  first = tmp;

  // 遍历可变参数,注意 va_start(ap, oss) 在外层函数 ceph_argparse_witharg 中被调用
  // does this argument match any of the possibilities?
  while (1) {
    // 得到指定进行对比的选项
    const char *a = va_arg(ap, char*);
    if (a == NULL) // 最后一个可变参数是 NULL,因此 while 循环能够正常结束,注意 va_end(ap) 在外层函数 ceph_argparse_witharg 中被调用
      return 0;

    // 将指定的选项中的中划线转成下划线,实际上不需要,因为 ceph 支持的选项都是使用的下划线
    int strlen_a = strlen(a);
    char a2[strlen_a + 1];
    dashes_to_underscores(a, a2);

    if (strncmp(a2, first, strlen(a2)) == 0) { // 命令行与指定的选项匹配
      if (first[strlen_a] == '=') { // 命令行为 --option=arg / -o=arg 的形式
        *ret = first + strlen_a + 1;
        i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 option 选项: --option=arg / -o=arg
        return 1;
      } else if (first[strlen_a] == '\0') { // 命令行参数可能为 --option arg / -o arg 的形式
        // find second part (or not)
        if (i + 1 == args.end()) {
          oss << "Option " << *i << " requires an argument." << std::endl;
          i = args.erase(i);
          return -EINVAL;
        }

        i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 option 选项: --option / -o
        *ret = *i;
        i = args.erase(i); // 从命令行 vector<const char*> 容器中删除匹配的命令行 option 选项值:arg
        return 1;
      }
    }
  }
}

显然,几个 ceph_argparse_xxx 的处理都大同小异,如果与指定的选项匹配就返回 true,并且根据选项类型(即 flag 选项 或 option 选项)以不同的方式获取选项值。

同时需要注意的时,一旦选项匹配,命令行选项(如果是 option 选项,可能还包括 option 选项值)就会从命令行 vector<const char*> 容器中删除, 这在 librados 与主进程的其他命令行处理库协作时需要特别注意。

上面分析了 ceph_argparse 模块的底层实现细节,现在回到 md_config_t,对于参数的解析,它主要包括 parse_config_filesparse_argv 两个主要的接口:

  1. md_config_t::parse_config_files

如果用户没有使用 --conf / -c 指定配置文件,则会尝试读取 CEPH_CONF 环境变量,如果环境变量也没有定义,则会使用默认的配置文件($data_dir/config, /etc/ceph/$cluster.conf, ~/.ceph/$cluster.conf, $cluster.conf),最后得到配置文件是使用 ;,= \t 等字符分隔的一个列表。

然后调用 parse_config_files_impl 解析所有的配置文件:

int md_config_t::parse_config_files_impl(const std::list<std::string> &conf_files, std::ostream *warnings) {
  assert(lock.is_locked());

  // open new conf
  list<string>::const_iterator c;
  for (c = conf_files.begin(); c != conf_files.end(); ++c) { // 遍历配置文件
    cf.clear();
    string fn = *c;
    expand_meta(fn, warnings);
    // 调用 ConfFile::parse_file 逐行解析配置文件,并形成按节(section)组织的结构(以节名为 key):map<string, ConfSection>
    // 其中 ConfSection 为:set<ConfLine>,而 ConfLine 是一组 kv 键值对
    // 在 ConfFile::process_line 过程中 ConfLine 可能会用来保存节名,但在最终的 set<ConfLine> 结构中,ConfLine 只保存着键值对
    int ret = cf.parse_file(fn.c_str(), &parse_errors, warnings);
    if (ret == 0) // 任何一个配置文件解析成功,就不再遍历下一个配置文件了,因此用户在命令行指定多个配置文件没有意义
      break;
    else if (ret != -ENOENT)
      return ret;
  }
  // it must have been all ENOENTs, that's the only way we got here
  if (c == conf_files.end())
    return -ENOENT;

  if (cluster.size() == 0) { // 如果没有定义集群名,则截取找到的配置文件的文件名作为集群名,即 xxx.conf -> xxx
    /*
     * If cluster name is not set yet, use the prefix of the
     * basename of configuration file as cluster name.
     */
    auto start = c->rfind('/') + 1;
    auto end = c->find(".conf", start);
    if (end == c->npos) {
      /*
       * If the configuration file does not follow $cluster.conf
       * convention, we do the last try and assign the cluster to
       * 'ceph'.
       */
      cluster = "ceph";
    }
    else {
      cluster = c->substr(start, end - start);
    }
  }

  std::vector<std::string> my_sections;
  _get_my_sections(my_sections); // 与自身有关的配置节,依次为 [type.id], [type], [global]


  // 遍历 ceph 配置参数选项,即 common/options.cc 中定义的所有配置参数
  for (const auto &i : schema) {
    const auto &opt = i.second;

    std::string val;
    // 遍历与自身有关的配置节,并从每一节的配置中查找是否定义了当前的配置参数选项,如果在某一节中找到了配置,则跳过剩下的节,
    // 因此 [type.id], [type], [global] 的配置优先级为从高到低
    int ret = _get_val_from_conf_file(my_sections, opt.name, val, false);
    if (ret == 0) {
      std::string error_message;
      int r = set_val_impl(val, opt, &error_message); // 设置
      if (warnings != nullptr && (r != 0 || !error_message.empty())) {
        *warnings << "parse error setting '" << opt.name << "' to '" << val
            << "'";
        if (!error_message.empty()) {
          *warnings << " (" << error_message << ")";
        }
        *warnings << std::endl;
      }
    }
  }

  // 遍历日志子系统
  // subsystems?
  for (size_t o = 0; o < subsys.get_num(); o++) {
    // 构建日志级别选项 debug_xxx,注意默认的日志子系统名称是 none
    std::string as_option("debug_");
    as_option += subsys.get_name(o);

    std::string val;
    // 与前面遍历配置参数类似,查找是否配置了日志子系统的日志打印级别
    int ret = _get_val_from_conf_file(my_sections, as_option.c_str(), val,
        false);
    if (ret == 0) {
      int log, gather;
      int r = sscanf(val.c_str(), "%d/%d", &log, &gather);
      if (r >= 1) {
        if (r < 2)
          gather = log;
        //  cout << "config subsys " << subsys.get_name(o) << " log " << log << " gather " << gather << std::endl;
        subsys.set_log_level(o, log);  // 设置
        subsys.set_gather_level(o, gather);
      }
    }
  }

  ...

  return 0;
}

其处理流程比较简单,主要就是先读取配置文件,并以节的形式组织配置项,而每一节的内部又由一组 kv 键值对组成,然后遍历所有已知的配置项,从配置文件中查找是否定义了该配置项,如果找到了则用该配置项覆盖代码中定义的默认值。

从上面的处理流程显然可以看到,在配置文件中定义 ceph 不支持的配置项是没有任何问题的。

  1. md_config_t::parse_argv

最核心的处理流程如下:

std::string val;
for (std::vector<const char*>::iterator i = args.begin(); i != args.end();) {
  if (strcmp(*i, "--") == 0) {
    /* Normally we would use ceph_argparse_double_dash. However, in this
     * function we *don't* want to remove the double dash, because later
     * argument parses will still need to see it. */
    break;
  }
  else if (ceph_argparse_flag(args, i, "--show_conf", (char*) NULL)) { // 打印找到的配置文件中的内容
    cerr << cf << std::endl;
    _exit(0);
  }
  else if (ceph_argparse_flag(args, i, "--show_config", (char*) NULL)) { // 显示与指定 entity 相关的所有选项,会对 $变量 进行展开
    show_config = true;
  }
  else if (ceph_argparse_witharg(args, i, &val, "--show_config_value", // 显示与指定 entity 的指定选项,会对 $变量 进行展开
      (char*) NULL)) {
    show_config_value = true;
    show_config_value_arg = val;
  }
  else if (ceph_argparse_flag(args, i, "--foreground", "-f", (char*) NULL)) {
    set_val_or_die("daemonize", "false");
  }
  else if (ceph_argparse_flag(args, i, "-d", (char*) NULL)) {
    set_val_or_die("daemonize", "false");
    set_val_or_die("log_file", "");
    set_val_or_die("log_to_stderr", "true");
    set_val_or_die("err_to_stderr", "true");
    set_val_or_die("log_to_syslog", "false");
  }
  // Some stuff that we wanted to give universal single-character options for
  // Careful: you can burn through the alphabet pretty quickly by adding
  // to this list.
  else if (ceph_argparse_witharg(args, i, &val, "--monmap", "-M",
      (char*) NULL)) {
    set_val_or_die("monmap", val.c_str());
  }
  else if (ceph_argparse_witharg(args, i, &val, "--mon_host", "-m",
      (char*) NULL)) {
    set_val_or_die("mon_host", val.c_str());
  }
  else if (ceph_argparse_witharg(args, i, &val, "--bind", (char*) NULL)) {
    set_val_or_die("public_addr", val.c_str());
  }
  else if (ceph_argparse_witharg(args, i, &val, "--keyfile", "-K",
      (char*) NULL)) {
    set_val_or_die("keyfile", val.c_str());
  }
  else if (ceph_argparse_witharg(args, i, &val, "--keyring", "-k",
      (char*) NULL)) {
    set_val_or_die("keyring", val.c_str());
  }
  else if (ceph_argparse_witharg(args, i, &val, "--client_mountpoint", "-r",
      (char*) NULL)) {
    set_val_or_die("client_mountpoint", val.c_str());
  }
  else {
    int r = parse_option(args, i, NULL);
    if (r < 0) {
      return r;
    }
  }
}

根据前面对 ceph_argparse_xxx 的分析,这里的逻辑显而易见,主要关注 parse_option 的处理:

int md_config_t::parse_option(std::vector<const char*>& args,
    std::vector<const char*>::iterator& i, ostream *oss) {
  int ret = 0;
  size_t o = 0;
  std::string val;

  // 遍历日志子系统
  // subsystems?
  for (o = 0; o < subsys.get_num(); o++) {
    // 构建待匹配的日志级别选项 --debug_xxx,注意默认的日志子系统名称是 none
    std::string as_option("--");
    as_option += "debug_";
    as_option += subsys.get_name(o);

    // 将命令行与对应的日志打印级别选项 --debug_xxx 进行匹配
    ostringstream err;
    if (ceph_argparse_witharg(args, i, &val, err, as_option.c_str(), (char*) NULL)) { // 命令行是一个 --debug_xxx 选项
      if (err.tellp()) {
        if (oss) {
          *oss << err.str();
        }
        ret = -EINVAL;
        break;
      }
      int log, gather;
      int r = sscanf(val.c_str(), "%d/%d", &log, &gather); // --debug_xxx 是 option 选项类型,选项值 arg 保存在出参 val 中
      if (r >= 1) {
        if (r < 2)
          gather = log;
        //    cout << "subsys " << subsys.get_name(o) << " log " << log << " gather " << gather << std::endl;
        subsys.set_log_level(o, log);
        subsys.set_gather_level(o, gather);
        if (oss)
          *oss << "debug_" << subsys.get_name(o) << "=" << log << "/" << gather
              << " ";
      }
      break;
    }
  }
  if (o < subsys.get_num()) {
    return ret;
  }

  // 命令行不是一个 --debug_xxx 选项,尝试匹配 ceph 配置参数选项
  std::string option_name;
  std::string error_message;
  o = 0;
  // 遍历 ceph 配置参数选项,即 common/options.cc 中定义的所有配置参数
  for (const auto& opt_iter : schema) {
    const Option &opt = opt_iter.second;
    ostringstream err;

    // 构建配置参数选项 --xxx
    std::string as_option("--");
    as_option += opt.name;
    option_name = opt.name;
    if (opt.type == Option::TYPE_BOOL) {
      int res;
      if (ceph_argparse_binary_flag(args, i, &res, oss, as_option.c_str(),
          (char*) NULL)) { // 这是一个 flag 选项,注意前面分析 ceph_argparse_binary_flag 实现时提到的 flag 选项的用法,切记 --flag true 这种是错误的方式,必须是 --flag=true 或者 --flag
        if (res == 0)
          ret = set_val_impl("false", opt, &error_message);
        else if (res == 1)
          ret = set_val_impl("true", opt, &error_message);
        else
          ret = res;
        break;
      } else {
        std::string no("--no-");
        no += opt.name;
        if (ceph_argparse_flag(args, i, no.c_str(), (char*) NULL)) { // 这是一个  --no-flag 选项
          ret = set_val_impl("false", opt, &error_message);
          break;
        }
      }
    } else if (ceph_argparse_witharg(args, i, &val, err, as_option.c_str(),
        (char*) NULL)) { // 这是一个 option 选项
      if (!err.str().empty()) {
        error_message = err.str();
        ret = -EINVAL;
        break;
      }
      if (oss && ((!opt.is_safe()) && (observers.find(opt.name) == observers.end()))) {
        *oss << "You cannot change " << opt.name << " using injectargs.\n";
        return -ENOSYS;
      }


      // option 选项的值记录在出参 val 中
      ret = set_val_impl(val, opt, &error_message);
      break;
    }
    ++o;
  }

  ...

  if (o == schema.size()) { // 没有匹配到任何配置参数
    // ignore
    ++i;
  }
  return ret;
}

parse_option 首先尝试遍历日志子系统选项,如果都匹配不上,则继续遍历并尝试匹配所有的 ceph 配置参数,如果还是没有匹配上,则说明这个命令行并不是一个 ceph 所认识的选项,直接跳过对它的处理(注意前面提到的,匹配上的命令行都会从 args 容器中删除)。


最后修改于 2019-02-23