【Linux】IO多路转接——epoll

news2024/11/23 22:45:55

目录

epoll初识

epoll的相关系统调用

epoll工作原理

epoll服务器

 epoll的优点

epoll工作方式

对比LT和ET


epoll初识

epoll也是系统提供的一个多路转接接口。

  • epoll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,与select和poll的定位是一样的,适用场景也相同。
  • epoll在命名上比poll多了一个e,这个e可以理解成是extend,epoll就是为了同时处理大量文件描述符而改进的poll。
  • epoll在2.5.44内核中被引进,它几乎具备了select和poll的所有优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll的相关系统调用

epoll有三个相关的系统调用,分别是epoll_create、epoll_ctl和epoll_wait。

epoll_create函数

epoll_create函数用于创建一个epoll模型,该函数的函数原型如下:

int epoll_create(int size);

参数说明:

  • size:自从Linux2.6.8之后,size参数是被忽略的,但size的值必须设置为大于0的值。

返回值说明:

  • epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置。

注意: 当不再使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。

epoll_ctl函数

epoll_ctl函数用于向指定的epoll模型中注册事件,该函数的函数原型如下:

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

参数说明:

  • epfd:指定的epoll模型。
  • op:表示具体的动作,用三个宏来表示。
  • fd:需要监视的文件描述符。
  • event:需要监视该文件描述符上的哪些事件。

第二个参数op的取值有以下三种:

  • EPOLL_CTL_ADD:注册新的文件描述符到指定的epoll模型中。
  • EPOLL_CTL_MOD:修改已经注册的文件描述符的监听事件。
  • EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符。

返回值说明:

  • 函数调用成功返回0,调用失败返回-1,同时错误码会被设置。

第四个参数对应的struct epoll_event结构如下:

struct epoll_event结构中有两个成员,第一个成员events表示的是需要监视的事件,第二个成员data是一个联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符。

events的常用取值如下:

  • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
  • EPOLLOUT:表示对应的文件描述符可以写。
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
  • EPOLLERR:表示对应的文件描述符发送错误。
  • EPOLLHUP:表示对应的文件描述符被挂断,即对端将文件描述符关闭了。
  • EPOLLET:将epoll的工作方式设置为边缘触发(Edge Triggered)模式。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听该文件描述符的话,需要重新将该文件描述符添加到epoll模型中。

 这些取值实际也是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。

epoll_wait函数

epoll_wait函数用于收集监视的事件中已经就绪的事件,该函数的函数原型如下:

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

参数说明:

  • epfd:指定的epoll模型。
  • events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)。
  • maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值。
  • timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)。

参数timeout的取值:

  • -1:epoll_wait调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:epoll_wait调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,epoll_wait检测后都会立即返回。
  • 特定的时间值:epoll_wait调用后在直到的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后epoll_wait进行超时返回。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

epoll_wait调用失败时,错误码可能被设置为:

  • EBADF:传入的epoll模型对应的文件描述符无效。
  • EFAULT:events指向的数组空间无法通过写入权限访问。
  • EINTR:此调用被信号所中断。
  • EINVAL:epfd不是一个epoll模型对应的文件描述符,或传入的maxevents值小于等于0。

epoll工作原理

红黑树和就绪队列

当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll结构体,也就是我们所说的epoll模型,eventpoll结构体当中的成员rbr和rdlist与epoll的使用方式密切相关。

struct eventpoll{
	...
	//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
	struct rb_root rbr;
	//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
	...
}

  • epoll模型当中的红黑树本质就是告诉内核,需要监视哪些文件描述符上的哪些事件,调用epll_ctl函数实际就是在对这颗红黑树进行对应的增删改操作。
  • epoll模型当中的就绪队列本质就是告诉内核,哪些文件描述符上的哪些事件已经就绪了,调用epoll_wait函数实际就是在从就绪队列当中获取已经就绪的事件。

在epoll中,对于每一个事件都会有一个对应的epitem结构体,红黑树和就绪队列当中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构当中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件。

