1 /*-
2  * Copyright (c) 2005-2006 The FreeBSD Project
3  * All rights reserved.
4  *
5  * Author: Victor Cruceru <soc-victor@freebsd.org>
6  *
7  * Redistribution of this software and documentation and use in source and
8  * binary forms, with or without modification, are permitted provided that
9  * the following conditions are met:
10  *
11  * 1. Redistributions of source code or documentation must retain the above
12  *    copyright notice, this list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in the
15  *    documentation and/or other materials provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27  * SUCH DAMAGE.
28  */
29 
30 /*
31  * Host Resources MIB for SNMPd. Implementation for hrProcessorTable
32  */
33 
34 #include <sys/param.h>
35 #include <sys/sysctl.h>
36 #include <sys/user.h>
37 
38 #include <assert.h>
39 #include <math.h>
40 #include <stdlib.h>
41 #include <string.h>
42 #include <syslog.h>
43 
44 #include "hostres_snmp.h"
45 #include "hostres_oid.h"
46 #include "hostres_tree.h"
47 
48 /*
49  * This structure is used to hold a SNMP table entry
50  * for HOST-RESOURCES-MIB's hrProcessorTable.
51  * Note that index is external being allocated & maintained
52  * by the hrDeviceTable code..
53  */
54 struct processor_entry {
55 	int32_t		index;
56 	const struct asn_oid *frwId;
57 	int32_t		load;		/* average cpu usage */
58 	int32_t		sample_cnt;	/* number of usage samples */
59 	int32_t		cur_sample_idx;	/* current valid sample */
60 	TAILQ_ENTRY(processor_entry) link;
61 	u_char		cpu_no;		/* which cpu, counted from 0 */
62 
63 	/* the samples from the last minute, as required by MIB */
64 	double		samples[MAX_CPU_SAMPLES];
65 	long		states[MAX_CPU_SAMPLES][CPUSTATES];
66 };
67 TAILQ_HEAD(processor_tbl, processor_entry);
68 
69 /* the head of the list with hrDeviceTable's entries */
70 static struct processor_tbl processor_tbl =
71     TAILQ_HEAD_INITIALIZER(processor_tbl);
72 
73 /* number of processors in dev tbl */
74 static int32_t detected_processor_count;
75 
76 /* sysctlbyname(hw.ncpu) */
77 static int hw_ncpu;
78 
79 /* sysctlbyname(kern.cp_times) */
80 static int cpmib[2];
81 static size_t cplen;
82 
83 /* periodic timer used to get cpu load stats */
84 static void *cpus_load_timer;
85 
86 /**
87  * Returns the CPU usage of a given processor entry.
88  *
89  * It needs at least two cp_times "tick" samples to calculate a delta and
90  * thus, the usage over the sampling period.
91  */
92 static int
93 get_avg_load(struct processor_entry *e)
94 {
95 	u_int i, oldest;
96 	long delta = 0;
97 	double usage = 0.0;
98 
99 	assert(e != NULL);
100 
101 	/* Need two samples to perform delta calculation. */
102 	if (e->sample_cnt <= 1)
103 		return (0);
104 
105 	/* Oldest usable index, we wrap around. */
106 	if (e->sample_cnt == MAX_CPU_SAMPLES)
107 		oldest = (e->cur_sample_idx + 1) % MAX_CPU_SAMPLES;
108 	else
109 		oldest = 0;
110 
111 	/* Sum delta for all states. */
112 	for (i = 0; i < CPUSTATES; i++) {
113 		delta += e->states[e->cur_sample_idx][i];
114 		delta -= e->states[oldest][i];
115 	}
116 	if (delta == 0)
117 		return 0;
118 
119 	/* Take idle time from the last element and convert to
120 	 * percent usage by contrasting with total ticks delta. */
121 	usage = (double)(e->states[e->cur_sample_idx][CPUSTATES-1] -
122 	    e->states[oldest][CPUSTATES-1]) / delta;
123 	usage = 100 - (usage * 100);
124 	HRDBG("CPU no. %d, delta ticks %ld, pct usage %.2f", e->cpu_no,
125 	    delta, usage);
126 
127 	return ((int)(usage));
128 }
129 
130 /**
131  * Save a new sample to proc entry and get the average usage.
132  *
133  * Samples are stored in a ringbuffer from 0..(MAX_CPU_SAMPLES-1)
134  */
135 static void
136 save_sample(struct processor_entry *e, long *cp_times)
137 {
138 	int i;
139 
140 	e->cur_sample_idx = (e->cur_sample_idx + 1) % MAX_CPU_SAMPLES;
141 	for (i = 0; cp_times != NULL && i < CPUSTATES; i++)
142 		e->states[e->cur_sample_idx][i] = cp_times[i];
143 
144 	e->sample_cnt++;
145 	if (e->sample_cnt > MAX_CPU_SAMPLES)
146 		e->sample_cnt = MAX_CPU_SAMPLES;
147 
148 	HRDBG("sample count for CPU no. %d went to %d", e->cpu_no, e->sample_cnt);
149 	e->load = get_avg_load(e);
150 
151 }
152 
153 /**
154  * Create a new entry into the processor table.
155  */
156 static struct processor_entry *
157 proc_create_entry(u_int cpu_no, struct device_map_entry *map)
158 {
159 	struct device_entry *dev;
160 	struct processor_entry *entry;
161 	char name[128];
162 
163 	/*
164 	 * If there is no map entry create one by creating a device table
165 	 * entry.
166 	 */
167 	if (map == NULL) {
168 		snprintf(name, sizeof(name), "cpu%u", cpu_no);
169 		if ((dev = device_entry_create(name, "", "")) == NULL)
170 			return (NULL);
171 		dev->flags |= HR_DEVICE_IMMUTABLE;
172 		STAILQ_FOREACH(map, &device_map, link)
173 			if (strcmp(map->name_key, name) == 0)
174 				break;
175 		if (map == NULL)
176 			abort();
177 	}
178 
179 	if ((entry = malloc(sizeof(*entry))) == NULL) {
180 		syslog(LOG_ERR, "hrProcessorTable: %s malloc "
181 		    "failed: %m", __func__);
182 		return (NULL);
183 	}
184 	memset(entry, 0, sizeof(*entry));
185 
186 	entry->index = map->hrIndex;
187 	entry->load = 0;
188 	entry->sample_cnt = 0;
189 	entry->cur_sample_idx = -1;
190 	entry->cpu_no = (u_char)cpu_no;
191 	entry->frwId = &oid_zeroDotZero; /* unknown id FIXME */
192 
193 	INSERT_OBJECT_INT(entry, &processor_tbl);
194 
195 	HRDBG("CPU %d added with SNMP index=%d",
196 	    entry->cpu_no, entry->index);
197 
198 	return (entry);
199 }
200 
201 /**
202  * Scan the device map table for CPUs and create an entry into the
203  * processor table for each CPU.
204  *
205  * Make sure that the number of processors announced by the kernel hw.ncpu
206  * is equal to the number of processors we have found in the device table.
207  */
208 static void
209 create_proc_table(void)
210 {
211 	struct device_map_entry *map;
212 	struct processor_entry *entry;
213 	int cpu_no;
214 	size_t len;
215 
216 	detected_processor_count = 0;
217 
218 	/*
219 	 * Because hrProcessorTable depends on hrDeviceTable,
220 	 * the device detection must be performed at this point.
221 	 * If not, no entries will be present in the hrProcessor Table.
222 	 *
223 	 * For non-ACPI system the processors are not in the device table,
224 	 * therefore insert them after checking hw.ncpu.
225 	 */
226 	STAILQ_FOREACH(map, &device_map, link)
227 		if (strncmp(map->name_key, "cpu", strlen("cpu")) == 0 &&
228 		    strstr(map->location_key, ".CPU") != NULL) {
229 			if (sscanf(map->name_key,"cpu%d", &cpu_no) != 1) {
230 				syslog(LOG_ERR, "hrProcessorTable: Failed to "
231 				    "get cpu no. from device named '%s'",
232 				    map->name_key);
233 				continue;
234 			}
235 
236 			if ((entry = proc_create_entry(cpu_no, map)) == NULL)
237 				continue;
238 
239 			detected_processor_count++;
240 		}
241 
242 	len = sizeof(hw_ncpu);
243 	if (sysctlbyname("hw.ncpu", &hw_ncpu, &len, NULL, 0) == -1 ||
244 	    len != sizeof(hw_ncpu)) {
245 		syslog(LOG_ERR, "hrProcessorTable: sysctl(hw.ncpu) failed");
246 		hw_ncpu = 0;
247 	}
248 
249 	HRDBG("%d CPUs detected via device table; hw.ncpu is %d",
250 	    detected_processor_count, hw_ncpu);
251 
252 	/* XXX Can happen on non-ACPI systems? Create entries by hand. */
253 	for (; detected_processor_count < hw_ncpu; detected_processor_count++)
254 		proc_create_entry(detected_processor_count, NULL);
255 
256 	len = 2;
257 	if (sysctlnametomib("kern.cp_times", cpmib, &len)) {
258 		syslog(LOG_ERR, "hrProcessorTable: sysctlnametomib(kern.cp_times) failed");
259 		cpmib[0] = 0;
260 		cpmib[1] = 0;
261 		cplen = 0;
262 	} else if (sysctl(cpmib, 2, NULL, &len, NULL, 0)) {
263 		syslog(LOG_ERR, "hrProcessorTable: sysctl(kern.cp_times) length query failed");
264 		cplen = 0;
265 	} else {
266 		cplen = len / sizeof(long);
267 	}
268 	HRDBG("%zu entries for kern.cp_times", cplen);
269 
270 }
271 
272 /**
273  * Free the processor table
274  */
275 static void
276 free_proc_table(void)
277 {
278 	struct processor_entry *n1;
279 
280 	while ((n1 = TAILQ_FIRST(&processor_tbl)) != NULL) {
281 		TAILQ_REMOVE(&processor_tbl, n1, link);
282 		free(n1);
283 		detected_processor_count--;
284 	}
285 
286 	assert(detected_processor_count == 0);
287 	detected_processor_count = 0;
288 }
289 
290 /**
291  * Refresh all values in the processor table. We call this once for
292  * every PDU that accesses the table.
293  */
294 static void
295 refresh_processor_tbl(void)
296 {
297 	struct processor_entry *entry;
298 	size_t size;
299 
300 	long pcpu_cp_times[cplen];
301 	memset(pcpu_cp_times, 0, sizeof(pcpu_cp_times));
302 
303 	size = cplen * sizeof(long);
304 	if (sysctl(cpmib, 2, pcpu_cp_times, &size, NULL, 0) == -1 &&
305 	    !(errno == ENOMEM && size >= cplen * sizeof(long))) {
306 		syslog(LOG_ERR, "hrProcessorTable: sysctl(kern.cp_times) failed");
307 		return;
308 	}
309 
310 	TAILQ_FOREACH(entry, &processor_tbl, link) {
311 		assert(hr_kd != NULL);
312 		save_sample(entry, &pcpu_cp_times[entry->cpu_no * CPUSTATES]);
313 	}
314 
315 }
316 
317 /**
318  * This function is called MAX_CPU_SAMPLES times per minute to collect the
319  * CPU load.
320  */
321 static void
322 get_cpus_samples(void *arg __unused)
323 {
324 
325 	HRDBG("[%llu] ENTER", (unsigned long long)get_ticks());
326 	refresh_processor_tbl();
327 	HRDBG("[%llu] EXIT", (unsigned long long)get_ticks());
328 }
329 
330 /**
331  * Called to start this table. We need to start the periodic idle
332  * time collection.
333  */
334 void
335 start_processor_tbl(struct lmodule *mod)
336 {
337 
338 	/*
339 	 * Start the cpu stats collector
340 	 * The semantics of timer_start parameters is in "SNMP ticks";
341 	 * we have 100 "SNMP ticks" per second, thus we are trying below
342 	 * to get MAX_CPU_SAMPLES per minute
343 	 */
344 	cpus_load_timer = timer_start_repeat(100, 100 * 60 / MAX_CPU_SAMPLES,
345 	    get_cpus_samples, NULL, mod);
346 }
347 
348 /**
349  * Init the things for hrProcessorTable.
350  * Scan the device table for processor entries.
351  */
352 void
353 init_processor_tbl(void)
354 {
355 
356 	/* create the initial processor table */
357 	create_proc_table();
358 	/* and get first samples */
359 	refresh_processor_tbl();
360 }
361 
362 /**
363  * Finalization routine for hrProcessorTable.
364  * It destroys the lists and frees any allocated heap memory.
365  */
366 void
367 fini_processor_tbl(void)
368 {
369 
370 	if (cpus_load_timer != NULL) {
371 		timer_stop(cpus_load_timer);
372 		cpus_load_timer = NULL;
373 	}
374 
375 	free_proc_table();
376 }
377 
378 /**
379  * Access routine for the processor table.
380  */
381 int
382 op_hrProcessorTable(struct snmp_context *ctx __unused,
383     struct snmp_value *value, u_int sub, u_int iidx __unused,
384     enum snmp_op curr_op)
385 {
386 	struct processor_entry *entry;
387 
388 	switch (curr_op) {
389 
390 	case SNMP_OP_GETNEXT:
391 		if ((entry = NEXT_OBJECT_INT(&processor_tbl,
392 		    &value->var, sub)) == NULL)
393 			return (SNMP_ERR_NOSUCHNAME);
394 		value->var.len = sub + 1;
395 		value->var.subs[sub] = entry->index;
396 		goto get;
397 
398 	case SNMP_OP_GET:
399 		if ((entry = FIND_OBJECT_INT(&processor_tbl,
400 		    &value->var, sub)) == NULL)
401 			return (SNMP_ERR_NOSUCHNAME);
402 		goto get;
403 
404 	case SNMP_OP_SET:
405 		if ((entry = FIND_OBJECT_INT(&processor_tbl,
406 		    &value->var, sub)) == NULL)
407 			return (SNMP_ERR_NO_CREATION);
408 		return (SNMP_ERR_NOT_WRITEABLE);
409 
410 	case SNMP_OP_ROLLBACK:
411 	case SNMP_OP_COMMIT:
412 		abort();
413 	}
414 	abort();
415 
416   get:
417 	switch (value->var.subs[sub - 1]) {
418 
419 	case LEAF_hrProcessorFrwID:
420 		assert(entry->frwId != NULL);
421 		value->v.oid = *entry->frwId;
422 		return (SNMP_ERR_NOERROR);
423 
424 	case LEAF_hrProcessorLoad:
425 		value->v.integer = entry->load;
426 		return (SNMP_ERR_NOERROR);
427 	}
428 	abort();
429 }
430