Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 5 Apr 2012 04:20:14 -0700
From:      David Wolfskill <david@catwhisker.org>
To:        Gleb Kurtsou <gleb.kurtsou@gmail.com>
Cc:        current@freebsd.org
Subject:   Re: Using TMPFS for /tmp and /var/run?
Message-ID:  <20120405112014.GH1420@albert.catwhisker.org>
In-Reply-To: <20120405074304.GA1048@reks>
References:  <20120404095035.GA1929@reks> <20120404133858.GB1420@albert.catwhisker.org> <20120405074304.GA1048@reks>

next in thread | previous in thread | raw e-mail | index | archive | help

--D+WCZLadiceW8Bs8
Content-Type: multipart/mixed; boundary="LWVQOr/QoF/fPPTS"
Content-Disposition: inline


--LWVQOr/QoF/fPPTS
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable

On Thu, Apr 05, 2012 at 10:43:04AM +0300, Gleb Kurtsou wrote:
> ...
> > Summary: as before, I believe that the patch didn't hurt anything, but
> > it also doesn't restrict the usable size of /tmp to the specified size
> > (from /etc/fstab):
> >=20
>=20
> I've checked on i386 and patch worked as expected, but it required
> previous patch. I've combined both patches. Could you try it.

To be very clear, I've attached the output from=20

	cd /sys && svn diff

Peace,
david
--=20
David H. Wolfskill				david@catwhisker.org
Depriving a girl or boy of an opportunity for education is evil.

See http://www.catwhisker.org/~david/publickey.gpg for my public key.

--LWVQOr/QoF/fPPTS
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename=diffs
Content-Transfer-Encoding: quoted-printable

Index: fs/tmpfs/tmpfs.h
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
--- fs/tmpfs/tmpfs.h	(revision 233868)
+++ fs/tmpfs/tmpfs.h	(working copy)
@@ -337,11 +337,10 @@
 	 * system, set during mount time.  This variable must never be
 	 * used directly as it may be bigger than the current amount of
 	 * free memory; in the extreme case, it will hold the SIZE_MAX
-	 * value.  Instead, use the TMPFS_PAGES_MAX macro. */
+	 * value. */
 	size_t			tm_pages_max;
=20
-	/* Number of pages in use by the file system.  Cannot be bigger
-	 * than the value returned by TMPFS_PAGES_MAX in any case. */
+	/* Number of pages in use by the file system. */
 	size_t			tm_pages_used;
=20
 	/* Pointer to the node representing the root directory of this
@@ -486,58 +485,32 @@
  * Memory management stuff.
  */
=20
-/* Amount of memory pages to reserve for the system (e.g., to not use by
+/*
+ * Amount of memory pages to reserve for the system (e.g., to not use by
  * tmpfs).
- * XXX: Should this be tunable through sysctl, for instance? */
-#define TMPFS_PAGES_RESERVED (4 * 1024 * 1024 / PAGE_SIZE)
+ */
+#define TMPFS_PAGES_MINRESERVED		(4 * 1024 * 1024 / PAGE_SIZE)
=20
 /*
- * Returns information about the number of available memory pages,
- * including physical and virtual ones.
- *
- * Remember to remove TMPFS_PAGES_RESERVED from the returned value to avoid
- * excessive memory usage.
- *
+ * Number of reserved swap pages should not be lower than
+ * swap_pager_almost_full high water mark.
  */
-static __inline size_t
-tmpfs_mem_info(void)
-{
+#define TMPFS_SWAP_MINRESERVED		1024
=20
-	return (swap_pager_avail + cnt.v_free_count + cnt.v_cache_count);
-}
+size_t tmpfs_mem_avail(void);
=20
-/* Returns the maximum size allowed for a tmpfs file system.  This macro
- * must be used instead of directly retrieving the value from tm_pages_max.
- * The reason is that the size of a tmpfs file system is dynamic: it lets
- * the user store files as long as there is enough free memory (including
- * physical memory and swap space).  Therefore, the amount of memory to be
- * used is either the limit imposed by the user during mount time or the
- * amount of available memory, whichever is lower.  To avoid consuming all
- * the memory for a given mount point, the system will always reserve a
- * minimum of TMPFS_PAGES_RESERVED pages, which is also taken into account
- * by this macro (see above). */
 static __inline size_t
