|
|
/*++
Copyright (c) 1989 Microsoft Corporation
Module Name:
mm.c
Abstract:
This module implements the memory managment routines for the SMB mini redirector
Author:
Balan Sethu Raman [SethuR] 7-March-1995
Revision History:
Notes:
The SMB mini redirector manipulates entities which have very different usage patterns. They range from very static entities ( which are allocated and freed with a very low frequency ) to very dynamic entities.
The entities manipulated in the SMB mini redirector are SMBCE_SERVER, SMBCE_NET_ROOT, SMBCE_VC, SMBCE_SESSION. These represent a connection to a server, a share on a particular server, a virtual circuit used in the connection and a session for a particular user.
These are not very dynamic, i.e., the allocation/deallocation is very infrequent. The SMB_EXCHANGE and SMBCE_REQUEST map to the SMB's that are sent along that a connection. Every file operation in turn maps to a certain number of calls for allocationg/freeing exchanges and requests. Therefore it is imperative that some form of scavenging/caching of recently freed entries be maintained to satisfy requests quickly.
In the current implementation the exchanges and requests are implemented using the zone allocation primitives.
The exchange allocation and free routines are currently implemented as wrappers around the RxAllocate and RxFree routines. It would be far more efficient if a look aside cache of some exchange instances are maintained.
--*/
#include "precomp.h"
#pragma hdrstop
#include <exsessup.h>
#include <mssndrcv.h>
#include <vcsndrcv.h>
#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, SmbMmAllocateSessionEntry)
#pragma alloc_text(PAGE, SmbMmFreeSessionEntry)
#pragma alloc_text(PAGE, SmbMmAllocateServerTransport)
#pragma alloc_text(PAGE, SmbMmFreeServerTransport)
#pragma alloc_text(PAGE, SmbMmInit)
#pragma alloc_text(PAGE, SmbMmTearDown)
#endif
#define SMBMM_ZONE_ALLOCATION 0x10
// The memory management package addresses a number of concerns w.r.t debugging
// and performance. By centralizing all the allocation/deallocation routines to
// thsi one module it is possible to build up profiles regarding various data
// structures used by the connection engine. In addition debugging support is
// provided by thereading together all allocated objects of a particular type
// are threaded together in a linked list according to type.
//
// At any stage by inspecting these lists the currently active instances of a
// particular type can be enumerated.
//
// Each type handled by this module is provided with two routines, e.g., for
// server entries there are SmbMmInitializeEntry and SmbMmUninitializeEntry. The
// first routine is called before handing over a pointer of a newly created
// instance. This will ensure that the instance is in a wll known initial state.
// Similarly the second routine is called just before deallocating the pool
// associated with the instance. This helps enforce the necessary integrity
// constraints, e.g., all enclosed pointers must be NULL etc.
//
// The pool allocation/deallocation is handled by the following routines
//
// SmbMmAllocateObjectPool/SmbMmFreeObjectPool
//
// SmbMmAllocateExchange/SmbMmFreeExchange
//
// The Object allocation routines are split up into two parts so as to be able to
// handle the session allocationson par with other objects even though they are
// further subtyped.
//
// On debug builds additional pool is allocated and the appropriate linking is
// done into the corresponding list. On retail builds these map to the regular
// pool allocation wrappers.
//
// Zone allocation to speed up memory management of RxCe entities.
//
ULONG SmbMmRequestZoneEntrySize; ZONE_HEADER SmbMmRequestZone; PVOID SmbMmRequestZoneSegmentPtr;
//
// Pool allocation resources and spin locks
//
KSPIN_LOCK SmbMmSpinLock;
ULONG SmbMmExchangeId;
//
// List of the various objects/exchanges allocated.
//
LIST_ENTRY SmbMmExchangesInUse[SENTINEL_EXCHANGE]; LIST_ENTRY SmbMmObjectsInUse[SMBCEDB_OT_SENTINEL];
ULONG ObjectSizeInBytes[SMBCEDB_OT_SENTINEL]; ULONG ExchangeSizeInBytes[SENTINEL_EXCHANGE];
//
// Lookaside lists for Exchange allocation
//
NPAGED_LOOKASIDE_LIST SmbMmExchangesLookasideList[SENTINEL_EXCHANGE];
INLINE PSMBCE_OBJECT_HEADER SmbMmAllocateObjectPool( SMBCEDB_OBJECT_TYPE ObjectType, ULONG PoolType, ULONG PoolSize) { KIRQL SavedIrql; PVOID pv = NULL; UCHAR Flags = 0;
PSMBCE_OBJECT_HEADER pHeader = NULL;
ASSERT((ObjectType >= 0) && (ObjectType < SMBCEDB_OT_SENTINEL));
if (ObjectType == SMBCEDB_OT_REQUEST) { // Acquire the resource lock.
KeAcquireSpinLock( &SmbMmSpinLock, &SavedIrql );
if (!ExIsFullZone( &SmbMmRequestZone )) { pv = ExAllocateFromZone( &SmbMmRequestZone ); Flags = SMBMM_ZONE_ALLOCATION; }
// Release the resource lock.
KeReleaseSpinLock( &SmbMmSpinLock, SavedIrql ); }
if (pv == NULL) { PLIST_ENTRY pListEntry;
pv = RxAllocatePoolWithTag( PoolType, PoolSize + sizeof(LIST_ENTRY), MRXSMB_MM_POOLTAG);
if (pv != NULL) { pListEntry = (PLIST_ENTRY)pv; pHeader = (PSMBCE_OBJECT_HEADER)(pListEntry + 1);
ExInterlockedInsertTailList( &SmbMmObjectsInUse[ObjectType], pListEntry, &SmbMmSpinLock); } } else { pHeader = (PSMBCE_OBJECT_HEADER)pv; }
if (pHeader != NULL) { // Zero the memory.
RtlZeroMemory( pHeader, PoolSize);
pHeader->Flags = Flags; }
return pHeader; }
VOID SmbMmFreeObjectPool( PSMBCE_OBJECT_HEADER pHeader) { KIRQL SavedIrql; BOOLEAN ZoneAllocation = FALSE; PLIST_ENTRY pListEntry;
ASSERT((pHeader->ObjectType >= 0) && (pHeader->ObjectType < SMBCEDB_OT_SENTINEL));
// Acquire the resource lock.
KeAcquireSpinLock( &SmbMmSpinLock, &SavedIrql );
// Check if it was a zone allocation
if (pHeader->Flags & SMBMM_ZONE_ALLOCATION) { ZoneAllocation = TRUE; ExFreeToZone(&SmbMmRequestZone,pHeader); } else { pListEntry = (PLIST_ENTRY)((PCHAR)pHeader - sizeof(LIST_ENTRY)); RemoveEntryList(pListEntry); }
// Release the resource lock.
KeReleaseSpinLock( &SmbMmSpinLock, SavedIrql );
if (!ZoneAllocation) { RxFreePool(pListEntry); } }
// Construction and destruction of various SMB connection engine objects
//
#define SmbMmInitializeServerEntry(pServerEntry) \
InitializeListHead(&(pServerEntry)->OutstandingRequests.ListHead); \ InitializeListHead(&(pServerEntry)->MidAssignmentRequests.ListHead); \ InitializeListHead(&(pServerEntry)->SecuritySignatureSyncRequests.ListHead); \ InitializeListHead(&(pServerEntry)->Sessions.ListHead); \ InitializeListHead(&(pServerEntry)->NetRoots.ListHead); \ InitializeListHead(&(pServerEntry)->VNetRootContexts.ListHead); \ InitializeListHead(&(pServerEntry)->ActiveExchanges); \ InitializeListHead(&(pServerEntry)->ExpiredExchanges); \ InitializeListHead(&(pServerEntry)->Sessions.DefaultSessionList); \ (pServerEntry)->pTransport = NULL; \ (pServerEntry)->pMidAtlas = NULL
#define SmbMmInitializeSessionEntry(pSessionEntry) \
InitializeListHead(&(pSessionEntry)->Requests.ListHead); \ InitializeListHead(&(pSessionEntry)->SerializationList); \ (pSessionEntry)->DefaultSessionLink.Flink = NULL; \ (pSessionEntry)->DefaultSessionLink.Blink = NULL
#define SmbMmInitializeNetRootEntry(pNetRootEntry) \
InitializeListHead(&(pNetRootEntry)->Requests.ListHead)
#define SmbMmUninitializeServerEntry(pServerEntry) \
ASSERT(IsListEmpty(&(pServerEntry)->OutstandingRequests.ListHead) && \ IsListEmpty(&(pServerEntry)->MidAssignmentRequests.ListHead) && \ IsListEmpty(&(pServerEntry)->SecuritySignatureSyncRequests.ListHead) && \ IsListEmpty(&(pServerEntry)->Sessions.ListHead) && \ IsListEmpty(&(pServerEntry)->NetRoots.ListHead) && \ ((pServerEntry)->pMidAtlas == NULL))
#define SmbMmUninitializeSessionEntry(pSessionEntry) \
ASSERT(IsListEmpty(&(pSessionEntry)->Requests.ListHead) && \ ((pSessionEntry)->DefaultSessionLink.Flink == NULL))
#define SmbMmUninitializeNetRootEntry(pNetRootEntry) \
ASSERT(IsListEmpty(&(pNetRootEntry)->Requests.ListHead))
#define SmbMmInitializeRequestEntry(pRequestEntry)
#define SmbMmUninitializeRequestEntry(pRequestEntry)
PVOID SmbMmAllocateObject( SMBCEDB_OBJECT_TYPE ObjectType) { PSMBCE_OBJECT_HEADER pHeader;
ASSERT((ObjectType >= 0) && (ObjectType < SMBCEDB_OT_SENTINEL));
pHeader = SmbMmAllocateObjectPool( ObjectType, NonPagedPool, ObjectSizeInBytes[ObjectType]);
if (pHeader != NULL) { pHeader->NodeType = SMB_CONNECTION_ENGINE_NTC(ObjectType); pHeader->State = SMBCEDB_START_CONSTRUCTION;
switch (ObjectType) { case SMBCEDB_OT_SERVER : SmbMmInitializeServerEntry((PSMBCEDB_SERVER_ENTRY)pHeader); break;
case SMBCEDB_OT_NETROOT : SmbMmInitializeNetRootEntry((PSMBCEDB_NET_ROOT_ENTRY)pHeader); break;
case SMBCEDB_OT_REQUEST : SmbMmInitializeRequestEntry((PSMBCEDB_REQUEST_ENTRY)pHeader); break;
default: ASSERT(!"Valid Type for SmbMmAllocateObject"); break; } }
return pHeader; }
VOID SmbMmFreeObject( PVOID pv) { PSMBCE_OBJECT_HEADER pHeader = (PSMBCE_OBJECT_HEADER)pv;
switch (pHeader->ObjectType) { case SMBCEDB_OT_SERVER : SmbMmUninitializeServerEntry((PSMBCEDB_SERVER_ENTRY)pHeader); break;
case SMBCEDB_OT_NETROOT : SmbMmUninitializeNetRootEntry((PSMBCEDB_NET_ROOT_ENTRY)pHeader); break;
case SMBCEDB_OT_REQUEST : SmbMmUninitializeRequestEntry((PSMBCEDB_REQUEST_ENTRY)pHeader); break;
default: ASSERT(!"Valid Type for SmbMmFreeObject"); break; }
SmbMmFreeObjectPool(pHeader); }
PSMBCEDB_SESSION_ENTRY SmbMmAllocateSessionEntry( PSMBCEDB_SERVER_ENTRY pServerEntry, BOOLEAN RemoteBootSession) { PSMBCEDB_SESSION_ENTRY pSessionEntry; SESSION_TYPE SessionType; ULONG SessionSize;
PAGED_CODE();
SessionSize = sizeof(SMBCEDB_SESSION_ENTRY) - sizeof(SMBCE_SESSION);
if (pServerEntry->Header.State != SMBCEDB_INVALID) { if ((pServerEntry->Server.DialectFlags & DF_EXTENDED_SECURITY) && !RemoteBootSession) { SessionSize += sizeof(SMBCE_EXTENDED_SESSION);
SessionType = EXTENDED_NT_SESSION; } else { // allocate a LANMAN session
SessionSize += sizeof(SMBCE_SESSION); SessionType = LANMAN_SESSION; } } else { SessionSize += sizeof(SMBCE_EXTENDED_SESSION); SessionType = UNINITIALIZED_SESSION; }
pSessionEntry = (PSMBCEDB_SESSION_ENTRY) SmbMmAllocateObjectPool( SMBCEDB_OT_SESSION, NonPagedPool, SessionSize);
if (pSessionEntry != NULL) { pSessionEntry->Header.NodeType = SMB_CONNECTION_ENGINE_NTC(SMBCEDB_OT_SESSION); pSessionEntry->Header.State = SMBCEDB_START_CONSTRUCTION; pSessionEntry->Session.Type = SessionType;
SmbMmInitializeSessionEntry(pSessionEntry);
SecInvalidateHandle( &pSessionEntry->Session.CredentialHandle ); SecInvalidateHandle( &pSessionEntry->Session.SecurityContextHandle );
if (SessionType == EXTENDED_NT_SESSION) { PSMBCE_EXTENDED_SESSION pExtSecuritySession = (PSMBCE_EXTENDED_SESSION)&pSessionEntry->Session; pExtSecuritySession->pServerResponseBlob = NULL; pExtSecuritySession->ServerResponseBlobLength = 0; } }
return pSessionEntry; }
VOID SmbMmFreeSessionEntry( PSMBCEDB_SESSION_ENTRY pSessionEntry) { PAGED_CODE();
if (pSessionEntry->Session.Type == EXTENDED_NT_SESSION) { // KERBEROS specific asserts
}
SmbMmUninitializeSessionEntry(pSessionEntry);
SmbMmFreeObjectPool(&pSessionEntry->Header); }
PVOID SmbMmAllocateExchange( SMB_EXCHANGE_TYPE ExchangeType, PVOID pv) { KIRQL SavedIrql; ULONG SizeInBytes; USHORT Flags = 0; PSMB_EXCHANGE pExchange = NULL; PLIST_ENTRY pListEntry;
ASSERT((ExchangeType >= 0) && (ExchangeType < SENTINEL_EXCHANGE));
if (pv==NULL) { pv = ExAllocateFromNPagedLookasideList( &SmbMmExchangesLookasideList[ExchangeType]); } else { Flags |= SMBCE_EXCHANGE_NOT_FROM_POOL; }
if (pv != NULL) { // Initialize the object header
pExchange = (PSMB_EXCHANGE)(pv);
// Zero the memory.
RtlZeroMemory( pExchange, ExchangeSizeInBytes[ExchangeType]);
pExchange->NodeTypeCode = SMB_EXCHANGE_NTC(ExchangeType); pExchange->NodeByteSize = (USHORT)ExchangeSizeInBytes[ExchangeType];
pExchange->SmbCeState = SMBCE_EXCHANGE_INITIALIZATION_START; pExchange->SmbCeFlags = Flags;
InitializeListHead(&pExchange->ExchangeList); InitializeListHead(&pExchange->CancelledList);
switch (pExchange->Type) { case CONSTRUCT_NETROOT_EXCHANGE: pExchange->pDispatchVector = &ConstructNetRootExchangeDispatch; break;
case TRANSACT_EXCHANGE : pExchange->pDispatchVector = &TransactExchangeDispatch; break;
case EXTENDED_SESSION_SETUP_EXCHANGE: pExchange->pDispatchVector = &ExtendedSessionSetupExchangeDispatch; break;
case ADMIN_EXCHANGE: pExchange->pDispatchVector = &AdminExchangeDispatch; break; }
// Acquire the resource lock.
KeAcquireSpinLock( &SmbMmSpinLock, &SavedIrql );
InsertTailList( &SmbMmExchangesInUse[pExchange->Type], &pExchange->SmbMmInUseListEntry);
pExchange->Id = SmbMmExchangeId++;
// Release the resource lock.
KeReleaseSpinLock( &SmbMmSpinLock, SavedIrql ); }
return pExchange; }
VOID SmbMmFreeExchange( PSMB_EXCHANGE pExchange) { if (pExchange != NULL) { SMB_EXCHANGE_TYPE ExchangeType;
KIRQL SavedIrql;
ExchangeType = pExchange->Type;
ASSERT((ExchangeType >= 0) && (ExchangeType < SENTINEL_EXCHANGE));
if (pExchange->WorkQueueItem.List.Flink != NULL) { //DbgBreakPoint();
}
// Acquire the resource lock.
KeAcquireSpinLock( &SmbMmSpinLock, &SavedIrql );
RemoveEntryList(&pExchange->SmbMmInUseListEntry);
// Release the resource lock.
KeReleaseSpinLock( &SmbMmSpinLock, SavedIrql );
if (!FlagOn(pExchange->SmbCeFlags,SMBCE_EXCHANGE_NOT_FROM_POOL)) { ExFreeToNPagedLookasideList( &SmbMmExchangesLookasideList[ExchangeType], pExchange); } } }
PVOID SmbMmAllocateServerTransport( SMBCE_SERVER_TRANSPORT_TYPE ServerTransportType) { PSMBCE_OBJECT_HEADER pHeader;
ULONG AllocationSize; ULONG PoolTag;
PAGED_CODE();
switch (ServerTransportType) { case SMBCE_STT_VC: AllocationSize = sizeof(SMBCE_SERVER_VC_TRANSPORT); PoolTag = MRXSMB_VC_POOLTAG; break;
case SMBCE_STT_MAILSLOT: AllocationSize = sizeof(SMBCE_SERVER_MAILSLOT_TRANSPORT); PoolTag = MRXSMB_MAILSLOT_POOLTAG; break;
default: ASSERT(!"Valid Server Transport Type"); return NULL; }
pHeader = (PSMBCE_OBJECT_HEADER) RxAllocatePoolWithTag( NonPagedPool, AllocationSize, PoolTag);
if (pHeader != NULL) { PSMBCE_SERVER_TRANSPORT pServerTransport;
RtlZeroMemory(pHeader,AllocationSize);
pHeader->ObjectCategory = SMB_SERVER_TRANSPORT_CATEGORY; pHeader->ObjectType = (UCHAR)ServerTransportType; pHeader->SwizzleCount = 0; pHeader->State = 0; pHeader->Flags = 0;
pServerTransport = (PSMBCE_SERVER_TRANSPORT)pHeader;
pServerTransport->pRundownEvent = NULL;
switch (ServerTransportType) { case SMBCE_STT_MAILSLOT: break;
case SMBCE_STT_VC: { PSMBCE_SERVER_VC_TRANSPORT pVcTransport;
pVcTransport = (PSMBCE_SERVER_VC_TRANSPORT)pHeader; } break;
default: break; } }
return pHeader; }
VOID SmbMmFreeServerTransport( PSMBCE_SERVER_TRANSPORT pServerTransport) { PAGED_CODE();
ASSERT((pServerTransport->SwizzleCount == 0) && (pServerTransport->ObjectCategory == SMB_SERVER_TRANSPORT_CATEGORY));
RxFreePool(pServerTransport); }
NTSTATUS SmbMmInit() /*++
Routine Description:
This routine initialises the connection engine structures for memory management
Return Value:
STATUS_SUCCESS if successful, otherwise an informative error code.
--*/ { NTSTATUS Status = STATUS_SUCCESS; ULONG ZoneSegmentSize;
PAGED_CODE();
// Initialize the resource lock for the zone allocator.
KeInitializeSpinLock( &SmbMmSpinLock );
SmbMmRequestZoneEntrySize = QuadAlign(sizeof(SMBCEDB_REQUEST_ENTRY));
// Currently the request zone size is restricted to that of a page. This can and should
// be fine tuned.
ZoneSegmentSize = PAGE_SIZE;
SmbMmRequestZoneSegmentPtr = RxAllocatePoolWithTag( NonPagedPool, ZoneSegmentSize, MRXSMB_MM_POOLTAG);
if (SmbMmRequestZoneSegmentPtr != NULL) { SMB_EXCHANGE_TYPE ExchangeType;
ExInitializeZone( &SmbMmRequestZone, SmbMmRequestZoneEntrySize, SmbMmRequestZoneSegmentPtr, ZoneSegmentSize );
// set up the sizes for allocation.
ObjectSizeInBytes[SMBCEDB_OT_SERVER] = sizeof(SMBCEDB_SERVER_ENTRY); ObjectSizeInBytes[SMBCEDB_OT_NETROOT] = sizeof(SMBCEDB_NET_ROOT_ENTRY); ObjectSizeInBytes[SMBCEDB_OT_SESSION] = sizeof(SMBCEDB_SESSION_ENTRY); ObjectSizeInBytes[SMBCEDB_OT_REQUEST] = sizeof(SMBCEDB_REQUEST_ENTRY);
ExchangeSizeInBytes[CONSTRUCT_NETROOT_EXCHANGE] = sizeof(SMB_CONSTRUCT_NETROOT_EXCHANGE); ExchangeSizeInBytes[TRANSACT_EXCHANGE] = sizeof(SMB_TRANSACT_EXCHANGE); ExchangeSizeInBytes[ORDINARY_EXCHANGE] = sizeof(SMB_PSE_ORDINARY_EXCHANGE); ExchangeSizeInBytes[EXTENDED_SESSION_SETUP_EXCHANGE] = sizeof(SMB_EXTENDED_SESSION_SETUP_EXCHANGE); ExchangeSizeInBytes[ADMIN_EXCHANGE] = sizeof(SMB_ADMIN_EXCHANGE);
InitializeListHead(&SmbMmExchangesInUse[CONSTRUCT_NETROOT_EXCHANGE]); ExInitializeNPagedLookasideList( &SmbMmExchangesLookasideList[CONSTRUCT_NETROOT_EXCHANGE], ExAllocatePoolWithTag, ExFreePool, 0, sizeof(SMB_CONSTRUCT_NETROOT_EXCHANGE), MRXSMB_MM_POOLTAG, 1);
InitializeListHead(&SmbMmExchangesInUse[TRANSACT_EXCHANGE]); ExInitializeNPagedLookasideList( &SmbMmExchangesLookasideList[TRANSACT_EXCHANGE], ExAllocatePoolWithTag, ExFreePool, 0, sizeof(SMB_TRANSACT_EXCHANGE), MRXSMB_MM_POOLTAG, 2);
InitializeListHead(&SmbMmExchangesInUse[ORDINARY_EXCHANGE]); ExInitializeNPagedLookasideList( &SmbMmExchangesLookasideList[ORDINARY_EXCHANGE], ExAllocatePoolWithTag, ExFreePool, 0, sizeof(SMB_PSE_ORDINARY_EXCHANGE), MRXSMB_MM_POOLTAG, 4);
InitializeListHead(&SmbMmExchangesInUse[EXTENDED_SESSION_SETUP_EXCHANGE]); ExInitializeNPagedLookasideList( &SmbMmExchangesLookasideList[EXTENDED_SESSION_SETUP_EXCHANGE], ExAllocatePoolWithTag, ExFreePool, 0, sizeof(SMB_EXTENDED_SESSION_SETUP_EXCHANGE), MRXSMB_MM_POOLTAG, 1);
InitializeListHead(&SmbMmExchangesInUse[ADMIN_EXCHANGE]); ExInitializeNPagedLookasideList( &SmbMmExchangesLookasideList[ADMIN_EXCHANGE], ExAllocatePoolWithTag, ExFreePool, 0, sizeof(SMB_ADMIN_EXCHANGE), MRXSMB_MM_POOLTAG, 1);
InitializeListHead(&SmbMmObjectsInUse[SMBCEDB_OT_SERVER]); InitializeListHead(&SmbMmObjectsInUse[SMBCEDB_OT_SESSION]); InitializeListHead(&SmbMmObjectsInUse[SMBCEDB_OT_NETROOT]); InitializeListHead(&SmbMmObjectsInUse[SMBCEDB_OT_REQUEST]);
SmbMmExchangeId = 1; } else { Status = STATUS_INSUFFICIENT_RESOURCES; }
return Status; }
VOID SmbMmTearDown() /*++
Routine Description:
This routine tears down the memory management structures in the SMB connection engine
--*/ { NTSTATUS Status;
PAGED_CODE();
// free the segment associated with RxCe object allocation.
RxFreePool(SmbMmRequestZoneSegmentPtr);
ExDeleteNPagedLookasideList( &SmbMmExchangesLookasideList[CONSTRUCT_NETROOT_EXCHANGE]);
ExDeleteNPagedLookasideList( &SmbMmExchangesLookasideList[TRANSACT_EXCHANGE]);
ExDeleteNPagedLookasideList( &SmbMmExchangesLookasideList[ORDINARY_EXCHANGE]);
ExDeleteNPagedLookasideList( &SmbMmExchangesLookasideList[EXTENDED_SESSION_SETUP_EXCHANGE]);
ExDeleteNPagedLookasideList( &SmbMmExchangesLookasideList[ADMIN_EXCHANGE]); }
|