Linux下使用C语言实现高并发服务器

news2024/11/30 12:37:22

高并发服务器

这一个课程的笔记
相关文章
协议
Socket编程
高并发服务器实现
线程池

使用多进程并发服务器时要考虑以下几点:

  1. 父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
  2. 系统内创建进程个数(与内存大小相关)
  3. 进程创建过多是否降低整体服务性能(进程调度)
  4. 每一个子进程的效率

在使用线程模型开发服务器时需考虑以下问题:

  1. 调整进程内最大文件描述符上限
  2. 线程如有共享数据,考虑线程同步
  3. 服务于客户端线程退出时,退出处理。(退出值,分离态)
  4. 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU

多路I/O转接服务器

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

主要使用的方法有三种

select

可以使用只一个命令监听一个新的连接(需要把监管使用的lfd给select函数处理), 之后进程可以使用accept获取cfd, select可以使用获取的cfd监听连接的客户端是否发过来数据

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
  2. 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);

nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态

这一个是他的循环的上限

readfds: 监控有读数据到达文件描述符集合,传入传出参数

writefds: 监控写数据到达文件描述符集合,传入传出参数

exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数

文件描述符指针结合, 传入传出参数, 把需要监听的文件描述符放到这几个数组里面

这几个的实现实际是一个和文件描述符表对应的位图

传入的时候是要交监听的, 传出来的是实际的有事件发生的, 不关心的事件可以发送一个NULL

timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询

struct timeval {
	long tv_sec; /* seconds */
	long tv_usec; /* microseconds */
};

返回值: 三个集合里面的事件发生的总个数

void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd位清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0

使用思路
  1. lfd = socket获取描述符, 用于处理连接
  2. bind 把这一个文件描述符和对应的端口连接
  3. listen设置监听的个数
  4. fd_set rset;设置一个位图
  5. FD_ZERO
  6. FD_SET
  7. int ret = select(lfd + 1, &rset, NULL, NULL, NULL);
  8. 检测实际的事件
if(ret >  0){
    //检测一下哪一个文件描述符被设置了
    if(FD_ISSET(lfd, &rset)){
        cfd = accept();
        FD_SET(cfd, &rset);
    } 
    //遍历其余的
}
实际实现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/select.h>

int main(void){
	int i, j, nready, ret;
	int maxfd = 0;
	int listenfd, connfd;

	char buf[BUFSIZ];

	struct sockaddr_in clie_addr, serv_addr;
	socklen_t clie_addr_len;

	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(listenfd == -1){
		perror("socket error");
		exit(1);
	}
	int opt = 1;
	//设置端口复用
	ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if(ret == -1){
		perror("setsockopt error");
		exit(1);
	}
	//设置监听端口
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(6666);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
	if(ret == -1){
		perror("bind error");
		exit(1);
	}

	ret = listen(listenfd, 128);
	if(ret == -1){
		perror("listen error");
		exit(1);
	}
	//两个文件描述符集合, 用于监听多个文件描述符
	fd_set rset, allset;
	maxfd = listenfd;

	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);

	while(1){
		rset = allset;
		//监听多个文件描述符
		nready = select(maxfd+1, &rset, NULL, NULL, NULL);
		if(nready < 0){
			perror("select error");
			exit(1);
		}
        //检测一下有没有连接事件
		if(FD_ISSET(listenfd, &rset)){
			clie_addr_len = sizeof(clie_addr);
			connfd = accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
			if(connfd == -1){
				perror("accept error");
				exit(1);
			}
			FD_SET(connfd, &allset);
            //记录一下现在的最大值
			if(connfd > maxfd){
				maxfd = connfd;
			}
			if(--nready == 0){
				continue;//只有一个连接, 不需要进一步处理
			}
		}
        //遍历其余的读取事件
		for(i = listenfd + 1; i <= maxfd; i++){
			if(FD_ISSET(i, &rset)){
                //获取数据, 这一个实例里面是每一次写入以后会读取服务器的返回值
				ret = read(i, buf, sizeof(buf));
				if(ret == 0){
					printf("client close\n");
					close(i);
                      FD_CLR(i, &allset);
                      int nowmax;
                      //处理一下最大值
                      for(j = listenfd + 1; j <= maxfd; j++){
                          if(FD_ISSET(j, &allset))
                              nowmax = j;
                      }
                      maxfd = nowmax;
				}else if(ret == -1){
					perror("read error");
					exit(1);
				}else{
                      //处理一次写入
					for(j = 0; j < ret; j++){
						buf[j] = toupper(buf[j]);
					}
					write(i, buf, ret);
				}
				if(--nready == 0){
					break;
				}
			}
		}

	}

}
优缺点
  • 缺点
  1. 文件描述符里面不连续的时候上面的效率比较低, 可以使用一个单数的数组记录使用文件描述符
  2. 监听的个数有上限, 最大1024
  3. 检测满足条件的fd, 自己添加逻辑提高比较小, 编码难度比较大
  • 优点

可以跨平台, windows, linux, macOS, Unix, mips都支持这一个

poll

这一个函数的是Linux才有的, 使用的时候不如epoll, 了解即可

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
  int fd; /* 文件描述符 */
  short events; /* 监控的事件 读写异常*/
  short revents; /* 监控事件中满足条件返回的事件(返回值) */
};

监听的文件描述符的数组, 实际是把select函数的参数分开了

  • 监听事件(主要使用的是是POLLIN(读), POLLOUT(写), POLLERR(错误))

POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND

POLLRDNORM 数据可读

POLLRDBAND 优先级带数据可读

POLLPRI 高优先级可读数据

POLLOUT 普通或带外数据可写

POLLWRNORM 数据可写

POLLWRBAND 优先级带数据可写

POLLERR 发生错误

POLLHUP 发生挂起

POLLNVAL 描述字不是一个打开的文件

如果传入的是0的话, 只会监听POLLERR, POLLHUP, POLLNVAL

nfds 监控数组中有多少文件描述符需要被监控

timeout 毫秒级等待

​ -1:阻塞等,#define INFTIM -1 Linux中没有定义此宏

​ 0:立即返回,不阻塞进程

​ >0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值

如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0

he field fd contains a file descriptor for an open file. If this field is negative, then the corresponding events field is ignored and the revents field returns zero. (This provides an easy way of ignoring a file descriptor for a single poll() call: simply negate the fd field.

相较于select而言,poll的优势:

  1. 传入、传出事件分离。无需每次调用时,重新设定监听事件。
  2. 文件描述符上限,可突破1024限制。能监控的最大上限数可使用配置文件调整。
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <poll.h>

int main(void){
	int i, j, nready, ret;
	int maxfd = 0;
	int listenfd, connfd;
	struct pollfd client[1024];//poll使用的文件描述符数组
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];

	struct sockaddr_in clie_addr, serv_addr;
	socklen_t clie_addr_len;
	//获取文件描述符
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(listenfd == -1){
		perror("socket error");
		exit(1);
	}
	int opt = 1;
	//设置端口复用
	ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if(ret == -1){
		perror("setsockopt error");
		exit(1);
	}
	//设置监听端口
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(6666);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
	if(ret == -1){
		perror("bind error");
		exit(1);
	}

	ret = listen(listenfd, 128);
	if(ret == -1){
		perror("listen error");
		exit(1);
	}
	//设置poll使用的结构体
	client[0].fd = listenfd;
	client[0].events = POLLIN;
	for(i = 1; i < 1024 ;i++){
		client[i].fd = -1;
	}
	maxfd = 0;

	while(1){

		nready = poll(client, maxfd+1, -1);
		if(nready < 0){
			perror("select error");
			exit(1);
		}
		if(client[0].revents & POLLIN){
			clie_addr_len = sizeof(clie_addr);
			connfd = accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
			printf("connect\n");
			if(connfd == -1){
				perror("accept error");
				exit(1);
			}
			for(i = 1;i < 1024; i++){
				if(client[i].fd < 0){
					client[i].fd = connfd;
					client[i].events = POLLIN;
					break;
				}
			}
			if(i == 1024){
				perror("too many client");
			}
			if(i>maxfd){
				maxfd = i;
			}
			if(--nready == 0){
				continue;
			}
		}
		//处理其他的文件描述符
		for(i = 1; i <= maxfd; i++){
			if(client[i].fd < 0){
				continue;
			}

			if(client[i].revents & POLLIN){
				ret = read(client[i].fd, buf, sizeof(buf));
				if(ret == 0){
					printf("client close\n");
					close(client[i].fd);
					int nowmax = 0;
					for(j = listenfd + 1; j <= maxfd; j++){
						if(client[j].fd > 0)
							nowmax = i;
					}
					maxfd = nowmax;
					client[i].fd = -1;
				}else if(ret == -1){
					perror("read error");
					exit(1);
				}else{
					for(j = 0; j < ret; j++){
						buf[j] = toupper(buf[j]);
					}
					write(client[i].fd, buf, ret);
				}
				if(--nready == 0){
					break;
				}

			}
		}

	}

}
优缺点
  • 优点

自带数据结构, 把监听事件和返回事件分离

可以拓展监听上限, 超出1024

  • 缺点

不可以跨平台

无法直接定位满足监听事件的描述符

epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行

epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

epoll_create打开

image-20240408163813369

#include <sys/epoll.h>
int epoll_create(int size);

这是一个平衡二叉树里面的红黑树,

In the initial epoll_create() implementation, the size argument informed the kernel of the number of file descriptors that the caller expected to add to the epoll instance. The kernel used this information as a hint for the amount of space to initially allocate in internal data structures describing events. (If necessary, the kernel would allocate more space if the caller’s usage exceeded the hint given in size.) Nowadays, this hint is no longer required (the kernel dynamically sizes the required data structures without needing the hint), but size must still be greater than zero, in order to ensure backward compatibility when new epoll applications are run on older kernels.

这一个参数使用一个大于0的数字就可以了, 现在的内核会自动分配

返回值是一个用于监听的文件描述符 , 这一个是红黑树的根节点

epoll_ctl控制
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd : 之前获取的描述符

op: 表示动作,用3个宏来表示:

​ EPOLL_CTL_ADD (注册新的fd到epfd),

​ EPOLL_CTL_MOD (修改已经注册的fd的监听事件),

​ EPOLL_CTL_DEL (从epfd删除一个fd, 取消监听);

这一个事件监听会在这一个文件关闭的时候退出

fd : 要监听的fd

event: 告诉内核需要监听的事件

typedef union epoll_data {
 void        *ptr;
 int          fd;	//对应监听事件的fd
 uint32_t     u32;	//这一个不使用
 uint64_t     u64;	//不使用
} epoll_data_t;

struct epoll_event {
 uint32_t     events;      /* Epoll events 主要还是EPOLLIN, EPOLLOUT, EPOLLERR*/
 epoll_data_t data;        /* User data variable */
};

返回值:成功:0;失败:-1,设置相应的errno

epoll_wait监听
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

epfd : 之前获取的文件描述符

events: 用来存内核得到事件的集合,可简单看作数组。这是一个传出数组

maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

timeout: 是超时时间

​ -1: 阻塞

​ 0: 立即返回,非阻塞

​ >0: 指定毫秒

返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

实际使用
  1. epoll_creat创建epoll使用的文件描述符
  2. 把获取的监听的文件描述符使用epoll_ctl进行监听
  3. 调用epoll_wait阻塞等待
  4. 返回以后判断是不是监听的文件描述符, 是的话把这一个描述符加入监听
  5. 不是的话处理一下连接客户端发送的数据
