Nginx被称为C++程序员必学的源码之一,我觉得名副其实,它的事件机制、内存管理、进程通信都可以说是顶级实践,非常值得学习。
Nginx源码比较多,本文只看几个重要的模块,更详细的内容请参考《深入理解nginx模块开发与架构》,这本书内容很全,本文只能是跟在后面做一点东施效颦的解读了。
Nginx的事件驱动架构
和传统服务器只将事件划分为连接建立和关闭事件不同,Nginx会将事件处理分成很多阶段进行:
划分方法如下:
- 将阻塞进程的方法按照相关的触发事件分解为两个阶段。第一个阶段用于调用非阻塞的方法处理事件,并且注册相应的fd到事件监听器中;第二个阶段用于处理fd的回调事件。
- 拆分阻塞事件。将一个大规模的阻塞事件通过网络事件或者定时器事件拆分成多个小事件。
- 将阻塞事件转换为定时器事件。将需要检查标识位的阻塞事件转换为定时检查的定时事件。
- 如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法
值得注意的是nginx没有使用多线程,所有的worker和master都是进程实现的。
进程同步
nginx在进程通信方面真的是教科书级别的案例,所有的进程通信方式都用了个遍。
因为一个进程要处理大量请求,所以nginx的处理方式是尽量不让进程休眠,所以自旋等待用的很多。
对于多线程而言,文件锁和信号量是不推荐使用的:文件锁很容易用错并且需要维护计数结构;信号量没有所有权(不等待信号量的线程也可以解锁信号量)。
nginx更多的也只是将其封装给多进程的ngx_shmtx_t互斥锁使用,实现了阻塞/非阻塞和先自旋再等待的功能而已。所以无论如何,文件锁和信号量能不用就不用。
共享内存
共享内存这里的操作有三种:使用mmap分配共享内存、以/dev/zero文件使用mmap映射共享内存、用shmget调用来分配共享内存。这三种先定了了哪个就用哪个。
从这里我们也可以看出这三种系统调用的区别:
- mmap匿名映射最简单也最推荐使用,但是有的posix系统不支持,mmap匿名映射几乎所有的unix系统都会保证返回填充0的页。但是注意这并不意味着malloc就会返回填充0的页了,因为mmap分配的页可能被重用。
- 以/dev/zero文件使用mmap映射共享内存,这其实也是某些unix系统实现匿名映射的方式。
- shmget支持最广泛,但是一般不推荐使用。
#if (NGX_HAVE_MAP_ANON)ngx_int_t
ngx_shm_alloc(ngx_shm_t *shm)
{shm->addr = (u_char *) mmap(NULL, shm->size,PROT_READ|PROT_WRITE,MAP_ANON|MAP_SHARED, -1, 0);if (shm->addr == MAP_FAILED) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"mmap(MAP_ANON|MAP_SHARED, %uz) failed", shm->size);return NGX_ERROR;}return NGX_OK;
}void
ngx_shm_free(ngx_shm_t *shm)
{if (munmap((void *) shm->addr, shm->size) == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"munmap(%p, %uz) failed", shm->addr, shm->size);}
}#elif (NGX_HAVE_MAP_DEVZERO)ngx_int_t
ngx_shm_alloc(ngx_shm_t *shm)
{ngx_fd_t fd;fd = open("/dev/zero", O_RDWR);if (fd == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"open(\"/dev/zero\") failed");return NGX_ERROR;}shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);if (shm->addr == MAP_FAILED) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"mmap(/dev/zero, MAP_SHARED, %uz) failed", shm->size);}if (close(fd) == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"close(\"/dev/zero\") failed");}return (shm->addr == MAP_FAILED) ? NGX_ERROR : NGX_OK;
}void
ngx_shm_free(ngx_shm_t *shm)
{if (munmap((void *) shm->addr, shm->size) == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"munmap(%p, %uz) failed", shm->addr, shm->size);}
}#elif (NGX_HAVE_SYSVSHM)#include <sys/ipc.h>
#include <sys/shm.h>ngx_int_t
ngx_shm_alloc(ngx_shm_t *shm)
{int id;id = shmget(IPC_PRIVATE, shm->size, (SHM_R|SHM_W|IPC_CREAT));if (id == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"shmget(%uz) failed", shm->size);return NGX_ERROR;}ngx_log_debug1(NGX_LOG_DEBUG_CORE, shm->log, 0, "shmget id: %d", id);shm->addr = shmat(id, NULL, 0);if (shm->addr == (void *) -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno, "shmat() failed");}if (shmctl(id, IPC_RMID, NULL) == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"shmctl(IPC_RMID) failed");}return (shm->addr == (void *) -1) ? NGX_ERROR : NGX_OK;
}void
ngx_shm_free(ngx_shm_t *shm)
{if (shmdt(shm->addr) == -1) {ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,"shmdt(%p) failed", shm->addr);}
}#endif
原子操作
对于x86上的原子操作,nginx使用总线锁来实现。
总线锁(见https://www.zhihu.com/column/p/24146167):
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// 将lock和old比较,如果相等则设置lock的值为set" cmpxchgl %3, %1; "// 如果相等设置返回值res" sete %0; ": "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory");return res;
}
自旋锁
自旋锁的使用场景在于不想陷入睡眠的nginx进程。如果进程陷入睡眠,那么下一次被唤醒的时间是不可控的(或者长到无法接受),因此,自旋等待是更好的方法。
这里的PAUSE是在许多架构体系中专门为了自旋锁而提供的指令,它会告诉CPU现在处于自旋锁等待状态,通常一些CPU会将自己置于节能状态,降低功耗。
实现如下:
void
ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
{
// lock为0表示锁被释放
// lock不为0表示锁被占用
// value表示希望当锁没有被任何进程持有时(lock为0),将lock设置为value则表示持有了锁
// spin表示在smp架构下,没有拿到锁时,等待其他处理器释放锁的时间
#if (NGX_HAVE_ATOMIC_OPS)ngx_uint_t i, n;for ( ;; ) {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++) {// 调用PAUSE指令 PAUSE指令是REP NOP(重复空操作)指令的一种特殊形式,其操作码为0xF3 0x90ngx_cpu_pause();}if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {return;}}}ngx_sched_yield();}#else#if (NGX_THREADS)#error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !#endif#endif
channel
nginx channel就是常见的socket通信,这里就不说了。
信号
通过sigaction,nginx会注册一组有回调的信号:
ngx_int_t
ngx_init_signals(ngx_log_t *log)
{ngx_signal_t *sig;struct sigaction sa;for (sig = signals; sig->signo != 0; sig++) {ngx_memzero(&sa, sizeof(struct sigaction));if (sig->handler) {sa.sa_sigaction = sig->handler;sa.sa_flags = SA_SIGINFO;} else {sa.sa_handler = SIG_IGN;}sigemptyset(&sa.sa_mask);// 注册信号if (sigaction(sig->signo, &sa, NULL) == -1) {
#if (NGX_VALGRIND)ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,"sigaction(%s) failed, ignored", sig->signame);
#elsengx_log_error(NGX_LOG_EMERG, log, ngx_errno,"sigaction(%s) failed", sig->signame);return NGX_ERROR;
#endif}}return NGX_OK;
}
信号量
信号量的初始化调用int sem_init(sem_t *sem, int pshared, unsigned int value);
其中 pshared标明了这个信号量被进程还是线程使用。
nginx将信号量当作互斥锁使用。
信号量是如何实现互斥锁功能的呢?例如,最初的信号量sem值为0,调用sem_post方法 将会把sem值加1,这个操作不会有任何阻塞;调用sem_wait方法将会把信号量sem的值减1, 如果sem值已经小于或等于0了,则阻塞住当前进程(进程会进入睡眠状态),直到其他进程 将信号量sem的值改变为正数后,这时才能继续通过将sem减1而使得当前进程继续向下执 行。因此,sem_post方法可以实现解锁的功能,而sem_wait方法可以实现加锁的功能。
文件锁
文件锁可以锁住部分文件区域(fcntl)或者整个文件(flock)。
fcntl和flock的区别如下:
- flock(2)仅可对整个文件加锁;fcntl(2)可对从单一字节到整个文件范围内的任意区域加锁。
- 通过flock(2)设置文件锁不受文件的打开访问模式标志影响;通过fcntl(2)设置文件锁时,锁的类型必须与文件的打开访问模式标志一致,即F_RDLCK/F_WRLCK分别对应于O_RDONLY/O_WRONLY;先后设置读写锁时,访问模式标志应为O_RDWR。
- 同一进程可以通过再次调用flock(2)或fcntl(2)的方式,对同一文件的共享锁与独占锁之间进行相互转换;内核保证fcntl(2)的原子性,但不保证flock(2)的原子性。
- 通过flock(2)设置的文件锁与系统文件表项相关联,而非进程的文件描述符或文件(inode)自身;通过fcntl(2)设置的文件锁与进程的文件描述符表项与系统的inode表项相关联,而非系统文件表项。
对于文件锁,Nginx封装了3个方法:
- ngx_trylock_fd实现了不会阻塞进程、不会使得进程 进入睡眠状态的互斥锁;
- ngx_lock_fd提供的互斥锁在锁已经被其他进程拿到时将会导致当前 进程进入睡眠状态,直到顺利拿到这个锁后,当前进程才会被Linux内核重新调度,所以它 是阻塞操作;
- ngx_unlock_fd用于释放互斥锁。
ngx_shmtx_t互斥锁
为了防止使用操作系统的信号量和互斥锁时导致当前进程被饿死(单线程架构),所以nginx实现了ngx_shmtx_t,用于多进程之间的同步。
这里使用原子操作、信号量、文件锁配合的方式实现:
typedef struct {
#if (NGX_HAVE_ATOMIC_OPS)// 支持原子操作,使用原子变量ngx_atomic_t *lock;
#if (NGX_HAVE_POSIX_SEM)// 支持原子操作和信号量// 如果支持信号量,那么在原子自旋等待失败后睡眠ngx_atomic_t *wait;ngx_uint_t semaphore;sem_t sem;
#endif
#else// 不支持原子操作,使用文件锁ngx_fd_t fd;u_char *name;
#endifngx_uint_t spin;
} ngx_shmtx_t;
如果ngx_shmtx_lock方法在运行一段 时间后,如果其他进程始终不放弃锁,那么当前进程将有可能强制性地获得到这把锁。我们看这里的代码。
文件锁
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
{if (mtx->name) {if (ngx_strcmp(name, mtx->name) == 0) {mtx->name = name;return NGX_OK;}// name与 mtx->name不一致,销毁之前的文件锁ngx_shmtx_destroy(mtx);}mtx->fd = ngx_open_file(name, NGX_FILE_RDWR, NGX_FILE_CREATE_OR_OPEN,NGX_FILE_DEFAULT_ACCESS);if (mtx->fd == NGX_INVALID_FILE) {ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_errno,ngx_open_file_n " \"%s\" failed", name);return NGX_ERROR;}// 只需要inode信息,所以调用unlink删除文件if (ngx_delete_file(name) == NGX_FILE_ERROR) {ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,ngx_delete_file_n " \"%s\" failed", name);}mtx->name = name;return NGX_OK;
}void
ngx_shmtx_destroy(ngx_shmtx_t *mtx)
{if (ngx_close_file(mtx->fd) == NGX_FILE_ERROR) {ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,ngx_close_file_n " \"%s\" failed", mtx->name);}
}ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{ngx_err_t err;err = ngx_trylock_fd(mtx->fd);if (err == 0) {return 1;}if (err == NGX_EAGAIN) {return 0;}#if __osf__ /* Tru64 UNIX */if (err == NGX_EACCES) {return 0;}#endifngx_log_abort(err, ngx_trylock_fd_n " %s failed", mtx->name);return 0;
}void
ngx_shmtx_lock(ngx_shmtx_t *mtx)
{ngx_err_t err;err = ngx_lock_fd(mtx->fd);if (err == 0) {return;}ngx_log_abort(err, ngx_lock_fd_n " %s failed", mtx->name);
}void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{ngx_err_t err;err = ngx_unlock_fd(mtx->fd);if (err == 0) {return;}ngx_log_abort(err, ngx_unlock_fd_n " %s failed", mtx->name);
}ngx_uint_t
ngx_shmtx_force_unlock(ngx_shmtx_t *mtx, ngx_pid_t pid)
{return 0;
}#endif
信号量+原子变量
#if (NGX_HAVE_ATOMIC_OPS)static void ngx_shmtx_wakeup(ngx_shmtx_t *mtx);ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
{mtx->lock = &addr->lock;if (mtx->spin == (ngx_uint_t) -1) {// 不能使用信号量,返回成功return NGX_OK;}mtx->spin = 2048;#if (NGX_HAVE_POSIX_SEM)mtx->wait = &addr->wait;if (sem_init(&mtx->sem, 1, 0) == -1) {ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,"sem_init() failed");} else {mtx->semaphore = 1;}#endifreturn NGX_OK;
}void
ngx_shmtx_destroy(ngx_shmtx_t *mtx)
{
#if (NGX_HAVE_POSIX_SEM)if (mtx->semaphore) {if (sem_destroy(&mtx->sem) == -1) {ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,"sem_destroy() failed");}}#endif
}ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{// 这里操作并非原子,ngx_atomic_cmp_set会保证*mtx->lock == 0 return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
}void
ngx_shmtx_lock(ngx_shmtx_t *mtx)
{ngx_uint_t i, n;ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx lock");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();}if (*mtx->lock == 0&& ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)){return;}}}#if (NGX_HAVE_POSIX_SEM)if (mtx->semaphore) {(void) ngx_atomic_fetch_add(mtx->wait, 1);if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {(void) ngx_atomic_fetch_add(mtx->wait, -1);return;}ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,"shmtx wait %uA", *mtx->wait);while (sem_wait(&mtx->sem) == -1) {ngx_err_t err;err = ngx_errno;if (err != NGX_EINTR) {ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, err,"sem_wait() failed while waiting on shmtx");break;}}ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,"shmtx awoke");continue;}#endifngx_sched_yield();}
}void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{if (mtx->spin != (ngx_uint_t) -1) {ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx unlock");}if (ngx_atomic_cmp_set(mtx->lock, ngx_pid, 0)) {ngx_shmtx_wakeup(mtx);}
}ngx_uint_t
ngx_shmtx_force_unlock(ngx_shmtx_t *mtx, ngx_pid_t pid)
{ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,"shmtx forced unlock");if (ngx_atomic_cmp_set(mtx->lock, pid, 0)) {ngx_shmtx_wakeup(mtx);return 1;}return 0;
}static void
ngx_shmtx_wakeup(ngx_shmtx_t *mtx)
{
#if (NGX_HAVE_POSIX_SEM)ngx_atomic_uint_t wait;if (!mtx->semaphore) {return;}for ( ;; ) {wait = *mtx->wait;if ((ngx_atomic_int_t) wait <= 0) {return;}if (ngx_atomic_cmp_set(mtx->wait, wait, wait - 1)) {break;}}ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,"shmtx wake %uA", wait);if (sem_post(&mtx->sem) == -1) {ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,"sem_post() failed while wake shmtx");}#endif
}
内存池
nginx内存池的设计很特殊,它的生命周期跟随请求或者连接。
这是因为连接往往是短暂的,因此内存池带来了分配内存的便利,又不会因为生命周期过长带来内存泄漏。
它的构成如下:
typedef struct {// 可分配的位置u_char *last;// 当前内存池的结尾u_char *end;// 下一个小块内存池ngx_pool_t *next;// 分配失败的次数,如果超过 4 ngx_pool_s 中的cunrrent指向nextngx_uint_t failed;
} ngx_pool_data_t;struct ngx_pool_s {// 描述小块内存池ngx_pool_data_t d;// 大内存池和小内存池的标准size_t max;// 指向第一个小块内存池ngx_pool_t *current;ngx_chain_t *chain;// 大内存池链表ngx_pool_large_t *large;ngx_pool_cleanup_t *cleanup;ngx_log_t *log;
};
流程如下:
这里很明显有一些内存浪费。
小块内存的大小默认(也是最大大小)是 (ngx_pagesize - 1) 也就是4095,主要是为了防止内存碎片过大。
master和worker的事件循环
worker
master
void
ngx_master_process_cycle(ngx_cycle_t *cycle)
{char *title;u_char *p;size_t size;ngx_int_t i;ngx_uint_t sigio;sigset_t set;struct itimerval itv;ngx_uint_t live;ngx_msec_t delay;ngx_core_conf_t *ccf;sigemptyset(&set);sigaddset(&set, SIGCHLD);sigaddset(&set, SIGALRM);sigaddset(&set, SIGIO);sigaddset(&set, SIGINT);sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,"sigprocmask() failed");}sigemptyset(&set);size = sizeof(master_process);for (i = 0; i < ngx_argc; i++) {size += ngx_strlen(ngx_argv[i]) + 1;}title = ngx_pnalloc(cycle->pool, size);if (title == NULL) {/* fatal */exit(2);}p = ngx_cpymem(title, master_process, sizeof(master_process) - 1);for (i = 0; i < ngx_argc; i++) {*p++ = ' ';p = ngx_cpystrn(p, (u_char *) ngx_argv[i], size);}ngx_setproctitle(title);ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);ngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_RESPAWN);ngx_start_cache_manager_processes(cycle, 0);ngx_new_binary = 0;delay = 0;sigio = 0;live = 1;for ( ;; ) {if (delay) {if (ngx_sigalrm) {sigio = 0;delay *= 2;ngx_sigalrm = 0;}ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"termination cycle: %M", delay);itv.it_interval.tv_sec = 0;itv.it_interval.tv_usec = 0;itv.it_value.tv_sec = delay / 1000;itv.it_value.tv_usec = (delay % 1000 ) * 1000;if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,"setitimer() failed");}}ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");sigsuspend(&set);ngx_time_update();ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"wake up, sigio %i", sigio);if (ngx_reap) {// 需要监控所有子进程ngx_reap = 0;ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");// 管理子进程live = ngx_reap_children(cycle);}// 所有子进程都退出或者程序终止if (!live && (ngx_terminate || ngx_quit)) {ngx_master_process_exit(cycle);}// 程序终止但还有子进程没退出if (ngx_terminate) {if (delay == 0) {delay = 50;}if (sigio) {sigio--;continue;}sigio = ccf->worker_processes + 2 /* cache processes */;// 等待,然后发送kill信号if (delay > 1000) {ngx_signal_worker_processes(cycle, SIGKILL);} else {ngx_signal_worker_processes(cycle,ngx_signal_value(NGX_TERMINATE_SIGNAL));}continue;}if (ngx_quit) {ngx_signal_worker_processes(cycle,ngx_signal_value(NGX_SHUTDOWN_SIGNAL));ngx_close_listening_sockets(cycle);continue;}if (ngx_reconfigure) {// 重新初始化ngx_reconfigure = 0;if (ngx_new_binary) {ngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_RESPAWN);ngx_start_cache_manager_processes(cycle, 0);ngx_noaccepting = 0;continue;}ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");cycle = ngx_init_cycle(cycle);if (cycle == NULL) {cycle = (ngx_cycle_t *) ngx_cycle;continue;}ngx_cycle = cycle;ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,ngx_core_module);// 初始化新的workerngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_JUST_RESPAWN);ngx_start_cache_manager_processes(cycle, 1);/* allow new processes to start */// 挂起一段时间ngx_msleep(100);live = 1;// 结束旧workerngx_signal_worker_processes(cycle,ngx_signal_value(NGX_SHUTDOWN_SIGNAL));}if (ngx_restart) {ngx_restart = 0;// 从旧的worker初始化新的workerngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_RESPAWN);ngx_start_cache_manager_processes(cycle, 0);live = 1;}if (ngx_reopen) {ngx_reopen = 0;ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");ngx_reopen_files(cycle, ccf->user);ngx_signal_worker_processes(cycle,ngx_signal_value(NGX_REOPEN_SIGNAL));}if (ngx_change_binary) {ngx_change_binary = 0;ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);}if (ngx_noaccept) {ngx_noaccept = 0;ngx_noaccepting = 1;ngx_signal_worker_processes(cycle,ngx_signal_value(NGX_SHUTDOWN_SIGNAL));}}
}
解决过期事件
epoll可能在处理事件的过程中有事件过期了,不能简单的把事件的文件描述符置为-1, 因为中间可能有事件重新打开这个文件描述符,这就导致新的描述符处理了旧的事件。
nginx通过添加标识位instance解决了这个问题。
缓存时间
nginx通过维护一个时间变量表示当前时间,如果设置了timer_resolution,那么就会通过settime系统调用定时更新resolution标识位,在ngx_epoll_process_events时更新缓存时间。
事件循环的优先级
惊群问题
slab内存和nginx缓存
slab内存
nginx在共享内存之上引入了slab内存来作为多进程复杂数据结构共享的抽象。
它的内存管理如下:
- 把整块内存按4KB分为许多页,这样,如果每一页只存放一种固定大小的内存块, 由于一页上能够分配的内存块数量是很有限的,所以可以在页首上用bitmap方式,按二进制 位表示页上对应位置的内存块是否在使用中。只是遍历bitmap二进制位去寻找页上的空闲内存块,使得消耗的时间很有限,例如bitmap占用的内存空间小导致CPU缓存命中率高,可以 按32或64位这样的总线长度去寻找空闲位以减少访问次数等。
- 基于空间换时间的思想,slab内存分配器会把请求分配的内存大小简化为极为有限的 几种(简化的方法有很多,例如可以按照fibonacci方法进行),而Nginx slab是按2的倍数, 将内存块分为8、16、32、64…字节,当申请的字节数大于8小于等于16时,就会使用16字 节的内存块,以此类推。所以,一种页面若存放的内存块大小为N字节,那么,使用者申请 的内存在N/2+1与N之间时,都将使用这种页面。这样最多会造成一倍内存的浪费,但使得 页种类大大减少了,这会降低碎片的产生,提高内存的利用率
- 让有限的几种页面构成链表,且各链表按序保存在数组中,这样一来,用直接寻址 法就可以快速找到。在Nginx slab中,用slots数组来存放链表首页。例如,如果申请的内存大 小为30字节,那么根据最小的内存块为8字节,可以算出从小到大第3种内存块存放的内存大 小为32字节,符合要求,从slots数组中取第3个元素则可以寻找到32字节的页面。
- 这些页面中分为空闲页、半满页、全满页。为什么要这么划分呢?因为上述的同种 页面链表不应当包含太多元素,否则分配内存时遍历链表一样非常耗时。所以,全满页应当 脱离链表,分配内存时不应当再访问到它。空闲页应该是超然的,如果这个页面曾经为32字 节的内存块服务,在它又成为空闲页时,下次便可以为128字节的内存块服务。因此,所有 的空闲页会单独构成一个空闲页链表。这里slots数组采用散列表的思想,用快速的直接寻址 方式将半满页展现在使用者面前。
- 虽然大部分情况下申请分配的内存块是小于4KB的,但极个别可能会有一些大于 4KB的内存分配请求,拒绝它则太粗暴了。对于此,可以用遍历空闲页链表寻找地址连续的 空闲页来分配,例如需要分配11KB的内存时,则遍历到3个地址连续的空闲页即可。
通过共享内存分配的空间布局如下:
slab被划分成四种内存块:
在slab中分配内存的流程如下: