Beata Michalska <b.michalska@xxxxxxxxxxx> writes: > Introduce configurable generic interface for file > system-wide event notifications, to provide file > systems with a common way of reporting any potential > issues as they emerge. > > The notifications are to be issued through generic > netlink interface by newly introduced multicast group. > > Threshold notifications have been included, allowing > triggering an event whenever the amount of free space drops > below a certain level - or levels to be more precise as two > of them are being supported: the lower and the upper range. > The notifications work both ways: once the threshold level > has been reached, an event shall be generated whenever > the number of available blocks goes up again re-activating > the threshold. > > The interface has been exposed through a vfs. Once mounted, > it serves as an entry point for the set-up where one can > register for particular file system events. > > Signed-off-by: Beata Michalska <b.michalska@xxxxxxxxxxx> > --- > Documentation/filesystems/events.txt | 232 ++++++++++ > fs/Kconfig | 2 + > fs/Makefile | 1 + > fs/events/Kconfig | 7 + > fs/events/Makefile | 5 + > fs/events/fs_event.c | 809 ++++++++++++++++++++++++++++++++++ > fs/events/fs_event.h | 22 + > fs/events/fs_event_netlink.c | 104 +++++ > fs/namespace.c | 1 + > include/linux/fs.h | 6 +- > include/linux/fs_event.h | 72 +++ > include/uapi/linux/Kbuild | 1 + > include/uapi/linux/fs_event.h | 58 +++ > 13 files changed, 1319 insertions(+), 1 deletion(-) > create mode 100644 Documentation/filesystems/events.txt > create mode 100644 fs/events/Kconfig > create mode 100644 fs/events/Makefile > create mode 100644 fs/events/fs_event.c > create mode 100644 fs/events/fs_event.h > create mode 100644 fs/events/fs_event_netlink.c > create mode 100644 include/linux/fs_event.h > create mode 100644 include/uapi/linux/fs_event.h > > diff --git a/Documentation/filesystems/events.txt b/Documentation/filesystems/events.txt > new file mode 100644 > index 0000000..c2e6227 > --- /dev/null > +++ b/Documentation/filesystems/events.txt > @@ -0,0 +1,232 @@ > + > + Generic file system event notification interface > + > +Document created 23 April 2015 by Beata Michalska <b.michalska@xxxxxxxxxxx> > + > +1. The reason behind: > +===================== > + > +There are many corner cases when things might get messy with the filesystems. > +And it is not always obvious what and when went wrong. Sometimes you might > +get some subtle hints that there is something going on - but by the time > +you realise it, it might be too late as you are already out-of-space > +or the filesystem has been remounted as read-only (i.e.). The generic > +interface for the filesystem events fills the gap by providing a rather > +easy way of real-time notifications triggered whenever something interesting > +happens, allowing filesystems to report events in a common way, as they occur. > + > +2. How does it work: > +==================== > + > +The interface itself has been exposed as fstrace-type Virtual File System, > +primarily to ease the process of setting up the configuration for the > +notifications. So for starters, it needs to get mounted (obviously): > + > + mount -t fstrace none /sys/fs/events > + > +This will unveil the single fstrace filesystem entry - the 'config' file, > +through which the notification are being set-up. > + > +Activating notifications for particular filesystem is as straightforward > +as writing into the 'config' file. Note that by default all events, despite > +the actual filesystem type, are being disregarded. > + > +Synopsis of config: > +------------------ > + > + MOUNT EVENT_TYPE [L1] [L2] > + > + MOUNT : the filesystem's mount point > + EVENT_TYPE : event types - currently two of them are being supported: > + > + * generic events ("G") covering most common warnings > + and errors that might be reported by any filesystem; > + this option does not take any arguments; > + > + * threshold notifications ("T") - events sent whenever > + the amount of available space drops below certain level; > + it is possible to specify two threshold levels though > + only one is required to properly setup the notifications; > + as those refer to the number of available blocks, the lower > + level [L1] needs to be higher than the upper one [L2] > + > +Sample request could look like the following: > + > + echo /sample/mount/point G T 710000 500000 > /sys/fs/events/config > + > +Multiple request might be specified provided they are separated with semicolon. > + > +The configuration itself might be modified at any time. One can add/remove > +particular event types for given fielsystem, modify the threshold levels, > +and remove single or all entries from the 'config' file. > + > + - Adding new event type: > + > + $ echo MOUNT EVENT_TYPE > /sys/fs/events/config > + > +(Note that is is enough to provide the event type to be enabled without > +the already set ones.) > + > + - Removing event type: > + > + $ echo '!MOUNT EVENT_TYPE' > /sys/fs/events/config > + > + - Updating threshold limits: > + > + $ echo MOUNT T L1 L2 > /sys/fs/events/config > + > + - Removing single entry: > + > + $ echo '!MOUNT' > /sys/fs/events/config > + > + - Removing all entries: > + > + $ echo > /sys/fs/events/config > + > +Reading the file will list all registered entries with their current set-up > +along with some additional info like the filesystem type and the backing device > +name if available. > + > +Final, though a very important note on the configuration: when and if the > +actual events are being triggered falls way beyond the scope of the generic > +filesystem events interface. It is up to a particular filesystem > +implementation which events are to be supported - if any at all. So if > +given filesystem does not support the event notifications, an attempt to > +enable those through 'config' file will fail. > + > + > +3. The generic netlink interface support: > +========================================= > + > +Whenever an event notification is triggered (by given filesystem) the current > +configuration is being validated to decide whether a userpsace notification > +should be launched. If there has been no request (in a mean of 'config' file > +entry) for given event, one will be silently disregarded. If, on the other > +hand, someone is 'watching' given filesystem for specific events, a generic > +netlink message will be sent. A dedicated multicast group has been provided > +solely for this purpose so in order to receive such notifications, one should > +subscribe to this new multicast group. As for now only the init network > +namespace is being supported. > + > +3.1 Message format > + > +The FS_NL_C_EVENT shall be stored within the generic netlink message header > +as the command field. The message payload will provide more detailed info: > +the backing device major and minor numbers, the event code and the id of > +the process which action led to the event occurrence. In case of threshold > +notifications, the current number of available blocks will be included > +in the payload as well. > + > + > + 0 1 2 3 > + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 > + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ > + | NETLINK MESSAGE HEADER | > + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ > + | GENERIC NETLINK MESSAGE HEADER | > + | (with FS_NL_C_EVENT as genlmsghdr cdm field) | > + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ > + | Optional user specific message header | > + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ > + | GENERIC MESSAGE PAYLOAD: | > + +---------------------------------------------------------------+ > + | FS_NL_A_EVENT_ID (NLA_U32) | > + +---------------------------------------------------------------+ > + | FS_NL_A_DEV_MAJOR (NLA_U32) | > + +---------------------------------------------------------------+ > + | FS_NL_A_DEV_MINOR (NLA_U32) | > + +---------------------------------------------------------------+ > + | FS_NL_A_CAUSED_ID (NLA_U32) | > + +---------------------------------------------------------------+ > + | FS_NL_A_DATA (NLA_U64) | > + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ > + > + > +The above figure is based on: > + http://www.linuxfoundation.org/collaborate/workgroups/networking/generic_netlink_howto#Message_Format > + > + > +4. API Reference: > +================= > + > + 4.1 Generic file system event interface data & operations > + > + #include <linux/fs_event.h> > + > + struct fs_trace_info { > + void __rcu *e_priv /* READ ONLY */ > + unsigned int events_cap_mask; /* Supported notifications */ > + const struct fs_trace_operations *ops; > + }; > + > + struct fs_trace_operations { > + void (*query)(struct super_block *, u64 *); > + }; > + > + In order to get the fireworks and stuff, each filesystem needs to setup > + the events_cap_mask field of the fs_trace_info structure, which has been > + embedded within the super_block structure. This should reflect the type of > + events the filesystem wants to support. In case of threshold notifications, > + apart from setting the FS_EVENT_THRESH flag, the 'query' callback should > + be provided as this enables the events interface to get the up-to-date > + state of the number of available blocks whenever those notifications are > + being requested. > + > + The 'e_priv' field of the fs_trace_info structure should be completely ignored > + as it's for INTERNAL USE ONLY. So don't even think of messing with it, if you > + do not want to get yourself into some real trouble. If still, you are tempted > + to do so - feel free, it's gonna be pure fun. Consider yourself warned. > + > + > + 4.2 Event notification: > + > + #include <linux/fs_event.h> > + void fs_event_notify(struct super_block *sb, unsigned int event_id); > + > + Notify the generic FS event interface of an occurring event. > + This shall be used by any file system that wishes to inform any potential > + listeners/watchers of a particular event. > + - sb: the filesystem's super block > + - event_id: an event identifier > + > + 4.3 Threshold notifications: > + > + #include <linux/fs_event.h> > + void fs_event_alloc_space(struct super_block *sb, u64 ncount); > + void fs_event_free_space(struct super_block *sb, u64 ncount); > + > + Each filesystme supporting the threshold notifications should call > + fs_event_alloc_space/fs_event_free_space respectively whenever the > + amount of available blocks changes. > + - sb: the filesystem's super block > + - ncount: number of blocks being acquired/released > + > + Note that to properly handle the threshold notifications the fs events > + interface needs to be kept up to date by the filesystems. Each should > + register fs_trace_operations to enable querying the current number of > + available blocks. > + > + 4.4 Sending message through generic netlink interface > + > + #include <linux/fs_event.h> > + > + int fs_netlink_send_event(size_t size, unsigned int event_id, > + int (*compose_msg)(struct sk_buff *skb, void *data), void *cbdata); > + > + Although the fs event interface is fully responsible for sending the messages > + over the netlink, filesystems might use the FS_EVENT multicast group to send > + their own custom messages. > + - size: the size of the message payload > + - event_id: the event identifier > + - compose_msg: a callback responsible for filling-in the message payload > + - cbdata: message custom data > + > + Calling fs_netlink_send_event will result in a message being sent by > + the FS_EVENT multicast group. Note that the body of the message should be > + prepared (set-up )by the caller - through compose_msg callback. The message's > + sk_buff will be allocated on behalf of the caller (thus the size parameter). > + The compose_msg should only fill the payload with proper data. Unless > + the event id is specified as FS_EVENT_NONE, it's value shall be added > + to the payload prior to calling the compose_msg. > + > + > diff --git a/fs/Kconfig b/fs/Kconfig > index ec35851..a89e678 100644 > --- a/fs/Kconfig > +++ b/fs/Kconfig > @@ -69,6 +69,8 @@ config FILE_LOCKING > for filesystems like NFS and for the flock() system > call. Disabling this option saves about 11k. > > +source "fs/events/Kconfig" > + > source "fs/notify/Kconfig" > > source "fs/quota/Kconfig" > diff --git a/fs/Makefile b/fs/Makefile > index a88ac48..bcb3048 100644 > --- a/fs/Makefile > +++ b/fs/Makefile > @@ -126,3 +126,4 @@ obj-y += exofs/ # Multiple modules > obj-$(CONFIG_CEPH_FS) += ceph/ > obj-$(CONFIG_PSTORE) += pstore/ > obj-$(CONFIG_EFIVAR_FS) += efivarfs/ > +obj-$(CONFIG_FS_EVENTS) += events/ > diff --git a/fs/events/Kconfig b/fs/events/Kconfig > new file mode 100644 > index 0000000..1c60195 > --- /dev/null > +++ b/fs/events/Kconfig > @@ -0,0 +1,7 @@ > +# Generic Files System events interface > +config FS_EVENTS > + bool "Generic filesystem events" > + select NET > + default y > + help > + Enable generic filesystem events interface > diff --git a/fs/events/Makefile b/fs/events/Makefile > new file mode 100644 > index 0000000..9c98337 > --- /dev/null > +++ b/fs/events/Makefile > @@ -0,0 +1,5 @@ > +# > +# Makefile for the Linux Generic File System Event Interface > +# > + > +obj-y := fs_event.o fs_event_netlink.o > diff --git a/fs/events/fs_event.c b/fs/events/fs_event.c > new file mode 100644 > index 0000000..1037311 > --- /dev/null > +++ b/fs/events/fs_event.c > @@ -0,0 +1,809 @@ > +/* > + * Generic File System Evens Interface > + * > + * Copyright(c) 2015 Samsung Electronics. All rights reserved. > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2. > + * > + * The full GNU General Public License is included in this distribution in the > + * file called COPYING. > + * > + * This program is distributed in the hope that it will be useful, but WITHOUT > + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or > + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for > + * more details. > + */ > +#include <linux/fs.h> > +#include <linux/module.h> > +#include <linux/mount.h> > +#include <linux/namei.h> > +#include <linux/nsproxy.h> > +#include <linux/parser.h> > +#include <linux/seq_file.h> > +#include <linux/slab.h> > +#include <linux/rcupdate.h> > +#include <net/genetlink.h> > +#include "../pnode.h" > +#include "fs_event.h" > + > +static LIST_HEAD(fs_trace_list); > +static DEFINE_MUTEX(fs_trace_lock); > + > +static struct kmem_cache *fs_trace_cachep __read_mostly; > + > +static atomic_t stray_traces = ATOMIC_INIT(0); > +static DECLARE_WAIT_QUEUE_HEAD(trace_wq); > +/* > + * Threshold notification state bits. > + * Note the reverse as this refers to the number > + * of available blocks. > + */ > +#define THRESH_LR_BELOW 0x0001 /* Falling below the lower range */ > +#define THRESH_LR_BEYOND 0x0002 > +#define THRESH_UR_BELOW 0x0004 > +#define THRESH_UR_BEYOND 0x0008 /* Going beyond the upper range */ > + > +#define THRESH_LR_ON (THRESH_LR_BELOW | THRESH_LR_BEYOND) > +#define THRESH_UR_ON (THRESH_UR_BELOW | THRESH_UR_BEYOND) > + > +#define FS_TRACE_ADD 0x100000 > + > +struct fs_trace_entry { > + struct kref count; > + atomic_t active; > + struct super_block *sb; > + unsigned int notify; > + struct path mnt_path; > + struct list_head node; > + > + struct fs_event_thresh { > + u64 avail_space; > + u64 lrange; > + u64 urange; > + unsigned int state; > + } th; > + struct rcu_head rcu_head; > + spinlock_t lock; > +}; > + > +static const match_table_t fs_etypes = { > + { FS_EVENT_GENERIC, "G" }, > + { FS_EVENT_THRESH, "T" }, > + { 0, NULL }, > +}; > + > +static inline int fs_trace_query_data(struct super_block *sb, > + struct fs_trace_entry *en) > +{ > + if (sb->s_etrace.ops && sb->s_etrace.ops->query) { > + sb->s_etrace.ops->query(sb, &en->th.avail_space); > + return 0; > + } > + > + return -EINVAL; > +} > + > +static inline void fs_trace_entry_free(struct fs_trace_entry *en) > +{ > + kmem_cache_free(fs_trace_cachep, en); > +} > + > +static void fs_destroy_trace_entry(struct kref *en_ref) > +{ > + struct fs_trace_entry *en = container_of(en_ref, > + struct fs_trace_entry, count); > + > + /* Last reference has been dropped */ > + fs_trace_entry_free(en); > + atomic_dec(&stray_traces); > +} > + > +static void fs_trace_entry_put(struct fs_trace_entry *en) > +{ > + kref_put(&en->count, fs_destroy_trace_entry); > +} > + > +static void fs_release_trace_entry(struct rcu_head *rcu_head) > +{ > + struct fs_trace_entry *en = container_of(rcu_head, > + struct fs_trace_entry, > + rcu_head); > + /* > + * As opposed to typical reference drop, this one is being > + * called from the rcu callback. This is to make sure all > + * readers have managed to safely grab the reference before > + * the change to rcu pointer is visible to all and before > + * the reference is dropped here. > + */ > + fs_trace_entry_put(en); > +} > + > +static void fs_drop_trace_entry(struct fs_trace_entry *en) > +{ > + struct super_block *sb; > + > + lockdep_assert_held(&fs_trace_lock); > + /* > + * The trace entry might have already been removed > + * from the list of active traces with the proper > + * ref drop, though it was still in use handling > + * one of the fs events. This means that the object > + * has been already scheduled for being released. > + * So leave... > + */ > + > + if (!atomic_add_unless(&en->active, -1, 0)) > + return; > + /* > + * At this point the trace entry is being marked as inactive > + * so no new references will be allowed. > + * Still it might be floating around somewhere > + * so drop the reference when the rcu readers are done. > + */ > + spin_lock(&en->lock); > + list_del(&en->node); > + sb = en->sb; > + en->sb = NULL; > + spin_unlock(&en->lock); > + > + rcu_assign_pointer(sb->s_etrace.e_priv, NULL); > + call_rcu(&en->rcu_head, fs_release_trace_entry); > + /* It's safe now to drop the reference to the super */ > + deactivate_super(sb); > + atomic_inc(&stray_traces); > +} > + > +static inline > +struct fs_trace_entry *fs_trace_entry_get(struct fs_trace_entry *en) > +{ > + if (en) { > + if (!kref_get_unless_zero(&en->count)) > + return NULL; > + /* Don't allow referencing inactive object */ > + if (!atomic_read(&en->active)) { > + fs_trace_entry_put(en); > + return NULL; > + } > + } > + return en; > +} > + > +static struct fs_trace_entry *fs_trace_entry_get_rcu(struct super_block *sb) > +{ > + struct fs_trace_entry *en; > + > + if (!sb) > + return NULL; > + > + rcu_read_lock(); > + en = rcu_dereference(sb->s_etrace.e_priv); > + en = fs_trace_entry_get(en); > + rcu_read_unlock(); > + > + return en; > +} > + > +static int fs_remove_trace_entry(struct super_block *sb) > +{ > + struct fs_trace_entry *en; > + > + en = fs_trace_entry_get_rcu(sb); > + if (!en) > + return -EINVAL; > + > + mutex_lock(&fs_trace_lock); > + fs_drop_trace_entry(en); > + mutex_unlock(&fs_trace_lock); > + fs_trace_entry_put(en); > + return 0; > +} > + > +static void fs_remove_all_traces(void) > +{ > + struct fs_trace_entry *en, *guard; > + > + mutex_lock(&fs_trace_lock); > + list_for_each_entry_safe(en, guard, &fs_trace_list, node) > + fs_drop_trace_entry(en); > + mutex_unlock(&fs_trace_lock); > +} > + > +static int create_common_msg(struct sk_buff *skb, void *data) > +{ > + struct fs_trace_entry *en = (struct fs_trace_entry *)data; > + struct super_block *sb = en->sb; > + > + if (nla_put_u32(skb, FS_NL_A_DEV_MAJOR, MAJOR(sb->s_dev)) > + || nla_put_u32(skb, FS_NL_A_DEV_MINOR, MINOR(sb->s_dev))) > + return -EINVAL; What about diskless(nfs,cifs,etc) filesystem? btrfs also has no valid sb->s_dev > + > + if (nla_put_u64(skb, FS_NL_A_CAUSED_ID, pid_vnr(task_pid(current)))) > + return -EINVAL; > + > + return 0; > +} > + > +static int create_thresh_msg(struct sk_buff *skb, void *data) > +{ > + struct fs_trace_entry *en = (struct fs_trace_entry *)data; > + int ret; > + > + ret = create_common_msg(skb, data); > + if (!ret) > + ret = nla_put_u64(skb, FS_NL_A_DATA, en->th.avail_space); > + return ret; > +} > + > +static void fs_event_send(struct fs_trace_entry *en, unsigned int event_id) > +{ > + size_t size = nla_total_size(sizeof(u32)) * 2 + > + nla_total_size(sizeof(u64)); > + > + fs_netlink_send_event(size, event_id, create_common_msg, en); > +} > + > +static void fs_event_send_thresh(struct fs_trace_entry *en, > + unsigned int event_id) > +{ > + size_t size = nla_total_size(sizeof(u32)) * 2 + > + nla_total_size(sizeof(u64)) * 2; > + > + fs_netlink_send_event(size, event_id, create_thresh_msg, en); > +} > + > +void fs_event_notify(struct super_block *sb, unsigned int event_id) > +{ > + struct fs_trace_entry *en; > + > + en = fs_trace_entry_get_rcu(sb); > + if (!en) > + return; > + > + spin_lock(&en->lock); > + if (atomic_read(&en->active) && (en->notify & FS_EVENT_GENERIC)) > + fs_event_send(en, event_id); > + spin_unlock(&en->lock); > + fs_trace_entry_put(en); > +} > +EXPORT_SYMBOL(fs_event_notify); > + > +void fs_event_alloc_space(struct super_block *sb, u64 ncount) > +{ > + struct fs_trace_entry *en; > + s64 count; > + > + en = fs_trace_entry_get_rcu(sb); > + if (!en) > + return; > + > + spin_lock(&en->lock); > + > + if (!atomic_read(&en->active) || !(en->notify & FS_EVENT_THRESH)) > + goto leave; > + /* > + * we shouldn't drop below 0 here, > + * unless there is a sync issue somewhere (?) > + */ > + count = en->th.avail_space - ncount; > + en->th.avail_space = count < 0 ? 0 : count; > + > + if (en->th.avail_space > en->th.lrange) > + /* Not 'even' close - leave */ > + goto leave; > + > + if (en->th.avail_space > en->th.urange) { > + /* Close enough - the lower range has been reached */ > + if (!(en->th.state & THRESH_LR_BEYOND)) { > + /* Send notification */ > + fs_event_send_thresh(en, FS_THR_LRBELOW); > + en->th.state &= ~THRESH_LR_BELOW; > + en->th.state |= THRESH_LR_BEYOND; > + } > + goto leave; > + } > + if (!(en->th.state & THRESH_UR_BEYOND)) { > + fs_event_send_thresh(en, FS_THR_URBELOW); > + en->th.state &= ~THRESH_UR_BELOW; > + en->th.state |= THRESH_UR_BEYOND; > + } > + > +leave: > + spin_unlock(&en->lock); > + fs_trace_entry_put(en); > +} > +EXPORT_SYMBOL(fs_event_alloc_space); > + > +void fs_event_free_space(struct super_block *sb, u64 ncount) > +{ > + struct fs_trace_entry *en; > + > + en = fs_trace_entry_get_rcu(sb); > + if (!en) > + return; > + > + spin_lock(&en->lock); > + > + if (!atomic_read(&en->active) || !(en->notify & FS_EVENT_THRESH)) > + goto leave; > + > + en->th.avail_space += ncount; > + > + if (en->th.avail_space > en->th.lrange) { > + if (!(en->th.state & THRESH_LR_BELOW) > + && en->th.state & THRESH_LR_BEYOND) { > + /* Send notification */ > + fs_event_send_thresh(en, FS_THR_LRABOVE); > + en->th.state &= ~(THRESH_LR_BEYOND|THRESH_UR_BEYOND); > + en->th.state |= THRESH_LR_BELOW; > + goto leave; > + } > + } > + if (en->th.avail_space > en->th.urange) { > + if (!(en->th.state & THRESH_UR_BELOW) > + && en->th.state & THRESH_UR_BEYOND) { > + /* Notify */ > + fs_event_send_thresh(en, FS_THR_URABOVE); > + en->th.state &= ~THRESH_UR_BEYOND; > + en->th.state |= THRESH_UR_BELOW; > + } > + } > +leave: > + spin_unlock(&en->lock); > + fs_trace_entry_put(en); > +} > +EXPORT_SYMBOL(fs_event_free_space); > + > +void fs_event_mount_dropped(struct vfsmount *mnt) > +{ > + /* > + * The mount is dropped but the super might not get released > + * at once so there is very small chance some notifications > + * will come through. > + * Note that the mount being dropped here might belong to a different > + * namespace - if this is the case, just ignore it. > + */ > + struct fs_trace_entry *en = fs_trace_entry_get_rcu(mnt->mnt_sb); > + struct vfsmount *en_mnt; > + > + if (!en || !atomic_read(&en->active)) > + return; > + /* > + * The entry once set, does not change the mountpoint it's being > + * pinned to, so no need to take the lock here. > + */ > + en_mnt = en->mnt_path.mnt; > + if (!(real_mount(mnt)->mnt_ns != (real_mount(en_mnt))->mnt_ns)) > + fs_remove_trace_entry(mnt->mnt_sb); > + fs_trace_entry_put(en); > +} > + > +static int fs_new_trace_entry(struct path *path, struct fs_event_thresh *thresh, > + unsigned int nmask) > +{ > + struct fs_trace_entry *en; > + struct super_block *sb; > + struct mount *r_mnt; > + > + en = kmem_cache_zalloc(fs_trace_cachep, GFP_KERNEL); > + if (unlikely(!en)) > + return -ENOMEM; > + /* > + * Note that no reference is being taken here for the path as it would > + * make the unmount unnecessarily puzzling (due to an extra 'valid' > + * reference for the mnt). > + * This is *rather* safe as the notification on mount being dropped > + * will get called prior to releasing the super block - so right > + * in time to perform appropriate clean-up > + */ > + r_mnt = real_mount(path->mnt); > + > + en->mnt_path.dentry = r_mnt->mnt.mnt_root; > + en->mnt_path.mnt = &r_mnt->mnt; > + > + sb = path->mnt->mnt_sb; > + en->sb = sb; > + /* > + * Increase the refcount for sb to mark it's being relied on. > + * Note that the reference to path is taken by the caller, so it > + * is safe to assume there is at least single active reference > + * to super as well. > + */ > + atomic_inc(&sb->s_active); > + > + nmask &= sb->s_etrace.events_cap_mask; > + if (!nmask) > + goto leave; > + > + spin_lock_init(&en->lock); > + INIT_LIST_HEAD(&en->node); > + > + en->notify = nmask; > + memcpy(&en->th, thresh, offsetof(struct fs_event_thresh, state)); > + if (nmask & FS_EVENT_THRESH) > + fs_trace_query_data(sb, en); > + > + kref_init(&en->count); > + > + if (rcu_access_pointer(sb->s_etrace.e_priv) != NULL) { > + struct fs_trace_entry *prev_en; > + > + prev_en = fs_trace_entry_get_rcu(sb); > + if (prev_en) { > + WARN_ON(prev_en); > + fs_trace_entry_put(prev_en); > + goto leave; > + } > + } > + atomic_set(&en->active, 1); > + > + mutex_lock(&fs_trace_lock); > + list_add(&en->node, &fs_trace_list); > + mutex_unlock(&fs_trace_lock); > + > + rcu_assign_pointer(sb->s_etrace.e_priv, en); > + synchronize_rcu(); > + > + return 0; > +leave: > + deactivate_super(sb); > + kmem_cache_free(fs_trace_cachep, en); > + return -EINVAL; > +} > + > +static int fs_update_trace_entry(struct path *path, > + struct fs_event_thresh *thresh, > + unsigned int nmask) > +{ > + struct fs_trace_entry *en; > + struct super_block *sb; > + int extend = nmask & FS_TRACE_ADD; > + int ret = -EINVAL; > + > + en = fs_trace_entry_get_rcu(path->mnt->mnt_sb); > + if (!en) > + return (extend) ? fs_new_trace_entry(path, thresh, nmask) > + : -EINVAL; > + > + if (!atomic_read(&en->active)) > + return -EINVAL; > + > + nmask &= ~FS_TRACE_ADD; > + > + spin_lock(&en->lock); > + sb = en->sb; > + if (!sb || !(nmask & sb->s_etrace.events_cap_mask)) > + goto leave; > + > + if (nmask & FS_EVENT_THRESH) { > + if (extend) { > + /* Get the current state */ > + if (!(en->notify & FS_EVENT_THRESH)) > + if (fs_trace_query_data(sb, en)) > + goto leave; > + > + if (thresh->state & THRESH_LR_ON) { > + en->th.lrange = thresh->lrange; > + en->th.state &= ~THRESH_LR_ON; > + } > + > + if (thresh->state & THRESH_UR_ON) { > + en->th.urange = thresh->urange; > + en->th.state &= ~THRESH_UR_ON; > + } > + } else { > + memset(&en->th, 0, sizeof(en->th)); > + } > + } > + > + if (extend) > + en->notify |= nmask; > + else > + en->notify &= ~nmask; > + ret = 0; > +leave: > + spin_unlock(&en->lock); > + fs_trace_entry_put(en); > + return ret; > +} > + > +static int fs_parse_trace_request(int argc, char **argv) > +{ > + struct fs_event_thresh thresh = {0}; > + struct path path; > + substring_t args[MAX_OPT_ARGS]; > + unsigned int nmask = FS_TRACE_ADD; > + int token; > + char *s; > + int ret = -EINVAL; > + > + if (!argc) { > + fs_remove_all_traces(); > + return 0; > + } > + > + s = *(argv); > + if (*s == '!') { > + /* Clear the trace entry */ > + nmask &= ~FS_TRACE_ADD; > + ++s; > + } > + > + if (kern_path_mountpoint(AT_FDCWD, s, &path, LOOKUP_FOLLOW)) > + return -EINVAL; > + > + if (!(--argc)) { > + if (!(nmask & FS_TRACE_ADD)) > + ret = fs_remove_trace_entry(path.mnt->mnt_sb); > + goto leave; > + } > + > +repeat: > + args[0].to = args[0].from = NULL; > + token = match_token(*(++argv), fs_etypes, args); > + if (!token && !nmask) > + goto leave; > + > + nmask |= token & FS_EVENTS_ALL; > + --argc; > + if ((token & FS_EVENT_THRESH) && (nmask & FS_TRACE_ADD)) { > + /* > + * Get the threshold config data: > + * lower range > + * upper range > + */ > + if (!argc) > + goto leave; > + > + ret = kstrtoull(*(++argv), 10, &thresh.lrange); > + if (ret) > + goto leave; > + thresh.state |= THRESH_LR_ON; > + if ((--argc)) { > + ret = kstrtoull(*(++argv), 10, &thresh.urange); > + if (ret) > + goto leave; > + thresh.state |= THRESH_UR_ON; > + --argc; > + } > + /* The thresholds are based on number of available blocks */ > + if (thresh.lrange < thresh.urange) { > + ret = -EINVAL; > + goto leave; > + } > + } > + if (argc) > + goto repeat; > + > + ret = fs_update_trace_entry(&path, &thresh, nmask); > +leave: > + path_put(&path); > + return ret; > +} > + > +#define DEFAULT_BUF_SIZE PAGE_SIZE > + > +static ssize_t fs_trace_write(struct file *file, const char __user *buffer, > + size_t count, loff_t *ppos) > +{ > + char **argv; > + char *kern_buf, *next, *cfg; > + size_t size, dcount = 0; > + int argc; > + > + if (!count) > + return 0; > + > + kern_buf = kmalloc(DEFAULT_BUF_SIZE, GFP_KERNEL); > + if (!kern_buf) > + return -ENOMEM; > + > + while (dcount < count) { > + > + size = count - dcount; > + if (size >= DEFAULT_BUF_SIZE) > + size = DEFAULT_BUF_SIZE - 1; > + if (copy_from_user(kern_buf, buffer + dcount, size)) { > + dcount = -EINVAL; > + goto leave; > + } > + > + kern_buf[size] = '\0'; > + > + next = cfg = kern_buf; > + > + do { > + next = strchr(cfg, ';'); > + if (next) > + *next = '\0'; > + > + argv = argv_split(GFP_KERNEL, cfg, &argc); > + if (!argv) { > + dcount = -ENOMEM; > + goto leave; > + } > + > + if (fs_parse_trace_request(argc, argv)) { > + dcount = -EINVAL; > + argv_free(argv); > + goto leave; > + } > + > + argv_free(argv); > + if (next) > + cfg = ++next; > + > + } while (next); > + dcount += size; > + } > +leave: > + kfree(kern_buf); > + return dcount; > +} > + > +static void *fs_trace_seq_start(struct seq_file *m, loff_t *pos) > +{ > + mutex_lock(&fs_trace_lock); > + return seq_list_start(&fs_trace_list, *pos); > +} > + > +static void *fs_trace_seq_next(struct seq_file *m, void *v, loff_t *pos) > +{ > + return seq_list_next(v, &fs_trace_list, pos); > +} > + > +static void fs_trace_seq_stop(struct seq_file *m, void *v) > +{ > + mutex_unlock(&fs_trace_lock); > +} > + > +static int fs_trace_seq_show(struct seq_file *m, void *v) > +{ > + struct fs_trace_entry *en; > + struct super_block *sb; > + struct mount *r_mnt; > + const struct match_token *match; > + unsigned int nmask; > + > + en = list_entry(v, struct fs_trace_entry, node); > + /* Do not show the entries outside current mount namespace */ > + r_mnt = real_mount(en->mnt_path.mnt); > + if (r_mnt->mnt_ns != current->nsproxy->mnt_ns) { > + if (!__is_local_mountpoint(r_mnt->mnt_mountpoint)) > + return 0; > + } > + > + sb = en->sb; > + > + seq_path(m, &en->mnt_path, "\t\n\\"); > + seq_putc(m, ' '); > + > + seq_escape(m, sb->s_type->name, " \t\n\\"); > + if (sb->s_subtype && sb->s_subtype[0]) { > + seq_putc(m, '.'); > + seq_escape(m, sb->s_subtype, " \t\n\\"); > + } > + > + seq_putc(m, ' '); > + if (sb->s_op->show_devname) { > + sb->s_op->show_devname(m, en->mnt_path.mnt->mnt_root); > + } else { > + seq_escape(m, r_mnt->mnt_devname ? r_mnt->mnt_devname : "none", > + " \t\n\\"); > + } > + seq_puts(m, " ("); > + > + nmask = en->notify; > + for (match = fs_etypes; match->pattern; ++match) { > + if (match->token & nmask) { > + seq_puts(m, match->pattern); > + nmask &= ~match->token; > + if (nmask) > + seq_putc(m, ','); > + } > + } > + seq_printf(m, " %llu %llu", en->th.lrange, en->th.urange); > + seq_puts(m, ")\n"); > + return 0; > +} > + > +static const struct seq_operations fs_trace_seq_ops = { > + .start = fs_trace_seq_start, > + .next = fs_trace_seq_next, > + .stop = fs_trace_seq_stop, > + .show = fs_trace_seq_show, > +}; > + > +static int fs_trace_open(struct inode *inode, struct file *file) > +{ > + return seq_open(file, &fs_trace_seq_ops); > +} > + > +static const struct file_operations fs_trace_fops = { > + .owner = THIS_MODULE, > + .open = fs_trace_open, > + .write = fs_trace_write, > + .read = seq_read, > + .llseek = seq_lseek, > + .release = seq_release, > +}; > + > +static int fs_trace_init(void) > +{ > + fs_trace_cachep = KMEM_CACHE(fs_trace_entry, 0); > + if (!fs_trace_cachep) > + return -EINVAL; > + init_waitqueue_head(&trace_wq); > + return 0; > +} > + > +/* VFS support */ > +static int fs_trace_fill_super(struct super_block *sb, void *data, int silen) > +{ > + int ret; > + static struct tree_descr desc[] = { > + [2] = { > + .name = "config", > + .ops = &fs_trace_fops, > + .mode = S_IWUSR | S_IRUGO, > + }, > + {""}, > + }; > + > + ret = simple_fill_super(sb, 0x7246332, desc); > + return !ret ? fs_trace_init() : ret; > +} > + > +static struct dentry *fs_trace_do_mount(struct file_system_type *fs_type, > + int ntype, const char *dev_name, void *data) > +{ > + return mount_single(fs_type, ntype, data, fs_trace_fill_super); > +} > + > +static void fs_trace_kill_super(struct super_block *sb) > +{ > + /* > + * The rcu_barrier here will/should make sure all call_rcu > + * callbacks are completed - still there might be some active > + * trace objects in use which can make calling the > + * kmem_cache_destroy unsafe. So we wait until all traces > + * are finally released. > + */ > + fs_remove_all_traces(); > + rcu_barrier(); > + wait_event(trace_wq, !atomic_read(&stray_traces)); > + > + kmem_cache_destroy(fs_trace_cachep); > + kill_litter_super(sb); > +} > + > +static struct kset *fs_trace_kset; > + > +static struct file_system_type fs_trace_fstype = { > + .name = "fstrace", > + .mount = fs_trace_do_mount, > + .kill_sb = fs_trace_kill_super, > +}; > + > +static void __init fs_trace_vfs_init(void) > +{ > + fs_trace_kset = kset_create_and_add("events", NULL, fs_kobj); > + > + if (!fs_trace_kset) > + return; > + > + if (!register_filesystem(&fs_trace_fstype)) { > + if (!fs_event_netlink_register()) > + return; > + unregister_filesystem(&fs_trace_fstype); > + } > + kset_unregister(fs_trace_kset); > +} > + > +static int __init fs_trace_evens_init(void) > +{ > + fs_trace_vfs_init(); > + return 0; > +}; > +module_init(fs_trace_evens_init); > + > diff --git a/fs/events/fs_event.h b/fs/events/fs_event.h > new file mode 100644 > index 0000000..23f24c8 > --- /dev/null > +++ b/fs/events/fs_event.h > @@ -0,0 +1,22 @@ > +/* > + * Copyright(c) 2015 Samsung Electronics. All rights reserved. > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2. > + * > + * The full GNU General Public License is included in this distribution in the > + * file called COPYING. > + * > + * This program is distributed in the hope that it will be useful, but WITHOUT > + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or > + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for > + * more details. > + */ > + > +#ifndef __GENERIC_FS_EVENTS_H > +#define __GENERIC_FS_EVENTS_H > + > +int fs_event_netlink_register(void); > +void fs_event_netlink_unregister(void); > + > +#endif /* __GENERIC_FS_EVENTS_H */ > diff --git a/fs/events/fs_event_netlink.c b/fs/events/fs_event_netlink.c > new file mode 100644 > index 0000000..0c97eb7 > --- /dev/null > +++ b/fs/events/fs_event_netlink.c > @@ -0,0 +1,104 @@ > +/* > + * Copyright(c) 2015 Samsung Electronics. All rights reserved. > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2. > + * > + * The full GNU General Public License is included in this distribution in the > + * file called COPYING. > + * > + * This program is distributed in the hope that it will be useful, but WITHOUT > + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or > + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for > + * more details. > + */ > +#include <linux/fs.h> > +#include <linux/init.h> > +#include <linux/kernel.h> > +#include <linux/sched.h> > +#include <linux/slab.h> > +#include <net/netlink.h> > +#include <net/genetlink.h> > +#include "fs_event.h" > + > +static const struct genl_multicast_group fs_event_mcgroups[] = { > + { .name = FS_EVENTS_MCAST_GRP_NAME, }, > +}; > + > +static struct genl_family fs_event_family = { > + .id = GENL_ID_GENERATE, > + .name = FS_EVENTS_FAMILY_NAME, > + .version = 1, > + .maxattr = FS_NL_A_MAX, > + .mcgrps = fs_event_mcgroups, > + .n_mcgrps = ARRAY_SIZE(fs_event_mcgroups), > +}; > + > +int fs_netlink_send_event(size_t size, unsigned int event_id, > + int (*compose_msg)(struct sk_buff *skb, void *data), > + void *cbdata) > +{ > + static atomic_t seq; > + struct sk_buff *skb; > + void *msg_head; > + int ret = 0; > + > + if (!size || !compose_msg) > + return -EINVAL; > + > + /* Skip if there are no listeners */ > + if (!genl_has_listeners(&fs_event_family, &init_net, 0)) > + return 0; > + > + if (event_id != FS_EVENT_NONE) > + size += nla_total_size(sizeof(u32)); > + size += nla_total_size(sizeof(u64)); > + skb = genlmsg_new(size, GFP_NOWAIT); > + > + if (!skb) { > + pr_debug("Failed to allocate new FS generic netlink message\n"); > + return -ENOMEM; > + } > + > + msg_head = genlmsg_put(skb, 0, atomic_add_return(1, &seq), > + &fs_event_family, 0, FS_NL_C_EVENT); > + if (!msg_head) > + goto cleanup; > + > + if (event_id != FS_EVENT_NONE) > + if (nla_put_u32(skb, FS_NL_A_EVENT_ID, event_id)) > + goto cancel; > + > + ret = compose_msg(skb, cbdata); > + if (ret) > + goto cancel; > + > + genlmsg_end(skb, msg_head); > + ret = genlmsg_multicast(&fs_event_family, skb, 0, 0, GFP_NOWAIT); > + if (ret && ret != -ENOBUFS && ret != -ESRCH) > + goto cleanup; > + > + return ret; > + > +cancel: > + genlmsg_cancel(skb, msg_head); > +cleanup: > + nlmsg_free(skb); > + return ret; > +} > +EXPORT_SYMBOL(fs_netlink_send_event); > + > +int fs_event_netlink_register(void) > +{ > + int ret; > + > + ret = genl_register_family(&fs_event_family); > + if (ret) > + pr_err("Failed to register FS netlink interface\n"); > + return ret; > +} > + > +void fs_event_netlink_unregister(void) > +{ > + genl_unregister_family(&fs_event_family); > +} > diff --git a/fs/namespace.c b/fs/namespace.c > index 82ef140..ec6e2ef 100644 > --- a/fs/namespace.c > +++ b/fs/namespace.c > @@ -1031,6 +1031,7 @@ static void cleanup_mnt(struct mount *mnt) > if (unlikely(mnt->mnt_pins.first)) > mnt_pin_kill(mnt); > fsnotify_vfsmount_delete(&mnt->mnt); > + fs_event_mount_dropped(&mnt->mnt); > dput(mnt->mnt.mnt_root); > deactivate_super(mnt->mnt.mnt_sb); > mnt_free_id(mnt); > diff --git a/include/linux/fs.h b/include/linux/fs.h > index b4d71b5..b7dadd9 100644 > --- a/include/linux/fs.h > +++ b/include/linux/fs.h > @@ -263,6 +263,10 @@ struct iattr { > * Includes for diskquotas. > */ > #include <linux/quota.h> > +/* > + * Include for Generic File System Events Interface > + */ > +#include <linux/fs_event.h> > > /* > * Maximum number of layers of fs stack. Needs to be limited to > @@ -1253,7 +1257,7 @@ struct super_block { > struct hlist_node s_instances; > unsigned int s_quota_types; /* Bitmask of supported quota types */ > struct quota_info s_dquot; /* Diskquota specific options */ > - > + struct fs_trace_info s_etrace; > struct sb_writers s_writers; > > char s_id[32]; /* Informational name */ > diff --git a/include/linux/fs_event.h b/include/linux/fs_event.h > new file mode 100644 > index 0000000..83e22dd > --- /dev/null > +++ b/include/linux/fs_event.h > @@ -0,0 +1,72 @@ > +/* > + * Generic File System Events Interface > + * > + * Copyright(c) 2015 Samsung Electronics. All rights reserved. > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2. > + * > + * The full GNU General Public License is included in this distribution in the > + * file called COPYING. > + * > + * This program is distributed in the hope that it will be useful, but WITHOUT > + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or > + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for > + * more details. > + */ > +#ifndef _LINUX_GENERIC_FS_EVETS_ > +#define _LINUX_GENERIC_FS_EVETS_ > +#include <net/netlink.h> > +#include <uapi/linux/fs_event.h> > + > +/* > + * Currently supported event types > + */ > +#define FS_EVENT_GENERIC 0x001 > +#define FS_EVENT_THRESH 0x002 > + > +#define FS_EVENTS_ALL (FS_EVENT_GENERIC | FS_EVENT_THRESH) > + > +struct fs_trace_operations { > + void (*query)(struct super_block *, u64 *); > +}; > + > +struct fs_trace_info { > + void __rcu *e_priv; /* READ ONLY */ > + unsigned int events_cap_mask; /* Supported notifications */ > + const struct fs_trace_operations *ops; > +}; > + > +#ifdef CONFIG_FS_EVENTS > + > +void fs_event_notify(struct super_block *sb, unsigned int event_id); > +void fs_event_alloc_space(struct super_block *sb, u64 ncount); > +void fs_event_free_space(struct super_block *sb, u64 ncount); > +void fs_event_mount_dropped(struct vfsmount *mnt); > + > +int fs_netlink_send_event(size_t size, unsigned int event_id, > + int (*compose_msg)(struct sk_buff *skb, void *data), > + void *cbdata); > + > +#else /* CONFIG_FS_EVENTS */ > + > +static inline > +void fs_event_notify(struct super_block *sb, unsigned int event_id) {}; > +static inline > +void fs_event_alloc_space(struct super_block *sb, u64 ncount) {}; > +static inline > +void fs_event_free_space(struct super_block *sb, u64 ncount) {}; > +static inline > +void fs_event_mount_dropped(struct vfsmount *mnt) {}; > + > +static inline > +int fs_netlink_send_event(size_t size, unsigned int event_id, > + int (*compose_msig)(struct sk_buff *skb, void *data), > + void *cbdata) > +{ > + return -ENOSYS; > +} > +#endif /* CONFIG_FS_EVENTS */ > + > +#endif /* _LINUX_GENERIC_FS_EVENTS_ */ > + > diff --git a/include/uapi/linux/Kbuild b/include/uapi/linux/Kbuild > index 68ceb97..dae0fab 100644 > --- a/include/uapi/linux/Kbuild > +++ b/include/uapi/linux/Kbuild > @@ -129,6 +129,7 @@ header-y += firewire-constants.h > header-y += flat.h > header-y += fou.h > header-y += fs.h > +header-y += fs_event.h > header-y += fsl_hypervisor.h > header-y += fuse.h > header-y += futex.h > diff --git a/include/uapi/linux/fs_event.h b/include/uapi/linux/fs_event.h > new file mode 100644 > index 0000000..d8b07da > --- /dev/null > +++ b/include/uapi/linux/fs_event.h > @@ -0,0 +1,58 @@ > +/* > + * Generic netlink support for Generic File System Events Interface > + * > + * Copyright(c) 2015 Samsung Electronics. All rights reserved. > + * > + * This program is free software; you can redistribute it and/or modify it > + * under the terms of the GNU General Public License version 2. > + * > + * The full GNU General Public License is included in this distribution in the > + * file called COPYING. > + * > + * This program is distributed in the hope that it will be useful, but WITHOUT > + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or > + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for > + * more details. > + */ > +#ifndef _UAPI_LINUX_GENERIC_FS_EVENTS_ > +#define _UAPI_LINUX_GENERIC_FS_EVENTS_ > + > +#define FS_EVENTS_FAMILY_NAME "fs_event" > +#define FS_EVENTS_MCAST_GRP_NAME "fs_event_mc_grp" > + > +/* > + * Generic netlink attribute types > + */ > +enum { > + FS_NL_A_NONE, > + FS_NL_A_EVENT_ID, > + FS_NL_A_DEV_MAJOR, > + FS_NL_A_DEV_MINOR, > + FS_NL_A_CAUSED_ID, > + FS_NL_A_DATA, > + __FS_NL_A_MAX, > +}; > +#define FS_NL_A_MAX (__FS_NL_A_MAX - 1) > +/* > + * Generic netlink commands > + */ > +#define FS_NL_C_EVENT 1 > + > +/* > + * Supported set of FS events > + */ > +enum { > + FS_EVENT_NONE, > + FS_WARN_ENOSPC, /* No space left to reserve data blks */ > + FS_WARN_ENOSPC_META, /* No space left for metadata */ > + FS_THR_LRBELOW, /* The threshold lower range has been reached */ > + FS_THR_LRABOVE, /* The threshold lower range re-activcated*/ > + FS_THR_URBELOW, > + FS_THR_URABOVE, > + FS_ERR_REMOUNT_RO, /* The file system has been remounted as RO */ > + FS_ERR_CORRUPTED /* Critical error - fs corrupted */ > + > +}; > + > +#endif /* _UAPI_LINUX_GENERIC_FS_EVENTS_ */ > + > -- > 1.7.9.5 > > -- > To unsubscribe, send a message with 'unsubscribe linux-mm' in > the body to majordomo@xxxxxxxxx. For more info on Linux MM, > see: http://www.linux-mm.org/ . > Don't email: <a href=mailto:"dont@xxxxxxxxx"> email@xxxxxxxxx </a>
Attachment:
signature.asc
Description: PGP signature