Memcache-网络线程模型-源码分析

memcached-version-1.4.25

介绍

memcache 网络模型是典型的单进程多线程模型,采用libevent处理网络请求,主进程负责将新来的连接分配给work线程,work线程负责处理连接,有点类似与负载均衡,通过主进程分发到对应的工作线程.

                                   主进程(master)
              
                 |                |                 |                  |
             phread1(work)    phread2(work)     phread3(work)      phread4(work) 

数据结构

memcache 会给每个线程都会创建一个 LIBEVENT_THREAD 线程结构体

typedef struct {
    pthread_t thread_id;        /* 线程id */
    struct event_base *base;    /* libevent handle this thread uses */
    struct event notify_event;  /* 注册事件 */
    int notify_receive_fd;      /* 读pipe管道文件描述符 */
    int notify_send_fd;         /* 写pipe管道文件描述符 */
    struct thread_stats stats;  /* 线程统计信息结构体,每个线程都会有自己的统计信息 */
    struct conn_queue *new_conn_queue; /* 当前线程待处理的连接队列,主线程负责将新的连接插入到该队列 */
    cache_t *suffix_cache;      /* suffix cache */
} LIBEVENT_THREAD;

memcache 给每一个网络连接都会创建一个 conn 结构体

typedef struct conn conn;
struct conn {
    int    sfd; // 当前连接文件描述符
    sasl_conn_t *sasl_conn;
    bool authenticated;
    enum conn_states  state; // 当前连接状态
    enum bin_substates substate; // key、value 处理类型
    rel_time_t last_cmd_time; // 最后一次访问时间
    struct event event; // 注册连接监听事件
    short  ev_flags;    //监听事件类型
    short  which;   /** which events were just triggered */

    char   *rbuf;   /* 网络连接读取的数据存放缓冲区地址 */
    char   *rcurr;  /* 当前读取缓冲区的位置 */
    int    rsize;   /* 每次从网络连接读取多少数据到缓冲区 */
    int    rbytes;  /* 缓冲区剩余待处理的字节数 */
    
    /* 跟上面读取一样,只不过这个是往客户端写 */
    char   *wbuf;
    char   *wcurr;
    int    wsize;
    int    wbytes;
    enum conn_states  write_and_go; /* 完成回写客户端之后赋值的状态,但是我看源码里并没有判断此状态 */
    void   *write_and_free; /** free this memory after finishing writing */

    char   *ritem;  /* 指向缓冲区待处理的数据位置 */
    int    rlbytes; /* 每次程序处理需要读取的字节数, 先从缓冲区buf读取,如果不够或则为空,则从网络连接里在读取 */

    void   *item;     /* 指向内存item指针  */

    //..........

    enum protocol protocol;   /* 协议包类型 (字符串、二进制) */
    enum network_transport transport; /* 网络连接类型 (TCP、UDP) */

   //..........

    bool   noreply;   /* 是否给客户端答复状态 */
    /* current stats command */
    struct {
        char *buffer;
        size_t size;
        size_t offset;
    } stats;

    /* 如果是二进制包, 则保存二进制包头 */
    protocol_binary_request_header binary_header;
    uint64_t cas; /* the cas to return */
    short cmd; /* 当前的命令类型(get、set、add) */
    int opaque;
    int keylen; /* key长度 */
    conn   *next;     /* Used for generating a list of conn structures */
    LIBEVENT_THREAD *thread; /* 当前连接属于那个线程的,保存对应线程的指针 */
};

新连接如何分配处理?

主进程负责监听端口如果有新的连接过来会先进行分配这个连接由那个work线程处理,确定一个work线程之后会把这个连接打包成一个 CQ_ITEM 结构体,然后丢给对应的work线conn_queue队列(上面线程结构体有这个属性),work线程从队列取出该结构体,获取一些参数值,然后创建一个 conn 结构体,监听并开始处理.

CQ_ITEM 结构体

typedef struct conn_queue_item CQ_ITEM;
struct conn_queue_item {
    int               sfd;         /* 文件描述符 */
    enum conn_states  init_state;  /* 连接状态 */
    int               event_flags; /* 监听事件类型 EV_READ | EV_PERSIST */
    int               read_buffer_size; /* 每次缓冲区读取size */
    enum network_transport     transport; /* TCP 或 UDP */
    CQ_ITEM          *next; /* 下一个 cq_item */
}

