Linux C++下网络编程之I/O复用

1. 概述

I/O复用,这里有两个关键词

  • I/O:就是指网络I/O,即客户端与服务器之间的网络通信
  • 复用:就是指重复使用,那重复使用什么呢?重复使用一个进程或线程

一台服务器上的某个进程可以被多个客户端访问,而通信时服务器需要和每个客户端之间构建socket连接,每个socket连接就是一个网络I/O,客户端与服务器之间的所有数据通信都要经过这个socket,I/O复用就是用一个进程来处理这些socket连接上发生的事件,如数据的发送、接收。 I/O复用使得服务器上的某个程序能同时监听多个文件描述符

网络程序需要使用I/O复用的情况主要包括:

  • 客户端程序要同时处理多个socket;
  • 客户端程序要同时处理用户输入和网络连接;
  • TCP服务器要同时处理监听socket(listenfd)和连接socket(connfd);
  • 服务器要同时处理TCP请求和UDP请求;
  • 服务器要同时监听多个端口,或者处理多种服务。

Linux下实现I/O复用的系统调用主要有:selectpollepoll

2. select 系统调用

select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读可写异常等事件。

#include <sys/select.h>

/* Check the first NFDS descriptors each in READFDS (if not NULL) for read
   readiness, in WRITEFDS (if not NULL) for write readiness, and in EXCEPTFDS
   (if not NULL) for exceptional conditions.  If TIMEOUT is not NULL, time out
   after waiting the interval specified therein.  Returns the number of ready
   descriptors, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int select (int __nfds, fd_set *__restrict __readfds,
		   fd_set *__restrict __writefds,
		   fd_set *__restrict __exceptfds,
		   struct timeval *__restrict __timeout);

参数说明:

  • nfds:指定被监听的文件描述符的总数,它通常被设置为select监听的文件描述符中的最大值加1,因为文件描述符是从0开始计数的;
  • readfswritefdsexceptfds:分别指向可读、可写和异常等事件对应的文件描述符集合;
  • timeout:设置函数的超时时间。

readfswritefdsexceptfds 参数都是 fd_set 结构体指针,它的定义如下:

/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;

/* Some versions of <linux/posix_types.h> define this macros.  */
#undef	__NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT.  */
#define __NFDBITS	(8 * (int) sizeof (__fd_mask))
#define	__FD_ELT(d)	((d) / __NFDBITS)
#define	__FD_MASK(d)	((__fd_mask) (1UL << ((d) % __NFDBITS)))

/* fd_set for select and pselect.  */
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 结构体内部仅仅包含一个长整型数组fds_bits,数组的每一个元素的每一位(bit)标记一个文件描述符。 fd_set 能容纳的文件描述符数量有 __FD_SETSIZE 指定。

timeout 参数是一个 timeval 结构体指针,其定义如下:

/* A time value that is accurate to the nearest
   microsecond but also has a range of years.  */
struct timeval
{
  __time_t tv_sec;		/* Seconds.  */
  __suseconds_t tv_usec;	/* Microseconds.  */
};

timeval tv_sectv_usec 分别表示秒和微秒。

select 系统调用中,若给timeout参数的tv_sectv_usec 都设置为 0 ,则 select 将立即返回,如果给timeout参数传递 NULL ,则 select 将一直阻塞,直到某个文件描述符就绪;

返回值:
select 成功时返回就绪(可读、可写和异常)文件描述符的总数,失败时返回-1并设置errno。

3. poll 系统调用

poll系统调用和select系统调用类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。

#include <sys/poll.h>

/* Poll the file descriptors described by the NFDS structures starting at
   FDS.  If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
   an event to occur; if TIMEOUT is -1, block until an event occurs.
   Returns the number of file descriptors with events, zero if timed out,
   or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

参数说明:

  • fds: 被监听事件集合,指定所有感兴趣文件描述符上发生的可读、可写和异常等事件;
  • nfds:指定被监听事件集合fds的大小;
  • timeout:指定poll函数的超时时间,当timeout -1 时,poll调用将永远阻塞,直到某个事件发生,当timeout 0 时,poll调用将立即返回。

fds是一个 pollfd 结构类型的指针,它的定义如下:

/* Data structure describing a polling request.  */
struct pollfd
  {
    int fd;			/* File descriptor to poll.  */
    short int events;		/* Types of events poller cares about.  */
    short int revents;		/* Types of events that actually occurred.  */
  };

