Linux的五种IO模型
操作系统将内存分为用户空间和内核空间,内核空间中存放的是内核代码和数据,例如进程、线程以及内存的管理,用户空间保存的是用户程序的代码和数据,一般是指应用程序。操作系统和驱动程序运行在内核空间,用户程序在用户空间运行,因此两者不能通过简单地指针传递完成数据传输,必须通过系统调用与内核协助完成IO。
内核会为每个IO设备维护一个缓冲区,当进行系统IO操作时,内核会先查看缓冲区中是否有相应的缓冲数据,如果没有则到设备中读取。完成一次网络输入操作一般包括两个阶段:1、等待网络数据到达网卡->并读取数据到内核临时缓冲区。2、从内核临时缓冲区复制数据到用户空间。
1、阻塞IO
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好,如果没有准备好则阻塞一直等待,进程不会去做其他工作。如果数据准备好,则将数据从内核空间拷贝到用户空间。
优点:能够保证所有的数据能够完整地读取。缺点:阻塞后,进程不能去做其他工作,导致系统浪费较大。
注意这里socket调用返回的套接字默认是阻塞的
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
//注意这里没有进行返回值的错误处理
#define BUFSIZE 64
void blockSocket(){
char buf[BUFSIZE];
uint16_t port = 2020;
int fd = open("1.txt",O_RDWR|O_APPEND|O_CREATE);
int sockfd = ::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
setnbAndcoeHel(sockfd);
struct sockaddr_in serSock;
serSock.sin_family = AF_INET;
serSock.sin_port = htons(port);
serSock.sin_addr.s_addr = htonl(INADDR_ANY);
::bind(sockfd,&sockSock,static_cast<socklen_t>(sizeof(sockaddr_in)));
::listen(sockfd,1024);
while(true){
int ret = recv(sockfd,buf,BUFSIZE,0);
if(ret <= 0) break;
write(fd,buf,ret);
}
}
2、非阻塞IO
我们可以将socket接口设置为非阻塞模式,告诉内核,在数据没有准备好时进行IO,不要将进程阻塞,而是返回给用户程序一个错误,由用户程序控制不断测试数据是否已经准备好(“轮询”),这同样是一种耗费CPU的方式。
#include <fcntl.h>
void setnbAndcoeHel(int socketfd){
int flags = ::fcntl(socketfd,F_GETFL,0);
flags |= O_NONBLOCK;
int res = ::fcntl(socketfd,F_SETFL,flags);
flags = ::fcntl(socketfd,F_GETFL,0);
flags |= FD_CLOEXEC;
res = ::fcntl(socketfd,F_SETFL,flags);
(void)res;
}
3、IO复用
我们平时用到的IO复用多数是select、poll以及epoll,这些函数同样会阻塞当前进程,与前两种不同的是,IO复用是阻塞在select、poll以及epoll这些系统调用上,而不是阻塞在recv(),read()这些IO操作上,并且IO复用可以监听多个套接口,而不是像前两种每次只能监听一个套接口。这里IO有两次阻塞,一次是select这些系统调用监听套接口时,另一次是读取临时缓冲区中的数据时。
#include <sys/epoll.h>
#include <vector>
int epollFd = epoll_create1(1024);
int add(int fd,int events){
struct epoll_event event;
event.events = events;
int ret = ::epoll_ctl(epollFd,EPOLL_CTL_ADD,fd,&event);
return ret;
}
int del(int fd,int events){
struct epoll_event event;
event.events = events;
int ret = ::epoll_ctl(epollFd,EPOLL_CTL_DEL,fd,&event);
return ret;
}
int mod(int fd,int events){
struct epoll_event event;
event.events = events;
int ret = ::epoll_ctl(epollFd,EPOLL_CLT_MOD,fd,&event);
}
int wait(int timeouts){
std::vector<struct epoll_event>events_;
int num = ::epoll_wait(&*events_.begin(),static_cast<int>(events_.size()),timeouts);
return num;
}
4、信号驱动IO
使用信号驱动IO,我们可以通过sigaction系统调用注册一个信号处理函数,进程可以继续运行不阻塞,当我们所监听的套接口数据就绪时,进程会受到一个SIGIO信号,此时进程执行信号处理函数,我们可以在信号处理函数将数据从内核的临时缓冲区拷贝到用户空间当中。
#include <sys/socket.h>
#include <errno.h>
#include <stdint.h>
#include <fcntl.h>
#include <ioctl.h>
#include <sys/arpa.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#define RECV_LEN 128
char recvBuf[RECV_LEN];
int listenFd;
static void sigio_handler(int signo){
if(signo != SIGIO) return;
printf("recv SIGIO(%d) from kernel\n",signo);
while(true){
int ret = recvfrom(listenFd,recvBuf,RECV_LEN,0,nullptr,nullptr);
if(ret <= 0){
if(errno == EINTR || errno == EAGAIN) continue;
else return;
}
printf("recv = %s\n",recvBuf);
}
}
static int create_socket(){
uint16_t port = 2020;
int sockfd = ::socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(port);
serAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(::bind(sockfd,static_cast<sockaddr*>(serAddr),sizeof(serAddr)) < 0){
return -1;
}
return sockfd;
}
void sigio_socket_init(int sockfd){
//设置套接字为非阻塞模式
int flags = fcntl(sockfd,F_GETFL,0);
flags |= O_NONBLOCK;
fcntl(sockfd,F_SETFL,flags);
//设置信号处理函数
signal(SIGIO,sigio_handler);
//设置该套接字的属主
int ret = fcntl(sockfd,F_SETOWN,getpid());
if(ret < 0){
perror("fcntl error");
exit(-1);
}
//开启该套接字的信号驱动式I/O
int on;
ret = ioctl(sockfd,FIOASYNC,&on);
if(ret < 0){
perror("ioclt error");
exit(-1);
}
}
int main(){
listenFd = create_socket();
if(listenfd < 0){
perror("prepare error");
exit(-1);
}
sigio_socket_init(listenFd);
while(1){
sleep(1);
}
return 0;
}
5、异步IO
异步IO和信号驱动IO的不同之处在于,信号驱动IO的SIGIO信号是通知主进程何时可以进行IO操作了,而异步IO是通知主进程何时完成了IO操作,也就是说内核不仅完成了IO通知的功能,还完成了数据从内核临时缓冲区到用户空间的转移工作,待完成这些IO操作之后,内核通过状态、通知和回调来通知主进程操作结果。
我们可以调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,并且在等待I/O完成期间,我们的进程不被阻塞。
需要注意的是不论是阻塞IO还是非阻塞IO都是同步IO模型,区别就在于系统调用是否需要等待数据就绪再继续运行,即是否等待第1步完成后再继续运行。异步IO的第1、2步都由内核完成,不会占用主进程的运行顺序。因此非阻塞IO能够让你在第1步时做其他工作,异步IO能够让你在第1、2步都可以做其他工作。