/** * @file memnotify.c * Memory use tracking and notification plugin for the Mode Control Entity *

* Copyright (c) 2014 - 2021 Jolla Ltd. * * @author Simo Piiroinen * * mce is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * mce 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mce. If not, see . */ #include "memnotify.h" #include "../mce.h" #include "../mce-log.h" #include "../mce-setting.h" #include #include #include #include /* ========================================================================= * * GENERIC_UTILITIES * ========================================================================= */ static int memnotify_char_is_black (int ch); static int memnotify_char_is_white (int ch); static char *memnotify_token_parse (char **ppos); static guint memnotify_iowatch_add (int fd, bool close_on_unref, GIOCondition cnd, GIOFunc io_cb, gpointer aptr); /* ========================================================================= * * LIMIT_OBJECTS * ========================================================================= */ /** Structure for holding for /dev/memnotify compatible limit data */ typedef struct { /** Estimate of number of non-discardable RAM pages */ gint mnl_used; /** Number of active RAM pages (TODO: what is it?) */ gint mnl_active; /** Number of RAM pages the system has*/ gint mnl_total; } memnotify_limit_t; static void memnotify_limit_clear (memnotify_limit_t *self); static int memnotify_limit_repr (const memnotify_limit_t *self, char *data, size_t size); static bool memnotify_limit_parse (memnotify_limit_t *self, const char *data); static bool memnotify_limit_exceeded (const memnotify_limit_t *self, const memnotify_limit_t *that); /* ========================================================================= * * STATUS_EVALUATION * ========================================================================= */ static memnotify_level_t memnotify_status_evaluate_level (void); static void memnotify_status_update_level (void); static void memnotify_status_update_triggers (void); static void memnotify_status_show_triggers (void); /* ========================================================================= * * KERNEL_INTERFACE * ========================================================================= */ /** Structure for holding memnotify device file descriptors etc */ typedef struct { /** Flag for: Slot is not a dummy * * The structures are allocated per notification level. To allow * dummy padding slots to remain zero initialized, this flag must * be set to true for slots that are actually used. */ bool mnd_in_use; /** Device file descriptor for /dev/memnotify * * If mnd_in_use is true, must be initialized to -1. */ int mnd_fd; /** Glib io watch id for mnd_fd * * If mnd_in_use is true, must be initialized to 0. */ guint mnd_rx_id; } memnotify_dev_t; static bool memnotify_dev_is_available (void); static gboolean memnotify_dev_rx_cb (GIOChannel *chn, GIOCondition cnd, gpointer aptr); static void memnotify_dev_close (memnotify_level_t lev); static bool memnotify_dev_open (memnotify_level_t lev); static bool memnotify_dev_set_trigger (memnotify_level_t lev, const memnotify_limit_t *limit); static bool memnotify_dev_get_status (memnotify_level_t lev, memnotify_limit_t *state); /* ========================================================================= * * DYNAMIC_SETTINGS * ========================================================================= */ static void memnotify_setting_cb (GConfClient *const gcc, const guint id, GConfEntry *const entry, gpointer const data); static void memnotify_setting_init (void); static void memnotify_setting_quit (void); /* ========================================================================= * * PLUGIN_INTEFACE * ========================================================================= */ static void memnotify_plugin_quit(void); static bool memnotify_plugin_init(void); G_MODULE_EXPORT const gchar *g_module_check_init (GModule *module); G_MODULE_EXPORT void g_module_unload (GModule *module); /* ========================================================================= * * GENERIC_UTILITIES * ========================================================================= */ /** Simple locale agnostic whitespace character predicate */ static int memnotify_char_is_white(int ch) { return (ch > 0) && (ch <= 32); } /** Simple locale agnostic non-white character predicate */ static int memnotify_char_is_black(int ch) { return (ch > 32); } /** Slice the next sequence of non-white characters from parse position */ static char * memnotify_token_parse(char **ppos) { unsigned char *pos = (unsigned char *)*ppos; // skip leading white space while( memnotify_char_is_white(*pos) ) ++pos; // find non-white part unsigned char *res = pos; while( memnotify_char_is_black(*pos) ) ++pos; if( *pos ) *pos++ = 0; // skip trailing white space while( memnotify_char_is_white(*pos) ) ++pos; return *ppos = (char *)pos, (char *)res; } /** Add a glib I/O notification for a file descriptor */ static guint memnotify_iowatch_add(int fd, bool close_on_unref, GIOCondition cnd, GIOFunc io_cb, gpointer aptr) { guint wid = 0; GIOChannel *chn = 0; if( !(chn = g_io_channel_unix_new(fd)) ) { goto EXIT; } g_io_channel_set_close_on_unref(chn, close_on_unref); cnd |= G_IO_ERR | G_IO_HUP | G_IO_NVAL; if( !(wid = g_io_add_watch(chn, cnd, io_cb, aptr)) ) goto EXIT; EXIT: if( chn != 0 ) g_io_channel_unref(chn); return wid; } /* ========================================================================= * * LIMIT_OBJECTS * ========================================================================= */ /** Reset limit object values */ static void memnotify_limit_clear(memnotify_limit_t *self) { self->mnl_used = 0; self->mnl_active = 0; self->mnl_total = 0; } /** Convert limit object values to /dev/memnotify compatible ascii form */ static int memnotify_limit_repr(const memnotify_limit_t *self, char *data, size_t size) { int res = snprintf(data, size, "used %d active %d total %d", self->mnl_used, self->mnl_active, self->mnl_total); return res; } /** Parse limit object from /dev/memnotify compatible ascii form */ static bool memnotify_limit_parse(memnotify_limit_t *self, const char *data) { bool res = false; char *tmp = 0; if( !self ) goto EXIT; memnotify_limit_clear(self); if( !data ) goto EXIT; if( !(tmp = strdup(data)) ) goto EXIT; res = true; for( char *pos = tmp; *pos; ) { char *key = memnotify_token_parse(&pos); char *val = memnotify_token_parse(&pos); char *end = 0; gint num = (gint)strtol(val, &end, 0); if( *key == 0 ) continue; if( end <= val || *end != 0 ) { mce_log(LL_WARN, "%s: '%s' is not a number", key, val); continue; } if( !strcmp(key, "used") ) { self->mnl_used = num; } else if( !strcmp(key, "active") ) { self->mnl_active = num; } else if( !strcmp(key, "total") ) { self->mnl_total = num; } else { mce_log(LL_DEBUG, "%s: unknown value", key); } } EXIT: free(tmp); return res; } /** Check if limit object values are exceeded by given state data */ static bool memnotify_limit_exceeded(const memnotify_limit_t *self, const memnotify_limit_t *state) { // limit <= state #define X(memb) (self->memb != 0 && self->memb <= state->memb) bool res = (X(mnl_used) || X(mnl_active) || X(mnl_total)); #undef X return res; } /* ========================================================================= * * STATUS_EVALUATION * ========================================================================= */ /** Configuration limits for normal/warning/critical levels */ static memnotify_limit_t memnotify_limit[] = { [MEMNOTIFY_LEVEL_NORMAL] = { .mnl_used = 0, .mnl_active = 0, .mnl_total = 0, }, [MEMNOTIFY_LEVEL_WARNING] = { // values come from config - disabled by default .mnl_used = 0, .mnl_active = 0, .mnl_total = 0, }, [MEMNOTIFY_LEVEL_CRITICAL] = { // values come from config - disabled by default .mnl_used = 0, .mnl_active = 0, .mnl_total = 0, }, }; /** Cached status read from kernel device */ static memnotify_limit_t memnotify_state = { .mnl_used = 0, .mnl_active = 0, .mnl_total = 0, }; /** Cached memory use level */ static memnotify_level_t memnotify_level = MEMNOTIFY_LEVEL_UNKNOWN; /** Check current memory status against triggering levels */ static memnotify_level_t memnotify_status_evaluate_level(void) { memnotify_level_t res = MEMNOTIFY_LEVEL_NORMAL; memnotify_level_t lev = MEMNOTIFY_LEVEL_NORMAL + 1; for( ; lev < G_N_ELEMENTS(memnotify_limit); ++lev ) { if( memnotify_limit_exceeded(memnotify_limit+lev, &memnotify_state) ) res = lev; } return res; } /** Re-evaluate memory use level and broadcast dbus signal if changed */ static void memnotify_status_update_level(void) { memnotify_level_t level = memnotify_status_evaluate_level(); if( memnotify_level == level ) goto EXIT; memnotify_level = level; datapipe_exec_full(&memnotify_level_pipe, GINT_TO_POINTER(level)); EXIT: return; } /** Set kernel side triggering levels and update current status */ static void memnotify_status_update_triggers(void) { /* Program new limits to kernel side */ memnotify_dev_set_trigger(MEMNOTIFY_LEVEL_WARNING, memnotify_limit + MEMNOTIFY_LEVEL_WARNING); memnotify_dev_set_trigger(MEMNOTIFY_LEVEL_CRITICAL, memnotify_limit + MEMNOTIFY_LEVEL_CRITICAL); /* Read current status and re-evaluate level * * The MEMNOTIFY_LEVEL_WARNING is just a slot for which we should * have an open /dev/memnotify file descriptor. */ if( memnotify_dev_get_status(MEMNOTIFY_LEVEL_WARNING, &memnotify_state) ) memnotify_status_update_level(); } /** Log current memory level configuration for debugging purposes */ static void memnotify_status_show_triggers(void) { for( size_t i = 0; i < G_N_ELEMENTS(memnotify_limit); ++i ) { char tmp[256]; memnotify_limit_repr(memnotify_limit+i, tmp, sizeof tmp); mce_log(LL_DEBUG, "%s: %s", memnotify_level_repr(i), tmp); } } /* ========================================================================= * * KERNEL_INTERFACE * ========================================================================= */ /** Path to memonotify device node */ static const char memnotify_dev_path[] = "/dev/memnotify"; /** Tracking data for open /dev/memnotify instances */ static memnotify_dev_t memnotify_dev[MEMNOTIFY_LEVEL_COUNT] = { [MEMNOTIFY_LEVEL_WARNING] = { .mnd_in_use = true, .mnd_fd = -1, .mnd_rx_id = 0, }, [MEMNOTIFY_LEVEL_CRITICAL] = { .mnd_in_use = true, .mnd_fd = -1, .mnd_rx_id = 0, }, /* Note: Any uninitialized slots will have mnd_in_use==false and * are ignored by memnotify_dev_xxx() functions. */ }; /** Probe if the memnotify device node is present */ static bool memnotify_dev_is_available(void) { return access(memnotify_dev_path, R_OK|W_OK) == 0; } /** Input watch callback for memonotify device node */ static gboolean memnotify_dev_rx_cb(GIOChannel *chn, GIOCondition cnd, gpointer aptr) { (void) chn; (void) cnd; memnotify_level_t lev = GPOINTER_TO_INT(aptr); gboolean keep_going = FALSE; if( !memnotify_dev[lev].mnd_rx_id ) goto EXIT; mce_log(LL_DEBUG, "notify trigger (%s)", memnotify_level_repr(lev)); if( cnd & ~G_IO_IN ) { mce_log(LL_WARN, "unexpected input watch condition"); goto EXIT; } if( !memnotify_dev_get_status(lev, &memnotify_state) ) goto EXIT; keep_going = TRUE; memnotify_status_update_level(); EXIT: if( !keep_going && memnotify_dev[lev].mnd_rx_id ) { memnotify_dev[lev].mnd_rx_id = 0; mce_log(LL_CRIT, "disabling input watch"); } return keep_going; } /** Close memonotify device node and remove associated io watch */ static void memnotify_dev_close(memnotify_level_t lev) { if( !memnotify_dev[lev].mnd_in_use ) goto EXIT; if( memnotify_dev[lev].mnd_rx_id ) { g_source_remove(memnotify_dev[lev].mnd_rx_id), memnotify_dev[lev].mnd_rx_id = 0; } if( memnotify_dev[lev].mnd_fd != -1 ) { close(memnotify_dev[lev].mnd_fd), memnotify_dev[lev].mnd_fd = -1; } EXIT: return; } /** Open memonotify device node and install io watch for it */ static bool memnotify_dev_open(memnotify_level_t lev) { bool res = false; if( !memnotify_dev[lev].mnd_in_use ) goto EXIT; if( (memnotify_dev[lev].mnd_fd = open(memnotify_dev_path, O_RDWR)) == -1 ) { mce_log(LL_ERR, "could not open: %s: %m", memnotify_dev_path); goto EXIT; } memnotify_dev[lev].mnd_rx_id = memnotify_iowatch_add(memnotify_dev[lev].mnd_fd, false, G_IO_IN, memnotify_dev_rx_cb, GINT_TO_POINTER(lev)); if( !memnotify_dev[lev].mnd_rx_id ) { mce_log(LL_ERR, "could add iowatch: %s", memnotify_dev_path); goto EXIT; } if( !memnotify_dev_get_status(lev, &memnotify_state) ) goto EXIT; res = true; memnotify_status_update_level(); EXIT: // all or nothing if( !res ) memnotify_dev_close(lev); return res; } static void memnotify_dev_close_all(void) { for( memnotify_level_t lev = 0; lev < MEMNOTIFY_LEVEL_COUNT; ++lev ) memnotify_dev_close(lev); } static bool memnotify_dev_open_all(void) { bool res = false; for( memnotify_level_t lev = 0; lev < MEMNOTIFY_LEVEL_COUNT; ++lev ) { if( !memnotify_dev[lev].mnd_in_use ) continue; if( !memnotify_dev_open(lev) ) goto EXIT; } res = true; EXIT: // all or nothing if( !res ) memnotify_dev_close_all(); return res; } /** Program kernel side memory use notification limits */ static bool memnotify_dev_set_trigger(memnotify_level_t lev, const memnotify_limit_t *limit) { bool res = false; char tmp[256]; if( memnotify_dev[lev].mnd_fd == -1 ) goto EXIT; int todo = memnotify_limit_repr(limit, tmp, sizeof tmp); if( todo <= 0 ) goto EXIT; int done = write(memnotify_dev[lev].mnd_fd, tmp, todo); if( done != todo ) goto EXIT; mce_log(LL_DEBUG, "write %s -> %s", memnotify_level_repr(lev), tmp); res = true; EXIT: return res; } /** Read current memory use status from kernel side */ static bool memnotify_dev_get_status(memnotify_level_t lev, memnotify_limit_t *state) { bool res = false; char tmp[256]; if( memnotify_dev[lev].mnd_fd == -1 ) { mce_log(LL_WARN, "device not opened"); goto EXIT; } errno = 0; int done = read(memnotify_dev[lev].mnd_fd, tmp, sizeof tmp - 1); if( done <= 0 ) { mce_log(LL_ERR, "no data: %m"); goto EXIT; } tmp[done-1] = 0; mce_log(LL_DEBUG, "read %s <- %s", memnotify_level_repr(lev), tmp); if( !memnotify_limit_parse(state, tmp) ) goto EXIT; res = true; EXIT: return res; } /* ========================================================================= * * DYNAMIC_SETTINGS * ========================================================================= */ /** GConf notification id for memnotify.warning.used level */ static guint memnotify_setting_warning_used_id = 0; /** GConf notification id for memnotify.warning.active level */ static guint memnotify_setting_warning_active_id = 0; /** GConf notification id for memnotify.critical.used level */ static guint memnotify_setting_critical_used_id = 0; /** GConf notification id for memnotify.critical.active level */ static guint memnotify_setting_critical_active_id = 0; /** GConf callback for memnotify related settings * * @param gcc (not used) * @param id Connection ID from gconf_client_notify_add() * @param entry The modified GConf entry * @param data (not used) */ static void memnotify_setting_cb(GConfClient *const gcc, const guint id, GConfEntry *const entry, gpointer const data) { const GConfValue *gcv = gconf_entry_get_value(entry); (void)gcc; (void)data; /* Key is unset */ if (gcv == NULL) { mce_log(LL_DEBUG, "GConf Key `%s' has been unset", gconf_entry_get_key(entry)); goto EXIT; } if( id == memnotify_setting_warning_used_id ) { gint old = memnotify_limit[MEMNOTIFY_LEVEL_WARNING].mnl_used; gint val = gconf_value_get_int(gcv); if( old != val ) { mce_log(LL_DEBUG, "memnotify.warning.used: %d -> %d", old, val); memnotify_limit[MEMNOTIFY_LEVEL_WARNING].mnl_used = val; memnotify_status_update_triggers(); } } else if( id == memnotify_setting_warning_active_id ) { gint old = memnotify_limit[MEMNOTIFY_LEVEL_WARNING].mnl_active; gint val = gconf_value_get_int(gcv); if( old != val ) { mce_log(LL_DEBUG, "memnotify.warning.active: %d -> %d", old, val); memnotify_limit[MEMNOTIFY_LEVEL_WARNING].mnl_active = val; memnotify_status_update_triggers(); } } else if( id == memnotify_setting_critical_used_id ) { gint old = memnotify_limit[MEMNOTIFY_LEVEL_CRITICAL].mnl_used; gint val = gconf_value_get_int(gcv); if( old != val ) { mce_log(LL_DEBUG, "memnotify.critical.used: %d -> %d", old, val); memnotify_limit[MEMNOTIFY_LEVEL_CRITICAL].mnl_used = val; memnotify_status_update_triggers(); } } else if( id == memnotify_setting_critical_active_id ) { gint old = memnotify_limit[MEMNOTIFY_LEVEL_CRITICAL].mnl_active; gint val = gconf_value_get_int(gcv); if( old != val ) { mce_log(LL_DEBUG, "memnotify.critical.active: %d -> %d", old, val); memnotify_limit[MEMNOTIFY_LEVEL_CRITICAL].mnl_active = val; memnotify_status_update_triggers(); } } else { mce_log(LL_WARN, "Spurious GConf value received; confused!"); } EXIT: return; } /** Get initial setting values and start tracking changes */ static void memnotify_setting_init(void) { /* memnotify.warning.used level */ mce_setting_notifier_add(MCE_SETTING_MEMNOTIFY_WARNING_PATH, MCE_SETTING_MEMNOTIFY_WARNING_USED, memnotify_setting_cb, &memnotify_setting_warning_used_id); mce_setting_get_int(MCE_SETTING_MEMNOTIFY_WARNING_USED, &memnotify_limit[MEMNOTIFY_LEVEL_WARNING].mnl_used); /* memnotify.warning.active level */ mce_setting_notifier_add(MCE_SETTING_MEMNOTIFY_WARNING_PATH, MCE_SETTING_MEMNOTIFY_WARNING_ACTIVE, memnotify_setting_cb, &memnotify_setting_warning_active_id); mce_setting_get_int(MCE_SETTING_MEMNOTIFY_WARNING_ACTIVE, &memnotify_limit[MEMNOTIFY_LEVEL_WARNING].mnl_active); /* memnotify.critical.used level */ mce_setting_notifier_add(MCE_SETTING_MEMNOTIFY_CRITICAL_PATH, MCE_SETTING_MEMNOTIFY_CRITICAL_USED, memnotify_setting_cb, &memnotify_setting_critical_used_id); mce_setting_get_int(MCE_SETTING_MEMNOTIFY_CRITICAL_USED, &memnotify_limit[MEMNOTIFY_LEVEL_CRITICAL].mnl_used); /* memnotify.critical.active level */ mce_setting_notifier_add(MCE_SETTING_MEMNOTIFY_CRITICAL_PATH, MCE_SETTING_MEMNOTIFY_CRITICAL_ACTIVE, memnotify_setting_cb, &memnotify_setting_critical_active_id); mce_setting_get_int(MCE_SETTING_MEMNOTIFY_CRITICAL_ACTIVE, &memnotify_limit[MEMNOTIFY_LEVEL_CRITICAL].mnl_active); memnotify_status_show_triggers(); } /** Stop tracking setting changes */ static void memnotify_setting_quit(void) { mce_setting_notifier_remove(memnotify_setting_warning_used_id), memnotify_setting_warning_used_id = 0; mce_setting_notifier_remove(memnotify_setting_warning_active_id), memnotify_setting_warning_active_id = 0; mce_setting_notifier_remove(memnotify_setting_critical_used_id), memnotify_setting_critical_used_id = 0; mce_setting_notifier_remove(memnotify_setting_critical_active_id), memnotify_setting_critical_active_id = 0; } /* ========================================================================= * * PLUGIN_INTEFACE * ========================================================================= */ static void memnotify_plugin_quit(void) { memnotify_dev_close_all(); memnotify_setting_quit(); } static bool memnotify_plugin_init(void) { bool success = false; memnotify_setting_init(); if( !memnotify_dev_open_all() ) goto EXIT; memnotify_status_update_triggers(); success = true; EXIT: /* All or nothing */ if( !success ) memnotify_plugin_quit(); return success; } /** Init function for the memnotify plugin * * @param module (not used) * * @return NULL on success, a string with an error message on failure */ const gchar *g_module_check_init(GModule *module) { (void)module; /* Check if some memory pressure plugin has already taken over */ memnotify_level_t level = datapipe_get_gint(memnotify_level_pipe); if( level != MEMNOTIFY_LEVEL_UNKNOWN ) { mce_log(LL_DEBUG, "level already set to %s; memnotify disabled", memnotify_level_repr(level)); goto EXIT; } /* Do not even attempt to set up tracking if the memnotify * device node is not available */ if( !memnotify_dev_is_available() ) { /* Since it is expectional that /dev/memnotify is present, * we must not complain about it missing in default verbosity * level */ mce_log(LL_NOTICE, "memnotify not available"); /* The plugin stays loaded, but no signals are emitted and * level query will return "unknown". */ goto EXIT; } /* Initialize */ if( !memnotify_plugin_init() ) { mce_log(LL_WARN, "memnotify plugin init failed"); goto EXIT; } mce_log(LL_NOTICE, "memnotify plugin active"); EXIT: return NULL; } /** Exit function for the memnotify plugin * * @param module (not used) */ void g_module_unload(GModule *module) { (void)module; mce_log(LL_DEBUG, "unloading memnotify plugin"); memnotify_plugin_quit(); return; }