UNIX网络编程4 使用epoll

使用:http://blog.chinaunix.net/uid-20384806-id-1954307.html
优点:http://blog.csdn.net/wangpengqi/article/details/9933011


源码分析 http://blog.csdn.net/chen19870707/article/details/42525887


 int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核需要建通的数目一共有多大,epoll句柄会占用一个fd值。
 
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 epoll的事件注册函数,第一个参数是创建的epoll句柄,第二个参数表示动作:
 EPOLL_CTL_ADD 注册新的fd到epfd中
 EPOLL_CTL_MOD 修改已经注册的fd的监听事件
 EPOLL_CTL_DEL 从epfd中删除一个fd
 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事
 struct epoll_event {
  _unit32_t events;
  epoll_data_t data;
 }
 events可以是一下几个宏的组合
 EPOLLOIN 表示对应的文件描述符可以读,包括对端的socket正常关闭
 EPOLLOUT 可写
 EPOLLPRI 有紧急的数据可读
 EPOLLERR 发生错误
 EPOLLHUP 被挂断
 EPOLLET  将EPOLL设为边缘触发,这是相对于水平触发来说的
 EPOLLONESHOT 只监听一次事件,完了还还需要监听这个fd的话需要重新加入到EPOLL队列里
 epoll_data_t data是一个结构体,里面包含了fd。
 
 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
 参数events用来从内核得到事件的集合,maxevents告知内核这个events有多大,timeout可以设超时毫秒或立即返回或阻塞,
 该函数返回要处理的事件数目,或者超时无事件0。


 水平触发:LT,默认工作方式,同时支持block和non-block socket,内核通知就绪以后如果不对就绪的fd进行IO操作,内核还是会继续通知你。
 边缘触发:ET,高速工作方式,只支持non-block socket,当描述符从未就绪变为就绪时,内核会通过epoll告诉你,以后不会再为那个文件描述符发送更多的就绪通知,

 直到下次有新的数据进来的时候才会再次触发就绪事件。


使用epoll的一个服务端程序(不完善):

//:epollServer.c
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

#define SERV_PORT 6000
#define MAX_FD 1024
#define BUF_SIZE 1024
#define EVENTS_NUM 20
#define TIMEOUT 1000
#define MAX_BACK 20

void setnonblocking(int sock)
{
	int opts;
	opts = fcntl(sock, F_GETFL);
	if (opts < 0)
	{
		perror("fcntl(sock, GETFL)");
		exit(1);
	}
	opts = opts | O_NONBLOCK;
	if (fcntl(sock, F_SETFL, opts) < 0)
	{
		perror("fcntl(sock, SETFL, opts)");
		exit(1);
	}
}
int main(int argc, char** argv)
{
	int listenfd, connfd, sockfd, epfd, nfds, n = 0, i = 0;
	char line[BUF_SIZE+1];
	socklen_t clilen;
	struct epoll_event ev, events[EVENTS_NUM];
	epfd = epoll_create(MAX_FD);
	struct sockaddr_in serveraddr, clientaddr;
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	setnonblocking(listenfd);

	ev.data.fd = listenfd;
	ev.events = EPOLLIN | EPOLLET;
	//注册epoll事件
	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERV_PORT);
	bzero(line, BUF_SIZE);
	bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
	listen(listenfd, MAX_BACK);

	for (; ;)
	{
		//等待epoll事件的发生
		nfds = epoll_wait(epfd, events, EVENTS_NUM, TIMEOUT);
		for (i = 0; i < nfds; i ++)
		{
			// 在listenfd上发生读事件,说明有新的客户端连接
			if (events[i].data.fd == listenfd)
			{
				//sockaddr 和sockaddr_in的区别:二者占用的内存空间一样大可互相转化,前者在socket.h中后者在in.h中,
				//sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
				//而sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作。使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。
				connfd = accept(listenfd, (struct sockaddr * )&clientaddr, &clilen);
				if (connfd < 0)
				{
					perror("accept error, connfd < 0");
				}
				setnonblocking(connfd);
				char *str = inet_ntoa(clientaddr.sin_addr);
				printf("Accept a connection from %s\n", str);
				//注册这个connfd的ev
				ev.data.fd = connfd;
				ev.events = EPOLLIN | EPOLLET;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

			}
			//已连接的客户,收到数据
			else if (events[i].events & EPOLLIN)
			{
				printf("EPOLLIN\n");
				if ((sockfd = events[i].data.fd) < 0)
					continue;
				//注意这里,边缘触发应该保证读完缓冲区
				//可以用循环来反复读取,当n==0的时候表示读完
				if ((n = read(sockfd, line, BUF_SIZE)) < 0)
				{
					if (errno == ECONNRESET)
					{
						close(sockfd);
						events[i].data.fd = -1;
					}
					else
					{
						printf("Readline error\n");
					}
				}
				else if (n == 0)
				{
					printf("Null message\n");
					close(sockfd);
					events[i].data.fd = -1;
				}
				else
				{
					line[n] = '\0';
					printf("Read from %d: %s\n", sockfd, line);

					//注册写操作事件
					//ev.data.fd = sockfd;
					//ev.events = EPOLLOUT | EPOLLET;
					//epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
					//这里直接写回给客户端,没有再使用epoll调度写事件
					write(sockfd, line, n);
				}
			}
			//如果有数据要发送,本程序中没什么用。。。
			else if (events[i].events & EPOLLOUT)
			{
				sockfd = events[i].data.fd;
				write(sockfd, line, n);
				ev.data.fd = sockfd;
				ev.events = EPOLLIN | EPOLLET;
				epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
			}
			//有错的话是否需要close
			else
			{
				printf("Other events!\n");
				//close(events[i].data.fd);
				events[i].data.fd = -1;
			}
		}
	}

	//epoll fd 会占用一个文件句柄
	close(epfd);
	return 0;
}

epoll的优势(参考http://blog.csdn.net/chen19870707/article/details/42525887):

①从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。

② 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

③ epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。

如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。
1.执行epoll_create时,创建了红黑树和就绪list链表。
2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。

你可能感兴趣的:(UNIX网络编程4 使用epoll)