On Tue, Mar 13, 2018 at 10:42 AM, Chengguang Xu <cgxu519@xxxxxxx> wrote: > In current code, regular file and directory use same struct > ceph_file_info to store fs specific data so the struct has to > include some fields which are only used for directory > (e.g., readdir related info), when having plenty of regular files, > it will lead to memory waste. > > This patch introduces dedicated ceph_dir_file_info cache for > directory and delete readdir related thins from ceph_file_info, > so that regular file does not include those unused fields anymore. > Also, chagned to manipulate fscache after reuglar file acquires > ceph_file_info successfully. > Applied with some modifications. I remove fscache related change, pass 'dfi' directly to readdir helper functions. Regards Yan, Zheng > Signed-off-by: Chengguang Xu <cgxu519@xxxxxxx> > --- > Changes since v2: > - Rebase to testing tree. > - Introduce dfi only for readdir related things. > > Changes since v1: > - Modify ceph_dir_file_info to include ceph_file_info instead of pointing to it. > > fs/ceph/dir.c | 161 +++++++++++++++++++++++-------------------- > fs/ceph/file.c | 91 ++++++++++++++++-------- > fs/ceph/super.c | 8 +++ > fs/ceph/super.h | 9 +++ > include/linux/ceph/libceph.h | 1 + > 5 files changed, 169 insertions(+), 101 deletions(-) > > diff --git a/fs/ceph/dir.c b/fs/ceph/dir.c > index 51d09a3..be485c1 100644 > --- a/fs/ceph/dir.c > +++ b/fs/ceph/dir.c > @@ -105,15 +105,17 @@ static int fpos_cmp(loff_t l, loff_t r) > static int note_last_dentry(struct ceph_file_info *fi, const char *name, > int len, unsigned int next_offset) > { > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > + > char *buf = kmalloc(len+1, GFP_KERNEL); > if (!buf) > return -ENOMEM; > - kfree(fi->last_name); > - fi->last_name = buf; > - memcpy(fi->last_name, name, len); > - fi->last_name[len] = 0; > - fi->next_offset = next_offset; > - dout("note_last_dentry '%s'\n", fi->last_name); > + kfree(dfi->last_name); > + dfi->last_name = buf; > + memcpy(dfi->last_name, name, len); > + dfi->last_name[len] = 0; > + dfi->next_offset = next_offset; > + dout("note_last_dentry '%s'\n", dfi->last_name); > return 0; > } > > @@ -176,6 +178,7 @@ static int __dcache_readdir(struct file *file, struct dir_context *ctx, > int shared_gen) > { > struct ceph_file_info *fi = file->private_data; > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > struct dentry *parent = file->f_path.dentry; > struct inode *dir = d_inode(parent); > struct dentry *dentry, *last = NULL; > @@ -280,9 +283,9 @@ static int __dcache_readdir(struct file *file, struct dir_context *ctx, > err = ret; > dput(last); > /* last_name no longer match cache index */ > - if (fi->readdir_cache_idx >= 0) { > - fi->readdir_cache_idx = -1; > - fi->dir_release_count = 0; > + if (dfi->readdir_cache_idx >= 0) { > + dfi->readdir_cache_idx = -1; > + dfi->dir_release_count = 0; > } > } > return err; > @@ -290,17 +293,20 @@ static int __dcache_readdir(struct file *file, struct dir_context *ctx, > > static bool need_send_readdir(struct ceph_file_info *fi, loff_t pos) > { > - if (!fi->last_readdir) > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > + > + if (!dfi->last_readdir) > return true; > if (is_hash_order(pos)) > - return !ceph_frag_contains_value(fi->frag, fpos_hash(pos)); > + return !ceph_frag_contains_value(dfi->frag, fpos_hash(pos)); > else > - return fi->frag != fpos_frag(pos); > + return dfi->frag != fpos_frag(pos); > } > > static int ceph_readdir(struct file *file, struct dir_context *ctx) > { > struct ceph_file_info *fi = file->private_data; > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > struct inode *inode = file_inode(file); > struct ceph_inode_info *ci = ceph_inode(inode); > struct ceph_fs_client *fsc = ceph_inode_to_client(inode); > @@ -358,9 +364,9 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > CEPH_MDS_OP_LSSNAP : CEPH_MDS_OP_READDIR; > > /* discard old result, if any */ > - if (fi->last_readdir) { > - ceph_mdsc_put_request(fi->last_readdir); > - fi->last_readdir = NULL; > + if (dfi->last_readdir) { > + ceph_mdsc_put_request(dfi->last_readdir); > + dfi->last_readdir = NULL; > } > > if (is_hash_order(ctx->pos)) { > @@ -374,7 +380,7 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > } > > dout("readdir fetching %llx.%llx frag %x offset '%s'\n", > - ceph_vinop(inode), frag, fi->last_name); > + ceph_vinop(inode), frag, dfi->last_name); > req = ceph_mdsc_create_request(mdsc, op, USE_AUTH_MDS); > if (IS_ERR(req)) > return PTR_ERR(req); > @@ -390,8 +396,8 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > __set_bit(CEPH_MDS_R_DIRECT_IS_HASH, &req->r_req_flags); > req->r_inode_drop = CEPH_CAP_FILE_EXCL; > } > - if (fi->last_name) { > - req->r_path2 = kstrdup(fi->last_name, GFP_KERNEL); > + if (dfi->last_name) { > + req->r_path2 = kstrdup(dfi->last_name, GFP_KERNEL); > if (!req->r_path2) { > ceph_mdsc_put_request(req); > return -ENOMEM; > @@ -401,10 +407,10 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > cpu_to_le32(fpos_hash(ctx->pos)); > } > > - req->r_dir_release_cnt = fi->dir_release_count; > - req->r_dir_ordered_cnt = fi->dir_ordered_count; > - req->r_readdir_cache_idx = fi->readdir_cache_idx; > - req->r_readdir_offset = fi->next_offset; > + req->r_dir_release_cnt = dfi->dir_release_count; > + req->r_dir_ordered_cnt = dfi->dir_ordered_count; > + req->r_readdir_cache_idx = dfi->readdir_cache_idx; > + req->r_readdir_offset = dfi->next_offset; > req->r_args.readdir.frag = cpu_to_le32(frag); > req->r_args.readdir.flags = > cpu_to_le16(CEPH_READDIR_REPLY_BITFLAGS); > @@ -428,35 +434,35 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > if (le32_to_cpu(rinfo->dir_dir->frag) != frag) { > frag = le32_to_cpu(rinfo->dir_dir->frag); > if (!rinfo->hash_order) { > - fi->next_offset = req->r_readdir_offset; > + dfi->next_offset = req->r_readdir_offset; > /* adjust ctx->pos to beginning of frag */ > ctx->pos = ceph_make_fpos(frag, > - fi->next_offset, > + dfi->next_offset, > false); > } > } > > - fi->frag = frag; > - fi->last_readdir = req; > + dfi->frag = frag; > + dfi->last_readdir = req; > > if (test_bit(CEPH_MDS_R_DID_PREPOPULATE, &req->r_req_flags)) { > - fi->readdir_cache_idx = req->r_readdir_cache_idx; > - if (fi->readdir_cache_idx < 0) { > + dfi->readdir_cache_idx = req->r_readdir_cache_idx; > + if (dfi->readdir_cache_idx < 0) { > /* preclude from marking dir ordered */ > - fi->dir_ordered_count = 0; > + dfi->dir_ordered_count = 0; > } else if (ceph_frag_is_leftmost(frag) && > - fi->next_offset == 2) { > + dfi->next_offset == 2) { > /* note dir version at start of readdir so > * we can tell if any dentries get dropped */ > - fi->dir_release_count = req->r_dir_release_cnt; > - fi->dir_ordered_count = req->r_dir_ordered_cnt; > + dfi->dir_release_count = req->r_dir_release_cnt; > + dfi->dir_ordered_count = req->r_dir_ordered_cnt; > } > } else { > dout("readdir !did_prepopulate\n"); > /* disable readdir cache */ > - fi->readdir_cache_idx = -1; > + dfi->readdir_cache_idx = -1; > /* preclude from marking dir complete */ > - fi->dir_release_count = 0; > + dfi->dir_release_count = 0; > } > > /* note next offset and last dentry name */ > @@ -470,14 +476,14 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > if (err) > return err; > } else if (req->r_reply_info.dir_end) { > - fi->next_offset = 2; > + dfi->next_offset = 2; > /* keep last name */ > } > } > > - rinfo = &fi->last_readdir->r_reply_info; > + rinfo = &dfi->last_readdir->r_reply_info; > dout("readdir frag %x num %d pos %llx chunk first %llx\n", > - fi->frag, rinfo->dir_nr, ctx->pos, > + dfi->frag, rinfo->dir_nr, ctx->pos, > rinfo->dir_nr ? rinfo->dir_entries[0].offset : 0LL); > > i = 0; > @@ -521,27 +527,28 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > ctx->pos++; > } > > - ceph_mdsc_put_request(fi->last_readdir); > - fi->last_readdir = NULL; > + ceph_mdsc_put_request(dfi->last_readdir); > + dfi->last_readdir = NULL; > > - if (fi->next_offset > 2) { > - frag = fi->frag; > + if (dfi->next_offset > 2) { > + frag = dfi->frag; > goto more; > } > > /* more frags? */ > - if (!ceph_frag_is_rightmost(fi->frag)) { > - frag = ceph_frag_next(fi->frag); > + if (!ceph_frag_is_rightmost(dfi->frag)) { > + frag = ceph_frag_next(dfi->frag); > if (is_hash_order(ctx->pos)) { > loff_t new_pos = ceph_make_fpos(ceph_frag_value(frag), > - fi->next_offset, true); > + dfi->next_offset, true); > if (new_pos > ctx->pos) > ctx->pos = new_pos; > /* keep last_name */ > } else { > - ctx->pos = ceph_make_fpos(frag, fi->next_offset, false); > - kfree(fi->last_name); > - fi->last_name = NULL; > + ctx->pos = ceph_make_fpos(frag, dfi->next_offset, > + false); > + kfree(dfi->last_name); > + dfi->last_name = NULL; > } > dout("readdir next frag is %x\n", frag); > goto more; > @@ -553,20 +560,22 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > * were released during the whole readdir, and we should have > * the complete dir contents in our cache. > */ > - if (atomic64_read(&ci->i_release_count) == fi->dir_release_count) { > + if (atomic64_read(&ci->i_release_count) == > + dfi->dir_release_count) { > spin_lock(&ci->i_ceph_lock); > - if (fi->dir_ordered_count == atomic64_read(&ci->i_ordered_count)) { > + if (dfi->dir_ordered_count == > + atomic64_read(&ci->i_ordered_count)) { > dout(" marking %p complete and ordered\n", inode); > /* use i_size to track number of entries in > * readdir cache */ > - BUG_ON(fi->readdir_cache_idx < 0); > - i_size_write(inode, fi->readdir_cache_idx * > + BUG_ON(dfi->readdir_cache_idx < 0); > + i_size_write(inode, dfi->readdir_cache_idx * > sizeof(struct dentry*)); > } else { > dout(" marking %p complete\n", inode); > } > - __ceph_dir_set_complete(ci, fi->dir_release_count, > - fi->dir_ordered_count); > + __ceph_dir_set_complete(ci, dfi->dir_release_count, > + dfi->dir_ordered_count); > spin_unlock(&ci->i_ceph_lock); > } > > @@ -576,15 +585,17 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx) > > static void reset_readdir(struct ceph_file_info *fi) > { > - if (fi->last_readdir) { > - ceph_mdsc_put_request(fi->last_readdir); > - fi->last_readdir = NULL; > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > + > + if (dfi->last_readdir) { > + ceph_mdsc_put_request(dfi->last_readdir); > + dfi->last_readdir = NULL; > } > - kfree(fi->last_name); > - fi->last_name = NULL; > - fi->dir_release_count = 0; > - fi->readdir_cache_idx = -1; > - fi->next_offset = 2; /* compensate for . and .. */ > + kfree(dfi->last_name); > + dfi->last_name = NULL; > + dfi->dir_release_count = 0; > + dfi->readdir_cache_idx = -1; > + dfi->next_offset = 2; /* compensate for . and .. */ > fi->flags &= ~CEPH_F_ATEND; > } > > @@ -594,6 +605,7 @@ static void reset_readdir(struct ceph_file_info *fi) > */ > static bool need_reset_readdir(struct ceph_file_info *fi, loff_t new_pos) > { > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > struct ceph_mds_reply_info_parsed *rinfo; > loff_t chunk_offset; > if (new_pos == 0) > @@ -601,10 +613,10 @@ static bool need_reset_readdir(struct ceph_file_info *fi, loff_t new_pos) > if (is_hash_order(new_pos)) { > /* no need to reset last_name for a forward seek when > * dentries are sotred in hash order */ > - } else if (fi->frag != fpos_frag(new_pos)) { > + } else if (dfi->frag != fpos_frag(new_pos)) { > return true; > } > - rinfo = fi->last_readdir ? &fi->last_readdir->r_reply_info : NULL; > + rinfo = dfi->last_readdir ? &dfi->last_readdir->r_reply_info : NULL; > if (!rinfo || !rinfo->dir_nr) > return true; > chunk_offset = rinfo->dir_entries[0].offset; > @@ -615,6 +627,7 @@ static bool need_reset_readdir(struct ceph_file_info *fi, loff_t new_pos) > static loff_t ceph_dir_llseek(struct file *file, loff_t offset, int whence) > { > struct ceph_file_info *fi = file->private_data; > + struct ceph_dir_file_info *dfi = CEPH_DFI(fi); > struct inode *inode = file->f_mapping->host; > loff_t retval; > > @@ -638,8 +651,8 @@ static loff_t ceph_dir_llseek(struct file *file, loff_t offset, int whence) > } else if (is_hash_order(offset) && offset > file->f_pos) { > /* for hash offset, we don't know if a forward seek > * is within same frag */ > - fi->dir_release_count = 0; > - fi->readdir_cache_idx = -1; > + dfi->dir_release_count = 0; > + dfi->readdir_cache_idx = -1; > } > > if (offset != file->f_pos) { > @@ -1370,7 +1383,7 @@ static void ceph_d_prune(struct dentry *dentry) > static ssize_t ceph_read_dir(struct file *file, char __user *buf, size_t size, > loff_t *ppos) > { > - struct ceph_file_info *fi = file->private_data; > + struct ceph_dir_file_info *dfi = file->private_data; > struct inode *inode = file_inode(file); > struct ceph_inode_info *ci = ceph_inode(inode); > int left; > @@ -1379,12 +1392,12 @@ static ssize_t ceph_read_dir(struct file *file, char __user *buf, size_t size, > if (!ceph_test_mount_opt(ceph_sb_to_client(inode->i_sb), DIRSTAT)) > return -EISDIR; > > - if (!fi->dir_info) { > - fi->dir_info = kmalloc(bufsize, GFP_KERNEL); > - if (!fi->dir_info) > + if (!dfi->dir_info) { > + dfi->dir_info = kmalloc(bufsize, GFP_KERNEL); > + if (!dfi->dir_info) > return -ENOMEM; > - fi->dir_info_len = > - snprintf(fi->dir_info, bufsize, > + dfi->dir_info_len = > + snprintf(dfi->dir_info, bufsize, > "entries: %20lld\n" > " files: %20lld\n" > " subdirs: %20lld\n" > @@ -1404,10 +1417,10 @@ static ssize_t ceph_read_dir(struct file *file, char __user *buf, size_t size, > (long)ci->i_rctime.tv_nsec); > } > > - if (*ppos >= fi->dir_info_len) > + if (*ppos >= dfi->dir_info_len) > return 0; > - size = min_t(unsigned int, size, fi->dir_info_len-*ppos); > - left = copy_to_user(buf, fi->dir_info + *ppos, size); > + size = min_t(unsigned int, size, dfi->dir_info_len-*ppos); > + left = copy_to_user(buf, dfi->dir_info + *ppos, size); > if (left == size) > return -EFAULT; > *ppos += (size - left); > diff --git a/fs/ceph/file.c b/fs/ceph/file.c > index 2de7031..c782beb 100644 > --- a/fs/ceph/file.c > +++ b/fs/ceph/file.c > @@ -95,36 +95,61 @@ static __le32 ceph_flags_sys2wire(u32 flags) > return req; > } > > +static int ceph_init_file_info(struct inode *inode, struct file *file, > + int fmode, bool isdir) > +{ > + struct ceph_file_info *fi; > + struct ceph_dir_file_info *dfi; > + > + dout("%s %p %p 0%o (%s)\n", __func__, inode, file, > + inode->i_mode, isdir ? "dir" : "regular"); > + BUG_ON(inode->i_fop->release != ceph_release); > + > + if (isdir) { > + dfi = kmem_cache_zalloc(ceph_dir_file_cachep, GFP_KERNEL); > + if (!dfi) { > + ceph_put_fmode(ceph_inode(inode), fmode); /* clean up */ > + return -ENOMEM; > + } > + > + file->private_data = dfi; > + fi = &dfi->file_info; > + dfi->next_offset = 2; > + dfi->readdir_cache_idx = -1; > + } else { > + fi = kmem_cache_zalloc(ceph_file_cachep, GFP_KERNEL); > + if (!fi) { > + ceph_put_fmode(ceph_inode(inode), fmode); /* clean up */ > + return -ENOMEM; > + } > + > + file->private_data = fi; > + ceph_fscache_register_inode_cookie(inode); > + ceph_fscache_file_set_cookie(inode, file); > + } > + > + fi->fmode = fmode; > + spin_lock_init(&fi->rw_contexts_lock); > + INIT_LIST_HEAD(&fi->rw_contexts); > + > + return 0; > +} > + > /* > * initialize private struct file data. > * if we fail, clean up by dropping fmode reference on the ceph_inode > */ > static int ceph_init_file(struct inode *inode, struct file *file, int fmode) > { > - struct ceph_file_info *fi; > int ret = 0; > > switch (inode->i_mode & S_IFMT) { > case S_IFREG: > - ceph_fscache_register_inode_cookie(inode); > - ceph_fscache_file_set_cookie(inode, file); > case S_IFDIR: > - dout("init_file %p %p 0%o (regular)\n", inode, file, > - inode->i_mode); > - fi = kmem_cache_zalloc(ceph_file_cachep, GFP_KERNEL); > - if (!fi) { > - ceph_put_fmode(ceph_inode(inode), fmode); /* clean up */ > - return -ENOMEM; > - } > - fi->fmode = fmode; > - > - spin_lock_init(&fi->rw_contexts_lock); > - INIT_LIST_HEAD(&fi->rw_contexts); > - > - fi->next_offset = 2; > - fi->readdir_cache_idx = -1; > - file->private_data = fi; > - BUG_ON(inode->i_fop->release != ceph_release); > + ret = ceph_init_file_info(inode, file, fmode, > + S_ISDIR(inode->i_mode)); > + if (ret) > + return ret; > break; > > case S_IFLNK: > @@ -399,15 +424,27 @@ int ceph_release(struct inode *inode, struct file *file) > { > struct ceph_inode_info *ci = ceph_inode(inode); > struct ceph_file_info *fi = file->private_data; > + struct ceph_dir_file_info *dfi; > > - dout("release inode %p file %p\n", inode, file); > - ceph_put_fmode(ci, fi->fmode); > - if (fi->last_readdir) > - ceph_mdsc_put_request(fi->last_readdir); > - kfree(fi->last_name); > - kfree(fi->dir_info); > - WARN_ON(!list_empty(&fi->rw_contexts)); > - kmem_cache_free(ceph_file_cachep, fi); > + if (S_ISDIR(inode->i_mode)) { > + dout("release inode %p dir file %p\n", inode, file); > + WARN_ON(!list_empty(&fi->rw_contexts)); > + > + dfi = CEPH_DFI(fi); > + ceph_put_fmode(ci, fi->fmode); > + > + if (dfi->last_readdir) > + ceph_mdsc_put_request(dfi->last_readdir); > + kfree(dfi->last_name); > + kfree(dfi->dir_info); > + kmem_cache_free(ceph_dir_file_cachep, dfi); > + } else { > + dout("release inode %p regular file %p\n", inode, file); > + WARN_ON(!list_empty(&fi->rw_contexts)); > + > + ceph_put_fmode(ci, fi->fmode); > + kmem_cache_free(ceph_file_cachep, fi); > + } > > /* wake up anyone waiting for caps on this inode */ > wake_up_all(&ci->i_cap_wq); > diff --git a/fs/ceph/super.c b/fs/ceph/super.c > index 880e83e..286e8db 100644 > --- a/fs/ceph/super.c > +++ b/fs/ceph/super.c > @@ -699,6 +699,7 @@ static void destroy_fs_client(struct ceph_fs_client *fsc) > struct kmem_cache *ceph_cap_flush_cachep; > struct kmem_cache *ceph_dentry_cachep; > struct kmem_cache *ceph_file_cachep; > +struct kmem_cache *ceph_dir_file_cachep; > > static void ceph_inode_init_once(void *foo) > { > @@ -735,6 +736,10 @@ static int __init init_caches(void) > if (!ceph_file_cachep) > goto bad_file; > > + ceph_dir_file_cachep = KMEM_CACHE(ceph_dir_file_info, SLAB_MEM_SPREAD); > + if (!ceph_dir_file_cachep) > + goto bad_dir_file; > + > error = ceph_fscache_register(); > if (error) > goto bad_fscache; > @@ -742,6 +747,8 @@ static int __init init_caches(void) > return 0; > > bad_fscache: > + kmem_cache_destroy(ceph_dir_file_cachep); > +bad_dir_file: > kmem_cache_destroy(ceph_file_cachep); > bad_file: > kmem_cache_destroy(ceph_dentry_cachep); > @@ -767,6 +774,7 @@ static void destroy_caches(void) > kmem_cache_destroy(ceph_cap_flush_cachep); > kmem_cache_destroy(ceph_dentry_cachep); > kmem_cache_destroy(ceph_file_cachep); > + kmem_cache_destroy(ceph_dir_file_cachep); > > ceph_fscache_unregister(); > } > diff --git a/fs/ceph/super.h b/fs/ceph/super.h > index 4e619de..2e5ff4a 100644 > --- a/fs/ceph/super.h > +++ b/fs/ceph/super.h > @@ -678,6 +678,10 @@ struct ceph_file_info { > > spinlock_t rw_contexts_lock; > struct list_head rw_contexts; > +}; > + > +struct ceph_dir_file_info { > + struct ceph_file_info file_info; > > /* readdir: position within the dir */ > u32 frag; > @@ -695,6 +699,11 @@ struct ceph_file_info { > int dir_info_len; > }; > > +static inline struct ceph_dir_file_info *CEPH_DFI(struct ceph_file_info *fi) > +{ > + return container_of(fi, struct ceph_dir_file_info, file_info); > +} > + > struct ceph_rw_context { > struct list_head list; > struct task_struct *thread; > diff --git a/include/linux/ceph/libceph.h b/include/linux/ceph/libceph.h > index c2ec44c..49c93b9 100644 > --- a/include/linux/ceph/libceph.h > +++ b/include/linux/ceph/libceph.h > @@ -262,6 +262,7 @@ static inline int calc_pages_for(u64 off, u64 len) > extern struct kmem_cache *ceph_cap_flush_cachep; > extern struct kmem_cache *ceph_dentry_cachep; > extern struct kmem_cache *ceph_file_cachep; > +extern struct kmem_cache *ceph_dir_file_cachep; > > /* ceph_common.c */ > extern bool libceph_compatible(void *data); > -- > 1.8.3.1 > > -- > To unsubscribe from this list: send the line "unsubscribe ceph-devel" in > the body of a message to majordomo@xxxxxxxxxxxxxxx > More majordomo info at http://vger.kernel.org/majordomo-info.html -- To unsubscribe from this list: send the line "unsubscribe ceph-devel" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html