在没有多路复用IO之前,对于多路IO请求,一般只有阻塞与非阻塞IO两种方式
阻塞IO :需要结合多进程/多线程,每个进程/线程处理一路IO
缺点:客户端越多,需要创建的进程/线程越多,相对占用内存资源较多
非阻塞IO: 单进程可以处理,但是需要不断检测客户端是否发出IO请求,需要不断占用cpu,消耗 cpu 资源
多路复用IO简介
本质上就是通过复用一个进程来处理多个IO请求 本质上就是通过复用 个进程来处理多个IO请求 基本思想:由内核来监控多个文件描述符是否可以进行I/O操作,如果有就绪的文件描述符,将结果 告知给用户进程,则用户进程在进行相应的I/O操作
目前在Linux系统有三种多路复用I/O的方案
1. select方案
2. poll方案
3. epoll方案
IO—select
设计思想
通过单进程创建一个文件描述符集合,将需要监控的文件描述符添加到这个集合中
由内核负责监控文件描述符是否可以进行读写,一旦可以读写,则通知相应的进程进行相应的I/O操作、
select多路复用I/O在实现时主要是以调用 select 函数来实现
函数使用
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
函数功能
监控一组文件描述符,阻塞当前进程,由内核检测相应的文件描述符是否就绪,一旦有文件描述符就绪, 将就绪的文件描述符拷贝给进程,唤醒进程处理
函数参数
nfds:最大文件描述符加1
readfds:读文件描述符集合的指针
writefds:写文件描述符集合的指针
exceptfds:其他文件描述符集合的指针
timeout:超时时间结构体变量的指针
函数返回值
成功:返回已经就绪的文件描述符的个数。
如果设置timeout,超时就会返回0
失败:-1,并设置errno
超时时间的说明
如果timeout之后,文件描述符集合中没有任何就绪的文件描述符,select函数就会返回0 超时之后,timeout会被select函数修改,表示超时时间已经使用完。
如果想继续使用超时时间,需要备份之前的struct timeval 超时之后,表示没有就绪的文件描述符,此时文件描述符集合被赋值为空
因此,需要将之前的文件描述符集合进行备份。
例子
使用 select 监听有名管道,当有名管道有数据时,读取数据并打印
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH "/home/linux/name_pipe"
#define FD 0
int main(){
int fd = open(PATH,O_RDONLY);
printf("fd=%d\n",fd);
//读文件描述符集合
fd_set readfds;
//清空读文件描述符集合
FD_ZERO(&readfds);
//初始化读文件标识符
FD_SET(fd,&readfds);
FD_SET(FD,&readfds);
//备份读文件标识符
fd_set readfds_bak ;
//设置超时时间
struct timeval timeout= {.tv_sec=3,.tv_usec=0};
//备份时间
struct timeval timeout_bak;
int ret = 0;
//循环遍历
while(1){
readfds_bak = readfds;
timeout_bak = timeout;
ret = select(fd+1,&readfds_bak,NULL,NULL,&timeout_bak);
if(ret == -1){
perror("select\n");
exit(EXIT_FAILURE);
}else if(ret == 0){//超时
printf("timeout\n");
}else if(ret > 0){//对应的文件标识符就绪
for(int i = 0;i<ret;i++){
if(FD_ISSET(0,&readfds_bak)){
char buf[128]={0};
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("buf=%s\n",buf);
}
if(FD_ISSET(fd,&readfds_bak)){
char buff[128]={0};
memset(buff,0,sizeof(buff));
ssize_t bytes = read(fd,buff,sizeof(buff));
if(bytes > 0){
printf(" wirte buff:%s\n",buff);
}
}
}
}
}
return 0;
}
IO-poll
基本原理
多路复用poll的方式与select多路复用原理类似,但有很多地方不同,下面是具体的对比
在应用层是以结构体struct pollfd数组的形式来进行管理文件描述符,在内核中基于链表对数组进 行扩展;select方式以集合的形式管理文件描述符且最大支持1024个文件描述
poll将请求与就绪事件通过结构体进行分开
select将请求与就绪文件描述符存储在同一个集合中,导致每次都需要进行重新赋值才能进行下一 次的监控
在内核中仍然使用的是轮询的方式,与 select 相同,当文件描述符越来越多时,则会影响效率
函数使用
poll多路复用实现主要调用 poll 函数
函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数功能
监控多个文件描述符的变化
函数参数
fds:sturct pollfd结构体指针
nfds:fds结构体的数量
timeout:超时时间,单位为ms
参数相关说明
1. struct pollfd 结构体说明
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
例子
使用 poll 监听有名管道,当有名管道有数据时,读取数据并打印
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATH "/home/linux/name_pipe"
int main(){
int wfd = open(PATH,O_RDONLY);
printf("wfd=%d\n",wfd);
struct pollfd pfd[2];
pfd[0].fd = 0;
pfd[0].events = POLLIN;
pfd[1].fd = wfd;
pfd[1].events = POLLIN;
nfds_t nfds = 2;
int ret;
while(1){
ret = poll(pfd,nfds,1000);
if(ret == -1){
perror("poll\n");
exit(EXIT_FAILURE);
}else if(ret == 0){
printf("timeout\n");
}else if(ret >0){
for(int i = 0;i<nfds;i++){
if(pfd[i].revents == POLLIN){
if(pfd[i].fd == 0){
char buf[128]={0};
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("buf=%s\n",buf);
}
if( pfd[i].fd == wfd){
char buff[128]={0};
memset(buff,0,sizeof(buff));
ssize_t bytes = read(wfd,buff,sizeof(buff));
if(bytes > 0){
printf(" wirte buff:%s\n",buff);
}
}
}
}
}
}
return 0;
}
IO—epoll
epoll 基本原理
epoll相对于select与poll有较大的不同,主要是针对前面两种多路复用 IO 接口的不足
select/poll的不足:
select 方案使用数组存储文件描述符,最大支持1024个
select 每次调用都需要将文件描述符集合拷贝到内核中,非常消耗资源
poll 方案解决文件描述符存储数量限制问题,但其他问题没有得到解决
select / poll 底层使用轮询的方式检测文件描述符是否就绪,文件描述符越多,则效率越低
epoll优点:
epoll底层使用红黑树,没有文件描述符数量的限制,并且可以动态增加与删除节点,不用重复拷贝 epoll底层使用callback机制,没有采用遍历所有描述符的方式,效率较高
函数使用
epoll创建需要调用epoll_create函数,用于创建epoll实例
函数原型
int epoll_create(int size);
函数功能
创建一个epoll实例,分配相关的数据结构空间
函数参数
size:需要填一个大于0的数,从Linux 2.6.8开始,size参数被忽略
函数返回值
成功:返回epoll文件描述符
失败:返回-1,并设置errno
epoll_ctl函数
epoll控制函数主要用于文件描述符集合的管理,包括增加、修改、删除等操作。
函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数参数
epfd:epoll 实例
op:epoll 操作命令字
EPOLL_CTL_ADD:在epoll实例中添加新的文件描述符(相当于向红黑树中添加节点),并将事件与 fd关联
EPOLL_CTL_MOD:更改与目标文件描述符fd相关联的事件
EPOLL_CTL_DEL:从epoll实例中删除目标文件描述符fd ,事件参数被忽略
epoll_data是一个共用体,主要使用 fd 成员用于存储文件描述符
epoll 等待函数
epoll 等待事件发生(关联的文件描述符就绪),这里调用 epoll_wait 函数
函数原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数功能
等待文件描述符关联的事件发生
函数参数
epfd:epoll实例对象
events:存储就绪集合的数组的地址
maxevents:就绪集合的最大值
timeout:超时时间
函数返回值
成功:返回就绪的文件描述符数量,超时返回0
失败:返回-1,并设置errno
例子
使用epoll 监听有名管道,当有名管道有数据时,读取数据并打印
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATH "/home/linux/name_pipe"
int main(){
int wfd = open(PATH,O_RDONLY);
printf("wfd=%d\n",wfd);
//创造epoll
int efd = epoll_create(2);
if(efd == -1){
perror("epoll_create\n");
exit(EXIT_FAILURE);
}
//将描述符加入epoll
struct epoll_event ev[2];
ev[0].data.fd = 0;
ev[0].events = EPOLLIN;
ev[1].data.fd = wfd;
ev[1].events = EPOLLIN;
int ret;
ret = epoll_ctl(efd,EPOLL_CTL_ADD,wfd,ev+1);
if(ret == -1){
perror("epoll_ctl\n");
exit(EXIT_FAILURE);
}
ret = epoll_ctl(efd,EPOLL_CTL_ADD,0,ev);
if(ret == -1){
perror("epoll_ctl\n");
exit(EXIT_FAILURE);
}
//就绪集合
struct epoll_event events[10];
while(1){
int res = epoll_wait(efd,events,10,1000);
if(res == -1)
{
perror("epoll_wait");
exit(EXIT_FAILURE);
}
else if(res == 0)
{
printf("timeout\n");
}else if(res >0){
for(int i = 0;i<res;i++){
if(events[i].events == EPOLLIN ){
if(events[i].data.fd==0){
char buf[128]={0};
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("buf=%s\n",buf);
}
if( events[i].data.fd == wfd){
char buff[128]={0};
memset(buff,0,sizeof(buff));
ssize_t bytes = read(wfd,buff,sizeof(buff));
if(bytes > 0){
printf(" wirte buff:%s\n",buff);
}
}
}
}
}
}
return 0;
}
有名管道代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define PATH "/home/linux/name_pipe"
int main()
{
/* int pipe=mkfifo(PATH,0666);//如果管道未创建,创建管道
if(-1==pipe){
perror("mkfifo");
exit(EXIT_FAILURE);
}*/
int fd=open(PATH,O_WRONLY);
if(-1==fd){
perror("open");
exit(EXIT_FAILURE);
}
char buf[128]={0};
while(1){
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
ssize_t wbetys=write(fd,buf,sizeof(buf));
if(wbetys == -1){
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
char buff[128] = "exit";
if(strncmp(buf,buff,4) == 0){
break;
}
}
close(fd);
return 0;
}