Linux内核简单分析(2)——进程调度与切换

2023-05-16

进程的调度与切换是一个很复杂的话题,这里我更关心内核是如何实现的,而不是使用了什么策略,所以只讲进程的组织和切换方式,而对调度程序的实现和算法不作分析。
进程调度可参考:
Linux进程调度-------O(1)调度和CFS调度器
O(n)、O(1)和CFS调度器

进程调度综述

Linux内核在2.4版本以前的调度程序都十分简陋,但随着多核处理器和硬件的发展,对进程调度的要求也更高了,所以在2.5开发过程中开始采用一种叫O(1)调度器的调度程序,O(1)调度器得到了广泛使用,表现出近乎完美的性能和可扩展性,但是时间证明该调度器对于那些对响应时间比较敏感的程序不太友好,比如文本编辑器。在2.6内核的开发中开始引入一种公平调度的概念,并且最终在2.6.23内核版本中替代了O(1)调度算法,称为完全公平调度算法,简称CFS。

O(1)调度程序是将时间片分配给进程,而CFS是将处理器的使用比例划分给进程,这也是它们比较不同的一点。在实现上两者的数据结构也有很大不同,虽然O(1)调度器已经被CFS取代了,但是研究一下它们的结构对于我们程序开发来说还是有点用的。

O(1)调度器如何组织进程?

回顾一下进程的task_struct结构,在里面有一个struct list_head run_list;字段,内核通过这个run_list将处于TASK_RUNNING状态的进程连成一个链表。

list_head结构的定义如下:

struct list_head {
    struct list_head *next, *prev;
};

那么现在我们从list_head出发,一步步往上探索。
首先处于活跃状态的进程通过其task_struct->run_list字段相连,如下图:
在这里插入图片描述
但是系统中这么多进程,简单地把它们都连在一起显然不是明智的选择,因为这样要查找进程的时间就无法保证了,而进程调度程序又要求快速完成,所以内核把这些进程按优先级分开了。

进程的优先权的取值范围为0到139,那么就可以分成140个运行队列链表,然后把进程按照优先权插入到对应的链表中,这样查找进程的效率可以有很大提高。

实现按优先权分开的是一个prio_array_t类型的数据结构,该结构的定义如下:

typedef struct prio_array prio_array_t;

struct prio_array {
    unsigned int nr_active;
    unsigned long bitmap[BITMAP_SIZE];
    struct list_head queue[MAX_PRIO];
};

其中nr_active是链表中task_struct的数量,bitmap是一个位图,用来判断对应优先权的链表上是否为空,比如第0位为1,就说明优先权为0的链表上不为空。queue大小为140,存放各个优先权链表的表头。

在task_struct中有一个prio_array_t *array字段,指向的就是这个结构体,并且处于同一个prio_array结构体中的进程指向的array是一样的。
在这里插入图片描述

那么这个结构又是怎么给进程调度程序使用的呢?现在就轮到runqueue出场了,runqueue是一个pre_cpu变量的数据结构,即CPU的每个核有一个单独的runqueue,这样可以避免cpu之间产生竞争。
runqueue中的关键字段如下:

struct runqueue
{
	......
	prio_array_t *active;
	prio_array_t *expired;
	prio_array_t[2] arrays;
	...
}

runqueue中有两个队列,一个是活跃的进程active,一个是时间片耗尽的过期进程expired,它们指向arrays中相应的队列。如下图:
在这里插入图片描述
O(1)调度器在选择下一个调度进程时,不用遍历整个runqueue,而是直接从active数组中选择当前最高优先级队列中的第一个进程,时间复杂度为O(1),这就是O(1)调度算法名称的由来。

CFS如何组织进程?

从2.6.23内核开始,调度器采用了模块化设计的思想,这种模块化结构被称为调度器类。每个进程只属于一个调度器类,在调度的时候会从这些调度器类中选择一个优先级最高的调度器类,再由这个调度器类去选择下一个要调度的进程。调度器类有针对实时进程的调度类,有针对普通进程的CFS调度类,实时进程调度类的优先级比CFS高,所以只要有实时进程,那么就总会调用它而不会使用CFS。

大体结构如下图,其中调度器模块通过调度器类选择出一个进程,投入cpu中运行。
在这里插入图片描述

回到如何组织进程这个话题来,上面O(1)调度的进程组织是由runqueue完成的,同样,CFS的进程组织也是一个runqueue,但是却有了许多不同。

