進(jìn)程是一個(gè)可執行的程序,由私有虛擬地址空間、代碼、數據和其他操作系統資源(如進(jìn)程創(chuàng )建的文件、管道、同步對象等)組成。一個(gè)應用程序可以有一個(gè)或多個(gè)進(jìn)程,一個(gè)進(jìn)程可以有一個(gè)或多個(gè)線(xiàn)程,其中一個(gè)是主線(xiàn)程。
線(xiàn)程是操作系統分時(shí)調度分配CPU時(shí)間的基本實(shí)體。一個(gè)線(xiàn)程可以執行程序的任意部分的代碼,即使這部分代碼被另一個(gè)線(xiàn)程并發(fā)地執行;一個(gè)進(jìn)程的所有線(xiàn)程共享它的虛擬地址空間、全局變量和操作系統資源。
之所以有線(xiàn)程這個(gè)概念,是因為以線(xiàn)程而不是進(jìn)程為調度對象效率更高:
因為MFC沒(méi)有提供類(lèi)處理進(jìn)程,所以直接使用了Win32 API函數。
調用CreateProcess函數創(chuàng )建新的進(jìn)程,運行指定的程序。CreateProcess的原型如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
其中:
lpApplicationName指向包含了要運行模塊名字的字符串。
lpCommandLine指向命令行字符串。
lpProcessAttributes描述進(jìn)程的安全性屬性,NT下有用。
lpThreadAttributes描述進(jìn)程初始線(xiàn)程(主線(xiàn)程)的安全性屬性,NT下有用。
bInHeritHandles表示子進(jìn)程(被創(chuàng )建的進(jìn)程)是否可以繼承父進(jìn)程的句柄??梢岳^承的句柄有線(xiàn)程句柄、有名或無(wú)名管道、互斥對象、事件、信號量、映像文件、普通文件和通訊端口等;還有一些句柄不能被繼承,如內存句柄、DLL實(shí)例句柄、GDI句柄、URER句柄等等。
子進(jìn)程繼承的句柄由父進(jìn)程通過(guò)命令行方式或者進(jìn)程間通訊(IPC)方式由父進(jìn)程傳遞給它。
dwCreationFlags表示創(chuàng )建進(jìn)程的優(yōu)先級類(lèi)別和進(jìn)程的類(lèi)型。創(chuàng )建進(jìn)程的類(lèi)型分控制臺進(jìn)程、調試進(jìn)程等;優(yōu)先級類(lèi)別用來(lái)控制進(jìn)程的優(yōu)先級別,分Idle、Normal、High、Real_time四個(gè)類(lèi)別。
lpEnviroment指向環(huán)境變量塊,環(huán)境變量可以被子進(jìn)程繼承。
lpCurrentDirectory指向表示當前目錄的字符串,當前目錄可以繼承。
lpStartupInfo指向StartupInfo結構,控制進(jìn)程的主窗口的出現方式。
lpProcessInformation指向PROCESS_INFORMATION結構,用來(lái)存儲返回的進(jìn)程信息。
從其參數可以看出創(chuàng )建一個(gè)新的進(jìn)程需要指定什么信息。
從上面的解釋可以看出,一個(gè)進(jìn)程包含了很多信息。若進(jìn)程創(chuàng )建成功的話(huà),返回一個(gè)進(jìn)程信息結構類(lèi)型的指針。進(jìn)程信息結構如下:
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFORMATION;
進(jìn)程信息結構包括進(jìn)程句柄,主線(xiàn)程句柄,進(jìn)程ID,主線(xiàn)程ID。
進(jìn)程在以下情況下終止:
使用CreateThread函數創(chuàng )建線(xiàn)程,CreateThread的原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId
);
其中:
lpThreadAttributes表示創(chuàng )建線(xiàn)程的安全屬性,NT下有用。
dwStackSize指定線(xiàn)程棧的尺寸,如果為0則與進(jìn)程主線(xiàn)程棧相同。
lpStartAddress指定線(xiàn)程開(kāi)始運行的地址。
lpParameter表示傳遞給線(xiàn)程的32位的參數。
dwCreateFlages表示是否創(chuàng )建后掛起線(xiàn)程(取值CREATE_SUSPEND),掛起后調用ResumeThread繼續執行。
lpThreadId用來(lái)存放返回的線(xiàn)程ID。
進(jìn)程的每個(gè)優(yōu)先級類(lèi)包含了五個(gè)線(xiàn)程的優(yōu)先級水平。在進(jìn)程的優(yōu)先級類(lèi)確定之后,可以改變線(xiàn)程的優(yōu)先級水平。用SetPriorityClass設置進(jìn)程優(yōu)先級類(lèi),用SetThreadPriority設置線(xiàn)程優(yōu)先級水平。
Normal級的線(xiàn)程可以被除了Idle級以外的任意線(xiàn)程搶占。
以下情況終止一個(gè)線(xiàn)程:
當用TerminateProcess或者TerminateThread終止進(jìn)程或線(xiàn)程時(shí),DLL的入口函數DllMain不會(huì )被執行(如果有DLL的話(huà))。
如果希望每個(gè)線(xiàn)程都可以有線(xiàn)程局部(Thread local)的靜態(tài)存儲數據,可以使用TLS線(xiàn)程局部存儲技術(shù)。TLS為進(jìn)程分配一個(gè)TLS索引,進(jìn)程的每個(gè)線(xiàn)程通過(guò)這個(gè)索引存取自己的數據變量的拷貝。
TLS對DLL是非常有用的。當一個(gè)新的進(jìn)程使用DLL時(shí),在DLL入口函數DllMain中使用TlsAlloc分配TLS索引,TLS索引就作為進(jìn)程私有的全局變量被保存;以后,當該進(jìn)程的新的線(xiàn)程使用DLL時(shí)(Attahced to DLL),DllMain給它分配動(dòng)態(tài)內存并且使用TlsSetValue把線(xiàn)程私有的數據按索引保存。DLL函數可以使用TlsGetValue按索引讀取調用線(xiàn)程的私有數據。
TLS函數如下:
在進(jìn)程或DLL初始化時(shí)調用,并且把返回值(索引值)作為全局變量保存。
DWORD dwTlsIndex, //TLS index to set value for
LPVOID lpTlsValue //value to be stored
);
其中:
dwTlsIndex是TlsAlloc分配的索引。
lpTlsValue是線(xiàn)程在TLS槽中存放的數據指針,指針指向線(xiàn)程要保存的數據。
線(xiàn)程首先分配動(dòng)態(tài)內存并保存數據到此內存中,然后調用TlsSetValue保存內存指針到TLS槽。
DWORD dwTlsIndex // TLS index to retrieve value for
);
其中:
dwTlsIndex是TlsAlloc分配的索引。
當要存取保存的數據時(shí),使用索引得到數據指針。
DWORD dwTlsIndex // TLS index to free
);
其中:
dwTlsIndex是TlsAlloc分配的索引。
當每一個(gè)線(xiàn)程都不再使用局部存儲數據時(shí),線(xiàn)程釋放它分配的動(dòng)態(tài)內存。在TLS索引不再需要時(shí),使用TlsFree釋放索引。
同步可以保證在一個(gè)時(shí)間內只有一個(gè)線(xiàn)程對某個(gè)資源(如操作系統資源等共享資源)有控制權。共享資源包括全局變量、公共數據成員或者句柄等。同步還可以使得有關(guān)聯(lián)交互作用的代碼按一定的順序執行。
Win32提供了一組對象用來(lái)實(shí)現多線(xiàn)程的同步。
這些對象有兩種狀態(tài):獲得信號(Signaled)或者沒(méi)有或則信號(Not signaled)。線(xiàn)程通過(guò)Win32 API提供的同步等待函數(Wait functions)來(lái)使用同步對象。一個(gè)同步對象在同步等待函數調用時(shí)被指定,調用同步函數地線(xiàn)程被阻塞(blocked),直到同步對象獲得信號。被阻塞的線(xiàn)程不占用CPU時(shí)間。
同步對象有:Critical_section(關(guān)鍵段),Event(事件),Mutex(互斥對象),Semaphores(信號量)。
下面,解釋怎么使用這些同步對象。
首先,定義一個(gè)關(guān)鍵段對象cs:
CRITICAL_SECTION cs;
然后,初始化該對象。初始化時(shí)把對象設置為NOT_SINGALED,表示允許線(xiàn)程使用資源:
InitializeCriticalSection(&cs);
如果一段程序代碼需要對某個(gè)資源進(jìn)行同步保護,則這是一段關(guān)鍵段代碼。在進(jìn)入該關(guān)鍵段代碼前調用EnterCriticalSection函數,這樣,其他線(xiàn)程都不能執行該段代碼,若它們試圖執行就會(huì )被阻塞。
完成關(guān)鍵段的執行之后,調用LeaveCriticalSection函數,其他的線(xiàn)程就可以繼續執行該段代碼。如果該函數不被調用,則其他線(xiàn)程將無(wú)限期的等待。
首先,調用CreateEvent函數創(chuàng )建一個(gè)事件對象,該函數返回一個(gè)事件句柄。然后,可以設置(SetEvent)或者復位(ResetEvent)一個(gè)事件對象,也可以發(fā)一個(gè)事件脈沖(PlusEvent),即設置一個(gè)事件對象,然后復位它。復位有兩種形式:自動(dòng)復位和人工復位。在創(chuàng )建事件對象時(shí)指定復位形式。。
自動(dòng)復位:當對象獲得信號后,就釋放下一個(gè)可用線(xiàn)程(優(yōu)先級別最高的線(xiàn)程;如果優(yōu)先級別相同,則等待隊列中的第一個(gè)線(xiàn)程被釋放)。
人工復位:當對象獲得信號后,就釋放所有可利用線(xiàn)程。
最后,使用CloseHandle銷(xiāo)毀創(chuàng )建的事件對象。
首先,調用CreateMutex創(chuàng )建互斥對象;然后,調用等待函數,可以的話(huà)利用關(guān)鍵資源;最后,調用RealseMutex釋放互斥對象。
互斥對象可以在進(jìn)程間使用,但關(guān)鍵段對象只能用于同一進(jìn)程的線(xiàn)程之間。
在Win32中,信號量的數值變?yōu)?時(shí)給以信號。在有多個(gè)資源需要管理時(shí)可以使用信號量對象。
首先,調用CreateSemaphore創(chuàng )建一個(gè)信號量;然后,調用等待函數,如果允許的話(huà),則利用關(guān)鍵資源;最后,調用RealeaseSemaphore釋放信號量對象。
文件句柄(FILE HANDLES)
命名管道句柄(NAMED PIPE HANDELS)
控制臺輸入緩沖區句柄(CONSOLE INPUT BUFFER HANDLES)
通訊設備句柄(COMMUNICTION DEVICE HANDLES)
進(jìn)程句柄(PROCESS HANDLES)
線(xiàn)程句柄(THREAD HANDLES)
例如,當一個(gè)進(jìn)程或線(xiàn)程結束時(shí),進(jìn)程或線(xiàn)程句柄獲得信號,等待該進(jìn)程或者線(xiàn)程結束的線(xiàn)程被釋放。
Win32提供了一組等待函數用來(lái)讓一個(gè)線(xiàn)程阻塞自己的執行。等待函數分三類(lèi):
這類(lèi)函數包括:
SignalObjectAndWait
WaitForSingleObject
WaitForSingleObjectEx
函數參數包括同步對象的句柄和等待時(shí)間等。
在以下情況下等待函數返回:
同步對象獲得信號時(shí)返回;
等待時(shí)間達到了返回:如果等待時(shí)間不限制(Infinite),則只有同步對象獲得信號才返回;如果等待時(shí)間為0,則在測試了同步對象的狀態(tài)之后馬上返回。
這類(lèi)函數包括:
WaitForMultipleObjects
WaitForMultipleObjectsEx
MsgWaitForMultipleObjects
MsgWaitForMultipleObjectsEx
函數參數包括同步對象的句柄,等待時(shí)間,是等待一個(gè)還是多個(gè)同步對象等等。
在以下情況下等待函數返回:
一個(gè)或全部同步對象獲得信號時(shí)返回(在參數中指定是等待一個(gè)或多個(gè)同步對象);
等待時(shí)間達到了返回:如果等待時(shí)間不限制(Infinite),則只有同步對象獲得信號才返回;如果等待時(shí)間為0,則在測試了同步對象的狀態(tài)之后馬上返回。
這類(lèi)函數包括:
MsgWaitForMultipleObjectsEx
SignalObjectAndWait
WaitForMultipleObjectsEx
WaitForSingleObjectEx
這些函數主要用于重疊(Overlapped)的I/O(異步I/O)。
在Win32 API的基礎之上,MFC提供了處理線(xiàn)程的類(lèi)和函數。處理線(xiàn)程的類(lèi)是CWinThread,函數是AfxBeginThread、AfxEndThread等。
表5-6解釋了CWinThread的成員變量和函數。
CWinThread是MFC線(xiàn)程類(lèi),它的成員變量m_hThread和m_hThreadID是對應的Win32線(xiàn)程句柄和線(xiàn)程ID。
MFC明確區分兩種線(xiàn)程:用戶(hù)界面線(xiàn)程(User interface thread)和工作者線(xiàn)程(Worker thread)。用戶(hù)界面線(xiàn)程一般用于處理用戶(hù)輸入并對用戶(hù)產(chǎn)生的事件和消息作出應答。工作者線(xiàn)程用于完成不要求用戶(hù)輸入的任務(wù),如耗時(shí)計算。
Win32 API并不區分線(xiàn)程類(lèi)型,它只需要知道線(xiàn)程的開(kāi)始地址以便它開(kāi)始執行線(xiàn)程。MFC為用戶(hù)界面線(xiàn)程特別地提供消息泵來(lái)處理用戶(hù)界面的事件。CWinApp對象是用戶(hù)界面線(xiàn)程對象的一個(gè)例子,CWinApp從類(lèi)CWinThread派生并處理用戶(hù)產(chǎn)生的事件和消息。
通過(guò)以下步驟創(chuàng )建一個(gè)用戶(hù)界面線(xiàn)程:
程序員不必從CWinThread派生新的線(xiàn)程類(lèi),只需要提供一個(gè)控制函數,由線(xiàn)程啟動(dòng)后執行該函數。
然后,使用AfxBeginThread創(chuàng )建MFC線(xiàn)程對象和Win32線(xiàn)程對象。如果創(chuàng )建線(xiàn)程時(shí)沒(méi)有指定CREATE_SUSPENDED(創(chuàng )建后掛起),則創(chuàng )建的新線(xiàn)程開(kāi)始執行。
如果創(chuàng )建線(xiàn)程是指定了CREATE_SUSPENDED,則在適當的地方調用函數ResumeThread開(kāi)始執行線(xiàn)程。
雖然程序員沒(méi)有從CWinThread派生類(lèi),但是MFC給工作者線(xiàn)程提供了缺省的CWinThread對象。
用戶(hù)界面線(xiàn)程和工作者線(xiàn)程都是由AfxBeginThread創(chuàng )建的?,F在,考察該函數:MFC提供了兩個(gè)重載版的AfxBeginThread,一個(gè)用于用戶(hù)界面線(xiàn)程,另一個(gè)用于工作者線(xiàn)程,分別有如下的原型和過(guò)程:
用戶(hù)界面線(xiàn)程的AfxBeginThread的原型如下:
CWinThread* AFXAPI AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs)
其中:
參數1是從CWinThread派生的RUNTIME_CLASS類(lèi);
參數2指定線(xiàn)程優(yōu)先級,如果為0,則與創(chuàng )建該線(xiàn)程的線(xiàn)程相同;
參數3指定線(xiàn)程的堆棧大小,如果為0,則與創(chuàng )建該線(xiàn)程的線(xiàn)程相同;
參數4是一個(gè)創(chuàng )建標識,如果是CREATE_SUSPENDED,則在懸掛狀態(tài)創(chuàng )建線(xiàn)程,在線(xiàn)程創(chuàng )建后線(xiàn)程掛起,否則線(xiàn)程在創(chuàng )建后開(kāi)始線(xiàn)程的執行。
參數5表示線(xiàn)程的安全屬性,NT下有用。
工作者線(xiàn)程的AfxBeginThread的原型如下:
CWinThread* AFXAPI AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs)
其中:
參數1指定控制函數的地址;
參數2指定傳遞給控制函數的參數;
參數3、4、5分別指定線(xiàn)程的優(yōu)先級、堆棧大小、創(chuàng )建標識、安全屬性,含義同用戶(hù)界面線(xiàn)程。
不論哪個(gè)AfxBeginThread,首先都是創(chuàng )建MFC線(xiàn)程對象,然后創(chuàng )建Win32線(xiàn)程對象。在創(chuàng )建MFC線(xiàn)程對象時(shí),用戶(hù)界面線(xiàn)程和工作者線(xiàn)程的創(chuàng )建分別調用了不同的構造函數。用戶(hù)界面線(xiàn)程是從CWinThread派生的,所以,要先調用派生類(lèi)的缺省構造函數,然后調用CWinThread的缺省構造函數。圖8-1中兩個(gè)構造函數所調用的CommonConstruct是MFC內部使用的成員函數。

