【网络篇】Linux 下的五种IO模型(万字总结)

目录

一、阻塞IO(最普遍,绝大部分程序都是阻塞IO)

二、非阻塞IO(一直占用资源,CPU利用率高)

三、IO多路复用(重点)

1.select

2.poll 

3.epoll 

四、信号驱动IO

五、异步IO


扫描二维码关注公众号,回复: 15098571 查看本文章

一、阻塞IO(最普遍,绝大部分程序都是阻塞IO)

当没有输入内容可以疏导进程程序时,进行阻塞等待

进程程序调用read读取套接字(文件)内容,如果套接字没有数据可读,阻塞等待

如果写(输出)的数据量很大, 但是没有那么大的缓冲去可写,就阻塞等待

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


int main()
{
	
	//TCP服务端
	//1、创建套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	//2、绑定套接字
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(9999);
	serveraddr.sin_addr.s_addr = inet_addr("192.168.124.63");
	bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
	//3、监听套接字
	listen(sockfd,10);
	//4、接收(同意)套接字
	int client[3];
	for(int i = 0; i < 3 ;i++)	
	{
		client[i] = accept(sockfd,NULL,NULL);
	}
	printf("ok\n");
	
	//阻塞IO操作
	while(1)
	{
		char buf[20];
		memset(buf,0,20);
		read(client[0],buf,20);
		printf("0 is %s\n",buf);

		memset(buf,0,20);
		read(client[1],buf,20);
		printf("1 is %s\n",buf);
	
		memset(buf,0,20);
		read(client[2],buf,20);
		printf("2 is %s\n",buf);
	}


	return 0;
}

二、非阻塞IO(一直占用资源,CPU利用率高)

当进程进行IO操作(请求)——read、write时,进程通知内核,如果不能立即完成IO(读、写)操作,就立即结束返回,不进行本次IO操作。

默认一般都是阻塞IO,通过函数功能(fcnt())来实现原来的阻塞IO变成非阻塞IO,设置文件描述符(套接字)的属性为非阻塞。

fcnt()进行设置:先获取文件描述符的状态属性,修改对应的属性状态值,再设置回文件描述符1

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


int main(int argc,char * argv[])
{
	
	//TCP服务端
	//1、创建套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	//2、绑定套接字
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(atoi(argv[2]));
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	bind(sockfd,&serveraddr,sizeof(serveraddr));
	//3、监听套接字
	listen(sockfd,10);
	//4、接收(同意)套接字
	int client[3];
	for(int i = 0; i < 3 ;i++)	
	{
		client[i] = accept(sockfd,NULL,NULL);
		int flag = fcntl(client[i],F_GETFL);//获取fd的属性
		flag |= O_NONBLOCK;
		fcntl(client[i],F_SETFL,flag);//设置属性
	}
	printf("ok\n");


	//非阻塞IO操作,不管是否读取都会结束往下执行,必须要使用循环来轮训
	while(1)
	{
		char buf[20];
		memset(buf,0,20);
		if(read(client[0],buf,20) >= 0)//返回值判断是否成功接收read到数据
			printf("0 is %s\n",buf);

		memset(buf,0,20);
		if(read(client[1],buf,20) >= 0)
			printf("1 is %s\n",buf);
	
		memset(buf,0,20);
		if(read(client[2],buf,20) >= 0)
			printf("2 is %s\n",buf);
	}


	return 0;
}

三、IO多路复用(重点)

同时管理多个IO(文件描述符),当某个或多个文件描述符可以进行通信(可以执行输入输出IO操作),得到对应的文件描述符去进行读写IO操作

1.select

select是一种IO多路复用技术,是通过函数来实现的。它的原理是:
(1)构造列表:  首先要构造一个关于文件描述符的列表(需要监听的),列表数据类型为 fd_set,它是一个整型数组,总共是 1024 个比特位,每一个比特位代表一个文件描述符的状态。


(2)检测事件:调用 select() 。监听该列表中的文件描述符的事件,函数返回并修改文件描述符的列表中对应的值,0 表示没有检测到该事件,1 表示检测到该事件。检测的操作是由内核完成的。


(3)进行I/O 操作 :  select() 返回时告诉进程有多少描述符要进行 I/O 操作,接下来遍历文件描述符的列表进行 I/O 操作。


select 的缺点:

(1)开销大:调用select,需要把 fd 集合从用户态拷贝到内核态,需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时很大;


