ASIO IO_CONTEXT 源码整理

news2024/12/26 23:57:00

io_context关系图

在这里插入图片描述

io_context

在这里插入图片描述

io_context::io_context()
  : impl_(add_impl(new impl_type(*this,
          ASIO_CONCURRENCY_HINT_DEFAULT, false)))
{
}

io_context::io_context(int concurrency_hint): impl_(add_impl(new impl_type(*this, concurrency_hint == 1 ? ASIO_CONCURRENCY_HINT_1 : concurrency_hint, false)))
{
}

io_context::impl_type& io_context::add_impl(io_context::impl_type* impl)
{
  asio::detail::scoped_ptr<impl_type> scoped_impl(impl);
  asio::add_service<impl_type>(*this, scoped_impl.get());
  return *scoped_impl.release();
}

io_context继承自execution_context,execution_context内部维护了一个service的管理器services_register。 因为sheduler继承自service, 所以可以看到在io_context的构造中,会通过add_service 将impl_添加到 services_register中,之后strand、socket等都会通过UseService这个函数将自身添加到这个链表当中(后面具体介绍到strand的时候在具体说明)。

io_context 对外提供了一批的接口,但实际上都送hi由impl_这个成员变量来实现具体操作。在io_context.ipp中可以看到run,poll等主要行为都是impl的封装。

io_context::count_type io_context::run()
{
  asio::error_code ec;
  count_type s = impl_.run(ec);
  asio::detail::throw_error(ec);
  return s;
}

io_context::count_type io_context::poll()
{
  asio::error_code ec;
  count_type s = impl_.poll(ec);
  asio::detail::throw_error(ec);
  return s;
}

scheduler

scheduler 的实现是在scheduler.ipp 下。

构造函数

scheduler::scheduler(asio::execution_context& ctx,
    int concurrency_hint, bool own_thread)
  : asio::detail::execution_context_service_base<scheduler>(ctx),
    one_thread_(concurrency_hint == 1
        || !ASIO_CONCURRENCY_HINT_IS_LOCKING(
          SCHEDULER, concurrency_hint)
        || !ASIO_CONCURRENCY_HINT_IS_LOCKING(
          REACTOR_IO, concurrency_hint)),
    mutex_(ASIO_CONCURRENCY_HINT_IS_LOCKING(
          SCHEDULER, concurrency_hint)),
    task_(0),
    task_interrupted_(true),
    outstanding_work_(0),
    stopped_(false),
    shutdown_(false),
    concurrency_hint_(concurrency_hint),
    thread_(0)
{
  ASIO_HANDLER_TRACKING_INIT;

  if (own_thread)
  {
    ++outstanding_work_;
    asio::detail::signal_blocker sb;
    thread_ = new asio::detail::thread(thread_function(this));
  }
}

可以看到构造函数参数由3个,一个是context的引用,concurrency_hint_ 好像没在代码中看到由什么作用,而own_thread是用来标识schedule是否有自己的线程,不过io_context创建的schedule这个参数都是false。

run

std::size_t scheduler::run(asio::error_code& ec)
{
  ec = asio::error_code(); 
  if (outstanding_work_ == 0)
  {
    stop();
    return 0;
  }

  thread_info this_thread;
  this_thread.private_outstanding_work = 0;
  thread_call_stack::context ctx(this, this_thread);
  
  mutex::scoped_lock lock(mutex_);
  std::size_t n = 0;
  for (; do_run_one(lock, this_thread, ec); lock.lock())
    if (n != (std::numeric_limits<std::size_t>::max)())
      ++n;
  return n;
}

调用io_context的run 实际上执行的是scheduler的run,可以看到run做的事情不多,一个是检查outstanding_work_ 是否为0,如果是0则说明没有任务需要执行;第二步是为执行run的线程单独设立一个栈,然后将scheduler 和 线程信息放入到栈内,这边使用方法会比较复杂,主要是使用线程特有存储的方式,因为这个部分没太大影响,所以细节先不深入。最后获取锁然后循环执行do_run_one。

