65.9K
CodeProject 正在变化。 阅读更多。
Home

一个允许在单台机器上运行 SETI 多个实例的服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (16投票s)

2003年9月17日

CPOL

9分钟阅读

viewsIcon

99763

downloadIcon

678

使用服务在单台机器上运行 SETI 的多个实例,并指定每个实例可以使用哪个处理器。

引言

像许多CodeProject的会员一样,我是 http://setiathome.ssl.berkeley.edu/index.html 项目的长期成员。我早在1999年6月就加入了,在撰写本文时(2003年9月),我已经处理了大约23400个工作单元。

原则上很简单。你下载一个客户端程序,填写几个对话框,然后就忘了它。客户端程序下载一个要处理的数据块,进行处理,然后上传结果。接着它下载下一个数据块,重复这个过程。

SETI项目的人知道他们的用户群体 :) 大多数用户会满足于下载一个屏保版本的客户端程序,让它自己去运行。然而,我们中的一些人希望尽可能多地进行计算。SETI项目提供了一个命令行客户端,其功能与屏保版本相同。因此,出现了一整套“后续”市场,提供了各种附加组件,以确保我们的计算机每天都能计算尽可能多的SETI工作单元。

这是我对“后续”市场的谦虚且有些迟到的贡献,它实现了我想要达成的目标。我的目标是

  • 我希望将SETI作为服务进程运行,而无需任何用户干预。我们家有3台电脑。一台给我自己用,一台给我妻子用,一台给孩子们用。所有这些电脑都运行Windows 2000,所以重启相对不频繁。然而,重启确实会发生,而且我不想不得不跑到每台电脑上去确保SETI在它们上面运行。

    我可以通过将SETI的每个实例放到开始菜单上来实现这个目标,但这有一个缺点,那就是程序会运行在当前登录用户的桌面上。用户有时会关闭他们不了解的程序!

    更重要的是,我不想确保有人已经登录到每台电脑(并且保持登录状态)才能知道SETI在那上面运行。

  • 我想在一台多CPU的计算机上运行多个SETI实例。

  • 我想在一台多CPU的计算机上设置SETI实例的进程亲和性。

  • 我很懒。我不想编写一个管理客户端来指示服务如何运行多个实例。
第一个目标是通过使用Visual C++中的服务向导来创建一个服务,该服务即使在没有人登录的情况下也能运行SETI,并确保即使SETI意外终止也能继续运行。

其余的目标已在本篇文章中涵盖。需要注意的是,尽管我编写这个项目是为了支持SETI,但没有任何东西可以阻止它被用于任何类似的项目的。请注意,seti.exe 是硬编码到服务中的,因此需要进行重新编译才能使用正确的目标exe名称。

运行多个SETI实例

SETI命令行客户端假设它正在处理的数据文件位于可执行文件所在的目录中。当客户端开始运行时,它会创建一个锁文件。当它停止运行时,它会删除锁文件。如果你试图在同一个目录中启动客户端的第二个实例,第二个实例会检测到锁文件并弹出成千上万条错误消息来告诉你它已经在运行了。

解决方案显而易见。为每个SETI实例创建一个目录。但是,SetiService 需要知道要使用哪些目录。但我已经告诉过你我很懒。我不想编写一个管理客户端来设置注册表项来指示SetiService要使用哪些目录。取而代之的是,我选择了以下一系列规则。

  • SetiService 假定它要处理的任何目录(使用SETI客户端)都位于SetiService所在目录的子目录中。

  • SetiService 只会查找它所在目录下一层的子目录。

  • SetiService(显然)会检查每个子目录中是否都有一份客户端可执行文件副本。
对于满足上述条件的每个目录,SetiService都会创建一个线程,该线程反过来会生成一个SETI客户端的副本。

添加到由服务向导为你创建的CServiceModule::Run()函数中的相关代码如下所示。

