【Linux高级IO】select、poll、epoll

【Linux高级IO】select、poll、epoll

toc

作者:爱写代码的刚子

时间:2024.6.5

前言:本篇博客将会介绍面试重点考察的select、poll、epoll

IO: input && Output

read && write

  1. 应用层read&&write的时候,本质把数据从用户层写给OS —— 本质就是拷贝函数
  2. IO = 等 + 拷贝(要进行拷贝,必须先判断条件成立,即等读写事件就绪)

什么叫做高效IO?单位时间内,IO过程中,等的比重越小,IO效率越高!几乎所有的提高IO效率的策略,本质就是这个!

同步IO:参与了等或者参与了拷贝就是同步IO,信号驱动IO也是同步IO因为它参与了IO

异步IO:不参与IO,只是发起IO,最后拿结果。

五种IO模型

  • 阻塞IO:在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式

阻塞IO是最常见的IO模型.

在这里插入图片描述

  • 非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码

非阻塞效率高指的是他在轮询的时候可以做其他事情,非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询.这对CPU来说是较大的浪费,一般只有特定场景下才使用。

在这里插入图片描述

  • 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作.

在这里插入图片描述

  • IO多路转接:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

在这里插入图片描述

  • 异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

在这里插入图片描述

小结

  • 任何IO过程中,都包含两个步骤.第一是等待,第二是拷贝.而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间.让IO更高效,最核心的办法就是让等待的时间减少

高级IO重要概念

同步通信VS异步通信

同步和异步关注的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步 过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

这里的同步通信和进程之间的同步是完全不相干的概念.

  • 进程/线程同步也是进程/线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系. 尤其是在访问临界资源的时候.

阻塞VS非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

其他高级IO

非阻塞IO,记录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.

非阻塞IO

fcntl

一个文件描述符,默认都是阻塞IO

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
  • 通过这个接口可以设置文件描述符属性为非阻塞

传入的cmd的值不同,后面追加的参数也不相同。

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

实现函数SetNoBlock

基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞.

