IO多路复用之Select机制
1.1 基本概念
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
1.2 select函数
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); |
- 返回值就绪描述符的数目,超时返回0,出错返回-1
- 函数参数介绍(1)第一个参数nfds指定待测试的描述字个数,它的值是待测试的最大描述字加1 (如果文件描述符是5,那么nfds就填5+1)。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct 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)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds }; |
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
1.3 总结
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
1.4 示例1
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <linux/input.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define DEVICE "/dev/input/event3" //鼠标设备 fd_set read_set; struct input_event ev; int main() { int fd; fd=open(DEVICE,2); if(fd<0) { printf("驱动打开失败!\n"); } while(1) { FD_ZERO(&read_set); FD_SET(fd,&read_set); if(select(fd+1,&read_set,NULL,NULL,NULL)) { if(FD_ISSET(fd,&read_set)) { if(read(fd,&ev,sizeof(struct input_event))==sizeof(struct input_event)) //读取发生的事件 { printf("key=%d value = %d\n",ev.code,ev.value); } } } } return 0; } |
1.5 示例2
查看系统的标准文件描述符: [root@wbyq test_20180702]# ls /dev/std* -l lrwxrwxrwx. 1 root root 15 4月 17 11:34 /dev/stderr -> /proc/self/fd/2 lrwxrwxrwx. 1 root root 15 4月 17 11:34 /dev/stdin -> /proc/self/fd/0 lrwxrwxrwx. 1 root root 15 4月 17 11:34 /dev/stdout -> /proc/self/fd/1 |
网络通信里使用select机制:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> unsigned char rx_buff[1024]; unsigned int rx_cnt; unsigned char log_info[1024]; unsigned int log_cnt=0; /* TCP服务器创建 */ int main(int argc,char **argv) { int tcp_server_fd; //服务器套接字描述符 int tcp_client_fd; //客户端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //服务器的端口号 //判断传入的参数是否合理 if(argc!=2) { printf("参数格式:./tcp_server <端口号>\n"); return -1; } tcp_server_port=atoi(argv[1]); //将字符串转为整数 /*1. 创建网络套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP服务器端套接字创建失败!\n"); return -1; } /*2. 绑定端口号,创建服务器*/ tcp_server.sin_family=AF_INET; //IPV4协议类型 tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP服务器端口绑定失败!\n"); return -1; } /*3. 设置监听的客户端数量*/ listen(tcp_server_fd,10); /*4. 等待客户端连接*/ tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(tcp_client_fd<0) { printf("TCP服务器:等待客户端连接失败!\n"); return -1; } //打印连接的客户端地址信息 printf("已经连接的客户端信息: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*5. 数据通信*/ fd_set readfds; //读事件的文件操作集合 fd_set writefds;//写事件的文件操作集合 int select_state; //接收返回值 while(1) { /*5.1 清空文件操作集合*/ FD_ZERO(&readfds); FD_ZERO(&writefds); /*5.2 添加要监控的文件描述符*/ FD_SET(tcp_client_fd,&readfds); FD_SET(tcp_client_fd,&writefds); /*5.3 监控文件描述符*/ select_state=select(tcp_client_fd+1,&readfds,&writefds,NULL,NULL); if(select_state>0)//表示有事件产生 { /*5.4 测试指定的文件描述符是否产生了读事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 读取数据*/ rx_cnt=read(tcp_client_fd,rx_buff,1024); if(rx_cnt==0) { printf("对方已经断开连接!\n"); break; } sprintf(log_info,"server rx data[%d]",log_cnt++); write(tcp_client_fd,log_info,strlen(log_info)); //回发数据 write(tcp_client_fd,rx_buff,rx_cnt); //将收到的数据返回 } //判断是否产生了写事件 if(FD_ISSET(tcp_client_fd,&writefds)) { printf("写事件!\n"); //只要有写权限,写事件将会一直产生 //比如: //连接建立成功后可写 //缓冲区可写 } } else if(select_state<0) //表示产生了错误 { printf("select函数产生异常!\n"); break; } } /*6. 关闭连接*/ close(tcp_client_fd); } |
第二章 IO多路复用之poll机制
2.1 基本概念
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
2.2 poll函数
函数格式如下所示:
# include <poll.h> int poll ( struct pollfd * fds, unsigned int nfds, int timeout); |
pollfd结构体定义如下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ } ; |
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN | 有数据可读。 |
POLLRDNORM | 有普通数据可读。 |
POLLRDBAND | 有优先数据可读。 |
POLLPRI | 有紧迫数据可读。 |
POLLOUT | 写数据不会导致阻塞。 |
POLLWRNORM | 写普通数据不会导致阻塞。 |
POLLWRBAND | 写优先数据不会导致阻塞 |
POLLMSGSIGPOLL | 消息可用。 |
此外,revents域中还可能返回下列事件:
POLLER | 指定的文件描述符发生错误。 |
POLLHUP | 指定的文件描述符挂起事件。 |
POLLNVAL | 指定的文件描述符非法。 |
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。
使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。
POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
- 返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF | 一个或多个结构体中指定的文件描述符无效。 |
EFAULTfds | 指针指向的地址超出进程的地址空间。 |
EINTR | 请求的事件之前产生一个信号,调用可以重新发起。 |
EINVALnfds | 参数超出PLIMIT_NOFILE值。 |
ENOMEM | 可用内存不足,无法完成请求。 |
2.3 示例
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <poll.h> /*使用poll轮询机制*/ /*定义一个poll结构体数组,用来存放poll相关的信息*/ struct pollfd tiny4412_poll[1]; /* argv :字符串的个数 argc :存放字符串的数组 ./app /dev/led */ int main(int argv,char*argc[]) { int fb_count; /*存放poll函数返回值*/ int count=0; int fb1,tmp; if(argv!=2) { printf("传参方式: ./app /dev/<device filce>\n"); exit(-1); } /*打开设备文件,打开成功返回文件描述符*/ fb1=open(argc[1],O_RDWR); /*打开第一个驱动-KEY*/ if(fb1<0) { printf("open device_file error!\n"); exit(-1); } char key; printf("open device_file ok!\n"); tiny4412_poll[0].fd=fb1; /*检测的文件描述符*/ tiny4412_poll[0].events=POLLIN;/*检测的事件*/ while(1) { fb_count=poll(tiny4412_poll,1,3000); //printf("检测的文件描述符数量:%d\n",fb_count); if(tiny4412_poll[0].revents==POLLIN) { tmp=read(fb1,&key,sizeof(char)); /*读按键值*/ printf("APP--KEY: 0x%x\n",key); } count++; printf("计数值:%d\n",count); } close(fb1); return 0; } |
第三章 IO多路复用之epoll
3.1 基本知识
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
3.2 epoll接口
epoll操作过程需要三个接口,分别如下:
#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); |
(1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD | 注册新的fd到epfd中; |
EPOLL_CTL_MOD | 修改已经注册的fd的监听事件; |
EPOLL_CTL_DEL | 从epfd中删除一个fd; |
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
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队列里 |
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
3.3 示例
#include <stdio.h> #include <sys/epoll.h> #include <stdlib.h> /*定义最大的事件个数*/ #define MAX_EVENTS 10 /*定义epoll相关结构体*/ struct epoll_event ev[MAX_EVENTS]; struct epoll_event events[MAX_EVENTS]; /*定义需要用到的变量*/ int key=0,epollfd=0,cnt=0,nfds,n; int main(int argc,char **argv) { int fd; if(argc!=2) { printf("用法: ./app /dev/xx\n"); return -1; } fd=open(argv[1],2); if(fd<0) { printf("%s 设备节点打开失败!\n",argv[1]); return -1; } /*1.1 创建epoll专用的文件描述符*/ epollfd = epoll_create(10); if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } /*1.2 填充事件结构体*/ ev[0].events = EPOLLIN; ev[0].data.fd = fd; /*1.3 注册epoll事件*/ if (epoll_ctl(epollfd, EPOLL_CTL_ADD,fd,&ev[0]) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } /*1.4 循环检测是否有事件发生*/ for (;;) { nfds = epoll_wait(epollfd,events, MAX_EVENTS,3000); if(nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } /*遍历检测具体发生的事件*/ for (n = 0; n < nfds; ++n) { if (events[n].data.fd == fd) { /*读取按键值*/ read(fd,&key,4); printf("APP: 0x%X\n",key); } } cnt++; printf("cnt=%d\n",cnt); } return 0; } |