获取时间CLOCK_MONOTONIC学习——顺记第一次与开源项目交互

前段时间接触horst项目,该项目是Linux下WIFI分析软件,可在界面上显示(使用ncurses库)。花了一些时间——断断续续也有几周吧,基本上大致掌握了它的流程和原理。后续再抽时间写一写其源码过程。

一、CLOCK_MONOTONIC

程序获致时间可以通过clock_gettime函数,该函数可以获取不同类型的时间,如CLOCK_REALTIME、CLOCK_MONOTONIC,其中CLOCK_REALTIME获取的是实时时间,而CLOCK_MONOTONIC获取的是相对时间。使用CLOCK_MONOTONIC得到的时间值是通过jiffies值来计算的,是不随系统时间变化而变化的,特别适合于时间连续性的应用(比如超时的应用)。如果使用CLOCK_REALTIME的话,当手动或NTP修改系统时间时,获取的值就会变化,无法保持“时间”的连续性。

二、horst分析

horst使用“节点”来保存WIFI数据包信息,比如SSID、帧类型、信号强度、MAC地址,等,每一帧的信息使用链表nodes来连接,这样在显示时就可遍历整个链表拿到节点信息了。
horst有一个超时功能,参数说明如下:

1
-t <sec>      Node timeout in seconds (60)

默认为60秒,即每隔一分钟会更新更新所有的节点,太旧的信息会被删除,以保证当前节点是最新的,其机制就是用CLOCK_MONOTONIC获取时间做超时判断依据。但在实际中发现并不如此,研究代码后,发现有一处地方有笔误而导致。于是向horst提了bug,不久这个bug就修改了,这是第一次使用github的issue功能,同时也算是真正意思上的第一次向开源项目做贡献吧。

下面看看更新节点的过程。首先看main函数处理过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char** argv)
{
// ....
clock_gettime(CLOCK_MONOTONIC, &the_time); // 获取时间表
// ....

// 循环
while (!conf.do_change_channel || conf.channel_scan_rounds != 0)
{
// // 接收帧并处理并在界面上显示
receive_any(&waitmask); // 注:此函数中在处理帧时会调用node_update加入nodes
clock_gettime(CLOCK_MONOTONIC, &the_time); // 更新时间

node_timeout(); // 检测节点是否超时,如果超时,则删除链表中对应的节点
// ...
}
}

receive_any是horst最复杂的函数,本文不展开。在这个函数中会将符合要求的帧进入到全局链表中,然后更新时间戳。最后检测节点是否超时,如超时则删除节点。

下面是node_timeout代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void node_timeout(void)
{
struct node_info *n, *m, *n2, *m2;
struct chan_node *cn, *cn2;

// 如果时间还不到
if ((the_time.tv_sec - last_nodetimeout.tv_sec) < conf.node_timeout )
return;

// 超时,则遍历节点链表,删除已经超时的节点
list_for_each_safe(&nodes, n, m, list) {
// 注:根据节点last_seen时间戳与超时时间对比
if (n->last_seen < (the_time.tv_sec - conf.node_timeout)) {
list_del(&n->list);
if (n->essid != NULL)
remove_node_from_essid(n);
list_for_each_safe(&n->on_channels, cn, cn2, node_list) {
list_del(&cn->node_list);
list_del(&cn->chan_list);
cn->chan->num_nodes--;
free(cn);
}
/* remove AP pointers to this node */
list_for_each_safe(&nodes, n2, m2, list) {
if (n2->wlan_ap_node == n) {
DEBUG("remove AP refn");
n->wlan_ap_node = NULL;
}
}
free(n);
}
}
last_nodetimeout = the_time;
}

明显地,使用节点的last_seen字段做判断依据。这个字段在copy_nodeinfo函数中更新,代码如下:

1
2
3
4
5
6
7
8
9
static void copy_nodeinfo(struct node_info* n, struct packet_info* p)
{
struct node_info* ap;

memcpy(&(n->last_pkt), p, sizeof(struct packet_info));
// update timestamp
n->last_seen = time(NULL); // 这里使用time来获取上一次“存在”时间
n->pkt_count++;
}

问题出现在这个函数中,last_seen得到的是实时时间,值十分大,不会匹配node_timeout的条件,故不会出现节点超时情况。这个last_seen实际应该是the_time,因为在主函数中更新the_time,而超时依据也是the_time,这样才能把时间刻度统一起来。如下:

1
2
3
4
5
6
7
8
9
static void copy_nodeinfo(struct node_info* n, struct packet_info* p)
{
struct node_info* ap;

memcpy(&(n->last_pkt), p, sizeof(struct packet_info));
// update timestamp
n->last_seen = the_time.tv_sec; // 使用the_time,在主函数中更新
n->pkt_count++;
}

修改后,程序能如愿在超时时间后更新旧节点,显示新节点。 另外,保存文件也是使用the_time,这就有问题了,因为the_time值较小,得到的时间是1970年的。修正很简单,在此函数使用实时时间即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void write_to_file(struct packet_info* p)
{
char buf[40];
int i;
// bug 这里有问题,不应该拿the_time做时间转换的
struct tm* ltm = localtime(&the_time.tv_sec);

//timestamp, e.g. 2015-05-16 15:05:44.338806 +0300
i = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ltm);
i += snprintf(buf + i, sizeof(buf) - i, ".%06ld", (long)(the_time.tv_nsec / 1000));
i += strftime(buf + i, sizeof(buf) - i, " %z", ltm);
fprintf(DF, "%s, ", buf);
}

2016.10.14 周五 午休前 李迟