基于共享内存的异步无锁IPC类库 Traffic-SHM 源码阅读

全文目录

Trafic-SHM特点:

一、内存映射

二、内存对齐

三、字节顺序

四、数据结构

五、并发控制

六、关键代码

内存映射类MappedFile

数据结构类Queue

元数据类Metadata

数据块类Block

块标识类ACK

七、互斥原理


github项目地址:https://github.com/peptos/traffic-shm

Trafic-SHM特点:

1. Traffic-SHM使用sun.misc.Unsafe和FileChannel提供共享内存机制的纯Java实现,需要JDK 1.6+。共享内存是进程间通信的有效机制。 内存映射文件提供动态内存管理功能,允许应用程序以与将虚拟地址空间的物理内存共享段相同的方式访问磁盘上的文件。

2. Traffic-SHM使用非阻塞算法,实现多生产者/单消费者并发队列,可用于构建具有高吞吐量和低延迟的实时系统。

3. Traffic-SHM数据对齐方式为4字节对齐,字节顺序为大端模式。

4. Traffic-SHM提供一个OAOO(ONCE-AND-ONLY-ONCE)保证的FIFO队列。光标只能向前传送,一旦消息成功传递,消息就是AUTOMATIC ACKNOWLEDGMENT,这意味着一旦接收者接收到消息,就会确认消息。

一、内存映射

操作系统使用虚拟内存来进行内存管理。虚拟内存是一个抽象的概念,它为每个为应用进程提供了使用虚拟地址取代物理内存地址进行内存访问的功能。

操作系统通过将一个虚拟内存区域与一个磁盘上的普通文件关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射。Java语言中,可通过FileChannel类的map()方法进行操作,实现过程由操作系统的mmap()实现。

内存映射使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几个好处:

1. 用户进程把文件数据当作内存,所以无需发布read()或write()系统调用。

2. 当用户进程碰触到映射内存空间,缺页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。

3. 操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。

4. 数据总是按页对齐的,无需执行缓冲区拷贝。

5. 大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。

二、内存对齐

1. 页对齐 操作系统对内存采用页式管理机制,对于用mmap()映射的普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定。进程能够访问的有效地址大小取决于文件被映射部分的大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。示例如下:

因此,考虑到不同操作系统及处理器架构,内存映射文件的大小及mmap()映射的内存空间大小均需要按页大小进行对齐。

2. 数据类型对齐 简单来讲,数据类型对齐需要满足以下条件:数据所在的内存虚拟地址要能被这个数据的长度所整除。数据域单个数据块以4字节进行对齐。

三、字节顺序

不同的处理器架构使用不同的字节顺序存储数据,目前常见有大端小端两种存储方式。一个多位的整数将按照其存储地址的最低或最高字节排列,如果低位字节存放在内存的低地址端,高位字节存放在内存的高地址端,称为小端模式,反之为大端模式。

以0x0A0B0C0D为例:

小端模式:

1

2

3

4

0x0D

0x0C

0x0B

0x0A

大端模式:

1

2

3

4

0x0A

0x0B

0x0C

0x0D

为了保证在不同操作系统及处理器架构下的可移植性,Trafic-SHM约定按照大端模式存储数据

四、数据结构

如下图所示,队列由元数据域及数据域两部分组成,公共信息及读写位置指针在元数据域维护,数据块为数据域的最小单位(图有误,Block的顺序为ACK-Length-Payload)

同时为了节约内存空间,避免频繁映射造成的内存浪费,针对数据域采用了环形队列RingBuffer数据结构。

五、并发控制

可见性 为保证可见性,使用Unsafe的volatile方法对读写位置指针及ACK域进行操作。

原子性 为了保证在多生产者模式下的原子性,对ACK域的一个int类型先后进行了两次无符号short型填充。

六、关键代码

内存映射类MappedFile

关键成员:

类型

变量名

备注

RandomAccessFile

raf

随机读写文件

FileChannel

channel