首先,跟上文一样,一个cpu核有一个runqueue,该结构的定义如下:

struct rq {
	unsigned long nr_running;
	#define CPU_LOAD_IDX_MAX 5
	unsigned long cpu_load[CPU_LOAD_IDX_MAX];
	...
	struct load_weight load;
	struct cfs_rq cfs;
	struct rt_rq rt;
	struct task_struct *curr, *idle;
	u64 clock;
	...
}; 

但是该结构并不直接管理进程,而是内嵌了属于特定调度器类的子就绪队列,由该子就绪队列管理,比如其中的cfs_rq就是CFS调度器类使用的管理进程的就绪队列,rt_rq是实时进程调度器类使用的。我们关注的cfs_rq结构定义如下:

struct cfs_rq {
	struct load_weight load;
	unsigned long nr_running;
	u64 min_vruntime;
	struct rb_root tasks_timeline;
	struct rb_node *rb_leftmost;
	struct sched_entity *curr;
} 

由该结构可以看到,CFS使用红黑树来组织进程,而不是O(1)调度所使用的链表。其中tasks_timeline指向红黑树的根,而rb_leftmost指向红黑树最左节点。

该红黑树以一个虚拟运行时间vruntime作为排序key,最左节点的vruntime最小,每次调度就是直接调度最左节点对应的那个进程。vruntime越小,表示对处理器需求越大。在进程被调度后,其vruntime会稳定地增加,那么他在红黑树中的位置也逐渐往右移。CFS的完成公平调度的关键在于:越重要的进程的vruntime增长的越慢,因而向右移动的速度也慢,这样被调度的机会要大于不重要的进程;其次是如果进程进入睡眠,其vruntime不会增加,但其他的仍在增长,所以当他醒来时在红黑树的位置会更加靠左。

那么现在已经知道CFS采用红黑树组织进程了,但是红黑树节点又如何与task_struct联系起来呢?

我们从进程的task_struct来看,在task_struct中有一个const struct sched_class *sched_class,这个sched_class指针就是指向进程所属的调度器类。sched_class的结构定义如下,里面都是一些函数指针,不同的调度器类就是通过调整函数指针的指向来实现的。

struct sched_class {
	const struct sched_class *next;
	void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup);
	void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
	void (*yield_task) (struct rq *rq);
	void (*check_preempt_curr) (struct rq *rq, struct task_struct *p);
	struct task_struct * (*pick_next_task) (struct rq *rq);
	void (*put_prev_task) (struct rq *rq, struct task_struct *p);
	void (*set_curr_task) (struct rq *rq);
	void (*task_tick) (struct rq *rq, struct task_struct *p);
	void (*task_new) (struct rq *rq, struct task_struct *p);
}; 

task_struct中还有另外一个字段struct sched_entity se,这是一个调度实体。因为调度器不限于调度进程,还可以处理更大的实体,所以抽象出一个可调用实体的结构,该结构定义如下:

struct sched_entity {
	struct load_weight load; /* 用于负载均衡 */
	struct rb_node run_node;
	unsigned int on_rq;
	u64 exec_start;
	u64 sum_exec_runtime;
	u64 vruntime;
	u64 prev_sum_exec_runtime;
...
} 

其中第二个字段run_node是一个红黑树节点,这就是上面提到的CFS的红黑树里面的节点,注意这不是一个指针,而是内嵌在sched_enity结构里的。

每个cpu上的runqueue -> cfs使用的rq -> 组织进程的红黑树 -> 红黑树节点 -> sched_entity 的结构大概如下图:
在这里插入图片描述
而从task_struct 到 红黑树节点的路径大概如下图:
在这里插入图片描述

如何切换进程?

当调度器选出下一个要运行的进程后,就要执行进程切换,切换到下一个进程运行。进程切换又称为上下文切换,上下文切换由context_switch()函数负责处理,主要包含两个步骤:
(1)切换页全局目录,即切换虚拟地址空间,由switch_mm()完成。
(2)切换内核堆栈和硬件上下文,即把上一个进程的堆栈和寄存器状态切换到新进程的状态,由switch_to()完成。

switch_mm()

switch_mm()完成的主要是虚拟地址空间的切换。之前说过了每个进程的task_struct里都有一个mm_struct,该结构就是管理虚拟地址空间的。那么究竟怎样才能切换呢?

