Linux中一切皆檔案,不論是我們儲存在磁碟上的字元檔案,可執行檔案還是我們的接入電腦的I/O裝置等都被VFS抽象成了檔案,比如標準輸入裝置預設是鍵盤,我們在操作標準輸入裝置的時候,其實操作的是預設開啟的一個檔案描述符是0的檔案,而一切軟體操作硬體都需要通過OS,而OS操作一切硬體都需要相應的驅動程式,這個驅動程式裡配置了這個硬體的相應配置和使用方法。Linux的I/O分為阻塞I/O,非阻塞I/O,I/O多路複用,訊號驅動I/O四種。對於I/O裝置的驅動,一般都會提供關於阻塞和非阻塞兩種配置。我們最常見的I/O裝置之一–鍵盤(標準輸入裝置)的驅動程式預設是阻塞的。
多路複用就是為了使程式能夠從多個阻塞I/O中獲得自己想要的資料並繼續執行接下來的任務。其主要的思路就是同時監視多個檔案描述符,如果有檔案描述符的設定狀態的被觸發,就繼續執行程式,如果沒有任何一個檔案描述符的設定狀態被觸發,程式進入sleep
多路複用的一個主要用途就是實現”I/O多路複用併發伺服器”,和多執行緒併發或者多程式併發相比,這種伺服器的系統開銷更低,更適合做web伺服器,但是由於其並沒有實現真正的多工,所以當壓力大的時候,部分使用者的請求響應會較慢
阻塞I/O
阻塞I/O,就是當程式試圖訪問這個I/O裝置而這個裝置並沒有準備好的時候,裝置的驅動程式會通過核心讓這個試圖訪問的程式進入sleep狀態。阻塞I/O的一個好處就是可以大大的節約CPU時間,因為一旦一個程式試圖訪問一個沒有準備好的阻塞I/O,就會進入sleep狀態,而進入sleep狀態的程式是不在核心的程式排程連結串列中,直到目標I/O準備好了將其喚醒並加入排程連結串列,這樣就可以節約CPU時間。當然阻塞I/O也有其固有的缺點,如果程式試圖訪問一個阻塞I/O,但是否訪問成功並不對接下來的任務有決定性影響,那麼直接使其進入sleep狀態顯然會延誤其任務的完成。
- 典型的預設阻塞IO有標準輸入裝置,socket裝置,管道裝置等,當我們使用
gets()
,scanf()
,read()
等操作請求這些IO時而IO並沒有資料流入,就會造成程式的sleep。 程式會一直阻塞下去直到接收緩衝區中有資料可讀,此時核心再去喚醒該程式,通過相應的函式從中獲取資料。如果阻塞過程中對方發生故障,那麼這個程式將會永遠阻塞下去。 - 寫操作時發生阻塞的情況要比讀操作少,主要發生在要寫入的緩衝區的大小小於要寫入的資料量的情況下,這時寫操作將不進行任何任何拷貝工作,將發生阻塞。一旦傳送緩衝區內有足夠的空間,核心將喚醒程式,將資料從使用者緩衝區中拷貝到相應的傳送資料緩衝區。udp不用等待確認,沒有實際的傳送緩衝區,所以udp協議中不存在傳送緩衝區滿的情況,在udp套接字上執行的寫操作永遠都不會阻塞
現假設一個程式希望通過三個管道中任意一箇中讀取資料並顯示,虛擬碼如下
read(pipe_0,buf,sizeof(buf)); //sleep
print buf;
read(pipe_1,buf,sizeof(buf));
print buf;
read(pipe_2,buf,sizeof(buf));
print buf;
由於管道是阻塞I/O,所以如果pipe_0沒有資料流入,程式就是在第一個read()
處進入sleep狀態而即使pipe_1和pipe_2有資料流入也不會被讀取。
如果我們使用下述程式碼重新設定管道的阻塞屬性,顯然,如果三個管道都沒有資料流入,那麼程式就無法獲得請求的資料而繼續執行,倘若這些資料很重要(所以我們才要用阻塞I/O),那結果就會十分的糟糕,改為輪詢卻又大量的佔據CPU時間。
int fl = fcntl(pipe_fd, F_GETFL);
fcntl(pipe_fd, F_SETFL, fl | O_NONBLOCK);
如何讓程式同時監視三個管道,其中一個有資料就繼續執行而不會sleep,如果全部沒有資料流入再sleep,就是多路複用技術需要解決的問題。
非阻塞I/O
非阻塞I/O就是當一個程式試圖訪問一個I/O裝置的時候,無論是否從中獲取了請求的資料都會返回並繼續執行接下來的任務。,但非常適合請求是否成功對接下來的任務影響不大的I/O請求。但如果訪問一個非阻塞I/O,但這個請求如果失敗對程式接下來的任務有致命影響,最粗暴的就是使用while(1){read()}
輪詢。顯然,這種方式會佔用大量的CPU時間。對於非阻塞IO,除了直接返回,一個更重要的應用就是利用IO多路複用機制同時監視多個非阻塞IO。
select機制
select是一種非常”古老”的同步I/O介面,但是提供了一種很好的I/O多路複用的思路
模型
fd_set //建立fd_set物件,將來從中增減需要監視的fd
FD_ZERO() //清空fd_set物件
FD_SET() //將一個fd加入fd_set物件中
select() //監視fd_set物件中的檔案描述符
pselect() //先設定訊號遮蔽,再監視
FD_ISSET() //測試fd是否屬於fd_set物件
FD_CLR() //從fd_set物件中刪除fd
Note:
- select的第一個引數
nfds
是指集合中的最大的檔案描述符+1
,因為select會無差別遍歷整個檔案描述符表直到找到目標,而檔案描述符是從0開始的,所以一共是集合中的最大的檔案描述符+1
次。 - 上一條導致了這種機制的低效,如果需要監視的檔案描述符是0和100那麼每一次都會遍歷101次
- select()每次返回都會修改fd_set,如果要迴圈select(),需要先對初始的fd_set進行備
例子_I/O多路複用併發伺服器
關於server本身的程式設計模型,參見tcp/ip協議伺服器模型和udp/ip協議伺服器模型這裡僅是使用select實現偽並行的部分模型
#define BUFSIZE 100
#define MAXNFD 1024
int main()
{
/***********伺服器的listenfd已經準本好了**************/
fd_set readfds;
fd_set writefds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(listenfd, &readfds);
fd_set temprfds = readfds;
fd_set tempwfds = writefds;
int maxfd = listenfd;
int nready;
char buf[MAXNFD][BUFSIZE] = {0};
while(1){
temprfds = readfds;
tempwfds = writefds;
nready = select(maxfd+1, &temprfds, &tempwfds, NULL, NULL)
if(FD_ISSET(listenfd, &temprfds)){
//如果監聽到的是listenfd就進行accept
int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
//將新accept的scokfd加入監聽集合,並保持maxfd為最大fd
FD_SET(sockfd, &readfds);
maxfd = maxfd>sockfd?maxfd:sockfd;
//如果意見檢查了nready個fd,就沒有必要再等了,直接下一個迴圈
if(--nready==0)
continue;
}
int fd = 0;
//遍歷檔案描述符表,處理接收到的訊息
for(;fd<=maxfd; fd++){
if(fd == listenfd)
continue;
if(FD_ISSET(fd, &temprfds)){
int ret = read(fd, buf[fd], sizeof buf[0]);
if(0 == ret){ //客戶端連結已經斷開
close(fd);
FD_CLR(fd, &readfds);
if(maxfd==fd)
--maxfd;
continue;
}
//將fd加入監聽可寫的集合
FD_SET(fd, &writefds);
}
//找到了接收訊息的socket的fd,接下來將其加入到監視寫的fd_set中
//將在下一次while()迴圈開始監視
if(FD_ISSET(fd, &tempwfds)){
int ret = write(fd, buf[fd], sizeof buf[0]);
printf("ret %d: %d
", fd, ret);
FD_CLR(fd, &writefds);
}
}
}
close(listenfd);
}
poll機制
poll是一種基於select的改良機制,其針對select的一些缺陷進行了重新設計,包括不需要備份fd_set等等,但是依然是遍歷整個檔案描述符表,效率較低
模型
struct pollfd fds //建立一個pollfd型別的陣列
fds[0].fd //向fds[0]中放入需要監視的fd
fds[0].events //向fds[0]中放入需要監視的fd的觸發事件
POLLIN //I/O有輸入
POLLPRI //有緊急資料需要讀取
POLLOUT //I/O可寫
POLLRDHUP //流式套接字連線斷開或套接字處於半關閉狀態
POLLERR //錯誤條件(僅針對輸出)
POLLHUP //掛起(僅針對輸出)
POLLNVAL //無效的請求:fd沒有被開啟(僅針對輸出)
例子_I/O多路複用併發伺服器
/* ... */
int main()
{
/* ... */
struct pollfd myfds[MAXNFD] = {0};
myfds[0].fd = listenfd;
myfds[0].events = POLLIN;
int maxnum = 1;
int nready;
//準備二維陣列buf,每個fd使用buf的一行,資料干擾
char buf[MAXNFD][BUFSIZE] = {0};
while(1){
//poll直接返回event被觸發的fd的個數
nready = poll(myfds, maxnum, -1)
int i = 0;
for(;i<maxnum; i++){
//poll通過將相應的二進位制位置一來表示已經設定
//如果下面的條件成立,表示revent[i]裡的POLLIN位已經是1了
if(myfds[i].revents & POLLIN){
if(myfds[i].fd == listenfd){
int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
//將新accept的scokfd加入監聽集合
myfds[maxnum].fd = sockfd;
myfds[maxnum].events = POLLIN;
maxnum++;
//如果意見檢查了nready個fd,就直接下一個迴圈
if(--nready==0)
continue;
}
else{
int ret = read(myfds[i].fd, buf[myfds[i].fd], sizeof buf[0]);
if(0 == ret){ //如果連線斷開了
close(myfds[i].fd);
//初始化將檔案描述符表所有的檔案描述符標記為-1
//close的檔案描述符也標記為-1
//開啟新的描述符時從表中搜尋第一個-1
//open()就是這樣實現始終使用最小的fd
//這裡為了演示並沒有使用這種機制
myfds[i].fd = -1;
continue;
}
myfds[i].events = POLLOUT;
}
}
else if(myfds[i].revents & POLLOUT){
int ret = write(myfds[i].fd, buf[myfds[i].fd], sizeof buf[0]);
myfds[i].events = POLLIN;
}
}
}
close(listenfd);
}
epoll
epoll在poll基礎上實現的更為健壯的介面,它每次只會遍歷我們關心的檔案描述符,也是現在主流的web伺服器使用的多路複用技術,epoll一大特色就是支援EPOLLET(邊沿觸發)
和EPOLLLT (水平觸發)
,前者表示如果讀取之後緩衝區還有資料,那麼只要讀取結束,剩餘的資料也會丟棄,而後者表示裡面的資料不會丟棄,下次讀的時候還在,預設是EPOLLLT
模型
epoll_create() //建立epoll物件
struct epoll_event //準備事件結構體和事件結構體陣列
event.events
event.data.fd ...
epoll_ctl() //配置epoll物件
epoll_wait() //監控epoll物件中的fd及其相應的event
例子_I/O多路複用併發伺服器
/* ... */
int main()
{
/* ... */
/* 建立epoll物件 */
int epoll_fd = epoll_create(1024);
//準備一個事件結構體
struct epoll_event event = {0};
event.events = EPOLLIN;
event.data.fd = listenfd; //data是一個共用體,除了fd還可以返回其他資料
//ctl是監控listenfd是否有event被觸發
//如果發生了就把event通過wait帶出。
//所以,如果event裡不標明fd,我們將來獲取就不知道哪個fd
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listenfd, &event);
struct epoll_event revents[MAXNFD] = {0};
int nready;
char buf[MAXNFD][BUFSIZE] = {0};
while(1){
//wait返回等待的event發生的數目
//並把相應的event放到event型別的陣列中
nready = epoll_wait(epoll_fd, revents, MAXNFD, -1)
int i = 0;
for(;i<nready; i++){
//wait通過在events中設定相應的位來表示相應事件的發生
//如果輸入可用,那麼下面的這個結果應該為真
if(revents[i].events & EPOLLIN){
//如果是listenfd有資料輸入
if(revents[i].data.fd == listenfd){
int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
struct epoll_event event = {0};
event.events = EPOLLIN;
event.data.fd = sockfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event);
}
else{
int ret = read(revents[i].data.fd, buf[revents[i].data.fd], sizeof buf[0]);
if(0 == ret){
close(revents[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, revents[i].data.fd, &revents[i]);
}
revents[i].events = EPOLLOUT;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, revents[i].data.fd, &revents[i]);
}
}
else if(revents[i].events & EPOLLOUT){
int ret = write(revents[i].data.fd, buf[revents[i].data.fd], sizeof buf[0]);
revents[i].events = EPOLLIN;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, revents[i].data.fd, &revents[i]);
}
}
}
close(listenfd);
}