redis 為什么可以如此的高并發(fā)

本文的討論,暫時忽略redis數(shù)據(jù)結(jié)構(gòu)和算法層面的東西。


目錄

  • redis如此之快的原因
  • redis server啟動流程分析
  • IO模型介紹與epoll與redis
  • 總結(jié)
  • 推薦資料

redis如此之快聪姿,整體來說原因如下

  • 絕大部分請求是純粹的內(nèi)存操作(非诚疚荩快速)

  • 采用單線程,避免了不必要的上下文切換和競爭條件

  • 非阻塞IO 內(nèi)部實現(xiàn)采用epoll,采用了epoll+自己實現(xiàn)的簡單的事件框架统屈。epoll中的讀狂巢、寫撑毛、關(guān)閉、連接都轉(zhuǎn)化成了事件唧领,然后利用epoll的多路復(fù)用特性藻雌,絕不在io上浪費一點時間

這3個條件不是相互獨立的,特別是第一條斩个,如果請求都是耗時的胯杭,采用單線程吞吐量及性能可想而知了。應(yīng)該說redis為特殊的場景選擇了合適的技術(shù)方案受啥。


redis server啟動流程

redis_startup.jpeg

main 函數(shù)

int main(int argc, char **argv) {
struct timeval tv;

/* We need to initialize our libraries, and the server configuration. */
// 初始化庫
#ifdef INIT_SETPROCTITLE_REPLACEMENT
spt_init(argc, argv);
#endif
setlocale(LC_COLLATE,"");
zmalloc_enable_thread_safeness();
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
srand(time(NULL)^getpid());
gettimeofday(&tv,NULL);
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());

// 檢查服務(wù)器是否以 Sentinel 模式啟動
server.sentinel_mode = checkForSentinelMode(argc,argv);

// 初始化服務(wù)器
initServerConfig();

/* We need to init sentinel right now as parsing the configuration file
 * in sentinel mode will have the effect of populating the sentinel
 * data structures with master nodes to monitor. */
// 如果服務(wù)器以 Sentinel 模式啟動做个,那么進行 Sentinel 功能相關(guān)的初始化
// 并為要監(jiān)視的主服務(wù)器創(chuàng)建一些相應(yīng)的數(shù)據(jù)結(jié)構(gòu)
if (server.sentinel_mode) {
    initSentinelConfig();
    initSentinel();
}

