多路复用详解

LInux系统编程面试会问你点啥问题?

​ 进程和线程有啥区别

​ 网络编程七层模型

​ TCP UDP的区别

​ TCP三次握手

​ 进程间通信有哪些方式

​ 用过多路复用? 各有什么特点?

​ select ,缺点 一个进程中最多只能监测1024个设备

​ poll, 一个进程可以监测超过1024个设备

​ 但是select 和poll监测的效率低(轮询机制)

​ epoll, 效率更高 (回调机制)

多路复用

目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

1.使用 场景

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

1)当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。

2)当一个客户同时处理多个套接口时,这种情况是可能的,但很少出现。

3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

2.select

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

**基本原理:**select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  • select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024**。**

    可以通过cat /proc/sys/fs/file-max察看。

  • 进行扫描时是线性扫描,即采用轮询的方法,效率较低。

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

  • 需要在返回后,通过遍历文件描述符来获取已经就绪的文件。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

    动图封面

int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exeptfds, 
           struct timeval *timeout)
	numfds : 该参数值为需要监视的文件描述符的最大值加1

	readfds : 由select()监视的读文件描述符集合

	writefds :由select()监视的写文件描述符集合

	exeptfds :由select()监视的异常处理文件描述符集合
	返回值:大于0成功,返回准备好的文件描述符的数目;0时,超时;-1时,出错。
FD_ZERO(fd_set *set), 清除一个文件描述符集
FD_SET(int fd, fd_set *set), 将一个文件描述符加入文件描述符集中
FD_CLR(int fd, fd_set *set), 将一个文件描述符从文件描述符集中清除
FD_ISSET(int fd, fd_set *set), 如果文件描述符fd 为fd_set 集中的一个元素,则返回非零值,可以用于调用select()之后测试文件描述符集中的文件描述符是否有变化

struct timeval
{
	long tv_sec; /* 秒 */
	long tv_unsec; /* 微秒 */
}

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
 
/* 定义一个宏用来求两个数中的最大值 */
#define MAX(a, b)	((a) > (b) ? (a) : (b))
 
/* 定义缓冲区的大小 */
#define BUFFER_SIZE		512
 
