Swoole的内存模块-共享内存

前言

大家应该知道,swoole使用的是多进程模型,那么多进程之间怎么共享数据呢? 今天我就来给大家讲讲swoole多进程之间使用共享内存来共享数据。

这篇教程我参考了以下资料:

你如果有深厚的*unix环境编程经验,可以不用看我提供的参考列表。

请注意,接下来的文章中,如果知识点能简单的说清楚,我会在本篇中概括清楚,如果知识量比较大的,我会加上链接,大家可以跳转过去看完再回来继续。当然,你如果已经明白了这个知识点,那请继续。

源码解析

以下代码基于 swoole 4.4.16

共享内存的数据结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define SW_SHM_MMAP_FILE_LEN  64

typedef struct _swShareMemory_mmap
{
    size_t size;
    char mapfile[SW_SHM_MMAP_FILE_LEN];
    int tmpfd;
    int key;
    int shmid;
    void *mem;
} swShareMemory;
  • size: 共享内存的大小(包含swShareMemory结构体的大小)

  • mapfile: 共享内存使用的内存映射文件的文件名,最长64个字节。

  • tmpfd: 内存映射文件的描述符。

  • key: System V的shm系列函数创建共享内存的key的值。

  • shmid: System V的shm系列函数创建的共享内存的id(类似于fd)。

  • mem: void 类型的指针,这个变量里面存的是申请到的共享内存首地址,可以理解为相当于面向对象中的this指针。可以很方便的访问到swShareMemory结构体的各个变量

  • 源文件 include/swoole.h

共享内存的创建

我们先来看看swoole是怎么创建共享内存的,下面是mmap的封装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void *swShareMemory_mmap_create(swShareMemory *object, size_t size, char *mapfile)
{
    void *mem;
    int tmpfd = -1;
    int flag = MAP_SHARED;
    bzero(object, sizeof(swShareMemory));

#ifdef MAP_ANONYMOUS
    flag |= MAP_ANONYMOUS;
#else
    if (mapfile == NULL)
    {
        mapfile = "/dev/zero";
    }
    if ((tmpfd = open(mapfile, O_RDWR)) < 0)
    {
        return NULL;
    }
    strncpy(object->mapfile, mapfile, SW_SHM_MMAP_FILE_LEN);
    object->tmpfd = tmpfd;
#endif

#if defined(SW_USE_HUGEPAGE) && defined(MAP_HUGE_PAGE)
    if (size > 2 * 1024 * 1024)
    {
#if defined(MAP_HUGETLD)
        flag |= MAP_HUGETLB;
#elif defined(MAP_ALIGNED_SUPER)
        flag &= ~MAP_ANONYMOUS;
        flag |= MAP_ALIGNED_SUPER;
#endif
    }
#endif

    mem = mmap(NULL, size, PROT_READ | PROT_WRITE, flag, tmpfd, 0);
#ifdef MAP_FAILED
    if (mem == MAP_FAILED)
#else
    if (!mem)
#endif
    {
        swSysWarn("mmap(%ld) failed", size);
        return NULL;
    }
    else
    {
        object->size = size;
        object->mem = mem;
        return mem;
    }
}

通过以上代码,可以看到swoole是怎么封装mmap函数的。不得不说还是挺精妙的,那么我现在给大家一一解释各个代码的含义。 关于共享内存是怎么回事,mmap函数的详情,大家可以看一下我的另外一篇教程mmap函数详解

先给大家解释下几个常量的含义:

  • MAP_SHARED 与其它所有映射这个对象的进程共享映射空间。

  • MAP_ANONYMOUS 匿名映射,映射区不与任何文件关联。

  • MAP_HUGE_PAGE 是否支持大页模式分配内存

  • MAP_HUGETLB 使用大页模式

  • 7-20行: 使用了宏判断,是否支持匿名映射,注意MAP_ANONYMOUS这个宏定义在-std=c99模式下是不支持的,但是-std=gnu99是支持的。如果使用了MAP_ANONYMOUS,那么会忽视fd参数,在不支持MAP_ANONYMOUS这个宏的系统中,使用操作系统提供的/dev/zero文件提供匿名映射支持。

  • 22-33行: 这里要注意的是MAP_HUGE_PAGE这个宏定义,这个是linux内核在2.6.32 引入的一个flags,确定是否使用大页分配共享内存。内核传统使用的是4k小页,常见的体系架构都会提供两种大页模式,一般是2M和1G这两个size。使用大页可以减少页表的数量,从而减少TLB MISS的概率,提高系统访问性能,但是也会降低内存管理的粒度和灵活性,在使用小内存的时候造成内存浪费。 swoole在这里是判断要申请的内存size大于2M,就使用大页模式。 MAP_ALIGNED_SUPER 这个参数是FreeBSD中的特有参数,使用大页模式。

  • 34-50 mmap函数的调用,PROT_READ|PROT_WRITE 表示申请的映射区可读可写,最后把申请的的内存映射区地址赋值给传入的object->mem。以后就可以通过swShareMemory来使用了。

swoole的进程模型是通过master进程fork出其它的工作进程,各个进程之间都有亲缘关系,因此swShareMemory_mmap_create函数可以直接使用匿名映射或(/dev/zero设备)的方式创建共享内存映射,不需要使用open具体的共享内存文件或者使用shm_open函数来打开POSIX IPC名字。

swoole申请共享内存

