udhcp源码剖析(三)——DHCP服务器的初始化和配置

udhcpd_main的初始化和配置

udhcpd_main是DHCP服务器的入口,也是其主循环,在该函数中可以了解到DHCP服务的基本所有功能和流程。

开启LOG,初始化server_config

打开名为udhcpd的log文件,打印log信息,并将server_config初始化为0

if (!(opt & 1)) { /* no -f */
    bb_daemonize_or_rexec(0, argv);
    logmode &= ~LOGMODE_STDIO;
}

if (opt & 2) { /* -S */
    openlog(applet_name, LOG_PID, LOG_LOCAL0);
    logmode |= LOGMODE_SYSLOG;
}

读取配置文件信息read_config

DHCP服务器开启后,第一步就是从配置文件读取配置信息,写入server_config。如果未在函数入口指定配置文件路径,则读取默认路径
read_config函数定义在file.c中,用于读取配置文件,以后遇到类似需要读取配置信息的函数可以参照read_config的写法。理解read_config函数需要先看一个file.h中定义的一个辅助数据结构:

struct config_keyword {
    char keyword[14];
    int (*handler)(char *line, void *var);
    void *var;
    char def[30];
};

该结构的数组在file.c中初始化如下,可以看出config_keyword结构的四个成员中,keyword对应着server_config的变量名称,该成员名字所指示的就是server_config_t里的哪个成员;handler是一个函数指针,指向的函数用于读取该项配置信息,并把配置信息写到指定位置;第三项variable address设置的是把参数存储到的地址,第四项default是参数的默认值。

static const struct config_keyword keywords[] = {
    /* keyword       handler   variable address               default */
    {"start",        read_ip,  &(server_config.start_ip),     "192.168.0.20"},
    {"end",          read_ip,  &(server_config.end_ip),       "192.168.0.254"},
    {"interface",    read_str, &(server_config.interface),    "eth0"},
    {"option",       read_opt, &(server_config.options),      ""},
    {"opt",          read_opt, &(server_config.options),      ""},
    /* Avoid "max_leases value not sane" warning by setting default
     * to default_end_ip - default_start_ip + 1: */
    {"max_leases",   read_u32, &(server_config.max_leases),   "235"},
    {"remaining",    read_yn,  &(server_config.remaining),    "yes"},
    {"auto_time",    read_u32, &(server_config.auto_time),    "7200"},
    {"decline_time", read_u32, &(server_config.decline_time), "3600"},
    {"conflict_time",read_u32, &(server_config.conflict_time),"3600"},
    {"offer_time",   read_u32, &(server_config.offer_time),   "60"},
    {"min_lease",    read_u32, &(server_config.min_lease),    "60"},
    {"lease_file",   read_str, &(server_config.lease_file),   LEASES_FILE},
    {"pidfile",      read_str, &(server_config.pidfile),      "/var/run/udhcpd.pid"},
    {"notify_file",  read_str, &(server_config.notify_file),  ""},
    {"siaddr",       read_ip,  &(server_config.siaddr),       "0.0.0.0"},
    {"sname",        read_str, &(server_config.sname),        ""},
    {"boot_file",    read_str, &(server_config.boot_file),    ""},
    {"static_lease", read_staticlease, &(server_config.static_leases), ""},
    /* ADDME: static lease */
};

Handler中存储的函数指针指向的函数,都在file.c中定义,函数定义的类型如下,约定操作成功返回1,失败返回0

 int (const *handler)(const char *line, void *arg)