MFC使用CWinThread::CreateThread創(chuàng )建線(xiàn)程,不論對工作者線(xiàn)程或用戶(hù)界面線(xiàn)程,都指定線(xiàn)程的入口函數是_AfxThreadEntry。_AfxThreadEntry調用AfxInitThread初始化線(xiàn)程。
CreateThread和_AfxThreadEntry在線(xiàn)程的創(chuàng )建過(guò)程中使用同步手段交互等待、執行。CreateThread由創(chuàng )建線(xiàn)程執行,_AfxThreadEntry由被創(chuàng )建的線(xiàn)程執行,兩者通過(guò)兩個(gè)事件對象(hEvent和hEvent2)同步:
在創(chuàng )建了新線(xiàn)程之后,創(chuàng )建線(xiàn)程將在hEvent事件上無(wú)限等待直到新線(xiàn)程給出創(chuàng )建結果;新線(xiàn)程在創(chuàng )建成功或者失敗之后,觸發(fā)事件hEvent讓父線(xiàn)程運行,并且在hEven2上無(wú)限等待直到父線(xiàn)程退出CreateThread函數;父線(xiàn)程(創(chuàng )建線(xiàn)程)因為hEvent的置位結束等待,繼續執行,退出CreateThread之前觸發(fā)hEvent2事件;新線(xiàn)程(子線(xiàn)程)因為hEvent2的置位結束等待,開(kāi)始執行控制函數(工作者線(xiàn)程)或者進(jìn)入消息循環(huán)(用戶(hù)界面線(xiàn)程)。
MFC在線(xiàn)程創(chuàng )建中使用了如下數據結構:
struct _AFX_THREAD_STARTUP
{
//傳遞給線(xiàn)程啟動(dòng)的參數(IN)
_AFX_THREAD_STATE* pThreadState;//父線(xiàn)程的線(xiàn)程狀態(tài)
CWinThread* pThread; //新創(chuàng )建的MFC線(xiàn)程對象
DWORD dwCreateFlags; //線(xiàn)程創(chuàng )建標識
_PNH pfnNewHandler; //新線(xiàn)程的句柄
HANDLE hEvent; //同步事件,線(xiàn)程創(chuàng )建成功或失敗后置位
HANDLE hEvent2; //同步事件,新線(xiàn)程恢復執行后置位
//返回給創(chuàng )建線(xiàn)程的參數,在新線(xiàn)程恢復執行后賦值
BOOL bError; //如果創(chuàng )建發(fā)生錯誤,TRUE
};
該結構作為線(xiàn)程開(kāi)始函數的參數被傳遞給_beginthreadex函數來(lái)創(chuàng )建和啟動(dòng)線(xiàn)程。_beginthreadex函數是“C”的線(xiàn)程創(chuàng )建函數,具有如下原型:
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr );
圖8-2描述了上述過(guò)程。圖中表示,_AfxThreadEntry在啟動(dòng)線(xiàn)程時(shí),將創(chuàng )建本線(xiàn)程的線(xiàn)程狀態(tài),并且繼承父線(xiàn)程的模塊狀態(tài)。關(guān)于MFC狀態(tài),見(jiàn)第9章。