do_run_one

std::size_t scheduler::do_run_one(mutex::scoped_lock& lock,
    scheduler::thread_info& this_thread,
    const asio::error_code& ec)
{
  while (!stopped_)
  {
    if (!op_queue_.empty())  // 判断队列是否为空
    {
      // 不为空则取出头部
      // Prepare to execute first handler from queue.
      operation* o = op_queue_.front();
      op_queue_.pop();
      bool more_handlers = (!op_queue_.empty());  // 判断队列中是否还有剩余的任务
		
      if (o == &task_operation_)   //  判断是否是epoll标识的任务
      {
        task_interrupted_ = more_handlers;     

        if (more_handlers && !one_thread_)  // 如果还有其他任务,且不止一个线程
          wakeup_event_.unlock_and_signal_one(lock);  // 释放锁,并唤醒另一个线程
        else
          lock.unlock();  // 正常释放锁

        task_cleanup on_exit = { this, &lock, &this_thread };   //  构造一个IO任务的清理结构体,RAII方式析构时执行清理动作
        (void)on_exit;  

        // Run the task. May throw an exception. Only block if the operation
        // queue is empty and we're not polling, otherwise we want to return
        // as soon as possible.
        task_->run(more_handlers ? 0 : -1, this_thread.private_op_queue);  // 执行epoll的run,实际上时epoll_wait的封装
      }
      else
      {
        std::size_t task_result = o->task_result_;     // 获取临时数据,主要和 IO事件有关

        if (more_handlers && !one_thread_)  // 如果还有其他任务,且不止一个线程
          wake_one_thread_and_unlock(lock);    // 解锁并唤醒一条线程
        else
          lock.unlock();   //  解锁

        // Ensure the count of outstanding work is decremented on block exit.
        work_cleanup on_exit = { this, &lock, &this_thread };  // 构造一个普通任务的清理结构体,RAII方式析构时执行清理动作
        (void)on_exit;

        // Complete the operation. May throw an exception. Deletes the object.
        o->complete(this, ec, task_result);   // 执行任务
        this_thread.rethrow_pending_exception(); 

        return 1;
      }
    }
    else
    {
      wakeup_event_.clear(lock);    //释放锁
      wakeup_event_.wait(lock);   // 等待唤醒
    }
  }

  return 0;
}

当执行do_run_one时,队列中没有任务的话,线程就会执行通过wakeup_event_来等待唤醒,wakeup_event_在linux下是由posix_event实现,posix_event内部维护了::pthread_cond_t条件变量,所以线程会通过条件变量来实现等待和唤醒。

当队列中有任务得时候,如果不是task_operation_,则获取其task_result,然后执行其complete。任务的类型是scheduler_operation,后面介绍Post的时候会再详细说scheduler_operation内部情况,这边认为是执行任务队列中相对应的任务即可。

epoll

当队列中任务是task_operation_时,会调用task->run,这个task_在linux下类型是epoll_reactor,run实际上是对epoll_wait的封装。

void epoll_reactor::run(long usec, op_queue<operation>& ops)
{
	……
  // Calculate timeout. Check the timer queues only if timerfd is not in use.
  int timeout;  // 计算需要等待的时间
  if (usec == 0)
    timeout = 0;
  else
  {
    timeout = (usec < 0) ? -1 : ((usec - 1) / 1000 + 1);
    if (timer_fd_ == -1)
    {
      mutex::scoped_lock lock(mutex_);
      timeout = get_timeout(timeout);
    }
  }

  // Block on the epoll descriptor.
  epoll_event events[128];
  int num_events = epoll_wait(epoll_fd_, events, 128, timeout);  // epoll_wait
  ……

  // Dispatch the waiting events.
  for (int i = 0; i < num_events; ++i)
  {
    void* ptr = events[i].data.ptr;
    if (ptr == &interrupter_)   
    {
		 // 如果描述符是否打断标记的,则什么都不做
    }
    else
    {
      descriptor_state* descriptor_data = static_cast<descriptor_state*>(ptr);
      if (!ops.is_enqueued(descriptor_data))  // 判断任务是否已经在私有队列中了,
      {
        descriptor_data->set_ready_events(events[i].events);  //设置对应的事件
        ops.push(descriptor_data);  加入私有队列
      }
      else
      {
        descriptor_data->add_ready_events(events[i].events);  //额外添加对应事件
      }
    }
  }
  }
}

  class descriptor_state : operation
  { 
     …………
     void set_ready_events(uint32_t events) { task_result_ = events; }
     void add_ready_events(uint32_t events) { task_result_ |= events; }
     …………
  }

