一. select 系统调用
1. 函数说明
#include <sys/select.h>
#include <sys/time.h>int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
-
nfds
: 是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
。在linux系统中,select
的默认最大值为1024
。设置这个值的目的是为了不用每次都去轮询这1024
个fd
,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测nfds+1
个套接字就可以了,这样可以减少轮询时间以及系统的开销。 -
readfds
: 用来检测输入是否准备就绪的文件描述符集合(从内核读,用的最多) -
writefds
: 用来检测输出是否准备就绪的文件描述符集合(往内核写,比较少用) -
exceptset
; 很少用到,我没用过。用来检测异常情况是否发生的文件描述符集合。 -
timeout
: 堵塞的超时时间,如果为NULL
则堵塞。
系统调用 select() 会一直堵塞,直到一个或者多个文件描述符集合称为就绪态,select 就会往下执行。
套接字可写的条件
这个概念很常见,跟套接字可读一样。
- 使用
select
监听,如果是可读套接字的fd_set
,那么套接字对应的内核中,一旦收到数据,那么就会使select
返回,fd_set
准备就绪。 - 同样道理,使用
select
监听,如果是可写的套接字的fd_set
,那么套接字对应的内核中,一旦数据的缓冲区有空余的位置写数据,那么就会使select
返回,fd_set
准备就绪。这句话有些难理解。
具体的说:
一、 满足下列四个条件中的任何一个时,一个套接字准备好读。
- 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓存区低水位。 对于TCP和UDP套接字而言,缓冲区低水位的值默认为
1
。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。我们可以通过使用SO_RCVLOWAT
套接字选项(参见setsockopt函数)来设置该套接字的低水位大小。此种描述符就绪(可读)的情况下,当我们使用read/recv
等对该套接字执行读操作的时候,套接字不会阻塞,而是成功返回一个大于0
的值(即可读数据的大小)。 - 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作,将不会阻塞,而是返回0(也就是EOF)。
- 该套接字是一个listen的监听套接字,并且目前已经完成的连接数不为0。对这样的套接字进行accept操作通常不会阻塞。
- 有一个错误套接字待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
二、满足下列四个条件中的任何一个时,一个套接字准备好写。
- 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓存区低水位标记时,并且该套接字已经成功连接(UDP套接字不需要连接)。对于TCP和UDP而言,这个低水位的值默认为
2048
,而套接字默认的发送缓冲区大小是8k,这就意味着一般一个套接字连接成功后,就是处于可写状态的。我们可以通过SO_SNDLOWAT
套接字选项(参见setsockopt函数)来设置这个低水位。此种情况下,我们设置该套接字为非阻塞,对该套接字进行写操作(如write,send等),将不阻塞,并返回一个正值(例如由传输层接受的字节数,即发送的数据大小)。 - 该连接的写半部关闭(主动发送FIN包的TCP连接)。对这样的套接字的写操作将会产生SIGPIPE信号。所以我们的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。
- 使用非阻塞的connect套接字已建立连接,或者connect已经以失败告终。即connect有结果了。
- 有一个错误的套接字待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
- 后面有一个例子,是关于一个
readfds
的fd_set
是一个标准输入,一个writefds
的fd_set
是标准输出。使用select
堵塞等待。 - 当键盘有输入时
readfds
中是标准输出,select
会立马返回就绪 - 当监听
writefds
时,因为里面放的是标准输入,因为标准输入在正常情况下就是可写的,所以select
会立马返回并就绪,不会堵塞。
2. 数据类型 fd_set
这个网上资料很多,细节很多。我有空回来再补
操作 fd_set
的四个宏:
#include <sys/select.h> int FD_ZERO(int fd, fd_set *fdset); // 初始化:相当于将 fd_set 清零
int FD_CLR(int fd, fd_set *fdset); // 将文件描述符 fd 从 fd_set 中移除
int FD_SET(int fd, fd_set *fd_set); // 将文件描述符 fd 加入 fd_set 中
int FD_ISSET(int fd, fd_set *fdset); // 如果文件描述符 fd 是 fd_set 所指向的成员,返回 true
3. timeout
参数
- 类型:
struct timeval
#include <sys/time.h>struct timeval{long tv_sec; /*秒 */long tv_usec; /*微秒 */ }
上面说,timeout
为 NULL
时,select
会堵塞。如果 timeout
的两个值都为 0
,不会堵塞。select
直接立即返回
4. select
返回值
这个看多了用多了代码就会了,大部分使用select
的代码都会出现判断它的所有返回值的:
while(1)
{...ret = select(nfds, readfds, ...);if (ret > 0) {}else if (ret == 0) {}else if (ret < 0) {}...
}
返回值:
- 大于 0:返回一个或多个已经就绪的文件描述符
- 小于 0: 有错误发生:一般是
EBADF
和EINTR
。具体上网搜 - 等于 0: 超时,每个文件描述符集合会清空。
5. example
"t_select.h"
在文末获取。
#include <sys/time.h>
#include <sys/select.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include "t_select.h"/*** example:* ./t_select 10 0r 1w* * comment:* 10: timeout(second)* 0r: 以读形式读取标准输入* 1w: 以写形式去写标准输出
*/int main(int argc, char *argv[])
{fd_set readfds, writefds;int ready, nfds, fd, numRead, j;struct timeval timeout;struct timeval *pto;char buf[10]; /* Large enough to hold "rw\0" */if (argc < 2 || strcmp(argv[1], "--help") == 0)usageError(argv[0]);/* Timeout for select() is specified in argv[1] */// "-": 代表堵塞; 否则就是超时时间if (strcmp(argv[1], "-") == 0){pto = NULL; /* Infinite timeout */}else{pto = &timeout;timeout.tv_sec = getLong(argv[1], 0, "timeout");timeout.tv_usec = 0; /* No microseconds */}/* Process remaining arguments to build file descriptor sets */nfds = 0;FD_ZERO(&readfds);FD_ZERO(&writefds);for (j = 2; j < argc; j++){numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);if (numRead != 2)usageError(argv[0]);if (fd >= FD_SETSIZE)printf("file descriptor exceeds limit (%d)\n", FD_SETSIZE);if (fd >= nfds)nfds = fd + 1; /* Record maximum fd + 1 */if (strchr(buf, 'r') != NULL)FD_SET(fd, &readfds);if (strchr(buf, 'w') != NULL)FD_SET(fd, &writefds);}/* We've built all of the arguments; now call select() */// 第一个参数:套接字返回ready = select(nfds, &readfds, &writefds, NULL, pto);/* Ignore exceptional events */if (ready == -1)usageError("select");/* Display results of select() */printf("ready = %d\n", ready);for (fd = 0; fd < nfds; fd++)printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "",FD_ISSET(fd, &writefds) ? "w" : "");if (pto != NULL)printf("timeout after select(): %ld.%03ld\n",(long)timeout.tv_sec, (long)timeout.tv_usec / 1000);exit(EXIT_SUCCESS);
}
文末
/**************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>#define GET_NUM_H#define GN_NONNEG 01 /* Value must be >= 0 */
#define GN_GT_0 02 /* Value must be > 0 *//* By default, integers are decimal */
#define GN_ANY_BASE 0100 /* Can use any base - like strtol(3) */
#define GN_BASE_8 0200 /* Value is expressed in octal */
#define GN_BASE_16 0400 /* Value is expressed in hexadecimal *//* Print a diagnostic message that contains a function name ('fname'),the value of a command-line argument ('arg'), the name of thatcommand-line argument ('name'), and a diagnostic error message ('msg'). */
static void
gnFail(const char *fname, const char *msg, const char *arg, const char *name)
{fprintf(stderr, "%s error", fname);if (name != NULL)fprintf(stderr, " (in %s)", name);fprintf(stderr, ": %s\n", msg);if (arg != NULL && *arg != '\0')fprintf(stderr, " offending text: %s\n", arg);exit(EXIT_FAILURE);
}
/* Convert a numeric command-line argument ('arg') into a long integer,returned as the function result. 'flags' is a bit mask of flags controllinghow the conversion is done and what diagnostic checks are performed on thenumeric result; see get_num.h for details.'fname' is the name of our caller, and 'name' is the name associated withthe command-line argument 'arg'. 'fname' and 'name' are used to print adiagnostic message in case an error is detected when processing 'arg'. */static long
getNum(const char *fname, const char *arg, int flags, const char *name)
{long res;char *endptr;int base;if (arg == NULL || *arg == '\0')gnFail(fname, "null or empty string", arg, name);base = (flags & GN_ANY_BASE) ? 0 : (flags & GN_BASE_8) ? 8: (flags & GN_BASE_16) ? 16: 10;errno = 0;res = strtol(arg, &endptr, base);if (errno != 0)gnFail(fname, "strtol() failed", arg, name);if (*endptr != '\0')gnFail(fname, "nonnumeric characters", arg, name);if ((flags & GN_NONNEG) && res < 0)gnFail(fname, "negative value not allowed", arg, name);if ((flags & GN_GT_0) && res <= 0)gnFail(fname, "value must be > 0", arg, name);return res;
}
/* Convert a numeric command-line argument string to a long integer. See thecomments for getNum() for a description of the arguments to this function. */long getLong(const char *arg, int flags, const char *name)
{return getNum("getLong", arg, flags, name);
}
/* Convert a numeric command-line argument string to an integer. See thecomments for getNum() for a description of the arguments to this function. */int getInt(const char *arg, int flags, const char *name)
{long res;res = getNum("getInt", arg, flags, name);if (res > INT_MAX || res < INT_MIN)gnFail("getInt", "integer out of range", arg, name);return res;
}
/***********************************************************************************************************/static void usageError(const char *progName)
{fprintf(stderr, "Usage: %s {timeout|-} fd-num[rw]...\n", progName);fprintf(stderr, " - means infinite timeout; \n");fprintf(stderr, " r = monitor for read\n");fprintf(stderr, " w = monitor for write\n\n");fprintf(stderr, " e.g.: %s - 0rw 1w\n", progName);exit(EXIT_FAILURE);
}