文章目录

  • 1.在服务器端用多线程和单线程的方式设计相应服务器程序的优缺点?
  • 3.select
  • 2.poll
  • 3.epoll

1.在服务器端用多线程和单线程的方式设计相应服务器程序的优缺点?

  • 服务器程序的功能:
    服务端程序能够处理多个客户端传上来的请求,应对并发,采取以下的两种方式:

(1)如果用多线程的方式去设计,每个线程处理一个客户端,但是由于线程的上下文切换会有很大的开销,所以不建议。
(2)如果采用单线程的方式是可以处理数据连接的请求的,用单线程为啥不会造成网络数据的丢失呢?因为客户端的每个连接请求都是一个IO,IO是DMA控制器进行控制的,而不是由CPU控制的,所以数据不会丢失。

在LInux系统中,每个网络连接都是以文件描述符的形式,伪代码如下:

while(1)
{
	for (FDx in FD1-FD10)//遍历文件描述符
	{
		if (FDx有数据的话)
			读取FDx的数据;处理;
	}
}

由于有数据或者没数据,是由我们所写的程序所决定的,这样的效率很一般,所以用select、poll、epoll函数来提高效率

3.select

eg:

/*
1.下面的这块代码是准备文件描述符的数组fds,文件描述符集合;
2.创建socket服务端
*/
sockdf=socket(AF_INET,SOCK_STREAM,0);
memset()&addr,0,ziseof(addr);
addr.sin_family=AF_INET;
addr.sin_port=htons(2000);
addr.sin_port.s_addr=INADDR_ANY;
bind(sockfd,(struct sockaddr*)addr,sizeof(addr));
listen(sockfd,5);

for (int i=0;i<5;i++)
{
	memset(&client,0,sizeof(client));
	addrlen=sizeof(client);
	fds[i]=accept(sockfd, (struct sockaddr *)&client, &*addelen);
	/*
	1.创建了5个文件描述符,即:socket可以接受五个client端的连接,分别存访至数组fds中;
	2.accept返回的文件描述符的编号是随机的
	*/
	if (fds[i]>max)
		max=fds[i];//max是最大文件描述符的值
}
------------------------------------分割线----------------------------------------
	while (1)
	{
		FD_ZERO(&rset);
		for (int i=0;i<5;i++)
		{
			FD_SET(fds[i],&rset);
		}
		/*
		1.reset是bitmap类型的,表征哪个文件描述符是被启用的(监听的),不直接接受文件描述符
		2.bitmap默认是1024位的,默认是涵盖了所有的fds,eg:上面的fds数组的文件描述符
		的序号是1,2,5,7,9,那么在bitmap中,实际上是0110010101000....
		*/
		puts("round again");
		select(max+1,&rset,NULL,NULL,NULL);
		/*
		1.select(最大文件描述符+1,读文件描述符集合,写文件描述符集合,异常,超时时间);
		2.0-9是10个数,select会卡这10个数,所以设置成max+1
		3.总结:select函数将用户态的文件描述符拷贝到内核,让内核来判断哪一个文件描述符有数据,
			当里面一个有数据或者多个有数据的时候,select函数会返回,有数据的fd会被置位,select函数
			返回,遍历文件描述符,看哪一个文件描述符被置位set了,就从哪个fd读数据
		4.select函数缺点:bitmap有上限,只有1024个;rset不可重用,每次都要初始化bitmap;用户态
		和内核态的切换仍有开销;select函数返回后,需要O(n)再次遍历哪些文件描述有数据;
		*/		
		for (int i=0;i<5;i++)
		{
			if (FD_ISSET(fds[i],&rset))
			/*
			1.遍历fd,判断哪一个fd被set置位
			2.被置位的fd,会被读出来,然后进行一些处理puts
			3.有可能两个fd都有数据,所以在返回的时候,要遍历5个文件描述符
			*/
			{
				memset(buffer,0,MAXBUF);
				read(fds[i]),buffer,MAXBUF);
				puts(buffer);
			}
		}
	}
  • rset作用的进一步解释

(1)select函数会将用户空间的rset拷贝至内核态,由内核判断每个fd是否有数据(内核态判断数据比用户态的效率要高,用户态执行的程序,是要进行用户态和内核态的切换的,如果每次这样切换,效率不高,所以select将rset全都拷贝至内核态,让内核态判断是否有数据)

