V4L2的消息同大部分的消息组织形式类似,可以理解为都是以队列的形式,有人往里面push,有人去pop。
kernel版本:V3.4。
一、V4L2消息队列的准备知识
1.1 struct v4l2_event
V4L2的单个消息用此结构体来表述的:
struct v4l2_event { __u32 type; union { struct v4l2_event_vsync vsync; struct v4l2_event_ctrl ctrl; struct v4l2_event_frame_sync frame_sync; __u8 data[64]; } u; __u32 pending; __u32 sequence; struct timespec timestamp; __u32 id; __u32 reserved[8]; };
type:消息类型。目前发现v4l2原生的类型包含下面这些,这些用户可以根据需要自己定制。最后这个V4L2_EVENT_PRIVATE_START就是为扩展做准备的。
#define V4L2_EVENT_ALL 0 #define V4L2_EVENT_VSYNC 1 #define V4L2_EVENT_EOS 2 #define V4L2_EVENT_CTRL 3 #define V4L2_EVENT_FRAME_SYNC 4 #define V4L2_EVENT_PRIVATE_START 0x08000000
data:用户自定义的消息实体/内容。
sequence:消息的序列号(不是index)。和struct v4l2_fh的成员sequence相同。(后面会结合代码分析)
timestamp:消息发送的时间戳。代码中没发现具体使用处,应该是用来判断消息生命周期吧。
pending:记录消息队列中尚未处理的消息数量。
id:命令ID。
1.2 struct v4l2_event_subscription
订阅消息发起时,订阅的消息用此结构体描述:
struct v4l2_event_subscription { __u32 type; __u32 id; __u32 flags; __u32 reserved[5]; };
订阅发起者既可以是内核模块的,也可以是用户空间的。强调一下:只有在订阅消息后,别的模块才能把消息放到该模块的消息队列中,没有定义的消息是无法queue进去的。
1.3 struct v4l2_fh
消息队列就是用此结构体描述的:struct v4l2_fh { struct list_head list; struct video_device *vdev; struct v4l2_ctrl_handler *ctrl_handler; enum v4l2_priority prio; /* Events */ wait_queue_head_t wait; struct list_head subscribed; /* Subscribed events */ struct list_head available; /* Dequeueable events */ unsigned int navailable; u32 sequence; };
list:链表入口。因为一个设备可能有好几条消息队列。(一般情况下,打开1次设备,就会创建1个消息队列,不过一般情况下只会打开1次)
vdev:拥有此消息队列的设备。
ctrl_handler:其中牵扯很多其它的结构体,只需要了解这与控制元素相关就可以了。
prio:消息队列优先级。
wait:等待队列。由于可能有几个线程同时访问消息队列,所有等待消息队列可用的线程都会记录在该等待列表中。
subscribed:订阅消息链表。只有订阅的消息才能够queue到消息队列中,反之是无法queue到队列中的。
available:消息队列。可用的消息都以链表的形式保存在这里。
navailable:消息队列中可用消息的数量。
sequence:序列号。会一直自加下去,直到内核对象消亡。
上述几个结构体的组织结构大体如下所示:
注意:
<available>表示还没处理的消息。依然挂在链表中。
<subscribed>表示订阅的消息。单个订阅的消息仍然会保存几个子消息(毕竟一个消息可以发送多次)。要特别注意消息queue进来时,先会保存到subscribed对应的订阅消息列表,然后才会链接到available链表上,上面蓝色和橙色之间的箭头不一定是一一对应的。具体为什么会保存多个自消息,可以继续了解一下订阅消息结构体。
1.4 struct v4l2_subscribed_event
订阅消息队列是以该结构体存在:struct v4l2_subscribed_event { struct list_head list; u32 type; u32 id; u32 flags; struct v4l2_fh *fh; struct list_head node; void (*replace)(struct v4l2_event *old, const struct v4l2_event *new); void (*merge)(const struct v4l2_event *old, struct v4l2_event *new); unsigned elems; unsigned first; unsigned in_use; struct v4l2_kevent events[]; };
list:将订阅消息插入到队列里的链接指针。
flags:一些标志位记录。
fh:可以理解成,该消息挂在哪个队列之上。
replace:替换消息的方法。当队列只能存放1个消息时,才会触发此方法。(此方法在队列已满以及使用struct v4l2_ctrl控制)
merge:当队列有多余2个容量时,会触发merge方法。(此方法在队列已满以及使用struct v4l2_ctrl控制)
elems:队列中能存放消息的最大数量。此变量在订阅消息下发过来时确定,然后会由内核分配对应的内存。
first:可以理解成游标指针,记录当前队列中入队时间最久且需要处理的消息。
in_use:入队的消息数量。
events:这里存放的只是一个指针,其大小由发起订阅时确定。
小结:此订阅消息队列,可以理解成一类消息的组合。
二、Enqueue消息
这是消息入队的最重要的函数,此函数大概有如下几步:
1) 先检查消息是否在订阅消息队列中,不在的话,不允许该消息入队,反之允许入队。
2) 当该消息对应的订阅消息对象子消息满员之后,则将子消息队列中最老的那个替换掉,以留给新的消息。
3) 将新消息挂接到struct v4l2_fh可用链表中,以进行后续其它处理。static void __v4l2_event_queue_fh(struct v4l2_fh *fh, const struct v4l2_event *ev, const struct timespec *ts) { struct v4l2_subscribed_event *sev; struct v4l2_kevent *kev; bool copy_payload = true; /* 这里注意:前面说过消息必须先订阅,才能queue进来,此处可以看到, * 当检测到订阅消息列表中,没有当前消息,则直接return */ sev = v4l2_event_subscribed(fh, ev->type, ev->id); if (sev == NULL) return; fh->sequence++; // 消息序列号自动+1 /* 显然当in_use和elems相等时,此时消息数组已经满员了, * 此时需要将最旧的、没有处理的消息移除订阅消息队列 */ if (sev->in_use == sev->elems) { // 队列中已经慢了,将第一个消息移除队列,腾出空间。 kev = sev->events + sev_pos(sev, 0); list_del(&kev->list); sev->in_use--; // 入队消息数目-1 sev->first = sev_pos(sev, 1); //将可用指针指向第2个消息实体 fh->navailable--; // 消息队列消息数量-1 if (sev->elems == 1) { /* 如果子消息数量只有1个,则替换该消息, * 注意由于都是同类消息,只需要改变状态即可 */ if (sev->replace) { sev->replace(&kev->event, ev); copy_payload = false; } } else if (sev->merge) { struct v4l2_kevent *second_oldest; second_oldest = sev->events + sev_pos(sev, 0); sev->merge(&kev->event, &second_oldest->event); } } // 取出in_use所对应的消息实体,来存放具体的消息内容 kev = sev->events + sev_pos(sev, sev->in_use); kev->event.type = ev->type; if (copy_payload) { // 拷贝数据内容 kev->event.u = ev->u; } kev->event.id = ev->id; kev->event.timestamp = *ts; kev->event.sequence = fh->sequence; sev->in_use++; // 可用消息索引往后移动,相当于记录指针 /* 上面已经把消息添加到订阅消息子消息列表中了, * 这里把消息再次插入具体设备的消息队列中 */ list_add_tail(&kev->list, &fh->available); fh->navailable++; // 需处理消息数量+1 wake_up_all(&fh->wait); // 唤醒所有等待消息的线程 }
下图是struct v4l2_subscribed_event结构体中的struct v4l2_kevent子消息在订阅消息结构体中的存放形态:
上面子消息队列中已经入队了2个,则 in_use = 2,而此时记录指针指向[1],则 first = 1,剩下的红色都是空闲子消息缓存。子消息的结构如下所示:
struct v4l2_kevent { struct list_head list; struct v4l2_subscribed_event *sev; struct v4l2_event event; };
三、Dequeue消息
dequeue就是消息出队(类似于取buffer时用VIDIOC_DQBUF),与enqueue消息相比,dequeue就简单多了,大概就如下几步:
1) 检查消息队列是否是为空,不为空则从中取出子消息。
2) 将上面子消息,从上面的消息队列中删除,并减少消息队列中消息的数量。
3) 将消息对应的订阅消息对象的记录指针后移,以及入队消息数量-1。// 从参数中可以看到,v4l2_fh即消息队列,该队列被一个设备对象所拥有 static int __v4l2_event_dequeue(struct v4l2_fh *fh, struct v4l2_event *event) { struct v4l2_kevent *kev; unsigned long flags; spin_lock_irqsave(&fh->vdev->fh_lock, flags); // 判断队列中是否有可用的消息,没有就直接返回 if (list_empty(&fh->available)) { spin_unlock_irqrestore(&fh->vdev->fh_lock, flags); return -ENOENT; } WARN_ON(fh->navailable == 0); // 从可用列表中取出第1个消息的入口,并将该消息从消息列表中删除 kev = list_first_entry(&fh->available, struct v4l2_kevent, list); list_del(&kev->list); fh->navailable--; // 消息数量-1 // 将当前还需处理的消息列表,复制给刚才取出来的消息,以便它做其他操作 kev->event.pending = fh->navailable; *event = kev->event; // 将消息对应的订阅消息对象中的记录指针向后移1个 kev->sev->first = sev_pos(kev->sev, 1); kev->sev->in_use--; // 入队消息数量-1 spin_unlock_irqrestore(&fh->vdev->fh_lock, flags); return 0; }
四、案例学习
以高通daemon进程起来后,server就向video0 node中订阅了NEW_SESSION、DEL_SESSION等消息。
subscribe.type = MSM_CAMERA_V4L2_EVENT_TYPE; for (i = MSM_CAMERA_EVENT_MIN + 1; i < MSM_CAMERA_EVENT_MAX; i++) { subscribe.id = i; if (ioctl(hal_fd->fd[0], VIDIOC_SUBSCRIBE_EVENT, &subscribe) < 0) { goto subscribe_failed; } }
其中子消息的类型:
#define MSM_CAMERA_EVENT_MIN 0 #define MSM_CAMERA_NEW_SESSION (MSM_CAMERA_EVENT_MIN + 1) #define MSM_CAMERA_DEL_SESSION (MSM_CAMERA_EVENT_MIN + 2) #define MSM_CAMERA_SET_PARM (MSM_CAMERA_EVENT_MIN + 3) #define MSM_CAMERA_GET_PARM (MSM_CAMERA_EVENT_MIN + 4) #define MSM_CAMERA_MAPPING_CFG (MSM_CAMERA_EVENT_MIN + 5) #define MSM_CAMERA_MAPPING_SES (MSM_CAMERA_EVENT_MIN + 6) #define MSM_CAMERA_MSM_NOTIFY (MSM_CAMERA_EVENT_MIN + 7) #define MSM_CAMERA_EVENT_MAX (MSM_CAMERA_EVENT_MIN + 8)
上面是上层对应的代码,ioctl会调用到对应设备驱动实现的vidioc_subscribe_event,这里kernel已经实现了对应的接口。
// v4l2-ioctl.c case VIDIOC_SUBSCRIBE_EVENT: { struct v4l2_event_subscription *sub = arg; if (!ops->vidioc_subscribe_event) { break; } ret = ops->vidioc_subscribe_event(fh, sub); if (ret < 0) { dbgarg(cmd, "failed, ret=%ld", ret); break; } dbgarg(cmd, "type=0x%8.8x", sub->type); break; }
上面执行完成后,订阅消息NEW_SESSION、DEL_SESSION等消息会挂接到设备的fh->subscribed订阅链表上。下一个操作则从订阅列表上查找入队列的消息,存在的话允许如队列,反之不允许。
static void __v4l2_event_queue_fh(struct v4l2_fh *fh, const struct v4l2_event *ev, const struct timespec *ts) { struct v4l2_subscribed_event *sev; struct v4l2_kevent *kev; bool copy_payload = true; sev = v4l2_event_subscribed(fh, ev->type, ev->id); if (sev == NULL) { // 如果订阅过的话,就不会进这里来 return; } }
五、总结
看了半天代码就总结一句话“消息必须先订阅,才允许入队列”。同时也需要知道下面这点:
订阅消息对象默认元素数量为1,如果设置的话就按设置的来分配内存。