【Linux网络编程学习】I/O多路复用——select和poll
此為牛客Linux C++課程和黑馬Linux系統編程筆記。
0. I/O多路復用
所謂I/O就是對socket提供的內存緩沖區的寫入和讀出。
多路復用就是指程序能同時監聽多個文件描述符。
之前的學習中寫了多進程和多線程版的簡單服務器模型,但是有個問題:每次新來一個客戶端TCP連接請求,就需要新建一個進程或線程來與之進行信息傳輸,但是如果連接的客戶端太多,就會出現所謂C10K問題:
當創建的進程或線程多了,數據拷貝頻繁(緩存I/O、內核將數據拷貝到用戶進程空間、阻塞,進程/線程上下文切換消耗大,導致操作系統崩潰,這就是C10K問題的本質。
所以為每個請求分配一個進程/線程的方式不合適,進程或線程本身會消耗資源,且線程或進程調度也會消耗CPU資源。所以I/O 多路復用技術應運而生,讓一個進程或線程處理多個請求。
Linux 下實現 I/O 多路復用的系統調用主要有 select、poll 和 epoll,本篇介紹select和poll。
1. select
主旨思想:
操作時,該函數才返回。
a.這個函數是阻塞
b.函數對文件描述符的檢測的操作是由內核完成的
1.1 select API介紹
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);參數:
- nfds : 委托內核檢測的最大文件描述符的值 + 1
- readfds : 要檢測的文件描述符的讀的集合,即委托內核檢測哪些文件描述符的讀的屬性,對應的是對方發送過來的數據,因為讀是被動的接收數據,檢測的就是讀緩沖區
- writefds : 要檢測的文件描述符的寫的集合,即委托內核檢測哪些文件描述符的寫的屬性,委托內核檢測寫緩沖區是不是還可以寫數據(不滿的就可以寫)
- exceptfds : 檢測發生異常的文件描述符的集合
注意:以上三個是傳入傳出參數,以readfds為例,傳入的時候將需要監聽讀事件的文件描述符對應fd_set位置為1傳入,執行select后,select將把readfds中真正監聽到讀事件的文件描述符的對應位保持為1,而把沒有到讀事件的文件描述符的對應位修改為0,這樣我們再遍歷這個傳出的fd_set,哪個文件描述符對應位為1就說明監聽到了哪個文件描述符的讀事件。
- timeout : 設置的超時時間
NULL : 永久阻塞,直到檢測到了文件描述符有變化
tv_sec = 0 tv_usec = 0, 不阻塞
tv_sec > 0 tv_usec > 0, 阻塞對應的時間
返回值 :
- -1 : 失敗
- >0(n) : 檢測的集合中有n個文件描述符發生了變化
對于第2、3、4個參數涉及到的fd_set,它是一個文件描述符的位圖,默認為1024位,可以理解成一個一維數組,可把數組的下標看作文件描述符的值,數組的值為1表示需要監聽該下標所對應的文件描述符。關于fd_set的設置可使用以下宏:
// 將參數文件描述符fd對應的標志位設置為0 void FD_CLR(int fd, fd_set *set); // 判斷fd對應的標志位是0還是1, 返回值 : fd對應的標志位的值,0,返回0, 1,返回1 int FD_ISSET(int fd, fd_set *set); // 將參數文件描述符fd 對應的標志位,設置為1 void FD_SET(int fd, fd_set *set); // fd_set一共有1024 bit, 全部初始化為0 void FD_ZERO(fd_set *set);1.2 select使用過程示例
比如說現在客戶端A,B,C,D已經連接了服務器,其socket文件描述符分別是3、4、100、101。我們想要使用select監聽這四個客戶端的讀事件,該如何做?
如圖,首先建立一個fd_set,調用FD_ZERO初始化其每一位都是0,我們現在需要調用FD_SET把想要監聽的文件描述符的對應位置1:
這樣fd_set就設置好了,我們調用select,第一個參數是想監聽的最大文件描述符+1,所以傳入101+1;第二個參數傳入reads的地址。此后,內核便負責監聽這四個socket文件的讀緩沖區。
假如說,A和B發送了數據,調用select后,3、4對應的位不變,而沒有檢測到讀事件的100和101就被賦為0,然后傳出。我們通過FD_ISSET遍歷reads就可以判斷哪些客戶端發送了數據。
1.3 select示例程序
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/select.h>int main() {// 創建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 綁定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 監聽listen(lfd, 8);// 創建一個fd_set的集合,存放的是需要檢測的文件描述符fd_set rdset, tmp;FD_ZERO(&rdset);FD_SET(lfd, &rdset);int maxfd = lfd;while(1) {tmp = rdset;// 調用select系統函數,讓內核幫檢測哪些文件描述符有數據int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);if(ret == -1) {perror("select");exit(-1);} else if(ret == 0) {continue;} else if(ret > 0) {// 說明檢測到了有文件描述符的對應的緩沖區的數據發生了改變if(FD_ISSET(lfd, &tmp)) {// 表示有新的客戶端連接進來了struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);// 將新的文件描述符加入到集合中FD_SET(cfd, &rdset);// 更新最大的文件描述符maxfd = maxfd > cfd ? maxfd : cfd;}// 我們需要遍歷來確定哪個文件描述符發送來了數據// 這是select的缺點之一for(int i = lfd + 1; i <= maxfd; i++) {if(FD_ISSET(i, &tmp)) {// 說明這個文件描述符對應的客戶端發來了數據char buf[1024] = {0};int len = read(i, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");close(i);FD_CLR(i, &rdset);} else if(len > 0) {printf("read buf = %s\n", buf);write(i, buf, strlen(buf) + 1);}}}}}close(lfd);return 0; }1.4 select的缺點
2. poll
poll是對select的改進。poll和select的使用方法很像,但對select有以下改進:
2.1 poll API介紹
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);參數:
- fds : 是一個struct pollfd 結構體數組,這是一個需要檢測的文件描述符的集合
其中events和revents有以下可選項,選擇多個可用|進行連接
以下是創建該結構體的示例:
- nfds : 這個是第一個參數數組中最后一個有效元素的下標 + 1
- timeout : 阻塞時長
0 : 不阻塞
-1 : 阻塞,當檢測到需要檢測的文件描述符有變化,解除阻塞
>0 : 阻塞的時長
2.2 poll 示例程序
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <poll.h>int main() {// 創建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;// 綁定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));// 監聽listen(lfd, 8);// 初始化檢測的文件描述符數組struct pollfd fds[1024];for(int i = 0; i < 1024; i++) {fds[i].fd = -1;fds[i].events = POLLIN;}fds[0].fd = lfd;int nfds = 0;while(1) {// 調用poll系統函數,讓內核幫檢測哪些文件描述符有數據int ret = poll(fds, nfds + 1, -1);if(ret == -1) {perror("poll");exit(-1);} else if(ret == 0) {continue;} else if(ret > 0) {// 說明檢測到了有文件描述符的對應的緩沖區的數據發生了改變if(fds[0].revents & POLLIN) {// 表示有新的客戶端連接進來了struct sockaddr_in cliaddr;int len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);// 將新的文件描述符加入到集合中for(int i = 1; i < 1024; i++) {if(fds[i].fd == -1) {fds[i].fd = cfd;fds[i].events = POLLIN;break;}}// 更新最大的文件描述符的索引nfds = nfds > cfd ? nfds : cfd;}for(int i = 1; i <= nfds; i++) {if(fds[i].revents & POLLIN) {// 說明這個文件描述符對應的客戶端發來了數據char buf[1024] = {0};int len = read(fds[i].fd, buf, sizeof(buf));if(len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");close(fds[i].fd);fds[i].fd = -1;} else if(len > 0) {printf("read buf = %s\n", buf);write(fds[i].fd, buf, strlen(buf) + 1);}}}}}close(lfd);return 0; }總結
以上是生活随笔為你收集整理的【Linux网络编程学习】I/O多路复用——select和poll的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 黄埔女生剧情介绍
- 下一篇: 【Linux网络编程学习】I/O多路复用