标签:
一个输入操作通常包括两个不同的阶段:
1,等待数据准备好:
2,从内核中向进程复制数据:
对于一个套接字上的输入操作:
1,等待数据从网络中到达。当所有的等待分组到达时,它被复制到内核中的某个缓冲区。
2,数据从内核缓冲区复制到应用进程缓冲区。
套接口的默认状态是阻塞的。
这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待相应操作完成。
当然,也可以将其设置为非阻塞模式,调用一个函数
fcntl(fd,F_SETFL, flag | 0_NONBLOCK);
我们之前编写的(回射客户端)中就存在好多的函数,其中好多的都是跟阻塞相关的:
read,write,accept,connect,等等(客户端,服务端)
先来看看阻塞式的情况:
一个简单的recvfrom函数:(UNIX网络编程:page:123)
应用进程和内核之间的相互转化。。。。。。
这里选用recvfrom的原因在于:对于UDP而言,传送的是数据报,数据准备好读取的概念比较明确,数据报到达
或者数据报没有到达。但是对于TCP而言,以字节流为单位,来一个字节吗???理解不太容易。。。
还有,就是明确的:从应用进程切换到内核空间中,之后再切换回来的。。。
进程调用recvfrom函数,其系统调用直到数据报到达并且被复制到应用进程的缓冲区中或者发生错误才返回,
最常见的错误是:系统调用被信号中断。我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞
的,recvfrom成功返回后,应用进程开始处理数据报。。。。
1,输入操作,包括read,readv,recv,recvfrom,recvmsg共5个函数。如果某个进程对一个阻塞的TCP套接字
(默认设置)调用这些输入函数之一,而且该套接字的接收缓冲区中没有数据可读,该进程将被投入睡眠,直
到有一些数据可达。既然TCP是字节流协议,该进程的唤醒就是只要有一些数据到达,这些数据既可能是单个
字节,也可以是一个完整的TCP分节中的数据。如果想等到某个固定数目的数据可读为止,那么我们可以调用
我们的readn函数或者指定MSG_WAITALL标志。
对于UDP而言,是数据报协议,如果一个阻塞的UDP套接字的接收缓冲区为空,对它调用输入函数的进程将
被投入睡眠,直到有UDP数据报到达。
比如:对于read函数,从应用进程空间中运行切换到在内核空间中运行,一段时间后再切换会来。。。。。。
对于非阻塞的套接字,如果输入操作不能被满足(对于TCP套接字即至少有一个字节的数据可读,对于UDP
套接字即有一个完整的数据报可读),内核中没有给返回相应的数据,那么,调用将立即返回一个
EWOULDBLOCK(相当于-1)错误。需要数据的话,我们必须持续的调用这个函数(也就是所谓的循环接收
这种循环接收并不是阻塞,从而对CPU造成极大的浪费,这也就是:忙等待,想要等待一定的数据,但是
这些数据并没有到来,并且还占用着CPU,所以,不太经常使用这种I/O模型),
从而将内核空间的值拷贝到用户空间。一旦拷贝完成了,我们的read函数就可以返回了,返回的值也就不是-1了。
2,输出操作,包括write,writev,send, sendto 和 sendmsg共5个函数。对于一个TCP套接字而言,内核将从
应用进程的缓冲区到该套接字的发送缓冲区复制数据。对于阻塞的套接字,如果其发送缓冲区中没有空间,进程
将被投入到睡眠中,直到有空间为止。间接的相当于给了内核,再从内核中拿了出来。
对于一个非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个
EWOULDBLOCK错误。如果其发送缓冲区中还有一些空间,返回值将是内核能够复制到该缓冲区中的字节数。
这个字节数也称为不足计数(short count)。
UDP套接字不存在真正的发送缓冲区。内核只是复制应用进程数据,并把它沿协议栈向下传送,渐次冠以UDP
首部和IP首部。因此,对一个阻塞的UDP套接字(默认设置),输出函数调用将不会因与TCP套接字一样的原
因而阻塞,不过有可能因其他原有而阻塞
3,接收外来连接,即accept函数。如果对一个阻塞的套接字调用accept函数,并且没有新的连接到来,那么进程将
同样的被投入到睡眠状态中。
如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到来,accept调用将立即返回一
个:EWOULDBLOCK错误。
4,发起外出连接,即用于TCP的connect函数(我们知道connect同样可以用于UDP,不过它不能使一个“真正”的连
接建立起来,它只是使内核保存对端的IP地址和端口号。),TCP连接的建立涉及到一个三路握手的过程,而且
connect函数一直要等到客户收到对于自己的SYN的ACK确认为止才返回。这意味着TCP的每个connect总会阻塞其
调用进程至少一个到服务的RTT时间。
如果对一个非阻塞的TCP套接字调用connect,并且连接不能立即建立,那么连接的建立能照样发起(比如:送出
TCP三路握手的第一个分组),不过会返回一个EINPROGRESS错误。注意这个错误不同于上述三个情形中返回的
错误
二:I/O复用模型
I/O复用:类似与我们之前编写的回射服务器,客户端有fgets函数,write函数,read函数(标准输入和TCP套接
字。我们遇到的就是:当服务器进程被杀死后,客户端不能立即关掉,阻塞于(标准输入上)fgets函数,服务
器TCP虽然正确的给客户TCP发送了一个FIN,但是既然客户进程正阻塞于从标准输入读入的过程,它将看不到
这个read返回的EOF,直到从套接字读时为止(可能已经好一段时间了))。这样的话,我们的进程就需要一种
预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程,这就是I/O复用。
是由(select和poll这两个函数支持的)
我们用:SELECT来管理多个文件描述符,一旦其中的一个或者多个文件描述符检测到了有数据到来,那么select
返回,那么这个时候在调用recv,read函数的时候就不会阻塞了,这时就可以从内核中拷贝数据到用户空间了,一
旦拷贝完成,那么就可以返回了,所以说:这里的阻塞的时间只是提前到啦select中,
二:信号驱动式I/O模型
主要就是:我们用信号,让内核在描述符就绪时发送SIGIO信号通知我们,这种就是信号驱动式I/O模型。
过程是这样的:首先,我们需要建立一个SIGIO的信号处理程序,这时,我们需要一个系统调用:sigaction
函数,安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞,当
数据报准备好读取时,内核就为该进程产生一个SIGIO的信号。
这个时候在调用recv,read函数的时候就不会阻塞了,这时就可以从内核中拷贝数据到用户空间了,
,这个时候,相当于是通过read函数将数据从内核中拉到用户空间的,是一种拉的机制,并不是内核
的主动推送会来,一
旦拷贝完成,那么就可以返回了
二:异步I/O
这种I/O的效率最高,是通过aio_read来实现的,这个函数会递交一个请求和一个
缓冲区(buf,应用层的),这个时候即使内核中没有数据到来,那么这个函数
也立刻返回,一旦返回之后,应用进程就可以处理其他的进程了,也就基本上
实现了异步处理。如果有数据到来,那么内核也会自动的将数据拷贝到应用层的
缓冲区,也就是拷贝到之前的buf中去,一旦复制完成,会通过一个信号(递交在
aio_read中)来通知应用进程中的。这个时候上层程序直接处理这些数据。
跟信号驱动式I/O的区别在:异步I/O中接收到信号后,数据已经赋值完成了,已
经赋值到了buf空间中了(内核已经默认操作了),已经返回给用户了,之后不需
要再次调用recv等,是内核直接推送到用户缓冲区中的,这是一种推的机制。效
率相对于之前拉的机制,效率应该能更高一些。。。。
三:select函数
用select函数来管理多个I/O,一旦其中的一个I/O或多个I/O检测到我们所感兴趣的事件发生时,那么select函数
返回,返回值为检测到的事件的个数。并且返回那些I/O发生了事件。遍历这些事件,进而处理它
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
有数据可读的套接口,放到这个集合当中。一旦有数据可读,那么select返回。同理:第三个参数是可写的集合,
第四个参数是异常的集合。
第五个参数:指定超时时间,其中timeval结构用于指定这段时间的秒数和微秒数。
struct timeval { long tv_sec; //seconds long tv_usec; //microseconds }
来之前事件还没有产生,那么也会返回,这个时候返回的是0,
select返回失败为-1。
第一个参数:为存放到这些集合中最大描述符加1。(readset集合中存放(1,3,8事件),writest集合中存放
(4,9),exceptset中不存方),那么此刻maxfdp1就是10
关于select:我们经常会用到以下四个宏:
void FD_ZERO(fd_set *fdset); //clear all bits in fdset void FD_SET(int fd, fd_set *faset); //turn on the bit for fd in fdset void FD_CLR(int fd, fd_set *fdset); //turn off the bit for fd in fdset; int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset?
本文大部分内容摘自:UNIX网络编程
标签:
原文地址:http://blog.csdn.net/msdnwolaile/article/details/51865681