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.c
l2cap_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.c
hci_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.chci_cc_read_buffer_size函数:
    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的CMD事件–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函数中:
    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);
    这个主要是上层通过ioctl的接口hci_sock_ioctl来设置mtu

参考

蓝牙开发那些事儿(7)——l2cap层连接过程