↓ 跳过正文
  1. Posts/

用 process_vm_readv() 实现多进程内存队列

·913 字·2 分钟·

一,问题背景
#

多进程之间需要传输大量数据的时候,比如多进程 RPC 框架的进程之间通信,常用共享内存队列。

但是共享内存队列难免会有 入队+出队 2次 memcpy 。

而且要变长共享内存队列,如果支持多生产者进程+多消费者进程 ,就要处理线程安全方面的问题, 比较麻烦。

process_vm_readv() , process_vm_writev() 是 Linux 3.2 新增的 syscall,用于在多个进程的地址空间之间,高效传输大块数据。

https://www.man7.org/linux/man-pages/man2/process_vm_readv.2.html

https://github.com/open-mpi/ompi/blob/master/opal/mca/btl/sm/btl_sm_get.c#L96

在此, 我提个设想,可以用 process_vm_readv 实现一个多进程内存队列,相比之下,优势是:

  1. 在处理 多线程/多进程 并发时,更简单
  2. 省掉一次 memcpy。

下面 demo 代码演示了这个思路。 为了方便,直接用 posix mq 来传输 iovec 数组,也没有考虑多路复用。 真实项目可能需要 eventfd 之内的机制来做 notify。

实测了下,3个进程加起来有 5.5 GB/秒 的速度。

二,代码
#


#include <assert.h>
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <unistd.h>

#include <iostream>
#include <sstream>
#include <vector>
using namespace std;

struct MemMessage {
    pid_t pid = 0;
    vector<iovec> mem_vec;

public:
    MemMessage() {}

    string DebugString() const {
        ostringstream oss;
        oss << "pid=" << pid;
        for (const auto& mem : mem_vec) {
            oss << " iov={" << mem.iov_base << "," << mem.iov_len << "}";
        }
        return oss.str();
    }

    string ToString() const {
        pid_t from_pid = getpid();
        string buff;
        buff.append((const char*)&from_pid, sizeof(from_pid));
        for (const auto& mem : mem_vec) {
            buff.append((const char*)&mem, sizeof(mem));
        }
        return buff;
    }

    bool FromString(const char* buff, size_t buff_len) {
        size_t pos = 0;
        if (buff_len < sizeof(pid)) {
            return false;
        }
        pid = *((pid_t*)buff);
        pos += sizeof(pid);
        for (; pos + sizeof(iovec) <= buff_len; pos += sizeof(iovec)) {
            mem_vec.resize(mem_vec.size() + 1);
            auto& iov = mem_vec.back();
            memcpy(reinterpret_cast<char*>(&iov), buff + pos, sizeof(iov));
        }
        return true;
    }

    void CopyToMem(string& buff) {
        size_t sum = 0;
        for (const auto& iov : mem_vec) {
            sum += iov.iov_len;
        }

        buff.resize(sum, '\0');
        iovec local;
        local.iov_base = &buff[0];
        local.iov_len = buff.size();

        auto nread = process_vm_readv(pid, &local, 1, &mem_vec[0], mem_vec.size(), 0);
        // assert(nread >= 0);
        if (nread <= 0) {
            cout << getpid() << " process_vm_readv from=" << DebugString() << " sum=" << sum << " len=" << nread
                 << endl;
            cout << "pid=" << pid << " " << strerror(errno) << endl;
        }
    }
};

class MemQueue {
    mqd_t m_mq;
    struct mq_attr m_attr;

public:
    int Init(const string& queue_name, bool read_write = false) {
        memset(&m_attr, 0, sizeof(m_attr));
        m_attr.mq_flags = 0;
        m_attr.mq_maxmsg = 10;
        m_attr.mq_msgsize = 4096;
        const int oflag = read_write ? O_WRONLY | O_CREAT : O_RDONLY;
        m_mq = mq_open(queue_name.c_str(), oflag, S_IRWXU, &m_attr);
        // m_mq = mq_open(queue_name.c_str(), oflag);
        cout << getpid() << " mq_open=" << queue_name << " oflag=" << oflag << " ret=" << m_mq << endl;
    }

    int PushMemMsg(const MemMessage& msg) {
        string buff = msg.ToString();
        return mq_send(m_mq, buff.c_str(), buff.size(), 0);
    }

    bool PopMemMsg(MemMessage& msg) {
        string buff(m_attr.mq_msgsize, '\0');
        unsigned prio = 0;
        const auto ret = mq_receive(m_mq, &buff[0], buff.size(), &prio);
        if (ret < 0) {
            return false;
        }
        buff.resize(ret);
        msg.FromString(buff.data(), buff.size());
        return true;
    }
};

const static string queue_name = "/test_queue";

enum STATUS { STATUS_WAITING = 0, STATUS_RUNNING = 1, STATUS_EXIT = 2 };

struct ControlCenter {
    int proc_status;
};

ControlCenter* control = nullptr;

void child_proc() {
    const auto ret = fork();
    assert(ret >= 0);
    if (ret != 0) {
        return;
    }

    while (STATUS_WAITING == control->proc_status) {
        usleep(1000);
    }

    MemQueue q;
    q.Init(queue_name);
    size_t bytes = 0;
    while (STATUS_RUNNING == control->proc_status) {
        MemMessage msg;
        if (q.PopMemMsg(msg)) {
            string buff;
            msg.CopyToMem(buff);
            bytes += buff.size();
        } else {
            usleep(1000);
        }
    }
    cout << getpid() << " bytes=" << bytes << " exit" << endl;

    exit(0);
}