成员说明:

  • fd:指定文件描述符;
  • events:指明poll监听fd上的哪些事情,它是一系列事件的按位或,具体事件见头文件:<bits/poll.h>;
  • revents:由内核修改,以通知应用程序fd上实际发生了哪些事件。

返回值:
poll 成功时返回就绪(可读、可写和异常)文件描述符的总数,失败时返回-1并设置errno。

4. epoll 系列系统调用

4.1 内核事件表

epoll 是Linux特有的I/O复用函数。它在实现和使用上与selectpoll有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像 selectpoll 那样每次调用都要重复传入文件描述符集或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下epoll_create函数来创建:

#include <sys/epoll.h>

/* Creates an epoll instance.  Returns an fd for the new instance.
   The "size" parameter is a hint specifying the number of file
   descriptors to be associated with the new instance.  The fd
   returned by epoll_create() should be closed with close().  */
extern int epoll_create (int __size) __THROW;

参数说明:

  • size: 并不起作用,只是给内核一个提示,告诉它事件表需要多大,epoll_create1 函数已丢弃size参数。

epoll_create函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表

下面的函数用来操作epoll的内核事件表:

#include <sys/epoll.h>

/* Manipulate an epoll instance "epfd". Returns 0 in case of success,
   -1 in case of error ( the "errno" variable will contain the
   specific error code ) The "op" parameter is one of the EPOLL_CTL_*
   constants defined above. The "fd" parameter is the target of the
   operation. The "event" parameter describes which events the caller
   is interested in and any associated user data.  */
extern int epoll_ctl (int __epfd, int __op, int __fd,
		      struct epoll_event *__event) __THROW;

参数说明:

  • epfd:内核事件表文件描述符,epoll_create函数的返回值;
  • op:指定操作类型;
  • fd:指定要操作的文件描述符;
  • event: 指定事件。

参数op的可选操作类型有如下3种:

  • EPOLL_CTL_ADD:往事件表中注册fd上的事件;
  • EPOLL_CTL_MOD:修改fd上的注册事件;
  • EPOLL_CTL_DEL:删除fd上的注册事件。

参数event epoll_event 结构指针,其定义如下:

#include <sys/epoll.h>

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

成员说明:

  • events:描述事件类型,具体事件见头文件: <sys/epoll.h>里的 EPOLL_EVENTS 枚举类型;
  • data:存储用户数据。

成员data是个 epoll_data_t 结构体类型,其定义为:

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

epoll_data_t 是个联合体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可用来指定与fd相关的用户数据。但由于 epoll_data_t 是个联合体,不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_tfd成员,而在ptr指向的用户数据中包含fd

返回值:
epoll_ctl成功时返回0,失败则返回-1并设置errno。

4.2 epoll_wait 函数

epoll系列系统调用的主要接口是epoll_wait函数,它在一段时间内等待一组文件描述符上的事件。

#include <sys/epoll.h>

/* Wait for events on an epoll instance "epfd". Returns the number of
   triggered events returned in "events" buffer. Or -1 in case of
   error with the "errno" variable set to the specific error code. The
   "events" parameter is a buffer that will contain triggered
   events. The "maxevents" is the maximum number of events to be
   returned ( usually size of "events" ). The "timeout" parameter
   specifies the maximum wait time in milliseconds (-1 == infinite).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int epoll_wait (int __epfd, struct epoll_event *__events,
		       int __maxevents, int __timeout);

参数说明:

  • epfd:内核事件表文件描述符,epoll_create函数的返回值;
  • eventsepoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到events参数指向的数组中,这个数组只用于输出epoll_wait检测到的就绪事件;
  • maxevents:指定最多监听多少个事件;
  • timeout:指定epoll_wait函数的超时时间,当timeout -1 时,epoll_wait调用将永远阻塞,直到某个事件发生,当timeout 0 时,epoll_wait调用将立即返回。

返回值:
epoll_wait成功时返回就绪的文件描述符的个数,失败则返回-1并设置errno。

5. 三组I/O复用函数的比较

select、poll和epoll的区别
系统调用selectpollepoll
事件集合用户通过3个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件
应用程序索引就绪文件描述符的时间复杂度O(n)O(n)O(1)
最大支持文件描述符数一般有最大限制6553565535
工作模式LTLT支持ET高效模式
内核实现和工作效率采用轮询方式来检测就绪事件,算法时间复杂度为O(n)采用轮询方式来检测就绪事件,算法时间复杂度为O(n)采用回调方式来检测就绪事件,算法时间复杂度为O(1)

6. 参考书籍

Linux高性能服务器编程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MinBadGuy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值