/*-------------------------------------------------------------------------
*
* posix_shmem.c
*	  Implement shared memory using POSIX facilities
*
* These routines represent a fairly thin layer on top of POSIX shared
* memory functionality. SysV facilities are used to make a very small
* shared memory segment for that is used for the mutual exclusion-like
* protection of the POSIX shared memory segment.
*
* Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"

#include <signal.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>
#ifdef HAVE_SYS_IPC_H
#include <sys/ipc.h>
#endif
#ifdef HAVE_SYS_SHM_H
#include <sys/shm.h>
#endif
#ifdef HAVE_KERNEL_OS_H
#include <kernel/OS.h>
#endif

#include "miscadmin.h"
#include "storage/ipc.h"
#include "storage/pg_shmem.h"


typedef key_t IpcMemoryKey;		/* shared memory key passed to shmget(2) */
typedef int IpcMemoryId;		/* shared memory ID returned by shmget(2) */

#define IPCProtection	(0600)	/* access/modify by user only */

#ifdef SHM_SHARE_MMU			/* use intimate shared memory on Solaris */
#define PG_SHMAT_FLAGS			SHM_SHARE_MMU
#else
#define PG_SHMAT_FLAGS			0
#endif

#define IPCProtection	(0600)	/* access/modify by user only */
#define IPCNameLength		32	/* see GeneratePosixIPCName() */

unsigned long UsedShmemSegID = 0;
void	   *UsedSysVShmemSegAddr = NULL;
void	   *UsedPosixShmemSegAddr = NULL;

static void GeneratePosixIPCName(int memKey, char *dest);
static void *InternalIpcMemoryCreate(IpcMemoryKey memKey, Size size, void **sysVShmemSegAddr);
static void SysVIpcMemoryDetach(int status, Datum shmaddr);
static void SysVIpcMemoryDelete(int status, Datum shmId);
static void PosixIpcMemoryDetach(int status, Datum shmaddr);
static void PosixIpcMemoryDelete(int status, Datum memKey);
static PGShmemHeader *PGSharedMemoryAttach(IpcMemoryKey key, IpcMemoryId *shmid,
										  void **sysVShmemSegAddr);

/*
 *	GeneratePosixIPCName(memKey, dest)
 *
 * Generate a shared memory object key name using the argument key for use
 * with the POSIX shared memory routines.
 * This uses the magic number and text to prevent collisions from other
 * processes. 
 */
static void
GeneratePosixIPCName(int memKey, char *dest)
{
	/* This must be 31 characters or less for portability (i.e. Mac OS X) */
	snprintf(dest, IPCNameLength, "PostgreSQL.%x.%x", memKey, PGShmemMagic);
}

/*
 *	InternalIpcMemoryCreate(memKey, size)
 *
 * Attempt to create a new shared memory segment with the specified key.
 * Will fail (return NULL) if such a segment already exists.  If successful,
 * attach the segment to the current process and return its attached address.
 * On success, callbacks are registered with on_shmem_exit to detach and
 * delete the segment when on_shmem_exit is called.
 *
 * If we fail with a failure code other than collision-with-existing-segment,
 * print out an error and abort.  Other types of errors are not recoverable.
 */
