場(chǎng)景:設(shè)計(jì)一個(gè)高性能的網(wǎng)絡(luò)服務(wù)器,能夠供多個(gè)客戶端同時(shí)進(jìn)行連接,并且能夠處理這些客戶端傳上來(lái)的請(qǐng)求
應(yīng)對(duì)并發(fā),可以設(shè)計(jì)一個(gè)多線程的程序,每個(gè)傳上來(lái)的請(qǐng)求都開一個(gè)線程。存在一個(gè)弊端,需要CPU上下文的切換,代價(jià)高
如何使用單線程解決問(wèn)題?
每一個(gè)網(wǎng)絡(luò)連接在內(nèi)核中以文件描述符的形式存在
如果服務(wù)器正在處理A的請(qǐng)求,此時(shí)B發(fā)送一個(gè)請(qǐng)求,B的請(qǐng)求會(huì)被丟棄嗎?不會(huì),因?yàn)樘幚鞩O的設(shè)備不是CPU,而是專門的DMA
最簡(jiǎn)單粗暴的方法,缺點(diǎn)是仍然由CPU判斷是否有數(shù)據(jù)
while(1){
for(fdx in (fda ~ fde)){
if(fdx有數(shù)據(jù)){
讀fdx并處理;
}
}
}
select
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(socket, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
for(i = 0; i < 5; i++){
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
fds[i] = accept(socket, (struct sockaddr*)&client, &addrlen);
if(fds[i] > max)
max = fds[i];
}
/*----------------------*/
while(1){
FD_ZERO(&rset);
for(i = 0; i < 5; i++)
FD_SET(fds[i], &rset);
puts("round again");
select(max + 1, &rset, NULL, NULL, NULL);
for(i = 0; i < 5; i++){
if(FD_ISSET(fds[i], &rset)){
memset(buffer, 0, MAXBUF);
read(fds[i], buffer, MAXBUF);
puts(buffer);
}
}
}
上一個(gè)部分主要是為了準(zhǔn)備文件描述符的數(shù)組fds。首先創(chuàng)建了一個(gè)socket的服務(wù)端,然后創(chuàng)建了五個(gè)文件描述符
文件描述符是一些隨機(jī)的不重復(fù)的數(shù),將其中的最大值存到max中
select方法的參數(shù):讀文件描述符集合、rset、寫文件描述符集合、異常描述符集合、超時(shí)時(shí)間
這里關(guān)心的是讀文件描述符,因?yàn)橐x取網(wǎng)絡(luò)連接中的數(shù)據(jù),將寫文件描述符集合、異常描述符集合設(shè)置為NULL,超時(shí)時(shí)間NULL表示使用默認(rèn)時(shí)間
rset是一個(gè)bitmap,用來(lái)表示哪一個(gè)文件描述符是被啟用/監(jiān)聽的,bitmap有1024位,哪一位為1表示哪一個(gè)文件描述符被監(jiān)聽
select將rset從用戶態(tài)拷貝到內(nèi)核態(tài),由內(nèi)核態(tài)直接判斷文件描述符是否有數(shù)據(jù)的操作;暴力方法判斷時(shí)需要反復(fù)從用戶態(tài)切換到內(nèi)核態(tài),效率更低
select函數(shù)的執(zhí)行流程
- select是一個(gè)阻塞函數(shù),當(dāng)沒(méi)有數(shù)據(jù)時(shí),會(huì)一直阻塞在select函數(shù)那一行
- 當(dāng)有數(shù)據(jù)時(shí)會(huì)將rset中對(duì)應(yīng)的那一位置位
- select函數(shù)返回,不再阻塞
- 遍歷文件描述符數(shù)組,判斷哪個(gè)fd被置位了
- 讀取數(shù)據(jù),處理
select函數(shù)的缺點(diǎn)
- bitmap默認(rèn)大小為1024,雖然可以調(diào)整但是有限度
- rset每次循環(huán)都需要重置,不可重復(fù)使用
- 盡管將rset從用戶態(tài)拷貝到內(nèi)核態(tài)由內(nèi)核判斷是否有數(shù)據(jù),但還是有拷貝的開銷
- 當(dāng)有數(shù)據(jù)時(shí)select就會(huì)返回,但是select函數(shù)并不知道哪個(gè)文件描述符有數(shù)據(jù)了,后面還需要再次對(duì)文件描述符遍歷
poll
struct pollfd{
int fd;
short events;
short revents;
}
/*----------*/
for(i = 0; i < 5; i++){
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
pollfds[i].fd = accept(socket, (struct sockaddr*)&client, &addrlen);
pollfds[I].events = POLLIN;
}
sleep(1);
/*----------*/
while(1){
puts("round again");
poll(pollfds, 5, 50000);
for(i = 0; i < 5; i++){
if(pollfds[i].revents & POLLIN){
pollfds[i].revents = 0;
memset(buffer, 0, MAXBUF);
read(pollfds[i].fd, buffer, MAXBUF);
puts(buffer);
}
}
}
poll函數(shù)的參數(shù)
- 自定義的結(jié)構(gòu)體數(shù)組
- 數(shù)組的長(zhǎng)度
- 超時(shí)時(shí)間
自定義結(jié)構(gòu)體
- fd,文件描述符
- events,在意的事件是什么,讀是POLLIN,寫是POLLOUT,讀和寫都在意用或
- revents,對(duì)events的回饋,開始時(shí)為0,當(dāng)有數(shù)據(jù)可讀時(shí)就置為POLLIN,類似于上面的rset
poll函數(shù)的執(zhí)行流程
- 將五個(gè)fd從用戶態(tài)拷貝到內(nèi)核態(tài)
- poll為阻塞方法,執(zhí)行poll方法,如果有數(shù)據(jù)會(huì)將fd對(duì)應(yīng)的revents置為POLLIN
- poll方法返回
- 循環(huán)遍歷,查找哪個(gè)fd被置位為POLLIN了
- 將revents重置為0,便于復(fù)用
- 對(duì)置位的fd進(jìn)行讀取和處理
解決了select的哪些缺陷
- 使用數(shù)組,大小不止1024,解決了bitmap的大小限制
- 每次置位revents字段,revents可以恢復(fù),解決了rset不可重用的情況
- 3和4的缺陷未被解決,因?yàn)閮烧咴硐嗤?/li>
epoll
struct epoll_events[5];
int epfd = epoll_create(10);
...
...
for(i = 0; i < 5; i++){
static struct epoll_event ev;
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
ev.data.fd = accept(socket, (struct sockaddr*)&client, &addrlen);
ev.events = POLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}
/*---------*/
while(1){
puts("round again");
nfds = epoll_wait(epfd, events, 5, 10000);
for(i = 0; i < nfds; i++){
memset(buffer, 0, MAXBUF);
read(events[I].data.fd, buffer, MAXBUF);
puts(buffer);
}
}
epoll準(zhǔn)備過(guò)程
- 利用epoll_create創(chuàng)建epfd,epfd相當(dāng)于一個(gè)白板,用來(lái)作為epoll_wait的第一個(gè)參數(shù),epoll_create的參數(shù)沒(méi)有多大的實(shí)際意義,可以隨意取值,用戶態(tài)和內(nèi)核態(tài)共享epfd
- 利用epoll_ctl對(duì)epfd進(jìn)行配置,添加了五個(gè)fd-events的數(shù)據(jù),沒(méi)有revents
epoll的執(zhí)行流程
- 當(dāng)有數(shù)據(jù)的時(shí)候,會(huì)把相應(yīng)的文件描述符置位,但是epoll沒(méi)有revent標(biāo)志位,并非真正的置位,而是將有數(shù)據(jù)的文件描述符放到隊(duì)首
- epoll返回有數(shù)據(jù)的文件描述符的個(gè)數(shù)
- 根據(jù)返回的個(gè)數(shù),讀取前n個(gè)文件描述符即可
- 讀取并處理