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.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.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