具体定义的handler函数及其解释如下,这些函数的具体实现在以后有机会再补充,对于学习网络通信中的字节序、ip、字符串和整形数的转换是很好的材料。

  1. read_ip: 把字符串格式的IP地址转换成uint32_t的IP格式,并存储到arg所指的内存。
  2. read_u32: 把字符串格式的数转换成uint32_t类型数字,并存储到arg所指的内存。
  3. read_yn: line字符串是“yes” 还是 “no”?若“yes”,将arg所指的内存赋1,反之,赋0
  4. read_str:首先free(*arg)释放掉*arg指的内存,再根据字符串line的大小分配内存,并把line里的值复制到里面,让*arg指向新分配的内存。
  5. read_opt: 从字符串line里读取options写到*arg所指的struct option_set链表里。(链表的每个结点data以CLV方式组织数据)。
    read_opt对选项的信息的读取借助里也借助结构数组(在options.c里定义的),所以函数比较复杂,这里只用知道函数作用。
  6. read_staticlease:从line字符串里读取MAC、IP地址字符串里MAC与IP用’/t’or’=’作为分隔)再把MAC和IP添加到*arg所指的static_lease链表里。static_lease链表是dhcp Server管理已租赁出去的IP和获得该IP的客户机的MAC绑定。
  7. read_mac: 从字符串格式MAC地址里读出MAC并转化的MAC存储arg所指的内存里
    Read_config函数首先读取配置文件,然后结合辅助结构config_keyword,将读取的配置信息载入到server_config中。Read_config函数具体实现如下:
int read_config(char *file)
{
    FILE *in;
    char buffer[READ_CONFIG_BUF_SIZE], *token, *line;
    int i, lm = 0;

    /*载入默认值*/
//依次对keywords的每一项操作,如果default参数存在,则将default参数写入参数var, 
    //因为var保存的是server_config元素的指针,则参数被写如server_config对应的项
    for (i = 0; i < ARRAY_SIZE(keywords); i++)
        if (keywords[i].def[0])
            keywords[i].handler(keywords[i].def, keywords[i].var);

    /* 以只读模式打开配置文件 */           
    in = fopen_or_warn(file, "r");
    if (!in) {
        return 0;
    }
    /* 依次读取文件各行,,长度为80字节 */
    while (fgets(buffer, READ_CONFIG_BUF_SIZE, in)) {
        char debug_orig[READ_CONFIG_BUF_SIZE];
        char *p;

        lm++;

        /* 如有换行符’\n’,将换行符替换为结束符’\0’ */
        p = strchr(buffer, '\n');
        if (p) *p = '\0';
        strncpy(orig, buffer, 80);
        /* 如有’#’,将’#’替换为结束符’\0’,因为读到的’#’之后的内容是注释 */
        if (ENABLE_FEATURE_UDHCP_DEBUG) strcpy(debug_orig, buffer);
        p = strchr(buffer, '#');
        if (p) *p = '\0';

        /* 根据制表符’\t’定位到token,即配置信息头,就像config_keyword里的keyword */
        if (!(token = strtok(buffer, " \t"))) continue;
        /* 如果token为空,即该行没有信息,为注释,则continue跳过,读下一行 */
        if (!(line = strtok(NULL, ""))) continue;

        /* eat leading whitespace 开始若有空格、制表或等号,则跳过*/
        line = skip_whitespace(line);
        /* eat trailing whitespace 尾部若有空格,则从后向前依次设为’\0’截断*/
        i = strlen(line) - 1;
        while (i >= 0 && isspace(line[i]))
            line[i--] = '\0';


        /* 此时token和line指针指向的字符串,前后都没有无用的空格,且都以’\0’结尾, 可以直接用于字符串比较 */        

        /* 依次在keywords[]中找到和token匹配的keyword,不区分大小写*/
        for (i = 0; i < ARRAY_SIZE(keywords); i++)
            if (!strcasecmp(token, keywords[i].keyword))
                if (!keywords[i].handler(line, keywords[i].var)) {
                    bb_error_msg("cannot parse line %d of %s", lm, file);
                    if (ENABLE_FEATURE_UDHCP_DEBUG)
                        bb_error_msg("cannot parse '%s'", debug_orig);
                    /* reset back to the default value */
                    keywords[i].handler(keywords[i].def, keywords[i].var);
                }
    }
    fclose(in);
    server_config.start_ip = ntohl(server_config.start_ip);
    server_config.end_ip = ntohl(server_config.end_ip);
    return 1;
}

