Linux之IO多路複用

一、什麼是IO多路複用

關於這部分內容請移步到下面的鏈接進行學習,本文主要介紹select,poll和epoll三種多路複用的實現。

https://www.zhihu.com/question/32163005

 

二、select的實現

2.1 select的原理:

user space創建一張存放文件描述符的表,(該表的大小爲1024個bit,受限於能打開文件個數),並且將需要監測的文件描述符在表內對應的bit置1,(如下圖,需要監測描述符值爲3和1020的兩個文件描述符,我們將監測表內的第3和第1020個bit置1,其他的都爲0),當調用select函數的時候,select函數會將該表拷貝到kernel space,然後進行輪訓操作,當某些文件不能進行IO操作時,kernel space會把傳來表內的位置0,能進行IO操作的則保留原來的1,然後通過select函數將表返回給user space,user space遍歷返回的表就能找到那個文件能進行IO操作。然後調用對應函數進行操作即可。

 

 

2.2 select函數的基本流程

使用select函數的基本流程:

(1)創建監聽描述符表     

(2)把需要監聽的描述符放到表中(將相應的位置一)

(3)計算出被監聽的描述符中的最大值

(4)調用select函數

(5)select函數返回之後,遍歷被監聽表,找到能進行操作的文件,並調用相應函數

(6)重複調用用2、3、4、5步

 

2.3 相關函數介紹

select函數:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

參數含義:

nfds:指定測試的描述符個數,它的值是衆多待測試描述符中值最大的那個再加1。

 

readfds、writefds、exceptfds:是指定我們讓內核測試讀寫和異常測試條件的描述表,若無需監測某一條件,將其設置爲NULL即可。

timeout:設置內核等待時間。設置爲NULL時爲阻塞等待。

 

對文件描述符表操作函數:

void FD_ZERO(fd_set *fdset);             //清空文件描述符監聽表
void FD_SET(int fd, fd_set *fdset);      //將指定文件描述符添加到表內,表內對應位置1
void FD_CLR(int fd, fd_set *fdset);      //將指定文件描述符從表內移除,表內對應位置0
void FD_ISSET(int fd, fd_set *fdset);    //判斷指定文件是否可進行操作,能操作返回1,否則返回0

 

2.4 具體實現代碼

代碼爲socket一個服務端同時和多個客戶機進行通訊的實例。

客戶端代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define SPORT 	12000                 //主機端口號
#define SIZE 	200                  
#define ADDR	"127.0.0.1"

int CreateConnect(const char* ip, int port);

int main(int argc, const char* argv[])
{
	
	int sockfd = 0;
	int len = 0;
	struct sockaddr_in addr;
	char str[SIZE] = {0};
	
	sockfd = CreateConnect(ADDR, SPORT);
	if ( 0 > sockfd)
	{
		perror("sockInit is failed");
		return -1;
	}
	
	while (1)
	{
		fgets(str, SIZE - 1, stdin);
		
		if (strncmp(str, "quit", 4) == 0)
		{
			break;
		}
		
		send(sockfd, str, strlen(str), 0);
		
		if (0 < recv(sockfd, str, SIZE - 1, 0))
		{
			printf("get server data %s\n", str);
		}
	}
	
	close(sockfd);
	return 0;
}

int CreateConnect(const char* ip, int port)
{
	int sockfd = 0;
	int len = 0;
	struct sockaddr_in addr;
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (0 > sockfd)
	{
		perror("socket is failed");
		return -1;
	}
	printf("sockfd is ok\n");
	
	memset(&addr, 0, sizeof(len));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(SPORT);
	addr.sin_addr.s_addr = inet_addr(ADDR);
	
	if (0 > connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)))
	{
		perror("connect is failed");
		return -1;
	}
	printf("connect is ok\n");
	
	return sockfd;
}

 

服務端代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>

#define SPORT 	12000
#define SIZE 	200
#define ADDR	"127.0.0.1"

