背景:
最近在项目上遇到一个蓝牙的问题,项目基于嵌入式linux开发,系统上也没有集成bluetoothd服务,移植了一个Bluez的代码,开发了一个BLE的功能,与手机app端进行通信;基本功能没有什么问题,但根据反馈,连接稳定性不好,通常是有干扰,最大的问题就是,有概率在连接断开之后,手机APP再也连接不上,需要重跑嵌入式程序,手机APP才能重新建立连接;
我也没有接触过蓝牙协议栈的代码,带着问题去分析了一下源代码,也不知道理解得对不对,各位看官如果发现我理解不对的地方,还请告知在下;
分析:
从开发工程师上了解到,他使用的是开源的Bluez-5.63版本的代码,使用里面的peripheral目录下的例子修改的;我看了一下,peripheral下一共就没有几个文件:
其中, attach.c,通过名称可以大概看出应该是和蓝牙绑定相关的,里面实现了打开串口的操作,还有就是通过串口的fd进行ioctl,对蓝牙的一些标志的设置和复位。
static int open_serial(const char *path);
static int attach_proto(const char *path, unsigned int proto,
unsigned int flags);
void attach_start(void);
void attach_stop(void);
efivars.c是efi相关的变量操作,具体是干啥用的我也没搞懂,主要提供了对变量的读取和写入的两个接口;
int efivars_read(const char *name, uint32_t *attributes,
void *data, size_t size);
int efivars_write(const char *name, uint32_t attributes,
const void *data, size_t size);
log.c是类似提供了日志的功能,但是好像代码中也没有用上这个接口,所以这个好像也没什么用;
void log_open(void);
// 打开了/dev/kmsg节点,返回kmsg_fd
void log_close(void);
//关闭kmsg_fd
剩下的main.c、gap.c和gatt.c是这次分析的主要对象,大部分的功能都是在这几个文件中实现的,我这里根据main的流程去分析。
int main(int argc, char *argv[])
{
int exit_status;
if (getpid() == 1 && getppid() == 0)
is_init = true;
mainloop_init();
printf("Bluetooth periperhal ver %s\n", VERSION);
prepare_filesystem();
if (is_init) {
uint8_t addr[6];
if (efivars_read("BluetoothStaticAddress", NULL,
addr, 6) < 0) {
printf("Generating new persistent static address\n");
if (util_getrandom(addr, sizeof(addr), 0) < 0) {
perror("Failed to get random static address");
return EXIT_FAILURE;
}
/* Overwrite the MSB to make it a static address */
addr[5] = 0xc0;
efivars_write("BluetoothStaticAddress",
EFIVARS_NON_VOLATILE |
EFIVARS_BOOTSERVICE_ACCESS |
EFIVARS_RUNTIME_ACCESS,
addr, 6);
}
gap_set_static_address(addr);
run_shell();
}
log_open();
gap_start();
if (is_init)
attach_start();
exit_status = mainloop_run_with_signal(signal_callback, NULL);
if (is_init)
attach_stop();
gap_stop();
log_close();
return exit_status;
}
先看看大的流程,main里面调用了mainloop_init进行了初始化,这个mainloop_init没有在main里面定义,这里先不分析。
prepare_filesystem和efivars_read、efivars_write主要的作用就是看有没有设置BluetoothStaticAddress这个变量,如果有,就读取这个变量的内容来设置一个静态的MAC地址。设置静态MAC地址这里调用的是gap_set_static_address函数进行设置,gap_set_static_address函数是在gap.c里定义的,其实就是把addr赋值给gap.c里的全局变量而已,完全可以自己修改这个全局数组实现。
static uint8_t static_addr[6] = { 0x90, 0x78, 0x56, 0x34, 0x12, 0xc0 };
void gap_set_static_address(uint8_t addr[6])
{
memcpy(static_addr, addr, sizeof(static_addr));
printf("Using static address %02x:%02x:%02x:%02x:%02x:%02x\n",
static_addr[5], static_addr[4], static_addr[3],
static_addr[2], static_addr[1], static_addr[0]);
}
然后就是gap_start函数了,之后是mainloop_run_with_signal,看上去是进入主循环了,main函数会在这里阻塞住,直到signal的到来,然后函数返回,执行gap_stop函数。
重点看一下gap_start函数,这个函数在gap.c中定义
static struct mgmt *mgmt = NULL;
void gap_start(void)
{
mgmt = mgmt_new_default();
if (!mgmt) {
fprintf(stderr, "Failed to open management socket\n");
return;
}
if (!mgmt_send(mgmt, MGMT_OP_READ_COMMANDS,
MGMT_INDEX_NONE, 0, NULL,
read_commands_complete, NULL, NULL)) {
fprintf(stderr, "Failed to read supported commands\n");
return;
}
}
这个函数中,使用mgmt_new_default创建了mgmt,这个mgmt是个全局变量指针,文件中有大量的mgmt_send函数会使用这个指针作为句柄进行消息的发送,看上去像是一个类似socket的东西;
mgmt_new_default函数在src/shared/mgmt.c中定义
struct mgmt *mgmt_new_default(void)
{
struct mgmt *mgmt;
union {
struct sockaddr common;
struct sockaddr_hci hci;
} addr;
int fd;
fd = socket(PF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK,
BTPROTO_HCI);
if (fd < 0)
return NULL;
memset(&addr, 0, sizeof(addr));
addr.hci.hci_family = AF_BLUETOOTH;
addr.hci.hci_dev = HCI_DEV_NONE;
addr.hci.hci_channel = HCI_CHANNEL_CONTROL;
if (bind(fd, &addr.common, sizeof(addr.hci)) < 0) {
close(fd);
return NULL;
}
mgmt = mgmt_new(fd);
if (!mgmt) {
close(fd);
return NULL;
}
mgmt->close_on_unref = true;
return mgmt;
}
从函数中可以看出,mgmt确实和socket有关,它绑定了一个socket的fd句柄,这个socket是使用PF_BLUETOOTH创建的,绑定的地址中,使用了HCI_CHANNEL_CONTROL作为hci的channel,从这些信息中分析,这个mgmt应该就是可蓝牙管理控制相关的句柄。
struct mgmt {
int ref_count;
int fd;
bool close_on_unref;
struct io *io;
bool writer_active;
struct queue *request_queue;
struct queue *reply_queue;
struct queue *pending_list;
struct queue *notify_list;
unsigned int next_request_id;
unsigned int next_notify_id;
bool need_notify_cleanup;
bool in_notify;
void *buf;
uint16_t len;
uint16_t mtu;
mgmt_debug_func_t debug_callback;
mgmt_destroy_func_t debug_destroy;
void *debug_data;
};
从mgmt的结构体定义中看到,除了这个fd之外,结构体中一个io的数据,还有几个队列的数据,还有mtu等等的参数。其中这个io是比较关键的数据,可以进一步分析一下这个数据:
mgmt->io = io_new(fd);
if (!mgmt->io) {
free(mgmt->buf);
free(mgmt);
return NULL;
}
mgmt初始化的过程中,把socket得到的fd进行了一次io_new操作,映射到这个mgmt->io上了,这个io_new函数在bluez中有好几个定义的地方,追查代码发现,应该是对应src/shared/io-mainloop.c中定义的:
struct io *io_new(int fd)
{
struct io *io;
if (fd < 0)
return NULL;
io = new0(struct io, 1);
io->fd = fd;
io->events = 0;
io->close_on_destroy = false;
if (mainloop_add_fd(io->fd, io->events, io_callback,
io, io_cleanup) < 0) {
free(io);
return NULL;
}
return io_ref(io);
}
这里不难看出,它把这个fd通过mainloop_add_fd的函数添加到mainloop里面去了,这个函数有点像epoll的多并发IO操作,把某个fd添加到epoll里,并设置event事件,有事件到来,就调用io_callback函数;到这里也只是我的猜想,要证实那就继续追踪mainloop_add_fd函数,看看实现;
mainloop_add_fd函数的定义是在src/shared/mainloop.c中定义的。
int mainloop_add_fd(int fd, uint32_t events, mainloop_event_func callback,
void *user_data, mainloop_destroy_func destroy)
{
struct mainloop_data *data;
struct epoll_event ev;
int err;
if (fd < 0 || fd > MAX_MAINLOOP_ENTRIES - 1 || !callback)
return -EINVAL;
data = malloc(sizeof(*data));
if (!data)
return -ENOMEM;
memset(data, 0, sizeof(*data));
data->fd = fd;
data->events = events;
data->callback = callback;
data->destroy = destroy;
data->user_data = user_data;
memset(&ev, 0, sizeof(ev));
ev.events = events;
ev.data.ptr = data;
err = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, data->fd, &ev);
if (err < 0) {
free(data);
return err;
}
mainloop_list[fd] = data;
return 0;
}
可以看到,确实和我想象的一样,就是通过epoll_ctl接口把对应的fd和event添加到epoll的监听列表里了,事件关联的data的数据结构是mainloop_data,这里面包含了两个函数指针和一个用户数据指针。
struct mainloop_data {
int fd;
uint32_t events;
mainloop_event_func callback;
mainloop_destroy_func destroy;
void *user_data;
};
回到最开始,epoll的创建是在mainloop_init函数中完成的,这也就是mian流程里一开始调用了mainloop_init的作用,就是为了创建epoll并初始化相关的资源,为后续mgmt_new_default创建的socket可以进行监听。换句话说,mgmt_new_default其实就是创建了一个socket,并放到epoll的监听列表里监听了起来。
但在gap层面,mgmt_new_default并没有看到fd对应的回调的设置,这个fd如果有事件发生,是不是不需要gap进行介入处理?带着这个问题,我们回到gap_start继续分析;
数据发送流程:
mgmt_new_default之后,gap_start中调用一个mgmt_send函数就没有其他操作了,所以猫腻可能还在mgmt_send函数中。mgmt_send的定义也是在src/shared/mgmt.c中,和mgmt_new_default这个是同一个层面;
unsigned int mgmt_send(struct mgmt *mgmt, uint16_t opcode, uint16_t index,
uint16_t length, const void *param,
mgmt_request_func_t callback,
void *user_data, mgmt_destroy_func_t destroy)
{
struct mgmt_request *request;
if (!mgmt)
return 0;
request = create_request(mgmt, opcode, index, length, param,
callback, user_data, destroy);
if (!request)
return 0;
if (mgmt->next_request_id < 1)
mgmt->next_request_id = 1;
request->id = mgmt->next_request_id++;
if (!queue_push_tail(mgmt->request_queue, request)) {
free(request->buf);
free(request);
return 0;
}
wakeup_writer(mgmt);
return request->id;
}
从这个函数中可以看出,它调用了create_request函数创建了一个请求,然后把这个请求放到了mgmt->request_queue队列的尾部,然后调用wakeup_writer(mgmt)就没啥操作了;从这里看出,每一个请求,都对应有一个request->id,这个id是由mgmt里的next_request_id统一管理的,每对mgmt发一个请求,序号就递增;
这里可以顺便追踪一下消息发送的流程。跟踪这个mgmt->request_queue队列可以发现,在执行wakeup_writer函数的时候,会设置mgmt->io的EPOLLOUT事件,也就是前面说的socket的fd,如果这个fd是可以写的,就交给epoll去完成IO的操作;
bool io_set_write_handler(struct io *io, io_callback_func_t callback,
void *user_data, io_destroy_func_t destroy)
{
uint32_t events;
if (!io || io->fd < 0)
return false;
if (io->write_destroy)
io->write_destroy(io->write_data);
if (callback)
events = io->events | EPOLLOUT;
else
events = io->events & ~EPOLLOUT;
io->write_callback = callback;
io->write_destroy = destroy;
io->write_data = user_data;
if (events == io->events)
return true;
if (mainloop_modify_fd(io->fd, events) < 0)
return false;
io->events = events;
return true;
}
了解epoll模型的都知道,如果有EPOLLOUT事件,也就是fd可以写的时候,会触发回调,也就是wakeup_writer函数中的can_write_data函数最终会被调用;这个函数会最终触发发送数据的函数。
static bool can_write_data(struct io *io, void *user_data)
{
struct mgmt *mgmt = user_data;
struct mgmt_request *request;
bool can_write;
request = queue_pop_head(mgmt->reply_queue);
if (!request) {
/* only reply commands can jump the queue */
if (!queue_isempty(mgmt->pending_list))
return false;
request = queue_pop_head(mgmt->request_queue);
if (!request)
return false;
can_write = false;
} else {
/* allow multiple replies to jump the queue */
can_write = !queue_isempty(mgmt->reply_queue);
}
if (!send_request(mgmt, request))
return true;
return can_write;
}
send_request函数最终会调用io_send函数,把IO请求发送出去,io_send函数也有多个地方定义,根据前面的逻辑,应该也是io_mainloop.c里面的定义的,最终是调用writev系统调用接口将数据发送给内核;
ssize_t io_send(struct io *io, const struct iovec *iov, int iovcnt)
{
ssize_t ret;
if (!io || io->fd < 0)
return -ENOTCONN;
do {
ret = writev(io->fd, iov, iovcnt);
} while (ret < 0 && errno == EINTR);
if (ret < 0)
return -errno;
return ret;
}
应用层mgmt_send的数据发送流程就分析到这里,再深入就是内核驱动上面了。总的概括,mgmt_send发送的数据是通过mgmt->io进行交互的,最终的数据是通过系统调用writev发送给内核的;
数据接收流程:
看完数据的发送流程,只知道gap_start函数中把数据发送出去了,还需要分析一下数据是怎么接收回来的。
在前面讲到的mgmt_new_default函数中,除了创建一个socket的fd之外,其实在mgmt_new函数中还创建了一系列的队列,也包括前面发送流程中的请求队列,还有接收队列等等;
mgmt->request_queue = queue_new();
mgmt->reply_queue = queue_new();
mgmt->pending_list = queue_new();
mgmt->notify_list = queue_new();
除了这些基本的数据结构,还对这个socket的fd的io设置了回调函数:
if (!io_set_read_handler(mgmt->io, can_read_data, mgmt, NULL)) {
queue_destroy(mgmt->notify_list, NULL);
queue_destroy(mgmt->pending_list, NULL);
queue_destroy(mgmt->reply_queue, NULL);
queue_destroy(mgmt->request_queue, NULL);
io_destroy(mgmt->io);
free(mgmt->buf);
free(mgmt);
return NULL;
}
不出意外的话,这个io_set_read_handler函数还是在io_mainloop.c中定义的:
bool io_set_read_handler(struct io *io, io_callback_func_t callback,
void *user_data, io_destroy_func_t destroy)
{
uint32_t events;
if (!io || io->fd < 0)
return false;
if (io->read_destroy)
io->read_destroy(io->read_data);
if (callback)
events = io->events | EPOLLIN;
else
events = io->events & ~EPOLLIN;
io->read_callback = callback;
io->read_destroy = destroy;
io->read_data = user_data;
if (events == io->events)
return true;
if (mainloop_modify_fd(io->fd, events) < 0)
return false;
io->events = events;
return true;
}
还是熟悉的epoll模型,对这个fd进行了可读事件的绑定,当fd可以读的时候,会回调对应的callback函数,也就是mgmt_new中设置的can_read_data函数。
static bool can_read_data(struct io *io, void *user_data)
{
struct mgmt *mgmt = user_data;
struct mgmt_hdr *hdr;
struct mgmt_ev_cmd_complete *cc;
struct mgmt_ev_cmd_status *cs;
ssize_t bytes_read;
uint16_t opcode, event, index, length;
bytes_read = read(mgmt->fd, mgmt->buf, mgmt->len);
if (bytes_read < 0)
return false;
util_hexdump('>', mgmt->buf, bytes_read,
mgmt->debug_callback, mgmt->debug_data);
if (bytes_read < MGMT_HDR_SIZE)
return true;
hdr = mgmt->buf;
event = btohs(hdr->opcode);
index = btohs(hdr->index);
length = btohs(hdr->len);
if (bytes_read < length + MGMT_HDR_SIZE)
return true;
mgmt_ref(mgmt);
switch (event) {
case MGMT_EV_CMD_COMPLETE:
cc = mgmt->buf + MGMT_HDR_SIZE;
opcode = btohs(cc->opcode);
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] command 0x%04x complete: 0x%02x",
index, opcode, cc->status);
request_complete(mgmt, cc->status, opcode, index, length - 3,
mgmt->buf + MGMT_HDR_SIZE + 3);
break;
case MGMT_EV_CMD_STATUS:
cs = mgmt->buf + MGMT_HDR_SIZE;
opcode = btohs(cs->opcode);
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] command 0x%02x status: 0x%02x",
index, opcode, cs->status);
request_complete(mgmt, cs->status, opcode, index, 0, NULL);
break;
default:
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] event 0x%04x", index, event);
process_notify(mgmt, event, index, length,
mgmt->buf + MGMT_HDR_SIZE);
break;
}
mgmt_unref(mgmt);
return true;
}
can_read_data函数是接收内核消息的回调函数,根据event的类型分为MGMT_EV_CMD_COMPLETE和MGMT_EV_CMD_STATUS这些,这里的event和上面说的epoll的event不是一个概念,这里的event对应的是mgmt消息的opcode;暂时也不用管这些类型,从 can_read_data函数可以看到,不管是哪种类型,都会调用request_complete函数进行对内核消息的处理。
static void request_complete(struct mgmt *mgmt, uint8_t status,
uint16_t opcode, uint16_t index,
uint16_t length, const void *param)
{
struct opcode_index match = { .opcode = opcode, .index = index };
struct mgmt_request *request;
request = queue_remove_if(mgmt->pending_list,
match_request_opcode_index, &match);
if (!request) {
util_debug(mgmt->debug_callback, mgmt->debug_data,
"Unable to find request for opcode 0x%04x",
opcode);
/* Attempt to remove with no opcode */
request = queue_remove_if(mgmt->pending_list,
match_request_index,
UINT_TO_PTR(index));
}
if (request) {
if (request->callback)
request->callback(status, length, param,
request->user_data);
destroy_request(request);
}
wakeup_writer(mgmt);
}
request_complete函数中,从mgmt->pending_list队列里查找与opcode和index均匹配的request,并调用request->callback函数。这里,实际上找到匹配的request后,就会从mgmt->pending_list列表里删除这个request了,但是request的空间还没有释放,调用request->callback完成后,使用destroy_request(request)函数释放掉真正的request空间。
这里的request->callback就是每个mgmt_send函数传递下来的callback函数指针。前面说到,mgmt_send发送数据的时候是最终调用到send_request函数发送的,send_request函数在调用io_send之后,还会把这个请求放到mgmt->pending_list队列中,便于接收内核消息,只有收到内核消息之后,这个request才会真正完成使命。
总的来说,mgmt监听并接收内核消息的动作在mgmt_new_default的过程就已经设置好了,每个消息通过mgmt_send发送的时候,只需要绑定对应的回调函数,内核有这个消息的回复的时候,就会自动触发这个回调函数,总的消息接收过程大概是这个样子。
蓝牙GAP初始化流程:
有了上面说的数据发送流程和数据接收流程的基础之后,我们继续分析gap_start的过程。gap_start中我们更关注的是消息的内容了。
从前面的逻辑可以知道,gap_start中发送了MGMT_OP_READ_COMMANDS这个命令之后,如果内核收到并回复的话,会触发read_commands_complete函数。
static void read_commands_complete(uint8_t status, uint16_t len,
const void *param, void *user_data)
{
const struct mgmt_rp_read_commands *rp = param;
uint16_t num_commands;
bool ext_index_list = false;
int i;
if (status) {
fprintf(stderr, "Reading index list failed: %s\n",
mgmt_errstr(status));
return;
}
num_commands = le16_to_cpu(rp->num_commands);
for (i = 0; i < num_commands; i++) {
uint16_t op = get_le16(rp->opcodes + 1);
if (op == MGMT_OP_READ_EXT_INDEX_LIST)
ext_index_list = true;
else if (op == MGMT_OP_READ_ADV_FEATURES)
adv_features = true;
}
if (ext_index_list) {
mgmt_register(mgmt, MGMT_EV_EXT_INDEX_ADDED, MGMT_INDEX_NONE,
ext_index_added_event, NULL, NULL);
mgmt_register(mgmt, MGMT_EV_EXT_INDEX_REMOVED, MGMT_INDEX_NONE,
ext_index_removed_event, NULL, NULL);
if (!mgmt_send(mgmt, MGMT_OP_READ_EXT_INDEX_LIST,
MGMT_INDEX_NONE, 0, NULL,
read_ext_index_list_complete, NULL, NULL)) {
fprintf(stderr, "Failed to read extended index list\n");
return;
}
} else {
mgmt_register(mgmt, MGMT_EV_INDEX_ADDED, MGMT_INDEX_NONE,
index_added_event, NULL, NULL);
mgmt_register(mgmt, MGMT_EV_INDEX_REMOVED, MGMT_INDEX_NONE,
index_removed_event, NULL, NULL);
if (!mgmt_send(mgmt, MGMT_OP_READ_INDEX_LIST,
MGMT_INDEX_NONE, 0, NULL,
read_index_list_complete, NULL, NULL)) {
fprintf(stderr, "Failed to read index list\n");
return;
}
}
}
read_commands_complete函数中,对应的消息数据结构是struct mgmt_rp_read_commands
struct mgmt_rp_read_commands {
uint16_t num_commands;
uint16_t num_events;
uint16_t opcodes[0];
} __packed;
从这个数据结构中,可以大概知道,大概率是查询蓝牙控制器支持多少个命令,支持多少事件等等;
但这里似乎有个bug,我不知道是什么原因,for循环中应该是通过i去遍历所有的支持的命令,但是确使用了+1的操作,如果是+1,根本没必要循环,作者的本意应该是遍历吧,有没有知道的看官?我看了一下github上的代码也是+1,这里是我没有搞懂的地方;
通过控制器是否支持MGMT_OP_READ_EXT_INDEX_LIST确定以哪种方式注册不同的事件函数回调。
mgmt_register函数应该和mgmt_send函数类似,注册的对应事件后,对应的回调函数会被触发。
通过跟踪上面的几个情况,会发现,无论是哪种情况,回调函数都是统一进行了发送MGMT_OP_READ_INFO命令,并都绑定了read_info_complete函数。也就是说,无论MGMT_OP_READ_COMMANDS命令发送后内核消息返回哪一个,最终都会触发MGMT_OP_READ_INFO的发送,并最后回调到read_info_complete函数。那我们只需要继续关注read_info_complete函数就行。
MGMT_OP_READ_INFO命令对应返回的消息数据结构是struct mgmt_rp_read_info:
struct mgmt_rp_read_info {
bdaddr_t bdaddr;
uint8_t version;
uint16_t manufacturer;
uint32_t supported_settings;
uint32_t current_settings;
uint8_t dev_class[3];
uint8_t name[MGMT_MAX_NAME_LENGTH];
uint8_t short_name[MGMT_MAX_SHORT_NAME_LENGTH];
} __packed;
这里面包含了控制器的MAC地址,版本,厂家等信息,以及supported_settings,表示支持的设置,current_settings表示当前的设置,还有名称、类型等内容。
read_info_complete函数里面根据支持的设置和当前的设置,注册了一系列的事件回调,用于监听蓝牙的事件发生,这其中就包括设置变更事件、名称改变事件、秘钥变更事件、连接事件、断开连接事件、认证失败事件、配对事件、取消配对事件、连接参数更新事件、广播事件等;然后就是初始化流程:
1、发送MGMT_OP_SET_POWERED命令关闭蓝牙;
2、发送MGMT_SETTING_LE设置蓝牙是BLE模式;
3、发送MGMT_OP_SET_SECURE_CONN设置蓝牙加密连接;
4、发送MGMT_SETTING_DEBUG_KEYS关闭调试秘钥;
5、发送MGMT_SETTING_BONDABLE设置可以被发现;
6、发送MGMT_OP_SET_STATIC_ADDRESS设置蓝牙地址;
7、发送MGMT_OP_READ_ADV_FEATURES设置广播信息;
函数中还调用gatt_server_start函数进行了Gatt服务的初始化流程;从上面的各种事件和下发命令看出,gap应该处理蓝牙连接相关的功能,包括配对、连接、广播等等;gap.c大概的工作内容就是这些,还有部分就是各个事件回调的处理,这里的gap.c比较简单,各个事件回调只是调用printf进行打印了一下,并没有做什么实际性的处理。
蓝牙GATT初始化流程:
上面的gap初始化流程中,在初始化的过程中,调用了gatt_server_start函数触发了GATT层的初始化,这里看看gatt层主要做了哪些工作。
void gatt_server_start(void)
{
struct sockaddr_l2 addr;
if (att_fd >= 0)
return;
att_fd = socket(PF_BLUETOOTH, SOCK_SEQPACKET | SOCK_CLOEXEC,
BTPROTO_L2CAP);
if (att_fd < 0) {
fprintf(stderr, "Failed to create ATT server socket: %m\n");
return;
}
memset(&addr, 0, sizeof(addr));
addr.l2_family = AF_BLUETOOTH;
addr.l2_cid = htobs(ATT_CID);
memcpy(&addr.l2_bdaddr, static_addr, 6);
addr.l2_bdaddr_type = BDADDR_LE_RANDOM;
if (bind(att_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
fprintf(stderr, "Failed to bind ATT server socket: %m\n");
close(att_fd);
att_fd = -1;
return;
}
if (listen(att_fd, 1) < 0) {
fprintf(stderr, "Failed to listen on ATT server socket: %m\n");
close(att_fd);
att_fd = -1;
return;
}
gatt_db = gatt_db_new();
if (!gatt_db) {
close(att_fd);
att_fd = -1;
return;
}
populate_gap_service(gatt_db);
populate_devinfo_service(gatt_db);
gatt_cache = gatt_db_new();
conn_list = queue_new();
if (!conn_list) {
gatt_db_unref(gatt_db);
gatt_db = NULL;
close(att_fd);
att_fd = -1;
return;
}
mainloop_add_fd(att_fd, EPOLLIN, att_conn_callback, NULL, NULL);
}
从gatt_server_start中可以看到,gatt也初始化了一个socket的att_fd句柄,然后设置了这个att_fd和BDADDR_LE_RANDOM以及static_addr绑定起来了,这个static_addr就是最开始说的main函数中设置的静态MAC地址,在gap中也有涉及这个地址,这个att_fd句柄最终通过mainloop_add_fd函数添加到了epoll监听列表里,有前面的基础就知道,如果这个att_fd有对应的EPOLLIN事件,会触发att_conn_callback函数回调,这个函数稍后再分析,先看看gatt_server_start还做了哪些事情。
gatt_server_start中通过gatt_db_new函数获取了一个gatt_db
struct gatt_db *gatt_db_new(void)
{
struct gatt_db *db;
db = new0(struct gatt_db, 1);
db->crypto = bt_crypto_new();
db->services = queue_new();
db->notify_list = queue_new();
db->next_handle = 0x0001;
return gatt_db_ref(db);
}
可以看到这个gatt_db管理着服务列表和通知列表;
gatt_server_start中还通过populate_gap_service和populate_devinfo_service两个函数注册了俩个服务,闭着眼睛想都知道,这两个服务肯定是挂载gatt_db->services的列表下了。
static void populate_gap_service(struct gatt_db *db)
{
struct gatt_db_attribute *service;
bt_uuid_t uuid;
bt_uuid16_create(&uuid, UUID_GAP);
service = gatt_db_add_service(db, &uuid, true, 6);
bt_uuid16_create(&uuid, GATT_CHARAC_DEVICE_NAME);
gatt_db_service_add_characteristic(service, &uuid,
BT_ATT_PERM_READ,
BT_GATT_CHRC_PROP_READ,
gap_device_name_read, NULL, NULL);
gatt_db_service_set_active(service, true);
}
static void populate_devinfo_service(struct gatt_db *db)
{
struct gatt_db_attribute *service;
bt_uuid_t uuid;
bt_uuid16_create(&uuid, 0x180a);
service = gatt_db_add_service(db, &uuid, true, 17);
gatt_db_service_set_active(service, true);
}
从代码层面理解得话,每个服务有唯一的UUID,服务下有多个characteristic,每个characteristic也有唯一的UUID,每个characteristic通过gatt_db_service_add_characteristic函数与对应的service绑定在一起,每个characteristic有多个attribute,例如有读属性,写属性,通知属性等。最后通过gatt_db_service_set_active(service, true)激活服务。
前面提到,gatt_server_start最终是绑定fd和att_conn_callback函数,当fd可以读的时候,对应了有蓝牙连接的事件,也就是说手机app连接蓝牙的时候,att_conn_callback函数会被调用。再看看是怎么处理蓝牙连接的。
static void att_conn_callback(int fd, uint32_t events, void *user_data)
{
struct gatt_conn *conn;
struct sockaddr_l2 addr;
socklen_t addrlen;
int new_fd;
if (events & (EPOLLERR | EPOLLHUP)) {
mainloop_remove_fd(fd);
return;
}
memset(&addr, 0, sizeof(addr));
addrlen = sizeof(addr);
new_fd = accept(att_fd, (struct sockaddr *) &addr, &addrlen);
if (new_fd < 0) {
fprintf(stderr, "Failed to accept new ATT connection: %m\n");
return;
}
conn = gatt_conn_new(new_fd);
if (!conn) {
fprintf(stderr, "Failed to create GATT connection\n");
close(new_fd);
return;
}
if (!queue_push_tail(conn_list, conn)) {
fprintf(stderr, "Failed to add GATT connection\n");
gatt_conn_destroy(conn);
close(new_fd);
}
printf("New device connected\n");
}
当有新的连接过来的时候,程序会通过accept函数接受一个连接。这个和TCP是挺类似的,前面也是使用socket-bind-listen-accept,也就是说,前面的监听的att_fd句柄就类似于TCP服务器上的socketFd,这里的accept返回的new_fd就对应于一个客户端的fd。然后gatt_conn_new函数应该把fd通过epoll监听起来,这样才能接受app发送的消息。
static struct gatt_conn *gatt_conn_new(int fd)
{
struct gatt_conn *conn;
uint16_t mtu = 0;
conn = new0(struct gatt_conn, 1);
if (!conn)
return NULL;
conn->att = bt_att_new(fd, false);
if (!conn->att) {
fprintf(stderr, "Failed to initialze ATT transport layer\n");
free(conn);
return NULL;
}
bt_att_set_close_on_unref(conn->att, true);
bt_att_register_disconnect(conn->att, gatt_conn_disconnect, conn, NULL);
bt_att_set_security(conn->att, BT_SECURITY_MEDIUM);
conn->gatt = bt_gatt_server_new(gatt_db, conn->att, mtu, 0);
if (!conn->gatt) {
fprintf(stderr, "Failed to create GATT server\n");
bt_att_unref(conn->att);
free(conn);
return NULL;
}
conn->client = bt_gatt_client_new(gatt_cache, conn->att, mtu, 0);
if (!conn->gatt) {
fprintf(stderr, "Failed to create GATT client\n");
bt_gatt_server_unref(conn->gatt);
bt_att_unref(conn->att);
free(conn);
return NULL;
}
bt_gatt_client_ready_register(conn->client, client_ready_callback,
conn, NULL);
bt_gatt_client_set_service_changed(conn->client,
client_service_changed_callback, conn, NULL);
return conn;
}
这里没有那么明显的通过mainloop_add_fd监听客户端连接的fd,而是通过bt_att_new函数进行处理,底层应该也是这个接口。通过分析,也确实是这样,bt_att_new->bt_att_chan_new->io_new->mainloop_add_fd。
bt_att_new函数除了监听fd,还设置了断开回调函数gatt_conn_disconnect,应该是蓝牙断开的时候会触发这个函数。还有一个是注册了client_ready_callback函数,从名称上看应该是客户端就绪的函数回调。断开回调函数也只是把这个conn从连接列表里移除和释放资源而已。
static void gatt_conn_disconnect(int err, void *user_data)
{
struct gatt_conn *conn = user_data;
printf("Device disconnected: %s\n", strerror(err));
queue_remove(conn_list, conn);
gatt_conn_destroy(conn);
}
总体上,GATT层大概也是这些内容,不同的服务可能有不同的特征和属性,GATT完成后,app上蓝牙连接后,应该是能够看到这些注册的服务和服务有哪些特征、哪些属性等等。具体的消息交换,可能针对某个服务的某个特性进行的,不是针对整个gatt进行的,gatt只是提供了一个服务平台。
回归问题:
整个peripheral框架大概的内容是分析完成了,从上面的分析可以看到,无论是gap还是gatt,都是依赖于mainloop中的epoll进行的,这也是我分析的是比较困惑的地方。可以先看看mainloop的实现。
mainloop中定义了一个全局的指针数组,大小是128。
#define MAX_MAINLOOP_ENTRIES 128
static struct mainloop_data *mainloop_list[MAX_MAINLOOP_ENTRIES];
在mainloop_init的时候,会将这128个数组全部都指向NULL;
void mainloop_init(void)
{
unsigned int i;
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
for (i = 0; i < MAX_MAINLOOP_ENTRIES; i++)
mainloop_list[i] = NULL;
epoll_terminate = 0;
mainloop_notify_init();
}
mianloop_run函数会创建epoll,然后循环用epoll_wait来等待事件到来,然后进行处理。熟悉epoll模型的看官应该都清楚,这流程没有什么异常。
int mainloop_run(void)
{
unsigned int i;
while (!epoll_terminate) {
struct epoll_event events[MAX_EPOLL_EVENTS];
int n, nfds;
nfds = epoll_wait(epoll_fd, events, MAX_EPOLL_EVENTS, -1);
if (nfds < 0)
continue;
for (n = 0; n < nfds; n++) {
struct mainloop_data *data = events[n].data.ptr;
data->callback(data->fd, events[n].events,
data->user_data);
}
}
for (i = 0; i < MAX_MAINLOOP_ENTRIES; i++) {
struct mainloop_data *data = mainloop_list[i];
mainloop_list[i] = NULL;
if (data) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, data->fd, NULL);
if (data->destroy)
data->destroy(data->user_data);
free(data);
}
}
close(epoll_fd);
epoll_fd = 0;
mainloop_notify_exit();
return exit_status;
}
在监听fd的时候,上面会调用mainloop_add_fd来增加fd到epoll监听列表中,也就是通过epoll_ctl去增加监听事件。但是,对于mainloop_list的维护,这里确采用了fd作为数组的下标进行维护,我有点不理解。
int mainloop_add_fd(int fd, uint32_t events, mainloop_event_func callback,
void *user_data, mainloop_destroy_func destroy)
{
struct mainloop_data *data;
struct epoll_event ev;
int err;
if (fd < 0 || fd > MAX_MAINLOOP_ENTRIES - 1 || !callback)
return -EINVAL;
data = malloc(sizeof(*data));
if (!data)
return -ENOMEM;
memset(data, 0, sizeof(*data));
data->fd = fd;
data->events = events;
data->callback = callback;
data->destroy = destroy;
data->user_data = user_data;
memset(&ev, 0, sizeof(ev));
ev.events = events;
ev.data.ptr = data;
err = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, data->fd, &ev);
if (err < 0) {
free(data);
return err;
}
mainloop_list[fd] = data;
return 0;
}
fd的范围可能会超过128,虽然在函数中对fd的范围进行了约束,最终数组肯定不会越界。但是超过128的fd却无法被监听起来,也就是说,超过128后,这个蓝牙模型就不能用了,监听不了事件,其他的回调也不会被触发。各位看官有没有知道这个是什么原因?为啥官方要这样定义和维护mainloop_list?
这个也是为啥app与设备连接-断开多次之后,fd增长超过128,这个时候accept是成功的,也就是app的连接请求是被设备accept的,但是app发数据,设备却感知不到的原因。重启程序后,fd又从小开始,于是又正常了。
于是我对mainloop动手了,增加了两个函数。
static uint32_t get_idle_index(void)
{
uint32_t i = 0;
for (i = 0; i < MAX_MAINLOOP_ENTRIES; i++) {
struct mainloop_data *data = mainloop_list[i];
if(data == NULL){
return i;
}
}
return MAX_MAINLOOP_ENTRIES;
}
static uint32_t get_index_by_fd(int fd)
{
uint32_t i = 0;
for (i = 0; i < MAX_MAINLOOP_ENTRIES; i++) {
struct mainloop_data *data = mainloop_list[i];
if(data && data->fd == fd){
return i;
}
}
return MAX_MAINLOOP_ENTRIES;
}
新增fd的时候不再是直接用fd作为下标对mainloop_list进行管理了,而是通过查询mainloop_list中空的项,返回对应的下标进行控制。
测试发现问题确实解决了,即使是超大的fd,也不会有问题。
唯一的解释是,这个代码只能是个参考例子,不能作为正式使用。但这个mainloop在各个地方都有用到,也解释不通。
不明觉厉。