以下是 Linux 的 IO model 的說明,另外 select, poll 及 epoll 是 IO multiplexing 的三種方法。
Kernal Space, User Space
Linux 是多工作業系統,因此有可能會發生多個 process 競爭相同的記憶體位址的狀況,為了解決衝突的問題,由 kernel 進行資源分配,也可避免資源佔用的問題,也可避免異動了別的 process 的資源而造成系統 crash。
CPU 執行程式時,會在 user space 與 kernel space 之間來回切換,user space 的系統函式庫,會轉換為 kernel space 的 system call,並由 kernel 處理,當 system call 完成後,就會回到 user space 繼續下去。
32 bits 的 OS,定址空間是 2^32 也就是 4G。kernel space 限制為 1G (虛擬地址0xC0000000到0xFFFFFFFF),而 user space 為 3G (虛擬地址0x00000000到0xBFFFFFFF),由各 process 使用。
64 bits 的 OS,會將 virtual address 分成一半,第一個 bit 為 0 是 user space,第一個 bit 為 1 是 kernel space,理論上是 8EB+8EB。但目前 processors 只實作了 48 bits,也就是 128TB+128TB。
process context switching
為了控制讓多個 process 分享系統資源,kernel 必須能夠儲存目前在 CPU 運作的 process,載入並執行新的 process,這個切換方式稱為 context switching,時間長短由硬體運算能力決定。
context switching 有以下的步驟
1.儲存 CPU 的 context,包括program counter和其他 register 2.更新PCB (Process Control Block) 3.把進程的PCB移入相應的queue,如 ready/blocking 等隊列 4.選擇另一個進程執行,並更新其PCB 5.更新memory的資料結構 6.恢復 CPU context
FD: file descriptor
file descriptor 是指向檔案 reference 的抽象概念。他是非負整數的索引值,指向 kernel 為每一個 process 維護的開啟檔案的記錄表,當程式打開或建立一個檔案,kernel 就會產生一個 file descriptor 給 process。 每一個 linux process 都有三個標準的 POSIX file descriptor: stdin 0, stdout 1, stderr 2
Buffered I/O
大多數文件系統的默認I/O 操作都是 Buffered I/O,在 Linux 會將 IO 資料先暫存在 page cache 中,也就是先複製到 kernel 的 buffer,然後再由 kernel buffer 複製到 user space。
Buffered I/O 分離了 user space 及實際的儲存設備,可以減少 HD 的讀取次數,提高系統效能。
但也因為多次複製,可能會造成 CPU 及 cache buffer 的消耗,有些特殊的應用,會避開 kernel cache buffer,而直接由 user space 儲存到 HD,以獲取更高的效能。
IO model
因為資料會先複製到 kernel buffer 裡面,然後再複製到 user space,當對一個資料進行 read,會經歷兩個階段:
- waiting for data to be ready
- copying the data from the kernel to the process
因為兩階段的 IO,linux 產生了五種 IO model
- blocking IO
- nonblocking IO
- IO multiplexing
- signal driven IO (不常用)
- asynchronous IO
blocking IO
linux 預設大部分的 socket 都是使用 blocking IO,當 process 呼叫 recv_from,會進入 wait for data 階段,在這個階段的 process 會進入 blocking 狀態,直到 kernel 將資料複製到 user space,該 process 才會解除 blocking 狀態,重新運作。
blocking IO 就是兩個階段的 IO 都被 block
nonblocking IO
當 process 呼叫 recv_from 如果 kernel 還沒將資料準備好,他不會 block process,而是產生 error,直到 kernel 將資料準備好,就會複製到 user space,並完成該讀取的工作。
nonblocking 需要 process 不斷向 kernel 詢問,資料是否 ready。
IO multiplexing
這就是常見的 select, poll, epoll,也稱為 event driven IO。這個方式可讓單一 porcess 就可以處理多個 IO,他會不斷地 polling 多個 socket,當某個 socket 有收到資料,就會主動callback 通知 process。
如果是 select,當 process 呼叫了 select,該 process 就會被 block,同時 kernel 會監控所有 select 處理的 sockets,如果有資料,select 就會 return,然後再由 process 呼叫 read,將資料由 kernel 複製到 user space。
這個方法類似 blocking IO,但進行了兩個 system call (select 及 recv_from),但 select 可處理多個 sockets。
select/epoll 的優點是可以處理多個 sockets,而不是效能。一般在 IO multiplexing 中,socket 都是設定為 non-blocking 的,process 是在 select 被 block 而不是 recv_from。
signal driven
先通知 kernel 如果某個 socket 有資料時,就以 signal 通知 process,process 在第二個步驟,才會被 block。
asynchronous
當 process 進行 read,就可以處理別的事情,當 kernel 收到非同步 read,就會馬上 return,直到將資料複製到 user space,完成後,才會發送 signal 給 process,通知已經完成了 read。
Comparison
non-blocking 跟 asynchronous 是不同的
synchronous 跟 asynchronous 的差異是 IO operation 會不會 blocking process,因此前面四種 model 都屬於 synchronous IO
nonblocking IO 中,在複製資料到 user space 的步驟,還是會有 blocking 的狀態
IO Multiplexing: select, poll, epoll
IO Multiplexing 可讓單一 process 監視多個 fd,當某個 fd 有資料,就可通知 process 進行 IO 操作,select, poll, epoll 都是同步 IO,都需要自己進行讀寫,在讀寫的過程中,process 都是被 blocked。
- select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 可監視 writefd, readfd, 及 exceptfd。呼叫 select 後,該 process 會被 blocked,直到某個 fd ready 或是 timeout。當 select return 後,必須要 traverse 所有 fdset,來找到 ready 的 fd。
select 在所有平台都支援,缺點是監視的 fd 有數量上限,通常是 1024,但可修改 macro 或是重新編譯 kernel 增加這個上限。
- poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
poll 使用一個 pollfd pointer 表示 fd,該 pollfd 包含要監視的 event及發生的 event,pollfd 沒有數量上限。poll return 後,必須 traverse pollfd,找到 ready 的 fd。
- epoll
這是在 linux kernel 2.6 以後提供的,epoll 將跟 process 有關的 fd 事件,存放在 event table 裡面。
// size 為監視的 fd 數量
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_create(int size);
當產生了 epoll 後,會佔用一個 fd value,不同於 select 必須提供最大監視 fd 數量 +1,size 並不是該 epoll 能監視的 fd 數量上限,而是配置 kernel 內部資料的建議參數。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd: 是 epoll_create 的 return value
- op: 有三個 macro 表示 operation: EPOLLCTLADD, EPOLLCTLDEL, EPOLLCTLMOD,分別是新增、刪除、修改 fd 監視的 events
- fd: 需要監視的 fd
- epoll_event: 告訴 kernel 要監視什麼 event
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; //events 是以下幾個 macro 的集合: EPOLLIN:表示對應的文件描述符可以讀(包括對端SOCKET正常關閉) EPOLLOUT:表示對應的文件描述符可以寫 EPOLLPRI:表示對應的文件描述符有緊急的資料可讀(這裡應該表示有外部資料到來) EPOLLERR:表示對應的文件描述符發生錯誤 EPOLLHUP:表示對應的文件描述符被掛斷 EPOLLET: 將EPOLL設為 Edge Triggered 模式,這是相對於水平觸發(Level Triggered)來說的 EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 epfd 的 IO event,最多回傳 maxevents 個 events,events 是事件的集合,maxevents 不能超過 epoll_create 的size
timeout 為 0 表示要馬上 return,如果回傳的事件數量為 0 表示發生了 timeout
epoll 對 fd 的操作有兩種模式: LT (Level Trigger) 及 ET (Edge Trigger)
LT: 當 epollwait 偵測到 fd 事件發生,將該事件通知 process,該 process 可不立刻處理該 event,當下次呼叫 epollwait 時,會再次通知 process 這個事件
同時支援 blocking 與 non-blocking socket,可對該 ready 的 fd 進行 IO,如果不做,kernel 會持續通知 ready
ET: 當 epollwait 偵測到 fd 事件發生,將該事件通知 process,該 process 必須立刻處理該 event,如果沒有處理,當下次呼叫 epollwait 時,不會再次通知 process 這個事件
這是高速運作方式,只支援 non-blocking socket
在 select/poll 中,process 必須呼叫某些 function,kernel 才會對該 fd 進行監視,而 epoll 事先利用 epollctl 註冊 fd,當某個 fd ready 後,會透過 callback 機制,啟動這個 fd,當 process 呼叫 epollwait 就可得到通知。
因為 epoll 去掉了 traverse fds 的步驟,因此可以快速處理 IO event。
epoll 監視的 fd 數量沒有限制,通常是可以打開的文件的數量,可查詢 cat /proce/sys/fs/file-max
得知。select 的缺點,是該 process 可打開的 fd 有數量上限。
如果沒有大量的 idle/dead connection,epoll 的效率不會比 select/poll 高很多。
References
Linux IO模式及 select、poll、epoll詳解
沒有留言:
張貼留言