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

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介:

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

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



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273973

相关文章
|
9月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
455 1
|
安全 Java 编译器
线程安全问题和锁
本文详细介绍了线程的状态及其转换,包括新建、就绪、等待、超时等待、阻塞和终止状态,并通过示例说明了各状态的特点。接着,文章深入探讨了线程安全问题,分析了多线程环境下变量修改引发的数据异常,并通过使用 `synchronized` 关键字和 `volatile` 解决内存可见性问题。最后,文章讲解了锁的概念,包括同步代码块、同步方法以及 `Lock` 接口,并讨论了死锁现象及其产生的原因与解决方案。
418 10
线程安全问题和锁
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
415 2
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
1344 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
319 6
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
运维 API 计算机视觉
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
360 2

热门文章

最新文章