Linux poll 函數用法

poll 函數用於檢測一組文件描述符(File Descriptor, fd)上的可讀可寫和出錯事件,其函數簽名如下:

#include <poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

參數解釋:

  • fds:指向一個結構體數組的首個元素的指針,每個數組元素都是一個 struct pollfd 結構,用於指定檢測某個給定的 fd 的條件;

  • nfds:參數 fds 結構體數組的長度,nfds_t 本質上是 unsigned long int,其定義如下:

typedef unsigned long int nfds_t;

  • timeout:表示 poll 函數的超時時間,單位為毫秒。

struct pollfd 結構體定義如下:

struct pollfd {
int fd; /* 待檢測事件的 fd */
short events; /* 關心的事件組合 */
short revents; /* 檢測後的得到的事件類型 */
};

struct pollfdevents 欄位是由開發者來設置,告訴內核我們關注什麼事件,而 revents 欄位是 poll 函數返回時內核設置的,用以說明該 fd 發生了什麼事件。eventsrevents 一般有如下取值:

poll 檢測一組 fd 上的可讀可寫和出錯事件的概念與前文介紹 select 的事件含義一樣,這裡就不再贅述。pollselect 相比具有如下優點:

  • poll 不要求開發者計算最大文件描述符加 1 的大小;
  • 相比於 selectpoll 在處理大數目的文件描述符的時候速度更快;
  • poll 沒有最大連接數的限制,原因是它是基於鏈表來存儲的;
  • 在調用 poll 函數時,只需要對參數進行一次設置就好了。

我們來看一個具體的例子:

/**
* 演示 poll 函數的用法,poll_server.cpp
* zhangyl 2019.03.16
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>

//無效fd標記
#define INVALID_FD -1

int main(int argc, char* argv[])
{
//創建一個偵聽socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}

//將偵聽socket設置為非阻塞的
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
{
close(listenfd);
std::cout << "set listenfd to nonblock error." << std::endl;
return -1;
}

//復用地址和埠號
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));

//初始化伺服器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}

//啟動偵聽
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}

std::vector<pollfd> fds;
pollfd listen_fd_info;
listen_fd_info.fd = listenfd;
listen_fd_info.events = POLLIN;
listen_fd_info.revents = 0;
fds.push_back(listen_fd_info);

//是否存在無效的fd標誌
bool exist_invalid_fd;
int n;
while (true)
{
exist_invalid_fd = false;
n = poll(&fds[0], fds.size(), 1000);
if (n < 0)
{
//被信號中斷
if (errno == EINTR)
continue;

//出錯,退出
break;
}
else if (n == 0)
{
//超時,繼續
continue;
}

for (size_t i = 0; i < fds.size(); ++i)
{
// 事件可讀
if (fds[i].revents & POLLIN)
{
if (fds[i].fd == listenfd)
{
//偵聽socket,接受新連接
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//接受客戶端連接, 並加入到fds集合中
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd != -1)
{
//將客戶端socket設置為非阻塞的
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set clientfd to nonblock error." << std::endl;
}
else
{
struct pollfd client_fd_info;
client_fd_info.fd = clientfd;
client_fd_info.events = POLLIN;
client_fd_info.revents = 0;
fds.push_back(client_fd_info);
std::cout << "new client accepted, clientfd: " << clientfd << std::endl;
}
}
}
else
{
//普通clientfd,收取數據
char buf[64] = { 0 };
int m = recv(fds[i].fd, buf, 64, 0);
if (m <= 0)
{
if (errno != EINTR && errno != EWOULDBLOCK)
{
//出錯或對端關閉了連接,關閉對應的clientfd,並設置無效標誌位
for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++ iter)
{
if (iter->fd == fds[i].fd)
{
std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl;
close(fds[i].fd);
iter->fd = INVALID_FD;
exist_invalid_fd = true;
break;
}
}
}
}
else
{
std::cout << "recv from client: " << buf << ", clientfd: " << fds[i].fd << std::endl;
}
}
}
else if (fds[i].revents & POLLERR)
{
//TODO: 暫且不處理
}

}// end outer-for-loop

if (exist_invalid_fd)
{
//統一清理無效的fd
for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); )
{
if (iter->fd == INVALID_FD)
iter = fds.erase(iter);
else
++iter;
}
}
}// end while-loop

//關閉所有socket
for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++ iter)
close(iter->fd);

return 0;
}

編譯上述程序生成 poll_server 並運行,然後使用 nc 命令模擬三個客戶端並給 poll_server 發送消息,效果如下:

由於 nc 命令是以
作為結束標誌的,所以 poll_server 收到客戶端消息時顯示時分兩行的。

通過上面的示例代碼,我們也能看出 poll 函數存在的一些缺點:

  • 在調用 poll 函數時,不管有沒有有意義,大量的 fd 的數組被整體在用戶態和內核地址空間之間複製;
  • 與 select 函數一樣,poll 函數返回後,需要遍歷 fd 集合來獲取就緒的 fd,這樣會使性能下降;
  • 同時連接的大量客戶端在一時刻可能只有很少的就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。

select 函數實現非阻塞的 connect 原理一樣,我們可以使用 poll 去實現,即通過 poll 檢測 clientfd 在一定時間內是否可寫,示例代碼如下:

/**
* Linux 下使用poll實現非同步的connect,linux_nonblocking_connect_poll.cpp
* zhangyl 2019.03.16
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"

int main(int argc, char* argv[])
{
//1.創建一個socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}

//連接成功以後,我們再將 clientfd 設置成非阻塞模式,
//不能在創建時就設置,這樣會影響到 connect 函數的行為
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set socket to nonblock error." << std::endl;
return -1;
}

//2.連接伺服器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
for (;;)
{
int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (ret == 0)
{
std::cout << "connect to server successfully." << std::endl;
close(clientfd);
return 0;
}
else if (ret == -1)
{
if (errno == EINTR)
{
//connect 動作被信號中斷,重試connect
std::cout << "connecting interruptted by signal, try again." << std::endl;
continue;
} else if (errno == EINPROGRESS)
{
//連接正在嘗試中
break;
} else {
//真的出錯了,
close(clientfd);
return -1;
}
}
}

pollfd event;
event.fd = clientfd;
event.events = POLLOUT;
int timeout = 3000;
if (poll(&event, 1, timeout) != 1)
{
close(clientfd);
std::cout << "[poll] connect to server error." << std::endl;
return -1;
}

if (!(event.revents & POLLOUT))
{
close(clientfd);
std::cout << "[POLLOUT] connect to server error." << std::endl;
return -1;
}

int err;
socklen_t len = static_cast<socklen_t>(sizeof err);
if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
return -1;

if (err == 0)
std::cout << "connect to server successfully." << std::endl;
else
std::cout << "connect to server error." << std::endl;

//5. 關閉socket
close(clientfd);

return 0;
}

運行效果與前面的 select 實現這個非阻塞的 connect 一樣,這裡就不再給出運行效果圖了。


本文首發於『easyserverdev』公眾號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能伺服器開發 QQ 群一起交流: 578019391 。


推薦閱讀:
相关文章