(2)支持数量少 :select 支持的文件描述符数量太小了,默认是 1024(由 fd_set 决定);


(3)文件描述符集合不能重用,因为内核每次检测到事件都会修改,所以每次都需要重置;


(4)定位不深 :每次 select 返回后,只能知道有几个 fd 发生了事件,但是具体哪几个还需要遍历文件描述符集合进一步判断

相关API函数: 

#include <sys/select.h>

//功能:实现IO多路复用,管理多个文件描述符

 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

//阻塞监视已经管理的文件描述符,直到管理的文件描述符中有文件描述符可以进行IO操作,得到对应的文件操作符(会把要监视的表中清空,然后剩下已经可以进行IO操作的文件描述符),之后进行需要的IO操作处理

参数1:

int nfds:管理的最大的文件描述符+1(范围)

参数2:

fd_set *readfds:管理的读的文件描述符集合表(只管你的读),不需要写NULL

参数3:

 fd_set *writefds:管理的写的文件描述符集合表(只管你的写),不需要写NULL

参数4:

fd_set *exceptfds:管理的异常表,不需要写NULL

参数5:

struct timeval *timeout:管理的时间,如果为空表示阻塞,就一直监视到有文件描述符可以进行IO操作为止

struct timeval {

             time_t      tv_sec;         /* seconds */

             suseconds_t tv_usec;        /* microseconds */

           };

返回值:

成功:0

失败:-1

管理文件描述符的表:

1.从fd_set表中删除某个文件描述符

void FD_CLR(int fd, fd_set *set);

2.判断fd_set表中是否存在某个fd

int  FD_ISSET(int fd, fd_set *set);

3.把文件描述符加入到表中

void FD_SET(int fd, fd_set *set);

4.清空表

void FD_ZERO(fd_set *set);

实现代码: 

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


int main(int argc,char * argv[])
{
	
	//TCP服务端
	//1、创建套接字
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	//2、绑定套接字
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(atoi(argv[2]));
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	bind(sockfd,&serveraddr,sizeof(serveraddr));
	//3、监听套接字
	listen(sockfd,10);
	
	//4、接收(同意)套接字
	int maxfd = 2;//表示当前的最大文件描述符
	fd_set rfds;//定义表变量
	FD_ZERO(&rfds);//清空表
	
	FD_SET(sockfd,&rfds);
	if(maxfd < sockfd)
		maxfd = sockfd;
	
	printf("ok\n");

	while(1)
	{
	fd_set temp = rfds;
	int ret = select(maxfd+1,&temp,NULL,NULL,NULL);//IO多路复用,管理多个文件描述符
	if(ret < 0)
	{
		printf("select error\n");
		return -1;
	}
	else
	{
		printf("select ok\n");
	}
	//在select成功后,对应的表中只剩下就绪的IO文件描述符
	printf("num is %d\n",ret);

	//已经可以进行IO操作的文件描述符进行处理
	for(int i = 0; i <= maxfd; i++)
	{
		if( FD_ISSET(i,&temp) )//为真,表示i 在rfds表中
		{
			if(i == sockfd)
			{
				int clientfd = accept(sockfd,NULL,NULL);//建立连接
				printf("accept\n");
				
				FD_SET(clientfd,&rfds);//在管理中,添加通信的套接字
        			if(maxfd < clientfd)
                			maxfd = clientfd;
			}
			else
			{
				char buf[20];
				memset(buf,0,20);
				read(i,buf,20);
				printf("%s\n",buf);
				write(i,buf,20);
			}
		}
	}

	}

	return 0;
}

2.poll 一个一个去比较

        用一个文件描述符集合,每次只轮询一个集合,也是返回事件个数(只返回个数),效率比select高。poll支持的文件描述符没有限制,实现原理和select类似。

poll特点:

(1)实现数据结构:链表,文件描述符没有最大限制

(2)每次调用都需要将fd集合从用户态拷贝到内核态

(3)内核需要遍历所有fd,效率低

相关API函数:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

 参数1:

struct pollfd *fds:fds:指向元素类型为struct poll_fd类型的首元素
struct pollfd {
               int   fd;         /* 文件描述符 */
               short events;     /* 请求的事件 */
               short revents;    /* 返回的事件 */
           };
fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。

events:指定监测fd的事件(输入、输出、错误),每一个事件有多个取值

revents:对文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回

events和revents取值如下:

 

参数2:nfds_t nfds

nfds_t nfds:调用者应该在nfds中指定fds数组中的项数,即用来指定第一个参数数组元素个数

参数3:

int timeout:是poll函数调用阻塞的时间,单位:毫秒;和 select 一样,最后一个参数 timeout 指定 poll() 将在超时前等待一个事件多长事件

-1:表示出错了
0: 表示超时了
>0:表示产生时间的个数

返回值:

0: 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
-1:设置 errno 为下列值之一:

    EBADF:一个或多个结构体中指定的文件描述符无效。
    EFAULT:fds 指针指向的地址超出进程的地址空间。
    EINTR:请求的事件之前产生一个信号,调用可以重新发起。
    EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
    ENOMEM:可用内存不足,无法完成请求。

 实现代码

服务端:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
 #include <poll.h>
 
int main()
{
    
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    printf("socket ok %d\n", sockfd);
 
    
    struct sockaddr_in serveraddr, caddr;
    serveraddr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = inet_addr("192.168.124.63");//根据网络自己设置
 
    
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    printf("bind ok.\n");
 
    
    if (listen(sockfd, 5) < 0)
    {
        perror("listen err.");
        return -1;
    }
    printf("listen ok.\n");
 
    
     struct pollfd fds[20]={};  //大小自己确定,没有限定个数
    
     fds[0].fd = 0;
     fds[0].events=POLLIN;//读事件
 
     fds[1].fd=sockfd;
     fds[1].events=POLLIN;
 
 
    int n = 2, ret;
    char buf[128];
    int recvbyte;
    
    while (1)
    {
        ret = poll(fds, n, -1);
        if (ret < 0)
        {
            perror("poll err.");
            return -1;
        }
        //处理事件
        for (int i = 0; i < n; i++)
        {
            if (fds[i].revents == POLLIN)
            {
                if (fds[i].fd == 0)
                {
                    fgets(buf, sizeof(buf), stdin);
                    printf("key:%s\n", buf);
                    
                    for(int j=2;j<n;j++)
                    {
                        send(fds[j].fd,buf,sizeof(buf),0);
                    }
                }
                else if (fds[i].fd == sockfd)
                {
              
                    acceptfd = accept(sockfd, (struct sockaddr *)&caddr, sizeof(caddr));
 
                    if (acceptfd < 0)
                    {
                        perror("accept err.");
                        return -1;
                    }
                    printf("accept ok.\n");
 
                    
                    printf("ip:%s ,port:%d\n",
                           inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
 
                    
                    fds[n].fd=acceptfd;
                    fds[n].events=POLLIN;
                    n++;
                }
                else
                {
                    
                    recvbyte = recv(fds[i].fd, buf, sizeof(buf), 0);
                    if (recvbyte < 0)
                    {
                        perror("recv err.");
                        // return -1;
                    }
                    else if (recvbyte == 0)
                    {
                        printf("%d client exit.\n",fds[i].fd);
                        close(fds[i].fd);
                        fds[i]=fds[n-1];
                        n--;
                        i--;              
                        break;
                    }
                    else
                    {
                        printf("%d buf:%s\n",fds[i].fd, buf);
                    }
                }
            }
        }
    }
    close(sockfd);
    return 0;
}

客户端 

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
 
 
int main()
{
    int sockfd, acceptfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("socket OK!\n");
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = inet_addr("192.168.124.63");//根据网络自己设置
    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    {
        perror("connect err");
        return -1;
    }
    printf("connect OK!\n");
    
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        char buf[123]="";
        while (1)
        {
            scanf("%s", buf);
            send(sockfd, buf, 32, 0);
        }
    }
    else
    {
        char buf1[32] = "";
        int recvfd;
        while (1)
        {
            recvfd = recv(sockfd, buf1, 32, 0);
            if (recvfd < 0)
            {
                perror("recv err");
                return -1;
            }
            else
            {
                printf("%s\n", buf1);
            }
        }
        wait(NULL);
    }
 
    
    return 0;
}

3.epoll 

        epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;红黑树是一种平衡二叉树,时间复杂度为 O(log n),就算这个池子就算不断的增删改,也能保持非常稳定的查找性能。

epoll是一种更加高效的IO复用技术,epoll的使用步骤即原理如下:

(1)epoll_create()

