linux v4l2学习之-消息机制
感兴趣可以加QQ群85486140,大家一起交流相互学习下!
文章目录
v4l2的消息同大部分的消息组织形式类似,可以理解成都是以队列的形式,有人往里面push,有人去get。看了大半天才把消息队列的机制了解清楚。在了解之前需要重温一下下面几个结构体。
kernel版本:kernel3.4
一、v4l2消息队列理解准备条件
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[64]:这里用户可以根据实际情况需要,用这64个字节定义自己的消息实体。
-
sequence:这个用来记录该消息的序列号(并不是index),其和
struct v4l2_fh
中的sequence是一致的,后面会结合代码分析。 -
timestamp:消息发送的时间戳,代码中也没发现具体使用的地方,应该是用来判断消息生命周期准备吧。暂且不关心
-
pending:用来记录消息队列中尚未处理的消息数量。
-
id:命令id.
1.2 struct v4l2_event_subscription
订阅消息发起时,订阅的消息用此结构体描述。此订阅发起者既可以是kenle模块,也可以是用户空间的管理者。这里第一次情调一下消息只有订阅后,别的模块才能把消息放到模块的消息队列中,没有定义的消息是无法queue进去的
struct v4l2_event_subscription {
__u32 type;
__u32 id;
__u32 flags;
__u32 reserved[5];
};
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 event */
unsigned int navailable;
u32 sequence;
};
- list:链表入口,因为一个设备可能有好几条消息队列。(一般情况下打开一次设备会创建一个消息队列,不过一般请看下只会打开一次)
- 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 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:订阅消息队列中最多能存放消息的数量,此变量在订阅消息下发过来时确定,然后会由kernel分配对应的内存。
- first:这个可以理解成游标指针,记录当前订阅消息队列中入队时间最久且需要处理的消息。
- in_use:入队的消息数量
- events:这里存放的只是一个指针,其大小由发起订阅时确定。
小结:此订阅消息,可以理解成一类消息的组合
二、Enqueue消息
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;
/* Are we subscribed? */
//这里特别要注意了,前面说过消息必须先订阅,才能queue进来,这了可以看到
//当检测到订阅消息列表中,没有当前消息,则直接return.
sev = v4l2_event_subscribed(fh, ev->type, ev->id);
if (sev == NULL)
return;
/* Increase event sequence number on fh. */
//消息序列号自动+1.
fh->sequence++;
/* Do we have any free events? */
//显然当in_use和elems相等时,此时消息数组已经满员了,此时需要将最旧的
//没有处理的消息移除订阅消息队列。
if (sev->in_use == sev->elems) {
/* no, remove the oldest one */
//队列中已经慢了,将第一个消息移除队列,腾出空间。
kev = sev->events + sev_pos(sev, 0);
list_del(&kev->list);
sev->in_use--; //如队列消息数目-1.
sev->first = sev_pos(sev, 1); //将可用指针指向第二个消息实体
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 =
sev->events + sev_pos(sev, 0);
sev->merge(&kev->event, &second_oldest->event);
}
}
/* Take one and fill it. */
//这了取出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);//唤醒所有等待消息的线程
}
这里就是如队列消息的最重要的函数,此函数大概有下面几个流程。
- 1.先检查消息是否存在订阅消息列表中,不存在的话不允许该消息入队列,反之允许消息入队列。
- 2.当该消息对应的订阅消息对象子消息满员之后,则将子消息队列中最老的那个替换掉,以留给新的消息。
- 3.将新消息挂接到v4l2_fh可用链表中,以进行后续其它处理
注意:下图是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时用dequeue_buffer
),相比较消息的enqueue操作,dequeue操作简单多了。这里仍然根据代码流程来分析。
//从参数中可以看到,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);
//从可用列表中取出第一个消息的入口,并将该消息从消息列表中删除。
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;
}
取消息相对简单多了,大概就下面几步
- 1.检查消息列表是否是为空,不为空则从中取出子消息。
- 2.将上面子消息,从上面的消息列表中删除,并减少消息队列中消息的数量
- 3.将消息对应的订阅消息对象的记录指针后移,以及如队列消息-1.
四、案例学习
- 上层
这里以高通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;
/* Are we subscribed? */
sev = v4l2_event_subscribed(fh, ev->type, ev->id);
if (sev == NULL)//如果订阅过的话,这里就不会进来
return;
五、总结
看了半天代码就总结一句话“消息必须先订阅,才允许入队列”。同时也需要知道下面几点。
- .订阅消息对象默认元素数量为1,如果设置的话就按设置的来分配内存。