Linux C select 的学习

一. select 系统调用

1. 函数说明

cpp 复制代码
#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 。设置这个值的目的是为了不用每次都去轮询这1024fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测nfds+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。

  • readfds: 用来检测输入是否准备就绪的文件描述符集合(从内核读,用的最多)

  • writefds: 用来检测输出是否准备就绪的文件描述符集合(往内核写,比较少用)

  • exceptset; 很少用到,我没用过。用来检测异常情况是否发生的文件描述符集合。

  • timeout: 堵塞的超时时间,如果为 NULL 则堵塞。

系统调用 select() 会一直堵塞,直到一个或者多个文件描述符集合称为就绪态,select 就会往下执行。

套接字可写的条件

这个概念很常见,跟套接字可读一样。

  1. 使用 select 监听,如果是可读套接字的 fd_set,那么套接字对应的内核中,一旦收到数据,那么就会使 select 返回,fd_set准备就绪。
  2. 同样道理,使用 select 监听,如果是可写的套接字的 fd_set,那么套接字对应的内核中,一旦数据的缓冲区有空余的位置写数据,那么就会使 select 返回,fd_set准备就绪。这句话有些难理解。

具体的说:

一、 满足下列四个条件中的任何一个时,一个套接字准备好读。

  1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓存区低水位。 对于TCP和UDP套接字而言,缓冲区低水位的值默认为1。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。我们可以通过使用SO_RCVLOWAT套接字选项(参见setsockopt函数)来设置该套接字的低水位大小。此种描述符就绪(可读)的情况下,当我们使用read/recv等对该套接字执行读操作的时候,套接字不会阻塞,而是成功返回一个大于0的值(即可读数据的大小)。
  2. 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作,将不会阻塞,而是返回0(也就是EOF)。
  3. 该套接字是一个listen的监听套接字,并且目前已经完成的连接数不为0。对这样的套接字进行accept操作通常不会阻塞。
  4. 有一个错误套接字待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

二、满足下列四个条件中的任何一个时,一个套接字准备好写。

  1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓存区低水位标记时,并且该套接字已经成功连接(UDP套接字不需要连接)。对于TCP和UDP而言,这个低水位的值默认为2048,而套接字默认的发送缓冲区大小是8k,这就意味着一般一个套接字连接成功后,就是处于可写状态的。我们可以通过SO_SNDLOWAT套接字选项(参见setsockopt函数)来设置这个低水位。此种情况下,我们设置该套接字为非阻塞,对该套接字进行写操作(如write,send等),将不阻塞,并返回一个正值(例如由传输层接受的字节数,即发送的数据大小)。
  2. 该连接的写半部关闭(主动发送FIN包的TCP连接)。对这样的套接字的写操作将会产生SIGPIPE信号。所以我们的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。
  3. 使用非阻塞的connect套接字已建立连接,或者connect已经以失败告终。即connect有结果了。
  4. 有一个错误的套接字待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
  • 后面有一个例子,是关于一个readfdsfd_set 是一个标准输入,一个 writefdsfd_set 是标准输出。使用 select 堵塞等待。
  • 当键盘有输入时 readfds 中是标准输出, select 会立马返回就绪
  • 当监听writefds时,因为里面放的是标准输入,因为标准输入在正常情况下就是可写的,所以 select 会立马返回并就绪,不会堵塞。

2. 数据类型 fd_set

这个网上资料很多,细节很多。我有空回来再补

操作 fd_set 的四个宏:

cpp 复制代码
#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
cpp 复制代码
#include <sys/time.h>

struct timeval{
        long tv_sec;   /*秒 */
        long tv_usec;  /*微秒 */   
    }

上面说,timeoutNULL 时,select 会堵塞。如果 timeout 的两个值都为 0,不会堵塞。select直接立即返回

4. select 返回值

这个看多了用多了代码就会了,大部分使用select 的代码都会出现判断它的所有返回值的:

cpp 复制代码
while(1)
{
    ...
    ret = select(nfds, readfds, ...);
    if (ret > 0) {}
    else if (ret == 0) {}
    else if (ret < 0) {}
    ...
}

返回值:

  • 大于 0:返回一个或多个已经就绪的文件描述符
  • 小于 0: 有错误发生:一般是 EBADFEINTR 。具体上网搜
  • 等于 0: 超时,每个文件描述符集合会清空。

5. example

"t_select.h" 在文末获取。

cpp 复制代码
#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);
}

文末

cpp 复制代码
/**************************************************************************/
#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 that
   command-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 controlling
   how the conversion is done and what diagnostic checks are performed on the
   numeric result; see get_num.h for details.

   'fname' is the name of our caller, and 'name' is the name associated with
   the command-line argument 'arg'. 'fname' and 'name' are used to print a
   diagnostic 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 the
   comments 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 the
   comments 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);
}
相关推荐
tokepson7 小时前
Mysql下载部署方法备份(Windows/Linux)
linux·服务器·windows·mysql
zz_nj9 小时前
工作的环境
linux·运维·服务器
极客先躯10 小时前
如何自动提取Git指定时间段的修改文件?Win/Linux双平台解决方案
linux·git·elasticsearch
suijishengchengde10 小时前
****LINUX时间同步配置*****
linux·运维
qiuqyue11 小时前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
linux·运维·网络
切糕师学AI11 小时前
Linux 操作系统简介
linux
南烟斋..11 小时前
GDB调试核心指南
linux·服务器
爱跑马的程序员12 小时前
Linux 如何查看文件夹的大小(du、df、ls、find)
linux·运维·ubuntu
oMcLin14 小时前
如何在 Ubuntu 22.04 LTS 上部署并优化 Magento 电商平台,提升高并发请求的响应速度与稳定性?
linux·运维·ubuntu
Qinti_mm14 小时前
Linux io_uring:高性能异步I/O革命
linux·i/o·io_uring