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、字符串和整形數的轉換是很好的材料。
- read_ip: 把字符串格式的IP地址轉換成uint32_t的IP格式,並存儲到arg所指的內存。
- read_u32: 把字符串格式的數轉換成uint32_t類型數字,並存儲到arg所指的內存。
- read_yn: line字符串是“yes” 還是 “no”?若“yes”,將arg所指的內存賦1,反之,賦0
- read_str:首先free(*arg)釋放掉*arg指的內存,再根據字符串line的大小分配內存,並把line裏的值複製到裏面,讓*arg指向新分配的內存。
- read_opt: 從字符串line裏讀取options寫到*arg所指的struct option_set鏈表裏。(鏈表的每個結點data以CLV方式組織數據)。
read_opt對選項的信息的讀取藉助裏也藉助結構數組(在options.c裏定義的),所以函數比較複雜,這裏只用知道函數作用。 - read_staticlease:從line字符串裏讀取MAC、IP地址字符串裏MAC與IP用’/t’or’=’作爲分隔)再把MAC和IP添加到*arg所指的static_lease鏈表裏。static_lease鏈表是dhcp Server管理已租賃出去的IP和獲得該IP的客戶機的MAC綁定。
- 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之前等待的時間。