redis事件機制

TuxedoLinux發表於2018-06-03

redis事件機制

redis並沒有採用libevent庫作為事件機制的底層實現,而是自己對io多路複用進行了封裝,即可以採用select、epoll、evport、kqueue作為底層的實現。redis客戶端與服務端進行通訊時,redis提供了命令事件(也就是檔案事件)。另外redis還提供了定時事件,用於對系統實時性要求進行處理,以及處理使用者的業務需求。
先來看下redis事件機制的整體結構, 接下來的每個小結都圍繞這個結構進行分析。

一、建立/監聽socket
redis服務端在初始化函式initServer中會進行建立socket、繫結socket的操作。將服務端的指定ip或者所有ip都設定為可以監聽來自客戶端的連線請求。
//建立套接字、繫結套接字
int listenToPort(int port, int *fds, int *count);

引數port指繫結socket的具體埠

fds用於儲存所有繫結的socket, 例如:"假設服務端有兩個ip:192.168.100.10 , 192.168.100.11,如果對這兩個ip都進行繫結,則fds中將儲存繫結後的兩個套接字fd"

count 用於儲存具體有多少個套接字進行了繫結

          建立並繫結socket後、服務端可以在這些socket上監聽來自客戶端的連線請求。

二、建立事件管理器

redis服務端在初始化函式initServer中,會建立一個事件管理器物件,用於管理命令事件、時間事件。函式aeCreateEventLoop將建立一個事件管理器,setsize引數指的是socket描述符的個數,服務端初始化時將該引數設為最大socket個數。函式將返回一個事件管理器物件。

  1. aeEventLoop *aeCreateEventLoop(int setsize)   
  2. {  
  3.     aeEventLoop *eventLoop;  
  4.     int i;  
  5.     // 建立事件狀態結構  
  6.     if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL)   
  7.     goto err;  
  8.   
  9.     <span style="color:#ff0000;">// 建立未就緒事件表、就緒事件表</span>  
  10.     eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);  
  11.     eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);  
  12.     if (eventLoop->events == NULL || eventLoop->fired == NULL)   
  13.     goto err;  
  14.       
  15.     // 設定陣列大小  
  16.     eventLoop->setsize = setsize;  
  17.     // 初始化執行最近一次執行時間  
  18.     eventLoop->lastTime = time(NULL);  
  19.   
  20.     // 初始化時間事件結構  
  21.     eventLoop->timeEventHead = NULL;  
  22.     eventLoop->timeEventNextId = 0;  
  23. <span style="color:#ff0000;">    //將多路複用io與事件管理器關聯起來</span>  
  24.     if (aeApiCreate(eventLoop) == -1)  
  25.         goto err;  
  26.   
  27.     // 初始化監聽事件  
  28.     for (i = 0; i < setsize; i++)  
  29.         eventLoop->events[i].mask = AE_NONE;  
  30.   
  31.     // 返回事件迴圈  
  32.     return eventLoop;  
  33. err:  
  34.    ...  
  35. }  

1、首先建立一個aeEventLoop物件

2、建立一個未就緒事件表、就緒事件表。events指標指向未就緒事件表、fired指標指向就緒事件表。表的內容在後面新增具體事件時進行初始化。

3、呼叫aeApiCreate建立一個epoll例項

  1. static int aeApiCreate(aeEventLoop *eventLoop)   
  2. {  
  3.     aeApiState *state = zmalloc(sizeof(aeApiState));  
  4.   
  5.     // 初始化epoll就緒事件表  
  6.     state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);  
  7.   
  8.     // 建立 epoll 例項  
  9.     state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */  
  10.   
  11.     // 事件管理器與epoll關聯  
  12.     eventLoop->apidata = state;  
  13.     return 0;  
  14. }  
  1. typedef struct aeApiState   
  2. {  
  3.     // epoll_event 例項描述符  
  4.     int epfd;  
  5.     <span style="color:#ff0000;">// 儲存epoll就緒事件表</span>  
  6.     struct epoll_event *events;  
  7. } aeApiState;  
aeApiCreate內部將建立一個aeApiState物件,並將物件儲存在apidata中,這樣aeApiCreate與aeApiState就關聯起來了。

物件中epfd儲存epoll的標識,events是一個就緒事件陣列,當有事件發生時,所有發生的事件都將儲存在這個陣列中。這個就緒事件陣列由應用層開闢空間、核心負責把所有發生的事件填充到該陣列。

三、建立命令事件(也就是檔案事件)