-TMPFS_PAGES_MAX(struct tmpfs_mount *tmp)
+tmpfs_pages_used(struct tmpfs_mount *tmp)
 {
-	size_t freepages;
+	const size_t node_size =3D sizeof(struct tmpfs_node) +
+	    sizeof(struct tmpfs_dirent);
+	size_t meta_pages;
=20
-	freepages =3D tmpfs_mem_info();
-	freepages -=3D freepages < TMPFS_PAGES_RESERVED ?
-	    freepages : TMPFS_PAGES_RESERVED;
-
-	return MIN(tmp->tm_pages_max, freepages + tmp->tm_pages_used);
+	meta_pages =3D howmany((uintmax_t)tmp->tm_nodes_inuse * node_size,
+	    PAGE_SIZE);
+	return (meta_pages + tmp->tm_pages_used);
 }
=20
-/* Returns the available space for the given file system. */
-#define TMPFS_META_PAGES(tmp) (howmany((tmp)->tm_nodes_inuse * (sizeof(str=
uct tmpfs_node) \
-				+ sizeof(struct tmpfs_dirent)), PAGE_SIZE))
-#define TMPFS_FILE_PAGES(tmp) ((tmp)->tm_pages_used)
-
-#define TMPFS_PAGES_AVAIL(tmp) (TMPFS_PAGES_MAX(tmp) > \
-			TMPFS_META_PAGES(tmp)+TMPFS_FILE_PAGES(tmp)? \
-			TMPFS_PAGES_MAX(tmp) - TMPFS_META_PAGES(tmp) \
-			- TMPFS_FILE_PAGES(tmp):0)
-
 #endif
=20
 /* --------------------------------------------------------------------- */
Index: fs/tmpfs/tmpfs_subr.c
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
--- fs/tmpfs/tmpfs_subr.c	(revision 233868)
+++ fs/tmpfs/tmpfs_subr.c	(working copy)
@@ -59,6 +59,76 @@
=20
 SYSCTL_NODE(_vfs, OID_AUTO, tmpfs, CTLFLAG_RW, 0, "tmpfs file system");