#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>

int main(void){
	int i, j, nready, ret;
	int listenfd, connfd, efd;
	struct epoll_event temp, ep[1024];
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];

	struct sockaddr_in clie_addr, serv_addr;
	socklen_t clie_addr_len;
	//获取文件描述符
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(listenfd == -1){
		perror("socket error");
		exit(1);
	}
	int opt = 1;
	//设置端口复用
	ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if(ret == -1){
		perror("setsockopt error");
		exit(1);
	}
	//设置监听端口
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(6666);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
	if(ret == -1){
		perror("bind error");
		exit(1);
	}

	ret = listen(listenfd, 128);
	if(ret == -1){
		perror("listen error");
		exit(1);
	}
    
	//获取一个树根
	efd = epoll_create(1024);
	if(efd < 0){
		perror("epoll creat error");
		exit(0);
	}
    
	//把监听使用的节点添加
	temp.events = EPOLLIN;
	temp.data.fd = listenfd;
	ret = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &temp);
	if(ret == -1){
		perror("epoll ctl error");
		exit(1);
	}

	while(1){
        //开始等待
		nready =epoll_wait(efd, ep, 1024, -1);
		if(nready < 0){
			perror("select error");
			exit(1);
		}
        //处理
		for(i = 0 ; i < nready ; i++){
			if(!ep[i].events & EPOLLIN)
				continue;
			if(ep[i].data.fd == listenfd){
				//这是一个连接事件
                  clie_addr_len = sizeof(clie_addr);
				connfd = accept(listenfd, (struct sockaddr *)&clie_addr,
						&clie_addr_len);
				//打印一下连接端口的信息
				printf("connect %s :%d\n", 
						inet_ntop(AF_INET, &clie_addr.sin_addr,
							str, sizeof(str)), 
						ntohs(clie_addr.sin_port));
				if(connfd == -1){
					perror("accept error");
						exit(1);
				}
				temp.events = EPOLLIN;
				temp.data.fd = connfd;
				ret = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &temp);
				if(ret < 0){
					perror("epoll ctl con error");
					exit(1);
				}
			}else{
				//处理其他的文件
				if(ep[i].events & EPOLLIN){
					ret = read(ep[i].data.fd, buf, sizeof(buf));
					if(ret == 0){
						//连接结束
						printf("client close\n");
						close(ep[i].data.fd);
						epoll_ctl(efd, EPOLL_CTL_DEL, ep[i].data.fd, NULL);
					}else if(ret == -1){
                        //read错误, 最好判断一下errno
						perror("read error");
						epoll_ctl(efd, EPOLL_CTL_DEL, ep[i].data.fd, NULL);
						close(ep[i].data.fd);
					}else{
						for(j = 0; j < ret; j++){
							buf[j] = toupper(buf[j]);
						}
						write(ep[i].data.fd, buf, ret);
					}
				}
			}
		}
	}
}

poll和epoll突破1024的限制

可以使用cat命令查看一个进程可以打开的socket描述符上限。

cat /proc/sys/fs/file-max

image-20240408162256242

这一个计算机可以打开的最多的文件的个数

之前使用的ulimit -a查看的是这一个用户下面的进程可以打开的文件的个数

image-20240408162517790

如有需要,可以通过修改配置文件的方式修改该上限值。

sudo vi /etc/security/limits.conf

在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。

* soft nofile 65536

* hard nofile 100000

img

image-20240408162924777

soft nofile :可打开的文件描述符的最大数(超过会警告);
hard nofile :可打开的文件描述符的最大数(超过会报错);

也可以使用命令ulimit -n 数值进行改变[改变以后需要注销用户让他生效]

Linux中soft nproc 、soft nofile和hard nproc以及hard nofile配置-CSDN博客

事件模型

EPOLL事件有两种模型:

Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。

