仿muduo库实现并发服务器

  • 一.eventloop模块
    • 1.成员变量
      • std::thread::id _thread_id;//线程ID
      • Poller _poll;
      • int _event_fd;
      • std::vector<Function<Function>> _task;
      • TimerWheel _timer_wheel
    • 2.EventLoop构造
    • 3.针对eventfd的操作
    • 4.针对poller的操作
    • 5.针对threadID的操作
    • 6.针对TaskQueue的操作
    • 7.针对定时器的操作
    • 8.EventLoop的主要工作
  • 二.全部代码

一.eventloop模块

一个Eventloop对应一个线程

Eventloop模块它是进行事件监控以及事件处理的模块。
一个Eventlopp对应一个线程。
当Eventloop监控了一个连接,而这个连接一旦有事件就绪,就会去处理。
如果这个连接在多个线程中都触发了事件(前提是描述符被多个线程Eventloop监控),就会存在线程安全问题。所以为了避免这个问题,就要求连接必须在一个线程中处理。

也就是连接的所有操作都必须在同一个线程中处理,将连接的事件监控,以及连接事件处理,关闭连接等操作都放在同一个线程中进行。

也因为连接的内部函数可能会被不同的线程同时执行,就会存在线程安全问题。

如何保证一个连接的所有操作都在同一个线程中执行呢?

连接是无法与线程进行绑定的,但是连接可以与Eventloop绑定,而Eventloop则与线程是绑定的。
所以每个连接都有对应的Eventloop,只要让连接去对应的Eventloop中执行就是在同一个线程中执行。

在这里插入图片描述
这其实就相当于把我们的channel跟eventloop给关联到了一起之间
的关联到了一起,而eventloop又跟线程是一一对应的,它是在线程里边去运行的。
eventloop模块的功能都是在同一个线程里边去完成的,一旦它监控了连接,并且连接绑定的是同一个eventloop,那也就意味着。这个连接呢,它的一个监控以及它的处理是同一个线程里边。

eventloop的处理流程:
1.首先在线程中对连接进行事件监控
2.当连接有事件就绪则就进行事件处理(调用回调函数)
3.所有的就绪事件都处理完了,再去任务队列中将任务一一取出来执行。


如何保证处理连接的回调函数中的操作都是在同一个线程中进行的呢?


解决方法:任务队列
给每个Eventloop都添加一个任务队列。
对连接的所有操作(事件监控,事件处理,关闭连接等)都进行一次封装,向外部提供的连接操作并不直接执行,而是将连接的操作以任务形式压入到任务队列里。在这里插入图片描述

不过要注意并不是将所有回调函数都压入到任务队列中去,当前执行函数的线程如果就是该连接绑定的eventloop对应的线程,那么就可以直接对函数执行,不需要再压入到任务队列中,如果不一样那么就需要压入到任务队列中。

因为如果别的线程要执行当前的线程中的conn的对象的函数的时候,是不会执行的,因为会涉及到安全问题,所以就需要吧他执行的任务放到这个任务队列中去,让当前线程自己执行。


通知机制eventfd
因为有可能因为等待描述符IO事件就绪,导致执行流流程阻塞,这时候任务队列中的任务将得不到胁行因此得有一个事件通知的东西,能够唤醒事件监控的阻塞

当线程将任务压入到任务队列时,会存在这样情况,任务队列里有任务,但不能执行。
为什么呢?因为epoll会因为没有事件就绪而阻塞住,一旦阻塞住了就无法往后执行任务队列里的任务了。
所以我们就需要一个通知机制,当一旦任务队列中有任务时,就唤醒可能阻塞的epoll监控。

1.成员变量