// 檢查用戶是否指定了配置文件,或者配置選項
if (argc >= 2) {
    int j = 1; /* First option to parse in argv[] */
    sds options = sdsempty();
    char *configfile = NULL;

    /* Handle special options --help and --version */
    // 處理特殊選項 -h 滚局、-v 和 --test-memory
    if (strcmp(argv[1], "-v") == 0 ||
        strcmp(argv[1], "--version") == 0) version();
    if (strcmp(argv[1], "--help") == 0 ||
        strcmp(argv[1], "-h") == 0) usage();
    if (strcmp(argv[1], "--test-memory") == 0) {
        if (argc == 3) {
            memtest(atoi(argv[2]),50);
            exit(0);
        } else {
            fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
            fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
            exit(1);
        }
    }

    /* First argument is the config file name? */
    // 如果第一個參數(shù)(argv[1])不是以 "--" 開頭
    // 那么它應(yīng)該是一個配置文件
    if (argv[j][0] != '-' || argv[j][1] != '-')
        configfile = argv[j++];

    /* All the other options are parsed and conceptually appended to the
     * configuration file. For instance --port 6380 will generate the
     * string "port 6380\n" to be parsed after the actual file name
     * is parsed, if any. */
    // 對用戶給定的其余選項進行分析居暖,并將分析所得的字符串追加稍后載入的配置文件的內(nèi)容之后
    // 比如 --port 6380 會被分析為 "port 6380\n"
    while(j != argc) {
        if (argv[j][0] == '-' && argv[j][1] == '-') {
            /* Option name */
            if (sdslen(options)) options = sdscat(options,"\n");
            options = sdscat(options,argv[j]+2);
            options = sdscat(options," ");
        } else {
            /* Option argument */
            options = sdscatrepr(options,argv[j],strlen(argv[j]));
            options = sdscat(options," ");
        }
        j++;
    }
    if (configfile) server.configfile = getAbsolutePath(configfile);
    // 重置保存條件
    resetServerSaveParams();

    // 載入配置文件, options 是前面分析出的給定選項
    loadServerConfig(configfile,options);
    sdsfree(options);

    // 獲取配置文件的絕對路徑
    if (configfile) server.configfile = getAbsolutePath(configfile);
} else {
    redisLog(REDIS_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");
}

// 將服務(wù)器設(shè)置為守護進程
if (server.daemonize) daemonize();

// 創(chuàng)建并初始化服務(wù)器數(shù)據(jù)結(jié)構(gòu)
initServer();

// 如果服務(wù)器是守護進程藤肢,那么創(chuàng)建 PID 文件
if (server.daemonize) createPidFile();

// 為服務(wù)器進程設(shè)置名字
redisSetProcTitle(argv[0]);

// 打印 ASCII LOGO
redisAsciiArt();

// 如果服務(wù)器不是運行在 SENTINEL 模式太闺,那么執(zhí)行以下代碼
if (!server.sentinel_mode) {
    /* Things not needed when running in Sentinel mode. */
    // 打印問候語
    redisLog(REDIS_WARNING,"Server started, Redis version " REDIS_VERSION);
#ifdef __linux__
    // 打印內(nèi)存警告
    linuxOvercommitMemoryWarning();
#endif
    // 從 AOF 文件或者 RDB 文件中載入數(shù)據(jù)
    loadDataFromDisk();
    // 啟動集群?
    if (server.cluster_enabled) {
        if (verifyClusterConfigWithData() == REDIS_ERR) {
            redisLog(REDIS_WARNING,
                "You can't have keys in a DB different than DB 0 when in "
                "Cluster mode. Exiting.");
            exit(1);
        }
    }
    // 打印 TCP 端口
    if (server.ipfd_count > 0)
        redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
    // 打印本地套接字端口
    if (server.sofd > 0)
        redisLog(REDIS_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
} else {
    sentinelIsRunning();
}

/* Warning the user about suspicious maxmemory setting. */
// 檢查不正常的 maxmemory 配置
if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
    redisLog(REDIS_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
}

// 運行事件處理器谤草,一直到服務(wù)器關(guān)閉為止
aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);

// 服務(wù)器關(guān)閉跟束,停止事件循環(huán)
aeDeleteEventLoop(server.el);

return 0;
}

initServer 函數(shù)莺奸,創(chuàng)建TCP Server丑孩,啟動服務(wù)器,通過 listenToPort()完成 bind與listen灭贷,aeCreateFileEvent()注冊事件監(jiān)聽温学,監(jiān)聽結(jié)果有acceptTcpHandler()函數(shù)處理

void initServer() {
int j;

// 設(shè)置信號處理函數(shù)
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();

// 設(shè)置 syslog
if (server.syslog_enabled) {
    openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
        server.syslog_facility);
}

// 初始化并創(chuàng)建數(shù)據(jù)結(jié)構(gòu)
server.current_client = NULL;
server.clients = listCreate();
server.clients_to_close = listCreate();
server.slaves = listCreate();
server.monitors = listCreate();
server.slaveseldb = -1; /* Force to emit the first SELECT command. */
server.unblocked_clients = listCreate();
server.ready_keys = listCreate();
server.clients_waiting_acks = listCreate();
server.get_ack_from_slaves = 0;
server.clients_paused = 0;

// 創(chuàng)建共享對象
createSharedObjects();
adjustOpenFilesLimit();
server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
server.db = zmalloc(sizeof(redisDb)*server.dbnum);

