OpenSSL多线程互斥的解决方案–一种新的锁

自己曾经将基于传统套接字的通信程序修改为了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 */

}

#endif

...

}

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);

#endif

}

void _LeaveCriticalSection( CCriticalSection &cs ,char * szFile, int nLine )

{

cs.Unlock();

#ifdef _TEST_VERSION

g_nDebug--;

printf("/nLeaveCriticalSection:%d/nFile:%s Line:%d/n/n",g_nDebug,szFile,nLine);

#endif

}

函数声明中用引用是因为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的时候,就意味着有释放操作在临界区,就需要加锁,否则直通。

cs.Lock();

return TRUE;

}

else if( g_iCount )

{

cs.Lock();

return TRUE;

}

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 )

{

cs.Unlock();

if ( bShouldPro ) //对于释放操作,需要递减全局变量

{

g_iCount--;

}

}

在介绍使用之前,简单说一下为何将递增和递减全局变量的操作放到加锁和开锁的外面,这个全局变量相当于一位观察者,或者说一个第三方的证人,它必然需要先于竞争的双方到场,如果将对该全局变量的操作放到加锁和开锁的里面,那么就会在加锁和递增全局变量之间留下空隙,竞争的读方或者写方就有可能先于证人到场,这样很容易被钻空子,引发事故,所谓钻空子就是钻进了入了无人监控的区域,没有证人是危险的...至于这个锁的使用非常简单,如果是读方或者写方就是如下加锁和解锁:

_EnterCriticalSection_v2(g_SSL_MUTEX,false,__FILE__,__LINE__);

_LeaveCriticalSection_v2(g_SSL_MUTEX,false,__FILE__,__LINE__);

如果是引发不公平的释放操作,那么只需要将false换成true就可以了。

这个锁不仅仅可以用到这个场景,任何复杂的三角恋爱关系的场景都可以使用。

原文链接: https://blog.csdn.net/dog250/article/details/5303379

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    OpenSSL多线程互斥的解决方案--一种新的锁

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/410334

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午11:57
下一篇 2023年4月26日 上午11:57

相关推荐