上一章说到了Linux内核网络子系统中beacon帧是如何产生与发送的。下面我们来看一看beacon帧是如何接收并提取信息的。

Linux内核是通过中断来对接收到的数据进行响应的。当硬件检测到有接收数据的时候,产生一个中断,中断触发下半部的tasklet机制,在802.11协议栈这里会调用ieee80211_tasklet_handler()函数。我们来看一看函数体:

static void ieee80211_tasklet_handler(unsigned long data)
{
	struct ieee80211_local *local = (struct ieee80211_local *) data;
	struct sk_buff *skb;


	while ((skb = skb_dequeue(&local->skb_queue)) ||
	       (skb = skb_dequeue(&local->skb_queue_unreliable))) {
		switch (skb->pkt_type) {
		case IEEE80211_RX_MSG:
			/* Clear skb->pkt_type in order to not confuse kernel
			 * netstack. */
			skb->pkt_type = 0;
			ieee80211_rx(&local->hw, skb);
			break;
		case IEEE80211_TX_STATUS_MSG:
			...
		default:
			...
		}
	}
}
系统收到数据时会开辟一个sk_buff缓存空间进行数据的存储,ieee80211_tasklet_handler()触发后对sk_buff中存储的数据帧进行判断,如果是接收来的数据(MPDU),则进入ieee80211_rx()函数:
void ieee80211_rx(struct ieee80211_hw *hw, struct sk_buff *skb)
{
	struct ieee80211_local *local = hw_to_local(hw);
	struct ieee80211_rate *rate = NULL;
	struct ieee80211_supported_band *sband;
	struct ieee80211_rx_status *status = IEEE80211_SKB_RXCB(skb);


	...


	__ieee80211_rx_handle_packet(hw, skb);


	rcu_read_unlock();


	return;
 drop:
	kfree_skb(skb);
}
EXPORT_SYMBOL(ieee80211_rx);
下面我们进入ieee80211_scan_rx()函数看看帧信息是如何被扫描和提取出来的(具体细节请看注释):
void ieee80211_scan_rx(struct ieee80211_local *local, struct sk_buff *skb)
{
	struct ieee80211_rx_status *rx_status = IEEE80211_SKB_RXCB(skb);
	struct ieee80211_sub_if_data *sdata1, *sdata2;
	struct ieee80211_mgmt *mgmt = (void *)skb->data;			/*接收到的beacon帧的具体信息存储在sk_buff的data成员变量里,data是一个char型指针,指向具体的数据区域,这里用ieee80211_mgmt结构体将data中的前面一部分信息提取出来,分别放入ieee80211_mgmt的各个成员分量中,包括MAC头、固有字段以及可选字段的起始地址*/
	struct ieee80211_bss *bss;
	u8 *elements;
	struct ieee80211_channel *channel;
	size_t baselen;
	struct ieee802_11_elems elems;

	if (skb->len < 24 ||
	    (!ieee80211_is_probe_resp(mgmt->frame_control) &&
	     !ieee80211_is_beacon(mgmt->frame_control)))
		return;

	sdata1 = rcu_dereference(local->scan_sdata);
	sdata2 = rcu_dereference(local->sched_scan_sdata);

	if (likely(!sdata1 && !sdata2))
		return;

	if (ieee80211_is_probe_resp(mgmt->frame_control)) {
		/* ignore ProbeResp to foreign address */
		if ((!sdata1 || !ether_addr_equal(mgmt->da, sdata1->vif.addr)) &&
		    (!sdata2 || !ether_addr_equal(mgmt->da, sdata2->vif.addr)))
			return;

		elements = mgmt->u.probe_resp.variable;					
		baselen = offsetof(struct ieee80211_mgmt, u.probe_resp.variable);
	} else {
		baselen = offsetof(struct ieee80211_mgmt, u.beacon.variable);		/*beacon帧分为MAC头部、固有字段(也称强制字段)和可选字段,这里将可选字段的起始地址取出来赋给elements指针,用于可选字段信息的提取*/
		elements = mgmt->u.beacon.variable;
	}
	if (baselen > skb->len)
		return;
	ieee802_11_parse_elems(elements, skb->len - baselen, false, &elems);		/*进入可选字段提取信息,由于我们是要在beacon帧中添加信息,而添加信息只能在可选字段进行,因此需要重点关注这个函数*/


	...
}

ieee80211_rx()函数再调用__ieee80211_rx_handle_packet(),__ieee80211_rx_handle_packet()是接收帧的处理函数,会对帧类型进行判断,如果检测出该帧是beacon帧(或sta主动扫描后从AP端返回的响应帧),则进入ieee80211_scan_rx()函数对帧信息进行扫描。

static void __ieee80211_rx_handle_packet(struct ieee80211_hw *hw,
					 struct sk_buff *skb)
{
	struct ieee80211_local *local = hw_to_local(hw);
	struct ieee80211_sub_if_data *sdata;
	struct ieee80211_hdr *hdr;
	__le16 fc;
	struct ieee80211_rx_data rx;
	struct ieee80211_sub_if_data *prev;
	struct sta_info *sta, *tmp, *prev_sta;
	int err = 0;

