一. 關(guān)于epoll
創(chuàng)新互聯(lián)建站成都網(wǎng)站建設(shè)按需策劃設(shè)計(jì),是成都網(wǎng)站開發(fā)公司,為成都汽車玻璃修復(fù)提供網(wǎng)站建設(shè)服務(wù),有成熟的網(wǎng)站定制合作流程,提供網(wǎng)站定制設(shè)計(jì)服務(wù):原型圖制作、網(wǎng)站創(chuàng)意設(shè)計(jì)、前端HTML5制作、后臺(tái)程序開發(fā)等。成都網(wǎng)站推廣熱線:13518219792對(duì)于IO復(fù)用模型,前面談?wù)撨^了關(guān)于select和poll函數(shù)的使用,select提供給用戶一個(gè)關(guān)于存儲(chǔ)事件的數(shù)據(jù)結(jié)構(gòu)fd_set來統(tǒng)一監(jiān)測(cè)等待事件的就緒,分為讀、寫和異常事件集;而poll則是用一個(gè)個(gè)的pollfd類型的結(jié)構(gòu)體管理事件的文件描述符和事件所關(guān)心的events,并通過結(jié)構(gòu)體里面的輸出型參數(shù)revents來通知用戶事件的就緒狀態(tài);
但是對(duì)于上述兩種函數(shù),都是需要用戶遍歷所有的事件集合來確定到底是哪一個(gè)或者是哪些事件已經(jīng)就緒可以進(jìn)行數(shù)據(jù)的處理了,因此當(dāng)要處理等待的事件比較多時(shí),就會(huì)有數(shù)據(jù)復(fù)制和系統(tǒng)遍歷的開銷導(dǎo)致效率并不高效;針對(duì)select和poll的缺點(diǎn),另外一種相對(duì)高效的處理IO復(fù)用的函數(shù)就出現(xiàn)了,那就是epoll;
二. epoll相關(guān)函數(shù)的使用
首先,和select及poll函數(shù)不同的是,epoll并沒有直接的一個(gè)用epoll來命名的函數(shù)使用,而是分別提供出來三個(gè)函數(shù):epoll_create、epoll_ctl和epoll_wait;
epoll_create
epoll_create函數(shù)創(chuàng)建一個(gè)epoll的“實(shí)例”,請(qǐng)求內(nèi)核分配一個(gè)指定大小的空間用于事件的后臺(tái)存儲(chǔ),函數(shù)參數(shù)size只是一個(gè)關(guān)于內(nèi)核如何維護(hù)內(nèi)部結(jié)構(gòu)的提示,不過現(xiàn)在這個(gè)size已經(jīng)被忽略并不需要在意了;
函數(shù)成功會(huì)返回一個(gè)引用新創(chuàng)建的epoll實(shí)例的一個(gè)文件描述符,用于隨后調(diào)用其他的epoll函數(shù)的結(jié)構(gòu),如果不再需要的話,應(yīng)當(dāng)使用close函數(shù)關(guān)閉,這時(shí)內(nèi)核會(huì)銷毀該epoll實(shí)例并釋放相關(guān)資源;如果函數(shù)失敗會(huì)返回-1并置相應(yīng)的錯(cuò)誤碼;
2. epoll_ctl
函數(shù)參數(shù)中,
epfd是用epoll_create創(chuàng)建出來的epoll文件描述符,用來操縱epoll實(shí)例;
op是要對(duì)創(chuàng)建出的epoll實(shí)例進(jìn)行操作,而op的操作選項(xiàng)有如下三種宏:
EPOLL_CTL_ADD用于在epfd標(biāo)識(shí)的epoll實(shí)例中添加登記要處理的事件;
EPOLL_CTL_MOD用于更改特定的文件描述符所關(guān)心的事件;
EPOLL_CTL_DEL用于刪除在epoll實(shí)例中登記的事件,標(biāo)識(shí)并不需要再關(guān)心了;
fd是指要進(jìn)行數(shù)據(jù)IO的事件的文件描述符,也就是用戶需要進(jìn)行操作的事件的文件描述符;
event是一個(gè)epoll_event的結(jié)構(gòu)體,用于存放需要對(duì)fd進(jìn)行操作的相關(guān)信息:
結(jié)構(gòu)體中,
events表示文件描述符fd所對(duì)應(yīng)的事件所關(guān)心的操作,是相應(yīng)的比特位的設(shè)置,有如下幾種宏:
如上的宏中,最主要使用的有如下幾種:
EPOLLIN表示fd可以進(jìn)行數(shù)據(jù)的讀??;
EPOLLOUT表示fd可以進(jìn)行數(shù)據(jù)的寫入;
EPOLLPRI表示當(dāng)前有緊急數(shù)據(jù)可供讀取;
EPOLLERR表示當(dāng)前事件發(fā)生錯(cuò)誤;
EPOLLHUP表示當(dāng)前事件被掛斷;
EPOLLET將相關(guān)的文件描述符設(shè)置為邊緣觸發(fā),因?yàn)槟J(rèn)是水平觸發(fā)的;對(duì)于LT和ET模式下面會(huì)討論;
對(duì)于結(jié)構(gòu)體中的data則是一個(gè)聯(lián)合,用于表示有關(guān)文件描述符操作的數(shù)據(jù)信息:
ptr是指向數(shù)據(jù)緩沖區(qū)的一個(gè)指針;
fd是相應(yīng)操作的文件描述符;
epoll_ctl函數(shù)成功返回0,失敗返回-1并置相應(yīng)的錯(cuò)誤碼;
3. epoll_wait
如果說上面的epoll_create和epoll_ctl是為了進(jìn)行相關(guān)事件的操作而進(jìn)行的準(zhǔn)備工作,那么真正和select及poll函數(shù)一樣用來進(jìn)行多個(gè)事件的等待就緒則就是epoll_wait函數(shù)了:
函數(shù)參數(shù)中,
epfd是用epoll_create創(chuàng)建出的epoll實(shí)例的文件描述符;
events是上述的一個(gè)結(jié)構(gòu)體的指針,這里一般是一個(gè)數(shù)組的首地址,是一個(gè)輸入輸出型參數(shù),當(dāng)作為輸入時(shí),是用戶提供給系統(tǒng)一個(gè)用來存放就緒事件的地址空間,而作為輸出型參數(shù)時(shí),系統(tǒng)會(huì)將就緒的事件放入其中供用戶提取,因此不可以為NULL;
maxevents是events的大小;
timeout則是設(shè)置等待的超時(shí)時(shí)間,單位為毫秒;
這里值得一提的是,既然epoll是select和poll的改進(jìn),那么其最主要的高效就是體現(xiàn)在epoll_wait的返回值:
函數(shù)失敗返回-1并置相應(yīng)的錯(cuò)誤碼;
函數(shù)返回0表示超時(shí),預(yù)定時(shí)間內(nèi)并沒有事件就緒;
當(dāng)函數(shù)返回值大于0時(shí),是告訴用戶當(dāng)前事件集中已經(jīng)就緒的IO事件的個(gè)數(shù),并且將其按序從頭開始排列在了用戶提供的空間events內(nèi),因此,不需要像select和poll那樣遍歷整個(gè)事件集找出就緒的事件,只需要在相應(yīng)的數(shù)組中從頭訪問固定的返回值的個(gè)數(shù)就拿到了所有就緒的事件了;
三. 栗子時(shí)間
同樣的,使用epoll相關(guān)的接口函數(shù),可以自主來編寫一個(gè)基于TCP協(xié)議的服務(wù)端,其基本步驟如下:
首先,先要?jiǎng)?chuàng)建出一個(gè)監(jiān)聽socket,綁定好本地網(wǎng)絡(luò)地址信息并將其處于監(jiān)聽狀態(tài),但是這里,為了使其更為高效,還需要調(diào)用setsockopt函數(shù)來將其屬性設(shè)定為SO_REUSEADDR,使其地址信息可被重用;
調(diào)用epoll_create創(chuàng)建出一個(gè)關(guān)于epoll實(shí)例的文件描述符,用于以后操作epoll相關(guān)函數(shù);
調(diào)用epoll_ctl函數(shù),將監(jiān)聽socket登記添加到epoll實(shí)例中;
定義一個(gè)epoll_event結(jié)構(gòu)體數(shù)組,用戶指定大小,供系統(tǒng)存放就緒的IO事件;
調(diào)用epoll_wait進(jìn)行事件的就緒等待,并接收其返回值;
當(dāng)epoll_wait返回時(shí),對(duì)返回的事件一一進(jìn)行判斷處理,如果是監(jiān)聽事件就緒,表明有連接請(qǐng)求需要處理,并將新的套接字添加進(jìn)epoll實(shí)例中;如果是其他socket就緒,表明數(shù)據(jù)就緒可以進(jìn)行讀取和寫入了;
當(dāng)連接的一端關(guān)閉或者epoll實(shí)例使用完畢的時(shí)候,需要調(diào)用close函數(shù)關(guān)閉相應(yīng)的文件描述符回收資源;
server客戶端程序設(shè)計(jì)如下:
#include#include #include #include #include #include #include #include #include #include #include #include #define _BACKLOG_ 5 //網(wǎng)絡(luò)中連接請(qǐng)求等待隊(duì)列大值 #define _MAX_NUM_ 20 //事件就緒隊(duì)列存儲(chǔ)空間 #define _DATA_SIZE_ 1024 //數(shù)據(jù)緩沖區(qū)大小 //因?yàn)閑poll_event結(jié)構(gòu)體中的data成員是一個(gè)聯(lián)合體,因此當(dāng)需要同時(shí)使用聯(lián)合中的fd和ptr的時(shí)候就會(huì)有問題 //因此可以將其各自單獨(dú)拿出存儲(chǔ) typedef struct data_buf { int _fd; char _buf[_DATA_SIZE_]; }data_buf_t, *data_buf_p; //命令行參數(shù)的格式判斷 void Usage(const char *argv) { assert(argv); printf("Usage: %s [ip] [port]\n", argv); exit(0); } //創(chuàng)建監(jiān)聽套接字 static int CreateListenSock(int ip, int port) { int sock = socket(AF_INET, SOCK_STREAM, 0);//創(chuàng)建新socket if(sock < 0) { perror("socket"); exit(1); } int opt = 1;//調(diào)用setsockopt函數(shù)使當(dāng)server首先斷開連接的時(shí)候避免進(jìn)入一個(gè)TIME_WAIT的等待時(shí)間 if(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt"); exit(2); } //設(shè)置本地網(wǎng)絡(luò)地址信息 struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = ip; //綁定套接字和本地網(wǎng)絡(luò)信息 if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0) { perror("bind"); exit(3); } //設(shè)定套接字為監(jiān)聽狀態(tài) if(listen(sock, _BACKLOG_) < 0) { perror("listen"); exit(4); } return sock; } //執(zhí)行epoll void epoll_server(int listen_sock) { //創(chuàng)建出一個(gè)epoll實(shí)例,獲取其文件描述符,大小隨意指定 int epoll_fd = epoll_create(256); if(epoll_fd < 0) { perror("epoll_create"); exit(5); } //定義一個(gè)epoll_event結(jié)構(gòu)體用于向epoll實(shí)例中注冊(cè)需要IO的事件信息 struct epoll_event ep_ev; ep_ev.events = EPOLLIN; ep_ev.data.fd = listen_sock; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ep_ev) < 0) { perror("epoll_ctl"); exit(6); } //申請(qǐng)一個(gè)確定的空間提供給系統(tǒng),用于存放就緒事件隊(duì)列 struct epoll_event evs[_MAX_NUM_]; int maxnum = _MAX_NUM_;//提供的空間大小 int timeout = 10000;//設(shè)定超時(shí)時(shí)間,如果為-1,則以阻塞方式一直等待 int ret = 0;//epoll_wait的返回值,獲取就緒事件的個(gè)數(shù) while(1) { switch((ret = epoll_wait(epoll_fd, evs, maxnum, timeout))) { case -1://出錯(cuò) perror("epoll_wait"); break; case 0://超時(shí) printf("timeout...\n"); break; default://至少有一個(gè)事件就緒 { int i = 0; for(; i < ret; ++i) { //判斷是否為監(jiān)聽套接字,如果是,獲取連接請(qǐng)求 if((evs[i].data.fd == listen_sock) && (evs[i].events & EPOLLIN)) { struct sockaddr_in client; socklen_t client_len = sizeof(client); //處理連接請(qǐng)求,獲取新的通信套接字 int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len); if(accept_sock < 0) { perror("accept"); continue; } printf("connect with a client...[fd]:%d [ip]:%s [port]:%d\n", accept_sock, inet_ntoa(client.sin_addr), ntohs(client.sin_port)); //將新的事件添加進(jìn)epoll實(shí)例中 ep_ev.events = EPOLLIN; ep_ev.data.fd = accept_sock; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accept_sock, &ep_ev) < 0) { perror("epoll_ctl"); close(accept_sock); } } else//除了監(jiān)聽套接字之外的IO套接字 { //如果為讀事件就緒 if(evs[i].events & EPOLLIN) { //申請(qǐng)空間用于同時(shí)存儲(chǔ)文件描述符和緩沖區(qū)地址 data_buf_p _data = (data_buf_p)malloc(sizeof(data_buf_t)); if(!_data) { perror("malloc"); continue; } _data->_fd = evs[i].data.fd; printf("read from fd: %d\n", _data->_fd); //從緩沖區(qū)中讀取數(shù)據(jù) ssize_t size = read(_data->_fd, _data->_buf, sizeof(_data->_buf)-1); if(size < 0)//讀取出錯(cuò) printf("read error...\n"); else if(size == 0)//遠(yuǎn)端關(guān)閉連接 { printf("client closed...\n"); //收尾工作,將事件從epoll實(shí)例中移除,關(guān)閉文件描述符和防止內(nèi)存泄露 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL); close(_data->_fd); free(_data); } else { //讀取成功,輸出數(shù)據(jù) (_data->_buf)[size] = '\0'; printf("client# %s", _data->_buf); fflush(stdout); //將事件改為關(guān)心寫事件,進(jìn)行回寫 ep_ev.data.ptr = _data; ep_ev.events = EPOLLOUT; //在epoll實(shí)例中更改同一個(gè)事件 epoll_ctl(epoll_fd, EPOLL_CTL_MOD, _data->_fd, &ep_ev); } } else if(evs[i].events & EPOLLOUT)//判斷為寫事件就緒 { data_buf_p _data = (data_buf_p)evs[i].data.ptr; //向緩沖區(qū)中回寫數(shù)據(jù) write(_data->_fd, _data->_buf, strlen(_data->_buf)); //寫完之后就進(jìn)行完畢一次通信,進(jìn)行收尾 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL); close(_data->_fd); free(_data); } else {} } } } break; } } } int main(int argc, char *argv[]) { if(argc != 3)//判斷命令行參數(shù)的正確性 Usage(argv[0]); //獲取端口號(hào)和IP地址 int port = atoi(argv[2]); int ip = inet_addr(argv[1]); //獲取監(jiān)聽套接字 int listen_sock = CreateListenSock(ip, port); //進(jìn)行epoll操作 epoll_server(listen_sock); close(listen_sock);//關(guān)閉文件描述符 return 0; }
這里要說明一下,系統(tǒng)內(nèi)部其實(shí)是為epoll相關(guān)的操作維護(hù)了一棵平衡搜索二叉樹和一張鏈表,如果用戶一次性提供出來的空間不夠存放所有就緒的事件,那么下一次系統(tǒng)會(huì)將剩下的再提供出來,因此不必要擔(dān)心提供給epoll_wait的結(jié)構(gòu)體數(shù)組空間的問題;
運(yùn)行程序:
左邊為server端,右邊為使用telnet請(qǐng)求連接端
因?yàn)樵O(shè)計(jì)的是一問一答的模式,因此在server端收到連接請(qǐng)求和數(shù)據(jù)之后,將數(shù)據(jù)讀取出再回寫回連接請(qǐng)求端,就認(rèn)為完成了一次通信;
如上的模式,還可以用瀏覽器來進(jìn)行測(cè)試,只是當(dāng)瀏覽器進(jìn)行連接請(qǐng)求之后,server端就認(rèn)為收到了數(shù)據(jù),轉(zhuǎn)而需要進(jìn)行回寫,而回寫的內(nèi)容則有所要求,因?yàn)榇蟛糠譃g覽器所使用的是HTTP協(xié)議,因此在瀏覽器接收的時(shí)候,應(yīng)該收到的是server端寫回的作為響應(yīng)的消息,而這里,HTTP的響應(yīng)由三部分組成,狀態(tài)行、消息報(bào)頭和響應(yīng)正文,而作為狀態(tài)行的格式為“協(xié)議版本+響應(yīng)狀態(tài)碼+表示狀態(tài)碼的文本”,過多的內(nèi)容并不屬于本篇文章的討論范圍,因此不贅述,總之,作為響應(yīng)消息,server端寫回的內(nèi)容應(yīng)該是如下格式:
char *msg = "HTTP/1.1 200 OK\r\n\r\nHello, what can i do for you ? :)\r\n"; write(_data->_fd, msg, strlen(msg));
運(yùn)行server端程序,打開瀏覽器輸入IP和端口號(hào):
當(dāng)瀏覽器連接上server時(shí),server端會(huì)接收到關(guān)于瀏覽器方面的信息,也就是獲取了瀏覽器的請(qǐng)求信息,而之后會(huì)將響應(yīng)消息返回給瀏覽器,而瀏覽器會(huì)根據(jù)接收到的響應(yīng)消息得到正文內(nèi)容并顯示出來,如右邊的顯示(使用本地環(huán)回IP進(jìn)行的測(cè)試即127.0.0.1);
四. 水平觸發(fā)和邊緣觸發(fā)
當(dāng)epoll_wait在進(jìn)行多個(gè)事件的等待時(shí),如果有數(shù)據(jù)發(fā)送到緩沖區(qū)中時(shí),則表示當(dāng)前事件處于就緒狀態(tài),則需要返回來通知用戶“有數(shù)據(jù)來了,可以進(jìn)行處理了”,那么對(duì)于系統(tǒng)通知用戶的方式,就分為水平式觸發(fā)和邊緣式觸發(fā):
水平觸發(fā)(Level Trigger)簡稱LT,其特點(diǎn)是當(dāng)數(shù)據(jù)到來的時(shí)候會(huì)通知用戶,如果用戶一次數(shù)據(jù)處理并沒有將緩沖區(qū)中的數(shù)據(jù)全部取走還留有一部分,那么下一次再進(jìn)行相同事件的epoll_wait的時(shí)候系統(tǒng)會(huì)認(rèn)為事件仍然是就緒的,還會(huì)繼續(xù)通知用戶來取走剩下的數(shù)據(jù),因此,水平觸發(fā)的特點(diǎn)是:只要數(shù)據(jù)緩沖區(qū)中有數(shù)據(jù),當(dāng)前的IO事件始終都是就緒的,epoll_wait始終會(huì)返回有效值通知用戶程序;
邊緣觸發(fā)(Edge Triggered)簡稱ET,當(dāng)有數(shù)據(jù)到來的時(shí)候仍然會(huì)返回通知用戶程序,但是和水平觸發(fā)不同的是,如果用戶在通知一次后對(duì)數(shù)據(jù)的IO處理并不完全,也就是一次處理之后緩沖區(qū)中還留有數(shù)據(jù),那么再次返回進(jìn)行epoll_wait的時(shí)候就不會(huì)再表明當(dāng)前事件是就緒的了,只有當(dāng)這個(gè)事件再次有數(shù)據(jù)到達(dá)時(shí)才會(huì)再一次通知用戶程序來處理數(shù)據(jù),因此,邊緣觸發(fā)的特點(diǎn)是:只有當(dāng)數(shù)據(jù)到來的時(shí)候系統(tǒng)才會(huì)通知用戶程序且只會(huì)通知一次,如果還有數(shù)據(jù)沒有處理完,只有等到再次有數(shù)據(jù)到來的時(shí)候才會(huì)再次滿足事件就緒,epoll_wait返回通知用戶程序處理數(shù)據(jù);
這里需要注意的是:對(duì)于邊緣式觸發(fā),因?yàn)橹挥挟?dāng)數(shù)據(jù)到來時(shí)系統(tǒng)才會(huì)通知用戶程序一次,如果當(dāng)前的IO接口工作于阻塞模式,那么當(dāng)一個(gè)事件被阻塞的時(shí)候,其他事件的就緒也就只會(huì)被通知一次但并得不到處理,因此會(huì)導(dǎo)致多數(shù)據(jù)的堆積,所以,當(dāng)使用邊緣式觸發(fā)的時(shí)候:
最好將當(dāng)前的IO接口設(shè)定為非阻塞的;
當(dāng)一個(gè)IO事件進(jìn)行數(shù)據(jù)的讀取和寫入的時(shí)候,最好一次性就將緩沖區(qū)中的數(shù)據(jù)全部都處理完;因此,對(duì)于數(shù)據(jù)的讀取,可以用一個(gè)循環(huán)來每次讀取特定的長度,當(dāng)最后一次讀取的長度小于特定的長度時(shí),就可以認(rèn)為當(dāng)前緩沖區(qū)的數(shù)據(jù)已經(jīng)全部讀取完畢終止循環(huán);但是,不可避免的是,如果最后一次的讀取恰好也就是特定的長度,那么在此進(jìn)行讀取緩沖區(qū)中數(shù)據(jù)為0,就會(huì)返回一個(gè)EAGAIN的錯(cuò)誤碼,這個(gè)就可以作為循環(huán)的終止條件;
EAGAIN的錯(cuò)誤碼為11,可在/usr/include/asm-generic/errno.h及errno-base.h中查到:
若輸出其對(duì)應(yīng)錯(cuò)誤描述,為:Resource temporarily unavailable,意思是資源暫時(shí)不可用,可以try again;
將IO接口設(shè)置為非阻塞的,可以調(diào)用fcntl函數(shù):
函數(shù)參數(shù)中,
fd表示要進(jìn)行操作的文件描述符;
cmd表示要進(jìn)行的操作;
至于后面的參數(shù),則有cmd來決定;
在這里要設(shè)置文件接口為非阻塞的,首先要將cmd設(shè)置為F_GETFL,表示獲取當(dāng)前文件描述符的標(biāo)志,因?yàn)橹匦略O(shè)定時(shí)需要用到;之后需要再次調(diào)用fcntl函數(shù),將cmd設(shè)定為F_SETFL,要重新設(shè)置文件描述符的標(biāo)志,其中有一個(gè)選項(xiàng)就是O_NONBLOCK;
對(duì)于fcntl函數(shù)的返回值,根據(jù)操作的不同而不同:
對(duì)比水平觸發(fā)和邊緣觸發(fā),可以發(fā)現(xiàn)水平觸發(fā)對(duì)于數(shù)據(jù)的處理來說是更安全更可靠的,而邊緣觸發(fā)是要更為高效的,因此,選擇哪種通知方式,可以依情況而定;
因?yàn)樯厦娴某绦蛑?,默認(rèn)epoll_wait的通知方式是LT也就是水平觸發(fā)的,要將其改為高效一些的ET邊緣觸發(fā)模式,則需要滿足如上所述的非阻塞條件和數(shù)據(jù)一次性讀取完畢條件:
首先將事件的IO接口設(shè)置為非阻塞模式,則在listen socket創(chuàng)建中以及每一次有新的連接請(qǐng)求獲得新的IO文件描述符之后,都需要調(diào)用如下的函數(shù):
int set_non_block(int fd) { //獲取當(dāng)前文件描述符的文件標(biāo)識(shí) int old_fl = fcntl(fd, F_GETFL); if(old_fl < 0) { perror("fcntl"); return -1; } //將文件描述符所對(duì)應(yīng)的事件設(shè)置為非阻塞模式 if(fcntl(fd, F_SETFL, old_fl|O_NONBLOCK)) { perror("fcntl"); return -1; } return 0; }
其次,就需要自行封裝出一個(gè)函數(shù)來進(jìn)行循環(huán)地獲取或者寫入緩沖區(qū)中數(shù)據(jù),直到?jīng)]有數(shù)據(jù)可讀為止,這是為了避免邊緣觸發(fā)的特點(diǎn)帶來的數(shù)據(jù)擁堵不能夠被處理的現(xiàn)象:
//讀取數(shù)據(jù) ssize_t MyRead(int fd, char *buf, size_t size) { assert(buf); int index = 0; ssize_t ret = 0; //如果讀取到的數(shù)據(jù)等于0,則說明遠(yuǎn)端關(guān)閉連接,直接返回0 //而如果為非0,不管是大于零還是出錯(cuò)小于零都需要進(jìn)入循環(huán) while((ret = read(fd, buf+index, size-index))) { if(errno == EAGAIN)//如果錯(cuò)誤碼為EAGAIN,則說明讀取完畢,打印出錯(cuò)誤碼和錯(cuò)誤消息并退出 { printf("read errno: %d\n", errno); perror("read"); break; } index += ret; } return (ssize_t)index;//返回獲得的總數(shù)據(jù)量 } //寫入數(shù)據(jù) ssize_t MyWrite(int fd, char* buf, size_t size) { assert(buf); int index = 0; ssize_t ret = -1; //和讀取數(shù)據(jù)一樣,當(dāng)寫入數(shù)據(jù)量為0的時(shí)候直接返回0 //否則,返回值為非零進(jìn)入循環(huán) while((ret = write(fd, buf+index, size-index))) { if(errno == EAGAIN)//當(dāng)數(shù)據(jù)全部寫完的時(shí)候返回錯(cuò)誤碼為EAGAIN { printf("write errno: %d\n", errno); perror("write"); break; } index += ret; } return (ssize_t)index;//和讀取數(shù)據(jù)相同,返回寫入的總數(shù)據(jù)量 }
將上面修改的代碼添加到上述例子中之后,運(yùn)行程序:
分析一下程序結(jié)果,會(huì)發(fā)現(xiàn)第一次連接并沒有什么問題,得到了一問一答的結(jié)果,但是如果第二次連接包括以后的多次連接,所發(fā)送的數(shù)據(jù)就無法被server端接收到,反而被認(rèn)為連接端已經(jīng)關(guān)閉了,因此server端就主動(dòng)關(guān)閉了連接和相關(guān)事件的清除;這是怎么一回事呢?
這是因?yàn)椋谏厦嫠庋b的數(shù)據(jù)的讀寫函數(shù)中,當(dāng)?shù)谝淮芜B接進(jìn)行數(shù)據(jù)的讀取,讀取完畢緩沖區(qū)中所有的數(shù)據(jù)之后,再次進(jìn)行read就會(huì)出錯(cuò),因而錯(cuò)誤碼被置為了EAGAIN,而錯(cuò)誤碼errno是個(gè)全局變量,所以當(dāng)再次或者多次連接進(jìn)行數(shù)據(jù)的讀取的時(shí)候,即使讀到了數(shù)據(jù)read的返回值大于零,但進(jìn)入循環(huán)進(jìn)行
if(errno == EAGAIN)
判斷的時(shí)候,errno已經(jīng)被第一次連接置為了EAGAIN,而運(yùn)行是在同一個(gè)進(jìn)程當(dāng)中的,所以始終滿足上述條件跳出循環(huán),返回值為0,之后再進(jìn)行判斷,就會(huì)認(rèn)為并沒有讀到數(shù)據(jù),轉(zhuǎn)而關(guān)閉相應(yīng)的文件描述符;
這就是在一個(gè)函數(shù)中使用了全局變量造成了函數(shù)的不可重入性;
要解決上述問題,
可以在上述的判斷條件增加一個(gè)條件,即:
if((ret < 0) && (errno == EAGAIN)) { printf("read errno: %d\n", errno); perror("read"); break; }
當(dāng)read出錯(cuò)進(jìn)入循環(huán)的時(shí)候,要和read成功分開進(jìn)行操作,這樣就不會(huì)有誤了,雖然無法避免使用全局變量errno,但是可以通過read的返回值來進(jìn)一步加強(qiáng)判斷;
2. 另外有一種方法,就是可以用多進(jìn)程來操作,即將errno變成某一個(gè)進(jìn)程專屬的全局變量,也就是當(dāng)一個(gè)IO的讀事件就緒的時(shí)候,就創(chuàng)建出一個(gè)子進(jìn)程來進(jìn)行緩沖區(qū)中數(shù)據(jù)的讀寫,將進(jìn)行epoll_wait之后的讀事件就緒以后的代碼改為如下:
else { if(evs[i].events & EPOLLIN)//讀事件就緒 { data_buf_p _data = (data_buf_p)malloc(sizeof(data_buf_t)); if(!_data) { perror("malloc"); continue; } _data->_fd = evs[i].data.fd; printf("read from fd: %d\n", _data->_fd); //創(chuàng)建進(jìn)程 pid_t id = fork(); if(id < 0)//創(chuàng)建失敗 perror("fork"); else if(id == 0)//子進(jìn)程 { printf("child proc: %d\n", getpid()); ssize_t size = MyRead(_data->_fd, _data->_buf, sizeof(_data->_buf)-1); //ssize_t size = read(_data->_fd, _data->_buf, sizeof(_data->_buf)-1); if(size < 0) printf("read error...\n"); else if(size == 0) { printf("client closed...\n"); exit(12); //epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL); //close(_data->_fd); //free(_data); } else { (_data->_buf)[size] = '\0'; printf("client# %s", _data->_buf); fflush(stdout); ep_ev.data.ptr = _data; ep_ev.events = EPOLLOUT | EPOLLET; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, _data->_fd, &ep_ev); } } else { pid_t ret = wait(NULL); if(ret < 0) perror("waitpid"); else printf("wait success : %d\n", ret); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL); close(_data->_fd); free(_data); } } else if(evs[i].events & EPOLLOUT) { data_buf_p _data = (data_buf_p)evs[i].data.ptr; MyWrite(_data->_fd, _data->_buf, strlen(_data->_buf)); //epoll_ctl(epoll_fd, EPOLL_CTL_DEL, _data->_fd, NULL); //close(_data->_fd); //free(_data); exit(11); }
這里要解釋:當(dāng)創(chuàng)建一個(gè)子進(jìn)程的時(shí)候,子進(jìn)程復(fù)制父進(jìn)程的PCB,自然也就會(huì)獲取其相應(yīng)的文件描述符進(jìn)行操作,但是當(dāng)需要改變其內(nèi)容的時(shí)候,比如文件描述符和epoll實(shí)例,子進(jìn)程就會(huì)進(jìn)行寫時(shí)拷貝,這個(gè)時(shí)候已經(jīng)不能單單進(jìn)行子進(jìn)程中關(guān)閉文件描述符和釋放空間的操作了,因?yàn)檫@并沒有起到實(shí)際效果,只不過是清除了拷貝出來的內(nèi)容而已,這就是為什么上面的程序中注釋掉了子進(jìn)程中的收尾工作,轉(zhuǎn)而在父進(jìn)程中進(jìn)行;而與此同時(shí),父進(jìn)程是需要進(jìn)行等待的,如果不進(jìn)行等待就會(huì)導(dǎo)致同一個(gè)IO事件的亂序而無法達(dá)到預(yù)期的效果;
運(yùn)行程序:
其實(shí),對(duì)于函數(shù)的可重入性,不免就會(huì)想到線程的安全問題,那么上面的程序如果給改成多線程的話是能不能行呢?
對(duì)于線程而言,是共享進(jìn)程的資源的,而errno是一個(gè)全局變量,在整個(gè)進(jìn)程空間內(nèi)都有效,因此,對(duì)于多線程也是同樣共享這一個(gè)全局變量的,雖然全局變量是臨界資源,但上述的問題并不是因?yàn)闋帄Z臨界資源而造成的,因?yàn)槭褂昧薴or循環(huán)來一個(gè)一個(gè)地處理IO事件,而是前一個(gè)操作對(duì)全局變量的改變影響了后來的操作,這是典型的函數(shù)的可重入性,函數(shù)的可重入性并不等同于線程安全,它需要函數(shù)內(nèi)部使用的變量全部來自于自身的??臻g,因此,如果用多線程或者線程互斥來進(jìn)行操作是沒有什么變化的。
《完》
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場(chǎng)景需求。