IO多路複用之Select機制
1.1 基本概念
IO多路複用是指核心一旦發現程序指定的一個或者多個IO條件準備讀取,它就通知該程序。IO多路複用适用如下場合:
(1)當客戶處理多個描述字時(一般是互動式輸入和網絡套接口),必須使用I/O複用。
(2)當一個客戶同時處理多個套接口時,而這種情況是可能的,但很少出現。
(3)如果一個TCP伺服器既要處理監聽套接口,又要處理已連接配接套接口,一般也要用到I/O複用。
(4)如果一個伺服器即要處理TCP,又要處理UDP,一般要使用I/O複用。
(5)如果一個伺服器要處理多個服務或多個協定,一般要使用I/O複用。
與多程序和多線程技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必建立程序/線程,也不必維護這些程序/線程,進而大大減小了系統的開銷。
1.2 select函數
該函數準許程序訓示核心等待多個事件中的任何一個發送,并隻在有一個或多個事件發生或經曆一段指定的時間後才喚醒。函數原型如下:
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); |
- 傳回值就緒描述符的數目,逾時傳回0,出錯傳回-1
-
函數參數介紹(1)第一個參數nfds指定待測試的描述字個數,它的值是待測試的最大描述字加1 (如果檔案描述符是5,那麼nfds就填5+1)。
(2)中間的三個參數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指針。struct 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); // 檢查集合中指定的檔案描述符是否可以讀寫 |
(3)timeout告知核心等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用于指定這段時間的秒數和微秒數。
struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds }; |
這個參數有三種可能:
(1)永遠等待下去:僅在有一個描述字準備好I/O時才傳回。為此,把該參數設定為空指針NULL。
(2)等待一段固定時間:在有一個描述字準備好I/O時傳回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
(3)根本不等待:檢查描述字後立即傳回,這稱為輪詢。為此,該參數必須指向一個timeval結構,而且其中的定時器值必須為0。
1.3 總結
select的幾大缺點:
(1)每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
(2)同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支援的檔案描述符數量太小了,預設是1024
1.4 示例1
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <linux/input.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define DEVICE "/dev/input/event3" //滑鼠裝置 fd_set read_set; struct input_event ev; int main() { int fd; fd=open(DEVICE,2); if(fd<0) { printf("驅動打開失敗!\n"); } while(1) { FD_ZERO(&read_set); FD_SET(fd,&read_set); if(select(fd+1,&read_set,NULL,NULL,NULL)) { if(FD_ISSET(fd,&read_set)) { if(read(fd,&ev,sizeof(struct input_event))==sizeof(struct input_event)) //讀取發生的事件 { printf("key=%d value = %d\n",ev.code,ev.value); } } } } return 0; } |
1.5 示例2
檢視系統的标準檔案描述符: [root@wbyq test_20180702]# ls /dev/std* -l lrwxrwxrwx. 1 root root 15 4月 17 11:34 /dev/stderr -> /proc/self/fd/2 lrwxrwxrwx. 1 root root 15 4月 17 11:34 /dev/stdin -> /proc/self/fd/0 lrwxrwxrwx. 1 root root 15 4月 17 11:34 /dev/stdout -> /proc/self/fd/1 |
網絡通信裡使用select機制:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> unsigned char rx_buff[1024]; unsigned int rx_cnt; unsigned char log_info[1024]; unsigned int log_cnt=0; /* TCP伺服器建立 */ int main(int argc,char **argv) { int tcp_server_fd; //伺服器套接字描述符 int tcp_client_fd; //用戶端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //伺服器的端口号 //判斷傳入的參數是否合理 if(argc!=2) { printf("參數格式:./tcp_server <端口号>\n"); return -1; } tcp_server_port=atoi(argv[1]); //将字元串轉為整數 /*1. 建立網絡套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP伺服器端套接字建立失敗!\n"); return -1; } /*2. 綁定端口号,建立伺服器*/ tcp_server.sin_family=AF_INET; //IPV4協定類型 tcp_server.sin_port=htons(tcp_server_port);//端口号指派,将本地位元組序轉為網絡位元組序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP位址指派給結構體成員 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP伺服器端口綁定失敗!\n"); return -1; } /*3. 設定監聽的用戶端數量*/ listen(tcp_server_fd,10); /*4. 等待用戶端連接配接*/ tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(tcp_client_fd<0) { printf("TCP伺服器:等待用戶端連接配接失敗!\n"); return -1; } //列印連接配接的用戶端位址資訊 printf("已經連接配接的用戶端資訊: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*5. 資料通信*/ fd_set readfds; //讀事件的檔案操作集合 fd_set writefds;//寫事件的檔案操作集合 int select_state; //接收傳回值 while(1) { /*5.1 清空檔案操作集合*/ FD_ZERO(&readfds); FD_ZERO(&writefds); /*5.2 添加要監控的檔案描述符*/ FD_SET(tcp_client_fd,&readfds); FD_SET(tcp_client_fd,&writefds); /*5.3 監控檔案描述符*/ select_state=select(tcp_client_fd+1,&readfds,&writefds,NULL,NULL); if(select_state>0)//表示有事件産生 { /*5.4 測試指定的檔案描述符是否産生了讀事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 讀取資料*/ rx_cnt=read(tcp_client_fd,rx_buff,1024); if(rx_cnt==0) { printf("對方已經斷開連接配接!\n"); break; } sprintf(log_info,"server rx data[%d]",log_cnt++); write(tcp_client_fd,log_info,strlen(log_info)); //回發資料 write(tcp_client_fd,rx_buff,rx_cnt); //将收到的資料傳回 } //判斷是否産生了寫事件 if(FD_ISSET(tcp_client_fd,&writefds)) { printf("寫事件!\n"); //隻要有寫權限,寫事件将會一直産生 //比如: //連接配接建立成功後可寫 //緩沖區可寫 } } else if(select_state<0) //表示産生了錯誤 { printf("select函數産生異常!\n"); break; } } /*6. 關閉連接配接*/ close(tcp_client_fd); } |
第二章 IO多路複用之poll機制
2.1 基本概念
poll的機制與select類似,與select在本質上沒有多大差别,管理多個描述符也是進行輪詢,根據描述符的狀态進行處理,但是poll沒有最大檔案描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量檔案描述符的數組被整體複制于使用者态和核心的位址空間之間,而不論這些檔案描述符是否就緒,它的開銷随着檔案描述符數量的增加而線性增大。
2.2 poll函數
函數格式如下所示:
# include <poll.h> int poll ( struct pollfd * fds, unsigned int nfds, int timeout); |
pollfd結構體定義如下:
struct pollfd { int fd; /* 檔案描述符 */ short events; /* 等待的事件 */ short revents; /* 實際發生了的事件 */ } ; |
每一個pollfd結構體指定了一個被監視的檔案描述符,可以傳遞多個結構體,訓示poll()監視多個檔案描述符。每個結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼,核心在調用傳回時設定這個域。events域中請求的任何事件都可能在revents域中傳回。合法的事件如下:
POLLIN | 有資料可讀。 |
POLLRDNORM | 有普通資料可讀。 |
POLLRDBAND | 有優先資料可讀。 |
POLLPRI | 有緊迫資料可讀。 |
POLLOUT | 寫資料不會導緻阻塞。 |
POLLWRNORM | 寫普通資料不會導緻阻塞。 |
POLLWRBAND | 寫優先資料不會導緻阻塞 |
POLLMSGSIGPOLL | 消息可用。 |
此外,revents域中還可能傳回下列事件:
POLLER | 指定的檔案描述符發生錯誤。 |
POLLHUP | 指定的檔案描述符挂起事件。 |
POLLNVAL | 指定的檔案描述符非法。 |
這些事件在events域中無意義,因為它們在合适的時候總是會從revents中傳回。
使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。
POLLIN | POLLPRI等價于select()的讀事件,POLLOUT |POLLWRBAND等價于select()的寫事件。
POLLIN等價于POLLRDNORM |POLLRDBAND,而POLLOUT則等價于POLLWRNORM。
例如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定 events為POLLIN |POLLOUT。在poll傳回時,我們可以檢查revents中的标志,對應于檔案描述符請求的events結構體。如果POLLIN事件被設定,則檔案描述符可以被讀取而不阻塞。如果POLLOUT被設定,則檔案描述符可以寫入而不導緻阻塞。這些标志并不是互斥的:它們可能被同時設定,表示這個檔案描述符的讀取和寫入操作都會正常傳回而不阻塞。
timeout參數指定等待的毫秒數,無論I/O是否準備好,poll都會傳回。timeout指定為負數值表示無限逾時,使poll()一直挂起直到一個指定事件發生;timeout為0訓示poll調用立即傳回并列出準備好I/O的檔案描述符,但并不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即傳回。
- 傳回值和錯誤代碼
成功時,poll()傳回結構體中revents域不為0的檔案描述符個數;如果在逾時前沒有任何事件發生,poll()傳回0;失敗時,poll()傳回-1,并設定errno為下列值之一:
EBADF | 一個或多個結構體中指定的檔案描述符無效。 |
EFAULTfds | 指針指向的位址超出程序的位址空間。 |
EINTR | 請求的事件之前産生一個信号,調用可以重新發起。 |
EINVALnfds | 參數超出PLIMIT_NOFILE值。 |
ENOMEM | 可用記憶體不足,無法完成請求。 |
2.3 示例
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <poll.h> /*使用poll輪詢機制*/ /*定義一個poll結構體數組,用來存放poll相關的資訊*/ struct pollfd tiny4412_poll[1]; /* argv :字元串的個數 argc :存放字元串的數組 ./app /dev/led */ int main(int argv,char*argc[]) { int fb_count; /*存放poll函數傳回值*/ int count=0; int fb1,tmp; if(argv!=2) { printf("傳參方式: ./app /dev/<device filce>\n"); exit(-1); } /*打開裝置檔案,打開成功傳回檔案描述符*/ fb1=open(argc[1],O_RDWR); /*打開第一個驅動-KEY*/ if(fb1<0) { printf("open device_file error!\n"); exit(-1); } char key; printf("open device_file ok!\n"); tiny4412_poll[0].fd=fb1; /*檢測的檔案描述符*/ tiny4412_poll[0].events=POLLIN;/*檢測的事件*/ while(1) { fb_count=poll(tiny4412_poll,1,3000); //printf("檢測的檔案描述符數量:%d\n",fb_count); if(tiny4412_poll[0].revents==POLLIN) { tmp=read(fb1,&key,sizeof(char)); /*讀按鍵值*/ printf("APP--KEY: 0x%x\n",key); } count++; printf("計數值:%d\n",count); } close(fb1); return 0; } |
第三章 IO多路複用之epoll
3.1 基本知識
epoll是在2.6核心中提出的,是之前的select和poll的增強版本。相對于select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個檔案描述符管理多個描述符,将使用者關系的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy隻需一次。
3.2 epoll接口
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); |
(1) int epoll_create(int size);
建立一個epoll的句柄,size用來告訴核心這個監聽的數目一共有多大。這個參數不同于select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll句柄後,它就是會占用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,是以在使用完epoll後,必須調用close()關閉,否則可能導緻fd被耗盡。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數,它不同與select()是在監聽事件時告訴核心要監聽什麼類型的事件epoll的事件注冊函數,它不同與select()是在監聽事件時告訴核心要監聽什麼類型的事件,而是在這裡先注冊要監聽的事件類型。第一個參數是epoll_create()的傳回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD | 注冊新的fd到epfd中; |
EPOLL_CTL_MOD | 修改已經注冊的fd的監聽事件; |
EPOLL_CTL_DEL | 從epfd中删除一個fd; |
第三個參數是需要監聽的fd,第四個參數是告訴核心需要監聽什麼事,struct epoll_event結構如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; |
events可以是以下幾個宏的集合:
EPOLLIN | 表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉); |
EPOLLOUT | 表示對應的檔案描述符可以寫; |
EPOLLPRI | 表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來); |
EPOLLERR | 表示對應的檔案描述符發生錯誤; |
EPOLLHUP | 表示對應的檔案描述符被挂斷; |
EPOLLET | 将EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水準觸發(Level Triggered)來說的。 |
EPOLLONESHOT | 隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡 |
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的産生,類似于select()調用。參數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個maxevents的值不能大于建立epoll_create()時的size,參數timeout是逾時時間(毫秒,0會立即傳回,-1将不确定,也有說法說是永久阻塞)。該函數傳回需要處理的事件數目,如傳回0表示已逾時。
3.3 示例
#include <stdio.h> #include <sys/epoll.h> #include <stdlib.h> /*定義最大的事件個數*/ #define MAX_EVENTS 10 /*定義epoll相關結構體*/ struct epoll_event ev[MAX_EVENTS]; struct epoll_event events[MAX_EVENTS]; /*定義需要用到的變量*/ int key=0,epollfd=0,cnt=0,nfds,n; int main(int argc,char **argv) { int fd; if(argc!=2) { printf("用法: ./app /dev/xx\n"); return -1; } fd=open(argv[1],2); if(fd<0) { printf("%s 裝置節點打開失敗!\n",argv[1]); return -1; } /*1.1 建立epoll專用的檔案描述符*/ epollfd = epoll_create(10); if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } /*1.2 填充事件結構體*/ ev[0].events = EPOLLIN; ev[0].data.fd = fd; /*1.3 注冊epoll事件*/ if (epoll_ctl(epollfd, EPOLL_CTL_ADD,fd,&ev[0]) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } /*1.4 循環檢測是否有事件發生*/ for (;;) { nfds = epoll_wait(epollfd,events, MAX_EVENTS,3000); if(nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } /*周遊檢測具體發生的事件*/ for (n = 0; n < nfds; ++n) { if (events[n].data.fd == fd) { /*讀取按鍵值*/ read(fd,&key,4); printf("APP: 0x%X\n",key); } } cnt++; printf("cnt=%d\n",cnt); } |