在上一節中我們考察了磁盤的類驅動,包括由類驅動所創建的FDO和PDO兩種設備對象,以及這兩種設備對象之間的關係。
在“類(Class)”驅動的下面本應該是“端口(Port)”驅動,但是實際的設計和實現中常常把端口驅動也分成兩個子層,把其中與具體產品密切相關的成分分離出來,成爲“小端口(Miniport)”驅動。特別地,對於塊存儲設備,Windows採用的就是Class/Port/Miniport的結構方案,並由微軟提供塊存儲設備的端口驅動,這樣具體設備的廠家就只要爲自己的產品開發小端口驅動就可以了。
我們現在要考察的就是這樣一個分成兩層的端口驅動,主要是由DDK提供的一個小端口驅動aha154x.sys,其代碼在DDK的src/storage/miniport/aha154x中。Aha154x是由Adaptec公司提供的SCSI總線適配器。
ULONG //src/storage/miniport/aha154x
DriverEntry(IN PVOID DriverObject, IN PVOID Argument2)
{
HW_INITIALIZATION_DATA hwInitializationData;
......
// Zero out structure.
for (i=0; i<sizeof(HW_INITIALIZATION_DATA); i++)
((PUCHAR)&hwInitializationData)[i] = 0;
// Set size of hwInitializationData.
hwInitializationData.HwInitializationDataSize = sizeof(HW_INITIALIZATION_ DATA);
// Set entry points.
hwInitializationData.HwInitialize = A154xHwInitialize;
hwInitializationData.HwResetBus = A154xResetBus;
hwInitializationData.HwStartIo = A154xStartIo;
hwInitializationData.HwInterrupt = A154xInterrupt;
hwInitializationData.HwFindAdapter = A154xFindAdapter;
hwInitializationData.HwAdapterState = A154xAdapterState;
hwInitializationData.HwAdapterControl = A154xAdapterControl;
// Indicate no buffer mapping but will need physical addresses.
hwInitializationData.NeedPhysicalAddresses = TRUE;
// Specify size of extensions.
hwInitializationData.DeviceExtensionSize = sizeof(HW_DEVICE_EXTENSION);
hwInitializationData.SpecificLuExtensionSize = sizeof(HW_LU_EXTENSION);
// Specifiy the bus type.
hwInitializationData.AdapterInterfaceType = Isa; //這是ISA總線上的SCSI磁盤適配器
hwInitializationData.NumberOfAccessRanges = 2;
// Ask for SRB extensions for CCBs.
hwInitializationData.SrbExtensionSize = sizeof(CCB);
// The adapter count is used by the find adapter routine to track how
// which adapter addresses have been tested.
context.adapterCount = 0;
context.biosScanStart = 0;
isaStatus = ScsiPortInitialize(DriverObject, Argument2, &hwInitializationData, &context);
// Now try to configure for the Mca bus. Specifiy the bus type.
hwInitializationData.AdapterInterfaceType = MicroChannel;
context.adapterCount = 0;
context.biosScanStart = 0;
mcaStatus = ScsiPortInitialize(DriverObject, Argument2, &hwInitializationData, &context);
return(mcaStatus < isaStatus ? mcaStatus : isaStatus);
} // end A154xEntry()
這個模塊的DriverEntry()與前面Disk的DriverEntry()有了形式上的不同,注意這裏的函數指針都在一個HW_INITIALIZATION_DATA數據結構hwInitializationData中,而不是主功能函數數組中。這裏沒有直接設置主功能函數,而且既不直接創建設備對象,也不提供AddDevice函數。事實上,這些都是由ScsiPortInitialize()實現的。
那麼這裏的ScsiPortInitialize()又是從哪來的呢?DDK甚至不說是誰提供了這個函數,而只是在srb.h中提供了這個函數的聲明:
SCSIPORT_API ULONG
ScsiPortInitialize(IN PVOID Argument1, IN PVOID Argument2,
IN struct _HW_INITIALIZATION_DATA *HwInitializationData, IN PVOID HwContext);
意思是:“你別管是誰提供的,反正到時候有個什麼東西會向你提供這個函數就是了”。
幸好我們有工具depends.exe,可以看到這個函數是由scsiport.sys導出的,可惜DDK中卻不提供這個模塊的源代碼(要不然就不像是微軟了)。幸好,我們還有ReactOS,那裏也實現了scsiport.sys,從那裏我們可以看到其DriverEntry()的源代碼:
NTSTATUS STDCALL //ReactOS-0.3.0/drivers/storage/scsiport
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
DPRINT("ScsiPort Driver %s/n", VERSION);
return(STATUS_SUCCESS);
}
原來如此。當然,我們也可以拿來微軟的scsiport.sys,從其程序入口地址開始反彙編,讀不了幾條指令就應該可以發現這個祕密。
這個DriverEntry()揭示的事實是:SCSI磁盤的類驅動scsiport.sys並不創建自己的設備對象,因而是無形的,更無從出現在設備對象的堆疊中。換言之,scsiport.sys的作用僅僅是爲小端口驅動提供庫函數。由此也可見,“小端口驅動”乃是把端口驅動中的許多公共庫函數剝離以後所剩下來的那一小部分。
前面的代碼中調用了ScsiPortInitialize()兩次,第一次是針對ISA總線(實際上包括PCI總線);第二次是針對“微通道”總線MicroChannel。後者現在已經很少用了。
如前所述,DDK中並不提供ScsiPortInitialize()及其所屬類驅動的代碼,所以下面屬於類驅動的有關代碼均取自ReactOS。我們先分段閱讀ScsiPortInitialize()。
ULONG STDCALL
ScsiPortInitialize(IN PVOID Argument1, IN PVOID Argument2,
IN struct _HW_INITIALIZATION_DATA *HwInitializationData, IN PVOID HwContext)
{
PDRIVER_OBJECT DriverObject = (PDRIVER_OBJECT)Argument1;
......
if ((HwInitializationData->HwInitialize == NULL) ||
(HwInitializationData->HwStartIo == NULL) ||
(HwInitializationData->HwInterrupt == NULL) ||
(HwInitializationData->HwFindAdapter == NULL) ||
(HwInitializationData->HwResetBus == NULL))
return(STATUS_INVALID_PARAMETER);
DriverObject->DriverStartIo = NULL;
DriverObject->MajorFunction[IRP_MJ_CREATE] = ScsiPortCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = ScsiPortCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ScsiPortDeviceControl;
DriverObject->MajorFunction[IRP_MJ_SCSI] = ScsiPortDispatchScsi;
SystemConfig = IoGetConfigurationInformation();
DeviceExtensionSize = sizeof(SCSI_PORT_DEVICE_EXTENSION) +
HwInitializationData->DeviceExtensionSize;
PortConfigSize = sizeof(PORT_CONFIGURATION_INFORMATION) +
HwInitializationData->NumberOfAccessRanges * sizeof(ACCESS_RANGE);
MaxBus = (HwInitializationData->AdapterInterfaceType == PCIBus) ? 8 : 1;
PortDeviceObject = NULL;
BusNumber = 0;
SlotNumber.u.AsULONG = 0;
while (TRUE)
{ //爲配備的每條SCSI總線都創建一個設備對象
/* Create a unicode device name */
swprintf (NameBuffer, L"//Device//ScsiPort%lu", SystemConfig->ScsiPortCount);
RtlInitUnicodeString (&DeviceName, NameBuffer);
DPRINT("Creating device: %wZ/n", &DeviceName);
/* Create the port device */
Status = IoCreateDevice (DriverObject, DeviceExtensionSize, &DeviceName,
FILE_DEVICE_CONTROLLER, 0, FALSE, &PortDeviceObject);
if (!NT_SUCCESS(Status)) ......
/* Increase the stacksize. We reenter our device on IOCTL_SCSI_MINIPORT */
PortDeviceObject->StackSize++; //使StackSize變成2
/* Set the buffering strategy here... */
PortDeviceObject->Flags |= DO_DIRECT_IO;
PortDeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
DeviceExtension = PortDeviceObject->DeviceExtension;
RtlZeroMemory(DeviceExtension, DeviceExtensionSize);
DeviceExtension->Length = DeviceExtensionSize;
DeviceExtension->DeviceObject = PortDeviceObject;
DeviceExtension->PortNumber = SystemConfig->ScsiPortCount;
DeviceExtension->MiniPortExtensionSize = HwInitializationData->Device ExtensionSize;
DeviceExtension->LunExtensionSize = HwInitializationData->SpecificLu ExtensionSize;
DeviceExtension->SrbExtensionSize = HwInitializationData->SrbExtension Size;
DeviceExtension->HwStartIo = HwInitializationData->HwStartIo;
DeviceExtension->HwInterrupt = HwInitializationData->HwInterrupt;
DeviceExtension->AdapterObject = NULL;
DeviceExtension->MapRegisterCount = 0;
DeviceExtension->PhysicalAddress.QuadPart = 0ULL;
DeviceExtension->VirtualAddress = NULL;
DeviceExtension->CommonBufferLength = 0;
/* Initialize the device base list */
InitializeListHead (&DeviceExtension->DeviceBaseListHead);
/* Initialize the irp lists */
InitializeListHead (&DeviceExtension->PendingIrpListHead);
DeviceExtension->NextIrp = NULL;
DeviceExtension->PendingIrpCount = 0;
DeviceExtension->ActiveIrpCount = 0;
/* Initialize LUN-Extension list */
InitializeListHead (&DeviceExtension->LunExtensionListHead);
/* Initialize the spin lock in the controller extension */
KeInitializeSpinLock (&DeviceExtension->Lock);
/* Initialize the DPC object */
IoInitializeDpcRequest (PortDeviceObject, ScsiPortDpc); //初始化本設備對象的DPC
/* Initialize the device timer */
DeviceExtension->TimerState = IDETimerIdle;
DeviceExtension->TimerCount = 0;
IoInitializeTimer (PortDeviceObject, ScsiPortIoTimer, DeviceExtension);
/* Allocate and initialize port configuration info */
DeviceExtension->PortConfig = ExAllocatePool (NonPagedPool, PortConfigSize);
if (DeviceExtension->PortConfig == NULL) ......
RtlZeroMemory (DeviceExtension->PortConfig, PortConfigSize);
PortConfig = DeviceExtension->PortConfig;
PortConfig->Length = sizeof(PORT_CONFIGURATION_INFORMATION);
PortConfig->SystemIoBusNumber = BusNumber;
PortConfig->AdapterInterfaceType = HwInitializationData->AdapterInter faceType;
PortConfig->InterruptMode =
(PortConfig->AdapterInterfaceType == PCIBus) ? LevelSensitive : Latched;
PortConfig->MaximumTransferLength = SP_UNINITIALIZED_VALUE;
PortConfig->NumberOfPhysicalBreaks = SP_UNINITIALIZED_VALUE;
PortConfig->DmaChannel = SP_UNINITIALIZED_VALUE;
PortConfig->DmaPort = SP_UNINITIALIZED_VALUE;
PortConfig->DmaWidth = 0;
PortConfig->DmaSpeed = Compatible;
PortConfig->AlignmentMask = 0;
PortConfig->NumberOfAccessRanges = HwInitializationData->NumberOfAccess Ranges;
PortConfig->NumberOfBuses = 0;
for (i = 0; i < SCSI_MAXIMUM_BUSES; i++) //SCSI_MAXIMUM_BUSES定義爲8
PortConfig->InitiatorBusId[i] = 255;
PortConfig->ScatterGather = FALSE;
PortConfig->Master = FALSE;
PortConfig->CachesData = FALSE;
PortConfig->AdapterScansDown = FALSE;
PortConfig->AtdiskPrimaryClaimed = SystemConfig->AtDiskPrimaryAddress Claimed;
PortConfig->AtdiskSecondaryClaimed =
SystemConfig->AtDiskSecondaryAddressClaimed;
PortConfig->Dma32BitAddresses = FALSE;
PortConfig->DemandMode = FALSE;
PortConfig->MapBuffers = HwInitializationData->MapBuffers;
PortConfig->NeedPhysicalAddresses = HwInitializationData->NeedPhysical Addresses;
PortConfig->TaggedQueuing = HwInitializationData->TaggedQueuing;
PortConfig->AutoRequestSense = HwInitializationData->AutoRequestSense;
PortConfig->MultipleRequestPerLu = HwInitializationData->MultipleRequest PerLu;
PortConfig->ReceiveEvent = HwInitializationData->ReceiveEvent;
PortConfig->RealModeInitialized = FALSE;
PortConfig->BufferAccessScsiPortControlled = FALSE;
PortConfig->MaximumNumberOfTargets = SCSI_MAXIMUM_TARGETS;
PortConfig->SrbExtensionSize = HwInitializationData->SrbExtensionSize;
PortConfig->SpecificLuExtensionSize = HwInitializationData->SpecificLu ExtensionSize;
PortConfig->AccessRanges = (ACCESS_RANGE(*)[])(PortConfig + 1);
/* Search for matching PCI device */
if ((HwInitializationData->AdapterInterfaceType == PCIBus) &&
(HwInitializationData->VendorIdLength > 0) &&
(HwInitializationData->VendorId != NULL) &&
(HwInitializationData->DeviceIdLength > 0) &&
(HwInitializationData->DeviceId != NULL))
{ //總線類型爲PCI
/* Get PCI device data */
if (!SpiGetPciConfigData (HwInitializationData, PortConfig,
BusNumber, &SlotNumber))
{
Status = STATUS_UNSUCCESSFUL;
goto ByeBye;
}
}
/* Note: HwFindAdapter is called once for each bus */
Again = FALSE;
DPRINT("Calling HwFindAdapter() for Bus %lu/n", PortConfig->SystemIo BusNumber);
Result = (HwInitializationData->HwFindAdapter)(
&DeviceExtension->MiniPortDeviceExtension, HwContext,
0, /* BusInformation */ "", /* ArgumentString */
PortConfig, &Again);
if (Result == SP_RETURN_FOUND)
{
DPRINT("ScsiPortInitialize(): Found HBA! (%x)/n", PortConfig->Bus InterruptVector);
if (DeviceExtension->VirtualAddress == NULL &&
DeviceExtension->SrbExtensionSize)
{
ScsiPortGetUncachedExtension(&DeviceExtension->MiniPortDevice Extension,
PortConfig, 0);
}
/* Register an interrupt handler for this device */ //設置中斷向量等
MappedIrq = HalGetInterruptVector(PortConfig->AdapterInterfaceType,
PortConfig->SystemIoBusNumber, PortConfig->BusInterruptLevel,
PortConfig->BusInterruptLevel, &Dirql, &Affinity);
Status = IoConnectInterrupt(&DeviceExtension->Interrupt, ScsiPortIsr,
DeviceExtension, NULL, MappedIrq, Dirql, Dirql,
PortConfig->InterruptMode, TRUE, Affinity, FALSE);
if (!NT_SUCCESS(Status)) ......
if (!(HwInitializationData->HwInitialize)(
&DeviceExtension->MiniPortDeviceExtension))
{ //硬件初始化失敗
DbgPrint("HwInitialize() failed!");
Status = STATUS_UNSUCCESSFUL;
goto ByeBye;
}
/* Initialize port capabilities */
DeviceExtension->PortCapabilities = ExAllocatePool(NonPagedPool,
sizeof(IO_SCSI_CAPABILITIES));
if (DeviceExtension->PortCapabilities == NULL)
{
Status = STATUS_INSUFFICIENT_RESOURCES;
goto ByeBye;
}
PortCapabilities = DeviceExtension->PortCapabilities;
PortCapabilities->Length = sizeof(IO_SCSI_CAPABILITIES);
if (PortConfig->ScatterGather == FALSE ||
PortConfig->NumberOfPhysicalBreaks >= (0x100000000LL >> PAGE_SHIFT)||
PortConfig->MaximumTransferLength <
PortConfig->NumberOfPhysicalBreaks * PAGE_SIZE)
{
PortCapabilities->MaximumTransferLength =
PortConfig->MaximumTransferLength;
}
else
{
PortCapabilities->MaximumTransferLength =
PortConfig->NumberOfPhysicalBreaks * PAGE_SIZE;
}
PortCapabilities->MaximumPhysicalPages =
PortCapabilities->MaximumTransferLength / PAGE_SIZE;
PortCapabilities->SupportedAsynchronousEvents = 0; /* FIXME */
PortCapabilities->AlignmentMask = PortConfig->AlignmentMask;
PortCapabilities->TaggedQueuing = PortConfig->TaggedQueuing;
PortCapabilities->AdapterScansDown = PortConfig->AdapterScansDown;
PortCapabilities->AdapterUsesPio = TRUE; /* FIXME */
/* Scan the adapter for devices */
SpiScanAdapter(DeviceExtension); //掃描同一條SCSI總線上的磁盤
/* Build the registry device map */ //把掃描的發現記錄在註冊表中
SpiBuildDeviceMap (DeviceExtension, (PUNICODE_STRING)Argument2);
/* Create the dos device link */ //生成DOS設備名並建立符號連接
swprintf(DosNameBuffer, L"//??//Scsi%lu:", SystemConfig->ScsiPort Count);
RtlInitUnicodeString(&DosDeviceName, DosNameBuffer);
IoCreateSymbolicLink(&DosDeviceName, &DeviceName);
/* Update the system configuration info */
if (PortConfig->AtdiskPrimaryClaimed == TRUE)
SystemConfig->AtDiskPrimaryAddressClaimed = TRUE;
if (PortConfig->AtdiskSecondaryClaimed == TRUE)
SystemConfig->AtDiskSecondaryAddressClaimed = TRUE;
SystemConfig->ScsiPortCount++;
PortDeviceObject = NULL;
DeviceFound = TRUE;
} //end if (Result == SP_RETURN_FOUND)
else
{
DPRINT("HwFindAdapter() Result: %lu/n", Result);
ExFreePool (PortConfig);
IoDeleteDevice (PortDeviceObject);
PortDeviceObject = NULL;
}
DPRINT("Bus: %lu MaxBus: %lu/n", BusNumber, MaxBus);
if (BusNumber >= MaxBus)
{
Status = STATUS_SUCCESS;
goto ByeBye;
}
if (Again == FALSE)
{
BusNumber++;
SlotNumber.u.AsULONG = 0;
}
} //end while
ByeBye:
/* Clean up the mess */
if (PortDeviceObject != NULL)
{
DPRINT("Delete device: %p/n", PortDeviceObject);
DeviceExtension = PortDeviceObject->DeviceExtension;
if (DeviceExtension->PortCapabilities != NULL)
{
IoDisconnectInterrupt (DeviceExtension->Interrupt);
ExFreePool (DeviceExtension->PortCapabilities);
}
if (DeviceExtension->PortConfig != NULL)
{
ExFreePool (DeviceExtension->PortConfig);
}
IoDeleteDevice (PortDeviceObject);
}
return (DeviceFound == FALSE) ? Status : STATUS_SUCCESS;
}
我們關心的是設備驅動的框架,而不是SCSI磁盤的細節,這裏的許多代碼都與SCSI總線和磁盤的細節有關,要對這些細節做一介紹一方面說來話長,另一方面也不免離題,所以這裏只對代碼中那些框架性的東西做一說明。有關SCSI磁盤的詳情則需要參考這方面的專門資料。
先看主功能函數的設置。注意這裏沒有提供針對IRP_MJ_READ、IRP_MJ_WRITE的主功能函數,這是因爲對SCSI磁盤的操作是通過SCSI_REQUEST_BLOCK數據結構即“SCSI請求塊”SRB的傳遞而完成的,這些操作都統一抽象爲“SCSI操作”,在“SCSI操作”中又分出讀、寫等具體的操作,所以這裏提供了針對IRP_MJ_SCSI的主功能函數。其實,IRP在某種意義上正是對SRB的模仿。
一臺機器上可以接好幾條“SCSI”總線,每條SCSI總線上則可以接好幾個磁盤。這裏通過一個while循環爲每條SCSI總線都創建一個設備對象,併爲其設置好中斷向量和DPC對象。這些設備對象都指向同一個驅動對象,所以IRP無論進入哪一個設備對象都會執行相同的主功能函數。這些設備對象是由同一個驅動對象創建的,所以都掛在這個驅動對象的隊列中。注意這裏並沒有爲每個磁盤都創建一個設備對象,因爲SCSI總線的控制是集中式的,在同一條總線上不同的磁盤具有不同的“邏輯單元號”LUN,對不同磁盤的操作只是體現在SRB中不同的邏輯單元號。
代碼中沒有提供AddDevice函數,這是因爲所創建的設備對象不再堆疊到別的設備對象上,IRP一旦進入這些設備對象就不再下傳了。如前所述,在某種意義上IRP是對SRB的模仿,所以也可以認爲IRP進入這些設備對象以後就轉化成SRB繼續下傳,不過此時是通過SCSI總線的硬件下傳了。就IRP而言,這些設備對象已經是末梢、到了盡頭了。
在這個例子中,端口驅動是無形的,它沒有自己的設備對象,當然更談不上出現在堆疊中。出現在堆疊中的是小端口驅動的設備對象,這些設備對象當然是指向小端口驅動對象,主功能函數指針的數組也在小端口驅動對象中。但是這些主功能函數卻是由端口驅動模塊提供的,所以端口驅動模塊相當於一個程序庫,也可以看做是對內核及其設備驅動框架的擴充。顯然,在這裏端口驅動模塊的裝載必須在小端口驅動模塊之前。但是,由端口驅動模塊提供的主功能函數卻不知道怎樣與具體設備的硬件接口打交道,於是小端口驅動模塊又得反過來爲其提供一組函數,這就是HW_INITIALIZATION_DATA結構中那些函數指針的作用。
相比之下,在前面鼠標器驅動的那個例子中,那裏沒有小端口驅動,而端口驅動卻有個設備對象在堆疊中,這是因爲沒有把公共的庫函數剝離出去。至於怎麼剝離,剝離以後形成什麼樣的邊界,那就是設計者的事了。所以,端口驅動和小端口驅動之間的關係只是二者之間的事,二者的開發者之間有個約定就行,在這方面並不存在某種統一的範式,更不遵循由IRP+IoCallDriver()所構成的標準界面。
最後還要說明,我們在這裏所考察的小端口驅動aha154x.sys是用於SCSI磁盤的,所以其端口驅動是scsiport.sys。可是如果用的是IDE磁盤呢?IDE磁盤的小端口驅動是pciide.sys+pciidex.sys,與其相配的端口驅動則是atapi.sys。
此外,從Windows Server 2003開始又引入了另一個端口驅動storport.sys來取代scsiport.sys,以支持磁盤陣列RAID並提供“高性能計算”所需的其他支持。
總結一下SCSI磁盤的類驅動和端口驅動,我們看到:
— 驅動模塊disk.sys的DriverEntry()通過由classpnp.sys提供的ClassInitialize()和ClassInitializeEx()進行初始化。disk.sys爲此而準備的數據結構爲CLASS_INIT_DATA。disk.sys有自己的設備對象,並出現在相關的設備對象堆疊中,所以是“有形”的;而classpnp.sys本身不創建自己的設備對象,所以是“無形”的,只是一個動態連接庫。
— 驅動模塊aha154x.sys的DriverEntry()通過由scsiport.sys提供的ScsiPortInitialize()進行初始化。aha154x.sys爲此準備的數據結構爲HW_INITIALIZATION_DATA。aha154x.sys有自己的設備對象,並出現在相關的設備對象堆疊中,所以是“有形”的;而scsiport.sys本身不創建自己的設備對象,所以是“無形”的,只是一個動態連接庫。
數據結構CLASS_INIT_DATA和HW_INITIALIZATION_DATA的本質都是向上層模塊提供下層模塊中的函數指針和數據,只是兩個數據結構所面向的層次不同。
就是因爲數據結構所面向的層次不同,disk.sys稱爲“類驅動(Class Driver)”,而aha154x.sys稱爲“小端口驅動”。
在本書中我們所關心的主要是磁盤驅動的系統結構,而不是磁盤操作的算法、流程和細節。對於有關細節有興趣的讀者可以自己進一步研讀代碼,或者參考《Linux內核源代碼情景分析》一書中Linux內核所實現的磁盤驅動。實際上,拋開系統結構上的不同,剩下來的就很接近了。具體的細節可能不同,但是大部分的算法和流程其實是很接近的。比方說,對於訪問磁盤上扇區的先後需要進行調度,以提高訪問的效率,爲此Linux內核中實現了一個“電梯算法”,而Windows則在類驅動中實現了類似的算法,叫C-Look。當然,由於文件系統的結構不同,這些算法也不會完全相同,但是所體現的思路則大同小異。所以,兩個系統之間有着“他山之石,可以攻玉”的關係,瞭解任何一方對於瞭解另一方都是大有幫助的。