private:// 每一个eventloop对应一个线程,当线程创建时,eventloop就会被创建出来并绑定线程的idstd::thread::id _thread_id;// eventloop是一个监控管理模块,里面封装了poller,用来监控所有的连接的事件Poller _poll;// 需要一个通知机制eventfd,用来唤醒可能因为IO事件监控没有事件就绪而阻塞,也就是epoll阻塞,需要唤醒它,往后执行任务队列里的任务int _event_fd;// eventfd也是一个描述符,也可以挂到poll里进行事件监控,poller监控的对象是Channel*,所以通过Channel来管理event_fdstd::unique_ptr<Channel> _eventfd_channel;// 每个eventllop中都有对应的任务队列,里面存放着外界调用的函数using Function = std::function<void()>;// 如果别的线程要执行当前的线程中的conn的对象的函数的时候,是不会执行的,因为会涉及到安全问题,所以就需要吧他执行的任务放到这个任务队列中去,让当前线程自己执行,std::vector<Function> _task;// 任务队列可能存在线程安全问题,所以需要一把锁保护它std::mutex _mutex;//eventloop中还存在的功能是定时任务,所以需要时间轮定时器TimerWheel _timer_wheel;

std::thread::id _thread_id;//线程ID

作用1:绑定线程
一个eventloop对应一个线程,当线程创建后,该线程就会创建eventloop,并且eventloop会绑定该线程的id。

作用2:用来标识当前线程是否与eventloop绑定的线程一致
如果连接的一个函数回调里面要执行一个任务,那么这个任务如果本身就是有eventloop对应的线程执行的,那么该任务是可以直接执行的,如果不是eventloop对应的线程执行的那么就需要压入到任务队列去。

当事件就绪,需要处理的时候,处理的过程中,如果需要对连接进行某些操作,那么这些操作必须在当前eventloop对应的线程中执行,而不能由其他eventloop线程执行,要保证对连接的各项操作都是线程安全的。
1.如果执行的操作本就在线程中,不需要将操作压入队列了,可以直接执行
2.如果执行的操作不再线程中,才需要加入任务池,等到事件处理完了然后执行任务

Poller _poll;

作用:对所有连接进行事件监控,以及事件处理

int _event_fd;

int _event_fd
std::unique_ptr _eventfd_channel;

eventfd事件通知机制,用来唤醒阻塞epoll。
eventfd也是一个文件描述符,则也可以挂到epoll上监控,即poller也可以对eventfd进行监控,而epoll监控的对象是channel,所以通过channel来管理eventfd。

std::vector<Function> _task;

using Function =std::function<void()>;
std::vector _task;
//任务队列可能存在线程安全问题,所以需要一把锁保护它
std::mutex _mutex;

作用:如果别的线程要执行当前的线程中的conn的对象的函数的时候,是不会执行的,因为会涉及到安全问题,所以就需要吧他执行的任务放到这个任务队列中去,让当前线程自己执行。

任务队列中的任务都是由其他线程通过调用RunInloop压入的函数。这个任务队列是存在线程安全的,所以在访问时,需要加锁访问。
不需要每次都一次一次的加锁访问任务队列,我们只需要加锁一次,然后定义一个临时的任务队列,将原来的任务队列中的任务全部交换到临时的中去,当全部取出来就可以解锁了。然后执行临时队列中的任务即可。

这里是引用
在这里插入图片描述
当获取到一个新连接之后,会调用这里的NewConnection函数,这个函数里面会为新连接创建connection,并且绑定eventloop,再下面这里设置非活跃超时销毁 以及 可读事件监听时,就会涉及当前线程,操作另一个线程的connection对应的操作函数

此时可能会有线程安全的问题,所以封装成任务加到队列里面,交由从属线程执行

TimerWheel _timer_wheel

eventloop另外一个主要功能就是管理着所有的定时任务,添加定时任务,刷新定时任务等。
所以封装一个时间轮定时器,用来对定时任务进行操作。
定时器里面有一个timerfd,本质也是一个计数器,一旦创建,内核就会每个一段事件往该描述符中发送次数。可以实现每秒钟执行一下任务。

2.EventLoop构造

需要构造的主要由线程id,事件通知event_fd以及管理该event_fd的channel对象。
需要创建一个eventfd,并且new一个channel对象管理event_fd,并设置该描述符事件就绪时的回调函数,设置可读监控。

