Linux蓝牙文件传输速率低问题分析及延伸
Linux蓝牙文件传输速率低问题分析及延伸
[TOC]
背景
最近调试了一个BUG:在两台装Kylin的PC之间,连接蓝牙,然后相互传输文件,速率只有20几Kb/s。正常情况有100多Kb/s。
使用了obex协议传输文件
分析
分析系统日志,未有明显的错误或警告日志
开始抓包分析
发送方的抓包:
从上图可以看出,在发送obex包的时候,中间夹杂着很多sco的语音包
接收方的抓包:
从上图可以看出,在接收obex包的时候,中间也夹杂着很多sco的语音包,而且obex包被拆包了,拆成了好几个包
当2台电脑连接成功后,应用上层会默认切换音频通路及输入输出设备,并默认会开启SCO语音链路。经验证,手动切换输入设备,obex传输文件速度就会恢复成正常的100多Kb/s。还有一种方式,在已配对并且非连接状态下,进行蓝牙文件传输,速率也是正常的
从发送和接收的抓包来看,抓到的发送和接收obex包的时候都有分包,只不过发送分包大些,接收的分包小些。
延伸:ACL数据包及L2CAP分包重组
文件传输走的是ACL链路,ACL数据包及L2CAP分包重组
我们看看ACL的数据包格式:
其中header中有个与分包组包相关的标志位 PB Flag,它的说明如下:
代码分析
l2cap重要的功能之一就是处理拆包和组包,一起看看kernel里l2cap的acl链路分包和组包相关代码
接收组包处理:
代码路径:net/bluetooth/l2cap_core.cl2cap_recv_acldata函数中相关flag的处理核心代码:
...
switch (flags) {
case ACL_START:
case ACL_START_NO_FLUSH:
case ACL_COMPLETE:
    if (conn->rx_len) {
        BT_ERR("Unexpected start frame (len %d)", skb->len);
        kfree_skb(conn->rx_skb);
        conn->rx_skb = NULL;
        conn->rx_len = 0;
        l2cap_conn_unreliable(conn, ECOMM);
    }
    /* Start fragment always begin with Basic L2CAP header */
    if (skb->len < L2CAP_HDR_SIZE) {
        BT_ERR("Frame is too short (len %d)", skb->len);
        l2cap_conn_unreliable(conn, ECOMM);
        goto drop;
    }
    hdr = (struct l2cap_hdr *) skb->data;
    len = __le16_to_cpu(hdr->len) + L2CAP_HDR_SIZE;
    if (len == skb->len) {
        /* Complete frame received */
        l2cap_recv_frame(conn, skb);
        return;
    }
    BT_DBG("Start: total len %d, frag len %d", len, skb->len);
    if (skb->len > len) {
        BT_ERR("Frame is too long (len %d, expected len %d)",
                skb->len, len);
        l2cap_conn_unreliable(conn, ECOMM);
        goto drop;
    }
    /* Allocate skb for the complete frame (with header) */
    conn->rx_skb = bt_skb_alloc(len, GFP_KERNEL);
    if (!conn->rx_skb)
        goto drop;
    skb_copy_from_linear_data(skb, skb_put(conn->rx_skb, skb->len),
                    skb->len);
    conn->rx_len = len - skb->len;
    break;
case ACL_CONT:
    BT_DBG("Cont: frag len %d (expecting %d)", skb->len, conn->rx_len);
    if (!conn->rx_len) {
        BT_ERR("Unexpected continuation frame (len %d)", skb->len);
        l2cap_conn_unreliable(conn, ECOMM);
        goto drop;
    }
    if (skb->len > conn->rx_len) {
        BT_ERR("Fragment is too long (len %d, expected %d)",
                skb->len, conn->rx_len);
        kfree_skb(conn->rx_skb);
        conn->rx_skb = NULL;
        conn->rx_len = 0;
        l2cap_conn_unreliable(conn, ECOMM);
        goto drop;
    }
    skb_copy_from_linear_data(skb, skb_put(conn->rx_skb, skb->len),
                    skb->len);
    conn->rx_len -= skb->len;
    if (!conn->rx_len) {
        /* Complete frame received. l2cap_recv_frame
            * takes ownership of the skb so set the global
            * rx_skb pointer to NULL first.
            */
        struct sk_buff *rx_skb = conn->rx_skb;
        conn->rx_skb = NULL;
        l2cap_recv_frame(conn, rx_skb);
    }
    break;
}
...
发送拆包处理:
代码路径:net/bluetooth/hci_core.chci_queue_acl函数中,拆包相关处理的核心代码:
list = skb_shinfo(skb)->frag_list;
if (!list) {
    /* Non fragmented */
    BT_DBG("%s nonfrag skb %p len %d", hdev->name, skb, skb->len);
    skb_queue_tail(queue, skb);
} else {
    /* Fragmented */
    BT_DBG("%s frag %p len %d", hdev->name, skb, skb->len);
    skb_shinfo(skb)->frag_list = NULL;
    /* Queue all fragments atomically. We need to use spin_lock_bh
        * here because of 6LoWPAN links, as there this function is
        * called from softirq and using normal spin lock could cause
        * deadlocks.
        */
    spin_lock_bh(&queue->lock);
    __skb_queue_tail(queue, skb);
    flags &= ~ACL_START;
    flags |= ACL_CONT;
    do {
        skb = list; list = list->next;
        hci_skb_pkt_type(skb) = HCI_ACLDATA_PKT;
        hci_add_acl_hdr(skb, conn->handle, flags);
        BT_DBG("%s frag %p len %d", hdev->name, skb, skb->len);
        __skb_queue_tail(queue, skb);
    } while (list);
    spin_unlock_bh(&queue->lock);
}
基本流程是先判断frag_list有没有拆包的list,有的话就设置ACL_CONT标志,并加进发送队列。
而frag_list是怎么来的呢?主要是在l2cap_skbuff_fromiovec函数中处理:
static inline int l2cap_skbuff_fromiovec(struct l2cap_chan *chan,
					 struct msghdr *msg, int len,
					 int count, struct sk_buff *skb)
{
	struct l2cap_conn *conn = chan->conn;
	struct sk_buff **frag;
	int sent = 0;
	if (!copy_from_iter_full(skb_put(skb, count), count, &msg->msg_iter))
		return -EFAULT;
	sent += count;
	len  -= count;
	/* Continuation fragments (no L2CAP header) */
	frag = &skb_shinfo(skb)->frag_list;
	while (len) {
		struct sk_buff *tmp;
        /* 取conn->mtu和len中的最小值 */
		count = min_t(unsigned int, conn->mtu, len); 
		tmp = chan->ops->alloc_skb(chan, 0, count,
					   msg->msg_flags & MSG_DONTWAIT);
		if (IS_ERR(tmp))
			return PTR_ERR(tmp);
		*frag = tmp;
		if (!copy_from_iter_full(skb_put(*frag, count), count,
				   &msg->msg_iter))
			return -EFAULT;
		sent += count;
		len  -= count;
		skb->len += (*frag)->len;
		skb->data_len += (*frag)->len;
		frag = &(*frag)->next;
	}
	return sent;
}
所以最关键的是conn->mtu,而这个又是怎么来的呢?
针对于ACL链路(其他SCO、LE、AMP是有差异的),是在l2cap_conn_add函数中:
conn->mtu = hcon->hdev->acl_mtu;
这个acl_mtu赋值则有2处:
net/bluetooth/hci_event.c的hci_cc_read_buffer_size函数:
而这个函数是在处理HCI的CMD事件–hdev->acl_mtu = __le16_to_cpu(rp->acl_mtu); hdev->sco_mtu = rp->sco_mtu; hdev->acl_pkts = __le16_to_cpu(rp->acl_max_pkt); hdev->sco_pkts = __le16_to_cpu(rp->sco_max_pkt);HCI_OP_READ_BUFFER_SIZE中调用。
进一步,在bredr_setup时,会去向控制器请求HCI_OP_READ_BUFFER_SIZE来获取acl的mtu:/* Read Buffer Size (ACL mtu, max pkt, etc.) */ hci_req_add(req, HCI_OP_READ_BUFFER_SIZE, 0, NULL);- 在
hci_dev_cmd处理中:case HCISETACLMTU: hdev->acl_mtu = *((__u16 *) &dr.dev_opt + 1); hdev->acl_pkts = *((__u16 *) &dr.dev_opt + 0); break;hci_sock_ioctl函数中:
这个主要是上层通过ioctl的接口case HCISETSCAN: case HCISETAUTH: case HCISETENCRYPT: case HCISETPTYPE: case HCISETLINKPOL: case HCISETLINKMODE: case HCISETACLMTU: case HCISETSCOMTU: if (!capable(CAP_NET_ADMIN)) return -EPERM; return hci_dev_cmd(cmd, argp);hci_sock_ioctl来设置mtu 




 