Linux高性能服务器编程(9)

Linux高性能服务器编程(9)

网络程序如下情况使用I/O复用技术:

①客户端程序要同时处理多个socket
②客户端程序要同时处理用户输入和网络连接
③TCP服务器要同时处理监听socket和连接socket(这是IO复用使用最多的场合)
④服务器要同时处理TCP请求和UDP请求。(比如回射服务器)
⑤服务器要同时监听多个端口,或者处理多种服务。

select系统调用

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

select系统调用的原型如下:

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

1)nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
2)readfds、writefds和exceptfds参数分别指向可读、可写和异常事件对应的文件描述符集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这三个参数是fd_set结构指针类型。fd_set结构体的定义如下:

#include<typesizes.h>
#define __FD_SETSIZE 1024

#inlucde<sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8 * (int) sizeof(__fd_mask))
typedef struct
{
	#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结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作过于繁琐,我们应该使用下面的一系列宏来访问fd_set结构体中的位:

#include<sys/select>
FD_ZERO(fd_set *fdset);					/* 清除fdset的所有位 */
FD_SET(int fd,fd_set *fdset);			/* 设置fdset的位fd */
FD_CLR(int fd,fd_set *fdset);			/* 清除fdset的位fd */
int FD_ISSET(int fd,fd_set *fdset);		/* 测试fdset的位fd是否被设置 */

3)timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。timeval结构体定义如下:

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

select提供一个微秒级的定时方式。如果给timeout变量的tv_sec和tv_usec成员都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-1并设置errno。如果在select等待期间,程序收到信号,则select立即返回-1,并设置errno为EINTR。

文件描述符就绪条件
在网络编程中,下列情况下socket可读:
①socket内核接收缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回地字节数大于0.
②socket通信的对方关闭连接。此时对该socket的读操作将返回0
③监听socket上有新的连接请求
④socket上有未处理的错误。此时可以使用getsockopt来读取和清除该错误。

下列情况socket可写:
①socket内核发送缓冲区的可用字节数大于或等于低水位标记SO_SNDLOWAT。此时可以无阻塞地写该socket,并写操作返回的字节数大于0
②socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
③socket使用非阻塞connect连接成功或者失败(超时)之后。
④socket上有未处理的错误。此时可以使用getsockopt来读取和清除该错误。

网络程序中,select能处理的异常情况只能有一种:socket上接收到带外数据。

poll系统调用

poll原型:

#include<poll.h>
{
	int fd;			/* 文件描述符 */
	short events;	/* 注册的事件 */
	short revents;	/* 实际发生的事件,由内核填充 */
};

1)fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生哪些事件。poll支持的事件类型如表。

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

表中POLLRDNORM,POLLRDBAND,POLLWRNORM,POLLWRBAND由XOPEN规范定义。它们实际上是将POLLIN事件和POLLOUT事件分的更细致,以区别对待普通数据和优先数据。但Linux完全不支持
2)nfds参数指定被监听事件集合fds的大小。其类型nfds_t的定义如下:

typedef unsigned long int nfds_t;

3)timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。

epoll系列系统调用

内核事件表
epoll是Linux特有的I/O复用函数。epoll是用一组函数来完成任务。epoll需要一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下epoll_create函数来创建:

#include<sys/epoll.h>
int epoll_create(int size);

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

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

fd参数是要操作的文件描述符,op参数指定操作类型。操作类型有如下3种:
EPOLL_CTL_ADD,往事件表中注册fd上的事件
EPOLL_CTL_MOD,修改fd上的注册事件
EPOLL_CTL_DEL,删除fd上的注册事件
event参数指定事件,它是epoll_event结构指针类型。epoll_event的定义如下:

struct epoll_event
{
	__uint32_t events;		/* epoll事件 */
	epoll_data_t data;		/* 用户数据 */
};

其中event成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前面加上“E”,比如epoll的数据可读事件是EPOLLIN。但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。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_t的fd成员,而在ptr指向的用户数据中包含fd。
epoll_ctl成功时返回0,失败则返回-1并设置errno

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

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

该函数成功时返回就绪文件描述符个数,失败时返回-1并设置errno。
maxevents参数指定最多监听多少个事件,必须大于0.
epoll_wait函数如果检测到事件,就将所有的就绪的事件从内核事件表(由epfd参数指定)中负责到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大提高了应用程序索引就绪文件描述符的效率。

LT和ET模式
LT(电平触发)模式和ET(边沿触发)模式。LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
LT模式下,当epoll_wait检测到其上有事件发生并将此事件通知该应用程序后,应用程序可以不立即处理该事件。当下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件直到该事件被处理。ET模式时,epoll_wait检测到其上有事件发生并将此事通知应用程序后,应用程序必须立即处理该事件,因为后续epoll_wait调用将不再向应用程序通知这一事件。可见ET模式降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

EPOLLONESHOT事件
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。反向思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值