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.mzph.cn/news/808764.shtml

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

相关文章

AI电商图制作解决方案助力企业高效营销

电商行业蓬勃发展&#xff0c;一张吸睛的电商海报或宣传视频往往能够成为企业吸引顾客、提升品牌形象的利器。然而&#xff0c;传统电商图制作流程繁琐&#xff0c;需要投入大量时间和人力资源&#xff0c;成为众多企业面临的难题。为了解决这一问题&#xff0c;美摄科技凭借其…

前端学习之路-项目实战(1)

每日吐槽&#xff1a;有一个奇怪的问题&#xff0c;怎么一眼看出一个求职者是否是培训班出来的&#xff0c;有的求职上写着&#xff0c;希望大家坦诚一点&#xff0c;but&#xff0c;你这艘诚实的泰坦尼克号终究还是撞上了社会阴暗面的冰山&#xff0c;OMG&#xff0c;不让包装…

Leetcode 239. 滑动窗口最大值和Leetcode 347. 前 K 个高频元素

目录标题 Leetcode 239. 滑动窗口最大值题目描述C语言代码和题解解题思路 Leetcode 347. 前 K 个高频元素题目描述C语言题解和思路解题思路 Leetcode 239. 滑动窗口最大值 题目描述 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最…

Tensorflow(GPU版本配置)一步到位!!!

Tensorflow&#xff08;GPU版本配置&#xff09;一步到位&#xff01;&#xff01;&#xff01; CUDA安装CUDA配置Tensorflow配置常见的包 CUDA安装 配置了N次的Tensorflow–Gpu版本&#xff0c;完成了踩坑&#xff0c;这里以配置Tensorflow_gpu 2.6.0为例子进行安装 以下为ten…

数学之光照亮AI之路:探究数学背景在人工智能学习中的优势

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已成为引领未来发展的重要力量。然而&#xff0c;对于许多初涉此领域的学习者来说&#xff0c;AI的复杂性和深度常常让他们望而却步。有趣的是&#xff0c;那些数学基础扎实的人在学习AI时&#xff0c;往往…

【canvas】canvas基础使用(六):图形阴影

简言 学习使用canvas的阴影属性。 阴影 shadowBlur 阴影模糊 CanvasRenderingContext2D.shadowBlur 是 Canvas 2D API 描述模糊效果程度的属性&#xff1b;它既不对应像素值也不受当前转换矩阵的影响。默认值是 0。 语法&#xff1a; ctx.shadowBlur level; 选项值&#x…

Python中的错误处理 - 使用try、except、else和finally进行解释,并附带代码示例

最近&#xff0c;我的经理委派我创建一个自动报告。我设计的报告非常简单。它包括一些来自数据库的数字和一些基本的数学运算。我很兴奋最终可以向公司展示我的惊人的Python技能。 我完成并交付了产品。一切都很顺利。至少&#xff0c;直到大约两周后。我的报告由于除以零错误…

Linux使用C语言实现Socket编程

Socket编程 这一个课程的笔记 相关文章 协议 Socket编程 高并发服务器实现 线程池 网络套接字 socket: &#xff08;电源&#xff09;插座&#xff08;电器上的&#xff09;插口&#xff0c;插孔&#xff0c;管座 在通信过程中, 套接字是成对存在的, 一个客户端的套接字, 一个…

PUBG绝地求生更新后无法启动/更新后卡顿?3个解决方法分享

在绝地求生中&#xff0c;团队协作也是非常重要的一点。咱们可以运用strong标签来着重“团队协作”。与队友紧密配合、彼此援助&#xff0c;一起拟定战术和战略&#xff0c;将会大大提高在游戏中获得成功的几率。在绝地求生中获得成功并不简单&#xff0c;但只需咱们把握了一些…

linux的io的知识大全

