/* $Copyright:	$
 * Copyright (c) 1984, 1985, 1986, 1987, 1988, 1989, 1990 
 * Sequent Computer Systems, Inc.   All rights reserved.
 *  
 * This software is furnished under a license and may be used
 * only in accordance with the terms of that license and with the
 * inclusion of the above copyright notice.   This software may not
 * be provided or otherwise made available to, or used by, any
 * other person.  No title to or ownership of the software is
 * hereby transferred.
 */

/* @(#)spec_subr.c	1.4 87/03/11 NFSSRC */

/*
 * Copyright (c) 1986 by Sun Microsystems, Inc.
 */

#ident "$Header: spec_subr.c 1.23 90/01/06 $"

/*
 * spec_subr.c
 *	specfs device file system support routines
 */

/* $Log:	spec_subr.c,v $
 */
#include "sys/types.h"
#include "sys/sysmacros.h"
#include "sys/signal.h"
#include "sys/param.h"
#include "sys/debug.h"
#include "sys/systm.h"
#include "sys/mutex.h"
#include "sys/user.h"
#include "sys/errno.h"
#include "sys/buf.h"
#include "sys/vfs.h"
#include "sys/vnode.h"
#include "sys/conf.h"
#include "sys/devsw.h"
#include "sys/vtoc.h"

#include "sys/snode.h"
#include "sys/ufsinode.h"	/* for MAKEIMODE() */

#include "sys/vmmeter.h"
#include "sys/vmparam.h"
#include "sys/pte.h"
#include "sys/intctl.h"
#include "sys/plocal.h"
#include "sys/cmn_err.h"

extern struct vnodeops spec_vnodeops;
struct vnode *bdevvp();

/*
 * Snode lookup stuff.
 *
 * These routines maintain a table of "common" VBLK snodes hashed by dev
 * so that the snode for an dev can be found if it already exists.
 *
 * Hash list is locked by snode_list.
 *
 * NOTE: Only the "common" VBLK snodes allocated by bdevvp() are saved in
 *       the hashtable.
 *
 * NOTE: STABLESIZE must be a power of 2 for stablehash to work!
 */

lock_t	snode_list;

#define	STABLESIZE	16
#define	stablehash(dev)	((major(dev) + minor(dev)) & (STABLESIZE-1))

static
struct snode *stable[STABLESIZE];
int	stablesize = STABLESIZE;	/* mainly for crash(1M) to read */


/*
 * specsp()
 *	Allocate an snode structure and do rudimentary initialization.
 *
 * The snode is returned unlocked with no references.
 *
 * Remaining initialization is performed by SN_INIT(), SN_VATTR_INIT,
 * plus any special initialization by caller.
 *
 * Some snode users prefer to allocate snodes their own way rather than
 * use this routine.  E.g., fifosp() & cn_init()
 */

static int
specsp(spp)
	struct snode **spp;
{
	struct snode *sp;
	extern caddr_t kmem_zalloc();

	SPECPRINTF5("specsp: calling kmem_zalloc\n");
	sp = (struct snode *)kmem_zalloc((uint)sizeof(*sp));
	SPECPRINTF5("specsp: kmem_zalloc returned\n");
	STOV(sp)->v_op = &spec_vnodeops;
	init_rwsema(&STOV(sp)->v_nodemutex, RWSEMA_SWP);
	init_lock(&STOV(sp)->v_mutex);
	*spp = sp;
	SPECPRINTF3("specsp: allocated snode 0x%x\n", sp);
	return(0);
}

/*
 * spec_free()
 *	Deallocate an snode structure allocated by specsp().
 *
 * Called with snode locked; the lock is vaporized.
 */

spec_free(sp)
	struct snode *sp;
{
	SPECPRINTF5("spec_free: calling kmem_free for snode 0x%x\n", sp);
	kmem_free((caddr_t)sp, (uint)sizeof(*sp));
	SPECPRINTF3("spec_free: freed snode 0x%x\n", sp);
}

