目錄
13、SOCKET-IO複用技術
1、五種I/O模型
- 阻塞I/O
- 非阻塞I/O
- I/O複用(select和poll)
- 信號驅動I/O
- 異步I/O
2、阻塞I/O模型
- 最流行的I/O模型是阻塞I/O模型,缺省時,所有的套接口都是阻塞的
3、非阻塞I/O模型
- 當我們把一個套接口設置爲非阻塞方式時,即通知內核:當請求的I/O操作非得讓進程睡眠不能完成時,不要讓進程睡眠,而應返回一個錯誤
應用程序連續不斷地查詢內核,看看某操作是否準備好,這對cpu時間是極大的浪費,一般只在專門提供某種功能的系統中才會用到
4、I/O複用模型
- 有了I/O複用,我們就可以調用select或poll,在這兩個系統調用的某一個上阻塞,而不是真正阻塞於真正的I/O系統調用
5、信號驅動I/O模型
- 我們也可以用信號,讓內核在描述字準備好時用信號SIGIO通知我們,我們將此方法稱爲信號驅動I/O
6、異步I/O模型
- 異步I/O是Posix.1的1993版本中的新內容,我們讓內核啓動操作,並在整個操作完成後通知我們
7、I/O複用
- 如果一個或多個I/O條件滿足(例如:輸入已準備好被讀,或者描述字可以承接更多輸出的時候)我們就能夠被通知到,這樣的能力被稱爲I/O複用,是由函數select和poll支持的
I/O複用網絡應用場合
- 當客戶處理多個描述字
- 一個客戶同時處理多個套接口
- 如果一個tcp服務器既要處理監聽套接口,又要處理連接套接口
- 如果一個服務器既要處理TCP,又要處理UDP
8、shutdown函數
- 功能:關閉套接字兩端或一端的socket
#include <sys/socket.h>
int shutdown(int sockfd,int howto);
-
參數:
- SHUT_RD:關閉連接的讀這一半,不再接收套接口中的數據且現留在套接口接收緩衝區中的數據都作廢
- SHUT_WR:關閉連接的寫這一半,在TCP場合下,這稱爲爲半關閉。當前留在套接口發送緩衝區中的數據都被髮送,後跟正常的tcp連接終止序列
- SHUT_RDWR 連接的讀這一半和寫這一半都關閉
-
返回值:成功:0,失敗:錯誤代碼
shutdown與close的區別
- 終止網絡連接的正常方法是調用close,但close有兩個限制可由函數shutdown來避免。
- close將描述字的訪問計數減1,僅在此計數爲0時才關閉套接口;用shutdown我們可以激發TCP的正常連接終止序列,而不管訪問計數
- Close終止了數據傳送的兩個方向:讀和寫。由於TCP連接是全雙工的,有很多時候我們要通知另一端我們已完成了數據發送,即使一端仍有許多數據要發送也是如此。
9、select函數
- 這個函數允許進程指示內核等待多個事件中的任一個發生,並僅在一個或多個事件發生或經過某指定的時間後才喚醒進程
- 功能:提供了即時響應多個套接的讀寫事件
#include <sys/select.h>
#include <sys/socket.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *except,const struct timeval *timeout);
struct timeval(
long tv_sec; //秒
long tv_usec;//微秒
);
- 參數
- maxfdp1:等待最大套接字值加1,(等待套接字的數量)
- readset:要檢查讀事件的容器
- writeset:要檢查寫事件的容器
- timeout:超時時間
- 返回值:
- 返回觸發套件接字的個數
timeout參數
- 永遠等待下去:僅在有一個描述字準備好I/O時才返回,爲此,我們將timeout設置爲空指針
- 等待固定時間:在有一個描述字準備好I/O是返回,但不超過由timeout參數所指timeval結構中指定的秒數和微秒數
- 根本不等待:檢查描述字後立即返回,這稱爲輪詢。定時器的值必須爲0
fd_set參數
- select使用描述字集,它一般是一個整數數組,每個數中的每一位對應一個描述字。
- 使用fd_set數據類型來表示這個描述字集,我們不用去關心具體的實現細節。
操作fd_set的四個宏
- void FD_ZERO(fd_set *fdset); //清空描述字集合
- void FD_SET(int fd, fd_set *fdset); //添加一個描述字到集合中
- void FD_CLR(int fd, fd_set *fdset); //從集合中刪除一個描述字
- int FD_ISSET(int fd, fd_set *fdset);//描述字是否在該集合中
select函數返回值
- 當返回時,結果指示哪些描述字已準備好。
- 返回時,我們用宏FD_ISSET來測試結構fd_set中的描述字。描述字集中任何與沒有準備好的描述字相對應的位返回時清成0。爲此,每次調用select時,我們都得將所有描述字集中關心的都置爲1
- 如果在任何描述字準備好之前定時器時間到,則返回0
- 返回-1表示有錯。
select缺點
- 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
- 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
- select支持的文件描述符數量太小了,默認是1024
select示例
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <error.h>
#include <termios.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define ERR_EXIT(M)\
do\
{\
perror(M);\
exit(1);\
}while(0);
int main(int argc,char *argv[])
{
int sockfd = socket(PF_INET,SOCK_STREAM,0);
if(sockfd == -1)
ERR_EXIT("socket");
int on = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in sockaddr;
bzero(&sockaddr,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
sockaddr.sin_port = htons(5566);
if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
ERR_EXIT("bind");
if(listen(sockfd,5) == -1)
ERR_EXIT("listen");
int maxfd = sockfd;
int client[FD_SETSIZE];
fd_set rset;
fd_set allset;
FD_ZERO(&rset);
FD_ZERO(&allset);
FD_SET(sockfd,&allset);
int nready;
int conn;
int i;
for(i = 0;i < FD_SETSIZE;i++)
client[i] = -1;
while(1){
rset = allset;
nready = select(maxfd + 1,&rset,NULL,NULL,NULL);
if(nready == -1)
ERR_EXIT("select");
//新客戶端
if(FD_ISSET(sockfd,&rset)){
struct sockaddr_in peer_addr;
memset(&peer_addr,0,sizeof(peer_addr));
socklen_t socklen = sizeof(peer_addr);
conn = accept(sockfd,(struct sockaddr *)&peer_addr,&socklen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0;i < FD_SETSIZE;i++){
if(client[i] < 0){
client[i] = conn;
break;
}
}
FD_SET(conn,&allset);
if(conn > maxfd)
maxfd = conn;
if(--nready <= 0)//1件事情
continue;
}
//已連接FD產生可讀事件
for( i = 0;i < FD_SETSIZE;i++){
if(FD_ISSET(client[i],&rset)){
conn = client[i];
char buf[1024]={0};
int nread = read(conn,buf,sizeof(buf));
if(nread == 0){//對方關閉
printf("client is close\n");
FD_CLR(conn,&allset);
client[i] = -1;
close(conn);
}
fputs(buf,stdout);
write(conn,buf,nread);
memset(buf,0,1024);
if(--nready <= 0)//1件事情
break;
}
}
}
return 0;
}
10、poll函數
- poll函數和select類似,但它是用文件描述符而不是條件的類型來組織信息的.
- 也就是說,一個文件描述符的可能事件都存儲在struct pollfd中.與之相反,select用事件的類型來組織信息,而且讀,寫和錯誤情況都有獨立的描述符掩碼.
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);
- 參數:
- fdarray是一個pollfd的結構體數組用來表示文件描述符的監視信息
- nfds參數給出了要監視的描述符數目
- timeout參數是一個用豪秒錶示的時間,是poll在返回前沒有接收事件是應等待的時間,如果timeout的值爲-1,poll就永遠不會超時.如果整數值爲32個比特,那麼最大超時週期約爲30分鐘
- 返回值:準備好描述字的個數,0-超時,1-出錯
pollfd結構體
- fd是文件描述符值
- event和revents是通過代表各種事件的標準符進行邏輯或運算構建而成的
struct pollfd
{
int fd;
short events; //感興趣的事件
short revents; //fd上觸發的事情
}
poll函數事件標誌
事件標誌符 | 含義 |
---|---|
POLLIN | 無阻塞地讀除了具有高優先級的數據之外的數據 |
POLLRONORM | 無阻塞地讀常規數據 |
POLLRDBAND | 無阻塞地讀具有優先級的數據 |
POLLOUT | 無阻塞的寫常規數據 |
poll優缺點
-
缺點:
- 每次調用poll,都需要把fdarray數組從用戶態拷貝到內核態,這個開銷在連接數fd很多時會很大。
- 同時每次調用poll都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
-
優點:
- 支持的文件描述符數量沒有限制。
與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理。
poll示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define PORT 5566
#define MAXSIZE 1024
#define FDSIZE 1000
#define ERR_EXIT(M)\
do\
{\
perror(M);\
exit(1);\
}while(0);
int main()
{
int sockfd = socket(PF_INET,SOCK_STREAM,0);
if(sockfd == -1)
ERR_EXIT("socket");
int optval = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in sockaddr;
bzero(&sockaddr,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
//sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
sockaddr.sin_port = htons(PORT);
if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
ERR_EXIT("bind");
if(listen(sockfd,5) == -1)
ERR_EXIT("listen");
/*開始poll流程*/
int connfd;
struct sockaddr_in clientaddr;
memset(&clientaddr,0,sizeof(clientaddr));
socklen_t clientaddrlen;
struct pollfd clientfds[FDSIZE];//監聽1000個
int maxi;
int i,n;
/*初始化客戶連接描述符*/
for(i = 0;i < FDSIZE;i++)
clientfds[i].fd = -1;
//添加監聽描述符
clientfds[0].fd = sockfd;
clientfds[0].events = POLLIN;
maxi = 0;
//循環處理
while(1){
//獲取可用描述符的個數
int nready = poll(clientfds,maxi+1,-1);//永遠不會超時
if(nready == -1){
ERR_EXIT("poll");
}else if(nready == 0){//超時
printf("select timeout!\n");
continue;
}
//測試監聽描述符是否準備好
if(clientfds[0].revents & POLLIN){
clientaddrlen = sizeof(clientaddr);
//接受新的連接
connfd = accept(sockfd,(struct sockaddr *)&clientaddr,&clientaddrlen);
if(connfd == -1){
ERR_EXIT("accept");
}
printf("accept a new client:%s:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
//將新的連接描述符添加到數組中
for(i = 1;i < FDSIZE;i++){
if(clientfds[i].fd < 0){
clientfds[i].fd = connfd;
clientfds[i].events = POLLIN;
break;
}
}
if(i == FDSIZE){
fprintf(stderr,"Too many clients.\n");
exit(1);
}
maxi = (i > maxi ? i : maxi);
}
//檢查客戶端連接是否有讀事件
for(int i = 1;i <= maxi;i++){
if(clientfds[i].fd < 0)
continue;
if(clientfds[i].revents & POLLIN){
char buf[MAXSIZE] = {0};
n = read(clientfds[i].fd,buf,sizeof(buf));
if(n == 0){
close(clientfds[i].fd);
clientfds[i].fd = -1;
printf("client is closed.\n");
continue;
}
printf("read msg is:%s\n",buf);
write(clientfds[i].fd,buf,n);
}
}
}
return 0;
}
11、epoll函數
- 相對於select和poll來說,epoll更加靈活,沒有描述符限制。
- epoll使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。
11-1、epoll_create
- 創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。從linux2.6.8之後size參數被忽略。
- 需要注意的是,當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
int epoll_create(int size);
查看最大數:
cat /proc/sys/fs/file-max
11-2、epoll_ctl
- 註冊要監聽的事件類型。select是在監聽事件時告訴內核要監聽什麼類型的事件。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data{
void *ptr;
int fd; /*文件描述符*/
uint32_t u32;
uint64_t u64;
}epoll_data_t;
- 參數
- epfd:epoll_create()的返回值
- op:表示動作,用三個宏來表示:
- EPOLL_CTL_ADD:註冊新的fd到epfd中;
- EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
- EPOLL_CTL_DEL:從epfd中刪除一個fd
- fd:需要監聽的fd
- event是告訴內核需要監聽什麼事件
struct epoll_event結構
參數 | 描述 |
---|---|
EPOLLIN | 表示對應的文件描述符可以讀(包括對端SOCKET正常關閉) |
EPOLLOUT | 表示對應的文件描述符可以寫 |
EPOLLPRI | 表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來) |
EPOLLERR | 表示對應的文件描述符發生錯誤 |
EPOLLHUP | 表示對應的文件描述符被掛斷 |
EPOLLET | 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的 |
EPOLLONESHOT | 只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏 |
11-3、epoll_wait
- 等待事件的產生,類似於select()調用。
int epoll_wait(int epfd,
struct epoll_event * events,
int maxevents,
int timeout);
-
參數:
- events:用來從內核得到事件的集合
- maxevents:告之內核這個events有多大,這個值不能大於創建epoll_create()時的size
- timeout:超時時間(毫秒,0會立即返回,-1不會超時)。
-
返回值:該函數返回需要處理的事件數目,如返回0表示已超時。
11-4、Epoll工作模式
-
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,區別如下:
- LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
- ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。
ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。
11-5、epoll示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#define EPOLLSIZE 1024
#define PORT 5566
#define ERR_EXIT(M) do{perror(M);exit(1);}while(0)
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
ERR_EXIT("socket");
int opt = 1;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) == -1)
ERR_EXIT("setsockopt");
struct sockaddr_in sockaddr;
bzero(&sockaddr,sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
sockaddr.sin_port = htons(PORT);
if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
ERR_EXIT("bind");
if(listen(sockfd,EPOLLSIZE) == -1)
ERR_EXIT("listen");
int epfd = epoll_create(EPOLLSIZE);
if(epfd == -1)
ERR_EXIT("epoll_create");
struct epoll_event events[EPOLLSIZE],ep_event;
bzero(&ep_event,sizeof(ep_event));
ep_event.events = EPOLLIN;
ep_event.data.fd = sockfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ep_event) == -1)
ERR_EXIT("epoll_ctl");
int nready = 0;
for(;;){
nready = epoll_wait(epfd,events,EPOLLSIZE,-1);
if(nready == -1)
ERR_EXIT("epoll_wait");
for(int i = 0;i < nready;i++){
if(events[i].events == EPOLLIN){
if(events[i].data.fd == sockfd){
struct sockaddr_in caddr;
bzero(&caddr,sizeof(caddr));
socklen_t addrlen = sizeof(caddr);
int cfd = accept(sockfd,(struct sockaddr *)&caddr,&addrlen);
if(cfd == -1)
ERR_EXIT("accept");
ep_event.events = EPOLLIN;
ep_event.data.fd = cfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ep_event) == -1)
ERR_EXIT("epoll_ctl_add");
printf("connect: ip:%s,port:%d\n",inet_ntoa(caddr.sin_addr),caddr.sin_port);
}else{
int cfd = events[i].data.fd;
char buf[1024]={0};
int nread = read(cfd,buf,sizeof(buf));
if(nread == 0){
if(epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ep_event) == -1)
ERR_EXIT("epoll_ctl_del");
}else if(nread == -1){
ERR_EXIT("read");
}else{
printf("client:%s",buf);
if(write(cfd,buf,sizeof(buf)) == -1)
ERR_EXIT("write");
}
}
}
}
}
return 0;
}
12、總結
- select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和合poll在“醒着”的時候要遍歷整個fd集,而epoll在“醒着”的時候只要判斷一下就緒鏈表是否爲空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
- select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。