int SockfdInit(const char* ip, int port);
int SelectTask(int fd, int* numfd, fd_set* readfds, fd_set* tempfds);


int main(int argc, const char* argv[])
{
	int ret = 0;
	int sockfd = 0;
	int maxfd = 0;
	fd_set rdfs;
	fd_set tempfds;
	
	
	sockfd = SockfdInit(ADDR, SPORT);
	if (0 > sockfd)
	{
		perror("socket is failed");
		return -1;
	}
	
	FD_ZERO(&rdfs);                          //初始化監聽表
	FD_ZERO(&tempfds);                       //初始化備份表
	
	FD_SET(sockfd, &rdfs);                   //將socket添加到表內進行監聽
	FD_SET(sockfd, &tempfds);
	
	maxfd = (maxfd > sockfd) ? (maxfd) : (sockfd);   //獲取最大文件描述符的值
	
	while (1)
	{
		printf("調用sele\n");
		rdfs = tempfds;                      //注意select每次調用都會改變傳入參數,所以需要將參數進行備份
		
		ret = select(maxfd + 1, &rdfs, NULL, NULL, NULL);
		if (0 > ret)                        
		{
			perror("select is failed");
			break;
		}
		else if (0 == ret)
		{
			perror("timeout");
			break;
		}
		
		SelectTask(sockfd, &maxfd, &rdfs, &tempfds);
	}
	
	close(sockfd);
	return 0;
}



int SockfdInit(const char* ip, int port)
{
	if (NULL == ip)
	{
		printf("請輸入正確的ip地址");
		return -1;
	}
	
	int ret = 0;
	int len = 0;
	int sockfd = 0;
	struct sockaddr_in addr;
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (0 > sockfd)
	{
		perror("sockfd is failed");
		return -1;
	}
	printf("socket is ok\n");
	
	int on = 1;
	ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));		//設置端口複用
	if (0 > ret)
	{
		perror("setsockopt is failed");
		return -1;
	}
	printf("setsockopt is ok\n");
	
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	
	if (0 > bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)))
	{
		perror("bind is failed");
		return -1;
	}
	printf("bind is ok\n");
	
	if (0 > listen(sockfd, 5))
	{
		perror("listen is failed");
		return -1;
	}
	printf("listen is ok\n");
	
	return sockfd;
}

int SelectTask(int fd, int* numfd, fd_set* readfds, fd_set* tempfds)
{
	int i = 0;
	int len = 0;
	int ret = 0;
	int newfd = 0;
	char str[SIZE] = {0};
	struct sockaddr_in addr;
	
	if (FD_ISSET(fd, readfds))          //先判斷是否爲socket能操作,因爲tcp通信等待連接有時間限制
	{
		len = sizeof(addr);
		newfd = accept(fd, (struct sockaddr*)&addr, &len);
		if (0 > newfd)
		{
			perror("accept is failed");
			close(newfd);
			return -1;
		}
		printf("accept is ok  port = %d\n", ntohs(addr.sin_port));
		
		FD_SET(newfd, tempfds);        //將新連接的客戶端也添加到表內進行監聽
		*numfd = (*numfd > newfd) ? (*numfd) : (newfd);  //找到最大文件描述符
	}
	
	for (i = 0; i <= *numfd; i++)     //遍歷返回的表,並將數據返回到對應的客戶端
	{
		if (FD_ISSET(i, readfds) && i != fd)
		{
			memset(str, 0, sizeof(str));
			ret = recv(i, str, SIZE - 1, 0);
			if (ret < 0)
			{
				perror("recv failed");
				FD_CLR(i , tempfds);
				close(i);
				continue;
			}
			else if (ret == 0)
			{
				FD_CLR(i , tempfds);
				close(i);
				continue;
			}
			printf("get %d client data %s\n", i, str);
			send(i, str, strlen(str), 0);
		}
		
	}
	
	return 0;
}

 

