日常知识点之网络面试八股文(tcp,惊群现象,协程)

建立在一直学习的基础上,我总觉得对于下面的问题我能做出一些理解。

但是我明白的知道,不做一做实际的整理或者测试,我的理解总有一种建立在理论之上,似懂非懂的感觉。

结合百度,下面这些问题我按照自己的理解为自己做一些知识备份,如果有不对的知识点,请指正。。。

0:总结

把做以下梳理时,相关的理解写在前面。

1:在整理listen和accept,以及半连接队列和全连接队列时,整理相关知识。

listen后,开始三次握手,三次握手的过程中,内核协议栈会维持一个半连接队列和全连接队列。

listen的参数backlog,和操作系统参数配合,决定了半连接队列和全连接队列的大小,如果超过,对丢弃连接或者发送rst报文

accept是三次握手最后收到ack完成后,从全连接队列中取一个已完成的连接。

日常知识点之网络面试八股文(tcp,惊群现象,协程)

参考:TCP实战二(半连接队列、全连接队列) - 控球强迫症 - 博客园 (cnblogs.com)

2:tcp一些有用的知识点

tcp通过序列号和ack重传机制,实现有序传输。

通过慢启动,拥塞避免,快重传,快恢复,以及滑动窗口(发送窗口,限制窗口,接收窗口,慢启动门限),实现拥塞控制和流量控制。

close_wait大量出现是因为服务器没有及时close,closing是同时调用close会出现的状态。

recv函数接收长度为0,是对端发送了close,服务器应该尽快进行close操作。

recv函数在事件失效,或者内部没有数据时,会返回-1,errno为again,在et模式循环读取时很有用的知识点。

3:惊群相关

高版本的linux操作系统底层已经对惊群做了处理,只会唤醒第一个线程,所以一般模拟的时候,惊群现象也不会出现了。

这样看,高版本的linux环境,惊群问题本身得到处理,但是不能过多依赖操作系统喽?

可以模拟惊群现象,增加耗时等待。。。

4:遗留问题:

1:tcp是基于流式传输的,那么send时,如果缓冲区有多个包的数据,是怎么处理的?

2:nginx事件触发的代码逻辑,以及accept锁,和worker进程的负载均衡,可以整理一下。

===》如何使accept锁尽快释放?

3:协程基于setjmp/longjmp(state threads),基于glibc的ucontext组件(coroutine),基于汇编(libco,ntyco)以及网上说的基于switch-case的奇淫技巧来实现(Protothreads),以及其中用汇编实现过程中涉及到的一个C语言的hook技巧

4:协程与网络io的关系?如果搭配使用呢?

5:尝试使用以下strace

1:tcp在listen时的参数backlog的意义?

直接在linux环境上,使用man listen查看相关描述,对其进行理解。

 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>

 int listen(int sockfd, int backlog);
/*
DESCRIPTION
       listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).

       The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.

       The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.  If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying
       protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
       
....
       */

翻译一下上述描述:

/*
listen() 将 sockfd 引用的套接字标记为被动套接字,即将使用 accept(2) 接受传入连接请求的套接字。
        sockfd 参数是一个文件描述符,它引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字。
        backlog 参数定义了 sockfd 的挂起连接队列可以增长到的最大长度。 如果在队列已满时连接请求到达,客户端可能会收到带有 ECONNREFUSED 指示的错误,或者,如果底层协议支持重传,请求可以被忽略,以便稍后重新尝试连接成功。
        */

理解:

===》listen函数基于已经(执行过bind操作)绑定的一个sockfd,此后,客户端连接这个fd时,内核会维持一个半连接队列和全连接队列

===》客户端进行connect连接时,内核会维持通过一个半连接队列维持这些connect的clientfd(客户端发送syn,服务端回复ayn_ack后)

===》backlog 描述了半连接队列和全连接队列的大小(和内核设置的大小一起决定了半连接队列和全连接队列的大小)。

===》如果队列已满时到达,客户端会收到ECONNREFUSED 指示的错误(这个时connect的返回值吗? 服务器拒绝,回复rst时的场景)

=========》半连接队列和全连接队列满时,有新的连接时,要么直接丢弃,要么会回复rst报文。

===》支持重新发送connect的话,会稍后连接上.(服务器会回复rst报文,让客户端重发)

2:accept发生在三次握手的哪一步?

同样的,在linux环境上使用man accept对该函数进行了解。 (这里只粘贴了部分,相关的error返回值自行查看)

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

//这里直接翻译查出的英文描述相关
/***************************************************
描述
       accept() 系统调用与基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)一起使用。
       它为侦听套接字 sockfd 提取挂起连接队列中的第一个连接请求,创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符。
       ====> 新创建的套接字不处于监听状态。原始套接字 sockfd 不受此调用的影响。

       参数 sockfd 是一个使用 socket(2) 创建的套接字,使用 bind(2) 绑定到本地地址,并在 listen(2) 之后侦听连接。
       参数 addr 是指向 sockaddr 结构的指针。
       ====>如通信层所知,该结构用对等套接字的地址填充。返回地址 addr 的确切格式由套接字的地址族确定(参见socket(2) 和相应的协议手册页)。 
       ====>addr为NULL时,不填任何内容;在这种情况下,不使用 addrlen,也应该为 NULL。
	   addrlen 参数是一个值结果参数:调用者必须将其初始化为包含 addr 指向的结构的大小(以字节为单位);返回时它将包含对等地址的实际大小。
	   ====>如果提供的缓冲区太小,返回的地址会被截断;在这种情况下,addrlen 将返回一个大于提供给调用的值。

       如果队列中没有挂起的连接,并且套接字没有被标记为非阻塞,accept() 会阻塞调用者,直到连接出现。如果套接字被标记为非阻塞并且队列中没有挂起的连接,则 accept()失败并出现错误 EAGAIN 或 EWOULDBLOCK。

       为了通知套接字上的传入连接,您可以使用 select(2) 或 poll(2)。尝试新连接时将传递一个可读事件,然后您可以调用 accept() 以获取该连接的套接字。或者,你可以设置套接字在套接字上发生活动时传递 SIGIO;有关详细信息,请参阅套接字(7)。

       对于某些需要显式确认的协议,例如 DECNet,accept() 可以被认为只是将下一个连接请求出队而不是暗示确认。对新文件的正常读取或写入可以暗示确认描述符,并且可以通过关闭新套接字来暗示拒绝。目前只有 DECNet 在 Linux 上具有这些语义。

       如果 flags 为 0,则 accept4() 与 accept() 相同。可以在标志中对以下值进行按位或运算以获得不同的行为:

       SOCK_NONBLOCK 在新打开的文件描述上设置 O_NONBLOCK 文件状态标志。使用此标志可节省对 fcntl(2) 的额外调用以实现相同的结果。
       SOCK_CLOEXEC 在新文件描述符上设置 close-on-exec (FD_CLOEXEC) 标志。请参阅 open(2) 中对 O_CLOEXEC 标志的描述,了解这可能有用的原因。
返回值
       成功时,这些系统调用返回一个非负整数,它是已接受套接字的描述符。出错时,返回 -1,并适当设置 errno。

错误处理
       Linux accept()(和 accept4())将新套接字上已经挂起的网络错误作为来自 accept() 的错误代码传递。此行为不同于其他 BSD 套接字实现。为了可靠运行,应用程序应检测网络错误
       在 accept() 之后为协议定义并通过重试将它们视为 EAGAIN。对于 TCP/IP,它们是 ENETDOWN、EPROTO、ENOPROTOOPT、EHOSTDOWN、ENONET、EHOSTUNREACH、EOPNOTSUPP 和 ENETUNREACH。
 *******************************/

理解:

===》listen函数执行后,针对这个fd,内核维持了一个半连接队列和一个全连接队列(描述了三次握手过程中,客户端的连接情况)

===》accept是基于已经执行过bind,listen后的连接的fd(这个服务端fd内部会维持一个半连接和全连接队列)进行操作的。(从全连接队列中取一个已连接)

===》accept从全连接队列(已完成连接完成队列中)提取一个,创建一个新的fd。

===》accept函数参数可以获取连接客户端的ip和端口相关信息,

===》accept如果处理的是非阻塞的fd,没有链接时,返回-1,出现错误 EAGAIN 或 EWOULDBLOCK

===》accept可以配合io多路复用(select,poll,epoll)使用

===》accept4是一个可以直接设置fd相关配置,和accept函数功能相同,扩展的一个函数吧。

3:tcp和udp的区别?

3.1:tcp是面向连接的,使用时必须一对一连接(三次握手四次挥手),udp时无连接的,直接发送,可以一对多

3.2:tcp通过“ack+重复发送”的机制实现数据的可靠到达,肯定对速度有影响,udp只管发送,容易造成网络拥塞。

===》tcp如何控制可靠到达以及传输速率? (通过网络拥塞控制(慢启动,拥塞避免,快重传,快恢复),流量控制(滑动窗口(接收窗口,拥塞窗口,发送窗口,慢启动门限共同作用)))

======》慢启动和拥塞避免通过 慢启动门限,控制发送窗口大小的改变

======》快重传:连续三次收到一个确认ack,快速重传

======》快恢复:改变拥塞窗口值、发送窗口值为原来的一半,继续通过慢启动和拥塞避免逻辑进行控制。

======》除此之外:tcp还有一些定时器,连接定时器,重传定时器(RTO),坚持定时器(persist timer),延迟定时器(Delayed ACK),保活定时器(Keep Alive),FIN_WAIT_2定时器,TIME_WAIT定时器

3.3:tcp是面向字节流的传输控制协议,udp是面向报文数据报协议。

面向字节流:即,协议栈接收到的数据,一股脑全仍在缓存中,我们通过recv从缓存中取数据,是不知道边界在哪的,需要上层协议控制。

===》tcp保证了这些包的顺序,可靠得,以流得形式塞入到缓冲区中。

面向数据报:recvfrom时,每次从缓存中取数据都是按接受的报文一个一个取,底层协议栈是做了边界处理的,如果recvfrom一次没有取全,下次也是从下一个报文处取,没取到的就丢了。

===》udp不保证数据得可靠,这里得包可能会丢,可能会乱序。

3.4:tcp保证了可靠性(牺牲速率),udp保证了速率(不可靠)

TCP适合对传输效率要求低,但准确率要求高的应用场景,比如万维网(HTTP)、文件传输(FTP)、电子邮件(SMTP)等。

udp适用于对传输效率要求高,但准确率要求低的应用场景,比如域名转换(DNS)(数据包小,用udp对网络影响不大)、远程文件服务器(NFS)等。

遗留一个思考:tcp得接收缓冲区可以理解,(一般应该不会有这种现象吧!)那么tcp得发送缓冲区,如果缓冲区缓存过多得数据,tcp是怎么区分缓存得这些包得边界得? 还是按流处理?

4:大量close_wait的原因

4.1:四次挥手了解close_wait出现的原因

一般tcp连接的断开,是由客户端主动发起的。

从下图可以看出,colse_wait状态是由于客户端主动断开连接,服务端回复客户端后,迟迟没有主动调用close发起请求断开导致。
日常知识点之网络面试八股文(tcp,惊群现象,协程)

4.2:close_wait的代码分析

这里有一个知识点,我们在实现tcp服务端代码时,采用io多路复用也好:

===》客户端调用close时,服务端会触发recv接收长度为0;

===》在采用et模式进行循环读数据时,读到最后一个,返回-1, 错误码errno是eagain。

分析:当我们recv返回值为0时,说明客户端主动发起关闭请求,我们应该在服务端尽快关闭该连接执行close触发服务端的主动关闭流程。

===》如果这里没有关闭或者又阻塞执行的动作,必然会出现大量的close_wait状态

5:closing出现的原因

先找个tcp状态迁移图看一下closing状态:
日常知识点之网络面试八股文(tcp,惊群现象,协程)

从图中可以看出,closing状态出现的场景时,当状态处于FIN_WAIT_1时,接收到对端的FIN报文,而不是正常的期望已发送的FIN回复ACK报文,

这种场景说明是两端同时发送FIN请求,同时执行close的场景(发送fin后没有收到ack报文,却收到对端的fin文本)

当接收到对端的ack后,就会变为time_wait状态(确保自己的ack对端收到),知道再变成closed状态。

6:eagain的原因(et模式可以以EAGAIN判断读完)

//在linux环境下执行man recv查看返回值的分析描述。
EAGAIN 或 EWOULDBLOCK
    套接字被标记为非阻塞并且接收操作将阻塞,或者已设置接收超时并且在接收数据之前超时已过期。 POSIX.1 允许在这种情况下返回任一错误,并且不需要这些配置stants 具有相同的值,因此可移植应用程序应检查这两种可能性。

个人理解:

===》也就是说,对于非阻塞的fd,在进行实际接收处理时,这个数据已经过期/超时无效了(被别人处理了)

场景:

===》epoll的lt模式下,多线程同时对一个fd进行处理,会出现eagain
===》epoll的et模式下,在事件触发,循环读数据 ,直到读数据到-1时,会是这个errno。

原来是tcp在recv时,没有数据会返回-1,errno为eagain

返回值为0时表示客户端断开连接

7:tcp如何保证顺序

tcp协议头中一个序列号字段,通过该序列号以及ack机制,保证tcp的可靠顺序接收。

8:epoll的惊群如何解决?

8.1:什么是惊群

惊群现象模拟参考:Linux网络编程“惊群”问题总结 - Rabbit_Dale - 博客园 (cnblogs.com)

惊群是针对服务器而言的,

===》作为服务器,往往要考虑大量连接,高并发的场景,需要设计特定的网络模型(如nginx的多进程)

===》在多进程或者多线程的网络模型中,如果处理不当,就会有惊群现象。

===》例如:作为一个tcp的服务器,设计多进程对accept,recv和send进行处理,多个进程共同管理一个epoll,当事件触发时,所有进程就会有响应,但是实际处理该事件的,只会是一个进程使用epoll_wait取数据成功

使用strace结合多线程监听一个epoll的场景,演示一下惊群的现象:

//例如 作为tcp的服务端,主线程创建了十个进程,对epoll进行监听处理。
// 注意  我用多线程并没有测试成功,因为一个进程的多个线程之间,其实还是按照一定的先后顺序调度执行, ===》是保证了epoll_wait的及时提取,以及操作系统本身对惊群已经做了处理??  不过类似多进程,在多线程的epoll_wait后面加个sleep(),应该也是能看到的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/socket.h>
#include <arpa/inet.h>

#include <sys/epoll.h>

#include <fcntl.h>
#include <unistd.h>  
#include <errno.h>

#include <unistd.h>    //pid_t fork(void);
#include <sys/types.h>
#include <sys/wait.h>  //wait

#define VPS_PORT 9999

void fork_test();
//惊群测试的逻辑
int fork_test_shocking_herd(); 

int main()
{
	//先对进程的创建进行一个测试
	//fork_test(); //不要一起测试
	//惊群测试的逻辑
	fork_test_shocking_herd(); 
	return 0;
}

void fork_test()
{
	//创建十个进程试试
	pid_t pid;
	for(int i=0; i<10; i++)
	{
		pid = fork();
		if(pid == 0)
		{
			printf("%d : fork() success [%d][%d] \n",i, getpid(), getppid());
			break;
		}

		if(pid >0)
		{
			wait(NULL); //这里应该放在最后 不然等一个子进程结束才下一个
			printf("%d: fork() parent pid is[%d] \n", i, getppid());
		}

		if(pid < 0)
		{
			printf("%d: fork() error \n\n", i);
		}
	}
	printf("my pid success is[%d][%d] \n", getpid(), getppid());
	return;
}

int vps_init_socket();
int vps_create_epoll_and_addfd(int listenfd);
int vps_epoll_wait_do_cycle(int epfd, int listenfd);
int fork_test_shocking_herd()
{
	//创建epoll, 创建listendfd
	int fd = vps_init_socket();
	if(fd < 0)
	{
		printf("create vps socket fd error. \n");
		return -1;
	}else
	{
		printf("create vps socket fd:%d success. \n",fd);
	}
	int epfd = -1;
	//创建epoll,并加入fd,返回epoll
	epfd = vps_create_epoll_and_addfd(fd);
	if(epfd == -1)
	{
		printf("create epoll fd error.\n");
		close(fd);
		return -1;
	}
	printf("\n\n create epoll and listenfd success :[%d:%d] \n", epfd, fd);

	//创建十个进程,对epoll进行监听
	pid_t pid;
	for(int i=0; i<10;i++)
	{
		pid = fork();
		if(pid == 0)
		{
			//这里对epoll事件进行处理  epoll_wait处理
			printf("%d : fork() success [%d][%d] \n",i, getpid(), getppid());
			vps_epoll_wait_do_cycle(epfd, fd);
			break;
		}

		if(pid < 0)
		{
			printf("fork() error \n\n");
		}
	}
	//主进程wait等待进程的结束
	if(pid > 0)
	{
		int status;
		wait(&status);
		close(fd);
		close(epfd);
	}
	
	return 0;
}
//设置fd非阻塞  默认情况下  fd是阻塞的
int SetNonblock(int fd) {
	int flags;
	flags = fcntl(fd, F_GETFL, 0);
	if (flags < 0)
		return flags;
	flags |= O_NONBLOCK;
	if (fcntl(fd, F_SETFL, flags) < 0) 
		return -1;
	return 0;
}

//创建 服务端socket,这里的ip和port写死了
int vps_init_socket()
{
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0)
	{
		printf("create socket error. \n");
		return -1;
	}
	//设置fd非阻塞  设置端口可重用
	SetNonblock(fd);
	int optval = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));

	//定义fd相关的参数进行绑定
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(struct sockaddr_in));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(VPS_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	if(bind(fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) < 0)
	{
		printf("vps socket bind error \n");
		return -1;
	}

	//设置fd为被动套接字 供accept用  设置listen队列的大小 
	if(listen(fd , 20) < 0)
	{
		printf("vps socket listen error \n");
		return -1;
	}

	printf("create and set up socket success. start accept.. \n");
	return fd;
}

int vps_create_epoll_and_addfd(int listenfd)
{
	//创建epoll
	int epfd = -1;
	epfd = epoll_create(1); //参数已经忽略必须大于0
	if(epfd == -1)
	{
		printf("create vsp epoll error. \n");
		return -1;
	}
	//epoll_ctl加入一个节点
	struct epoll_event event;
	event.data.fd = listenfd;
	event.events = EPOLLIN | EPOLLET;  //监听接入 采用ET

	if(epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event) == -1)
	{
		printf("vps epoll add listenfd error. \n");
		close(epfd);
		return -1;
	}
	printf("vps epoll create success and add listenfd success.[%d] \n", epfd);
	return epfd;
}

// 事件触发  处理连接请求
int vps_accept_exec(int epfd, int listenfd)
{
	//有链接来了   需要epoll接收  epoll_ctl加入监听可读事件
	struct  sockaddr_in cliaddr;
	socklen_t clilen = sizeof(struct sockaddr_in);

	//et模式  把连接都拿出来
	int clifd = -1;
	int ret = 0;
	int success_time = 0;
	while(clifd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen))
	{
		//accept 正常返回非负整数  出错时返回-1 这个debug调试一下吧
		if(clifd == -1)
		{
			//资源暂时不可用  应该重试  但是不应该无限重试
			if (((errno == EAGAIN) || (errno == EWOULDBLOCK) )&& ret <3) 
			{
				ret++;
				continue;
			}
			printf(" accept error: [%s]\n", strerror(errno));
			return -1;
		}
		//对已经连接的fd进行处理  应该加入epoll
		SetNonblock(clifd);
		//加入epoll
		struct epoll_event clifd_event;
		clifd_event.data.fd = clifd;
		clifd_event.events = EPOLLIN | EPOLLET; //ET模式要循环读
		if(epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &clifd_event) == -1)
		{
			printf("vps accetp epoll ctl error . \n");
			close(clifd);
			return -1;
		}
		success_time++;
		printf("accept success. [%d:%s:%d] connected \n",clifd, inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
	}
	if(success_time == 0)
	{
		printf("\n\n accept error [%d:%d] \n\n",getpid(), getppid());
	}
	return 0;
}

// 事件触发  处理可读请求 读数据 这里没监听可写,
int vps_recv_exec(int epfd, int connfd)
{
	//这里是真正的业务处理,接收数据并且主动发送一个返回数据。
	//如果有数据  进行接收  直到接收完了,关闭连接
	printf("start recv data from client [%d].",connfd);
	//这里业务场景不频繁  客户端每发送一次就终止?
	//尽量是让客户端主动断开,
	//可以自己实现一个定时器,检测主动断开处理
	char recv_data[1024] = {0};
	int datalen = -1;
	//可能有信号中断   接收长度是-1的场景
	while(1){
		//不能把 ==0加在这里  否则会在客户端断开的时候死循环
		while((datalen = read(connfd,  recv_data,  1024)) > 0 )
		{
			printf("recv from [%d] data len[%d], data[%s] \n", connfd, datalen, recv_data);
			memset(recv_data, 0, 1024);
		}

		//在客户端关闭 断开连接的时候   接收长度才为0
		printf("recv from [fd:%d] end \n", connfd);

		//给接收到的报文一个回复报文 这里可以保存一些fd和客户端的ip和port相关关系,进行回复消息构造
		const char * send_data = "hi i have recv your msg \n";
		if(strlen(send_data) ==  write(connfd, send_data, strlen(send_data)))
		{
			printf("send buff succes [len:%lu]%s", strlen(send_data), send_data);
		}

		//服务器接收空包是因为客户端关闭导致的,着了应该关闭对应的fd并从epoll中移除
		if(datalen == 0)
		{
			if(epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, 0) == -1)
			{
				printf("vps [fd:%d] close ,remove from epoll event error\n", connfd);
			}else
			{
				printf("vps [fd:%d] close ,remove from epoll event success\n", connfd);
				close(connfd);
			}
			
			break;
		}


		//等于0 可能是读到结束
		if(datalen == -1)
		{
			printf("recv end error: [%s]\n", strerror(errno));//必然触发 已经接收完了
			if (errno == EWOULDBLOCK && errno == EINTR) //不做处理
			{
				continue;
			}
			//这里要不要移除这个fd呢? 按照移除进行处理 tcp就是短连接了
			// if(epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, 0) == -1)
			// {
			// 		printf("vps client [%d] remove from epoll error\n", connectfd);
			// }else
			// {
			// 		printf("vps client [%d] remove from epoll success\n", connectfd);
			// }
			
			// close(connfd);
			break;
		}
	}
	
	return 0;
}

//使用epoll_wait对epfd进行监听  然后业务处理 
int vps_epoll_wait_do_cycle(int epfd, int listenfd)
{
	struct epoll_event event_wait[1024];
	int nready = 0;

	while(1) //如果多线程 这里应该设置终止标志
	{
		//int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
		nready = epoll_wait(epfd, event_wait, 1024, -1);
		sleep(2);
		printf(" \n\n  i have get the event [%d:%d] \n", getpid(),getppid());
		if(nready < 0)
		{
			if (errno == EINTR)// 信号被中断
    		{
	    		printf("vps epoll_wait return and errno is EINTR \n");
                continue;
    		}
            printf("vps epoll_wait error.[%s]\n", strerror(errno));
            break;
		}

		if(nready == 0)
		{
			continue;
		}

		//这里已经有相关的事件触发了 进行业务处理
		for(int i = 0; i<nready; i++)
		{
			//处理可读,区分listenfd
			if(event_wait[i].events & EPOLLIN)
			{
				if(event_wait[i].data.fd == listenfd)
				{
					//处理accept 这里应该监听可读 不监听可写
					vps_accept_exec(epfd, event_wait[i].data.fd);
				}else
				{
					//处理recv, 可能对端主动关闭,
					vps_recv_exec(epfd, event_wait[i].data.fd); 
				}
			}

			//这种情况下应该从epoll中移除,并关闭fd
    		//这里如果不是客户端发完就终止的业务,我们是不是不del,只有异常时del
    		if (event_wait[i].events & (EPOLLERR | EPOLLHUP)) //EPOLLHUP 已经读完
    		{
    			printf("epoll error [EPOLLERR | EPOLLHUP].\n");
    			epoll_ctl(epfd, EPOLL_CTL_DEL, event_wait[i].data.fd, NULL);
    			close(event_wait[i].data.fd);
    		}
		}
	} 
	return 0;
}

现象:

使用多进程模拟惊群的时候,发现竟然也看不到现象,是现在版本的linux已经对惊群做了处理,只会唤醒一个进程。

在epoll_wait后加个sleep(2),可以看到有惊群的现象。。。

strace ./xxx ===>从图中可以看到创建了十个进程,一次请求,其实唤醒了多个进程。

//注意:惊群是因为取事件不及时 来一个事件,唤醒了多个进程同时进行处理,但是却只需要一个处理
//操作系统已经做了规避,这里为了模拟,在这里加了sleep模拟
	while(1) 
	{
		nready = epoll_wait(epfd, event_wait, 1024, -1);
		sleep(2);  //注意   
		printf(" \n\n  i have get the event [%d:%d] \n", getpid(),getppid());
		...
	}

日常知识点之网络面试八股文(tcp,惊群现象,协程)

8.2:惊群的处理方案

出现惊群的根因还是:多个线程对同一个事件进行了处理。

解决思路:还是在竞争资源上做想法,(保证只有一个线程处理这个事件)

8.2.1:思考处理方案

====》一个进程进行accept处理,其他进程分别处理其他连接的fd(recv和send),(有accept处理就扔给一个线程/协程/进程专门去处理接收和发送) (memcached处理)

========》这里对accept新建的fd传给工作线程,不能确保工作负载,accept的线程和工作线程都维持了各自的事件管理。

====》类似nginx多进程网络模型,使用accept锁对读事件进行加锁,保证只有一个进程能处理这个事件。

========》accept互斥锁实际上是一个跨进程锁,放在一个所有进程共享的内存中

========》所有的进程,先抢占互斥锁,抢到的进程进行accept连接,然后进行读取,解析,处理请求,(一个请求完全由一个进程进行处理,并且只在一个进程中处理) 这里要考虑及时释放accept锁,稍后处理一些事件。

8.2.2:nginx accept锁

描述:个人对nginx这块源码没有完全理解,但是基于理论可能关于accept锁这块是这样理解的(毕竟没研究代码,可能不靠谱)

=======》开启accept锁,是为了处理连接到来,多个进程都处理的惊群现象。

=======》nginx更多的是作为一个web服务器,基于http的一个请求,一个回复的场景。 那么关于该业务的场景,使用accept锁,提取到已经触发到的事件,进行新连接和接收处理。

=======》nginx内部各个worker进程的负载均衡管理,是通过一个变量控制的,占总连接数的7/8 (总链接数应该是根据操作系统设置和配置生效的吧,与性能和内存有关)

