IO多路复用
定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力
作用:(低开销解决阻塞)
应用程序通常需要处理来自多条事件流中的事件,比如电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如web服务器如nginx,需要同时处理来来自N个客户端的事件。
逻辑控制流在时间上的重叠叫做 并发
而CPU单核在同一时刻只能做一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不同事件流的时间片交替进行)。在计算机系统中,我们用线程或者进程来表示一条执行流,通过不同的线程或进程在操作系统内部的调度,来做到对CPU处理的时分复用。这样多个事件流就可以并发进行 ,不需要一个等待另一个太久,在用户看起来他们似乎就是并行在做一样。
成本:
使用并发处理的成本:
线程/进程创建成本
CPU切换不同线程/进程成本 Context Switch
多线程的资源竞争
设置了一种可以在单线程/进程中处理多个事件流的方法:
一种答案就是IO多路复用。
因此IO多路复用解决的本质问题是在用更少的资源完成更多的事。
IO模型
1、阻塞IO(实现简单,资源消耗小,但是处理效率低,响应时间长)
2、非阻塞IO EAGAIN 忙等待 errno(处理效率高,但是资源占用太大)
3、信号驱动IO SIGIO 用的相对少(异步处理,效率高,但是更复杂且兼容性低)
4、并行模型 进程,线程(并行处理,能充分利用CPU资源,但是资源开销大,且有同步问题)
5, IO多路复用 select、poll、epoll(使用与高并发场所,资源利用率高,不需要进程这么大的资源开销,但其复杂度更高)
1、阻塞IO ===》最常用 默认设置
2、非阻塞IO ===》在阻塞IO的基础上调整其为不再阻塞等待。
在程序执行阶段调整文件的执行方式为非阻塞:
===》fcntl() ===>动态调整文件的阻塞属性
3.信号驱动io
文件描述符需要追加 O_ASYNC 标志。
设备有io事件可以执行时,内核发送SIGIO信号。
1.追加标志
int flag ;
flag = fcntl(fd,F_GETFL,0); //获得flag
fcntl(fd,F_SETFL,flag | O_ASYNC); //设置异步标志
(设置后进程在I/O操作中无需阻塞,在操作完成后发送信号,通知进程,
此时进程会检索I/O操作的结果)
2.设置信号接收者
fcntl(fd,F_SETOWN,getpid());//常用设置(设置接收信号的进程id)
3.对信号进行捕获
signal(SIGIO,myhandle);//注册自定义函数myhandle
4.并发
1.进程
2.线程
IO 多路复用 ===》并发服务器 ===》TCP协议
3、select循环服务器 ===> 用select函数来动态检测有数据流动的文件描述符
select()
select函数是IO多路复用的函数,它主要的功能是用来等文件描述符中的事件是否就绪,select可以使我们在同时等待多个文件缓冲区 ,减少IO等待的时间,能够提高进程的IO效率。
select与epoll最大的不同就是监视文件描述符的工作是自己来做还是交给内核来做,这也是epoll的效率得到大幅提高的原因
#include <sys/select.h>
#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);
功能:完成指定描述符集合中有效描述符的动态检测。(可以放1024,一般监测读事件)
该函数具有阻塞等待功能,在函数执行完毕后。(全都没有才阻塞)
目标测试集合中将只保留最后有数据的描述符。(读到几个返回几个,至少有一个,0会阻塞)
参数:nfds 描述符的上限值,一般是链接后描述符的最大值+1;<nfds
readfds 只读描述符集
writefds 只写描述符集
exceptfds 异常描述符集
以上三个参数都是 fd_set * 的描述符集合类型
timeout 检测超时 如果是NULL表示一直检测不超时 。
返回值:超时 0(等待之间过了没读到)(需要设置超时值)
失败 -1(没读到)
成功 >0
为了配合select函数执行,有如下宏函数:
void FD_CLR(int fd, fd_set *set);
功能:将指定的set集合中编号为fd的描述符号删除。
int FD_ISSET(int fd, fd_set *set);
功能:判断值为fd的描述符是否在set集合中,
如果在则返回真,否则返回假。
void FD_SET(int fd, fd_set *set);
功能:将指定的fd描述符,添加到set集合中。
void FD_ZERO(fd_set *set);
功能:将指定的set集合中所有描述符删除。
select与poll的功能实现类似,所以跳过poll来了解epoll
epoll
epoll机制的实现主要依赖于红黑树和就绪链表两个核心数据结构。
红黑树(一种二叉树):在Linux内核中,epoll中的每个监视的文件描述符都会维护一个对应的红黑树节点。这些红黑树节点按照文件描述符的大小有序排列,方便进行快速的查找和插入操作。红黑树的特点是平衡性和快速的查找、插入和删除操作。
就绪链表:为了提高效率,epoll维护了一个就绪链表,用于记录所有已经就绪的文件描述符。当一个文件描述符上的事件被触发时,内核会将该文件描述符添加到就绪链表中。
也就是说,通过与内核共用了状态机制,大幅提高了对于文件的处理速度
epoll_create()
int epoll_create(int size);
功能:epoll_create函数用于创建epoll文件。
参数:size:目前内核还没有实际使用,只要大于0就行。
返回值:成功:返回epoll文件描述符。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:epoll_ctl函数用于增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中。
参数:epfd:epoll文件描述符。
op:操作码
EPOLL_CTL_ADD:插入事件
EPOLL_CTL_DEL:删除事件
EPOLL_CTL_MOD:修改事件
fd:epoll事件绑定的套接字文件描述符。
events:epoll事件结构体。
struct epoll_event{
uint32_t events; //epoll事件,参考事件列表
epoll_data_t data;
} ;
typedef union epoll_data {
void *ptr;
int fd; //套接字文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t; //一般选用fd
返回值:
成功:返回0。
失败:返回-1,并设置errno。
一般操作过程(写成添加函数):
int add_fd(int epfd ,int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
int ret= epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);
if(-1 == ret)
{
perror("add fd");
exit(1);
}
return ret;
}
epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:epoll_wait用于监听epoll事件。
参数:
epfd:epoll文件描述符。
events:epoll事件数组。
maxevents:epoll事件数组长度。
timeout:超时时间,
小于0:阻塞。
等于0:非阻塞。
大于0:阻塞等待直到超时,超时后返回,单位毫秒。
返回值:
小于0:出错。
等于0:超时。
大于0:返回就绪事件个数。
例程:
while(1)
{
//3 wait
int ep_ret= epoll_wait(epfd,rev,2,-1); // struct epoll_event rev[2]={0};
int i = 0 ;
for(i=0;i<ep_ret;i++)
{
if(rev[i].data.fd == fd) //某个正在接收数据的管道
{
char buf[256]={0};
read(fd,buf,sizeof(buf));
printf("fifo %s\n",buf);
}
if(0 == rev[i].data.fd ) //标准输入
{
char buf[256]={0};
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("terminal:%s\n",buf);
}
}
}