conn_queue 队列结构体,用于指向分配给自己 CQ_ITEM

typedef struct conn_queue CQ;
struct conn_queue {
    CQ_ITEM *head;
    CQ_ITEM *tail;
    pthread_mutex_t lock;
};

memcache 网络线程模型

memcache 网络线程模型

memcache 线程模型初始化

网络连接数初始化函数 conn_init ,就是设定最大连接数

main_base = event_init(); //主进程even事件初始化

static void conn_init(void) {
    /* We're unlikely to see an FD much higher than maxconns. */
    int next_fd = dup(1);
    int headroom = 10;      /* account for extra unexpected open FDs */
    struct rlimit rl;
    
    // 默认最大连接数
    // settings.maxconns 启动memcache的时候指定
    max_fds = settings.maxconns + headroom + next_fd;
    
    // 先尝试获取系统进程最大打开文件描述符数
    // 如果获取成功则按进程最大打开文件描述符
    // 设置最大连接数
    if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
        max_fds = rl.rlim_max;
    } else {
        fprintf(stderr, "Failed to query maximum file descriptor; "
                       "falling back to maxconns\n");
    }

   close(next_fd);
    
   // 根据最大文件描述符数量,创建conn结构体指针数组
   if ((conns = calloc(max_fds, sizeof(conn *))) == NULL) {
        fprintf(stderr, "Failed to allocate connection structures\n");
       /* This is unrecoverable so bail out early. */
        exit(1);
   }
}

初始化线程函数 memcached_thread_init

// settings.num_threads 线程数
// main_base 主进程事件
void memcached_thread_init(int nthreads, struct event_base *main_base) {
    int         i;
    int         power;
    
    // 初始化锁
    for (i = 0; i < POWER_LARGEST; i++) {
        pthread_mutex_init(&lru_locks[i], NULL);
    }
    pthread_mutex_init(&worker_hang_lock, NULL);
    pthread_mutex_init(&init_lock, NULL);
    pthread_cond_init(&init_cond, NULL);
    pthread_mutex_init(&cqi_freelist_lock, NULL);
    cqi_freelist = NULL;

    /* 根据线程数,设定hash表段锁的颗粒度 */
    if (nthreads < 3) {
        power = 10;
    } else if (nthreads < 4) {
        power = 11;
    } else if (nthreads < 5) {
        power = 12;
    } else {
        /* 8192 buckets, and central locks don't scale much past 5 threads */
        power = 13;
    }
    // 不能超过最大值 hashpower = 16
    if (power >= hashpower) {
        fprintf(stderr, "Hash table power size (%d) cannot be equal to or less than item lock table (%d)\n", hashpower, power);
        fprintf(stderr, "Item lock table grows with `-t N` (worker threadcount)\n");
        fprintf(stderr, "Hash table grows with `-o hashpower=N` \n");
        exit(1);
    }
    
    //hash表item锁数量
    item_lock_count = hashsize(power); //#define hashsize(n) ((ub4)1<<(n))
    item_lock_hashpower = power;
    //申请item锁
    item_locks = calloc(item_lock_count, sizeof(pthread_mutex_t));
    if (! item_locks) {
        perror("Can't allocate item locks");
        exit(1);
    }
    //初始化item锁
    for (i = 0; i < item_lock_count; i++) {
        pthread_mutex_init(&item_locks[i], NULL);
    }
    // 根据线程数,创建线程结构体
    threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));
    if (! threads) {
        perror("Can't allocate thread descriptors");
        exit(1);
    }
    // 保存主进程的事件及线程id
    dispatcher_thread.base = main_base;
    dispatcher_thread.thread_id = pthread_self();
    
    // 根据线程数创建pipe管道,每个线程都监听自己管道的文件描述符
    for (i = 0; i < nthreads; i++) {
        int fds[2];
        if (pipe(fds)) {
            perror("Can't create notify pipe");
            exit(1);
        }
        //读写 pipe fd
        threads[i].notify_receive_fd = fds[0];
        threads[i].notify_send_fd = fds[1];
        //设置线程监听事件及创建该线程连接队列等.
        setup_thread(&threads[i]);
        /* Reserve three fds for the libevent base, and two for the pipe */
        stats.reserved_fds += 5;
    }

    /* 开始创建线程 */
    for (i = 0; i < nthreads; i++) {
        create_worker(worker_libevent, &threads[i]);
    }

    /* 等待所有线程创建完毕之后,再返回 */
    pthread_mutex_lock(&init_lock);
    wait_for_thread_registration(nthreads);
    pthread_mutex_unlock(&init_lock);
}