struct epitem{
	struct rb_node rbn; //红黑树节点
	struct list_head rdllink; //双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll *ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}
  • 对于epitem结构当中rbn成员来说,ffd与event的含义是,需要监视ffd上的event事件是否就绪。
  • 对于epitem结构当中的rdlink成员来说,ffd与event的含义是,ffd上的event事件已经就绪了。

说明一下:

  • 红黑树是一种二叉搜索树,因此必须有键值key,而这里的文件描述符就天然的可以作为红黑树的key值。
  • 调用epoll_ctl向红黑树当中新增节点时,如果设置了EPOLLONESHOT选项,当监听完这次事件后,如果还需要继续监听该文件描述符则需要重新将其添加到epoll模型中,本质就是当设置了EPOLLONESHOT选项的事件就绪时,操作系统会自动将其从红黑树当中删除。
  • 而如果调用epoll_ctl向红黑树当中新增节点时没有设置EPOLLONESHOT,那么该节点插入红黑树后就会一直存在,除非用户调用epoll_ctl将该节点从红黑树当中删除。

回调机制

所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法,这个回调方法在内核中叫ep_poll_callback。

  • 对于select和poll来说,操作系统在监视多个文件描述符上的事件是否就绪时,需要让操作系统主动对这多个文件描述符进行轮询检测,这一定会增加操作系统的负担。
  • 而对于epoll来说,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中。
  • 当用户调用epoll_wait函数获取就绪事件时,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可。

采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当事件就绪时会自动调用对应的回调函数进行处理。

说明一下:

  • 只有添加到红黑树当中的事件才会与底层建立回调方法,因此只有当红黑树当中对应的事件就绪时,才会执行对应的回调方法将其添加到就绪队列当中。
  • 当不断有监视的事件就绪时,会不断调用回调方法向就绪队列当中插入节点,而上层也会不断调用epoll_wait函数从就绪队列当中获取节点,这是典型的生产者消费者模型。
  • 由于就绪队列可能会被多个执行流同时访问,因此必须要使用互斥锁对其进行保护,eventpoll结构当中的lock和mtx就是用于保护临界资源的,因此epoll本身是线程安全的。
  • eventpoll结构当中的wq(wait queue)就是等待队列,当多个执行流想要同时访问同一个epoll模型时,就需要在该等待队列下进行等待。

epoll三部曲

总结一下,epoll的使用过程就是三部曲:

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

epoll服务器

为了简单演示一下epoll的使用方式,这里我们也实现一个简单的epoll服务器,该服务器也只是读取客户端发来的数据并进行打印。

EpollServer类

EpollServer类当中除了需要包含监听套接字和端口号两个成员变量之外,最好将epoll模型对应的文件描述符也作为一个成员变量。

  • 在构造EpollServer对象时,需要指明epoll服务器的端口号,当然也可以在初始化epoll服务器的时候指明。
  • 在初始化epoll服务器的时候调用Socket类当中的函数,依次进行套接字的创建、绑定和监听,此外epoll模型的创建可以在服务器初始化的时候进行。
  • 在析构函数中调用close函数,将监听套接字和epoll模型对应的文件描述符进行关闭。

代码如下:

#include "socket.hpp"
#include <sys/epoll.h>

#define BACK_LOG 5
#define SIZE 256

class EpollServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
	int _epfd; //epoll模型
public:
	EpollServer(int port)
		: _port(port)
	{}
	void InitEpollServer()
	{
		_listen_sock = Socket::SocketCreate();
		Socket::SocketBind(_listen_sock, _port);
		Socket::SocketListen(_listen_sock, BACK_LOG);
		
		//创建epoll模型
		_epfd = epoll_create(SIZE);
		if (_epfd < 0){
			std::cerr << "epoll_create error" << std::endl;
			exit(5);
		}
	}
	~EpollServer()
	{
		if (_listen_sock >= 0){
			close(_listen_sock);
		}
		if (_epfd >= 0){
			close(_epfd);
		}
	}
};

运行服务器

