赞
踩
I/O(input/output)也就是输入和输出,在著名的冯诺依曼体系结构当中,将数据从输入设备拷贝到内存就叫做输入,将数据从内存拷贝到输出设备就叫做输出。
IO存在最主要的问题就是效率问题,IO的效率极为低下的,我们以读取数据为例:
由此我们就可以知道IO的本质就是等待 + 数据拷贝,只要缓冲区中没有数据,read/recv就会一直阻塞等待,直到缓冲区中出现数据,然后进行拷贝,所以说read/recv就会花费大量时间在等这一操作上面,这就是一种低效的IO模式。
我们如果想要解决这个问题,就需要让等的比重降低,这样,IO的效率就提高了,接下来我们以钓鱼人的例子来理解一下IO模型。
IO的过程其实跟钓鱼是非常相似的,IO中等的过程其实就相当于钓鱼等待鱼上钩的过程,而拷贝到过程就相当于把鱼从水里装进桶里的过程。
我们来看下面这五个人的钓鱼方式:
张三,李四和王五钓鱼的效率一样吗?
张三,李四和王五钓鱼的效率钓鱼的效率本质上是一样的,因为他们都是拿着一根鱼竿,在等待鱼上钩,鱼咬钩的概率是一样的。
他们只不过是等待鱼上钩的方式不一样,张三是死等,李四是定期检查浮标,王五则是通过铃铛的提示来判断鱼是否上钩。
谁的效率更高?
显而易见,赵六的效率是最高的,因为赵六有100根鱼竿,上鱼的概率是最大的,单位时间内,赵六鱼上钩的效率远远大于张三,李四和王五。
因为赵六减少了等待的概率发生,增加了拷贝的时间,所以效率是最高的。
如何看待田七钓鱼方式?
田七是将钓鱼这件事交给自己的司机去做了,自己就可以去干其他事情了,他并不关心司机是怎么钓鱼的,司机可以采用张三,李四,王五和赵六中的任意一种方式,田七只关心最后将桶装满了没。
田七并没有参与钓鱼的过程,他将钓鱼的任务安排给了司机,在司机钓鱼期间他可以做任何事情,如果将钓鱼看作是一种IO的话,那田七的这种钓鱼方式就叫做异步IO。
而对于张三、李四、王五、赵六来说,他们都需要自己等鱼上钩,当鱼上钩后又需要自己把鱼从河里钓上来,对应到IO当中就是需要自己进行数据的拷贝,因此他们四个人的钓鱼方式都叫做同步IO。
这五个人的钓鱼方式对应了五种IO模型:
阻塞IO
阻塞IO就是在内核将数据准备好之前,系统调用会一直等待。
图示如下:
所有的套接字,默认的都是阻塞方式;
非阻塞IO
非阻塞IO就是,如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。
图示如下:
非阻塞IO往往需要程序员以循环的方式反复尝试读写文件描述符,这个过程称为轮询,这对CPU来说是较大的浪费,一般只有特定场景下才使用。
阻塞IO与非阻塞IO的最大区别就在于阻塞IO是操作系统识别到数据就绪后唤醒进程或线程,而非阻塞IO是用户一直进行检测,直到数据准备就绪。
信号驱动IO
信号驱动IO就是当内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
图示如下:
当底层数据就绪的时候会向当前进程或线程递交SIGIO信号,因此可以通过signal或sigaction函数将SIGIO的信号处理程序自定义为需要进行的IO操作,当底层数据就绪时就会自动执行对应的IO操作。
IO多路转接
IO多路转接也叫做IO多路复用,能够同时等待多个文件描述符的就绪状态。
异步IO
异步IO就是由内核在数据拷贝完成时,通知应用程序。
图示如下:
进行异步IO需要调用一些异步IO的接口,异步IO接口调用后会立马返回,因为异步IO不需要你进行“等”和“拷贝”的操作,这两个动作都由操作系统来完成,你要做的只是发起IO,当IO完成后操作系统会通知应用程序,因此进行异步IO的进程或线程并不参与IO的所有细节。
同步和异步关注的是消息通信机制。
为什么非阻塞IO在没有得到结果之前就返回了?
因此,在进行非阻塞IO时,在没有得到结果之前,虽然这个调用会返回,但后续还需要继续进行轮询检测,因此可以理解成调用还没有返回,而只有当某次轮询检测到数据就绪,并且完成数据拷贝后才认为该调用返回了。
同步通信 VS 同步与互斥
在多进程与多线程里面有同步与互斥的概念,IO中也存在同步的概念,但是这两个同步是完全不相干的。
阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。
非阻塞IO,记录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。
我们可以用read函数从标准输入当中读取数据为例:
#include <iostream> #include <unistd.h> int main() { char buffer[1024]; while (true) { ssize_t s = read(0, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; std::cout << "echo# " << buffer << std::endl; } else { std::cout << "read error" << std::endl; } } return 0; }
程序运行以后,我们会发现,如果我们不进行数据的输入操作,程序就会一直阻塞住,根本原因就是底层数据没有就绪,read函数在阻塞式等待。
当我们输入数据以后,此时read函数就会检测到底层的数据已经就绪了,就会将缓冲区中的数据拷贝到我们的buffer数组中,并且将读取到的数据输出到显示器上面,最后我们就看到了我们输入的字符串。
打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要在使用open函数打开文件时携带O_NONBLOCK
或O_NDELAY
选项,此时就能够以非阻塞的方式打开文件。
我们一般用统一的方式来进行非阻塞设置,就是fcntl函数。
fcntl函数
fcntl函数的原型如下:
int fcntl(int fd, int cmd, ... /* arg */ );
参数说明:
fd
:已经打开的文件描述符。cmd
:需要进行的操作。…
:可变参数,传入的cmd值不同,后面追加的参数也不同。fcntl函数常用的5种功能与其对应的cmd取值如下:
返回值说明:
实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。
bool SetNoBlock(int fd)
{
// 在底层获取fd对应文件描述符的标志位
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
return false;
// 设置非阻塞IO
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
return true;
}
此时我们在以非阻塞轮询方式读取标准输入。
#include <iostream> #include <unistd.h> #include <fcntl.h> #include <cstring> bool SetNoBlock(int fd) { // 在底层获取fd对应文件描述符的标志位 int fl = fcntl(fd, F_GETFL); if (fl < 0) return false; // 设置非阻塞IO fcntl(fd, F_SETFL, fl | O_NONBLOCK); return true; } int main() { SetNoBlock(0); char buffer[1024]; while (true) { sleep(1); ssize_t s = read(0, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; std::cout << "echo# " << buffer << std::endl; } else { std::cout << "read error " << "errno: " << errno << "errstring: " << strerror(errno) << std::endl; if (errno == EWOULDBLOCK || errno == EAGAIN) { std::cout << "当前0号fd数据未就绪,请再试一次" << std::endl; continue; } else if (errno == EINTR) { std::cout << "当前IO信号可能被中断,请再试一次" << std::endl; continue; } } } return 0; }
需要注意的是,调用read函数以后,如果底层数据没有就绪,就会立马返回一个错误信息,但是此时我们是需要对返回的的错误信息进行甄别的,我们需要知道是真的出错了还是只是底层数据没有就绪。如果错误码的值是EAGAIN
或EWOULDBLOCK
,说明本次调用read函数出错是因为底层数据还没有就绪,因此后续还应该继续调用read函数进行轮询检测数据是否就绪,当数据继续时再进行数据的读取。
此外,调用read函数在读取到数据之前可能会被其他信号中断,此时read函数也会以出错的形式返回,此时的错误码会被设置为EINTR,此时应该重新执行read函数进行数据的读取。
程序运行以后,底层数据如果没有就绪,此时read函数就会轮询进行检测:
一旦我们进行了输入操作,此时read函数就会在轮询检测时检测到,紧接着立马将数据读取到从内核拷贝到我们传入的buffer数组当中,并且将读取到的数据输出到显示器上面,然后继续进行轮询检测。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。