public: // 构造EventLoop() : _thread_id(std::this_thread::get_id()),_event_fd(CreateEventFd()),_eventfd_channel(new Channel(this, _event_fd)),_timer_wheel(this){// 设置_event_fd的读回调函数,当事件就绪就会去执行_eventfd_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd, this));// 启动event_fd的读事件监控_eventfd_channel->EnableRead();}

3.针对eventfd的操作

对事件通知的操作主要有三个:
1.创建事件通知event_fd
2.构造event_fd可读事件就绪后的回调函数,即读取event_fd中的通知次数。
3.构造event_fd通知函数,也就是唤醒机制,本质上就是往event_fd描述符中发送一次通知。当发生一次数据,event_fd描述符的可读事件就会就绪,那么epoll就会被唤醒,往后执行。

public://针对通知机制event_fd的操作:创建eventfd,读取eventfd中数据(就绪回到函数),网eventfd中写数据(唤醒epoll)//1.创建通知机制event_fdstatic int CreateEventFd(){int efd=eventfd(0,EFD_CLOEXEC|EFD_NONBLOCK);if(efd<0){ERRLog("createeventfd failed");abort();//异常退出}return efd;}//2.构建event_fd的读事件回调函数,读取eventfd中的通知次数,单纯的就是读取出来,没有作用void ReadEventFd(){int res;ssize_t ret=(_event_fd,&res,sizeof(res));if(ret<0){// 有两种情况是合法的套接字是没有问题的if (errno == EAGAIN) // EAGAIN表示sock缓冲区中没有数据可读了return ;if (errno == EINTR) // EINTR表示在读取的过程中被信号中断了return ;ERRLog("readevent failed");abort();}}//3.构建唤醒机制,本质就是玩eventfd中发送一个数据,那么event的读事件就绪就会唤醒阻塞的epollvoid WakeupEventFd(){ int val=1;`在这里插入代码片`ssize_t ret=write(_event_fd,&val,sizeof(val));if(ret<0){// 有两种情况是合法的套接字是没有问题的if (errno == EAGAIN) // EAGAIN表示sock缓冲区中没有空间可写了return ;if (errno == EINTR) // EINTR表示在写入的过程中被信号中断了return ;ERRLog("wakeupevent failed");abort();}}

4.针对poller的操作

主要是封装poller中的添加/修改事件监控操作和移除事件监控操作。从而让channel对象可以调用Eventlooop中的事件监控操作对描述符进行监控。

public://针对poller的操作,要封装添加事件监控和移除事件监控的操作//1.添加或者修改某个连接的事件监控void UpdateEvent(Channel*channel){return _poll.UpdateEvent(channel);}     //2.移除某个连接的事件监控void RemoveEvent(Channel*channel){return _poll.RemoveEvent(channel);}channel对象中调用eventloop中的事件监控操作:
// 在Channel前面只是声明了Eventloop,但Channel不知道Eventloopr类里的成员函数,在类里无法调用Eventloop的对象函数,只能放在外面
void Channel::Update()
{return _loop->UpdateEvent(this);
}
void Channel::Remove()
{return _loop->RemoveEvent(this);
}

5.针对threadID的操作

主要判断当前的线程是否与eventloop对应的线程是一致的,如果是一致的那么对于连接的操作可以直接执行,如果不是就需要压入到任务对立中执行。

public://针对线程id的操作//用来判断当前的线程是否是创建eventloop的线程bool IsInLopp(){return _thread_id==std::this_thread::get_id();} 

6.针对TaskQueue的操作

对于任务队列的操作主要有:
1.将任务函数压入到任务队列中
2.根据线程id,决定是否真正压入到任务队列中
3.执行任务队列中的所有任务。

要注意在任务函数压入到任务队列中后,理论上队列中的任务就必须有当前线程去执行,但是会存在epoll因为没有事件就绪而阻塞住,所以压入一个任务后,就必须唤醒epoll。