用于读写的文件连接通道

long

size

自定义映射空间大小

long

address

文件地址

AtomicBoolean

closed

文件关闭标识(原子)

映射方法:

//方法声明
MappedFile mappedFile = MappedFile.with(File file, long size);
MappedFile mappedFile = MappedFile.with(String file, long size);
//外部调用
MappedFile mappedFile = MappedFile.with("/Users/peptos/shm", 200000L);
System.out.println(mappedFile.getAddress());

1. MappedFile中的with方法将shm创建为随机读写文件(RandomAccessFile)raf,返回一个新创建的MappedFile对象。

2. MappedFile的构造函数中,通过pageAlign方法(实际是Util类的align方法)进行页对齐,通过raf对象的getChannel方法获取文件连接通道(用于文件读写),调用map方法进行内存映射。

页对齐:size为自定义的文件大小,pageSize方法通过UNSAFE.getPageSize方法获得内存的页大小alignment。当size为0时,返回大小为0;当size不为0时,向上取整为页大小alignment的整数倍。

//MappedFile类
private static long pageAlign(long size) {
    return Util.align(size, pageSize());
}
//Util类
public static long align(long value, long alignment) {
    long mask = alignment - 1;
    return (value + mask) & ~mask; //((value + mask) / alignment) * alignment
}

内存映射:利用channel的lock方法上独有锁,直到size大小的文件创建完成才释放锁。map0方法进行入参检查后,利用反射递归调用map0方法,按页大小递增提升size和光标,进行文件映射。

