server: Use a single inotify watch, as it scales better with a large
number of directories.
diff --git a/server/change.c b/server/change.c
index e32997e..aa8e0cb 100644
--- a/server/change.c
+++ b/server/change.c
@@ -28,6 +28,9 @@
#include <stdlib.h>
#include <signal.h>
#include <sys/stat.h>
+#include <sys/types.h>
+#include <limits.h>
+#include <dirent.h>
#include <errno.h>
#ifdef HAVE_SYS_ERRNO_H
#include <sys/errno.h>
@@ -126,6 +129,12 @@
#endif
+struct inode;
+
+static void free_inode( struct inode *inode );
+
+static struct fd *inotify_fd;
+
struct change_record {
struct list entry;
int action;
@@ -143,10 +152,10 @@
int notified; /* SIGIO counter */
int want_data; /* return change data */
long signaled; /* the file changed */
- struct fd *inotify_fd; /* inotify file descriptor */
- int wd; /* inotify watch descriptor */
struct list change_q; /* change readers */
struct list change_records; /* data for the change */
+ struct list in_entry; /* entry in the inode dirs list */
+ struct inode *inode; /* inode of the associated directory */
};
static struct fd *dir_get_fd( struct object *obj );
@@ -241,30 +250,6 @@
sigprocmask( SIG_UNBLOCK, &sigset, NULL );
}
-struct object *create_dir_obj( struct fd *fd )
-{
- struct dir *dir;
-
- dir = alloc_object( &dir_ops );
- if (!dir)
- return NULL;
-
- list_init( &dir->change_q );
- list_init( &dir->change_records );
- dir->event = NULL;
- dir->filter = 0;
- dir->notified = 0;
- dir->signaled = 0;
- dir->want_data = 0;
- dir->inotify_fd = NULL;
- dir->wd = -1;
- grab_object( fd );
- dir->fd = fd;
- set_fd_user( fd, &dir_fd_ops, &dir->obj );
-
- return &dir->obj;
-}
-
static void dir_dump( struct object *obj, int verbose )
{
struct dir *dir = (struct dir *)obj;
@@ -352,8 +337,11 @@
if (dir->filter)
remove_change( dir );
- if (dir->inotify_fd)
- release_object( dir->inotify_fd );
+ if (dir->inode)
+ {
+ list_remove( &dir->in_entry );
+ free_inode( dir->inode );
+ }
async_terminate_queue( &dir->change_q, STATUS_CANCELLED );
while ((record = get_first_change_record( dir ))) free( record );
@@ -364,6 +352,12 @@
release_object( dir->event );
}
release_object( dir->fd );
+
+ if (inotify_fd && list_empty( &change_list ))
+ {
+ release_object( inotify_fd );
+ inotify_fd = NULL;
+ }
}
static struct dir *
@@ -391,6 +385,87 @@
#ifdef USE_INOTIFY
+#define HASH_SIZE 31
+
+struct inode {
+ struct list dirs; /* directory handles watching this inode */
+ struct list ino_entry; /* entry in the inode hash */
+ struct list wd_entry; /* entry in the watch descriptor hash */
+ dev_t dev; /* device number */
+ ino_t ino; /* device's inode number */
+ int wd; /* inotify's watch descriptor */
+};
+
+struct list inode_hash[ HASH_SIZE ];
+struct list wd_hash[ HASH_SIZE ];
+
+static struct inode *inode_from_wd( int wd )
+{
+ struct list *bucket = &wd_hash[ wd % HASH_SIZE ];
+ struct inode *inode;
+
+ LIST_FOR_EACH_ENTRY( inode, bucket, struct inode, wd_entry )
+ if (inode->wd == wd)
+ return inode;
+
+ return NULL;
+}
+
+static inline struct list *get_hash_list( dev_t dev, ino_t ino )
+{
+ return &inode_hash[ (ino ^ dev) % HASH_SIZE ];
+}
+
+static struct inode *get_inode( dev_t dev, ino_t ino )
+{
+ struct list *bucket = get_hash_list( dev, ino );
+ struct inode *inode;
+
+ LIST_FOR_EACH_ENTRY( inode, bucket, struct inode, ino_entry )
+ if (inode->ino == ino && inode->dev == dev)
+ return inode;
+
+ return NULL;
+}
+
+static struct inode *create_inode( dev_t dev, ino_t ino )
+{
+ struct inode *inode;
+
+ inode = malloc( sizeof *inode );
+ if (inode)
+ {
+ list_init( &inode->dirs );
+ inode->ino = ino;
+ inode->dev = dev;
+ inode->wd = -1;
+ list_add_tail( get_hash_list( dev, ino ), &inode->ino_entry );
+ }
+ return inode;
+}
+
+static void inode_set_wd( struct inode *inode, int wd )
+{
+ if (inode->wd != -1)
+ list_remove( &inode->wd_entry );
+ inode->wd = wd;
+ list_add_tail( &wd_hash[ wd % HASH_SIZE ], &inode->wd_entry );
+}
+
+static void free_inode( struct inode *inode )
+{
+ if (!list_empty( &inode->dirs ))
+ return;
+
+ if (inode->wd != -1)
+ {
+ inotify_remove_watch( get_unix_fd( inotify_fd ), inode->wd );
+ list_remove( &inode->wd_entry );
+ }
+ list_remove( &inode->ino_entry );
+ free( inode );
+}
+
static int inotify_get_poll_events( struct fd *fd );
static void inotify_poll_event( struct fd *fd, int event );
static int inotify_get_info( struct fd *fd );
@@ -442,12 +517,49 @@
}
}
+static unsigned int filter_from_event( struct inotify_event *ie )
+{
+ unsigned int filter = 0;
+
+ if (ie->mask & (IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE | IN_CREATE))
+ filter |= FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME;
+ if (ie->mask & IN_MODIFY)
+ filter |= FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE;
+ if (ie->mask & IN_ATTRIB)
+ filter |= FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY;
+ if (ie->mask & IN_ACCESS)
+ filter |= FILE_NOTIFY_CHANGE_LAST_ACCESS;
+ if (ie->mask & IN_CREATE)
+ filter |= FILE_NOTIFY_CHANGE_CREATION;
+
+ return filter;
+}
+
+static void inotify_notify_all( struct inotify_event *ie )
+{
+ struct inode *inode;
+ struct dir *dir;
+ unsigned int filter;
+
+ inode = inode_from_wd( ie->wd );
+ if (!inode)
+ {
+ fprintf( stderr, "no inode matches %d\n", ie->wd);
+ return;
+ }
+
+ filter = filter_from_event( ie );
+
+ LIST_FOR_EACH_ENTRY( dir, &inode->dirs, struct dir, in_entry )
+ if (filter & dir->filter)
+ inotify_do_change_notify( dir, ie );
+}
+
static void inotify_poll_event( struct fd *fd, int event )
{
int r, ofs, unix_fd;
char buffer[0x1000];
struct inotify_event *ie;
- struct dir *dir = get_fd_user( fd );
unix_fd = get_unix_fd( fd );
r = read( unix_fd, buffer, sizeof buffer );
@@ -464,7 +576,7 @@
break;
ofs += offsetof( struct inotify_event, name[ie->len] );
if (ofs > r) break;
- inotify_do_change_notify( dir, ie );
+ inotify_notify_all( ie );
}
}
@@ -473,26 +585,19 @@
return 0;
}
-static inline struct fd *create_inotify_fd( struct dir *dir )
+static inline struct fd *create_inotify_fd( void )
{
int unix_fd;
unix_fd = inotify_init();
if (unix_fd<0)
return NULL;
- return create_anonymous_fd( &inotify_fd_ops, unix_fd, &dir->obj );
+ return create_anonymous_fd( &inotify_fd_ops, unix_fd, NULL );
}
-static int inotify_adjust_changes( struct dir *dir )
+static int map_flags( unsigned int filter )
{
- unsigned int filter = dir->filter;
unsigned int mask = 0;
- char link[32];
-
- if (!dir->inotify_fd)
- {
- if (!(dir->inotify_fd = create_inotify_fd( dir ))) return 0;
- }
if (filter & FILE_NOTIFY_CHANGE_FILE_NAME)
mask |= (IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE | IN_CREATE);
@@ -511,15 +616,131 @@
if (filter & FILE_NOTIFY_CHANGE_SECURITY)
mask |= IN_ATTRIB;
- sprintf( link, "/proc/self/fd/%u", get_unix_fd( dir->fd ) );
- dir->wd = inotify_add_watch( get_unix_fd( dir->inotify_fd ), link, mask );
- if (dir->wd != -1)
- set_fd_events( dir->inotify_fd, POLLIN );
+ return mask;
+}
+
+static int inotify_add_dir( char *path, unsigned int filter )
+{
+ int wd = inotify_add_watch( get_unix_fd( inotify_fd ),
+ path, map_flags( filter ) );
+ if (wd != -1)
+ set_fd_events( inotify_fd, POLLIN );
+ return wd;
+}
+
+static unsigned int filter_from_inode( struct inode *inode )
+{
+ unsigned int filter = 0;
+ struct dir *dir;
+
+ LIST_FOR_EACH_ENTRY( dir, &inode->dirs, struct dir, in_entry )
+ filter |= dir->filter;
+
+ return filter;
+}
+
+static int init_inotify( void )
+{
+ int i;
+
+ if (inotify_fd)
+ return 1;
+
+ inotify_fd = create_inotify_fd();
+ if (!inotify_fd)
+ return 0;
+
+ for (i=0; i<HASH_SIZE; i++)
+ {
+ list_init( &inode_hash[i] );
+ list_init( &wd_hash[i] );
+ }
+
return 1;
}
+static int inotify_adjust_changes( struct dir *dir )
+{
+ unsigned int filter;
+ struct inode *inode;
+ struct stat st;
+ char path[32];
+ int wd, unix_fd;
+
+ if (!inotify_fd)
+ return 0;
+
+ unix_fd = get_unix_fd( dir->fd );
+
+ inode = dir->inode;
+ if (!inode)
+ {
+ /* check if this fd is already being watched */
+ if (-1 == fstat( unix_fd, &st ))
+ return 0;
+
+ inode = get_inode( st.st_dev, st.st_ino );
+ if (!inode)
+ inode = create_inode( st.st_dev, st.st_ino );
+ if (!inode)
+ return 0;
+ list_add_tail( &inode->dirs, &dir->in_entry );
+ dir->inode = inode;
+ }
+
+ filter = filter_from_inode( inode );
+
+ sprintf( path, "/proc/self/fd/%u", unix_fd );
+ wd = inotify_add_dir( path, filter );
+ if (wd == -1) return 0;
+
+ inode_set_wd( inode, wd );
+
+ return 1;
+}
+
+#else
+
+static int init_inotify( void )
+{
+ return 0;
+}
+
+static int inotify_adjust_changes( struct dir *dir )
+{
+ return 0;
+}
+
+static void free_inode( struct inode *inode )
+{
+ assert( 0 );
+}
+
#endif /* USE_INOTIFY */
+struct object *create_dir_obj( struct fd *fd )
+{
+ struct dir *dir;
+
+ dir = alloc_object( &dir_ops );
+ if (!dir)
+ return NULL;
+
+ list_init( &dir->change_q );
+ list_init( &dir->change_records );
+ dir->event = NULL;
+ dir->filter = 0;
+ dir->notified = 0;
+ dir->signaled = 0;
+ dir->want_data = 0;
+ dir->inode = NULL;
+ grab_object( fd );
+ dir->fd = fd;
+ set_fd_user( fd, &dir_fd_ops, &dir->obj );
+
+ return &dir->obj;
+}
+
/* enable change notifications for a directory */
DECL_HANDLER(read_directory_changes)
{
@@ -556,6 +777,7 @@
/* assign it once */
if (!dir->filter)
{
+ init_inotify();
insert_change( dir );
dir->filter = req->filter;
dir->want_data = req->want_data;
@@ -570,9 +792,7 @@
reset_event( event );
/* setup the real notification */
-#ifdef USE_INOTIFY
if (!inotify_adjust_changes( dir ))
-#endif
dnotify_adjust_changes( dir );
set_error(STATUS_PENDING);