linux文件復(fù)用

本文摘抄自linux基礎(chǔ)編程

IO概念

Linux的內(nèi)核將所有外部設(shè)備都可以看做一個文件來操作。那么我們對與外部設(shè)備的操作都可以看做對文件進(jìn)行操作。
我們對一個文件的讀寫,都通過調(diào)用內(nèi)核提供的系統(tǒng)調(diào)用;內(nèi)核給我們返回一個filede scriptor(fd,文件描述符)。而對一個socket的讀寫也會有相應(yīng)的描述符,稱為socketfd(socket描述符)。描述符就是一個數(shù)字,指向內(nèi)核中一個結(jié)構(gòu)體(文件路徑,數(shù)據(jù)區(qū),等一些屬性)。那么我們的應(yīng)用程序?qū)ξ募淖x寫就通過對描述符的讀寫完成
linux將內(nèi)存分為內(nèi)核區(qū),用戶區(qū)。linux內(nèi)核給我們管理所有的硬件資源,應(yīng)用程序通過調(diào)用系統(tǒng)調(diào)用和內(nèi)核交互,達(dá)到使用硬件資源的目的。應(yīng)用程序通過系統(tǒng)調(diào)用read發(fā)起一個讀操作,這時候內(nèi)核創(chuàng)建一個文件描述符,并通過驅(qū)動程序向硬件發(fā)送讀指令,并將讀的的數(shù)據(jù)放在這個描述符對應(yīng)結(jié)構(gòu)體的內(nèi)核緩存區(qū)中,然后再把這個數(shù)據(jù)讀到用戶進(jìn)程空間中,這樣完成了一次讀操作;
但是大家都知道I/O設(shè)備相比cpu的速度是極慢的。linux提供的read系統(tǒng)調(diào)用,也是一個阻塞函數(shù)。這樣我們的應(yīng)用進(jìn)程在發(fā)起read系統(tǒng)調(diào)用時,就必須阻塞,就進(jìn)程被掛起而等待文件描述符的讀就緒,那么什么是文件描述符讀就緒,什么是寫就緒?

  • 讀就緒:就是這個文件描述符的接收緩沖區(qū)中的數(shù)據(jù)字節(jié)數(shù)大于等于套接字接收緩沖區(qū)低水位標(biāo)記的當(dāng)前大小;
  • 寫就緒:該描述符發(fā)送緩沖區(qū)的可用空間字節(jié)數(shù)大于等于描述符發(fā)送緩沖區(qū)低水位標(biāo)記的當(dāng)前大小。(如果是socket fd,說明上一個數(shù)據(jù)已經(jīng)發(fā)送完成)。

接收低水位標(biāo)記和發(fā)送低水位標(biāo)記:由應(yīng)用程序指定,比如應(yīng)用程序指定接收低水位為64個字節(jié)。那么接收緩沖區(qū)有64個字節(jié),才算fd讀就緒;
綜上所述,一個基本的IO,它會涉及到兩個系統(tǒng)對象,一個是調(diào)用這個IO的進(jìn)程對象,另一個就是系統(tǒng)內(nèi)核(kernel)。當(dāng)一個read操作發(fā)生時,它會經(jīng)歷兩個階段:
1:通過read系統(tǒng)調(diào)用想內(nèi)核發(fā)起讀請求。內(nèi)核向硬件發(fā)送讀指令,并等待讀就緒。
2:內(nèi)核把將要讀取的數(shù)據(jù)復(fù)制到描述符所指向的內(nèi)核緩存區(qū)中。將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到用戶進(jìn)程空間中。

IO模型

在linux系統(tǒng)下面,根據(jù)IO操作的是否被阻塞以及同步異步問題進(jìn)行分類,可以得到下面五種IO模型:
1、阻塞I/O模型
最流行的I/O模型是阻塞I/O模型,缺省情形下,所有文件操作都是阻塞的。我們以套接口為例來講解此模型。在進(jìn)程空間中調(diào)用recvfrom,其系統(tǒng)調(diào)用直到數(shù)據(jù)報到達(dá)且被拷貝到應(yīng)用進(jìn)程的緩沖區(qū)中或者發(fā)生錯誤才返回,期間一直在等待。我們就說進(jìn)程在從調(diào)用recvfrom開始到它返回的整段時間內(nèi)是被阻塞的。

