自旋锁
1 自旋锁是不会让进程睡眠的,如果获取不到,会一直忙等(占用cpu)【理想情况下会一直忙等】,所以他适用于 在一些关键场景必须获锁的场景但又持锁事件非常短的场景. (如果持锁时间较长则不适合用自旋锁而需要用互斥锁,让其他抢不到的进入睡眠防止浪费cpu)
nginx的自旋锁实现
void ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int value, ngx_uint_t spin)
{
ngx_uint_t i,n;
//若无法获取所,则会一直在里面循环
for (;;) {
// lock为0表示未有其他进程获取, cmp_set成果表示自己将value设置成功了
// 一般这里会用进程pid做为value,设置到lock上。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
//多处理器
if (ngx_ncpu > 1 ) {
for (n = 1; n < spin; n<<=1) {
for (i = 0; i < n; i++) {
ngx_cpu_pause() ;// 将cpu将为低功耗,注意此时进程还是处于占用cpu状态
}
//再次check下是否能获锁
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value) {
return;
}
}
}
//主动释放cpu,供其他进程使用,注意此时虽然让出了cpu但进程处于可运行状态,一旦调度到高进程依然立即可使用cpu,此时只是“暂时”让出cpu让其他进程运行下。
ngx_sched_yiled();
}
}
而这里 ngx_atomic_cmp_set这种原子操作,在x86机器上,一般都是通过嵌入汇编实现对硬件的操作,以达到原子操作的实现。
static ngx_inline ngx_atomic_uint_t
ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,
ngx_atomic_uint_t set)
{
u_char res;
__asm__ volatile (
NGX_SMP_LOCK
" cmpxchgl %3, %1; "
" sete %0; "
: "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory");
return res;
}
static ngx_inline ngx_atomic_int_t
ngx_atomic_fetch_add(ngx_atomic_t *value, ngx_atomic_int_t add)
{
__asm__ volatile (
NGX_SMP_LOCK
" xaddl %0, %1; "
: "+r" (add) : "m" (*value) : "cc", "memory");
return add;
}
而nginx在何处会用到这种自旋锁呢? 目前好像并没有看到使用场景。。。 [且慢]
而这种lock原子变量,也是一般会指向某个共享内存的地址,配合共享内存的使用.
2 共享内存
多进程之间可以使用同一片共享内存,共享内存也是通过底层的mmap构建而已:
type struct {
u_char *addr; //指向内存起始地址
size_t size;//共享内存大小(长度)
ngx_str_t name;//共享内存名字
ngx_log_t *log;
ngx_uint_t exists
};
创建
ngx_int_t ngx_shm_alloc(ngx_shm_t *shm)
{
shm->addr = (u_char*) mmap(NULL, shm->sizee, PROTO_READ|PROTO|WRITE, xx, -1,0);
if (shm->addr == MAP_FAILED){
return NGX_ERROR;
}
return NGX_OK;
}
而使用共享内存,需要有互斥机制,所以共享内存一般会搭配共享内存锁来使用,此时就需要引出相关互斥机制:linux提供了几种:原子变量(atomic), 信号量(sempahore),文件锁,通过结构体ngx_shmtx_t将其统一起来:
锁机制
typedef struct {
#if (NGX_HAVE_ATOMIC_OPS) //原子锁
ngx_atomic_t *lock;
# if (NGX_HAVEPOSIX_SEM) //信号量
ngx_aotmic_t *wait;
ngx_uint_t semphore;
sem_t sem;
#endif
#else
ngx_fd_t fd; //文件锁
u_char *name
#endif
ngx_uint_t spin;
}
在使用共享内存时,会在共享内存里先放上这么一把锁在其中:
在每次创建完共享内存后,都会有一个init初始化的过程,注意在ngx_init_cycle中,对共享内存做了统一的初始化:
ngx_init_cycle {
...;
if (ngx_shm_alloc(&shm_zone[i].shm) ! = NGX_OK) .. //创建
if (ngx_init_zone_pool(cycle, &shm_zone[i])!= NGX_OK) ..// 里面就包括了对shmtx的创建+初始化
}
static ngx_int_t
ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn) {
ngx_slab_pool_t *sp;
sp = (ngx_slab_pool_t*) zn->shm.addr; //注意,这里就是创建了shmtx
//这里shpool是ngx_slab_pool结构体,将其放在了shm的最开始的部分。而在shpool中就存放
了相关的ngx_shmtx_t结构体 (在nginx中使用共享内存都通过了slab的机制)
{
typedef struct {
....;
ngx_shmtx_t mutex; //注意是结构体不是指针
...;
} ngx_slab_pool_t ;
所以上面的,将shpool指向了shm开头的部分,也就是自动创建了该mutex。
}
if (ngx_shmtx_create(&sp->mutex, &sp->lock, file) != NGX_OK) ;; // 这里即是初始化shmtx锁。
=================================
对于shmtx的初始化有几个(参考shmtx的构成)原子锁初始化,sempore初始化,文件锁初始化
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char*name)
{
原子变量初始化
mtx->lock = &addr->lock; //原子变量:也是指向放在共享内存中的某个地址
// 信号量初始化
mtx->wait = &addr->wait;
if (sem_init(&mtx->sem, 1,0) == -1) { ...
} else {
mtx->semapohre = 1;
}
return NGX_OK;
}
所以准确地说,这里的“创建” 也只是分配一下该结构体的内存,初始化是将其结构体成员指向完成。
既然创建+初始化了,然后就是使用 (抢锁 + 放锁):
抢锁 有几个方式
1 互斥锁(进程一定获取到锁,否则睡眠or忙等)
2 尝试获锁(不一定要获取到)
//由于使用semaphore会将导致进程睡眠,故而这种try锁不能使用,只能用原子变量or文件锁(此处没列出文件锁的方式)
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid))
}
这里将通过原子操作(嵌入汇编)实现对硬件的操作。成与不成都会立即返回,所以需要返回值来判断是否获取成功与否
互斥锁:由于不获取OK进程是不会返回的,所以这里没有返回值,一旦返回肯定是获取成功了
否则进程会进入忙等(类似自旋锁) or 睡眠状态
而对于共享内存的操作,一般都需要这种锁:
void ngx_shmtx_lock(ngx_shmtx_t *mtx)
{
for (;;) {
if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
return;
}
if (ngx_ncpu > 1 ){
for (n =1; n < mtx->spin; n<<=1) {
for (i=0;i<n;i++) {
ngx_cpu_pause(); // 让cpu进入省电模式
}
if(*mtx->lock==0&&ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
return;
}
}
}
if (mtx->semaphore) { //支持信号量
while(sem_wait(&mtx->sem) == -1) { //sem_wait会让进程阻塞,睡眠
break;
}
continue // 继续进入for死循环
}
ngx_sched_yiled(); // 注意这是在 不支持信号量下会主动让出cpu 但还是处于可执行状态(没有被阻塞主,还是可以调度被cpu调度上,但此时主动切出去了cpu,但不会返回用户态,进程处于可运行状态,可以被选中后再次调度)
}
}
看这个锁:如果不支持信号量,其实和之前的自旋锁没有任何区别,都是忙等不让出 or 主动让出cpu切换(此时还是会被调度到,处于可运行状态), 总之不会主动返回。
而在支持信号量下,进程阻塞睡眠直到资源可用。
放锁:从加锁过程来看,放锁需要做的事情,一是置空原子变量,而是如果有进程因为semaphore而睡眠 需要通过sem_post让内核唤醒。
ngx_shmtx_unlock(ngx_tshmtx_t *mtx) { if (ngx_atomic_cmp_set(mtx->loc, ngx_pid, 0)) { //说明该进程之前持有锁否则会执行cmp失败 ngx_shmtx_wakeup(mtx); //尝试唤醒阻塞在该锁上的进程【信号量方式阻塞的进程)(如果有) } } static void ngx_shmtx_wakeup(shm_mtx_t *mtx) { if (!mtx->semaphore) { return; } for (;;) { wait = *mtx->wait; if (ngx_atomic_cmp_set(mtx->wait, wait, wait-1)) { break; } } if (sem_post(&mtx->sem) == -1) { //会立即返回,让内核唤醒其他进程(信号量的用法) } }
小结:
原子锁、文件锁、信号量 提供的是机制,而互斥锁、自旋锁、try锁是利用机制而来实现的目的。
在使用ngx_shmt_t时,都要三个步骤:创建分配、初始化、使用。
1 这里思考一个问题:为何在抢锁时候 ngx_atomic_cmp_set(lock , 0, pid)需要调用多次才ok,而放锁ngx_atomic_cmp_set(lock, pid, 0)只需要调用一次。
因为在抢锁时cmp_set不一定会成功返回,因为其他进程如果成功了,你就fail了。而在放锁时,
只有持锁进程会调用成功(一次+必然),其他进程都会失败,所以不管成功与否都只需要调用一次即可。
2 nginx使用的是try锁来避免惊群的,而不是spinlock(spinlock会忙等,不适合用在这个场景)
实际上spinlock的使用场景比较有限,目前nginx中其实并没有场景使用到。
3 nginx中涉及到共享内存操作的(比如统计、比如reqlimit等功能),使用的都是互斥锁。