For a nice code savings... chunkd/Makefile.am | 1 chunkd/chunkd.h | 28 +---- chunkd/cldu.c | 64 +++++------ chunkd/server.c | 289 +++++++++++++---------------------------------------- chunkd/util.c | 23 ---- configure.ac | 3 6 files changed, 116 insertions(+), 292 deletions(-) diff --git a/chunkd/Makefile.am b/chunkd/Makefile.am index 78bba72..a45a89b 100644 --- a/chunkd/Makefile.am +++ b/chunkd/Makefile.am @@ -10,4 +10,5 @@ chunkd_SOURCES = chunkd.h \ objcache.c chunkd_LDADD = \ ../lib/libhail.la @GLIB_LIBS@ @CRYPTO_LIBS@ \ + @EVENT_LIBS@ \ @SSL_LIBS@ @TOKYOCABINET_LIBS@ @XML_LIBS@ @LIBCURL@ diff --git a/chunkd/chunkd.h b/chunkd/chunkd.h index 937573c..5be155a 100644 --- a/chunkd/chunkd.h +++ b/chunkd/chunkd.h @@ -28,7 +28,7 @@ #include <chunk_msg.h> #include <hail_log.h> #include <tchdb.h> -#include <cldc.h> /* for cld_timer */ +#include <event.h> #include <objcache.h> #ifndef ARRAY_SIZE @@ -77,6 +77,8 @@ struct client { char addr_host[64]; /* ASCII version of inet addr */ char addr_port[16]; /* ASCII version of port */ int fd; /* socket */ + struct event ev; + short ev_mask; /* EV_READ and/or EV_WRITE */ char user[CHD_USER_SZ + 1]; @@ -172,18 +174,10 @@ struct server_stats { unsigned long opt_write; /* optimistic writes */ }; -struct server_poll { - short events; /* POLL* from poll.h */ - bool busy; /* if true, do not poll us */ - - /* callback function, data */ - bool (*cb)(int fd, short events, void *userdata); - void *userdata; -}; - struct server_socket { int fd; const struct listen_cfg *cfg; + struct event ev; struct list_head sockets_node; }; @@ -207,14 +201,15 @@ struct server { char *pid_file; /* PID file */ int pid_fd; + struct event_base *evbase_main; + struct list_head listeners; struct list_head sockets; /* points into listeners */ - GHashTable *fd_info; - GThreadPool *workers; /* global thread worker pool */ int max_workers; int worker_pipe[2]; + struct event worker_ev; struct list_head wr_trash; unsigned int trash_sz; @@ -311,11 +306,6 @@ extern void syslogerr(const char *prefix); extern void strup(char *s); extern int write_pid_file(const char *pid_fn); extern int fsetflags(const char *prefix, int fd, int or_flags); -extern void timer_init(struct cld_timer *timer, const char *name, - void (*cb)(struct cld_timer *), void *userdata); -extern void timer_add(struct cld_timer *timer, time_t expires); -extern void timer_del(struct cld_timer *timer); -extern time_t timers_run(void); extern char *time2str(char *strbuf, time_t time); extern void hexstr(const unsigned char *buf, size_t buf_len, char *outstr); @@ -328,7 +318,7 @@ extern bool cli_err(struct client *cli, enum chunk_errcode code, bool recycle_ok extern int cli_writeq(struct client *cli, const void *buf, unsigned int buflen, cli_write_func cb, void *cb_data); extern bool cli_wr_sendfile(struct client *, cli_write_func); -extern bool cli_rd_set_poll(struct client *cli, bool readable); +extern void cli_rd_set_poll(struct client *cli, bool readable); extern void cli_wr_set_poll(struct client *cli, bool writable); extern bool cli_cb_free(struct client *cli, struct client_write *wr, bool done); @@ -336,7 +326,7 @@ extern bool cli_write_start(struct client *cli); extern int cli_req_avail(struct client *cli); extern int cli_poll_mod(struct client *cli); extern bool worker_pipe_signal(struct worker_info *wi); -extern bool tcp_cli_event(int fd, short events, void *userdata); +extern void tcp_cli_event(int fd, short events, void *userdata); extern void resp_init_req(struct chunksrv_resp *resp, const struct chunksrv_req *req); diff --git a/chunkd/cldu.c b/chunkd/cldu.c index dd8b67c..026c523 100644 --- a/chunkd/cldu.c +++ b/chunkd/cldu.c @@ -21,6 +21,7 @@ #include "hail-config.h" #include <sys/types.h> +#include <sys/time.h> #include <sys/socket.h> #include <glib.h> #include <syslog.h> @@ -39,21 +40,23 @@ struct cld_host { }; struct cld_session { - bool forced_hosts; /* Administrator overrode default CLD */ - bool is_dead; - struct ncld_sess *nsess; /* library state */ + bool forced_hosts; /* Administrator overrode default CLD */ + bool is_dead; + struct ncld_sess *nsess; /* library state */ - int actx; /* Active host cldv[actx] */ - struct cld_host cldv[N_CLD]; + int actx; /* Active host cldv[actx] */ + struct cld_host cldv[N_CLD]; - struct cld_timer timer; - int event_pipe[2]; + int event_pipe[2]; + struct event ev; + struct event ev_timer; + bool have_timer; - char *ffname; - struct ncld_fh *ffh; /* keep open for lock */ - uint32_t nid; - const char *ourhost; /* N.B. points to some global data. */ - struct geo *ploc; /* N.B. points to some global data. */ + char *ffname; + struct ncld_fh *ffh; /* keep open for lock */ + uint32_t nid; + const char *ourhost; /* N.B. points to some global data. */ + struct geo *ploc; /* N.B. points to some global data. */ void (*state_cb)(enum st_cld); }; @@ -114,7 +117,9 @@ static void cldu_sess_proc(struct cld_session *cs) newactive = cldu_nextactive(cs); if (cldu_set_cldc(cs, newactive)) { /* Oops, should not happen. Just loop, then... */ - timer_add(&cs->timer, time(NULL) + 30); + struct timeval tv = { 30, 0 }; + evtimer_add(&cs->ev_timer, &tv); + cs->have_timer = true; return; } cs->is_dead = false; @@ -127,14 +132,14 @@ static void cldu_sess_proc(struct cld_session *cs) } } -static void cldu_timer_event(struct cld_timer *timer) +static void cldu_timer_event(int fd, short events, void *userdata) { - struct cld_session *cs = timer->userdata; + struct cld_session *cs = userdata; cldu_sess_proc(cs); } -static bool cldu_pipe_event(int fd, short events, void *userdata) +static void cldu_pipe_event(int fd, short events, void *userdata) { struct cld_session *cs = userdata; unsigned char cmd; @@ -145,7 +150,6 @@ static bool cldu_pipe_event(int fd, short events, void *userdata) cldu_sess_proc(cs); else applog(LOG_WARNING, "Stray CLD event pipe poll"); - return true; } static void cldu_sess_event(void *priv, uint32_t what) @@ -473,7 +477,6 @@ int cld_begin(const char *thishost, uint32_t nid, char *infopath, struct geo *locp, void (*cb)(enum st_cld)) { static struct cld_session *cs = &ses; - struct server_poll *sp; int rfd; int retry_cnt; int newactive; @@ -489,7 +492,7 @@ int cld_begin(const char *thishost, uint32_t nid, char *infopath, // memset(&ses, 0, sizeof(struct cld_session)); cs->state_cb = cb; - timer_init(&cs->timer, "chunkd_cldu_timer", cldu_timer_event, cs); + evtimer_set(&cs->ev_timer, cldu_timer_event, cs); cldu_saveargs(cs, infopath, nid, thishost, locp); @@ -528,18 +531,13 @@ int cld_begin(const char *thishost, uint32_t nid, char *infopath, } rfd = cs->event_pipe[0]; - sp = calloc(1, sizeof(*sp)); - if (!sp) { - applog(LOG_ERR, "No core"); + event_set(&cs->ev, rfd, EV_READ | EV_PERSIST, cldu_pipe_event, cs); + + if (event_add(&cs->ev, NULL) < 0) { + applog(LOG_ERR, "event_add cldu fail"); goto err_sp; } - sp->events = POLLIN; - sp->cb = cldu_pipe_event; - sp->userdata = cs; - - g_hash_table_insert(chunkd_srv.fd_info, GINT_TO_POINTER(rfd), sp); - /* * FIXME: We should find next suitable host according to * the priority and weight (among those which are up). @@ -560,7 +558,7 @@ int cld_begin(const char *thishost, uint32_t nid, char *infopath, return 0; err_net: - g_hash_table_remove(chunkd_srv.fd_info, GINT_TO_POINTER(rfd)); + event_del(&cs->ev); err_sp: close(cs->event_pipe[0]); close(cs->event_pipe[1]); @@ -599,7 +597,10 @@ void cld_end(void) if (!cs->nid) return; - timer_del(&cs->timer); + if (cs->have_timer) { + evtimer_del(&cs->ev_timer); + cs->have_timer = false; + } if (cs->nsess) { ncld_sess_close(cs->nsess); @@ -615,8 +616,7 @@ void cld_end(void) } } - g_hash_table_remove(chunkd_srv.fd_info, - GINT_TO_POINTER(cs->event_pipe[0])); + event_del(&cs->ev); close(cs->event_pipe[0]); close(cs->event_pipe[1]); diff --git a/chunkd/server.c b/chunkd/server.c index 999ae86..f975e0b 100644 --- a/chunkd/server.c +++ b/chunkd/server.c @@ -39,7 +39,6 @@ #include <arpa/inet.h> #include <stdbool.h> #include <syslog.h> -#include <poll.h> #include <argp.h> #include <errno.h> #include <time.h> @@ -251,11 +250,13 @@ static char *get_hostname(void) static void term_signal(int signo) { server_running = false; + event_loopbreak(); } static void stats_signal(int signo) { dump_stats = true; + event_loopbreak(); } #define X(stat) \ @@ -271,42 +272,6 @@ static void stats_dump(void) #undef X -static struct server_poll *srv_poll_lookup(int fd) -{ - return g_hash_table_lookup(chunkd_srv.fd_info, GINT_TO_POINTER(fd)); -} - -static bool srv_poll_del(int fd) -{ - return g_hash_table_remove(chunkd_srv.fd_info, GINT_TO_POINTER(fd)); -} - -static bool srv_poll_mask(int fd, short mask_set, short mask_clear) -{ - struct server_poll *sp; - - sp = srv_poll_lookup(fd); - if (!sp) - return false; - - sp->events = (sp->events & ~mask_clear) | mask_set; - - return true; -} - -static bool srv_poll_ready(int fd) -{ - struct server_poll *sp; - - sp = srv_poll_lookup(fd); - if (!sp) - return false; - - sp->busy = false; - - return true; -} - void resp_init_req(struct chunksrv_resp *resp, const struct chunksrv_req *req) { @@ -359,12 +324,13 @@ static void cli_free(struct client *cli) cli_out_end(cli); cli_in_end(cli); + if (cli->ev_mask && (event_del(&cli->ev) < 0)) + applog(LOG_ERR, "TCP cli poll del failed"); + /* clean up network socket */ if (cli->fd >= 0) { if (cli->ssl) SSL_shutdown(cli->ssl); - if (!srv_poll_del(cli->fd)) - applog(LOG_ERR, "TCP cli poll del failed"); if (close(cli->fd) < 0) syslogerr("close(2) TCP client socket"); } @@ -422,22 +388,46 @@ static bool cli_evt_recycle(struct client *cli, unsigned int events) return true; } -bool cli_rd_set_poll(struct client *cli, bool readable) +static void cli_ev_update(struct client *cli, short new_mask) +{ + if (cli->ev_mask == new_mask) + return; + + if (cli->ev_mask) + if (event_del(&cli->ev) < 0) + applog(LOG_ERR, "unable to unready cli fd"); + if (new_mask) { + event_set(&cli->ev, cli->fd, new_mask | EV_PERSIST, + tcp_cli_event, cli); + if (event_add(&cli->ev, NULL) < 0) + applog(LOG_ERR, "unable to ready cli fd"); + } + + cli->ev_mask = new_mask; +} + +void cli_rd_set_poll(struct client *cli, bool readable) { + short new_ev_mask = cli->ev_mask; + if (readable) - srv_poll_mask(cli->fd, POLLIN, 0); + new_ev_mask |= EV_READ; else - srv_poll_mask(cli->fd, 0, POLLIN); + new_ev_mask &= ~EV_READ; - return true; + cli_ev_update(cli, new_ev_mask); } void cli_wr_set_poll(struct client *cli, bool writable) { + short new_ev_mask = cli->ev_mask; + if (writable) - srv_poll_mask(cli->fd, POLLOUT, 0); + new_ev_mask |= EV_WRITE; else - srv_poll_mask(cli->fd, 0, POLLOUT); + new_ev_mask &= ~EV_WRITE; + + cli_ev_update(cli, new_ev_mask); } static int cli_wr_iov(struct client *cli, struct iovec *iov, int max_iov) @@ -1347,20 +1337,13 @@ static void tcp_cli_wr_event(int fd, short events, void *userdata) cli_writable(cli); } -bool tcp_cli_event(int fd, short events, void *userdata) +void tcp_cli_event(int fd, short events, void *userdata) { struct client *cli = userdata; bool loop = false, disposing = false; - bool do_poll = true; - if (events & POLLHUP) { - cli->state = evt_dispose; - cli_free(cli); - return true; - } - - if (events & POLLOUT) - tcp_cli_wr_event(fd, events & ~POLLIN, userdata); + if (events & EV_WRITE) + tcp_cli_wr_event(fd, events & ~EV_READ, userdata); if (cli->write_want_read) { cli->write_want_read = false; @@ -1368,21 +1351,16 @@ bool tcp_cli_event(int fd, short events, void *userdata) } else loop = true; + if (!(events & EV_READ) && (cli->state != evt_dispose)) + return; + while (loop) { disposing = (cli->state == evt_dispose); loop = state_funcs[cli->state](cli, events); } - - if (disposing) - do_poll = false; - - if (do_poll && !srv_poll_ready(fd)) - applog(LOG_ERR, "unable to ready cli fd for polling"); - - return true; /* continue main loop; do NOT terminate server */ } -static bool tcp_srv_event(int fd, short events, void *userdata) +static void tcp_srv_event(int fd, short events, void *userdata) { struct server_socket *sock = userdata; socklen_t addrlen = sizeof(struct sockaddr_in6); @@ -1390,12 +1368,13 @@ static bool tcp_srv_event(int fd, short events, void *userdata) char host[64]; char port[16]; int on = 1; - struct server_poll *sp; cli = cli_alloc(); if (!cli) { applog(LOG_ERR, "out of memory"); - return false; /* end main loop; terminate server */ + server_running = false; + event_loopbreak(); + return; } /* receive TCP connection from kernel */ @@ -1416,15 +1395,8 @@ static bool tcp_srv_event(int fd, short events, void *userdata) applog(LOG_WARNING, "TCP_NODELAY failed: %s", strerror(errno)); - sp = calloc(1, sizeof(*sp)); - if (!sp) - goto err_out_fd; - - sp->events = POLLIN; - sp->cb = tcp_cli_event; - sp->userdata = cli; - - g_hash_table_insert(chunkd_srv.fd_info, GINT_TO_POINTER(cli->fd), sp); + event_set(&cli->ev, cli->fd, EV_READ | EV_PERSIST, + tcp_cli_event, cli); /* pretty-print incoming cxn info */ getnameinfo((struct sockaddr *) &cli->addr, addrlen, @@ -1438,15 +1410,17 @@ static bool tcp_srv_event(int fd, short events, void *userdata) strcpy(cli->addr_host, host); strcpy(cli->addr_port, port); - if (!srv_poll_ready(fd)) + if (event_add(&cli->ev, NULL) < 0) { applog(LOG_ERR, "unable to ready srv fd for polling"); + goto err_out_fd; + } + cli->ev_mask = EV_READ; - return true; /* continue main loop; do NOT terminate server */ + return; err_out_fd: err_out: cli_free(cli); - return true; /* continue main loop; do NOT terminate server */ } static int net_write_port(const char *port_file, const char *port_str) @@ -1466,25 +1440,6 @@ static int net_write_port(const char *port_file, const char *port_str) return 0; } -static bool pipe_watch(int pipe_fd_0, - bool (*cb)(int fd, short events, void *userdata), - void *userdata) -{ - struct server_poll *sp; - - sp = calloc(1, sizeof(*sp)); - if (!sp) - return false; - - sp->events = POLLIN; - sp->cb = cb; - sp->userdata = userdata; - - g_hash_table_insert(chunkd_srv.fd_info, GINT_TO_POINTER(pipe_fd_0), sp); - - return true; -} - static int net_open_socket(const struct listen_cfg *cfg, int addr_fam, int sock_type, int sock_prot, int addr_len, void *addr_ptr) @@ -1492,7 +1447,6 @@ static int net_open_socket(const struct listen_cfg *cfg, struct server_socket *sock; int fd, on; int rc; - struct server_poll *sp; fd = socket(addr_fam, sock_type, sock_prot); if (fd < 0) { @@ -1532,26 +1486,21 @@ static int net_open_socket(const struct listen_cfg *cfg, INIT_LIST_HEAD(&sock->sockets_node); - sp = calloc(1, sizeof(*sp)); - if (!sp) { - free(sock); - rc = -ENOMEM; - goto err_out_fd; - } - - sp->events = POLLIN; - sp->cb = tcp_srv_event; - sp->userdata = sock; - - g_hash_table_insert(chunkd_srv.fd_info, GINT_TO_POINTER(fd), sp); + event_set(&sock->ev, fd, EV_READ | EV_PERSIST, + tcp_srv_event, sock); sock->fd = fd; sock->cfg = cfg; list_add_tail(&sock->sockets_node, &chunkd_srv.sockets); + if (event_add(&sock->ev, NULL) < 0) + goto err_out_sock; + return fd; +err_out_sock: + free(sock); err_out_fd: close(fd); return rc; @@ -1721,121 +1670,33 @@ bool worker_pipe_signal(struct worker_info *wi) return true; } -static bool worker_pipe_evt(int fd, short events, void *userdata) +static void worker_pipe_evt(int fd, short events, void *userdata) { struct worker_info *wi = NULL; if (read(fd, &wi, sizeof(wi)) != sizeof(wi)) { applog(LOG_ERR, "worker pipe input failed: %s", strerror(errno)); - return false; + return; } wi->pipe_ev(wi); - - if (!srv_poll_ready(fd)) - applog(LOG_ERR, "unable to ready worker fd for polling"); - - return true; -} - -static void fill_poll_arr(gpointer key, gpointer val, gpointer userdata) -{ - int fd = GPOINTER_TO_INT(key); - struct server_poll *sp = val; - GArray *pollers = userdata; - struct pollfd pfd; - - if (sp->busy || !sp->events) - return; - - pfd.fd = fd; - pfd.events = sp->events; - pfd.revents = 0; - - g_array_append_val(pollers, pfd); } static int main_loop(void) { - GArray *pollers; - time_t next_timeout; - - pollers = g_array_sized_new(FALSE, FALSE, sizeof(struct pollfd), 200); - if (!pollers) - return 1; - - next_timeout = timers_run(); + int rc = 0; while (server_running) { - struct pollfd *pfd; - int i, fired, rc; - - /* zero, then refill poll array */ - g_array_set_size(pollers, 0); - g_hash_table_foreach(chunkd_srv.fd_info, fill_poll_arr,pollers); - - /* poll for fd activity, or next timer event */ - rc = poll(&g_array_index(pollers, struct pollfd, 0), - pollers->len, - next_timeout ? (next_timeout * 1000) : -1); - if (rc < 0) { - syslogerr("poll"); - if (errno != EINTR) - break; - } - - gettimeofday(¤t_time, NULL); - - /* determine which fd's fired; call their callbacks */ - fired = 0; - for (i = 0; i < pollers->len; i++) { - struct server_poll *sp; - bool runrunrun; - - /* ref pollfd struct */ - pfd = &g_array_index(pollers, struct pollfd, i); - - /* if no events fired, move on to next */ - if (!pfd->revents) - continue; - - fired++; - - sp = srv_poll_lookup(pfd->fd); - if (G_UNLIKELY(!sp)) { - /* BUG! */ - continue; - } - - sp->busy = true; - - /* call callback, shutting down server if requested */ - runrunrun = sp->cb(pfd->fd, pfd->revents, sp->userdata); - if (!runrunrun) { - server_running = false; - break; - } - - /* if we reached poll(2) activity count, it is - * pointless to continue looping - */ - if (fired == rc) - break; - } + event_dispatch(); if (dump_stats) { dump_stats = false; stats_dump(); } - - next_timeout = timers_run(); } - - if (strict_free) - g_array_free(pollers, TRUE); - - return 0; + + return rc; } int main (int argc, char *argv[]) @@ -1877,6 +1738,7 @@ int main (int argc, char *argv[]) g_thread_init(NULL); chunkd_srv.bigmutex = g_mutex_new(); SSL_library_init(); + chunkd_srv.evbase_main = event_init(); /* init SSL */ SSL_load_error_strings(); @@ -1927,21 +1789,13 @@ int main (int argc, char *argv[]) signal(SIGTERM, term_signal); signal(SIGUSR1, stats_signal); - chunkd_srv.fd_info = g_hash_table_new_full(g_direct_hash, - g_direct_equal, - NULL, free); - if (!chunkd_srv.fd_info) { - rc = 1; - goto err_out_session; - } - chunkd_srv.max_workers = 10; chunkd_srv.workers = g_thread_pool_new(worker_thread, NULL, chunkd_srv.max_workers, FALSE, NULL); if (!chunkd_srv.workers) { rc = 1; - goto err_out_fd_info; + goto err_out_session; } if (objcache_init(&chunkd_srv.actives) != 0) { @@ -1959,9 +1813,11 @@ int main (int argc, char *argv[]) rc = 1; goto err_out_chk_pipe; } - if (!pipe_watch(chunkd_srv.worker_pipe[0], worker_pipe_evt, NULL)) { + event_set(&chunkd_srv.worker_ev, chunkd_srv.worker_pipe[0], + EV_READ | EV_PERSIST, worker_pipe_evt, NULL); + if (event_add(&chunkd_srv.worker_ev, NULL) < 0) { rc = 1; - goto err_out_chk_pipe; + goto err_out_worker_pipe; } if (fs_open()) { @@ -2006,9 +1862,6 @@ err_out_objcache: err_out_workers: if (strict_free) g_thread_pool_free(chunkd_srv.workers, TRUE, FALSE); -err_out_fd_info: - if (strict_free) - g_hash_table_destroy(chunkd_srv.fd_info); err_out_session: unlink(chunkd_srv.pid_file); close(chunkd_srv.pid_fd); diff --git a/chunkd/util.c b/chunkd/util.c index 82995be..1a7a932 100644 --- a/chunkd/util.c +++ b/chunkd/util.c @@ -193,29 +193,6 @@ char *time2str(char *strbuf, time_t src_time) return strbuf; } -struct cld_timer_list timer_list; - -void timer_init(struct cld_timer *timer, const char *name, - void (*cb)(struct cld_timer *), void *userdata) -{ - cld_timer_init(timer, name, cb, userdata); -} - -void timer_add(struct cld_timer *timer, time_t expires) -{ - cld_timer_add(&timer_list, timer, expires); -} - -void timer_del(struct cld_timer *timer) -{ - cld_timer_del(&timer_list, timer); -} - -time_t timers_run(void) -{ - return cld_timers_run(&timer_list); -} - #ifndef HAVE_STRNLEN size_t strnlen(const char *s, size_t maxlen) { diff --git a/configure.ac b/configure.ac index 32b23de..67929da 100644 --- a/configure.ac +++ b/configure.ac @@ -84,6 +84,8 @@ AC_CHECK_LIB(db-5.0, db_create, DB4_LIBS=-ldb-5.0, AC_CHECK_LIB(db-4.4, db_create, DB4_LIBS=-ldb-4.4, AC_CHECK_LIB(db-4.3, db_create, DB4_LIBS=-ldb-4.3, [AC_MSG_ERROR([Missing required libdb 4.x])])))))))) +AC_CHECK_LIB(event, event_base_new, EVENT_LIBS=-levent, + [AC_MSG_ERROR([Missing required libevent])]) AC_CHECK_LIB(crypto, MD5_Init, CRYPTO_LIBS=-lcrypto) AC_CHECK_LIB(ssl, SSL_new, SSL_LIBS=-lssl) AC_SEARCH_LIBS(argp_parse, argp) @@ -133,6 +135,7 @@ AC_SUBST(DB4_LIBS) AC_SUBST(CRYPTO_LIBS) AC_SUBST(SSL_LIBS) AC_SUBST(HAIL_FUSE_PROGS) +AC_SUBST(EVENT_LIBS) AC_CONFIG_FILES([ Makefile -- To unsubscribe from this list: send the line "unsubscribe hail-devel" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html