阻塞IO模型:调用IO系统调用的进程会一直阻塞
,直到内核中数据拷贝
完成。应用程序调用一个IO函数
,导致应用程序阻塞
,等待内核数据准备好。 如果数据没有准备好,一直等待到数据
准备好了为止,然后将数据从内核拷贝到用户空间并且返回成功指示。
注:阻塞IO是最常见的IO模型
非阻塞IO:非阻塞IO通过进程反复
调用IO函数多次系统调用,并马上返回,但是在数据拷贝
的过程中,进程是阻塞的。如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK
错误码。
注:非阻塞IO往往需要程序员循环的方式反复
尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一 般只有特定场景下才使用。
信号驱动I/O
,并捕捉一个信号
处理函数,进程继续运行并不阻塞。当数据准备好时,内核向该进程发送一个SIGIO
信号,可以在信号处理函数
中调用I/O操作函数
处理数据。两次调用,两次返回。IO多路转接:主要是select
和epoll
。对一个IO
端口,两次调用,两次返回,比阻塞IO
并没有什么优越性,关键是能实现同时
对多个IO端口进行监听。I/O复用模型会用到select、poll、epoll
函数,这几个函数也会使进程阻塞
,但是和阻塞I/O
所不同的的,这两个函数可以同时阻塞多个I/O操作
。而且可以同时对存在多个读操作,多个写操作
的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数
。虽然从流程图上看起来和阻塞IO类似,但是实际上最核心在于IO多路转接
能够同时等待多个文件描述符
的就绪状态。
异步IO:数据拷贝的时候进程无需阻塞
。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的结果,通过状态、通知或者回调函数来通知调用者
的输入输出操作。由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
总结:任何IO过程中, 都包含两个步骤,第一是等待
, 第二是拷贝
,而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间,为了让IO更高效, 最核心的办法就是让等待的时间尽量少。
同步通信与异步通信的区别:
调用
时,在没有得到结果
之前,该调用就不返回
。但是一旦调用返回,就得到返回结果。换句话说,就是由调用者主动等待
这个调用的结果。直接返回
了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻
得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数
处理这个调用。同步与互斥区别:
一个
访问者对其进行访问,具有唯一性和排它性
。但互斥无法限制
访问者对资源的访问顺序,即访问是无序
的。互斥的基础
上(大多数情况),通过其它机制
实现访问者对资源的有序
访问。在大多数情况下,同步已经实现了互斥
,特别是所有写入资源的情况必定是互斥
的。并发进程在一些关键点上可能需要互相等待与互通消息
,这种相互制约的等待与互通信息称为进程同步
。 实际上进程互斥也是一种同步,他协调多个进程互斥
进入同一个临界资源对应的临界区。同步是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序
而等待、 传递信息所产生的制约关系,尤其其是在访问临界资源的时候。阻塞和非阻塞的区别:
调用结果
返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。有人会把阻塞调用
和同步调用
等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。不会阻塞
当前线程,而是继续处理别的事。总结:
功能
,该功能没有结束前,我死等结果。(回调通知)
,自己去做自己的事。数据
或者没有得到结果之前,我不会返回。select
通知调用者fcntl: 该函数参数存在一个文件描述符,默认为阻塞IO
根据传入的cmd
的值不同,后面追加的参数也不相同,它的功能有五种:
利用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞
。
基于fcntl实现轮询方式读取标准输入:
#include
#include
#include
#include
using namespace std;
//设置该文件描述符为非阻塞
void SetNoBlock(int fd)
{
int f = fcntl(fd, F_GETFL);
if(f < 0){
perror("use fcntl");
return;
}
fcntl(fd, F_SETFL, f | O_NONBLOCK);
}
int main()
{
SetNoBlock(0);//设置标准输入为非阻塞式
while(1)
{
char buf[1024] = {0};
ssize_t rs = read(0, buf, sizeof(buf) - 1);
if(rs <= 0){
perror("use read");
sleep(1);
continue;
}
cout << buf << endl;
}
return 0;
}
F_GETFL
将当前的文件描述符的属性取出来
。F_SETFL
将文件描述符设置回去,设置回去的同时加上一个O_NONBLOCK
参数,表示非阻塞。操作系统提供select
函数来实现多路复用输入/输出
模型。select
系统调用是用来让我们的程序监视多个文件描述符
的状态变化的,程序会停在select
这里等待,直到被监视的文件描述符有一个或多个
发生了状态改变。
参数解释:
nfds
是需要监视的最大的文件描述符值+1
。readfds\writefds\exceptfds
分别对应于需要检测的可读文件描述符
的集合,可写
文件描述符的集合及异常
文件描述符的集合 。timeval
,用来设置select
的等待时间。参数timeout:
select
没有timeout,select
将一直被阻塞,直到某个文件描述符上发生了事件。描述符集合
的状态,然后立即返回
并不等待外部事件的发生。时间段
里没有事件发生,select
将超时返回。fd_set结构: 这个结构实质上就是一个整数数组
,更严格的说是一个"位图"
。 使用位图中对应的位来表示要监视的文件描述符
。操作系统提供了一组很方便的接口来操作这个位图:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set);// 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部
timeval结构体:
timeval结构用于描述一段时间长度,如果在这个时间内需要监视的描述符没有事件发生则函数返回,返回值为0。
select函数返回值:
文件描述符
状态已改变的个数描述词状态
改变前已超过timeout
时间没有返回-1
,错误原因存于errno
,此时参数readfds,writefds, exceptfds
和timeout的值变成不可预测错误值可能为:
文件
已关闭信号
所中断n
为负值内存
不足select常见使用方法:
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){//业务}
理解select执行过程:
取fd_set
长度为1
字节,fd_set
中的每个bit
可以对应一个文件描述符fd
。则1字节长的fd_set
最大可以对应8个fd
。
fd_set set; FD_ZERO(&set);
则set用位表示是00000000
。fd=5
,执行FD_SET(fd,&set);
后set变为00010000
(第5位置为1)fd=2,fd=1
,则set
变为00010011select(6,&set,0,0,0)
阻塞等待fd=1,fd=2
上都发生可读
事件,则select
返回,此时set
变为 00000011。没有事件发生的fd=5
被清空。socket就绪条件: socket也是一种文件描述符。
读就绪:
socket
在内核中接收缓冲区
中的字节数大于等于低水位标记SO_RCVLOWAT
。 此时可以无阻塞
的读该文件描述符并且返回值大于0
socket
TCP通信中,对端关闭连接,此时对该socket
读则返回0socket
上有新的连接请求socket
上有未处理的错误写就绪:
SO_SNDLOWAT
,此时可以无阻塞
的写,并且返回值大于0
(close或者shutdown)
,对一个写操作被关闭的socket
进行写操作会触发SIGPIPE
信号connect
连接成功或失败之后未读取
的错误select的特点:
文件描述符个数
取决与sizeof(fd_set)
的值。服务器上sizeof(fd_set)=512
,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512*8=4096
。fd
加入select
监控集的同时,还要再使用一个数据结构array
保存放到select
监控集中的fd
。 一是用于在select
返回后,array
作为源数据
和fd_set
进行FD_ISSET
判断;二是select
返回后会把以前加入的但并无事件发生的fd
清空,则每次开始select
前都要重新从array
取得 fd
逐一加入(FD_ZERO最先),扫描array
的同时取得fd
最大值maxfd
,用于select
的第一个参数。select的缺点:
select
,都需要手动设置fd
集合。fd
集合从用户态拷贝到内核态
,这个开销在很多时会很大。内核遍历
传递进来的所有fd,这个开销在很多时也很大。文件描述符
数量太小。#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define FILE "/dev/input/mouse0"
int main()
{
fd_set read_fds;
struct timeval time;
time.tv_sec = 10; //设置阻塞超时时间为10秒钟
time.tv_usec = 0;
char buf [10];
//打开设备文件
int fd = open(FILE, O_RDONLY);
if(fd < 0){
perror("use open");
exit(-1);
}
//构建IO多路复用
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
FD_SET(fd, &read_fds);
int ret = select(fd+1, &read_fds, NULL, NULL, &time);
if(ret < 0){
perror("use select");
exit(-1);
}
else if(0 == ret){
cout << "等待超时" << endl;
}
else{
if(FD_ISSET(0, &read_fds)){//如果是键盘,读取键盘的数据
memset(buf, 0 ,sizeof(buf)-1);
read(0, buf, sizeof(buf)-1);
cout << buf << endl;
}
if(FD_ISSET(1, &read_fds))//如果是鼠标,读取鼠标的数据
{
memset(buf, 0 ,sizeof(buf)-1);
read(1, buf, sizeof(buf)-1);
cout << buf << endl;
}
}
return 0;
}
poll函数:
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//pollfd结构体
struct pollfd {
int fd; /* file descriptor */
short events; /* 请求事件 */
short revents; /* 返回事件 */
};
fds
是一个poll
函数监听的结构列表,每一个元素中包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合。nfds
表示fds
数组的长度。timeout
表示poll
函数的超时时间,单位是毫秒(ms)
。返回值说明:
poll的优点:
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
pollfd结构包含了要监视
的event
和发生的event
,不再使用select“参数-值”
传递的方式,接口使用比 select
更方便。
poll并没有最大数量限制
。
poll的缺点:
select
函数一样,poll
返回后需要轮询pollfd
来获取就绪的描述符。poll
都需要把大量的pollfd
结构从用户态拷贝到内核
中。就绪状态
, 因此随着监视的描述符
数量的增长,其效率也会线性下降。实现一个监测标准输入:
#include
#include
#include
#include
#include
using namespace std;
int main()
{
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
int time = 10000;
while(1)
{
int ret = poll(&poll_fd, 1 ,time);
if(ret < 0)
{
perror("use poll");
continue;
}
else if(ret == 0){
cout << "poll timeout!" << endl;
continue;
}
else{
char buf[100];
read(0, buf, sizeof(buf));
cout << buf << endl;
}
}
return 0;
}
当套接字比较多的时候,每次select()
都要通过遍历FD_SETSIZE
个Socket
来完成调度,不管哪个Socket
是活跃的,都遍历一遍。这会浪费很多CPU
时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll做的。
epoll支持水平触发和边缘触发,最大的特点在于边缘触发
,它只告诉进程哪些fd
刚刚变为就需态
,并且只会通知一次。还有一个特点是,epoll
使用“事件”
的就绪通知方式,通过epoll_ctl
注册fd
,一旦该fd
就绪,内核就会采用类似callback
的回调机制
来激活该fd
,epoll_wait
便可以收到通知。
epoll相关系统调用:
epoll_create: 创建一个epoll
的句柄,size
表示监听数目的大小。创建完句柄它会自动占用一个fd
值,使用完epoll一定要记得close
,不然fd会被消耗完。
int epoll_create(int size);
//size参数被忽略
epoll_ctl: 这是epoll
的事件注册函数,和select
不同的是select
在监听的时候会告诉内核监听什么样的事件,而epoll
必须在epoll_ctl
先注册要监听的事件类型。
它的第一个参数是epoll_creat
的执行结果。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
select()
是在监听事件时告诉内核要监听什么类型
的事件, 而是在这里先注册要监听的事件类型。epoll_create()
的返回值(epoll的句柄)。动作
,用三个宏来表示。fd
。内核需要监听
什么类型事件。第二个参数表示的三个宏:
fd
到epfd
中fd
的监听事件epfd
中删除一个fd
struct epoll_event结构体:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
SOCKET
正常关闭)(Edge Triggered)
模式, 这是相对于水平触发(Level Triggered)
来说的socket
的话, 需要再次把这个socket
加入到EPOLL
队列里epoll_wait函数: 收集在epoll
监控的事件中已经发送的事件。等待事件的发生,类似于select
的调用。
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
参数说明:
events
是分配好的epoll_event
结构体数组。epoll
将会把发生的事件赋值到events
数组中 (events不可以是空指针,内核只负责把数据复制到这个events
数组中,不会去帮助我们在用户态中分配内存)。maxevents
告诉内核这个events
有多大,这个maxevents
的值不能大于创建epoll_create()
时的size
。timeout
是超时时间 (毫秒,0会立即返回,-1是永久阻塞)返回值:
I/O上
已准备好的文件描述符数目0
表示已超时0
表示函数失败epoll的优点(和 select 的缺点对应):
epoll
拆分成了三个函数,但是反而使用起来更方便高效。不需要每次循环
都设置关注的文件描述符,也做到了输入输出
参数分离开。EPOLL_CTL_ADD
将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll
都是每次循环都要进行拷贝)遍历
,而是使用回调函数
的方式,将就绪的文件描述符
结构加入到就绪队列中,epoll_wait
返回直接访问就绪队列就知道哪些文件描述符
就绪。这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会受到影响。epoll工作方式: epoll有2种工作方式水平触发(LT)和边缘触发(ET)
。
水平触发Level Triggered 工作模式 :
epoll
检测到socket
上事件就绪的时候,可以不立刻进行处理或者只处理一部分1K
数据, 缓冲区中还剩1K
数据, 在第二次调用 epoll_wait
时, epoll_wait
仍然会立刻返回并通知socket
读事件就绪。epoll_wait
才不会立刻返回。阻塞
读写和非
阻塞读写。注:epoll默认状态下就是LT工作模式
边缘触发Edge Triggered工作模式 : 如果我们在第1步将socket
添加到epoll
描述符的时候使用了EPOLLET
标志epoll
进入ET
工作模式。
epoll
检测到socket
上事件就绪时, 必须立刻处理。如上面的例子,虽然只读了1K
的数据,但是缓冲区还剩1K
的数据,在第二次调用 epoll_wait
的时候, epoll_wait
不会再返回了。ET
模式使用epoll。但是LT
只支持非阻塞的读写。理解ET模式和非阻塞文件描述符: 使用ET
模式的epoll
需要将文件描述设置为非阻塞
。
假设服务器接受到一个10k
的请求, 会向客户端返回一个应答数据,如果客户端收不到应答, 不会发送第二个10k请求。如果服务端写的代码是阻塞式的read
, 并且一次只 read 1k
数据的话,剩下9k的数据在缓冲区中,此时由于epoll 是ET
模式并不会认为文件描述符
读就绪。epoll_wait
就不会再次返回,剩下的 9k
数据会一直在缓冲区中,直到下一次客户端再给服务器写数据,epoll_wait
才能返回,**但是服务器只读到1k个数据, 要10k读完才会给客户端返回响应数据 ,客户端要读到服务器的响应, 才会发送下一个请求 ,客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据。**所以使用非阻塞轮询
的方式来读缓冲区, 保证一定能把完整的请求都读出来。
epoll的适用场景: 对于多连接
, 且多连接中只有一部分连
接比较活跃时, 比较适合使用epoll。例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll. 如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根 据需求和场景特点来决定使用哪种IO模型。
注:epoll惊群问题: http://blog.csdn.net/fsmiy/article/details/36873357
select、poll、epoll三者优缺点对比:
https://blog.csdn.net/qq1910084514/article/details/86593686
https://www.jianshu.com/p/6a6845464770
https://blog.csdn.net/jay900323/article/details/18141217