调用task_->run的时候,传入的参数有两个,一个是等待的时间,另一个是线程的私有队列。当scheduler的公共队列中有其他任务时,则传入0,调用不会阻塞。不然执行的线程就会一直等待在epoll_wait上,直到有I/O任务完成或者被中断。如果是正常的任务完成,则将任务放入私有队列等待后续放回倒scheduler的公共队列中。

这边说下asio这里是怎么做到打断epoll_wait的,

epoll_reactor::epoll_reactor(...){
	....
	  // Add the interrupter's descriptor to epoll.
	  epoll_event ev = { 0, { 0 } };
	  ev.events = EPOLLIN | EPOLLERR | EPOLLET;
	  ev.data.ptr = &interrupter_;
	  epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, interrupter_.read_descriptor(), &ev);
	  interrupter_.interrupt();
	....
	
}

void epoll_reactor::interrupt()
{
  epoll_event ev = { 0, { 0 } };
  ev.events = EPOLLIN | EPOLLERR | EPOLLET;
  ev.data.ptr = &interrupter_;
  epoll_ctl(epoll_fd_, EPOLL_CTL_MOD, interrupter_.read_descriptor(), &ev);
}

在epoll_reactor的构造函数里,会通过pipe两个fd,一个用来读,一个用来写,保存在interrupter_中,并在构造时就将其塞入epoll中。然后在写的fd中写入8个字节。因为触发模式的ET的,所以epoll中不会一直触发。当有线程执行interrupt函数的时候,会刷新读fd的描述符,导致阻塞在epoll_wait的线程被打断返回。

task_cleanup, work_cleanup

struct scheduler::task_cleanup
{
  ~task_cleanup()
  {
    if (this_thread_->private_outstanding_work > 0) 
    {
      asio::detail::increment(
          scheduler_->outstanding_work_,
          this_thread_->private_outstanding_work);
    }
    this_thread_->private_outstanding_work = 0;
    
    lock_->lock();
    scheduler_->task_interrupted_ = true;
    scheduler_->op_queue_.push(this_thread_->private_op_queue);
    scheduler_->op_queue_.push(&scheduler_->task_operation_);
  }

  scheduler* scheduler_;
  mutex::scoped_lock* lock_;
  thread_info* this_thread_;
};

struct scheduler::work_cleanup
{
  ~work_cleanup()
  {
    if (this_thread_->private_outstanding_work > 1)
    {
      asio::detail::increment(
          scheduler_->outstanding_work_,
          this_thread_->private_outstanding_work - 1);
    }
    else if (this_thread_->private_outstanding_work < 1)
    {
      scheduler_->work_finished();
    }
    this_thread_->private_outstanding_work = 0;

#if defined(ASIO_HAS_THREADS)
    if (!this_thread_->private_op_queue.empty())
    {
      lock_->lock();
      scheduler_->op_queue_.push(this_thread_->private_op_queue);
    }
#endif // defined(ASIO_HAS_THREADS)
  }

  scheduler* scheduler_;
  mutex::scoped_lock* lock_;
  thread_info* this_thread_;
};

task_cleanup, work_cleanup 内容级别上差不多,只是一个是清理epoll任务的结构,一个是清理普通任务的结构。主要执行的逻辑是将自身的private_outstanding_work累加到scheduler的outstanding_work上,并将私有队列的任务都移动到scheduler的公共队列上去。