设置线程监听事件及创建该线程连接队列函数 setup_thread

static void setup_thread(LIBEVENT_THREAD *me) {
    //初始化当前线程的event事件
    me->base = event_init();
    if (! me->base) {
        fprintf(stderr, "Can't allocate event base\n");
        exit(1);
    }

    /* 设置一个pipe管道监听事件,这就是上面说的,当有一个新的连接分配给
       当前线程时,就会通知该文件描述符 me->notify_receive_fd 调用回调
       函数 thread_libevent_process 参数就是 me 当前线程结构体指针 */
    event_set(&me->notify_event, me->notify_receive_fd,
              EV_READ | EV_PERSIST, thread_libevent_process, me);
    event_base_set(me->base, &me->notify_event);

    if (event_add(&me->notify_event, 0) == -1) {
        fprintf(stderr, "Can't monitor libevent notify pipe\n");
        exit(1);
    }
    
    //创建一个连接队列
    me->new_conn_queue = malloc(sizeof(struct conn_queue));
    if (me->new_conn_queue == NULL) {
        perror("Failed to allocate memory for connection queue");
        exit(EXIT_FAILURE);
    }
    //初始化连接队列
    cq_init(me->new_conn_queue);

    if (pthread_mutex_init(&me->stats.mutex, NULL) != 0) {
        perror("Failed to initialize mutex");
        exit(EXIT_FAILURE);
    }
    
    // 创建一块 cache
    me->suffix_cache = cache_create("suffix", SUFFIX_SIZE, sizeof(char*),
                                    NULL, NULL);
    if (me->suffix_cache == NULL) {
        fprintf(stderr, "Failed to create suffix cache\n");
        exit(EXIT_FAILURE);
    }
}

创建线程函数 create_worker

static void create_worker(void *(*func)(void *), void *arg) {
   pthread_attr_t  attr;
   int             ret;

   pthread_attr_init(&attr);
   // 创建线程,线程函数指针 func = worker_libevent 
   if ((ret = pthread_create(&((LIBEVENT_THREAD*)arg)->thread_id, &attr, func, arg)) != 0) {
       fprintf(stderr, "Can't create thread: %s\n",
                strerror(ret));
        exit(1);
   }
}

worke 线程执行函数入口

static void *worker_libevent(void *arg) {
    LIBEVENT_THREAD *me = arg;

    /* Any per-thread setup can happen here; memcached_thread_init() will block until
     * all threads have finished initializing.
     */
    register_thread_initialized();
    
    // 实际上就是进行 event_loop 开始监听每个线程事件
    event_base_loop(me->base, 0);
    
    return NULL;
}

现在工作线程初始化完毕,开始初始化主进程(线程),主线程初始化就是正常socket模式监听端口,然后设置event监听事件

(1) sfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)
(2) setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags));
(3) bind(sfd, next->ai_addr, next->ai_addrlen)
(4) listen(sfd, settings.backlog)

创建一个连接 conn 结构体,因为本机打开了一个端口产生了一个网络文件描述符,所以给改文件描述符创建一个 conn,并加入主线程 main_base 事件里面了,进行监听,处理新的连接分配工作
conn_new (sfd, conn_listening, EV_READ | EV_PERSIST, 1,transport, main_base)

创建连接函数 conn_new

// sfd 网络文件描述符
// init_state 连接状态 (主线程创建的本机端口的默认连接状态都会是 conn_listening ) 就是代表只把新的连接分配到work线程,不作其它处理
// event_flags 事件监听类型
// read_buffer_size 读到缓冲区size
// transport TCP 、 UDP
// base 主线程的 event 或者 work线程的 event