首先虚拟内存机制主要靠页表实现,学过操作系统的应该知道页表是怎么回事,现在Linux一般使用多级页表,在cr3寄存器中保存了页全局目录(pgd)的地址,那么修改cr3寄存器的指向,就相当于修改页目录,也就是切换地址空间了。所以switch_mm()主要就是把新进程next->pgd加载到cr3寄存器中,实现地址空间的切换。

虽然切换地址空间后进程并没有马上完成切换,还是prev进程,但是并不会对prev造成影响,因为现在进程prev是处在内核态的,而内核空间的页表映射对于所有进程来说都是相同的。

switch_to()

switch_to()是一个宏,其内容是特定于体系结构的汇编代码,下面我们讨论的是基于X86体系的代码。switch_to()的参数为三个task_struct指针prev、next、last,前两个分别是当前执行进程、选出来的下一个执行进程,之所以有第三个参数是因为新进程需要知道是从哪个进程切换进来的,所以要设法保存上一个进程描述符的指针,具体的做法就不说了,避免纠结于这些细节。switch_to()执行的操作大致如下:
(1)将prev、next分别保存在eax、ebx寄存器。这样做主要是为了后面的传参。

(2)将eflags、ebp寄存器内容保存在prev内核栈中。切换进程时已经由用户态陷入内核态了,用户态的寄存器信息已经保存在内核栈中了,而这里保存的这两个应该是内核态的寄存器信息。

(3)将esp寄存器内容保存到prev->thread.esp中,然后把next->thread.esp写入esp寄存器中。因为esp寄存器是内核栈的栈顶指针,所以修改esp就相当于切换了内核栈,现在修改后esp就指向了next进程的内核栈了。而把旧进程prev的栈顶指针保存到task_struct中的thread字段中,thread是一个thread_struct结构类型的指针,该结构主要保存寄存器信息,其中就包括了esp。

(4)将后面某条指令的地址保存到prev->thread.eip中,然后把next->thread.eip压入next内核栈中。这个指令的地址将会是当前进程被切换出去,再被切换回来后指行的指令地址。同理,next->thread.eip也是next进程即将执行的指令地址,注意这里esp已经是next的栈指针了,所以压入的是next的内核栈。

(5)调用__switch_to(),完成进程切换。执行汇编指令jmp __switch_to,这条指令使代码执行跳转到__switch_to()处,__switch_to()的参数为prev和next,不过不同于函数调用,该宏将直接从eax、ebx寄存器去参数,这也是(1)的原因。__switch_to()完成的任务主要是修改TSS段。

TSS称为任务状态段(Task State Segment),原本是intel设计用来完成进程硬件上下文切换的,设计是每个进程都有一个TSS,其中保存进程的寄存器等状态信息。但Linux并不采用,而是只让每个CPU使用一个TSS,由tr寄存器指向,因为tr始终指向一个TSS,不用像intel设计的那样随进程的切换而不断切换,所以开销相对也小了。并且因为Linux另外使用了一个结构thread_struct保存寄存器信息(参见上文),所以对于TSS也只是使用其中很少的一部分内容。Linux系统主要是在两个方面用到了TSS:

  • 进程从用户态陷入到内核态时需要从TSS中取得内核栈的栈顶指针放到esp中。
  • 用户态读写IO需要访问TSS的权限位图。

所以,在切换进程时,需要更新TSS里的esp0和IO权位图,这就是__switch_to()完成的内容。(之所以是esp0是因为TSS里有为多种特权级别设计的esp0,esp1,esp2,而Linux只使用esp0)

具体的操作就是用next->thread.esp去更新TSS.esp0,在有必要的时候去修改io_bitmap。当然,__switch_to()做的工作远远不止这些,还有许多操作比如更新线程的局部存储段(thread local storage),关于段的内容以后有机会再来分析一下。

当__switch_to()执行结束,准备返回时,会从栈中弹出返回地址,装入eip程序计数器中。注意,这里的esp已经指向新进程next的内核栈了,所以弹出的地址就是刚才压入的next->thread.ei(见(4)),也就是说,现在执行的代码已经是新进程的了,进程切换完成了。