static void *
InternalIpcMemoryCreate(IpcMemoryKey memKey, Size size, void **sysVShmemSegAddr)
{
	IpcMemoryId		shmid;
	void		   *memAddress;
	char			keyName[IPCNameLength];
	int				fd;
	struct stat		statbuf;

	/* Open a one byte SysV memory segment to use like a mutex */
	shmid = shmget(memKey, 1, IPC_CREAT | IPC_EXCL | IPCProtection);

	if (shmid < 0)
	{
		/*
		 * Fail quietly if error indicates a collision with existing segment.
		 * One would expect EEXIST, given that we said IPC_EXCL, but perhaps
		 * we could get a permission violation instead?  Also, EIDRM might
		 * occur if an old seg is slated for destruction but not gone yet.
		 */
		if (errno == EEXIST || errno == EACCES
#ifdef EIDRM
			|| errno == EIDRM
#endif
			)
			return NULL;

		/*
		 * Else complain and abort
		 */
		ereport(FATAL,
				(errmsg("could not create shared memory segment: %m"),
		  errdetail("Failed system call was shmget(key=%lu, size=%lu, 0%o).",
					(unsigned long) memKey, (unsigned long) size,
					IPC_CREAT | IPC_EXCL | IPCProtection),
				 (errno == EINVAL) ?
				 errhint("This error usually means that PostgreSQL's request for a shared memory "
		  "segment exceeded your kernel's SHMMAX parameter.  You can either "
						 "reduce the request size or reconfigure the kernel with larger SHMMAX.  "
				  "To reduce the request size (currently %lu bytes), reduce "
			   "PostgreSQL's shared_buffers parameter (currently %d) and/or "
						 "its max_connections parameter (currently %d).\n"
						 "If the request size is already small, it's possible that it is less than "
						 "your kernel's SHMMIN parameter, in which case raising the request size or "
						 "reconfiguring SHMMIN is called for.\n"
		"The PostgreSQL documentation contains more information about shared "
						 "memory configuration.",
						 (unsigned long) size, NBuffers, MaxBackends) : 0,
				 (errno == ENOMEM) ?
				 errhint("This error usually means that PostgreSQL's request for a shared "
				   "memory segment exceeded available memory or swap space. "
				  "To reduce the request size (currently %lu bytes), reduce "
			   "PostgreSQL's shared_buffers parameter (currently %d) and/or "
						 "its max_connections parameter (currently %d).\n"
		"The PostgreSQL documentation contains more information about shared "
						 "memory configuration.",
						 (unsigned long) size, NBuffers, MaxBackends) : 0,
				 (errno == ENOSPC) ?
				 errhint("This error does *not* mean that you have run out of disk space. "
						 "It occurs either if all available shared memory IDs have been taken, "
						 "in which case you need to raise the SHMMNI parameter in your kernel, "
		  "or because the system's overall limit for shared memory has been "
				 "reached.  If you cannot increase the shared memory limit, "
		  "reduce PostgreSQL's shared memory request (currently %lu bytes), "
			"by reducing its shared_buffers parameter (currently %d) and/or "
						 "its max_connections parameter (currently %d).\n"
		"The PostgreSQL documentation contains more information about shared "
						 "memory configuration.",
						 (unsigned long) size, NBuffers, MaxBackends) : 0));
	}

	/* Register on-exit routine to delete the new segment */
	on_shmem_exit(SysVIpcMemoryDelete, Int32GetDatum(shmid));

	/* OK, should be able to attach to the segment */
	*sysVShmemSegAddr = shmat(shmid, NULL, PG_SHMAT_FLAGS);

	if (*sysVShmemSegAddr == (void *) -1)
		elog(FATAL, "shmat(id=%d) failed: %m", shmid);

	/* Register on-exit routine to detach new segment before deleting */
	on_shmem_exit(SysVIpcMemoryDetach, PointerGetDatum(*sysVShmemSegAddr));

	/* Record key and ID in lockfile for data directory. */
	RecordSharedMemoryInLockFile((unsigned long) memKey,
								 (unsigned long) shmid);
	
	/* Create a POSIX shared memory segment for the actual data */
	GeneratePosixIPCName(memKey, keyName);
	fd = shm_open(keyName, O_RDWR | O_CREAT | O_EXCL, IPCProtection);
	
	if (fd < 0)
	{
		/*
		 * Fail quietly if error indicates a collision with existing segment.
		 * One would expect EEXIST, given that we said O_EXCL.
		 */
		if (errno == EEXIST || errno == EACCES)
			return NULL;
		
		/*
		 * Else complain and abort
		 */
		ereport(FATAL,
				(errmsg("could not create shared memory segment: %m"),
				 errdetail("Failed system call was shm_open(name=%s, oflag=%lu, mode=%lu).",
						   keyName, (unsigned long) O_CREAT | O_EXCL,
						   (unsigned long) IPCProtection),
				 (errno == EMFILE) ?
				 errhint("This error means that the process has reached its limit "
						 "for open file descriptors.") : 0,
				 (errno == ENOSPC) ?
				 errhint("This error means the process has ran out of address "
						 "space.") : 0));
	}
	
	/* Register on-exit routine to delete the new segment */
	on_shmem_exit(PosixIpcMemoryDelete, Int32GetDatum(memKey));
	
	/* Increase the size of the file descriptor to the desired length */
	if (fstat(fd, &statbuf) < 0)
		elog(FATAL, "fstat(fd=%d) failed: %m", fd);
	
	if (statbuf.st_size < size)
		ftruncate(fd, size);
	
	/* OK, should be able to attach to the segment */
	memAddress = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);
	
	if (memAddress == (void *) -1)
		elog(FATAL, "mmap(fd=%d) failed: %m", fd);
	
	/* Register on-exit routine to detach new segment before deleting */
	on_shmem_exit(PosixIpcMemoryDetach, PointerGetDatum(memAddress));

	return memAddress;
}