C语言的io操作 写文件 #include<stdio.h> #include<string.h>#define FILE_NAME "log.txt" int main() {FILE * fp fopen(FILE_NAME, "w");if(fpNULL){printf("fopen error!\n");}const char* msg "hello zk\n";int c…

北邮通报学生联名举报导师事件,导师PUA学生,学生只有压榨它

诸葛亮为激司马懿出战&#xff0c;派使者送给他女人的衣服&#xff0c;司马懿欣然接受后问使者&#xff1a;“你们家丞相都管些什么呀&#xff1f;” 使者回答道&#xff1a;“我们丞相从行军布阵到饮食起居&#xff0c;无一不亲自过问。” 司马懿听到心中大喜&#xff0c;心想…

【ARM Coresight SOC-600 -- ETF Flushin无法接收到 CTI 发出 triggerout 信号问题分析】

请阅读【嵌入式开发必备专栏 】 文章目录 问题背景波形分析问题背景 在做验证的时候,准备通过 CTI2 给 SOC 上的 ETF 触发一个 flushin 动作,然后stop住 formatter,结果一致发现没有成功,接下来就是分析的过程了。 首先检查了代码,没有发现代码有什么问题(一般自己写的代…

学习大数据,所需要的java(Maven)基础(1)

文章目录 使用Maven的优势第三方jar包添加第三方jar包获取jar包之间的依赖关系jar包之间的冲突处理将项目拆分成多个工程模块 实现项目的分布式部署Maven是什么自动化构建工具构建的概念构建环节自动化构建 Maven如何使用安装Maven核心程序maven联网问题Maven中的settings配置在…

Linux下批量的批量操作

批量删除docker 镜像 docker images | grep ent-form-web |awk ‘{print $3}’ | xargs docker rmi docker images: 列出所有的docker 镜像 docker images | grep ent-form-web : 选取出结果带 ent-form-web的信息 docker images | grep ent-form-web |awk ‘{print $3}’ 选取…

为什么在学校很难真正学好嵌入式?

10几年前,我是读电气工程专业,学了很多东西,结构,电机、绘图,plc等等.. 其实,都没什么鸟用,出来还是像个废物。 后面我自学转了单片机开发,说句难听点,自己买个开发板都比在学校学得深。 可能是这个专业的问题,主攻不是嵌入式方向,老师用汇编点个流水灯,这门课就…

CMake学习笔记(三)区分macro与function

目录 共同点--形式类似 macro的形式 function的形式 不同点 1 输入参数的替换阶段不同 macro function 2 输入参数作用域不同 macro function 共同点--形式类似 macro的形式 macro(宏名 输入参数名).... endmacro() function的形式 function(函数名 输入参数名).…

20232831 2023-2024-2 《网络攻防实践》第5次作业

目录 20232831 2023-2024-2 《网络攻防实践》第5次作业1.实验内容&#xff08;1&#xff09;防火墙配置&#xff08;具体IP配置参考自己的IP设置&#xff09;&#xff08;2&#xff09;动手实践&#xff1a;Snort&#xff08;3&#xff09;分析配置规则 2.实验过程3.学习中遇到…

基于小程序实现的校园失物招领系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

第十二天--二维数组的彻底解刨--地址

1.二维数组我们用父子的地址来称呼二维数组的地址 比如arr[3][4] 这里的arr是二维数组的首地址&#xff0c;也是父数组的首地址&#xff0c;也是子数组的首地址 arr1父数组的地址偏移1&#xff0c;实际上是偏移了4*416个字节 arr[0]是子数组的首地址&#xff0c;arr[0]1是子数…

langchain txt 文档加载,分割

stuff 策略 加载 arXiv 论文&#xff0c;让模型总结前 2000 字 这里采用的是 stuff 策略&#xff0c;也就是将一大段文本。按字数分割成 N 个文本块&#xff0c;又合并成一个大的文本块。 对超大规模不友好&#xff0c;没有区分文档重要性&#xff0c;适合文档量较少场景 i…