/*
 * cdevvp()
 *	Returns a special vnode for the given character dev.
 *
 * Used (by STREAMS clone open) when there is no (known) vnode for the 
 * character device so specvp() cannot be used.
 *
 * The uid & gid are saved for latter VOP_GETATTR() calls.
 *
 * The vnode is returned locked with a reference to be deleted by the caller.
 */
struct vnode *
cdevvp(dev, uid, gid)
	dev_t	dev;
	uid_t	uid;
	gid_t	gid;
{
	struct snode *sp;
	mode_t	imode;
	int error;

	SPECPRINTF5("cdevvp: begin major %d, minor %d, uid %d, gid %d\n",
			major(dev), minor(dev), uid, gid);

	SPECPRINTF5("cdevvp: calling specsp\n");

	/* We know that specsp() never really fails */

	if ((error = specsp(&sp)) != 0) {
		cmn_err(CE_PANIC, "cdevvp: specsp returned error %d\n", error);
		/*
		 *+ The internal kernel procedure, specsp(), returned an
		 *+ unexpected error to the internal kernel procedure
		 *+ cdevvp().  This indicates an internal kernel software
		 *+ problem.
		 */
		
	}
	imode = MAKEIMODE(VCHR, 0);	/* fstat(2) returns ufs-style mode */
	SN_INIT(sp, NULL, VCHR, dev);
	SN_VATTR_INIT(sp, imode, uid, gid);
	SPECPRINTF1("cdevvp: major %d, minor %d; allocated snode 0x%x\n",
			major(dev),minor(dev), sp);
	return (STOV(sp));
}

/*
 * bdevfind()
 *	Find the "common" VBLK snode for the specified block dev.
 *
 * If found, return the snode unlocked with (an additional) reference held, 
 *	to be released by the caller.
 * If not found, return NULL with snode_list locked;  caller must
 *	v_lock(&snode_list, *iplp);
 *
 * Note that a tricky race with {spec,fifo}_inactive is avoided.
 * See the comment in specvp().
 */

static struct snode *
bdevfind(dev, iplp)
	dev_t dev;
	spl_t *iplp;
{
	struct snode *sp;
	int hashindex;

	SPECPRINTF5("bdevfind: begin major %d, minor %d\n",
			major(dev), minor(dev));

	hashindex = stablehash(dev); /* compute before p_lock for performance */

	*iplp = p_lock(&snode_list, SPLFS);

	SPECPRINTF5("bdevfind: got snode_list\n");

	sp = stable[hashindex];
	while (sp != NULL) {
		if (sp->s_dev == dev) {
			SPECPRINTF5("bdevfind: about to p_sema snode 0x%x\n",
						sp);
			(void)p_writer_v_lock(&STOV(sp)->v_nodemutex,
					    PVNOD, &snode_list);
			VN_HOLD(STOV(sp));
			VN_UNLOCKNODE(STOV(sp));

			ASSERT(sp->s_dev == dev,"bdevvp: snode/vnode mismatch");
			/*
			 *+ An internal kernel consistency check has failed.
			 *+ This indicates a kernel software bug.
			 */

			ASSERT_DEBUG(STOV(sp)->v_rdev == dev,
				"specvp: v_rdev mismatch");
			ASSERT_DEBUG(STOV(sp)->v_type == VBLK,
				"specvp: v_type mismatch");
			ASSERT_DEBUG(sp->s_realvp == NULL,
				"specvp: s_realvp mismatch");
			ASSERT_DEBUG(sp->s_bdevvp == STOV(sp),
				"specvp: s_bdevvp mismatch");

			SPECPRINTF3("bdevfind: major %d, minor %d is snode 0x%x\n",
					major(dev), minor(dev), sp);
			return(sp);
		}
		SPECPRINTF9("bdevfind: examine snode 0x%x, major %d, minor %d\n",
				sp, major(sp->s_dev), minor(sp->s_dev));
		sp = sp->s_next;
	}
	SPECPRINTF3("bdevfind: major %d, minor %d NOT FOUND\n",
			major(dev), minor(dev));
	return(NULL);
}