在访问到任务队列时,都必须加锁访问,保证线程安全。
执行任务队列中的操作的思想是:先将任务中的所有任务都交换出来,再依次执行。

public://针对任务队列_task的操作//1.将外界的任务(函数)插入到任务队列里void TaskInLoop(Function cb){{std::unique_lock<std::mutex> _lock(_mutex);//规定一个作用域,并且顶一个临时的锁对象,在定义域内加锁,出来就解锁。_task.push_back(cb);}//当任务队列里有任务了就要把它执行掉,但存在epoll因为没有事件就绪而阻塞住无法执行任务队列里的任务//所以插入一个就需要唤醒epoll防止它在阻塞中WakeupEventFd();}//2.有的任务是不用压入到任务队列里执行的,如果是eventloop对应的线程调用外部函数,则就直接去执行,如果是其他线程调用的,就需要压入到任务队列里去//所以外面真正调用的都是这个接口void RunInLoop(Function cb){if(IsInLopp())return cb();//如果是eventloop对应的线程调用,则直接执行//如果不是则需要压入对eventloop的任务队列里执行return TaskInLoop(cb);}//3.执行任务队列里的任务void RunAllTask(){//利用一个临时vector,将task里面的任务全部交换出来,再全部执行std::vector<Function> temp_task;{//访问任务队列就需要加锁std::unique_lock<std::mutex> _lock(_mutex);_task.swap(temp_task);}//执行任务队列里的任务for(auto &task:temp_task){task();} }

7.针对定时器的操作

针对定时器的操作很简单,就是添加定时任务,刷新定时任务(本质就是延迟事件),终止定时任务,判断是否有该定时任务等操作。
就是封装一个定时器的任务即可。

public://针对定时器timer_wheel的操作//1.添加定时任务void AddTask(uint64_t id, uint32_t timeout, const TaskFunc cb){return _timer_wheel.AddTask(id,timeout,cb);}//2.刷新,延迟定时任务void RefreshTask(uint64_t id){return _timer_wheel.RefreshTask(id);}//3.终止定时任务void CancelTimer(uint64_t id){return _timer_wheel.CancelTimer(id);}//4.查看是否有该定时任务bool HasTimer(uint64_t id){return _timer_wheel.HasTimer(id);}

8.EventLoop的主要工作

eventloop的主要工作:
1.对描述符进行事件监控
2.描述符的事件就绪,则进行事件处理
3.执行任务队列中的任务。
循环往后的执行。

public://Eventloop的主要任务void Start(){while(1){//1.进行事件监控,并获取就绪的事件和fdstd::vector<Channel*> active;_poll.Poll(&active);//2.进行就绪事件处理for(auto &it: active){it->HandlerEvent();}//3.执行任务队列里的任务RunAllTask();}}   
};

二.全部代码