2、非阻塞I/O模型
進(jìn)程把一個套接口設(shè)置成非阻塞是在通知內(nèi)核:當(dāng)所請求的I/O操作不能滿足要求時候,不把本進(jìn)程投入睡眠,而是返回一個錯誤。也就是說當(dāng)數(shù)據(jù)沒有到達(dá)時并不等待,而是以一個錯誤返回。

3、I/O復(fù)用模型
linux提供select/poll,進(jìn)程通過將一個或多個fd傳遞給select或poll系統(tǒng)調(diào)用,阻塞在select;這樣select/poll可以幫我們偵測許多fd是否就緒。但是select/poll是順序掃描fd是否就緒,而且支持的fd數(shù)量有限
linux還提供了一個epoll系統(tǒng)調(diào)用,epoll是基于事件驅(qū)動方式,而不是順序掃描,當(dāng)有fd就緒時,立即回調(diào)函數(shù)rollback

4、信號驅(qū)動異步I/O模型
首先開啟套接口信號驅(qū)動I/O功能, 并通過系統(tǒng)調(diào)用sigaction安裝一個信號處理函數(shù)(此系統(tǒng)調(diào)用立即返回,進(jìn)程繼續(xù)工作,它是非阻塞的)。當(dāng)數(shù)據(jù)報準(zhǔn)備好被讀時,就為該進(jìn)程生成一個SIGIO信號。隨即可以在信號處理程序中調(diào)用recvfrom來讀數(shù)據(jù)報,井通知主循環(huán)數(shù)據(jù)已準(zhǔn)備好被處理中。也可以通知主循環(huán),讓它來讀數(shù)據(jù)報。

5、異步I/O模型
告知內(nèi)核啟動某個操作,并讓內(nèi)核在整個操作完成后(包括將數(shù)據(jù)從內(nèi)核拷貝到用戶自己的緩沖區(qū))通知我們。這種模型與信號驅(qū)動模型的主要區(qū)別是:信號驅(qū)動I/O:由內(nèi)核通知我們何時可以啟動一個I/O操作;異步I/O模型:由內(nèi)核通知我們I/O操作何時完成

非阻塞IO詳解

通過上面,我們知道,所有的IO操作在默認(rèn)情況下,都是屬于阻塞IO。盡管上圖中所示的反復(fù)請求的非阻塞IO的效率底下(需要反復(fù)在用戶空間和進(jìn)程空間切換和判斷,把一個原本屬于IO密集的操作變?yōu)镮O密集和計算密集的操作),但是在后面IO復(fù)用中,需要把IO的操作設(shè)置為非阻塞的,此時程序?qū)枞趕elect和poll系統(tǒng)調(diào)用中。把一個IO設(shè)置為非阻塞IO有兩種方式:在創(chuàng)建文件描述符時,指定該文件描述符的操作為非阻塞在創(chuàng)建文件描述符以后,調(diào)用fcntl()函數(shù)設(shè)置相應(yīng)的文件描述符為非阻塞
創(chuàng)建描述符時,利用open函數(shù)和socket函數(shù)的標(biāo)志設(shè)置返回的fd/socket描述符為O_NONBLOCK。

int sd=socket(int domain, int type|O_NONBLOCK, int protocol);  
int fd=open(const char *pathname, int flags|O_NONBLOCK);  

創(chuàng)建描述符后,通過調(diào)用fcntl函數(shù)設(shè)置描述符的屬性為O_NONBLOCK

#include <unistd.h>  
#include <fcntl.h>  
  
int fcntl(int fd, int cmd, ... /* arg */ );  
  
//例子  
if (fcntl(fd, F_SETFL, fcntl(sockfd, F_GETFL, 0)|O_NONBLOCK) == -1) {  
    return -1;  
}  
    return 0;  
}  

IO復(fù)用詳解

在IO編程過程中,當(dāng)需要處理多個請求的時,可以使用多線程和IO復(fù)用的方式進(jìn)行處理。上面的圖介紹了整個IO復(fù)用的過程,它通過把多個IO的阻塞復(fù)用到一個select之類的阻塞上,從而使得系統(tǒng)在單線程的情況下同時支持處理多個請求和多線程/進(jìn)程比較,I/O多路復(fù)用的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要建立新的進(jìn)程或者線程,也不必維護(hù)這些線程和進(jìn)程。IO復(fù)用常見的應(yīng)用場景:

  • 客戶程序需要同時處理交互式的輸入和服務(wù)器之間的網(wǎng)絡(luò)連接。
  • 客戶端需要對多個網(wǎng)絡(luò)連接作出反應(yīng)。
  • 服務(wù)器需要同時處理多個處于監(jiān)聽狀態(tài)和多個連接狀態(tài)的套接字
    服務(wù)器需要處理多種網(wǎng)絡(luò)協(xié)議的套接字。

