IO复用(五)

一 函数说明

1 原型

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

2 使用方面

  1、select函数通过三个fd_set结构体变量分别给内核传递用户关注的所有可读、可写、异常事件,这使得select不能处理更多的事件类型,并且内核也通过这三个结构体变量返回就绪的文件描述符,所以每次使用之前,都必须重新设置这三个结构体变量。
  2、poll函数将用户关注的文件描述符以及其关注的事件、内核返回的文件描述符上发生的事件分离开表示,并且通过 一个用户数组将所有的文件描述符传递给内核。因此,poll函数能关注的事件类型更多,每次调用也不需要重新设置。
  3、epoll是通过一组函数来完成的,epoll通过epoll_create创建一个内核事件表,通过epoll_ctl函数添加、删除、修改事件。epoll_wait只需要从内核事件表中读取用户的注册的事件。

3 使用限制

  1、select所使用的fd_set结构实际上是一个整形数组,32bit系统上关注的文件描述符最多1024个,最大文件描述符数1023。
  2、poll和epoll分别用nfds和maxevents参数指定最多监听多少个文件描述符,这两个数值都能达到系统允许打开的最大文件描述符数,65535。

4 使用效率

  1、select、poll每次调用都需要将用户空间的文件描述符拷贝到内核空间,epoll则直接从内核读取。效率更高。
  2、select、poll每次都将所有的文件描述符(就绪的和未就绪的)返回,所以应用程序检索就绪文件描述符的时间复杂度为O(n),epoll通过events参数返回所有就绪的文件描述符,应用程序检索就绪文件描述符的时间复杂度为O(1)。
  3、select、poll只能工作在效率较低的LT模式,而epoll则能工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,从而进一步减少事件被触发的次数。

5 内核效率

  select和poll采用轮询的方式:即每次都需要扫描整个注册的文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此,内核中检测就绪文件描述符的算法时间复杂度为O(n), epoll则采取回调的方式,内核检测到就绪文件描述符,就触发回调函数,将文件描述符及发生的事件插入内核就绪事件队列,因此,epoll在内核中检测就绪文件描述符的算法时间复杂度为O(1)。但是,当链接的活动比较频繁是,select和poll的效率比epoll要高,因为epoll的回调函数调用过去频繁,所以,epoll适用于链接较多,但是活动不频繁的情况。

6 epoll模式

  Level-triggered :水平触发,缺省模式,LT模式时,事件就绪时,假设对事件没做处理,内核会反复通知事件就绪。
  edge-triggered :边缘触发,ET模式时,事件就绪时,假设对事件没做处理,内核不会反复通知事件就绪。

二 select

  select 函数返回值:

负值:select错误;
正值:某些文件可读写或出错;
0:等待超时,没有可读写或错误的文件。

  select() 用来等待文件描述词状态的改变。参数 n 代表最大的文件描述词加1,参数 readfdswritefdsexceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况其实这个nfds其实填进去的就是 maxfd+1, 而 maxfd 是当前监听信号的最大值,比如监听0(键盘) 及tcp通信中的套接字。一般是从3开始增长。
  如果建立一个tcp通信模型, 创建一个服务器那么就会产生一个3号的套接字,相当于文件描述符。可以利用文件io进行读写操作。那么在利用select实现io多了复用时就会产生监听3这个套接字。因此此时的 maxfd = 3, 那么 ndfs = maxfd + 1 = 4; 其实这个ndfs就像一个空间,或者位置,保存一个递增的数据。这个数字可以是tcp套接字也可以是文件描述符。
  比如如果监听 3 , 4 , 5 , 6 , 7这5个文件描述符,或者套接字, 那么就需要8个位置,因为,文件描述符是从0开始的。 如果此时你将nfds置为8那么一切正常,1号位置么有内容, 知道4号位置 存放3 监听 , 5号位置存4 6号位置存5 , 7号位置存6 , 8号位置存7 。 切记不能因为这里只有5个需要监听的对象就将 nfds = 5, 如果置5 说明只有5个位置,但是 nfds 里面只能存放连续的监听对象(文件描述符)如果中间监听对象缺省,可以不坚听,但是位置一定要保留。所以对于以上的情况 nfds 为最大的 fd = 7 加上1 即 maxfd = 7 nfds = maxfd+1 = 8;底下的宏提供了处理这三种描述词组的方式:
  FD_CLR(inr fd,fd_set* set); 用来清除描述词组set中相关fd的位。
  FD_ISSET(int fd,fd_set *set); 用来测试描述词组set中相关fd的位是否为真。
  FD_SET(int fd,fd_set*set); 用来设置描述词组set中相关fd的位。
  FD_ZERO(fd_set *set); 用来清除描述词组set的全部位。
  select:对于参数timeouttimeval的结构如下:

struct timeval{
	long tv_sec;  /*secons*/
	long tv_usec;  /*microseconds*/
}

  select 的阻塞时间,是两个参数的时间和。如下就是使用 select 实现定时器功能:

void seconds_sleep(unsigned seconds)
{
    struct timeval tv;
    tv.tv_sec=seconds;
    tv.tv_usec=500 * 1000;
    int err;
    do{
       err=select(0,NULL,NULL,NULL,&tv);
    }while(err<0 && errno==EINTR);
}

三 poll

  内核2.6.11对应的实现代码为:[fs/select.c -->sys_poll]
  Linux内核空间申请和IO操作:https://blog.csdn.net/essity/article/details/85002392
  数据结构:

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
typedef struct poll_table_struct {     //include/linux/poll.h
	poll_queue_proc qproc;
} poll_table;
struct poll_wqueues {   //include/linux/poll.h
	poll_table pt; //函数指针
	struct poll_table_page * table;
	int error;
};
struct pollfd {
	int fd;
	short events;
	short revents;
};
struct poll_list {
	struct poll_list *next;
	int len;
	struct pollfd entries[0];
};
asmlinkage long sys_poll(struct pollfd __user * ufds, unsigned int nfds, long timeout)
{
	struct poll_wqueues table;
 	int fdcount, err;
 	unsigned int i;
 	//链表头
	struct poll_list *head;
	//链表当前节点
 	struct poll_list *walk;

	/* Do a sanity check on nfds ... */
	/*文件描述符个数的检查,max_fdset文件描述符当前最大编号,open_max每个
	进程最大可拥有256个文件描述符,但是这个没有太大的意义,随着版本更新会变的。
	支持65535。*/
	if (nfds > current->files->max_fdset && nfds > OPEN_MAX)
		return -EINVAL;

	if (timeout) {
		/* Careful about overflow in the intermediate values */
		if ((unsigned long) timeout < MAX_SCHEDULE_TIMEOUT / HZ)
			timeout = (unsigned long)(timeout*HZ+999)/1000+1;
			/*timeout参数合法,就按这种算法给一个值,例如timeout=5000,
			最终的值就是5001.999,这种变化0.0*/
		else /* Negative or overflow */
			timeout = MAX_SCHEDULE_TIMEOUT;
	}

	poll_initwait(&table);
/*void poll_initwait(struct poll_wqueues *pwq)
  {
  	 &(pwq->pt)->qproc = __pollwait; //此行已经被我“翻译”了,方便观看
	 pwq->error = 0;
     pwq->table = NULL;
  }*/
	head = NULL;
	walk = NULL;
	i = nfds;
	err = -ENOMEM;
	/*循环里面建立一个链表,每个链表的节点是一个page大小(通常是4k),这链表节
	点由一个指向struct poll_list的指针掌控,而众多的struct pollfd就通
	过struct_list的entries成员访问。上面的循环就是把用户态的struct pollfd拷进
	这些entries里。当用户传入的fd很多时,由于poll系统调用每次都要把所有
	struct pollfd拷进内核,所以参数传递和页分配此时就成了poll系统调用的性
	能瓶颈。*/
	while(i!=0) {
		struct poll_list *pp;
		pp = kmalloc(sizeof(struct poll_list)+
				sizeof(struct pollfd)*
				(i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i),
					GFP_KERNEL);
		/*为每一个文件描述符开辟内存,i的个数如果小于4096,就按i分配,否则就
		按4096B分配,也就是说可以分配不超过4096B大小*/
		if(pp==NULL)
			goto out_fds;
		pp->next=NULL;
		pp->len = (i>POLLFD_PER_PAGE?POLLFD_PER_PAGE:i);
		if (head == NULL)
			head = pp;
		else
			walk->next = pp;

		walk = pp;
		//将用户空间的数据拷入内核空间
		if (copy_from_user(pp->entries, ufds + nfds-i, 
				sizeof(struct pollfd)*pp->len)) {
			err = -EFAULT;
			goto out_fds;
		}
		i -= pp->len;
	}
	fdcount = do_poll(nfds, head, &table, timeout);

	/* OK, now copy the revents fields back to user space. */
	walk = head;
	err = -EFAULT;
	while(walk != NULL) {
		struct pollfd *fds = walk->entries;
		int j;

		for (j=0; j < walk->len; j++, ufds++) {
			if(__put_user(fds[j].revents, &ufds->revents))
				goto out_fds;
		}
		walk = walk->next;
  	}
	err = fdcount;
	if (!fdcount && signal_pending(current))
		err = -EINTR;
out_fds:
	walk = head;
	while(walk!=NULL) {
		struct poll_list *pp = walk->next;
		kfree(walk);
		walk = pp;
	}
	/*唤醒文件描述符并将之从等待队列中删除*/
	poll_freewait(&table);
	return err;
}
static int do_poll(unsigned int nfds,  struct poll_list *list,
			struct poll_wqueues *wait, long timeout)
{
	int count = 0;
	poll_table* pt = &wait->pt;

	if (!timeout)
		pt = NULL;
 