/*
 * bdevadd()
 *	Add a "common" VBLK snode to the hash list.
 *
 * Called/returns with snode_list locked.
 */

static
bdevadd(sp)
	struct snode *sp;
{
	int index = stablehash(sp->s_dev);

	ASSERT_DEBUG(sp->s_next == NULL, "bdevadd: sp already in stable");

	sp->s_next = stable[index];
	stable[index] = sp;

	SPECPRINTF3("bdevadd: snode 0x%x, major %d, minor %d, type %d\n",
			sp, major(sp->s_dev), minor(sp->s_dev),
			STOV(sp)->v_type);

}

/*
 * bdevrem()
 *	Remove a "common" VBLK snode from the hash list.
 *
 * Called/returns with snode_list locked.
 */

static
bdevrem(sp)
	struct snode *sp;
{
	struct snode *st;
	struct snode *stprev = NULL;

	SPECPRINTF5("bdevrem: begin snode 0x%x, major %d, minor %d, type %d\n",
			sp, major(sp->s_dev), minor(sp->s_dev),
			STOV(sp)->v_type);

	ASSERT_DEBUG((sp->s_realvp == NULL) && (STOV(sp)->v_type == VBLK),
			"bdevdel: invalid snode type");

	st = stable[stablehash(sp->s_dev)]; 
	while (st != NULL) { 
		if (st == sp) { 
			if (stprev == NULL) {
				stable[stablehash(sp->s_dev)] = st->s_next;
			} else {
				stprev->s_next = st->s_next;
			}
			sp->s_next = NULL;
			SPECPRINTF3("bdevrem: removed snode 0x%x\n", sp);
			return;
		}	
		stprev = st;
		st = st->s_next;
	}	
	cmn_err(CE_PANIC, "bdevdel: lost snode");
	/*
	 *+ An inconsistency was found in the kernel block snode list.
	 *+ This indicates a kernel software bug.
	 */
}

/*
 * bdevvp()
 *	Returns a special vnode for the given block dev.
 *
 * The returned vnode is the one which is "common" to all the snodes 
 * which represent this same block device.
 *
 * The vnode is returned unlocked but with a reference to be deleted 
 * by the caller.
 */
struct vnode *
bdevvp(dev)
	dev_t dev;
{
	struct snode *sp;
	struct snode *sp2;
	int error;
	spl_t ipl;
	
	SPECPRINTF5("bdevvp: begin major %d, minor %d\n",major(dev),minor(dev));

	/*
	 * Caller wants a "common" block device snode to share among
	 * vnodes.  See if we already made one for someone else.
	 */

	if (sp = bdevfind(dev, &ipl)) {
		SPECPRINTF1("bdevvp: major %d, minor %d is old snode 0x%x\n",
					major(dev), minor(dev), sp);
		return(STOV(sp));
	}

	/*
	 * Unsuccessful bdevfind() returns with snode_list locked.
	 * Unlock it and start racing with other bdevvp()'s.
	 * We'll resolve the winner later.
	 */

	v_lock(&snode_list, ipl);

	/*
	 * Allocate a new snode. (We know that specsp() never really fails.)
	 */

	if ((error = specsp(&sp)) != 0) {
		cmn_err(CE_PANIC, "bdevvp: specsp returned error %d\n", error);
		/*
		 *+ The internal kernel procedure, specsp(), returned an
		 *+ unexpected error to the internal kernel procedure
		 *+ bdevvp().  This indicates an internal kernel software
		 *+ problem.
		 */
		
	}
	SN_INIT(sp, NULL, VBLK, dev);
	SN_VATTR_INIT(sp, 0, 0, 0);	/* for s_[amc]time */
	STOV(sp)->v_flag |= VNBACCT;	/* let bio avoid VN_HOLD/RELE */
	sp->s_bdevvp = STOV(sp);

	/*
	 * Before we add the new snode to the "common" VBLK list,
	 * see if we lost a race with another bdevvp().
	 */
	if (sp2 = bdevfind(dev, &ipl)) {
		/* Lost a race.  Free ours and use theirs. */
		spec_free(sp);
		SPECPRINTF1("bdevvp: major %d, minor %d is old snode 0x%x - Lost Race\n",
					major(dev), minor(dev), sp2);
		return(STOV(sp2));
	}

	/*
	 * We won the race.  Enter ours into the list.  
	 * Unsuccessful bdevfind() returns with snode_list locked.
	 */

	bdevadd(sp);
	v_lock(&snode_list, ipl);

	SPECPRINTF1("bdevvp: major %d, minor %d is NEW snode 0x%x\n",
				major(dev), minor(dev), sp);
	VN_UNLOCKNODE(STOV(sp));
	return (STOV(sp));
}

