自己寫的 Tiny web server "Bad file descriptor"出錯?

自己寫的tiny web server,epoll + 多線程。epoll監聽套接字,有http請求到來時,開啟一個線程處理請求,測試時,讀數據時報錯 「Bad file descriptor」

man.cpp

int main()
{
......
pthread_attr_init(pthread_attr_detach);
pthread_attr_setdetachstate(pthread_attr_detach, PTHREAD_CREATE_DETACHED)

int n, fd;
while(true) {
n = tews_epoll_wait(epollfd, events, MAXEVENTS, -1);
for (int i = 0; i &< n; i++) { fd = events[i].data.fd; /* 監聽描述符可讀: 有新的連接 */ if (fd == listenfd) { int connfd = accept(listenfd, (SA*)clientaddr, clientlen); /* 將新的連接設置為非阻塞,把連接的套接字加入epoll監聽並設置工作模式為ET */ rc = make_fd_non_blocking(connfd); rc = tews_add_event(epollfd, connfd, EPOLLIN | EPOLLET); } /* 連接套接字有數據可讀 */ else { if (events[i].events EPOLLIN) { /* epoll不再監聽這個客戶端套接字 */ tews_delete_event(epollfd, fd, EPOLLIN | EPOLLET); pthread_create(tid, pthread_attr_detach, thread_worker, (void*)fd); } } } } }

這是http.cpp中thread_worker();

void* thread_worker(void* arg)
{
int conn_fd = *(int*)(arg);
/* 用於接收http請求報文的buff */
char *buff = (char*)malloc(BUF_SIZE);

int nread = 0, n = 0;
for ( ; ; ) {
if ( (n = read(conn_fd, buff, BUF_SIZE-1)) &> 0)
nread += n;
else if (n == 0)
break;
else if (n == -1 errno == EAGAIN)
break;
else {
log_err(__FILE__, __LINE__, "read conn_fd"); // 這裡報錯"Bad file descriptor"
goto out;
}
}

if (nread != 0) {
// 解析報文, 構造響應報文,sendfile發送請求文件
}
out:
close(conn_fd);
return NULL;
}

伺服器主機為阿里雲學生主機:CPU: 1核 內存:2 GB (I/O優化) 1Mbps。

自己用telnet 測試時能正常完成。想用webbench壓測試試:

webbench -c 10 -t 10 http://IP:8080/index.html。

結果很低並發量,伺服器就掛了,出錯信息:

[ERROR] (http.cpp:68) read conn_fd: Bad file descriptor。

自己感覺自己代碼邏輯沒有問題,想請求大佬,我錯在哪裡?


你參數傳遞的是指向fd的指針(地址)而不是fd數值。這樣出現的問題就是線程中的fd並不獨立,而是全部指向同一個fd。出現這種問題的過程之一是:

第一個請求進來拿到指向編號233的fd指針,於是去讀寫,在讀寫過程中進來第二個請求進來這個233被main線程修改成234(fd = events[i].data.fd;)進而第一個線程的fd也變成了234。此時如果1線程作業完畢於是close 234。線程2去讀就出問題了。

所以你的代碼改進點就在於讓每個線程拿到的fd是獨立的。

至於epoll配合多線程,一般而言不會把fd分配到線程中去,多此一舉,你每個線程直接accpet不就行了?

epoll換句話說所有的eventloop配合多線程的常見模型是這樣的,loop線程負責讀寫,線程負責處理內容和喚醒loop線程。

具體怎麼實施呢,loop監聽到EPOLLIN事件之後malloc一塊buffer(readbuf),過程中會realloc這塊buf直到讀完(這裡有一個細節如果是LT模式,一般不採用循環讀,而是讀一次記錄pos然後等待下一次EPOLLIN調用在realloc buf讀一次,如果還沒讀完,再記錄pos再等待可讀事件,避免飢餓問題,試想這個fd是用戶上傳一個10G文件,如果循環處理這一個fd,別的fd就得不到快速響應了)。然後把這個buf丟給線程。線程拿到「內容」去做業務處理,做完之後也不能直接通過fd來write和close,把結果寫到sendbuf並喚醒loop線程來寫。如何喚醒呢?loop線程需要額外監聽一個pipe的fd。線程在準備好sendbuf之後write(pipe_fd,b"x",1)就可以喚醒loop線程,loop則需要判斷fd是pipe那麼就把所有的寫buf給回寫掉(仍然要注意報文過大不要循環寫,由looper自己來調度)。


《Unix 網路編程》第3版第26.4節解釋了為什麼不能這樣傳線程參數。

話說你既然每個連接起一個線程,那就不必用 epoll 了。


你傳給每個線程的都是同一個地址,並且每次在那個地址上反覆讀寫,當然是錯的了。

如果只是想熟悉一下socket api,不求代碼質量,不妨試試new一份fd,拷貝到那個線程:

pthread_create(tid, pthread_attr_detach, thread_worker, (void*)new int (fd) );

當然這樣可能不符合C++的資源管理理念。


因為你給線程傳的參數是一個局部變數(也就是fd)的指針,這樣所有線程都在訪問這一個局部變數,當然會出問題。如果只是單純想解決Bad file descriptor問題,可以用(void*)(long)fd的方式來傳值,雖然很醜陋。

另外你為每個TCP connection起了一個線程,那就不必用epoll nonblocking I/O了,直接上blocking I/O。epoll是用來做I/O多路復用(I/O multiplexing)的介面,而你卻並沒有復用線程(即一個線程處理多個TCP connection),所以這樣寫是多此一舉。

給題主推薦三份資料,可以按順序閱讀:

1. &<深入理解計算機系統&>第三版第12章並發編程。

2. &卷1第三版第6章I/O模型,第30章客戶端/伺服器設計範式。

3. &第三章多線程伺服器的適用場合與常用編程模型。

加油!


傳參錯誤,這種必須值傳遞,arg=connfd


推薦閱讀:

Linux的epoll使用LT+非阻塞IO和ET+非阻塞IO有效率上的區別嗎?
多線程下epoll如何保證event.data.ptr不成為野指針?
Android的界面組件不能被子線程訪問是什麼意思呢?
操作系統時間調度基本單位是內核線程還是進程?

TAG:epoll | 網路編程 | 多線程 | Web伺服器 |