529 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			529 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
#define ALLOCATE
 | 
						|
#include <errno.h>
 | 
						|
#include <minix/mthread.h>
 | 
						|
#include <string.h>
 | 
						|
#include <machine/param.h>
 | 
						|
#include <sys/mman.h>
 | 
						|
#include "global.h"
 | 
						|
#include "proto.h"
 | 
						|
 | 
						|
static int mthread_increase_thread_pool(void);
 | 
						|
static void mthread_thread_init(mthread_thread_t thread, mthread_attr_t
 | 
						|
	*tattr, void *(*proc)(void *), void *arg);
 | 
						|
 | 
						|
static void mthread_thread_stop(mthread_thread_t thread);
 | 
						|
static void mthread_trampoline(void);
 | 
						|
 | 
						|
static int initialized = 0;
 | 
						|
#ifndef PGSHIFT
 | 
						|
# define PGSHIFT	12	/* XXX: temporarily for ACK */
 | 
						|
#endif
 | 
						|
#define MTHREAD_GUARDSIZE 	(1 << PGSHIFT) 	/* 1 page */
 | 
						|
 | 
						|
static struct __mthread_attr default_attr = {	MTHREAD_STACK_MIN,
 | 
						|
						NULL,
 | 
						|
						MTHREAD_CREATE_JOINABLE,
 | 
						|
						NULL, NULL };
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_equal				     *
 | 
						|
 *===========================================================================*/
 | 
						|
int mthread_equal(l, r)
 | 
						|
mthread_thread_t l;
 | 
						|