conn *conn_new(const int sfd, enum conn_states init_state,
                const int event_flags,
                const int read_buffer_size, enum network_transport transport,
                struct event_base *base) {
    conn *c;

    assert(sfd >= 0 && sfd < max_fds);
    c = conns[sfd];

    if (NULL == c) {
        // 创建一个conn结构体,并指向
        if (!(c = (conn *)calloc(1, sizeof(conn)))) {
            STATS_LOCK();
            stats.malloc_fails++;
            STATS_UNLOCK();
            fprintf(stderr, "Failed to allocate connection object\n");
            return NULL;
        }
        MEMCACHED_CONN_CREATE(c);
        
        // 初始化每个字段
        c->rbuf = c->wbuf = 0;
        c->ilist = 0;
        c->suffixlist = 0;
        c->iov = 0;
        c->msglist = 0;
        c->hdrbuf = 0;

        c->rsize = read_buffer_size;
        c->wsize = DATA_BUFFER_SIZE;
        c->isize = ITEM_LIST_INITIAL;
        c->suffixsize = SUFFIX_LIST_INITIAL;
        c->iovsize = IOV_LIST_INITIAL;
        c->msgsize = MSG_LIST_INITIAL;
        c->hdrsize = 0;
        // 创建读写缓冲区
        c->rbuf = (char *)malloc((size_t)c->rsize);
        c->wbuf = (char *)malloc((size_t)c->wsize);
        c->ilist = (item **)malloc(sizeof(item *) * c->isize);
        c->suffixlist = (char **)malloc(sizeof(char *) * c->suffixsize);
        c->iov = (struct iovec *)malloc(sizeof(struct iovec) * c->iovsize);
        c->msglist = (struct msghdr *)malloc(sizeof(struct msghdr) * c->msgsize);

        if (c->rbuf == 0 || c->wbuf == 0 || c->ilist == 0 || c->iov == 0 ||
                c->msglist == 0 || c->suffixlist == 0) {
            conn_free(c);
            STATS_LOCK();
            stats.malloc_fails++;
            STATS_UNLOCK();
            fprintf(stderr, "Failed to allocate buffers for connection\n");
            return NULL;
        }
        
        // 统计增加
        STATS_LOCK();
        stats.conn_structs++;
        STATS_UNLOCK();
        
        // 保存当前网络文件描述符
        c->sfd = sfd;
        // 指针保存到conns
        conns[sfd] = c;
    }

    c->transport = transport;
    c->protocol = settings.binding_protocol;

    /* unix socket mode doesn't need this, so zeroed out.  but why
     * is this done for every command?  presumably for UDP
     * mode.  */
    if (!settings.socketpath) {
        c->request_addr_size = sizeof(c->request_addr);
    } else {
        c->request_addr_size = 0;
    }

    //.......................

    c->state = init_state;
    c->rlbytes = 0;
    c->cmd = -1;
    c->rbytes = c->wbytes = 0;
    c->wcurr = c->wbuf;
    c->rcurr = c->rbuf;
    c->ritem = 0;
    c->icurr = c->ilist;
    c->suffixcurr = c->suffixlist;
    c->ileft = 0;
    c->suffixleft = 0;
    c->iovused = 0;
    c->msgcurr = 0;
    c->msgused = 0;
    c->authenticated = false;

    c->write_and_go = init_state;
    c->write_and_free = 0;
    c->item = 0;

    c->noreply = false;
    
    // 设置监听事件,回调函数 event_handler 
    event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
    event_base_set(base, &c->event);
    c->ev_flags = event_flags;

    if (event_add(&c->event, 0) == -1) {
        perror("event_add");
        return NULL;
    }

    STATS_LOCK();
    stats.curr_conns++;
    stats.total_conns++;
    STATS_UNLOCK();

    MEMCACHED_CONN_ALLOCATE(c->sfd);

    return c;
}

创建完主线程 conn 之后,主线程开始进入事件监听环节 event_base_look(main_base, 0)

说明

