Linux 高性能服务器网络编程(三 IO复用)

IO复用使得程序能够同时监听多个文件描述符,对提高服务器性能十分重要

IO复用

select函数

在指定时间内,监听用户感兴趣的文件描述符上的可读,可写和异常事件

select(I/O多工机制)

表头文件
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
定义函数
int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);

函数说明
select()用来等待文件描述词状态的改变。

参数n代表最大的文件描述词加1,
参数readfds、writefds 和exceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况。

底下的宏提供了处理这三种描述词组的方式:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set); 用来清除描述词组set的全部位

参数

timeout为结构timeval,用来设置select()的等待时间,其结构定义如下
struct timeval
{
time_t tv_sec;
time_t tv_usec;
};

返回值

如果参数timeout设为NULL则表示select()没有timeout。

错误代码

执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

在这里插入图片描述

poll

与select类似,在指定时间内轮询一定数量的文件描述符,以测试其是否有就绪者

函数原型如下:

#include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

1)fds 是指定所有感兴趣的文件描述符上发生的可读,可写和异常的事件

struct pollfd
{	
	int fd; // 文件描述符
	short events; //注册时间
	short revents; //实际发生的事件,由内核填充
};
事件描述是否作为输出是否作为输出
POLLIN有数据可读
POLLRDNORM有普通数据可读
POLLRDBAND有优先数据可读
POLLPRI有紧迫数据可读(linux不支持)
POLLOUT写数据不会导致阻塞
POLLWRNORM写普通数据不会导致阻塞
POLLWRBAND写优先数据不会导致阻塞
POLLMSGSIGPOLL消息可用
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作
POLLERR错误
POLLHUP挂起
POLLNVAL文件描述符没有打开

2)nfds参数指定被监听事件集合fds的大小。

typedef	unsigned long int nfds_t

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

epoll系列系统调用

Linux EPOLL https://blog.csdn.net/eyucham/article/details/86502117

内核事件表

epoll 是Linux特有的IO复用函数,它是使用一组函数来完成任务,其次epoll将用户关心的文件描述符上的事件放在内核一个事件表中。epoll函数是多路复用IO接口select和poll函数的增强版本。显著减少程序在大量并发连接中只有少量活跃的情况下CPU利用率,他不会复用文件描述符集合来传递结果,而迫使开发者每次等待事件之前都必须重新设置要等待的文件描述符集合,另外就是获取事件时无需遍历整个文件描述符集合,只需要遍历被内核异步唤醒加入ready队列的描述符集合就行了 。

  • epoll需要使用一个额外的文件描述符,来标示内核中的这个事件表,这个文件描述符有epoll_wait创建
#include <sys/epoll.h>

int epoll_create( int size)

生成一个epoll函数专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件。 size 就是你在这个 Epoll fd 上能关注的最大 socket fd 数,大小自定,只要内存足够。

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参数指定事件

struct epoll_event
{
	__uint32_t events; //epoll事件
	epoll_data_t data;//用户数据
}
  • events 参数与poll事件类型基本相同,不过对应的宏加‘E’。EPOLL有额外的事件类型:EPOLLET和EPOLLONESHOT
  • epoll_data_t 定义
  • epoll_ctl成功返回0,失败返回-1并且设置errno
typedef union epoll_data
{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;

注:使用最多的是fd:指定事件从属的目标文件描述符

#include<sys/epoll.h>
int epoll_wait( int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 成功返回就绪文件描述符的个数,失败时返回-1并且设置errno
    *timeout : 与poll接口timeout参数一样
    *maxevents : 指定最多监听多少各事件,必须大于0
    *events : 如果检测到事件,就将所有就绪的事件从内核事件表(epfd指定)中复制到events指向的数组中。

EPLOLL的LT(水平触发)与ET(边缘触发)

都是在非阻塞的情况下,不然读写事件会因为没有后续事件而一直处于阻塞状态

LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

EPOLLONESHOT事件

一个socket连接在任意时段都只能被一个线程处理,保证连接完整性避免了很多竞态

select/poll/epoll的区别

epoll适用于连接数量多但是活动数量比较少,select/poll适合活动连接比较多

在这里插入图片描述

应用:非阻塞connect

connect出错时出现的一种errno值:EINPROGRESS,发生在对非阻塞socket调用connect,而连接没有建立的时候。这种情况下我们可以使用select/poll/epoll来监听这个连接失败的socket上的可写事件。当select等函数返回后在利用getsockopt读取错误码并清除错误码。如果错误码为0则连接建立,否则连接失败

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 1023

int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

int unblock_connect( const char* ip, int port, int time )
{
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
    int fdopt = setnonblocking( sockfd );
    ret = connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) );
    if ( ret == 0 )
    {
        printf( "connect with server immediately\n" );
        fcntl( sockfd, F_SETFL, fdopt );
        return sockfd;
    }
    else if ( errno != EINPROGRESS )
    {
        printf( "unblock connect not support\n" );
        return -1;
    }

    fd_set readfds;
    fd_set writefds;
    struct timeval timeout;

    FD_ZERO( &readfds );
    FD_SET( sockfd, &writefds );

    timeout.tv_sec = time;
    timeout.tv_usec = 0;

    ret = select( sockfd + 1, NULL, &writefds, NULL, &timeout );
    if ( ret <= 0 )
    {
        printf( "connection time out\n" );
        close( sockfd );
        return -1;
    }

    if ( ! FD_ISSET( sockfd, &writefds  ) )
    {
        printf( "no events on sockfd found\n" );
        close( sockfd );
        return -1;
    }

    int error = 0;
    socklen_t length = sizeof( error );
    if( getsockopt( sockfd, SOL_SOCKET, SO_ERROR, &error, &length ) < 0 )
    {
        printf( "get socket option failed\n" );
        close( sockfd );
        return -1;
    }

    if( error != 0 )
    {
        printf( "connection failed after select with the error: %d \n", error );
        close( sockfd );
        return -1;
    }
    
    printf( "connection ready after select with the socket: %d \n", sockfd );
    fcntl( sockfd, F_SETFL, fdopt );
    return sockfd;
}

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int sockfd = unblock_connect( ip, port, 10 );
    if ( sockfd < 0 )
    {
        return 1;
    }
    shutdown( sockfd, SHUT_WR );
    sleep( 200 );
    printf( "send data out\n" );
    send( sockfd, "abc", 3, 0 );
    //sleep( 600 );
    return 0;
}

