On 02/13/14 09:01, Pawel Osciak wrote: > Hi Hans, > Thanks for the patch. I'm really happy to see this, it's a great idea > and it will be very useful. > > Two comments: > - What do you think about moving the debug stuff to something like > videobuf2-debug.{c,h} instead? It's not quite worth it at the moment IMHO. > - At this point vb2_buffer_done() shouldn't be required to be balanced > with buf_queue(). I know later patches in this series will require it, > but at this point it's not true. Perhaps we should move this to the > 8th patch or after it? I don't feel too strong about this though. I disagree with you here. It has always been required, except nobody noticed it. Without the call to vb2_buffer_done the finish memop will never be called. That's always been wrong. Regards, Hans > > One more nit inline. > > But in general: > > Acked-by: Pawel Osciak <pawel@xxxxxxxxxx> > > > On Thu, Feb 6, 2014 at 8:02 PM, Hans Verkuil <hverkuil@xxxxxxxxx> wrote: >> From: Hans Verkuil <hans.verkuil@xxxxxxxxx> >> >> When a vb2_queue is freed check if all the mem_ops and queue ops were balanced. >> So the number of calls to e.g. buf_finish has to match the number of calls to >> buf_prepare, etc. >> >> This code is only enabled if CONFIG_VIDEO_ADV_DEBUG is set. >> >> Signed-off-by: Hans Verkuil <hans.verkuil@xxxxxxxxx> >> --- >> drivers/media/v4l2-core/videobuf2-core.c | 233 ++++++++++++++++++++++++------- >> include/media/videobuf2-core.h | 43 ++++++ >> 2 files changed, 226 insertions(+), 50 deletions(-) >> >> diff --git a/drivers/media/v4l2-core/videobuf2-core.c b/drivers/media/v4l2-core/videobuf2-core.c >> index 5a5fb7f..07b58bd 100644 >> --- a/drivers/media/v4l2-core/videobuf2-core.c >> +++ b/drivers/media/v4l2-core/videobuf2-core.c >> @@ -33,12 +33,63 @@ module_param(debug, int, 0644); >> printk(KERN_DEBUG "vb2: " fmt, ## arg); \ >> } while (0) >> >> -#define call_memop(q, op, args...) \ >> - (((q)->mem_ops->op) ? \ >> - ((q)->mem_ops->op(args)) : 0) >> +#ifdef CONFIG_VIDEO_ADV_DEBUG >> + >> +/* >> + * If advanced debugging is on, then count how often each op is called, >> + * which can either be per-buffer or per-queue. >> + * >> + * If the op failed then the 'fail_' variant is called to decrease the >> + * counter. That makes it easy to check that the 'init' and 'cleanup' >> + * (and variations thereof) stay balanced. >> + */ >> + >> +#define call_memop(vb, op, args...) \ >> +({ \ >> + struct vb2_queue *_q = (vb)->vb2_queue; \ >> + dprintk(2, "call_memop(%p, %d, %s)%s\n", \ >> + _q, (vb)->v4l2_buf.index, #op, \ >> + _q->mem_ops->op ? "" : " (nop)"); \ >> + (vb)->cnt_mem_ ## op++; \ >> + _q->mem_ops->op ? _q->mem_ops->op(args) : 0; \ >> +}) >> +#define fail_memop(vb, op) ((vb)->cnt_mem_ ## op--) >> + >> +#define call_qop(q, op, args...) \ >> +({ \ >> + dprintk(2, "call_qop(%p, %s)%s\n", q, #op, \ >> + (q)->ops->op ? "" : " (nop)"); \ >> + (q)->cnt_ ## op++; \ >> + (q)->ops->op ? (q)->ops->op(args) : 0; \ >> +}) >> +#define fail_qop(q, op) ((q)->cnt_ ## op--) >> + >> +#define call_vb_qop(vb, op, args...) \ >> +({ \ >> + struct vb2_queue *_q = (vb)->vb2_queue; \ >> + dprintk(2, "call_vb_qop(%p, %d, %s)%s\n", \ >> + _q, (vb)->v4l2_buf.index, #op, \ >> + _q->ops->op ? "" : " (nop)"); \ >> + (vb)->cnt_ ## op++; \ >> + _q->ops->op ? _q->ops->op(args) : 0; \ >> +}) >> +#define fail_vb_qop(vb, op) ((vb)->cnt_ ## op--) >> + >> +#else >> + >> +#define call_memop(vb, op, args...) \ >> + ((vb)->vb2_queue->mem_ops->op ? (vb)->vb2_queue->mem_ops->op(args) : 0) >> +#define fail_memop(vb, op) >> >> #define call_qop(q, op, args...) \ >> - (((q)->ops->op) ? ((q)->ops->op(args)) : 0) >> + ((q)->ops->op ? (q)->ops->op(args) : 0) >> +#define fail_qop(q, op) >> + >> +#define call_vb_qop(vb, op, args...) \ >> + ((vb)->vb2_queue->ops->op ? (vb)->vb2_queue->ops->op(args) : 0) >> +#define fail_vb_qop(vb, op) >> + >> +#endif >> >> #define V4L2_BUFFER_MASK_FLAGS (V4L2_BUF_FLAG_MAPPED | V4L2_BUF_FLAG_QUEUED | \ >> V4L2_BUF_FLAG_DONE | V4L2_BUF_FLAG_ERROR | \ >> @@ -61,7 +112,7 @@ static int __vb2_buf_mem_alloc(struct vb2_buffer *vb) >> for (plane = 0; plane < vb->num_planes; ++plane) { >> unsigned long size = PAGE_ALIGN(q->plane_sizes[plane]); >> >> - mem_priv = call_memop(q, alloc, q->alloc_ctx[plane], >> + mem_priv = call_memop(vb, alloc, q->alloc_ctx[plane], >> size, q->gfp_flags); >> if (IS_ERR_OR_NULL(mem_priv)) >> goto free; >> @@ -73,9 +124,10 @@ static int __vb2_buf_mem_alloc(struct vb2_buffer *vb) >> >> return 0; >> free: >> + fail_memop(vb, alloc); >> /* Free already allocated memory if one of the allocations failed */ >> for (; plane > 0; --plane) { >> - call_memop(q, put, vb->planes[plane - 1].mem_priv); >> + call_memop(vb, put, vb->planes[plane - 1].mem_priv); >> vb->planes[plane - 1].mem_priv = NULL; >> } >> >> @@ -87,11 +139,10 @@ free: >> */ >> static void __vb2_buf_mem_free(struct vb2_buffer *vb) >> { >> - struct vb2_queue *q = vb->vb2_queue; >> unsigned int plane; >> >> for (plane = 0; plane < vb->num_planes; ++plane) { >> - call_memop(q, put, vb->planes[plane].mem_priv); >> + call_memop(vb, put, vb->planes[plane].mem_priv); >> vb->planes[plane].mem_priv = NULL; >> dprintk(3, "Freed plane %d of buffer %d\n", plane, >> vb->v4l2_buf.index); >> @@ -104,12 +155,11 @@ static void __vb2_buf_mem_free(struct vb2_buffer *vb) >> */ >> static void __vb2_buf_userptr_put(struct vb2_buffer *vb) >> { >> - struct vb2_queue *q = vb->vb2_queue; >> unsigned int plane; >> >> for (plane = 0; plane < vb->num_planes; ++plane) { >> if (vb->planes[plane].mem_priv) >> - call_memop(q, put_userptr, vb->planes[plane].mem_priv); >> + call_memop(vb, put_userptr, vb->planes[plane].mem_priv); >> vb->planes[plane].mem_priv = NULL; >> } >> } >> @@ -118,15 +168,15 @@ static void __vb2_buf_userptr_put(struct vb2_buffer *vb) >> * __vb2_plane_dmabuf_put() - release memory associated with >> * a DMABUF shared plane >> */ >> -static void __vb2_plane_dmabuf_put(struct vb2_queue *q, struct vb2_plane *p) >> +static void __vb2_plane_dmabuf_put(struct vb2_buffer *vb, struct vb2_plane *p) >> { >> if (!p->mem_priv) >> return; >> >> if (p->dbuf_mapped) >> - call_memop(q, unmap_dmabuf, p->mem_priv); >> + call_memop(vb, unmap_dmabuf, p->mem_priv); >> >> - call_memop(q, detach_dmabuf, p->mem_priv); >> + call_memop(vb, detach_dmabuf, p->mem_priv); >> dma_buf_put(p->dbuf); >> memset(p, 0, sizeof(*p)); >> } >> @@ -137,11 +187,10 @@ static void __vb2_plane_dmabuf_put(struct vb2_queue *q, struct vb2_plane *p) >> */ >> static void __vb2_buf_dmabuf_put(struct vb2_buffer *vb) >> { >> - struct vb2_queue *q = vb->vb2_queue; >> unsigned int plane; >> >> for (plane = 0; plane < vb->num_planes; ++plane) >> - __vb2_plane_dmabuf_put(q, &vb->planes[plane]); >> + __vb2_plane_dmabuf_put(vb, &vb->planes[plane]); >> } >> >> /** >> @@ -246,10 +295,11 @@ static int __vb2_queue_alloc(struct vb2_queue *q, enum v4l2_memory memory, >> * callback, if given. An error in initialization >> * results in queue setup failure. >> */ >> - ret = call_qop(q, buf_init, vb); >> + ret = call_vb_qop(vb, buf_init, vb); >> if (ret) { >> dprintk(1, "Buffer %d %p initialization" >> " failed\n", buffer, vb); >> + fail_vb_qop(vb, buf_init); >> __vb2_buf_mem_free(vb); >> kfree(vb); >> break; >> @@ -321,18 +371,77 @@ static int __vb2_queue_free(struct vb2_queue *q, unsigned int buffers) >> } >> >> /* Call driver-provided cleanup function for each buffer, if provided */ >> - if (q->ops->buf_cleanup) { >> - for (buffer = q->num_buffers - buffers; buffer < q->num_buffers; >> - ++buffer) { >> - if (NULL == q->bufs[buffer]) >> - continue; >> - q->ops->buf_cleanup(q->bufs[buffer]); >> - } >> + for (buffer = q->num_buffers - buffers; buffer < q->num_buffers; >> + ++buffer) { >> + if (q->bufs[buffer]) >> + call_vb_qop(q->bufs[buffer], buf_cleanup, q->bufs[buffer]); >> } >> >> /* Release video buffer memory */ >> __vb2_free_mem(q, buffers); >> >> +#ifdef CONFIG_VIDEO_ADV_DEBUG >> + /* >> + * Check that all the calls were balances during the life-time of this >> + * queue. If not (or if the debug level is 1 or up), then dump the >> + * counters to the kernel log. >> + */ >> + if (q->num_buffers) { >> + bool unbalanced = q->cnt_start_streaming != q->cnt_stop_streaming || >> + q->cnt_wait_prepare != q->cnt_wait_finish; >> + >> + if (unbalanced || debug) { >> + pr_info("vb2: counters for queue %p:%s\n", q, >> + unbalanced ? " UNBALANCED!" : ""); >> + pr_info("vb2: setup: %u start_streaming: %u stop_streaming: %u\n", >> + q->cnt_queue_setup, q->cnt_start_streaming, >> + q->cnt_stop_streaming); >> + pr_info("vb2: wait_prepare: %u wait_finish: %u\n", >> + q->cnt_wait_prepare, q->cnt_wait_finish); >> + } >> + q->cnt_queue_setup = 0; >> + q->cnt_wait_prepare = 0; >> + q->cnt_wait_finish = 0; >> + q->cnt_start_streaming = 0; >> + q->cnt_stop_streaming = 0; >> + } >> + for (buffer = 0; buffer < q->num_buffers; ++buffer) { >> + struct vb2_buffer *vb = q->bufs[buffer]; >> + bool unbalanced = vb->cnt_mem_alloc != vb->cnt_mem_put || >> + vb->cnt_mem_prepare != vb->cnt_mem_finish || >> + vb->cnt_mem_get_userptr != vb->cnt_mem_put_userptr || >> + vb->cnt_mem_attach_dmabuf != vb->cnt_mem_detach_dmabuf || >> + vb->cnt_mem_map_dmabuf != vb->cnt_mem_unmap_dmabuf || >> + vb->cnt_buf_queue != vb->cnt_buf_done || >> + vb->cnt_buf_prepare != vb->cnt_buf_finish || >> + vb->cnt_buf_init != vb->cnt_buf_cleanup; >> + >> + if (unbalanced || debug) { >> + pr_info("vb2: counters for queue %p, buffer %d:%s\n", >> + q, buffer, unbalanced ? " UNBALANCED!" : ""); >> + pr_info("vb2: buf_init: %u buf_cleanup: %u buf_prepare: %u buf_finish: %u\n", >> + vb->cnt_buf_init, vb->cnt_buf_cleanup, >> + vb->cnt_buf_prepare, vb->cnt_buf_finish); >> + pr_info("vb2: buf_queue: %u buf_done: %u\n", >> + vb->cnt_buf_queue, vb->cnt_buf_done); >> + pr_info("vb2: alloc: %u put: %u prepare: %u finish: %u mmap: %u\n", >> + vb->cnt_mem_alloc, vb->cnt_mem_put, >> + vb->cnt_mem_prepare, vb->cnt_mem_finish, >> + vb->cnt_mem_mmap); >> + pr_info("vb2: get_userptr: %u put_userptr: %u\n", >> + vb->cnt_mem_get_userptr, vb->cnt_mem_put_userptr); >> + pr_info("vb2: attach_dmabuf: %u detach_dmabuf: %u map_dmabuf: %u unmap_dmabuf: %u\n", >> + vb->cnt_mem_attach_dmabuf, vb->cnt_mem_detach_dmabuf, >> + vb->cnt_mem_map_dmabuf, vb->cnt_mem_unmap_dmabuf); >> + pr_info("vb2: get_dmabuf: %u num_users: %u vaddr: %u cookie: %u\n", >> + vb->cnt_mem_get_dmabuf, >> + vb->cnt_mem_num_users, >> + vb->cnt_mem_vaddr, >> + vb->cnt_mem_cookie); >> + } >> + } >> +#endif >> + >> /* Free videobuf buffers */ >> for (buffer = q->num_buffers - buffers; buffer < q->num_buffers; >> ++buffer) { >> @@ -424,7 +533,7 @@ static bool __buffer_in_use(struct vb2_queue *q, struct vb2_buffer *vb) >> * case anyway. If num_users() returns more than 1, >> * we are not the only user of the plane's memory. >> */ >> - if (mem_priv && call_memop(q, num_users, mem_priv) > 1) >> + if (mem_priv && call_memop(vb, num_users, mem_priv) > 1) >> return true; >> } >> return false; >> @@ -703,8 +812,10 @@ static int __reqbufs(struct vb2_queue *q, struct v4l2_requestbuffers *req) >> */ >> ret = call_qop(q, queue_setup, q, NULL, &num_buffers, &num_planes, >> q->plane_sizes, q->alloc_ctx); >> - if (ret) >> + if (ret) { >> + fail_qop(q, queue_setup); >> return ret; >> + } >> >> /* Finally, allocate buffers and video memory */ >> ret = __vb2_queue_alloc(q, req->memory, num_buffers, num_planes); >> @@ -723,6 +834,8 @@ static int __reqbufs(struct vb2_queue *q, struct v4l2_requestbuffers *req) >> >> ret = call_qop(q, queue_setup, q, NULL, &num_buffers, >> &num_planes, q->plane_sizes, q->alloc_ctx); >> + if (ret) >> + fail_qop(q, queue_setup); >> >> if (!ret && allocated_buffers < num_buffers) >> ret = -ENOMEM; >> @@ -803,8 +916,10 @@ static int __create_bufs(struct vb2_queue *q, struct v4l2_create_buffers *create >> */ >> ret = call_qop(q, queue_setup, q, &create->format, &num_buffers, >> &num_planes, q->plane_sizes, q->alloc_ctx); >> - if (ret) >> + if (ret) { >> + fail_qop(q, queue_setup); >> return ret; >> + } >> >> /* Finally, allocate buffers and video memory */ >> ret = __vb2_queue_alloc(q, create->memory, num_buffers, >> @@ -828,6 +943,8 @@ static int __create_bufs(struct vb2_queue *q, struct v4l2_create_buffers *create >> */ >> ret = call_qop(q, queue_setup, q, &create->format, &num_buffers, >> &num_planes, q->plane_sizes, q->alloc_ctx); >> + if (ret) >> + fail_qop(q, queue_setup); >> >> if (!ret && allocated_buffers < num_buffers) >> ret = -ENOMEM; >> @@ -882,12 +999,10 @@ EXPORT_SYMBOL_GPL(vb2_create_bufs); >> */ >> void *vb2_plane_vaddr(struct vb2_buffer *vb, unsigned int plane_no) >> { >> - struct vb2_queue *q = vb->vb2_queue; >> - >> if (plane_no > vb->num_planes || !vb->planes[plane_no].mem_priv) >> return NULL; >> >> - return call_memop(q, vaddr, vb->planes[plane_no].mem_priv); >> + return call_memop(vb, vaddr, vb->planes[plane_no].mem_priv); >> >> } >> EXPORT_SYMBOL_GPL(vb2_plane_vaddr); >> @@ -905,12 +1020,10 @@ EXPORT_SYMBOL_GPL(vb2_plane_vaddr); >> */ >> void *vb2_plane_cookie(struct vb2_buffer *vb, unsigned int plane_no) >> { >> - struct vb2_queue *q = vb->vb2_queue; >> - >> if (plane_no > vb->num_planes || !vb->planes[plane_no].mem_priv) >> return NULL; >> >> - return call_memop(q, cookie, vb->planes[plane_no].mem_priv); >> + return call_memop(vb, cookie, vb->planes[plane_no].mem_priv); >> } >> EXPORT_SYMBOL_GPL(vb2_plane_cookie); >> >> @@ -938,12 +1051,19 @@ void vb2_buffer_done(struct vb2_buffer *vb, enum vb2_buffer_state state) >> if (state != VB2_BUF_STATE_DONE && state != VB2_BUF_STATE_ERROR) >> return; >> >> +#ifdef CONFIG_VIDEO_ADV_DEBUG >> + /* >> + * Although this is not a callback, it still does have to balance >> + * with the buf_queue op. So update this counter manually. >> + */ >> + vb->cnt_buf_done++; >> +#endif >> dprintk(4, "Done processing on buffer %d, state: %d\n", >> vb->v4l2_buf.index, state); >> >> /* sync buffers */ >> for (plane = 0; plane < vb->num_planes; ++plane) >> - call_memop(q, finish, vb->planes[plane].mem_priv); >> + call_memop(vb, finish, vb->planes[plane].mem_priv); >> >> /* Add the buffer to the done buffers list */ >> spin_lock_irqsave(&q->done_lock, flags); >> @@ -1067,19 +1187,20 @@ static int __qbuf_userptr(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> >> /* Release previously acquired memory if present */ >> if (vb->planes[plane].mem_priv) >> - call_memop(q, put_userptr, vb->planes[plane].mem_priv); >> + call_memop(vb, put_userptr, vb->planes[plane].mem_priv); >> >> vb->planes[plane].mem_priv = NULL; >> vb->v4l2_planes[plane].m.userptr = 0; >> vb->v4l2_planes[plane].length = 0; >> >> /* Acquire each plane's memory */ >> - mem_priv = call_memop(q, get_userptr, q->alloc_ctx[plane], >> + mem_priv = call_memop(vb, get_userptr, q->alloc_ctx[plane], >> planes[plane].m.userptr, >> planes[plane].length, write); >> if (IS_ERR_OR_NULL(mem_priv)) { >> dprintk(1, "qbuf: failed acquiring userspace " >> "memory for plane %d\n", plane); >> + fail_memop(vb, get_userptr); >> ret = mem_priv ? PTR_ERR(mem_priv) : -EINVAL; >> goto err; >> } >> @@ -1090,9 +1211,10 @@ static int __qbuf_userptr(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> * Call driver-specific initialization on the newly acquired buffer, >> * if provided. >> */ >> - ret = call_qop(q, buf_init, vb); >> + ret = call_vb_qop(vb, buf_init, vb); >> if (ret) { >> dprintk(1, "qbuf: buffer initialization failed\n"); >> + fail_vb_qop(vb, buf_init); >> goto err; >> } >> >> @@ -1108,7 +1230,7 @@ err: >> /* In case of errors, release planes that were already acquired */ >> for (plane = 0; plane < vb->num_planes; ++plane) { >> if (vb->planes[plane].mem_priv) >> - call_memop(q, put_userptr, vb->planes[plane].mem_priv); >> + call_memop(vb, put_userptr, vb->planes[plane].mem_priv); >> vb->planes[plane].mem_priv = NULL; >> vb->v4l2_planes[plane].m.userptr = 0; >> vb->v4l2_planes[plane].length = 0; >> @@ -1173,14 +1295,15 @@ static int __qbuf_dmabuf(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> dprintk(1, "qbuf: buffer for plane %d changed\n", plane); >> >> /* Release previously acquired memory if present */ >> - __vb2_plane_dmabuf_put(q, &vb->planes[plane]); >> + __vb2_plane_dmabuf_put(vb, &vb->planes[plane]); >> memset(&vb->v4l2_planes[plane], 0, sizeof(struct v4l2_plane)); >> >> /* Acquire each plane's memory */ >> - mem_priv = call_memop(q, attach_dmabuf, q->alloc_ctx[plane], >> + mem_priv = call_memop(vb, attach_dmabuf, q->alloc_ctx[plane], >> dbuf, planes[plane].length, write); >> if (IS_ERR(mem_priv)) { >> dprintk(1, "qbuf: failed to attach dmabuf\n"); >> + fail_memop(vb, attach_dmabuf); >> ret = PTR_ERR(mem_priv); >> dma_buf_put(dbuf); >> goto err; >> @@ -1195,10 +1318,11 @@ static int __qbuf_dmabuf(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> * the buffer(s).. >> */ >> for (plane = 0; plane < vb->num_planes; ++plane) { >> - ret = call_memop(q, map_dmabuf, vb->planes[plane].mem_priv); >> + ret = call_memop(vb, map_dmabuf, vb->planes[plane].mem_priv); >> if (ret) { >> dprintk(1, "qbuf: failed to map dmabuf for plane %d\n", >> plane); >> + fail_memop(vb, map_dmabuf); >> goto err; >> } >> vb->planes[plane].dbuf_mapped = 1; >> @@ -1208,9 +1332,10 @@ static int __qbuf_dmabuf(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> * Call driver-specific initialization on the newly acquired buffer, >> * if provided. >> */ >> - ret = call_qop(q, buf_init, vb); >> + ret = call_vb_qop(vb, buf_init, vb); >> if (ret) { >> dprintk(1, "qbuf: buffer initialization failed\n"); >> + fail_vb_qop(vb, buf_init); >> goto err; >> } >> >> @@ -1242,9 +1367,9 @@ static void __enqueue_in_driver(struct vb2_buffer *vb) >> >> /* sync buffers */ >> for (plane = 0; plane < vb->num_planes; ++plane) >> - call_memop(q, prepare, vb->planes[plane].mem_priv); >> + call_memop(vb, prepare, vb->planes[plane].mem_priv); >> >> - q->ops->buf_queue(vb); >> + call_vb_qop(vb, buf_queue, vb); >> } >> >> static int __buf_prepare(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> @@ -1295,8 +1420,11 @@ static int __buf_prepare(struct vb2_buffer *vb, const struct v4l2_buffer *b) >> ret = -EINVAL; >> } >> >> - if (!ret) >> - ret = call_qop(q, buf_prepare, vb); >> + if (!ret) { >> + ret = call_vb_qop(vb, buf_prepare, vb); >> + if (ret) >> + fail_vb_qop(vb, buf_prepare); >> + } >> if (ret) >> dprintk(1, "qbuf: buffer preparation failed: %d\n", ret); >> vb->state = ret ? VB2_BUF_STATE_DEQUEUED : VB2_BUF_STATE_PREPARED; >> @@ -1393,6 +1521,8 @@ static int vb2_start_streaming(struct vb2_queue *q) >> >> /* Tell the driver to start streaming */ >> ret = call_qop(q, start_streaming, q, atomic_read(&q->queued_count)); >> + if (ret) >> + fail_qop(q, start_streaming); > > I wonder whether we should treat ENOBUFS as a failed callback. It > won't balance then either, will it? That's correct. Which is why patch 8/10 replaces the whole ENOBUFS idea with a cleaner solution. Regards, Hans -- To unsubscribe from this list: send the line "unsubscribe linux-media" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html