/* Open the TCP listening socket for the user commands. */
// 打開 TCP 監(jiān)聽端口,用于等待客戶端的命令請求
if (server.port != 0 &&
    listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
    exit(1);

/* Open the listening Unix domain socket. */
// 打開 UNIX 本地端口
if (server.unixsocket != NULL) {
    unlink(server.unixsocket); /* don't care if this fails */
    server.sofd = anetUnixServer(server.neterr,server.unixsocket,
        server.unixsocketperm, server.tcp_backlog);
    if (server.sofd == ANET_ERR) {
        redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr);
        exit(1);
    }
    anetNonBlock(NULL,server.sofd);
}

/* Abort if there are no listening sockets at all. */
if (server.ipfd_count == 0 && server.sofd < 0) {
    redisLog(REDIS_WARNING, "Configured to not listen anywhere, exiting.");
    exit(1);
}

/* Create the Redis databases, and initialize other internal state. */
// 創(chuàng)建并初始化數(shù)據(jù)庫結(jié)構(gòu)
for (j = 0; j < server.dbnum; j++) {
    server.db[j].dict = dictCreate(&dbDictType,NULL);
    server.db[j].expires = dictCreate(&keyptrDictType,NULL);
    server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
    server.db[j].ready_keys = dictCreate(&setDictType,NULL);
    server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
    server.db[j].eviction_pool = evictionPoolAlloc();
    server.db[j].id = j;
    server.db[j].avg_ttl = 0;
}

// 創(chuàng)建 PUBSUB 相關(guān)結(jié)構(gòu)
server.pubsub_channels = dictCreate(&keylistDictType,NULL);
server.pubsub_patterns = listCreate();
listSetFreeMethod(server.pubsub_patterns,freePubsubPattern);
listSetMatchMethod(server.pubsub_patterns,listMatchPubsubPattern);

server.cronloops = 0;
server.rdb_child_pid = -1;
server.aof_child_pid = -1;
aofRewriteBufferReset();
server.aof_buf = sdsempty();
server.lastsave = time(NULL); /* At startup we consider the DB saved. */
server.lastbgsave_try = 0;    /* At startup we never tried to BGSAVE. */
server.rdb_save_time_last = -1;
server.rdb_save_time_start = -1;
server.dirty = 0;
resetServerStats();
/* A few stats we don't want to reset: server startup time, and peak mem. */
server.stat_starttime = time(NULL);
server.stat_peak_memory = 0;
server.resident_set_size = 0;
server.lastbgsave_status = REDIS_OK;
server.aof_last_write_status = REDIS_OK;
server.aof_last_write_errno = 0;
server.repl_good_slaves_count = 0;
updateCachedTime();

/* Create the serverCron() time event, that's our main way to process
 * background operations. */
// 為 serverCron() 創(chuàng)建時間事件
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
    redisPanic("Can't create the serverCron time event.");
    exit(1);
}

/* Create an event handler for accepting new connections in TCP and Unix
 * domain sockets. */
// 為 TCP 連接關(guān)聯(lián)連接應(yīng)答(accept)處理器
// 用于接受并應(yīng)答客戶端的 connect() 調(diào)用
for (j = 0; j < server.ipfd_count; j++) {
    if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
        acceptTcpHandler,NULL) == AE_ERR)
        {
            redisPanic(
                "Unrecoverable error creating server.ipfd file event.");
        }
}

// 為本地套接字關(guān)聯(lián)應(yīng)答處理器
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
    acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");

/* Open the AOF file if needed. */
// 如果 AOF 持久化功能已經(jīng)打開甚疟,那么打開或創(chuàng)建一個 AOF 文件
if (server.aof_state == REDIS_AOF_ON) {
    server.aof_fd = open(server.aof_filename,
                           O_WRONLY|O_APPEND|O_CREAT,0644);
    if (server.aof_fd == -1) {
        redisLog(REDIS_WARNING, "Can't open the append-only file: %s",
            strerror(errno));
        exit(1);
    }
}

