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

高并发服务器

这一个课程的笔记
相关文章
协议
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;	//对应监听事件的fduint32_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错误, 最好判断一下errnoperror("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.rhkb.cn/news/305186.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker从入门到熟悉

一、什么是docker&#xff1f; Docker是一个用于开发&#xff0c;交付和运行应用程序的开放平台。Docker使您能够将应用程序与基础架构分开&#xff0c;从而可以快速交付软件。借助Docker&#xff0c;您可以以与管理应用程序相同的方式来管理基础架构。通过利用Docker的快速交付…

Java——数组练习

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

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

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

超越ChatGPT,国内快速访问的强大 AI 工具 Claude

claude 3 opus面世后&#xff0c;网上盛传吊打了GPT-4。网上这几天也已经有了许多应用&#xff0c;但竟然还有很多小伙伴不知道国内怎么用gpt&#xff0c;也不知道怎么去用这个据说已经吊打了gpt-4的claude3。 今天我们想要进行的一项尝试就是—— 用claude3和gpt4&#xff0c…

GeoServer:忘记密码重置

操作步骤 1. 找到data_dir/security/usergroup/default目录下的users.xml文件&#xff0c; 2.修改password为plain:geoserver&#xff0c; 这里无论原来的密码是什么&#xff0c;改为plain:geoserver之后&#xff0c;就可以通过admin&#xff1a;geoserver默认账户密码登录了。…

[计算机效率] 鼠标手势工具:WGestures(解放键盘的超级效率工具)

3.22 鼠标手势工具&#xff1a;WGestures 通过设置各种鼠标手势和操作进行绑定。当用户通过鼠标绘制出特定的鼠标手势后就会触发已经设置好的操作。有点像浏览器中的鼠标手势&#xff0c;通过鼠标手势操纵浏览器做一些特定的动作。这是一款强大的鼠标手势工具&#xff0c;可以…

2024年能源环境、材料科学与人工智能国际会议(ICEEMSAI2024)

2024年能源环境、材料科学与人工智能国际会议(ICEEMSAI2024) 会议简介 2024国际能源环境、材料科学和人工智能大会&#xff08;ICEEMSAI 2024&#xff09;主要围绕能源环境、物质科学和人工智慧等研究领域&#xff0c;旨在吸引能源环境、先进材料和人工智能专家学者、科技人员…

C++入门语法(命名空间缺省函数函数重载引用内联函数nullptr)

目录 前言 1. 什么是C 2. C关键字 3. 命名空间 3.1 命名空间的定义 3.2 命名空间的使用 4. C输入和输出 5. 缺省函数 5.1 概念 5.2 缺省参数分类 6. 函数重载 6.1 概念 6.2 为何C支持函数重载 7. 引用 7.1 概念 7.2 特性 7.3 常引用 7.4 引用与指针的区别 7…

最近一些前端面试问题整理

最近一些前端面试问题整理 4月8号1. TS 中的 类型别名 和接口的区别是什么&#xff1f;2. 什么是深拷贝和浅拷贝&#xff1f;深浅拷贝的方法有哪些&#xff1f;浅拷贝&#xff08;Shallow Copy&#xff09;深拷贝&#xff08;Deep Copy&#xff09;区别总结 3. 使用 JSON.strin…

Python中Python-docx 包的run介绍

先对run做一个简单地介绍。每个paragraph对象都包含一个run对象的列表。举例&#xff1a; 这是一个简短的段落。 from docx import Document doc Document("1.docx") #上面这段话保存在1.docx中 print("这一段的run个数是&#xff1a;",len(doc.paragr…

GitHub repository - Code - Issues - Pull Requests - Wiki

GitHub repository - Code - Issues - Pull Requests - Wiki 1. Code2. Issues3. Pull Requests4. WikiReferences 1. Code 显示该仓库中的文件列表。仓库名下方是该仓库的简单说明和 URL. 2. Issues 用于 BUG 报告、功能添加、方向性讨论等&#xff0c;将这些以 Issue 形式进…

ESXI 中安装 虚拟机 麒麟v10 操作系统

浏览器访问登录ESXI 上传镜像文件 创建新虚拟机 选择虚拟机名称和操作系统 选择存储 配置虚拟机 配置虚拟机 cpu 内存 硬盘 并选择虚拟机驱动 配置完成后&#xff0c;点击下一步&#xff0c;并点击完成。 开机 完成后选择该虚拟机&#xff0c;并打开电源 等待出现以下界面…

[Linux_IMX6ULL驱动开发]-驱动的分层及实现

目录 驱动分层的思路 驱动分层的实现 上层驱动的实现 次设备号的使用 上层驱动代码 底层驱动的实现 底层驱动c文件的实现 底层驱动头文件实现 应用层文件的实现 驱动分层的思路 在上一篇文章中&#xff0c;博主实现了通过寄存器控制引脚&#xff0c;以此来达到控制LE…

代码随想录-KMP算法

LeetCode28: . - 力扣&#xff08;LeetCode&#xff09; public class KMP {public static void main(String[] args) {KMP kmpnew KMP();kmp.strStr("aabaabaaf","aabaaf");}public int strStr(String haystack, String needle) {//获取next数组int[] nex…

ENVI实战—一文搞定NDVI计算和MNDWI计算

实验1&#xff1a;使用波段计算器计算波段值 目的&#xff1a;熟练掌握ENVI中波段计算器的使用方法&#xff0c;学会波段之间的运算。 过程&#xff1a; ①数据导入&#xff1a;打开ENVI5.6&#xff0c;在“文件”选项卡中选择“打开”&#xff0c;打开此前裁剪好的Sentinel…

外包干了3天,技术退步明显.......

先说一下自己的情况&#xff0c;大专生&#xff0c;19年通过校招进入杭州某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了四年的功能测…

✔ ★Java项目——设计一个消息队列(二)

Java项目——设计一个消息队列 四. 项⽬创建五. 创建核⼼类创建 Exchange&#xff08;名字、类型、持久化&#xff09;创建 MSGQueue&#xff08;名字、持久化、独占标识&#xff09;创建 Binding&#xff08;交换机名字、队列名字、bindingKey用于与routingKey匹配&#xff09…

【经典算法】LeetCode25:K 个一组翻转链表(Java/C/Python3,Hard)

#算法 目录 题目描述思路及实现方式一&#xff1a;递归思路代码实现Java 版本C 语言版本Python3 版本 复杂度分析 方式二&#xff1a;迭代和原地反转思路代码实现Java 版本C 语言版本Python3 版本 复杂度分析 总结相似题目 标签&#xff1a;链表、递归 题目描述 给你链表的头…

代码随想录阅读笔记-二叉树【总结】

二叉树的理论基础 代码随想录 (programmercarl.com)&#xff1a;二叉树的种类、存储方式、遍历方式、定义方式 二叉树的遍历方式 深度优先遍历 代码随想录阅读笔记-二叉树【递归遍历】-CSDN博客&#xff1a;递归三部曲初次亮相代码随想录阅读笔记-二叉树【迭代遍历】-CSDN博…

2024年3月文章一览

2024年3月编程人总共更新了12篇文章&#xff1a; 1.2024年2月文章一览 2.Programming Abstractions in C阅读笔记&#xff1a;p308-p311 3.Programming Abstractions in C阅读笔记&#xff1a;p312-p326 4.Programming Abstractions in C阅读笔记&#xff1a;p327-p330 5.…