多路I/O复用:epoll整理

epoll相关接口整理

  • 1. epoll的优势:
  • 2. epoll 的三个系统调用接口
    • 2.1 int epoll_create (int size)
    • 2.2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    • 2.3 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 3. epoll两种工作模式 LT 和 ET
  • 4. 实例
  • 5. 参考

1. epoll的优势:

(1) epoll模型是基于事件的通知方式,事先为每个建立连接的文件描述符注册事件,一旦该描述符就绪,内核会采用类似callback的回调机制,将文件描述符加入epoll指定的文件描述符集中。虽然epoll机制中返回的同样是就绪文件描述符的数量,但epoll中的文件描述符集只存储了就绪的文件描述符,服务器进程无需再扫描所有连接的文件描述符。
(2) epoll 没有对fd描述符有限制,理论上取决于系统内存大小,可以通过cat/proc/sys/fs/file-max查看,大概1G内存创建10w个连接
在这里插入图片描述
(3) epoll的具体实现使用mmap(内存映射机制)加速内核与用户空间消息传递,不必再将内核中的文件描述符复制到内存空间。

2. epoll 的三个系统调用接口

2.1 int epoll_create (int size)

SYNOPSIS:  #include 
FUNCTION:  epoll_create
DESCRIPTION:	
epoll_create() returns a file descriptor referring to the new epoll instance
(创建一个epoll实例与文件描述符)。
PARAMETER: 参数size 为该epoll中可监听的文件描述符的最大个数。
RETURN VALUE:
函数调用成功,返回一个非负的文件描述符;否则失败,返回-1并设置错误
On success, these system calls return a nonnegative file descriptor. On error, -1 is returned, and errno is set to indicate the error.
ERRORS:EINVAL size is not positive(失败并报参数size 不是正数的错误)

2.2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

SYNOPSIS:  #include 
FUNCTION:  epoll_ctl
DESCRIPTION:用于添加,修改,删除要监听的event事件。
	Valid values for the op argument are:
    EPOLL_CTL_ADD:将新的fd注册到epfd(epoll实例对应得文件符)中
    EPOLL_CTL_MOD:修改已注册的fd
    EPOLL_CTL_DEL:删除epfd中的fd
PARAMETER: 
(1)Op参数:
    EPOLL_CTL_ADD:将新的fd注册到epfd(epoll实例对应得文件符)中
    EPOLL_CTL_MOD:修改已注册的fd
    EPOLL_CTL_DEL:删除epfd中的fd
(2)参数event: 用于设置要监听的事件,event是一个epoll_event类型的指针,其结构体定义如下:
     struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
};
struct epoll_event中成员events表示要监控的事件,该成员由一些单一事件组成的位集,宏定义如下:
事件宏
•	EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
•	EPOLLOUT: 表示对应的文件描述符可以写;
•	EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
•	EPOLLERR: 表示对应的文件描述符发生错误;
•	EPOLLHUP: 表示对应的文件描述符被挂断;
•	EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
•	EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
struct epoll_event中成员data的数据类型是共用体epoll_data_t,其定义如下
 	typedef union epoll_data {
           void        *ptr;
           int          fd;
           uint32_t     u32;
           uint64_t     u64;
  	 } epoll_data_t;

RETURN VALUE:函数调用成功,返回0;否则失败,返回-1并设置错误

ERRORS:
 (1)EBADF:  epfd or fd is not a valid file descriptor.
EEXIST: op was EPOLL_CTL_ADD, and the supplied file descriptor fd is already registered with this epoll instance.

 (2)EINVAL: epfd is not an epoll file descriptor, or fd is the same as epfd, or the requested operation op is not supported by this interface.

 (3)ENOENT:op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance.

 (4)ENOMEM:There was insufficient memory to handle the requested op control operation.

 (5)ENOSPC:The limit imposed by /proc/sys/fs/epoll/max_user_watches was encountered while trying to register (EPOLL_CTL_ADD) a new file descriptor on an epoll instance.  See epoll for further details.

 (6) EPERM:The target file fd does not support epoll.  This error can occur if fd refers to, for example, a regular file or a directory.

2.3 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

SYNOPSIS:  #include 
FUNCTION:  epoll_wait
DESCRIPTION:	
	用于等待epoll句柄中所监控的事件的发生,当有一个或者多个事件发生或等待超时后,epoll_wait返回。
PARAMETER: The memory
(1)	events: 指向发生epoll_create()调用时系统事先预备的空间,当有监听事件发生时,内核将该事件复制到此空间中
(2)	events:表示events事件的大小,不能超过epoll_create()调用时所传入参数的大小
(3)	timeout: 超时时间,用于设置epoll_wait的工作方式。若设置为0,则立刻返回;若设置为-1,则无限等待;否则表示等待一定时长。