	for (;;) {
		struct poll_list *walk;
		/*set_current_state和signal_pending,当用户程序在调用poll后挂
		起时,发信号可以让程序迅速退出poll调用,而通常的系统调用是
		不会被信号打断的。即将当前进程状态设置为可中断的,即可唤醒的。*/
		set_current_state(TASK_INTERRUPTIBLE);
		walk = list;
		while(walk != NULL) {
		/*当用户传入的fd很多时(比如1000个),对do_pollfd就会调用很多次,poll
		效率瓶颈的另一原因就在这里。*/
			do_pollfd( walk->len, walk->entries, &pt, &count);
			walk = walk->next;
		}
		pt = NULL;
		/*count值为0或者超时或者收到信号,那就结束了*/
		if (count || !timeout || signal_pending(current))
			break;
		/*count赋值为0,wait在上一个函数中初始化的时候,就将error改为0了*/
		count = wait->error;
		if (count)
			break;
		/*让current挂起,别的进程跑,timeout到了以后再回来运行current*/
		timeout = schedule_timeout(timeout);
	}
	/*将当前进程状态设置为运行状态*/
	__set_current_state(TASK_RUNNING);
	return count;
}
static void do_pollfd(unsigned int num, struct pollfd * fdpage,
	poll_table ** pwait, int *count)
{
	int i;

	for (i = 0; i < num; i++) {
		int fd;
		unsigned int mask;
		struct pollfd *fdp;

		mask = 0;
		//通过偏移量检查每一个文件描述符,由fdp指向
		fdp = fdpage+i;
		fd = fdp->fd;
		if (fd >= 0) {
		/*根据进程文件描述符得到文件对象的地址(每个文件描述符都有其对应的
		打开的文件)*/
			struct file * file = fget(fd);
			mask = POLLNVAL;/*文件描述符没有打开*/
			if (file != NULL) {
			/*事件可读可写*/
				mask = DEFAULT_POLLMASK;
				/*检查文件是否支持操作*/
				if (file->f_op && file->f_op->poll)
				/*然后就调用poll的回调函数,驱动程序。如果fd对应的是某个
				socket,do_pollfd调用的就是网络设备驱动实现的poll;如果fd对应
				的是某个ext3文件系统上的一个打开文件,那do_pollfd调用的就
				是ext3文件系统驱动实现的poll。一句话,这个file->f_op->poll是
				设备驱动程序实现的,设备驱动程序的标准实现是:调用poll_wait,即
				以设备自己的等待队列为参数调用struct poll_table的回调函数。*/
					mask = file->f_op->poll(file, *pwait);
				/*当前文件描述符的事件| 错误|挂起(一般都是由于管道写端关闭而
				触发的POLLHUP)*/
				mask &= fdp->events | POLLERR | POLLHUP;
				/*释放对文件的引用*/
				fput(file);
			}
			if (mask) {
				*pwait = NULL;
				(*count)++;/*就绪文件描述符计数++*/
			}
		}
		//设置fd事件类型
		fdp->revents = mask;
	}
}

  大概理解加猜测一下 __poll_wait() 函数的意思[fs/select.c–>__poll_wait()]:

void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *_p)
{
	/*得到一个p变量*/
	struct poll_wqueues *p = container_of(_p, struct poll_wqueues, pt);
	/*得到一个poll_table_page变量*/
	struct poll_table_page *table = p->table;
	/*判错语句或者为空*/
	if (!table || POLL_TABLE_FULL(table)) {
		struct poll_table_page *new_table;

		new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL);
		if (!new_table) {
			p->error = -ENOMEM;
			__set_current_state(TASK_RUNNING);
			return;
		}
		/*进行连接*/
		new_table->entry = new_table->entries;
		new_table->next = table;
		p->table = new_table;
		table = new_table;
	}

	/* Add a new entry */
	{
		struct poll_table_entry * entry = table->entry;
		table->entry = entry+1;
	 	get_file(filp);
	 	entry->filp = filp;
		entry->wait_address = wait_address;
		/*初始化等待队列*/
		init_waitqueue_entry(&entry->wait, current);
		/*加入等待队列*/
		add_wait_queue(wait_address,&entry->wait);
	}
}

在这里插入图片描述
  __pollwaitpoll 中的核心回调函数,且每个 socket 自己都带有一个等待队列 sk_sleep__poll_wait 的作用就是创建了上图所示的数据结构(一次 __poll_wait 即一次设备 poll 调用只创建一个 poll_table_entry),并通过 struct poll_table_entrywait 成员,把 current 挂在了设备的等待队列上,此处的等待队列是wait_address。
  poll系统调用的原理了:先注册回调函数 __poll_wait,再初始化 table 变量(类型
struct poll_wqueues),接着拷贝用户传入的 struct pollfd (其实主要是fd),然后轮流调用所有 fd 对应的 poll(把 current 挂到各个fd对应的设备等待队列上)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上的进程,这时 current 便被唤醒了。

四 epoll

1 epoll结构流程

在这里插入图片描述

2 linux下epoll如何实现高效处理百万句柄的

  首先要调用 epoll_create 建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
  epoll_ctl 可以操作上面建立的epoll,例如,将刚建立的 socket 加入到 epoll 中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
  epoll_wait在调用时,在给定的 timeout 时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
  从上面的调用方式就可以看到epoll比 select/poll 的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给 select/poll 系统调用,这意味着需要将用户态的socket列表 copy 到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用 epoll_wait 时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在 epoll_ctl 中拿到了要监控的句柄列表。
  所以,实际上在你调用 epoll_create 后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的 socket 句柄。
  在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
  epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

static int __init eventpoll_init(void)  
{  
    ... ...  
  
    /* Allocates slab cache used to allocate "struct epitem" items */  
    epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),  
            0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,  
            NULL, NULL);  
  
    /* Allocates slab cache used to allocate "struct eppoll_entry" */  
    pwq_cache = kmem_cache_create("eventpoll_pwq",  
            sizeof(struct eppoll_entry), 0,  
            EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);  
  
 ... ...  

  epoll的高效就在于,当我们调用 epoll_ctl 往里塞入百万个句柄时,epoll_wait 仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后 epoll_ctl 传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。所以,epoll_wait 非常高效。
  而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait 仅需要从内核态 copy 少量的句柄到用户态而已,如何能不高效?!
  那么,这个准备就绪list链表是怎么维护的呢?当我们执行 epoll_ctl 时,除了把 socket 放到 epoll 文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
  如此,一颗红黑树,一张准备就绪句柄链表,少量的内核 cache,就帮我们解决了大并发下的 socket 处理问题。执行 epoll_create 时,创建了红黑树和就绪链表,执行 epoll_ctl 时,如果增加 socket 句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行 epoll_wait 时立刻返回准备就绪链表里的数据即可。
  最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。
  这件事怎么做到的呢?当一个 socket 句柄上有事件时,内核会把该句柄插入上面所说的准备就绪 list 链表,这时我们调用 epoll_wait,会把准备就绪的 socket 拷贝到用户态内存,然后清空准备就绪 list 链表,最后,epoll_wait 干了件事,就是检查这些 socket,如果不是 ET 模式(就是 LT 模式的句柄了),并且这些 socket 上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait 每次都会返回。而ET模式的句柄,除非有新中断到,即使 socket 上的事件没有处理完,也是不会次次从 epoll_wait 返回的。

3 epoll_create(int size)

  size:监听数,监听文件描述符的个数,与内存大小有关。

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

epfd:为 epoll_creat 的句柄–具体是那一颗 epoll 树。
op:表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的 fdepfd)
EPOLL_CTL_MOD (修改已经注册的 fd 的监听事件)
EPOLL_CTL_DEL (从 epfd 删除一个 fd)
evnet:告诉内核需要监听的事件。

struct epoll_event{
	_uint32_t events; /*Epoll events*/
	epoll_data_t data; /*User data variable*/
};
typedef union epoll_data{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;

  data: 用户数据
  data 是一个联合体类型,可以是指针,文件描述符,整形(机器字长)。这个数据传给 epoll 以后,epoll 不会使用,只会在对应的事件触发后原样的返回给用户。实际开发中一般都是保存添加的套接字的描述符,用于当 epoll 事件返回时识别 fd。如果有需要传其它值也可以,比如改成一个结构体或者对象的地址(可以避免一次使用套接字查找对象操作)。
  events: 事件集合
  通过位掩码的方式表示不同的事件,可以同时设置多个,通过“|” 连接,可选项如下。

事件类型描述
EPOLLIN文件描述符是否可读(包括对端socket 正常关闭)
EPOLLOUT文件描述符是否可写
EPOLLRDHUP对端关闭连接(被动),或者套接字处于半关闭状态(主动),这个事件会被触发。当使用边缘触发模式时,很方便写代码测试连接的对端是否关闭了连接
EPOLLPRI文件描述符是否异常
EPOLLERR文件描述符是否错误。如果文件描述符已经关闭,继续写入也会收到这个事件。这个事件用户不设置也会被上报
EPOLLHUP套接字被挂起,这个事件用户不设置也会被上报
EPOLLET设置epoll的触发模式为边缘触发模式。如果没有设置这个参数,epoll默认情况下是水平触发模式
EPOLLONESHOT设置添加的事件只触发一次,当epoll_wait(2)报告一次事件后,这个文件描述符后续所有的事件都不会再报告。只是禁用,文件描述符还在监视队列中,用户可以通过epoll_ctl()的EPOLL_CTL_MOD重新添加事件

5 epoll_wait等待已注册事件触发

  等待事件的产生,若超过 timeout 还没有触发,就超时:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);          
int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);

  我们仅说第一种函数:
  第一个参数 epfd 是由 epoll_create 生成的 epoll 专用的文件描述符;
  第二个参数 events 和上面最后一个参数一样,也是指向 epoll_event 类型结构体的指针,不过现在是作为一个容器使用,用来从内核得到发生的事件的集合;
  第三个参数 maxevents 用来告知这个容器有多大(事件数组成员个数),也就是每次能处理的事件个数;
  第四个参数 timeout 是等待I/O事件发生的超时值(单位我也不太清楚);一般置为-1即可,-1代表将“block indefinitely”将无限期等待永久阻塞,也有说导致不确定结果,这取决于怎么翻译了。置为0将使得函数立刻返回,无论有没有事件发生,也就是设置为非阻塞。
  返回值是存放在第二个参数 events 数组容器内的实际成员个数。也就是发生了的需要处理的事件个数,如果返回0表示已超时,出错返回-1。
  epoll_wait 函数的运行过程是:程序阻塞在这个函数,等侍注册在 epfd 上的 socket fd 的事件的发生,如果发生则将发生的 sokct fd 和事件类型放入到 events 数组中。