/****************************************************************************/
/*	SysVIpcMemoryDetach(status, shmaddr)	removes a shared memory segment		*/
/*										from process' address spaceq		*/
/*	(called as an on_shmem_exit callback, hence funny argument list)		*/
/****************************************************************************/
static void
SysVIpcMemoryDetach(int status, Datum shmaddr)
{
	if (shmdt(DatumGetPointer(shmaddr)) < 0)
		elog(LOG, "shmdt(%p) failed: %m", DatumGetPointer(shmaddr));
}

/****************************************************************************/
/*	SysVMemoryDelete(status, shmId)		deletes a shared memory segment		*/
/*	(called as an on_shmem_exit callback, hence funny argument list)		*/
/****************************************************************************/
static void
SysVIpcMemoryDelete(int status, Datum shmId)
{
	if (shmctl(DatumGetInt32(shmId), IPC_RMID, NULL) < 0)
		elog(LOG, "shmctl(%d, %d, 0) failed: %m",
			 DatumGetInt32(shmId), IPC_RMID);
}

/****************************************************************************/
/*	PosixIpcMemoryDetach(status, shmaddr)	removes a shared memory segment		*/
/*										from process' address spaceq		*/
/*	(called as an on_shmem_exit callback, hence funny argument list)		*/
/****************************************************************************/
static void
PosixIpcMemoryDetach(int status, Datum shmaddr)
{
	PGShmemHeader  *hdr;
	hdr = (PGShmemHeader *) DatumGetPointer(shmaddr);
	if (munmap(DatumGetPointer(shmaddr), hdr->totalsize) < 0)
		elog(LOG, "munmap(%p, ...) failed: %m", DatumGetPointer(shmaddr));
}

/****************************************************************************/
/*	PosixMemoryDelete(status, memKey)		deletes a shared memory segment		*/
/*	(called as an on_shmem_exit callback, hence funny argument list)		*/
/****************************************************************************/
static void
PosixIpcMemoryDelete(int status, Datum memKey)
{
	char		keyName[IPCNameLength];
	GeneratePosixIPCName(memKey, keyName);
	if (shm_unlink(keyName) < 0)
		elog(LOG, "shm_unlink(%s) failed: %m", keyName);
}

/*
 * PGSharedMemoryIsInUse
 *
 * Is a previously-existing shmem segment still existing and in use?
 *
 * The point of this exercise is to detect the case where a prior postmaster
 * crashed, but it left child backends that are still running.	Therefore
 * we only care about shmem segments that are associated with the intended
 * DataDir.  This is an important consideration since accidental matches of
 * shmem segment IDs are reasonably common.
 */
