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,設置error爲EINTR)
select函數的第四個參數說的“異常”並不是說文件描述符出現了錯誤,在Linux上一個異常情況只會在下面兩種情況下發生:
- 連接到處於信包模式下的僞終端主設備上的從設備狀態發生了改變;
- 流式套接字上接收到了帶外數據。
FD_ZERO(fd_set *fds): 清空fds與所有文件句柄的聯繫。
FD_SET(int fd, fd_set *fds):建立文件句柄fd與fds的聯繫。
FD_CLR(int fd, fd_set *fds):清除文件句柄fd與fds的聯繫。
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);
fds爲pollfd結構體數組,定義如下:
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事件類型: