目前支持I/O多路复用的系统调用有select、poll、epoll,I/O多路就是 通过一种机制,一个线程可以监视多个文件描述符,一旦文件描述符就绪,能够通知程序进行相应的读写操作。select、poll、epoll本质上都是同步I/O操作,因为他们都需要在读写事件就绪后由线程自己负责读写操作,整个读写过程使阻塞的。而异步I/O是不需要自己进行读写的,内核会负责将数据从内核拷贝到用户空间。
多路指网络连接,复用指的是同一个线程。
如果使用多线程+阻塞IO达到类似的效果,但是每个socket对应一个线程,对于长连接来说,线程资源一直不会释放,就造成性能问题。
此时用IO多路复用,使用一个线程去监视多个socket,只有真正有读写事件的时候才会去通过用户线程执行。
适用场景:
- 客户端需要同时处理多个socket请求
- 客户端程序需要同时处理用户输入和网络连接
- 服务器需要同时处理监听socket和连接socket
select
int select (int n,fd_set *readfds,fd_set *weitefds,fd_set *exceptfds,struct timeval *timeout);
优点:
- select目前几乎所有的平台上都支持,跨平台性
缺点:
- 单个线程可以监视的文件描述符的数量存在最大限制,它是由一个FD_SETSIZE设置的,默认最大值为1024.
- fd集合在内核中被置位过,与传入的fd不可重用,需要重新设置希望内核监视的标志位
- 每次调用select都需要将集合从用户态拷贝到内核态,响应时再从内核态拷贝回用户态
- 每次调用时,内核需要进行一次遍历标志位,传回用户态,用户也需要进行一个遍历,O(n)
poll
int poll(struct pollfd *fds,unsigned int nfds,int timeout);
不同于select使用位图来表示fdset的方式,poll则是定义了一个结构体来存储相关事件的信息。
struct pollfd{
int fd; //需要监视的文件描述符
short events; //需要监视的事件
short revents; //发生的事件
};
优点:
- poll使用 pollfd数组代替了bitmap,没有最大数量限制
- 利用结构体,每次只需要修改revents即可。pollfd可以重用
缺点:
- 每次调用poll时,还是需要将pollfd数组从用户态拷贝到内核态,返回时也一样
- 每次对pollfd数组查询时都需要进行遍历查询,随着监视的文件描述符不断增大,效率也线性降低
epoll
int epoll_create(int size); //创建一个epoll的文件描述符
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);
struct eventpoll{
struct rb_root rbr; //红黑树,存储需要监控的事件
struct list_head rdlist; //双链表存储需要返回给用户的已响应事件
};
struct epoll_event{
__uint32_t events; //监听的事件
epoll_data_t data; //监听的文件描述符
};
事件响应过程:
- epoll再内核中使用红黑树来跟踪进程的所有待检测的文件描述符,把需要监控的文件描述符用
epoll_ctl()函数加入到内核中的红黑树中。增删查的事件复杂度都是O(logn)。每次只需要传入新的待检测文件描述符,旧的文件描述符都还存储在内核中,减少了内核和用户空间之间大量的数据拷贝和内存分配。 - epoll通过事件驱动的机制,内核中维护了一个链表来记录就绪事件。当有个事件响应时,通过回调函数内核会将其加入到就绪事件列表中。当用户调用
epoll_wait()函数时,只返回有事件响应的文件描述符对象。
事件触发模式:
- 边缘触发(ET),当监视的文件描述符上有事件发生,只会从
epoll_wait()中苏醒一次,即使内核缓缓冲区中还有数据没读完,也只会苏醒一次,所有ET模式下需要一次性将内核缓冲区的数据读取完。 - 水平触发(LT),当监视的文件描述符上有事件发生,会不断的从
epoll_wait()中苏醒,知道内核缓冲区的数据被读完。
举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;
如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。
边缘触发的效率比水平触发的效率高,可以减少 epoll_wait()的系统调用函数。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发。
优点:
- 监听的文件描述符不受限制
- epoll内核使用红黑树维护所有的fd,查询效率高,返回的是响应事件的链表
- epoll的效率不会因为监视的文件描述符的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个文件描述符的回调函数实现的。只有就绪的fd才会执行回调函数。
代码
select代码
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main(){
//创建socket
int lfd = socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
//监听
listen(lfd,8);
//创建一个fd_set集合,存放的是需要检测的文件描述符,前三位默认012被占用
fd_set rdset,tmp;
//将集合中的位清空
FD_ZERO(&rdset);
//将监听文件描述符加入到集合中
FD_SET(lfd,&rdset);
//当前最大的文件描述符,select时使用
int maxfd = lfd;
while(1)
{
//需要备份
tmp = rdset;
//调用select,检测哪些文件描述符有数据
int ret = select(maxfd+1,&tmp,NULL,NULL,NULL);
if(ret==-1)
{
perror("select");
exit(-1);
}
//设置非阻塞时可能会返回0
else if(ret==0){
continue;
}
else if(ret>0)
{
//检查监听文件描述符
if(FD_ISSET(lfd,&tmp)){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
//添加需要监听的文件描述符
FD_SET(cfd,&rdset);
maxfd = maxfd>cfd?maxfd:cfd;
}
for(int i=lfd+1;i<=maxfd;i++)
{
if(FD_ISSET(i,&tmp)){
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
poll代码
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
int lfd = socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
listen(lfd,8);
//建立pollfd数组,数组大小由自己定义,没有限制
struct pollfd fds[1024];
for(int i=0;i<1024;i++)
{
//初始化每个pollfd对象
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;
while(1){
int ret = poll(fds,nfds+1,-1);
if(ret == -1) {
perror("poll");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
if(fds[0].revents & POLLIN){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
for(int i=1;i<1024;i++)
{
if(fds[i].fd==-1)
{
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
// 更新最大的文件描述符的索引
nfds = nfds > cfd ? nfds : cfd;
}
for(int i=1;i<=nfds;i++)
{
if(fds[i].fd!=-1 && fds[i].revents & POLLIN){
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
epoll代码
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main(){
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
//调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
//创建epoll对象中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
//将监听文件描述符添加到
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);
//返回的可操作文件描述符
struct epoll_event epevs[1024];
while(1){
int ret = epoll_wait(epfd,epevs,1024,-1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i=0;i<ret;i++)
{
int curfd = epevs[i].data.fd;
if(curfd==lfd){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
char cliIP[16];
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,cliIP,sizeof(cliIP));
unsigned short clientPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIP, clientPort);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
}else{
if(!(epevs[i].events & EPOLLIN)){
continue;
}
// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf)-1);
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
//从实例中删除这个文件描述符
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
版权声明:本文为why18767183086原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。