poll

poll 实际上和run 基本一致,区别只是poll调用的是do_poll_one, 而do_poll_one 里也只是将do_run_one里的while循环去掉,且在poll前判断队列里是否有任务,没有直接返回。可以认为poll是不会阻塞的。

post

看完上面的执行任务的过程,这边开始看一下投递任务的过程。投递任务可以使用post 和 dispatch,这边先看post

template <typename LegacyCompletionHandler>
ASIO_INITFN_AUTO_RESULT_TYPE(LegacyCompletionHandler, void ())
io_context::post(ASIO_MOVE_ARG(LegacyCompletionHandler) handler)
{
  return async_initiate<LegacyCompletionHandler, void ()>(
      initiate_post(), handler, this);
}

async_initiate 函数会对参数和返回值类型进行一些处理,然后对于进行调用。这边initiate_post是一个struct, 所以会调用其operator()。

struct io_context::initiate_post
{
  template <typename LegacyCompletionHandler>
  void operator()(ASIO_MOVE_ARG(LegacyCompletionHandler) handler,
      io_context* self) const
  {
    // If you get an error on the following line it means that your handler does
    // not meet the documented type requirements for a LegacyCompletionHandler.
    ASIO_LEGACY_COMPLETION_HANDLER_CHECK(LegacyCompletionHandler, handler) type_check;

    detail::non_const_lvalue<LegacyCompletionHandler> handler2(handler);  // 非const

    bool is_continuation = asio_handler_cont_helpers::is_continuation(handler2.value);

	// 这边非常绕,实际上是ASIO的内存管理机制。
    typedef detail::completion_handler<
      typename decay<LegacyCompletionHandler>::type, executor_type> op;
    typename op::ptr p = { detail::addressof(handler2.value),
        op::ptr::allocate(handler2.value), 0 };
    p.p = new (p.v) op(handler2.value, self->get_executor());

    ASIO_HANDLER_CREATION((*self, *p.p, "io_context", self, 0, "post"));

    self->impl_.post_immediate_completion(p.p, is_continuation); // 最终调用scheduler的post_immediate_completion
    p.v = p.p = 0;
  }
};

上面的代码非常复杂和繁琐,实际上是为了使用ASIO的内存管理机制生成一个completion_handler类型的对象。生成完对象后,通过post_immediate_completion丢入到scheduler的公共队列中。

scheduler_operation

在这里插入图片描述
scheduler_operation 是scheduler 执行任务的基本单位。scheduler_operation主要目得是为了将外部函数的类型进行擦除,对scheduler来说调度时为统一的类型。不过其各个子类中会对原始的函数进行二次擦除。

template <typename Handler, typename IoExecutor>
class completion_handler : public operation
{
public:
  ASIO_DEFINE_HANDLER_PTR(completion_handler);

  completion_handler(Handler& h, const IoExecutor& io_ex)
    : operation(&completion_handler::do_complete), // 构造时传入的实际是这个类的do_complete函数
      handler_(ASIO_MOVE_CAST(Handler)(h)),
      work_(handler_, io_ex)
  {
  }

  static void do_complete(void* owner, operation* base,
      const asio::error_code& /*ec*/,
      std::size_t /*bytes_transferred*/)
  {
    // 整个过程包装非常复杂,更具体的细节我也没深究
    // Take ownership of the handler object.
    completion_handler* h(static_cast<completion_handler*>(base));
    ptr p = { asio::detail::addressof(h->handler_), h, h };

    ASIO_HANDLER_COMPLETION((*h));
    // Take ownership of the operation's outstanding work.
    handler_work<Handler, IoExecutor> w( ASIO_MOVE_CAST2(handler_work<Handler, IoExecutor>)(h->work_));
    Handler handler(ASIO_MOVE_CAST(Handler)(h->handler_));
    p.h = asio::detail::addressof(handler);
    p.reset();

    // Make the upcall if required.
    if (owner)
    {
      fenced_block b(fenced_block::half);
      ASIO_HANDLER_INVOCATION_BEGIN(());
      w.complete(handler, handler);   // 执行对应函数
      ASIO_HANDLER_INVOCATION_END;
    }
  }
  private:
  Handler handler_;
  handler_work<Handler, IoExecutor> work_;
};



