select函數詳解
-
-
-
- 背景
- 說明
-
- 定義
- 介紹、
- 參數說明
- 原理
- 傳回值
- pselect
- 總結
- 案例
-
- 案例1
- 案例2
-
-
說明:本文整合網絡資源和man幫助文檔,請酌情參考。
背景
select函數是實作IO多路複用的一種方式。
什麼是IO多路複用?
舉一個簡單地網絡伺服器的例子,如果你的伺服器需要和多個用戶端保持連接配接,處理用戶端的請求,屬于多程序的并發問題,如果建立很多個程序來處理這些IO流,會導緻CPU占有率很高。是以人們提出了I/O多路複用模型:一個線程,通過記錄I/O流的狀态來同時管理多個I/O。
select隻是IO複用的一種方式,其他的還有:poll,epoll等。
說明
定義
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
介紹、
select()函數允許程式監視多個檔案描述符,等待所監視的一個或者多個檔案描述符變為“準備好”的狀态。所謂的”準備好“狀态是指:檔案描述符不再是阻塞狀态,可以用于某類IO操作了,包括可讀,可寫,發生異常三種。
我們使用select來監視檔案描述符時,要向核心傳遞的資訊包括:
1、我們要監視的檔案描述符個數
2、每個檔案描述符,我們可以監視它的一種或多種狀态,包括:可讀,可寫,發生異常三種。
3、要等待的時間,監視是一個過程,我們希望核心監視多長時間,然後傳回給我們監視結果呢?
4、監視結果包括:準備好了的檔案描述符個數,對于讀,寫,異常,分别是哪兒個檔案描述符準備好了。
參數說明
**nfds:**是一個整數值, 表示集合中所有檔案描述符的範圍,即所有檔案描述符的最大值+1。在windows中不需要管這個。
linux select第一個參數的函數: 待測試的描述集的總個數。 但要注意, 待測試的描述集總是從0, 1, 2, …開始的。 是以, 假如你要檢測的描述符為8, 9, 10, 那麼系統實際也要監測0, 1, 2, 3, 4, 5, 6, 7, 此時真正待測試的描述符的個數為11個, 也就是max(8, 9, 10) + 1
注意:
1、果你要檢測描述符8, 9, 10, 但是你把select的第一個參數定為8, 實際上隻檢測0到7, 是以select不會感覺到8, 9, 10描述符的變化。
2、果你要檢測描述符8, 9, 10, 且你把select的第一個參數定為11, 實際上會檢測0-10, 但是, 如果你不把描述如0 set到描述符中, 那麼select也不會感覺到0描述符的變化。
是以, select感覺到描述符變化的必要條件是, 第一個參數要合理, 比如定義為fdmax+1, 且把需要檢測的描述符set到描述集中。
fd_set:
一個檔案描述符集合儲存在fd_set變量中,可讀,可寫,異常這三個描述符集合需要使用三個變量來儲存,分别是 readfds,writefds,exceptfds。我們可以認為一個fd_set變量是由很多個二進制構成的數組,每一位表示一個檔案描述符是否需要監視。
對于fd_set類型的變量,我們隻能使用相關的函數來操作。
void FD_CLR(int fd, fd_set *set);//清除某一個被監視的檔案描述符。
int FD_ISSET(int fd, fd_set *set);//測試一個檔案描述符是否是集合中的一員
void FD_SET(int fd, fd_set *set);//添加一個檔案描述符,将set中的某一位設定成1;
void FD_ZERO(fd_set *set);//清空集合中的檔案描述符,将每一位都設定為0;
使用案例:
fd_set readfds;
int fd;
FD_ZERO(&readfds)//新定義的變量要清空一下。相當于初始化。
FD_SET(fd,&readfds);//把檔案描述符fd加入到readfds中。
//select 傳回
if(FD_ISSET(fd,&readset))//判斷是否成功監視
{
//dosomething
}
readfds:
監視檔案描述符的一個集合,我們監視其中的檔案描述符是不是可讀,或者更準确的說,讀取是不是不阻塞了。
writefds:
監視檔案描述符的一個集合,我們監視其中的檔案描述符是不是可寫,或者更準确的說,寫入是不是不阻塞了。
exceptfds:
用來監視發生錯誤異常檔案
timeout
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
}
timeout表示select傳回之前的時間上限。
如果timeout==NULL,無期限等待下去,這個等待可以被一個信号中斷,隻有當一個描述符準備好,或者捕獲到一個信号時函數才會傳回。如果是捕獲到信号,select傳回-1,并将變量errno設定成EINTR。
如果timeout->tv_sec0 && timeout->tv_sec0 ,不等待直接傳回,加入的描述符都會被測試,并且傳回滿足要求的描述符個數,這種方法通過輪詢,無阻塞地獲得了多個檔案描述符狀态。
如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的時間。當有描述符複合條件或者超過逾時時間的話,函數傳回。等待總是會被信号中斷。
原理
了解select模型的關鍵在于了解fd_set,為說明友善,取fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd。
執行fd_set set;FD_ZERO(&set);則set用位表示是0000,0000。
若fd=5,執行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)
若再加入fd=2,fd=1,則set變為0001,0011
執行select(6,&set,0,0,0)阻塞等待
若fd=1,fd=2上都發生可讀事件,則select傳回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。
傳回值
成功時:傳回三中描述符集合中”準備好了“的檔案描述符數量。
逾時:傳回0
錯誤:傳回-1,并設定 errno
EBADF:集合中包含無效的檔案描述符。(檔案描述符已經關閉了,或者檔案描述符上已經有錯誤了)。
EINTR:捕獲到一個信号。
EINVAL:nfds是負的或者timeout中包含的值無效。
ENOMEM:無法為内部表配置設定記憶體。
pselect
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
select和pselect有三個主要的差別:
1、select逾時使用的是struct timeval,用秒和微秒計時,而pselect使用struct timespec ,用秒和納秒。
struct timespec{
time_t tv_sec;//秒
long tv_nsec;//納秒
}
2、select會更新逾時參數timeout 以訓示還剩下多少時間,pselect不會。
3、select沒有sigmask參數.
sigmask:這個參數儲存了一組核心應該打開的信号(即:從調用線程的信号掩碼中删除)
當pselect的sigmask==NULL時pselect和select一樣
當sigmask!=NULL時,等效于以下原子操作:
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);
接收信号的程式通常隻使用信号處理程式來引發全局标志。全局标志将訓示事件必須被處理。在程式的主循環中。一個信号将導緻select和pselect傳回-1 并将erron=EINTR。
我們經常要在主循環中處理信号,主循環的某個位置将會檢查全局标志,那麼我們會問:如果信号在條件之後,select之前到達怎麼辦。答案是select會無限期阻塞。
這種情況很少見,但是這就是為什麼出現了pselect。因為他是類似原子操作的。
舉個栗子:
static volatile sig_atomic_t got_SIGCHLD = 0;
static void
child_sig_handler(int sig)
{
got_SIGCHLD = 1;
}
int
main(int argc, char *argv[])
{
sigset_t sigmask, empty_mask;
struct sigaction sa;
fd_set readfds, writefds, exceptfds;
int r;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
sa.sa_flags = 0;
sa.sa_handler = child_sig_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
sigemptyset(&empty_mask);
for (;;) { /* main loop */
/* Initialize readfds, writefds, and exceptfds
before the pselect() call. (Code omitted.) */
r = pselect(nfds, &readfds, &writefds, &exceptfds,
NULL, &empty_mask);
if (r == -1 && errno != EINTR) {
/* Handle error */
}
if (got_SIGCHLD) {
got_SIGCHLD = 0;
/* Handle signalled event here; e.g., wait() for all
terminated children. (Code omitted.) */
}
/* main body of program */
}
}
總結
select()可以同時監視多個描述符,如果他們沒有活動,則正确地将程序置于休眠狀态。Unix程式員們經常要處理多個檔案描述符的I/O,他們的資料流可能是間歇性的。如果隻建立read或者write會導緻程式阻塞。
在我們使用select的時候,需要注意:
1、我們應該總是設定timeout=0,因為如果沒有可用的資料,程式在運作時間裡将無視可做。依賴逾時的代碼通常是不可移植,并且很難調試。
2、nfds的值一要準備且适當。
3、如果在調用完select之後,你不想檢查結果,也不想做出适當的響應,那麼檔案描述符不需要添加到集合中。
4、select傳回後,所有的檔案描述符都應該被檢查,看看他們是否準備好了。
5、read,recv,write,send,這幾個函數不一定讀/寫你所請求的全部資料。如果他們讀/寫全部資料,是因為低流量負載和快速流。情況并非重視如此,應該處理你的函數僅管理發送或接收單個位元組的情況。
6、除非你真的确信你有少量的資料要處理,否則不要一次隻讀一個位元組,當你每次都能緩沖的時候,盡可能多的讀取資料是非常低效的。
7、read,recv,write,send和select都會有傳回-1的情況,并set errno的值。這些errno必須被恰當的處理。如果你的程式不會接收到任何信号,那麼errno永遠都不會等于EINTR,如果你的程式并不會設定非阻塞IO,那麼errno就不會等于EAGAIN。
8、調用read,recv,write,send,不要使buffer的長度為0;
9、如果read,recv,write,send調用失敗,并且傳回的errno不是7中說的那兩種情況,或者傳回0,意思是“end-of-file”,這種情況下我們不應再将檔案描述符傳遞給select。
10、每次調用select之前,timeout都用重新設定。
11、由于select()修改其檔案描述符集,如果調用在循環中使用,則必須在每次調用之前重新初始化這些集。
大多數的作業系統都支援select。相比于試圖用線程,程序,IPCS,信号,記憶體共享等方式來解決問題,select函數更有效且輕松。系統調用poll和select相似,在監視稀疏檔案集合的時候更加有效。poll現在也在被廣泛的使用,但沒有select簡便。linux專用的epoll在監視大連資料時比select和poll更加有效。
案例
案例1
下面是"man select "幫助文檔中案例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
fd_set rfds;//定義一個能儲存檔案描述符集合的變量
struct timeval tv;//定義逾時時間
int retval;//儲存傳回值
/* Watch stdin (fd 0) to see when it has input. */
/* 監測标準輸入流(fd=0)看什麼時候又輸入*/
FD_ZERO(&rfds);//初始化集合
FD_SET(0, &rfds);//把檔案描述符0加入到監測集合中。
/* Wait up to five seconds. */
/* 設定逾時時間為5s */
tv.tv_sec = 5;
tv.tv_usec = 0;
/*調用select函數,将檔案描述符集合設定成讀取監測 */
retval = select(1, &rfds, NULL, NULL, &tv);
/* Don't rely on the value of tv now! */
/* 這時候的tv值是不可依賴的 */
/*根據傳回值類型判斷select函數 */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
/* 因為值增加了一個fd,如果傳回值>0,則說明fd=0在集合中。*/
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
案例2
下面是"man select_tut "幫助文檔中案例:
這個例子更好的說明了select函數的作用,這是一個TCP轉發相關的程式,從一個端口轉發到另一個端口
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
static int forward_port;
#undef max
#define max(x,y) ((x) > (y) ? (x) : (y))
static int listen_socket(int listen_port)
{
struct sockaddr_in a;
int s;
int yes;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
return -1;
}
yes = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(char *) &yes, sizeof(yes)) == -1) {
perror("setsockopt");
close(s);
return -1;
}
memset(&a, 0, sizeof(a));
a.sin_port = htons(listen_port);
a.sin_family = AF_INET;
if (bind(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
perror("bind");
close(s);
return -1;
}
printf("accepting connections on port %d\n", listen_port);
listen(s, 10);
return s;
}
static int connect_socket(int connect_port, char *address)
{
struct sockaddr_in a;
int s;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
close(s);
return -1;
}
memset(&a, 0, sizeof(a));
a.sin_port = htons(connect_port);
a.sin_family = AF_INET;
if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) {
perror("bad IP address format");
close(s);
return -1;
}
if (connect(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
perror("connect()");
shutdown(s, SHUT_RDWR);
close(s);
return -1;
}
return s;
}
#define SHUT_FD1 do { \
if (fd1 >= 0) { \
shutdown(fd1, SHUT_RDWR); \
close(fd1); \
fd1 = -1; \
} \
} while (0)
#define SHUT_FD2 do { \
if (fd2 >= 0) { \
shutdown(fd2, SHUT_RDWR); \
close(fd2); \
fd2 = -1; \
} \
} while (0)
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int h;
int fd1 = -1, fd2 = -1;
char buf1[BUF_SIZE], buf2[BUF_SIZE];
int buf1_avail, buf1_written;
int buf2_avail, buf2_written;
//我們希望調用主函數的時候,要指明,本地端口,發送端口,還有發送的ip位址
if (argc != 4) {
fprintf(stderr, "Usage\n\tfwd <listen-port> "
"<forward-to-port> <forward-to-ip-address>\n");
exit(EXIT_FAILURE);
}
// 忽略SIGPIPE這個信号,這個信号常出現在網絡程式設計中,通路一個已經關閉的檔案描述符時候出現。
signal(SIGPIPE, SIG_IGN);
//确定發送端口
forward_port = atoi(argv[2]);
//監聽本地端口
h = listen_socket(atoi(argv[1]));
if (h == -1)
exit(EXIT_FAILURE);
for (;;) {
int r, nfds = 0;
fd_set rd, wr, er;
FD_ZERO(&rd);
FD_ZERO(&wr);
FD_ZERO(&er);
FD_SET(h, &rd);
// 擷取nfds的值。并把fd1,fd2分别加入到,可讀,可寫,異常監視集合中去。
nfds = max(nfds, h);
if (fd1 > 0 && buf1_avail < BUF_SIZE) {
FD_SET(fd1, &rd);
nfds = max(nfds, fd1);
}
if (fd2 > 0 && buf2_avail < BUF_SIZE) {
FD_SET(fd2, &rd);
nfds = max(nfds, fd2);
}
if (fd1 > 0 && buf2_avail - buf2_written > 0) {
FD_SET(fd1, &wr);
nfds = max(nfds, fd1);
}
if (fd2 > 0 && buf1_avail - buf1_written > 0) {
FD_SET(fd2, &wr);
nfds = max(nfds, fd2);
}
if (fd1 > 0) {
FD_SET(fd1, &er);
nfds = max(nfds, fd1);
}
if (fd2 > 0) {
FD_SET(fd2, &er);
nfds = max(nfds, fd2);
}
//開始監視
r = select(nfds + 1, &rd, &wr, &er, NULL);
if (r == -1 && errno == EINTR)
continue;
if (r == -1) {
perror("select()");
exit(EXIT_FAILURE);
}
if (FD_ISSET(h, &rd)) {
unsigned int l;
struct sockaddr_in client_address;
memset(&client_address, 0, l = sizeof(client_address));
r = accept(h, (struct sockaddr *) &client_address, &l);
if (r == -1) {
perror("accept()");
} else {
SHUT_FD1;
SHUT_FD2;
buf1_avail = buf1_written = 0;
buf2_avail = buf2_written = 0;
fd1 = r;
fd2 = connect_socket(forward_port, argv[3]);
if (fd2 == -1)
SHUT_FD1;
else
printf("connect from %s\n",
inet_ntoa(client_address.sin_addr));
}
}
/* NB: read oob data before normal reads */
if (fd1 > 0)
if (FD_ISSET(fd1, &er)) {
char c;
r = recv(fd1, &c, 1, MSG_OOB);
if (r < 1)
SHUT_FD1;
else
send(fd2, &c, 1, MSG_OOB);
}
if (fd2 > 0)
if (FD_ISSET(fd2, &er)) {
char c;
r = recv(fd2, &c, 1, MSG_OOB);
if (r < 1)
SHUT_FD2;
else
send(fd1, &c, 1, MSG_OOB);
}
if (fd1 > 0)
if (FD_ISSET(fd1, &rd)) {
r = read(fd1, buf1 + buf1_avail,
BUF_SIZE - buf1_avail);
if (r < 1)
SHUT_FD1;
else
buf1_avail += r;
}
if (fd2 > 0)
if (FD_ISSET(fd2, &rd)) {
r = read(fd2, buf2 + buf2_avail,
BUF_SIZE - buf2_avail);
if (r < 1)
SHUT_FD2;
else
buf2_avail += r;
}
if (fd1 > 0)
if (FD_ISSET(fd1, &wr)) {
r = write(fd1, buf2 + buf2_written,
buf2_avail - buf2_written);
if (r < 1)
SHUT_FD1;
else
buf2_written += r;
}
if (fd2 > 0)
if (FD_ISSET(fd2, &wr)) {
r = write(fd2, buf1 + buf1_written,
buf1_avail - buf1_written);
if (r < 1)
SHUT_FD2;
else
buf1_written += r;
}
/* check if write data has caught read data */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* one side has closed the connection, keep
writing to the other side until empty */
if (fd1 < 0 && buf1_avail - buf1_written == 0)
SHUT_FD2;
if (fd2 < 0 && buf2_avail - buf2_written == 0)
SHUT_FD1;
}
exit(EXIT_SUCCESS);
}
上面的程式可以應用于大多數類型的TCP連接配接,包括telnet伺服器對OOB信号的轉發。它處理了同時在兩個方向上流動這一棘手問題。你可能會想,使用連個程序不是更有效嗎?事實上使用兩個程序會更複雜。另一個想法是使用fcntl設定非阻塞的I/O使用,這也有弊端,因為它使用非常低效的逾時。
這個程式不能處理同時有多個連接配接的情況,但很容易擴充。你隻需要為每個連接配接建立一個buffer。目前的程式中,新的連接配接會導緻舊的連接配接被覆寫丢棄。