6 错误码解释:

错误码ID解释
EBADFepfd或者fd不是一个有效的文件描述符
EEXIST当参数是EPOLL_CTL_ADD时,当添加到fd已经在epfd中时,重复添加
EINVAL1、当epfd不是一个文件描述符,或者fd是一个epfd,或者op是不支持的参数。2、设置了参数EPOLLEXCLUSIVE,却没有和其它有效的参数一起设置。3、使用参数EPOLL_CTL_MOD 时同时包含了。4、使用参数EPOLL_CTL_MOD 时,当前epfd中的fd之前已经被设置了
ELOOPepoll监视队列是可以添加epoll描述符的,就是支持嵌套。当嵌套关系成环时,或者嵌套深度超过5层时,会报这个错误
ENOENT使用EPOLL_CTL_MOD 和EPOLL_CTL_DEL添加 修改时,修改的套接字却不在epoll的监视队列中
ENOMEM操作所需要的内存不够
ENOSPC当EPOLL_CTL_ADD添加时,已经超过了epoll的规格限制
EPERM添加的fd不支持epoll。比如添加的是普通文件描述符

五 epoll示例

epoll示例

1 服务端

  下面是一个使用epoll机制在Linux上编写的简单套接字程序示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
 
#define MAX_EVENTS 10
#define MAX_BUFFER_SIZE 1024
 
int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_size;
    char buffer[MAX_BUFFER_SIZE];
    struct epoll_event event, events[MAX_EVENTS];
 
    // 创建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }
 
    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6868);
    //server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //绑定127.0.0.1
 
    memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));
 
    // 绑定套接字到地址
    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Error binding socket");
        exit(EXIT_FAILURE);
    }
 
    // 监听
    if (listen(server_socket, 5) < 0) {
        perror("Error listening");
        exit(EXIT_FAILURE);
    }
 
    // 创建epoll实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        perror("Error creating epoll instance");
        exit(EXIT_FAILURE);
    }
 
    // 设置event结构体
    event.events = EPOLLIN;
    event.data.fd = server_socket;
 
    // 将socket添加到epoll实例中
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) < 0) {
        perror("Error adding socket to epoll instance");
        exit(EXIT_FAILURE);
    }
 
    while (1) {
        int num_ready = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_ready < 0) {
            perror("Error waiting for events");
            exit(EXIT_FAILURE);
        }
 
        for (int i = 0; i < num_ready; i++) {
            if (events[i].data.fd == server_socket) {
                // 检测到新的客户端连接请求
                addr_size = sizeof(client_addr);
                client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_size);
 
                // 设置client_socket为非阻塞
                int flags = fcntl(client_socket, F_GETFL, 0);
                fcntl(client_socket, F_SETFL, flags | O_NONBLOCK);
 
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_socket;
 
                // 将客户端socket添加到epoll实例中
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) < 0) {
                    perror("Error adding client socket to epoll instance");
                    exit(EXIT_FAILURE);
                }
 
                printf("New client connected: %s\n", inet_ntoa(client_addr.sin_addr));
            } else {
                // 处理客户端发送的数据
                int client_fd = events[i].data.fd;
                memset(buffer, 0, MAX_BUFFER_SIZE);
 
                int num_bytes = recv(client_fd, buffer, MAX_BUFFER_SIZE, 0);
                if (num_bytes < 0) {
                    perror("Error receiving data");
                    close(client_fd);
                    continue;
                } else if (num_bytes == 0) {
                    // 客户端连接关闭
                    printf("Client disconnected\n");
                    close(client_fd);
                    continue;
                }
 
                // 处理接收到的数据
                printf("Received data from client: %s\n", buffer);
 
                // 将数据发送回客户端
                send(client_fd, buffer, num_bytes, 0);
            }
        }
    }
 
    // 关闭套接字和epoll实例
    close(server_socket);
    close(epoll_fd);
 
    return 0;
}

  这个程序创建了一个服务器套接字,使用 epoll 机制监听连接请求和处理客户端发送的数据。它首先创建了一个套接字 server_socket,并将其绑定到地址。然后通过 listen 函数开始监听连接请求。
  接下来,程序创建了一个 epoll 实例 epoll_fd,并使用 epoll_create 函数进行创建。然后,将服务器套接字添加到 epoll 实例中,通过 epoll_ctl 函数实现。接下来,程序进入一个无限循环中,使用 epoll_wait 函数等待事件发生。一旦有事件发生,通过遍历 events 数组处理每个事件。
  当检测到一个新的客户端连接请求时,程序通过 accept 函数接受新的客户端连接,并将新的客户端套接字设置为非阻塞模式。然后,将客户端套接字添加到 epoll 实例中。
  当客户端发送数据时,程序通过 recv 函数接收数据,并处理接收到的数据。然后,将数据发送回客户端,使用 send 函数。
  最后,在循环结束时,程序关闭服务器套接字和 epoll 实例。
  在C语言中,struct sockaddr_in 是一个用于存储网络地址的结构体,其中包括IP地址和端口号。它定义在 <netinet/in.h> 头文件中。这个结构体的定义如下:

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // Zero padding to make the struct the same size as struct sockaddr
};

  其中最后一个成员 sin_zero 是一个填充字段,其目的是为了保证 struct sockaddr_in 结构体的总大小和 struct sockaddr 结构体的大小相同,因为在socket API中,地址通常是通过 struct sockaddr 类型来传递的。为了确保类型兼容和内存布局的一致性,sin_zero 成员被添加到 struct sockaddr_in 结构体中,当使用这个结构体时,通常需要将此字段设置为全 0。
  memset(server_addr.sin_zero, ‘\0’, sizeof(server_addr.sin_zero)); 使用 memset 函数将 sin_zero 字段的内存设置为0。这里的 ‘\0’ 是空字符(null terminator),用于表示字符串的结束,在内存中其值为0。这行代码保证了填充字段没有留下任何未定义的数据,满足某些系统和库对结构体初始化的要求。
  在许多实现中,这个填充可能并不是严格必要的,因为sockaddr_in和sockaddr的转换通常都能正常工作,但按照好的编程习惯,仍然建议对这部分内存进行清零处理。
  请注意,此示例程序是一个简单的示例,为了简洁起见,没有进行错误处理和边界检查。在实际编程中,您需要根据需求进行适当的错误处理和边界检查。此外,此示例使用了阻塞的 recv 和 send 函数,您可以根据需要使用非阻塞的I/O函数。