bool
PGSharedMemoryIsInUse(unsigned long id1, unsigned long id2)
{
	IpcMemoryId shmId = (IpcMemoryId) id2;
	struct shmid_ds shmStat;
	char		keyName[IPCNameLength];

#ifndef WIN32
	struct stat statbuf;
	PGShmemHeader *hdr;
	int			fd, isValidHeader;
#endif

	/*
	 * We detect whether a shared memory segment is in use by seeing whether
	 * it (a) exists and (b) has any processes are attached to it. First,
	 * check the SysV segment so we can tell if any other processes are
	 * attached.
	 */
	if (shmctl(shmId, IPC_STAT, &shmStat) < 0)
	{
		/*
		 * EINVAL actually has multiple possible causes documented in the
		 * shmctl man page, but we assume it must mean the segment no longer
		 * exists.
		 */
		if (errno == EINVAL)
			return false;

		/*
		 * EACCES implies that the segment belongs to some other userid, which
		 * means it is not a Postgres shmem segment (or at least, not one that
		 * is relevant to our data directory).
		 */
		if (errno == EACCES)
			return false;

		/*
		 * Otherwise, we had better assume that the segment is in use. The
		 * only likely case is EIDRM, which implies that the segment has been
		 * IPC_RMID'd but there are still processes attached to it.
		 */
		return true;
	}

	/* If it has no attached processes, it's not in use */
	if (shmStat.shm_nattch == 0)
		return false;

	/*
	 * Try to attach to the POSIX segment and see if it matches our data
	 * directory. This avoids shmid-conflict problems on machines that are
	 * running several postmasters under the same userid.  On Windows, which
	 * doesn't have useful inode numbers, we can't do this so we punt and
	 * assume there is a conflict.
	 */
#ifdef WIN32
	return true;
#else
	fd = shm_open(keyName, O_RDWR, 0);
	if (fd < 0)
		return true;			/* if can't attach, be conservative */
	
	if (stat(DataDir, &statbuf) < 0)
	{
		close(fd);
		return true;			/* if can't stat, be conservative */
	}

	hdr = (PGShmemHeader *) mmap(NULL, sizeof(PGShmemHeader),
								 PROT_READ, MAP_SHARED, fd, 0);
	close(fd);

	if (hdr == (PGShmemHeader *) -1)
		return true;			/* if can't attach, be conservative */
	
	/*
	 * If false, it's either not a Postgres segment, or not one for my data
	 * directory.  In either case it poses no threat.
	 * If true, trouble -- looks a lot like there's still live backends.
	 */
	isValidHeader = hdr->magic == PGShmemMagic
					&& hdr->device == statbuf.st_dev
					&& hdr->inode == statbuf.st_ino;
	
	munmap((void *) hdr, sizeof(PGShmemHeader));
	
	return isValidHeader;
#endif
}


/*
 * PGSharedMemoryCreate
 *
 * Create a shared memory segment of the given size and initialize its
 * standard header.  Also, register an on_shmem_exit callback to release
 * the storage.
 *
 * Dead Postgres segments are recycled if found, but we do not fail upon
 * collision with non-Postgres shmem segments.	The idea here is to detect and
 * re-use keys that may have been assigned by a crashed postmaster or backend.
 *
 * makePrivate means to always create a new segment, rather than attach to
 * or recycle any existing segment.
 *
 * The port number is passed for possible use as a key (for SysV, we use
 * it to generate the starting shmem key).	In a standalone backend,
 * zero will be passed.
 */
