epoll+消息隊列-通過使用pthread_cond_signal的一個簡單實現

第一次用epoll去實現一個服務器,
之前並不清楚epoll的用法,
瞭解之後才發現epoll服務器的主線程其實最好和處理業務的代碼分開,
也就是說:
epoll響應外界的io請求,當epoll得到一個請求的時候,扔到一個消息隊列中,然後epoll直接返回,再去等待io請求.而消息隊列會通知多個線程去處理這些業務邏輯.

epoll第一次用,消息隊列更是第一次用,開始一直在想,怎麼寫個阻塞的隊列,而且要有主動通知的功能,想了一會兒發現pthread_cond_wait和pthread_cond_signal可以實現,於是就簡單的試試,下面的代碼已經可以實現我剛纔想要得到的那個模型,細節就不管了.

對消息隊列熟悉的同學請幫忙提點意見,並告訴我下還有哪些方法可以實現阻塞的消息隊列.

[color=orange]對代碼的解釋和描述都寫到註釋中了.[/color]

/*
幾個用到的類型定義以及全局變量bq
*/
char smtp_cmd_format;
struct epoll_event ev, events[MAX_EPOLL_SIZE];
int kdpfd,nfds;
struct block_queue
{
int queue[THREADS_COUNT];
long size;
pthread_cond_t cond;
pthread_mutex_t mutex;
}block_queue_t;
block_queue_t bq;
struct block_queue_param
{
void* func;
void* queue;
}block_queue_param_t;



void *block_queue(void * param)
{
void(* func)(void* );
int fd,i;
/*
由於block_queue是pthread_create的回調方法,
所以block_queue的參數必須是void*類型
*/
block_queue_t* bque = (block_queue_param_t*)param->queue;
/*
param->func是block_queue解鎖時需要調用的函數,
而這個函數的參數是一個int fd,
該fd是消息隊列中剛剛插入的一個元素.
*/
func = (block_queue_param_t*)param->func;

for(;;)
{
/*
lock->wait->unlock
這是經典的模式,
切記:
pthread_cond_wait的方法自帶了先解鎖,再等待,最後再加鎖的代碼
*/
pthread_mutex_lock(bque->mutex);
/*
線程在pthread_cond_wait這裏被block住了,
當聽到pthread_cond_signal通知的時候,
內核會從阻塞隊列裏面通過先進先出的原則喚醒一個線程,
這個線程會執行pthread_cond_wait之後的代碼.
*/
pthread_cond_wait(bque->cond,bque->mutex);

if(bque->size==0)
{
/*
啥也不做
*/
}else
{
fd = bque->queue[0];
/*
移動隊列,
由於該隊列是簡單的用數組保存fd,
所以移動這個操作必不可少,但肯定性能比鏈表差很多,
這是懶惰的代價
*/
for(i = 0; i < bque->size; ++i)
bque->queue[i] = bque->queue[i+1];
bque->queue[bque->size-1] = 0;
bque->size--;
/*
執行被喚醒後的方法,參數是剛剛插入到隊列中的一個fd
*/
func(fd);
}

pthread_mutex_unlock(bque->mutex);
}
}

void insert_queue(struct block_queue *bque,int fd)
{
/*
加鎖->通知->解鎖

將元素插入隊列之前需要先加鎖
*/
pthread_mutex_lock(bque->mutex);
/*
檢查隊列目前的大小,
檢查1:
當大小已經達到定義的數組大小THREADS_COUNT時,
拋棄該fd,說明服務器忙不過來了,消息隊列已經滿了

檢查2:
當大小超過數組定義的大小THREADS_COUNT時,
肯定發生了異常,那就直接退出服務吧.
*/
if(bque->size == THREADS_COUNT)
return;
/*
bque->size其實也是隊列末尾位置指針,
當插入一個元素後,這個指針自然也要向後移動一位.
*/
bque->queue[bque->size+1] = fd;
if(++bque->size > THREADS_COUNT)
{
fprintf(stderr,"Queue size over folow.%d",bque->size);
exit 1;
}
/*
當元素插入bque隊列時,
該通過pthread_cond_signal通知內核去調度wait的線程了
*/
pthread_cond_signal(bque->cond);
pthread_mutex_unlock(bque->mutex);

}

