在做rfid项目,数据处理包括几个步骤:采集数据、处理数据、发送数据到上层应用。采集数据用c#写的,处理数据和发送数据用java写的,因此想实现基于消息订阅发布的多进程通信架构,这样c#采集的数据就可以发送给java进行处理了,而且系统各部分是解耦的,耦合点在于订阅的消息格式。为了简单,仅考虑单服务器的情况,不考虑负载均衡。经过一番搜索,找到了理想的方式----共享内存。本文主要描述了如何用共享内存实现进程间通信库,有兴趣可以看原文:http://www.codeproject.com/Articles/14740/Fast-IPC-Communication-Using-Shared-Memory-and-Int,原文的代码如评论所说,存在不少bug,我修改后放到了github上,感兴趣的同学请点这里

下面是原文的翻译。

 

介绍

    IPC (Inter Process Communication 进程间通信) 已经被很多文章讨论过了,因此没必要写另一篇说如何实现IPC了。然而,关于如何实现高效的IPC类的信息却比较少,本文就是面向这一主题的。

    由于已经有不少信息说明白了IPC的实现,我就不会深入讨论如何实现IPC,而是会主要讨论如何实现高效的IPC。

    有很多种IPC的实现方式,下面列举一些:

  • Shared memory  

  • TCP 

  • Named Pipe  

  • File Mapping  

  • Mailslots  

  • MSMQ (Microsoft Queue Solution) 

    这么多选择,哪种最快呢?答案很简单,对任何问题而言没有完美的解决方案。每种都有优缺点,但是有一个是明显快于其他的----shared memory 共享内存。

    共享内存不仅是最容易实现的,而且也是最快的。为什么它是最快的,我听见你问?最小的成本。成本产生在你调用另一个函数的时候。如果你的IPC没有调用任何其他内核函数或库函数,那么你就避免了一个巨大的瓶颈。共享内存IPC不需要调用第三方函数。

    现在我们知道了一种最好的使用最广泛的IPC机制,下满我们仅需要找到一种时间效率最高的实现方式。

 

背景

    在任何一种IPC实现中,server/client 几乎总是最好的实现方式。为了简单,把通信定为单向的,数据从client发送到server。修改IPC为全双工也很简单,只需要在两个进程各创建一个server。单向通信让你能够集中处理性能问题。

    要写一个高效共享内存IPC,你需要实现几样东西。

    第一,你需要多个预分配的block。原因是当一个线程拥有CPU执行时间,它可以写在不阻塞的情况下向多个block写数据。(译注:block就是共享内存块。如果仅有一个block,那么线程写完一个block后必须阻塞以等待另一进程的线程读完block。然后才能再次写block。)一个最快的优化是批量提交请求。如果仅仅提交一个block,然后等待conext switch到server去处理block,将会极其的低效。我的实现用下面的block structure:

// Block that represents a piece of data to transmit between the
// client and server
struct Block
{
// Variables
// Next block in the single linked list
volatile LONG    Next;
// Amount of data help in this block
DWORD            Amount;
// Data contained in this block
BYTE            Data[IPC_BLOCK_SIZE];
};

第二,你需要跨进程的线程同步。没有这个,多个线程可能写同一个block,容易造成数据错误或死锁(100%CPU使用率)。在我的实现中,我使用events,因为他们很快,比semaphore或mutex快。

// Handle to the mapped memory file
HANDLE                m_hMapFile;
// Event used to signal when data exists
HANDLE                m_hSignal;
// Event used to signal when some blocks become availa
HANDLE                m_hAvail;

    最后,你需要一种方法,用来当server线程等待数据时阻塞线程执行。一种很高效的方法是WaitForSingleObject函数。我的实现使用这个函数等待named event。

    大多数共享内存IPC实现类似于下面:

Server:

    1. 创建命名共享内存,其大小固定位X,包含N个block。

    2. 创建线程间同步对象,用来防止并发访问和竞争条件。

    3. 等待block就绪的事件被触发。

    4. 处理block,然后重新把block标记为可用。

    5. go to step 3.

Client:

    1. 打开命名共享内存。

    2. 等待block变为可用。

    3. 向block写入数据。

    4. 触发block就绪事件。

    5. go to step 2.

快速同步

使用共享内存的一个最大的问题就是防止多线程同时访问block。我们,因此,需要一种方法只允许一个线程同时访问block。

    你遇到的第一个障碍就是如何组织block。你必须高效的实现两组block。一组用于写新数据,一组用于处理已写入的数据。一种很有效的方式是创建2个双端链表,分别用于实现上述两组block。程序员仅需要使用关键段就可以保护两个链表了。这种方法工作的很好,是我的第一个IPC实现。但是,他的带宽速度令人失望。

    使用关键段和双端链表,我仅能做到每秒传输40-50k block,在3.2Ghz P4,在我到达100%CUP利用率之前,感觉很一般。

    那么,瓶颈在哪里呢?同步。大部分CPU时间浪费在进入和离开关键段,阻塞CPU执行和线程上下文切换。我知道的最好的建议来自图形引擎设计,“最快的多边形是不需要画的多边形。”用在这里,最快的关键段是不使用关键段。但是如果不用关键段,我们用什么保护链表呢?答案很简单,使用多线程安全的链表,单链表。InterlockedCompareExchange

