目录
一 函数说明
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,参数 readfds
、writefds
和 exceptfds
称为描述词组,是用来回传该描述词的读,写或例外的状况其实这个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:对于参数timeout
,timeval
的结构如下:
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.youkuaiyun.com/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);
}
}
__pollwait
是 poll
中的核心回调函数,且每个 socket
自己都带有一个等待队列 sk_sleep
。__poll_wait
的作用就是创建了上图所示的数据结构(一次 __poll_wait
即一次设备 poll
调用只创建一个 poll_table_entry
),并通过 struct poll_table_entry
的 wait
成员,把 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
(注册新的 fd
到 epfd
)
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 | 解释 |
---|---|
EBADF | epfd或者fd不是一个有效的文件描述符 |
EEXIST | 当参数是EPOLL_CTL_ADD时,当添加到fd已经在epfd中时,重复添加 |
EINVAL | 1、当epfd不是一个文件描述符,或者fd是一个epfd,或者op是不支持的参数。2、设置了参数EPOLLEXCLUSIVE,却没有和其它有效的参数一起设置。3、使用参数EPOLL_CTL_MOD 时同时包含了。4、使用参数EPOLL_CTL_MOD 时,当前epfd中的fd之前已经被设置了 |
ELOOP | epoll监视队列是可以添加epoll描述符的,就是支持嵌套。当嵌套关系成环时,或者嵌套深度超过5层时,会报这个错误 |
ENOENT | 使用EPOLL_CTL_MOD 和EPOLL_CTL_DEL添加 修改时,修改的套接字却不在epoll的监视队列中 |
ENOMEM | 操作所需要的内存不够 |
ENOSPC | 当EPOLL_CTL_ADD添加时,已经超过了epoll的规格限制 |
EPERM | 添加的fd不支持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>
#include <time.h>
#define MAX_EVENTS 10
#define MAX_BUFFER_SIZE 1024
int LocTime()
{
time_t rawtime;
struct tm * timeinfo;
// 获取当前时间
time(&rawtime);
// 将时间转换为本地时间
timeinfo = localtime(&rawtime);
// 打印当前时间
printf("[%d-%02d-%02d %02d:%02d:%02d]",
timeinfo->tm_year + 1900, // 年份从1900年开始计数
timeinfo->tm_mon + 1, // 月份从0开始计数,所以要加1
timeinfo->tm_mday, // 日期
timeinfo->tm_hour, // 小时
timeinfo->tm_min, // 分钟
timeinfo->tm_sec); // 秒
return 0;
}
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(23554);
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);
}
LocTime();
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)
{
// 客户端连接关闭
LocTime();
printf("Client disconnected\n");
close(client_fd);
continue;
}
// 处理接收到的数据
LocTime();
printf("Received data from client: %s\n", buffer);
// 将数据发送回客户端
send(client_fd, buffer, num_bytes, 0);
LocTime();
printf("Send data to client: %s\n", buffer);
}
}
}
// 关闭套接字和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 有若干优势:
- 非阻塞 I/O: 可以将套接字设置为非阻塞模式,然后使用 epoll 来检测何时可以在不阻塞的情况下发送数据。
- 效率: 当有大量的套接字需要同时发送数据时,使用 epoll 可以减少CPU时间片的浪费,并减少上下文切换,因为可以仅在写入操作能够进行时才尝试发送数据。
- 可扩展性: 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 动态添加和删除客户端套接字
- 动态添加
为每个客户端都维护一个 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上的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技术简介