=20
+static long tmpfs_swap_reserved =3D TMPFS_SWAP_MINRESERVED * 2;
+
+static long tmpfs_pages_reserved =3D TMPFS_PAGES_MINRESERVED;
+
+static int
+sysctl_mem_reserved(SYSCTL_HANDLER_ARGS)
+{
+	int error;
+	long pages, bytes, reserved;
+
+	pages =3D *(long *)arg1;
+	bytes =3D pages * PAGE_SIZE;
+
+	error =3D sysctl_handle_long(oidp, &bytes, 0, req);
+	if (error || !req->newptr)
+		return (error);
+
+	pages =3D bytes / PAGE_SIZE;
+	if (arg1 =3D=3D &tmpfs_swap_reserved)
+		reserved =3D TMPFS_SWAP_MINRESERVED;
+	else
+		reserved =3D TMPFS_PAGES_MINRESERVED;
+	if (pages < reserved)
+		return (EINVAL);
+
+	*(long *)arg1 =3D pages;
+	return (0);
+}
+
+SYSCTL_PROC(_vfs_tmpfs, OID_AUTO, memory_reserved, CTLTYPE_LONG|CTLFLAG_RW,
+    &tmpfs_pages_reserved, 0, sysctl_mem_reserved, "L", "reserved memory");
+SYSCTL_PROC(_vfs_tmpfs, OID_AUTO, swap_reserved, CTLTYPE_LONG|CTLFLAG_RW,
+    &tmpfs_swap_reserved, 0, sysctl_mem_reserved, "L", "reserved swap memo=
ry");
+
+size_t
+tmpfs_mem_avail(void)
+{
+	vm_ooffset_t avail_swap, avail_mem;
+
+	avail_swap =3D swap_pager_avail - tmpfs_swap_reserved;
+	if (__predict_false(avail_swap <=3D 0)) {
+		/* FIXME No swap or disabled swap check */
+		if (swap_pager_avail =3D=3D 0)
+			avail_swap =3D 0;
+		else
+			return (0);
+	}
+	avail_mem =3D cnt.v_free_count + cnt.v_cache_count - tmpfs_pages_reserved;
+	if (__predict_false(avail_mem < 0))
+		avail_mem =3D 0;
+	return (avail_swap + avail_mem);
+}
+
+static size_t
+tmpfs_pages_check_avail(struct tmpfs_mount *tmp, size_t req_pages)
+{
+	size_t avail;
+
+	avail =3D tmpfs_mem_avail();
+	if (avail < req_pages)
+		return (0);
+
+	if (tmp->tm_pages_max !=3D SIZE_MAX)
+		avail =3D tmp->tm_pages_max - tmpfs_pages_used(tmp);
+		if (avail < req_pages)
+			return (0);
+
+	return (1);
+}
+
 /* --------------------------------------------------------------------- */
=20
 /*
@@ -99,6 +169,8 @@
=20
 	if (tmp->tm_nodes_inuse >=3D tmp->tm_nodes_max)
 		return (ENOSPC);
+	if (tmpfs_pages_check_avail(tmp, 1) =3D=3D 0)
+		return (ENOSPC);
=20
 	nnode =3D (struct tmpfs_node *)uma_zalloc_arg(
 				tmp->tm_node_pool, tmp, M_WAITOK);
@@ -917,7 +989,7 @@
 	MPASS(oldpages =3D=3D uobj->size);
 	newpages =3D OFF_TO_IDX(newsize + PAGE_MASK);
 	if (newpages > oldpages &&
-	    newpages - oldpages > TMPFS_PAGES_AVAIL(tmp))
+	    tmpfs_pages_check_avail(tmp, newpages - oldpages) =3D=3D 0)
 		return (ENOSPC);
=20
 	VM_OBJECT_LOCK(uobj);
Index: fs/tmpfs/tmpfs_vfsops.c
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
--- fs/tmpfs/tmpfs_vfsops.c	(revision 233868)
+++ fs/tmpfs/tmpfs_vfsops.c	(working copy)
@@ -90,6 +90,8 @@
 	struct tmpfs_node *node =3D (struct tmpfs_node *)mem;
=20
 	node->tn_gen++;
+	if (node->tn_gen =3D=3D 0)
+		node->tn_gen =3D (arc4random() / 2) + 1;
 	node->tn_size =3D 0;
 	node->tn_status =3D 0;
 	node->tn_flags =3D 0;
@@ -114,7 +116,7 @@
 	node->tn_id =3D 0;
=20
 	mtx_init(&node->tn_interlock, "tmpfs node interlock", NULL, MTX_DEF);
-	node->tn_gen =3D arc4random();
+	node->tn_gen =3D (arc4random() / 2) + 1;
=20
 	return (0);
 }
@@ -127,17 +129,59 @@
 	mtx_destroy(&node->tn_interlock);
 }
=20
+/*
+ * XXX Rename to vfs_getopt_size()
+ */
 static int
+tmpfs_getopt_size(struct vfsoptlist *opts, const char *name, u_quad_t *dat=
a)
+{
+	char *opt_value, *vtp;
+	quad_t	iv;
+	int error, opt_len;
+
+	error =3D vfs_getopt(opts, name, (void **)&opt_value, &opt_len);
+	if (error !=3D 0)
+		return (error);
+	if (opt_len =3D=3D 0 || opt_value =3D=3D NULL)
+		return (EINVAL);
+	if (opt_value[0] =3D=3D '\0' || opt_value[opt_len - 1] !=3D '\0')
+		return (EINVAL);
+
+	iv =3D strtoq(opt_value, &vtp, 0);
+	if (vtp =3D=3D opt_value || (vtp[0] !=3D '\0' && vtp[1] !=3D '\0'))
+		return (EINVAL);
+	if (iv < 0)
+		return (EINVAL);
+	switch (vtp[0]) {
+	case 't': case 'T':
+		iv *=3D 1024;
+	case 'g': case 'G':
+		iv *=3D 1024;
+	case 'm': case 'M':
+		iv *=3D 1024;
+	case 'k': case 'K':
+		iv *=3D 1024;
+	case '\0':
+		break;
+	default:
+		return (EINVAL);
+	}
+	*data =3D iv;
+
+	return (0);
+}
+
+static int
 tmpfs_mount(struct mount *mp)
 {
+	const size_t nodes_per_page =3D howmany(PAGE_SIZE,
+	    sizeof(struct tmpfs_dirent) + sizeof(struct tmpfs_node));
 	struct tmpfs_mount *tmp;
 	struct tmpfs_node *root;
-	size_t pages;
-	uint32_t nodes;
 	int error;
 	/* Size counters. */
-	u_int nodes_max;
-	u_quad_t size_max, maxfilesize;
+	u_quad_t pages;
+	u_quad_t nodes_max, size_max, maxfilesize;
=20
 	/* Root node attributes. */
 	uid_t root_uid;
@@ -173,17 +217,16 @@
 	if (mp->mnt_cred->cr_ruid !=3D 0 ||
 	    vfs_scanopt(mp->mnt_optnew, "mode", "%ho", &root_mode) !=3D 1)
 		root_mode =3D va.va_mode;
-	if (vfs_scanopt(mp->mnt_optnew, "inodes", "%u", &nodes_max) !=3D 1)
+	if (tmpfs_getopt_size(mp->mnt_optnew, "inodes", &nodes_max) !=3D 0)
 		nodes_max =3D 0;
-	if (vfs_scanopt(mp->mnt_optnew, "size", "%qu", &size_max) !=3D 1)
+	if (tmpfs_getopt_size(mp->mnt_optnew, "size", &size_max) !=3D 0)
 		size_max =3D 0;
-	if (vfs_scanopt(mp->mnt_optnew, "maxfilesize", "%qu",
-	    &maxfilesize) !=3D 1)
+	if (tmpfs_getopt_size(mp->mnt_optnew, "maxfilesize", &maxfilesize) !=3D 0)
 		maxfilesize =3D 0;
=20
 	/* Do not allow mounts if we do not have enough memory to preserve
 	 * the minimum reserved pages. */
-	if (tmpfs_mem_info() < TMPFS_PAGES_RESERVED)
+	if (tmpfs_mem_avail() < TMPFS_PAGES_MINRESERVED)
 		return ENOSPC;
=20
 	/* Get the maximum number of memory pages this file system is
@@ -196,21 +239,27 @@
 		pages =3D howmany(size_max, PAGE_SIZE);
 	MPASS(pages > 0);
=20
+	if (pages < SIZE_MAX / PAGE_SIZE)
+		size_max =3D pages * PAGE_SIZE;
+	else
+		size_max =3D SIZE_MAX;
+
 	if (nodes_max <=3D 3) {
-		if (pages > UINT32_MAX - 3)
-			nodes =3D UINT32_MAX;
+		if (pages < INT_MAX / nodes_per_page)
+			nodes_max =3D pages * nodes_per_page;
 		else
-			nodes =3D pages + 3;
-	} else
-		nodes =3D nodes_max;
-	MPASS(nodes >=3D 3);
+			nodes_max =3D INT_MAX;
+	}
+	if (nodes_max > INT_MAX)
+		nodes_max =3D INT_MAX;
+	MPASS(nodes_max >=3D 3);
=20
 	/* Allocate the tmpfs mount structure and fill it. */
 	tmp =3D (struct tmpfs_mount *)malloc(sizeof(struct tmpfs_mount),
 	    M_TMPFSMNT, M_WAITOK | M_ZERO);
=20
 	mtx_init(&tmp->allnode_lock, "tmpfs allnode lock", NULL, MTX_DEF);
-	tmp->tm_nodes_max =3D nodes;
+	tmp->tm_nodes_max =3D nodes_max;
 	tmp->tm_nodes_inuse =3D 0;
 	tmp->tm_maxfilesize =3D maxfilesize > 0 ? maxfilesize : UINT64_MAX;
 	LIST_INIT(&tmp->tm_nodes_used);
@@ -355,16 +404,16 @@
 	if (tfhp->tf_len !=3D sizeof(struct tmpfs_fid))
 		return EINVAL;
=20
-	if (tfhp->tf_id >=3D tmp->tm_nodes_max)
+	if (tfhp->tf_id > INT_MAX || tfhp->tf_id <=3D 0)
 		return EINVAL;
=20
 	found =3D FALSE;
=20
 	TMPFS_LOCK(tmp);
 	LIST_FOREACH(node, &tmp->tm_nodes_used, tn_entries) {
-		if (node->tn_id =3D=3D tfhp->tf_id &&
-		    node->tn_gen =3D=3D tfhp->tf_gen) {
-			found =3D TRUE;
+		if (node->tn_id =3D=3D tfhp->tf_id) {
+			if (node->tn_gen =3D=3D tfhp->tf_gen)
+				found =3D TRUE;
 			break;
 		}
 	}
@@ -373,7 +422,7 @@
 	if (found)
 		return (tmpfs_alloc_vp(mp, node, LK_EXCLUSIVE, vpp));
=20
-	return (EINVAL);
+	return (ESTALE);
 }
=20
 /* --------------------------------------------------------------------- */
@@ -382,22 +431,26 @@
 static int
 tmpfs_statfs(struct mount *mp, struct statfs *sbp)
 {
-	fsfilcnt_t freenodes;
 	struct tmpfs_mount *tmp;
+	size_t used;
=20
 	tmp =3D VFS_TO_TMPFS(mp);
=20
 	sbp->f_iosize =3D PAGE_SIZE;
 	sbp->f_bsize =3D PAGE_SIZE;
=20
-	sbp->f_blocks =3D TMPFS_PAGES_MAX(tmp);
-	sbp->f_bavail =3D sbp->f_bfree =3D TMPFS_PAGES_AVAIL(tmp);
-
-	freenodes =3D MIN(tmp->tm_nodes_max - tmp->tm_nodes_inuse,
-	    TMPFS_PAGES_AVAIL(tmp) * PAGE_SIZE / sizeof(struct tmpfs_node));
-
-	sbp->f_files =3D freenodes + tmp->tm_nodes_inuse;
-	sbp->f_ffree =3D freenodes;
+	used =3D tmpfs_pages_used(tmp);
+	if (tmp->tm_pages_max !=3D SIZE_MAX)
+		 sbp->f_blocks =3D tmp->tm_pages_max;
+	else
+		 sbp->f_blocks =3D used + tmpfs_mem_avail();
+	if (sbp->f_blocks <=3D used)
+		sbp->f_bavail =3D 0;
+	else
+		sbp->f_bavail =3D sbp->f_blocks - used;
+	sbp->f_bfree =3D sbp->f_bavail;
+	sbp->f_files =3D tmp->tm_nodes_max;
+	sbp->f_ffree =3D tmp->tm_nodes_max - tmp->tm_nodes_inuse;
 	/* sbp->f_owner =3D tmp->tn_uid; */
=20
 	return 0;
Index: conf/newvers.sh
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
--- conf/newvers.sh	(revision 233868)
+++ conf/newvers.sh	(working copy)
@@ -87,55 +87,25 @@
 v=3D`cat version` u=3D${USER:-root} d=3D`pwd` h=3D${HOSTNAME:-`hostname`} =
t=3D`date`
 i=3D`${MAKE:-make} -V KERN_IDENT`
=20
-for dir in /bin /usr/bin /usr/local/bin; do
-	if [ -x "${dir}/svnversion" ] ; then
-		svnversion=3D${dir}/svnversion
-		break
-	fi
-done
-if [ -d "${SYSDIR}/../.git" ] ; then
-	for dir in /bin /usr/bin /usr/local/bin; do
-		if [ -x "${dir}/git" ] ; then
-			git_cmd=3D"${dir}/git --git-dir=3D${SYSDIR}/../.git"
-			break
-		fi
-	done
+if [ -r ${SYSDIR}/conf/get_version_from_vcs.sh ] ; then
+	. ${SYSDIR}/conf/get_version_from_vcs.sh
+else
+	# Fallback function to get a "version ID" if we can't find
+	# a replacement function.
+	get_version_from_vcs() {
+		date +%s
+	}
 fi
+version_from_vcs=3D`get_version_from_vcs`
=20
-if [ -n "$svnversion" ] ; then
-	svn=3D`cd ${SYSDIR} && $svnversion`
-	case "$svn" in
-	[0-9]*)	svn=3D" r${svn}" ;;
-	*)	unset svn ;;
-	esac
+if [ -n "$version_from_vcs" ]; then
+	version_from_vcs=3D" $version_from_vcs"
 fi
=20
-if [ -n "$git_cmd" ] ; then
-	git=3D`$git_cmd rev-parse --verify --short HEAD 2>/dev/null`
-	svn=3D`$git_cmd svn find-rev $git 2>/dev/null`
-	if [ -n "$svn" ] ; then
-		svn=3D" r${svn}"
-		git=3D"=3D${git}"
-	else
-		svn=3D`$git_cmd log | fgrep 'git-svn-id:' | head -1 | \
-		     sed -n 's/^.*@\([0-9][0-9]*\).*$/\1/p'`
-		if [ -n $svn ] ; then
-			svn=3D" r${svn}"
-			git=3D"+${git}"
-		else
-			git=3D" ${git}"
-		fi
-	fi
-	if $git_cmd --work-tree=3D${SYSDIR}/.. diff-index \
-	    --name-only HEAD | read dummy; then
-		git=3D"${git}-dirty"
-	fi
-fi
-
 cat << EOF > vers.c
 $COPYRIGHT
-#define SCCSSTR "@(#)${VERSION} #${v}${svn}${git}: ${t}"
-#define VERSTR "${VERSION} #${v}${svn}${git}: ${t}\\n    ${u}@${h}:${d}\\n"
+#define SCCSSTR "@(#)${VERSION} #${v}${version_from_vcs}: ${t}"
+#define VERSTR "${VERSION} #${v}${version_from_vcs}: ${t}\\n    ${u}@${h}:=
${d}\\n"
 #define RELSTR "${RELEASE}"
=20
 char sccs[sizeof(SCCSSTR) > 128 ? sizeof(SCCSSTR) : 128] =3D SCCSSTR;

--LWVQOr/QoF/fPPTS--

--D+WCZLadiceW8Bs8
Content-Type: application/pgp-signature
Content-Disposition: inline

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.19 (FreeBSD)

iEYEARECAAYFAk99f+0ACgkQmprOCmdXAD3FhgCfaMe8kd8u5RK+KBIlS6eh7W3U
9m4An1g1/9qlKdk4iIgfkRmf6Os52suH
=s6F7
-----END PGP SIGNATURE-----

--D+WCZLadiceW8Bs8--



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20120405112014.GH1420>