114-select(基础)

1. 问题提出

接下来再回到IO 多路复用文中提出的问题(请允许我稍作修改):

// fd0 表示标准输入,即 0 号描述符, fd1, fd2 分别是以只读的方式打开的两个不同有名管道的描述符(a.fifo, b.fifo)

int n;
char buf[64];
int fd0 = STDIN_FILENO;
int fd1 = open("a.fifo", O_RDONLY);
int fd2 = open("b.fifo", O_RDONLY);

while(1) {
  n = read(fd0, buf, 64);
  write(STDOUT_FILENO, buf, n);

  n = read(fd1, buf, 64);
  write(STDOUT_FILENO, buf, n);

  n = read(fd2, buf, 64);
  write(STDOUT_FILENO, buf, n);
}

这段程序存在的问题之前我也讨论过了,只要有一个 read 发生阻塞,即使其它描述符上有数据可读,也没办法执行。

而我们之前的解决方案就是通过 I/O Multiplexing 技术,我们设想了一个 select 函数,来帮我们管理这 3 个描述符。

2. select 函数

实际上,select 函数是真实存在的,它的原型比我们之前提到的稍稍复杂:

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

我知道,就算你看到这个函数你也不会觉得它有多么复杂,这多亏了在上一文中我们非常细致的描述了 fd_set 是什么玩意儿。

2.1 参数

这里的 select 函数提供的参数比较多,其中有三个参数都是 fd_set 类型,先讲这个:

  • readfds,你想监听这个集合中的描述符中是否有数据可读。
  • writefds,你想监听这个集合中的描述符是否可写,写 IO 当然也会发生阻塞,比如缓冲区满了。
  • exceptfds,你想监听这个集合中的描述符是否发生异常,有两种情况,一种常见的是在网络编程中出现,另一种不太常见的是在伪终端中出现,这两种我们都没学过,所以暂时不去管它。

对于我们第 1 节中的问题来说,我们只关心三个描述符是否有数据可读,因此对于 writefds 和 exceptfds 都可以设置成空。

参数 nfds 表示,在传入参数的那三个集合中,最大的描述符的值 + 1. 比如 readfds 中保存了 1、3、5 号描述符,writefds 中保存了 2、3、6,exceptfds 为空,那么 nfds 就应该设置成最大的描述符的值(6) + 1,也就是 nfds = 7.

最后一个参数是等待时间,如果传空,表示永远等待,直到指定的三个集合中的描述符有事件(有数据可读、有数据可写、有异常事件发生)到来。

如果不传空值,则表示最长愿意等待多久。

它的结构如下:

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

如果给三个集合都设置成空,nfds = 0,只给时间参数传值,那么 select 函数就相当于一个加强版本的 sleep 函数,因为它提供的时间精度是微秒。

2.2 返回值

为什么返回值要作当独一节?主要还是有点多……

select 函数的返回值体现在两方面:

  • 函数返回值
  • 修改三个集合参数

2.2.1 函数返回值

对于函数返回值来说,主要有 3 种情况:

(1) 返回值 < 0,这种一般表示函数执行出错,比如使用了不可用的描述符。有时候,select 会被信号打断,也会返回 < 0 的值。因为 select 函数是不支持自动重启动的,所以被信号打断会立即返回,然后把 errno 的值设置成 EINTR.

(2) 返回值 = 0,这种只对设置了超时时间的方式才会出现,如果返回 0,表示超时时间到了,还没有事件发生。

(3) 返回值 > 0,表示监听的描述符中,有几个事件发生。也就是读集合中的事件数 + 写集合中的事件数 + 异常集合中的事件数。就算不同集合间有重复的描述符,也会累计。

2.2.2 修改参数

对于 select 函数,如何才能知道哪些描述符上发生了事件,其实在IO 多路复用一文中已经简单提到过,它主要修改三个传入的描述符集合。