目前支持I/O復(fù)用的系統(tǒng)調(diào)用有select、pselect、poll、epoll,下面幾小結(jié)分別來學(xué)習(xí)一下select和epoll的使用。

select

#include <sys/select.h>  
  
int select(int maxfdps, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);  
  
struct timeval{  
    long tv_sec; //秒  
    long tv_usec; //微秒  
};  
void FD_CLR(int fd, fd_set *set); //將一個給定的文件描述符從集合中刪除  
int  FD_ISSET(int fd, fd_set *set);  // 檢查集合中指定的文件描述符是否可以讀寫 ?  
void FD_SET(int fd, fd_set *set); //將一個給定的文件描述符加入集合之中  
void FD_ZERO(fd_set *set);//清空集合  

struct timeval *time結(jié)構(gòu)體告知內(nèi)核等待所指定描述字中的任何一個就緒可花多少時間
參數(shù)取值:
(1)(struct timeval *)0:永遠(yuǎn)等待下去,僅在有一個描述字準(zhǔn)備好I/O時才返回。
(2)struct timeval *time:在有一個描述字準(zhǔn)備好I/O時返回,但是不超過由該參數(shù)所指向的timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)。如果超過時間,沒有描述字準(zhǔn)備好,那就返回0。如果秒=微秒=0,檢查描述字后立即返回,此時相當(dāng)于輪詢。
中間的三個參數(shù)readset、writeset和exceptset指定我們要讓內(nèi)核測試讀、寫和異常條件的描述字。如果我們對某一個的條件不感興趣,就可以把它設(shè)為空指針。
fd_set可以理解為一個集合,這個集合中存放的是文件描述符,可通過上面的四個宏進(jìn)行設(shè)置(注意,fd_set不是struct fd_set。。。 剛剛開始調(diào)試程序就犯錯誤,返回storage size of 'fds' isn't known)。
第一個參數(shù)maxfdp1指定待測試的描述字個數(shù),它的值是待測試的最大描述字加1(因此我們把該參數(shù)命名為maxfdp1),描述字0、1、2...maxfdp1-1均將被測試。

pselect

#include <sys/select.h>  
  
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,  const sigset_t *sigmask);  
struct timespec{  
    time_t tv_sec; //秒  
    long tv_nsec; //納秒  
};  

比較select和pselect函數(shù),我們發(fā)現(xiàn)在原型上面有兩個不同:
1、pselect使用timespec結(jié)構(gòu),而不使用timeval結(jié)構(gòu)。t
imespec結(jié)構(gòu)是POSIX的又一個發(fā)明。 這兩個結(jié)構(gòu)的區(qū)別在于第二個成員:新結(jié)構(gòu)的該成員tv_nsec指定納秒數(shù),而舊結(jié)構(gòu)的該成員tv_usec指定微秒數(shù)。
2、pselect函數(shù)增加了第六個參數(shù):一個指向信號掩碼的指針。該參數(shù)允許程序先禁止遞交某些信號,再測試由這些當(dāng)前被禁止的信號處理函數(shù)設(shè)置的全局變量,然后調(diào)用pselect,告訴它重新設(shè)置信號掩碼。