至此 (work线程 及 主线程) 全部初始化完毕,并设置完成一些自己的监听事件

(1) 网络连接分配 - 回调函数 -> thread_libevent_process
(2) 网络连接处理 - 回调函数 -> event_handler

目前工作线程只监听自己的管道文件描述符,当管道文件描述符有活动时执行回调 thread_libevent_process ,然后 conn_new() 创建一个网络连接并加入到当前工作线程的监听事件集合里面,并设置回调函数为 event_handler,这样的话工作线程除了监听管道文件描述符,还会监听网络连接文件描述符,哪个文件描述符有活动,就执行那个文件描述符所绑定的回调函数。

1、event_handler 函数

void event_handler(const int fd, const short which, void *arg) {
    conn *c;

    c = (conn *)arg;
    assert(c != NULL);

    c->which = which;

    /* sanity */
    if (fd != c->sfd) {
        if (settings.verbose > 0)
            fprintf(stderr, "Catastrophic: event fd doesn't match conn fd!\n");
        conn_close(c);
        return;
    }
    
    //调用状态机函数
    drive_machine(c);

    /* wait for next event */
    return;
}

drive_machine 状态机函数,就是根据连接的状态进行对应的处理

// 新连接创建默认的状态是 conn_listening 所以这里只看这个状态的处理
static void drive_machine(conn *c) {
    bool stop = false;
    int sfd;
    socklen_t addrlen;
    struct sockaddr_storage addr;
    int nreqs = settings.reqs_per_event;
    int res;
    const char *str;
#ifdef HAVE_ACCEPT4
    static int  use_accept4 = 1;
#else
    static int  use_accept4 = 0;
#endif

    assert(c != NULL);
    
    //循环处理conn连接
    while (!stop) {

        switch(c->state) {
        case conn_listening: //新连接分配状态
            addrlen = sizeof(addr);
#ifdef HAVE_ACCEPT4
            if (use_accept4) {
                sfd = accept4(c->sfd, (struct sockaddr *)&addr, &addrlen, SOCK_NONBLOCK);
            } else {
                sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
            }
#else
            //accept一个当前连接文件描述符
            sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
#endif
            if (sfd == -1) {
                //.....
                perror(use_accept4 ? "accept4()" : "accept()");
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    /* these are transient, so don't log anything */
                    stop = true;
                } else if (errno == EMFILE) {
                    if (settings.verbose > 0)
                        fprintf(stderr, "Too many open connections\n");
                    //如果连接太多且文件描述符不够用,会先暂时拒绝连接请求
                    //当有可用文件描述符时会打开,下面会说明此函数内部流程
                    accept_new_conns(false);
                    stop = true;
                } else {
                    perror("accept()");
                    stop = true;
                }
                break;
            }
            if (!use_accept4) {
                if (fcntl(sfd, F_SETFL, fcntl(sfd, F_GETFL) | O_NONBLOCK) < 0) {
                    perror("setting O_NONBLOCK");
                    close(sfd);
                    break;
                }
            }
            //如果当前连接数大于启动时设定的最大连接数则报错
            if (settings.maxconns_fast &&
                stats.curr_conns + stats.reserved_fds >= settings.maxconns - 1) {
                str = "ERROR Too many open connections\r\n";
                res = write(sfd, str, strlen(str));
                close(sfd);
                STATS_LOCK();
                stats.rejected_conns++;
                STATS_UNLOCK();
            } else {
                //调度连接,选择那个work线程处理该连接
                //选择完成之后,创建该连接的conn结构体
                //默认状态为 conn_new_cmd 新命令然后继续监听
                //再触发事件之后就会根据状态走下面的流程
                dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
                                     DATA_BUFFER_SIZE, tcp_transport);
            }
            stop = true;
            break;
        //这些状态就是分配完连接之后,在处理该连接时的状态流程。
        case conn_waiting:
            //......
        case conn_read:
            //......
        case conn_parse_cmd :
            //......
        case conn_new_cmd:
            //......
        case conn_nread:
            //......
        case conn_swallow:
            //......
        case conn_write:
            //......
        case conn_mwrite:
            //......
        case conn_closing:
            //......
        case conn_closed:
            //......
        case conn_max_state:
            //......
        }
    }
    return;
}

