1. 程式人生 > >Linux學習之網路程式設計(epoll的用法)

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。