Level Triggered (LT) 水平触发只要有数据都会触发(缓冲区里面的数据没有读取完就会触发)。

默认使用的时候水平触发

思考如下步骤:

  1. 假定我们已经把一个用来从管道中读取数据的文件描述符(rfd)添加到epoll描述符。
  2. 管道的另一端写入了2KB的数据
  3. 调用epoll_wait,并且它会返回rfd,说明它已经准备好读取操作
  4. 读取1KB的数据
  5. 调用epoll_wait……

在这个过程中,有两种工作模式:

ET模式

ET模式即Edge Triggered工作模式。

如果我们在第1步将rfd添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

  1. 基于非阻塞文件句柄
  2. 只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

event.events = EPOLLIN | EPOLLET;//监听的时候使用ET模式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/epoll.h>
#include <unistd.h>

int main(void){
	int efd, i;
	int pfd[2];//管道使用的文件描述符
	pid_t pid;
	char buf[10];
	char ch = 'a';

	pipe(pfd);//创建一个管道
	pid = fork();

	if(pid == 0){
		//子进程
		//子进程关闭读端
		close(pfd[0]);
		while(1){
			//子进程写数据到管道
			for(i = 0 ;i < 5 ;i++)
				buf[i] = ch;
			buf[i-1] = '\n';
			ch++;
			for(;i<10;i++)
				buf[i] = ch;
			buf[i-1] = '\n';
			if(ch > 'z')
				ch = 'a';
			write(pfd[1], buf, sizeof(buf));
			sleep(5);
		}
		close(pfd[1]);
	}else if(pid > 0){
		//父进程
		//父进程关闭写端
		close(pfd[1]);
		struct epoll_event event;
		struct epoll_event revents[10];
		int ret, len;

		efd = epoll_create(1);
		event.events = EPOLLIN | EPOLLET;//监听的时候使用ET模式
		event.data.fd = pfd[0];
		epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
		
		while(1){
			ret = epoll_wait(efd, revents, 10, -1);
			if(ret > 0){
				for(i = 0; i < ret; i++){
					if(revents[i].data.fd == pfd[0]){
						len = read(pfd[0], buf, sizeof(buf)/2);//这一个读取的时候只会读取一半
						write(STDOUT_FILENO, buf, len);
					}
				}
			}
		}
		close(pfd[0]);
		close(efd);
	}else{
		perror("fork");
		exit(1);
	}
	return 0;
}

使用这一个模式的时候, 虽然还有数据, 但是还是会阻塞, 每5秒打印一行数据

LT模式

LT模式即Level Triggered工作模式。

与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。

比较

LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

如果读取数据的时候不需要读取所有的数据, 其余的数据可以丢弃的时候, 使用这一个模式会导致问题

ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).

如果在epoll的ET模式下使用阻塞方式进行操作,可能会导致以下问题:

  1. 事件堆积:阻塞方式读取数据时,如果应用程序没有处理完当前的事件,而新的事件已经到达,这些事件会被内核忽略,从而导致事件堆积。这样会造成较差的性能,因为应用程序无法及时处理新到达的事件。
  2. 资源浪费:阻塞模式下,当没有数据可读时,应用程序会一直阻塞在读取操作上,浪费了CPU资源。在应用程序一直阻塞等待数据可读的过程中,无法进行其他任务的处理,造成资源的浪费。
  3. 接收数据不及时:由于阻塞模式下需要等待数据到达才能读取,会导致延迟增加。对于实时性要求较高的应用程序,阻塞模式可能无法满足要求。

这一个模式一般用于对速率要求比较高的地方, 这个时候最好不要阻塞, 读取的时候需要把所有的数据读取完, 否则下一次的连接会延迟