调度分配连接到 -> work线程函数 dispatch_conn_new

void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
                       int read_buffer_size, enum network_transport transport) {
    
    //从 cq_item 空闲链表获取一个
    CQ_ITEM *item = cqi_new();
    char buf[1];
    if (item == NULL) {
        close(sfd);
        /* given that malloc failed this may also fail, but let's try */
        fprintf(stderr, "Failed to allocate memory for connection object\n");
        return ;
    }
    
    //按顺序获取一个线程id
    int tid = (last_thread + 1) % settings.num_threads;
    
    //定位到该线程地址
    LIBEVENT_THREAD *thread = threads + tid;

    last_thread = tid;
    
    //将一些参数赋值给 cq_item 结构体
    item->sfd = sfd;
    item->init_state = init_state;
    item->event_flags = event_flags;
    item->read_buffer_size = read_buffer_size;
    item->transport = transport;
    
    //写入当前获取到的线程cq_item队列里面去
    cq_push(thread->new_conn_queue, item);

    MEMCACHED_CONN_DISPATCH(sfd, thread->thread_id);
    //写入一个字符 'c' 到当前获取的线程 (thread->notify_send_fd) 管道文件描述符里
    //触发该线程监听事件并回调 thread_libevent_process 函数,最终完成新连接分配工作
    buf[0] = 'c';
    if (write(thread->notify_send_fd, buf, 1) != 1) {
        perror("Writing to thread notify pipe");
    }
}

2、thread_libevent_process 函数

static void thread_libevent_process(int fd, short which, void *arg) {
    LIBEVENT_THREAD *me = arg; //对应的线程结构体指针,分配给那个线程,就是那个线程的指针
    CQ_ITEM *item;
    char buf[1];
    
    //读取一个字节,因为主线程有新连接分配给工作线程的时候,会往该工作线程的管道写入一个字符 'c'
    if (read(fd, buf, 1) != 1)
        if (settings.verbose > 0)
            fprintf(stderr, "Can't read from libevent pipe\n");

    switch (buf[0]) {
    case 'c':
    item = cq_pop(me->new_conn_queue); // 从当前连接队列取出刚才主线程 dispatch_conn_new() 写入的 cq_item

    if (NULL != item) {
        // 创建一个网络连接,并往当前线程 me->base 加入一个监听事件 回调函数就是 event_handler -> drive_machine 
        // 状态为 item->init_state = conn_new_cmd
        conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                           item->read_buffer_size, item->transport, me->base);
        if (c == NULL) {
            if (IS_UDP(item->transport)) {
                fprintf(stderr, "Can't listen for events on UDP socket\n");
                exit(1);
            } else {
                if (settings.verbose > 0) {
                    fprintf(stderr, "Can't listen for events on fd %d\n",
                        item->sfd);
                }
                close(item->sfd);
            }
        } else {
            //将当前线程指针赋给c->thread字段
            c->thread = me;
        }
        //释放item, 就是重新加入到空闲cq_item链表里面
        cqi_free(item);
    }
        break;
    /* we were told to pause and report in */
    case 'p':
    register_thread_initialized();
        break;
    }
}

memcache 如果没有可用的文件描述符该怎么办?

因为有可能并发量过大一瞬间导致大量的连接,超过了系统设置的最大文件描述符数量,这个时候 memcache 实际上会拒绝连接的,这个拒绝连接是指拒绝TCP三次握手(减轻服务器负担),然后内部开启定时器不断的检查是否有可用的文件描述,当有可用的文件描述符时,会在打开 socket

上面在 accept 获取一个文件描述符如果返回 EMFILE 这个错误代表文件描述符耗尽,然后执行 accept_new_conns(false)

accept_new_conns 函数

void accept_new_conns(const bool do_accept) {
    pthread_mutex_lock(&conn_lock);
    do_accept_new_conns(do_accept);
    pthread_mutex_unlock(&conn_lock);
}

do_accept_new_conns 函数

