【Linux系列】多路复用IO,从底层理解select

 2023-09-05 阅读 96 评论 0

摘要:网络数据是怎么接收到的 网卡收到网线传送过来的数据,经过硬件电路传送到内存,将数据写在内存的某个地址中。 网卡将数据写入内存后,网卡产生一个中断,通过总线把电信号发送给中断控制器。中断控制器把中断发送给处理器,也就是通过电信号给

网络数据是怎么接收到的

在这里插入图片描述
网卡收到网线传送过来的数据,经过硬件电路传送到内存,将数据写在内存的某个地址中。
在这里插入图片描述
网卡将数据写入内存后,网卡产生一个中断,通过总线把电信号发送给中断控制器。中断控制器把中断发送给处理器,也就是通过电信号给处理器特定的管脚发送一个高电平。处理器立即停止它正在做的事,然后跳转到内存中预定义的位置开始执行那里的代码,这个预定义的位置由内核设置的,是中断处理程序的入口点。

对于每个中断线,处理器会跳到对应的一个唯一的位置,这样,内核就知道接收中断的IRQ号,然后调用函数do_IRQ(),最终调到网卡中断处理程序去处理数据。

read读不到数据怎么阻塞的

网络应用程序调用到read读取数据时,如果没有数据就一直阻塞。阻塞的话,就会将当前进程从工作队列中移除,放入到等待队列中。当数据到达时,又放入工作队列中。

假如A进程是一个网络应用程序,调用socket函数,文件系统会创建一个有接收缓冲、发送缓冲和等待队列的sock对象,这个等待队列指向了所有需要等待该socket事件的进程。
在这里插入图片描述
当进程执行到read时,A进程就从工作队列中移到该socket的等待队列中,A进程就被阻塞。
在这里插入图片描述
阻塞期间,如果来数据了,中断处理就会将数据装入接收队列,然后唤醒A进程,移动到工作队列中。

如何同时监控多个socket

每个read只能监控一个socket,select的想法是,预先传入一个socket数组,如果数组中的所有socket都没有数据,进程就被挂起,直到有socket收到数据,唤醒进程。

如果需要监控1,2,3三个socket,就把A进程加入三个socket的等待队列中。
在这里插入图片描述
当其中任何一个socket有数据,就将进程唤醒,从所有的等待队列中移除,加入工作队列。被唤醒后,A进程知道至少有一个socket有数据了,遍历一遍,就能得到所有就绪的socket。
在这里插入图片描述
这里有两个问题:

1.每次调用select都需要将进程加入到所有要监控的socket的等待队列中,每次唤醒都要从所有的队列中移除。并且每次都要将整个fds数组传递给内核。开销比较大。

2.被唤醒后,进程并不知道哪些socket有数据,需要遍历。

epoll的改进

1.select低效的第一个原因,是因为每次调用它都需要维护等待队列和阻塞进程,而大多数情况,需要将进程放入哪些socket的等待队列都是固定的,不需要每次都修改。所以,epoll将等待队列和阻塞进程分开了,使用epoll_ctl维护等待队列,使用epoll_wait阻塞进程。

2.对于select不知道哪些socket收到数据,需要遍历,epoll内部维护了一个就绪队列,收到数据的socket直接加入就绪队列,当进程被唤醒,只要获取就绪队列rdlist就能知道哪些socket收到数据了。
在这里插入图片描述

epoll的实现

调用epoll_create,返回一个文件描述符,内核会创建eventpoll对象。

通过epoll_ctl,将向eventpoll中的红黑树添加所有要监控的socket,和删除不需要监控的socket。

执行epoll_wait,由于rdlist为空,将A进程放入eventpoll的等待队列,阻塞进程。当某个socket收到数据,就加入到rdlist,同时唤醒eventpoll中的等待进程,加入工作队列。所有有数据的socket都在rdlist中。
在这里插入图片描述

select的使用

select系统调用是用来让我们的程序监视多个文件描述状态变化的。程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生状态变化。通常I/O操作有两个步骤,一个是等,另一个是数据搬迁。select主要是在等的这个状态阻塞着直到事件发生。

头文件:

#include<sys/select.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/time.h>

函数原型:

int select(int nfds,fd_set *reads,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

参数:
nfds:是需要监视的最大的文件描述符的值+1。

fd_set:
fd_set底层是用位图实现的,每一个位都代表一个文件描述符。readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符结合,可写描述符集合,异常文件描述符集合,他们都是输入输出型参数。

当作为输入参数时:只要文件描述符集合中对应的位上为1,就表示select需要监视这个描述符的状态。比如readfds里面的文件描述符就代表他们需要等待读事件,writefds里面的文件描述符就代表他们需要等待写事件。

当作为输出参数时,只要文件描述符集合中对应的位上为1,就代表他们等待的事件已经就绪,这是由内核设定的。

timeout:设置超时时间。
timeout里面的成员设定为特定的时间值:
如果在这段时间里面没有事件发生,select将超时返回。struct timeval结构用于描述一段时间长度,如果在这个时间内,需要监听的描述符没有事件发生则函数返回,返回值为0。

struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}

