diff options
Diffstat (limited to 'misc-utils/lsfd.c')
-rw-r--r-- | misc-utils/lsfd.c | 794 |
1 files changed, 608 insertions, 186 deletions
diff --git a/misc-utils/lsfd.c b/misc-utils/lsfd.c index f8537a7..771daef 100644 --- a/misc-utils/lsfd.c +++ b/misc-utils/lsfd.c @@ -31,36 +31,58 @@ #include <getopt.h> #include <ctype.h> #include <search.h> +#include <poll.h> +#include <sys/select.h> +#include <sys/uio.h> #include <linux/sched.h> #include <sys/syscall.h> -#include <linux/kcmp.h> + +#ifdef HAVE_LINUX_KCMP_H +# include <linux/kcmp.h> static int kcmp(pid_t pid1, pid_t pid2, int type, unsigned long idx1, unsigned long idx2) { return syscall(SYS_kcmp, pid1, pid2, type, idx1, idx2); } +#else +# ifndef KCMP_FS +# define KCMP_FS 0 +# endif +# ifndef KCMP_VM +# define KCMP_VM 0 +# endif +# ifndef KCMP_FILES +# define KCMP_FILES 0 +# endif +static int kcmp(pid_t pid1 __attribute__((__unused__)), + pid_t pid2 __attribute__((__unused__)), + int type __attribute__((__unused__)), + unsigned long idx1 __attribute__((__unused__)), + unsigned long idx2 __attribute__((__unused__))) +{ + /* lsfd uses kcmp only for optimization. If the platform doesn't provide + * kcmp, just returning an error is acceptable. */ + errno = ENOSYS; + return -1; +} +#endif /* See proc(5). * Defined in linux/include/linux/sched.h private header file. */ #define PF_KTHREAD 0x00200000 /* I am a kernel thread */ #include "c.h" -#include "nls.h" -#include "xalloc.h" #include "list.h" #include "closestream.h" +#include "column-list-table.h" #include "strutils.h" #include "procfs.h" #include "fileutils.h" #include "idcache.h" #include "pathnames.h" -#include "libsmartcols.h" - #include "lsfd.h" -#include "lsfd-filter.h" -#include "lsfd-counter.h" /* * /proc/$pid/mountinfo entries @@ -129,6 +151,27 @@ static const struct colinfo infos[] = { [COL_BLKDRV] = { "BLKDRV", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("block device driver name resolved by /proc/devices") }, + [COL_BPF_MAP_ID] = { "BPF-MAP.ID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("bpf map id associated with the fd") }, + [COL_BPF_MAP_TYPE] = { "BPF-MAP.TYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("bpf map type (decoded)") }, + [COL_BPF_MAP_TYPE_RAW]= { "BPF-MAP.TYPE.RAW", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("bpf map type (raw)") }, + [COL_BPF_NAME] = { "BPF.NAME", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("bpf object name") }, + [COL_BPF_PROG_ID] = { "BPF-PROG.ID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("bpf program id associated with the fd") }, + [COL_BPF_PROG_TYPE] = { "BPF-PROG.TYPE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("bpf program type (decoded)") }, + [COL_BPF_PROG_TYPE_RAW]= { "BPF-PROG.TYPE.RAW", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("bpf program type (raw)") }, [COL_CHRDRV] = { "CHRDRV", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("character device driver name resolved by /proc/devices") }, @@ -147,18 +190,21 @@ static const struct colinfo infos[] = { [COL_ENDPOINTS] = { "ENDPOINTS", 0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING, N_("IPC endpoints information communicated with the fd") }, - [COL_FLAGS] = { "FLAGS", - 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, - N_("flags specified when opening the file") }, + [COL_EVENTFD_ID] = {"EVENTFD.ID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("eventfd ID") }, + [COL_EVENTPOLL_TFDS] = {"EVENTPOLL.TFDS", + 0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_NUMBER, + N_("file descriptors targeted by the eventpoll file") }, [COL_FD] = { "FD", 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("file descriptor for the file") }, + [COL_FLAGS] = { "FLAGS", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("flags specified when opening the file") }, [COL_FUID] = { "FUID", 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("user ID number of the file's owner") }, - [COL_INODE] = { "INODE", - 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, - N_("inode number") }, [COL_INET_LADDR] = { "INET.LADDR", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("local IP address") }, @@ -171,6 +217,15 @@ static const struct colinfo infos[] = { [COL_INET6_RADDR] = { "INET6.RADDR", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("remote IPv6 address") }, + [COL_INODE] = { "INODE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("inode number") }, + [COL_INOTIFY_INODES] = { "INOTIFY.INODES", + 0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING, + N_("list of monitoring inodes (cooked)") }, + [COL_INOTIFY_INODES_RAW]={ "INOTIFY.INODES.RAW", + 0, SCOLS_FL_WRAP, SCOLS_JSON_ARRAY_STRING, + N_("list of monitoring inodes (raw, don't decode devices)") }, [COL_KNAME] = { "KNAME", 0.4, SCOLS_FL_TRUNC, SCOLS_JSON_STRING, N_("name of the file (raw)") }, @@ -243,12 +298,18 @@ static const struct colinfo infos[] = { [COL_POS] = { "POS", 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("file position") }, + [COL_PTMX_TTY_INDEX] = { "PTMX.TTY-INDEX", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, + N_("tty index of the counterpart") }, [COL_RAW_PROTOCOL] = { "RAW.PROTOCOL", 0, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("protocol number of the raw socket") }, [COL_RDEV] = { "RDEV", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("device ID (if special file)") }, + [COL_SIGNALFD_MASK] = { "SIGNALFD.MASK", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("masked signals") }, [COL_SIZE] = { "SIZE", 4, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("file size"), }, @@ -261,12 +322,15 @@ static const struct colinfo infos[] = { [COL_SOCK_PROTONAME] = { "SOCK.PROTONAME", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("protocol name") }, + [COL_SOCK_SHUTDOWN] = { "SOCK.SHUTDOWN", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("shutdown state of socket ([-r?][-w?])") }, [COL_SOCK_STATE] = { "SOCK.STATE", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, - N_("State of socket") }, + N_("state of socket") }, [COL_SOCK_TYPE] = { "SOCK.TYPE", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, - N_("Type of socket") }, + N_("type of socket") }, [COL_SOURCE] = { "SOURCE", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("file system, partition, or device containing file") }, @@ -288,6 +352,18 @@ static const struct colinfo infos[] = { [COL_TID] = { "TID", 5, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("thread ID of the process opening the file") }, + [COL_TIMERFD_CLOCKID] = { "TIMERFD.CLOCKID", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("clockid") }, + [COL_TIMERFD_INTERVAL] = { "TIMERFD.INTERVAL", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_FLOAT, + N_("interval") }, + [COL_TIMERFD_REMAINING]= { "TIMERFD.REMAINING", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_FLOAT, + N_("remaining time") }, + [COL_TUN_IFACE] = { "TUN.IFACE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("network interface behind the tun device") }, [COL_TYPE] = { "TYPE", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("file type (cooked)") }, @@ -324,6 +400,9 @@ static const struct colinfo infos[] = { [COL_USER] = { "USER", 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("user of the process") }, + [COL_XMODE] = { "XMODE", + 0, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, + N_("extended version of MDOE (rwxD[Ll]m)") }, }; static const int default_columns[] = { @@ -331,7 +410,7 @@ static const int default_columns[] = { COL_PID, COL_USER, COL_ASSOC, - COL_MODE, + COL_XMODE, COL_TYPE, COL_SOURCE, COL_MNT_ID, @@ -345,7 +424,7 @@ static const int default_threads_columns[] = { COL_TID, COL_USER, COL_ASSOC, - COL_MODE, + COL_XMODE, COL_TYPE, COL_SOURCE, COL_MNT_ID, @@ -432,6 +511,12 @@ static const struct counter_spec default_counter_specs[] = { } }; +/* "userdata" used by callback for libsmartcols filter */ +struct filler_data { + struct proc *proc; + struct file *file; +}; + struct lsfd_control { struct libscols_table *tb; /* output */ struct list_head procs; /* list of all processes */ @@ -443,10 +528,11 @@ struct lsfd_control { threads : 1, show_main : 1, /* print main table */ show_summary : 1, /* print summary/counters */ - sockets_only : 1; /* display only SOCKETS */ + sockets_only : 1, /* display only SOCKETS */ + show_xmode : 1; /* XMODE column is enabled. */ - struct lsfd_filter *filter; - struct lsfd_counter **counters; /* NULL terminated array. */ + struct libscols_filter *filter; /* filter */ + struct libscols_filter **ct_filters; /* counters (NULL terminated array) */ }; static void *proc_tree; /* for tsearch/tfind */ @@ -476,13 +562,7 @@ static int column_name_to_id(const char *name, size_t namesz) return i; } warnx(_("unknown column: %s"), name); - - return LSFD_FILTER_UNKNOWN_COL_ID; -} - -static int column_name_to_id_cb(const char *name, void *data __attribute__((__unused__))) -{ - return column_name_to_id(name, strlen(name)); + return -1; } static int get_column_id(int num) @@ -499,12 +579,13 @@ static const struct colinfo *get_column_info(int num) return &infos[ get_column_id(num) ]; } -static struct libscols_column *add_column(struct libscols_table *tb, const struct colinfo *col) +static struct libscols_column *add_column(struct libscols_table *tb, + const struct colinfo *col, int extra) { struct libscols_column *cl; int flags = col->flags; - cl = scols_table_new_column(tb, col->name, col->whint, flags); + cl = scols_table_new_column(tb, col->name, col->whint, flags | extra); if (cl) { scols_column_set_json_type(cl, col->json_type); if (col->flags & SCOLS_FL_WRAP) { @@ -519,7 +600,8 @@ static struct libscols_column *add_column(struct libscols_table *tb, const struc return cl; } -static struct libscols_column *add_column_by_id_cb(struct libscols_table *tb, int colid, void *data) +static struct libscols_column *add_column_by_id(struct lsfd_control *ctl, + int colid, int extra) { struct libscols_column *cl; @@ -528,15 +610,13 @@ static struct libscols_column *add_column_by_id_cb(struct libscols_table *tb, in assert(colid < LSFD_N_COLS); - cl = add_column(tb, infos + colid); + cl = add_column(ctl->tb, infos + colid, extra); if (!cl) err(EXIT_FAILURE, _("failed to allocate output column")); columns[ncolumns++] = colid; - if (colid == COL_TID) { - struct lsfd_control *ctl = data; + if (colid == COL_TID) ctl->threads = 1; - } return cl; } @@ -560,8 +640,8 @@ static void add_mnt_ns(ino_t id) nmax = (nspaces + 16) / 16 * 16; if (nmax <= nspaces + 1) { nmax += 16; - mnt_namespaces = xrealloc(mnt_namespaces, - sizeof(ino_t) * nmax); + mnt_namespaces = xreallocarray(mnt_namespaces, + nmax, sizeof(ino_t)); } mnt_namespaces[nspaces++] = id; } @@ -592,6 +672,9 @@ static const struct file_class *stat2class(struct stat *sb) if (is_nsfs_dev(dev)) return &nsfs_file_class; + if (is_mqueue_dev(dev)) + return &mqueue_file_class; + return &file_class; default: break; @@ -604,7 +687,10 @@ static struct file *new_file(struct proc *proc, const struct file_class *class) { struct file *file; + assert(class); file = xcalloc(1, class->size); + file->class = class; + file->proc = proc; INIT_LIST_HEAD(&file->files); @@ -631,11 +717,6 @@ static struct file *copy_file(struct file *old) static void file_set_path(struct file *file, struct stat *sb, const char *name, int association) { - const struct file_class *class = stat2class(sb); - - assert(class); - - file->class = class; file->association = association; file->name = xstrdup(name); file->stat = *sb; @@ -660,7 +741,7 @@ static void free_file(struct file *file) } -static struct proc *new_process(pid_t pid, struct proc *leader) +static struct proc *new_proc(pid_t pid, struct proc *leader) { struct proc *proc = xcalloc(1, sizeof(*proc)); @@ -670,6 +751,7 @@ static struct proc *new_process(pid_t pid, struct proc *leader) INIT_LIST_HEAD(&proc->files); INIT_LIST_HEAD(&proc->procs); + INIT_LIST_HEAD(&proc->eventpolls); proc->kthread = 0; return proc; @@ -737,11 +819,12 @@ static struct file *collect_file_symlink(struct path_cxt *pc, class = stat2class(&sb); if (sockets_only - /* A nsfs is not a socket but the nsfs can be used to - * collect information from other network namespaces. - * Besed on the information, various columns of sockets. + /* A nsfs file is not a socket but the nsfs file can + * be used as a entry point to collect information from + * other network namespaces. Besed on the information, + * various columns of sockets can be filled. */ - && (class != &sock_class)&& (class != &nsfs_file_class)) + && (class != &sock_class) && (class != &nsfs_file_class)) return NULL; f = new_file(proc, class); file_set_path(f, &sb, sym, assoc); @@ -809,7 +892,7 @@ static void parse_maps_line(struct path_cxt *pc, char *buf, struct proc *proc) "-%"SCNx64 /* end */ " %4[^ ]" /* mode */ " %"SCNx64 /* offset */ - " %lx:%lx" /* maj:min */ + " %lx:%lx" /* maj:min */ " %"SCNu64, /* inode */ &start, &end, modestr, &offset, @@ -841,9 +924,6 @@ static void parse_maps_line(struct path_cxt *pc, char *buf, struct proc *proc) */ goto try_map_files; f = new_file(proc, stat2class(&sb)); - if (!f) - return; - file_set_path(f, &sb, path, -assoc); } else { /* As used in tcpdump, AF_PACKET socket can be mmap'ed. */ @@ -857,9 +937,6 @@ static void parse_maps_line(struct path_cxt *pc, char *buf, struct proc *proc) if (ul_path_readlink(pc, sym, sizeof(sym), map_file) < 0) return; f = new_file(proc, stat2class(&sb)); - if (!f) - return; - file_set_path(f, &sb, sym, -assoc); } @@ -920,7 +997,7 @@ static void collect_execve_file(struct path_cxt *pc, struct proc *proc, static void collect_fs_files(struct path_cxt *pc, struct proc *proc, bool sockets_only) { - enum association assocs[] = { ASSOC_EXE, ASSOC_CWD, ASSOC_ROOT }; + enum association assocs[] = { ASSOC_CWD, ASSOC_ROOT }; const char *names[] = { [ASSOC_CWD] = "cwd", [ASSOC_ROOT] = "root", @@ -929,12 +1006,26 @@ static void collect_fs_files(struct path_cxt *pc, struct proc *proc, sockets_only); } -static void collect_namespace_files(struct path_cxt *pc, struct proc *proc) +static void collect_namespace_files_tophalf(struct path_cxt *pc, struct proc *proc) { enum association assocs[] = { ASSOC_NS_CGROUP, ASSOC_NS_IPC, ASSOC_NS_MNT, + }; + const char *names[] = { + [ASSOC_NS_CGROUP] = "ns/cgroup", + [ASSOC_NS_IPC] = "ns/ipc", + [ASSOC_NS_MNT] = "ns/mnt", + }; + collect_outofbox_files(pc, proc, assocs, names, ARRAY_SIZE(assocs), + /* Namespace information is alwasys needed. */ + false); +} + +static void collect_namespace_files_bottomhalf(struct path_cxt *pc, struct proc *proc) +{ + enum association assocs[] = { ASSOC_NS_NET, ASSOC_NS_PID, ASSOC_NS_PID4C, @@ -944,9 +1035,6 @@ static void collect_namespace_files(struct path_cxt *pc, struct proc *proc) ASSOC_NS_UTS, }; const char *names[] = { - [ASSOC_NS_CGROUP] = "ns/cgroup", - [ASSOC_NS_IPC] = "ns/ipc", - [ASSOC_NS_MNT] = "ns/mnt", [ASSOC_NS_NET] = "ns/net", [ASSOC_NS_PID] = "ns/pid", [ASSOC_NS_PID4C] = "ns/pid_for_children", @@ -977,6 +1065,14 @@ static void free_nodev(struct nodev *nodev) free(nodev); } +void add_nodev(unsigned long minor, const char *filesystem) +{ + struct nodev *nodev = new_nodev(minor, filesystem); + unsigned long slot = nodev->minor % NODEV_TABLE_SIZE; + + list_add_tail(&nodev->nodevs, &nodev_table.tables[slot]); +} + static void initialize_nodevs(void) { int i; @@ -1008,7 +1104,7 @@ const char *get_nodev_filesystem(unsigned long minor) return NULL; } -static void add_nodevs(FILE *mnt) +static void read_mountinfo(FILE *mnt) { /* This can be very long. A line in mountinfo can have more than 3 * paths. */ @@ -1017,15 +1113,12 @@ static void add_nodevs(FILE *mnt) while (fgets(line, sizeof(line), mnt)) { unsigned long major, minor; char filesystem[256]; - struct nodev *nodev; - int slot; - /* 23 61 0:22 / /sys rw,nosuid,nodev,noexec,relatime shared:2 - sysfs sysfs rw,seclabel */ - if(sscanf(line, "%*d %*d %lu:%lu %*s %*s %*s %*[^-] - %s %*[^\n]", + if(sscanf(line, "%*d %*d %lu:%lu %*s %*s %*s %*[^-] - %255s %*[^\n]", &major, &minor, filesystem) != 3) /* 1600 1458 0:55 / / rw,nodev,relatime - overlay overlay rw,context="s... */ - if (sscanf(line, "%*d %*d %lu:%lu %*s %*s %*s - %s %*[^\n]", + if (sscanf(line, "%*d %*d %lu:%lu %*s %*s %*s - %255s %*[^\n]", &major, &minor, filesystem) != 3) continue; @@ -1034,10 +1127,7 @@ static void add_nodevs(FILE *mnt) if (get_nodev_filesystem(minor)) continue; - nodev = new_nodev(minor, filesystem); - slot = minor % NODEV_TABLE_SIZE; - - list_add_tail(&nodev->nodevs, &nodev_table.tables[slot]); + add_nodev(minor, filesystem); } } @@ -1060,6 +1150,15 @@ static void finalize_ipc_table(void) list_free(&ipc_table.tables[i], struct ipc, ipcs, free_ipc); } +struct ipc *new_ipc(const struct ipc_class *class) +{ + struct ipc *ipc = xcalloc(1, class->size); + ipc->class = class; + INIT_LIST_HEAD(&ipc->endpoints); + INIT_LIST_HEAD(&ipc->ipcs); + return ipc; +} + struct ipc *get_ipc(struct file *file) { int slot; @@ -1090,6 +1189,18 @@ void add_ipc(struct ipc *ipc, unsigned int hash) list_add(&ipc->ipcs, &ipc_table.tables[slot]); } +void init_endpoint(struct ipc_endpoint *endpoint) +{ + INIT_LIST_HEAD(&endpoint->endpoints); +} + +void add_endpoint(struct ipc_endpoint *endpoint, struct ipc *ipc) +{ + endpoint->ipc = ipc; + list_add(&endpoint->endpoints, &ipc->endpoints); +} + + static void fill_column(struct proc *proc, struct file *file, struct libscols_line *ln, @@ -1107,6 +1218,18 @@ static void fill_column(struct proc *proc, } } +static int filter_filler_cb( + struct libscols_filter *fltr __attribute__((__unused__)), + struct libscols_line *ln, + size_t colnum, + void *userdata) +{ + struct filler_data *fid = (struct filler_data *) userdata; + + fill_column(fid->proc, fid->file, ln, get_column_id(colnum), colnum); + return 0; +} + static void convert_file(struct proc *proc, struct file *file, struct libscols_line *ln) @@ -1114,8 +1237,11 @@ static void convert_file(struct proc *proc, { size_t i; - for (i = 0; i < ncolumns; i++) + for (i = 0; i < ncolumns; i++) { + if (scols_line_is_filled(ln, i)) + continue; fill_column(proc, file, ln, get_column_id(i), i); + } } static void convert(struct list_head *procs, struct lsfd_control *ctl) @@ -1129,23 +1255,34 @@ static void convert(struct list_head *procs, struct lsfd_control *ctl) list_for_each (f, &proc->files) { struct file *file = list_entry(f, struct file, files); struct libscols_line *ln = scols_table_new_line(ctl->tb, NULL); - struct lsfd_counter **counter = NULL; + struct libscols_filter **ct_fltr = NULL; if (!ln) err(EXIT_FAILURE, _("failed to allocate output line")); + if (ctl->filter) { + int status = 0; + struct filler_data fid = { + .proc = proc, + .file = file + }; + + scols_filter_set_filler_cb(ctl->filter, + filter_filler_cb, (void *) &fid); + if (scols_line_apply_filter(ln, ctl->filter, &status)) + err(EXIT_FAILURE, _("failed to apply filter")); + if (status == 0) { + scols_table_remove_line(ctl->tb, ln); + continue; + } + } convert_file(proc, file, ln); - if (!lsfd_filter_apply(ctl->filter, ln)) { - scols_table_remove_line(ctl->tb, ln); - continue; - } - - if (!ctl->counters) + if (!ctl->ct_filters) continue; - for (counter = ctl->counters; *counter; counter++) - lsfd_counter_accumulate(*counter, ln); + for (ct_fltr = ctl->ct_filters; *ct_fltr; ct_fltr++) + scols_line_apply_filter(ln, *ct_fltr, NULL); } } } @@ -1161,12 +1298,13 @@ static void delete(struct list_head *procs, struct lsfd_control *ctl) list_free(procs, struct proc, procs, free_proc); scols_unref_table(ctl->tb); - lsfd_filter_free(ctl->filter); - if (ctl->counters) { - struct lsfd_counter **counter; - for (counter = ctl->counters; *counter; counter++) - lsfd_counter_free(*counter); - free(ctl->counters); + scols_unref_filter(ctl->filter); + + if (ctl->ct_filters) { + struct libscols_filter **ct_fltr; + for (ct_fltr = ctl->ct_filters; *ct_fltr; ct_fltr++) + scols_unref_filter(*ct_fltr); + free(ctl->ct_filters); } } @@ -1365,6 +1503,169 @@ unsigned long add_name(struct name_manager *nm, const char *name) return e->id; } +static void walk_threads(struct lsfd_control *ctl, struct path_cxt *pc, + pid_t pid, struct proc *proc, + void (*cb)(struct lsfd_control *, struct path_cxt *, + pid_t, struct proc *)) +{ + DIR *sub = NULL; + pid_t tid = 0; + + while (procfs_process_next_tid(pc, &sub, &tid) == 0) { + if (tid == pid) + continue; + (*cb)(ctl, pc, tid, proc); + } +} + +static int pollfdcmp(const void *a, const void *b) +{ + const struct pollfd *apfd = a, *bpfd = b; + + return apfd->fd - bpfd->fd; +} + +static void mark_poll_fds_as_multiplexed(char *buf, + pid_t pid, struct proc *proc) +{ + long fds; + long nfds; + + struct iovec local; + struct iovec remote; + ssize_t n; + + struct list_head *f; + + if (sscanf(buf, "%lx %lx", &fds, &nfds) != 2) + return; + + if (nfds == 0) + return; + + local.iov_len = sizeof(struct pollfd) * nfds; + local.iov_base = xmalloc(local.iov_len); + remote.iov_len = local.iov_len; + remote.iov_base = (void *)fds; + + n = process_vm_readv(pid, &local, 1, &remote, 1, 0); + if (n < 0 || ((size_t)n) != local.iov_len) + goto out; + + qsort(local.iov_base, nfds, sizeof(struct pollfd), pollfdcmp); + + list_for_each (f, &proc->files) { + struct file *file = list_entry(f, struct file, files); + if (is_opened_file(file) && !file->multiplexed) { + int fd = file->association; + if (bsearch(&(struct pollfd){.fd = fd,}, local.iov_base, + nfds, sizeof(struct pollfd), pollfdcmp)) + file->multiplexed = 1; + } + } + + out: + free(local.iov_base); +} + +static void mark_select_fds_as_multiplexed(char *buf, + pid_t pid, struct proc *proc) +{ + long nfds; + long fds[3]; + + struct iovec local[3]; + fd_set local_set[3]; + struct iovec remote[3]; + ssize_t n; + ssize_t expected_n = 0; + + struct list_head *f; + + if (sscanf(buf, "%lx %lx %lx %lx", &nfds, fds + 0, fds + 1, fds + 2) != 4) + return; + + if (nfds == 0) + return; + + for (int i = 0; i < 3; i++) { + /* If the remote address for the fd_set is 0x0, no set is tehre. */ + remote[i].iov_len = local[i].iov_len = fds[i]? sizeof(local_set[i]): 0; + expected_n += (ssize_t)local[i].iov_len; + local[i].iov_base = local_set + i; + remote[i].iov_base = (void *)(fds[i]); + } + + n = process_vm_readv(pid, local, 3, remote, 3, 0); + if (n < 0 || n != expected_n) + return; + + list_for_each (f, &proc->files) { + struct file *file = list_entry(f, struct file, files); + if (is_opened_file(file) && !file->multiplexed) { + int fd = file->association; + if (nfds <= fd) + continue; + if ((fds[0] && FD_ISSET(fd, (fd_set *)local[0].iov_base)) + || (fds[1] && FD_ISSET(fd, (fd_set *)local[1].iov_base)) + || (fds[2] && FD_ISSET(fd, (fd_set *)local[2].iov_base))) + file->multiplexed = 1; + } + } +} + +static void parse_proc_syscall(struct lsfd_control *ctl __attribute__((__unused__)), + struct path_cxt *pc, pid_t pid, struct proc *proc) +{ + char buf[BUFSIZ]; + char *ptr = NULL; + long scn; + + if (procfs_process_get_syscall(pc, buf, sizeof(buf)) <= 0) + return; + + errno = 0; + scn = strtol(buf, &ptr, 10); + if (errno) + return; + if (scn < 0) + return; + + switch (scn) { +#ifdef SYS_poll + case SYS_poll: + mark_poll_fds_as_multiplexed(ptr, pid, proc); + break; +#endif +#ifdef SYS_ppoll + case SYS_ppoll: + mark_poll_fds_as_multiplexed(ptr, pid, proc); + break; +#endif +#ifdef SYS_ppoll_time64 + case SYS_ppoll_time64: + mark_poll_fds_as_multiplexed(ptr, pid, proc); + break; +#endif + +#ifdef SYS_select + case SYS_select: + mark_select_fds_as_multiplexed(ptr, pid, proc); + break; +#endif +#ifdef SYS_pselect6 + case SYS_pselect6: + mark_select_fds_as_multiplexed(ptr, pid, proc); + break; +#endif +#ifdef SYS_pselect6_time64 + case SYS_pselect6_time64: + mark_select_fds_as_multiplexed(ptr, pid, proc); + break; +#endif + } +} + static void read_process(struct lsfd_control *ctl, struct path_cxt *pc, pid_t pid, struct proc *leader) { @@ -1374,7 +1675,7 @@ static void read_process(struct lsfd_control *ctl, struct path_cxt *pc, if (procfs_process_init_path(pc, pid) != 0) return; - proc = new_process(pid, leader); + proc = new_proc(pid, leader); proc->command = procfs_process_get_cmdname(pc, buf, sizeof(buf)) > 0 ? xstrdup(buf) : xstrdup(_("(unknown)")); procfs_process_get_uid(pc, &proc->uid); @@ -1397,6 +1698,10 @@ static void read_process(struct lsfd_control *ctl, struct path_cxt *pc, proc->kthread = !!(flags & PF_KTHREAD); free(pat); } + if (proc->kthread && !ctl->threads) { + free_proc(proc); + goto out; + } collect_execve_file(pc, proc, ctl->sockets_only); @@ -1404,20 +1709,43 @@ static void read_process(struct lsfd_control *ctl, struct path_cxt *pc, || kcmp(proc->leader->pid, proc->pid, KCMP_FS, 0, 0) != 0) collect_fs_files(pc, proc, ctl->sockets_only); + /* Reading /proc/$pid/mountinfo is expensive. + * mnt_namespaces is a table for avoiding reading mountinfo files + * for an identical mnt namespace. + * + * After reading a mountinfo file for a mnt namespace, we store $mnt_id + * identifying the mnt namespace to mnt_namespaces. + * + * Before reading a mountinfo, we look up the mnt_namespaces with $mnt_id + * as a key. If we find the key, we can skip the reading. + * + * To utilize mnt_namespaces, we need $mnt_id. + * So we read /proc/$pid/ns/mnt here. However, we should not read + * /proc/$pid/ns/net here. When reading /proc/$pid/ns/net, we need + * the information about backing device of "nsfs" file system. + * The information is available in a mountinfo file. + */ + + /* 1/3. read /proc/$pid/ns/mnt */ + if (proc->ns_mnt == 0) + collect_namespace_files_tophalf(pc, proc); + + /* 2/3. read /proc/$pid/mountinfo */ if (proc->ns_mnt == 0 || !has_mnt_ns(proc->ns_mnt)) { FILE *mnt = ul_path_fopen(pc, "r", "mountinfo"); if (mnt) { - add_nodevs(mnt); + read_mountinfo(mnt); if (proc->ns_mnt) add_mnt_ns(proc->ns_mnt); fclose(mnt); } } - collect_namespace_files(pc, proc); + /* 3/3. read /proc/$pid/ns/{the other than mnt} */ + collect_namespace_files_bottomhalf(pc, proc); /* If kcmp is not available, - * there is no way to no whether threads share resources. + * there is no way to know whether threads share resources. * In such cases, we must pay the costs: call collect_mem_files() * and collect_fd_files(). */ @@ -1434,22 +1762,20 @@ static void read_process(struct lsfd_control *ctl, struct path_cxt *pc, if (tsearch(proc, &proc_tree, proc_tree_compare) == NULL) errx(EXIT_FAILURE, _("failed to allocate memory")); + if (ctl->show_xmode) + parse_proc_syscall(ctl, pc, pid, proc); + /* The tasks collecting overwrites @pc by /proc/<task-pid>/. Keep it as * the last path based operation in read_process() */ - if (ctl->threads && leader == NULL) { - DIR *sub = NULL; - pid_t tid = 0; - - while (procfs_process_next_tid(pc, &sub, &tid) == 0) { - if (tid == pid) - continue; - read_process(ctl, pc, tid, proc); - } - } + if (ctl->threads && leader == NULL) + walk_threads(ctl, pc, pid, proc, read_process); + else if (ctl->show_xmode) + walk_threads(ctl, pc, pid, proc, parse_proc_syscall); + out: /* Let's be careful with number of open files */ - ul_path_close_dirfd(pc); + ul_path_close_dirfd(pc); } static void parse_pids(const char *str, pid_t **pids, int *count) @@ -1470,7 +1796,7 @@ static void parse_pids(const char *str, pid_t **pids, int *count) errx(EXIT_FAILURE, _("out of range value for pid specification: %ld"), v); (*count)++; - *pids = xrealloc(*pids, (*count) * sizeof(**pids)); + *pids = xreallocarray(*pids, *count, sizeof(**pids)); (*pids)[*count - 1] = (pid_t)v; while (next && *next != '\0' @@ -1530,44 +1856,66 @@ static void collect_processes(struct lsfd_control *ctl, const pid_t pids[], int ul_unref_path(pc); } +static void __attribute__((__noreturn__)) list_colunms(const char *table_name, + FILE *out, + int raw, + int json) +{ + struct libscols_table *col_tb = xcolumn_list_table_new(table_name, out, raw, json); + + for (size_t i = 0; i < ARRAY_SIZE(infos); i++) + xcolumn_list_table_append_line(col_tb, infos[i].name, + infos[i].json_type, "<boolean>", + _(infos[i].help)); + + scols_print_table(col_tb); + scols_unref_table(col_tb); + + exit(EXIT_SUCCESS); +} + +static void print_columns(FILE *out, const char *prefix, const int cols[], size_t n_cols) +{ + fprintf(out, "%15s: ", prefix); + for (size_t i = 0; i < n_cols; i++) { + if (i) + fputc(',', out); + fputs(infos[cols[i]].name, out); + } + fputc('\n', out); +} + static void __attribute__((__noreturn__)) usage(void) { FILE *out = stdout; - size_t i; fputs(USAGE_HEADER, out); fprintf(out, _(" %s [options]\n"), program_invocation_short_name); fputs(USAGE_OPTIONS, out); - fputs(_(" -l, --threads list in threads level\n"), out); - fputs(_(" -J, --json use JSON output format\n"), out); - fputs(_(" -n, --noheadings don't print headings\n"), out); - fputs(_(" -o, --output <list> output columns\n"), out); - fputs(_(" -r, --raw use raw output format\n"), out); - fputs(_(" -u, --notruncate don't truncate text in columns\n"), out); - fputs(_(" -p, --pid <pid(s)> collect information only specified processes\n"), out); - fputs(_(" -i[4|6], --inet[=4|6] list only IPv4 and/or IPv6 sockets\n"), out); - fputs(_(" -Q, --filter <expr> apply display filter\n"), out); - fputs(_(" --debug-filter dump the internal data structure of filter and exit\n"), out); - fputs(_(" -C, --counter <name>:<expr>\n" - " define custom counter for --summary output\n"), out); - fputs(_(" --dump-counters dump counter definitions\n"), out); - fputs(_(" --summary[=<when>] print summary information (only, append, or never)\n"), out); + fputs(_(" -l, --threads list in threads level\n"), out); + fputs(_(" -J, --json use JSON output format\n"), out); + fputs(_(" -n, --noheadings don't print headings\n"), out); + fputs(_(" -o, --output <list> output columns (see --list-columns)\n"), out); + fputs(_(" -r, --raw use raw output format\n"), out); + fputs(_(" -u, --notruncate don't truncate text in columns\n"), out); + fputs(_(" -p, --pid <pid(s)> collect information only specified processes\n"), out); + fputs(_(" -i[4|6], --inet[=4|=6] list only IPv4 and/or IPv6 sockets\n"), out); + fputs(_(" -Q, --filter <expr> apply display filter\n"), out); + fputs(_(" --debug-filter dump the internal data structure of filter and exit\n"), out); + fputs(_(" -C, --counter <name>:<expr> define custom counter for --summary output\n"), out); + fputs(_(" --dump-counters dump counter definitions\n"), out); + fputs(_(" --summary[=<when>] print summary information (only, append, or never)\n"), out); fputs(USAGE_SEPARATOR, out); - printf(USAGE_HELP_OPTIONS(30)); - - fprintf(out, USAGE_COLUMNS); + fputs(_(" -H, --list-columns list the available columns\n"), out); + fprintf(out, USAGE_HELP_OPTIONS(30)); - for (i = 0; i < ARRAY_SIZE(infos); i++) - fprintf(out, " %16s %-10s%s\n", infos[i].name, - infos[i].json_type == SCOLS_JSON_STRING? "<string>": - infos[i].json_type == SCOLS_JSON_ARRAY_STRING? "<string>": - infos[i].json_type == SCOLS_JSON_NUMBER? "<number>": - "<boolean>", - _(infos[i].help)); + fputs(USAGE_DEFAULT_COLUMNS, out); + print_columns(out, _("Default"), default_columns, ARRAY_SIZE(default_columns)); + print_columns(out, _("With --threads"), default_threads_columns, ARRAY_SIZE(default_threads_columns)); - printf(USAGE_MAN_TAIL("lsfd(1)")); + fprintf(out, USAGE_MAN_TAIL("lsfd(1)")); exit(EXIT_SUCCESS); } @@ -1595,24 +1943,49 @@ static void append_filter_expr(char **a, const char *b, bool and) free(tmp); } -static struct lsfd_filter *new_filter(const char *expr, bool debug, const char *err_prefix, struct lsfd_control *ctl) +static struct libscols_filter *new_filter(const char *expr, bool debug, struct lsfd_control *ctl) { - struct lsfd_filter *filter; - const char *errmsg; + struct libscols_filter *f; + struct libscols_iter *itr; + int nerrs = 0; + const char *name = NULL; - filter = lsfd_filter_new(expr, ctl->tb, - LSFD_N_COLS, - column_name_to_id_cb, - add_column_by_id_cb, ctl); - errmsg = lsfd_filter_get_errmsg(filter); - if (errmsg) - errx(EXIT_FAILURE, "%s%s", err_prefix, errmsg); - if (debug) { - lsfd_filter_dump(filter, stdout); - exit(EXIT_SUCCESS); + f = scols_new_filter(NULL); + if (!f) + err(EXIT_FAILURE, _("failed to allocate filter")); + if (expr && scols_filter_parse_string(f, expr) != 0) + errx(EXIT_FAILURE, _("failed to parse \"%s\": %s"), expr, + scols_filter_get_errmsg(f)); + + itr = scols_new_iter(SCOLS_ITER_FORWARD); + if (!itr) + err(EXIT_FAILURE, _("failed to allocate iterator")); + + while (scols_filter_next_holder(f, itr, &name, 0) == 0) { + struct libscols_column *col = scols_table_get_column_by_name(ctl->tb, name); + + if (!col) { + int id = column_name_to_id(name, strlen(name)); + if (id >= 0) + col = add_column_by_id(ctl, id, SCOLS_FL_HIDDEN); + if (!col) { + nerrs++; /* report all unknown columns */ + continue; + } + } + scols_filter_assign_column(f, itr, name, col); } - return filter; + scols_free_iter(itr); + + if (debug) + scols_dump_filter(f, stdout); + if (nerrs) + exit(EXIT_FAILURE); + if (debug) + exit(EXIT_SUCCESS); + + return f; } static struct counter_spec *new_counter_spec(const char *spec_str) @@ -1660,47 +2033,54 @@ static void free_counter_spec(struct counter_spec *counter_spec) free(counter_spec); } -static struct lsfd_counter *new_counter(const struct counter_spec *spec, struct lsfd_control *ctl) +static struct libscols_filter *new_counter(const struct counter_spec *spec, struct lsfd_control *ctl) { - struct lsfd_filter *filter; + struct libscols_filter *f; + struct libscols_counter *ct; + + f = new_filter(spec->expr, false, ctl); + + ct = scols_filter_new_counter(f); + if (!ct) + err(EXIT_FAILURE, _("failed to allocate counter")); + + scols_counter_set_name(ct, spec->name); + scols_counter_set_func(ct, SCOLS_COUNTER_COUNT); - filter = new_filter(spec->expr, false, - _("failed in making filter for a counter: "), - ctl); - return lsfd_counter_new(spec->name, filter); + return f; } -static struct lsfd_counter **new_counters(struct list_head *specs, struct lsfd_control *ctl) +static struct libscols_filter **new_counters(struct list_head *specs, struct lsfd_control *ctl) { - struct lsfd_counter **counters; + struct libscols_filter **ct_filters; size_t len = list_count_entries(specs); size_t i = 0; struct list_head *s; - counters = xcalloc(len + 1, sizeof(struct lsfd_counter *)); + ct_filters = xcalloc(len + 1, sizeof(struct libscols_filter *)); list_for_each(s, specs) { struct counter_spec *spec = list_entry(s, struct counter_spec, specs); - counters[i++] = new_counter(spec, ctl); + ct_filters[i++] = new_counter(spec, ctl); } - assert(counters[len] == NULL); + assert(ct_filters[len] == NULL); - return counters; + return ct_filters; } -static struct lsfd_counter **new_default_counters(struct lsfd_control *ctl) +static struct libscols_filter **new_default_counters(struct lsfd_control *ctl) { - struct lsfd_counter **counters; + struct libscols_filter **ct_filters; size_t len = ARRAY_SIZE(default_counter_specs); size_t i; - counters = xcalloc(len + 1, sizeof(struct lsfd_counter *)); + ct_filters = xcalloc(len + 1, sizeof(struct libscols_filter *)); for (i = 0; i < len; i++) { const struct counter_spec *spec = default_counter_specs + i; - counters[i] = new_counter(spec, ctl); + ct_filters[i] = new_counter(spec, ctl); } - assert(counters[len] == NULL); + assert(ct_filters[len] == NULL); - return counters; + return ct_filters; } static void dump_default_counter_specs(void) @@ -1758,28 +2138,35 @@ static struct libscols_table *new_summary_table(struct lsfd_control *ctl) return tb; } -static void fill_summary_line(struct libscols_line *ln, struct lsfd_counter *counter) +static void emit_summary(struct lsfd_control *ctl) { - char *str = NULL; + struct libscols_iter *itr; + struct libscols_filter **ct_fltr; + struct libscols_table *tb = new_summary_table(ctl); - xasprintf(&str, "%llu", (unsigned long long)lsfd_counter_value(counter)); - if (!str) - err(EXIT_FAILURE, _("failed to add summary data")); - if (scols_line_refer_data(ln, 0, str)) - err(EXIT_FAILURE, _("failed to add summary data")); + itr = scols_new_iter(SCOLS_ITER_FORWARD); - if (scols_line_set_data(ln, 1, lsfd_counter_name(counter))) - err(EXIT_FAILURE, _("failed to add summary data")); -} + for (ct_fltr = ctl->ct_filters; *ct_fltr; ct_fltr++) { + struct libscols_counter *ct = NULL; -static void emit_summary(struct lsfd_control *ctl, struct lsfd_counter **counter) -{ - struct libscols_table *tb = new_summary_table(ctl); + scols_reset_iter(itr, SCOLS_ITER_FORWARD); + while (scols_filter_next_counter(*ct_fltr, itr, &ct) == 0) { + char *str = NULL; + struct libscols_line *ln; + + ln = scols_table_new_line(tb, NULL); + if (!ln) + err(EXIT_FAILURE, _("failed to allocate summary line")); - for (; *counter; counter++) { - struct libscols_line *ln = scols_table_new_line(tb, NULL); - fill_summary_line(ln, *counter); + xasprintf(&str, "%llu", scols_counter_get_result(ct)); + if (scols_line_refer_data(ln, 0, str)) + err(EXIT_FAILURE, _("failed to add summary data")); + if (scols_line_set_data(ln, 1, scols_counter_get_name(ct))) + err(EXIT_FAILURE, _("failed to add summary data")); + } } + + scols_free_iter(itr); scols_print_table(tb); scols_unref_table(tb); @@ -1801,6 +2188,23 @@ static void attach_xinfos(struct list_head *procs) } } +static void set_multiplexed_flags(struct list_head *procs) +{ + struct list_head *p; + list_for_each (p, procs) { + struct proc *proc = list_entry(p, struct proc, procs); + struct list_head *f; + list_for_each (f, &proc->files) { + struct file *file = list_entry(f, struct file, files); + if (is_opened_file(file) && !file->multiplexed) { + int fd = file->association; + if (is_multiplexed_by_eventpoll(fd, &proc->eventpolls)) + file->multiplexed = 1; + } + } + } +} + /* Filter expressions for implementing -i option. * * To list up the protocol names, use the following command line @@ -1826,7 +2230,7 @@ static const char *inet46_subexpr = INET_SUBEXP_BEGIN int main(int argc, char *argv[]) { - int c; + int c, collist = 0; size_t i; char *outarg = NULL; char *filter_expr = NULL; @@ -1863,6 +2267,7 @@ int main(int argc, char *argv[]) { "summary", optional_argument, NULL, OPT_SUMMARY }, { "counter", required_argument, NULL, 'C' }, { "dump-counters",no_argument, NULL, OPT_DUMP_COUNTERS }, + { "list-columns",no_argument, NULL, 'H' }, { NULL, 0, NULL, 0 }, }; @@ -1871,7 +2276,7 @@ int main(int argc, char *argv[]) textdomain(PACKAGE); close_stdout_atexit(); - while ((c = getopt_long(argc, argv, "no:JrVhluQ:p:i::C:s", longopts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "no:JrVhluQ:p:i::C:sH", longopts, NULL)) != -1) { switch (c) { case 'n': ctl.noheadings = 1; @@ -1943,10 +2348,17 @@ int main(int argc, char *argv[]) print_version(EXIT_SUCCESS); case 'h': usage(); + case 'H': + collist = 1; + break; default: errtryhelp(EXIT_FAILURE); } } + + if (collist) + list_colunms("lsfd-columns", stdout, ctl.raw, ctl.json); /* print and exit */ + if (argv[optind]) errtryhelp(EXIT_FAILURE); @@ -1982,7 +2394,7 @@ int main(int argc, char *argv[]) /* create output columns */ for (i = 0; i < ncolumns; i++) { const struct colinfo *col = get_column_info(i); - struct libscols_column *cl = add_column(ctl.tb, col); + struct libscols_column *cl = add_column(ctl.tb, col, 0); if (!cl) err(EXIT_FAILURE, _("failed to allocate output column")); @@ -1994,9 +2406,9 @@ int main(int argc, char *argv[]) } } - /* make fitler */ + /* make filter */ if (filter_expr) { - ctl.filter = new_filter(filter_expr, debug_filter, "", &ctl); + ctl.filter = new_filter(filter_expr, debug_filter, &ctl); free(filter_expr); } @@ -2011,9 +2423,9 @@ int main(int argc, char *argv[]) /* make counters */ if (ctl.show_summary) { if (list_empty(&counter_specs)) - ctl.counters = new_default_counters(&ctl); + ctl.ct_filters = new_default_counters(&ctl); else { - ctl.counters = new_counters(&counter_specs, &ctl); + ctl.ct_filters = new_counters(&counter_specs, &ctl); list_free(&counter_specs, struct counter_spec, specs, free_counter_spec); } @@ -2022,16 +2434,26 @@ int main(int argc, char *argv[]) if (n_pids > 0) sort_pids(pids, n_pids); - /* collect data */ + if (scols_table_get_column_by_name(ctl.tb, "XMODE")) + ctl.show_xmode = 1; + + /* collect data + * + * The call initialize_ipc_table() must come before + * initialize_classes. + */ initialize_nodevs(); + initialize_ipc_table(); initialize_classes(); initialize_devdrvs(); - initialize_ipc_table(); collect_processes(&ctl, pids, n_pids); free(pids); attach_xinfos(&ctl.procs); + if (ctl.show_xmode) + set_multiplexed_flags(&ctl.procs); + convert(&ctl.procs, &ctl); @@ -2039,15 +2461,15 @@ int main(int argc, char *argv[]) if (ctl.show_main) emit(&ctl); - if (ctl.show_summary && ctl.counters) - emit_summary(&ctl, ctl.counters); + if (ctl.show_summary && ctl.ct_filters) + emit_summary(&ctl); /* cleanup */ delete(&ctl.procs, &ctl); - finalize_ipc_table(); finalize_devdrvs(); finalize_classes(); + finalize_ipc_table(); finalize_nodevs(); return 0; |