這裏我們發現服務器端創建了兩個監聽表,rdfs和tempfds,調用selecet函數之前,先將tempfds表的數據傳給rdfs表,然後將rdfs表傳遞到select函數去調用select函數,並且在SelectTask函數中,每次對錶進行操作的時候,都是對tempfds表操作,這是因爲select函數每次調用的時候會改變傳入函數中的參數。我們需要將數據備份

 

三、poll的原理

3.1 poll的原理

user space創建一個結構體數組,數組中每一個成員由文件描述符、當前監聽的事件、返回的事件三個元素構成(數組大小自己設置,每個元素就是一個文件),在調用poll之前,把前兩個元素寫好,然後調用poll,這個函數會拷貝表到kernel space,輪詢,在poll返回時,如果某個文件可以進行IO操作,那麼,它對應的“返回的事件”將被修改,user space再次遍歷返回的數組,查看數組中每一個成員對應在的“返回的事件”,然後,調用相應的函數進行操作。

struct pollfd 
{
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

監聽事件events自己設置,判斷IO是否能操作時,就是判斷revents是否與我們設置的events相同即可。

往監聽數組中添加監聽文件和移除監聽文件流程如下圖所示:

3.2 poll的流程

使用poll函數的基本流程:

(1)創建監聽數組    

(2)把需要監聽的文件放到數組中,並且設置好監聽事件

(3)計算監聽數組中,監聽事件存放在最大的索引值

(4)調用poll函數

(5)poll函數返回之後,遍歷被監聽數組,找到能進行操作的文件,並調用相應函數

(6)重複調用用4、5步

 

3.3 相關函數

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

參數:

fds:監聽數組

nfds:監聽數組的長度

timeout:等待時間時間的,單位爲ms

 

3.4 具體實現代碼

代碼爲socket一個服務端同時和多個客戶機進行通訊的實例。

客戶端代碼與上文中select舉例一致。

服務端代碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <poll.h>

#define SPORT 	12000
#define SIZE 	200
#define ADDR	"127.0.0.1"

int SockfdInit(const char* ip, int port);
int PollTask(struct pollfd *pfd, int* index, int maxnum);


int main(int argc, const char* argv[])
{
	int i = 0;
	int ret = 0;
	int index = 0;
	int sockfd = 0;
	struct pollfd pfd[SIZE];    
	
	
	sockfd = SockfdInit(ADDR, SPORT);
	if (0 > sockfd)
	{
		perror("socket is failed");
		return -1;
	}
	
	for (i = 0; i < SIZE; i++)                  //初始化poll表
	{
		pfd[i].fd = -1;
		pfd[i].events = POLLIN;             //默認監聽時間都爲讀事件        
	}
	
	pfd[0].fd = sockfd;                        
	index++;                                    //監聽數組內存放監聽事件的最大索引值
	
	while (1)
	{
		printf("調用poll\n");
		
		ret = poll(pfd, SIZE, 3000);        //等待事件觸發事件爲3000ms
		if (0 > ret)
		{
			perror("select is failed");
			break;
		}
		else if (0 == ret)
		{
			perror("timeout");
			continue;
		}
		
		PollTask(pfd, &index, SIZE);
	}
	
	close(sockfd);
	return 0;
}



int SockfdInit(const char* ip, int port)
{
	if (NULL == ip)
	{
		printf("請輸入正確的ip地址");
		return -1;
	}
	
	int ret = 0;
	int len = 0;
	int sockfd = 0;
	struct sockaddr_in addr;
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (0 > sockfd)
	{
		perror("sockfd is failed");
		return -1;
	}
	printf("socket is ok\n");
	
	int on = 1;
	ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	if (0 > ret)
	{
		perror("setsockopt is failed");
		return -1;
	}
	printf("setsockopt is ok\n");
	
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	
	if (0 > bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)))
	{
		perror("bind is failed");
		return -1;
	}
	printf("bind is ok\n");
	
	if (0 > listen(sockfd, 5))
	{
		perror("listen is failed");
		return -1;
	}
	printf("listen is ok\n");
	
	return sockfd;
}