class EventLoop
{
private://每一个eventloop对应一个线程,当线程创建时,eventloop就会被创建出来并绑定线程的idstd::thread::id _thread_id;    //eventloop是一个监控管理模块,里面封装了poller,用来监控所有的连接的事件Poller _poll;//需要一个通知机制eventfd,用来唤醒可能因为IO事件监控没有事件就绪而阻塞,也就是epoll阻塞,需要唤醒它,往后执行任务队列里的任务int _event_fd;//eventfd也是一个描述符,也可以挂到poll里进行事件监控,poller监控的对象是Channel*,所以通过Channel来管理event_fdstd::unique_ptr<Channel> _eventfd_channel;//每个eventllop中都有对应的任务队列,里面存放着外界调用的函数using Function =std::function<void()>;//如果别的线程要执行当前的线程中的conn的对象的函数的时候,是不会执行的,因为会涉及到安全问题,所以就需要吧他执行的任务放到这个任务队列中去,让当前线程自己执行,std::vector<Function> _task;//任务队列可能存在线程安全问题,所以需要一把锁保护它std::mutex _mutex;public: //构造EventLoop():_thread_id(std::this_thread::get_id()),_event_fd(CreateEventFd()),_eventfd_channel(new Channel(this,_event_fd)){//设置_event_fd的读回调函数,当事件就绪就会去执行_eventfd_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd,this));//启动event_fd的读事件监控_eventfd_channel->EnableRead();}
public://针对通知机制event_fd的操作:创建eventfd,读取eventfd中数据(就绪回到函数),网eventfd中写数据(唤醒epoll)//1.创建通知机制event_fdstatic int CreateEventFd(){int efd=eventfd(0,EFD_CLOEXEC|EFD_NONBLOCK);if(efd<0){ERRLog("createeventfd failed");abort();//异常退出}return efd;}//2.构建event_fd的读事件回调函数,读取eventfd中的通知次数,单纯的就是读取出来,没有作用void ReadEventFd(){int res;ssize_t ret=(_event_fd,&res,sizeof(res));if(ret<0){// 有两种情况是合法的套接字是没有问题的if (errno == EAGAIN) // EAGAIN表示sock缓冲区中没有数据可读了return ;if (errno == EINTR) // EINTR表示在读取的过程中被信号中断了return ;ERRLog("readevent failed");abort();}}//3.构建唤醒机制,本质就是玩eventfd中发送一个数据,那么event的读事件就绪就会唤醒阻塞的epollvoid WakeupEventFd(){ int val=1;ssize_t ret=write(_event_fd,&val,sizeof(val));if(ret<0){// 有两种情况是合法的套接字是没有问题的if (errno == EAGAIN) // EAGAIN表示sock缓冲区中没有空间可写了return ;if (errno == EINTR) // EINTR表示在写入的过程中被信号中断了return ;ERRLog("wakeupevent failed");abort();}}
public://针对poller的操作,要封装添加事件监控和移除事件监控的操作//1.添加或者修改某个连接的事件监控void UpdateEvent(Channel*channel){return _poll.UpdateEvent(channel);}     //2.移除某个连接的事件监控void RemoveEvent(Channel*channel){return _poll.RemoveEvent(channel);}
public://针对线程id的操作//用来判断当前的线程是否是创建eventloop的线程bool IsInLopp(){return _thread_id==std::this_thread::get_id();} 
public://针对任务队列_task的操作//1.将外界的任务(函数)插入到任务队列里void TaskInLoop(Function cb){{std::unique_lock<std::mutex> _lock(_mutex);//规定一个作用域,并且顶一个临时的锁对象,在定义域内加锁,出来就解锁。_task.push_back(cb);}//当任务队列里有任务了就要把它执行掉,但存在epoll因为没有事件就绪而阻塞住无法执行任务队列里的任务//所以插入一个就需要唤醒epoll防止它在阻塞中WakeupEventFd();}//2.有的任务是不用压入到任务队列里执行的,如果是eventloop对应的线程调用外部函数,则就直接去执行,如果是其他线程调用的,就需要压入到任务队列里去//所以外面真正调用的都是这个接口void RunInLoop(Function cb){if(IsInLopp())cb();//如果是eventloop对应的线程调用,则直接执行//如果不是则需要压入对eventloop的任务队列里执行TaskInLoop(cb);}//3.执行任务队列里的任务void RunAllTask(){//利用一个临时vector,将task里面的任务全部交换出来,再全部执行std::vector<Function> temp_task;{//访问任务队列就需要加锁std::unique_lock<std::mutex> _lock(_mutex);_task.swap(temp_task);}//执行任务队列里的任务for(auto &task:temp_task){task();} }
public://Eventloop的主要任务void Start(){while(1){//1.进行事件监控,并获取就绪的事件和fdstd::vector<Channel*> active;_poll.Poll(&active);//2.进行就绪事件处理for(auto &it: active){it->HandlerEvent();}//3.执行任务队列里的任务RunAllTask();}}   };
#include "../Server.hpp"void Handleclose(Channel *channel)
{ERRLog("close:%d",channel->Fd());channel->Remove(); // 移除监控delete channel;
}
void HandleRead(Channel *channel)
{int fd = channel->Fd();char buf[1024] = {0};int ret = recv(fd, buf, 1023, 0);if (ret <= 0){return Handleclose(channel);}buf[ret] = 0;ERRLog("%s",buf);channel->EnableWrite(); // 启动可写事件
}
void HandleError(Channel *channel)
{return Handleclose(channel); // 关闭释放
}
void HandleEvent(EventLoop* loop,Channel *channel,uint64_t timerid)
{loop->RefreshTask(timerid);
}
void HandleWrite(Channel *channel)
{int fd = channel->Fd();const char *data = "陶恩威你好呀";int ret = send(fd, data, strlen(data), 0);if (ret < 0){return Handleclose(channel); // 关闭释放}channel->DisableWrite(); // 关闭写监控
}
void Acceptor(EventLoop *loop, Channel *lst_channel)
{int fd = lst_channel->Fd();int newfd = accept(fd, NULL, NULL);if (newfd < 0)return;uint64_t timerid=rand()%1000;Channel *channel = new Channel(loop, newfd);//获得一个新连接并挂到loop上channel->SetReadCallback(std::bind(HandleRead, channel));    // 为通信套接字设置可读事件的回调函数channel->SetWriteCallback(std::bind(HandleWrite, channel));  // 可写事件的回调函数channel->SetCloseCallback(std ::bind(Handleclose, channel)); // 关闭事件的回调函数channel->SetErrorCallback(std::bind(HandleError, channel));  // 关闭事件的回调函数channel->SetEventCallback(std::bind(HandleEvent, loop,channel,timerid));  // 关闭事件的回调函数//定时器的设置必须要在启动监听之前,防止先监听后立刻有了事件,但还没定时任务。该连接就会多存活loop->AddTask(timerid,10,std::bind(Handleclose,channel));channel->EnableRead();
}
int main()
{//Poller poll;srand(time(NULL));EventLoop loop;Socket lst_sock;lst_sock.CreateServer(8886);Channel *lst_channel = new Channel(&loop, lst_sock.Fd());              // 为listen套接字创建一个channel对象lst_channel->SetReadCallback(std::bind(Acceptor, &loop, lst_channel)); // 为listen套接字设置读回调函数Accept,用来接收新连接,并创建Channel对象,添加事件监控,启动写事件监控等lst_channel->EnableRead();                                             // 启动读事件监控loop.Start();lst_sock.Close();return 0;}
#include "../Server.hpp"
int main()
{Socket cli_sock;cli_sock.CreateClient(8886, "112.126.85.136");for(int i=0;i<5;i++){std::string str = "tew";cli_sock.Send(str.c_str(), str.size());char buf[1024] = {0};int n = cli_sock.Recv(buf, 1023);buf[n] = 0;DBG_LOG("%s", buf);sleep(1);}while(1)sleep(1);return 0;
}

在这里插入图片描述

这个简单的服务器流程,就是一开始创建了一个eventloop。然后创建出监听连接,并绑定该eventloop,然后就设置该连接的回调函数。
最后启动lst_channel->EnableRead(); 这个函数就是将该套接字挂到poller上去进行事件监听。当有新连接时,读事件就绪,就会调用Acceptor,在Acceptor中,首先会创建一个新的套接字,并且也绑定该eventloop,然后设置对应的回调函数,最后启动channel->EnableRead();这个函数就是将新的套接字挂到poller上去进行事件监听。当有数据来时,读事件就会就绪。

添加定时销毁就是将任务添加到时间轮里,然后连接每操作一次都会调用一次任意事件,进行刷新时长。

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

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

相关文章

Redis 加锁、解锁

Redis 加锁和解锁的应用 上代码 应用调用示例 RedisLockEntity lockEntityYlb RedisLockEntity.builder().lockKey(TradeConstants.HP_APP_AMOUNT_LOCK_PREFIX appUser.getAccount()).value(orderId).build();boolean isLockedYlb false;try {if (redisLock.tryLock(lockE…

在 Windows 上为 WSL 增加 root 账号密码并通过 Shell 工具连接

1. 为 WSL 设置 root 用户密码 在 Windows 上使用 WSL&#xff08;Windows Subsystem for Linux&#xff09;时&#xff0c;默认情况下并没有启用 root 账号的密码。为了通过 SSH 或其他工具以 root 身份连接到 WSL&#xff0c;我们需要为 root 用户设置密码。 设置 root 密码步…

2730、找到最长的半重复子字符穿

题目&#xff1a; 解答&#xff1a; 窗口为[left&#xff0c;right]&#xff0c;ans为窗口长度&#xff0c;same为子串长度&#xff0c;窗口满足题设条件&#xff0c;即只含一个连续重复字符&#xff0c;则更新ans&#xff0c;否则从左边开始一直弹出&#xff0c;直到满足条件…

MCP Java SDK源码分析

MCP Java SDK源码分析 一、引言 在当今人工智能飞速发展的时代&#xff0c;大型语言模型&#xff08;LLMs&#xff09;如GPT - 4、Claude等展现出了强大的语言理解和生成能力。然而&#xff0c;这些模型面临着一个核心限制&#xff0c;即无法直接访问外部世界的数据和工具。M…

[Linux]内核如何对信号进行捕捉

要理解Linux中内核如何对信号进行捕捉&#xff0c;我们需要很多前置知识的理解&#xff1a; 内核态和用户态的区别CPU指令集权限内核态和用户态之间的切换 由于文章的侧重点不同&#xff0c;上面这些知识我会在这篇文章尽量详细提及&#xff0c;更详细内容还得请大家查看这篇…

设计模式-观察者模式、命令模式

观察者模式Observer&#xff08;观察者&#xff09;—对象行为型模式定义&#xff1a;定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象,在它的状态发生变化时,会通知所有的观察者.先将 Observer A B C 注册到 Observable &#xff0c;那么当 Observable 状态…

【Unity笔记01】基于单例模式的简单UI框架

单例模式的UIManagerusing System.Collections; using System.Collections.Generic; using UnityEngine;public class UIManager {private static UIManager _instance;public Dictionary<string, string> pathDict;public Dictionary<string, GameObject> prefab…

深入解析 OPC UA:工业自动化与物联网的关键技术

在当今快速发展的工业自动化和物联网&#xff08;IoT&#xff09;领域&#xff0c;数据的无缝交换和集成变得至关重要。OPC UA&#xff08;Open Platform Communications Unified Architecture&#xff09;作为一种开放的、跨平台的工业通信协议&#xff0c;正在成为这一领域的…

MCP 协议的未来发展趋势与学习路径

MCP 协议的未来发展趋势 6.1 MCP 技术演进与更新 MCP 协议正在快速发展&#xff0c;不断引入新的功能和改进。根据 2025 年 3 月 26 日发布的协议规范&#xff0c;MCP 的最新版本已经引入了多项重要更新&#xff1a; 1.HTTP Transport 正式转正&#xff1a;引入 Streamable …

硬件嵌入式学习路线大总结(一):C语言与linux。内功心法——从入门到精通,彻底打通你的任督二脉!

嵌入式工程师学习路线大总结&#xff08;一&#xff09; 引言&#xff1a;C语言——嵌入式领域的“屠龙宝刀”&#xff01; 兄弟们&#xff0c;如果你想在嵌入式领域闯出一片天地&#xff0c;C语言就是你手里那把最锋利的“屠龙宝刀”&#xff01;它不像Python那样优雅&#xf…

MCP server资源网站去哪找?国内MCP服务合集平台有哪些?

在人工智能飞速发展的今天&#xff0c;AI模型与外部世界的交互变得愈发重要。一个好的工具不仅能提升开发效率&#xff0c;还能激发更多的创意。今天&#xff0c;我要给大家介绍一个宝藏平台——AIbase&#xff08;<https://mcp.aibase.cn/>&#xff09;&#xff0c;一个…

修改Spatial-MLLM项目,使其专注于无人机航拍视频的空间理解

修改Spatial-MLLM项目&#xff0c;使其专注于无人机航拍视频的空间理解。以下是修改方案和关键代码实现&#xff1a; 修改思路 输入处理&#xff1a;将原项目的视频文本输入改为单一无人机航拍视频/图像输入问题生成&#xff1a;自动生成空间理解相关的问题&#xff08;无需用户…

攻防世界-Reverse-insanity

知识点 1.ELF文件逆向 2.IDApro的使用 3.strings的使用 步骤 方法一&#xff1a;IDA 使用exeinfo打开&#xff0c;发现是32位ELF文件&#xff0c;然后用ida32打开。 找到main函数&#xff0c;然后F5反编译&#xff0c;得到flag。 tip&#xff1a;该程序是根据随机函数生成…

【openp2p】 学习1:P2PApp和优秀的go跨平台项目

P2PApp下面给出一个基于 RESTful 风格的 P2PApp 管理方案示例,供二次开发或 API 对接参考。核心思路就是把每个 P2PApp 当成一个可创建、查询、修改、启动/停止、删除的资源来管理。 一、P2PApp 资源模型 P2PApp:id: string # 唯一标识name: string # …

边缘设备上部署模型的限制之一——显存占用:模型的参数量只是冰山一角

边缘设备上部署模型的限制之一——显存占用&#xff1a;模型的参数量只是冰山一角 在边缘设备上部署深度学习模型已成为趋势&#xff0c;但资源限制是其核心挑战之一。其中&#xff0c;显存&#xff08;或更广义的内存&#xff09;占用是开发者们必须仔细考量的重要因素。许多…

脑机新手指南(二十一)基于 Brainstorm 的 MEG/EEG 数据分析(上篇)

一、脑机接口与神经电生理技术概述 脑机接口&#xff08;Brain-Computer Interface, BCI&#xff09;是一种在大脑与外部设备之间建立直接通信通道的技术&#xff0c;它通过采集和分析大脑信号来实现对设备的控制或信息的输出。神经电生理信号作为脑机接口的重要数据来源&…

[Linux]内核态与用户态详解

内核态和用户态是针对CPU状态的描述。在内核态可以执行一切特权代码&#xff0c;在用户态只能执行那些受限级别的代码。如果需要调用特权代码需要进行内核态切换。 一、内核态和用户态概况 内核态&#xff1a; 系统中既有操作系统的程序&#xff0c;也有普通用户程序。为了安…

如何查看每个磁盘都安装了哪些软件或程序并卸载?

步骤如下&#xff1a; 1、点击电脑桌面左下角&#xff1a; 2、选择【应用和功能】 3、点击下拉框&#xff0c;选择想要查看的磁盘&#xff0c;下方显示的就是所有C磁盘下安装的软件和程序 卸载方法&#xff1a; 点击对应的应用&#xff0c;然后点击卸载即可&#xff1a;

记录一次莫名奇妙的跨域502(badgateway)错误

这里图片加载不了&#xff0c;原文请访问&#xff1a;原文链接 公司的项目&#xff0c;这几天添加了一个统计功能&#xff0c; 本地测试没太大问题&#xff0c;上线后有一个问题&#xff0c;具体现象描述如下&#xff1a; 统计首页接口大约有5-6个&#xff0c;也就是同时需要…

Linux之线程

Linux之线程 线程之形线程接口线程安全互斥锁条件变量&信号量生产者与消费者模型线程池 线程之形 进程是资源分配的基本单位&#xff0c;而线程是进程内部的一个执行单元&#xff0c;也是 CPU 调度的基本单位。 线程之间共享进程地址空间、文件描述符与信号处理&#xff0…