LinuxI高性能服务器之I/O复用(13)

前言

客户端程序要同时处理多个socket。比如本章将要讨论的非阻塞connect技术
客户端程序要同时处理用户输入和网络连接。比如本章将要讨论的聊天室程序。

服务器要同时监听多个端口,或者处理多种服务。比如本章将要讨论的xinetd服务器。
服务器要同时处理TCP请求和UDP请求。比如本章将要讨论的回射服务器。
服务器要同时监听多个端口,或者处理多种服务。比如本章将要讨论的xinetd服务器。
需要指出的是,I/O复用虽然能同时监听多个文件描述符但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

select

select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。先介绍select系统调用的API,然后讨论select判断文件描述符就绪的条件,最后给出它在处理带外数据中的实际应用。

select API

#include<sys/select.h>
int select(int nfds,fd_set* readfs,fd_set* writefds,fd_set* exceptfds,
struct timeval timeout);

typedef struct
{
/*XPG4.2requiresthismembername.Otherwiseavoidthename
fromtheglobalnamespace.*/
#ifdef__USE_XOPEN
__fd_maskfds_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_ZERO(&set); /*将set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)*/

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

1 ) nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
2). readfds、writefds 和 exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。sclect调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
3.由以上定义可见,select给我们提供了一个微秒级的定时方式。如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。sclect成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-1并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR

文件描述就绪条件

socket 可读
1 .socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
2.socket通信的对方关闭连接。此时对该socket的读操作将返回0。
3.监听socket 上有新的连接请求。
4.socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误

socket可写:
1.socket内核发送缓存区中的可用字节数大于或等于其低水位SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
2.socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
3.socket使用非阻塞connect连接成功或者失败(超时)之后。
4.socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

socket异常
sock接收到异常数据

实例跳转

poll

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

#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);//ms
struct pollfd
{

int fd;
short events;//注册事件
short revents;//内核填充
}