PGShmemHeader *
PGSharedMemoryCreate(Size size, bool makePrivate, int port)
{
	IpcMemoryKey	NextShmemSegID;
	void			*posixShmemSegAddr, *sysVShmemSegAddr;
	PGShmemHeader	*hdr;
	IpcMemoryId		shmid;
	char			keyName[IPCNameLength];

#ifndef WIN32
	struct stat		statbuf;
#endif

	/* Room for a header? */
	Assert(size > MAXALIGN(sizeof(PGShmemHeader)));

	/* Make sure PGSharedMemoryAttach doesn't fail without need */
	UsedSysVShmemSegAddr = NULL;
	UsedPosixShmemSegAddr = NULL;

	/* Loop till we find a free IPC key */
	NextShmemSegID = port * 1000;

	for (NextShmemSegID++;; NextShmemSegID++)
	{
		/* Try to create new segment */
		posixShmemSegAddr = InternalIpcMemoryCreate(NextShmemSegID,
											size, &sysVShmemSegAddr);
		if (posixShmemSegAddr)
			break;				/* successful create and attach */

		/* Check shared memory and possibly remove and recreate */

		if (makePrivate)		/* a standalone backend shouldn't do this */
			continue;

		if ((posixShmemSegAddr = PGSharedMemoryAttach(NextShmemSegID,
										&shmid,	&sysVShmemSegAddr)) == NULL)
			continue;			/* can't attach, not one of mine */

		/*
		 * If I am not the creator and it belongs to an extant process,
		 * continue.
		 */
		hdr = (PGShmemHeader *) posixShmemSegAddr;
		if (hdr->creatorPID != getpid())
		{
			if (kill(hdr->creatorPID, 0) == 0 || errno != ESRCH)
			{
				munmap(posixShmemSegAddr, hdr->totalsize);
				shmdt(sysVShmemSegAddr);
				continue;		/* segment belongs to a live process */
			}
		}

		/*
		 * The segment appears to be from a dead Postgres process, or from a
		 * previous cycle of life in this same process.  Zap both the SysV
		 * and POSIX segments, if possible.
		 * This probably shouldn't fail, but if it does, assume the segment
		 * belongs to someone else after all, and continue quietly.
		 * The POSIX segment should be zapped first to avoid deadlock.
		 */
		GeneratePosixIPCName(NextShmemSegID, keyName);
		munmap(posixShmemSegAddr, hdr->totalsize);
		if (shm_unlink(keyName) < 0)
			continue;
		
		shmdt(sysVShmemSegAddr);
		if (shmctl(shmid, IPC_RMID, NULL) < 0)
			continue;

		/*
		 * Now try again to create the segment.
		 */
		posixShmemSegAddr = InternalIpcMemoryCreate(NextShmemSegID, size, &sysVShmemSegAddr);
		if (posixShmemSegAddr)
			break;				/* successful create and attach */

		/*
		 * Can only get here if some other process managed to create the same
		 * shmem key before we did.  Let him have that one, loop around to try
		 * next key.
		 */
	}

	/*
	 * OK, we created a new segment.  Mark it as created by this process. The
	 * order of assignments here is critical so that another Postgres process
	 * can't see the header as valid but belonging to an invalid PID!
	 */
	hdr = (PGShmemHeader *) posixShmemSegAddr;
	hdr->creatorPID = getpid();
	hdr->magic = PGShmemMagic;

#ifndef WIN32
	/* Fill in the data directory ID info, too */
	if (stat(DataDir, &statbuf) < 0)
		ereport(FATAL,
				(errcode_for_file_access(),
				 errmsg("could not stat data directory \"%s\": %m",
						DataDir)));
	hdr->device = statbuf.st_dev;
	hdr->inode = statbuf.st_ino;
#endif

	/*
	 * Initialize space allocation status for segment.
	 */
	hdr->totalsize = size;
	hdr->freeoffset = MAXALIGN(sizeof(PGShmemHeader));

	/* Save info for possible future use */
	UsedPosixShmemSegAddr = posixShmemSegAddr;
	UsedSysVShmemSegAddr = sysVShmemSegAddr;
	UsedShmemSegID = (unsigned long) NextShmemSegID;

	return hdr;
}

#ifdef EXEC_BACKEND

/*
 * PGSharedMemoryReAttach
 *
 * Re-attach to an already existing shared memory segment.	In the non
 * EXEC_BACKEND case this is not used, because postmaster children inherit
 * the shared memory segment attachment via fork().
 *
 * UsedShmemSegID, UsedShmemSegAddr and UsedSysVShmemSegAddr are
 * implicit parameters to this routine.  The caller must have already
 * restored them to the postmaster's values.
 */
