在Linux系统编程中,会遇到一个进程处理多个IO问题,例如:
//本例想实现循环读取鼠标和键盘的输入
int fd_mice = open("/dev/input/mice", O_RDONLY); //获取鼠标的文件描述符
int fd_key = open("/dev/input/event1",O_RDONLY); //获取键盘的文件描述符
char buf[1024];
while(1)
{
if(read(fd_key,buf,sizeof(buf))>0) printf("keyboard event");
if(read(fd_mice,buf,sizeof(buf))>0) printf("mice event");
}
由于read为阻塞模式, 如果2个read 写在一个循环中, 当没有键盘事件时,程序会一直阻塞,直到键盘可读,这时候如果键盘一直没有数据,但鼠标有数据了,while循环也是无法往下运行的。
想要同时监控键盘和鼠标的数据,不造成程序的阻塞,是有一些方法的:
例如,可以开2个进程或者线程,一个监控键盘事件, 一个监控鼠标事件。但是如果是服务器,有10000个人要连接呢?开10000个线程资源消耗就大了。
或者,把2个fd 设置为非阻塞的fcntl(fd_mice,F_SETFL,fcntl(fd_mice,F_GETFL)|O_NONBLOCK); 但是这样做,while循环就会不停的轮询这2条非阻塞的fd,CPU的消耗就太高了。
所以要同时处理多路IO时, 我们用select, epoll来解决问题.
select
select()要引入头文件 sys/select.h,功能:“让程序能够监控文件描述符,直到文件描述符的I/O事件变成就绪状态,也就是文件描述符变成可读写。”
在看select前,要了解2个结构体类型:
fd_set是 sys/select.h 中定义的 文件描述符集合结构体
typedef struct{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
//原型里面一堆"_",其实都是long int的宏定义,结构体里面只有一个成员 fds_bits[],我们可以把它理解成一个 int数组.
timeval是 sys/time.h 中定义的 时间结构体:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set **exceptfds, struct timeval *timeout);
返回值:就绪描述符的数目,超时返回0,出错返回-1
1.第一个参数: 最大的文件描述符+1
2.中间的三个参数readset、writeset和exceptset指定读、写和异常条件的描述字。如果对某一个的条件不感兴趣,可以设置NULL。fd_set定义的集合,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
3.第三个参数是超时时间。
//本例用select解决循环读取鼠标和键盘的输入
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/select.h>
int main()
{
int fd_mice = open("/dev/input/mice", O_RDONLY);
int fd_key = open("/dev/input/event1",O_RDONLY);
char buf[1024];
fd_set readfds; //申请一个文件描述符集合
FD_ZERO(&readfds); //清空集合
FD_SET(fd_mice,&readfds); //把fd_mice添加到集合
FD_SET(fd_key,&readfds); //把fd_key添加到集合
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 5;
while(1)
{
int ret = select(1024, &readfds, NULL, NULL, &timeout);
if(ret==0) continue; //阻塞超时,无就绪
if(ret<0 && errno == EINTR) continue; //被信号打断
if(ret <0) break; //错误
if(FD_ISSET(fd_key, &readfds)) //位操作,判断fd_key事件是否在readfds集合中激活
{
read(fd_key, buf, sizeof(buf));
printf("keyboard event");
}
if(FD_ISSET(fd_mice, &readfds)) //位操作,判断fd_mice事件是否在readfds集合中激活
{
read(fd_mice, buf, sizeof(buf));
printf("keyboard event");
}
}
}
epoll
epoll分为LT水平触发模型和ET边沿触发模型
LT模型类似于原来的select/poll操作,只要事件还没有处理完就会一直通知直到处理为止.
ET模型当套接字“状态发生变化”时候触发一次通知.
epoll只有3个函数:
int epoll_create(int size);
返回值:返回一个epoll的epfd操作句柄,内部是个RB-Tree,占用一个fd,所以不用了需要close。
参数:size用来告诉内核这个fd监听的数目一共有多大,在Linux 2.6.8后这个参数已经忽略了,传值时大于0即可。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
返回值:成功返回0,失败返回-1,设置errno。
参数:epfd: epoll_create的返回值,epoll句柄。
op: 操作宏,有EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL
fd : 要操作的fd
event:要监控的事件
struct epoll_event结构如下:
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 */
};
events可以是以下几个宏的集合:
EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
返回值:>0 就绪的fd数目,0 没有fd就绪, -1 错误并设置errno
参数: epfd epoll_create的返回值
events 就绪events集合
maxevents 集合大小
timeout 超时时间
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/epoll.h>
int main()
{
int epollfd = epoll_create(512);
int fd_key = open("/dev/input/event1", O_RDONLY|O_NONBLOCK);
int fd_mice = open("/dev/input/mice", O_RDONLY|O_NONBLOCK);
struct epoll_event ev;
ev.events = EPOLLIN; // 监控可读
ev.data.fd = fd_key;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_key, &ev);
ev.data.fd = fd_mice;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_mice, &ev);
struct epoll_event evout[2];
char buf[1024];
while(1)
{
int ret = epoll_wait(epollfd, evout, 2, 5000);
if(ret == 0) continue; // 超时
if(ret < 0 && errno == EINTR) continue; // 被信号打断
if(ret < 0) break; // 错误发生了
// ret > 0情况
int i;
for(i=0; i<ret; ++i)
{
int fd = evout[i].data.fd;
if(read(fd, buf, sizeof(buf)) < 0)
{
// close自动将它从epoll中移除
close(fd);
}
if(fd == fd_key) printf("键盘有消息\n");
else if(fd == fd_mice) printf("鼠标有消息\n");
}
}
}