1. poll詳解
函數原型
intpoll(struct?pollfd *fd,?nfds_t?nfds,?int?timeout);
函數參數
fd:數組的地址,struct pollfd all[120]; 其中struct pollfd結構體如下
structpollfd {
int? ?fd; ? ? ? ??/* 文件描述符 */
short?events; ? ??/* 等待的事件 */
short?revents; ? ?/* 實際發(fā)生的事件 */
? ? };
結構體紅各項含義如下:
文件描述符fd:表示要堅持測的fd,通過 open("a.txt", O_wronly | O_append); 獲得。
events:要等待的事件
revents:實際發(fā)生的事件,它是內核給的反饋,在select的時候,會有一個備份來供內核修改并傳出。
nfds:數組的最大長度, 數組中最后一個使用的元素下標+1
內核會輪詢檢測fd數組的每個文件描述符
timeout:
1:永久阻塞
0:調用完成立即返回
>0:等待的時長毫秒
函數返回值:IO發(fā)生變化的文件描述符的個數。
2. epoll詳解
(1)API介紹
intepoll_create(int?size);
函數功能:生成一個epoll專用的文件描述符,實際上就是生成一個epoll樹的根結點。
函數參數:size,epoll樹上能掛的最大文件描述符數量。表示我想在這個樹節(jié)點上掛size個節(jié)點,假如實際上的節(jié)點大于size的話epoll會自動擴展,所以這個大小可以隨便傳,不用太在意。但是這個擴展也是有上限的,如果電腦內存是1G,那么擴展的上限是10萬(2G就是20萬。。。通過加內存可以擴大上限)。
函數返回值:函數返回值是樹的根節(jié)點,在后面用到epft參數的時候,都是指這個返回值,也就是樹的根節(jié)點。
intepoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event *event);
函數功能:用于控制某個epoll文件描述符事件,可以注冊、修改、刪除。
函數參數:
epfd:epoll_create()函數生成的專用文件描述符。
op:
EPOLL_CTL_ADD ? ? ? —— ?注冊
EPOLL_CTL_MOD ? ? ?—— ?修改
EPOLL_CTL_DEL ? ? ? ?—— ?刪除
fd:關聯(lián)的文件描述符
event:告訴內核要監(jiān)聽什么事件
EPOLLIN ? ? —— 讀
EPOLLOUT —— 寫
EPOLLERR ?—— 異常
structepoll_event {/* 該結構體主要存放和fd有關的信息 */
uint32_t? ? ?events; ? ? ? ? ? ? ? ? ? ? ? ? ?
epoll_data_t?data;?
? ? ? };
typedefunion epoll_data {
void? ? ? ? ?*ptr;
int? ? ? ? ? fd;
uint32_t? ? ?u32;
uint64_t? ? ?u64;
? ? ? }?epoll_data_t;
epoll_data_t是一個聯(lián)合體union,四個成員共用同一塊內存,也就是說四個成員我們只能用一個,一般情況下我們用fd,這個fd實際上就是epoll_ctl()函數的第三個參數fd。
如果我們想在epoll樹上掛載更多信息,而不僅僅是fd文件描述符的話,我們可以把更多信息封裝在結構體中,并把該結構體傳給epoll_data_t結構體的ptr指針,這樣就可以在epoll樹上掛載和fd有關的更多信息。
structsockInfo
? ? ? ? {
int? ? ? ? ?fd;
structsockaddr_inaddr;
? ? ? ? };
比如說,要獲取發(fā)生變化的fd對應的client的IP和port,就可以利用指針ptr,這樣的話聯(lián)合epoll_data_t中的fd就不能用了,我們把文件描述符傳給sockInfo的fd即可完成fd信息的掛載。
intepoll_wait(
int?epfd,
struct?epoll_event* events, ?/* 結構體數組 */
int?maxevents,
int?timeout
);
函數功能:等待IO事件發(fā)生(可以設置阻塞),epoll_wait()函數相當于前面講的select()或poll()函數,表示委托內核去進行檢測。epoll_event通過返回值和傳出參數events來實現(xiàn)把哪幾個fd發(fā)生變化告訴server進程的目的。首先,每當有fd變化,就把這個fd對應的樹節(jié)點拷貝到events數組中,最后,有幾個fd變化,就返回幾。這樣只要根據返回值和參數events就可以遍歷出所有變化的fd以及相關信息。
函數參數:
epfd:要檢測的句柄
events:用于回傳待處理事件的數組。它是一個傳出參數,需要提前分配內存,哪個fd發(fā)生變化了,就把哪個fd的樹節(jié)點(struct epoll_event)拷貝一份放到這個數組中。這樣epoll就能返回是哪個fd發(fā)生了變化。
maxevents:告訴內核events的大小,因為內核要把發(fā)生變化的fd對應的樹節(jié)點拷貝到數組中,所以要知道數組大小。
timeout:為超時時間
1:永久阻塞
0:立即返回
>0
函數返回值:有多少個fd發(fā)生了變化就返回幾(變化的fd信息存在events數組中)。
(2)epoll樹
(3)epoll模型
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/epoll.h>
intmain(int?argc, constchar* argv[])
{
if(argc <?2)
? ? {
printf("eg: ./a.out portn");
exit(1);
? ? }
structsockaddr_inserv_addr;
socklen_t?serv_len =?sizeof(serv_addr);
int?port =?atoi(argv[1]);?//字符串轉整形值
// 創(chuàng)建套接字
int?lfd =?socket(AF_INET, SOCK_STREAM,?0);
// 初始化服務器 sockaddr_in?
memset(&serv_addr,?0, serv_len);
? ? serv_addr.sin_family = AF_INET;?// 地址族?
? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY);?// 監(jiān)聽本機所有的IP
? ? serv_addr.sin_port =?htons(port);?// 設置端口?
// 綁定IP和端口
? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 設置同時監(jiān)聽的最大個數
? ??listen(lfd,?36);
printf("Start accept ......n");
structsockaddr_inclient_addr;
socklen_t?cli_len =?sizeof(client_addr);
// 創(chuàng)建epoll樹根節(jié)點
int?epfd =?epoll_create(2000);
// 初始化epoll樹
structepoll_eventev;
? ? ev.events = EPOLLIN;
? ? ev.data.fd = lfd;
? ??epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
//存放發(fā)生變化的fd對應的樹節(jié)點
structepoll_eventall[2000];
while(1)
? ? {
// 使用epoll通知內核fd 文件IO檢測
int?ret =?epoll_wait(epfd, all,?sizeof(all)/sizeof(all[0]),?-1);
// 遍歷all數組中的前ret個元素 //ret表示有幾個變化的fd,變化的fd都存在all數組中
for(int?i=0; i<ret; ++i)
? ? ? ? {
int?fd = all[i].data.fd;
// 判斷是否有新連接
if(fd == lfd)
? ? ? ? ? ? {
// 接受連接請求 // accept不阻塞,因為已經有連接
int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);
if(cfd ==?-1)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ??perror("accept error");
exit(1);
? ? ? ? ? ? ? ? }
// 將新得到的cfd掛到樹上
structepoll_eventtemp;
? ? ? ? ? ? ? ? temp.events = EPOLLIN;?//檢測cfd對應的讀緩沖區(qū),是否有數據傳入
? ? ? ? ? ? ? ? temp.data.fd = cfd;
? ? ? ? ? ? ? ??epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客戶端信息
char?ip[64] = {0};
printf("New Client IP: %s, Port: %dn",
? ? ? ? ? ? ? ??inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),
? ? ? ? ? ? ? ? ? ? ? ??ntohs(client_addr.sin_port));
? ? ? ? ? ? }
else
? ? ? ? ? ? {
// 處理已經連接的客戶端發(fā)送過來的數據
if(!all[i].events & EPOLLIN)?//只處理讀事件
? ? ? ? ? ? ? ? {
continue;
? ? ? ? ? ? ? ? }
/*
? ? ? ? ? ? ? ? 假如說client發(fā)送過了100個數據,也就是serve的read緩沖區(qū)有100個數據,
? ? ? ? ? ? ? ? 但是調用recv函數的時候只能讀50個數據,而本次循環(huán)只調用了一次recv,
? ? ? ? ? ? ? ? 那么只能下次循環(huán)再讀剩余的50個數據,所以下次循環(huán)檢測的時候,
? ? ? ? ? ? ? ? epoll_wait還是會返回,因為緩沖區(qū)還是剩余數據。這就是水平觸發(fā)模式。
? ? ? ? ? ? ? ? 這樣的話雖然client只發(fā)了1次,但是epoll_wait會通知兩次server去讀數據。
? ? ? ? */
// 讀數據
char?buf[1024] = {0};
int?len =?recv(fd, buf,?sizeof(buf),?0);
if(len ==?-1)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ??perror("recv error");
exit(1);
? ? ? ? ? ? ? ? }
elseif(len ==?0)
? ? ? ? ? ? ? ? {
printf("client disconnected ....n");
//close(fd);
// fd從epoll樹上刪除
? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);
// 掛樹的時候需要ev,把ev掛在樹上刪除寫NULL就行了
if(ret ==?-1)
? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ??perror("epoll_ctl del error");
exit(1);
? ? ? ? ? ? ? }
? ? ? ? ? ? ? ?close(fd);
? ? ? ? ? ? ? ? }
else
? ? ? ? ? ? ? ? {
printf(" recv buf: %sn", buf);
? ? ? ? ? ? ? ? ? ??write(fd, buf, len);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ??close(lfd);
return0;
}
epoll維護的紅黑樹是存在一個共享內存中,內核和用戶都可以通過操作這個共享內存來操作樹,不需要內核態(tài)和用戶態(tài)的切換,也不需要兩種狀態(tài)之間的數據拷貝,所以效率更高。
(4)epoll的三種工作模式
水平觸發(fā)模式 ? - (根據讀來解釋)
只要fd對應的緩沖區(qū)有數據,epoll_wait就會返回
返回的次數與發(fā)送數據的次數沒有關系
epoll默認的工作模式
邊沿觸發(fā)模式 - ET
fd - 默認阻塞屬性
客戶端給server發(fā)數據:
發(fā)一次數據server 的 epoll_wait就返回一次
不在乎數據是否讀完
如果讀不完,如何把數據全部讀出來?
while(recv());
數據讀完之后recv會阻塞
解決阻塞問題 —— 設置非阻塞fd
對于epoll_wait()來說,epoll_wait 調用次數越多, 系統(tǒng)的開銷越大。
水平觸發(fā)模式會多次返回,只要server的read緩沖區(qū)有數據,epoll_wait就返回,也就會通知server去讀數據,那么在循環(huán)檢測的時候,只要server的read緩沖區(qū)有數據,epoll_wait就會多次調用,多次返回,并通知server去讀數據;假如說client發(fā)送過了100個數據,也就是serve的read緩沖區(qū)有100個數據,但是調用recv函數的時候只能讀50個數據,而本次循環(huán)只調用了一次recv,那么只能下次循環(huán)再讀剩余的50個數據,所以下次循環(huán)檢測的時候,epoll_wait還是會返回,因為緩沖區(qū)還是剩余數據。這就是水平觸發(fā)模式。這樣的話雖然client只發(fā)了1次,但是epoll_wait會通知兩次server去讀數據。
—— (printf函數是標準C庫函數,C庫函數都有一個默認緩沖區(qū),printf的大小是8K。printf函數是行緩沖,使用printf函數的時候,如果不加 n 會默認等到寫滿的時候才打印內容,加 n 會強制把緩沖區(qū)的內容打印出來。另外 表示結束,不加 就會一直輸出直到遇到 ,用write(STDOUT_FILENO)替代printf函數就可以解決這些問題。)
邊沿觸發(fā)模式,client發(fā)一次數據epoll_wait只返回一次,也就只讀一次,這樣的話server的read緩沖區(qū)可能會有很多數據堆積,server讀數據的時候可能讀到的是上一次剩余的數據,并且只有client發(fā)的時候,epoll_wait才會通知server去讀數據,邊沿觸發(fā)模式盡可能減少了epoll_wait的調用次數,缺點是數據有可能讀不完導致堆積;
邊沿非阻塞觸發(fā)
效率最高
如何設置非阻塞
open()
設置flags
必須 O_WDRW | O_NONBLOCK
終端文件: /dev/tty
fcntl
int flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
如何將緩沖區(qū)的全部數據都讀出?
while(recv() >?0)
? ? ?{
? ? ??printf();
? ? ?}
當緩沖區(qū)數據讀完之后, 返回值是否為0?
阻塞狀態(tài)
數據讀完之后,recv阻塞
非阻塞狀態(tài)
強行讀了一個沒有數據的緩沖區(qū)(fd),數據已經被讀完了,因為是非阻塞,所以在while循環(huán)中recv還要繼續(xù)讀,導致返回-1
判斷 errno == EAGAIN
示例
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.h>
intmain(int?argc, constchar* argv[])
{
if(argc <?2)
? ? {
printf("eg: ./a.out portn");
exit(1);
? ? }
structsockaddr_inserv_addr;
socklen_t?serv_len =?sizeof(serv_addr);
int?port =?atoi(argv[1]);
// 創(chuàng)建套接字
int?lfd =?socket(AF_INET, SOCK_STREAM,?0);
// 初始化服務器 sockaddr_in?
memset(&serv_addr,?0, serv_len);
? ? serv_addr.sin_family = AF_INET; ? ? ? ? ? ? ? ? ??// 地址族?
? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY); ? ?// 監(jiān)聽本機所有的IP
? ? serv_addr.sin_port =?htons(port); ? ? ? ? ? ?// 設置端口?
// 綁定IP和端口
? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 設置同時監(jiān)聽的最大個數
? ??listen(lfd,?36);
printf("Start accept ......n");
structsockaddr_inclient_addr;
socklen_t?cli_len =?sizeof(client_addr);
// 創(chuàng)建epoll樹根節(jié)點
int?epfd =?epoll_create(2000);
// 初始化epoll樹
structepoll_eventev;
// 設置邊沿觸發(fā)
? ? ev.events = EPOLLIN;?//監(jiān)聽的文件描述符沒必要邊沿觸發(fā),主要是通信的cfd
? ? ev.data.fd = lfd;
? ??epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
structepoll_eventall[2000];
while(1)
? ? {
// 使用epoll通知內核fd 文件IO檢測
int?ret =?epoll_wait(epfd, all,?sizeof(all)/sizeof(all[0]),?-1);
printf("================== epoll_wait =============n");
// 遍歷all數組中的前ret個元素
for(int?i=0; i<ret; ++i)
? ? ? ? {
int?fd = all[i].data.fd;
// 判斷是否有新連接
if(fd == lfd)
? ? ? ? ? ? {
// 接受連接請求
int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);
if(cfd ==?-1)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ??perror("accept error");
exit(1);
? ? ? ? ? ? ? ? }
// 設置文件cfd為非阻塞模式
int?flag =?fcntl(cfd, F_GETFL);
? ? ? ? ? ? ? ? flag |= O_NONBLOCK;
? ? ? ? ? ? ? ??fcntl(cfd, F_SETFL, flag);
// 將新得到的cfd掛到樹上
structepoll_eventtemp;
// 設置邊沿觸發(fā)
? ? ? ? ? ? ? ? temp.events = EPOLLIN | EPOLLET;
? ? ? ? ? ? ? ? temp.data.fd = cfd;
? ? ? ? ? ? ? ??epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客戶端信息
char?ip[64] = {0};
printf("New Client IP: %s, Port: %dn",
? ? ? ? ? ? ? ??inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),
? ? ? ? ? ? ? ??ntohs(client_addr.sin_port));
? ? ? ? ? ? }
else
? ? ? ? ? ? {
// 處理已經連接的客戶端發(fā)送過來的數據
if(!all[i].events & EPOLLIN)?
? ? ? ? ? ? ? ? {
continue;
? ? ? ? ? ? ? ? }
// 讀數據
char?buf[5] = {0};
int?len;
// 循環(huán)讀數據
while( (len =?recv(fd, buf,?sizeof(buf),?0)) >?0?)
? ? ? ? ? ? ? ? {
// 數據打印到終端
//不要用printf,因為printf如果找不到 n 字符會出現(xiàn)亂碼,打印不出來等問題
? ? ? ? ? ? ? ? ? ??write(STDOUT_FILENO, buf, len);
// 發(fā)送給客戶端
? ? ? ? ? ? ? ? ? ??send(fd, buf, len,?0);
? ? ? ? ? ? ? ? }
if(len ==?0)
? ? ? ? ? ? ? ? {
printf("客戶端斷開了連接n");
? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);
if(ret ==?-1)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ??perror("epoll_ctl - del error");
exit(1);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ??close(fd);
? ? ? ? ? ? ? ? }
elseif(len ==?-1)
? ? ? ? ? ? ? ? {
//數據已經被讀完了,因為是非阻塞,所以在while循環(huán)中recv還要繼續(xù)讀,導致返回-1
if(errno == EAGAIN)
? ? ? ? ? ? ? ? ? ? {
printf("緩沖區(qū)數據已經讀完n");
? ? ? ? ? ? ? ? ? ? }
else
? ? ? ? ? ? ? ? ? ? {
//這才是真正的recv錯誤
printf("recv error----n");
exit(1);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
#if?0
if(len ==?-1)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ??perror("recv error");
exit(1);
? ? ? ? ? ? ? ? }
elseif(len ==?0)
? ? ? ? ? ? ? ? {
printf("client disconnected ....n");
// fd從epoll樹上刪除
? ? ? ? ? ? ? ? ? ? ret =?epoll_ctl(epfd, EPOLL_CTL_DEL, fd,?NULL);
if(ret ==?-1)
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ??perror("epoll_ctl - del error");
exit(1);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ??close(fd);
? ? ? ? ? ? ? ? }
else
? ? ? ? ? ? ? ? {
// printf(" recv buf: %sn", buf);
? ? ? ? ? ? ? ? ? ??write(STDOUT_FILENO, buf, len);
? ? ? ? ? ? ? ? ? ??write(fd, buf, len);
? ? ? ? ? ? ? ? }
#endif
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ??close(lfd);
return0;
}
5)文件描述符1024限制
對于select來說,無法突破文件描述符1024上限,因為select是通過數組實現(xiàn)的。poll和epoll可以突破1024限制,poll是內部鏈表實現(xiàn),而epoll是紅黑樹實現(xiàn)。
查看受計算機硬件限制的文件描述符上限可以通過下面命令
cat?/proc/sys/fs/file-max
同樣,我們也可以通過修改配置文件來修改這個上限,但是,我們在程序中設置的時候不能超過硬件限制的上限
vim /etc/security/limits.conf
- soft ?nofile ? ?8000 ? ? ?—— 也可以通過命令ulimit -n 2000來修改為2000
- hard ?nofile ? 8000 ? ? ?—— 硬件資源限制
修改后重啟系統(tǒng)即可起效。