2 客户端

  在Linux系统中,epoll 是一个高效的多路复用IO接口,它可以用于同时监控多个文件描述符,来检测它们是否有IO事件发生。在网络编程中,epoll 常用于接收端来管理多个客户端连接。然而,epoll 也同样适用于发送端,特别是当程序需要管理大量的出站连接时。
  在发送端使用 epoll 有若干优势:

  1. 非阻塞 I/O: 可以将套接字设置为非阻塞模式,然后使用 epoll 来检测何时可以在不阻塞的情况下发送数据。
  2. 效率: 当有大量的套接字需要同时发送数据时,使用 epoll 可以减少CPU时间片的浪费,并减少上下文切换,因为可以仅在写入操作能够进行时才尝试发送数据。
  3. 可扩展性: epoll 比传统的多路I/O复用方法(如 select 和 poll)具有更好的可扩展性,并且当监控的文件描述符数量增加时,其性能不会显著下降。
      下面是一个简单的例子代码,演示了如何在Linux环境下使用epoll来监控socket的发送情况:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
 
#define MAX_EVENTS 10
#define SERVER_PORT 6868
#define SERVER_IP "127.0.0.1"
 
int set_non_blocking(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        return -1;
    }
 
    flags |= O_NONBLOCK;
    if (fcntl(sockfd, F_SETFL, flags) == -1) {
        return -1;
    }
 
    return 0;
}
 
int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
       perror("epoll_create1 failed");
       exit(EXIT_FAILURE);
    }
 
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
 
    // 设置套接字为非阻塞模式
    if (set_non_blocking(socket_fd) == -1) {
        perror("set_non_blocking failed");
        exit(EXIT_FAILURE);
    }
 
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
 
    // 连接到服务器
    if (connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        if (errno != EINPROGRESS) { // 非阻塞socket在连接时会返回EINPROGRESS
            perror("connect failed");
            exit(EXIT_FAILURE);
        }
    }
 
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLOUT | EPOLLET;  // 关注可写事件,使用边缘触发模式
    ev.data.fd = socket_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &ev) == -1) {
        perror("epoll_ctl: socket_fd");
        exit(EXIT_FAILURE);
    }
 
    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
 
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == socket_fd && (events[i].events & EPOLLOUT)) {
                // 套接字准备好写入,发送数据
                const char *msg = "Hello, Server!";
                ssize_t bytes_sent = send(socket_fd, msg, strlen(msg), 0);
                if (bytes_sent < 0) {
                    // 发送失败的处理
                    perror("send failed");
                    close(socket_fd);
                    exit(EXIT_FAILURE);
                } else {
                    printf("Message sent: %s\n", msg);
                    // 为了简化示例,发送成功后就退出循环
                    close(socket_fd);
                    close(epoll_fd);
                    exit(EXIT_SUCCESS);
                }
            }
        }
    }
 
    close(epoll_fd);
    return 0;
}

  注意:这个示例假设与服务器的连接已经建立,并准备发送数据。如果服务器没有运行在端口 6868 或者服务器拒绝连接,那么 connect 调用将失败。
  在运行这个代码前,确保本地的服务器正在监听端口 6868,否则 connect 调用将不会成功。此外,该例子只发送一次数据并在发送成功后立即关闭socket和epoll文件描述符,这只是为了示范目的。实际使用中,可能希望保持连接并继续根据需要进行数据发送。

3 多个客户端

  想要在一个进程中管理多个到同一个服务器的连接,不需要为每个连接创建新的进程。而是在同一个进程中打开多个套接字,并将它们全部注册到同一个epoll实例。如下所示:

