Linux學習之網路程式設計(epoll的用法)
言之者無罪,聞之者足以戒。 - “詩序”
epoll相關的函式包含在標頭檔案<sys/epoll.h>
epoll是Linux核心為處理大批量控制代碼而作了改進的poll,是Linux下多路複用IO介面select/poll的增強版本,它能顯著減少程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。
1. int epoll_create(int size);
說明:建立一個epoll控制代碼,size用來告訴核心這個監聽的數目一共有多大。這個引數不同於select()中的第一個引數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制代碼後,它就是會佔用一個fd值,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。
引數size:用來告訴核心要監聽的數目一共有多少個。
返回值:成功返回一個非負整數的檔案描述符,作為建立好的epoll控制代碼。失敗返回-1,錯誤資訊可以通過errno獲得。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
說明:epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。
引數epfd:epoll_create()函式返回的epoll控制代碼。
引數op:操作選項。
引數fd:要進行操作的目標檔案描述符。
引數event:struct epoll_event結構指標,將fd和要進行的操作關聯起來。
返回值:成功返回0,作為建立好的epoll控制代碼。失敗返回-1,錯誤資訊可以通過errno獲得。
引數op的可選值有以下3個:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
struct epoll_event結構如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下幾個巨集的集合:
EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
說明:等待事件的產生。
引數epfd:epoll_create()函式返回的epoll控制代碼。
引數events:struct epoll_event結構指標,用來存放從核心得到事件的集合。
引數 maxevents:告訴核心這個events有多大
引數 timeout: 等待時的超時時間,以毫秒為單位。
返回值:成功返回需要處理的事件數目。失敗返回0,表示等待超時。
注:epoll有兩種工作方式:
LT(level triggered,水平觸發)是預設的工作方式,並且同時支援block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
ET (edge-triggered,邊緣觸發)是高速工作方式,只支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,直到你做了某些操作導致那個檔案描述符不再為就緒狀態了(比如,你在傳送,接收或者接收請求,或者傳送接收的資料少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),核心不會發送更多的通知(only once)。
下面直接給出程式碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#define SERV_PORT 8888
#define MAX_LISTEN_QUE 5
#define MAX_BUFFER_SIZE 100
#define RT_ERR (-1)
#define RT_OK 0
#define MAX_EVENTS 500
//建立套接字子函式
int IPv4_tcp_create_socked(void)
{
int listenfd,sockfd,opt = 1;
struct sockaddr_in server,client;
socklen_t len;
int timep;
int ret;
//建立套接字
listenfd = socket(AF_INET,SOCK_STREAM,0);//ipv4,全雙工通訊
if(listenfd < 0){
perror("cretae socket error\n");
return -1;
}
//設定地址重用
if((ret = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) < 0){
perror("set sockopt failure\n");
return -1;
}
//初始化伺服器結構體
bzero(&server,sizeof(server));
server.sin_family = AF_INET;//ipv4
server.sin_port = htons(SERV_PORT);//埠號(主機序轉換到網路序)
server.sin_addr.s_addr = htonl(INADDR_ANY);//允許所有的客戶端連線
len = sizeof(struct sockaddr);
//繫結埠號,IP到套接字
if(bind(listenfd,(struct sockaddr *)&server,len) < 0){
perror("bind error\n");
return -1;
}
//設定最大連線數
listen(listenfd,MAX_LISTEN_QUE);
return listenfd;
}
//資料處理子函式
int Process_data(int sockfd)
{
int bytes;
char buf[MAX_BUFFER_SIZE];
char *s = buf;
char flag = 1;
int len;
while(flag)
{
//讀取資料
bytes = recv(sockfd,s,100,0);
if(bytes < 0){
//判斷出錯的型別是不是已經讀完
if(errno == EAGAIN){
printf("no data\n");
break;
}
perror("recv error\n");
return -1;
}
//客戶端斷開連線
if(bytes == 0){
return -2;
}
if(bytes == 100){
flag = 1;
}
else{
flag = 0;
}
//調整儲存資料指標
s += bytes;
//獲得讀取的位元組數
len += bytes;
printf("bytes:%d\n",bytes);
}
printf("buf:%s\n",buf);
send(sockfd,buf,len,0);
return 0;
}
int main(int argc,char *argv[])
{
int listenfd,sockfd;
int epollfd,fds;
struct epoll_event ev,events[MAX_EVENTS];
int i,rv;
struct sockaddr_in client;
int len;
len = sizeof(struct sockaddr_in);
//建立epoll控制代碼
epollfd = epoll_create(MAX_EVENTS);
if(epollfd < 0){
perror("epoll_create error\n");
return -1;
}
//呼叫建立套接字函式
listenfd = IPv4_tcp_create_socked();
//把套接字設定為非阻塞方式
fcntl(listenfd,F_SETFL,O_NONBLOCK);
//設定要監聽的套接字的可讀監聽模式
ev.data.fd = listenfd;
ev.events = EPOLLIN;
//epoll的註冊函式(新增要監聽的套接字)
rv = epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&ev);
if(rv < 0){
perror("epoll_ctl error\n");
return -1;
}
while(1)
{
//等待事件的產生
fds = epoll_wait(epollfd,events,MAX_EVENTS,-1);
if(fds < 0){
perror("epoll_wait error\n");
return -1;
}
for(i = 0;i < fds;i++)
{
//判斷監聽的套接字是不是我們建立的監聽套接字
if(events[i].data.fd == listenfd)
{
sockfd = accept(listenfd,(struct sockaddr *)&client,&len);
if(sockfd <0){
perror("accept error\n");
continue;
}
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;//設定為讀監聽並且是邊沿觸發
//epoll的註冊函式(新增要監聽的套接字)
epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&ev);
continue;
}//如果不是我們建立的套接字,就是有資料到來
else{
//呼叫資料處理函式(傳入的入口引數是我們建立的通訊套接字)
rv = Process_data(events[i].data.fd);
if(rv == -2){
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,&ev);
close(events[i].data.fd);
continue;
}
}
}
}
}
上面的程式碼用到了函式fcntl,這個函式上面我也沒有給出解釋,下面說一下這個函式:
4、fcntl(int fd, int cmd, ... /* arg */ )
引數fd:建立的套接字的目標檔案描述符
引數cmd:要執行的控制操作
常用的用法:
(1)把一個套接字設定為非阻塞型:cmd為F_SETFL,flags“包含”O_NONBLOCK。(fcntl(listenfd,F_SETFL,O_NONBLOCK))
(2)把一個套接字設定成一旦其狀態發生變化,核心就產生一個SIGIO:cmd為F_SETFL,flags“包含”O_ASYNC。
(3)關於套接字的當前屬主。
fcntl函式有5種功能:
1.複製一個現有的描述符(cmd=F_DUPFD).
2.獲得/設定檔案描述符標記(cmd=F_GETFD或F_SETFD).
3.獲得/設定檔案狀態標記(cmd=F_GETFL或F_SETFL).
4.獲得/設定非同步I/O所有權(cmd=F_GETOWN或F_SETOWN).
5.獲得/設定記錄鎖(cmd=F_GETLK,F_SETLK或F_SETLKW).
cmd的選項:
F_DUPFD 返回一個如下描述的(檔案)描述符:
(1)最小的大於或等於arg的一個可用的描述符
(2)與原始操作符一樣的某物件的引用
(3)如果物件是檔案(file)的話,返回一個新的描述符,這個描述符與arg共享相同的偏移量(offset)
(4)相同的訪問模式(讀,寫或讀/寫)
(5)相同的檔案狀態標誌(如:兩個檔案描述符共享相同的狀態標誌)
(6)與新的檔案描述符結合在一起的close-on-exec標誌被設定成交叉式訪問execve(2)的系統呼叫
F_GETFD 取得與檔案描述符fd聯合close-on-exec標誌,類似FD_CLOEXEC.如果返回值和FD_CLOEXEC進行與運算結果是0的話,檔案保持交叉式訪問exec(),否則如果通過exec執行的話,檔案將被關閉(arg被忽略)
F_SETFD 設定close-on-exec旗標。該旗標以引數arg的FD_CLOEXEC位決定。
F_GETFL 取得fd的檔案狀態標誌,如同下面的描述一樣(arg被忽略)
F_SETFL 設定給arg描述符狀態標誌,可以更改的幾個標誌是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。
F_GETOWN 取得當前正在接收SIGIO或者SIGURG訊號的程序id或程序組id,程序組id返回成負值(arg被忽略)
F_SETOWN 設定將接收SIGIO和SIGURG訊號的程序id或程序組id,程序組id通過提供負值的arg來說明,否則,arg將被認為是程序id
命令字(cmd)F_GETFL和F_SETFL的標誌如下面的描述:
O_NONBLOCK 非阻塞I/O;如果read(2)呼叫沒有可讀取的資料,或者如果write(2)操作將阻塞,read或write呼叫返回-1和EAGAIN錯誤
O_APPEND 強制每次寫(write)操作都新增在檔案大的末尾,相當於open(2)的O_APPEND標誌
O_DIRECT 最小化或去掉reading和writing的快取影響.系統將企圖避免快取你的讀或寫的資料;如果不能夠避免快取,那麼它將最小化已經被快取了的數 據造成的影響.如果這個標誌用的不夠好,將大大的降低效能
O_ASYNC 當I/O可用的時候,允許SIGIO訊號傳送到程序組,例如:當有資料可以讀的時候
注意: 在修改檔案描述符標誌或檔案狀態標誌時必須謹慎,先要取得現在的標誌值,然後按照希望修改它,最後設定新標誌值。不能只是執行F_SETFD或F_SETFL命令,這樣會關閉以前設定的標誌位。
fcntl的返回值: 與命令有關。如果出錯,所有命令都返回-1,如果成功則返回某個其他值。下列三個命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一個返回新的檔案描述符,第二個返回相應標誌,最後一個返回一個正的程序ID或負的程序組ID。