void
PGSharedMemoryReAttach(void)
{
	IpcMemoryId shmid;
	void	   *hdr;
	void	   *origUsedSysVShmemSegAddr, origUsedPosixShmemSegAddr;
	origUsedSysVShmemSegAddr = UsedSysVShmemSegAddr;
	origUsedPosixShmemSegAddr = UsedPosixShmemSegAddr;

	Assert(UsedSysVShmemSegAddr != NULL
		   && UsedPosixShmemSegAddr != NULL);
	Assert(IsUnderPostmaster);

#ifdef __CYGWIN__
	/* cygipc (currently) appears to not detach on exec. */
	PGSharedMemoryDetach();
	UsedSysVShmemSegAddr = origUsedSysVShmemSegAddr;
	UsedPosixShmemSegAddr = origUsedPosixShmemSegAddr;
#endif

	elog(DEBUG3, "attaching to %p", UsedSysVShmemSegAddr);
	hdr = (void *) PGSharedMemoryAttach((IpcMemoryKey) UsedShmemSegID,
										&shmid, &UsedSysVShmemSegAddr);
	if (hdr == NULL)
		elog(FATAL, "could not reattach to shared memory "
			 "(key=%d, posixAddr=%p, sysVAddr=%p): %m",
			 (int) UsedShmemSegID, UsedPosixShmemSegAddr, UsedSysVShmemSegAddr);
	if (hdr != origUsedPosixShmemSegAddr)
		elog(FATAL, "reattaching to shared memory returned unexpected address "
			 "(got %p, expected %p)", hdr, origUsedPosixShmemSegAddr);

	UsedPosixShmemSegAddr = hdr;		/* probably redundant */
	
}
#endif   /* EXEC_BACKEND */

/*
 * PGSharedMemoryDetach
 *
 * Detach from the shared memory segment, if still attached.  This is not
 * intended for use by the process that originally created the segment
 * (it will have an on_shmem_exit callback registered to do that).	Rather,
 * this is for subprocesses that have inherited an attachment and want to
 * get rid of it.
 */
void
PGSharedMemoryDetach(void)
{
	PGShmemHeader  *hdr;
	if (UsedPosixShmemSegAddr != NULL)
	{
		hdr = (PGShmemHeader *) UsedPosixShmemSegAddr;
		if (munmap(UsedPosixShmemSegAddr, hdr->totalsize) < 0)
			elog(LOG, "munmap(%p) failed: %m", UsedPosixShmemSegAddr);
			
		if ((shmdt(UsedSysVShmemSegAddr) < 0)
#if defined(EXEC_BACKEND) && defined(__CYGWIN__)
		/* Work-around for cygipc exec bug */
			&& shmdt(NULL) < 0
#endif
			)
			elog(LOG, "shmdt(%p) failed: %m", UsedSysVShmemSegAddr);
		UsedSysVShmemSegAddr = NULL;
		UsedPosixShmemSegAddr = NULL;
	}
}


/*
 * Attach to shared memory and make sure it has a Postgres header
 * Populates shmid and sysVShmemSegAddr with their new values.
 * Returns attach address if OK, else NULL
 */
static PGShmemHeader *
PGSharedMemoryAttach(IpcMemoryKey key, IpcMemoryId *shmid, void **sysVShmemSegAddr)
{
	PGShmemHeader *hdr;
	char		keyName[IPCNameLength];
	Size		size;
	int			fd;
	
	/* First, open the SysV shared memory 'mutex' */
	if ((*shmid = shmget(key, sizeof(PGShmemHeader), 0)) < 0)
		return NULL;

	*sysVShmemSegAddr = shmat(*shmid, UsedSysVShmemSegAddr, PG_SHMAT_FLAGS);
	
	/* Then open the POSIX shared memory */
	GeneratePosixIPCName(key, keyName);
	if ((fd = shm_open(keyName, O_RDWR, 0)) < 0)
		return NULL;
	
	hdr = (PGShmemHeader *) mmap(UsedPosixShmemSegAddr, sizeof(PGShmemHeader),
								 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	
	if (hdr == (PGShmemHeader *) -1)
	{
		close(fd);
		return NULL;			/* failed: must be some other app's */
	}
	
	if (hdr->magic != PGShmemMagic)
	{
		close(fd);
		munmap((void *) hdr, sizeof(PGShmemHeader));
		shmdt((void *) sysVShmemSegAddr);
		return NULL;			/* segment belongs to a non-Postgres app */
	}
	
	/* Since the segment has a valid Postgres header, unmap and re-map it
	 * with the proper size */
	size = hdr->totalsize;
	munmap((void *) hdr, sizeof(PGShmemHeader));
	hdr = (PGShmemHeader *) mmap(UsedPosixShmemSegAddr, size,
								 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);	

	return hdr;
}
