1. 函数说明
1.1 select
/* According to POSIX.1-2001 */
#include
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set); //清空集合
void FD_CLR(int fd, fd_set *set); //将一个给定的文件描述符从集合中删除
void FD_SET(int fd, fd_set *set); //将一个给定的文件描述符加入集合之中
int FD_ISSET(int fd, fd_set *set); //检查集合中指定的文件描述符是否可以读写
struct timeval {
long tv_sec; /* seconds 秒*/
long tv_usec; /* microseconds 微妙*/
};
该函数准许进程指示内核等待多个事件中的任何一个发生,并在有一个或多个事件发生或经历一段指定的时间后才被唤醒。
- 第一个参数 nfds 指定待测试的描述符个数,其值为最大的文件描述符加1,描述符0、1、2...nfds-1均将被测试(因为文件描述符是从0开始的)。
- 中间的三个参数 readset、writeset、exceptset 指定我们要让内核测试读、写、异常条件的描述符。如果对某一个的条件不感兴趣,就可以把它设为 NULL。struct fd_set 可以理解为一个集合(实际上是一long类型的数组),这个集合中存放的是文件描述符,可通过上面那四个函数进行设置。
- timeout 告知内核等待所指定描述符中的任何一个就绪最多等待时长。没有等到就绪就会超时。该参数控制三种可能:
① 传 NULL 时,一直阻塞直到有感兴趣的描述符上有IO事件就绪。
② timeval结构中指定的秒数和微秒数都是0,检查描述符后立即返回,不阻塞。
③ timeval结构中指定的秒数和微秒数指定了时长,在指定时长超时之前有 IO事件就绪返回,或者超时返回。
1.2 poll
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor 文件描述符 */
short events; /* requested events 等待的事件 */
short revents; /* returned events 实际发生了的事件 */
};
每一个 struct pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,让 poll() 监视多个文件描述符。每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域。revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回。
合法的事件如下:
常量 | 说明 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述字不是一个打开的文件 |
POLLIN | POLLPRI 等价于 select() 的读事件
POLLOUT 等价于 select() 的写事件
① timeout 参数指定等待的毫秒数,在超时之前有 IO 事件就绪或者超时,poll 都会返回
② timeout 为负数值,表示一直阻塞直到一个指定事件发生,poll 才返回。
③ timeout 为0,poll 调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。
返回值和错误代码
成功时:poll() 返回结构体中 revents 域不为0的文件描述符个数。
超时后:如果在超时前没有任何事件发生,poll()返回0。
失败时:poll() 返回-1,并设置errno为下列值之一
EFAULT 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVAL 参数超出 PLIMIT_NOFILE 值。
ENOMEM 可用内存不足,无法完成请求。
1.3 epoll
#include
int epoll_create(int size);
int epoll_create1(int flags);
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);
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 */
};
(1)创建 epoll 实例
现在一般使用 epoll_create1(EPOLL_CLOEXEC),原因:
- 在linux 内核版本大于2.6.8 后,epoll_create(int size) 这个 size 参数就被弃用了,但是传入的值必须大于0。最初实现版本时, size参数的作用告诉内核需要使用多少个文件描述符。内核会使用 size 的大小去申请对应的内存。现在这个size参数不再使用了,内核会动态的申请需要的内存。
- 使用 epoll_create1() 的优点是它允许你指定标志,指定 EPOLL_CLOEXEC 标记在执行另一个进程时文件描述符会自动关闭。
(2)epoll 的事件注册
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
先注册要监听的事件类型。
第一个参数是 epoll_create1 的返回值
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中
EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件
EPOLL_CTL_DEL :从 epfd 中删除一个 fd
第三个参数是需要监听的 fd
第四个参数是告诉内核需要监听什么事,struct epoll_event 结构见上面代码
events 可以是以下几个宏的集合:
常量 | 说明 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) |
EPOLLOUT | 表示对应的文件描述符可以写 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) |
EPOLLERR | 表示对应的文件描述符发生错误 |
EPOLLHUP | 表示对应的文件描述符被挂断 |
EPOLLET | 将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的,LT是缺省的工作方式 |
EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个 socket 加入到 EPOLL 队列里 |
LT (Level Triggered) 水平触发:默认的模式。内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你。
ET (Edge Triggered) 边缘触发:“高速”模式。内核只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。在ET模式下,read一个fd的时候一定要把数据读完。
(3)等待事件的产生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待事件的产生,类似于 select() 调用。
参数 events 用来从内核得到事件的集合
参数 maxevents 告之内核这个 events 有多大
参数 timeout 是超时时间(毫秒,0会立即返回,-1一直阻塞直到有IO事件就绪)
该函数返回需要处理的事件数目,如返回0表示已超时。
2. 三者之间的关联和区别
关联
select,poll,epoll 都是 I/O 多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。
区别
关于最大连接数先理清楚2个概念:
⑴ 一个进程能打开的最大文件描述符,使用 ulimit -n 或者 cat /proc/进程号/limits |grep "Max open file"
❶ 使用 ulimit -n xxx 修改(只针对当前session有效),
❷ 通过 setrlimit 系统调用修改(只对当前进程有效),
❸ 修改 /etc/security/limits.conf 在该文件中添加以下两行:
* soft nofile 100000
* hard nofile 100000
这个值是可以修改的,但是最大不要超过系统所能打开的最大数,超过了也没有什么意义。
⑵ 一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看。
- 注意:对于服务器程序来说并不是“理论”最大只能打开65535个 socket 连接。65535 只是linux系统的最大可用socket端口,比如在一台服务器(这里指主机而非服务程序)上“理论”上最多只能发起65535个客户端连接(实际要小于)这是对的,因为每发起一个连接都需要一个端口。服务器程序只需监听几个端口就够了,它的上限是系统所能打开的最大文件描述符的数量。不要搞混了这两个概念。
> 支持一个进程所能打开的最大连接数
- select 最大连接数有一定限制的,由 FD_SETSIZE 值决定的,默认值是2048。可以对进行修改,需要重新编译内核,但是性能可能会受到影响。
- poll 最大连接数上限是系统能最大可以打开文件的数目,一般来说这个数目受限于系统内存,1GB内存的机器上大约是10万左右,具体数目可以 cat /proc/sys/fs/file-max。
- epoll 与 poll 一样,最大连接数上限是系统能最大可以打开文件的数目。
> 连接数剧增后带来的 IO 效率问题
- select 每次调用都对所有的连接进行线性遍历,所以随着连接数的增加会造成遍历速度的线性下降的性能问题。
- poll 与 select 有相同的问题
- epoll 是事件驱动的,内核中的实现是根据每个连接 fd 上的 callback 函数来实现的,只有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能也会有性能问题。
> 消息传递方式
- select 内核需要拷贝消息到用户空间。
- poll 内核需要拷贝消息到用户空间。
- epoll 通过内核和用户空间共享一块内存来实现,无需拷贝。
一般来说编写高并发服务器程序都会首先 epoll 因为这种环境下其性能最好,但是在连接数少并且连接都十分活跃的情况下,select 和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。