首先我們看看信號掩碼的概念:在POSIX下,每個進(jìn)程有一個信號掩碼(signal mask)。簡單地說,信號掩碼是一個“位圖”,其中每一位都對應(yīng)著一種信號。如果位圖中的某一位為1,就表示在執(zhí)行當(dāng)前信號的處理程序期間相應(yīng)的信號暫時被“屏蔽”,使得在執(zhí)行的過程中不會嵌套地響應(yīng)那種信號。為什么對某一信號進(jìn)行屏蔽呢?我們來看一下對CTRL_C的處理。大家知道,當(dāng)一個程序正在運行時,在鍵盤上按一下CTRL_C,內(nèi)核就會向相應(yīng)的進(jìn)程發(fā)出一個SIGINT 信號,而對這個信號的默認(rèn)操作就是通過do_exit()結(jié)束該進(jìn)程的運行。但是,有些應(yīng)用程序可能對CTRL_C有自己的處理,所以就要為SIGINT另行設(shè)置一個處理程序,使它指向應(yīng)用程序中的一個函數(shù),在那個函數(shù)中對CTRL_C這個事件作出響應(yīng)。但是,在實踐中卻發(fā)現(xiàn),兩次CTRL_C事件往往過于密集,有時候剛剛進(jìn)入第一個信號的處理程序,第二個SIGINT信號就到達(dá)了,而第二個信號的默認(rèn)操作是殺死進(jìn)程,這樣,第一個信號的處理程序根本沒有執(zhí)行完。為了避免這種情況的出現(xiàn),就在執(zhí)行一個信號處理程序的過程中將該種信號自動屏蔽掉。所謂“屏蔽”,與將信號忽略是不同的,它只是將信號暫時“遮蓋”一下,一旦屏蔽去掉,已到達(dá)的信號又繼續(xù)得到處理

在我們開發(fā)過程中,如果進(jìn)程阻塞于select函數(shù),此時該阻塞被信號所打斷,select返回-1,errno=EINTR的錯誤。由于這個原因,我們才有了pselect函數(shù)在信號上的優(yōu)化處理。因此在信號和select都被使用的系統(tǒng)里,pselect有其作用。否則和select沒有任何區(qū)別。

poll

#include <poll.h>  
  
int poll(struct pollfd *fds, nfds_t nfds, int timeout);  
    struct pollfd {  
        int fd; /* file descriptor */  
        short events; /* requested events to watch */  
        short revents; /* returned events witnessed */  
       };   

和select()不一樣,poll()沒有使用低效的三個基于位的文件描述符set,而是采用了一個單獨的結(jié)構(gòu)體pollfd數(shù)組,由fds指針指向這個數(shù)組的第一個元素,其中nfds表示該數(shù)組的大小。每一個pollfd 結(jié)構(gòu)體指定了一個被監(jiān)視的文件描述符,每個結(jié)構(gòu)體的events域是監(jiān)視該文件描述符的事件掩碼,由用戶來設(shè)置這個域。revents域是文件描述符的操作結(jié)果事件掩碼。內(nèi)核在調(diào)用返回時設(shè)置這個域**。events域中請求的任何事件都可能在revents域中返回,而部分事件不能出現(xiàn)在events中,詳細(xì)如下表。

POLLIN:普通或優(yōu)先級帶數(shù)據(jù)可讀
POLLRDNORM:普通數(shù)據(jù)可讀
POLLRDBAND:優(yōu)先級帶數(shù)據(jù)可讀
POLLPRI:高優(yōu)先級數(shù)據(jù)可讀
POLLOUT:普通數(shù)據(jù)可寫
POLLWRNORM:普通數(shù)據(jù)可寫
POLLWRBAND:優(yōu)先級帶數(shù)據(jù)可寫
POLLERR:發(fā)生錯誤
POLLHUP:發(fā)生掛起
POLLNVAL:描述字不是一個打開的文件
注意:后三個只能作為描述字的返回結(jié)果存儲在revents中,而不能作為測試條件用于events中。
最后一個參數(shù)timeout是指定poll函數(shù)返回前等待多長時間

成功時,**poll()返回結(jié)構(gòu)體中revents域不為0的文件描述符個數(shù);如果在超時前沒有任何事件發(fā)生,poll()返回0;失敗時,poll()返回-1。
關(guān)鍵代碼如下:

client[0].fd = listenfd;         /*將數(shù)組中的第一個元素設(shè)置成監(jiān)聽描述字*/  
client[0].events = POLLIN;    
while(1)  
{  
    nready = poll(client, maxi+1,INFTIM); //將進(jìn)程阻塞在poll上  
    if( client[0].revents & POLLIN/*POLLRDNORM*/ ) /*先測試監(jiān)聽描述字*/  

epoll

在linux的網(wǎng)絡(luò)編程中,很長的一段時間都在使用select來做事件觸發(fā)。然而select逐漸暴露出了一些缺陷,使得linux不得不在新的內(nèi)核中尋找出替代方案,那就是epoll。其實,epoll與select原理類似,只不過,epoll作出了一些重大改進(jìn),即:
1.支持一個進(jìn)程打開大數(shù)目的socket描述符(FD)select 最不能忍受的是一個進(jìn)程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯然太少了。這時候你一是可以選擇修改這個宏然后重新編譯內(nèi)核,不過資料也同時指出這樣會帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案),不過雖然linux上面創(chuàng)建進(jìn)程的代價比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,舉個例子,在1GB內(nèi)存的機器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
2.IO效率不隨FD數(shù)目增加而線性下降傳統(tǒng)的select/poll另一個致命弱點就是當(dāng)你擁有一個很大的socket集合,不過由于網(wǎng)絡(luò)延時,任一時間只有部分的socket是"活躍"的,但是select/poll每次調(diào)用都會線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進(jìn)行操作---這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有"活躍"的socket才會主動的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會,在這點上,epoll實現(xiàn)了一個"偽"AIO,因為這時候推動力在os內(nèi)核。
3.使用mmap加速內(nèi)核與用戶空間的消息傳遞。這點實際上涉及到epoll的具體實現(xiàn)了。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實現(xiàn)的
4.內(nèi)核微調(diào)這一點其實不算epoll的優(yōu)點了,而是整個linux平臺的優(yōu)點。也許你可以懷疑linux平臺,但是你無法回避linux平臺賦予你微調(diào)內(nèi)核的能力。

epoll api函數(shù)比較簡單,包括

  • 創(chuàng)建一個epoll描述符
  • 添加監(jiān)聽事件
  • 阻塞等待所監(jiān)聽的事件發(fā)生
  • 關(guān)閉epoll描述符,如下:
#include <sys/epoll.h>  
#include <unistd.h>  

int epoll_create(int size); //epoll描述符  
int close(int fd);//關(guān)閉epoll描述符  

創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,epoll本身就占用一個fd值,所以用完后必須調(diào)用close()關(guān)閉,以防止fd被耗盡。

#include <sys/epoll.h>  
#include <unistd.h>  
  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//添加監(jiān)聽事件  
  
typedef union epoll_data {  
    void        *ptr;  
    int          fd;  
    uint32_t     u32;  
    uint64_t     u64;  
} epoll_data_t;  
  
struct epoll_event {  
    uint32_t     events;      /* Epoll events */  
    epoll_data_t data;        /* User data variable */  
};  

epoll_ctl為事件注冊函數(shù),第一個參數(shù)是epoll_create()的返回值,第二個參數(shù)表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;

第三個參數(shù)是需要監(jiān)聽的描述符fd,第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事件,struct epoll_event結(jié)構(gòu)如上所示,其中events為需要注冊的事件,可以為下面幾個宏的集合:
EPOLLIN:表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;
EPOLLET:將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。詳細(xì)見下面描述
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里。

操作系統(tǒng)對被注冊的文件描述符的事件發(fā)送以后有兩種處理方式,分別有LT和ET模式,默認(rèn)情況下為LT,如果需要設(shè)置為ET模式,設(shè)置

struct epoll_event.events|EPOLLET。

LT(level triggered)是缺省的工作方式,并且同時支持block和no-block socket.在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進(jìn)行IO操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你的,所以,這種模式編程出錯誤可能性要小一點,傳統(tǒng)的select/poll都是這種模型的代表
ET (edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數(shù)據(jù)少于一定量時導(dǎo)致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(only once),不過在TCP協(xié)議中,ET模式的加速效用仍需要更多的benchmark確認(rèn)。

struct epoll_event.data為一個epoll_data_t類型的聯(lián)合體,詳細(xì)結(jié)構(gòu)如上,其中可以保存指針,描述符,32/64位的整數(shù)。如果在監(jiān)聽過程中,該描述符上面有相應(yīng)的事件發(fā)生,系統(tǒng)將會把該字段返回。先看監(jiān)聽函數(shù)吧。看完就知道

#include <sys/epoll.h>  
#include <unistd.h>  
  
int epoll_wait(int epfd, struct epoll_event *events,  int maxevents, int timeout);//阻塞等待所監(jiān)聽的事件發(fā)生  

阻塞監(jiān)聽函數(shù),類似于select()調(diào)用。參數(shù)events用來從內(nèi)核得到事件的集合,返回的結(jié)構(gòu)也是struct epoll_event,其中event為相應(yīng)的事件,data為注冊時,設(shè)置的值(常見情況,data設(shè)置為注冊的描述符,這樣就可以對相應(yīng)的描述符進(jìn)行IO操作)。maxevents告之內(nèi)核這個events有多大,這個maxevents的值不能大于創(chuàng)建epoll_create()時的size,參數(shù)timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時。

struct epoll_event ev, *events;   
int kdpfd = epoll_create(100);   
ev.events = EPOLLIN | EPOLLET; // 注意這個EPOLLET,指定了邊緣觸發(fā)   
ev.data.fd =listener;   
epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev);   
for(;;)   
{   
    nfds = epoll_wait(kdpfd, events, maxevents, -1);  
    for(n = 0; n < nfds; ++n)  
     {   
        if(events[n].data.fd == listener)  
        {   
                client = accept(listener, (struct sockaddr *) &local, &addrlen);   
                if(client < 0){   
                        perror("accept");   
                        continue;   
                }   
                setnonblocking(client);   
                 ev.events = EPOLLIN | EPOLLET;   
                 ev.data.fd = client;   
                if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)   
            {   
                        fprintf(stderr, "epoll set insertion error: fd=%d0, client);   
                        return -1;   
                }   
             }else{  
            do_use_fd(events[n].data.fd);   
        }                     
     }   
}  

