一、select函数
#include <sys/select.h>
#include <sys/time.h>
//若有返回描述符个数,超时返回0,出错返回-1
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *excepset, const struct timeval *timeout);
//timeval结构
struct timeval {
long tv_sec;
long tv_usec;
}
//设置fd_set的四个宏
FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)参数说明: 1. maxfd为最大的描述符加1
2. 中间3个参数为我们要让黑河测试读写和异常条件的描述符。
3. timeval结构,超时条件,可以作为更加精确的时钟。
函数说明:
select最多能处理FD_SIZE个描述符,如果要处理的超过1024,只能采用多进程。
常见使用select的多进程模型时:一个进程专门accept。
函数说明:
select最多能处理FD_SETSIZE个描述符号,成功后将fd传给子进程处理,父进程可以根据子进程负载分派。
简单的select使用:
void str_cli(FILE *fp, int sockfd){
int maxfd;
fd_set rset;
for(; ;){
FD_ZERO(&rset);
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfd = max(fileno(fp), sockfd) + 1;
if(select(maxfd, &rset, NULL, NULL, NULL) < 0){
perror("select error: ");
}
if(FD_ISSET(sockfd, &rset)) {
//...
}
if(FD_ISSET(fileno(fp),&rset)) {
//....
}
}
}
二、poll函数
#include <sys/poll.h>
//返回值和select一样
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
//pollfd结构
struct pollfd {
int fd; //描述符
short events; //需要检查就绪的描述符状态
short revents;//返回的描述符状态
}参数说明:1. fdarray指向结构数组的第一个指针。
2. nfds表示结构数组中的元素个数
函数说明:(下面摘抄http://kaiyuan.blog.51cto.com/930309/341121的内容)
poll是一个系统调用,其内核入口函数为sys_poll,sys_poll几乎不做任何处理直接调用do_sys_poll,do_sys_poll的执行过程可以分为三个部分:
1.将用户传入的pollfd数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个O(n)操作,这一步的代码在do_sys_poll中包括从函数开始到调用do_poll前的部分。
2,查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起操作是通过调用schedule_timeout执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是O(n),这里面不包括等待时间。相关代码在do_poll函数中.
3,将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是O(n),具体代码包括do_sys_poll函数中调用do_poll后到结束的部分。
poll函数简单使用(UNP上的例子截取):
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
listen(listenfd, LISTENQ);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 indicates available entry */
/* include fig02 */
for ( ; ; ) {
nready = Poll(client, maxi+1, INFTIM);
if (client[0].revents & POLLRDNORM) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
//............下面添加描述符,设置需要监听的描述符状态
}
三、epoll函数
epoll最大的好处就是不会随着监听fd的数目的增长而降低效率。epoll的接口有三个函数:1. int epoll_create(int size);
函数说明:创建一个epoll句柄,size告诉内核监听的数目,需要注意的是,创建epoll句柄需要消耗一个fd,使用完后必须关闭close()关闭,在linux下查看
/proc/fd能够看到该句柄。
2.int epoll_clt(int epfd, int op, int fd, struct epoll_event *event);
函数说明:epoll的事件注册函数,通过op参数,注册要监听的事件类型。
参数说明:
1. epfd: epoll_create()函数的返回值
2. op:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
3. fd : 需要监听的fd
4. struct epoll_event *: 结构如下
struct epoll_event {
_uint32_t envents; //表示对应的描述符可读还是可写。。。等需要监听的状态
epoll_data_t data;
}
具体的数据类型,参照sys/epoll头文件中定义如下:
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
EPOLLET = 1u << 31
#define EPOLLET EPOLLET
};
/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
#define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */
#define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */
#define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */
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 */
} __EPOLL_PACKED;3.int epoll_wait (int __epfd, struct epoll_event *__events,int __maxevents, int __timeout);函数说明: 等待监听事件的发生,和select()一样。
参数说明:
1, epfd:和上面一样
2. events: 从内核得到的事件集合
3.maxevnets: 表示events大小
4.timeout:超时
说明:Epoll的两种工作模式
一、Edge Triggered 模式
ET是高速模式,只支持no-block socket。在这种模式下,当描述符从为就绪变为就绪时,内核通过epoll通知进程。如果你不做处理,内核不会再为那个描述符发送更的就绪通知,直到你做了某些操作倒是那个文件描述符不再为就绪状态。内核只会发送一次通知(only once)。
当使用ET模式的时候,产生EPOLLIN事件,读数据需要考虑recv()返回的大小如果等于请求大小,那么很可能缓冲区还有数据未读完,就是意味着该时间还没有处理完,所以还需要再次读写。
假设现在对方发送了2k的数据,而我们先读取了1k,然后这时调用了epoll_wait,如果是边沿触发,那么这个fd变成就绪状态就会从epoll 队列移除,很可能epoll_wait 会一直阻塞,忽略尚未读取的1k数据,与此同时对方还在等待着我们发送一个回复ack,表示已经接收到数据;如果是电平触发,那么epoll_wait 还会检测到可读事件而返回,我们可以继续读取剩下的1k 数据。
如下程序:
while(rs){
buflen = recv(fd, buf, sizeof(buf)); //非阻塞
if(buflen < 0){
if(error == EAGAIN){ //由于是无阻塞的,当error为EAGIAN表示当前缓冲区没有数据可读
break;
}else{
return;
}
}else if(buflen == 0){
//对端socket正常关闭
}
if(buflen == sizeof(buf)){
rs = 1;
}else{
rs = 0;
}
}
二、Level Triggered缺省的工作方式,同时支持block和no-block socket。在该工作模式下,内核通知进程一个文件描述符就绪,然后进程可以对这个就绪的fd进行IO操作。如果你不做任何操作,内核还是会继续通知你。所以这种模式编程出错的可能行要小一点,这一点select和poll都是该模式。
三、select poll和epoll的区别
1、相比于select与poll,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
2、epoll的实现是基于回调的,如果fd有期望的事件发生就通过回调函数将其加入epoll就绪队列中,也就是说它只关心“活跃”的fd,与fd数目无关。
3、内核 / 用户空间 内存拷贝问题,如何让内核把 fd消息通知给用户空间呢?在这个问题上select/poll采取了内存拷贝方法。而epoll采用了共享内存的方式。
4、epoll不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个fd集合
本文详细介绍了Linux下的三种I/O多路复用机制:select、poll和epoll的使用方法及原理。select和poll采用轮询方式,而epoll采用回调机制,效率更高。epoll还支持边沿触发和电平触发两种模式。

1537

被折叠的 条评论
为什么被折叠?



