Linux下的系统I/O通过以下几个函数实现: open(),close(),recv(),send(),lseek(),read(),write()
要是我们在服务器程序中对监听套接字不进行非阻塞设置,那么整个程序中调用以上函数处理I/O事件默认就是阻塞的.
阻塞I/O
对于阻塞读,即调用read,recv等,将数据从内核态复制到用户态.内核缓冲区没数据就一直等,不做其他事情.有数据的话,就按照有多少读多少的原则,读完立即返回执行后面的操作.
对于阻塞写,即调用send,write等,将数据从用户态复制到内核态.因为知道要写的数据量,所以数据没完成复制的话,就一直阻塞,直到完成复制才返回.
非阻塞I/O
对于非阻塞I/O读,内核缓冲区中要是有数据,处理方法和阻塞I/O是一样的.要是没有数据的话,还是在检测完内核缓冲区后会返回一个错误码,用户可以在程序中对于不同的错误码进行决策,即是否进行再次读取操作.常见的错误吗就是errno,EWOULDBLOCK等
对于非阻塞I/O写,就是能写多少就写多少,因为有时候考虑到网络拥塞情况,写的量根据网络层的内存情况来决定,要是没有足够的空间,就会出现写不完的情况,对于阻塞I/O,写操作除非被中断,否则一直等到写完为止;而非阻塞I/O是能写多少算多少.
无论是阻塞还是非阻塞,读系统调用要是返回0的话,就表明连接已经断开.就不必进行读取了.在程序中对套接字进行以下设置使程序中系统调用成为非阻塞:
int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
或者
recv(sockfd, buff, buff_size, MSG_DONTWAIT);
send(sockfd, buff, buff_size, MSG_DONTWAIT);
I/O复用模型
I/O复用主要是由系统调用的select和poll实现的,它们可以阻塞多个I/O操作,内核对select负责的多个socket进行监视,要是有某个socket数据准备好,select返回该套接字的可读或者可写的信息,系统调用recv来进行处理.
信号驱动式I/O
在程序中安装一个信号处理函数,当用户进程收到来自内核的数据已准备就绪的信号时,进程可以在相应的信号处理函数中调用recv进行处理.当然在没有收到信号之前,进程继续运行并不阻塞.
异步I/O
系统调用read之后,可以立即做其他的事,在内核的角度来说,当其接收到一个read之后,首先会立刻返回,所以用户进程不会阻塞,当内核将数据准备好并复制到用户内存之后,内核会通过信号来通知进程,表示这一次read已经完成.我们常说的同步I/O就不一样,要是这个I/O操作没有完成,他就使这个进程阻塞.
结合上面基本的I/O操作我们已经介绍完了,总结:
信号驱动I/O和异步I/O两者都需要内核通过信号通知进程,前者通知进程何时该通过系统调用处理来自内核的数据,即该启动一个I/O操作,而后者则是通知进程I/O操作何时完成.
区别阻塞和非阻塞的关键:进行I/O操作的系统调用是否立即返回.返回为非阻塞,否则为阻塞.
区别同步与异步:I/O操作是否使进程阻塞,阻塞是同步,否则是异步.