1. epoll
? ? ? ? epoll是為克服select、poll每次監(jiān)聽都需要在用戶、內(nèi)核空間反復(fù)拷貝,以及需要用戶程序自己遍歷發(fā)現(xiàn)有變化的文件描述符的缺點(diǎn)的多路IO復(fù)用技術(shù)。
epoll原理
創(chuàng)建內(nèi)核空間的紅黑樹;
將需要監(jiān)聽的文件描述符上樹;
內(nèi)核監(jiān)聽紅黑樹上文件描述符的變化;
返回有變化的文件描述符。
epoll優(yōu)點(diǎn)
? ? ? ? ① 無需在用戶、內(nèi)核空間反復(fù)拷貝數(shù)據(jù);
? ? ? ? ② 內(nèi)核返回發(fā)生變化的文件描述符,無需用戶遍歷所有文件描述符。
2. epoll API
(1)epoll_create 創(chuàng)建紅黑樹
#include<sys/epoll.h>
int epoll_create(int size);
/*
功能:
創(chuàng)建內(nèi)核中的epoll紅黑樹;
參數(shù):
size:監(jiān)聽的文件描述符上限,kernel 2.6版本后寫1即可,會(huì)自動(dòng)擴(kuò)展。
返回值:
成功:返回紅黑樹的句柄(相當(dāng)于操作樹的入口)。
失?。?1,會(huì)設(shè)置errno。
*/
(2)epoll_ctl 上樹、下樹、修改節(jié)點(diǎn)的監(jiān)聽事件
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/*
功能:
上樹、下樹、修改節(jié)點(diǎn)。
參數(shù):
epfd:紅黑樹的句柄,epoll_create的返回值。
op:對(duì)文件描述符fd的操作
EPOLL_CTL_ADD:將文件描述符fd上樹
EPOLL_CTL_MOD:修改文件描述符fd的事件
EPOLL_CTL_DEL:將文件描述符fd下樹
fd:op要操作的文件描述符
event:用于對(duì)特定的文件描述符事件進(jìn)行設(shè)置。
返回值:
成功:
失敗:
*/
struct epoll_event {
uint32_t events; // 監(jiān)聽的事件
epoll_data_t data; // 需要監(jiān)聽的文件描述符(共用體中的fd)
}
/*
參數(shù) events:
EPOLLIN:讀事件
EPOLLOUT:寫事件
*/
typedef union epoll_data {
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
(2)epoll_wait 監(jiān)聽
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
/*
功能:
監(jiān)聽紅黑樹上文件描述符的變化;
參數(shù):
epfd:紅黑樹的句柄(epoll_create的返回值);
events:接收發(fā)送變化的文件描述符的數(shù)組地址
maxevents:數(shù)組元素的個(gè)數(shù)
timeout:
> 0:監(jiān)聽超時(shí)時(shí)間(多久監(jiān)聽一次);
0:無文件描述符變化則立即返回;
-1:阻塞監(jiān)聽到有文件描述符變化才返回
返回值:
成功:0表示沒有文件描述符發(fā)生變化;否則返回發(fā)生變化的文件描述符的個(gè)數(shù)
失?。?1,調(diào)用錯(cuò)誤,會(huì)設(shè)置errno
*/
3. epoll使用示例
(1)監(jiān)聽管道
? ? ? ? 子進(jìn)程每3s向管道寫數(shù)據(jù),父進(jìn)程使用epoll監(jiān)聽管道,有數(shù)據(jù)可讀則讀出管道中的數(shù)據(jù)。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/epoll.h>
int main(int argc, const char* argv[]) {
int fd[2];
pipe(fd);
pid_t pid;
pid = fork();
if (0 == pid) { // 子進(jìn)程
close(fd[0]);
char buf[5];
char ch = 'a';
while (1) {
memset(buf, ch++, 5);
write(fd[1], buf, 5);
sleep(3);
}
} else { // 父進(jìn)程
close(fd[1]);
// 創(chuàng)建紅黑樹
int epfd = epoll_create(1);
// 上樹
struct epoll_event ev;
ev.data.fd = fd[0];
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &ev); // 將fd[0]上樹,同時(shí)使用ev結(jié)構(gòu)體來設(shè)置監(jiān)聽fd[0]的讀事件。
// 監(jiān)聽
struct epoll_event evs[1]; // 接收從內(nèi)核返回的有變化的文件描述符的數(shù)組。
while (1) {
int n = epoll_wait(epfd, evs, 1, -1);
if (1 == n) {
char buf[64] = "";
n = read(fd[0], buf, 64);
if (n <= 0) {
printf("子進(jìn)程關(guān)閉了寫端");
close(fd[0]);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd[0], &ev); // 下樹
break;
} else {
printf("讀到子進(jìn)程寫的內(nèi)容:%s\n", buf);
}
}
}
}
return 0;
}
運(yùn)行結(jié)果:
(2)epoll實(shí)現(xiàn)簡(jiǎn)單并發(fā)服務(wù)器示例:
#include<stdio.h>
#include<sys/epoll.h>
#include"wrap.h"
int main(int argc, const char* argv[]) {
// 1.創(chuàng)建socket,綁定
int lfd = tcp4bind(8888, NULL);
// 2.監(jiān)聽
Listen(lfd, 128);
// 3.創(chuàng)建樹
int epfd = epoll_create(1);
// 5.將lfd上樹
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// 4.while epoll_wait監(jiān)聽
struct epoll_event evs[1024];
while (1) {
int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞監(jiān)聽到有文件描述符變化才返回
if (n < 0) { // 調(diào)用出錯(cuò)
perror("epoll_wait");
break;
} else if (0 == n) {
continue;
} else { // 有文件描述符變化
for (int i = 0;i < n;i++) {
// 若lfd有讀事件
if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
struct sockaddr_in cliaddr;
char ip[16] = "";
socklen_t len = sizeof(cliaddr);
int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取
printf("新連接到來:IP = %s, port = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上樹;監(jiān)聽cfd的讀事件
} else if (evs[i].events & EPOLLIN) { // 若cfd有讀事件
char buf[1024] = "";
int n = read(evs[i].data.fd, buf, 1024);
if (n < 0) { // 出錯(cuò)
perror("read");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下樹
} else if (0 == n) { // 客戶端關(guān)閉
printf("客戶端關(guān)閉.\n");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下樹
} else {
write(STDOUT_FILENO, buf, 1024); / printf依賴于字符串終止符'\0',而write輸出指定長(zhǎng)度的字符串。
}
}
}
}
}
return 0;
}
運(yùn)行結(jié)果:
4.? epoll的兩種工作方式
epoll有兩種工作方式:水平觸發(fā)(LT)、邊緣觸發(fā)(ET)。
(1)水平觸發(fā)
? ? ? ? 如監(jiān)聽文件描述符的讀緩沖區(qū)時(shí),只要讀緩沖區(qū)有數(shù)據(jù)就會(huì)觸發(fā)epoll_wait。例如讀緩沖區(qū)有數(shù)據(jù),只要沒讀干凈,就會(huì)觸發(fā)epoll_wait。
? ? ? ? 如監(jiān)聽文件描述符的寫緩沖區(qū)時(shí),只要可寫就會(huì)觸發(fā)epoll_wait,因此監(jiān)聽寫緩沖區(qū)時(shí)推薦使用邊緣觸發(fā)。
(2)邊緣觸發(fā)
????????如監(jiān)聽文件描述符的讀緩沖區(qū)時(shí),讀緩沖區(qū)有數(shù)據(jù)到來才會(huì)觸發(fā)epoll_wait;與水平觸發(fā)不一樣,緩沖區(qū)數(shù)據(jù)沒讀干凈且無數(shù)據(jù)到來,則下次不會(huì)再觸發(fā)epoll_wait,因此要求一次性將讀緩沖區(qū)數(shù)據(jù)讀干凈。
????????如監(jiān)聽文件描述符的寫緩沖區(qū)時(shí),寫緩沖區(qū)數(shù)據(jù)從有到無才會(huì)觸發(fā)epoll_wait。
epoll默認(rèn)工作方式為水平觸發(fā),但推薦使用邊緣觸發(fā),以減少epoll_wait系統(tǒng)調(diào)用次數(shù)。
5. epoll的邊緣觸發(fā)使用示例
使用邊緣觸發(fā),主要兩點(diǎn):1. 監(jiān)聽的事件加上邊緣觸發(fā)的屬性;2. 只要觸發(fā)就一次性將事情處理完。
1. 監(jiān)聽的事件加上邊緣觸發(fā)的屬性
無需將監(jiān)聽的文件描述符設(shè)置為邊緣觸發(fā),而是將與客戶端通信的文件描述符設(shè)置為邊緣觸發(fā),需將上面的 “epoll實(shí)現(xiàn)簡(jiǎn)單并發(fā)服務(wù)器示例” 代碼第44行:
ev.events = EPOLLIN;
?改為如下,即加上邊緣觸發(fā)的屬性。
ev.events = EPOLLIN | EPOLLET;
2. 只要觸發(fā)就一次性將事情處理完
? ? ? ? 以讀事件為例,將上面的 “epoll實(shí)現(xiàn)簡(jiǎn)單并發(fā)服務(wù)器示例” 一次性讀取字節(jié)數(shù)由1024B變?yōu)?B,則大多數(shù)情況下無法一次read調(diào)用就讀完緩沖區(qū)中所有數(shù)據(jù),因此循環(huán)讀取緩沖區(qū),直至讀完。
? ? ? ? 由于是循環(huán)讀取,直至讀完,因此文件描述符cfd需要設(shè)置為非阻塞,否則循環(huán)到最后一次無數(shù)據(jù)可讀時(shí),read函數(shù)將阻塞,無法返回繼續(xù)監(jiān)聽。
????????而水平觸發(fā)時(shí),結(jié)合上面代碼,read是阻塞的,但通常不會(huì)阻塞住。因?yàn)槭侵灰袛?shù)據(jù)可讀就會(huì)觸發(fā)epoll_wait,無數(shù)據(jù)就不會(huì)觸發(fā),因此不會(huì)阻塞住。
上述 "epoll實(shí)現(xiàn)簡(jiǎn)單并發(fā)服務(wù)器示例"?改為邊緣觸發(fā):
#include<stdio.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"
int main(int argc, const char* argv[]) {
// 1.創(chuàng)建socket,綁定
int lfd = tcp4bind(8888, NULL);
// 2.監(jiān)聽
Listen(lfd, 128);
// 3.創(chuàng)建樹
int epfd = epoll_create(1);
// 5.將lfd上樹
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// 4.while epoll_wait監(jiān)聽
struct epoll_event evs[1024];
while (1) {
int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞監(jiān)聽到有文件描述符變化才返回
if (n < 0) { // 調(diào)用出錯(cuò)
perror("epoll_wait");
break;
} else if (0 == n) {
continue;
} else { // 有文件描述符變化
for (int i = 0;i < n;i++) {
// 若lfd有讀事件
if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
struct sockaddr_in cliaddr;
char ip[16] = "";
socklen_t len = sizeof(cliaddr);
int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取
/* 設(shè)置cfd非阻塞 */
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
printf("新連接到來:IP = %s, port = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上樹;監(jiān)聽cfd的讀事件
} else if (evs[i].events & EPOLLIN) { // 若cfd有讀事件
while (1) { // 循環(huán)讀取
char buf[4] = "";
/*緩沖區(qū)無數(shù)據(jù)時(shí),以阻塞的方式讀取,則會(huì)阻塞等待;若以非阻塞的方式讀取,則返回-1,并且設(shè)置errno為EAGAIN*/
int n = read(evs[i].data.fd, buf, 4);
if (n < 0) { // 出錯(cuò)
if (EAGAIN == errno) { // 緩沖區(qū)被讀干凈,則繼續(xù)下一次監(jiān)聽
break;
}
perror("read");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下樹
break;
} else if (0 == n) { // 客戶端關(guān)閉
printf("客戶端關(guān)閉.\n");
close(evs[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下樹
break;
} else {
write(STDOUT_FILENO, buf, 4); // printf依賴于字符串終止符'\0',而write輸出指定長(zhǎng)度的字符串。
}
}
}
}
}
}
return 0;
}
運(yùn)行結(jié)果:文章來源:http://www.zghlxwxcb.cn/news/detail-440083.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-440083.html
到了這里,關(guān)于Linux多路IO復(fù)用:epoll的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!