/*
 * Return a shadow special vnode (snode) for the this supplied VBLK, VCHR, 
 * or VFIFO vnode.
 *
 * If no snode exists for this vnode create one and attach it to the supplied
 * vnode.  If the snode for this vnode is already attached to it, return the
 * snode (ref count is incremented).  The snode will be unattached from the
 * vnode when spec_inactive/fifo_inactive is called.
 *
 * vp (vnode) is supplied locked by caller and caller holds a reference.
 * vp (vnode) is returned unlocked and with the caller's reference released.
 *	(snode will always have a reference to the shadow vnode)
 * vpp (vnode/snode) is returned locked.
 *	with (an additional) reference held, to be released by the caller.
 *
 * There are races between multiple specvp() invocations which are 
 * avoided by holding the supplied vnode, vp, locked until all specvp()
 * allocation is done.  The only lengthy operation performed while holding
 * vp locked is the actual snode memory allocation which may block.  If this
 * occurs, however, then this is the first VOP_LOOKUP() for this vp and
 * anyone else who wants to use it would need to first do a VOP_LOOKUP()
 * which would come right here anyway.
 *
 * There are races between specvp() and spec_inactive()/fifo_inactive() which
 * are avoided by the p_sema_v_lock() below.
 *
 * The protocol for updating v_snode and s_realvp to avoid races with
 * VOP_INACTIVE() works like this:
 *
 *	sp->s_realvp is only set (by specvp) or cleared (by sunsave) while
 *		sp is locked.
 *	sp->s_realvp is only read (by specvp and others) while sp is locked
 *		or a reference is held.
 *
 *	vp->v_snode  is only set (by specvp) while v_snode and vp are locked.
 *	vp->v_snode  is only cleared (by sunsave) while v_snode and snode_list 
 *		are both locked AND no one is blocked waiting to lock v_snode.
 *		The last condition synchronizes with the p_sema_v_lock used
 *		by specvp.
 *	vp->v_snode  is only read (by specvp) while snode_list and vp are
 *		locked, and v_snode is subsequently locked simultaneous with
 *		unlocking snode_list (via p_sema_v_lock).
 *		Thus racing with sunsave() clearing v_snode is prevented 
 *		because snode_list is locked or we are blocked locking the
 *		snode.  Racing with other specvp()'s is prevented because
 *		vp is locked.
 */
int
specvp(vp, vpp)
	struct vnode *vp;
	struct vnode **vpp;
{
	struct snode *sp;
	int error;
	extern int fifosp();
	struct vattr va;
	spl_t	ipl;

	ASSERT_DEBUG(vp, "specvp: null vnode");

	SPECPRINTF5("specvp: begin vp 0x%x, major %d, minor %d, type %d\n",
			vp, major(vp->v_rdev), minor(vp->v_rdev), vp->v_type);

	ASSERT_DEBUG((vp->v_type==VBLK)||(vp->v_type==VCHR)||(vp->v_type==VFIFO),
			"specvp: not a device special vp");

	/*
	 * See if this vnode already has an snode allocated for it
	 * by a previous specvp().  snode_list lock is held to avoid
	 * a tricky race with {spec,fifo}_inactive().  See comment above.
	 */

	ipl = p_lock(&snode_list, SPLFS);

	if (vp->v_snode) {
		/*
		 * Subsequent specvp() for same vp:  snode already allocated.
		 *
		 * We must unlock vp before locking v_snode.
		 */
		sp = vp->v_snode;
		VN_UNLOCKNODE(vp);	/* unlock vnode, keep reference */

		SPECPRINTF5("specvp: found v_snode 0x%x; locking snode\n", sp);

		(void)p_writer_v_lock(&STOV(sp)->v_nodemutex, PVNOD, &snode_list);

		ASSERT(sp->s_realvp == vp, "specvp: snode/vnode mismatch");
		/*
		 *+ An internal kernel consistency check has failed.
		 *+ This indicates a kernel software bug.
		 */
		VN_HOLD(STOV(sp));
		VN_RELE(vp);		/* snode already owns a reference */
		*vpp = STOV(sp);

		SPECPRINTF1("specvp: vp 0x%x, major %d, minor %d, type %d; REUSE v_snode 0x%x\n",
			vp, major(vp->v_rdev), minor(vp->v_rdev), vp->v_type,
			sp);

		return(0);
	}

	/*
	 * There is no snode for this vnode currently.
	 * Another specvp() cannot add one for this vnode while we hold the 
	 * vnode locked, so it is safe to drop the snode_list lock.
	 */
	v_lock(&snode_list, ipl);

	/*
	 * Need to make a new snode.  Allocate space for one and initialize it.
	 */

	SPECPRINTF5("specvp: no v_snode; making new snode\n");

	error = (vp->v_type == VFIFO) ? fifosp(&sp) : specsp(&sp);
	if (error) {
		VN_PUT(vp);

		SPECPRINTF1("specvp: vp 0x%x, major %d, minor %d, type %d; fifosp/specsp ERROR %d\n",
		   vp, major(vp->v_rdev), minor(vp->v_rdev), vp->v_type, error);

		return(error);
	}
	SN_INIT(sp, vp->v_vfsp, vp->v_type, vp->v_rdev);

	/* No need to call SN_VATTR_INIT() since we have an s_realvp */

	/*
	 * Link the real vnode and the snode together.
	 *
	 * At this point, the caller's reference to the vnode is
	 * "converted" into the snode's reference to it's shadow (s_realvp).
	 */

	sp->s_realvp = vp;	/* so specfs VOP's can find the real vnode */

	vp->v_snode = sp;	/* so specvp() can find the snode already  */
				/* 	allocated to a vnode		   */

	/*
	 * init the times in the snode to those in the vnode
	 *	VOP_GETATTR expects to be called with a locked vnode
	 */
	SPECPRINTF5("specvp: calling VOP_GETATTR\n");

	if (!VOP_GETATTR(vp, &va, u.u_cred)) {
		sp->s_atime = va.va_atime;
		sp->s_mtime = va.va_mtime;
		sp->s_ctime = va.va_ctime;
	}

	if (vp->v_type == VBLK) {
		/*
		 * Find/allocate a "common" vnode shared by all 
		 * snodes referencing this block device.
		 *
		 * We call bdevvp() holding both vp and sp locked, but this 
		 * will not result in deadlock because bdevvp() only searches
		 * for (and locks) snodes created by bdevvp().
		 * Must have snode_list unlocked to call bdevvp().
		 */
		sp->s_bdevvp = bdevvp(vp->v_rdev);
	}
	VN_UNLOCKNODE(vp);

	SPECPRINTF1("specvp: vp 0x%x, major %d, minor %d, type %d; NEW snode 0x%x\n",
		vp, major(vp->v_rdev), minor(vp->v_rdev), vp->v_type, sp);

	*vpp = STOV(sp);
	return(0);
}


/*
 * sunsave()
 *	Remove a snode from the table.
 *
 * Called from specfs VOP_INACTIVE() routines: spec_inactive, fifo_inactive.
 *
 * Called with the snode locked and snode_list unlocked.
 *
 * Returns 1 if successful and snode may be deallocated.
 *	The snode is still locked and snode_list is unlocked.
 * Returns 0 if unsuccessful (lost a race with bdevvp() or specvp() trying
 *	to allocate this snode); caller must NOT deallocate the snode.
 *	The snode is returned unlocked and snode_list is unlocked.
 *
 * Follows a lock protocol with specvp(), which avoids deadlocks between 
 * inactive and lookup calls.  See the comment in specvp().
 *
 */
sunsave(sp)
	struct snode *sp;
{
	struct vnode *vp, *bvp;
	spl_t	ipl;

	SPECPRINTF5("sunsave: begin snode 0x%x, type %d\n",
			sp, STOV(sp)->v_type);

	ipl = p_lock(&snode_list, SPLFS);
	/*
	 * If this is true, it means a lookup raced with
	 * an inactive call. The lookup is allowed to
	 * win here.
	 */
	if (VN_WAITERS(STOV(sp))) {
		VN_UNLOCKNODE(STOV(sp));
		v_lock(&snode_list, ipl);

		SPECPRINTF3("sunsave: ABORT - snode 0x%x (type %d) is wanted\n",
				sp, STOV(sp)->v_type);
		return(0);
	}

	if ((sp->s_realvp == NULL) && (STOV(sp)->v_type == VBLK))
		bdevrem(sp);
	/* must zap s_realvp and v_snode before unlocking snode_list */
	vp = sp->s_realvp;
	bvp = sp->s_bdevvp;
	sp->s_realvp = NULL;
	if (vp)
		vp->v_snode = NULL;
	sp->s_bdevvp = NULL;
	v_lock(&snode_list, ipl);
	/*
	 * must do VN_RELE's at SPL0, with no locks
	 * held.  ifree() might need to do a disk I/O
	 */
	if (vp) {
		ASSERT(!bvp || (vp->v_type == VBLK),
			"sunsave: s_bdevvp with non VBLK");
		/*
		 *+ An internal consistency check in sunsave()
		 *+ detected a problem.  This indicates a
		 *+ kernel software bug.
		 */
		SPECPRINTF5("sunsave: VN_RELE s_realvp 0x%x\n", vp);

		VN_RELE(vp);
		if (bvp) {
			SPECPRINTF5("sunsave: VN_RELE s_bdevvp 0x%x\n", bvp);

			VN_RELE(bvp);
		}
	}

	SPECPRINTF3("sunsave: snode 0x%x, type %d, s_realvp 0x%x, s_bdevvp 0x%x\n",
			sp, STOV(sp)->v_type, vp, bvp);
	return(1);
}

#define SPEC_GATE	63

/*
 * Do boot time initialization of specfs data structures.
 */

spec_init()
{
	init_lock(&snode_list);
	init_lock(&spec_nodeid_mutex);
	spec_lastnodeid = 0;

	SPECPRINTF5("spec_init: finished initialization\n");
}

/*
 * Engage write-protect on block device.
 *
 * protect == 1  =>  Turn on write protect on device.
 * protect == 0  =>  Turn off write protect of device.
 */

spec_writeprotect(vp, protect)
	struct vnode *vp;
	int protect;
{
	ASSERT(vp->v_type == VBLK, "spec_writeprotect: vnode not VBLK");
	/*
	 *+ The spec_writeprotect routine was called with a devvp that
	 *+ does not represent a block special device.
	 */

	switch (protect) {
	case 1:
		(void) DEV_IOCTL(VTOS(vp)->s_devp, V_WRITEPROTECT, 0, 0);
		break;
	case 0:
		(void) DEV_IOCTL(VTOS(vp)->s_devp, V_WRITEENABLE, 0, 0);
		break;
	default:
		cmn_err(CE_PANIC, "spec_writeprotect: bad protect argument");
		/*
		 *+ The spec_writeprotect routine was called with a bad
		 *+ argument.  This indicates a
		 *+ probable coding error in the kernel.
		 */
	}
}
