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
get_avg_load(struct processor_entry * e)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
save_sample(struct processor_entry * e,long * cp_times)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 *
proc_create_entry(u_int cpu_no,struct device_map_entry * map)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
create_proc_table(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
free_proc_table(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
refresh_processor_tbl(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
get_cpus_samples(void * arg __unused)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
start_processor_tbl(struct lmodule * mod)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
init_processor_tbl(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
fini_processor_tbl(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
op_hrProcessorTable(struct snmp_context * ctx __unused,struct snmp_value * value,u_int sub,u_int iidx __unused,enum snmp_op curr_op)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