int PollTask(struct pollfd *pfd, int* index, int maxnum)
{
	int i = 0;
	int len = 0;
	int ret = 0;
	int newfd = 0;
	char str[SIZE] = {0};
	struct sockaddr_in addr;
	
	if (pfd[0].revents & POLLIN)
	{
		len = sizeof(addr);
		newfd = accept(pfd[0].fd, (struct sockaddr*)&addr, &len);
		if (0 > newfd)
		{
			perror("accept is failed");
			close(newfd);
			return -1;
		}
		printf("accept is ok  port = %d\n", ntohs(addr.sin_port));
		
		if (*index >= maxnum)            //計算監聽數組中存放事件的最大索引值
		{
			printf("polltable is full \n");
			close(newfd);
		}
		else
		{
			for (i = 0; i < maxnum; i++)
			{
				if (pfd[i].fd == -1)
				{
					pfd[i].fd = newfd;
					(*index)++;
					break;
				}
			}
		}
	}
	
	for (i = 1; i < maxnum; i++)        //遍歷能操作的文件,並將數據原路返回
	{
		if (pfd[i].revents & POLLIN)
		{
			memset(str, 0, sizeof(str));
			ret = recv(pfd[i].fd, str, SIZE - 1, 0);
			if (ret < 0)
			{
				perror("recv failed");
				
				close(pfd[i].fd);
				pfd[i].fd = -1;
				(*index)--;
				break;
			}
			else if (ret == 0)
			{
				printf("client %d is closed\n", pfd[i].fd);
				close(pfd[i].fd);
				pfd[i].fd = -1;
				(*index)--;
				continue;
			}
			printf("get %d client data %s\n", i, str);
			send(pfd[i].fd, str, strlen(str), 0);
		}
		
	}
	
	return 0;
}

 

四、epoll的原理

創建epoll相關鏈表,返回epollFd,把需要監聽的文件描述符,及監聽的事件用結構體進行組合,把這個結構體加入到epollFd對應的鏈表中;開始監聽等待, 在kernel space輪詢時,發現某些文件可以進行IO操作時,kernel space會將這些文件放入到一個數組中,epoll等待返回時,把數組拷貝到user space,user space遍歷返回的數組,查看數組中每一個成員的“事件”,然後,調用相應的函數進行操作。

使用epoll的時候,我們創建一個文件描述符鏈表,每次調用epoll函數的時候,系統會遍歷這個鏈表,查看那個文件可以進行操作,並且將可以操作的文件存放到一個數組裏面,並且返回一個數組長度的值,然後我們遍歷該數組執行相應函數即可。

 

4.1 epoll流程

(1) 創建一個epoll相關的鏈表,返回epoll的文件描述符 

(2)將監聽的文件描述符放在鏈表中   

(3) 啓動epoll的監聽

(4) epoll返回時,將所有可以進行IO操作的文件放在一個數組中,用戶遍歷該數組進行相應的IO操作

(5)重複3、4步
        

4.2相關函數

int epoll_create(int size);

函數參數size無意義,可以隨意填寫。

 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函數參數:

epfd:監聽鏈表描述符

op:需要執行的操作 EPOLL_CTL_ADD給監聽鏈表中添加文件,EPOLL_CTL_DEL從監聽鏈表中移除文件

fd:需要操作的文件

event:需要監聽的事件,EPOLLIN文件輸入,EPOLLIN文件輸出

 

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函數參數:

epfd:監聽鏈表

events:用於存放能操作文件的數組

maxevents:存放能操作文件的數組最大長度

timeout:等待時間發生時間,單位ms

函數返回值爲能操作的事件的數量。

 

4.3 具體實現代碼

代碼爲socket一個服務端同時和多個客戶機進行通訊的實例。

客戶端代碼與上文中select舉例一致。

服務端代碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/epoll.h>

#define SPORT 	12000
#define SIZE 	200
#define ADDR	"127.0.0.1"

int SockfdInit(const char* ip, int port);
int EpollTask(int sofd, int epfd);
int getLink(int sofd, int epfd);