如果某个集合中的描述符上有事件到来,在 select 返回时,会保留该描述符,所有其它未发生事件的描述符全部清除。

这是一种很重要的方式,因为我们必须要知道哪些描述符发生了事件,比如对于 readfds 中的描述符,我们只能去 read 发生了事件的描述符,如果不这样,就会导致阻塞。

对于超时参数来说,如果在超时时间到达前发生异常或有事件到来,则该参数会被被更新为剩余时间。

3. 实验

程序 select.c 从标准输入、a.fifo 和 b.fifo 中读数据并打印到屏幕。

程序 writepipe.c 主要向管道文件写数据。

3.1 代码

// select.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

int process(char* prompt, int fd) {
  int n;
  char buf[64];
  char line[64];
  n = read(fd, buf, 64);
  if (n < 0) {
    // read 执行出错
    PERR("read");
  }
  else if (n == 0) {
    // 如果对端关闭,read 返回 0
    sprintf(line, "%s closed\n", prompt);
    puts(line);
    return 0;
  }
  else if (n > 0) {
    buf[n] = 0;
    sprintf(line, "%s say: %s", prompt, buf);
    puts(line);
  }
  return n;
}

int main () {
  int n, res;
  char buf[64];

  fd_set st; 
  FD_ZERO(&st);

  int fd0 = STDIN_FILENO;
  int fd1 = open("a.fifo", O_RDONLY);
  printf("open pipe: fd = %d\n", fd1);
  int fd2 = open("b.fifo", O_RDONLY);
  printf("open pipe: fd = %d\n", fd2);

  FD_SET(fd0, &st);
  FD_SET(fd1, &st);
  FD_SET(fd2, &st);

  // 最后一个 open 的描述符值是最大的
  int maxfd = fd2 + 1;

  while(1) {
    // 因为 tmpset 参数会被 select 修改,所以要重新赋值。
    fd_set tmpset = st;
    res = select(maxfd, &tmpset, NULL, NULL, NULL);

    if (res < 0) {
      // select 执行出错,对于被信号中断的,需要单独处理,这里暂时不考虑,后面的文章会讲
      PERR("select");
    }
    else if (res == 0) {
      // 超时,先不用管
      continue;
    }

    // 判断返回的集合中是否包含对应的描述符,如果包含,说明的事件(可读)到来。
    if (FD_ISSET(fd0, &tmpset)) {
      n = process("fd0", fd0);
      // 如果返回值为 0,表示对端关闭,后面的也一样。
      if (n == 0) FD_CLR(fd0, &st);
    }
    if (FD_ISSET(fd1, &tmpset)) {
      n = process("fd1", fd1);
      if (n == 0) FD_CLR(fd1, &st);
    }
    if (FD_ISSET(fd2, &tmpset)) {
      n = process("fd2", fd2);
      if (n == 0) FD_CLR(fd2, &st);
    }
  }
}

3.2 writepipe 程序

writepipe 程序主要向管道文件写数据。它从命令行接收管道文件的名字。

// writepipe.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
  if (argc < 2) {
    printf("Usage: %s <fifoname>\n", argv[0]);
    return 1;
  }
  char buf[64];
  int n;
  int fd = open(argv[1], O_WRONLY);
  if (fd < 0) {
    perror("open pipe");
    return 1;
  }
  while(1) {
    n = read(STDIN_FILENO, buf, 64);
    write(fd, buf, n); 
  }
  return 0;
}

3.3 编译和运行

  • 编译
$ gcc select.c -o select
$ gcc writepipe.c -o writepipe
  • 运行

先创建两个管道文件:

$ mkfifo a.fifo
$ mkfifo b.fifo

打开三个终端,分别运行:

$ ./select

$ ./writepipe a.fifo

$ ./writepipe b.fifo


这里写图片描述
图1 运行结果

4. 总结

  • 掌握 select 的用法

实际上,在网络编程中,select 用来同时处理多个连接也是一种常用的手段。

练习:完成本文中的实验。

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值