/* 程序的入口函数 */
int main(int argc, char *argv[])
{
	int fd0, fd1, fd2, max_fd;
	fd_set read_set, tmp_set;
	char buf[BUFFER_SIZE];
	int ret, read_len;
 
	/* 打开三个文件 */
	if((fd0 = open("in0", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in0 error!\n");
		return -1;
	}
	if((fd1 = open("in1", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in1 error!\n");
		return -1;
	}
	if((fd2 = open("in2", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in2 error!\n");
		return -1;
	}
 
	/* 取出三个文件描述符当中最大的一个 */
	max_fd = MAX(MAX(fd0, fd1), fd2);
 
	/* 初始化read_set文件描述符集合 */
	FD_ZERO(&read_set);
	FD_SET(fd0, &read_set);
    FD_SET(fd1, &read_set); 
    FD_SET(fd2, &read_set);
 
	/* 进入循环操作 */
	while(1)
	{
		tmp_set = read_set;
		/*
		 *	监视三个文件的读操作,一直等待
		 *		返回值 : 0 超时 -1 出错 大于0 成功
		 */
		ret = select(max_fd + 1, &tmp_set, NULL, NULL, NULL);
 
		if(ret < 0)		/* 出错 */
		{
			printf("select error!\n");
			return -1;
		}
		else if (ret == 0)	/* 超时 */
		{
			printf("time out!\n");
			return 0;
		}
		else if(ret > 0)	/* 成功 */
		{
			if(FD_ISSET(fd0, &tmp_set))	/* in0 有数据可读 */
			{
				memset(buf, 0, BUFFER_SIZE);
				read_len = read(fd0, buf, BUFFER_SIZE);
				if(read_len > 0)
				{
					buf[read_len] = '\0';
					printf("Read from in0 : %s\n", buf);
				}
			}
			if(FD_ISSET(fd1, &tmp_set))	/* in1 有数据可读 */
			{
				memset(buf, 0, BUFFER_SIZE);
				read_len = read(fd1, buf, BUFFER_SIZE);
				if(read_len > 0)
				{
					buf[read_len] = '\0';
					printf("Read from in1 : %s\n", buf);
				}
			}
			if(FD_ISSET(fd2, &tmp_set))	/* in2 有数据可读 */
			{
				memset(buf, 0, BUFFER_SIZE);
				read_len = read(fd2, buf, BUFFER_SIZE);
				if(read_len > 0)
				{
					buf[read_len] = '\0';
					printf("Read from in2 : %s\n", buf);
				}
			}
		}
	}
	
	return 0;
}
mkfifo in0
mkfifo in1
mkfifo in2
gcc select.c
./a.out
#另启窗口
echo aaa >in0
echo bbb >in1
echo ccc >in1

3.poll

poll本质上和select没有区别 , 但它没有最大连接数的限制,原因是它是基于链表来存储的。

int poll(struct pollfd *fds, int numfds, int timeout);
	struct pollfd
    {
        int fd; /* 需要监听的文件描述符 */
        short events; /* 需要监听的事件 */
        short revents; /* 已发生的事件 */
    }
	events成员描述需要监听哪些类型的事件,可以用以下几种标志来描述:
         POLLIN:文件中有数据可读,下面实例中使用到了这个标志
         POLLPRI::文件中有紧急数据可读
         POLLOUT:可以向文件写入数据
         POLLERR:文件中出现错误,只限于输出
         POLLHUP:与文件的连接被断开了,只限于输出
         POLLNVAL:文件描述符是不合法的,即它并没有指向一个成功打开的文件
    numfds: 需要监听的文件个数,即第一个参数所指向的数组中的元素数目

	timeout : 表示超时时间,0时立即返回,负数时则永远等待
	函数返回值, 大于0成功,等于0超时,-1表示出错。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
 
/* 定义缓冲区的大小 */
#define BUFFER_SIZE		512
 
/* 程序的入口函数 */
int main(int argc, char *argv[])
{
	struct pollfd fds[3];
	char buf[BUFFER_SIZE];
	int i, ret, read_len;
 
	/* 打开三个文件 */
	if((fds[0].fd = open("in0", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in0 error!\n");
		return -1;
	}
	if((fds[1].fd  = open("in1", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in1 error!\n");
		return -1;
	}
	if((fds[2].fd  = open("in2", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in2 error!\n");
		return -1;
	}

	//memset(fds, sizeof(fds), 0);
 
	/* 初始化各文件描述符需要监听的事件 */
	for(i = 0; i < sizeof(fds)/sizeof(fds[0]); i++)
	{
		fds[i].events = POLLIN;
	}
 
	/* 进入循环操作 */
	while(1)
	{
		/*
		 *	监视三个文件的读操作,一直等待
		 *		返回值 : 0 超时 -1 出错 大于0 成功
		 */
		ret = poll(fds, sizeof(fds)/sizeof(fds[0]), -1);
 
		if(ret < 0)		/* 出错 */
		{
			printf("poll error!\n");
			return -1;
		}
		else if (ret == 0)	/* 超时 */
		{
			printf("time out!\n");
			return 0;
		}
		else if(ret > 0)	/* 成功 */
		{
			for(i = 0; i < sizeof(fds)/sizeof(fds[0]); i++)
			{
				if(fds[i].revents & POLLIN)	/* 对应文件有数据可读 */
				{
					memset(buf, 0, BUFFER_SIZE);
					read_len = read(fds[i].fd, buf, BUFFER_SIZE);
					if(read_len > 0)
					{
						buf[read_len] = '\0';
						printf("Read from in%d : %s\n", i, buf);
					}
				}
			}
		}
	}
	
	return 0;
}

4.epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

**基本原理:**epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll的优点:

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降
  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

int epoll_create(int size);
	自从linux2.6.8之后,size参数是被忽略的。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	epfd, epoll_create函数的返回值
    op, 常用宏
        EPOLL_CTL_ADD:注册新的fd到epfd中;
        EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
        EPOLL_CTL_DEL:从epfd中删除一个fd;
    fd, 需要监听的文件描述符
    event, 需要监听的事件
        struct epoll_event {

               __uint32_t   events;      /* Epoll events */

               epoll_data_t data;        /* User data variable */

           };
		  EPOLLIN :表示对应的文件描述符可以读
           EPOLLOUT:表示对应的文件描述符可以写
           EPOLLPRI:表示文件描述符有紧急的数据可读
           EPOLLERR:表示文件描述符发生错误
           EPOLLHUP:表示文件描述符被挂断
           EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式
           EPOLLONESHOT:只监听一次事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
        参数events用来从内核得到事件的集合
        参数events用来从内核得到事件的集合
        maxevents告之内核这个events有多大
        参数timeout是超时时间(
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
 
/* 定义缓冲区的大小 */
#define BUFFER_SIZE		512
#define MAX_EVENTS 1024

 
/* 程序的入口函数 */
int main(int argc, char *argv[])
{
	struct epoll_event event, events[MAX_EVENTS];
	char buf[BUFFER_SIZE];
	int i, ret, read_len;
	int fds[3] = {0};
	
	//创建epoll实例 内核中
	int epollfd = epoll_create(MAX_EVENTS);
	if(epollfd == -1){
		perror("epoll create");
		return -1;
	}
	

	/* 打开三个文件 */
	if((fds[0] = open("in0", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in0 error!\n");
		return -1;
	}
	event.data.fd = fds[0];
	event.events = EPOLLIN;//监听读
	if(epoll_ctl(epollfd, EPOLL_CTL_ADD, event.data.fd, &event)){
		perror("epoll add error");
		return -1;
	}


	if((fds[1]  = open("in1", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in1 error!\n");
		return -1;
	}
	event.data.fd = fds[1];
	event.events = EPOLLIN;//监听读
	if(epoll_ctl(epollfd, EPOLL_CTL_ADD, event.data.fd, &event)){
		perror("epoll add error");
		return -1;
	}


	if((fds[2]  = open("in2", O_RDONLY|O_NONBLOCK)) < 0)
	{
		printf("open in2 error!\n");
		return -1;
	}
	event.data.fd = fds[2];
	event.events = EPOLLIN;//监听读
	if(epoll_ctl(epollfd, EPOLL_CTL_ADD, event.data.fd, &event)){
		perror("epoll add error");
		return -1;
	}

 
	/* 进入循环操作 */
	while(1)
	{

		int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
		if (nfds == -1){
			perror("epoll wait");
			return -1;
		}
		int n = 0;
		for(n=0; n<nfds; n++){
			if(events[n].data.fd == fds[0]){
				memset(buf, 0, BUFFER_SIZE);
				read_len = read(fds[0], buf, BUFFER_SIZE);
				if(read_len > 0)
				{
					buf[read_len] = '\0';
					printf("Read from in0 : %s\n", buf);
				}
			}			
			if(events[n].data.fd == fds[1]){
				memset(buf, 0, BUFFER_SIZE);
				read_len = read(fds[1], buf, BUFFER_SIZE);
				if(read_len > 0)
				{
					buf[read_len] = '\0';
					printf("Read from in1 : %s\n", buf);
				}
			}			
			if(events[n].data.fd == fds[2]){
				memset(buf, 0, BUFFER_SIZE);
				read_len = read(fds[2], buf, BUFFER_SIZE);
				if(read_len > 0)
				{
					buf[read_len] = '\0';
					printf("Read from in2 : %s\n", buf);
				}
			}			
		}
	}
	return 0;
}

在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点 :

  • 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
  • select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

5.内核源码分析

1683868803410

SYSCALL_DEFINE1(epoll_create, int, size)
{
    if (size <= 0)
        return -EINVAL;

    return sys_epoll_create1(0);
}
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    // kzalloc(sizeof(*ep), GFP_KERNEL),用的是内核空间
    error = ep_alloc(&ep);
    // 获取尚未被使用的文件描述符,即描述符数组的槽位
    fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
    // 在匿名inode文件系统中分配一个inode,并得到其file结构体
    // 且file->f_op = &eventpoll_fops
    // 且file->private_data = ep;
    file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
                 O_RDWR | (flags & O_CLOEXEC));
    // 将file填入到对应的文件描述符数组的槽里面
    fd_install(fd,file);             
    ep->file = file;
    return fd;
}

最后epoll_create生成的文件描述符如下图所示

Linux下epoll使用源码样例分析

所有的epoll系统调用都是围绕eventpoll结构体做操作,现简要描述下其中的成员:

/*
 * 此结构体存储在file->private_data中
 */
struct eventpoll {
    // 自旋锁,在kernel内部用自旋锁加锁,就可以同时多线(进)程对此结构体进行操作
    // 主要是保护ready_list
    spinlock_t lock;
    // 这个互斥锁是为了保证在eventloop使用对应的文件描述符的时候,文件描述符不会被移除掉
    struct mutex mtx;
    // epoll_wait使用的等待队列,和进程唤醒有关
    wait_queue_head_t wq;
    // file->poll使用的等待队列,和进程唤醒有关
    wait_queue_head_t poll_wait;
    // 就绪的描述符队列
    struct list_head rdllist;
    // 通过红黑树来组织当前epoll关注的文件描述符
    struct rb_root rbr;
    // 在向用户空间传输就绪事件的时候,将同时发生事件的文件描述符链入到这个链表里面
    struct epitem *ovflist;
    // 对应的user
    struct user_struct *user;
    // 对应的文件描述符
    struct file *file;
    // 下面两个是用于环路检测的优化
    int visited;
    struct list_head visited_list_link;
};
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
        int, maxevents, int, timeout)
{
    /* 检查epfd是否是epoll\_create创建的fd */
    // 调用ep_poll
    error = ep_poll(ep, events, maxevents, timeout);
    ...
}
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)
{
    __add_wait_queue_exclusive(&ep->wq, &wait);
    。。
}

当设备(文件)IO就绪时,在驱动程序中调用

wake_up_interruptible_sync_poll
    __wake_up_sync_key
    	__wake_up_common
    		curr->func(curr, mode, wake_flags, key) //回调ep_poll_callback{
    				list_add_tail(&epi->rdllink, &ep->rdllist);//添加到就绪队列
    				/*
                     * Wake up ( if active ) both the eventpoll wait list and the ->poll()
                     * wait list.
                     */
                    if (waitqueue_active(&ep->wq))
                        wake_up_locked(&ep->wq);
                    if (waitqueue_active(&ep->poll_wait))
                        pwake++;

			}

img

当进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象 ,这个eventpoll对象既是一个设备,也是一个进程。

说它是一个设备,是因为它有一个成员变量wait_queue_head_t wq;,设备是否就绪取决于成员变量rdllist是否为空,为空时调用epoll_wait的进程A就睡眠在wq指向的等待队列上。

请添加图片描述

说它是一个进程,是因为内核会将eventpoll管理的设备对象(epitem)添加到真正监测的设备对应的等待队列中去。

poll()
* wait list.
*/
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;

		}



 [外链图片转存中...(img-WbqVdbZw-1720947587083)] 





当进程调用 `epoll_create` 方法时,内核会创建一个 `eventpoll` 对象 ,**这个eventpoll对象既是一个设备,也是一个进程。**

说它是一个设备,是因为它有一个成员变量wait_queue_head_t wq;,设备是否就绪取决于成员变量rdllist是否为空,为空时调用epoll_wait的进程A就睡眠在wq指向的等待队列上。

 [外链图片转存中...(img-bKSFGO46-1720947587083)] 

说它是一个进程,是因为内核会将eventpoll管理的设备对象(epitem)添加到真正监测的设备对应的等待队列中去。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值