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之前等待的時間。

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