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之前等待的时间。