服务器初始化完毕后就可以开始运行了,而epoll服务器要做的就是不断调用epoll_wait函数,从就绪队列当中获取就绪事件进行处理即可。

  • 首先,在epoll服务器开始死循环调用epoll_wait函数之前,需要先调用epoll_ctl将监听套接字添加到epoll模型当中,表示服务器刚开始运行时只需要监视监听套接字的读事件。
  • 此后,epoll服务器就不断调用epoll_wait函数监视读事件是否就绪。如果epoll_wait函数的返回值大于0,则说明已经有文件描述符的读事件就绪,并且此时的返回值代表的就是有事件就绪的文件描述符个数,接下来就应该对就绪事件进行处理。
  • 如果epoll_wait函数的返回值等于0,则说明timeout时间耗尽,此时直接准备进行下一次epoll_wait调用即可。如果epoll_wait函数的返回值为-1,此时也让服务器准备进行下一次epoll_wait调用,但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用epoll_wait函数。

代码如下:

#include "socket.hpp"
#include <sys/epoll.h>

#define BACK_LOG 5
#define SIZE 256
#define MAX_NUM 64

class EpollServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
	int _epfd; //epoll模型
public:
	void Run()
	{
		AddEvent(_listen_sock, EPOLLIN); //将监听套接字添加到epoll模型中,并关心其读事件
		for (;;){
			struct epoll_event revs[MAX_NUM];
			int num = epoll_wait(_epfd, revs, MAX_NUM, -1);
			if (num < 0){
				std::cerr << "epoll_wait error" << std::endl;
				continue;
			}
			else if (num == 0){
				std::cout << "timeout..." << std::endl;
				continue;
			}
			else{
				//正常的事件处理
				//std::cout<<"有事件发生..."<<std::endl;
				HandlerEvent(revs, num);
			}
		}
	}
private:
	void AddEvent(int sock, uint32_t event)
	{
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = sock;
		
		epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
	}
};

说明一下:

  • 默认情况下,只要底层有就绪事件没有处理,epoll也会一直通知用户,也就是调用epoll_wait会一直成功返回,并将就绪的事件拷贝到我们传入的数组当中。
  • 需要注意的是,所谓的事件处理并不是调用epoll_wait将底层就绪队列中的就绪事件拷贝到用户层,比如当这里的读事件就绪后,我们应该调用accept获取底层建立好的连接,或调用recv读取客户端发来的数据,这才算是将读事件处理了。
  • 如果我们仅仅是调用epoll_wait将底层就绪队列当中的事件拷贝到应用层,那么这些就绪事件实际并没有被处理掉,底层注册的回调函数会被再次调用,将就绪的事件重新添加到就绪队列当中,本质原因就是我们实际并没有对底层就绪的数据进行读取。

事件处理

如果底层就绪队列当中有就绪事件,那么调用epoll_wait函数时就会将底层就绪队列中的事件拷贝到用户提供的revs数组当中,接下来epoll服务器就应该对就绪事件进行处理了,事件处理过程如下:

  • 根据调用epoll_wait时得到的返回值,来判断操作系统向revs数组中拷贝了多少个struct epoll_event结构,进而对这些文件描述符上的事件进行处理。
  • 对于每一个拷贝上来的struct epoll_event结构,如果该结构当中的events当中包含读事件,则说明该文件描述符对应的读事件就绪,但接下来还需要进一步判断该文件描述符是监听套接字还是与客户端建立的套接字。
  • 如果是监听套接字的读事件就绪,则调用accept函数将底层建立好的连接获取上来,并调用epoll_ctl函数将获取到的套接字添加到epoll模型当中,表示下一次调用epoll_wait函数时需要监视该套接字的读事件。
  • 如果是与客户端建立的连接对应的读事件就绪,则调用recv函数读取客户端发来的数据,并将读取到的数据在服务器端进行打印。
  • 如果在调用recv函数时发现客户端将连接关闭或recv函数调用失败,则epoll服务器也直接关闭对应的连接,并调用epoll_ctl函数将该连接对应的文件描述符从epoll模型中删除,表示下一次调用epoll_wait函数时无需再监视该套接字的读事件。

代码如下:

#include "socket.hpp"
#include <sys/epoll.h>

#define BACK_LOG 5
#define SIZE 256
#define MAX_NUM 64

class EpollServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
	int _epfd; //epoll模型