class scheduler_operation ASIO_INHERIT_TRACKED_HANDLER
{
public:
  typedef scheduler_operation operation_type;

  void complete(void* owner, const asio::error_code& ec,
      std::size_t bytes_transferred)
  {
    func_(owner, this, ec, bytes_transferred);  //执行构造函数时传入的函数
  }

protected:
  typedef void (*func_type)(void*,
      scheduler_operation*,
      const asio::error_code&, std::size_t);

  scheduler_operation(func_type func)
    : next_(0),
      func_(func),
      task_result_(0)
  {
  }

private:
  friend class op_queue_access;
  scheduler_operation* next_;
  func_type func_;
protected:
  friend class scheduler;
  unsigned int task_result_; // Passed into bytes transferred.
};

scheduler_operation 的complete过程还是比较简单的, 执行传入的函数,并将scheduler、operation、errcode、bytes_transferred 4个参数传入。但因为字类实际上包装的时候,传入的时直接的do_complete函数,其内部包装非常复杂,目前我也还没特别搞清楚。

post_immediate_completion

void scheduler::post_immediate_completion(
    scheduler::operation* op, bool is_continuation)
{
#if defined(ASIO_HAS_THREADS)
  if (one_thread_ || is_continuation)
  {
    if (thread_info_base* this_thread = thread_call_stack::contains(this))
    {
      ++static_cast<thread_info*>(this_thread)->private_outstanding_work;
      static_cast<thread_info*>(this_thread)->private_op_queue.push(op);
      return;
    }
  }
#else // defined(ASIO_HAS_THREADS)
  (void)is_continuation;
#endif // defined(ASIO_HAS_THREADS)

  work_started();
  mutex::scoped_lock lock(mutex_);
  op_queue_.push(op);
  wake_one_thread_and_unlock(lock);
}

执行到post_immediate_completion,可以看出传入的时operation 的指针, 宏定义内部是对单线程的优化,但schduler指定单线程运行的时候,这边就会使用无锁版本进行插入。多线程下,可以看到需要先获取到锁,然后插入到公共队列中。然后执行wake_one_thread_and_unlock 尝试唤醒一条线程。

wake_one_thread_and_unlock

void scheduler::wake_one_thread_and_unlock(
    mutex::scoped_lock& lock)
{
  if (!wakeup_event_.maybe_unlock_and_signal_one(lock))
  {
    if (!task_interrupted_ && task_)
    {
      task_interrupted_ = true;
      task_->interrupt();
    }
    lock.unlock();
  }
}

唤醒线程的方式也很简单,先尝试使用信号量唤醒等待中的线程,如果没有线程等待在信号量上的话,且当前没有打断过epoll,那么就会尝试对阻塞在epoll上的线程进行打断。

总结

io_context中主要的代码流程还是简单的梳理了一下,如果不太看重各个位置实现的细节的话吗,io_context的代码还是稍微能看懂一点的,但如果要对细节也了如指掌的话,确实很有难度。尤其是关于operation申请内存和调用的位置确实没怎么懂;不过我这边整理得不是很好,都是看到哪里就整理到哪里,看起来会比较得乱。

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

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

相关文章

一千元以内的蓝牙耳机推荐,2023年最值得入手的蓝牙耳机分享

对于蓝牙耳机的选购技巧&#xff0c;我还是比较了解的&#xff0c;也知道有哪些蓝牙耳机比较好用&#xff0c;音质也好&#xff0c;但还是有很多人不知道该如何选购耳机&#xff0c;我也总是被问到蓝牙耳机挑选的相关问题&#xff0c;今天就来跟大家一起来了解了解什么蓝牙耳机…