if ((hStop = CreateEvent(NULL, TRUE, FALSE, NULL)) != HANDLE(NULL))
{
    TCHAR           tszTemp[_MAX_PATH],
                    tszDrive[_MAX_DRIVE],
                    tszPath[_MAX_PATH],
                    tszFileName[_MAX_FNAME],
                    tszExtension[_MAX_EXT];
    CString         csPath,
                    csSetiPath;
    WIN32_FIND_DATA FindFileData;
    HANDLE          hFind;

    // Get our module name and search for seti subdirectories
    GetModuleFileName(NULL, tszTemp, sizeof(tszTemp));
    _splitpath(tszTemp, tszDrive, tszPath, tszFileName, tszExtension);
    csPath = tszDrive;
    csPath += tszPath;
    csSetiPath = csPath;
    csPath += _T("*.*");

    if ((hFind = FindFirstFile(csPath,&FindFileData)) !=INVALID_HANDLE_VALUE)
    {
        do
        {
            if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            {
                // It's a directory. Now check that it's not . or .. 
                // (parent directory or current directory)
                if (
                    _tcscmp(FindFileData.cFileName, _T(".")) != 0
                     &&
                     _tcscmp(FindFileData.cFileName, _T("..")) != 0
                )
                {
                    // It's a candidate directory so set up a new instance 
                    // of our runInfo struct and initialise it.
                    runInfo *sRun = new runInfo;

                    sRun->m_csWorkingFolder = csSetiPath;
                    sRun->m_csWorkingFolder += FindFileData.cFileName;
                    sRun->m_csExeName = sRun->m_csWorkingFolder;
                    sRun->m_csExeName += _T("\\seti.exe");
                    sRun->me = this;
                    sRun->dwProcessor = 0;
                    sRun->m_hArray[1] = hStop;

                    // Found a candidate directory, does it contain seti.exe?
                    if (_access(sRun->m_csExeName, 0) == 0)
                    {
                        // Now check if there's a process affinity file...
                        CString csTemp;

                        for (int i = 0; i < 32; i++)
                        {
                            csTemp.Format(
                                _T("%s\\%d"), sRun->m_csWorkingFolder, i
                            );

                            if (_access(csTemp, 0) == 0)
                                sRun->dwProcessor |= 0x1 << i;
                        }

                        _beginthread(RunSETI, 0, LPVOID(sRun));
                    }
                    else
                        delete sRun;
                }
            }
        } while (FindNextFile(hFind, &FindFileData));

        FindClose(hFind);
        LogEvent(_T("Finished directory search loop"));
    }

// The following code is part of the wizard generated stuff.
    MSG msg;

    while (GetMessage(&msg, 0, 0, 0))
        DispatchMessage(&msg);

    SetEvent(hStop);
}
else
    LogEvent(_T("Unable to create Stop Event, exiting"));
代码首先尝试创建一个未命名的事件,该事件稍后用于向每个线程发出退出信号。如果无法创建事件,服务将终止。

然后,我们使用GetModuleFileName API获取服务本身的完整路径和文件名。它返回当前运行的可执行文件的完全限定名称。我们将它分解成各个部分,然后构建一个搜索字符串(在csPath中)和一个仅引用SetiService当前所在目录的字符串。

然后,我们遍历该目录查找子目录。每个目录都会被测试,以确保它不是当前目录(.)或父目录(..),然后确保它包含SETI客户端可执行文件的副本。如果目录通过了所有这些测试,则会启动一个线程,并将我们为该目录初始化的runInfo结构传递给它。

暂时忽略for循环。

线程过程如下所示。

void RunSETI(LPVOID data)
{
    {
        runInfo *me = (runInfo *) data;
        BOOL    bStop = FALSE;

        _ASSERTE(me);

        STARTUPINFO         si;
        PROCESS_INFORMATION pi;

restart:

        memset(&si, 0, sizeof(si));
        si.cb = sizeof(si);
        si.dwFlags = STARTF_USESHOWWINDOW;
        si.wShowWindow = SW_HIDE;

        me->me->LogEvent(_T("About to start %s"), me->m_csExeName);

        if (CreateProcess(me->m_csExeName, NULL, NULL, NULL, FALSE,
                          CREATE_NO_WINDOW, NULL, 
                          me->m_csWorkingFolder, &si, &pi))
        {
            me->m_hArray[0] = pi.hProcess;

            if (me->dwProcessor)
            {
                me->me->LogEvent("Attempting to set %s to processor %d",
                                 me->m_csExeName,
                                 me->dwProcessor);
                SetProcessAffinityMask(pi.hProcess, me->dwProcessor);
            }

            while (bStop == FALSE)
            {
                switch (WaitForMultipleObjects(2, me->m_hArray,
                        FALSE, INFINITE))
                {
                case WAIT_TIMEOUT:
                    continue;

                case WAIT_OBJECT_0:
                    goto restart;

                case WAIT_OBJECT_0 + 1:
                    bStop = TRUE;
                    break;
                }
            }

            TerminateProcess(me->m_hArray[0], 1);
            delete me;
        }
        else
            me->me->LogEvent(_T("Attempting to start %s returned code %d"),
                             me->m_csExeName,
                             GetLastError());
    }
}
这很简单。我们初始化几个结构,设置一些标志,并创建一个SETI客户端的运行实例。在CreateProcess调用中唯一值得关注的是我们为实例指定了工作目录。

然后,线程会休眠,直到发生以下两种情况之一。

  • SetiService被停止。

  • SETI客户端停止运行。
