/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2009 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* * CPU Performance Counter system calls and device driver. * * This module uses a combination of thread context operators, and * thread-specific data to export CPU performance counters * via both a system call and a driver interface. * * There are three access methods exported - the 'shared' device * and the 'private' and 'agent' variants of the system call. * * The shared device treats the performance counter registers as * a processor metric, regardless of the work scheduled on them. * The private system call treats the performance counter registers * as a property of a single lwp. This is achieved by using the * thread context operators to virtualize the contents of the * performance counter registers between lwps. * * The agent method is like the private method, except that it must * be accessed via /proc's agent lwp to allow the counter context of * other threads to be examined safely. * * The shared usage fundamentally conflicts with the agent and private usage; * almost all of the complexity of the module is needed to allow these two * models to co-exist in a reasonable way. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int kcpc_copyin_set(kcpc_set_t **set, void *ubuf, size_t len); static int kcpc_verify_set(kcpc_set_t *set); static uint32_t kcpc_nvlist_npairs(nvlist_t *list); /* * Generic attributes supported regardless of processor. */ #define ATTRLIST "picnum" #define SEPARATOR "," /* * System call to access CPU performance counters. */ static int cpc(int cmd, id_t lwpid, void *udata1, void *udata2, void *udata3) { kthread_t *t; int error; int size; const char *str; int code; /* * This CPC syscall should only be loaded if it found a PCBE to use. */ ASSERT(pcbe_ops != NULL); if (curproc->p_agenttp == curthread) { /* * Only if /proc is invoking this system call from * the agent thread do we allow the caller to examine * the contexts of other lwps in the process. And * because we know we're the agent, we know we don't * have to grab p_lock because no-one else can change * the state of the process. */ if ((t = idtot(curproc, lwpid)) == NULL || t == curthread) return (set_errno(ESRCH)); ASSERT(t->t_tid == lwpid && ttolwp(t) != NULL); } else t = curthread; if (t->t_cpc_set == NULL && (cmd == CPC_SAMPLE || cmd == CPC_RELE)) return (set_errno(EINVAL)); switch (cmd) { case CPC_BIND: /* * udata1 = pointer to packed nvlist buffer * udata2 = size of packed nvlist buffer * udata3 = User addr to return error subcode in. */ rw_enter(&kcpc_cpuctx_lock, RW_READER); if (kcpc_cpuctx || dtrace_cpc_in_use) { rw_exit(&kcpc_cpuctx_lock); return (set_errno(EAGAIN)); } if (kcpc_hw_lwp_hook() != 0) { rw_exit(&kcpc_cpuctx_lock); return (set_errno(EACCES)); } /* * An LWP may only have one set bound to it at a time; if there * is a set bound to this LWP already, we unbind it here. */ if (t->t_cpc_set != NULL) (void) kcpc_unbind(t->t_cpc_set); ASSERT(t->t_cpc_set == NULL); if ((error = kcpc_copyin_set(&t->t_cpc_set, udata1, (size_t)udata2)) != 0) { rw_exit(&kcpc_cpuctx_lock); return (set_errno(error)); } if ((error = kcpc_verify_set(t->t_cpc_set)) != 0) { rw_exit(&kcpc_cpuctx_lock); kcpc_free_set(t->t_cpc_set); t->t_cpc_set = NULL; if (copyout(&error, udata3, sizeof (error)) == -1) return (set_errno(EFAULT)); return (set_errno(EINVAL)); } if ((error = kcpc_bind_thread(t->t_cpc_set, t, &code)) != 0) { rw_exit(&kcpc_cpuctx_lock); kcpc_free_set(t->t_cpc_set); t->t_cpc_set = NULL; /* * EINVAL and EACCES are the only errors with more * specific subcodes. */ if ((error == EINVAL || error == EACCES) && copyout(&code, udata3, sizeof (code)) == -1) return (set_errno(EFAULT)); return (set_errno(error)); } rw_exit(&kcpc_cpuctx_lock); return (0); case CPC_SAMPLE: /* * udata1 = pointer to user's buffer * udata2 = pointer to user's hrtime * udata3 = pointer to user's tick */ /* * We only allow thread-bound sets to be sampled via the * syscall, so if this set has a CPU-bound context, return an * error. */ if (t->t_cpc_set->ks_ctx->kc_cpuid != -1) return (set_errno(EINVAL)); if ((error = kcpc_sample(t->t_cpc_set, udata1, udata2, udata3)) != 0) return (set_errno(error)); return (0); case CPC_PRESET: case CPC_RESTART: /* * These are valid only if this lwp has a bound set. */ if (t->t_cpc_set == NULL) return (set_errno(EINVAL)); if (cmd == CPC_PRESET) { /* * The preset is shipped up to us from userland in two * parts. This lets us handle 64-bit values from 32-bit * and 64-bit applications in the same manner. * * udata1 = index of request to preset * udata2 = new 64-bit preset (most sig. 32 bits) * udata3 = new 64-bit preset (least sig. 32 bits) */ if ((error = kcpc_preset(t->t_cpc_set, (intptr_t)udata1, ((uint64_t)(uintptr_t)udata2 << 32ULL) | (uint64_t)(uintptr_t)udata3)) != 0) return (set_errno(error)); } else { /* * udata[1-3] = unused */ if ((error = kcpc_restart(t->t_cpc_set)) != 0) return (set_errno(error)); } return (0); case CPC_ENABLE: case CPC_DISABLE: udata1 = 0; /*FALLTHROUGH*/ case CPC_USR_EVENTS: case CPC_SYS_EVENTS: if (t != curthread || t->t_cpc_set == NULL) return (set_errno(EINVAL)); /* * Provided for backwards compatibility with CPCv1. * * Stop the counters and record the current counts. Use the * counts as the preset to rebind a new set with the requests * reconfigured as requested. * * udata1: 1 == enable; 0 == disable * udata{2,3}: unused */ rw_enter(&kcpc_cpuctx_lock, RW_READER); if ((error = kcpc_enable(t, cmd, (int)(uintptr_t)udata1)) != 0) { rw_exit(&kcpc_cpuctx_lock); return (set_errno(error)); } rw_exit(&kcpc_cpuctx_lock); return (0); case CPC_NPIC: return (cpc_ncounters); case CPC_CAPS: return (pcbe_ops->pcbe_caps); case CPC_EVLIST_SIZE: case CPC_LIST_EVENTS: /* * udata1 = pointer to user's int or buffer * udata2 = picnum * udata3 = unused */ if ((uintptr_t)udata2 >= cpc_ncounters) return (set_errno(EINVAL)); size = strlen( pcbe_ops->pcbe_list_events((uintptr_t)udata2)) + 1; if (cmd == CPC_EVLIST_SIZE) { if (suword32(udata1, size) == -1) return (set_errno(EFAULT)); } else { if (copyout( pcbe_ops->pcbe_list_events((uintptr_t)udata2), udata1, size) == -1) return (set_errno(EFAULT)); } return (0); case CPC_ATTRLIST_SIZE: case CPC_LIST_ATTRS: /* * udata1 = pointer to user's int or buffer * udata2 = unused * udata3 = unused * * attrlist size is length of PCBE-supported attributes, plus * room for "picnum\0" plus an optional ',' separator char. */ str = pcbe_ops->pcbe_list_attrs(); size = strlen(str) + sizeof (SEPARATOR ATTRLIST) + 1; if (str[0] != '\0') /* * A ',' separator character is necessary. */ size += 1; if (cmd == CPC_ATTRLIST_SIZE) { if (suword32(udata1, size) == -1) return (set_errno(EFAULT)); } else { /* * Copyout the PCBE attributes, and then append the * generic attribute list (with separator if necessary). */ if (copyout(str, udata1, strlen(str)) == -1) return (set_errno(EFAULT)); if (str[0] != '\0') { if (copyout(SEPARATOR ATTRLIST, ((char *)udata1) + strlen(str), strlen(SEPARATOR ATTRLIST) + 1) == -1) return (set_errno(EFAULT)); } else if (copyout(ATTRLIST, (char *)udata1 + strlen(str), strlen(ATTRLIST) + 1) == -1) return (set_errno(EFAULT)); } return (0); case CPC_IMPL_NAME: case CPC_CPUREF: /* * udata1 = pointer to user's buffer * udata2 = unused * udata3 = unused */ if (cmd == CPC_IMPL_NAME) { str = pcbe_ops->pcbe_impl_name(); ASSERT(strlen(str) < CPC_MAX_IMPL_NAME); } else { str = pcbe_ops->pcbe_cpuref(); ASSERT(strlen(str) < CPC_MAX_CPUREF); } if (copyout(str, udata1, strlen(str) + 1) != 0) return (set_errno(EFAULT)); return (0); case CPC_INVALIDATE: kcpc_invalidate(t); return (0); case CPC_RELE: if ((error = kcpc_unbind(t->t_cpc_set)) != 0) return (set_errno(error)); return (0); default: return (set_errno(EINVAL)); } } /* * The 'shared' device allows direct access to the * performance counter control register of the current CPU. * The major difference between the contexts created here and those * above is that the context handlers are -not- installed, thus * no context switching behaviour occurs. * * Because they manipulate per-cpu state, these ioctls can * only be invoked from a bound lwp, by a caller with the cpc_cpu privilege * who can open the relevant entry in /devices (the act of holding it open * causes other uses of the counters to be suspended). * * Note that for correct results, the caller -must- ensure that * all existing per-lwp contexts are either inactive or marked invalid; * that's what the open routine does. */ /*ARGSUSED*/ static int kcpc_ioctl(dev_t dev, int cmd, intptr_t data, int flags, cred_t *cr, int *rvp) { kthread_t *t = curthread; processorid_t cpuid; void *udata1 = NULL; void *udata2 = NULL; void *udata3 = NULL; int error; int code; STRUCT_DECL(__cpc_args, args); STRUCT_INIT(args, flags); if (curthread->t_bind_cpu != getminor(dev)) return (EAGAIN); /* someone unbound it? */ cpuid = getminor(dev); if (cmd == CPCIO_BIND || cmd == CPCIO_SAMPLE) { if (copyin((void *)data, STRUCT_BUF(args), STRUCT_SIZE(args)) == -1) return (EFAULT); udata1 = STRUCT_FGETP(args, udata1); udata2 = STRUCT_FGETP(args, udata2); udata3 = STRUCT_FGETP(args, udata3); } switch (cmd) { case CPCIO_BIND: /* * udata1 = pointer to packed nvlist buffer * udata2 = size of packed nvlist buffer * udata3 = User addr to return error subcode in. */ if (t->t_cpc_set != NULL) { (void) kcpc_unbind(t->t_cpc_set); ASSERT(t->t_cpc_set == NULL); } if ((error = kcpc_copyin_set(&t->t_cpc_set, udata1, (size_t)udata2)) != 0) { return (error); } if ((error = kcpc_verify_set(t->t_cpc_set)) != 0) { kcpc_free_set(t->t_cpc_set); t->t_cpc_set = NULL; if (copyout(&error, udata3, sizeof (error)) == -1) return (EFAULT); return (EINVAL); } if ((error = kcpc_bind_cpu(t->t_cpc_set, cpuid, &code)) != 0) { kcpc_free_set(t->t_cpc_set); t->t_cpc_set = NULL; /* * Subcodes are only returned for EINVAL and EACCESS. */ if ((error == EINVAL || error == EACCES) && copyout(&code, udata3, sizeof (code)) == -1) return (EFAULT); return (error); } return (0); case CPCIO_SAMPLE: /* * udata1 = pointer to user's buffer * udata2 = pointer to user's hrtime * udata3 = pointer to user's tick */ /* * Only CPU-bound sets may be sampled via the ioctl(). If this * set has no CPU-bound context, return an error. */ if (t->t_cpc_set == NULL) return (EINVAL); if ((error = kcpc_sample(t->t_cpc_set, udata1, udata2, udata3)) != 0) return (error); return (0); case CPCIO_RELE: if (t->t_cpc_set == NULL) return (EINVAL); return (kcpc_unbind(t->t_cpc_set)); default: return (EINVAL); } } /* * The device supports multiple opens, but only one open * is allowed per processor. This is to enable multiple * instances of tools looking at different processors. */ #define KCPC_MINOR_SHARED ((minor_t)0x3fffful) static ulong_t *kcpc_cpumap; /* bitmap of cpus */ /*ARGSUSED1*/ static int kcpc_open(dev_t *dev, int flags, int otyp, cred_t *cr) { processorid_t cpuid; int error; ASSERT(pcbe_ops != NULL); if ((error = secpolicy_cpc_cpu(cr)) != 0) return (error); if (getminor(*dev) != KCPC_MINOR_SHARED) return (ENXIO); if ((cpuid = curthread->t_bind_cpu) == PBIND_NONE) return (EINVAL); if (cpuid > max_cpuid) return (EINVAL); rw_enter(&kcpc_cpuctx_lock, RW_WRITER); if (++kcpc_cpuctx == 1) { ASSERT(kcpc_cpumap == NULL); /* * Bail out if DTrace is already using the counters. */ if (dtrace_cpc_in_use) { kcpc_cpuctx--; rw_exit(&kcpc_cpuctx_lock); return (EAGAIN); } kcpc_cpumap = kmem_zalloc(BT_SIZEOFMAP(max_cpuid + 1), KM_SLEEP); /* * When this device is open for processor-based contexts, * no further lwp-based contexts can be created. * * Since this is the first open, ensure that all existing * contexts are invalidated. */ kcpc_invalidate_all(); } else if (BT_TEST(kcpc_cpumap, cpuid)) { kcpc_cpuctx--; rw_exit(&kcpc_cpuctx_lock); return (EAGAIN); } else if (kcpc_hw_cpu_hook(cpuid, kcpc_cpumap) != 0) { kcpc_cpuctx--; rw_exit(&kcpc_cpuctx_lock); return (EACCES); } BT_SET(kcpc_cpumap, cpuid); rw_exit(&kcpc_cpuctx_lock); *dev = makedevice(getmajor(*dev), (minor_t)cpuid); return (0); } /*ARGSUSED1*/ static int kcpc_close(dev_t dev, int flags, int otyp, cred_t *cr) { rw_enter(&kcpc_cpuctx_lock, RW_WRITER); BT_CLEAR(kcpc_cpumap, getminor(dev)); if (--kcpc_cpuctx == 0) { kmem_free(kcpc_cpumap, BT_SIZEOFMAP(max_cpuid + 1)); kcpc_cpumap = NULL; } ASSERT(kcpc_cpuctx >= 0); rw_exit(&kcpc_cpuctx_lock); return (0); } /* * Sane boundaries on the size of packed lists. In bytes. */ #define CPC_MIN_PACKSIZE 4 #define CPC_MAX_PACKSIZE 10000 /* * Sane boundary on the number of requests a set can contain. */ #define CPC_MAX_NREQS 100 /* * Sane boundary on the number of attributes a request can contain. */ #define CPC_MAX_ATTRS 50 /* * Copy in a packed nvlist from the user and create a request set out of it. * If successful, return 0 and store a pointer to the set we've created. Returns * error code on error. */ int kcpc_copyin_set(kcpc_set_t **inset, void *ubuf, size_t len) { kcpc_set_t *set; int i; int j; char *packbuf; nvlist_t *nvl; nvpair_t *nvp = NULL; nvlist_t *attrs; nvpair_t *nvp_attr; kcpc_attr_t *attrp; nvlist_t **reqlist; uint_t nreqs; uint64_t uint64; uint32_t uint32; uint32_t setflags = (uint32_t)-1; char *string; char *name; if (len < CPC_MIN_PACKSIZE || len > CPC_MAX_PACKSIZE) return (EINVAL); packbuf = kmem_alloc(len, KM_SLEEP); if (copyin(ubuf, packbuf, len) == -1) { kmem_free(packbuf, len); return (EFAULT); } if (nvlist_unpack(packbuf, len, &nvl, KM_SLEEP) != 0) { kmem_free(packbuf, len); return (EINVAL); } /* * The nvlist has been unpacked so there is no need for the packed * representation from this point on. */ kmem_free(packbuf, len); i = 0; while ((nvp = nvlist_next_nvpair(nvl, nvp)) != NULL) { switch (nvpair_type(nvp)) { case DATA_TYPE_UINT32: if (strcmp(nvpair_name(nvp), "flags") != 0 || nvpair_value_uint32(nvp, &setflags) != 0) { nvlist_free(nvl); return (EINVAL); } break; case DATA_TYPE_NVLIST_ARRAY: if (strcmp(nvpair_name(nvp), "reqs") != 0 || nvpair_value_nvlist_array(nvp, &reqlist, &nreqs) != 0) { nvlist_free(nvl); return (EINVAL); } break; default: nvlist_free(nvl); return (EINVAL); } i++; } /* * There should be two members in the top-level nvlist: * an array of nvlists consisting of the requests, and flags. * Anything else is an invalid set. */ if (i != 2) { nvlist_free(nvl); return (EINVAL); } if (nreqs > CPC_MAX_NREQS) { nvlist_free(nvl); return (EINVAL); } /* * The requests are now stored in the nvlist array at reqlist. * Note that the use of kmem_zalloc() to alloc the kcpc_set_t means * we don't need to call the init routines for ks_lock and ks_condv. */ set = kmem_zalloc(sizeof (kcpc_set_t), KM_SLEEP); set->ks_req = (kcpc_request_t *)kmem_zalloc(sizeof (kcpc_request_t) * nreqs, KM_SLEEP); set->ks_nreqs = nreqs; /* * If the nvlist didn't contain a flags member, setflags was initialized * with an illegal value and this set will fail sanity checks later on. */ set->ks_flags = setflags; /* * Initialize bind/unbind set synchronization. */ set->ks_state &= ~KCPC_SET_BOUND; /* * Build the set up one request at a time, always keeping it self- * consistent so we can give it to kcpc_free_set() if we need to back * out and return and error. */ for (i = 0; i < nreqs; i++) { nvp = NULL; set->ks_req[i].kr_picnum = -1; while ((nvp = nvlist_next_nvpair(reqlist[i], nvp)) != NULL) { name = nvpair_name(nvp); switch (nvpair_type(nvp)) { case DATA_TYPE_UINT32: if (nvpair_value_uint32(nvp, &uint32) == EINVAL) goto inval; if (strcmp(name, "cr_flags") == 0) set->ks_req[i].kr_flags = uint32; if (strcmp(name, "cr_index") == 0) set->ks_req[i].kr_index = uint32; break; case DATA_TYPE_UINT64: if (nvpair_value_uint64(nvp, &uint64) == EINVAL) goto inval; if (strcmp(name, "cr_preset") == 0) set->ks_req[i].kr_preset = uint64; break; case DATA_TYPE_STRING: if (nvpair_value_string(nvp, &string) == EINVAL) goto inval; if (strcmp(name, "cr_event") == 0) (void) strncpy(set->ks_req[i].kr_event, string, CPC_MAX_EVENT_LEN); break; case DATA_TYPE_NVLIST: if (strcmp(name, "cr_attr") != 0) goto inval; if (nvpair_value_nvlist(nvp, &attrs) == EINVAL) goto inval; nvp_attr = NULL; /* * If the picnum has been specified as an * attribute, consume that attribute here and * remove it from the list of attributes. */ if (nvlist_lookup_uint64(attrs, "picnum", &uint64) == 0) { if (nvlist_remove(attrs, "picnum", DATA_TYPE_UINT64) != 0) panic("nvlist %p faulty", (void *)attrs); set->ks_req[i].kr_picnum = uint64; } if ((set->ks_req[i].kr_nattrs = kcpc_nvlist_npairs(attrs)) == 0) break; if (set->ks_req[i].kr_nattrs > CPC_MAX_ATTRS) goto inval; set->ks_req[i].kr_attr = kmem_alloc(set->ks_req[i].kr_nattrs * sizeof (kcpc_attr_t), KM_SLEEP); j = 0; while ((nvp_attr = nvlist_next_nvpair(attrs, nvp_attr)) != NULL) { attrp = &set->ks_req[i].kr_attr[j]; if (nvpair_type(nvp_attr) != DATA_TYPE_UINT64) goto inval; (void) strncpy(attrp->ka_name, nvpair_name(nvp_attr), CPC_MAX_ATTR_LEN); if (nvpair_value_uint64(nvp_attr, &(attrp->ka_val)) == EINVAL) goto inval; j++; } ASSERT(j == set->ks_req[i].kr_nattrs); default: break; } } } nvlist_free(nvl); *inset = set; return (0); inval: nvlist_free(nvl); kcpc_free_set(set); return (EINVAL); } /* * Count the number of nvpairs in the supplied nvlist. */ static uint32_t kcpc_nvlist_npairs(nvlist_t *list) { nvpair_t *nvp = NULL; uint32_t n = 0; while ((nvp = nvlist_next_nvpair(list, nvp)) != NULL) n++; return (n); } /* * Performs sanity checks on the given set. * Returns 0 if the set checks out OK. * Returns a detailed error subcode, or -1 if there is no applicable subcode. */ static int kcpc_verify_set(kcpc_set_t *set) { kcpc_request_t *rp; int i; uint64_t bitmap = 0; int n; if (set->ks_nreqs > cpc_ncounters) return (-1); if (CPC_SET_VALID_FLAGS(set->ks_flags) == 0) return (-1); for (i = 0; i < set->ks_nreqs; i++) { rp = &set->ks_req[i]; /* * The following comparison must cast cpc_ncounters to an int, * because kr_picnum will be -1 if the request didn't explicitly * choose a PIC. */ if (rp->kr_picnum >= (int)cpc_ncounters) return (CPC_INVALID_PICNUM); /* * Of the pics whose physical picnum has been specified, make * sure each PIC appears only once in set. */ if ((n = set->ks_req[i].kr_picnum) != -1) { if ((bitmap & (1 << n)) != 0) return (-1); bitmap |= (1 << n); } /* * Make sure the requested index falls within the range of all * requests. */ if (rp->kr_index < 0 || rp->kr_index >= set->ks_nreqs) return (-1); /* * Make sure there are no unknown flags. */ if (KCPC_REQ_VALID_FLAGS(rp->kr_flags) == 0) return (CPC_REQ_INVALID_FLAGS); } return (0); } static struct cb_ops cb_ops = { kcpc_open, kcpc_close, nodev, /* strategy */ nodev, /* print */ nodev, /* dump */ nodev, /* read */ nodev, /* write */ kcpc_ioctl, nodev, /* devmap */ nodev, /* mmap */ nodev, /* segmap */ nochpoll, /* poll */ ddi_prop_op, NULL, D_NEW | D_MP }; /*ARGSUSED*/ static int kcpc_probe(dev_info_t *devi) { return (DDI_PROBE_SUCCESS); } static dev_info_t *kcpc_devi; static int kcpc_attach(dev_info_t *devi, ddi_attach_cmd_t cmd) { if (cmd != DDI_ATTACH) return (DDI_FAILURE); kcpc_devi = devi; return (ddi_create_minor_node(devi, "shared", S_IFCHR, KCPC_MINOR_SHARED, DDI_PSEUDO, 0)); } /*ARGSUSED*/ static int kcpc_getinfo(dev_info_t *devi, ddi_info_cmd_t cmd, void *arg, void **result) { switch (cmd) { case DDI_INFO_DEVT2DEVINFO: switch (getminor((dev_t)arg)) { case KCPC_MINOR_SHARED: *result = kcpc_devi; return (DDI_SUCCESS); default: break; } break; case DDI_INFO_DEVT2INSTANCE: *result = 0; return (DDI_SUCCESS); default: break; } return (DDI_FAILURE); } static struct dev_ops dev_ops = { DEVO_REV, 0, kcpc_getinfo, nulldev, /* identify */ kcpc_probe, kcpc_attach, nodev, /* detach */ nodev, /* reset */ &cb_ops, (struct bus_ops *)0, NULL, ddi_quiesce_not_needed, /* quiesce */ }; static struct modldrv modldrv = { &mod_driverops, "cpc sampling driver", &dev_ops }; static struct sysent cpc_sysent = { 5, SE_NOUNLOAD | SE_ARGC | SE_32RVAL1, cpc }; static struct modlsys modlsys = { &mod_syscallops, "cpc sampling system call", &cpc_sysent }; #ifdef _SYSCALL32_IMPL static struct modlsys modlsys32 = { &mod_syscallops32, "32-bit cpc sampling system call", &cpc_sysent }; #endif static struct modlinkage modl = { MODREV_1, &modldrv, &modlsys, #ifdef _SYSCALL32_IMPL &modlsys32, #endif }; int _init(void) { if (kcpc_init() != 0) return (ENOTSUP); return (mod_install(&modl)); } int _fini(void) { return (mod_remove(&modl)); } int _info(struct modinfo *mi) { return (mod_info(&modl, mi)); }