void do_accept_new_conns(const bool do_accept) {
    conn *next;
    
    //static conn* listen_conn 本机绑定的端口也会使用conn结构体
    for (next = listen_conn; next; next = next->next) {
        if (do_accept) {
            //do_accept = true 执行这步
            //恢复当前绑定端口的文件描述符的事件
            update_event(next, EV_READ | EV_PERSIST);
            //恢复当前socket连接队列最大限额 settings.backlog
            if (listen(next->sfd, settings.backlog) != 0) {
                perror("listen");
            }
        }
        else {
            //do_accept = false 执行这步
            //先暂时关闭当前绑定端口的文件描述符的事件
            update_event(next, 0);
            //把当前socket连接队列最大限额置0,就是代表不再进行TCP三次握手.
            if (listen(next->sfd, 0) != 0) {
                perror("listen");
            }
        }
    }

    if (do_accept) {
        struct timeval maxconns_exited;
        uint64_t elapsed_us;
        gettimeofday(&maxconns_exited,NULL);
        //统计信息
        STATS_LOCK();
        elapsed_us =
            (maxconns_exited.tv_sec - stats.maxconns_entered.tv_sec) * 1000000
            + (maxconns_exited.tv_usec - stats.maxconns_entered.tv_usec);
        stats.time_in_listen_disabled_us += elapsed_us;
        stats.accepting_conns = true;
        STATS_UNLOCK();
    } else {
        //统计信息
        STATS_LOCK();
        stats.accepting_conns = false;
        gettimeofday(&stats.maxconns_entered,NULL);
        stats.listen_disabled_num++;
        STATS_UNLOCK();
        //allow_new_conns = false 代表当前不能进行新连接创建
        allow_new_conns = false;
        //执行
        maxconns_handler(-42, 0, 0);
    }
}

maxconns_handler 函数

static void maxconns_handler(const int fd, const short which, void *arg) {
    struct timeval t = {.tv_sec = 0, .tv_usec = 10000};

    if (fd == -42 || allow_new_conns == false) {
        /* reschedule in 10ms if we need to keep polling */
        //这里可以看到设置了一个定时器,每 10ms 回调一次当前函数
        //直到 allow_new_conns = true 为止,因为等于 true 就代表
        //已经有空闲文件描述符了,可以重新建立连接。
        evtimer_set(&maxconnsevent, maxconns_handler, 0);
        event_base_set(main_base, &maxconnsevent);
        evtimer_add(&maxconnsevent, &t);
    } else {
        //删除定时器
        evtimer_del(&maxconnsevent);
        //重新调用 accept_new_conns 这次参数 do_accept = true
        accept_new_conns(true);
    }
}

当 memcache 处理完一个连接,关闭的时候会 allow_new_conns = true

memcache 关闭连接函数 conn_close

static void conn_close(conn *c) {
    assert(c != NULL);

    /* delete the event, the socket and the conn */
    event_del(&c->event);

    if (settings.verbose > 1)
        fprintf(stderr, "<%d connection closed.\n", c->sfd);
    
    //释放当前连接申请的一些资源数据
    conn_cleanup(c);

    MEMCACHED_CONN_RELEASE(c->sfd);
    conn_set_state(c, conn_closed);
    //关闭连接
    close(c->sfd);
    
    //添加一个conn_lock互斥锁,防止跟上面accept_new_conns函数
    //同时操作 allow_new_conns 变量而冲突
    pthread_mutex_lock(&conn_lock);
    //这里每次关闭一个文件描述符之后都会allow_new_conns = true 
    allow_new_conns = true;
    pthread_mutex_unlock(&conn_lock);
        
    //为什么不能同时操作 allow_new_conns 变量?
    //因为上面函数会把这个变量置为false,然后根据false添加定时器,让服务器缓冲休息一下(等待更多的连接释放)
    //但如果不加锁,可能会导致上面函数把这个变量置为false的同时这里把这个变量置为true了
    //所以就导致上面函数判断这个变量等于false失败,所以也不会添加定时器了而且马上恢复服务器
    //连接功能.
    
    STATS_LOCK();
    stats.curr_conns--;
    STATS_UNLOCK();

    return;
}

结束

以上介绍的大致就是 memcache 内部如何实现 <b>单进程多线程</b> 处理网络请求的具体实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容