LT(Level-Triggered)和ET(Edge-Triggered)是两种不同的触发方式,它们支持的模式不同的原因主要有以下几点:

  1. 触发时机:LT模式在文件描述符还有未处理的数据时会持续触发,即文件描述符可读或可写时都会触发。而ET模式只在文件描述符状态发生变化时触发一次,即文件描述符从未就绪到就绪时触发。
  2. 处理效率:ET模式相较于LT模式可以提高处理效率。因为ET模式只在状态发生变化时才触发,应用程序需要立即对变化的事件进行处理,避免错过任何已就绪的事件。而LT模式在每次循环中都会检查已就绪的文件描述符,即使应用程序在某一次循环中没有对文件描述符进行操作。
  3. 应用场景:LT模式适用于对实时性要求不高的场景,例如网络编程中的服务器监听请求。而ET模式适用于对实时性要求较高的场景,例如实时数据处理或高并发服务器。

[(1 封私信) 如何理解Epoll中的LT和ET模式,底层实现又是怎么样的? - 知乎 (zhihu.com)](https://www.zhihu.com/question/403893498#:~:text=而 et,模式呢,数据从内核拷贝到用户空间后,内核不会重新将就绪事件节点添加回就绪队列,当事件在用户空间处理完后,用户空间根据需要重新将这个事件通过 epoll_ctl 添加回就绪队列(又或者这个节点因为有新的数据到来,重新触发了就绪事件而被添加)。)

(1 封私信) epoll中ET/LT触发模式分别适用的场景是什么? - 知乎 (zhihu.com)

反应堆模型

实际是使用epoll的ET模式加非阻塞加返回联合体里面的void *ptr

可以使用这一个结构体里面加一个fd和一个回调函数

使用这一个模型的时候处理的方式会发生改变

  • 普通模型

socket, bind, listen – epoll_creat 创建一个红黑树 – 返回 epfd – epoll_ctl 在树上加一个监听节点 – while(1) – epoll_wait 监听 – 监听事件发生 – 返回数组 – 判断返回元素 – lfd – accept / cfd – read() – 大小写转换 – write

  • 反应堆模型

socket, bind, listen – epoll_creat 创建一个红黑树 – 返回 epfd – epoll_ctl 在树上加一个监听节点 – while(1) – epoll_wait 监听 – 监听事件发生 – 返回数组 – 判断返回元素 – lfd – accept / cfd – 调用读回调函数 – epoll_ctl把这一个节点取下来 – 改为监听写事件 – epoll_ctl把这一个节点加回去 – epoll_wait等待可写 – 写回调函数 – 把这一个节点取下来 – 改为读事件 – 加回去

这一个加了一下判断是不是可以写, 是的话再发送数据

实际的操作改为使用回调函数

struct myevent_s {
	int fd;				//记录文件描述符
    int events;			//记录当前监听的事件
    void *arg;			//一个数据
    void (*call_back)(int fd, int events, void *arg);		//回调函数
    int status;						//记录在不在红黑树里面
    char buf[BUFLEN];
    int len;						
    long last_active;				//记录最后一次的连接时间, 长时间连接不断开踢出去  
};

高并发服务器epoll接口、epoll Reactor(反应堆)模型详解 - 知乎 (zhihu.com)

[epoll原理详解及epoll反应堆模型-CSDN博客](https://zhuanlan.zhihu.com/p/161073519)

优缺点

  • 优点

高效, 使用简单, 不受文件描述符的个数的限制

  • 缺点

不能跨平台, Linux专有

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1586277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《看漫画学C++》第12章 可大可小的“容器”——向量

在C编程的世界里&#xff0c;数组是一种基础且广泛使用的数据结构。然而&#xff0c;传统的静态数组在大小固定、管理不便等方面的局限性&#xff0c;常常让开发者感到束手束脚。幸运的是&#xff0c;C标准库中的vector类为我们提供了一种更加灵活、高效的动态数组解决方案。 …

Day96:云上攻防-云原生篇Docker安全系统内核版本漏洞CDK自动利用容器逃逸

目录 云原生-Docker安全-容器逃逸&系统内核漏洞 云原生-Docker安全-容器逃逸&docker版本漏洞 CVE-2019-5736 runC容器逃逸(需要管理员配合触发) CVE-2020-15257 containerd逃逸(启动容器时有前提参数) 云原生-Docker安全-容器逃逸&CDK自动化 知识点&#xff1…

2017NOIP普及组真题 4. 跳房子

线上OJ&#xff1a; 一本通&#xff1a;http://ybt.ssoier.cn:8088/problem_show.php?pid1417\ 核心思想 首先、本题中提到 “ 至少 要花多少金币改造机器人&#xff0c;能获得 至少 k分 ”。看到这样的话语&#xff0c;基本可以考虑要使用 二分答案。 那么&#xff0c;本题中…

缓存穿透、缓存雪崩、缓存击穿的区别

缓存三兄弟&#xff08;穿透、雪崩、击穿&#xff09; 缓存穿透 定义 查询一个redis和数据库都不存在的值&#xff0c;数据库不存在则不会将数据写入缓存&#xff0c;所以这些请求每次都达到数据库。 解决办法是缓存空对象&#xff0c;如果数据库不存在则将空对象存入缓存&…

政安晨:【深度学习神经网络基础】(三)—— 激活函数

目录 线性激活函数 阶跃激活函数 S型激活函数 双曲正切激活函数 修正线性单元 Softmax激活函数 偏置扮演什么角色&#xff1f; 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 政安晨的机器学习笔记 希望政安晨的博客能够对您有所裨…

[RK3399 Linux] 移植Linux 5.2.8内核详解

背景是在RK3399上面移植Rockchip官方提供的u-boot 2017.09 一、linux内核 1.1 源码下载 内核源码下载地址为:《https://www.kernel.org/》: 也可以到内核镜像网址下载https://mirrors.edge.kernel.org/pub/linux/kernel/,这里下载速度更快。 如果下载速度太慢,无法下载,…

理解 编译和链接

目录 1. 翻译环境和运行环境 2. 翻译环境 2.1 预处理&#xff08;预编译&#xff09; 2.2 编译 2.2.1 词法分析&#xff1a; 2.2.2 语法分析 2.2.3 语义分析 2.3 汇编 2.4 链接 3. 运行环境 1. 翻译环境和运行环境 在ANSI C的任何一种实现中&#xff0c;存在两个不同…

git修改本地提交历史邮箱地址

1、Git&#xff08;Git&#xff09; 2、修改Git本地提交历史中的邮箱地址 使用 git rebase 命令进行交互式重置。 具体步骤如下&#xff1a;&#xff08;https://git-scm.com/docs/git-rebase&#xff09; 1、查看提交历史&#xff1a; 使用 git log 命令列出提交历史&#x…

完整的项目源码!在线考试完整系统源码(可接私活)

最近有一些读者问我有没有完整的基于SpringbootVue的项目源码&#xff0c;今天给大家整理了一下&#xff0c;并且录制了搭建的教程&#xff0c;无偿分享给大家。 一、系统运行图 1、登陆页面 2、后台管理 3、全套环境资源 ​源码文件部分截图&#xff0c;带视频教程 ​ 在实际…

WPS的JS宏如何批量实现文字的超链接

表格中需要对文字进行超链接&#xff0c;每个链接指引到不同的地址。例如&#xff1a; 实现如下表格中&#xff0c;文件名称超级链接到对应的文件路径上&#xff0c;点击对应的文件名称&#xff0c;即可打开对应的文件。 序号文件名称文件路径1变更申请与处理表.xls文档\系统…

java八股——消息队列MQ

上一篇传送门&#xff1a;点我 目前只学习了RabbitMQ&#xff0c;后续学习了其他MQ后会继续补充。 MQ有了解过吗&#xff1f;说说什么是MQ&#xff1f; MQ是Message Queue的缩写&#xff0c;也就是消息队列的意思。它是一种应用程序对应用程序的通信方法&#xff0c;使得应用…

多线程回答的滚瓜烂熟,面试官问我虚线程了解吗?我说不太了解!

Java虚拟线程&#xff08;Virtual Threads&#xff09;标志着Java在并发编程领域的一次重大飞跃&#xff0c;特别是从Java 21版本开始。这项新技术的引入旨在克服传统多线程和线程池存在的挑战。 多线程和线程池 在Java中&#xff0c;传统的多线程编程依赖于Thread类或实现Ru…

Python(1):认识Python并且了解一些简单函数

文章目录 一、Python的优势及其使用场景二、Python环境的安装三、Python中的变量及其命名四、Python中的注释五、一些简单常见的函数和认识ASCII表六、Python导入模块的方式 一、Python的优势及其使用场景 优点&#xff1a; 开发效率高&#xff1a;Python具有非常强大的第三方…

Java——数组练习

目录 一.数组转字符串 二.数组拷贝 三.求数组中元素的平均值 四.查找数组中指定元素(顺序查找) 五.查找数组中指定元素(二分查找) 六.数组排序(冒泡排序) 七.数组逆序 一.数组转字符串 代码示例&#xff1a; import java.util.Arrays int[] arr {1,2,3,4,5,6}; String…

220 基于matlab的考虑直齿轮热弹耦合的动力学分析

基于matlab的考虑直齿轮热弹耦合的动力学分析&#xff0c;输入主动轮、从动轮各类参数&#xff0c;考虑润滑油温度、润滑油粘度系数等参数&#xff0c;输出接触压力、接触点速度、摩擦系数、对流传热系数等结果。程序已调通&#xff0c;可直接运行。 220直齿轮热弹耦合 接触压力…

出游旅行,不能错过的华为nova 12 Ultra4个AI小技巧

随着AI功能的快速普及&#xff0c;让我们的日常生活和工作借助这些工具变得越来越高效。今天就分享4个超级实用的华为nova 12 Ultra自带的AI小技巧&#xff1a;小艺智能魔法抠图、AI消除、图库搜索、小艺帮写&#xff0c;看看有哪些还是你不知道的&#xff1f; 1. 小艺智能…

Windows:Redis数据库图形化中文工具软件——RESP(3)

这个是用于连接redis数据库的软件工具&#xff0c;安装在windows上的图形化界面&#xff0c;并且支持中文&#xff0c;是在github上的一个项目 1.获取安装包 发布 lework/RedisDesktopManager-Windows (github.com)https://github.com/lework/RedisDesktopManager-Windows/rel…

紧急 CCF-C ICPR 2024摘要投稿日期延期至4月10日 速投速成就科研梦

会议之眼 快讯 第27届ICPR&#xff08;The International Conference on Pattern Recognition&#xff09;即国际模式识别会议将于 2024年 12月1日-5日在印度加尔各答的比斯瓦孟加拉会议中心举行&#xff01;ICPR是国际模式识别协会的旗舰会议&#xff0c;也是模式识别、计算机…

OpenHarmony南向开发案例:【智能保险柜】

样例简介 智能保险柜实时监测保险柜中振动传感器&#xff0c;当有振动产生时及时向用户发出警报。在连接网络后&#xff0c;配合数字管家应用&#xff0c;用户可以远程接收智能保险柜的报警信息。后续可扩展摄像头等设备&#xff0c;实现对危险及时报警&#xff0c;及时处理&a…

LeetCode-2529题:正整数和负整数的最大计数(原创)

【题目描述】 给你一个按 非递减顺序 排列的数组 nums &#xff0c;返回正整数数目和负整数数目中的最大值。换句话讲&#xff0c;如果 nums 中正整数的数目是 pos &#xff0c;而负整数的数目是 neg &#xff0c;返回 pos 和 neg二者中的最大值。注意&#xff1a;0 既不是正整…