目录
- 13、SOCKET-IO复用技术
-
- 1、五种I/O模型
- 2、阻塞I/O模型
- 3、非阻塞I/O模型
- 4、I/O复用模型
- 5、信号驱动I/O模型
- 6、异步I/O模型
- 7、I/O复用
- 8、shutdown函数
- 9、select函数
- 10、poll函数
- 11、epoll函数
-
- 11-1、epoll_create
- 11-2、epoll_ctl
- 11-3、epoll_wait
- 11-4、Epoll工作模式
- 11-5、epoll示例
- 12、总结
13、SOCKET-IO复用技术
1、五种I/O模型
- 阻塞I/O
- 非阻塞I/O
- I/O复用(select和poll)
- 信号驱动I/O
- 异步I/O
2、阻塞I/O模型
- 最流行的I/O模型是阻塞I/O模型,缺省时,所有的套接口都是阻塞的
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL1cGVNdXVq1UeRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL5YzN3EjMwIjMyITNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
3、非阻塞I/O模型
- 当我们把一个套接口设置为非阻塞方式时,即通知内核:当请求的I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误
应用程序连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,一般只在专门提供某种功能的系统中才会用到
4、I/O复用模型
- 有了I/O复用,我们就可以调用select或poll,在这两个系统调用的某一个上阻塞,而不是真正阻塞于真正的I/O系统调用
5、信号驱动I/O模型
- 我们也可以用信号,让内核在描述字准备好时用信号SIGIO通知我们,我们将此方法称为信号驱动I/O
6、异步I/O模型
- 异步I/O是Posix.1的1993版本中的新内容,我们让内核启动操作,并在整个操作完成后通知我们
7、I/O复用
- 如果一个或多个I/O条件满足(例如:输入已准备好被读,或者描述字可以承接更多输出的时候)我们就能够被通知到,这样的能力被称为I/O复用,是由函数select和poll支持的
I/O复用网络应用场合
- 当客户处理多个描述字
- 一个客户同时处理多个套接口
- 如果一个tcp服务器既要处理监听套接口,又要处理连接套接口
- 如果一个服务器既要处理TCP,又要处理UDP
8、shutdown函数
- 功能:关闭套接字两端或一端的socket
#include <sys/socket.h>
int shutdown(int sockfd,int howto);
- 参数:
- SHUT_RD:关闭连接的读这一半,不再接收套接口中的数据且现留在套接口接收缓冲区中的数据都作废
- SHUT_WR:关闭连接的写这一半,在TCP场合下,这称为为半关闭。当前留在套接口发送缓冲区中的数据都被发送,后跟正常的tcp连接终止序列
- SHUT_RDWR 连接的读这一半和写这一半都关闭
- 返回值:成功:0,失败:错误代码
shutdown与close的区别
- 终止网络连接的正常方法是调用close,但close有两个限制可由函数shutdown来避免。
- close将描述字的访问计数减1,仅在此计数为0时才关闭套接口;用shutdown我们可以激发TCP的正常连接终止序列,而不管访问计数
- Close终止了数据传送的两个方向:读和写。由于TCP连接是全双工的,有很多时候我们要通知另一端我们已完成了数据发送,即使一端仍有许多数据要发送也是如此。
9、select函数
- 这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程
- 功能:提供了即时响应多个套接的读写事件
#include <sys/select.h>
#include <sys/socket.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *except,const struct timeval *timeout);
struct timeval(
long tv_sec; //秒
long tv_usec;//微秒
);
- 参数
- maxfdp1:等待最大套接字值加1,(等待套接字的数量)
- readset:要检查读事件的容器
- writeset:要检查写事件的容器
- timeout:超时时间
- 返回值:
- 返回触发套件接字的个数
timeout参数
- 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将timeout设置为空指针
- 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数
- 根本不等待:检查描述字后立即返回,这称为轮询。定时器的值必须为0
fd_set参数
- select使用描述字集,它一般是一个整数数组,每个数中的每一位对应一个描述字。
- 使用fd_set数据类型来表示这个描述字集,我们不用去关心具体的实现细节。
操作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);//描述字是否在该集合中
select函数返回值
- 当返回时,结果指示哪些描述字已准备好。
- 返回时,我们用宏FD_ISSET来测试结构fd_set中的描述字。描述字集中任何与没有准备好的描述字相对应的位返回时清成0。为此,每次调用select时,我们都得将所有描述字集中关心的都置为1
- 如果在任何描述字准备好之前定时器时间到,则返回0
- 返回-1表示有错。
select缺点
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
select示例
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <error.h>
#include <termios.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define ERR_EXIT(M)\
do\
{\
perror(M);\
exit(1);\
}while(0);
int main(int argc,char *argv[])
{
int sockfd = socket(PF_INET,SOCK_STREAM,0);
if(sockfd == -1)
ERR_EXIT("socket");
int on = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in sockaddr;
bzero(&sockaddr,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
sockaddr.sin_port = htons(5566);
if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
ERR_EXIT("bind");
if(listen(sockfd,5) == -1)
ERR_EXIT("listen");
int maxfd = sockfd;
int client[FD_SETSIZE];
fd_set rset;
fd_set allset;
FD_ZERO(&rset);
FD_ZERO(&allset);
FD_SET(sockfd,&allset);
int nready;
int conn;
int i;
for(i = 0;i < FD_SETSIZE;i++)
client[i] = -1;
while(1){
rset = allset;
nready = select(maxfd + 1,&rset,NULL,NULL,NULL);
if(nready == -1)
ERR_EXIT("select");
//新客户端
if(FD_ISSET(sockfd,&rset)){
struct sockaddr_in peer_addr;
memset(&peer_addr,0,sizeof(peer_addr));
socklen_t socklen = sizeof(peer_addr);
conn = accept(sockfd,(struct sockaddr *)&peer_addr,&socklen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0;i < FD_SETSIZE;i++){
if(client[i] < 0){
client[i] = conn;
break;
}
}
FD_SET(conn,&allset);
if(conn > maxfd)
maxfd = conn;
if(--nready <= 0)//1件事情
continue;
}
//已连接FD产生可读事件
for( i = 0;i < FD_SETSIZE;i++){
if(FD_ISSET(client[i],&rset)){
conn = client[i];
char buf[1024]={0};
int nread = read(conn,buf,sizeof(buf));
if(nread == 0){//对方关闭
printf("client is close\n");
FD_CLR(conn,&allset);
client[i] = -1;
close(conn);
}
fputs(buf,stdout);
write(conn,buf,nread);
memset(buf,0,1024);
if(--nready <= 0)//1件事情
break;
}
}
}
return 0;
}
10、poll函数
- poll函数和select类似,但它是用文件描述符而不是条件的类型来组织信息的.
- 也就是说,一个文件描述符的可能事件都存储在struct pollfd中.与之相反,select用事件的类型来组织信息,而且读,写和错误情况都有独立的描述符掩码.
- 参数:
- fdarray是一个pollfd的结构体数组用来表示文件描述符的监视信息
- nfds参数给出了要监视的描述符数目
- timeout参数是一个用豪秒表示的时间,是poll在返回前没有接收事件是应等待的时间,如果timeout的值为-1,poll就永远不会超时.如果整数值为32个比特,那么最大超时周期约为30分钟
- 返回值:准备好描述字的个数,0-超时,1-出错
pollfd结构体
- fd是文件描述符值
- event和revents是通过代表各种事件的标准符进行逻辑或运算构建而成的
struct pollfd
{
int fd;
short events; //感兴趣的事件
short revents; //fd上触发的事情
}
poll函数事件标志
事件标志符 | 含义 |
---|---|
POLLIN | 无阻塞地读除了具有高优先级的数据之外的数据 |
POLLRONORM | 无阻塞地读常规数据 |
POLLRDBAND | 无阻塞地读具有优先级的数据 |
POLLOUT | 无阻塞的写常规数据 |
poll优缺点
- 缺点:
- 每次调用poll,都需要把fdarray数组从用户态拷贝到内核态,这个开销在连接数fd很多时会很大。
- 同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- 优点:
- 支持的文件描述符数量没有限制。
与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。
poll示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define PORT 5566
#define MAXSIZE 1024
#define FDSIZE 1000
#define ERR_EXIT(M)\
do\
{\
perror(M);\
exit(1);\
}while(0);
int main()
{
int sockfd = socket(PF_INET,SOCK_STREAM,0);
if(sockfd == -1)
ERR_EXIT("socket");
int optval = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in sockaddr;
bzero(&sockaddr,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
//sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
sockaddr.sin_port = htons(PORT);
if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
ERR_EXIT("bind");
if(listen(sockfd,5) == -1)
ERR_EXIT("listen");
/*开始poll流程*/
int connfd;
struct sockaddr_in clientaddr;
memset(&clientaddr,0,sizeof(clientaddr));
socklen_t clientaddrlen;
struct pollfd clientfds[FDSIZE];//监听1000个
int maxi;
int i,n;
/*初始化客户连接描述符*/
for(i = 0;i < FDSIZE;i++)
clientfds[i].fd = -1;
//添加监听描述符
clientfds[0].fd = sockfd;
clientfds[0].events = POLLIN;
maxi = 0;
//循环处理
while(1){
//获取可用描述符的个数
int nready = poll(clientfds,maxi+1,-1);//永远不会超时
if(nready == -1){
ERR_EXIT("poll");
}else if(nready == 0){//超时
printf("select timeout!\n");
continue;
}
//测试监听描述符是否准备好
if(clientfds[0].revents & POLLIN){
clientaddrlen = sizeof(clientaddr);
//接受新的连接
connfd = accept(sockfd,(struct sockaddr *)&clientaddr,&clientaddrlen);
if(connfd == -1){
ERR_EXIT("accept");
}
printf("accept a new client:%s:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
//将新的连接描述符添加到数组中
for(i = 1;i < FDSIZE;i++){
if(clientfds[i].fd < 0){
clientfds[i].fd = connfd;
clientfds[i].events = POLLIN;
break;
}
}
if(i == FDSIZE){
fprintf(stderr,"Too many clients.\n");
exit(1);
}
maxi = (i > maxi ? i : maxi);
}
//检查客户端连接是否有读事件
for(int i = 1;i <= maxi;i++){
if(clientfds[i].fd < 0)
continue;
if(clientfds[i].revents & POLLIN){
char buf[MAXSIZE] = {0};
n = read(clientfds[i].fd,buf,sizeof(buf));
if(n == 0){
close(clientfds[i].fd);
clientfds[i].fd = -1;
printf("client is closed.\n");
continue;
}
printf("read msg is:%s\n",buf);
write(clientfds[i].fd,buf,n);
}
}
}
return 0;
}
11、epoll函数
- 相对于select和poll来说,epoll更加灵活,没有描述符限制。
- epoll使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
11-1、epoll_create
- 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。从linux2.6.8之后size参数被忽略。
- 需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
查看最大数:
cat /proc/sys/fs/file-max
11-2、epoll_ctl
- 注册要监听的事件类型。select是在监听事件时告诉内核要监听什么类型的事件。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data{
void *ptr;
int fd; /*文件描述符*/
uint32_t u32;
uint64_t u64;
}epoll_data_t;
- 参数
- epfd:epoll_create()的返回值
- op:表示动作,用三个宏来表示:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd
- fd:需要监听的fd
- event是告诉内核需要监听什么事件
struct epoll_event结构
参数 | 描述 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) |
EPOLLOUT | 表示对应的文件描述符可以写 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) |
EPOLLERR | 表示对应的文件描述符发生错误 |
EPOLLHUP | 表示对应的文件描述符被挂断 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的 |
EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 |
11-3、epoll_wait
- 等待事件的产生,类似于select()调用。
int epoll_wait(int epfd,
struct epoll_event * events,
int maxevents,
int timeout);
- 参数:
- events:用来从内核得到事件的集合
- maxevents:告之内核这个events有多大,这个值不能大于创建epoll_create()时的size
- timeout:超时时间(毫秒,0会立即返回,-1不会超时)。
- 返回值:该函数返回需要处理的事件数目,如返回0表示已超时。
11-4、Epoll工作模式
- epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,区别如下:
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
11-5、epoll示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#define EPOLLSIZE 1024
#define PORT 5566
#define ERR_EXIT(M) do{perror(M);exit(1);}while(0)
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
ERR_EXIT("socket");
int opt = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in sockaddr;
bzero(&sockaddr,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
sockaddr.sin_port = htons(PORT);
if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
ERR_EXIT("bind");
if(listen(sockfd,EPOLLSIZE) == -1)
ERR_EXIT("listen");
int epfd = epoll_create(EPOLLSIZE);
if(epfd == -1)
ERR_EXIT("epoll_create");
struct epoll_event events[EPOLLSIZE],ep_event;
bzero(&ep_event,sizeof(ep_event));
ep_event.events = EPOLLIN;
ep_event.data.fd = sockfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ep_event) == -1)
ERR_EXIT("epoll_ctl");
int nready = 0;
for(;;){
nready = epoll_wait(epfd,events,EPOLLSIZE,-1);
if(nready == -1)
ERR_EXIT("epoll_wait");
for(int i = 0;i < nready;i++){
if(events[i].events == EPOLLIN){
if(events[i].data.fd == sockfd){
struct sockaddr_in caddr;
bzero(&caddr,sizeof(caddr));
socklen_t addrlen = sizeof(caddr);
int cfd = accept(sockfd,(struct sockaddr *)&caddr,&addrlen);
if(cfd == -1)
ERR_EXIT("accept");
ep_event.events = EPOLLIN;
ep_event.data.fd = cfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ep_event) == -1)
ERR_EXIT("epoll_ctl_add");
printf("connect: ip:%s,port:%d\n",inet_ntoa(caddr.sin_addr),caddr.sin_port);
}else{
int cfd = events[i].data.fd;
char buf[1024]={0};
int nread = read(cfd,buf,sizeof(buf));
if(nread == 0){
if(epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ep_event) == -1)
ERR_EXIT("epoll_ctl_del");
}else if(nread == -1){
ERR_EXIT("read");
}else{
printf("client:%s",buf);
if(write(cfd,buf,sizeof(buf)) == -1)
ERR_EXIT("write");
}
}
}
}
}
return 0;
}
12、总结
- select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和合poll在“醒着”的时候要遍历整个fd集,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
- select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。