前言: 五种基本的IO模型本文主要讲解其中的概念,也会给出些比较简单的代码例子。IO就是输入输出,相信大家是理解输入缓冲区和输出缓冲区的,用户层读取数据是从输入缓存区中读取,发送数据是发送到输出缓冲区中。能否从输入缓存区中读取数据呢?能否向输出缓冲区中写入数据呢?这个过程用户层一般不清楚,谁清楚?操作系统。操作系统来通知用户,用户就得等着被通知。怎么等?这个怎么等 就衍生出五种IO模型。
1. 阻塞等待
所有的套接字默认情况下都是阻塞等待,这是最为常见的IO模型。它就是如果等事件还不就绪,它就一直等,也不返回,直到等事件就绪并且它把数据拷贝回来了,它才返回。阻塞等待它完成等+拷贝全过程,等是阻塞式等。
图解:
例子:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
char buffer[10];
ssize_t n = read(0,buffer,sizeof(buffer)-1);
if(n>0)
{
读取成功
buffer[n] = 0;
printf("读取成功:%s",buffer);
}
else
{
/// 数据没准备好或者读取失败
printf("数据报没有准备好\n");
}
}
return 0;
}
这段代码非常简单,我们会看到什么现象呢?我如果键盘不输入数据,那么进程就会一直卡着等待,直到我输入数据后,才能读取。而且else的语句一直不会被执行,因为程序在read处就阻塞住了。
我不输入,被阻塞的情况:
输入数据后:
用ps -l 查看进程状态:
2. 非阻塞等待
非阻塞等待,它就不会等待,如果缓冲区中数据报没有准备好,那么就直接函数返回,并且还带回一个错误码:EWOULDBLOCK。
非阻塞等待只检测一次如果有数据报就给你拷贝回来了,没有就算了。所以程序员要使用非阻塞等待的话就需要设置循环检测读写描述符(fd),这个过程就是轮询。
图解:
例子:
我们可以将套接字改为非阻塞式等待,就改上面的代码:
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
void SetNoblock(int fd)
{
int f1 = fcntl(fd,F_GETFL);
if(f1 < 0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
return;
}
int main()
{
SetNoblock(0);
while(1)
{
char buffer[10];
ssize_t n = read(0,buffer,sizeof(buffer)-1);
if(n>0)
{
读取成功
buffer[n] = 0;
printf("读取成功:%s",buffer);
}
else
{
/// 数据没准备好或者读取失败
printf("数据报没有准备好\n");
sleep(1);
continue;
}
}
return 0;
}
我们先来看现象:
如果不输入数据,它会立马返回,而不是卡着不动,这里的else后的语句就排上用场了:
如果输入数据,就会读取,读取完数据后,若不输入那么还是轮询检测:
如何设置非阻塞呢?这就需要用到一个函数接口:fcntl()
根据不同的cmd,后面的可变参数是不同的。
- 参数cmd:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
改变文件的状态用的就是第三个cmd参数。
- F_GETFL是用于取得文件的状态标志
比如:
int f1 = fcntl(fd,F_GETFL);
fcntl返回值是一个位图然后赋值给了f1,f1是代表文件状态属性的位图。
- F_SETFL是用于改变文件状态的,分别可以追加O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC状态。
比如:
fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
看后面这个f1 | O_NONBLOCK,f1是fd原来的文件状态属性,这是个位图,然后按位与上O_NONBLOCK就是使得文件描述符的状态变为非阻塞,注意阻塞状态是默认的属性。
看一些O_APPEND, O_NONBLOCK它们都是位图:
这个函数的返回值,如果出错统一返回-1,其余的返回值还得看具体的cmd(命令)。
3. 信号驱动IO
信号驱动IO中,当文件描述符上可执行IO操作时,进程请求内核为自己发送一个信号。
进程可以执行其他任何任务直到IO就绪位置,此时内核会发送信号给进程。进程直接调用recvfrom去拷贝数据。相当于 进程没有等,进程可以处理其他事情,等数据报在缓冲区准备好了,内核非常清楚,然后发送给进程一个信号,进程收到信号后,直接就可以从缓冲区拷贝了。
图解:
这个信号一般是SIGIO。
4. IO多路转接
IO多路转接和信号驱动IO有点像:
- 信号驱动IO:数据准备好了,内核是通过发送信号的方式来通知用户
- IO多路转接:数据准备好了,用户层有三种方式来接收内核的通知
这三种方式分别是:
- select
- poll
- epoll
现在最常用的是epoll,而且epoll还是用起来最简单的。
IO多路转接也不需要等待,等的事情我不操心,等数据准备好了,内核通知就好了,我怎么接收通知呢?或者说怎么拿到通知,处理通知呢?可以有三种方式。关于这三种方式,后续我会出几篇博客,分别讲述。
图解:
5. 异步IO
异步IO比它们都绝,上俩个,它们干脆不等了,异步IO干脆就不管了。异步IO就是将等和拷贝完全托管给内核,等内核把数据拷贝完成后,会通知进程一下。
当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。
图解:
6.“ 等事件”就绪概念
IO操作的核心俩步:
- 等
- 拷贝
那么等这个事情,内核是如何判断等成功了呢?这里说的是socket“等事件就绪条件”
。
- 读事件就绪:
- (1)该套接字接收缓冲区中的数据字节数大于等于套接字接收缓存区低水位。对于TCP和UDP套接字而言,缓冲区低水位的值默认为1。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。我们可以通过使用SO_RCVLOWAT套接字选项(参见setsockopt函数)来设置该套接字的低水位大小。此种描述符就绪(可读)的情况下,当我们使用read/recv等对该套接字执行读操作的时候,套接字不会阻塞,而是成功返回一个大于0的值(即可读数据的大小)。
(2)该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作,将不会阻塞,而是返回0(也就是EOF)。
(3)该套接字是一个listen的监听套接字,并且目前已经完成的连接数不为0。对这样的套接字进行accept操作通常不会阻塞。
(4)有一个错误套接字待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
- 写事件就绪:
- (1)该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓存区低水位标记时,并且该套接字已经成功连接(UDP套接字不需要连接)。对于TCP和UDP而言,这个低水位的值默认为2048,而套接字默认的发送缓冲区大小是8k,这就意味着一般一个套接字连接成功后,就是处于可写状态的。我们可以通过SO_SNDLOWAT套接字选项(参见setsockopt函数)来设置这个低水位。此种情况下,我们设置该套接字为非阻塞,对该套接字进行写操作(如write,send等),将不阻塞,并返回一个正值(例如由传输层接受的字节数,即发送的数据大小)。
(2)该连接的写半部关闭(主动发送FIN包的TCP连接)。对这样的套接字的写操作将会产生SIGPIPE信号。所以我们的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。
(3)使用非阻塞的connect套接字已建立连接,或者connect已经以失败告终。即connect有结果了。
(4)有一个错误的套接字待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
7. 阻塞 VS 非阻塞
- 阻塞IO调用 :在用户进程(线程)中调用执行的时候,进程会等待该IO操作,而使得其他操作无法执行。
- 非阻塞IO调用:在用户进程中调用执行的时候,无论成功与否,该IO操作会立即返回,之后进程可以进行其他操作(当然如果是读取到数据,一般就接着进行数据处理)。
8. 同步 VS 异步
-
同步IO:导致请求进程阻塞,直到I/O操作完成。
-
异步IO:不导致请求进程阻塞。
-
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
-
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.