//MappedFile类
private long map() {
    if (closed.get()) {
        throw new IllegalArgumentException("MappedFile has been closed");
    }
    try {
        FileLock lock = channel.lock();
        try {
            raf.setLength(size);
        } finally {
            lock.release();
        }
        return map0(channel, FileChannel.MapMode.READ_WRITE, 0L, size);
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

private static long map0(FileChannel fileChannel, FileChannel.MapMode mode, long position, long size)  {
    if (position < 0) {
        throw new IllegalArgumentException("Attempt to access a negative position: " + position);
    }

    if (Util.isWindows() && size > 4L << 30) {
        throw new IllegalArgumentException("Mapping more than 4096 MB is unusable on Windows, size = " + (size >> 20) + " MiB");
    }

    Method map0 = Util.getMethod(fileChannel.getClass(), "map0", int.class, long.class, long.class);
    return (Long) Util.invokeMethod(map0, fileChannel, modeFor(mode), mapAlign(position), pageAlign(size));
}

数据结构类Queue

内存数据读写操作基于sun.misc.Unsafe,支持一些CAS原子操作。

关键成员:

Metadata

metadata

元数据,在队列的最开始

Cursor

readCursor

读指针

通过偏移量offset标识指针位置

Cursor

writeCursor

写指针

通过偏移量offset标识指针位置

读数据方法:

通过queue.poll().getPayload()读取数据(Queue对象的poll方法获得队首的块Block,Block对象的getPayload方法获得块中的字节数据)。

写指针writeCursor为负代表处于队列已满状态,反之为队列未满状态。在未满状态下若读指针位置不能在写指针之前则返回null。

通过UNSAFE类的getIntVolatile方法获得队列位置的数据类型ACK(可见性),若为数据类型ACK.DATA则通过read方法读取数据块。

读取数据块时用Block类的静态方法deserialize进行块与队列空间的解绑(该方法被synchronized修饰,保证互斥)。通过readCursor对象的update方法更新指针位置(mode>0表示队列未满的情况,反之代表队列已满的情况)。最后,在读指针的位置加上ACK.FIN结束标志。

//外部调用
Queue queue = Queue.map("/Users/peptos/shm", 2000L, 1, 0);
queue.init();

while (true) {
	Block block = queue.poll();
	if (block != null) {
		System.out.println(new String(block.getPayload(), "UTF-8"));
	} else {
		Util.pause(10);
	}
}

//Queue类
public Block poll() {
    long read = readCursor.offset();
    long write = writeCursor.offset();

    long read_abs = Math.abs(read);
    long write_abs = Math.abs(write);

    if (read * write > 0) {
        if (read_abs >= write_abs) {
            return null;
        }
    }

    int ack = UNSAFE.getIntVolatile(address + read_abs);

    if (ack == ACK.DATA) {
        return read(read, read_abs);
    }
    return null;
}

private Block read(long read, long read_abs) {
    long mode = read;

    Block block = Block.deserialize(capacity, address, read_abs + Constant.INT_SIZE);

    long shift = read_abs + block.sizeof();

    if (shift >= capacity) {
        shift = Metadata.ORIGIN_OFFSET + shift % capacity;
        mode = -mode;
    }

    if (readCursor.update(read, (mode < 0) ? -shift : shift)) {
        UNSAFE.putOrderedInt(address + read_abs, ACK.FIN);
        return block;
    }
    return null;
}

写数据方法:

通过queue.offer(new Block(bytes))写入数据。

mode>0表示队列未满的情况,写指针在读指针前,数据写后才能被读;反之代表队列已满的情况,写指针在读指针后,数据被读取后才能够继续写入。

如果数据偏移shift(写指针位置+写入数据)大于列表容量capacity,说明写入数据之后队列会从未满状态(mode>0)变为已满状态(mode<0),再检查写入后写指针位置是否会超过读指针位置(会超过说明被读取的空间大小不足以写入新数据),检查通过后执行写入。

通过writeCursor对象的update方法更新指针位置(写指针writeCursor为负代表处于队列已满状态,反之为队列未满状态),通过block对象的serialize方法绑定数据块与队列位置。

//外部调用
Queue queue = Queue.map("/Users/peptos/shm", 2000L, 1, 0);

String string = "hello, world";
byte[] bytes = string.getBytes("UTF-8");
System.out.println(queue.offer(new Block(bytes)));

queue.close();

//Queue类
public boolean offer(Block block) {
    Assert.notNull(block);
    Assert.notNull(block.getPayload());

    return 1 == write(writeCursor.offset(), readCursor.offset(), block);
}

private int write(long write, long read, Block block) {
    long mode = write;
    long write_abs = Math.abs(write);
    long read_abs = Math.abs(read);

    long shift = write_abs + block.sizeof();

    if (shift > capacity) {
        mode = -mode;
        shift = Metadata.ORIGIN_OFFSET + shift % capacity;
    }

    if (mode * read < 0) {
        if (shift >= read_abs - Constant.INT_SIZE) {
            return -1;
        }
    }

    if (writeCursor.update(write, (mode < 0) ? -shift : shift)) {
        block.serialize(capacity, address, write_abs);
        return 1;
    }
    return 0;
}

元数据类Metadata

在Queue的最开始,一共32字节(用Metadata.ORIGIN_OFFSET代表),每一个队列Queue会包含一个Metadata。

Magic Number

Version

(Minor+Major)

Id

Index

readCursor

writeCursor

4B

4B

4B

4B

8B

8B

数据块类Block

ACK

Length

Payload

4B

4B

不定

对于写入序列化过程serialize(已经通过Queue对象的write方法确认可以写入):

1. offset + sizeof() <= capacity

剩余空间能够放下整个block,直接写入。连续调withACK-withLength-withPayload为block赋值(ACK.SEGMENT),最后再调ack方法将ACK段改为ACK.DATA。

2. available(capacity - offset) < Constant.INT_SIZE(Integer.SIZE / Byte.SIZE)

剩余空间放不下一个ACK(不足4字节),从队首开始写(跳过Metadata.ORIGIN_OFFSET)。

3. available >= Constant.INT_SIZE && available < PADDING(Constant.INT_SIZE * 2)

剩余空间能放下ACK无法放下Length(不足8字节),在队尾放ACK,在队首放Length和Payload(跳过Metadata.ORIGIN_OFFSET)。

4. available >= PADDING

剩余空间能放下ACK和Length,无法放下block的其他部分,在队尾放ACK和Length,在队首放Payload内容(跳过Metadata.ORIGIN_OFFSET)

//Queue类
block.serialize(capacity, address, write_abs);

//Block类
public void serialize(long capacity, long address, long offset) {
    if (offset + sizeof() <= capacity) {
        //no overflow
        serialize(address + offset);
        return;
    } else {
        long available = capacity - offset;
        if (available < Constant.INT_SIZE) {
            //whole block overflow
            serialize(address + Metadata.ORIGIN_OFFSET);
            return;
        } else if (available >= Constant.INT_SIZE && available < PADDING) {
            //put ack in, length & payload overflow
            withACK(address + offset);
            withPayload(withLength(address + Metadata.ORIGIN_OFFSET));
            ack(address + offset);
            return;
        } else {
            //payload overflow
            withLength(withACK(address + offset));
            available -= PADDING;
            if (available > 0) {
                setBytes(payload, address + offset + PADDING, available);
            }
            setBytes(payload, available, address + Metadata.ORIGIN_OFFSET, length - available);
            ack(address + offset);
            return;
        }
    }
}

private void serialize(long address) {
    withPayload(withLength(withACK(address)));
    ack(address);
}

private long withACK(long address) {
    UNSAFE.putInt(address, ACK.SEGMENT);
    return address + Constant.INT_SIZE;
}

private long withLength(long address) {
    putInt(address, this.length);
    return address + Constant.INT_SIZE;
}

private long withPayload(long address) {
    setBytes(this.payload, address, this.length);
    return address + align(this.length);
}

private long ack(long address) {
    UNSAFE.putOrderedInt(address, ACK.DATA);
    return address + Constant.INT_SIZE;
}

对于读出反序列化过程(已经通过Queue对象的read方法确认可以读取,因为已经确认过ACK.DATA,因此从read_abs + Constant.INT_SIZE开始读取):

相当于serialize的逆过程,通过getBytes方法(底层调用sun.misc.Unsafe对象的copyMemory本地方法)将payload的数据打包成block返回。

//Queue类
Block block = Block.deserialize(capacity, address, read_abs + Constant.INT_SIZE);

//Block类
public synchronized static Block deserialize(long capacity, long address, long offset) {
    long available = capacity - offset;
    if (available < Constant.INT_SIZE) {
        return deserialize(address + Metadata.ORIGIN_OFFSET);
    } else {
        int length = getInt(address + offset);
        offset += Constant.INT_SIZE;
        available -= Constant.INT_SIZE;

        byte[] payload = new byte[length];

        if (available >= length) {
            getBytes(address + offset, payload, length);
            return new Block(payload);
        } else {
            if (available > 0) {
                getBytes(address + offset, payload, available);
            }
            getBytes(address + Metadata.ORIGIN_OFFSET, payload, available, length - available);
            return new Block(payload);
        }
    }
}

private static Block deserialize(long address) {
    int length = getInt(address);
    address += Constant.INT_SIZE;

    byte[] payload = new byte[length];
    getBytes(address, payload, length);

    return new Block(payload);
}

块标识类ACK

SEGMENT

0x0000FFFF

正在写入的Block

DATA

0xFFFFFFFF

写入完成的Block

FIN

0x11111111

已被读出的Block

七、互斥原理

读操作有synchronized修饰的Block类deserialize方法保证互斥(单消费者)

写操作并没有类似的互斥方法,在并发场景下主要依靠sun.misc.Unsafe类的compareAndSwapLong方法检查写指针更新操作保证互斥。

上文提到,在进行写操作的时候会先检查空间是否已满,空间未满的情况下会先调用写指针writeCursor的update方法更新指针偏移。

实际调用:

//Queue类write方法
if (writeCursor.update(write, (mode < 0) ? -shift : shift)) {
    block.serialize(capacity, address, write_abs);
    return 1;
}

//Cursor类
public boolean update(long expected, long value) {
    return UNSAFE.compareAndSwapLong(address + offset, expected, value);
}

//UNSAFE类
public static boolean compareAndSwapLong(long address, long expected, long value) {
    return compareAndSwapLong(null, address, expected, value);
}

底层sun.misc.Unsafe类的compareAndSwapLong方法:

参考https://www.cnblogs.com/mickole/articles/3757278.html

/***
   * Compares the value of the long field at the specified offset
   * in the supplied object with the given expected value, and updates
   * it if they match.  The operation of this method should be atomic,
   * thus providing an uninterruptible way of updating a long field.
   * 在obj的offset位置比较long field和期望的值,如果相同则更新。这个方法
   * 的操作应该是原子的,因此提供了一种不可中断的方式更新long field。
   * 
   * @param obj the object containing the field to modify.
   *              包含要修改field的对象 
   * @param offset the offset of the long field within <code>obj</code>.
   *               <code>obj</code>中long型field的偏移量
   * @param expect the expected value of the field.
   *               希望field中存在的值
   * @param update the new value of the field if it equals <code>expect</code>.
   *               如果期望值expect与field的当前值相同,设置filed的值为这个新值
   * @return true if the field was changed.
   *              如果field的值被更改
   */
  public native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);

Unsafe类提供了硬件级别的原子操作,CAS操作(Compare And Swap)也就是比较并交换,在目标内存位置的实际值(*offset),如果与预期原值(expect)相等的话,就把内存位置的值更新成预期新值(update),并返回true;如果不相等,就不更新,并返回false。

在项目中,用写指针writeCursor的内存地址中的值(实际上也就是当前的写指针地址write)作为原值,把write作为预期原值与其进行比较,若两者相等,将值更新为与其新值shift(或-shift),返回true;否则不进行更新,返回false。由此可以实现写操作的互斥。

可以用测试类TestMappedFile中的testThread用多线程模拟多进程进行写操作的并发调试。

//TestMappedFile类
@Test
public void testThread() throws Exception {
    final Queue queue = Queue.map("/Users/peptos/shm", 2000L, 1, 0);

    for (int thread = 0; thread < 100; thread++) {

        final Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3000; i++) {
                    String str = "----";
                    String string = str + Thread.currentThread().getName() + "|" + i + str;
                    byte[] bytes = string.getBytes();
                    boolean res = queue.offer(new Block(bytes));
                    System.out.println(Thread.currentThread().getName() + "|" + i + ":" + res);
                }
            }
        });
        t.start();
    }
}

