1引言
在内存里维护着一个保存了文件md5码的树形结构来模拟实际的文件系统,定时遍历文件系统与该树形结构,发现不同或比较存在两者的md5码,来判断文件是否发生变化。这个过程是比较复杂的,而且bug不断,所以最后弃之不用了。
有人说用ReadDirectoryChangeW来做的话会很方便,不过一来执着于只用Tcl来解决问题,再者长久没用C,在Tcl 实现文件监控也暂时搁置下来了。事情的发展总是需要一些触发,前些时候在看oratcl的源码,大致想弄清楚Tcl与Oracle之间编码转化的问题,最后问题倒是没有解决,但是对于使用C来开发Tcl模块有了初步感性上的认识。当然,尝试的对象就是文件监控了。
2Hello,World
下面就编写这个模块时的一些想法结合必要的框架,来介绍一下开发Tcl的C extension。而关于编程的学习,一般总是从Hello,World开始的,这里也不例外。例子来自于Tcl的wiki站,先列出代码:
#include<tcl.h>
static int
Hello_Cmd(ClientData cdata,Tcl_Interp*interp,
int objc,Tcl_Obj*CONST objv[])
{
Tcl_SetObjResult(interp,Tcl_NewStringObj("Hello, World!",-1));
return TCL_OK;
}
int DLLEXPORT
Hello_Init(Tcl_Interp*interp)
{
if(Tcl_InitStubs(interp,TCL_VERSION,0)== NULL){
return TCL_ERROR;
}
if(Tcl_PkgProvide(interp,"Hello","1.0")== TCL_ERROR){
return TCL_ERROR;
}
Tcl_CreateObjCommand(interp,"hello", Hello_Cmd,NULL,NULL);
return TCL_OK;
}
当Tcl调用以上代码编译而成的dll文件时会查函数PKG_Init作为模块的入口,其中PKG是模块的名字,这里就是Hello。在这个函数中,首先使用Tcl_initStubs来判断使用该模块的当前Tcl的版本。当该函数的第3个参数为1时,明确只有与TCL_VERSION一致的Tcl解析器,比如8.4版本,才能使用该模块;如果像上面例子里为0的话,则高于TCL_VERSION版本的解析器,比如8.5或者8.6也可以使用该模块。之后是一个提供模块信息的函数Tcl_PkgProvide。最后,也是最重要的部分,就是声明模块提供可用命令的函数Tcl_CreateObjCommand。另外还有一个比较旧的命令提供类似的功能,Tcl_CreateCommand,两者的区别在于,前者更好地支持参数的类型转换,所以现在一般只用前者。例子中这个命令的意思是,对于该模块中的hello命令,事实上将执行名为Hello_cmd的函数。最后,返回TCL_OK,表示成功创建
C编写Tcl文件系统监测扩展
韩奕伟,刘生远
(舟山广播电视总台,舟山316000)
摘要:ReadDirectoryChangesW是Windows系统下用于监测文件系统变化的API函数。使用Tcl的C扩展API对
其进行必要的整合与包装,分别从同步调用与异步调用两个角度介绍了如何实现阻塞与非阻塞的Tcl模块,并在此
基础上结合Tcl的时间通知机制,实现了单线程非阻塞的文件监控模式。
关键词:Tcl;ReadDirectoryChangesW;事件;异步;编码
C Programming for Tcl Extension to Monitor Directory
HAN Yiwei,LIU Shenyuan
(Zhoushan General Station of Broadcast and TV,Zhoushan316000)
Abstract:ReadDirectoryChangesW is the Windows API t o trace the changes of a certain directory monitored.A Tcl
extension of the same functionality is implemented in both ways of the synchronous and asynchronous use of the Windows
API.Finally,an event loop mechanism is integrated to this extension to provide the non-blocking monitoring operation in
one thread.
Key words:Tcl;ReadDirectoryChangesW;event;asynchronous;encoding
作者简介:韩亦伟(1960-),男,工程师,研究方向:广播发
射、电视监控。
收稿日期:2010-10-20
24
--
块。而这里的Hello_cmd也很简单,Tcl_SetObjResult设置了在Tcl中调用hello返回的结果。
3ReadDirectoryChangesW
MSDN对这个函数讲得很清楚,网上也有大量的同步监测例子。该函数对指定文件夹中进行监控,并返回相关的信息。该函数有两种调用的方式,一种是同步调用,即在一个线程里使用阻塞的方式循环查询;另一种是异步调用,函数只在发现有文件变化的时候通知线程。
BOOL WINAPI ReadDirectoryChangesW(
__in HANDLE hDirectory,
__out LPVOID lpBuffer,
__in DWORD nBufferLength,
__in BOOL bWatchSubtree,
__in DWORD dwNotifyFilter,
__out_opt LPDWORD lpBytesReturned,
__inout_opt LPOVERLAPPED lpOverlapped,
__in_opt LPOVERLAPPED_COMPLETION_ROUTINE
lpCompletionRoutine
);
第一个参数就是被监控文件夹的句柄,它必须以FILE_LIST_DIRECTORY形式来打开;第二个参数用于存储文件变化的信息;第三个参数用于指定第二个参数的大小,一般sizeof第二个参数一下行了;第四个是指是否监控子目录;第五个参数指定监控文件变化的类型,比如是否仅仅监控读操作或者写操作或者两者兼顾;第六个参数在同步监控的情况下用于设置存入第二个参数的文件变化信息的长度,而在异步调用中是NULL;第七个参数只在异步调用中使用,是异步调用的关键所在;第八个参数是异步调用时的可选参数,取决于使用者决定采用何种异步方式,倘若被设定则每次发现文件变化都会调用该参数指向的函数。另外还可以考虑IOCP,即完成端口。但考虑到完成端口的使用需要另起一个线程,并在其中循环检测IOCP的可用状态,与同步并无实质区别。倘若调用它的Tcl解析器不支持多线程,则可能无
法使用。即使可以使用多线程,也完全可以在Tcl语言层次上实现,所以在这里不考虑使用IOCP。
将该函数包装成Tcl模块,由易到难,分别是同步调用、多线程异步调用和事件通知形式调用。这里多线程的形式是C 层次上的异步结合Tcl层次上线程来实现的;而事件通知则更涉及到Tcl的事件循环API,相比较而言又多出了一个实现目标,故最复杂。
4同步调用
很长一段时间里困扰于阻塞,因此对此一直深恶痛绝,平时里一般是避之不及,这里之所以要写这个同步的例子是因为它最简单,适合说明问题。
无论为Tcl写怎么样的C Extension,首先是要写好入口函数来进行初始化,像上一章的例子Hello_Init,在这里则是Winnotify_Init。在其中将watch命令绑定到Watch_Cmd函数,具体说明一下。
static int Watch_Cmd(ClientData cdata,Tcl_Interp *interp,
int objc,Tcl_Obj*CONST objv[])
{
char*dir;
if(objc==1)
{
dir=".";
}
else if(objc==2)
{
dir=Tcl_GetStringFromObj(objv[1],NULL);
}
else
{
Tcl_WrongNumArgs(interp,1,objv,"?path?");
return TCL_ERROR;
}
HANDLE handle=CreateFile(dir,
FILE_LIST_DIRECTORY,
FILE_SHARE_READ|
FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
NULL);
char notify[1024]={0};
FILE_NOTIFY_INFORMATION*pnotify=
(FILE_NOTIFY_INFORMATION*)notify;
DWORD cbBytes;
while(ReadDirectoryChangesW(handle,
¬ify,
sizeof(notify),
true,
FILE_NOTIFY_CHANGE_LAST_WRITE|
FILE_NOTIFY_CHANGE_FILE_NAME,
&cbBytes,
NULL,NULL))
{
if(FILE_ACTION_MODIFIED==pnotify-> Action)
{
printf("%s is modified\n",pnotify-> FileName);
}
else if(FILE_ACTION_ADDED==pnotify-> Action)
{
printf("%s is added\n",pnotify->FileName);
}
else if(FILE_ACTION_REMOVED==pnotify-> Action)
{
printf("%s is removed\n",pnotify-> FileName);
25
--
}
}
return TCL_OK;
}
还是从接收Tcl参数开始,watch命令接受一个可选的参数用于指定监控的文件夹,当忽略该参数时,默认为当前路径。对应的Watch_Cmd函数的objc表示了watch命令共包含了几个词,当等于1时,表示不带可选参数;等于2时则将提取objv[1]作为指定的被监控文件夹;当参数数目不正确时,则使用Tcl_WrongNumArgs来定义一个错误信息并返回TCL_ERROR。
Tcl_WrongnumArgs的第二个参数表示在objv中顺序提取的词的数量,它们将与第四个参数结合作为返回信息。这里Tcl_WrongnumArgs(interp,1,objv,"?path?")设置的信息将是“watch?path?”。
之后打开一个文件句柄,并将它交给ReadDirectoryChan-gesW循环检测。每当检测到变化的时候,执行循环体内的命令,即根据pnotify->Action打印文件变化的情况。这部分跟Tcl完全没有关系,而且为了简单起见把编码的情况也忽略了,所以无法监控中文目录和文件,但这并不重要,关键是要明白整个流程。
5多线程调用
同步调用的缺点很多,不单阻塞任务效率低下,如果逻辑一旦复杂需要申请内存,那么还要精心设计内存释放。而像上面这个例子除非设置遇到某种异常或者捕获某个中断,不然无法退出循环,内存的分配和释放也只有在一次循环发生时一起执行。因此,无论什么时候最后是否采用多线程,我个人更倾向于异步执行。
5.1异步的ReadDirectoryChangesW
在多线程的Winnotify版本中,异步地使用ReadDirectoryChangesW,与同步调用不同的是,必须设置该函数的第七个参数lpOverlapped,该参数将被第八个参数lpCompletionRoutine所指向的函数调用。通常情况下,需要在lpOverlapped的基础上再定义一个数据结构,利用出递给处理函数的首地址来得到所需要的信息。在这里我重新定义了如下的结构:
typedef struct DIR_MONITOR{
OVERLAPPED ol;
HANDLE hDir;
BYTE buffer[32*BUFFER_SIZE];
DWORD notifyFilter;
BOOL fStop;
Tcl_Interp*interp;
char pThread[TCL_THREAD_NAME_LENGTH];
}*HDIR_MONITOR;
该结构中的第一个成员便是用于事件通知的,接着分别是文件句柄、接受文件变更信息的存储、指定监测文件变更的类型、是否停止监测、当前使用的Tcl解析器和Tcl中的主线程名称。其中fstop之前的部分基本上适用于ReadDirectoryChangesW的定义,如buffer即是获取文件变更信息的最重要的数据结构,而interp和pThread则将用于监测线程通知主线程所发生的变化。当启动监测函数之后,线程并未被阻塞,
只有当监测到文件变更时,线程才会调用lpCompletionRoutine并读取HDIR_MONITOR指向的数据结构中的数据,并在处理完相关信息之后再调用监测函数来启动下一次异步监测。具体的代码就不贴出来了。
5.2Tcl语言级的线程支持
由于Tcl提供了Thread模块,相比较API而言,不但功能上并未减少太多,而且更易于维护,所以完成启动异步监测之后,在Tcl语言级别完成多线程间的通信更方便。也就是说,在主线程新开一个或者多个线程用于监测文件夹,而主线程本身则处理监测到的信息并根据这些信息来完成进一步的工作。可以使用如下的程序流程:
package require Thread
proc proxy{file type}{
puts"$file is$type"
}
set watched_path d:/s/c
set main_thread_id[thread::id]
set watch_thread[thread::create"
load winnotify.dll
winnotify::watch$main_thread_id$watched_path
thread::wait
winnotify::stop
"]
vwait forever
在主线程中,定义了一个默认用于处理监测线程返回信息的函数proxy,定义了被监测目录与主线程id,后两者被作为参数传递给了监测线程。当启动监测之后,监测线程使用thread::wait来启动Tcl的事件循环,并在其后明确使用了winnotify::stop清理监测过程中所使用内存空间,该指令相对应的函数Stop_Cmd通过调用StopMonitoring来完成清理任务,Stop_Cmd由于比较简单就不列出来了。
void StopMonitoring(HDIR_MONITOR pMonitor)
{
if(pMonitor)
{
pMonitor->fStop=true;
CancelIo(pMonitor->hDir);
if(!HasOverlappedIoCompleted(&pMonitor->ol)) {
SleepEx(5,true);
}
CloseHandle(pMonitor->ol.hEvent);
CloseHandle(pMonitor->hDir);
HeapFree(GetProcessHeap(),0,pMonitor);
}
}
由于该函数需要传入参数pMonitor,因此在模块入口函数
26 --
Winnotify_Init中定义winnotify::watch和winnotify::stop之前,就应该实现定义并初始化pMontor,然后以ClientData的形式
供watch和stop命令使用。
HDIR_MO NITOR pMonitor=(HDIR_MONITOR) HeapAlloc(
GetProcessHeap(),HEAP_ZERO_MEMORY, sizeof(*pMonitor));
Tcl_CreateObjCommand(interp,"winnotify::watch",
Watch_Cmd,(ClientData)pMonitor,NULL);
Tcl_CreateObjCommand(interp,"winnotify::stop",
Stop_Cmd,(ClientData)pMonitor,NULL); 5.3通知主线程
当监视线程发现文件变化之后,需要通知主线程来完成接下来相对应的工作,比如最简单的仅仅是打印变化消息。和线程的实现类似,通知主线程的实现也可以分为使用API 实现和在Tcl语言级实现。
使用API,一般调用Tcl_ThreadQueueEvent在指定线程的事件队列中添加一个新的事件。该函数接收3个参数,分别是threadId、evPtr和position,表示线程的Id、某个具体的事件和插入事件队列的位置。事实上,要得到第一个参数就非常困难,由于Tcl语言级的线程Id与API中接收的Tcl_ThreadId数据结构没有太多共同之处,除了Tcl_GetCurrentThread用于得到当前线程Id外,没有能得到某个线程Id号的API,所以这不是一个直截了当的工作。
的确是有一个公共的数据结构维护着Tcl语言级Id号到Tcl_ThreadId数据结构的映射,可以通过它来得到有关线程的信息;当然还可以在主线程启动之后直接调用Tcl_GetCurrentThread得到主线程Id数据结构,并将其作为模块winnotidy指令的一部分参数传递给监控线程来调用。
相比较而言,在Tcl语言级实现监控线程对主线程的通知,显得方便很多。首先还是要得到主线程的Tcl语言级的Id 号。这有很多方法,可以将其作为watch命令的一个参数;也可以在监控线程中使用thread::na
mes得到所有线程之后再进行筛选,等等。当监控线程发现文件变化后,异步监控触发相应的lpCompletionRoutine函数,在其中使用thread::send指令给主线程Id号发送执行命令。下面就是该函数的代码:
static void CALLBACK MonitorCallback(DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped)
{
TCHAR szFile[BUFFER_SIZE];
PFILE_NOTIFY_INFORMATION pNotify;
HDIR_MONITOR pMonitor=(HDIR_MONITOR) lpOverlapped;
size_t offset=0;
BOOL RefreshMonitoring(HDIR_MONITOR pMonitor);
if(dwErrorCode==ERROR_SUCCESS)
{
do
{
pNotify=(PFILE_NOTIFY_INFORMATION) &pMonitor->buffer[offset];
offset+=pNotify->NextEntryOffset;
int count=WideCharToMultiByte(CP_ACP,0,
pNotify->FileName,
pNotify->FileNameLength/sizeof(WCHAR),
szFile,MAX_PATH-1,NULL,NULL);
szFile[count]=TEXT('\0');
char script[MAX_PATH]={0};
switch(pNotify->Action)
{
case FILE_ACTION_ADDED:
sprintf(script,
"thread::send%s{proxy%s added}",
pMonitor->pThread,szFile);
Tcl_EvalEx(pMonitor->interp,script,-1, TCL_EVAL_GLOBAL);
break;
case FILE_ACTION_MODIFIED:
sprintf(script,
"thread::send%s{proxy%s modified}",
pMonitor->pThread,szFile);
Tcl_EvalEx(pMonitor->interp,script,-1, TCL_EVAL_GLOBAL);
break;
case FILE_ACTION_REMOVED:
sprintf(script,
"thread::send%s{proxy%s removed}",
pMonitor->pThread,szFile);
Tcl_EvalEx(pMonitor->interp,script,-1, TCL_EVAL_GLOBAL);
break;
default:
{
//NOTHING
}
}
}while(pNotify->NextEntryOffset!=0);
}
if(!pMonitor->fStop)
{
RefreshMonitoring(pMonitor);
}
}
在上面的代码里,Tcl_EvalEx负责在当前的Tcl解析器中执行一段指令。第一个参数是指向当前Tcl解析器的指针;第二个参数是执行的具体指令;第三个则是需要执行的指令的长度,一般去-1表示全部;第四个则是执行该指令的Tcl环境,TCL_EVAL_GLOBAL即是表示在全局环境中执行,如换成TCL_EVAL_DIRECT,则在当前Tcl模块函数的环境中执行,在这个例子里没有区别。
对于thread::send,还是应谨慎使用,有时候它会造成死
27
--
锁。比如:
thread::send$tid_1"thread::send[thread::id]{puts hello}"
这段代码将永远不会返回,主线程等待tid_1完成任务,而tid_1则等待主线程执行打印hello的任务,于是死锁便产生。虽然这个例子很愚蠢,但是说明了危险的存在,有时候即使使用异步选项-async也不一定有效。
6异步事件通知
仅仅在ReadDirectoryChangesW中使用异步的最好结果,也就是另开一个线程让监控在其中执行,这么做看起来事实上与同步调用并无区别。其实纯粹使用C编程时,即使采用IOCP也无非是这种模式,IOCP还是会在另一个线程中循环地检测。幸运的是Tcl有基于事件通知的消息传递机制,在异步调用监测的同时,结合Tcl的事件通知便可以实现在单线程中非阻塞地进行文件监视。
与多线程监测的MonitorCallback函数类似,在事件通知的异步通知函数里,仅仅是将Tcl_EvalEx换成了Tcl_QueueEvent,用于给当前Tcl解析器的事件队列中添加新的事件。它接受两个参数,分别是evtPtr和position,表示事件与事件加入队列的位置,这基本上和Tcl_ThreadQueueEvent 很类似。之后还应调用Tcl_DoOneEvent,来处理事件队列中的一个事件,如果成功的话将立即返回,如果失败则可能会阻塞,这取决于传入的参数值。如参数TCL_DONT_WAIT,则无论成功与否都直接返回。
etvPtr是Tcl_Event数据结构的指针,该结构的初始化需要使用Tcl_Alloc来分配内存空间,并且需要给其中的proc成员指定一个执行函数,作为当事件触发时的处理函数。另外,为了方便给处理函数传递必要的参数,应将Tcl_Event包装成新的数据结构,在这里有如下的形式:
typedef struct CEvent{
Tcl_Event evt;
Tcl_Interp*interp;
char script[BUFFER_SIZE];
}CEvent;
除了处于首地址位置的Tcl_Event结构外,还有指向当前Tcl解析器的指针和用于存储Tcl指令的缓冲区。当Tcl事件执行时,Tcl_Event数据结构指针将作为事件处理函数的第三个参数,而在处理函数内部应转换该结构为Cevent结构,从而得到Tcl解析器和执行指令。
static int cHandle(Tcl_Event*evtPtr,int flag)
{
CEvent*event=(CEvent*)evtPtr;
Tcl_EvalEx(event->interp,event->script,-1, TCL_EVAL_GLOBAL);
return1;
}
之后的代码就一清二楚了,但需要注意的是该函数必须是要返回的一个int值,而且返回1或者0意义大不
相同。当返回1时,表示执行完该处理函数后事件将从事件队列中删除,而返回0则表示在事件队列中继续保留该事件。在Tcl的API文档中虽有说明,但写得实在是很偏僻,一不留神就容易造成奇怪的现象。
7编码
即使使用了WideCharToMultiByte,除非是直接打印,一旦被传入Tcl解析器执行后,这些数据依然会变成一堆乱码,这跟Tcl解析器对字符串的默认处理方式有关。在之前的解决Oratcl的问题时,已经深有体会了。比如“你好”一词,存入Oracle数据库后就成了一堆乱码,说是乱码却也是有迹可循。首先将“你好”用utf8解码,之后再用系统默认比如cp935编码出来,就成了真正存入数据库的数据。事实上,所有进入Tcl的数据都会被自动转化为utf8编码,因此在使用Tcl的API时,时刻要注意做必要的转换。
比如对于打开一个被监视文件夹,倘若是一个中文文件夹,那么对于从Tcl指令watch传入的参数需要重新将其转换为系统编码所表示的字符。
Tcl_UtfToExternalDString(NULL,
Tcl_GetStringFromObj(objv[i],NULL),
-1,ds);
第一个参数表示转换用的编码,当NULL时使用系统默认值;第二个就是从watch指令中得到被监控文件的名称,-1表示使用全部长度的数据,ds是Tcl_DString结构的指针,用于动态存储Tcl中的字符串。一般使用如下的顺序初始化该结构,用完之后需要明确使用Tcl_DstringFree释放分配的内存。
Tcl_DString*ds=(Tcl_DString*)Tcl_Alloc(sizeof (Tcl_DString));
Tcl_DStringInit(ds);
在这个例子里还有一个需要处理字符串编码的位置,就是当发生文件变化时,事件传递回来的数据可能包含中文字符,这时候需要反过来将该数据转换为Utf8的编码,这样才能保证在Tcl中打印出正确编码的文字。
int count=WideCharToMultiByte(CP_ACP,0,
pNotify->FileName,pNotify->FileNameLength/ sizeof(WCHAR),
szFile,MAX_PATH-1,NULL,NULL);
中文写代码软件
szFile[count]=TEXT('\0');
Tcl_ExternalToUtfDString(NULL,szFile,-1,ds);
参考文献
[1]Brent B.Welch,Ken Jones,Jeffrey Hobbs.Practical Program-
ming in Tcl and Tk[M].NJ:Prentice Hall PTR,2003. [2]Joe Armstrong.Programming Erlang[M].北京:人民邮电出
版社,2009.
[3]杨剑峰.分布式系统原理与范型[M].北京:清华大学出
版社,2008.
28 --

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。