1、IO模型
所谓IO模型其实就是研究的就是读写一个文件描述符的几种不同的方式,大致可以分为:
1)阻塞IO
读:
如果有数据(即使小于你要读取的字节数),直接读取数据
如果没有数据,则阻塞等待,直到有数据或者出错
写:
如果有空间(即使小于你要写入的字节数),直接写入数据
如果没有空间,则阻塞等待,直到有空间或者出错
2)非阻塞IO
读:
如果有数据,则立即读取数据
如果没有数据,则立即返回,不等待
写:
如果有空间,则立即写入数据
如果没有空间,则立即返回,不等待
3)IO多路复用
允许同时对多个IO进行控制,同时监听多个文件描述符是否就绪(是否可读/可写/出错)
允许一个进程同时等待多个文件描述符,直到某个描述符就绪(可读/可写/出错),再进行IO操作。
例子:
一个服务器,可能同时会有多个客户端与之发生通信
可以在服务器使用多线程技术 与 每一个客户端进行通信
缺点:并不知道何时与客户端进行通信
==》IO多路复用
通过一定手段 监听每个客户端的读写状态 以及 服务器的连接状态
2、IO多路复用
select / poll / epoll 都是IO多路复用的机制,都是用来监视多个文件描述符,等待IO事件的发生。
1)select
具体实现为:
(1)将 服务器 和 连接到服务器的客户端 的套接字描述符 都添加到一个 fd_set 集合中
(2)将 fd_set 集合中 所有的文件描述符 都拷贝到内核中
(3)内核会自动注册一个函数 pollwait()函数 用来轮询所有的文件描述符的读写状态
(4)一旦 有一个或者多个文件描述的状态发生了改变,select函数就会返回
(5)将 fd_set 集合 从内核中 拷贝到 用户空间 ,此时 状态发生改变的文件描述符就已经被 标记了
注意:用类型 fd_set 来表示一个文件描述符集合
可能要监听:
是否可读
是否可写
是否出错
需要用到3个fd_set集合 来表示要监听可读的文件描述符的集合
可写的文件描述符集合
出错的文件描述符集合
NAME
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
SYNOPSIS
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
功能:IO多路复用
参数:
nfds:所有要监听的文件描述符的最大值 +1
readfds:可读的文件描述符集合
select函数返回时,把整个集合返回,其中 可读的已经被标记了
writefds:可写的文件描述符集合
select函数返回时,把整个集合返回,其中 可写的已经被标记了
exceptfds:出错的文件描述符集合
select函数返回时,把整个集合返回,其中 出错的已经被标记了
timeout:超时时间
struct timeval
{
long tv_sec; /* 秒 seconds */
long tv_usec; /* 微秒 microseconds */
};
注意:
在调用之前,调用者传入的参数 是指“超时时间”
在函数返回之后,这个timeout就表示“剩余时间”
返回值:
>0 表示已经就绪的文件描述符的个数
==0 表示超时
==-1 出错,同时errno被设置
void FD_CLR(int fd, fd_set *set);
功能:把fd 从set集合中移除
int FD_ISSET(int fd, fd_set *set);
功能:判断fd是否在set集合中 (判断是否就绪)
返回值:1 存在 0 不存在
void FD_SET(int fd, fd_set *set);
功能:把fd加入到set集合中
void FD_ZERO(fd_set *set);
功能:把set集合清空
例子: 1)select延时效果 struct timeval timeout; timeout.tv_sec = 10; timeout.tv_usec = 0; select( 100, NULL, NULL, NULL, &timeout ); 2)IO多路复用 利用select 实现基于TCP的一对多的通信 select_server.c / select_client.c int main( int argc, char * argv[] ) { //1.创建套接字 socket int server_fd = socket( AF_INET, SOCK_STREAM, 0 ); if( server_fd == -1 ) { perror("socket server error "); return -1; } printf("server_fd = %d\n", server_fd ); //2.绑定服务器的ip和端口 bind (服务器的ip和端口) struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; //协议族 server_addr.sin_port = htons( atoi(argv[2]) ); //端口号 ( 网络字节序 ) inet_aton( argv[1] , &server_addr.sin_addr ); //ip地址 //设置端口号重用 int n = 1; setsockopt( server_fd, SOL_SOCKET, SO_REUSEPORT, &n, sizeof(n) ); int re = bind( server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr) ); if( re == -1 ) { perror("bind server error "); close( server_fd ); return -1; } printf("bind success! \n"); //3.监听 listen re = listen( server_fd, 5 ); if( re == -1 ) { perror("listen server error "); close( server_fd ); return -1; } printf("listen success! \n"); //========== select() IO多路复用 ======================== int client_fd[256] = {0}; //存储已经连接到服务器的客户端的套接字描述符 int num = 0; //记录客户端的个数 int i; int max_fd = server_fd; //文件描述符中的最大值,初始时 最大值一定是服务器的套接字描述符 //超时时间 struct timeval timeout; //可读的集合 fd_set read_fds; while( 1 ) { timeout.tv_sec = 2; timeout.tv_usec = 0; //将 read_fds 清空 FD_ZERO( &read_fds ); //将 服务器的套接字 加入到 read_fds 集合中 FD_SET( server_fd, &read_fds ); //将 连接到服务器的客户端 的套接字 加入到 read_fds 集合中 for( i=0; i<num; i++ ) { FD_SET( client_fd[i], &read_fds ); } //select 监听 re = select( max_fd+1, &read_fds, NULL, NULL, &timeout ); if( re > 0 ) { //判断read_fds 集合中 是否有文件描述符就绪 //服务器就绪 : 新的客户端来连接 //客户端就绪 : 表示客户端上有数据可读 if( FD_ISSET( server_fd, &read_fds ) ) //服务器就绪 --> 去接受客户端的连接请求 { //接受连接请求 accept struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); int new_fd = accept( server_fd, (struct sockaddr *)&client_addr, &len ); if( new_fd == -1 ) { perror("accept error "); break; } printf("accept success \n"); printf("client ip = %s\n", inet_ntoa(client_addr.sin_addr) ); //把新的客户端的套接字保存 client_fd[num] = new_fd; num++; if( new_fd > max_fd ) //更新文件描述符中的最大值 { max_fd = new_fd; } } //客户端就绪 ---> 读取数据 for( i=0; i<num; i++ ) { //判断客户端的套接字描述符是否可读 if( FD_ISSET( client_fd[i], &read_fds ) ) { //读取数据 //在实际工作在,对于客户端的处理,一般采用线程来处理 char buf[128] = {0}; int r = recv( client_fd[i], buf, sizeof(buf), 0 ); if( r > 0 ) { printf("recv : %s\n", buf ); } if( buf[0] == '#' ) { close( server_fd ); return -1; } } } } else if( re == 0 ) { //超时 printf( "超时 \n" ); } else { //出错 perror( "select error " ); break; } } //关闭套接字 close( server_fd ); }
2)poll
poll的作用和实现原理 和 select 是类似的,select监听的文件描述符 在内核中监听的时候存储在数组中而poll存储在链表上。并且poll的功能和select类似,select “监听”多个文件描述符是否就绪 ,而 poll 是用一个结构体 struct pollfd{} 来描述监听请求 。
struct pollfd
{
int fd; /* 指定要监听的文件描述符 file descriptor */
short events; /* 监听事件 requested events */
在Linux内核中 事件是用 bit-fields 位域实现的
POLLIN 可读事件
POLLOUT 可写事件
POLLERR 出错事件
...
例子:
可读可写事件 POLLIN | POLLOUT
short revents; /* 返回已经就绪的事件 returned events */
在调用poll函数后,revents 字段会被填充相应的事件
例子:
//判断是否已经可读
struct pollfd pfd;
if( pfd.revents & POLLIN )
{
//可读
}
};
NAME
poll - wait for some event on a file descriptor
SYNOPSIS
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
参数:
fds:结构体指针,指向要监听的文件描述符数组
nfds:结构体数组的元素个数
timeout:超时时间 单位:ms
返回值:
>0 表示已经就绪的文件描述符的个数
==0 表示超时
==-1 出错,同时errno被设置
练习: 利用poll 来实现基于TCP的一对多的通信 poll_server.c / poll_client.c #define MAX_NUM 256 //1.创建套接字 //2.设置服务器的ip和端口 //3.绑定 //4.监听 //================= poll() IO多路复用 =================== //定义一个 struct pollfd 结构体数组,来保存要监听的文件描述符 struct pollfd fds[MAX_NUM]; int client_fd[MAX_NUM] = {0}; //存储已经连接到服务器的客户端的套接字描述符 int num = 0; //记录客户端的个数 int i; struct sockaddr_in client_addr[MAX_NUM]; while( 1 ) { //把服务器的文件描述符 加入到 fds结构体数组中 fds[0] fds[0].fd = server_fd; fds[0].events = POLLIN; //要监听的事件:可读 fds[0].revents = 0; //把客户端的文件描述符 加入到 fds结构体数组中 for( i=0; i<num; i++ ) { fds[i+1].fd = client_fd[i]; fds[i+1].events = POLLIN; //要监听的事件:可读 fds[i+1].revents = 0; } //poll() 监听 re = poll( fds, MAX_NUM, 2000 ); if( re > 0 ) { //服务器就绪 ---> 接受客户端的连接请求 if( fds[0].revents & POLLIN ) { socklen_t len = sizeof(client_addr[num]); //接受连接请求 accept int new_fd = accept( server_fd, (struct sockaddr *)&client_addr[num], &len ); if( new_fd == -1 ) { perror("accept error "); break; } printf("accept success \n"); printf("client ip = %s\n", inet_ntoa(client_addr[num].sin_addr) ); //把新的客户端的套接字保存 client_fd[num] = new_fd; num++; } else //客户端就绪 ---> 读取数据 { for( i=0; i<num; i++ ) { if( fds[i+1].revents & POLLIN ) { //读取数据 char buf[128] = {0}; int r = recv( client_fd[i], buf, sizeof(buf), 0 ); if( r > 0 ) { printf("%s : %s\n", inet_ntoa(client_addr[i].sin_addr), buf ); } } } } } else if( re == 0 ) { //超时 printf( "超时 \n" ); } else { //出错 perror( "poll error " ); break; } }
3)epoll --- epoll_create / epoll_ctl / epoll_wait
(3.1) epoll_create()
NAME
epoll_create - open an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_create(int size);
功能: 创建一个epoll实例 用来监听其他的文件描述符的状态
参数:
size:该参数已经被忽略了,现在只需要给一个大于0数即可
返回值:
成功,返回epoll实例对象,就是一个文件描述符
失败,返回-1,同时errno被设置
创建一个epoll实例 用来监听其他的文件描述符的状态 ,则要被监听的文件描述符 就必须加入到epoll实例中 。
(3.2) epoll_ctl()
NAME
epoll_ctl - control interface for an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能: 控制监听的文件描述符的状态
参数:
epfd:指定要操作的epoll实例
op: 选项
EPOLL_CTL_ADD 添加 把一个要监听的文件描述符添加到epoll实例中
EPOLL_CTL_MOD 修改 修改一个已经在epoll实例中的文件描述符的监听事件
EPOLL_CTL_DEL 删除 从epoll实例中删除一个文件描述符
fd:指定要操作的文件描述符
event:指定要监听的事件 结构体
struct epoll_event
{
uint32_t events; /* 要监听的事件 Epoll events */
EPOLLIN 可读事件
EPOLLOUT 可写事件
EPOLLERR 出错事件
EPOLLET 边缘触发模式
...
LT 级别触发 Level-triggered
只要有数据,就会不停地往上报告事件
默认:LT
ET 边缘触发 Edge-triggered
只要数据的变化(数量),才报告事件
epoll_data_t data; /* 用来存储用户的数据 User data variable */
};
typedef union epoll_data
{
void *ptr; //用户的指针数据
int fd; //文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
返回值:
成功,返回0
失败,返回-1,同时errno被设置
(3.3) epoll_wait()
NAME
epoll_wait - wait for an I/O event on an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能: 用来等待监听事件的发生
参数:
epfd:指定要操作的epoll实例
events: 结构体事件数组,用来存储已经就绪的事件的信息
maxevents:结构体数组中,最大可以保存多少个事件
timeout:超时时间 单位:ms
返回值:
>0 表示已经就绪的文件描述符的个数
==0 表示超时
==-1 出错,同时errno被设置
利用epoll 实现UDP的简单通信 epoll_udp_server.c / epoll_udp_client.c //1.创建套接字 UDP //2.设置服务器的ip和端口 //3.绑定 //================= epoll() IO多路复用 =================== //(1)创建一个epoll实例,用来监听其他的文件描述符的状态 int epfd = epoll_create( 10 ); if( epfd == -1 ) { perror("epoll_create error "); close( server_fd ); return -1; } printf("epoll_create success! \n"); //(2)把要监听的文件描述符 加入到epoll实例中 struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; //要监听的事件:可读 | 边缘触发模式 ev.data.fd = server_fd; re = epoll_ctl( epfd, EPOLL_CTL_ADD, server_fd, &ev ); if( re == -1 ) { perror("epoll_ctl error "); close( server_fd ); return -1; } printf("epoll_ctl success! \n"); struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); while( 1 ) { struct epoll_event ee[10]; //保存已经就绪的事件信息的数组 int i; //(3)等待监听事件的发生 int num = epoll_wait( epfd, ee, 10, 2000 ); if( num > 0 ) { //判断是否已经可读 for( i=0; i<num; i++ ) { if( ee[i].events & EPOLLIN ) { //接收数据 char buf[128] = {0}; re = recvfrom( server_fd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len ); if( re > 0 ) { printf("recv : %s\n", buf ); } else { perror("recvfrom error "); break; } } } } else if( num == 0 ) { //超时 printf( "超时 \n" ); } else { //出错 perror( "epoll_wait error " ); break; } }
3、 select、poll、epoll 的区别
select、poll、epoll 都是IO多路复用技术,都是用来监听多个文件描述符的状态变化(是否可读/可写/出错)的
他们的区别:
1) select
时间复杂度 O(n) 无差别轮询
缺点:
监听文件描述符的数量上有限制,基于数组来存储的
轮询,效率低
文件描述符需要 维护在一个比较大的数组中,在用户空间和内核空间中传递时 开销比较大
2) poll
时间复杂度 O(n) 轮询
缺点:
轮询,效率低
文件描述符需要 维护在一个比较大的空间中,在用户空间和内核空间中传递时 开销比较大
优点:
没有最大连接数量的限制,基于链表来存储的
3) epoll
时间复杂度 O(1)
优点:
没有最大连接数量的限制
效率提升,不是轮询方式,不会随着fd数量的增加 而效率下降
内存拷贝开销比较小,利用内存映射
总结:
综上所述,在选择select、poll、epoll时,
要根据具体的应用场景、系统资源、连接数等因素 以及 三者自身的特点 进行选择。
(1)表面上看epoll的性能好,但是在连接数量比较少且都比较活跃的情况下
select和poll的性能 可能比epoll好,毕竟epoll的通知机制需要很多函数的回调
(2)select低效是因为每次都需要去轮询,但是低效也是相对的,视情况而定,
也可以通过良好的设计改善