非阻塞I/O
系統調用有兩類,“低速”系統調用和其他,而低速系統調用可能使進程永遠阻塞,例如讀某些終端設備文件,文件中並沒有數據,讀操作可能使調用者永遠阻塞。
非阻塞I/O使我們可以的一些操作不會永遠的阻塞,如果操作繼續執行將會進入阻塞狀態,則調用立即出錯返回。
比如對於一個給定的描述符,有兩種方法指定其爲非阻塞I/O:
1. 調用open函數打開文件時指定O_NONBLOCK標誌
2. 打開文件後對文件描述符調用fcntl修改文件狀態標誌,同上。
阻塞 非阻塞 異步特點
阻塞IO的特點就是在IO執行的兩個階段(準備數據和拷貝數據)都被阻塞了
非阻塞IO的特點是用戶進程需要不點的主動詢問內核數據準備好了沒有
IO多路複用通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select函數就可以返回
異步IO用戶進程發起read操作後可以立刻開始去做其他事情,而內核會等待數據準備完成然後拷貝到用戶內存,完成後給用戶進程發送一個signal告訴它read操作完成
記錄鎖
當一個進程正在讀或者修改文件的某個部分的時候,使用記錄鎖可以防止其他進程修改同意文件區。
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*struct flock flockptr */);
/*struct flock {
short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
};*/
l_type 鎖的類型:F_RDLCK(共享讀鎖),F_WRLCK(獨佔寫鎖),F_UNLCK(解鎖一個區域)
l_whence 設定偏移位置,SEEK_SET, SEEK_CUR, SEEK_END
l_start 設定區域起始位置偏移量
l_len 設定鎖定區域的字節長度
l_pid 持有鎖的進程ID,僅由F_GETLK返回
讀鎖可以共享,說明第一個進程對文件上鎖爲讀鎖時,其他進程也可以對文件上鎖區域獲取讀鎖進行讀取操作。假如進程想在讀鎖的基礎上獲取寫鎖則會被阻塞。而如果第一個進程對文件上鎖爲寫鎖的話,那麼其他進程對文件無論希望獲取什麼鎖都會被阻塞。
寫鎖例子:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/file.h>
int main()
{
int fd = open("test.txt",O_RDWR|O_CREAT,0644);
if(fd < 0)
{
printf("open err\n");
return -1;
}
struct flock myflock;
myflock.l_type = F_WRLCK; //獨佔性寫鎖 F_RDLCK爲共享讀鎖
myflock.l_whence = SEEK_SET; //偏移位置爲文件開頭
myflock.l_start = 0; //指定鎖的區域開頭
myflock.l_len = 1024; //指定鎖的區域大小
fcntl(fd, F_SETLKW, &myflock); //鎖文件,鎖不上說明有別人上鎖,就阻塞等待
printf("get lock\n");
myflock.l_type = F_UNLCK; //解鎖
fcntl(fd, F_SETLKW, &myflock);
close(fd);
return 0;
}
I/O多路轉接
select函數
在所有POSIX兼容的平臺上,select函數使我們可執行I/O多路轉接。
#include <sys/select.h>
int select(int maxfd, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);
第一個參數爲最大文件描述符編號值加一
中間三個參數readfds,writefds,exceptfds是指向描述符集的指針,分別爲我們關心的可讀,可寫,處於異常條件的描述符集合
最後一個參數爲timeval結構體,指定等待描述符準備好的最大時間,若指定NULL則表示阻塞等待,指定0表示不阻塞立即返回,指定非0值爲正常指定時間
select例子
#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/types.h>
#include<fcntl.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#define MAXSIZE 1024
int main()
{
int fd_key, fd_mouse;
char buf[MAXSIZE];
// 爲了代碼整潔性可讀性未加錯誤判斷,並且打開此文件可能要root權限
fd_key = open("/dev/input/event1", O_RDONLY);
fd_mouse = open("/dev/input/mice", O_RDONLY);
// 第一個參數 最大文件描述符+1
int maxfd = fd_key > fd_mouse ? fd_key : fd_mouse;
maxfd++;
printf("%d\n",maxfd);
while(1)
{
fd_set rset;
FD_ZERO(&rset); //將rset所有位置0
FD_SET(fd_key, &rset);
FD_SET(fd_mouse, &rset);
struct timeval tv;
tv.tv_sec = 1; //秒
tv.tv_usec = 0; //微秒
int ret = select(maxfd, &rset, NULL, NULL, &tv);
if(ret < 0)
{
if(errno == EINTR) //被信號中斷
continue;
return -1;
}
else if(ret > 0)
{
if(FD_ISSET(fd_key, &rset))
{
//判斷是鍵盤操作
memset(buf, 0, sizeof(buf));
read(fd_key, buf, sizeof(buf));
printf("keyboard event\n");
}
else if(FD_ISSET(fd_mouse, &rset))
{
//判斷是鼠標操作
memset(buf, 0, sizeof(buf));
read(fd_mouse, buf, sizeof(buf));
printf("mouse event\n");
}
}
}
close(fd_key);
close(fd_mouse);
return 0;
}
epoll函數
在linux的網絡編程中,很長的時間都在使用select來做事件觸發。在linux新的內核中,有了一種替換它的機制,就是epoll。
#include <sys/epoll.h>
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);
epoll_create
創建一個epoll句柄,size用來告訴內核這個監聽的數目一共多大,但在Linux 2.6.8開始廢棄這個參數,但依然要大於0。
當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
epoll_ctl
epoll的事件註冊函數
第一個參數爲epoll_create返回值
第二個參數表示動作
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;第三個參數爲需要監聽的fd
第四個參數struct epoll_event告訴內核要監聽什麼事件
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 */
};
event的取值:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏
epoll_wait
等待事件的產生,參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個 maxevents的值不能大於創建epoll_create()時的size,參數timeout是超時時間。該函數返回需要處理的事件數目,如返回0表示已超時。
epoll對文件描述符的操作有兩種模式:LT和ET。LT模式是默認模式,LT模式與ET模式的區別如下:
LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。
下次調用epoll_wait時,會再次響應應用程序並通知此事件。
ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。
如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。
(對應epoll_ctl內的struct epoll_event結構體中的__uint32_t enents的(ET)EPOLLET模式,默認是LT模式)
select和epoll的區別
select出現早,使用位域來表示描述符集合
其缺點是描述符數量的限制,在Linux上一般爲1024,大規模文件通過遍歷fdset找到就緒的描述符,效率低。epoll出現晚,使用紅黑樹來保存文件集合,大規模文件描述符效率高