nginx 自旋锁 & 共享内存&多进程通信相关笔记

自旋锁

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等功能),使用的都是互斥锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值