Swoole的锁与信号(一) - 源码详解

前言 swoole采用的是多进程模型,并且在master进程中reactor用的是多线程模型,那么他们之间怎么保证数据正确的同步和更新呢? swoole对php使用层也提供了进程间锁与进程间无锁计数器,让php开发者能够很方便的实现一些需要多进程间同步的功能,那么它是怎么做到的呢? 这篇文章全篇帮你解答,锁与信号。 swoole中的锁与信号主要使用的是pthread系列函数来实现,也有自己实现的互斥锁。 锁的类型有很多,各有各的应用场景,我们只有深刻的理解了它们,才能更好的使用。 锁的类型:互斥锁,读写锁,文件锁,自旋锁,原子锁,信号量. 这篇文章的内容会比较多,有很多前置的知识需要学习,我列出来了我写的一系列文章,大家可以去学习。当然,如果你已经了解了这些前置知识,可以直接看后续的内容。 swoole的使用文档 进程间锁lock 进程间无锁计数器 Atomic 锁的原理相关基础 互斥锁与条件变量 读写锁的实现 文件锁与记录锁 自旋锁与原子操作 互斥锁属性详解 PHP中如何应用这些锁 在PHP中要使用Swoole提供的锁非常简单。 只需要:$lock = new Swoole\Lock(SWOOLE_MUTEX); 就可以获得一个互斥锁. Swoole提供了五种类型的锁。 锁类型 说明 SWOOLE_FILELOCK 文件锁 SWOOLE_RWLOCK 读写锁 SWOOLE_SEM 信号量 SWOOLE_MUTEX 互斥锁 SWOOLE_SPINLOCK 自旋锁 具体使用情况可以参考swoole的文档,进程间锁Lock. 如果你看完文档或者已经理解了swoole提供的锁的使用方法,那么我们继续。 讲到swoole锁的实现,需要明确两种类型的锁。 swoole提供给PHP开发者使用的锁。这个锁就是以上提供的五种锁。 swoole内部使用锁。swoole内部使用的锁是自旋锁。 那么接下来我们深入源码去理解这些锁的实现吧! Swoole中的锁 一、swLock数据结构 理解swoole的锁,必须先了解swLock这个结构体。 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 typedef struct _swLock { int type; union { swMutex mutex; #ifndef _WIN32 #ifdef HAVE_RWLOCK swRWLock rwlock; #endif #ifdef HAVE_SPINLOCK swSpinLock spinlock; #endif swFileLock filelock; swSem sem; swAtomicLock atomlock; #endif } object; int (*lock_rd)(struct _swLock *); int (*lock)(struct _swLock *); int (*unlock)(struct _swLock *); int (*trylock_rd)(struct _swLock *); int (*trylock)(struct _swLock *); int (*free)(struct _swLock *); } swLock; 在swoole中,无论哪种锁,它的数据结构都是swLock,它由几个关键的结构构成。

自旋锁与原子操作

概述 引用维基百科: 自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此操作系统的实现在很多地方往往用自旋锁。 Windows操作系统提供的轻型读写锁(SRWLock)内部就用了自旋锁。显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。 获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的。通常用test-and-set等原子操作来实现。 自旋锁互斥锁对比 自旋锁spinlock 不会使线程发生切换,互斥锁mutex在获取不到锁的时候线程会sleep。 互斥锁mutex获取锁分为两个阶段,第一阶段在用户态获取锁,如果获取不到,第二阶段则会产生系统调用,进入sleep,当锁可用后再恢复上下文,继续竞争锁。 自旋锁spinlock 优点,没有昂贵的系统调用,一直处于用户态,执行速度快。 自旋锁spinlock 缺点一直占用cpu,而且还会锁总线,锁定状态,其他处理器一直在空转。 互斥锁mutex 优点,不会忙等,得不到锁会sleep。 互斥锁mutex 缺点,sleep后会陷入内核态,恢复起来需要昂贵的系统调用。 从上所属,我们不难得出结论, 自旋锁spinlock和互斥锁mutex的应用场景。 自旋锁spinlock适合临界区特别简短的场景,中间不能有显示或隐式的系统调用,执行速度要非常快。不要在临界区调用read``write``open这样的函数。 使用自旋锁 自旋锁相关api 初始化自旋锁 1 int pthread_spin_init(pthread_spinlock_t *lock, int pshared); pshared有两个值 PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。 PTHREAD_PROCESS_PRIVATE: 仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。 获取自旋锁 1 int pthread_spin_lock(pthread_spinlock_t *lock); 当一个线程获取自旋锁的的时候,这个锁未被其他线程持有,则获取到锁,如果已被其他线程持有,则一直阻塞空转直到获取到该锁。 非阻塞获取自旋锁 1 int pthread_spin_trylock(pthread_spinlock_t *lock); 释放一个自旋锁 1 int pthread_spin_unlock(pthread_spinlock_t *lock); 销毁一个自旋锁 1 int pthread_spin_destroy(pthread_spinlock_t *lock); 原子操作 原子操作,可以理解为不可再次细分,也不可打断的一组操作。如果说一组操作是原子操作,那么这组操作要么一次成功,要么全部失败。其他的操作是不能插入进来的。 在linux中对原子操作提供了两种形式: 对整数的操作 对单独位的操作 Gcc 4.1.2版本之后,对X86或X86_64支持内置原子操作,不需要提供其他第三方库。 提供的函数api如下: type __sync_fetch_and_add (type *ptr, type value, …) 将value加到ptr上,结果更新到ptr,并返回操作之前*ptr的值

互斥锁属性详解

互斥锁属性说明 使用互斥锁可以是线程按顺序执行,互斥锁通过确保一次只有一个线程/进程执行代码临界段来同步多个线程/进程。互斥锁还可以保护单线程代码。要更改缺省的互斥锁属性,可以对属性对象进程声明和初始化。 1 2 //互斥锁动态初始化函数原型 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr *attr); 在使用动态互斥锁初始化函数pthread_mutex_init()初始化互斥锁的时候,attr参数指定了新创建互斥锁的属性。如果attr参数为NULL,则默认属性为快速互斥锁。如果要自定义属性则需要使用pthread_mutexattr_init()函数来创建属性。 互斥锁属性操作方法 互斥锁属性对象初始化 1 2 3 4 5 6 //原型: int pthread_mutexattr_init(pthread_mutexattr_t *mattr); #include <pthread.h> pthread_mutexattr_t mattr; int ret; ret = pthread_mutexattr_init(&mattr); //成功返回0,其他任何返回值都表示出现了错误 初始化的互斥锁属性默认是PTHREAD_PROCESS_PRIVATE,也就是说只能在当前进程内使用该互斥锁,如果要在共享内存中,多进程使用互斥锁,需要调用pthread_mutexattr_setpshared把属性设置为PTHREAD_PROCESS_SHARED. 互斥锁属性对象销毁 1 2 3 4 5 6 //原型: int pthread_mutexattr_destroy(pthread_mutexattr_t *mattr) #include <pthread.h> pthread_mutexattr_t mattr; int ret; ret = pthread_mutexattr_destroy(&mattr); //成功返回0,其他任何返回值都表示出现了错误 pthread_mutexattr_init 与pthread_mutexattr_destroy 必须成对出现,不然会产生内存泄露。 设置互斥锁的使用范围 互斥锁变量可以是进程内专用的变量,也可以是系统范围(多进程)的变量,要在多进程内共享互斥锁,可以在共享内存中创建互斥锁,并将pshared属性设置为PTHREAD_PROCESS_SHARED,如果互斥锁的pshared属性设置为PTHREAD_PROCESS_PRIVATE,则只有同一个进程内创建的线程才能处理该互斥锁. 1 2 3 //原型: int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared); pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); //成功返回0,其他任何返回值都表示出现了错误 获取互斥锁的使用范围 1 2 3 4 //原型: int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared); int pshared; pthread_mutexattr_getpshared(&mattr,&pshared); //成功返回0,其他任何返回值都表示出现了错误 获取/设置互斥锁属性的类型 1 2 int pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type); int pthread_mutexattr_gettype(pthread_mutexattr_t *attr,int *type); PTHREAD_MUTEX_DEFAULT(缺省的互斥锁类型属性):这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起不可预料的结果。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。POSIX标准规定,对于某一具体的实现,可以把这种类型的互斥锁定义为其他类型的互斥锁。

互斥锁与条件变量

互斥锁 互斥锁的作用 锁的种类有很多,包括互斥锁、文件上、读写锁等等,这些锁各有各的应用场景,这里我们主要探讨互斥锁的作用。 互斥锁指代互相排斥,它是最基本的同步形式,主要作用可以区分为以下两种,分别是: 保护共享数据。 在多线程/多进程编程中,多个线程/进程同时访问同一个数据,为了保护数据操作的准确性,需要加锁来实现保证这段数据在任何时刻只有且仅有一个线程/进程能够操作它。 保持操作的互斥。 一个程序有多个操作,但是同一时间内只有一个操作能执行,使用互斥锁,A/B两个操作就能控制A执行的时候B不能执行,同理B执行的时候A不能执行。 初始化锁 POSIX互斥锁被声明为具有pthread_mutex_t数据类型的变量,有两种方式来初始化它。 使用常值PTHREAD_MUTEX_INITIALIZER静态初始化 1 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER 动态初始化 1 2 3 //原型: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr *attr); pthread_mutex_t lock;/* 互斥锁定义 */ pthread_mutex_init(&lock,NULL);/* 动态初始化, 成功返回0,失败返回非0 */ 动态创建互斥锁的时候attr参数指定的是新建互斥锁的属性,如果参数为NULL,则使用默认的互斥锁属性互斥锁属性详解请看我的另外一篇文章互斥锁属性详解。 加锁 多个线程/进程在对互斥锁上锁的过程中,如果某个线程已经锁住了互斥锁,其他线程使用pthread_mutex_lock给这个互斥锁上锁将会阻塞到它解锁为止,如果使用非阻塞加锁函数pthread_mutex_trylock加锁,且该互斥锁已锁住,则它就会返回一个EBUSY错误。 阻塞加锁 1 2 // 原型: int pthread_mutex_lock(pthread_mutex_t *mutex) pthread_mutex_lock(&lock); 非阻塞加锁 1 2 // 原型: int pthread_mutex_trylock(pthread_mutex_t *mutex) pthread_mutex_trylock(&lock); 解锁 使用该函数解锁的前提是互斥锁处于锁定状态,而且调用本函数的线程/进程必须是给这个互斥锁加锁的线程/进程,也就是常说的解铃还须系铃人。 ``` // 原型: int pthread_mutex_unlock(pthread_mutex_t *mutex) pthread_mutex_unlock(&lock); ``` 如果多个线程/进程阻塞在等待同一个互斥锁上,那么当该互斥锁解锁时,哪一个线程/进程会开始运行呢? 这里要涉及到线程/进程的优先级调度,这个是操作系统维护的,我们先不讨论,大家只要知道操作系统为每个线程/进程都赋予了不同的优先级,同步函数(互斥锁,读写锁,信号量)将唤醒优先级最高的被阻塞线程/进程。 销毁锁 注意pthread_mutex_destory 必须是和pthread_mutex_init 成对出现的,缺一不可。 使用PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁不需要调用该方法释放。 这里还有一个要注意的地方,调用pthread_mutex_destory后,存储互斥锁的相关内存并不会被释放,需要手动调用free来释放该内存

Swoole的内存模块-共享内存

前言 大家应该知道,swoole使用的是多进程模型,那么多进程之间怎么共享数据呢? 今天我就来给大家讲讲swoole多进程之间使用共享内存来共享数据。 这篇教程我参考了以下资料: 你如果有深厚的*unix环境编程经验,可以不用看我提供的参考列表。 UNIX网络编程:卷2进程间通信(unpicp) 第12,13,14章 Leo Yang的知乎文章(Swoole 源码分析——内存模块之共享内存) 相关源代码 src/memory/shared_memory.cc 请注意,接下来的文章中,如果知识点能简单的说清楚,我会在本篇中概括清楚,如果知识量比较大的,我会加上链接,大家可以跳转过去看完再回来继续。当然,你如果已经明白了这个知识点,那请继续。 源码解析 以下代码基于 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 (!

IPC之共享内存

共享内存区介绍 进程间通讯有很多方法,其中最快的形式要数共享内存。 从实现标准上来说,共享内存可分为POSIX共享内存区和System V 共享内存区,从概念上来是是类似的。 两个进程直接访问同一段共享内存。减少了数据的复制次数和上下文切换成本, 但是这种快是有代价的,那就是没有内核来帮你维护数据同步,两个进程同时能够访问这段内存,会造成数据同步问题,这个时候需要使用到锁来解决这个问题,不过这篇文章暂时不涉及到锁,我们详细的来了解下内存映射的概念。 从实现标准上来说,共享内存可分为POSIX共享内存区和System V 共享内存区,从概念上来是是类似的。 POSIX共享内存区 每个进程都拥有操作系统虚拟给自己的一段连续的私有内存区域,但是多个进程要共享一段内存就需要操作系统从空闲内存池中分配,分配好以后,需要连接它的进程进行申请连接,这个过程就叫做共享内存映射。映射完成后,每个进程就可以像访问自己的私有内存一样去访问共享内存区域,从而跟其他进程进行通讯。请看下图: 上图很清晰的描述了共享内存的原理,但是该怎么操作才能申请共享内存呢?接下来我会详细描述这个过程。 进程分为有亲缘关系进程和无亲缘关系进程,它们的通讯方式也不尽相同。 有亲缘关系的进程间共享内存有三种办法。 使用内存映射文件,open函数打开。 BSD系列的系统提供了匿名内存映射标识MAP_ANONYMOUS直接映射内存。 打开/dev/zero设备文件匿名映射。 /dev/zero在类UNIX系统中是一个特殊的设备文件,当你读它的时候,它会提供无限的空字符。 Link 维基百科 无亲缘关系的进程间共享内存有两种办法: 内存映射文件,open函数打开。 共享内存区对象,通过shm_open 函数打开一个POSIX IPC名称。 函数的使用 使用POSIX共享内存区要用到shm_open,shm_unlink,ftruncate,mmap,munmap等函数,接下来我为大家详细说明。 shm_open 创建或打开一个IPC对象。 函数原型: 1 2 3 4 5 #include >sys/mman.h> int shm_open(const char *name, int oflag, mode_t mode); //返回: 若成功则为非负描述符,若出错则为-1 参数: name 这是一个"Posix IPC名字",符合已有的路径名规则,必须是斜杠符/开头,可以是真实的路径名,也可以不是。 oflag 可选标识有O_RDONLY,O_RDWR,O_CREAT,O_EXCL,O_NONBLOCK,O_TRUNC,其中O_RDONLY,O_RDWR两个标识必须有一个存在。 mode 创建一个新的消息队列,信号量或共享内存区对象时,需要设置这个权限位参数,权限位参数值如下: 常值 说明 S_IRUSR 用户(属主)读 S_IWUSR 用户(属主)写 S_IRGRP (属)组成员读 S_IWGRP (属)组成员读写 S_IROTH 其他用户读 S_IWOTH 其他用户写 shm_unlink 删除一个IPC对象。跟所有其他unlink函数一样,删除一个IPC对象不会影响对于其底层支撑对象的已存在引用,只有该对象的所有引用全部关闭,这个对象才会清除。删除一个IPC对象仅仅是为了防止后续对其调用。

理解内存映射mmap,一篇就够了

Mmap基础概念 mmap是一种文件映射方法,可以将一个文件或者其它的操作系统支持映射的对象映射到内存中,并且连接到进程的地址空间,使得进程能在用户态直接操作这段内存地址。 这段进程中映射的内存与文件的磁盘地址通过操作系统做了一一对应。 进程可以直接通过指针的方式操作读写这段内存,而操作系统会自动把有改动的脏页面写回到对应的文件磁盘中,这样对文件的操作不用调用read,write等系统调用函数,减少了数据copy,减少了内核态与用户态切换开销,提升了性能。 当然,内核对这段内存区域的操作也直接反应到了用户空间,从而实现了不同进程空间的文件共享。 图1: 如图所示,进程的地址空间是由一个一个的虚拟内存区域构成的,操作系统内核把这些虚拟内存区域连接起来组成了一个逻辑上的连续地址的虚拟内存。 其中共享内存只是其中的一段,也是通过操作系统映射出来加入到进程所见的这段连续内存。 mmap与常规文件操作的对比 系统调用write方式写文件的过程 步骤 1: 进程(用户态)调用write系统调用,并且告诉内核需要写入的数据内存起始地址和长度(告诉内核需要写入的数据在那里)。 步骤 2:内核收到write调用,校验用户态的数据,并把这些数据copy到kernel buffer 中,也就是Page cache. 步骤 3: 操作系统调度,将这些数据异步写入磁盘。 mmap 写入文件的过程 调用mmap映射文件,内核会为该文件在内核态空间创建一个虚拟page cache(未分配物理空间),并把这段内存空间的地址给到进程(用户态)。 进程(用户态)将要写入的数据直接copy到对应的mmap地址(内核态),这里只进行了一次内存copy。 若要写入的mmap地址未对应物理内存,则产生缺页异常,由内核处理,生成真正的物理地址。若已经分配了物理地址,则直接copy到物理内存。 操作系统执行系统调用异步将脏页写回磁盘。 从理论上分析,这两种方式性能对比, 若是每次写入的数据大小接近于Page size(默认是4096),那么write调用于mmap的写性能应该比较接近,因为系统调用的此处相近。 若每次写入的数据非常小,那么mmap的性能应该是比write调用的方式要快很多很多,因为每次缺页异常是一次性产生一个page cache。那么总的系统调用要比write少很多。 read 读数据过程 进程(用户态) 调用read系统调用,告诉内核要读取的数据。 内核执行系统调用,把要读取的数据写入内核缓存(第一次copy)。 内核再次把数据copy给进程(用户态)的buffer(第二次copy)。 mmap 读数据过程 调用mmap映射文件,内核会为该文件在内核态空间创建一个虚拟page cache(未分配物理空间),并把这段内存空间的地址给到进程(用户态)。 进程(用户态) 直接操作这段内存,如果物理地址不存在,产生业异常,copy数据到内核page cache(一次copy)。 进程对这端内存直接操作。 从上面的读过程可以看出,mmap读比read读要少一次内存copy,理论上来说性能要快的多的多。 mmap的优缺点 优点: 实现了内核空间与用户空间的高效交互。两个空间各自的修改直接反应在映射区域,从而被对方及时捕捉。 操作文件就像操作内存一样,对文件的读取操作跨过了页缓存,减少了数据的copy次数。用内存读写取代传统的I/O读写,大大提升了文件的读写效率。 提供了进程间共享内存和相互通讯的方式,亲缘进程或无亲缘进程,都可以通过把自身用户空间的映射到同一片区域,达到进程间共享或进程间通讯的目的。 可以实现高效的大规模数据传输,使用磁盘代替内存空间。使用mmap映射,解决内存空间不足。 缺点: 因为mmap的映射跟内核的page cache密切相关,当申请的空间很小,内核也会生成一个等于page size大小的页,造成空间浪费。 申请的内存无法扩展,如果要扩展,只能重新申请一个更大的共享内存映射,再把老的数据copy过去,所以对变长文件不适合使用mmap。 Mmap相关函数详解 mmap函数 mmap函数把一个文件或者一个Posix共享内存区对象映射到调用内存的地址空间。 使用该函数有三个目的: 使用普通文件以提供内存映射I/O。 使用特殊文件以提供匿名内存映射。 使用shm_open以提供无亲缘关系进程间的Posix共享内存区. mmap 函数原型: 1 2 3 #include <sys/mman.
0%