public:
	void HandlerEvent(struct epoll_event revs[], int num)
	{
		for (int i = 0; i < num; i++){
			int fd = revs[i].data.fd; //就绪的文件描述符
			if (fd == _listen_sock&&revs[i].events&EPOLLIN){ //连接事件就绪
				struct sockaddr_in peer;
				memset(&peer, 0, sizeof(peer));
				socklen_t len = sizeof(peer);
				int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
				if (sock < 0){ //获取连接失败
					std::cerr << "accept error" << std::endl;
					continue;
				}
				std::string peer_ip = inet_ntoa(peer.sin_addr);
				int peer_port = ntohs(peer.sin_port);
				std::cout << "get a new link[" << peer_ip << ":" << peer_port << "]" << std::endl;
				
				AddEvent(sock, EPOLLIN); //将获取到的套接字添加到epoll模型中,并关心其读事件
			}
			else if (revs[i].events&EPOLLIN){ //读事件就绪
				char buffer[64];
				ssize_t size = recv(fd, buffer, sizeof(buffer)-1, 0);
				if (size > 0){ //读取成功
					buffer[size] = '\0';
					std::cout << "echo# " << buffer << std::endl;
				}
				else if (size == 0){ //对端连接关闭
					std::cout << "client quit" << std::endl;
					close(fd);
					DelEvent(fd); //将文件描述符从epoll模型中删除
				}
				else{
					std::cerr << "recv error" << std::endl;
					close(fd);
					DelEvent(fd); //将文件描述符从epoll模型中删除
				}
			}
		}
	}
private:
	void AddEvent(int sock, uint32_t event)
	{
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = sock;
		
		epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
	}
	void DelEvent(int sock)
	{
		epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
	}
};

epoll服务器测试

运行epoll服务器时需要先实例化出一个EpollServer对象,对epoll服务器进行初始化后就可以运行服务器了。

代码如下:

#include "epoll_server.hpp"
#include <string>

static void Usage(std::string proc)
{
	std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
	if (argc != 2){
		Usage(argv[0]);
		exit(1);
	}
	int port = atoi(argv[1]);

	EpollServer* svr = new EpollServer(port);
	svr->InitEpollServer();
	svr->Run();
	
	return 0;
}

因为编写epoll服务器在调用epoll_wait函数时,我们将timeout的值设置成了-1,因此运行服务器后如果没有客户端发来连接请求,那么服务器就会在调用epoll_wait函数后进行阻塞等待。

当我们用telnet工具连接epoll服务器后,epoll服务器调用的epoll_wait函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,并打印输出客户端的IP和端口号,此时客户端发来的数据也能够成功被epoll服务器收到并进行打印输出。

此外,我们这里编写的也是一个单进程的epoll服务器,但是它可以同时为多个客户端提供服务。

我们可以用ls /proc/PID/fd命令,查看当前epoll服务器的文件描述符的使用情况。其中文件描述符0、1、2是默认打开的,分别对应的是标准输入、标准输出和标准错误,3号文件描述符对应的是监听套接字,4号文件描述符对应的是服务器创建的epoll模型,5号和6号文件描述符对应的分别是正在访问服务器的两个客户端。

当服务器端检测到客户端退出后,也会关闭对应的连接,此时epoll服务器对应的5号和6号文件描述符就关闭了。

epoll服务器整体编程的顺序思路

 epoll的优点

  • 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效。
  • 数据拷贝轻量:只在新增监视事件的时候调用epoll_ctl将数据从用户拷贝到内核,而select和poll每次都需要重新将监视的事件从用户拷贝到内核。此外,调用epoll_wait获取就绪事件时,只会拷贝就绪的事件,不会进行不必要的拷贝操作。
  • 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中。调用epoll_wait时直接访问就绪队列就知道哪些文件描述符已经就绪,检测是否有文件描述符就绪的时间复杂度是O(1),因为本质只需要判断就绪队列是否为空即可。
  • 没有数量限制:监视的文件描述符数目无上限,只要内存允许,就可以一直向红黑树当中新增节点。