现在,我使用InterlockedPopEntrySList和 InterlockedPushEntrySList函数加单链表。但是,我遇到了很棘手的问题。一开始,我在同一进程中测试IPC server 和 IPC client 的时候,看起来很完美,但是当我把两个类放到不同的进程中去的时候,就进入了两难的境地,虚拟内存。

    单链表中的每个block都有一个指向下一block的指针。现在,如果我们深入看SLIST_ENTRY就会发现下面的问题:

typedef struct _SLIST_ENTRY {
struct _SLIST_ENTRY* Next;
} SLIST_ENTRY,  *PSLIST_ENTRY;

标准windows interlocked list 使用内存指针指向下一block。但是当你使用多进程时就会有严重的问题,因为其他进程使用的是完全不同的地址空间(译注:地址空间指进程的虚拟内存空间)。内存块的指针在一个线程上下文中为合法的,只要上下文切换到另一进程的线程的时候,指针就不再合法,导致非法访问。

InterlockedPopEntrySList不能使用了。但是理念可以使用,我们仅需要不用指针重写此功能。这就是我的block stuct的由来。如果你看定义,就会发现:

volatile LONG    Next;

volatile语法告诉编译器确保这个变量不使用CPU cache。如果CPU使用cache,它就会假设Next指针是被缓存的值,在读缓存和使用变量之间另一个线程可能改变它的值。另外,注意到变量类型为LONG。这是因为它表示的是下一block到此block的偏移字节数,这使得next指针总是相对于当前地址空间。我们现在需要为这个新的block struct实现我们自己的InterlockedPushEntrySList和 InterlockedPopEntrySList功能。这是我的实现:

void osIPC::Client::postBlock(Block *pBlock)
{
// Loop attempting to add the block to
// the singlely linked list
LONG blockIndex = (PointerConverter(pBlock).ptrLong -
PointerConverter(&m_pBuf->m_Blocks).ptrLong)
/ sizeof(Block);
for (;;) {
LONG iFirst = pBlock->Next = m_pBuf->m_Filled;
if (InterlockedCompareExchange(&m_pBuf->m_Filled,
blockIndex, iFirst) == iFirst)
break;
}
// Signal the event
SetEvent(m_hSignal);
};

osIPC::Block* osIPC::Client::getBlock(void)
{
// Loop attempting to grab a block
for (;;) {
LONG blockIndex = m_pBuf->m_Available;
if (blockIndex == 0) return NULL;
Block *pBlock = m_pBuf->m_Blocks + blockIndex;
if (InterlockedCompareExchange(&m_pBuf->m_Available,
pBlock->Next, blockIndex) == blockIndex)
return pBlock;
}
};

m_pBuf 是指向共享内存的指针,共享内存结构如下:

 

struct MemBuff
{
// List of available blocks
volatile LONG    m_Available;
// List of blocks that have been filled with data
volatile LONG    m_Filled;
// Array of buffers that are used in the communication
Block            m_Blocks[IPC_BLOCK_COUNT];
};

 

server 的函数相似,与client是相反的。

WaitForSingleObject and Signals

我们现在同时操作两个list,不可能出现多线程问题。现在需要一种方法通知client一些block可写,通知server一些block已写完可以处理。

    可以通过事件和适当超时设置的WaitForSingleObject函数来实现。一旦你有了这些和lists,你就有了高效的IPC,只要线程有充足的CPU时间,就不会进入阻塞状态或者调用额外的函数。

速度

    你现在肯定想知道这种实现到底能有多快。它真的很快!不仅block的每秒传输速率很快,只要你保证你的代码最优化,带宽也能很快。

    在我的3.2Ghz P4 1024 MB RAM laptop,我最近测试了一下速度。

带宽测试 (Packet Size: 3000 Bytes) -> 800,000,000 Bytes/Sec

wKiom1Ny34GyVD2jAAMopDEyS9g389.jpg

速率测试 (Packet Size: 100 Bytes) -> 650,000 Packets/Sec

wKiom1Ny3-zh2hqlAANmrnyKP3w048.jpg

    这些图自己已经说明了,我到目前没有见过哪些实现接近它的速度。我在双核机器上做了测试,client和server运行在不同的CPU上,带宽达到了1,000,000,000 Bytes/Sec,速率达到 2,000,000 Packets / Sec!!!!!!

    最有趣的性能获得,是在多client压力测试下出现的。只要block数随着client数增加,当client数适中时,总带宽就很难收到影响。