* Portions Copyright 2011 Martin Matuska
* Portions Copyright 2012 Pawel Jakub Dawidek <pawel@dawidek.net>
* Copyright (c) 2012, Joyent, Inc. All rights reserved.
- * Copyright 2011 Nexenta Systems, Inc. All rights reserved.
- * Copyright (c) 2012, Joyent, Inc. All rights reserved.
- * Copyright (c) 201i3 by Delphix. All rights reserved.
+ * Copyright 2015 Nexenta Systems, Inc. All rights reserved.
+ * Copyright (c) 2014, Joyent, Inc. All rights reserved.
+ * Copyright (c) 2011, 2015 by Delphix. All rights reserved.
* Copyright (c) 2013 by Saso Kiselkov. All rights reserved.
* Copyright (c) 2013 Steven Hartland. All rights reserved.
- * Copyright (c) 2014, Nexenta Systems, Inc. All rights reserved.
+ * Copyright (c) 2016 Actifio, Inc. All rights reserved.
*/
/*
#include <sys/zfeature.h>
#include <linux/miscdevice.h>
+#include <linux/slab.h>
#include "zfs_namecheck.h"
#include "zfs_prop.h"
#include "zfs_deleg.h"
#include "zfs_comutil.h"
+/*
+ * Limit maximum nvlist size. We don't want users passing in insane values
+ * for zc->zc_nvlist_src_size, since we will need to allocate that much memory.
+ */
+#define MAX_NVLIST_SRC_SIZE KMALLOC_MAX_SIZE
+
kmutex_t zfsdev_state_lock;
zfsdev_state_t *zfsdev_state_list;
int zfs_set_prop_nvlist(const char *, zprop_source_t, nvlist_t *, nvlist_t *);
static int get_nvlist(uint64_t nvl, uint64_t size, int iflag, nvlist_t **nvp);
-#if defined(HAVE_DECLARE_EVENT_CLASS)
-void
-__dprintf(const char *file, const char *func, int line, const char *fmt, ...)
-{
- const char *newfile;
- size_t size = 4096;
- char *buf = kmem_alloc(size, KM_SLEEP);
- char *nl;
- va_list adx;
-
- /*
- * Get rid of annoying prefix to filename.
- */
- newfile = strrchr(file, '/');
- if (newfile != NULL) {
- newfile = newfile + 1; /* Get rid of leading / */
- } else {
- newfile = file;
- }
-
- va_start(adx, fmt);
- (void) vsnprintf(buf, size, fmt, adx);
- va_end(adx);
-
- /*
- * Get rid of trailing newline.
- */
- nl = strrchr(buf, '\n');
- if (nl != NULL)
- *nl = '\0';
-
- /*
- * To get this data enable the zfs__dprintf trace point as shown:
- *
- * # Enable zfs__dprintf tracepoint, clear the tracepoint ring buffer
- * $ echo 1 > /sys/module/zfs/parameters/zfs_flags
- * $ echo 1 > /sys/kernel/debug/tracing/events/zfs/enable
- * $ echo 0 > /sys/kernel/debug/tracing/trace
- *
- * # Dump the ring buffer.
- * $ cat /sys/kernel/debug/tracing/trace
- */
- DTRACE_PROBE4(zfs__dprintf,
- char *, newfile, char *, func, int, line, char *, buf);
-
- kmem_free(buf, size);
-}
-#endif /* HAVE_DECLARE_EVENT_CLASS */
-
static void
history_str_free(char *buf)
{
break;
case ZFS_PROP_QUOTA:
+ case ZFS_PROP_FILESYSTEM_LIMIT:
+ case ZFS_PROP_SNAPSHOT_LIMIT:
if (!INGLOBALZONE(curproc)) {
uint64_t zoned;
char setpoint[MAXNAMELEN];
/*
* Unprivileged users are allowed to modify the
- * quota on things *under* (ie. contained by)
+ * limit on things *under* (ie. contained by)
* the thing they own.
*/
if (dsl_prop_get_integer(dsname, "zoned", &zoned,
dd = clone->ds_dir;
error = dsl_dataset_hold_obj(dd->dd_pool,
- dd->dd_phys->dd_origin_obj, FTAG, &origin);
+ dsl_dir_phys(dd)->dd_origin_obj, FTAG, &origin);
if (error != 0) {
dsl_dataset_rele(clone, FTAG);
dsl_pool_rele(dp, FTAG);
if ((error = ddi_copyin((void *)(uintptr_t)nvl, packed, size,
iflag)) != 0) {
vmem_free(packed, size);
- return (error);
+ return (SET_ERROR(EFAULT));
}
if ((error = nvlist_unpack(packed, size, &list, 0)) != 0) {
int error = 0;
if (get_zfs_sb(name, zsbp) != 0)
- error = zfs_sb_create(name, zsbp);
+ error = zfs_sb_create(name, NULL, zsbp);
if (error == 0) {
- rrw_enter(&(*zsbp)->z_teardown_lock, (writer) ? RW_WRITER :
+ rrm_enter(&(*zsbp)->z_teardown_lock, (writer) ? RW_WRITER :
RW_READER, tag);
if ((*zsbp)->z_unmounted) {
/*
* thread should be just about to disassociate the
* objset from the zsb.
*/
- rrw_exit(&(*zsbp)->z_teardown_lock, tag);
+ rrm_exit(&(*zsbp)->z_teardown_lock, tag);
return (SET_ERROR(EBUSY));
}
}
static void
zfs_sb_rele(zfs_sb_t *zsb, void *tag)
{
- rrw_exit(&zsb->z_teardown_lock, tag);
+ rrm_exit(&zsb->z_teardown_lock, tag);
if (zsb->z_sb) {
deactivate_super(zsb->z_sb);
int error;
zfs_log_history(zc);
error = spa_destroy(zc->zc_name);
- if (error == 0)
- zvol_remove_minors(zc->zc_name);
+
return (error);
}
}
nvlist_free(config);
-
- if (props)
- nvlist_free(props);
+ nvlist_free(props);
return (error);
}
zfs_log_history(zc);
error = spa_export(zc->zc_name, NULL, force, hardforce);
- if (error == 0)
- zvol_remove_minors(zc->zc_name);
+
return (error);
}
const char *propname = nvpair_name(pair);
zfs_prop_t prop = zfs_name_to_prop(propname);
uint64_t intval;
- int err;
+ int err = -1;
if (prop == ZPROP_INVAL) {
if (zfs_prop_userquota(propname))
case ZFS_PROP_REFQUOTA:
err = dsl_dataset_set_refquota(dsname, source, intval);
break;
+ case ZFS_PROP_FILESYSTEM_LIMIT:
+ case ZFS_PROP_SNAPSHOT_LIMIT:
+ if (intval == UINT64_MAX) {
+ /* clearing the limit, just do it */
+ err = 0;
+ } else {
+ err = dsl_dir_activate_fs_ss_limit(dsname);
+ }
+ /*
+ * Set err to -1 to force the zfs_set_prop_nvlist code down the
+ * default path to set the value in the nvlist.
+ */
+ if (err == 0)
+ err = -1;
+ break;
case ZFS_PROP_RESERVATION:
err = dsl_dir_set_reservation(dsname, source, intval);
break;
err = zvol_set_volsize(dsname, intval);
break;
case ZFS_PROP_SNAPDEV:
- err = zvol_set_snapdev(dsname, intval);
+ err = zvol_set_snapdev(dsname, source, intval);
break;
case ZFS_PROP_VERSION:
{
volblocksize = zfs_prop_default_numeric(
ZFS_PROP_VOLBLOCKSIZE);
- if ((error = zvol_check_volblocksize(
+ if ((error = zvol_check_volblocksize(fsname,
volblocksize)) != 0 ||
(error = zvol_check_volsize(volsize,
volblocksize)) != 0)
if (error == 0) {
error = zfs_set_prop_nvlist(fsname, ZPROP_SRC_LOCAL,
nvprops, outnvl);
- if (error != 0)
- (void) dsl_destroy_head(fsname);
- }
-
-#ifdef _KERNEL
- if (error == 0 && type == DMU_OST_ZVOL)
- zvol_create_minors(fsname);
-#endif
+ if (error != 0) {
+ spa_t *spa;
+ int error2;
+ /*
+ * Volumes will return EBUSY and cannot be destroyed
+ * until all asynchronous minor handling has completed.
+ * Wait for the spa_zvol_taskq to drain then retry.
+ */
+ error2 = dsl_destroy_head(fsname);
+ while ((error2 == EBUSY) && (type == DMU_OST_ZVOL)) {
+ error2 = spa_open(fsname, &spa, FTAG);
+ if (error2 == 0) {
+ taskq_wait(spa->spa_zvol_taskq);
+ spa_close(spa, FTAG);
+ }
+ error2 = dsl_destroy_head(fsname);
+ }
+ }
+ }
return (error);
}
if (error != 0)
(void) dsl_destroy_head(fsname);
}
-
-#ifdef _KERNEL
- if (error == 0)
- zvol_create_minors(fsname);
-#endif
-
return (error);
}
error = dsl_dataset_snapshot(snaps, props, outnvl);
-#ifdef _KERNEL
- if (error == 0)
- zvol_create_minors(poolname);
-#endif
-
return (error);
}
* This function is best-effort. Callers must deal gracefully if it
* remains mounted (or is remounted after this call).
*
- * XXX: This function should detect a failure to unmount a snapdir of a dataset
- * and return the appropriate error code when it is mounted. Its Illumos and
- * FreeBSD counterparts do this. We do not do this on Linux because there is no
- * clear way to access the mount information that FreeBSD and Illumos use to
- * distinguish between things with mounted snapshot directories, and things
- * without mounted snapshot directories, which include zvols. Returning a
- * failure for the latter causes `zfs destroy` to fail on zvol snapshots.
+ * Returns 0 if the argument is not a snapshot, or it is not currently a
+ * filesystem, or we were able to unmount it. Returns error code otherwise.
*/
int
zfs_unmount_snap(const char *snapname)
{
- zfs_sb_t *zsb = NULL;
- char *dsname;
- char *fullname;
- char *ptr;
+ int err;
- if ((ptr = strchr(snapname, '@')) == NULL)
+ if (strchr(snapname, '@') == NULL)
return (0);
- dsname = kmem_alloc(ptr - snapname + 1, KM_SLEEP);
- strlcpy(dsname, snapname, ptr - snapname + 1);
- fullname = strdup(snapname);
-
- if (zfs_sb_hold(dsname, FTAG, &zsb, B_FALSE) == 0) {
- ASSERT(!dsl_pool_config_held(dmu_objset_pool(zsb->z_os)));
- (void) zfsctl_unmount_snapshot(zsb, fullname, MNT_FORCE);
- zfs_sb_rele(zsb, FTAG);
- }
-
- kmem_free(dsname, ptr - snapname + 1);
- strfree(fullname);
+ err = zfsctl_snapshot_unmount((char *)snapname, MNT_FORCE);
+ if (err != 0 && err != ENOENT)
+ return (SET_ERROR(err));
return (0);
}
for (pair = nvlist_next_nvpair(snaps, NULL); pair != NULL;
pair = nvlist_next_nvpair(snaps, pair)) {
(void) zfs_unmount_snap(nvpair_name(pair));
- (void) zvol_remove_minor(nvpair_name(pair));
}
return (dsl_destroy_snapshots_nvl(snaps, defer, outnvl));
err = dsl_destroy_snapshot(zc->zc_name, zc->zc_defer_destroy);
else
err = dsl_destroy_head(zc->zc_name);
- if (zc->zc_objset_type == DMU_OST_ZVOL && err == 0)
- (void) zvol_remove_minor(zc->zc_name);
+
return (err);
}
* the SPA supports it. We ignore any errors here since
* we'll catch them later.
*/
- if (nvpair_type(pair) == DATA_TYPE_UINT64 &&
- nvpair_value_uint64(pair, &intval) == 0) {
+ if (nvpair_value_uint64(pair, &intval) == 0) {
if (intval >= ZIO_COMPRESS_GZIP_1 &&
intval <= ZIO_COMPRESS_GZIP_9 &&
zfs_earlier_version(dsname,
return (SET_ERROR(ENOTSUP));
break;
+ case ZFS_PROP_VOLBLOCKSIZE:
+ case ZFS_PROP_RECORDSIZE:
+ /* Record sizes above 128k need the feature to be enabled */
+ if (nvpair_value_uint64(pair, &intval) == 0 &&
+ intval > SPA_OLD_MAXBLOCKSIZE) {
+ spa_t *spa;
+
+ /*
+ * If this is a bootable dataset then
+ * the we don't allow large (>128K) blocks,
+ * because GRUB doesn't support them.
+ */
+ if (zfs_is_bootfs(dsname) &&
+ intval > SPA_OLD_MAXBLOCKSIZE) {
+ return (SET_ERROR(ERANGE));
+ }
+
+ /*
+ * We don't allow setting the property above 1MB,
+ * unless the tunable has been changed.
+ */
+ if (intval > zfs_max_recordsize ||
+ intval > SPA_MAXBLOCKSIZE)
+ return (SET_ERROR(ERANGE));
+
+ if ((err = spa_open(dsname, &spa, FTAG)) != 0)
+ return (err);
+
+ if (!spa_feature_is_enabled(spa,
+ SPA_FEATURE_LARGE_BLOCKS)) {
+ spa_close(spa, FTAG);
+ return (SET_ERROR(ENOTSUP));
+ }
+ spa_close(spa, FTAG);
+ }
+ break;
+
case ZFS_PROP_SHARESMB:
if (zpl_earlier_version(dsname, ZPL_VERSION_FUID))
return (SET_ERROR(ENOTSUP));
}
#endif
-#ifdef _KERNEL
- if (error == 0)
- zvol_create_minors(tofs);
-#endif
-
/*
* On error, restore the original props.
*/
* zc_fromobj objsetid of incremental fromsnap (may be zero)
* zc_guid if set, estimate size of stream only. zc_cookie is ignored.
* output size in zc_objset_type.
- * zc_flags if =1, WRITE_EMBEDDED records are permitted
+ * zc_flags lzc_send_flags
*
* outputs:
* zc_objset_type estimated size, if zc_guid is set
offset_t off;
boolean_t estimate = (zc->zc_guid != 0);
boolean_t embedok = (zc->zc_flags & 0x1);
+ boolean_t large_block_ok = (zc->zc_flags & 0x2);
if (zc->zc_obj != 0) {
dsl_pool_t *dp;
}
if (dsl_dir_is_clone(tosnap->ds_dir))
- zc->zc_fromobj = tosnap->ds_dir->dd_phys->dd_origin_obj;
+ zc->zc_fromobj =
+ dsl_dir_phys(tosnap->ds_dir)->dd_origin_obj;
dsl_dataset_rele(tosnap, FTAG);
dsl_pool_rele(dp, FTAG);
}
off = fp->f_offset;
error = dmu_send_obj(zc->zc_name, zc->zc_sendobj,
- zc->zc_fromobj, embedok, zc->zc_cookie, fp->f_vnode, &off);
+ zc->zc_fromobj, embedok, large_block_ok,
+ zc->zc_cookie, fp->f_vnode, &off);
if (VOP_SEEK(fp->f_vnode, fp->f_offset, &off, NULL) == 0)
fp->f_offset = off;
if (error != 0)
return (error);
- error = dmu_object_next(os, &zc->zc_obj, B_FALSE,
- os->os_dsl_dataset->ds_phys->ds_prev_snap_txg);
+ error = dmu_object_next(os, &zc->zc_obj, B_FALSE, 0);
dmu_objset_rele(os, FTAG);
return (error);
if ((error = get_nvlist(zc->zc_nvlist_src,
zc->zc_nvlist_src_size, zc->zc_iflags, &nvlist)) != 0) {
VN_RELE(vp);
+ VN_RELE(ZTOV(sharedir));
ZFS_EXIT(zsb);
return (error);
}
static int
zfs_ioc_hold(const char *pool, nvlist_t *args, nvlist_t *errlist)
{
+ nvpair_t *pair;
nvlist_t *holds;
int cleanup_fd = -1;
int error;
if (error != 0)
return (SET_ERROR(EINVAL));
+ /* make sure the user didn't pass us any invalid (empty) tags */
+ for (pair = nvlist_next_nvpair(holds, NULL); pair != NULL;
+ pair = nvlist_next_nvpair(holds, pair)) {
+ char *htag;
+
+ error = nvpair_value_string(pair, &htag);
+ if (error != 0)
+ return (SET_ERROR(error));
+
+ if (strlen(htag) == 0)
+ return (SET_ERROR(EINVAL));
+ }
+
if (nvlist_lookup_int32(args, "cleanup_fd", &cleanup_fd) == 0) {
error = zfs_onexit_fd_hold(cleanup_fd, &minor);
if (error != 0)
return (error);
error = dsl_dataset_hold(dp, lastsnap, FTAG, &new);
+ if (error == 0 && !new->ds_is_snapshot) {
+ dsl_dataset_rele(new, FTAG);
+ error = SET_ERROR(EINVAL);
+ }
if (error != 0) {
dsl_pool_rele(dp, FTAG);
return (error);
}
error = dsl_dataset_hold(dp, firstsnap, FTAG, &old);
+ if (error == 0 && !old->ds_is_snapshot) {
+ dsl_dataset_rele(old, FTAG);
+ error = SET_ERROR(EINVAL);
+ }
if (error != 0) {
dsl_dataset_rele(new, FTAG);
dsl_pool_rele(dp, FTAG);
* innvl: {
* "fd" -> file descriptor to write stream to (int32)
* (optional) "fromsnap" -> full snap name to send an incremental from
+ * (optional) "largeblockok" -> (value ignored)
+ * indicates that blocks > 128KB are permitted
* (optional) "embedok" -> (value ignored)
* presence indicates DRR_WRITE_EMBEDDED records are permitted
* }
char *fromname = NULL;
int fd;
file_t *fp;
+ boolean_t largeblockok;
boolean_t embedok;
error = nvlist_lookup_int32(innvl, "fd", &fd);
(void) nvlist_lookup_string(innvl, "fromsnap", &fromname);
+ largeblockok = nvlist_exists(innvl, "largeblockok");
embedok = nvlist_exists(innvl, "embedok");
if ((fp = getf(fd)) == NULL)
return (SET_ERROR(EBADF));
off = fp->f_offset;
- error = dmu_send(snapname, fromname, embedok, fd, fp->f_vnode, &off);
+ error = dmu_send(snapname, fromname, embedok, largeblockok,
+ fd, fp->f_vnode, &off);
if (VOP_SEEK(fp->f_vnode, fp->f_offset, &off, NULL) == 0)
fp->f_offset = off;
* of bytes that will be written to the fd supplied to zfs_ioc_send_new().
*
* innvl: {
- * (optional) "fromsnap" -> full snap name to send an incremental from
+ * (optional) "from" -> full snap or bookmark name to send an incremental
+ * from
* }
*
* outnvl: {
zfs_ioc_send_space(const char *snapname, nvlist_t *innvl, nvlist_t *outnvl)
{
dsl_pool_t *dp;
- dsl_dataset_t *fromsnap = NULL;
dsl_dataset_t *tosnap;
int error;
char *fromname;
return (error);
}
- error = nvlist_lookup_string(innvl, "fromsnap", &fromname);
+ error = nvlist_lookup_string(innvl, "from", &fromname);
if (error == 0) {
- error = dsl_dataset_hold(dp, fromname, FTAG, &fromsnap);
- if (error != 0) {
- dsl_dataset_rele(tosnap, FTAG);
- dsl_pool_rele(dp, FTAG);
- return (error);
+ if (strchr(fromname, '@') != NULL) {
+ /*
+ * If from is a snapshot, hold it and use the more
+ * efficient dmu_send_estimate to estimate send space
+ * size using deadlists.
+ */
+ dsl_dataset_t *fromsnap;
+ error = dsl_dataset_hold(dp, fromname, FTAG, &fromsnap);
+ if (error != 0)
+ goto out;
+ error = dmu_send_estimate(tosnap, fromsnap, &space);
+ dsl_dataset_rele(fromsnap, FTAG);
+ } else if (strchr(fromname, '#') != NULL) {
+ /*
+ * If from is a bookmark, fetch the creation TXG of the
+ * snapshot it was created from and use that to find
+ * blocks that were born after it.
+ */
+ zfs_bookmark_phys_t frombm;
+
+ error = dsl_bookmark_lookup(dp, fromname, tosnap,
+ &frombm);
+ if (error != 0)
+ goto out;
+ error = dmu_send_estimate_from_txg(tosnap,
+ frombm.zbm_creation_txg, &space);
+ } else {
+ /*
+ * from is not properly formatted as a snapshot or
+ * bookmark
+ */
+ error = SET_ERROR(EINVAL);
+ goto out;
}
+ } else {
+ // If estimating the size of a full send, use dmu_send_estimate
+ error = dmu_send_estimate(tosnap, NULL, &space);
}
- error = dmu_send_estimate(tosnap, fromsnap, &space);
fnvlist_add_uint64(outnvl, "space", space);
- if (fromsnap != NULL)
- dsl_dataset_rele(fromsnap, FTAG);
+out:
dsl_dataset_rele(tosnap, FTAG);
dsl_pool_rele(dp, FTAG);
return (error);
}
-
static zfs_ioc_vec_t zfs_ioc_vec[ZFS_IOC_LAST - ZFS_IOC_FIRST];
static void
return (ptr);
}
-minor_t
-zfsdev_getminor(struct file *filp)
+int
+zfsdev_getminor(struct file *filp, minor_t *minorp)
{
+ zfsdev_state_t *zs, *fpd;
+
ASSERT(filp != NULL);
- ASSERT(filp->private_data != NULL);
+ ASSERT(!MUTEX_HELD(&zfsdev_state_lock));
+
+ fpd = filp->private_data;
+ if (fpd == NULL)
+ return (EBADF);
+
+ mutex_enter(&zfsdev_state_lock);
- return (((zfsdev_state_t *)filp->private_data)->zs_minor);
+ for (zs = zfsdev_state_list; zs != NULL; zs = zs->zs_next) {
+
+ if (zs->zs_minor == -1)
+ continue;
+
+ if (fpd == zs) {
+ *minorp = fpd->zs_minor;
+ mutex_exit(&zfsdev_state_lock);
+ return (0);
+ }
+ }
+
+ mutex_exit(&zfsdev_state_lock);
+
+ return (EBADF);
}
/*
}
zc->zc_iflags = flag & FKIOCTL;
- if (zc->zc_nvlist_src_size != 0) {
+ if (zc->zc_nvlist_src_size > MAX_NVLIST_SRC_SIZE) {
+ /*
+ * Make sure the user doesn't pass in an insane value for
+ * zc_nvlist_src_size. We have to check, since we will end
+ * up allocating that much memory inside of get_nvlist(). This
+ * prevents a nefarious user from allocating tons of kernel
+ * memory.
+ *
+ * Also, we return EINVAL instead of ENOMEM here. The reason
+ * being that returning ENOMEM from an ioctl() has a special
+ * connotation; that the user's size value is too small and
+ * needs to be expanded to hold the nvlist. See
+ * zcmd_expand_dst_nvlist() for details.
+ */
+ error = SET_ERROR(EINVAL); /* User's size too big */
+
+ } else if (zc->zc_nvlist_src_size != 0) {
error = get_nvlist(zc->zc_nvlist_src, zc->zc_nvlist_src_size,
zc->zc_iflags, &innvl);
if (error != 0)
}
- if (error == 0 && !(flag & FKIOCTL))
+ if (error == 0 && !(flag & FKIOCTL)) {
+ cookie = spl_fstrans_mark();
error = vec->zvec_secpolicy(zc, innvl, CRED());
+ spl_fstrans_unmark(cookie);
+ }
if (error != 0)
goto out;
static void
zfs_detach(void)
{
- int error;
zfsdev_state_t *zs, *zsprev = NULL;
- error = misc_deregister(&zfs_misc);
- if (error != 0)
- printk(KERN_INFO "ZFS: misc_deregister() failed %d\n", error);
-
+ misc_deregister(&zfs_misc);
mutex_destroy(&zfsdev_state_lock);
for (zs = zfsdev_state_list; zs != NULL; zs = zs->zs_next) {
{
int error;
- error = vn_set_pwd("/");
+ error = -vn_set_pwd("/");
if (error) {
printk(KERN_NOTICE
"ZFS: Warning unable to set pwd to '/': %d\n", error);
return (error);
}
+ if ((error = -zvol_init()) != 0)
+ return (error);
+
spa_init(FREAD | FWRITE);
zfs_init();
- if ((error = zvol_init()) != 0)
- goto out1;
-
zfs_ioctl_init();
if ((error = zfs_attach()) != 0)
- goto out2;
+ goto out;
tsd_create(&zfs_fsyncer_key, NULL);
tsd_create(&rrw_tsd_key, rrw_tsd_destroy);
return (0);
-out2:
- (void) zvol_fini();
-out1:
+out:
zfs_fini();
spa_fini();
+ (void) zvol_fini();
printk(KERN_NOTICE "ZFS: Failed to Load ZFS Filesystem v%s-%s%s"
", rc = %d\n", ZFS_META_VERSION, ZFS_META_RELEASE,
ZFS_DEBUG_STR, error);
_fini(void)
{
zfs_detach();
- zvol_fini();
zfs_fini();
spa_fini();
+ zvol_fini();
tsd_destroy(&zfs_fsyncer_key);
tsd_destroy(&rrw_tsd_key);