從圖8-2可以看出,AfxEndThread用來(lái)結束調用它的線(xiàn)程:它將清理本線(xiàn)程創(chuàng )建的MFC對象和釋放線(xiàn)程局部存儲分配的內存空間;調用CWinThread的虛擬函數Delete;調用“C”的結束線(xiàn)程函數_endthreadex釋放分配給線(xiàn)程的資源,但是不關(guān)閉線(xiàn)程句柄。
CWinThread::Delete的缺省實(shí)現是:如果本線(xiàn)程的成員函數m_bDelete為T(mén)RUE,則調用“C”運算符號delete銷(xiāo)毀MFC線(xiàn)程對象自身(delete this),這將導致線(xiàn)程對象的析構函數被調用。若析構函數檢測線(xiàn)程句柄非空則調用CloseHandle關(guān)閉它。
通常,讓m_bDelete為T(mén)RUE以便自動(dòng)地銷(xiāo)毀線(xiàn)程對象,釋放內存空間(MFC內存對象在堆中分配)。但是,有時(shí)候,在線(xiàn)程結束之后(Win32線(xiàn)程已經(jīng)不存在)保留MFC線(xiàn)程對象是有用的,當然程序員自己最后要記得銷(xiāo)毀該線(xiàn)程對象。
在MFC中,消息循環(huán)是由線(xiàn)程完成的。一般地,可以使用MFC缺省的消息循環(huán)(即使用函數CWindThrad::Run),但是,有些時(shí)候需要程序員自己實(shí)現一個(gè)線(xiàn)程的消息循環(huán),比如在用戶(hù)界面線(xiàn)程進(jìn)行一個(gè)長(cháng)時(shí)間計算處理或者等待另一個(gè)線(xiàn)程時(shí)。一般有如下形式:
while ( bDoingBackgroundProcessing)
{
MSG msg;
while ( ::PeekMessage( &msg, NULL,0, 0, PM_NOREMOVE ) )
{
if ( !PumpMessage( ) )
{
bDoingBackgroundProcessing = FALSE;
::PostQuitMessage( );
break;
}
}
// let MFC do its idle processing
LONG lIdle = 0;
while ( AfxGetApp()->OnIdle(lIdle++ ) );
// Perform some background processing here
// using another call to OnIdle
}
該段代碼的解釋參見(jiàn)圖5-3對線(xiàn)程的Run函數的圖解。
程序員實(shí)現線(xiàn)程的消息循環(huán)有兩個(gè)好處,一是顧及了MFC的Idle處理機制;二是在長(cháng)時(shí)間的處理中可以響應用戶(hù)產(chǎn)生的事件或者消息。
在同步對象上等待其他線(xiàn)程時(shí),也可以使用同樣的方式,只要把條件
bDoingBackgroundProcessing
換成如下形式:
WaitForSingObject(hHandleOfEvent,0) == WAIT_TIMEOUT
即可。
MFC處理線(xiàn)程和進(jìn)程時(shí)還引入了一個(gè)重要的概念:狀態(tài),如線(xiàn)程狀態(tài)(Thread State)、進(jìn)程狀態(tài)(Process State)、模塊狀態(tài)(Module State)等。由于這個(gè)概念在MFC中占有重要地位,涉及的內容比較多,所以專(zhuān)門(mén)在下一章來(lái)講述它。
聯(lián)系客服