1.poll支持事件

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLINRDNORM普通数据可读s
POLLRDBAND优先级带数据可读(Linux不支持
POLLPRI高优先级数据可读,比如TCP带外数据
POLLOUT数据(包括普通数据和优先数据)写
POLLWRNORM普通数据可读
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭或者对方关闭写操作,它有GNU引入
POLLERR错误
POLLHUP挂起 比如管道写端关闭,读端描述符将受到
POLLNAVL文件描述符没有打开

通常,应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。不过,自Linux内核2.6.17开始,GNU为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发。这为我们区分上述两种情况提供了一种更简单的方式。但使用POLLRDHUP事件时,我们需要在代码最开始处定义_GNU_SOURCE。

2.nfd参数指定被监听事件集合fds的大小

 typedef unsigned long int nfds_t

epoll

内核事件表

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

#include<sys/epoll.h>
int epoll_create(int size)
int epoll_ctl(int epfd,int op,struct epoll_event* event);

struct epoll_event
{
_uint32_t events;//事件
epoll_data_t data;//用户数据
}

size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

op参数指定操作

选项作用
EPOLL_CTLADD往事件表中注册fd上的事件
EPOLL_CTL_MOD修改fd上的注册事件
EPOLL_CTL_DEL删除fd上的注册事件

其中events成员描述事件类型。epoll支持的事件类型和poll 基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E",比如 epoll 的数据可读事件是EPOLLIN。但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。它们对于epoll 的高效运作非常关键

data成员用于存储用户数据,其类型epoll data t的定义如下:

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

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

epoll_wait函数

#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event* events,int maxevevnts,int timeout);

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events 指向的数组中。**这个数组只用于输出 epoll_wait检测到的就绪事件,**而不像select和poll 的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。

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

前面我们讨论了select、poll和 epoll三组IO复用系统调用,这3组系统调用都能同时监听多个文件描述符。它们将等待由timeout参数指定的超时时间,直到一个或者多个文件描述符上有事件发生时返回,返回值是就绪的文件描述符的数量。返回0表示没有事件发生。

现在我们从事件集、最大支持文件描述符数、工作模式和具体实现等四个方面进一步比较它们的异同,以明确在实际应用中应该选择使用哪个(或哪些)?

这3组函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。
select 的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此select需要提供3个这种类型的参数来分别传入和输出可读、可写及异常等事件。这一方面使得select不能处理更多类型的事件另一方面由于内核对fd_set集合的在线修改,应用程序下次调用select前不得不重置这3个fd_set集合

poll的参数类型pollfd则多少“聪明”一些。它把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁得多。并且内核每次修改的是pollfd结构体的revents成员,而events成员保持不变,因此下次调用poll时应用程序无须重置pollfd类型的事件集参数。由于每次select和 poll调用都返回整个用户注册的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为О (n)

epoll则采用与select和poll完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次 epoll_wait调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些事件。epoll_wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到O( 1)。

poll和 epoll_wait分别用nfds和 maxevents参数指定最多监听多少个文件描述符和事件。这两个数值都能达到系统允许打开的最大文件描述符数目,即65 535 (cat/proc/sys/fs/file-max)。而select允许监听的最大文件描述符数量通常有限制。虽然用户可以修改这个限制,但这可能导致不可预期的后果。

select和 poll都只能工作在相对低效的LT模式,而epoll则可以工作在ET高效模式。并且epoll 还支持EPOLLONESHOT事件。该事件能进一步减少可读、可写和异常等事件被触发的次数。

从实现原理上来说,select和 poll采用的都是轮询的方式,即每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此它们检测就绪事件的算法的时间复杂度是O(n)
epoll_wait则不同,它采用的是回调的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插人内核就绪事件队列。内核最后在适当的时机将该就绪事件队列中的内容拷贝到用户空间。因此 epoll_wait无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度是О (1)。但是,当活动连接比较多的时候,epoll_wait 的效率未必比 select和 poll 高,因为此时回调函数被触发得过于频繁。所以epoll_wait适用于连接数量多,但活动连接较少的情况

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

超级服务xinetd

Linux因特网服务inetd是超级服务。它同时管理着多个子服务,即监听多个端口。现在Linux系统上使用的inetd服务程序通常是其升级版本xinetd。xinetd程序的原理与 inetd相同,但增加了一些控制选项,并提高了安全性。下面我们从配置文件和工作流程两个方面对xinetd进行介绍。

xinetd配置文件

xinetd 采用/etc/xinetd.conf主配置文件和/etc/xinetd.d目录下的子配置文件来管理所有服务。主配置文件包含的是通用选项,这些选项将被所有子配置文件继承。不过子配置文件可以覆盖这些选项。每一个子配置文件用于设置一个子服务的参数。比如,telnet子服务的配置文件/etc/xinetd.d/telnet的典型内容如下:

service telnet
{
 flags           =REUSE
 socket_type     =stream
 wait            =no
 user            =root
 server          =/usr/sbin/in.telnetd
 log_on_failure  +=USERID
 disable          =no
}
项目含义
service服务名
flags设置连接标志 REUSE表示复用telnet连接的socket 该标志已经过时,每一个连接都已经过 时 每一个连接都默认启用REUSE标志
socket_type服务类型
wait**服务采用单线程方式( wait=yes)还是多线程方式( wait-no)。**单线程方式表示 xinetd只accept第一次连接,此后将由子服务进程来accept新连接。多线程方式表示xinetd一直负责accept连接,而子服务进程仅处理连接socket 上的数据读写
user子服务进程将以user指定的用户身份来运行
server子服务程序的完整路径
log_no_failure定义当启动服务启动时输出日志的参数
disable是否启动子服务

xinetd工作流程

cat /var/run/xinetd.pid
9543

telnet 192.168.1.109
ps -eo pid,ppid,pgid,sid,comm|grep 19858
 19858   2402  19858  19858 xinetd
 32244  19858  32244  19858 in.telnetd
 32245  19858  32244  19858 in.telnet


在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值