在从字符串里截取数据的过程中,使用到了两个关键的函数strspn和strcpsn,二者的含义刚好相反

  • Strspn:定义函数 size_t strspn (const char s,const char accept);函数说明 strspn()从参数s 字符串的开头计算连续的字符,而这些字符都完全是accept 所指字符串中的字符。简单的说,若strspn()返回的数值为n,则代表字符串s 开头连续有n 个字符都是属于字符串accept内的字符。返回值:返回字符串s开头连续包含字符串accept内的字符数目。可用于跳过开始的一些不需要的字符,只需将accept设为不需要字符的字符串。
  • Strcspn:定义函数:size_t strcspn(const char s, const char reject);函数说明:strcspn()从参数s 字符串的开头计算连续的字符, 而这些字符都完全不在参数reject 所指的字符串中. 简单地说, 若strcspn()返回的数值为n, 则代表字符串s 开头连续有n 个字符都不含字符串reject 内的字符.返回值:返回字符串s 开头连续不含字符串reject 内的字符数目。可用于定位到所需要的字节,只需将accept设为所需要定位字节的字符串。

将pid写入pid_file

获取当前进程的pid,并将pid写入pidfile文件。

/* 打开指定的pidfile,并返回pid_fd,获取当前进程pid,并写入pidfile*/
write_pidfile(server_config.pidfile);

将这种简单的功能封装进函数的意义在于,让代码主体显得简洁,可读性好,同时,封装的函数在该模块其他的地方也可以用到,可重用性好。所以这种功能单一的小代码,特别是包含防呆和边界校验的,最好封装成函数使用。

根据server_config的option设置lease

设置server_config.lease,若server_config有DHCP_LEASE_TIME对应的项,则将该项的data赋给server_config.lease,将网络字节序转为主机字节序保存;若没有,则server_config.lease为默认的LEASE_TIME。

option = find_option(server_config.options, DHCP_LEASE_TIME);
server_config.lease = LEASE_TIME;
if (option) {
    memcpy(&server_config.lease, option->data + 2, 4);
    server_config.lease = ntohl(server_config.lease);
}

初始化和配置leases

为全局变量leases分配内存空间,空间大小为sizeof(struct dhcpOfferedAddr) * server_config.max_leases(dhcpOfferedAddr结构长度*最大租约数),并初始化为0,随后从lease_file中读取lease数据填充到leases列表中。

/* Sanity check */
num_ips = server_config.end_ip - server_config.start_ip + 1;
if (server_config.max_leases > num_ips) {
    bb_error_msg("max_leases=%u is too big, setting to %u",
        (unsigned)server_config.max_leases, num_ips);
    server_config.max_leases = num_ips;
}

leases = xzalloc(server_config.max_leases * sizeof(*leases));
read_leases(server_config.lease_file);

读取接口信息

根据传入的interface信息,获取ifindex、server和arp信息。

if (read_interface(server_config.interface, &server_config.ifindex,
      &server_config.server, server_config.arp)) {
    retval = 1;
    goto ret;
}

创建内部socket通信,注册信号处理器

Socketpair会建立一对未命名的、相互连接的UNIX域套接字,通过在二维数组signal_pipe的一个成员中写入,另一个成员中读出,实现双向的通信。随后在注册信号处理器为signal_handler。

/* Setup the signal pipe */
    udhcp_sp_setup();

void FAST_FUNC udhcp_sp_setup(void)
{
    /* was socketpair, but it needs AF_UNIX in kernel */
    xpiped_pair(signal_pipe);
    close_on_exec_on(signal_pipe.rd);
    close_on_exec_on(signal_pipe.wr);
    ndelay_on(signal_pipe.wr);
    bb_signals(0
        + (1 << SIGUSR1)
        + (1 << SIGUSR2)
        + (1 << SIGTERM)
        , signal_handler);
}

进入超级循环

之前的步骤都是完成的DHCP启动的初始化和配置,但是尚不能提供任何DHCP服务,接下来进入super loop,在这里DHCP服务器组合各个模块,提供DHCP服务。
loop until universe collapses,一直循环直到系统的终结。。。
PS.在进入super loop之前,还需要更新time_end,

timeout_end = monotonic_sec() + server_config.auto_time;

auto_time是程序在写config file之前等待的时间。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章