int main(int argc, const char* argv[])
{
	int i = 0;
	int ret = 0;
	int sockfd = 0;
	int epollfd = 0;
	struct epoll_event event;
	
	sockfd = SockfdInit(ADDR, SPORT);
	if (0 > sockfd)
	{
		perror("socket is failed");
		return -1;
	}
	
	epollfd = epoll_create(1);
	if ( 0 > epollfd)
	{
		perror("epoll_create is failed");
		return -1;
	}
	
	event.events = EPOLLIN;
	event.data.fd = sockfd;
	
	ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event);   //將socket添加到監聽鏈表當中
	if (0 > ret)
	{
		close(epollfd);
		close(sockfd);
		perror("epoll_ctl is failed");
		return -1;
	}

	
	while (1)
	{
		printf("調用epoll_wait\n");
		struct epoll_event epoll_arr[10];               //自己根據實際情況設置監聽數組的大小
	
		ret = epoll_wait(epollfd, epoll_arr, 10, 15000);
		if (0 > ret)
		{
			perror("epoll is failed");
			break;
		}
		else if (0 == ret)
		{
			perror("timeout");
			break;
		}
		
		for (i = 0; i < ret; i++)
		{
			if (EPOLLIN == epoll_arr[i].events)
			{
				if (epoll_arr[i].data.fd == sockfd)    //有新的客戶端連接
				{
					getLink(epoll_arr[i].data.fd, epollfd);
				}
				else                                   //接收到客戶端的數據,原路返回
				{
					EpollTask(epoll_arr[i].data.fd, epollfd);
				}
			}
		}
		
	}
	
	close(sockfd);
	return 0;
}

int SockfdInit(const char* ip, int port)
{
	if (NULL == ip)
	{
		printf("請輸入正確的ip地址\n");
		return -1;
	}
	
	int sockfd = 0;
	struct sockaddr_in addr;
	int len = sizeof(addr);
	int ret = 0;
	struct timeval tm = 
	{
		.tv_sec = 3,
	};
		
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (0 > sockfd)
	{
		perror("socket is failed");
		return -1;
	}
	printf("sockfd is ok\n");
	
	int on = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tm, sizeof(tm));
	
	memset(&addr, 0, len);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	
	if (0 > bind(sockfd, (struct sockaddr*)&addr, len))
	{
		perror("bind is failed");
		return -1;
	}
	printf("bind is ok\n");
	
	if (0 > listen(sockfd, 5))
	{
		perror("listen is failed");
		return -1;
	}
	printf("listen is ok\n");
	
	
	return sockfd;
}

int getLink(int sofd, int epfd)
{
	int i = 0;
	int len = 0;
	int ret = 0;
	int newfd = 0;
	struct sockaddr_in addr;
	struct epoll_event event;
	
	len = sizeof(addr);
	newfd = accept(sofd, (struct sockaddr*)&addr, &len);
	if (0 > newfd)
	{
		perror("accept is failed");
		close(newfd);
		return -1;
	}
	printf("accept is ok  port = %d\n", ntohs(addr.sin_port));
		
	event.events = EPOLLIN;
	event.data.fd = newfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &event);
	
	return 0;
}

int EpollTask(int sofd, int epfd)
{
	int i = 0;
	int ret = 0;
	char str[SIZE] = {0};
	struct epoll_event event;
	
	memset(str, 0, sizeof(str));
	ret = recv(sofd, str, SIZE - 1, 0);
	if (ret < 0)
	{
		perror("recv failed");
		goto end;
	}
	else if (ret == 0)
	{
		goto end;
	}
	printf("get %d client data %s\n", sofd, str);
	send(sofd, str, strlen(str), 0);
	return 0;
end:
	epoll_ctl(epfd, EPOLL_CTL_ADD, sofd, NULL);
	close(sofd);
	return -1;
}


 

倉促成文,不當之處,尚祈方家和讀者批評指正。聯繫郵箱[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章