//UNSAFE类
public static boolean compareAndSwapLong(Object object, long offset, long expected, long value) {
    System.out.println(Thread.currentThread().getName() + "| field: " + getLong(offset) + " expected:" + expected + " value: " + value);
    assert8BytesAligned(offset);
    if (BIG_ENDIAN) {
        return unsafe.compareAndSwapLong(object, offset, expected, value);
    }
    return unsafe.compareAndSwapLong(object, offset, Bits.swap(expected), Bits.swap(value));
}

在Queue类write方法相应位置加上调试输出,实际进行调试时会发现:

在共享内存空间未满之前会因为写指针操作互斥(实际上是CAS操作未执行成功)而导致部分线程写入失败,可以看到因为写指针互斥的线程在执行unsafe.compareAndSwapLong时,目标内存位置的实际值与预期原值不相等,因此CAS操作未执行成功,返回false。

另外,在共享内存空间写满(没有读者取走数据)后会因为空间不足写入失败。

一旦写入失败,程序直接返回false,不再等待,因此该IPC Library是非阻塞的。


本文前五节主要参考项目README与其他网络分享进行修改总结。全文如有不足之处请及时指出,欢迎留言私信与我交流探讨。都看到这里了不点赞关注再走嘛?

发布了246 篇原创文章 · 获赞 316 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_20304723/article/details/103236080