swoole 申请共享内存使用sw_shm_malloc,sw_shm_calloc这两个函数,这两个函数都是为进程匿名申请一大块连续的内存.sw_shm_malloc是指定固定长度,sw_shm_calloc申请n个指定长度的内存,实际长度是size=_size*num。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void* sw_shm_malloc(size_t size)
{
    size = SW_MEM_ALIGNED_SIZE(size);
    swShareMemory object;
    void *mem;
    size += sizeof(swShareMemory);
    mem = swShareMemory_mmap_create(&object, size, NULL);
    if (mem == NULL)
    {
        return NULL;
    }
    else
    {
        memcpy(mem, &object, sizeof(swShareMemory));
        return (char *) mem + sizeof(swShareMemory);
    }
}

void* sw_shm_calloc(size_t num, size_t _size)
{
    swShareMemory object;
    void *mem;
    void *ret_mem;
    size_t size = sizeof(swShareMemory) + (num * _size);
    size = SW_MEM_ALIGNED_SIZE(size);

    mem = swShareMemory_mmap_create(&object, size, NULL);
    if (mem == NULL)
    {
        return NULL;
    }
    else
    {
        memcpy(mem, &object, sizeof(swShareMemory));
        ret_mem = (char *) mem + sizeof(swShareMemory);
        bzero(ret_mem, size - sizeof(swShareMemory));
        return ret_mem;
    }
}

申请内存的大小有个小技巧,首先通过SW_MEM_ALIGNED_SIZE宏做一次内存对齐,使得size永远是8的倍数。 再加上swShareMemory结构的长度,就是申请的共享内存的中长度了.申请共享内存成功后,把swShareMemory这个结构的object复制到内存的头部,再返回真正可用的共享内存首地址供调用方使用。 如果大家了解过redis的字符串底层实现,就知道这种做法跟sds结构是相似的。这样做的好处也是显而易见的,能够很方便的管理这段共享内存。

重新申请内存 realloc

如果我们申请了一段共享内存,使用的时候发现不够用了怎么办?这个时候就要使用到sw_shm_realloc 这个函数, 这个函数很简单,就是新申请一段共享内存,把旧的数据复制到新的内存中,然后再把旧的内存释放。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void* sw_shm_realloc(void *ptr, size_t new_size)
{
    swShareMemory *object = (swShareMemory *) ((char *) ptr - sizeof(swShareMemory));
    void *new_ptr;
    new_ptr = sw_shm_malloc(new_size);
    if (new_ptr == NULL)
    {
        return NULL;
    }
    else
    {
        memcpy(new_ptr, ptr, object->size);
        sw_shm_free(ptr);
        return new_ptr;
    }
}

这段代码 swShareMemory *object = (swShareMemory *) ((char *) ptr - sizeof(swShareMemory));大家注意到了没,怎么样使用swShareMemory这个对象?只需要把传入的内存对象指针往前移动sizeof(swShareMemory)长度,就能得到真正的共享内存首地址。也就能很方便的使用这个对象。接下来的代码大家都能看到这个用法。

修改共享内存权限

共享内存映射完成以后,有port标记的PROT_READ,PROT_WRITE,PROT_EXEC,PROT_NONE权限仍然可以被mprotect系统调用修改。具体用法参见 mmap详解

1
2
3
4
5
int sw_shm_protect(void *addr, int flags)
{
    swShareMemory *object = (swShareMemory *) ((char *) addr - sizeof(swShareMemory));
    return mprotect(object, object->size, flags);
}

释放共享内存

1
2
3
4
5
void sw_shm_free(void *ptr)
{
    swShareMemory *object = (swShareMemory *) ((char *) ptr - sizeof(swShareMemory));
    swShareMemory_mmap_free(object);
}
1
2
3
4
int swShareMemory_mmap_free(swShareMemory *object)
{
    return munmap(object->mem, object->size);
}

System V共享内存

System V是Unix操作系统的一个分支,是一些Unix共同特性的源头,linux实现了System V一套应用于系统的接口协议, POSIX相对于System V可以说是比较新的标准,语法也相对简单。所以swoole使用的基于POSIX协议实现的接口。 以下代码目前应该没有被使用,以后就说不准了。关于System V共享内存的知识,可以查看我的另外一篇文章 System V

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void *swShareMemory_sysv_create(swShareMemory *object, size_t size, int key)
{
    int shmid;
    void *mem;
    bzero(object, sizeof(swShareMemory));

    if (key == 0)
    {
        key = IPC_PRIVATE;
    }
    //SHM_R | SHM_W
    if ((shmid = shmget(key, size, IPC_CREAT)) < 0)
    {
        swSysWarn("shmget(%d, %ld) failed", key, size);
        return NULL;
    }
    if ((mem = shmat(shmid, NULL, 0)) == (void *) -1)
    {
        swSysWarn("shmat() failed");
        return NULL;
    }
    else
    {
        object->key = key;
        object->shmid = shmid;
        object->size = size;
        object->mem = mem;
        return mem;
    }
}

int swShareMemory_sysv_free(swShareMemory *object, int rm)
{
    int shmid = object->shmid;
    int ret = shmdt(object->mem);
    if (rm == 1)
    {
        shmctl(shmid, IPC_RMID, NULL);
    }
    return ret;
}

使用情况

写到这里,swoole的共享内存代码讲解完毕。我们来讲一下swoole到底哪里使用了它们吧!

  1. server的session_list
  2. server的task_result
  3. connection_list
  4. swoole table
  5. chanal
0%