RETURN VALUE:
	函数调用成功,返回就绪文件描述符的数量;若等待超时后并无就绪文件描述符则返回0;若调用失败则返回-1并设置错误。
ERRORS:
(1)	EBADF:  epfd is not a valid file descriptor.
(2)	EFAULT: The memory area pointed to by events is not accessible with write permissions.
(3)	EINTR: The call was interrupted by a signal handler before either  any of the requested events occurred or the timeout expired; see signal
(4)	EINVAL:epfd is not an epoll file descriptor, or maxevents is less than or equal to zero.

3. epoll两种工作模式 LT 和 ET

二者的差异在于 level-trigger (LT) 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger (ET) 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。

从socket读数据:
多路I/O复用:epoll整理_第1张图片
从socket写数据:
多路I/O复用:epoll整理_第2张图片
所以, 在epoll的ET模式下, 正确的读写方式为:
读: 只要可读, 就一直读, 直到返回0, 或者 errno = EAGAIN
写: 只要可写, 就一直写, 直到数据发送完, 或者 errno = EAGAIN

Epoll的工作模式在调用注册函数epoll_ctl()时确定,
由该函数的参数event的成员events指定。默认情况下epoll的工作模式为水平触发,若要将其设置为边缘触发模式,需使用宏EPOLLET对event进行设置,具体示例如下:
event.events=EPOLLIN|EPOLLET
之后需要在循环中不断调用,保证将文件描述符中的数据全部读出。
下面给出具体案例,来展示epoll在边缘触发模式下如何实现双端通讯。ET模式只能工作在非阻塞模式下,否则单纯使用epoll(单进程)无法同时出来多个文件描述符。在实现案例之前先介绍一下设置文件描述符状态的方法,linux系统中可使用fcntl()函数来设置文件描述符的属性。

	int fcntl(int fd,int cmd,…/*arg*/);
/**********************************************************
SYNOPSIS:  #include < fcntl.h>
FUNCTION:  epoll_ fcntl
DESCRIPTION:	获取或修改已打开文件的属性
PARAMETER: 
(1)	fd:为被操作文件描述符
(2)	cmd:为操作fd的命令
(3)	用来接收命令cmd所需要的参数,该值可以为空
**********************************************************/
案例:
	flag = fcntl(fd,F_GETFL);
	flag |= O_NONBLOCK;
	fcntl(fd,F_SETFL,flag);

4. 实例

使用epoll模型搭建多路I/O转接服务器,服务器可接受客户端数据并将接收的数据转换为大写,写回客户端;客户端可向服务端发送数据,并将服务端的数据打印在终端。
(1)服务端为LT模式时
使用epoll模型搭建多路I/O转接服务器,服务器可接受客户端数据并将接收的数据转换为大写,写回客户端;客户端可向服务端发送数据,并将服务端的数据打印在终端。

#include <cstdio>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<ctype.h>
#include<errno.h>

#define MAXLINE 80                   //缓冲区数组大小
#define SERV_PORT 58000               //端口号
#define OPEN_MAX 1024                //最大打开文件描述符的数量