timeout里面的成员等于0:表示非阻塞轮询方式,不断的去检测描述符集合的状态,然后立即返回。
timeout为NULL:表示以阻塞的方式等待事件发生。

返回值:
成功的话,返回文件描述符状态已改变的个数。如果返回0代表在描述符状态改变之前已经超过timeout时间。如果有错误发生的话,则返回-1。

下面的宏用来处理描述符集合:
void FD_CLR(int fd,fd_set* set); 用来清除描述符词组set中相关fd的位。
int FD_ISSET(int fd,fd_set* set); 用来测试set中相关fd的位是否为真。
void FD_SET(int fd,fd_set* set); 用来设置描述词组set中相关fd的位。
void FD_ZERO(fd_set *set);用来清除描述符词组set的全部位。

select模型:
select可监控的描述符取决于sizeof(fd_set)的值,因为文件描述符是用位图表示的,所以能监控的描述符的最大数量是sizeof(fd_set)*8,fd_set的大小可以调整。

将fd加入select监控集的同时,还要使用一个额外的数组保存select监控集中的fd。一方面是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断事件是否就绪。另一方面是select返回之后会把以前加入的但并无事件发生fd清空,这是由内核清空的,所以每次开始select前都要重新从array中取得fd加入到fd_set中。

还有就是因为select第一个参数是当前要监测的文件描述符的最大值加1,可以在扫描array的同时取得fd的最大值maxfd,用于select的第一个参数。
所以select的缺点就是,每次selcet之前都要遍历数组加入fd,select返回后还要遍历数组进行判断哪些事件已经就绪(FD_ISSET判断是否有事件发生)。

select的缺点:
1、每次调用select,都需要把fd集合从用户态拷贝到内核态。这个开销在fd很多的时候会很大。
2、select在返回之后,需要我们遍历数组去查找事件就绪的描述符。这个过程的时间复杂度是O(N)。而epoll它查找就绪事件的时候是O(1)。
3、select支持的文件描述符的数量太小了,默认是1024。

总结:针对select的缺点来看,即时fd_set可以改动,也不建议将它改的很大,因为一但支持的文件描述多了,效率自然也就降低了。

应用

例:实现一个服务器,使用select让服务器可以同时接受多个客户的链接,并将客户发送的数据打印出来。

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<unistd.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#define SIZE 128int startup(char *ip,int port)
{assert(ip);int sock=socket(AF_INET,SOCK_STREAM,0);if(sock<0){perror("socket");exit(0);}int opt=1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));struct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(port);local.sin_addr.s_addr=inet_addr(ip);if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){perror("bind");exit(1);}if(listen(sock,5)<0){perror("listen");exit(2);}return sock;
}int main(int argc,char* argv[])
{if(argc!=3){printf("usae: %s [IP] [PORT]\n",argv[0]);return 0;}int lis_sock=startup(argv[1],atoi(argv[2]));int gfds[SIZE];memset(gfds,-1,SIZE*4);fd_set rfds;FD_ZERO(&rfds);while(1){struct timeval timeout={5,0};gfds[0]=lis_sock;int max_fd=-1;int i=0;for(;i<SIZE;i++){if(max_fd<gfds[i]){max_fd=gfds[i];}if(gfds[i]>=0){FD_SET(gfds[i],&rfds);}}int ret=select(max_fd+1,&rfds,NULL,NULL,NULL);switch(ret){case 0:printf("timeout...\n");break;case -1:printf("error");break;default:if(FD_ISSET(gfds[0],&rfds)){struct sockaddr_in peer;socklen_t len=sizeof(peer);int connfd=accept(lis_sock,(struct sockaddr*)&peer,&len);if(connfd<0){perror("accept");}else{printf("client: %s:%d fd(%d)\n",inet_ntoa(peer.sin_addr),\ntohs(peer.sin_port),connfd);                    int k=0;for(;k<SIZE;k++){if(gfds[k]==-1){gfds[k]=connfd;break;}}if(k>=SIZE){close(connfd);gfds[k]=-1;}}}int j=1;for(;j<SIZE;j++){if(FD_ISSET(gfds[j],&rfds)){char buf[SIZE];ssize_t s=read(gfds[j],buf,sizeof(buf));if(s<0){perror("read");continue;}else if(s==0){printf("client is quit!\n");close(gfds[j]);gfds[j]=-1;}else{buf[s]=0;printf("client# %s\n",buf);}}}break;}}return 0;
}

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/655.html

上一篇:GF1信息整理
下一篇:I/O复用-select

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息