#include <sys/epoll.h>
#include <sys/socket.h>
// 其他必要的头文件...
 
#define MAX_EVENTS 10
 
int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
       perror("epoll_create1 failed");
       exit(EXIT_FAILURE);
    }
 
    struct epoll_event ev, events[MAX_EVENTS];
    int socket_fds[2];  // 假设我们有两个连接
 
    // 对每个套接字重复连接和设置过程
    for (int i = 0; i < 2; i++) {
        socket_fds[i] = /* 这里是创建套接字并连接到服务器的代码 */;
        ev.events = EPOLLOUT;
        ev.data.fd = socket_fds[i];
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fds[i], &ev) == -1) {
            perror("epoll_ctl: socket_fds[i]");
            exit(EXIT_FAILURE);
        }
    }
 
    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
 
        for (int i = 0; i < nfds; ++i) {
            if (events[i].events & EPOLLOUT) {
                // 在这里根据events[i].data.fd判断是哪个套接字准备好了,然后发送数据
                send(events[i].data.fd, /* data */, /* size */, /* flags */);
                // 处理发送逻辑
            }
        }
    }
 
    close(epoll_fd);
    for (int i = 0; i < 2; i++) {
        close(socket_fds[i]);  // 关闭套接字
    }
    return 0;
}

  在这个示例中,socket_fds 数组用来存储两个套接字描述符,并且都被添加到同一个epoll实例中。这样,在主循环中使用epoll_wait时可以同时监控两个套接字的事件状态。当套接字准备好写数据时,epoll_wait会返回并且通过检查events[i].events 来确定是哪个套接字准备好,并执行相应的send操作。
  上述代码是一个示意性的框架,其中需要填充创建套接字并连接到服务器的代码,以及进行实际数据发送的代码。此外,异常处理和清理操作(如关闭套接字)在实际应用中也需要妥善处理。

4 动态添加和删除客户端套接字

  1. 动态添加
      为每个客户端都维护一个 epoll 实例并不是一个可扩展或高效的解决方案。事实上,epoll 的主要优势之一就是能够使用单个 epoll 实例来监控多个文件描述符(如socket连接)。这样,使用单个线程或者进程就能够管理大量的客户端连接,从而显著减少系统资源的使用和上下文切换的开销。
      正确的做法是为所有的客户端连接使用同一个 epoll 实例。当有新的客户端连接时,可以把新的socket文件描述符添加到这个 epoll 实例中去。这个 epoll 实例会告诉哪些 socket 有事件需要处理,比如数据准备好读取或 socket 准备好写入数据。
      下面是一个简单的例子,展示了如何使用单个 epoll 实例来处理来自多个客户端的连接:
#define MAX_EVENTS 1024
 
// 创建并初始化epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
    // 处理错误
}
 
struct epoll_event event, events[MAX_EVENTS];
 
// 通过某种方式获取到一个监听socket_fd,例如bind和listen之后的socket
 
event.events = EPOLLIN; // 监控可读事件
event.data.fd = listen_socket_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_socket_fd, &event) == -1) {
    // 处理错误
}
 
while (1) {
    // 等待事件发生
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        // 处理错误
    }
 
    for (int i = 0; i < nfds; ++i) {
        if (events[i].data.fd == listen_socket_fd) {
            // 接受新的连接
            int client_fd = accept(listen_socket_fd, NULL, NULL);
            if (client_fd == -1) {
                // 处理错误
            }
 
            // 设置新的socket为非阻塞模式...
 
            // 将新的客户端socket添加到epoll实例中
            event.events = EPOLLIN | EPOLLET; // 边缘触发模式
            event.data.fd = client_fd;
            if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                // 处理错误
            }
        } else {
            // 处理客户端socket的事件:
            // 如果是EPOLLIN事件,读取数据
            // 如果是EPOLLOUT事件,发送数据
            // 如果有EPOLLERR或EPOLLHUP,处理断开连接
        }
    }
}
 
// 清理资源
close(epoll_fd);
// 关闭其他打开的sockets

  这个例子中,我们通过对每个新接受的客户端连接调用 epoll_ctl,让单个 epoll 实例监控多个客户端连接。在服务端程序运行期间,epoll_wait 调用返回准备好的事件,然后我们根据事件类别(可读、可写、错误等)来处理每个客户端的 socket
  使用这种方式,可以高效、可靠和可扩展地管理成千上万个并发连接。
2. 动态删除
  在同一个 epoll 实例中动态地删除多个客户端套接字,可以通过调用 epoll_ctl 函数并指定 EPOLL_CTL_DEL 操作来实现。当决定不再监控某个文件描述符时,需要从 epoll 的监控列表中移除它,以避免无用的资源占用和可能的错误触发。
  以下是一个简单的示例,说明如何删除多个套接字:

#include <sys/epoll.h>
// 其他必要的头文件...
 