mthread_thread_t r;
 | 
						|
{
 | 
						|
/* Compare two thread ids */
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure mthreads is initialized */
 | 
						|
 | 
						|
  return(l == r);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_create				     *
 | 
						|
 *===========================================================================*/
 | 
						|
int mthread_create(threadid, tattr, proc, arg)
 | 
						|
mthread_thread_t *threadid;
 | 
						|
mthread_attr_t *tattr;
 | 
						|
void *(*proc)(void *);
 | 
						|
void *arg;
 | 
						|
{
 | 
						|
/* Register procedure proc for execution in a thread. */
 | 
						|
  mthread_thread_t thread;
 | 
						|
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure mthreads is initialized */
 | 
						|
 | 
						|
  if (proc == NULL)
 | 
						|
	return(EINVAL);
 | 
						|
 | 
						|
  if (!mthread_queue_isempty(&free_threads)) {
 | 
						|
  	thread = mthread_queue_remove(&free_threads);
 | 
						|
  	mthread_thread_init(thread, tattr, proc, arg);
 | 
						|
 	used_threads++;
 | 
						|
 	if(threadid != NULL) 
 | 
						|
 		*threadid = (mthread_thread_t) thread;
 | 
						|
#ifdef MDEBUG
 | 
						|
 	printf("Inited thread %d\n", thread);
 | 
						|
#endif
 | 
						|
 	return(0);
 | 
						|
  } else  {
 | 
						|
  	if (mthread_increase_thread_pool() == -1) 
 | 
						|
  		return(EAGAIN);
 | 
						|
 | 
						|
  	return mthread_create(threadid, tattr, proc, arg);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_detach				     *
 | 
						|
 *===========================================================================*/
 | 
						|
int mthread_detach(detach)
 | 
						|
mthread_thread_t detach;
 | 
						|
{
 | 
						|
/* Mark a thread as detached. Consequently, upon exit, resources allocated for
 | 
						|
 * this thread are automatically freed.
 | 
						|
 */
 | 
						|
  mthread_tcb_t *tcb;
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure libmthread is initialized */
 | 
						|
 | 
						|
  if (!isokthreadid(detach)) 
 | 
						|
  	return(ESRCH);
 | 
						|
 | 
						|
  tcb = mthread_find_tcb(detach);
 | 
						|
  if (tcb->m_state == MS_DEAD) {
 | 
						|
  	return(ESRCH);
 | 
						|
  } else if (tcb->m_attr.ma_detachstate != MTHREAD_CREATE_DETACHED) {
 | 
						|
  	if (tcb->m_state == MS_EXITING) 
 | 
						|
  		mthread_thread_stop(detach);
 | 
						|
  	else 
 | 
						|
		tcb->m_attr.ma_detachstate = MTHREAD_CREATE_DETACHED;
 | 
						|
  }
 | 
						|
 | 
						|
  return(0);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_exit				     *
 | 
						|
 *===========================================================================*/
 | 
						|
void mthread_exit(value)
 | 
						|
void *value;
 | 
						|
{
 | 
						|
/* Make a thread stop running and store the result value. */
 | 
						|
  mthread_tcb_t *tcb;
 | 
						|
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure libmthread is initialized */
 | 
						|
 | 
						|
  tcb = mthread_find_tcb(current_thread);
 | 
						|
 | 
						|
  if (tcb->m_state == MS_EXITING)	/* Already stopping, nothing to do. */
 | 
						|
  	return;
 | 
						|
 | 
						|
  mthread_cleanup_values();
 | 
						|
 | 
						|
  tcb->m_result = value;
 | 
						|
  tcb->m_state = MS_EXITING;
 | 
						|
 | 
						|
  if (tcb->m_attr.ma_detachstate == MTHREAD_CREATE_DETACHED) {
 | 
						|
	mthread_thread_stop(current_thread);
 | 
						|
  } else {
 | 
						|
  	/* Joinable thread; notify possibly waiting thread */
 | 
						|
	if (mthread_cond_signal(&(tcb->m_exited)) != 0) 
 | 
						|
		mthread_panic("Couldn't signal exit");
 | 
						|
 | 
						|
	/* The thread that's actually doing the join will eventually clean
 | 
						|
	 * up this thread (i.e., call mthread_thread_stop).
 | 
						|
	 */
 | 
						|
  }
 | 
						|
 | 
						|
  mthread_schedule();
 | 
						|
}
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *			mthread_find_tcb				     *
 | 
						|
 *===========================================================================*/
 | 
						|
mthread_tcb_t * mthread_find_tcb(thread)
 | 
						|
mthread_thread_t thread;
 | 
						|
{
 | 
						|
  mthread_tcb_t *rt = NULL;
 | 
						|
 | 
						|
  if (!isokthreadid(thread)) mthread_panic("Invalid thread id");
 | 
						|
 | 
						|
  if (thread == MAIN_THREAD)
 | 
						|
  	rt = &mainthread;
 | 
						|
  else
 | 
						|
  	rt = threads[thread];
 | 
						|
 | 
						|
  return(rt);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *			mthread_increase_thread_pool			     *
 | 
						|
 *===========================================================================*/
 | 
						|
static int mthread_increase_thread_pool(void)
 | 
						|
{
 | 
						|
/* Increase thread pool. No fancy algorithms, just double the size. */
 | 
						|
  mthread_tcb_t **new_tcb;
 | 
						|
  int new_no_threads, old_no_threads, i;
 | 
						|
 | 
						|
  old_no_threads = no_threads;
 | 
						|
 | 
						|
  if (old_no_threads == 0)
 | 
						|
  	new_no_threads = NO_THREADS;
 | 
						|
  else
 | 
						|
	new_no_threads = 2 * old_no_threads;
 | 
						|
 | 
						|
 | 
						|
  if (new_no_threads >= MAX_THREAD_POOL) {
 | 
						|
  	mthread_debug("Reached max number of threads");
 | 
						|
  	return(-1);
 | 
						|
  }
 | 
						|
 | 
						|
  /* Allocate space to store pointers to thread control blocks */
 | 
						|
  if (old_no_threads == 0)	/* No data yet: allocate space */
 | 
						|
  	new_tcb = calloc(new_no_threads, sizeof(mthread_tcb_t *));
 | 
						|
  else				/* Preserve existing data: reallocate space */
 | 
						|
	new_tcb = realloc(threads, new_no_threads * sizeof(mthread_tcb_t *));
 | 
						|
 | 
						|
  if (new_tcb == NULL) {
 | 
						|
  	mthread_debug("Can't increase thread pool");
 | 
						|
  	return(-1);
 | 
						|
  }
 | 
						|
 | 
						|
  /* Allocate space for thread control blocks itself */
 | 
						|
  for (i = old_no_threads; i < new_no_threads; i++) {
 | 
						|
  	new_tcb[i] = malloc(sizeof(mthread_tcb_t));
 | 
						|
  	if (new_tcb[i] == NULL) {
 | 
						|
  		mthread_debug("Can't allocate space for tcb");
 | 
						|
  		return(-1);
 | 
						|
  	}
 | 
						|
  	memset(new_tcb[i], '\0', sizeof(mthread_tcb_t)); /* Clear entry */
 | 
						|
  }
 | 
						|
 | 
						|
  /* We can breath again, let's tell the others about the good news */
 | 
						|
  threads = new_tcb; 
 | 
						|
  no_threads = new_no_threads;
 | 
						|
 | 
						|
  /* Add newly available threads to free_threads */
 | 
						|
  for (i = old_no_threads; i < new_no_threads; i++) {
 | 
						|
	mthread_queue_add(&free_threads, i);
 | 
						|
	mthread_thread_reset(i);
 | 
						|
  }
 | 
						|
 | 
						|
#ifdef MDEBUG
 | 
						|
  printf("Increased thread pool from %d to %d threads\n", old_no_threads,
 | 
						|
  	 new_no_threads);
 | 
						|
#endif
 | 
						|
  return(0);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_init				     *
 | 
						|
 *===========================================================================*/
 | 
						|
void mthread_init(void)
 | 
						|
{
 | 
						|
/* Initialize thread system; allocate thread structures and start creating
 | 
						|
 * threads.
 | 
						|
 */
 | 
						|
 | 
						|
  if (initialized) return;
 | 
						|
 | 
						|
  no_threads = 0;
 | 
						|
  used_threads = 0;
 | 
						|
  need_reset = 0;
 | 
						|
  running_main_thread = 1;	/* mthread_init can only be called from the
 | 
						|
				 * main thread. Calling it from a thread will
 | 
						|
				 * not enter this clause.
 | 
						|
				 */
 | 
						|
 | 
						|
  if (mthread_getcontext(&(mainthread.m_context)) == -1)
 | 
						|
	mthread_panic("Couldn't save state for main thread");
 | 
						|
  current_thread = MAIN_THREAD;
 | 
						|
 | 
						|
  mthread_init_valid_mutexes();
 | 
						|
  mthread_init_valid_conditions();
 | 
						|
  mthread_init_valid_attributes();
 | 
						|
  mthread_init_keys();
 | 
						|
  mthread_init_scheduler();
 | 
						|
 | 
						|
  initialized = 1;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_join				     *
 | 
						|
 *===========================================================================*/
 | 
						|
int mthread_join(join, value)
 | 
						|
mthread_thread_t join;
 | 
						|
void **value;
 | 
						|
{
 | 
						|
/* Wait for a thread to stop running and copy the result. */
 | 
						|
 | 
						|
  mthread_tcb_t *tcb;
 | 
						|
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure libmthread is initialized */
 | 
						|
 | 
						|
  if (!isokthreadid(join))
 | 
						|
  	return(ESRCH);
 | 
						|
  else if (join == current_thread) 
 | 
						|
	return(EDEADLK);
 | 
						|
 | 
						|
  tcb = mthread_find_tcb(join);
 | 
						|
  if (tcb->m_state == MS_DEAD) 
 | 
						|
  	return(ESRCH);
 | 
						|
  else if (tcb->m_attr.ma_detachstate == MTHREAD_CREATE_DETACHED) 
 | 
						|
	return(EINVAL);
 | 
						|
 | 
						|
  /* When the thread hasn't exited yet, we have to wait for that to happen */
 | 
						|
  if (tcb->m_state != MS_EXITING) {
 | 
						|
  	mthread_cond_t *c;
 | 
						|
  	mthread_mutex_t *m;
 | 
						|
 | 
						|
  	c = &(tcb->m_exited);
 | 
						|
  	m = &(tcb->m_exitm);
 | 
						|
 | 
						|
  	if (mthread_mutex_init(m, NULL) != 0)
 | 
						|
		mthread_panic("Couldn't initialize mutex to join\n");
 | 
						|
 | 
						|
	if (mthread_mutex_lock(m) != 0)
 | 
						|
		mthread_panic("Couldn't lock mutex to join\n");
 | 
						|
 | 
						|
	if (mthread_cond_wait(c, m) != 0) 
 | 
						|
		mthread_panic("Couldn't wait for join condition\n");
 | 
						|
		
 | 
						|
	if (mthread_mutex_unlock(m) != 0)
 | 
						|
		mthread_panic("Couldn't unlock mutex to join\n");
 | 
						|
 | 
						|
	if (mthread_mutex_destroy(m) != 0)
 | 
						|
		mthread_panic("Couldn't destroy mutex to join\n");
 | 
						|
  }
 | 
						|
 | 
						|
  /* Thread has exited; copy results */
 | 
						|
  if(value != NULL)
 | 
						|
	*value = tcb->m_result;
 | 
						|
 | 
						|
  /* Deallocate resources */
 | 
						|
  mthread_thread_stop(join);
 | 
						|
  return(0);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_once				     *
 | 
						|
 *===========================================================================*/
 | 
						|
int mthread_once(once, proc)
 | 
						|
mthread_once_t *once;
 | 
						|
void (*proc)(void);
 | 
						|
{
 | 
						|
/* Run procedure proc just once */
 | 
						|
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure libmthread is initialized */
 | 
						|
 | 
						|
  if (once == NULL || proc == NULL) 
 | 
						|
  	return(EINVAL);
 | 
						|
 | 
						|
  if (*once != 1) proc();
 | 
						|
  *once = 1;
 | 
						|
  return(0);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_self				     *
 | 
						|
 *===========================================================================*/
 | 
						|
mthread_thread_t mthread_self(void)
 | 
						|
{
 | 
						|
/* Return the thread id of the thread calling this function. */
 | 
						|
 | 
						|
  MTHREAD_CHECK_INIT();	/* Make sure libmthread is initialized */
 | 
						|
 | 
						|
  return(current_thread);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_thread_init			     *
 | 
						|
 *===========================================================================*/
 | 
						|
static void mthread_thread_init(thread, tattr, proc, arg)
 | 
						|
mthread_thread_t thread;
 | 
						|
mthread_attr_t *tattr;
 | 
						|
void *(*proc)(void *);
 | 
						|
void *arg;
 | 
						|
{
 | 
						|
/* Initialize a thread so that it, when unsuspended, will run the given
 | 
						|
 * procedure with the given parameter. The thread is marked as runnable.
 | 
						|
 */
 | 
						|
 | 
						|
#define THIS_CTX (&(threads[thread]->m_context))
 | 
						|
  mthread_tcb_t *tcb;
 | 
						|
  size_t stacksize;
 | 
						|
  char *stackaddr;
 | 
						|
 | 
						|
  tcb = mthread_find_tcb(thread);
 | 
						|
  tcb->m_next = NULL;
 | 
						|
  tcb->m_state = MS_DEAD;
 | 
						|
  tcb->m_proc = proc;
 | 
						|
  tcb->m_arg = arg;
 | 
						|
  /* Threads use a copy of the provided attributes. This way, if another
 | 
						|
   * thread modifies the attributes (such as detach state), already running
 | 
						|
   * threads are not affected.
 | 
						|
   */
 | 
						|
  if (tattr != NULL)
 | 
						|
  	tcb->m_attr = *((struct __mthread_attr *) *tattr);
 | 
						|
  else {
 | 
						|
  	tcb->m_attr = default_attr;
 | 
						|
  }
 | 
						|
 | 
						|
  if (mthread_cond_init(&(tcb->m_exited), NULL) != 0)
 | 
						|
  	mthread_panic("Could not initialize thread");
 | 
						|
 | 
						|
  tcb->m_context.uc_link = NULL;
 | 
						|
 | 
						|
  /* Construct this thread's context to run procedure proc. */
 | 
						|
  if (mthread_getcontext(&(tcb->m_context)) == -1)
 | 
						|
  	mthread_panic("Failed to initialize context state");
 | 
						|
 | 
						|
  stacksize = tcb->m_attr.ma_stacksize;
 | 
						|
  stackaddr = tcb->m_attr.ma_stackaddr;
 | 
						|
 | 
						|
  if (stacksize == (size_t) 0) {
 | 
						|
	/* User provided too small a stack size. Forget about that stack and
 | 
						|
	 * allocate a new one ourselves.
 | 
						|
	 */
 | 
						|
	stacksize = (size_t) MTHREAD_STACK_MIN;
 | 
						|
	tcb->m_attr.ma_stackaddr = stackaddr = NULL;
 | 
						|
  }
 | 
						|
 | 
						|
  if (stackaddr == NULL) {
 | 
						|
	/* Allocate stack space */
 | 
						|
	size_t guarded_stacksize;
 | 
						|
	char *guard_start, *guard_end;
 | 
						|
 | 
						|
	stacksize = round_page(stacksize + MTHREAD_GUARDSIZE);
 | 
						|
	stackaddr = minix_mmap(NULL, stacksize,
 | 
						|
			       PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE,
 | 
						|
			       -1, 0);
 | 
						|
	if (stackaddr == MAP_FAILED)
 | 
						|
  		mthread_panic("Failed to allocate stack to thread");
 | 
						|
 | 
						|
#if defined(__i386__) || defined(__arm__)
 | 
						|
	guard_start = stackaddr;
 | 
						|
	guard_end = stackaddr + MTHREAD_GUARDSIZE;
 | 
						|
	guarded_stacksize = stackaddr + stacksize - guard_end;
 | 
						|
 | 
						|
	/* The stack will be used from (stackaddr+stacksize) to stackaddr. That
 | 
						|
	 * is, growing downwards. So the "top" of the stack may not grow into
 | 
						|
	 * stackaddr+MTHREAD_GUARDSIZE.
 | 
						|
	 *
 | 
						|
	 * +-------+ stackaddr + stacksize
 | 
						|
	 * |       |
 | 
						|
	 * |   |   |
 | 
						|
	 * |  \|/  |
 | 
						|
	 * |       |
 | 
						|
	 * +-------+ stackaddr + MTHREAD_GUARDSIZE
 | 
						|
	 * | GUARD |
 | 
						|
	 * +-------+ stackaddr
 | 
						|
	 */
 | 
						|
#else
 | 
						|
# error "Unsupported platform"
 | 
						|
#endif
 | 
						|
	stacksize = guarded_stacksize;
 | 
						|
	if (minix_munmap(guard_start, MTHREAD_GUARDSIZE) != 0)
 | 
						|
		mthread_panic("unable to unmap stack space for guard");
 | 
						|
	tcb->m_context.uc_stack.ss_sp = guard_end;
 | 
						|
  } else
 | 
						|
  	tcb->m_context.uc_stack.ss_sp = stackaddr;
 | 
						|
 | 
						|
  tcb->m_context.uc_stack.ss_size = stacksize;
 | 
						|
  makecontext(&(tcb->m_context), mthread_trampoline, 0);
 | 
						|
 | 
						|
  mthread_unsuspend(thread); /* Make thread runnable */
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_thread_reset			     *
 | 
						|
 *===========================================================================*/
 | 
						|
void mthread_thread_reset(thread)
 | 
						|
mthread_thread_t thread;
 | 
						|
{
 | 
						|
/* Reset the thread to its default values. Free the allocated stack space. */
 | 
						|
 | 
						|
  mthread_tcb_t *rt;
 | 
						|
  if (!isokthreadid(thread)) mthread_panic("Invalid thread id"); 
 | 
						|
 | 
						|
  rt = mthread_find_tcb(thread);
 | 
						|
  rt->m_tid = thread;
 | 
						|
  rt->m_next = NULL;
 | 
						|
  rt->m_state = MS_DEAD;
 | 
						|
  rt->m_proc = NULL;
 | 
						|
  rt->m_arg = NULL;
 | 
						|
  rt->m_result = NULL;
 | 
						|
  rt->m_cond = NULL;
 | 
						|
  if (rt->m_attr.ma_stackaddr == NULL) { /* We allocated stack space */
 | 
						|
	if (rt->m_context.uc_stack.ss_sp) {
 | 
						|
		if (minix_munmap(rt->m_context.uc_stack.ss_sp,
 | 
						|
				 rt->m_context.uc_stack.ss_size) != 0) {
 | 
						|
			mthread_panic("unable to unmap memory");
 | 
						|
		}
 | 
						|
	}
 | 
						|
	rt->m_context.uc_stack.ss_sp = NULL;
 | 
						|
  }
 | 
						|
  rt->m_context.uc_stack.ss_size = 0;
 | 
						|
  rt->m_context.uc_link = NULL;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_thread_stop			     *
 | 
						|
 *===========================================================================*/
 | 
						|
static void mthread_thread_stop(thread)
 | 
						|
mthread_thread_t thread;
 | 
						|
{
 | 
						|
/* Stop thread from running. Deallocate resources. */
 | 
						|
  mthread_tcb_t *stop_thread;
 | 
						|
 | 
						|
  if (!isokthreadid(thread)) mthread_panic("Invalid thread id"); 
 | 
						|
 | 
						|
  stop_thread = mthread_find_tcb(thread);
 | 
						|
 | 
						|
  if (stop_thread->m_state == MS_DEAD) {
 | 
						|
  	/* Already dead, nothing to do */
 | 
						|
  	return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (mthread_cond_destroy(&(stop_thread->m_exited)) != 0)
 | 
						|
  	mthread_panic("Could not destroy condition at thread deallocation\n");
 | 
						|
 | 
						|
  /* Can't deallocate ourselves (i.e., we're a detached thread) */
 | 
						|
  if (thread == current_thread) {
 | 
						|
	stop_thread->m_state = MS_NEEDRESET;
 | 
						|
	need_reset++;
 | 
						|
  } else {
 | 
						|
	mthread_thread_reset(thread);
 | 
						|
	used_threads--;
 | 
						|
	mthread_queue_add(&free_threads, thread);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/*===========================================================================*
 | 
						|
 *				mthread_trampoline			     *
 | 
						|
 *===========================================================================*/
 | 
						|
static void mthread_trampoline(void)
 | 
						|
{
 | 
						|
/* Execute the /current_thread's/ procedure. Store its result. */
 | 
						|
 | 
						|
  mthread_tcb_t *tcb;
 | 
						|
  void *r;
 | 
						|
 | 
						|
  tcb = mthread_find_tcb(current_thread);
 | 
						|
 | 
						|
  r = (tcb->m_proc)(tcb->m_arg);
 | 
						|
  mthread_exit(r); 
 | 
						|
}
 | 
						|
 |