4.IO复用

按顺序写我学到的知识:

同步IO/异步IO:
    1.描述的是C端
    2.同步IO是指,c端发出一个函数调用后,在没得到结果前,该调用就不返回,一直等待.
    3.异步IO是指,c端发出一个函数调用后,不等待结果,立即返回.S端在生成结果后,会利用通知或函数等方式通知C.

阻塞IO/非阻塞IO:
    1.描述的是S端
    2.阻塞IO是指,S端在收到一个请求后,在结果生成前,例如一个完整的UDP数据包到达前,当前线程被挂起,CPU不分配时间片.
    3.非阻塞IO是指,S端在收到一个请求后,在结果生成前,立即返回.

以下是阻塞IO的原理图:


当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据.对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来.而在用户进程这边,整个进程会被阻塞.当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来.
所以,blocking IO的特点就是在IO执行的两个阶段都被block了.

以下是非阻塞IO的原理图:


从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error.从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果.用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作.一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回.
所以,用户进程其实是需要不断的主动询问kernel数据好了没有.

以下是IO复用的原理图:


IO复用是指一个线程同时监听多个描述符,只要一个描述符就绪,就会通知用户程序对该描述符进行读写操作.
从图中可以看出,IO复用是分两个阶段的.
第一阶段,用户程序将全部描述符构造成一张列表,然后将该列表交接给系统程序,由系统程序来进行批量监听,一旦有某一描述符就绪,系统程序会通知用户程序.
第二阶段,用户程序知道某一描述符已就绪后,再对就绪描述符进行读写操作.

上面都是概念级别的,接下来写一写IO复用具体的代码实现,主要分select和epoll,因为这俩是最常用的.

select篇:

#include <sys/select.h>//select; 
#include <sys/time.h>//struct timeval;
 int select(//返回-1表示出错,返回0表示超时仍无描述符就绪,返回>0的数表示已就绪的描述符个数
      int nfds,//最大的文件描述符值+1
      fd_set *readset,//可读描述符集合
      fd_set *writeset,//可写描述符集合
      fd_set* exceptset,//异常描述符集合
      struct timeval *timeout//select最大监听时长.如果为NULL,阻塞等待.如果为0,立即返回
   );
FD_ZERO(fd_set *)//清空描述符集合
FD_SET(int, fd_set *)//向描述符集合添加一个描述符
FD_CLR(int, fd_set *)//从描述符集合删除一个描述符
FD_ISSET(int, fd_set *)//检测指定描述符是否在描述符集合中
//示例代码
fd_set rdset;
listNode *p=head;
int max_fd;
int ret=0;
struct timeval timeout={3,0};//设置超时时间
while(1)
{ 
    FD_ZERO(&rdset);//每次调用select之前都要清空fdset,并重新加入所有描述符
    p=head; 
    while(p)//按顺序,将所有描述符都加入列表
    {
        FD_SET(p->fd,&rdset);
        if(p->fd>max_fd)
            max_fd=p->fd;
        p=p->next;
    }
    ret=select(max_fd+1,&rdset,NULL,NULL,&timeout);
    if(ret<0)//返回-1,则出错
        error;
    if(ret==0)//timeout超时,仍没有任何描述符就绪
        continue;
    for(int i=0;i<=max_fd;++i)//依次检测每个描述符,若就绪,则调用相应的回调函数进行处理
    {
        if(FD_ISSET(i,&rdset)
        {  
            callback(i);
        }
    }
}

select有三个缺点:
    1.select是基于整型数组的,每一个整型数字有32bit,每一位可表示一个fd.例如如果最大描述符数为1024,则需要1024/32=32,即fd_set为int a[32]
    2.调用select前,都需先FD_ZERO清空描述符,再用FD_SET重新加入描述符.
    3.调用select时,会将fd_set从用户态copy到内核态,如有一描述符就绪,又会从内核态copy到用户态.
    4.用户程序在知道有描述符就绪时,会遍历全部描述符,来查找哪一个是就绪的.

epoll篇:
epoll是对select的改进:
    1.epoll是基于红黑树的,每加入一个监听的描述符,即是向红黑树中加入一个节点,因此没有最大描述符的限制.
    2.当要新加入或删除某个描述符时,只是在红黑树中加入或删除一个节点即可;不再需要每次都重新加入描述符.
    3.epoll采用了共享内存,因此用户空间和内核传递数据不再需要copy,使得效率大大提高.
    4.epoll会生成一个双向链表,每当有描述符就绪时,就将该描述符加入到双向链表中,用户程序不再需要轮循全部描述符.

#include <sys/epoll.h>
int epoll_create(int size);//自linux2.6.8之后,size参数被忽略
int epoll_ctl(//注册事件
	int epfd,//epoll_create()的返回值
	int op,//表示动作,EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;
	int fd,//需要监听的fd
	struct epoll_event *event//告诉内核需要监听什么事件
);
	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设为ET模式,这是相对于LT来说的
		EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
	//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
	typedef union epoll_data {
	    void *ptr;
	    int fd;
	    __uint32_t u32;
	    __uint64_t u64;
	} epoll_data_t;
int epoll_wait(//获得在epoll监听的描述符中已就绪的事件
	int epfd,//epoll_create()的返回值
	struct epoll_event * events,//分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中
	int maxevents,//告之内核这个events有多大
	int timeout//超时时间
);

//示例代码

int epfd;
struct epoll_event event;  
struct epoll_event *events;  
epfd = epoll_create(0);  
event.data.fd = listenfd;//listenfd为服务器端监听fd
event.events = EPOLLIN | EPOLLET;//读入,边缘触发方式  
s = epoll_ctl (epfd, EPOLL_CTL_ADD, listenfd, &event);  
events = calloc(MAXEVENTS, sizeof event);  
while(1) 
{
   nfds = epoll_wait(epfd,events,20,500);  
   for(i=0;i<nfds;++i)
   {
       if(events[i].data.fd==listenfd){//有新的连接 
           connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);//accept这个连接  
           ev.data.fd=connfd;  
           ev.events=EPOLLIN|EPOLLET;  
           epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);//将新的fd添加到epoll的监听队列中  
       } else if( events[i].events&EPOLLIN ) {//接收到数据,读socket  
           n = read(sockfd, line, MAXLINE)) < 0//读  
           ev.data.ptr = md;//md为自定义类型,添加数据  
           ev.events=EPOLLOUT|EPOLLET;  
           epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓  
       } else if(events[i].events&EPOLLOUT){ //有数据待发送,写socket 
           struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;//取数据  
           sockfd = md->fd;  
           send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );//发送数据  
           ev.data.fd=sockfd;  
           ev.events=EPOLLIN|EPOLLET;  
           epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时接收数据  
       } else  {  
           //其他的处理  
       }  
   }  
}

 

转载于:https://my.oschina.net/u/3474060/blog/903911

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值