/*
init_threads代碼是初始化線程組的,
隨便寫寫的,大家知道怎麼實現就行
*/
int init_threads()
{
size_t i=0;
block_queue_param_t bqp;
/*
smtp_echo是處理epoll扔進隊列中的fd的函數,
該方法實現了整個模型的業務邏輯,
整體代碼的IO處理+消息隊列以及業務處理分的很清晰,
三個模塊每個只有一處代碼和其它模塊通訊,沒有多少耦合.
*/
bqp.func = (void*)smtp_echo;
bqp.queue = (void*)bq;
pthread_cond_init(bqp.cond,NULL);
pthread_mutex_init(bqp.mutex,NULL);
for( i = 0; i < THREADS_COUNT; ++i)
{
pthread_t child_thread;
pthread_attr_t child_thread_attr;
pthread_attr_init(&child_thread_attr);
pthread_attr_setdetachstate(&child_thread_attr,PTHREAD_CREATE_DETACHED);
if( pthread_create(&child_thread,&child_thread_attr,block_queue, (void *)bqp) < 0 )
{
printf("pthread_create Failed : %s\n",strerror(errno));
return 1;
}
else
{
printf("pthread_create Success : %d\n",(int)child_thread);
return 0;
}
}

}

/*
handler是主線程訪問的方法,
主線程通過handler把一個fd扔到消息隊列之後,
不再做任何事情就直接返回了.

在我的應用中,主線程是個epoll實現的服務器,
由於epoll被響應的時候會知道哪些fd已經就位,
於是直接把就位的fd扔到消息隊列中就好了,
主線程在繼續等待其它fd的響應,而不需要去關心fd如何被處理.
*/

int handler(void* fd)
{
printf("handler:fd => %d\n",*(int *)(fd));
insert_queue(&bq,fd);
return 0;
}


/*
main函數是整個程序的入口點,
也是epoll服務器的實現,
epoll的思想很精髓,用法很簡單,
只要把man 4 epoll_ctl的例子copy出來,就可用了,
不過那個例子語法有點問題,
而且events數組是用指針,應該用[]實現,因爲指針沒有分配空間.
*/
int main(int argc, char **argv)
{
int server_socket = init_smtp();
int n;

if(init_threads() == 0)
printf("Success full init_threads.");

smtp_cmd_format = "^([a-zA-Z0-9]) (.*)$";
kdpfd = epoll_create(MAX_EPOLL_SIZE);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = server_socket;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, server_socket, &ev) < 0) {
fprintf(stderr, "epoll set insertion error: fd=%d < 0",
server_socket);
return -1;
}

/*
epoll的使用看這裏
*/
for(;;) {
struct sockaddr_in local;
socklen_t length = sizeof(local);
int client;

nfds = epoll_wait(kdpfd, events, MAX_EPOLL_SIZE, -1);

/*
當沒有事件要處理時,epoll會阻塞住,
否則,內核會填充events數組,裏面的每一個events[n].data.fd就是發生io時間的文件句柄
*/

for(n = 0; n < nfds; ++n) {
/*
這裏要判斷一下請求的來源,
if(events[n].data.fd == server_socket) {
這裏是新建的連接,
因爲io發生在server_socket上
}
else{
這裏是已有的連接,
因爲fd!= server_socket
那fd肯定是之前從server_socket接收到,
並且通過epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev)
加入到kdpfd所指向的內存空間中.
kdpfd其實是個文件句柄,在epoll_create(MAX_EPOLL_SIZE)時得到
}
*/
if(events[n].data.fd == server_socket) {
client = accept(server_socket, (struct sockaddr *) &local,&length);
if(client < 0){
perror("accept");
continue;
}
setnonblocking(client);
smtp_cmd("220",client);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
fprintf(stderr, "epoll set insertion error: fd=%d < 0",
client);
return -1;
}
}
else
/*
當已有的fd發生io操作時,
執行如下代碼.也就是把fd扔到消息隊列中.
*/
if(handler((void *)&events[n].data.fd) != 0)
perror("handler ret != 0");
}
}

close(server_socket);
return 0;
}
發佈了83 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章