/* 32 bit instances are limited to 4GB of address space, so if there is
 * no explicit limit in the user provided configuration we set a limit
 * at 3 GB using maxmemory with 'noeviction' policy'. This avoids
 * useless crashes of the Redis instance for out of memory. */
// 對于 32 位實例來說仗岖,默認將最大可用內(nèi)存限制在 3 GB
if (server.arch_bits == 32 && server.maxmemory == 0) {
    redisLog(REDIS_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
    server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
    server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION;
}

// 如果服務(wù)器以 cluster 模式打開逃延,那么初始化 cluster
if (server.cluster_enabled) clusterInit();

// 初始化復(fù)制功能有關(guān)的腳本緩存
replicationScriptCacheInit();

// 初始化腳本系統(tǒng)
scriptingInit();

// 初始化慢查詢功能
slowlogInit();

// 初始化 BIO 系統(tǒng)
bioInit();
}

acceptTcpHandler 函數(shù),創(chuàng)建一個 TCP 連接處理器轧拄,accept 客戶端請求

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[REDIS_IP_STR_LEN];
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
REDIS_NOTUSED(privdata);

while(max--) {
    // accept 客戶端連接
    cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
    if (cfd == ANET_ERR) {
        if (errno != EWOULDBLOCK)
            redisLog(REDIS_WARNING,
                "Accepting client connection: %s", server.neterr);
        return;
    }
    redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
    // 為客戶端創(chuàng)建客戶端狀態(tài)(redisClient)
    acceptCommonHandler(cfd,0);
   }
}

acceptCommonHandler()函數(shù)揽祥,首先創(chuàng)建client客戶端,然后是處理客戶端的請求檩电。新添加的客戶端令服務(wù)器的最大客戶端數(shù)量達到了拄丰,那么向新客戶端寫入錯誤信息,并關(guān)閉新客戶端

 #define MAX_ACCEPTS_PER_CALL 1000
 static void acceptCommonHandler(int fd, int flags) {
// 創(chuàng)建客戶端
redisClient *c;
if ((c = createClient(fd)) == NULL) {
    redisLog(REDIS_WARNING,
        "Error registering fd event for the new client: %s (fd=%d)",
        strerror(errno),fd);
    close(fd); /* May be already closed, just ignore errors */
    return;
}

/* If maxclient directive is set and this is one client more... close the
 * connection. Note that we create the client instead to check before
 * for this condition, since now the socket is already set in non-blocking
 * mode and we can send an error for free using the Kernel I/O */
// 如果新添加的客戶端令服務(wù)器的最大客戶端數(shù)量達到了
// 那么向新客戶端寫入錯誤信息俐末,并關(guān)閉新客戶端
// 先創(chuàng)建客戶端料按,再進行數(shù)量檢查是為了方便地進行錯誤信息寫入
if (listLength(server.clients) > server.maxclients) {
    char *err = "-ERR max number of clients reached\r\n";

    /* That's a best effort error message, don't check write errors */
    if (write(c->fd,err,strlen(err)) == -1) {
        /* Nothing to do, Just to avoid the warning... */
    }
    // 更新拒絕連接數(shù)
    server.stat_rejected_conn++;
    freeClient(c);
    return;
}

// 更新連接次數(shù)
server.stat_numconnections++;

// 設(shè)置 FLAG
c->flags |= flags;
}

IO模型介紹與epoll在redis的使用

考慮這樣一個問題:有10000個客戶端需要連上一個服務(wù)器并保持TCP連接,客戶端會不定時的發(fā)送請求給服務(wù)器卓箫,服務(wù)器收到請求后需及時處理并返回結(jié)果载矿。我們應(yīng)該怎么解決?

方案一:我們使用一個線程來監(jiān)聽,當(dāng)一個新的客戶端發(fā)起連接時烹卒,建立連接并new一個線程來處理這個新連接闷盔。

