引言

由于在工作中需要开发一套内存缓存服务,使用了共享内存作为多进程间的数据共享。为了提高共享内存缓存服务的性能,我找了一个类似的较为成熟的开源项目 libshmcache ,通过研究源码学习其中的优点并改进自己的模块。

libshmcache与redis相似的是都使用内存进行数据缓存;与redis不同的是,redis使用的进程自己申请的动态内存,而libshmcache使用的是共享内存。使用共享内存就意味着libshmcache主要的应用场景是同一台主机上的数据缓存。

我花了一周时间阅读了比较感兴趣的部分代码,收获不少,现就以下几个方面总结一下自己的心得:

  • 纯C语言开发的代码风格
  • hash table的原理和实现
  • gcc原子化操作接口
  • 有锁写和无锁读的实现细节
  • 共享内存的两套函数接口(POSIX和SystemV)

纯C语言开发时的代码风格

我在工作中使用比较多的开发语言是C++,对于C语言编写的这样规模的项目,还是第一次仔细深入地研究。C语言使用 struct 作为大多数自定义数据结构的关键字,相对于C++能够使用成员函数能够对类进行功能拓展,C语言比较常用的是将这个对象作为输入参数传到函数中。

纵观所有项目代码,我感受比较深的就是使用结构体中嵌套匿名结构体,这样做能够增强数据结构的层次感,示例代码如下:

struct shmcache_context {
    pid_t pid;
    int lock_fd;    //for file lock
    int detect_deadlock_clocks;
    struct shmcache_config config;
    struct shm_memory_info *memory;
    struct {
	struct shmcache_segment_info hashtable;
	struct {
	    int count;
	    struct shmcache_segment_info *items;
	} values;
    } segments;

    struct shmcache_value_allocator_context value_allocator;
    struct shmcache_list list;   //for value recycle
    bool create_segment;  //if check segment size                                  
};

注意 shmcache_context 中的匿名结构体 segmentsvalues ,这样的写法体现了相互包含关系,也使后续的操作该数据结构的语句更加容易理解。

另外对于联合体和位域这两种技术也是我在之前开发中使用比较少的,通过阅读源码能够让我对其有了更深刻的理解。示例代码如下:

union shm_hentry_offset {
    int64_t offset;
    struct {
	int index :16;
	int64_t offset :48;
    } segment;
};

这段代码使用了联合体赋予了 shm_hentry_offset 两种访问方式,又使用了位域将 int64_t 分割为两段。

hash table的原理和实现

libshmcache内部使用的是hash table做内部缓存的数据结构,这使查找的时间复杂度是O(1)。
之前看过一些介绍hash table的资料,对hash table的工作原理是有过一个基础的了解的,这次通过阅读源码,能够了解到hash table在代码实现上更加细节的内容。
对于hash计算中出现的hash值冲突,即在hash计算时出现了两个不同的key在经过hash计算后得到的bucket相同,libshmcache采用的解决方案是使用linked list来存放这些相同bucket对应的value。

gcc原子化操作接口

使用原子化操作接口能够解决一些并发读写问题,原子化操作相对于互斥锁执行更快。原子化操作也是一种无锁编程的方式。

有锁写和无锁读的实现

在libshmcache中,写操作通过 pthread_mutex_t 进行同步,而读操作是无锁的。
对于写操作来说,需要对hash table进行操作,这肯定是需要同步的。
pthread_mutex_t 保存在共享内存中,不同的进程通过映射共享内存就能获得同一个互斥量,通过这个互斥量就能完成进程间同步。

共享内存的两套函数接口(POSIX和SystemV)

在linux上使用共享内存时有两套接口 mmapshmgetmmapPOSIX 标准的接口,而 shmgetSystem V 标准的接口,两者都能够实现进程间共享内存,但他们在使用上还是有些区别的。对于 mmap 来说,需要在硬盘上创建一个文件,再将该文件映射到内存中。对于 shmget 来说,需要指定一个key,不同的进程通过相同的key就能映射到同一片内存。