int main(int argc, char* argv[]) {
    control = (ControlCenter*)mmap(NULL, sizeof(*control), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    control->proc_status = STATUS_WAITING;

    const static int proc_num = 3;
    for (int i = 0; i < proc_num; ++i) {
        child_proc();
    }

    MemQueue q;
    q.Init(queue_name, true);

    control->proc_status = STATUS_RUNNING;
    string str(1024 * 1024 * 10, 'a');
    for (int i = 0; i < 10000; ++i) {
        MemMessage msg;
        for (int t = 0; t < 3; ++t) {
            size_t base = rand() % str.size();
            size_t size = rand() % (str.size() - base);
            msg.mem_vec.emplace_back(iovec{&str[base], size});
        }
        const auto ret = q.PushMemMsg(msg);
        // cout << getpid() << " PushMemMsg=" << msg.DebugString() << " ret=" << ret << endl;

        // sleep(1);
    }

    control->proc_status = STATUS_EXIT;

    for (int i = 0; i < proc_num; ++i) {
        int status = 0;
        wait(&status);
    }

    return 0;
}

相关文章

构造最短程序打印自身的 MD5

·1129 字·3 分钟
一,介绍 # 比赛题目很简单:构造一个程序,在 stdout 上打印出自身的 MD5,程序越短越好。按最终程序文件大小字节数排名,文件越小,排名越靠前。 只能使用 ld-linux-x86-64.so, libc.so, libdl.so, libgcc_s.so, libm.so, libstdc++.so 。 禁止了 socket, shmget, fork, execvc 等 syscall 。 汇编高手如云,本人只做到 752 字节,只拿到 27 名。 但忙活好几天,学到不少东西,也有苦劳,还是值得记录一下。 基本是纯 C 实现,没有动用汇编。

基于 ElasticSearch 开发垂直搜索系统

·3979 字·8 分钟
一,背景介绍 # ElasticSearch 是由 Lucene 包装上分布式复制一致性算法等附加功能,构成的开源搜索引擎系统。 近两年在业界热度大增,主要有 3 种应用场景: 全文搜索引擎 NOSQL 数据库 日志分析数据库 ELK 很多垂直领域搜索需求,都可以基于 ElasticSearch 来设计架构。 ElasticSearch 能大幅度提升相关业务的迭代开发速度,实现类似 sql 数据库增删改查一样的快速开发。 并在相对高 qps 的在线业务中,保证毫秒级的延迟,提供极高的可用性和稳定性。 经过持续的研读官方文档,调研业界经验,并在实践中应用反思后,总结出一套架构方案。供参考,欢迎意见建议。

怎样让 C++ 中 throw exception 产生的 coredump 带上栈

·493 字·1 分钟
1. 问题 # 一个 C++ 程序,如果 throw 了 exception ,但是又没有 catch,那么一般会产生 coredump, 问题是,在 gcc 4.x 版本产生的 coredump 文件中,没有 throw 时候的堆栈信息,导致不知道是哪里 throw 的,没法查问题。 原因是 gcc 4.x 的 /libstdc++-v3/src/c++11/thread.cc:92 里面有个 catch(…),所以 stack unwind 了,就没了 throw 时候的 stack 。 void * execute_native_thread_routine(){ try { ... }catch(...){ std::terminate(); } } https://abcdabcd987.com/libstdc++-bug/ 一个解决办法是可以升级 GCC 7 ,或者可以用更简单的办法:

用 abstract unix socket 实现进程单实例运行

·802 字·2 分钟
一,问题背景 # 很多时候,我们需要确保进程只有一个实例运行。 有几种方法: http://stackoverflow.com/questions/2964391/preventing-multiple-process-instances-on-linux http://stackoverflow.com/questions/5339200/how-to-create-a-single-instance-application-in-c-or-c https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication/src 比较常规的做法,是对一个文件加文件锁 flock,比如对 pid 文件 flock( LOCK_EX|LOCK_NB ) 但是这种方法有些弊端: 如果文件被 mv 或者 rm,是会被绕过的。 如果磁盘故障比如磁盘满,目录没有写权限,会失败。 二,abstract namespace unix socket # http://linux.die.net/man/7/unix unix socket 有3种: 基于文件的 socketpair 创建的,匿名的 abstract namespace 的,Linux特有 Linux 下, AF_UNIX socket 支持一种特殊的 abstract namespace unix socket 。 相比 普通的基于文件系统的 unix socket,abstract namespace unix socket : 没有磁盘文件 进程挂了以后自动删除,无残留文件 无需担心与 文件系统上的文件冲突,不需要关心文件系统上的绝对路径是否存在的问题

用 DAT 重实现 CppJieba 中文分词算法,降低 99% 内存消耗

·995 字·2 分钟
一,问题背景 # 中文分词应用比较广泛的开源算法,是 jieba 结巴分词,结巴分词较高性能的实现是 C++ 版本的 CppJieba : https://github.com/yanyiwu/cppjieba 在实际使用 CppJieba 的过程中,我们发现 CppJieba 的内存占用比较高。 比如对一个 76W 词 大小 11MB 的词典 ,加载 2份 (比如为了支持平滑改动用户词典)就需要耗费 505MB内存。 这对一些多进程的后台服务,浪费大量内存,难以接受,因此这里希望削减内存耗费。 经过初步调查,确定改进方法,然后动手改造,最终把 505MB 缩减到了 4.7MB ,实现了 99% 内存降低。 此处也有 issue 讨论 https://github.com/yanyiwu/cppjieba/issues/3 代码在 https://github.com/byronhe/cppjieba 。