=======》在负载均衡的基础上,nginx worker进程会尝试获取accept锁,获取到锁后,会进行新连接的处理以及原来的事件处理,为了锁的尽快释放,接收到新连接是直接放入一个队列后释放锁的 (这里可能要了解一下源码。。。,有两个队列,连接队列和events队列)

注意:这里有一个细节,如何保证accept锁的快速释放,nginx是如何实现这个逻辑的?

nginx多进程的好处:与核绑定,解耦(主进程与各个工作进程各司其职),各个工作进程互不影响,单个进程之间不用加锁。

9:为什么会有协程?

参考:有了多线程,为什么还要有协程? - 知乎 (zhihu.com)

9.1:进程和线程调度开销,无法控制切换顺序,消耗内存

进程的内存空间是独立的,多进程之间的切换其实是耗费cpu的,以及进程间通信也比较麻烦,但是进程安全性高,一个进程崩溃,不会影响其他进程。

线程有自己独立的堆栈,但也可以访问进程的每个地址空间,由内核控制快速切换。 线程是不安全的,一个线程的崩溃可能导致进程的崩溃。

====》线程是要分配内存的,一个进程可以分配的线程的个数是有限的。

不论是进程还是线程,其实底层都由cpu调度管理,有调度开销,并且调度顺序由底层控制的。

9.2:协程:用户态控制切换

线程的频繁切换消耗cpu,可以在用户层对切换进行管理。 ===》省cpu

多线程为每个线程分配固定的内存以保障线程的运行,协程共用线程的内存,只是对相关运行程序进行调度管理。 ===》省内存,内存不再制约我们

协程进行开发,异步的性能,同步的编程方式,我们更好的理解业务 ===》便于理解流程

除此之外,多进程之间共享内存难,多线程之间无保护(导致进程崩溃问题),也可以规避掉。 ==》稳定

9.3:协程的实现方案以及开源库

9.3.1:协程库大概思路(纯属记忆,需要后期梳理)

总是就说协程是用户层管理的轻量级的线程,实现协程的原理,就是对程序运行的寄存器栈,参数等进行保存,并调度等待中的下一个协程。

(可能细节描述不清晰,后期学习个协程库再来)

====》实现协程的要点,就是定义存储相关寄存器栈,参数等程序运行上下文信息的结构

====》管理多个协程程序上下文堆栈,参数信息,进行调度切换(yeild 让出cpu, resume 重新恢复运行)

====》重点就是resume和yeild的实现逻辑。

9.3.2:协程实现方案、协程开源库

有很多的语言已经支持了协程,c/c++有一些协程库。

实现协程的关键点在于 resume函数和yeild函数的实现,主要涉及到切换寄存器信息,已知的,由如下几种方案:

====》1:基于setjmp/longjmp 实现, (state threads library ,c语言实现,srs流媒体服务器中用到该协程库)

====》2:基于glibc 的 ucontext组件实现 (coroutine

====》3:基于汇编实现 (libco,ntyco是基于汇编实现的协程库)

==》网上说,利用C语言语法switch-case的奇淫技巧来实现(Protothreads),这个可以后期看看。。。 (一个“蝇量级” C 语言协程库 | 酷 壳 - CoolShell

以前很早的时候接触一个项目用到了协程,使用的是汇编语言实现,并用到了c语言里的hook,,,这个待梳理一下。

以前也接触过coroutine,libco,但是是在自己刚进入工作的时候,可以对这个进行回顾。

10:协程与网络io的关系?

===》不清楚,结合百度理解一下,

协程的切换,单独的由用户层进行切换,那么如何选择切换顺序呢? 保存状态+切换

网络io:我们都知道可以使用io多路复用,事件监听对网络io进行处理。

协程:保存状态,管理了多个协程的切换。

网络io:可以通过事件触发,做对应的业务处理

===》两者结合,由事件触发网络io,带动协程的调度切换,是否可以实现高并发?

我开始试着积累一些常用代码,文章中涉及的代码:自己代码库中备用

我的知识储备更多来自这里,推荐你了解:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

上一篇:redis为什么单线程还这么快


下一篇:LVS基本信息