(2)若没有数据,程序会阻塞在select函数;
若有数据,首先将FD置位(表明:有数据来了,即将rset的某一位置位(rset中fd对应的那一位),插一个旗子),接着select函数会返回,不会阻塞;

2.poll

/*
1.poll一切都围绕struct pollfd结构体展开
2.直接存fd
3.events表明pollfd在意的事件是什么,读是POLLIN事件,写是POLLOUT事件
4.revents是对events的回馈,一开始为0
*/
struct pollfd
{
	int fd;
	short events;
	short revents;
}
/*
1.accept将fd直接存到struct pollfd中
2.读事件:POLLIN
*/
for (int i=0;i<5;i++)
{
	memset(&client,0,sizeof(client));
	addrlen=sizeof(client);
	pollfds[i].fd=accept(sockfd,(struct sockaddr*)&client,&addrlen);
	pollfds[i].events=POLLIN;
}
sleep(1);
-------------------------------------分割线---------------------------------------------

while(1)
{
	puts("round again");
	poll(pollfds,5,5000);
	/*
	1.pollfds(struct pollfd结构体数组,数组中有5个元素,超时时间)
	2.poll是阻塞函数,当一个或多个文件描述符有数据的时候,内核会将pollfd置位:
	置位的是revents字段,接着poll函数返回
	3.poll优点:解决了select的1024 bitmap的缺点;每次恢复revents,所以pollfds可以重用;
	*/
	for (int i=0;i<5;i++)
	{
		if (pollfds[i].revents & POLLIN)
		{
		/*
		1.判断revents是否被置位,如果被置位成POLLIN说明有数据可读
		2.revents置位后,需要将该位复位,恢复成0,所以可以重用pollfds
		*/
			pollfds[i].revents=0;
			memset(buffer,0,MAXBUF);
			read(pollfds[i].fd,buffer,MAXBUF);
			puts(buffer);
		}
	}
}
  • poll的工作原理与select一样:用户态将fd拷贝至内核态,内核态去监听fd上的数据

3.epoll

/*
1.三个重要函数:epoll_creat创建白板,epoll_ctl写字:写一个fd-events字段,epoll_wait
*/
struct epoll_event events[5];
int epfd=epoll_creat(10);
/*
*/
...
...
/*
1.下面的for循环在白板上写了5个fd-events的数据,即:得到epfd(包含了这5个fd-events)
2.与poll的结构体相比,epoll结构体中没有revents字段,上面的1即表明只有fd字段和events字段
*/
for (int i=0;i<5;i++)
{
	static struct epoll_event ev;
	memset(&client,0,sizeof(client));
	addrlen=sizeof(client);
	ev.data.fd=accept(sockfd,(struct sockaddr *)&client,&addrlen);
	ev.events=EPOLLIN;
	epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
}
-----------------------------------分割线-----------------------------------------------
while (1)
{
	puts("round again");
	nfds=epoll_wait(epfd,events,5,10000);
	/*
	1.epoll_wait函数是阻塞的
	2.有数据的话,置位使用重排技术
	3.nfds表明有多少个fd触发了事件,所以下面的for循环的时间复杂度是O(1),select和poll
	没有返回多少个fd触发了事件;
	4.epoll实现的软件:redis,nginx,javaN2o
	*/

	for (int i=0;i<nfds;i++)
	{
		memset(buffer,0,MAXBUF);
		read(events[i].data.fd,buffer, MAXBUF);
		puts(buffer);
	}
}
  • epfd暗含了这5个fd,用户态和内核态是共享这一块内存的,epfd是在用户态和内核态共享的,用户态不用向内核态拷贝数据,解决了select和poll内核态向用户态拷贝数据的开销
  • 置位使用重排技术,将有数据的fd的标志位放到第一个位置(黑色表示有数据),然后进行返回,返回有多少个fd触发了事件

IO多路复用:select,poll,epoll_内核态

  • 有数据时候:
  • 重排后:
  • 有数据的时候:
  • 重排后:

参考:https://www.bilibili.com/video/av68126222