(6)从prev栈中恢复到ebp、eflags寄存器。虽然(5)已经完成了进程切换,但是switch_to()实际上并没有结束,而是当该进程重新被调度,切换回来时,继续执行剩下的代码,也就是恢复寄存器信息,而这里也就是之前(4)所提到的某条命令的地址。

单看文字描述很容易看晕,可以结合下图理解:

首先开始的时候esp执行进程A的内核堆栈。
在这里插入图片描述
当要开始切换进程时,先将eflags、ebp寄存器内容保存到A的内核栈中。
在这里插入图片描述
接着将进程B的task_struct->thread.esp的值放到esp寄存器中,内核栈切换完成。
在这里插入图片描述
这时执行的代码仍然是进程A的switch_to,它把进程B的eip(B的task_struct->thread.eip)压入B的内核栈中,然后就跳转到__switch_to执行。注:eip就是切换到进程B后执行的代码地址。
在这里插入图片描述
当__switch_to执行完返回时,会从栈中弹出返回地址填入eip寄存器,然后从该地址处开始执行。那么这里就完成了进程的切换。进入B进程后,他会继续之前switch_to后面的操作,即出栈恢复eflags、ebp寄存器,到这里就是真正完成了。
在这里插入图片描述

代码的执行流大概如下:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux内核简单分析(2)——进程调度与切换 的相关文章

  • Visual Basic函数大全!

    VB函数大全 Abs 函数 返回数的绝对值 And 运算符 执行两个表达式的逻辑连接 Array 函数 返回含一数组的 变体 Asc 函数 返回字符串首字母的 ANSI 字符代码 赋值运算符 61 给变量或属性赋值 Atn 函数 返回数的反
  • 数据结构与算法:哈夫曼树(源码)!

    这些天明白了一个道理 xff0c 搞技术也是需要激情的 也不知道为什么这段过的感觉特别的不爽 xff0c 也不知道是因为快要考试了 xff0c 心里没底 xff0c 而带来的恐惧 xff0c 还是 搞技术太久 xff0c 心里想放个假 xf
  • SSH超实用分页实现(原创开源)!

    SSH的分页网上有不少的例子 xff0c 有利用session的 xff0c 有利用分页组件的 我几个师兄原来搞的SSH项目也有一个成熟的分页插件 具体业务实现类中的分页方法 xff1a lt bgsound cep 61 34 0 34
  • 欢迎访问我的主博(http://shiyangxt.cnblogs.com)

    JavaEye的朋友 xff0c 大家好 我是一名大二的学生 xff0c 对编程技术怀有很大的热情 我的技术方向是Java xff0c 但是我的主博并不在这里 xff0c 而在博客园 xff0c 欢迎大家访问我的主博 施杨de编程世界 我渴
  • Linux应用编程---14.UDP服务器、客户端编程

    Linux应用编程 14 UDP服务器 客户端编程 之前有介绍过UDP是一种无连接 尽最大努力交付 面向报文的协议 应用层交给UDP多长的报文 xff0c UDP就照样发送 Linux下UDP属于数据报socket 数据报socket流程图
  • 0816网络编程day5

    include lt stdio h gt include lt sys types h gt include lt sys socket h gt include lt arpa inet h gt include lt netinet
  • STL容器特征

    STL中顺序容器类和关联容器类的主要特征如下 xff1a 1 vector 内部数据结构 xff1a 数组 随机访问每个元素 xff0c 所需要的时间为常量 在末尾增加或删除元素所需时间与元素数目无关 xff0c 在中间或开头增加或删除元素
  • 数据结构——不带头结点的单链表的基本操作

    数据结构 不带头节点的单链表的基本操作 结构体的创建 xff1a span class token keyword typedef span span class token keyword struct span SListNode sp
  • HTTP请求/响应报文结构

    HTTP请求 响应报文结构 HTTP请求报文 一个HTTP请求报文由四个部分组成 xff1a 请求行 请求头部 空行 请求数据 1 请求行 请求行由请求方法字段 URL字段和HTTP协议版本字段3个字段组成 xff0c 它们用空格分隔 比如
  • C语言练习笔记 ~结构体2 ~ 结构体在内存中的对齐说明

    文章目录 1 结构体变量在内存中的对齐说明例1 1个char型变量例2 2个char型变量例3 1个int型变量例4 1个char型变量和1个int型变量例5 3个char型变量和1个int型变量例6 5个char型变量和1个int型变量例
  • 思岚激光雷达+cartographer建图

    系统环境 xff1a Ubuntu18 04 ROS Melodic gcc 7 5 0 1 安装思岚ROS包 1 1 clone并编译 cd catkin ws src git clone https github com Slamtec
  • 使用PyTorch+functorch计算并可视化NTK矩阵

    2022年3月 xff0c PyTorch发布了PyTorch1 11和functorch functorch灵感来自于Google JAX xff0c 旨在提供vmap和autodiff转换配合PyTorch使用 本文将演示如何使用PyT
  • libcurl异步请求+http长连接池

    由于公司项目 xff0c 需要localhost的形式高并发的http访问本机服务 xff0c 所以面临了两方面的问题 xff1a 1 http短连接会造成大量的time wait xff0c 影响服务器的性能 2 libcurl easy
  • VC实现http发送get和post请求

    VC实现http发送get和post请求 get请求 首先通过前面介绍的抓包工具获取请求的详细内容 xff0c 然后再通过VC拼接Header xff0c 函数如下 xff1a bool CXXX http get eng mode lt
  • 链表(图文详解)

    链表的概念 链表是一种物理存储结构上非连续 xff0c 非顺序的存储结构 xff0c 数据元素的逻辑顺序是通过链表中的指针链接次序实现的 链表的结构是多式多样的 xff0c 当时通常用的也就是两种 xff1a 无头单向非循环列表 xff1a
  • PCB上能上锡的那层叫什么?

  • C++常用数学函数

    C 43 43 中有个头文件math h xff0c 它是数学函数库 一些数学计算的公式的具体实现是放在math h里 xff0c 为了方便大家使用 xff0c 特在此总结常用的一些函数 1 三角函数 double sin double d
  • LimeSDR实验教程(6) 发射GPS

    下载程序 xff1a git clone https github com osqzss gps sdr sim git 编译安装 xff1a cd gps sdr sim gcc gpssim c lm O3 o gps sdr sim
  • 如何理解引用作为函数的返回值?

    如何理解引用作为函数的返回值 xff1f 1 引用作为函数的返回值时 xff0c 必须在定义函数时在函数名前将 amp 2 用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本 span class token comment 代码
  • 自制合成孔径雷达(2) SDR实现的对比(SDR实现测速雷达)

    我今天查了查资料 xff1a 技术干货 xff1a 用LimeSDR Mini制作一台软件定义多普勒雷达 搜狐汽车 搜狐网 查阅一些文献后 xff0c 笔者想探寻减少雷达系统所需的昂贵模拟前端部件数量的可能性 设计灵感来自于Gregory

随机推荐

  • 自制合成孔径雷达(3) doppler代码解读

    上一篇帖子 xff0c 看完了基于SDR的多普勒雷达 xff0c 就可以看看硬件雷达的多普勒测速的DSP代码了 先看一下这个图 xff1a 我们需要的多普勒频移的测量结果是从混频器 xff08 Multiply Conjugate xff0
  • 各类SDR的USB接口一致性测试

    最近用高带宽示波器测了好几个SDR产品的USB2接口一致性 由于探头数量只有1个 xff0c 所以不能测全所有的项目 但已经包含了最主要的USB眼图 xff08 信号质量 xff09 项目 测试场景 xff1a 待测件包含 xff1a 1
  • Portapack应用开发教程(十八)NavTex接收 D

    上回说到 xff0c 我现在已经做到用自己的gnuradio流图从音频信号做fsk解调 xff0c 得到方波 然后用c程序把方波转为二进制数 又用python把二进制数转为最终的字母 但是遗留问题是python解码 xff0c 起始位如果错
  • Portapack应用开发教程(十八)NavTex接收 E

    我现在已经成功把两部分解码的代码合并到一起 实现的功能是从stdout取出方波的采样点幅度 xff0c 然后把它转为解码输出 include lt stdio h gt include lt string gt include lt mat
  • 使用RTL-SDR和Matlab Simulink玩转软件无线电(二十一)

    3 13 扫描频谱 xff1a 把 25MHz 到 1 75GHz 的信号都收下来 这一节我们会做本章最后一个练习 xff0c 使用一个 RTL SDR 扫描整个频率范围内的信号 对于大多数 RTL SDR 设备来说 xff08 R820T
  • SDR# (SDRSharp)代码讲解 (一)

    SDR 也称SDRSharp 与Linux平台下常用的GQRX类似 xff0c 是目前Windows平台上最常用的频谱观察 xff0c 音频解调软件 xff0c 支持AM FM SSB等多种调制方式 以SDRSharp为基础又派生出了其它一
  • 自动跟随机器人教程(一)(树莓派、Arduino教程)

    机器人购买链接 xff1a https item taobao com item htm spm 61 a1z38n 10677092 0 0 59a21debCqLXYP amp id 61 532012951368 接下来打算发布一款自
  • 自动跟随机器人教程(二)硬件组装

    本机器人结构应该说比较简单 xff0c 除了上述图片里的4样东西外 xff0c 就是一个USB摄像头和一块航模专用12V锂电池 xff08 与电机电压一致 xff09 xff0c 一共6样东西 所有这些东西都不需要螺丝固定 xff0c 多数
  • LimeSDR 中文教程 (一)

    行业应用及合作请联系 j shao 64 limemicro com xff08 本文所有图片请参考Myriadrf官网原文 xff1a https myriadrf org blog limesdr made simple part 1
  • Linux系统——fork()函数详解(看这一篇就够了!!!)

    fork 函数详解 包看包会 xff01 xff01 xff01 1 fork 简介 函数原型 xff1a pid t fork void xff1b pid t为int类型 xff0c 进行了重载pid t getpid 获取当前进程的
  • HttpURLConnection详解、JSON的使用

    1 Http网络请求方法 Http的请求方法代表了客户端想对服务器进行的操作 xff0c 比如 xff1a POST GET HEAD PUT DELETE TRACE OPTIONS 常用的不过于CRUD四个 增 xff1a PUT 删
  • printf二进制输出

    include lt stdio h gt include lt conio h gt include lt stdlib h gt void main int i 61 31 char s 10 itoa i s 2 转换成字符串 xff
  • 秒懂HTTP之基本认证(Basic Authentication)

    版权申明 非商业目的注明出处可自由转载 博文地址 xff1a https blog csdn net ShuSheng0007 article details 89598299 出自 xff1a shusheng007 系列文章 xff1a
  • Linux内核学习(一)8086编程模型

    本文主要介绍Intel8086系列的编程模型 xff0c 包括分段与分页机制 任务切换过程以及中断处理系统 作为Linux内核学习曲线的起点 xff0c 本文的侧重点在于对于每个主题 xff0c 硬件上是如何实现的 xff0c 以及为软件
  • Ubuntu网络编程——TCP/IP

    常识 xff1a 裸机 xff1a 没有安装操作系统的计算机 如果想在裸机上运行自己所编写的程序 xff0c 就必须用机器语言写程序 桌面操作系统 xff1a windows macOS Linux 服务器操作系统 xff1a Linux
  • RoboMaster电控学习笔记——电机控制(1-CAN)

    Robomaster官方提供了一系列性能强大的直流无刷减速电机及配套电调 xff0c 这里介绍三款步兵上用的电机 amp 电调 M3508电机 amp C620电调 xff0c GM6020电机 xff08 内部集成电调 xff09 xff
  • linux下 在同一个线程建立TCP连接

    要实现在一个线程里建立TCP连接 xff0c 需要注意accept应在connect之后 xff0c 所以我将accept放在了tcp client里 这样 xff0c 才能得到accept返回的fd xff0c 从而进行read span
  • 【cpprestsdk】浅谈cpprestsdk线程池及使用

    cpprestsdk根据include文件夹可以看到共包含两部分内容 xff1a 1 pplx 2 cpprest pplx threadpool h源代码中创建线程池有两种方式 1 通过construct接口创建 xff0c 返回一个un
  • 丹尼带你入坑无人机3 - 四轴配件简介

    知道你的四轴里面每个小东东都是干嘛用的吗 xff1f 麻雀虽小 xff0c 五脏得全 简单说 xff0c 飞控就是大脑 xff0c 它能知道每一时刻无人机的状态 xff0c 并且给下一时刻需要作出的动作发出指令 电调就好比是神经单元 xff
  • Linux内核简单分析(2)——进程调度与切换

    进程的调度与切换是一个很复杂的话题 xff0c 这里我更关心内核是如何实现的 xff0c 而不是使用了什么策略 xff0c 所以只讲进程的组织和切换方式 xff0c 而对调度程序的实现和算法不作分析 进程调度可参考 xff1a Linux进