按顺序写我学到的知识:
同步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 {
//其他的处理
}
}
}