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步都可以做其他工作。