diff options
author | Bjoern Brandenburg <bbb@mpi-sws.org> | 2015-08-09 07:18:48 -0400 |
---|---|---|
committer | Bjoern Brandenburg <bbb@mpi-sws.org> | 2015-08-09 06:21:18 -0400 |
commit | 8e048c798adaabef530a1526f7ce8c6c3cd3475e (patch) | |
tree | 5a96b3eaeaafecec1bf08ba71a9d0084d39d46eb /litmus/litmus_proc.c | |
parent | bd175e94795774908317a861a883761b75750e35 (diff) |
Add LITMUS^RT core implementation
This patch adds the core of LITMUS^RT:
- library functionality (heaps, rt_domain, prioritization, etc.)
- budget enforcement logic
- job management
- system call backends
- virtual devices (control page, etc.)
- scheduler plugin API (and dummy plugin)
This code compiles, but is not yet integrated with the rest of Linux.
Diffstat (limited to 'litmus/litmus_proc.c')
-rw-r--r-- | litmus/litmus_proc.c | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/litmus/litmus_proc.c b/litmus/litmus_proc.c new file mode 100644 index 000000000000..2ef1669eff17 --- /dev/null +++ b/litmus/litmus_proc.c | |||
@@ -0,0 +1,573 @@ | |||
1 | /* | ||
2 | * litmus_proc.c -- Implementation of the /proc/litmus directory tree. | ||
3 | */ | ||
4 | |||
5 | #include <linux/sched.h> | ||
6 | #include <linux/slab.h> | ||
7 | #include <linux/uaccess.h> | ||
8 | #include <linux/seq_file.h> | ||
9 | |||
10 | #include <litmus/litmus.h> | ||
11 | #include <litmus/litmus_proc.h> | ||
12 | |||
13 | #include <litmus/clustered.h> | ||
14 | |||
15 | /* in litmus/litmus.c */ | ||
16 | extern atomic_t rt_task_count; | ||
17 | |||
18 | static struct proc_dir_entry *litmus_dir = NULL, | ||
19 | *curr_file = NULL, | ||
20 | *stat_file = NULL, | ||
21 | *plugs_dir = NULL, | ||
22 | #ifdef CONFIG_RELEASE_MASTER | ||
23 | *release_master_file = NULL, | ||
24 | #endif | ||
25 | *plugs_file = NULL, | ||
26 | *domains_dir = NULL, | ||
27 | *cpus_dir = NULL; | ||
28 | |||
29 | |||
30 | /* in litmus/sync.c */ | ||
31 | int count_tasks_waiting_for_release(void); | ||
32 | |||
33 | static int litmus_stats_proc_show(struct seq_file *m, void *v) | ||
34 | { | ||
35 | seq_printf(m, | ||
36 | "real-time tasks = %d\n" | ||
37 | "ready for release = %d\n", | ||
38 | atomic_read(&rt_task_count), | ||
39 | count_tasks_waiting_for_release()); | ||
40 | return 0; | ||
41 | } | ||
42 | |||
43 | static int litmus_stats_proc_open(struct inode *inode, struct file *file) | ||
44 | { | ||
45 | return single_open(file, litmus_stats_proc_show, PDE_DATA(inode)); | ||
46 | } | ||
47 | |||
48 | static const struct file_operations litmus_stats_proc_fops = { | ||
49 | .open = litmus_stats_proc_open, | ||
50 | .read = seq_read, | ||
51 | .llseek = seq_lseek, | ||
52 | .release = single_release, | ||
53 | }; | ||
54 | |||
55 | |||
56 | static int litmus_loaded_proc_show(struct seq_file *m, void *v) | ||
57 | { | ||
58 | print_sched_plugins(m); | ||
59 | return 0; | ||
60 | } | ||
61 | |||
62 | static int litmus_loaded_proc_open(struct inode *inode, struct file *file) | ||
63 | { | ||
64 | return single_open(file, litmus_loaded_proc_show, PDE_DATA(inode)); | ||
65 | } | ||
66 | |||
67 | static const struct file_operations litmus_loaded_proc_fops = { | ||
68 | .open = litmus_loaded_proc_open, | ||
69 | .read = seq_read, | ||
70 | .llseek = seq_lseek, | ||
71 | .release = single_release, | ||
72 | }; | ||
73 | |||
74 | |||
75 | |||
76 | |||
77 | /* in litmus/litmus.c */ | ||
78 | int switch_sched_plugin(struct sched_plugin*); | ||
79 | |||
80 | static ssize_t litmus_active_proc_write(struct file *file, | ||
81 | const char __user *buffer, size_t count, | ||
82 | loff_t *ppos) | ||
83 | { | ||
84 | char name[65]; | ||
85 | struct sched_plugin* found; | ||
86 | ssize_t ret = -EINVAL; | ||
87 | int err; | ||
88 | |||
89 | |||
90 | ret = copy_and_chomp(name, sizeof(name), buffer, count); | ||
91 | if (ret < 0) | ||
92 | return ret; | ||
93 | |||
94 | found = find_sched_plugin(name); | ||
95 | |||
96 | if (found) { | ||
97 | err = switch_sched_plugin(found); | ||
98 | if (err) { | ||
99 | printk(KERN_INFO "Could not switch plugin: %d\n", err); | ||
100 | ret = err; | ||
101 | } | ||
102 | } else { | ||
103 | printk(KERN_INFO "Plugin '%s' is unknown.\n", name); | ||
104 | ret = -ESRCH; | ||
105 | } | ||
106 | |||
107 | return ret; | ||
108 | } | ||
109 | |||
110 | static int litmus_active_proc_show(struct seq_file *m, void *v) | ||
111 | { | ||
112 | seq_printf(m, "%s\n", litmus->plugin_name); | ||
113 | return 0; | ||
114 | } | ||
115 | |||
116 | static int litmus_active_proc_open(struct inode *inode, struct file *file) | ||
117 | { | ||
118 | return single_open(file, litmus_active_proc_show, PDE_DATA(inode)); | ||
119 | } | ||
120 | |||
121 | static const struct file_operations litmus_active_proc_fops = { | ||
122 | .open = litmus_active_proc_open, | ||
123 | .read = seq_read, | ||
124 | .llseek = seq_lseek, | ||
125 | .release = single_release, | ||
126 | .write = litmus_active_proc_write, | ||
127 | }; | ||
128 | |||
129 | |||
130 | #ifdef CONFIG_RELEASE_MASTER | ||
131 | static ssize_t litmus_release_master_proc_write( | ||
132 | struct file *file, | ||
133 | const char __user *buffer, size_t count, | ||
134 | loff_t *ppos) | ||
135 | { | ||
136 | int cpu, err, online = 0; | ||
137 | char msg[64]; | ||
138 | ssize_t len; | ||
139 | |||
140 | len = copy_and_chomp(msg, sizeof(msg), buffer, count); | ||
141 | |||
142 | if (len < 0) | ||
143 | return len; | ||
144 | |||
145 | if (strcmp(msg, "NO_CPU") == 0) | ||
146 | atomic_set(&release_master_cpu, NO_CPU); | ||
147 | else { | ||
148 | err = sscanf(msg, "%d", &cpu); | ||
149 | if (err == 1 && cpu >= 0 && (online = cpu_online(cpu))) { | ||
150 | atomic_set(&release_master_cpu, cpu); | ||
151 | } else { | ||
152 | TRACE("invalid release master: '%s' " | ||
153 | "(err:%d cpu:%d online:%d)\n", | ||
154 | msg, err, cpu, online); | ||
155 | len = -EINVAL; | ||
156 | } | ||
157 | } | ||
158 | return len; | ||
159 | } | ||
160 | |||
161 | static int litmus_release_master_proc_show(struct seq_file *m, void *v) | ||
162 | { | ||
163 | int master; | ||
164 | master = atomic_read(&release_master_cpu); | ||
165 | if (master == NO_CPU) | ||
166 | seq_printf(m, "NO_CPU\n"); | ||
167 | else | ||
168 | seq_printf(m, "%d\n", master); | ||
169 | return 0; | ||
170 | } | ||
171 | |||
172 | static int litmus_release_master_proc_open(struct inode *inode, struct file *file) | ||
173 | { | ||
174 | return single_open(file, litmus_release_master_proc_show, PDE_DATA(inode)); | ||
175 | } | ||
176 | |||
177 | static const struct file_operations litmus_release_master_proc_fops = { | ||
178 | .open = litmus_release_master_proc_open, | ||
179 | .read = seq_read, | ||
180 | .llseek = seq_lseek, | ||
181 | .release = single_release, | ||
182 | .write = litmus_release_master_proc_write, | ||
183 | }; | ||
184 | #endif | ||
185 | |||
186 | int __init init_litmus_proc(void) | ||
187 | { | ||
188 | litmus_dir = proc_mkdir("litmus", NULL); | ||
189 | if (!litmus_dir) { | ||
190 | printk(KERN_ERR "Could not allocate LITMUS^RT procfs entry.\n"); | ||
191 | return -ENOMEM; | ||
192 | } | ||
193 | |||
194 | curr_file = proc_create("active_plugin", 0644, litmus_dir, | ||
195 | &litmus_active_proc_fops); | ||
196 | |||
197 | if (!curr_file) { | ||
198 | printk(KERN_ERR "Could not allocate active_plugin " | ||
199 | "procfs entry.\n"); | ||
200 | return -ENOMEM; | ||
201 | } | ||
202 | |||
203 | #ifdef CONFIG_RELEASE_MASTER | ||
204 | release_master_file = proc_create("release_master", 0644, litmus_dir, | ||
205 | &litmus_release_master_proc_fops); | ||
206 | if (!release_master_file) { | ||
207 | printk(KERN_ERR "Could not allocate release_master " | ||
208 | "procfs entry.\n"); | ||
209 | return -ENOMEM; | ||
210 | } | ||
211 | #endif | ||
212 | |||
213 | stat_file = proc_create("stats", 0444, litmus_dir, &litmus_stats_proc_fops); | ||
214 | |||
215 | plugs_dir = proc_mkdir("plugins", litmus_dir); | ||
216 | if (!plugs_dir){ | ||
217 | printk(KERN_ERR "Could not allocate plugins directory " | ||
218 | "procfs entry.\n"); | ||
219 | return -ENOMEM; | ||
220 | } | ||
221 | |||
222 | plugs_file = proc_create("loaded", 0444, plugs_dir, | ||
223 | &litmus_loaded_proc_fops); | ||
224 | |||
225 | domains_dir = proc_mkdir("domains", litmus_dir); | ||
226 | if (!domains_dir) { | ||
227 | printk(KERN_ERR "Could not allocate domains directory " | ||
228 | "procfs entry.\n"); | ||
229 | return -ENOMEM; | ||
230 | } | ||
231 | |||
232 | cpus_dir = proc_mkdir("cpus", litmus_dir); | ||
233 | if (!cpus_dir) { | ||
234 | printk(KERN_ERR "Could not allocate cpus directory " | ||
235 | "procfs entry.\n"); | ||
236 | return -ENOMEM; | ||
237 | } | ||
238 | |||
239 | return 0; | ||
240 | } | ||
241 | |||
242 | void exit_litmus_proc(void) | ||
243 | { | ||
244 | if (cpus_dir || domains_dir) { | ||
245 | deactivate_domain_proc(); | ||
246 | if (cpus_dir) | ||
247 | remove_proc_entry("cpus", litmus_dir); | ||
248 | if (domains_dir) | ||
249 | remove_proc_entry("domains", litmus_dir); | ||
250 | } | ||
251 | if (plugs_file) | ||
252 | remove_proc_entry("loaded", plugs_dir); | ||
253 | if (plugs_dir) | ||
254 | remove_proc_entry("plugins", litmus_dir); | ||
255 | if (stat_file) | ||
256 | remove_proc_entry("stats", litmus_dir); | ||
257 | if (curr_file) | ||
258 | remove_proc_entry("active_plugin", litmus_dir); | ||
259 | #ifdef CONFIG_RELEASE_MASTER | ||
260 | if (release_master_file) | ||
261 | remove_proc_entry("release_master", litmus_dir); | ||
262 | #endif | ||
263 | if (litmus_dir) | ||
264 | remove_proc_entry("litmus", NULL); | ||
265 | } | ||
266 | |||
267 | long make_plugin_proc_dir(struct sched_plugin* plugin, | ||
268 | struct proc_dir_entry** pde_in) | ||
269 | { | ||
270 | struct proc_dir_entry *pde_new = NULL; | ||
271 | long rv; | ||
272 | |||
273 | if (!plugin || !plugin->plugin_name){ | ||
274 | printk(KERN_ERR "Invalid plugin struct passed to %s.\n", | ||
275 | __func__); | ||
276 | rv = -EINVAL; | ||
277 | goto out_no_pde; | ||
278 | } | ||
279 | |||
280 | if (!plugs_dir){ | ||
281 | printk(KERN_ERR "Could not make plugin sub-directory, because " | ||
282 | "/proc/litmus/plugins does not exist.\n"); | ||
283 | rv = -ENOENT; | ||
284 | goto out_no_pde; | ||
285 | } | ||
286 | |||
287 | pde_new = proc_mkdir(plugin->plugin_name, plugs_dir); | ||
288 | if (!pde_new){ | ||
289 | printk(KERN_ERR "Could not make plugin sub-directory: " | ||
290 | "out of memory?.\n"); | ||
291 | rv = -ENOMEM; | ||
292 | goto out_no_pde; | ||
293 | } | ||
294 | |||
295 | rv = 0; | ||
296 | *pde_in = pde_new; | ||
297 | goto out_ok; | ||
298 | |||
299 | out_no_pde: | ||
300 | *pde_in = NULL; | ||
301 | out_ok: | ||
302 | return rv; | ||
303 | } | ||
304 | |||
305 | void remove_plugin_proc_dir(struct sched_plugin* plugin) | ||
306 | { | ||
307 | if (!plugin || !plugin->plugin_name){ | ||
308 | printk(KERN_ERR "Invalid plugin struct passed to %s.\n", | ||
309 | __func__); | ||
310 | return; | ||
311 | } | ||
312 | remove_proc_entry(plugin->plugin_name, plugs_dir); | ||
313 | } | ||
314 | |||
315 | |||
316 | |||
317 | /* misc. I/O helper functions */ | ||
318 | |||
319 | int copy_and_chomp(char *kbuf, unsigned long ksize, | ||
320 | __user const char* ubuf, unsigned long ulength) | ||
321 | { | ||
322 | /* caller must provide buffer space */ | ||
323 | BUG_ON(!ksize); | ||
324 | |||
325 | ksize--; /* leave space for null byte */ | ||
326 | |||
327 | if (ksize > ulength) | ||
328 | ksize = ulength; | ||
329 | |||
330 | if(copy_from_user(kbuf, ubuf, ksize)) | ||
331 | return -EFAULT; | ||
332 | |||
333 | kbuf[ksize] = '\0'; | ||
334 | |||
335 | /* chomp kbuf */ | ||
336 | if (ksize > 0 && kbuf[ksize - 1] == '\n') | ||
337 | kbuf[ksize - 1] = '\0'; | ||
338 | |||
339 | return ksize; | ||
340 | } | ||
341 | |||
342 | /* helper functions for clustered plugins */ | ||
343 | static const char* cache_level_names[] = { | ||
344 | "ALL", | ||
345 | "L1", | ||
346 | "L2", | ||
347 | "L3", | ||
348 | }; | ||
349 | |||
350 | int parse_cache_level(const char *cache_name, enum cache_level *level) | ||
351 | { | ||
352 | int err = -EINVAL; | ||
353 | int i; | ||
354 | /* do a quick and dirty comparison to find the cluster size */ | ||
355 | for (i = GLOBAL_CLUSTER; i <= L3_CLUSTER; i++) | ||
356 | if (!strcmp(cache_name, cache_level_names[i])) { | ||
357 | *level = (enum cache_level) i; | ||
358 | err = 0; | ||
359 | break; | ||
360 | } | ||
361 | return err; | ||
362 | } | ||
363 | |||
364 | const char* cache_level_name(enum cache_level level) | ||
365 | { | ||
366 | int idx = level; | ||
367 | |||
368 | if (idx >= GLOBAL_CLUSTER && idx <= L3_CLUSTER) | ||
369 | return cache_level_names[idx]; | ||
370 | else | ||
371 | return "INVALID"; | ||
372 | } | ||
373 | |||
374 | |||
375 | |||
376 | |||
377 | /* proc file interface to configure the cluster size */ | ||
378 | |||
379 | static ssize_t litmus_cluster_proc_write(struct file *file, | ||
380 | const char __user *buffer, size_t count, | ||
381 | loff_t *ppos) | ||
382 | { | ||
383 | enum cache_level *level = (enum cache_level *) PDE_DATA(file_inode(file)); | ||
384 | ssize_t len; | ||
385 | char cache_name[8]; | ||
386 | |||
387 | len = copy_and_chomp(cache_name, sizeof(cache_name), buffer, count); | ||
388 | |||
389 | if (len > 0 && parse_cache_level(cache_name, level)) { | ||
390 | printk(KERN_INFO "Cluster '%s' is unknown.\n", cache_name); | ||
391 | len = -EINVAL; | ||
392 | } | ||
393 | |||
394 | return len; | ||
395 | } | ||
396 | |||
397 | static int litmus_cluster_proc_show(struct seq_file *m, void *v) | ||
398 | { | ||
399 | enum cache_level *level = (enum cache_level *) m->private; | ||
400 | |||
401 | seq_printf(m, "%s\n", cache_level_name(*level)); | ||
402 | return 0; | ||
403 | } | ||
404 | |||
405 | static int litmus_cluster_proc_open(struct inode *inode, struct file *file) | ||
406 | { | ||
407 | return single_open(file, litmus_cluster_proc_show, PDE_DATA(inode)); | ||
408 | } | ||
409 | |||
410 | static const struct file_operations litmus_cluster_proc_fops = { | ||
411 | .open = litmus_cluster_proc_open, | ||
412 | .read = seq_read, | ||
413 | .llseek = seq_lseek, | ||
414 | .release = single_release, | ||
415 | .write = litmus_cluster_proc_write, | ||
416 | }; | ||
417 | |||
418 | struct proc_dir_entry* create_cluster_file(struct proc_dir_entry* parent, | ||
419 | enum cache_level* level) | ||
420 | { | ||
421 | struct proc_dir_entry* cluster_file; | ||
422 | |||
423 | |||
424 | cluster_file = proc_create_data("cluster", 0644, parent, | ||
425 | &litmus_cluster_proc_fops, | ||
426 | (void *) level); | ||
427 | if (!cluster_file) { | ||
428 | printk(KERN_ERR | ||
429 | "Could not cluster procfs entry.\n"); | ||
430 | } | ||
431 | return cluster_file; | ||
432 | } | ||
433 | |||
434 | static struct domain_proc_info* active_mapping = NULL; | ||
435 | |||
436 | static int litmus_mapping_proc_show(struct seq_file *m, void *v) | ||
437 | { | ||
438 | struct cd_mapping *mapping = (struct cd_mapping*) m->private; | ||
439 | |||
440 | if(!mapping) | ||
441 | return 0; | ||
442 | |||
443 | seq_printf(m, "%*pb\n", cpumask_pr_args(mapping->mask)); | ||
444 | return 0; | ||
445 | } | ||
446 | |||
447 | static int litmus_mapping_proc_open(struct inode *inode, struct file *file) | ||
448 | { | ||
449 | return single_open(file, litmus_mapping_proc_show, PDE_DATA(inode)); | ||
450 | } | ||
451 | |||
452 | static const struct file_operations litmus_domain_proc_fops = { | ||
453 | .open = litmus_mapping_proc_open, | ||
454 | .read = seq_read, | ||
455 | .llseek = seq_lseek, | ||
456 | .release = single_release, | ||
457 | }; | ||
458 | |||
459 | long activate_domain_proc(struct domain_proc_info* map) | ||
460 | { | ||
461 | int i; | ||
462 | char name[8]; | ||
463 | |||
464 | if (!map) | ||
465 | return -EINVAL; | ||
466 | if (cpus_dir == NULL || domains_dir == NULL) | ||
467 | return -EINVAL; | ||
468 | |||
469 | if (active_mapping) | ||
470 | deactivate_domain_proc(); | ||
471 | |||
472 | active_mapping = map; | ||
473 | |||
474 | for (i = 0; i < map->num_cpus; ++i) { | ||
475 | struct cd_mapping* m = &map->cpu_to_domains[i]; | ||
476 | snprintf(name, sizeof(name), "%d", m->id); | ||
477 | m->proc_file = proc_create_data(name, 0444, cpus_dir, | ||
478 | &litmus_domain_proc_fops, (void*)m); | ||
479 | } | ||
480 | |||
481 | for (i = 0; i < map->num_domains; ++i) { | ||
482 | struct cd_mapping* m = &map->domain_to_cpus[i]; | ||
483 | snprintf(name, sizeof(name), "%d", m->id); | ||
484 | m->proc_file = proc_create_data(name, 0444, domains_dir, | ||
485 | &litmus_domain_proc_fops, (void*)m); | ||
486 | } | ||
487 | |||
488 | return 0; | ||
489 | } | ||
490 | |||
491 | long deactivate_domain_proc() | ||
492 | { | ||
493 | int i; | ||
494 | char name[65]; | ||
495 | |||
496 | struct domain_proc_info* map = active_mapping; | ||
497 | |||
498 | if (!map) | ||
499 | return -EINVAL; | ||
500 | |||
501 | for (i = 0; i < map->num_cpus; ++i) { | ||
502 | struct cd_mapping* m = &map->cpu_to_domains[i]; | ||
503 | snprintf(name, sizeof(name), "%d", m->id); | ||
504 | remove_proc_entry(name, cpus_dir); | ||
505 | m->proc_file = NULL; | ||
506 | } | ||
507 | for (i = 0; i < map->num_domains; ++i) { | ||
508 | struct cd_mapping* m = &map->domain_to_cpus[i]; | ||
509 | snprintf(name, sizeof(name), "%d", m->id); | ||
510 | remove_proc_entry(name, domains_dir); | ||
511 | m->proc_file = NULL; | ||
512 | } | ||
513 | |||
514 | active_mapping = NULL; | ||
515 | |||
516 | return 0; | ||
517 | } | ||
518 | |||
519 | long init_domain_proc_info(struct domain_proc_info* m, | ||
520 | int num_cpus, int num_domains) | ||
521 | { | ||
522 | int i; | ||
523 | int num_alloced_cpu_masks = 0; | ||
524 | int num_alloced_domain_masks = 0; | ||
525 | |||
526 | m->cpu_to_domains = | ||
527 | kmalloc(sizeof(*(m->cpu_to_domains))*num_cpus, | ||
528 | GFP_ATOMIC); | ||
529 | if(!m->cpu_to_domains) | ||
530 | goto failure; | ||
531 | |||
532 | m->domain_to_cpus = | ||
533 | kmalloc(sizeof(*(m->domain_to_cpus))*num_domains, | ||
534 | GFP_ATOMIC); | ||
535 | if(!m->domain_to_cpus) | ||
536 | goto failure; | ||
537 | |||
538 | for(i = 0; i < num_cpus; ++i) { | ||
539 | if(!zalloc_cpumask_var(&m->cpu_to_domains[i].mask, GFP_ATOMIC)) | ||
540 | goto failure; | ||
541 | ++num_alloced_cpu_masks; | ||
542 | } | ||
543 | for(i = 0; i < num_domains; ++i) { | ||
544 | if(!zalloc_cpumask_var(&m->domain_to_cpus[i].mask, GFP_ATOMIC)) | ||
545 | goto failure; | ||
546 | ++num_alloced_domain_masks; | ||
547 | } | ||
548 | |||
549 | return 0; | ||
550 | |||
551 | failure: | ||
552 | for(i = 0; i < num_alloced_cpu_masks; ++i) | ||
553 | free_cpumask_var(m->cpu_to_domains[i].mask); | ||
554 | for(i = 0; i < num_alloced_domain_masks; ++i) | ||
555 | free_cpumask_var(m->domain_to_cpus[i].mask); | ||
556 | if(m->cpu_to_domains) | ||
557 | kfree(m->cpu_to_domains); | ||
558 | if(m->domain_to_cpus) | ||
559 | kfree(m->domain_to_cpus); | ||
560 | return -ENOMEM; | ||
561 | } | ||
562 | |||
563 | void destroy_domain_proc_info(struct domain_proc_info* m) | ||
564 | { | ||
565 | int i; | ||
566 | for(i = 0; i < m->num_cpus; ++i) | ||
567 | free_cpumask_var(m->cpu_to_domains[i].mask); | ||
568 | for(i = 0; i < m->num_domains; ++i) | ||
569 | free_cpumask_var(m->domain_to_cpus[i].mask); | ||
570 | kfree(m->cpu_to_domains); | ||
571 | kfree(m->domain_to_cpus); | ||
572 | memset(m, sizeof(*m), 0); | ||
573 | } | ||