调用epoll_create()会在内核中创建一个eventpoll结构体数据,称之为epoll对象,在这个结构体中有2个比较重要的数据成员,一个是需要检测的文件描述符的信息struct_root rbr(红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息(双向链表);

函数原型:

int epoll_create(int size);
返回一个句柄,之后epool的使用都是依靠这个句柄来标识,参数size是告诉epoll要处理的大致事件数目

举例:

epollfd = epoll_create(1024);
    if (epollfd == -1) 
        {
            perror("epoll_create");
            exit(EXIT_FAILURE);
         }

(2)epoll_ctrl()

调用epoll_ctrl()可以向epoll对象中添加、删除、修改要监听的文件描述符及事件;

我们拿到了一个epollfd, 这个epollfd 就能唯一代表这个 epoll 池。

注意:用户可以创建多个 epoll 池

然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 

函数原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_creat返回的句柄
op:指定监听对象的操作
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
fd:需要监听的socket句柄fd
event:告诉内核需要监听什么事的结构体,如下:

epoll_data_t;
struct epoll_event {
    __uint32_t events; /* 要监听的事件 */
    epoll_data_t data; /* User data variable */
}

举例:

epoll_ctl(epollfd, EPOLL_CTL_ADD, 10, &event)

 上面,我们就把句柄 11 放到这个池子里了,op(EPOLL_CTL_ADD)表明操作是增加,event 结构体可以指定监听事件类型,可读、可写。

(3)epoll_wait()

调用epoll_wt()可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。

epoll 跟底层对接的回调函数是:ep_poll_callback,这个函数其实很简单,做两件事情:

        1) 把事件就绪的 fd 对应的结构体放到一个特定的队列(就绪队列,ready list);

        2) 唤醒 epoll 

当 fd 满足可读可写的时候就会经过层层回调,最终调用到这个回调函数,把对应 fd 的结构体放入就绪队列中,从而把 epoll 从 epoll_wait处唤醒。

函数原型:

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

epfd:epoll的描述符。

events:分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中
(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

nmaxevents:表示本次可以返回的最大事件数目
            通常 maxevents参数与预分配的events数组的大小是相等的。

timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒)
        如果 timeout为0,则表示epoll_wait在 rdllist链表中为空,立刻返回,不会等待。

举例:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout:是epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件返回,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。

epoll的两种工作模式:

(1)LT 模式(水平触发)

LT (Level-Triggered)是缺省的工作方式,并且同时支持 Block和Nonblock Socket。在这种做法中,内核检测到一个文件描述符就绪了,然后可以对这个就绪的fd进行IO操作,如果不作任何操作,内核还是会继续通知。


(2)ET模式(边沿触发)

ET (Edge-Triggered)是高速工作方式,只支持Nonblock socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll检测到。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd进行IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

实现代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include<iostream>
const int MAX_EVENT_NUMBER = 10000; //最大事件数
// 设置句柄非阻塞
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 main(){

    // 创建套接字
    int nRet=0;
    int m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if(m_listenfd<0)
    {
        printf("socket fail!");
        return -1;
    }
    // 
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons(6666);

    int flag = 1;
    // 设置ip可重用
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    // 绑定端口号
    int ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    if(ret<0)
    {
        printf("fail to bind!,errno :%d",errno);
        return ret;
    }

    // 监听连接fd
    ret = listen(m_listenfd, 200);
    if(ret<0)
    {
        printf("fail to listen!,errno :%d",errno);
        return ret;
    }

    // 初始化红黑树和事件链表结构rdlist结构
    epoll_event events[MAX_EVENT_NUMBER];
    int m_epollfd = epoll_create(5);
    if(m_epollfd==-1)
    {
        printf("fail to epoll create!");
        return m_epollfd;
    }



    // 创建节点结构体将监听连接句柄
    epoll_event event;
    event.data.fd = m_listenfd;
    //设置该句柄为边缘触发(数据没处理完后续不会再触发事件,水平触发是不管数据有没有触发都返回事件)
    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    // 添加监听连接句柄作为初始节点进入红黑树结构中,该节点后续处理连接的句柄
    epoll_ctl(m_epollfd, EPOLL_CTL_ADD, m_listenfd, &event);

    //进入服务器循环
    while(1)
    {
        int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if (number < 0 && errno != EINTR)
        {
            printf( "epoll failure");
            break;
        }
        for (int i = 0; i < number; i++)
        {
            int sockfd = events[i].data.fd;
            // 属于处理新到的客户连接
            if (sockfd == m_listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);
                int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
                if (connfd < 0)
                {
                    printf("errno is:%d accept error", errno);
                    return false;
                }
                epoll_event event;
                event.data.fd = connfd;
                //设置该句柄为边缘触发(数据没处理完后续不会再触发事件,水平触发是不管数据有没有触发都返回事件),
                event.events = EPOLLIN | EPOLLRDHUP;
                // 添加监听连接句柄作为初始节点进入红黑树结构中,该节点后续处理连接的句柄
                epoll_ctl(m_epollfd, EPOLL_CTL_ADD, connfd, &event);
                setnonblocking(connfd);
            }
            else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
            {
                //服务器端关闭连接,
                epoll_ctl(m_epollfd, EPOLL_CTL_DEL, sockfd, 0);
                close(sockfd);
            }
            //处理客户连接上接收到的数据
            else if (events[i].events & EPOLLIN)
            {
                char buf[1024]={0};
                read(sockfd,buf,1024);
                printf("from client :%s");

                // 将事件设置为写事件返回数据给客户端
                events[i].data.fd = sockfd;
                events[i].events = EPOLLOUT | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
                epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);
            }
            else if (events[i].events & EPOLLOUT)
            {
                std::string response = "server response \n";
                write(sockfd,response.c_str(),response.length());

                // 将事件设置为读事件,继续监听客户端
                events[i].data.fd = sockfd;
                events[i].events = EPOLLIN | EPOLLRDHUP;
                epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);
            }
           
        }
    }


}