看我这篇没人比你更懂RecyclerView的预加载

实际上&#xff0c;预拉取(prefetch)机制作为RecyclerView的重要特性之一&#xff0c;常常与缓存复用机制一起配合使用、共同协作&#xff0c;极大地提升了RecyclerView整体滑动的流畅度。 并且&#xff0c;这种特性在ViewPager2中同样得以保留&#xff0c;对ViewPager2滑动效…

【面试题】请你谈谈MySQL性能调优的方法

【面试题】请你谈谈MySQL性能调优的方法 这个问题是一个开放性问题&#xff0c;本人这一段时间参加面试&#xff08;2022.12.26&#xff09;经常被问道...... 刚刚开始我回答的很混乱&#xff01;虽然真的知道MySQL性能调优的方法&#xff0c;也做过类似的工作&#xff0c;但…

【BF算法】

BF 算法 BF 算法精讲 在学习到字符串的匹配问题时&#xff0c;了解到了BF算法和KMP算法。 对比这两个算法&#xff0c;先了解BF算法&#xff1b; 字符串匹配问题&#xff0c;比如说&#xff1a;有一个主串 “abbbcdef” &#xff0c; 子串 “bbc”&#xff0c;该问题就是在主…

Linux基础 - DNS服务基础

‍‍&#x1f3e1;博客主页&#xff1a; Passerby_Wang的博客_CSDN博客-系统运维,云计算,Linux基础领域博主 &#x1f310;所属专栏&#xff1a;『Linux基础』 &#x1f30c;上期文章&#xff1a; Linux基础 - Web服务基础 &#x1f4f0;如觉得博主文章写的不错或对你有所帮助…

共享内存和消息队列

共享内存 共享内存指 (shared memory)在多处理器的计算机系统中&#xff0c;可以被不同中央处理器(CPU)访问的大容量内存。由于多个CPU需要快速访问存储器&#xff0c;这样就要对存储器进行缓存(Cache)。任何一个缓存的数据被更新后&#xff0c;由于其他处理器也可能要存取&am…

某程序员哀叹:有比我更惨的吗?工作6年攒了200万,高位接盘买了600万的房子,现在房子跌了100多万,每个月还要还2万房贷!...

最近这几年&#xff0c;“人间惨事”层出不穷&#xff0c;许多网友都在网上比惨&#xff0c;今天的故事主角是一位程序员。这位程序员哀叹&#xff1a;有比我更惨的吗&#xff1f;工作6年攒了200多万&#xff0c;凑了300万首付&#xff0c;在杭州未来科技城高位接盘买了600万的…

JavaScript普通函数与箭头函数有怎样的区别?

比较点 普通函数 箭头函数 具体案例 简写 / 箭头函数如果没有参数&#xff0c;同时函数体的返回值只有一句&#xff0c;则{}和return都可以省略。 1、函数简写 this指向 this总是指向调用它的对象&#xff0c;如果作为构造函数&#xff0c;它指向创建的对象实例 箭头…

【MySQL】深入理解数据库事务

文章目录优秀借鉴1、事务由来2、何为ACID2.1、Atomicity原子性2.2、Consistency一致性2.3、Isolation隔离性2.4、Durability持久性3、聊回事务3.1、概念3.2、语法3.2.1、开启事务3.2.2、提交事务3.2.3、回滚事务4、隔离级别4.1、引入4.2、并发问题4.2.1、脏读4.2.2、不可重复读…

Spring Cloud alibaba 使用Nacos配置中心

依赖管理 Spring Cloud Alibaba BOM 包含了它所使用的所有依赖的版本 请将下面的 BOM 添加到 pom.xml 中的 部分。 这将允许我们省略任何Maven依赖项的版本&#xff0c;而是将版本控制委派给BOM。 <dependencyManagement><dependencies><dependency><gr…

2022年底了,你们公司还好吗?我这里不太好