最后我們說一下,用來克服select/poll缺點的方法不只有epoll。epoll只是一種linux 的方案,在freeBSD下有kqueue,而dev/poll 是最古老的Solaris的方案,使用難度依次遞增。 kqueue 是freebsd 的寵兒,kqueue 實際上是一個功能相當(dāng)豐富的kernel 事件隊列,它不僅僅是select/poll 的升級,而且可以處理 signal 、目錄結(jié)構(gòu)變化、進(jìn)程等多種事件。Kqueue 是邊緣觸發(fā)的。

信號驅(qū)動異步IO詳解

上面介紹了不管是阻塞/非阻塞或者IO復(fù)用,在應(yīng)用程序?qū)用娑夹枰M(jìn)行阻塞或者輪詢指定的IO上面是否有相應(yīng)的事件發(fā)生。本節(jié)我們將將介紹一個全新的模型:異步模型,異步則意味著不需要用戶層進(jìn)行阻塞或者輪詢,在特定IO或者事件發(fā)生時候,會主動通知應(yīng)用程序IO就緒,本節(jié)介紹的通知機制即信號,我們把這個模型叫著信號驅(qū)動的異步I/O。

Unix上有定義了許多信號,源自Berkeley的實現(xiàn)使用的是SIGIO信號來支持套接字和終端設(shè)備上的信號驅(qū)動IO。在套接字IO中,信號驅(qū)動IO模型主要是在UDP套接字上使用,在TCP套接字上幾乎是沒有什么使用的(在TCP上,由于TCP是雙工的,它的信號產(chǎn)生過于平凡,并且信號的出現(xiàn)幾乎沒有告訴我們發(fā)生了什么事情。因此對于TCP套接字,SIGIO信號是沒有什么使用的)。
使用信號驅(qū)動異步IO主要有下面幾個步驟:
為了讓套接字描述符可以工作于信號驅(qū)動I/O模式,應(yīng)用進(jìn)程必須完成如下三步設(shè)置:
1.注冊SIGIO信號處理程序。(安裝信號處理器)
2.使用fcntl的F_SETOWN命令,設(shè)置套接字所有者。
3.使用fcntl的F_SETFL命令,置O_ASYNC和O_NONBLOCK標(biāo)志,允許套接字信號驅(qū)動I/O。

注意,必須保證在設(shè)置套接字所有者之前,向系統(tǒng)注冊信號處理程序,否則就有可能在fcntl調(diào)用后,信號處理程序注冊前內(nèi)核向應(yīng)用交付SIGIO信號,導(dǎo)致應(yīng)用丟失此信號。
代碼片段如下:

struct sigaction sigio_action;  
memset(&sigio_action, 0, sizeof(sigio_action));  
sigio_action.sa_flags = 0;  
sigio_action.sa_handler = do_sigio;//信號發(fā)生時的處理函數(shù)  
sigaction(SIGIO, &sigio_action, NULL);  
   
fcntl(listenfd1, F_SETOWN, getpid());  
int flags;  
flags = fcntl(listenfd1, F_GETFL, 0);  
flags |= O_ASYNC | O_NONBLOCK;  
fcntl(listenfd1, F_SETFL, flags);  

對同步異步以及阻塞與非阻塞的理解:

在處理 IO 的時候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是異步 IO。

阻塞同步是指一直等待結(jié)果
非阻塞同步是指立即返回錯誤的結(jié)果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內(nèi)容