注意:

  • 有人说epoll中使用了内存映射机制,内核可以直接将底层就绪队列通过mmap的方式映射到用户态,此时用户就可以直接读取到内核中就绪队列当中的数据,避免了内存拷贝的额外性能开销。
  • 有人说epoll中使用了内存映射机制,内核可以直接将底层就绪队列通过mmap的方式映射到用户态,此时用户就可以直接读取到内核中就绪队列当中的数据,避免了内存拷贝的额外性能开销。
  • 因此用户要获取内核当中的数据,势必还是需要将内核的数据拷贝到用户空间。

与select和poll的不同之处

  • 在使用select和poll时,都需要借助第三方数组来维护历史上的文件描述符以及需要监视的事件,这个第三方数组是由用户自己维护的,对该数组的增删改操作都需要用户自己来进行。
  • 而使用epoll时,不需要用户自己维护所谓的第三方数组,epoll底层的红黑树就充当了这个第三方数组的功能,并且该红黑树的增删改操作都是由内核维护的,用户只需要调用epoll_ctl让内核对该红黑树进行对应的操作即可。
  • 在使用多路转接接口时,数据流都有两个方向,一个是用户告知内核,一个是内核告知用户。select和poll将这两件事情都交给了同一个函数来完成,而epoll在接口层面上就将这两件事进行了分离,epoll通过调用epoll_ctl完成用户告知内核,通过调用epoll_wait完成内核告知用户。

epoll工作方式

epoll有两种工作方式,分别是水平触发工作模式和边缘触发工作模式。

水平触发(LT,Level Triggered)

  • 只要底层有事件就绪,epoll就会一直通知用户。
  • 就像数字电路当中的高电平触发一样,只要一直处于高电平,则会一直触发。

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

  • 由于在LT工作模式下,只要底层有事件就绪就会一直通知用户,因此当epoll检测到底层读事件就绪时,可以不立即进行处理,或者只处理一部分,因为只要底层数据没有处理完,下一次epoll还会通知用户事件就绪。
  • select和poll其实就是工作是LT模式下的。
  • 支持阻塞读写和非阻塞读写。

边缘触发(ET,Edge Triggered)

  • 只有底层就绪事件数量由无到有或由有到多发生变化的时候,epoll才会通知用户。
  • 就像数字电路当中的上升沿触发一样,只有当电平由低变高的那一瞬间才会触发。

如果要将epoll改为ET工作模式,则需要在添加事件时设置EPOLLET选项。  

  • 由于在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,因此当epoll检测到底层读事件就绪时,必须立即进行处理,而且必须全部处理完毕,因为有可能此后底层再也没有事件就绪,那么epoll就再也不会通知用户进行事件处理,此时没有处理完的数据就相当于丢失了。
  • ET工作模式下epoll通知用户的次数一般比LT少,因此ET的性能一般比LT性能更高,Nginx就是默认采用ET模式使用epoll的。
  • 只支持非阻塞的读写。

ET工作模式下应该如何进行读写

因为在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,这就倒逼用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了。

因此读数据时必须循环调用recv函数进行读取,写数据时必须循环调用send函数进行写入。

  • 当底层读事件就绪时,循环调用recv函数进行读取,直到某次调用recv读取时,实际读取到的字节数小于期望读取的字节数,则说明本次底层数据已经读取完毕了。
  • 但有可能最后一次调用recv读取时,刚好实际读取的字节数和期望读取的字节数相等,但此时底层数据也恰好读取完毕了,如果我们在调用recv函数进行读取,那么recv就会因为底层没有数据而被阻塞住。
  • 而这里的阻塞是非常严重的,就比如我们这里的写的服务器都是单进程的服务器,如果recv被阻塞住,并且此后该数据再也不就绪,那么就相当于我们的服务器挂掉了,因此在ET工作模式下循环调用recv函数进行读取时,必须将对应的文件描述符设置为非阻塞状态。
  • 调用send函数写数据时也是同样的道理,需要循环调用send函数进行数据的写入,并且必须将对应的文件描述符摄者为非阻塞状态。

强调: ET工作模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的,不是可选的。

对比LT和ET

  • 在ET模式下,一个文件描述符就绪之后,用户不会反复收到通知,看起来比LT更高效,但如果在LT模式下能够做到每次都将就绪的文件描述符立即全部处理,不让操作系统反复通知用户的话,其实LT和ET的性能也是一样的。
  • 此外,ET的编程难度比LT更高。

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

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

