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