信号

信号是由用户,系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或者系统异常

发送信号

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

pid : 发送的目标进程
在这里插入图片描述

在这里插入图片描述

Linux信号

定义在bits/signum.h,其中包括标准信号和POSIX实时信号,此处指讨论标准信号

在这里插入图片描述
在这里插入图片描述

网络编程常用信号:SIGHUP,SIGPIPE,SIGURG,SIGALRM,SIGCHLD

信号处理函数

signal

#include <signal.h>
//sig 信号类型
//handler : 函数指针
//return: 成功 前一次调用signal传入的函数指针 失败 : SIG_ERR,设置errno
typedef void (*__sighandler_t)(int);
_sighandler_t signal( int sig, _sighandler_t _handler);

sigaction

#include <signal.h>
/*
*act : 指定新的信号处理方式,oact输出先前的处理方式
*/
int sigaction( int sig, const struct sigaction *act, struct sigaction *oact)

struct sigaction
{
_sighandler_t sa_handler;
_sigset_t sa_mask;
int sa_flag;
void (*sa_restorer)(void);
}

统一事件源

IO复用系统调用来监听管道的读端的文件描述符上的可读事件,如此一来信号事件就能和其他IO事件一样的被处理,即统一事件源

SIGHUP/SIGPIPE/SIGURG

当挂起的进程控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序来说它们通常利用SIGHUP信号来强制服务器重读配置文件。

SIGPIPE:默认情况下往读端关闭管道或者socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并且处理该信号,因为程序收到的SIGPIPE信号的默认行为是结束进程,而我们不希望因为错误的写操作而导致程序推出。引起SIGPIPE信号的写操作将设置errno为EPIPE

SIGURG:内核通知应用程序带外数据到达的一种方式,另外一种方式是IO复用接收带外数据是返回,向应用程序报告socket上的异常事件

定时器

一段时间之后触发某段代码的机制,Linux提供了三种定时的方法,分别是
(1)socket 选项SO_RECVTIMEO 和 SO_SNDTIMEO
(2)SIGALRM信号
(3)IO复用系统调用的超时参数

socket 选项SO_RECVTIMEO 和 SO_SNDTIMEO

用来设置socket接收数据超时和发送数据超时时间

选项对系统调用的影响如下
在这里插入图片描述

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int timeout_connect( const char* ip, int port, int time )
{
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sockfd >= 0 );

    struct timeval timeout;
    timeout.tv_sec = time;
    timeout.tv_usec = 0;
    socklen_t len = sizeof( timeout );
    ret = setsockopt( sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len );
    assert( ret != -1 );

    ret = connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) );
    if ( ret == -1 )
    {
        if( errno == EINPROGRESS )
        {
            printf( "connecting timeout\n" );
            return -1;
        }
        printf( "error occur when connecting to server\n" );
        return -1;
    }

    return sockfd;
}

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int sockfd = timeout_connect( ip, port, 10 );
    if ( sockfd < 0 )
    {
        return 1;
    }
    return 0;
}

SIGALRM信号

由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号,因此我们可以利用该信号处理函数来处理定时任务

基于升序链表的定时器

定时器通常至少包括一个超时时间和一个任务回调函数

处理非活动连接
:服务器程序要定时处理非活动连接:类似与keeplive机制。在应用层实现类似功能,可使用alarm函数周期性的触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表的定时任务------关闭非活动连接

IO复用系统调用的超时参数

Linux 3组系统调用都带有超时参数,因为它们不仅仅能统一处理信号和IO时间,也能处理定时事件。但是由于IO复用系统调用可能在超时时间到期之前就返回,所以要利用其定时需要不断的更新定时参数以反应剩余时间

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值