void SetNoBlock(int fd) {int fl = fcntl(fd, F_GETFL);if (fl < 0) {perror("fcntl");return; }fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

实验:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
#include <cerrno>
#include <cstring>using namespace std;void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL);if (fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);cout << " set " << fd << " nonblock done" << endl;
}int main()
{char buffer[1024];SetNonBlock(0);sleep(1);while (true){// printf("Please Enter# ");// fflush(stdout);ssize_t n = read(0, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n - 1] = 0;cout << "echo : " << buffer << endl;}else if (n == 0){cout << "read done" << endl;break;}else{// 1. 设置成为非阻塞,如果底层fd数据没有就绪,recv/read/write/send, 返回值会以出错的形式返回// 2. a. 真的出错 b. 底层没有就绪// 3. 我怎么区分呢?通过errno区分!!!if (errno == EWOULDBLOCK){cout << "0 fd data not ready, try again!" << endl;// do_other_thing();sleep(1);}else{cerr << "read error, n = " << n << "errno code: "<< errno << ", error str: " << strerror(errno) << endl;}}}return 0;
}
  • 如果出错,errno也会被设置

在这里插入图片描述

在这里插入图片描述

  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
  • 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

I/O多路转接之select

初识select

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select函数原型

select的函数原型如下: #include <sys/select.h>

int select(int nfds, fd_set *restrict readfds,fd_set *restrict writefds, fd_set *restrict exceptfds,struct timeval *restrict timeout);

参数解释:

  • nfds = maxfd +1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间

返回值

n > 0:有n个fd就绪了

n==0:超时,没有错误,但是也没有fd就绪

n<0:等待出错了

获取时间的接口

在这里插入图片描述

其中struct timeval结构体

在这里插入图片描述

参数timeout取值:

  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

如果设置了timeout参数,就是输入输出型参数

关于fd_set结构(位图):

在这里插入图片描述

比特位的位置表示文件描述符编号(从右向左:0,1,2,3…),

比特位的内容0或者是1,表示是否需要内核关心相应的事件。

参数作为返回值返回时:比特位的内容0或者是1,表示上面相应的事件是否就绪

fd_set是一张位图,让用户<->内核传递fd是否就绪的信息的!

*fd_set readfds:fd_set内核提供的一种数据类型,它是位图

  • 输入时:用户告诉内核,我给你的一个或者多个fd,内核要关心fd上的读事件,读事件就绪了,要通知用户。

  • 输出时:内核告诉用户,用户让内核关心的多个fd中,有哪些已经就绪了,让用户来读取。

一组操作fd_set的接口,用来方便操作位图.

void FD_CLR(int fd, fd_set *set);// 用来清除描述词组set中相关fd 的位
int  FD_ISSET(int fd, fd_set *set);// 用来测试描述词组set中相关fd 的位是否为真 
void FD_SET(int fd, fd_set *set);// 用来设置描述词组set中相关fd 的位
void FD_ZERO(fd_set *set);// 用来清除描述词组set 的全部位

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。

函数返回值:

  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的 值变成不可预测。

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

常见的程序片段如下:

fs_set readset;
FD_SET(fd,&readset); 
select(fd+1,&readset,NULL,NULL,NULL); 
if(FD_ISSET(fd,readset)){......}

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.

  • 执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
  • 若fd=5,执行FD_SET(fd,&set); 后set变为0001,0000(第5位置为1)
  • 若再加入fd=2,fd=1,则set变为0001,0011
  • 执行 select(6,&set,0,0,0)阻塞等待
  • 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。注意:没有事件发生的fd=5被清空。

编写select服务器

Main.cc

#include "SelectServer.hpp"
#include <memory>int main()
{// std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;std::unique_ptr<SelectServer> svr(new SelectServer());svr->Init();svr->Start();return 0;
}

SelectServer.hpp

#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"using namespace std;static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;class SelectServer
{
public:SelectServer(uint16_t port = defaultport) : _port(port){for (int i = 0; i < fd_num_max; i++){fd_array[i] = defaultfd;// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;}}bool Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();return true;}void Accepter(){// 我们的连接事件就绪了std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会if (sock < 0) return;lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);// sock -> fd_array[]int pos = 1;for (; pos < fd_num_max; pos++) // 第二个循环{if (fd_array[pos] != defaultfd)continue;elsebreak;}if (pos == fd_num_max){lg(Warning, "server is full, close %d now!", sock);close(sock);}else{fd_array[pos] = sock;PrintFd();// TODO}}void Recver(int fd, int pos){// demochar buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;cout << "get a messge: " << buffer << endl;}else if (n == 0){lg(Info, "client quit, me too, close fd is : %d", fd);close(fd);fd_array[pos] = defaultfd; // 这里本质是从select中移除}else{lg(Warning, "recv error: fd is : %d", fd);close(fd);fd_array[pos] = defaultfd; // 这里本质是从select中移除}}void Dispatcher(fd_set &rfds){for (int i = 0; i < fd_num_max; i++) // 这是第三个循环{int fd = fd_array[i];if (fd == defaultfd)continue;if (FD_ISSET(fd, &rfds)){if (fd == _listensock.Fd()){Accepter(); // 连接管理器}else // non listenfd{Recver(fd, i);}}}}void Start(){int listensock = _listensock.Fd();fd_array[0] = listensock;for (;;){fd_set rfds;FD_ZERO(&rfds);int maxfd = fd_array[0];for (int i = 0; i < fd_num_max; i++) // 第一次循环{if (fd_array[i] == defaultfd)continue;FD_SET(fd_array[i], &rfds);if (maxfd < fd_array[i]){maxfd = fd_array[i];lg(Info, "max fd update, max fd is: %d", maxfd);}}// accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪// struct timeval timeout = {1, 0}; // 输入输出型参数,要进行周期的重复设置struct timeval timeout = {0, 0}; // 输入输出,可能要进行周期的重复设置// 如果事件就绪,上层不处理,select会一直通知!// select告诉用户就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞// rfds: 输入输出型参数。 1111 1111 -> 0000 0000int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);switch (n){case 0:cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;break;case -1:cerr << "select error" << endl;break;default:// 有事件就绪了,TODOcout << "get a new link!!!!!" << endl;Dispatcher(rfds); // 就绪的事件和fdbreak;}}}void PrintFd(){cout << "online fd list: ";for (int i = 0; i < fd_num_max; i++){if (fd_array[i] == defaultfd)continue;cout << fd_array[i] << " ";}cout << endl;}~SelectServer(){_listensock.Close();}private:Sock _listensock;uint16_t _port;int fd_array[fd_num_max];   // 数组, 来用户维护// int wfd_array[fd_num_max];
};

Socket.hpp

#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"enum
{SocketErr = 2,BindErr,ListenErr,
};// TODO
const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}public:void Socket(){sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0){lg(Fatal, "socker error, %s: %d", strerror(errno), errno);exit(SocketErr);}int opt = 1;setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}void Bind(uint16_t port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen(){if (listen(sockfd_, backlog) < 0){lg(Fatal, "listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);if(newfd < 0){lg(Warning, "accept error, %s: %d", strerror(errno), errno);return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));if(n == -1) {std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(sockfd_);}int Fd(){return sockfd_;}private:int sockfd_;
};

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件 描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE 信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

异常就绪

  • 有关TCP中的紧急指针

select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件 描述符,则我服务器上支持的最大文件描述符是512*8=4096.

  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,

    • 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得 fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注: fd_set的大小可以调整,可能涉及到重新编译内核.

select缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大(数据拷贝频率比较高)
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小.等待的fd是有上限的( fd_set 结构体位图就限制了select的上限)

I/O多路转接之poll

poll函数接口

#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd结构 
struct pollfd {int   fd;			/* file descriptor */short events;	/* requested events */short revents; /* returned events */
};

参数说明

  • fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返 回的事件集合.
  • nfds表示fds数组的长度.
  • timeout表示poll函数的超时时间, 单位是毫秒(ms).

events和revents的取值:

在这里插入图片描述

返回结果

  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时;
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回.

socket就绪条件

同select

poll的优点

不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现

  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。接口使用比select更方便。
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降).

poll的缺点

poll中监听的文件描述符数目增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效 率也会线性下降.

poll和select有一样的缺点:都需要遍历,在内核中需要检测哪些文件描述符就绪,在用户层检测哪些事件就绪

编写poll服务器

PollServer.hpp:

#pragma once#include <iostream>
#include <poll.h>
#include <sys/time.h>
#include "Socket.hpp"using namespace std;static const uint16_t defaultport = 8888;
static const int fd_num_max = 64;
int defaultfd = -1;
int non_event = 0;class PollServer
{
public:PollServer(uint16_t port = defaultport) : _port(port){for (int i = 0; i < fd_num_max; i++){_event_fds[i].fd = defaultfd;_event_fds[i].events = non_event;_event_fds[i].revents = non_event;// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;}}bool Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();return true;}void Accepter(){// 我们的连接事件就绪了std::string clientip;uint16_t clientport = 0;int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会if (sock < 0) return;lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);// sock -> fd_array[]int pos = 1;for (; pos < fd_num_max; pos++) // 第二个循环{if (_event_fds[pos].fd != defaultfd)continue;elsebreak;}if (pos == fd_num_max){lg(Warning, "server is full, close %d now!", sock);close(sock);// 扩容}else{// fd_array[pos] = sock;_event_fds[pos].fd = sock;_event_fds[pos].events = POLLIN;_event_fds[pos].revents = non_event;PrintFd();// TODO}}void Recver(int fd, int pos){// demochar buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;cout << "get a messge: " << buffer << endl;}else if (n == 0){lg(Info, "client quit, me too, close fd is : %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}else{lg(Warning, "recv error: fd is : %d", fd);close(fd);_event_fds[pos].fd = defaultfd; // 这里本质是从select中移除}}void Dispatcher(){for (int i = 0; i < fd_num_max; i++) // 这是第三个循环{int fd = _event_fds[i].fd;if (fd == defaultfd)continue;if (_event_fds[i].revents & POLLIN){if (fd == _listensock.Fd()){Accepter(); // 连接管理器}else // non listenfd{Recver(fd, i);}}}}void Start(){_event_fds[0].fd = _listensock.Fd();_event_fds[0].events = POLLIN;int timeout = 3000; // 3sfor (;;){int n = poll(_event_fds, fd_num_max, timeout);switch (n){case 0:cout << "time out... " << endl;break;case -1:cerr << "poll error" << endl;break;default:// 有事件就绪了,TODOcout << "get a new link!!!!!" << endl;Dispatcher(); break;}}}void PrintFd(){cout << "online fd list: ";for (int i = 0; i < fd_num_max; i++){if (_event_fds[i].fd == defaultfd)continue;cout << _event_fds[i].fd << " ";}cout << endl;}~PollServer(){_listensock.Close();}private:Sock _listensock;uint16_t _port;struct pollfd _event_fds[fd_num_max]; // 数组, 用户维护的!// struct pollfd *_event_fds;// int fd_array[fd_num_max];// int wfd_array[fd_num_max];
};

Main.cc:

#include "PollServer.hpp"
#include <memory>int main()
{// std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;// std::unique_ptr<SelectServer> svr(new SelectServer());std::unique_ptr<PollServer> svr(new PollServer());svr->Init();svr->Start();return 0;
}

I/O多路转接之epoll

epoll初识

按照man手册的说法: 是为处理大批量句柄而作了改进的poll. 它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44) 它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

epoll的相关系统调用

epoll的三个相关系统调用:

epoll_create

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

创建一个epoll的句柄

  • 自从linux2.6.8之后,size参数是被忽略的.
  • 用完之后, 必须调用close()关闭.

epoll_ctl(关心红黑树)

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

epoll的事件注册函数.

  • 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
  • 第一个参数是epoll_create()的返回值(epoll的句柄).
  • 第二个参数表示动作,用三个宏来表示.
  • 第三个参数是需要监听的fd.
  • 第四个参数是告诉内核需要监听什么事.

第二个参数的取值:

  • EPOLL_CTL_ADD :注册新的fd到epfd中;
  • EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL :从epfd中删除一个fd;

struct epoll_event结构如下:

在这里插入图片描述

events可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要 再次把这个socket加入到EPOLL队列里.

epoll_wait(关心就绪队列)

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

收集在epoll监控的事件中已经发送的事件.

  • 参数events是分配好的epoll_event结构体数组.(输出型参数)
  • epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存).
  • maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
  • 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
  • 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函 数失败.

epoll工作原理

在这里插入图片描述

epoll的整个工作流程:

在这里插入图片描述

  • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

在这里插入图片描述

struct eventpoll{..../*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 				struct rb_root rbr; /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ struct list_head rdlist;.... 
};
  • 每个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体

在这里插入图片描述

struct epitem{struct rb_node rbn;//红黑树节点struct list_head rdllink;//双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的eventpoll对象 struct epoll_event event; //期待发生的事件类型
}

epoll模型

在这里插入图片描述

操作系统中可能有多个epoll模型(也是一个地址,会被操作系统管理起来)

【问题】:进程是怎么找到epoll模型的呢?

进程中会为epoll模型创建struct file结构体,进程可以通过文件描述符(fd)来找到,从而可以找到所有的信息

事实上callback会和每个节点对应,更高效。

epoll和poll、select差别非常大

  • 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem 元素即可.
  • 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度 是O(1).

总结一下,epoll的使用过程:

  • 调用epoll_create创建一个epoll句柄;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪;

epoll的优点(和select的缺点对应)

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1)(检测事件就绪是O(1)的). 即使文件描述 符数目很多, 效率也不会受到影响.
  • 没有数量限制: 文件描述符数目无上限.(fd,event没有上限(由红黑树的大小决定,也就是计算机的内存决定的)),其实这颗红黑树其实相当于poll里面自己维护的数组,而epoll让用户不再自己维护事件描述符及其关心的事件,直接提供系统调用即可

epoll的返回值中n表示有几个fd就绪了,同时就绪事件在就绪队列中连续存放,n可以用来遍历就绪事件。

注意!!

网上有些博客说, epoll中使用了内存映射机制

  • 内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销.

这种说法是不准确的. 我们定义的struct epoll_event是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的.

epoll_wait 调用在内核态维护一个就绪列表,并在有就绪事件时将文件描述符列表拷贝到用户态。这种实现方式虽然涉及一定的内存拷贝,但它在性能和复杂性之间取得了平衡。

标准的 epoll 实现中并没有使用 mmap 来将就绪队列直接映射到用户态。

要多对比总结select, poll, epoll之间的优点和缺点(重要, 面试中常见).

注意epoll是负责等待的,不负责socket的创建、绑定和监听

编写epoll服务器

Epoller.hpp

#pragma once
#include "nocopy.hpp"
#include <sys/epoll.h>
#include <cerrno>
#include <cstring>class Epoller:public nocopy
{static const int size = 128;
public:Epoller(){_epfd = epoll_create(size);if(_epfd == -1){lg(Error,"epoll_create error: %s",strerror(errno));}else{lg(Info,"epoll_create sucess: %d",_epfd);}}int EpollerWait(struct epoll_event revents[] , int num){int n = epoll_wait(_epfd,revents,num,_timeout);return n;}int EpollerUpdate(int oper, int sock,uint32_t event){int n =0; if(oper==EPOLL_CTL_DEL){n = epoll_ctl(_epfd,oper,sock,nullptr);if(n!=0)//成功是0{lg(Error,"epoll_ctl delete error!");}}else{// EPOLL_CTL_MOD || EPOLL_CTL_ADDstruct epoll_event ev;ev.events = event;ev.data.fd = sock;//方便我们后面知道是哪一个fd就绪了n = epoll_ctl(_epfd,oper,sock,&ev);if(n!=0)//成功是0{lg(Error,"epoll_ctl error!");}}return n;}~Epoller(){if(_epfd>=0){close(_epfd);}}private:int _epfd;int _timeout{3000};
};

EpollServer.hpp

#pragma once#include <iostream>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.hpp"
#include <memory>
#include "Log.hpp"
#include "nocopy.hpp"uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);class EpollServer: public nocopy
{static const int num = 64;
public:EpollServer(uint16_t port):_port(port),_listsocket_ptr(new Sock()),_epoller_ptr(new Epoller()){}void Init(){_listsocket_ptr->Socket();_listsocket_ptr->Bind(_port);_listsocket_ptr->Listen();lg(Info, "create listen socket success: %d\n",_listsocket_ptr->Fd());}void Accepter(){//获取新链接std::string clientip;uint16_t clientport;int sock = _listsocket_ptr->Accept(&clientip,&clientport);if(sock > 0){//不能在这里读取,因为这只是链接建立好了,并不能在这里读取数据_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD,sock,EVENT_IN);lg(Info,"get a new link,client info@ %s:%d",clientip.c_str(),clientport);}}void Recver(int fd){// demochar buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;std::cout << "get a messge: " << buffer << std::endl;std::string echo_str = "server echo $ " +std::string(buffer);write(fd ,echo_str.c_str() ,echo_str.size());}else if (n == 0){lg(Info, "client quit, me too, close fd is : %d", fd);//细节3(先移除再关闭)_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd,0);close(fd);}else{lg(Warning, "recv error: fd is : %d", fd);_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd,0);close(fd);}}void Dispatcher(struct epoll_event revs[],int num){for(int i=0;i<num;++i){uint32_t events = revs[i].events;int fd = revs[i].data.fd;if(events & EVENT_IN){if(fd == _listsocket_ptr->Fd()){Accepter();}else{//其他fd上面的普通读事件就绪Recver(fd);}}else if(events & EVENT_OUT){}else{}}}void Start(){//将listensock添加到epoll中 -> listensock和他关心的事件,添加到内核epoll模型中rb_tree._epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD,_listsocket_ptr->Fd(),EVENT_IN);struct epoll_event revs[num];//这里规定了数组大小,但是没关系,一次捞不完等下一批捞,//一个文件描述符是否有读事件就绪的本质,看文件描述符及其关心的事件有没有放入到就绪队列中for(;;){int n = _epoller_ptr->EpollerWait(revs,num);if(n > 0){//有事件就绪了lg(Debug,"event happend,fd is : %d", revs[0].data.fd);Dispatcher(revs,n);}else if(n == 0){lg(Info, "time out ...");}else{lg(Error,"epoll wait error");}}}~EpollServer(){_listsocket_ptr->Close();}private:std::shared_ptr<Sock> _listsocket_ptr;std::shared_ptr<Epoller> _epoller_ptr;uint16_t _port;
};

Main.cc

#include <iostream>
#include <memory>
#include "EpollServer.hpp"
#include "Epoller.hpp"
int main(){std::unique_ptr<EpollServer> epoll_svr(new EpollServer(8888));epoll_svr->Init();epoll_svr->Start();Epoller ep;return 0;  
}

nocopy

#pragma onceclass nocopy
{
public:nocopy(){}nocopy(const nocopy &) = delete;const nocopy&operator=(const nocopy &) = delete;
};

Socket.hpp

#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"enum
{SocketErr = 2,BindErr,ListenErr,
};// TODO
const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}public:void Socket(){sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0){lg(Fatal, "socker error, %s: %d", strerror(errno), errno);exit(SocketErr);}int opt = 1;setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}void Bind(uint16_t port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen(){if (listen(sockfd_, backlog) < 0){lg(Fatal, "listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);if(newfd < 0){lg(Warning, "accept error, %s: %d", strerror(errno), errno);return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));if(n == -1) {std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(sockfd_);}int Fd(){return sockfd_;}private:int sockfd_;
};

Log.hpp

#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}// void logmessage(int level, const char *format, ...)// {//     time_t t = time(nullptr);//     struct tm *ctime = localtime(&t);//     char leftbuffer[SIZE];//     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),//              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,//              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//     // va_list s;//     // va_start(s, format);//     char rightbuffer[SIZE];//     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);//     // va_end(s);//     // 格式:默认部分+自定义部分//     char logtxt[SIZE * 2];//     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);//     // printf("%s", logtxt); // 暂时打印//     printLog(level, logtxt);// }void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暂时打印printLog(level, logtxt);}private:int printMethod;std::string path;
};Log lg;

epoll工作方式(水平触发LT和边缘触发ET)

水平触发Level Triggered工作模式

epoll默认状态下就是LT工作模式.

LT模式:事件到来,但是上层不处理,高电平,一直有效

  • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
  • 如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪.
  • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
  • 支持阻塞读写和非阻塞读写
边缘触发Edge Triggered工作模式

ET模式:从无到有,从有到无,变化的时候,才会通知我们一次

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.

  • 当epoll检测到socket上事件就绪时, 必须立刻处理.
  • 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了.
  • 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
  • ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
  • 只支持非阻塞的读写

select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET.

对比LT和ET

LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序员一次响应就绪过程中就把 所有的数据都处理完(循环读取,直到读取出错,如果底层没有数据了就会阻塞住(fd默认是阻塞的),所以ET模式下所有的fd必须是non_block的)(重点).所以ET的通知效率更高

相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到 每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.

如何理解ET的IO效率更高:ET要求程序员将本轮数据全部取走,从而让TCP向对方通告一个更大的窗口,从概率上让对方一次给本机发送更多的数据

另一方面, ET 的代码复杂程度更高了.

【问题】:但是ET一定比LT效率更高吗?

LT也可以将所有的fd设置成non_block,然后进行循环读取,通知第一次的时候,就全部取走就和ET一样了。

此外LT和ET的区别也是:一次向就绪队列里添加,还是次次添加

理解ET模式和非阻塞文件描述符

使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 “工程实践” 上的要求.

假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第 二个10k请求.

epoll的使用场景

epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.

  • 对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.

例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll. 如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型.

原因:

  1. 复杂性和开销epoll 的实现相对复杂,它需要管理内核中的事件列表。对于少量连接,这种复杂性和管理开销并不能带来显著的性能提升,反而可能因为不必要的管理导致性能下降。
  2. 轮询效率:对于少数连接,简单的轮询(例如 pollselect)足以胜任。因为连接数很少,轮询的开销也很小,使用这些机制更直接和高效。
  3. 资源消耗epoll 适用于大量连接的场景,因为它能够有效地处理大量并发连接而不显著增加资源消耗。但是,对于少数连接来说,这种优势并不明显,使用 epoll 反而可能造成资源浪费。
  4. 实现简便性selectpoll 的实现更为简单,代码易于维护和理解。在连接数较少时,选择这种更简单的实现方式可以降低代码复杂度,提高开发效率。

epoll中的惊群问题

惊群问题

其他资料

epoll详解

apache/nginx网络模型

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

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

相关文章

【全开源】安心护送非急救救护车转运平台小程序(FastAdmin+ThinkPHP+Uniap

&#x1f691;安心护送非急救救护车转运平台小程序——您的健康守护者&#x1f496; 安心护送转运平台小程序是一款基于FastAdminThinkPHPUniapp开发的非急救救护车租用转运平台小程序系统&#xff0c;可以根据运营者的业务提供类似短途接送救护服务&#xff0c;重症病人转运服…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《基于状态分解的综合能源系统完全分布式调度算法》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

sqli-labs 靶场 less-7 第七关详解:OUTFILE注入与配置

SQLi-Labs是一个用于学习和练习SQL注入漏洞的开源应用程序。通过它&#xff0c;我们可以学习如何识别和利用不同类型的SQL注入漏洞&#xff0c;并了解如何修复和防范这些漏洞。Less 7 SQLI DUMB SERIES-7判断注入点 进入页面中&#xff0c;并输入数据查看结果。 发现空数据提…

【C++11数据结构与算法】C++ 栈

C 栈(stack) 文章目录 C 栈(stack)栈的基本介绍栈的算法运用单调栈实战题LC例题&#xff1a;[321. 拼接最大数](https://leetcode.cn/problems/create-maximum-number/)LC例题&#xff1a;[316. 去除重复字母](https://leetcode.cn/problems/remove-duplicate-letters/) 栈的基…

下决心!学点前端图形开发来提升自已

前端图形开发已成为Web开发绕不开的部分。提升自己前端图形开发能力&#xff0c;才能快点在岗位中突围。但一看到各大实战教程的学习路径就犯难了。 内容不少&#xff0c;感觉学到老也跳不出来啊。还是走回老路&#xff1f;搞个热门国外开源框架应付算了。 我简单通过对图形系…

程序猿大战Python——流程控制——if基础语句

三大基本语句 目标&#xff1a;了解三大基本语句有哪些&#xff1f; Python中有三大基本语句&#xff0c;它们支撑起了程序的业务逻辑处理。 三大基本语句有&#xff1a; &#xff08;1&#xff09;顺序语句 &#xff08;2&#xff09;分支语句 &#xff08;3&#xff09;循…

树莓派 5 AI 套件(Hailo)示例

系列文章目录 前言 欢迎访问 Hailo Raspberry Pi 5 示例库。本项目展示了在 Raspberry Pi 5 上演示 Hailo AI 处理器功能的各种示例。这些示例将帮助您开始在嵌入式设备上使用人工智能。更多信息&#xff0c;请访问 Hailo 官方网站和 Hailo 社区论坛。 一、如何设置 Raspberry…

Apple - Quartz 2D Programming Guide

本文翻译自&#xff1a;Quartz 2D Programming Guide&#xff08;更新时间&#xff1a;2017-03-21 https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html#//apple_ref/doc/uid/TP300010…

Renesas MCU之SCI_SPI接口驱动LCD

目录 概述 1 软硬件介绍 1.1 软件版本信息 1.2 ST7796-LCD 1.3 MCU IO与LCD PIN对应关系 2 FSP配置项目 2.1 配置项目参数 2.2 生成项目框架 3 代码实现 3.1 SPI的库函数 3.1.1 R_SCI_SPI_Open() 3.1.2 R_SCI_SPI_Read() 3.1.3 R_SCI_SPI_Write() 3.2 应用函数…

【全开源】Workerman在线客服系统(ThinkPHP+FastAdmin+Workerman)

Workerman在线客服系统&#xff1a;高效沟通的新选择 基于ThinkPHPFastAdminWorkerman开发的一款实时在线客服系统&#xff0c;支持多客服(不限座席)、知识库、离线留言板、离线消息、历史会话、微信小程序接入、Uni-app接入(高级授权)、用户轨迹等功能。​ &#x1f4e2; 一…

如何远程连接Linux服务器?

远程连接Linux服务器是通过网络连接到位于远程位置的Linux服务器&#xff0c;以进行服务器管理和操作。远程连接使得系统管理员可以方便地远程访问服务器&#xff0c;进行配置、维护和故障排除等操作&#xff0c;而不必亲自在服务器前工作。以下是一些常用的远程连接方法&#…

python - Pandas缺失值处理

文中所用数据集已上传,找不到的可以私聊我 学习目标 知道空值和缺失值的区别以及缺失值的影响 知道如何查看数据集缺失值情况的方法 知道缺失值处理的办法 1 NaN简介 好多数据集都含缺失数据。缺失数据有多种表现形式 数据库中&#xff0c;缺失数据表示为NULL 在某些编程语…

【C语言】宏详解(上卷)

前言 紧接着预处理详解&#xff08;上卷&#xff09;&#xff0c;接下来我们来讲宏&#xff08;隶属于预处理详解系列&#xff09;。 #define定义宏 #define机制包括了一个规定&#xff0c;允许把参数替换到文本中&#xff0c;这种实现通常称为宏&#xff08;macro&#xff…

AI如何创造情绪价值

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面。从智能家居到自动驾驶&#xff0c;从医疗辅助到金融服务&#xff0c;AI技术的身影无处不在。而如今&#xff0c;AI更是涉足了一个全新的领域——创造情绪价值。 AI已经能够处…

2024年【天津市安全员C证】免费试题及天津市安全员C证试题及解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 天津市安全员C证免费试题是安全生产模拟考试一点通生成的&#xff0c;天津市安全员C证证模拟考试题库是根据天津市安全员C证最新版教材汇编出天津市安全员C证仿真模拟考试。2024年【天津市安全员C证】免费试题及天津市…

分享美好,高清无阻 - 直播极简联网解决方案

1、需求背景 随着移动互联网、UGC模式和直播平台的发展&#xff0c;网络直播的门槛日益降低&#xff0c;越来越多的人希望成为直播的主角。基于物联网的户外直播无线联网解决方案应运而生&#xff0c;满足直播者的需求。 户外直播无线联网解决方案提供了无处不在的直播体验&a…

SpringBoot与MyBatis的快速整合(基于注解)

文章目录 创建Spring Boot项目配置数据库连接信息编写MyBatis Mapper接口使用XML文件编写SQL映射配置数据源切换引入Druid依赖配置Druid数据源 配置MyBatis支持事务管理 在使用Spring Boot创建新项目或新模块时&#xff0c;如果需要使用MyBatis来进行数据库操作&#xff0c;可以…

uniapp实现微信小程序调用云函数【vue2】

在uniapp中的vue 2框架中想要改变默认的目录结构&#xff0c;将装有云函数的文件夹在运行后一起复制到unpackage 文件下&#xff0c;主要用 copy-webpack-plugin 方法来实现&#xff0c;具体步骤如下&#xff1a; 一、创建一个vue 2 框架的uniapp 二、新建一个文件夹装云函数 …

Git【版本控制命令】

02 【本地库操作】 1.git的结构 2.Git 远程库——代码托管中心 2.1 git工作流程 代码托管中心用于维护 Git 的远程库。包括在局域网环境下搭建的 GitLab 服务器&#xff0c;以及在外网环境下的 GitHub 和 Gitee (码云)。 一般工作流程如下&#xff1a; 1&#xff0e;从远程…

轻松掌握系统概况,提升工作效率

作为 Linux 系统管理员,我们经常需要了解系统的基本状况,比如当前时间、系统版本、内核信息、CPU 型号、内存使用等等。但是每次手动执行各种命令来获取这些信息,无疑是一件非常繁琐的事情。 幸运的是,我们可以通过编写一个简单的 shell 脚本来一键获取这些系统信息。让我们一…