本帖最后由 yunh 于 2014-3-5 17:26 编辑
前阵子看ACE 5.4.1 源码,看到 ACE 自带的一个名为Token library 的 subset,可以检测死锁,感觉真是很好很强大,但没想到实际上这个子系统的功能远远超过了模拟一个锁系统的能力——它实际上是一个分布式锁系统的一部分。一个核心的锁服务(Token Service)外加若干锁的客户端组成了这一切,锁服务本身并不是分布式的(目前还没有强大到这种地步),但使用锁的客户端可以不必位于一台机器上,对于某些分布式系统,就会比较有用。这篇文章就详细介绍如何使用锁服务来实现分布式系统的加锁保护。
首先是编译 ACE 库,需要在 config.h 中加入下面的宏定义,需要确保它是在包含相应平台头文件之前被定义:
- #define ACE_HAS_TOKENS_LIBRARY
- #define MAXHOSTNAMELEN 256
- #include "ace/config-win32.h"
复制代码 Tokens library 默认情况下是不编译的,只有加入 ACE_HAS_TOKENS_LIBRARY 才会生成代码,一方面锁的客户就可以使用 ACE_Local_Mutex/ACE_Remote_Mutex 这些类;另一方面锁服务也可以正确编译。
接着编译 ACE 分布式锁服务 netsvcs,它本身是 ACE 提供的,位于 ACE_Wrappers 目录下,与 apps 不同,netsvcs 包含的是常用的一些网络方面的功能,例如日志服务、时间服务、命名服务及锁服务,这些功能与 ace 实现某项功能紧密相关,例如我们都知道 ACE 的日志(ACE_DEBUG之类)是可以被重定向的,其中一种方式就是发往日志服务器,但是在 ace 目录中并没有看到服务器端的代码,其实它就在这里;同理,如果你只在一个进程内使用 Tokens library,那直接使用 ace.dll 就足够了,但如果想使用跨进程、跨机器的锁功能,就必需使用 netsvcs,可见它的地位是相当重要。
netsvcs 被设计成一个库,是不能直接运行的,但 ACE 同时提供了一个驱动它的项目,使用 ACE 的 Service Configration 框架,无缝的将这些服务加载并启动起来。首先是编译 netsvcs,我这个版本(5.4.1)居然有几个明显的编译错误,需要在这里提醒一下,如果你的版本可以直接编译通过,就可以略过不看。
1.Token_Handler.h(line:33) class ACE_Svc_Export ACE_Token_Handler : .... 报 ACE_Svc_Export 不识别,原因是缺少头文件包含,在同一文件加入下面的include就好了:- #include "ace/svc_export.h"
复制代码 2.Token_Handler.cpp(line:35) this->service_port_ = ACE_DEFAULT_SERVER_PORT; ... 报 service_port_ 未定义,发现类中并无该成员,而它只是起到局部变量的作用,所以改为局部变量,名称修改为 service_port:- u_short service_port = ACE_DEFAULT_SERVER_PORT;
复制代码 其余各处引用重命名即可。
3.为了可以观察到死锁的情况,需要在 ACE_Token_Acceptor::init 中加入下面代码:- if(ACE::debug())
- ACE_Token_Manager::instance()->debug(1);
复制代码 需要相应的头文件:- #include "ace/Token_Manager.h"
复制代码 ACE::debug() 返回的值则可以通过后面介绍的命令行参数来控制。
基本就是这些,编译生成 netsvcs.dll 与 main.exe,它们均位于 netsvcs/bin 目录中。接着修改配置文件,让 main 进程在启动时加载 Token Service,将默认的 svc.conf 复制一份重命名为 server.conf,打开后内容如下:- dynamic Logger Service_Object * ACE:_make_ACE_Logging_Strategy() "-s foobar -f STDERR|OSTREAM|VERBOSE"
- dynamic Time_Service Service_Object * netsvcs:_make_ACE_TS_Server_Acceptor() "-p 20222"
- dynamic Name_Server Service_Object * netsvcs:_make_ACE_Name_Acceptor() "-p 20012"
- # This service is disabled by default -- only enable it ACE is compiled with ACE_HAS_TOKENS_LIBRARY.
- # dynamic Token_Service Service_Object * netsvcs:_make_ACE_Token_Acceptor() "-p 10202"
- dynamic Server_Logging_Service Service_Object * netsvcs:_make_ACE_Server_Logging_Acceptor() active "-p 20009"
- dynamic Thr_Server_Logging_Service Service_Object * netsvcs:_make_ACE_Thr_Server_Logging_Acceptor() active "-p 20020"
- dynamic Client_Logging_Service Service_Object * netsvcs:_make_ACE_Client_Logging_Acceptor() active "-p 20009"
复制代码 其中注释的一句正是我们要用的 Token Service,因为 ACE 默认不生成它,所以也就默认不启动它,这里简单的将其解除注释即可,记住启动端口是10202。再编写一个启动脚本(run.bat),用来指定服务配置文件为 server.conf,且开启调试输出:- main.exe -d -f server.conf
- pause
复制代码 上面的脚本是基于 Windows 的,其它平台参数是一致的。开启调试的分布式锁服务启动时输出如下:- E:\ACE+TAO+CIAO-5.4.1\ACE_wrappers\netsvcs\bin>main.exe -d -f server.conf
- starting up daemon main.exe
- ACE_DLL_Handle::open: calling dlopen on "netsvcs"
- ACE_DLL_Handle::open: loading netsvcs (1966080)
- ACE_DLL_Handle::open: loading netsvcs (1966080)
- opening dynamic service Token_Service
- starting up Token Server at port 10202 on handle 200
- did dynamic on Token_Service, error = 0
复制代码 包含 Token Server 一句的输出表示服务正常启动。下面就可以看客户端代码如何编写了,这里给出一个例子:- #include "stdafx.h"
- #include "ace/Remote_Tokens.h"
- int ACE_TMAIN(int argc, ACE_TCHAR* argv[])
- {
- if(argc < 2)
- {
- ACE_DEBUG((LM_DEBUG, "need token names!\n"));
- return -1;
- }
- char* token_name = argv[1];
- ACE_Remote_Mutex mutex(token_name, 0, 1);
- ACE_TSS_Connection::set_server_address(ACE_INET_Addr(10202, ACE_LOCALHOST));
- int result = mutex.acquire();
- if(result == 0)
- {
- ACE_DEBUG((LM_DEBUG, "(%P/%t) acquire mutex!\n"));
- ACE_OS::sleep(10);
- mutex.release();
- }
- else
- ACE_DEBUG((LM_DEBUG, "%p\n", "acquire lock"));
- return 0;
- }
复制代码 客户端在命令行上需要一个锁名称参数,之后使用远程锁(Remote_Mutex)分别进行加锁、解锁操作,其中夹着一个sleep(10)调用,也就是说,如果我启动两个这样的进程,当一个获得锁陷入sleep后,另一个将在mutex.acquire()处阻塞,直到前一个调用mutex.release()为止,实际运行的情况恰好如此,可以参看图片附件:
运行情景:进程 A 获得锁后将同样想获得锁的进程 B 阻塞
调用命令行内容如下:- start ./remote.exe "A"
- start ./remote.exe "A"
复制代码 此时锁服务也有相应调试输出,可以看到获得锁的过程:- (1244) acquired A
- (1244) waiting for A, owner is /YUNHAI/2404/4268, total waiters == 2
- (1244) released A, owner is /YUNHAI/3028/3124, total waiters == 1
- recv failed: 远程主机强迫关闭了一个现有的连接。 got -1 bytes, expected 4 bytes
- collection releasing A
- (1244) released A, owner is /YUNHAI/3028/3124, total waiters == 1
- (1244) released A, owner is no owner, total waiters == 0
- recv failed: 远程主机强迫关闭了一个现有的连接。 got -1 bytes, expected 4 bytes
- collection releasing A
- (1244) release failed: Permission denied.
复制代码 每行前面的数字应当是当前线程ID,在分布式情况下没有什么意义,因为锁服务只有一个线程,且不是获取锁的线程,而后面输出的owner is才是真正的客户端代号(机器名/进程ID/线程ID)。
总结一下,ACE 提供的这个东西确实可用,但是有些小问题,例如当网络连接中断时,线程对应的锁会自动被放弃,而线程自己并不知道,其它线程此时可以继续获取这个锁,从而造成竞态条件,如果是工业级别的话,可能还需要加入自动重连的功能,不过总的来说,还是相当开眼界的,如果 ACE 标明这些代码的初始版本是从哪里得来的就更好了。
|