I/O複用系統調用之select()和poll()

I/O複用是一種讓進程預先告知內核的能力,使得內核一旦發現進程指定的一個或多個I/O條件就緒(如可以讀/寫了),內核就通知進程。主要有select、poll和epoll三種函數支持。調用這幾個函數時,不會阻塞在真正的I/O函數上(如read、write),而是阻塞在這幾個系統調用上,直到指定的I/O條件就緒。

下邊看看select系統調用的應用場景。

I/O複用系統調用之select()——I/O複用場景

以下情況需要使用I/O複用技術:

  • 客戶端需要同時處理多個socket;
  • 客戶端需要同時處理用戶輸入和網絡連接;
  • 服務器需要同時處理監聽socket和連接socket;
  • 服務器需要同時處理TCP請求和UDP請求;
  • 服務器需要同時監聽多個端口;

select()函數原型:

#include <sys/select.h>
int select(int nfds, //被監聽描述符總數,通常被設置爲select監聽的最大描述符號+1
fd_set* readfds, //接下來三個參數指向可讀、可寫、異常所對應描述符的集合
fd_set* writefds,
fd_set* exceptfds,
struct timeval* timeout); //設置爲NULL,則阻塞直到指定描述符事件發生,或設置超時時間沒有任何描述符事件就緒就返回0
 
