Linux網絡編程IO復用和模式
時間:2023-10-12 來源:華清遠見
IO多路復用概念
IO多路復用是指通過一種機制,使得單個進程可以監控多個文件描述符的可讀、可寫和異常等事件。常見的IO多路復用技術包括:select、epoll等。在實際應用中,IO多路復用可以提高程序的運行效率和性能,減少系統開銷,降低系統資源的使用率。它廣泛應用于網絡編程、服務器開發、操作系統等領域,可以幫助開發人員更好地處理大量的網絡連接和數據請求。以下主要以select和epoll展開敘述。
IO多路復用之select
select實現原理
IO多路復用select函數是一種實現并行IO的機制,可以同時處理多個socket連接,提高系統的性能和效率。在Linux中,select是一個阻塞函數,把需要管理的文件描述符添加到fd_set集合中,由select統一管理。如果所有的文件描述符都沒有數據準備好,那么select會一直阻塞等待,如果有文件描述符的數據準備好,select函數解除阻塞,但是要通過輪循的方式把有響應的文件描述符找出來。處理完后,繼續循環到select的位置監聽。
select相關知識
1、函數原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數說明
nfds:需要監控的最大文件描述符加1。
readfds:讀文件描述符集合。
writefds:寫文件描述符集合。
exceptfds:異常文件描述符集合。
timeout:select函數的超時時間設置,如果設置為NULL,則一直等待直到事件發生
返回值:
>0 : 表示準備好的文件描述符個數
=0: 超時解除阻塞
-1: 函數出錯
2、輔助函數
void FD_CLR(int fd, fd_set *set); //將fd從set集合中移除
int FD_ISSET(int fd, fd_set *set); //判斷fd是否在set集合中,如果在返回真,否則返回假
void FD_SET(int fd, fd_set *set); // 將fd加入set集合中
void FD_ZERO(fd_set *set); // 清空set集合中的內容
select應用案例
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
//成功返回監聽套接字, 失敗返回NULL
int sock_init()
{
int sockfd;
int ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
perror("socket");
return -1;
}
//設置套接字端口復用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in seraddr;
int addrlen = sizeof(struct sockaddr_in);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8001);
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
ret = bind(sockfd, (struct sockaddr*)&seraddr, addrlen);
if(ret<0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 10);
if(ret<0)
{
perror("bind");
return -1;
}
return sockfd;
}
int main()
{
char buff[1024];
int sockfd;
int ret;
int cfd;
sockfd = sock_init();
if(sockfd < 0)
{
return -1;
}
int maxfd;
fd_set set, rset;
FD_ZERO(&set); //清空集合
FD_SET(sockfd, &set); //文件描述符加入集合
maxfd = sockfd; //設置最大文件描述符
while(1)
{
rset = set;//由于select每次都會修改集合中的數據,所有每次都會將set的值拷貝給rset
printf("select..\n");
ret = select(maxfd+1, &rset, NULL, NULL, NULL);//調用select監聽rset集合
printf("select over..\n");
if(ret<0)
{
perror("select");
break;
}
if(FD_ISSET(sockfd, &rset)) //有客戶端請求連接
{
//1、接受客戶端
printf("accept...\n");
cfd = accept(sockfd, NULL, NULL);
printf("accept over...\n");
if(cfd<0)
{
perror("accept");
continue;
}
//2、將cfd加入set集合
FD_SET(cfd, &set);
//3、判斷最大值
if(maxfd<cfd)
{
maxfd = cfd;
}
}
for(int i=0; i<=maxfd; i++)
{
if(i == sockfd)
{
continue;
}
if(!FD_ISSET(i, &rset))
{
continue;
}
printf("read...\n");
ret = read(i, buff, 1024 );
printf("read over...\n");
if(ret<0)
{
perror("read");
//1、關閉文件描述符
close(i);
//2、從set集合中移除
FD_CLR(i, &set);
continue;
}
else if(0 == ret)
{
printf("tcp broken...\n");
//1、關閉文件描述符
close(i);
//2、從set集合中移除
FD_CLR(i, &set);
continue;
}
buff[ret] = '\0';
printf("buff: %s\n", buff);
}
}
return 0;
}
select優缺點
Select通過串行模擬并行可以實現服務端的多任務, 但是如果某個任務處理的時間很長,就會影響后面任務的處理。除此之外,select能夠監聽的文件描述符的個數是有限的,默認是1024。 而且select每次解除阻塞僅僅只是說明有套接字數據準備好了,具體是哪個套接字有數據要通過輪循遍歷的方式給找出來。
IO多路復用之epoll
epoll實現原理
Epoll是一種高效的I/O多路復用機制,它可以同時監控多個文件描述符,當其中任意一個文件描述符就緒時,就會通知應用程序進行相應的操作。相比于傳統的select和poll機制,epoll具有更高的性能和更好的擴展性。
Epoll的實現原理主要包括三個部分:epoll_create、epoll_ctl和epoll_wait。應用程序需要調用epoll_create函數創建一個epoll實例,該函數返回一個文件描述符,用于標識該實例。在Linux內核中,會為該實例創建一個紅黑樹和一個雙向鏈表,用于存儲待監控的文件描述符和相關的事件信息。接下來,應用程序可以通過epoll_ctl函數向epoll實例中添加、修改或刪除待監控的文件描述符和事件。該函數的第一個參數是epoll實例的文件描述符,第二個參數是操作類型(EPOLL_CTL_ADD、EPOLL_CTL_MOD或EPOLL_CTL_DEL),第三個參數是待監控的文件描述符,第四個參數是一個epoll_event結構體,用于指定待監控的事件類型和相關的數據。應用程序需要調用epoll_wait函數等待文件描述符就緒。該函數的第一個參數是epoll實例的文件描述符,第二個參數是一個epoll_event數組,用于存儲就緒的文件描述符和相關的事件信息,第三個參數是數組的大小,第四個參數是超時時間(單位為毫秒)。當有文件描述符就緒時,該函數會返回就緒的文件描述符數量,并將相關的事件信息存儲在epoll_event數組中。
Epoll的高效性和擴展性主要體現在以下幾個方面:
(1)Epoll使用紅黑樹和雙向鏈表存儲待監控的文件描述符和事件信息,可以快速地進行查找和插入操作,而不需要遍歷整個文件描述符集合。
(2)Epoll支持邊緣觸發和水平觸發兩種模式。邊緣觸發只在文件描述符狀態發生變化時通知應用程序,而水平觸發則會在文件描述符處于就緒狀態時不斷通知應用程序。邊緣觸發可以減少不必要的通知,提高效率。
(3)Epoll支持EPOLLONESHOT事件,可以確保每個文件描述符只被一個線程處理,避免了多個線程同時處理同一個文件描述符的情況。
(4)Epoll支持EPOLLEXCLUSIVE事件,可以確保每個文件描述符只被一個進程處理,避免了多個進程同時處理同一個文件描述符的情況。
Epoll是一種高效的I/O多路復用機制,可以大大提高應用程序的性能和可擴展性。在實際應用中,應該根據具體的需求選擇合適的觸發模式和事件類型,以達到最佳的性能和可靠性。
epoll相關知識
1、創建集合空間
int epoll_create(int size);
參數:
size : 指定文件描述符的個數
返回值:
成功:返回與集合關聯的文件描述符
失敗: -1
2、管理集合空間中的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數說明:
epfd:和集合關聯的文件描述符
op : 命令參數
EPOLL_CTL_ADD: 往集合中添加文件描述符
EPOLL_CTL_MOD: 修改集合中文件描述符的信息
EPOLL_CTL_DEL: 刪除集合中指定的文件描述符
fd: 操作的文件描述符
event: 如果是刪除操作,該參數忽略,直接傳NULL
如果是添加操作, 通過該參數告訴內核監聽指定文件描述符的指定時間
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 */
};
events: EPOLLIN(讀事件)
3、監聽集合中的文件描述符
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
參數說明:
epfd:和集合關聯的文件描述符
events:存放準備好文件描述符信息數組的起始地址
maxevents: 數組的最大元素個數
timeout :設置超時時間 (以毫秒為單位的)
>0 : 指定超時時間
=0 : 非阻塞函數
-1 : 永久阻塞
返回值:
>0 : 準備好的文件描述符個數
=0 :超時時間到了
-1 :出錯
epoll應用案例
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/epoll.h>
//成功返回監聽套接字, 失敗返回NULL
int sock_init()
{
int sockfd;
int ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
perror("socket");
return -1;
}
//設置套接字端口復用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in seraddr;
int addrlen = sizeof(struct sockaddr_in);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8001);
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
ret = bind(sockfd, (struct sockaddr*)&seraddr, addrlen);
if(ret<0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 10);
if(ret<0)
{
perror("bind");
return -1;
}
return sockfd;
}
int main()
{
char buff[1024];
int sockfd;
int ret, count;
int cfd, efd;
efd = epoll_create(100);
if(efd<0)
{
perror("epoll_create");
return -1;
}
sockfd = sock_init();
if(sockfd < 0)
{
return -1;
}
struct epoll_event ev;
struct epoll_event evs[10];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &ev);
while(1)
{
printf("wait...\n");
count = epoll_wait(efd, evs, 10, -1);
printf("wait over...\n");
if(count < 0)
{
perror("epoll_wait");
break;
}
for(int i=0; i<count; i++)
{
int temp = evs[i].data.fd;
if(temp == sockfd)// 有客戶端請求連接
{
//1、接收客戶端
printf("accept...\n");
cfd = accept(sockfd, NULL, NULL);
printf("accept over...\n");
if(cfd<0)
{
perror("accept");
continue;
}
//2、cfd 加入efd關聯的集合中
ev.data.fd = cfd;
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
}
else //已經連接過來的客戶端發來數據
{
printf("read..\n");
ret = read(temp, buff, 1024);
printf("read over..\n");
if(ret<0)
{
perror("read");
//1、關閉文件描述符
close(temp);
//2、從集合中移除
epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);
}
else if(ret == 0)
{
printf("tcp broken...\n");
//1、關閉文件描述符
close(temp);
//2、從集合中移除
epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);
}
buff[ret] = '\0';
printf("buff: %s\n", buff);
}
}
}
return 0;
}
epoll優缺點
缺點:通過串行模擬并行, 如果處理一個請求的時間過長,會影響后面任務的處理。這是所有多路IO轉接的通病。
優點:(1)監聽文件描述符的個數由用戶指定
(2)如果有套接字的數據準備好, 內核直接告訴你哪個套接字的數據準備就緒,不需要采用輪循的方式去查找