缺點:當(dāng)客戶端數(shù)量很多時,服務(wù)端線程數(shù)過多甫题,即便不壓垮服務(wù)器馁筐,由于CPU有限其性能也極其不理想。因此此方案不可用坠非。

redis_io_model1.png

方案二:我們使用一個線程監(jiān)聽敏沉,當(dāng)一個新的客戶端發(fā)起連接時,建立連接并使用線程池處理該連接炎码。

優(yōu)點:客戶端連接數(shù)量不會壓垮服務(wù)端盟迟。

缺點:服務(wù)端處理能力受限于線程池的線程數(shù),而且如果客戶端連接中大部分處于空閑狀態(tài)的話服務(wù)端的線程資源被浪費潦闲。

redis_io_model2.png

因此攒菠,一個線程僅僅處理一個客戶端連接無論如何都是不可接受的。那能不能一個線程處理多個連接呢歉闰?該線程輪詢每個連接辖众,如果某個連接有請求則處理請求,沒有請求則處理下一個連接和敬,這樣可以實現(xiàn)嗎凹炸?

方案三:I/O 多路復(fù)用技術(shù)

現(xiàn)代的UNIX操作系統(tǒng)提供了select/poll/kqueue/epoll這樣的系統(tǒng)調(diào)用,這些系統(tǒng)調(diào)用的功能是:你告知我一批套接字昼弟,當(dāng)這些套接字的可讀或可寫事件發(fā)生時啤它,我通知你這些事件信息。

select

UNIX everthing is a file,套接字也不例外,每一個套接字都有對應(yīng)的fd(即文件描述符)变骡。我們簡單看看這幾個系統(tǒng)調(diào)用的原型离赫。

select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)

對于select(),我們需要傳3個集合塌碌,r渊胸,w和e。其中台妆,r表示我們對哪些fd的可讀事件感興趣蹬刷,w表示我們對哪些fd的可寫事件感興趣,e表示我們對錯誤事件感興趣。每個集合其實是一個bitmap频丘,通過0/1表示我們感興趣的fd办成。例如,我們對于fd為6的可讀事件感興趣搂漠,那么r集合的第6個bit需要被設(shè)置為1迂卢。這個系統(tǒng)調(diào)用會阻塞,直到我們感興趣的事件(至少一個)發(fā)生桐汤。調(diào)用返回時而克,內(nèi)核同樣使用這3個集合來存放fd實際發(fā)生的事件信息。也就是說怔毛,調(diào)用前這3個集合表示我們感興趣的事件员萍,調(diào)用后這3個集合表示實際發(fā)生的事件。

select為最早期的UNIX系統(tǒng)調(diào)用拣度,它存在4個問題:

  • 這3個bitmap有大小限制(FD_SETSIZE碎绎,通常為1024);
  • 由于這3個集合在返回時會被內(nèi)核修改抗果,因此我們每次調(diào)用時都需要重新設(shè)置筋帖;
  • 我們在調(diào)用完成后需要掃描這3個集合才能知道哪些fd的讀/寫事件發(fā)生了,一般情況下全量集合比較大而實際發(fā)生讀/寫事件的fd比較少冤馏,效率比較低下日麸;
  • 內(nèi)核在每次調(diào)用都需要掃描這3個fd集合,然后查看哪些fd的事件實際發(fā)生逮光,在讀/寫比較稀疏的情況下同樣存在效率問題代箭。

poll

由于存在這些問題,于是人們對select進行了改進涕刚,從而有了poll嗡综。

poll(struct pollfd *fds, int nfds, int timeout)

struct pollfd {

int fd;/* 文件描述符 */

short events; /*等待的事件 */

short revents; /* 實際發(fā)生的事件*/

 }

具體使用列子參考 下載(實現(xiàn)server和client)