int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1 failed");
        exit(EXIT_FAILURE);
    }
 
    // 假设我们有一个socket_fds数组,包含了要监控的所有客户端套接字文件描述符
    int socket_fds[] = { /* ... 客户端套接字文件描述符列表 ... */ };
    int num_sockets = sizeof(socket_fds) / sizeof(socket_fds[0]);
 
    // 将所有客户端套接字添加到epoll监控
    for (int i = 0; i < num_sockets; ++i) {
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = socket_fds[i];
 
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fds[i], &ev) == -1) {
            perror("epoll_ctl: ADD");
            exit(EXIT_FAILURE);
        }
    }
 
    // ... 在这里进行一些IO操作 ...
 
    // 假设现在要移除多个客户端套接字
    for (int i = 0; i < num_sockets; ++i) {
        if (需要删除的条件) { // 这里应该是具体的逻辑条件,用来判断哪些套接字需要被删除
            if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fds[i], NULL) == -1) {
                perror("epoll_ctl: DEL");
                // 即使删除失败,你可能也希望继续尝试删除其他套接字
            }
        }
    }
 
    // 清理并关闭epoll实例
    close(epoll_fd);
    return 0;
}

  在上面的示例中,通过循环遍历一个包含多个套接字的数组,并使用条件来判断是否应该删除某个套接字。满足条件的套接字会通过 epoll_ctl 调用与 EPOLL_CTL_DEL 操作来从 epoll 实例中移除。在 EPOLL_CTL_DEL 操作中,事件参数可以是 NULL,因为删除操作不需要事件结构的信息。
  在实际的并发服务器应用程序中,可能需要对资源访问进行同步,以防止出现竞态条件。如果应用程序是多线程的,确保在访问和修改与 epoll 实例相关的共享资源时使用适当的锁机制。

六 windows下IOCP

Windows 下 IOCP 的简单使用

七 Windows上的epoll实现

1 wepoll简介

  GitHub地址
  wepoll是一个为Windows平台设计的库,它实现了与Linux相似的epoll API,提供了一种高效且可扩展的方式来处理大量套接字的状态通知。如果你正寻找一个在Windows上跨平台使用的,接近于Linux epoll功能的解决方案,那么wepoll是你的不二之选。
  wepoll的亮点在于其高效性和线程安全性。它可以处理数十万个套接字的监控,且支持多线程环境下的同步操作。此外,它提供了完整的事件模型,包括EPOLLIN、EPOLLOUT、EPOLLPRI和EPOLLRDHUP等。虽然目前只支持水平触发模式(EPOLLONESHOT),但已足以满足大部分应用需求。

2 应用场景

  高并发网络服务:对于需要处理大量并发连接的服务,如Web服务器、游戏服务器或流媒体服务器,wepoll能确保在Windows环境下保持高性能。
  跨平台移植:如果你有一个基于Linux的epoll的项目,现在希望将它迁移到Windows上,wepoll可以帮助你无缝过渡,减少代码改动。
  多线程应用:在需要多个线程共享并处理套接字的场景中,wepoll的线程安全特性使得协作更为简单。

3 项目特点

  高效性:wepoll能在Windows上实现与Linux类似的高效套接字事件监控。
  简易集成:只需两个文件(wepoll.c 和 wepoll.h),即可轻松添加到你的项目中。
  全面兼容:适用于Windows Vista及以上版本,并且兼容多种编译器,如MSVC、Clang和GCC。
  API一致性:尽可能地模仿了Linux原生epoll的API和行为,便于熟悉epoll的开发者快速上手。

八 Linux的epoll技术和Windows下的IOCP模型对比

c++网络编程下Linux的epoll技术和Windows下的IOCP模型
Windows完成端口与Linux epoll技术简介

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux IO复用是指在处理多个I/O事件时,通过一种机制使得一个进程可以同时监听多个I/O操作,从而提高程序的效率和性能。 在Linux系统中,常用的IO复用机制有三种:select、poll和epoll。 1. selectselect函数是最早引入的IO复用机制之一,它通过传入一组文件描述符集合,来监听这些文件描述符上是否有事件发生。当其中任意一个文件描述符上有事件发生时,select函数就会返回,然后程序可以通过遍历文件描述符集合来判断哪些文件描述符上有事件发生。 2. poll:poll函数是对select的改进,其使用方式和select类似。不同的是,poll函数使用一个pollfd结构数组来存储待监听的文件描述符及其对应的感兴趣事件,通过调用poll函数时传入这个数组来实现IO复用。相对于select,poll没有最大文件描述符数量的限制,并且效率更高。 3. epoll:epoll是Linux下最新的IO复用机制,它提供了更加高效的IO事件通知机制。epoll使用一个文件描述符来管理被监听的其他文件描述符,通过调用epoll_ctl函数向这个文件描述符中注册或者删除需要监听的文件描述符。当某个文件描述符上有事件发生时,epoll_wait函数会返回该文件描述符的相关信息给程序处理。相对于select和poll,epoll在处理大量连接时具有更好的性能。 总结来说,Linux IO复用机制可以让一个进程同时监听多个I/O事件,避免了使用阻塞IO时的等待时间,提高了程序的效率和性能。而select、poll和epoll是常用的IO复用机制。其中,epoll是效率最高的一种机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值