相关文章

torch_geometric实现GCN和LightGCN

torch_geometric实现GCN和LightGCN 题记demo示意图GCN代码LightGCN代码参考博文及感谢 题记 使用torch_geometric实现GCN和LightGCN&#xff0c;以后可能要用&#xff0c;做一下备份 demo示意图 GCN代码 X ′ D ^ − 1 / 2 A ^ D ^ − 1 / 2 X Θ \mathbf{X}^{\prime} \mat…

Linux 僵死进程

fork复制进程之后&#xff0c;会产生一个进程叫做子进程&#xff0c;被复制的进程就是父进程。不管父进程先结束&#xff0c;还是子进程先结束&#xff0c;对另外一个进程完全没有影响&#xff0c;父进程和子进程是两个不同的进程。 一、孤儿进程 现在有以下代码&#xff1a;…

【Vue-Router】路由元信息

路由元信息&#xff08;Route Meta Information&#xff09;是在路由配置中为每个路由定义的一组自定义数据。这些数据可以包含任何你希望在路由中传递和使用的信息&#xff0c;比如权限、页面标题、布局设置等。Vue Router 允许你在路由配置中定义元信息&#xff0c;然后在组件…

国产32位单片机XL32F001,带1 路 12bit ADC,I2C、SPI、USART 等外设

XL32F001 系列单片机采用高性能的 32 位 ARM Cortex-M0内核&#xff0c;宽电压工作范围的 MCU。嵌入 24KbytesFlash 和 3Kbytes SRAM 存储器&#xff0c;最高工作频率 24MHz。包含多种不同封装类型多款产品。芯片集成 I2C、SPI、USART 等通讯外设&#xff0c;1 路 12bit ADC&am…

【校招VIP】测试方案之测试用例分析

考点介绍 测试用例是测试岗面试和工作后的核心&#xff0c;在面试里对测试用例的分析是高频考查点。但是很多同学因为没有真实的商业产品需求&#xff0c;只能简单的看别人的用例学习&#xff0c;导致面试时被一个陌生问题卡住。 比如最简单的用户名密码输入&#xff0c;在商业…

2023年网络安全比赛--综合渗透测试(超详细)

一、竞赛时间 180分钟 共计3小时 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 1.扫描目标靶机将靶机开放的所有端口,当作flag提交(例:21,22,23); 2.扫描目标靶机将靶机的http服务版本信息当作flag提交(例:apache 2.3.4); 3.靶机网站存在目录遍历漏洞,请将…

数据库--SQL关键字的执行顺序

一条sql语句通常包括&#xff1a; select from join where group by having order by 聚合函数 limit top 浅谈执行顺序&#xff1a; 1&#xff09;、首先确定一点&#xff0c;并不是按照我们写的语句顺序&#xff0c;从左—>右执行的 2&#xff09;、…

JVM——分代收集理论和垃圾回收算法

一、分代收集理论 1、三个假说 弱分代假说&#xff1a;绝大多数对象都是朝生夕灭的。 强分代假说&#xff1a;熬过越多次垃圾收集过程的对象越难以消亡。 这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则&#xff1a;收集器应该将Java堆划分出不同的区域&…

R语言实现免疫浸润分析(1)

免疫浸润分析是生物信息学研究中的一项关键内容&#xff0c;它旨在评估肿瘤微环境中不同类型的免疫细胞组成。免疫细胞在肿瘤发展和治疗中起着至关重要的作用&#xff0c;因为它们可以影响肿瘤的生长、扩散和对治疗的响应。 为了了解免疫细胞在肿瘤中的分布和数量&#xff0c;…

【潮州饶平】联想 IBM x3850 x6 io主板故障 服务器维修

哈喽 最近比较忙也好久没有更新服务器维修案例了&#xff0c;这次分享一例潮州市饶平县某企业工厂一台IBM System x3850 x6服务器亮黄灯告警且无法正常开机的服务器故障问题。潮州饶平ibm服务器维修IO主板故障问题 故障如下图所示&#xff1a; 故障服务器型号&#xff1a;IBM 或…