以下这些也是和几个朋友聊天的时候慢慢聊出来的&#xff0c;不一定真实啊&#xff0c;当做大家开发累了以后的一点调味剂吧 一、宇宙厂 1.宇宙人员成本优化计划&#xff0c;随着各个业务确认了优化目标&#xff0c;将在接下来陆续开展。 某中台确认了指标&#xff0c;将在“在职…

力扣刷题记录——121买卖股票的最佳时机 和125. 验证回文串

本专栏主要记录力扣的刷题记录&#xff0c;备战蓝桥杯&#xff0c;供复盘和优化算法使用&#xff0c;也希望给大家带来帮助&#xff0c;博主是算法小白&#xff0c;希望各位大佬不要见笑&#xff0c;今天要分享的是——《121.买卖股票的最佳时机和125. 验证回文串》。 目录 12…

QT学习 控件(一):按钮类

文章目录Qt控件&#xff1a;按钮QPushButtonQToolButtonQCommandLinkButtonQRadioButtonQCheckBoxQButtonGroupQt控件&#xff1a;按钮 QAbstractButton的信号&#xff1a; void clicked(bool checked false) &#xff1a; 是否选中按钮void pressed()&#xff1a; 点击按钮v…

嵌入式C语言面向对象编程 --- 继承

上一篇文章主要讲述了 C 语言面向对象编程 – 封装的简单概念和实现,本篇文章继续来讨论一下,如何使用 C 语言实现面向对象编程的另一个重要特性:继承。 继承就是基于一个已有的类(一般称作父类或基类),再去重新声明或创建一个新的类,这个类可以称为子类或派生类。子类…

ES文件浏览器 如何提取盒子已安装(内置)软件APK 教程

ES文件浏览器( ES File Explorer)是一款功能强大免费的本地和网络文件管理器。 主要功能&#xff1a;文件管理&#xff1a;多种视图列表和排序方式&#xff0c;查看并打开各类文件&#xff0c;在本地SD卡、局域网、OTG设备之间任意传输文件。多选、复制、粘帖、剪贴板、查看属性…

海豚dolphinscheduler 通过shell 调用.sql文件 传参

1. 准备sql文件 1.1 资源中心--创建文件 1.2 文件格式选择 sql, 文件内容 填要执行的sql内容 1.3 点击创建保存 2.shell调用.sql文件 2.1 拖拽一个shell 节点 2.2 编辑shell节点 hive -e&#xff1a;后面跟hivesql字符串 例如&#xff1a;hive -e "select * from studen…

【十天成为红帽工程师】第七天 Ansible的模块使用

目录 一、ansible的配置文件和清单文件 二、ansible的模块 三、实际操作命令 一、ansible的配置文件和清单文件 &#xff08;一&#xff09;看ansible的配置文件 1、命令&#xff1a;ansible --version 2、一般的配置文件是&#xff1a;/etc/ansible/ansible.cfg PS&am…

查看磁盘分区

在Window上查看磁盘分区&#xff0c;既可以使用diskpart list vol命令&#xff0c;也可以使用diskmgmt.msc命令&#xff0c;下面分别介绍这2种命令查看方式。 1、diskpart方式 按WinR --> 输入: cmd --> diskpart --> list vol&#xff0c;如下图所示: ## 按WinR, …

借助 Material Design,帮助您打造更好的无障碍应用 (中篇)

随着时代的发展&#xff0c;"无障碍体验" 对开发者的意义也愈发重大&#xff0c;在上一篇文章中&#xff0c;我们为您介绍了辅助技术&#xff0c;层次结构&#xff0c;颜色和对比度等内容。本文将进一步为您介绍无障碍布局和排版、文案等相关的内容。布局和排版Mater…

【Java面经】一次颇为进阶的面试记录

工作之余又参加了一次面试&#xff0c;对我来说比之前的面试难度都提了一个度&#xff0c;面试官从公司场景引申聊到高并发和Redis的很多问题。 可惜我太菜了回答不上来&#xff0c;只能回答基础的问题。面完就是凉凉的味道。。 Redis相关 Redis的String是怎么实现的&#xff…