四、信号驱动IO

        信号驱动IO不是像select采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否有可读的数据,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。

一般通过如下 6 个步骤来使用信号驱动式 I/O 模型。

1. 为通知信号安装处理函数。

通过 sigaction() 来完成:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

默认情况下,这个通知信号为 SIGIO。

2.为文件描述符的设置属主。

通过 fcntl() 的 F_SETOWN 操作来完成:

fcntl(fd, F_SETOWN, pid)

属主是当文件描述符上可执行 I/O 时,会接收到通知信号的进程或进程组。

pid 为正整数时,代表了进程 ID 号。

pid 为负整数时,它的绝对值就代表了进程组 ID 号。

3. 使能非阻塞 I/O。

通过 fcntl() 的 F_SETFL 操作来完成:

flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

4.使能信号驱动 I/O。

通过 fcntl() 的 F_SETFL 操作来完成:

flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);

5.进程等待 "IO 就绪" 信号的到来。

当 I/O 操作就绪时,内核会给进程发送一个信号,然后调用在第 1 步中安装好的信号处理函数。

6.进程尽可能多地执行 I/O 操作。

循环执行 I/O 系统调用直到失败为止,此时错误码为 EAGAIN 或 EWOULDBLOCK。

原因:

信号驱动 I/O 提供的是边缘触发通知,即只有当 I/O 事件发生时我们才会收到通知,

且当文件描述符收到 I/O 事件通知时,并不知道要处理多少 I/O 数据。

参考代码:

#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int socket_fd = 0;

void do_sometime(int signal) {
    struct sockaddr_in cli_addr;
    int clilen = sizeof(cli_addr);
    int clifd = 0;

    char buffer[256] = {0};
    int len = recvfrom(socket_fd, buffer, 256, 0, (struct sockaddr *)&cli_addr,
                       (socklen_t)&clilen);
    printf("Mes:%s", buffer);

    sendto(socket_fd, buffer, len, 0, (struct sockaddr *)&cli_addr, clilen);
}

int main(int argc, char const *argv[]) {
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = do_sometime;
    sigaction(SIGIO, &act, NULL);  // 监听IO事件

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8888);
    servaddr.sin_addr.s_addr = INADDR_ANY;

    //设置将要在socket_fd上接收SIGIO的进程
    fcntl(socket_fd, F_SETOWN, getpid());

    int flags = fcntl(socket_fd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    flags |= O_ASYNC;
    fcntl(socket_fd, F_SETFL, flags);

    bind(socket_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    while (1) sleep(1);

    close(socket_fd);

    return 0;
}

五、异步IO

        其实经过了上面两个模型的优化,我们的效率有了很大的提升,但是我们当然不会就这样满足了,有没有更好的办法,通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。有没有一种方式,我只要发送一个请求我告诉内核我要读取数据,然后我就什么都不管了,然后内核去帮我去完成剩下的所有事情。这就是异步IO模型所要解决的问题

        异步IO模型只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用。

猜你喜欢

转载自blog.csdn.net/qq_53676406/article/details/129673823