客达天下项目案例

本资料转载于传智播客https://www.itheima.com/ https://space.bilibili.com/3493265607232348 黑马程序员主办的全日制统招大学——大同互联网职业技术学院 预计2024年开始招生&#xff0c;敬请持续关注&#xff01; B站视频入口&#xff1a;002_接口项目介绍_哔哩哔哩_bili…

互联网发展历程:从布线到无线,AC/AP的崭新时代

互联网的发展&#xff0c;一直在追求更便捷、更灵活的连接方式。在网络的早期&#xff0c;布线问题常常让人头疼。一项革命性的技术应运而生&#xff0c;那就是“无线AC/AP”。 布线问题的烦恼&#xff1a;繁琐的布线 早期网络的布线工作常常耗费时间和精力&#xff0c;尤其在大…

随机森林:人类基因组中病毒片段识别

百万年前人类基因组中基因组中就已经嵌入了病毒序列&#xff0c;其中一部分在某些条件下会致病&#xff0c;通过基因测序获得海量片段之后就可以判断正常基因和病毒序列了。 我们根据这种包含众多碱基的基因测序结果从中选取部分特征&#xff0c;关于特征的选取也是有好有坏的…

剑指offer62.圆圈中最后剩下的数字

这道题在算法课上的一个小故事上有一个类似的&#xff0c;就是一个军官打了败仗&#xff0c;带着他的几个兵逃到一个山洞&#xff0c;他们不想当俘虏想自杀&#xff0c;但是军官不想自杀但是又不好意思走&#xff0c;于是军官想了个办法&#xff0c;他们几个人围成一个圈&#…

数据库的新工具datagrip

datagrip的安装&#xff08;一路next即可&#xff09; 首先&#xff0c;双击datagrip安装包&#xff0c;会出现下面的界面&#xff0c;然后直接点击next 继续点击next 选中tatagrip&#xff0c;然后在点击next 点击install 勾选datagrip&#xff0c;然后在点击finish 直接点击…

【面试题】JavaScript高级四、高阶技巧

JavaScript高级四、高阶技巧 1、深浅拷贝 首先浅拷贝和深拷贝只针对引用类型 &#xff08;1&#xff09;浅拷贝 浅拷贝&#xff1a;拷贝对象的属性的值&#xff08;简单类型存的值就是值本身&#xff0c;引用类型存的值是对象的堆地址&#xff09;&#xff0c;所以如果拷贝的…

大模型PEFT技术原理(二):P-Tuning、P-Tuning v2

随着预训练模型的参数越来越大&#xff0c;尤其是175B参数大小的GPT3发布以来&#xff0c;让很多中小公司和个人研究员对于大模型的全量微调望而却步&#xff0c;近年来研究者们提出了各种各样的参数高效迁移学习方法&#xff08;Parameter-efficient Transfer Learning&#x…

gitee上传一个本地项目到一个空仓库

gitee上传一个本地项目到一个空仓库 引入 比如&#xff0c;你现在本地下载了一个半成品的框架&#xff0c;现在想要把这个本地项目放到gitee的仓库上&#xff0c;这时就需要我们来做到把这个本地项目上传到gitee上了。 具体步骤 1. 登录码云 地址&#xff1a;https://gite…

基于安防监控EasyCVR视频汇聚融合技术的运输管理系统的分析

一、项目背景 近年来&#xff0c;随着物流行业迅速发展&#xff0c;物流运输费用高、运输过程不透明、货损货差率高、供应链协同能力差等问题不断涌现&#xff0c;严重影响了物流作业效率&#xff0c;市场对于运输管理数字化需求愈发迫切。当前运输行业存在的难题如下&#xf…

Hlang社区项目说明

文章目录 前言Hlang社区技术前端后端 前言 Hello,欢迎来到本专栏&#xff0c;那么这也是第一次做这种类型的专栏&#xff0c;如有不做多多指教。那么在这里我要隆重介绍的就是这个Hlang这个项目。 首先&#xff0c;这里我要说明的是&#xff0c;我们的这个项目其实是分为两个…