int main()
{
    int listenfd, connfd, sockfd;
    int nready, efd;
    int res, maxi;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];
    socklen_t clilen;
    int client[OPEN_MAX];
    struct  sockaddr_in cliaddr, servaddr;
    struct  epoll_event tep, ep[OPEN_MAX];
    listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
    {
        perror("bind!");
        exit(EXIT_FAILURE);
    }
    if (listen(listenfd, 20) == -1)
    {
        perror("listen error");
        exit(EXIT_FAILURE);
    }

    //初始化client集合
    for (int i = 0; i < OPEN_MAX; i++)
    {
        client[i] = -1;
    }
    maxi = -1;    //初始化maxi
    efd = epoll_create(OPEN_MAX);   //创建epoll句柄
    if (efd == -1)
    {
        perror("epoll_create ");
        exit(EXIT_FAILURE);
    }
    //初始化
    tep.events = EPOLLIN;
    tep.data.fd = listenfd;

    //为服务器进程注册事件(listenfd)
    res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
    if (res == -1)
    {
        perror("epool_ctl");
        exit(EXIT_FAILURE);
    }
    for (;;)
    {
        nready = epoll_wait(efd, ep, OPEN_MAX, -1);   //阻塞监听
        if (nready == -1)
        {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
        printf("epoll_wait\n");
        for (int i = 0; i < nready; i++)
        {
            //若fd为listenfd,表示有连接请求到达
            if (ep[i].data.fd == listenfd)
            {
                clilen = sizeof(cliaddr);
                connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
                printf("received from %s at port %d\n",
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
                    ntohs(cliaddr.sin_port));
                //将accpet获取的文件描述符保存到cient[]数组中
                int j = 0;
                for (j = 0; j < OPEN_MAX; j++)
                {
                    if (client[j] < 0)
                    {
                        client[j] = connfd;
                        break;
                    }
                }
                if (j == OPEN_MAX)
                {
                    perror("too many clients");
                    exit(EXIT_FAILURE);
                }
                if (j > maxi)
                {
                    maxi = j;    //更新最大文件描述符
                }
                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                //为新建立连接的进程注册事件
                res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
                if (res == -1)
                {
                    perror("epoll_ctl");
                    exit(EXIT_FAILURE);
                }
            }
            else
            {
                sockfd = ep[i].data.fd;
                int n = read(sockfd, buf, MAXLINE);
                if (0 == n)
                {
                    int j = 0;
                    for (j = 0; j < maxi; j++)
                    {
                        if (client[j] == sockfd)
                        {
                            client[i] = -1;
                            break;
                        }
                    }
                    //取消监听
                    res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
                    if (-1 == res)
                    {
                        perror("epoll_ctl");
                        exit(EXIT_FAILURE);
                        close(sockfd);
                        printf("client [%d] closed connection\n", j);

                    }
                }
                else
                {
                    for (int j = 0; j < n; j++)
                    {
                        buf[j] = toupper(buf[j]);
                    }
                    write(sockfd, buf, n);
                }

            }
        }
    }

    close(listenfd);
    close(efd);

    return 0;

}

(2) 服务端为ET模式时
搭建边沿触发模式下的epoll服务器,使服务器可接收并处例客户端发送的数据

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

#define MAXLINE     10
#define SERV_PORT   58000
#define EPOLL_SIZE	50

void error_handling(const char* buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

int main()
{
	int listenfd, connfd;
	struct sockaddr_in serv_addr, clnt_addr;
	socklen_t clntaddr_len;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, flag;
	struct epoll_event ep_events[EPOLL_SIZE];
	struct epoll_event event;
	int epfd,event_cnt;

	listenfd = socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(SERV_PORT);
	if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
	{
		error_handling("bind error!");
	}
	if (listen(listenfd,20)== -1)
	{
		error_handling("listen() error");
	}
	printf("Accepting connection ...\n");
	clntaddr_len = sizeof(clnt_addr);
	connfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clntaddr_len);
	printf("received from %s at port %d\n", inet_ntop(AF_INET, &clnt_addr.sin_addr, str, sizeof(str)), ntohs(clnt_addr.sin_port));
	
	
	flag=fcntl(connfd, F_GETFL);
	flag |= O_NONBLOCK;
	fcntl(connfd, F_SETFL, flag);


	epfd = epoll_create(EPOLL_SIZE);
	if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event) == -1)
	{
		error_handling("epoll_ctl error!");
	}
	event.events = EPOLLIN | EPOLLET;
	event.data.fd = connfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);

	int len = 0;
	//获取数据
	while (1)
	{
		printf("epoll_wait begin\n");
		int res = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		printf("epoll_wait end res=%d\n", res);
		if (ep_events[0].data.fd==connfd)
		{
			while ((len=read(connfd,buf,MAXLINE/2))>0)
			{
				printf("%s", buf);
				write(STDOUT_FILENO, buf, len);
			}
		}
	}
	return 0;
}

(3)客户端

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



#define MAXLINE 80                   //缓冲区数组大小
#define SERV_PORT 58000              //端口号


int main()
{
	struct  sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;
	sockfd = socket(PF_INET, SOCK_STREAM, 0);
	if (sockfd == -1)
	{
		printf("create socket failed\n");
		exit(EXIT_FAILURE);
	}
	memset(&servaddr, 0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servaddr.sin_port = htons(SERV_PORT);

	if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0)
	{
		printf("connect failed\n");
		exit(EXIT_FAILURE);
	}
	while (fgets(buf, MAXLINE, stdin) != NULL)
	{
		write(sockfd, buf, strlen(buf));
		n = read(sockfd, buf, MAXLINE);
		if (n == 0)
		{
			printf("the other side has been closed.\n");
		}
		else
		{
			write(STDOUT_FILENO, buf, n);
		}
	}
	close(sockfd);
	return 0;
}

5. 参考

[1]: linux 手册
[2]: linux编程基础书籍
[3]: https://www.jianshu.com/p/ed1f9e9a1982

你可能感兴趣的:(多路I/O复用,linux,服务器)