自己曾經将基于傳統套接字的通信程式修改為了SSL的套接字程式,可是卻在運作中出了問題,具體就是在SSL_write的地方遇到了NULL指針,不是SSL為NULL了,而是其中的一個字段為NULL但是在SSL_write中卻用到了,是以就出錯了,究其原因,罪魁禍首就是多線程,openssl的文檔上也明文規定不能将一個SSL指針用于多個線程,可是我們程式的需求必須用于多個線程,在庫的實作與我們的需求沖突時,必然是修改我們的代碼,這也正是展現了當機制和政策沖突時,修改政策而不是機制。openssl中的SSL指針基本上就是在單線程中使用的,它的代碼絲毫沒有任何多線程的互斥:
void SSL_free(SSL *s)
{
int i;
if(s == NULL)
return;
i=CRYPTO_add(&s->references,-1,CRYPTO_LOCK_SSL); //這個reference看似是保護用的,其實呢?
#ifdef REF_PRINT
REF_PRINT("SSL",s);
#endif
if (i > 0) return;
#ifdef REF_CHECK
if (i < 0)
fprintf(stderr,"SSL_free, bad reference count/n");
abort(); /* ok */
}
...
int SSL_write(SSL *s,const void *buf,int num)
if (s->handshake_func == 0)
SSLerr(SSL_F_SSL_WRITE, SSL_R_UNINITIALIZED);
return -1;
if (s->shutdown & SSL_SENT_SHUTDOWN)
s->rwstate=SSL_NOTHING;
SSLerr(SSL_F_SSL_WRITE,SSL_R_PROTOCOL_IS_SHUTDOWN);
return(-1);
return(s->method->ssl_write(s,buf,num));
我們看看會引起問題的調用:
1:
if ( ssl == NULL || (ssl != NULL&&ssl->references == 0) )
ssl = NULL;
return -3;
nBytes=SSL_write(ssl, (const char*)(pEncryptData+nSendBytes), nEncryptDataLen-nSendBytes);
2:
SSL_free (k->ssl);
SSL *ssl = k->ssl;
k->ssl = NULL; //這一步做的很好,防止野指針,但是...
以上的兩個場景中的一個ssl和k->ssl指向的是同一個ssl,但是它們本身卻在不同的線程中,那麼雖然在場景2中有k->ssl的置空,但是另外一個線程的ssl指針卻安然無恙,是以在SSL_write之前的判斷中就隻能依賴ssl != NULL&&ssl->references == 0這個判定了,然後在其為真的情況下将該線程的ssl指針置空,這樣的話,看似嚴密的空指針檢測機制就弱了很多,而會引起問題的恰恰就是這個弱掉的部分,除此之外在單cpu上如果場景2在判斷之後SSL_write之前被中斷,然後排程場景1的線程執行SSL_free,然後當場景2的線程再回來的時候直接執行SSL_write,那麼結果可想而知,s->handshake_func == 0 的判斷雖然使得出事的機率減到了最小,但是畢竟還是有的,現在考慮多cpu情況,兩個場景在不同的cpu上同時運作,ssl的free函數将ssl指針釋放掉了一半的時候,恰恰能使得場景2的判斷通過,那麼接下來出事的機率會大很多,不光是write函數,read函數也一樣,沒有考慮多線程的指針同步保護,于是最魯莽的方式就是加入全局的臨界區或者互斥鎖,所有的ssl公用一個,但是這樣很容易死鎖,因為不會構成競争的兩個ssl指針會互相依賴,比如一個read依賴一個write,這樣一來接下來的解決方案就是一個通信通道一個鎖,但是實作起來非常複雜,于是最終的方式就是實作一個新的鎖機制。
openssl最令我欣賞的兩點,一個是openssl的棧式的過濾鈎子非常靈活,第二就是什麼事情都留給我做,太機制化了。以下這個是最原始的會導緻死鎖的實作:
static int g_nDebug=0;
void _EnterCriticalSection( CCriticalSection &cs ,char * szFile, int nLine )
cs.Lock();
#ifdef _TEST_VERSION
g_nDebug++;
printf("/nEnterCriticalSection:%d/nFile:%s Line:%d/n/n",g_nDebug,szFile,nLine);
void _LeaveCriticalSection( CCriticalSection &cs ,char * szFile, int nLine )
cs.Unlock();
g_nDebug--;
printf("/nLeaveCriticalSection:%d/nFile:%s Line:%d/n/n",g_nDebug,szFile,nLine);
函數聲明中用引用是因為CCriticalSection類的拷貝構造函數是私有的,而c/c++函數調用都是傳值的,是以當有對象作為參數時會調用拷貝構造函數,是以需要引用标記&,如果需要使用互斥,無論是SSL_read,SSL_write還是SSL_free,隻要是有ssl指針作為參數的調用裡,都可以在使用之前調用:
_EnterCriticalSection(g_SSL_MUTEX,__FILE__,__LINE__);
使用時候或者異常退出/傳回時調用:
_LeaveCriticalSection(g_SSL_MUTEX,__FILE__,__LINE__);
這裡的g_SSL_MUTEX就是那個全局的家夥!這樣毫無理智的調用肯定會引起死鎖的,比如read和write在不同線程的互鎖。之是以不直接使用cs.Lock()和cs.Unlock()而是将其封裝就是因為可以不必查找并修改很多東西隻需要修改實作就可以了,另外的好處就是可以輕松實作列印調試資訊。這裡初始版本的使用非常簡單,就是如上的兩個調用,在介紹我最終使用的版本之前,首先我想談一下關于網絡通信程式的個人想法,網絡通信程式一般不用多處理,也就是不宜使用多cpu共同處理,不知這是通信程式的本質決定的還是當初設計協定棧時的曆史原因,不管怎樣,我認為通信意味着點對點的聯系,即使是多點傳播也是n多個點對點的聯合,每一對節點就是一條虛拟連接配接,也就是一條虛電路,虛電路意味着該條虛電路的獨占性,既然獨占就不能有太多的競态,而一直以來的馮諾依曼體系的多處理意味着更多的競态,是以考慮一下tcp/ip的實作以及網卡驅動,一般需要将網卡和cpu綁定,這樣帶來的效率更高。網際網路通信和并行處理是這個世界目前的主角,然而看起來它們對對方并不友好,其實并不是這樣的,它們完全可以配合的很好,并行化的多處理僅僅展現在其資料加工上而不是協定棧中和資料流中。
好了,最後我們來看一下我的最終的鎖,這個鎖設計起來并沒有費多大周折,考慮需要互斥的雙方,讀和釋放以及寫和釋放,而讀和寫并不需要互斥,如此一來就不得不引入一個新的自由變量用于區分互斥的雙方是誰,如果它們需要互斥就鎖定,否則就直通。既然都需要和釋放互斥,那麼釋放這個操作顯得要比讀和寫特殊一些了,是以可以猜想讀和寫在形式上并不做區分,而都需要和釋放區分開來,于是就有了下面的設計:
static int g_iCount = 0; //引入一個變量來表示目前是否需要讀或者寫互斥
#define _EnterCriticalSection_v2( cs , bShouldPro, szFile, nLine ) /
bool bShouldUnlock = FALSE; / //定義成宏的形式就是在于我不想再引入全局變量了,宏可以定義局部變量,使用之。
bShouldUnlock = _EnterCriticalSection_v2__( cs, bShouldPro, szFile, nLine );
bool _EnterCriticalSection_v2__( CCriticalSection &cs , bool bShouldPro, char * szFile, int nLine )
if( bShouldPro ) //對于釋放操作,就是需要加鎖的操作 {
g_iCount++; //先遞增這個全局變量,當讀或者寫檢測到這個變量不為0的時候,就意味着有釋放操作在臨界區,就需要加鎖,否則直通。
return TRUE;
else if( g_iCount )
return FALSE;
#define _LeaveCriticalSection_v2( cs , bShouldPro, szFile, nLine ) /
if (bShouldUnlock) / //使用局部變量
_LeaveCriticalSection_v2__(cs , bShouldPro, szFile, nLine);
void _LeaveCriticalSection_v2__( CCriticalSection &cs , bool bShouldPro, char * szFile, int nLine )
if ( bShouldPro ) //對于釋放操作,需要遞減全局變量
g_iCount--;
在介紹使用之前,簡單說一下為何将遞增和遞減全局變量的操作放到加鎖和開鎖的外面,這個全局變量相當于一位觀察者,或者說一個第三方的證人,它必然需要先于競争的雙方到場,如果将對該全局變量的操作放到加鎖和開鎖的裡面,那麼就會在加鎖和遞增全局變量之間留下空隙,競争的讀方或者寫方就有可能先于證人到場,這樣很容易被鑽空子,引發事故,所謂鑽空子就是鑽進了入了無人監控的區域,沒有證人是危險的...至于這個鎖的使用非常簡單,如果是讀方或者寫方就是如下加鎖和解鎖:
_EnterCriticalSection_v2(g_SSL_MUTEX,false,__FILE__,__LINE__);
_LeaveCriticalSection_v2(g_SSL_MUTEX,false,__FILE__,__LINE__);
如果是引發不公平的釋放操作,那麼隻需要将false換成true就可以了。
這個鎖不僅僅可以用到這個場景,任何複雜的三角戀愛關系的場景都可以使用。
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1273973