poll調(diào)用需要傳遞的是一個pollfd結(jié)構(gòu)的數(shù)組,調(diào)用返回時結(jié)果信息也存放在這個數(shù)組里面副女。 pollfd的結(jié)構(gòu)中存放著fd蛤高、我們對該fd感興趣的事件(events)以及該fd實際發(fā)生的事件(revents)。poll傳遞的不是固定大小的bitmap碑幅,因此select的問題1解決了戴陡;poll將感興趣事件和實際發(fā)生事件分開了,因此select的問題2也解決了沟涨。但select的問題3和問題4仍然沒有解決恤批。

select問題3比較容易解決,只要系統(tǒng)調(diào)用返回的是實際發(fā)生相應(yīng)事件的fd集合裹赴,我們便不需要掃描全量的fd集合喜庞。

對于select的問題4,我們?yōu)槭裁葱枰看握{(diào)用都傳遞全量的fd呢棋返?內(nèi)核可不可以在第一次調(diào)用的時候記錄這些fd延都,然后我們在以后的調(diào)用中不需要再傳這些fd呢?

問題的關(guān)鍵在于無狀態(tài)睛竣。對于每一次系統(tǒng)調(diào)用晰房,內(nèi)核不會記錄下任何信息,所以每次調(diào)用都需要重復(fù)傳遞相同信息射沟。

上帝說要有狀態(tài)殊者,所以我們有了epoll和kqueue。

epoll和kqueue

epoll與kqueue類似验夯,只不過kqueue是用在FreeBSD(FreeBSD是一種自由的類Unix操作系統(tǒng))上面猖吴。
在redis里面也有類似的宏定義,在ae.c中

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
     #ifdef HAVE_EPOLL
     #include "ae_epoll.c"
  #else
      #ifdef HAVE_KQUEUE
      #include "ae_kqueue.c"
      #else
      #include "ae_select.c"
      #endif
   #endif
#endif

epoll主要使用以下三個函數(shù)

int epoll_create(int size);

創(chuàng)建一個epoll的句柄挥转,size用來告訴內(nèi)核需要監(jiān)聽的數(shù)目一共有多大海蔽。當(dāng)創(chuàng)建好epoll句柄后,它就是會占用一個fd值绑谣,在linux下如果查看/proc/進程id/fd/准潭,是能夠看到這個fd的,所以在使用完epoll后域仇,必須調(diào)用close() 關(guān)閉刑然,否則可能導(dǎo)致fd被耗盡。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epoll的事件注冊函數(shù)暇务,第一個參數(shù)是 epoll_create() 的返回值泼掠,第二個參數(shù)表示動作,使用如下三個宏來表示:

EPOLL_CTL_ADD    //注冊新的fd到epfd中垦细;
EPOLL_CTL_MOD    //修改已經(jīng)注冊的fd的監(jiān)聽事件择镇;
EPOLL_CTL_DEL    //從epfd中刪除一個fd;

第三個參數(shù)是需要監(jiān)聽的fd括改,第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事腻豌,struct epoll_event 結(jié)構(gòu)如下:

typedef union epoll_data
{
    void        *ptr;
    int          fd;
    __uint32_t   u32;
    __uint64_t   u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

events 可以是以下幾個宏的集合:

EPOLLIN     //表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
EPOLLOUT    //表示對應(yīng)的文件描述符可以寫;
EPOLLPRI    //表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來)吝梅;
EPOLLERR    //表示對應(yīng)的文件描述符發(fā)生錯誤虱疏;
EPOLLHUP    //表示對應(yīng)的文件描述符被掛斷;
EPOLLET     //將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式苏携,這是相對于水平觸發(fā)(Level Triggered)來說的做瞪。
EPOLLONESHOT//只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后右冻,如果還需要繼續(xù)監(jiān)聽這個socket的話装蓬,需要再次把這個socket加入到EPOLL隊列里。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

參數(shù)events用來從內(nèi)核得到事件的集合纱扭,maxevents 告之內(nèi)核這個events有多大牍帚,這個 maxevents 的值不能大于創(chuàng)建 epoll_create() 時的size,
參數(shù) timeout 是超時時間(毫秒乳蛾,0會立即返回履羞,-1將不確定,也有說法說是永久阻塞)屡久。該函數(shù)返回需要處理的事件數(shù)目忆首,如返回0表示已超時。

EPOLL事件有兩種模型 Level Triggered (LT)Edge Triggered (ET)

LT(level triggered被环,水平觸發(fā)模式)是缺省的工作方式糙及,并且同時支持 block 和 non-block socket。在這種做法中筛欢,內(nèi)核告訴你一個文件描述符是否就緒了浸锨,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作版姑,內(nèi)核還是會繼續(xù)通知你的柱搜,所以,這種模式編程出錯誤可能性要小一點剥险。

ET(edge-triggered聪蘸,邊緣觸發(fā)模式)是高速工作方式,只支持no-block socket表制。在這種模式下健爬,當(dāng)描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你么介。然后它會假設(shè)你知道文件描述符已經(jīng)就緒娜遵,并且不會再為那個文件描述符發(fā)送更多的就緒通知,等到下次有新的數(shù)據(jù)進來的時候才會再次出發(fā)就緒事件壤短。

一個例子見 下載

epoll的優(yōu)點

  1. epoll 沒有最大并發(fā)連接的限制设拟,上限是最大可以打開文件的數(shù)目慨仿,這個數(shù)字一般遠大于 2048, 一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大 ,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看纳胧。

  2. 效率提升镰吆, Epoll 最大的優(yōu)點就在于它只管你“活躍”的連接 ,而跟連接總數(shù)無關(guān)躲雅,因此在實際的網(wǎng)絡(luò)環(huán)境中, Epoll 的效率就會遠遠高于 select 和 poll 骡和。

  3. 內(nèi)存拷貝相赁, Epoll 在這點上使用了“共享內(nèi)存 ”,這個內(nèi)存拷貝也省略了

epoll與redis

我們下面來看下epoll在redis源碼中的使用

redis_epoll.png

總結(jié)

通過上面慰于,我們了解了redis的啟動過程钮科,以及常見的IO模型。redis使用了最簡單的方式實現(xiàn)了最高的并發(fā)婆赠,極簡原則绵脯。。

推薦資料

Redis的設(shè)計與實現(xiàn)

Redis 3.0代碼注釋(Redis的設(shè)計與實現(xiàn)作者實現(xiàn))

《Redis Command Reference》全文的中文翻譯版

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末休里,一起剝皮案震驚了整個濱河市蛆挫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妙黍,老刑警劉巖悴侵,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拭嫁,居然都是意外死亡可免,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門做粤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浇借,“玉大人,你說我怎么就攤上這事怕品「竟福” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵肉康,是天一觀的道長修己。 經(jīng)常有香客問我,道長迎罗,這世上最難降的妖魔是什么睬愤? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮纹安,結(jié)果婚禮上尤辱,老公的妹妹穿的比我還像新娘砂豌。我一直安慰自己,他們只是感情好光督,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布阳距。 她就那樣靜靜地躺著,像睡著了一般结借。 火紅的嫁衣襯著肌膚如雪筐摘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天船老,我揣著相機與錄音咖熟,去河邊找鬼。 笑死柳畔,一個胖子當(dāng)著我的面吹牛馍管,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播薪韩,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼确沸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了俘陷?” 一聲冷哼從身側(cè)響起罗捎,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拉盾,沒想到半個月后宛逗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡盾剩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年雷激,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片告私。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡屎暇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出驻粟,到底是詐尸還是另有隱情根悼,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布蜀撑,位于F島的核電站挤巡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏酷麦。R本人自食惡果不足惜矿卑,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沃饶。 院中可真熱鬧母廷,春花似錦轻黑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至业舍,卻和暖如春抖拦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舷暮。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工态罪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人脚牍。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓向臀,卻偏偏與公主長得像巢墅,于是被迫代替她去往敵國和親诸狭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容