timeval結構體如下:
struct timeval { //timeval的兩個域都爲0,則select不會阻塞,只是簡單的輪詢指定的描述符集合
long tv_sec; //秒數
long tv_usec; //微秒數
 
返回值: select成功返回就緒描述符的個數;
失敗返回-1(信號中斷返回-1,設置errorEINTR

select函數的第四個參數說的“異常”並不是說文件描述符出現了錯誤,在Linux上一個異常情況只會在下面兩種情況下發生:

  • 連接到處於信包模式下的僞終端主設備上的從設備狀態發生了改變;
  • 流式套接字上接收到了帶外數據。
FD_ZERO(fd_set *fds): 清空fds與所有文件句柄的聯繫。
FD_SET(int fd, fd_set *fds):建立文件句柄fdfds的聯繫。
FD_CLR(int fd, fd_set *fds):清除文件句柄fdfds的聯繫。
FD_ISSET(int fd, fd_set *fds):檢查fds聯繫的文件句柄fd是否

readfds、writefds和exceptions指向的結構體都是保存結果的值。由於這些結構體會在調用中被修改,如果要在循環中重複調用select(0,必須保證每次都要重新初始化它們。

可讀的條件:

  • socket內核接收緩衝區中字節大於等於低水位(低水位爲套接字選項中的內容)
  • socket對方關閉連接
  • 監聽socket上有新的連接
  • socket上有待處理的錯誤

可寫的條件

  • socket中發送緩衝區字節大於低水位
  • socket對方寫操作關閉
  • socket上使用非阻塞connect連接成功或者失敗之後
  • socket上有未處理的錯誤

select()調用的缺點:

  • 單個進程可監視的fd數量被限制(FD_SETSIZE限定了select能容納的最大描述符數,在Linux上通常爲1024);
  • 對socket採用輪詢的方法;
  • 需要維護一個用來存放大量fd的數據結構

select實例:

select監聽的描述符上有普通數據,標誌着這個select上有可讀事件;如果上邊有帶外數據標誌着select上有異常發生。

先看一個簡單的客戶/服務器回射程序(未使用select),客戶從標準輸入stdin讀入一行數據,發送給服務器,然後又從服務器讀取該行數據,回顯至客戶的標準輸出stdout。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/mman.h>
 
void sig_chld(int signo)
{
pid_t pid;
int stat;
 
while((pid = waitpid(-1,&stat,WNOHANG)) > 0)
printf("child %d terminated\n",pid);
return;
}
 
void str_echo(int sockfd)
{
ssize_t n;
char buf[BUFSIZ];
 
again:
while((n = read(sockfd, buf, BUFSIZ)) > 0)
write(sockfd,buf,n);
 
if(n < 0 && errno == EINTR)
goto again;
else if(n < 0)
printf("str_echo:%m\n"),exit(0);
}
 
int main(int argc, char **argv)
{
int listenfd,connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
void sig_chld(int);
 
listenfd = socket(AF_INET,SOCK_STREAM, 0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("192.168.234.129");
servaddr.sin_port = htons(13000);
 
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,3);
signal(SIGCHLD,sig_chld);
 
for(;;) {
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen)) < 0) {
if(errno == EINTR)
continue;
else
printf("accept: %m\n"),exit(1);
}
if((childpid = fork()) == 0) {
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
 
void str_cli(FILE *fp, int sockfd)
{
char sendline[BUFSIZ],recvline[BUFSIZ];
 
while(fgets(sendline,BUFSIZ,fp) != NULL) {
write(sockfd,sendline,strlen(sendline));
if(read(sockfd,recvline,BUFSIZ) == 0)
printf("readline:%m\n"),exit(1);
fputs(recvline,stdout);
}
}
 
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
 
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13000);
inet_aton("192.168.234.129",&servaddr.sin_addr);
 
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
 
str_cli(stdin,sockfd);
 
exit(0);
}

服務器程序運行,客戶端啓動,輸入test,回顯test正常: 
 
 
客戶端進程阻塞於stdin,等待標準輸入(example行),此時關閉服務器端程序,客戶端仍然阻塞於stdin。 
 
 
該客戶端程序中存在一個問題:當客戶程序阻塞於從標準輸入fgets讀數據時,服務器程序退出, 
客戶端無法立刻收到通知。爲了解決這個問題可以通過使用select重寫該客戶端程序的str_cli函數,使得服務器進程一終止,客戶就能立刻得到通知。

void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[BUFSIZ];
int n;
 
stdineof = 0;
FD_ZERO(&rset);
for(;;) {
if(stdineof == 0)
FD_SET(fileno(fp),&rset); //fileno函數把標準I/O文件指針轉換爲對應的
描述符
FD_SET(sockfd,&rset);
if(fileno(fp) > sockfd)
maxfdp1 = fileno(fp) + 1;
else
maxfdp1 = sockfd + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
 
if(FD_ISSET(sockfd,&rset)) {
if((n = read(sockfd, buf, BUFSIZ)) == 0) {
if(stdineof == 1){
printf("stdineof == 1\n");
return; }
else
printf("read:%m\n"),exit(1);
}
write(fileno(stdout), buf, n);
}
if(FD_ISSET(fileno(fp),&rset)) {
if((n = read(fileno(fp), buf, BUFSIZ)) == 0) {
stdineof = 1;
shutdown(sockfd, SHUT_WR);
FD_CLR(fileno(fp),&rset);
continue;
}
write(sockfd, buf, n);
}
}
}

poll()系統調用

和select()函數功能類似,

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
 
fdspollfd結構體數組,定義如下:
struct pollfd {
int fd; //文件描述符
short events; //註冊的事件,一系列事件按位或實現
short revents; //實際發生的事件,由內核填充,通知應用程序發生了哪些事件,若對某個描述符不感興趣可將該字段設置爲0
}
 
nfds指定了數組fds中元素的個數(nfds_t 爲無符號整型);
timeout參數同select()函數;

poll()返回值: 
-1:表示發生錯誤,可能是EINTR信號中斷; 
0:表示該調用在任意一個文件描述符稱爲就緒態之前超時; 
正整數:表示數組fds中擁有的非零revents字段的pollfd結構體數量。

select和poll返回正整數有一些差別。若一個文件描述符在返回的描述符集合中出現了不止一次,系統調用select()會將同一個文件描述符計數多次;而poll()系統調用返回的是就緒態的文件描述符個數,且一個文件描述符只會統計一次,就算在相應的revents字段中設定了多個位掩碼也是如此。

poll事件類型: 

發佈了45 篇原創文章 · 獲贊 17 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章