[Linux 高并發服務器] IO多路復用
文章概述
該文章為 牛客C++專案課:Linux高并發服務器 的個人筆記,記錄了IO多路復用相關的知識點
作者資訊
NEFU 2020級 zsl
ID:fishingrod/魚竿釣魚干
Email:851892190@qq.com
歡迎各位參考此博客,參考時在顯眼位置放置原文鏈接和作者基本資訊
參考資料
感謝前輩們留下的優秀資料,從中學到很多,冒昧參考,如有冒犯可以私信或者在評論區下方指出
| 標題 | 作者 | 參考處 |
|---|---|---|
| Linux 高并發服務器 | 牛客網 | 貫穿全文,以此為基礎 |
| select、poll和epoll的區別和 IO多路復用模型講解 | IT生涯 | 對比三zhongIO多路復用的補充 |
| IO多路復用底層原理分析 | CallMeJiaGu | 自己寫的檢測和select的差距在哪里 |
| 服務端經典的C10k問題(譯) | kbryanzhang | C10K問題譯文 |
| 高性能網路編程(二):上一個10年,著名的C10K并發連接問題 | Jack Jiang | C10問題概覽和分析 |
另外,各位可以看一下張龍遠前輩寫的《C++服務器開發精髓》個人覺得可以和牛客的課程對上,然后會有更多細節的補充,
正文部分
I/O 多路復用(I/O 多路轉接)的概念
I/O 多路復用使得程式能同時監聽多個檔案描述符,能夠提高程式的性能,Linux 下實作 I/O 多路復用的系統呼叫主要有 select、poll 和 epoll
這里的I/O實際上指的是讀寫緩沖區的操作,不要局限于檔案和記憶體之間的資訊傳輸
常見的IO模型
BIO模型
阻塞等待
我們直接設定阻塞的IO函式等待資料傳過來然后進行處理,這么做的好處是不占用CPU寶貴的時間片,但是同一時刻只能處理一個操作
為了解決這一問題,我們可以采用多行程和多執行緒并發技術,這么做的缺點是執行緒或行程會消耗資源,執行緒或行程調度也會消耗CPU資源
實際上多執行緒和多行程并沒有解決根本阻塞問題,在子行程子執行緒當中仍然會阻塞,只不過不會因為一個阻塞等待導致其他請求沒法處理
取自牛客網教程的ppt
C10K Problem
這里可以了解一下C10K問題,譯文和概覽分析已經放在參考資料里了
對于C10K問題的解決思路主要有兩個方向:
- 每個行程/執行緒處理單個鏈接,然后搭配多行程/執行緒
- 每個行程/執行緒處理多個鏈接,IO多路復用
第一個思路就是BIO模型,阻塞等待+多執行緒多行程,然而隨著互聯網的發展,并發需求越來越高,傳統的行程/執行緒服務器模型因為行程/執行緒消耗資源過大,無法真正解決C10K問題,C10K問題的核心在于減少CPU等核心計算資源的使用,
下面介紹的NIO模型搭配上IO多路復用技術可以較好的解決C10K問題
NIO 模型
非阻塞忙輪詢
程式每隔一段時間詢問一次有沒有資料有沒有到達,優點是提高了程式的執行效率,缺點是需要占用更多的CPU和系統資源
取自牛客網PPT
解決方案是采用I/O 多路復用技術
I/O 多路復用技術
如果讓我們直接寫一個NIO模型那么很可能是下面這樣的結構
代碼摘自IO多路復用底層原理分析
while true {
for(i in stream[]) {
if(i has data)
read until unavailable
}
}
這么做的缺點是,如果沒有準備就緒的流,那么就會浪費很多時間,解決方案是如果沒有就緒的流就阻塞起來,直到出現準備就緒的流
我個人感覺這個有點像多行程/執行緒模型當中的條件變數,檢測到符合條件的才開始作業,
select
委托內核查看有幾個檔案描述符有資料到了,但是不能告訴你具體是那幾個,底層檢測是靠二進制位來實作的,具體要看哪幾個還是要輪詢,
select的主旨思想:
摘自牛客PDF
- 首先要構造一個關于檔案描述符的串列,將要監聽的檔案描述符添加到該串列中,
- 呼叫一個系統函式,監聽該串列中的檔案描述符,直到這些描述符中的一個或者多個進行I/O
操作時,該函式才回傳,
a.這個函式是阻塞
b.函式對檔案描述符的檢測的操作是由內核完成的- 在回傳時,它會告訴行程有多少(哪些)描述符要進行I/O操作,
代碼示例:
服務端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
// 創建socket
int 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;
}
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;
}
客戶端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 創建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 連接服務器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服務器已經斷開連接...\n");
break;
}
// sleep(1);
usleep(1000);
}
close(fd);
return 0;
}
select的缺陷
- 每次呼叫select要把fd從用戶態拷貝到內核態,時間開銷大
- select在內核中也是對fd進行遍歷,如果fd很多開銷也很大
- select支持的檔案描述符數量太少了
- fds集合不能重用,每次需要重置
代碼示例
poll
poll和select區別不大,主要解決了selct檔案描述符限制的問題,因為他是用鏈表實作的,但是其他幾個缺陷還是沒有解決
代碼示例
服務端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
// 創建socket
int 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;
}
客戶端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 創建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 連接服務器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服務器已經斷開連接...\n");
break;
}
// sleep(1);
usleep(1000);
}
close(fd);
return 0;
}
epoll
摘自牛客網PPT
epoll相比select/poll有了以下改進
- 仍然會進入內核態,但是不用拷貝fd之類的資訊了
- 采用紅黑樹檢測檔案描述符號資訊,提高了檢測效率
- 使用雙向鏈表存放檢測到資料發生改變的檔案描述符
代碼示例
服務端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 創建socket
int 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);
// 呼叫epoll_create()創建一個epoll實體
int epfd = epoll_create(100);
// 將監聽的檔案描述符相關的檢測資訊添加到epoll實體中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監聽的檔案描述符有資料達到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有資料到達,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
客戶端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 創建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 連接服務器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服務器已經斷開連接...\n");
break;
}
// sleep(1);
usleep(1000);
}
close(fd);
return 0;
}
epoll作業模式
LT模式 水平觸發
epoll的預設模式,只要socket上有未讀完的資料,就會一直產生EPOLLIN
事件,同時支持block和no-block socket
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 創建socket
int 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);
// 呼叫epoll_create()創建一個epoll實體
int epfd = epoll_create(100);
// 將監聽的檔案描述符相關的檢測資訊添加到epoll實體中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監聽的檔案描述符有資料達到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有資料到達,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
ET模式 垂直觸發
epoll的高速作業方式,只支持no-block socket,每次新來資料就會觸發一次EPOLLIN事件,如果沒有新資料而且上次socket沒有讀完也不會再次觸發,ET模式很大程度上減少了epoll事件觸發次數,因此效率比LT模式高,
在使用的時候要注意,一定要使用no-block socket,并且回圈讀入
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main() {
// 創建socket
int 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);
// 呼叫epoll_create()創建一個epoll實體
int epfd = epoll_create(100);
// 將監聽的檔案描述符相關的檢測資訊添加到epoll實體中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監聽的檔案描述符有資料達到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 設定cfd屬性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
epev.events = EPOLLIN | EPOLLET; // 設定邊沿觸發
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 回圈讀取出所有資料
char buf[5];
int len = 0;
while( (len = read(curfd, buf, sizeof(buf))) > 0) {
// 列印資料
// printf("recv data : %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if(len == 0) {
printf("client closed....");
}else if(len == -1) {
if(errno == EAGAIN) {
printf("data over.....");
}else {
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
兩個模式對應的客戶端,可以拿來比較一下差異
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 創建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 連接服務器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
// sprintf(sendBuf, "send data %d", num++);
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服務器已經斷開連接...\n");
break;
}
}
close(fd);
return 0;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/356864.html
標籤:其他



