select 是linux i/o 復用技術之一
man 2 select
#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);
nfds是監聽文件描述符的總數。它通常被設置為select監聽的所有文件描述符的最大值加1.
readfds, writefds, exceptfds指向可讀、可寫、異常等事件對應的文件描述符集合。應用程序調用select時,通過這3個參數,傳入自己感興趣的文件描述符。select返回時,內核通過修改它們來通知應用程序哪些文件描述符已經就緒。我們看到,應用程序和內核都修改了這些參數。
fd_set 是結構體,定義如下:
typedef struct { /* XPG4.2 requires this member name. Otherwise avoid the name from the global namespace. */ #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->fds_bits) #else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->__fds_bits) #endif } fd_set;
該結構體只包含一個整形數組。其中,每個元素的一個位,標記一個文件描述符。fd_set 能容納的文件描述符數量由__FD_SETSIZE決定,默認1024.所以select能處理的最大文件描述符個數為1024.
對這個結構體的操作使用位運算。select.h文件中,提供了用來操作的宏
# define __FD_ZERO(set) \ do { \ unsigned int __i; \ fd_set *__arr = (set); \ for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \ __FDS_BITS (__arr)[__i] = 0; \ } while (0) #define __FD_SET(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) #define __FD_CLR(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
timeout參數設置超時時間,這是timeval結構體。使用指針是因為內核將修改它來告訴應用程序select等了多久。
struct timeval { __kernel_time_t tv_sec; /* seconds */ __kernel_suseconds_t tv_usec; /* microseconds */ };
如果timeout變量結構體成員都是0,則select立即返回。timeout為NULL,select一直阻塞,直到有文件描述符就緒。
select成功時返回就緒文件描述符總數。如果超時時間內沒有就緒的,返回0. select失敗時返回-1 並設置errno。如果select等待期間,程序收到信號,select立即返回-1,並設置errno為EINTR。
例子代碼,服務端程序:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/select.h> #include <sys/types.h> #include <sys/time.h> #include <arpa/inet.h> #include <netinet/in.h> #include <errno.h> #include <set> using namespace std; #define BUFSIZE 10 int createSocket() { struct sockaddr_in servaddr; int listenfd = -1; if (-1 == (listenfd = socket(PF_INET, SOCK_STREAM, 0))) { fprintf(stderr, "socket: %d, %s\n", errno, strerror(errno)); exit(1); } int reuseaddr = 1; if (-1 == setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr))) { fprintf(stderr, "setsockopt: %d, %s\n", errno, strerror(errno)); exit(1); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = PF_INET; servaddr.sin_port = htons(8008); inet_pton(PF_INET, "0.0.0.0", &servaddr.sin_addr); if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) { fprintf(stderr, "bind: %d, %s\n", errno, strerror(errno)); exit(1); } if (-1 == listen(listenfd, 5)) { fprintf(stderr, "listen: %d, %s\n", errno, strerror(errno)); exit(1); } return listenfd; } int main(int argc, char const *argv[]) { fd_set rfds; char buf[BUFSIZE] = {0}; int listenfd = createSocket(); // 保存所有的文件描述符 set<int> fdset; fdset.insert(listenfd); while (1) { FD_ZERO(&rfds); // 每次都要重新設置rfds.因為select返回時,rfds被內核改變,里面只保存了就緒的文件描述符 for (int fd : fdset) { FD_SET(fd, &rfds); } int ret = select(*fdset.rbegin()+1, &rfds, NULL, NULL, NULL); if (ret > 0) { for (int fd : fdset) { // 有新的連接 if (fd == listenfd && FD_ISSET(fd, &rfds)) { struct sockaddr_in client; socklen_t addrlen = sizeof(client); int cfd = -1; if (-1 == (cfd = accept(listenfd, (struct sockaddr*)&client, &addrlen))) { fprintf(stderr, "accept: %d, %s\n", errno, strerror(errno)); exit(1); } // 添加到 fd_set 結構體,並記錄到 set FD_SET(cfd, &rfds); fdset.insert(cfd); printf("get connection fd: %d\n", cfd); } else if (FD_ISSET(fd, &rfds)) { int lenrecv = -1; memset(buf, 0, BUFSIZE); lenrecv = recv(fd, buf, BUFSIZE-1, 0); if (lenrecv > 0) { printf("%s\n", buf); } else if (0 == lenrecv) { // 客戶端退出,刪除文件描述符,並關閉 fdset.erase(fd); FD_CLR(fd, &rfds); printf("delete connection fd: %d\n", fd); close(fd); } else { fprintf(stderr, "recv: %d, %s\n", errno, strerror(errno)); exit(1); } } } } else { fprintf(stderr, "select: %d, %s\n", errno, strerror(errno)); exit(1); } } close(listenfd); return 0; }