建立來事件管理器後、接下來就可以把具體的事件插入到事件管理器中。

  1. typedef struct aeFileEvent   
  2. {  
  3.     // 監聽事件型別掩碼,// 值可以是 AE_READABLE 或 AE_WRITABLE ,  
  4.     // 或者 AE_READABLE | AE_WRITABLE  
  5.     int mask;   
  6.     // 讀事件處理器  
  7.     aeFileProc *rfileProc;  
  8.     // 寫事件處理器  
  9.     aeFileProc *wfileProc;  
  10.     // 多路複用庫的私有資料  
  11.     void *clientData;  
  12. } aeFileEvent;  
aeFileEvent是命令事件結構,對於每一個具體的事件,都有讀處理函式、寫處理函式等。
  1. int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)  
  2. {  
  3.     // 取出檔案事件結構  
  4.     aeFileEvent *fe = &eventLoop->events[fd];  
  5.   
  6.     // 監聽指定 fd 的指定事件  
  7.     if (aeApiAddEvent(eventLoop, fd, mask) == -1)  
  8.         return AE_ERR;  
  9.   
  10.     // 設定檔案事件型別,以及事件的處理器  
  11.     fe->mask |= mask;  
  12.     if (mask & AE_READABLE)   
  13.     fe->rfileProc = proc;  
  14.     if (mask & AE_WRITABLE)   
  15.     fe->wfileProc = proc;  
  16.   
  17.     // 私有資料  
  18.     fe->clientData = clientData;  
  19.     return AE_OK;  
  20. }  
建立一個具體命令事件時,引數fd指的是具體的soket套接字,proc指fd產生事件時,具體的處理過程。
根據fd為索引在未就緒表中找到相應的元素,該陣列元素就被fd佔用。這個過程就相當於把fd插入到未就緒事件表中。接下來填充事件的回撥、引數、事件型別等引數。

除了將事件插入到未就緒表中外,還需要把fd對應的事件插入到具體的io複用中,本例為epoll。

  1. static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)   
  2. {  
  3.     aeApiState *state = eventLoop->apidata;  
  4.     struct epoll_event ee;  
  5.   
  6.     //如果 fd 沒有關聯任何事件,那麼這是一個 ADD 操作。如果已經關聯了某個/某些事件,那麼這是一個 MOD 操作。  
  7.     int op = eventLoop->events[fd].mask == AE_NONE ?  
  8.            EPOLL_CTL_ADD : EPOLL_CTL_MOD;  
  9.   
  10.     // 註冊事件到 epoll  
  11.     ee.events = 0;  
  12.     mask |= eventLoop->events[fd].mask; /* Merge old events */  
  13.     if (mask & AE_READABLE)   
  14.     ee.events |= EPOLLIN;  
  15.     if (mask & AE_WRITABLE)   
  16.     ee.events |= EPOLLOUT;  
  17.   
  18.     ee.data.u64 = 0; /* avoid valgrind warning */  
  19.     ee.data.fd = fd;  
  20.     //將事件加入epoll中  
  21.     if (epoll_ctl(state->epfd,op,fd,&ee) == -1)  
  22.         return -1;  
  23.   
  24.     return 0;  
  25. }  
epoll內部使用紅黑樹維護每一個事件。紅黑樹中的每一個節點都代表一個fd。當應用層註冊一個命令事件時,會將事件fd插入到紅黑樹中。應用層刪除一個事件時,也相應會從紅黑樹中刪除一個事件節點。

因此建立一個事件時、將會發生下面3個操作

1、建立事件並給事件賦值

2、將事件插入到未就緒事件表

3、將事件插入到epoll維護的紅黑樹中。

四、事件迴圈

程式將使用一個while死迴圈,一直維持著服務端的運轉。

  1. //事件處理器的主迴圈  
  2. void aeMain(aeEventLoop *eventLoop)   
  3. {  
  4.     eventLoop->stop = 0;  
  5.   
  6.     while (!eventLoop->stop)   
  7.     {  
  8.   
  9.         // 如果有需要在事件處理前執行的函式,那麼執行它  
  10.         if (eventLoop->beforesleep != NULL)  
  11.             eventLoop->beforesleep(eventLoop);  
  12.   
  13.         // 開始處理事件  
  14.         aeProcessEvents(eventLoop, AE_ALL_EVENTS);  
  15.     }  
  16. }  
aeProcessEvents將開始進行事件處理。即處理定時事件也處理命令事件。定時事件與命令事件可以同時發生。定時事件將在下一節講述,本節只講述命令事件。
  1. // 處理檔案事件,阻塞時間由 tvp 決定  
  2. numevents = aeApiPoll(eventLoop, tvp);  
  3. for (j = 0; j < numevents; j++)   
  4. {  
  5.     // 從已就緒陣列中獲取事件  
  6. <span style="white-space:pre">    </span>aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];  
  7.     int mask = eventLoop->fired[j].mask;  
  8.     int fd = eventLoop->fired[j].fd;  
  9.     int rfired = 0;  
  10.   
  11.     // 讀事件  
  12.     if (fe->mask & mask & AE_READABLE)   
  13.     {  
  14.           // rfired 確保讀/寫事件只能執行其中一個  
  15.           rfired = 1;  
  16.           //呼叫讀處理函式  
  17.           fe->rfileProc(eventLoop,fd,fe->clientData,mask);  
  18.     }  
  19.   
  20.     // 寫事件  
  21.     if (fe->mask & mask & AE_WRITABLE)   
  22.     {  
  23.         //呼叫寫處理函式  
  24.          if (!rfired || fe->wfileProc != fe->rfileProc)  
  25.           fe->wfileProc(eventLoop,fd,fe->clientData,mask);  
  26.     }  
  27. }  
aeApiPoll呼叫時將阻塞,直到有事件發生,或者超時,該函式才返回。函式返回時,已就緒表中儲存了所有就緒事件。

遍歷所有已經發生的事件,根據fd在未就緒表中找到相應的事件,然後呼叫事件處理函式。

當一輪事件執行完後,程式又進入最外層的事件迴圈中,接著處理剩於的事件。

aeApiPoll內部呼叫epoll系統呼叫,等待指定事件發生或者超時。如果有事件發生則eploo_wait返回非0,如果為超時,則返回0。

當有事件發生時,核心會把所有發生的事件由核心層拷貝到應用層。eploo_wait返回時state->events就是儲存是由核心返回的就緒表。


  1. static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)   
  2. {  
  3.     aeApiState *state = eventLoop->apidata;  
  4.     int retval, numevents = 0;  
  5.   
  6.     // 等待時間  
  7.     retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,  
  8.             tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);  
  9.   
  10.     // 有至少一個事件就緒?  
  11.     if (retval > 0)   
  12.     {  
  13.         int j;  
  14.   
  15.         // 為已就緒事件設定相應的模式  
  16.         // 並加入到 eventLoop 的 fired 陣列中  
  17.         numevents = retval;  
  18.         for (j = 0; j < numevents; j++)   
  19.     {  
  20.             int mask = 0;  
  21.             struct epoll_event *e = <span style="color:#ff0000;">state->events</span>+j;  
  22.   
  23.             if (e->events & EPOLLIN)  
  24.         mask |= AE_READABLE;  
  25.             if (e->events & EPOLLOUT)  
  26.         mask |= AE_WRITABLE;  
  27.             if (e->events & EPOLLERR)   
  28.         mask |= AE_WRITABLE;  
  29.             if (e->events & EPOLLHUP)  
  30.         mask |= AE_WRITABLE;  
  31.   
  32.             eventLoop->fired[j].fd = e->data.fd;  
  33.             eventLoop->fired[j].mask = mask;  
  34.         }  
  35.     }  
  36.       
  37.     // 返回已就緒事件個數  
  38.     return numevents;  
  39. }  
核心已經把就緒事件從核心拷貝到了應用層的epoll就緒表state->events中。之後aeApiPoll會把epoll就緒表state->events中的就緒事件拷貝到fired就緒表中

五、刪除事件

當不在需要某個事件時,需要把事件刪除掉。例如: 如果fd同時監聽讀事件、寫事件。當不在需要監聽寫事件時,可以把該fd的寫事件刪除。

  1. void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)  
  2. {  
  3.   
  4.     // 取出檔案事件結構  
  5.     aeFileEvent *fe = &eventLoop->events[fd];  
  6.   
  7.     // 未設定監聽的事件型別,直接返回  
  8.     if (fe->mask == AE_NONE)  
  9.     return;  
  10.   
  11.     // 計算新掩碼  
  12.     fe->mask = fe->mask & (~mask);  
  13.   
  14.     // 取消對給定 fd 的給定事件的監視  
  15.     aeApiDelEvent(eventLoop, fd, mask);  
  16. }  
  1. static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask)   
  2. {  
  3.     aeApiState *state = eventLoop->apidata;  
  4.     struct epoll_event ee;  
  5.     int mask = eventLoop->events[fd].mask & (~delmask);  
  6.   
  7.     ee.events = 0;  
  8.     if (mask & AE_READABLE)   
  9.     ee.events |= EPOLLIN;  
  10.     if (mask & AE_WRITABLE)   
  11.     ee.events |= EPOLLOUT;  
  12.       
  13.     ee.data.u64 = 0; /* avoid valgrind warning */  
  14.     ee.data.fd = fd;  
  15.     if (mask != AE_NONE)   
  16.     {  
  17.         epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);  
  18.     }   
  19.     else   
  20.     {  
  21.         epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);  
  22.     }  
  23. }  
刪除的過程可以總結為以下幾個步驟

1、根據fd在未就緒表中查詢到事件

2、取消該fd對應的相應事件識別符號

3、通知核心,核心會將紅黑樹上的相應事件也給取消。

相關文章