	...

	hdr = (struct ieee80211_hdr *)skb->data;
	ieee80211_parse_qos(&rx);
	ieee80211_verify_alignment(&rx);

	if (unlikely(ieee80211_is_probe_resp(hdr->frame_control) ||
		     ieee80211_is_beacon(hdr->frame_control)))
	{	    

		ieee80211_scan_rx(local, skb);		/*扫描帧信息*/

		}

	if (ieee80211_is_data(fc)) {
		
		...

	}

	...
 out:
	dev_kfree_skb(skb);
}

接收到的beacon帧的具体信息存储在sk_buff的data成员变量里,data是一个char型指针,指向具体的数据区域。函数首先用ieee80211_mgmt结构体将data中的前面一部分信息提取出来,分别放入ieee80211_mgmt的各个成员分量中,包括MAC头、固有字段以及可选字段的起始地址。进而将可选字段的起始地址取出来赋给elements指针,用于可选字段信息的提取。然后进入可选字段提取信息,由于我们是要在beacon帧中添加信息,而添加信息只能在可选字段进行,因此需要重点关注这个函数。下面就来看看函数ieee802_11_parse_elems():

static inline void ieee802_11_parse_elems(const u8 *start, size_t len,
					  bool action,
					  struct ieee802_11_elems *elems)
{
	ieee802_11_parse_elems_crc(start, len, action, elems, 0, 0);
}

ieee802_11_parse_elems_crc()函数直接被调用:

u32 ieee802_11_parse_elems_crc(const u8 *start, size_t len, bool action,
			       struct ieee802_11_elems *elems,
			       u64 filter, u32 crc)
{
	size_t left = len;
	const u8 *pos = start;
	bool calc_crc = filter != 0;
	DECLARE_BITMAP(seen_elems, 256);
	const u8 *ie;

	bitmap_zero(seen_elems, 256);
	memset(elems, 0, sizeof(*elems));
	elems->ie_start = start;
	elems->total_len = len;


	while (left >= 2) {
		u8 id, elen;
		bool elem_parse_failed;

		id = *pos++;
		elen = *pos++;
		left -= 2;


		if (elen > left) {
			elems->parse_error = true;
			break;
		}


		switch (id) {
		case WLAN_EID_SSID:
		case WLAN_EID_SUPP_RATES:
		case WLAN_EID_FH_PARAMS:
		case WLAN_EID_DS_PARAMS:
		case WLAN_EID_CF_PARAMS:
		case WLAN_EID_TIM:
		...
		case WLAN_EID_SECONDARY_CHANNEL_OFFSET:
		case WLAN_EID_WIDE_BW_CHANNEL_SWITCH:
	
		/*
		 * not listing WLAN_EID_CHANNEL_SWITCH_WRAPPER -- it seems possible
		 * that if the content gets bigger it might be needed more than once
		 */
			if (test_bit(id, seen_elems)) {
				elems->parse_error = true;
				left -= elen;
				pos += elen;
				
				continue;
			}
			break;
		}

		if (calc_crc && id < 64 && (filter & (1ULL << id)))
			crc = crc32_be(crc, pos - 2, elen + 2);

		elem_parse_failed = false;

		switch (id) {
		case WLAN_EID_SSID:
			elems->ssid = pos;
			elems->ssid_len = elen;
			break;
		
		...
		
		case WLAN_EID_TIMEOUT_INTERVAL:
			if (elen >= sizeof(struct ieee80211_timeout_interval_ie))
				elems->timeout_int = (void *)pos;
			else
				elem_parse_failed = true;
			break;

		default:
			break;
		}

		if (elem_parse_failed)
			elems->parse_error = true;
		else
			__set_bit(id, seen_elems);

		left -= elen;
		pos += elen;
	}


	if (left != 0)
		elems->parse_error = true;

	return crc;
}
        从函数中可以看出,系统获取了指向可选字段区域的指针以后,通过指针偏移的方法来读取元素标识符(Element ID)和字段长度(Length),然后读取字段内容,获取了一个字段的信息以后再将指针移到下一个字段的开头处(Element ID处)进行信息的读取。
        这就是beacon帧接收和帧信息提取的整个过程。

        写到这里,我们大致可以总结出两种信息读取的方法:

1)第一个方法是使用结构体从数据区域直接取数据,该方法可以快速的提取出位于数据区域前列的MAC头和强制字段,并将其分别放入结构体内对应的数据成员中。该方法提取数据较为迅速方便,但欠缺灵活性,因此用在固定字段的读取上(数据结构可以固定)。

2)可选字段的字段种类和长度都是不固定的,不能用结构体从数据区域直接读取,因此需要用到第二种方法:指针偏移。通过指针偏移将指针指向的数据区域的信息读取出来。虽然较为繁琐,但灵活性较高,适用于可选字段区域。


未完待续