SetiService停止时,每个线程会在终止其生成的(并且正在监视的)SETI客户端实例后关闭。

如果SETI客户端停止运行,SetiService会再次启动它。

指定处理器以运行实例

在多处理器机器上,您可以通过调用SetProcessAffinityMask() API来指定进程将在哪个(些)处理器上运行。该API接受一个进程句柄来指定要修改的进程,以及一个32位位掩码来指定要使用的处理器。设置位0,进程就可以在处理器0上运行。设置位1,嗯,你知道的。

这就是我们回到之前说的要你忽略的for循环的地方。

// Now check if there's a process affinity file...
CString csTemp;

for (int i = 0; i < 32; i++)
{
    csTemp.Format(_T("%s\\%d", sRun->m_csWorkingFolder, i);

    if (_access(csTemp, 0) == 0)
        sRun->dwProcessor |= 0x1 << i;
}
runInfo结构有一个初始化为0的DWORD字段。代码只是检查在SETI客户端可执行文件所在的目录中是否存在范围从0到31的编号文件。如果文件存在,则设置DWORD中相应的编号位。注意,代码只关心文件是否存在——它可以是零长度文件。还请注意,没有扩展名。

当线程过程启动SETI客户端实例时,它会检查DWORD的值。

if (me->dwProcessor)
{
    me->me->LogEvent("Attempting to set %s to processor %d", me->m_csExeName,
                     me->dwProcessor);
    SetProcessAffinityMask(pi.hProcess, me->dwProcessor);
}
如果值为非零,它将用作SetProcessAffinityMask()的参数。因此,可以很容易地控制每个SETI客户端实例被允许在哪些处理器上运行。另一方面,如果你指定了一个进程亲和性,但只有一个CPU可用,SetProcessAffinityMask()调用将被忽略。

在不重启服务的情况下更改正在运行的SETI实例

服务每分钟轮询一次每个目录。它会检查两件事。第一件事是处理器亲和性的变化。它会重新计算该目录的处理器亲和性,如果发现变化,则修改SETI实例的进程亲和性。

另一件事是检查是否存在(或不存在)disable文件。如果该文件在服务启动后出现在目录中,它将终止SETI实例;反之,如果该文件在那里并且消失了,服务将重启SETI实例。

只有当实例在服务启动时被启用运行时,服务才会执行此操作(它在启动时检查disable文件,如果disable文件存在则终止线程)。很明显,如果实例被禁用了,那么监视该目录的线程就会终止,并且不会注意到将来的变化。

这种额外的控制级别使得控制其他机器上运行的SETI实例变得相当容易。你可以通过创建或删除一个特定的文件来启用或禁用一个特定的SETI实例,并期望服务会在一分钟左右内注意到。我这样做的原因是我不想编写一个管理客户端(记住我很懒),并且可以使用网络文件系统访问来控制SetiService。

安装服务

如果你确实想使用该服务,安装非常简单。只需将setisrv.exe复制到一个目录,然后使用以下命令行将其注册为服务
setisrv /service
然后,在你安装setisrv.exe的目录中创建一个或多个子目录,并将你的SETI安装移动到这些目录中,每个目录一个。别忘了将你的SETI客户端可执行文件重命名为seti.exe。(下载的标准客户端文件名很长,其中包含平台信息等)。

卸载服务

运行此命令行。
setisrv /unregserver
然后删除包含setiservice和所有子目录的目录。

注释

  • 默认情况下,服务被安装为使用LocalSystem帐户运行。它这样运行得很愉快,但即使是管理员也无法终止由此启动的异常SETI进程。如果你想通过任务管理器控制SETI进程,你需要更改SetiService运行的帐户。这可以通过控制面板中的“服务”应用程序来完成。

  • SetiService启动的SETI进程不会显示在你的桌面上。一旦启动,你将无法以任何方式与它们进行交互。因此,在你第一次运行SETI以输入帐户信息并开始处理SETI数据时,必须手动启动一个新的SETI实例。但是,一旦你完成了这一点,SETI就会从其工作目录中的一个文件中获取用户帐户详细信息,并且不再需要手动干预。

  • 我只在双处理器Athlon MP系统上测试了多处理器支持。由于代码没有使用Athlon特定的功能,它应该可以在任何版本的多处理器内核上工作。

版本历史

  • 版本1 - 2003年9月17日。

  • 版本2 - 2003年10月6日。添加了一些关于设置进程亲和性的解释。

  • 版本3 - 2003年12月18日。更新的版本,支持运行时更改处理器亲和性,并允许启用/禁用特定的SETI实例。
  • © . All rights reserved.