趣谈Linux操作系统
刘超
网易杭州研究院云计算技术部首席架构师
立即订阅
19393 人已学习
课程目录
已完结 72 讲
0/4登录后,你可以任选4讲全文学习。
入门准备篇 (3讲)
开篇词 | 为什么要学习Linux操作系统?
免费
01 | 入学测验:你究竟对Linux操作系统了解多少?
02 | 学习路径:爬过这六个陡坡,你就能对Linux了如指掌
核心原理篇:第一部分 Linux操作系统综述 (3讲)
03 | 你可以把Linux内核当成一家软件外包公司的老板
04 | 快速上手几个Linux命令:每家公司都有自己的黑话
05 | 学会几个系统调用:咱们公司能接哪些类型的项目?
核心原理篇:第二部分 系统初始化 (4讲)
06 | x86架构:有了开放的架构,才能打造开放的营商环境
07 | 从BIOS到bootloader:创业伊始,有活儿老板自己上
08 | 内核初始化:生意做大了就得成立公司
09 | 系统调用:公司成立好了就要开始接项目
核心原理篇:第三部分 进程管理 (10讲)
10 | 进程:公司接这么多项目,如何管?
11 | 线程:如何让复杂的项目并行执行?
12 | 进程数据结构(上):项目多了就需要项目管理系统
13 | 进程数据结构(中):项目多了就需要项目管理系统
14 | 进程数据结构(下):项目多了就需要项目管理系统
15 | 调度(上):如何制定项目管理流程?
16 | 调度(中):主动调度是如何发生的?
17 | 调度(下):抢占式调度是如何发生的?
18 | 进程的创建:如何发起一个新项目?
19 | 线程的创建:如何执行一个新子项目?
核心原理篇:第四部分 内存管理 (7讲)
20 | 内存管理(上):为客户保密,规划进程内存空间布局
21 | 内存管理(下):为客户保密,项目组独享会议室封闭开发
22 | 进程空间管理:项目组还可以自行布置会议室
23 | 物理内存管理(上):会议室管理员如何分配会议室?
24 | 物理内存管理(下):会议室管理员如何分配会议室?
25 | 用户态内存映射:如何找到正确的会议室?
26 | 内核态内存映射:如何找到正确的会议室?
核心原理篇:第五部分 文件系统 (4讲)
27 | 文件系统:项目成果要归档,我们就需要档案库
28 | 硬盘文件系统:如何最合理地组织档案库的文档?
29 | 虚拟文件系统:文件多了就需要档案管理系统
30 | 文件缓存:常用文档应该放在触手可得的地方
核心原理篇:第六部分 输入输出系统 (5讲)
31 | 输入与输出:如何建立售前售后生态体系?
32 | 字符设备(上):如何建立直销模式?
33 | 字符设备(下):如何建立直销模式?
34 | 块设备(上):如何建立代理商销售模式?
35 | 块设备(下):如何建立代理商销售模式?
核心原理篇:第七部分 进程间通信 (7讲)
36 | 进程间通信:遇到大项目需要项目组之间的合作才行
37 | 信号(上):项目组A完成了,如何及时通知项目组B?
38 | 信号(下):项目组A完成了,如何及时通知项目组B?
39 | 管道:项目组A完成了,如何交接给项目组B?
40 | IPC(上):不同项目组之间抢资源,如何协调?
41 | IPC(中):不同项目组之间抢资源,如何协调?
42 | IPC(下):不同项目组之间抢资源,如何协调?
核心原理篇:第八部分 网络系统 (7讲)
43 预习 | Socket通信之网络协议基本原理
43 | Socket通信:遇上特大项目,要学会和其他公司合作
44 | Socket内核数据结构:如何成立特大项目合作部?
45 | 发送网络包(上):如何表达我们想让合作伙伴做什么?
46 | 发送网络包(下):如何表达我们想让合作伙伴做什么?
47 | 接收网络包(上):如何搞明白合作伙伴让我们做什么?
48 | 接收网络包(下):如何搞明白合作伙伴让我们做什么?
核心原理篇:第九部分 虚拟化 (7讲)
49 | 虚拟机:如何成立子公司,让公司变集团?
50 | 计算虚拟化之CPU(上):如何复用集团的人力资源?
51 | 计算虚拟化之CPU(下):如何复用集团的人力资源?
52 | 计算虚拟化之内存:如何建立独立的办公室?
53 | 存储虚拟化(上):如何建立自己保管的单独档案库?
54 | 存储虚拟化(下):如何建立自己保管的单独档案库?
55 | 网络虚拟化:如何成立独立的合作部?
核心原理篇:第十部分 容器化 (4讲)
56 | 容器:大公司为保持创新,鼓励内部创业
57 | Namespace技术:内部创业公司应该独立运营
58 | CGroup技术:内部创业公司应该独立核算成本
59 | 数据中心操作系统:上市敲钟
实战串讲篇 (9讲)
60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔
61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔
62 | 知识串讲:用一个创业故事串起操作系统原理(一)
63 | 知识串讲:用一个创业故事串起操作系统原理(二)
64 | 知识串讲:用一个创业故事串起操作系统原理(三)
65 | 知识串讲:用一个创业故事串起操作系统原理(四)
66 | 知识串讲:用一个创业故事串起操作系统原理(五)
67 | 期末测试:这些操作系统问题,你真的掌握了吗?
结束语 | 永远别轻视任何技术,也永远别轻视自己
免费
专栏加餐 (2讲)
学习攻略(一):学好操作系统,需要掌握哪些前置知识?
“趣谈Linux操作系统”食用指南
免费
趣谈Linux操作系统
登录|注册

16 | 调度(中):主动调度是如何发生的?

刘超 2019-05-03
上一节,我们为调度准备了这么多的数据结构,这一节我们来看调度是如何发生的。
所谓进程调度,其实就是一个人在做 A 项目,在某个时刻,换成做 B 项目去了。发生这种情况,主要有两种方式。
方式一:A 项目做着做着,发现里面有一条指令 sleep,也就是要休息一下,或者在等待某个 I/O 事件。那没办法了,就要主动让出 CPU,然后可以开始做 B 项目。
方式二:A 项目做着做着,旷日持久,实在受不了了。项目经理介入了,说这个项目 A 先停停,B 项目也要做一下,要不然 B 项目该投诉了。

主动调度

我们这一节先来看方式一,主动调度。
这里我找了几个代码片段。第一个片段是 Btrfs,等待一个写入Btrfs(B-Tree)是一种文件系统,感兴趣你可以自己去了解一下。
这个片段可以看作写入块设备的一个典型场景。写入需要一段时间,这段时间用不上 CPU,还不如主动让给其他进程。
static void btrfs_wait_for_no_snapshoting_writes(struct btrfs_root *root)
{
......
do {
prepare_to_wait(&root->subv_writers->wait, &wait,
TASK_UNINTERRUPTIBLE);
writers = percpu_counter_sum(&root->subv_writers->counter);
if (writers)
schedule();
finish_wait(&root->subv_writers->wait, &wait);
} while (writers);
}
另外一个例子是,从 Tap 网络设备等待一个读取。Tap 网络设备是虚拟机使用的网络设备。当没有数据到来的时候,它也需要等待,所以也会选择把 CPU 让给其他进程。
static ssize_t tap_do_read(struct tap_queue *q,
struct iov_iter *to,
int noblock, struct sk_buff *skb)
{
......
while (1) {
if (!noblock)
prepare_to_wait(sk_sleep(&q->sk), &wait,
TASK_INTERRUPTIBLE);
......
/* Nothing to read, let's sleep */
schedule();
}
......
}
你应该知道,计算机主要处理计算、网络、存储三个方面。计算主要是 CPU 和内存的合作;网络和存储则多是和外部设备的合作;在操作外部设备的时候,往往需要让出 CPU,就像上面两段代码一样,选择调用 schedule() 函数。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《趣谈Linux操作系统》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(26)

  • why
    - 调度, 切换运行进程, 有两种方式
        - 进程调用 sleep 或等待 I/O, 主动让出 CPU
        - 进程运行一段时间, 被动让出 CPU
    - 主动让出 CPU 的方式, 调用 schedule(), schedule() 调用 __schedule()
        - __schedule() 取出 rq; 取出当前运行进程的 task_struct
        - 调用 pick_next_task 取下一个进程
            - 依次调用调度类(优化: 大部分都是普通进程), 因此大多数情况调用 fair_sched_class.pick_next_task[_fair]
            - pick_next_task_fair 先取出 cfs_rq 队列, 取出当前运行进程调度实体, 更新 vruntime
            - pick_next_entity 取最左节点, 并得到 task_struct, 若与当前进程不一样, 则更新红黑树 cfs_rq
        - 进程上下文切换: 切换进程内存空间, 切换寄存器和 CPU 上下文(运行 context_switch)
            - context_switch() -> switch_to() -> __switch_to_asm(切换[内核]栈顶指针) -> __switch_to()
            - __switch_to() 取出 Per CPU 的 tss(任务状态段) 结构体
            - > x86 提供以硬件方式切换进程的模式, 为每个进程在内存中维护一个 tss, tss 有所有寄存器, 同时 TR(任务寄存器)指向某个 tss, 更改 TR 会触发换出 tss(旧进程)和换入 tss(新进程), 但切换进程没必要换所有寄存器
            - 因此 Linux 中每个 CPU 关联一个 tss, 同时 TR 不变, Linux 中参与进程切换主要是栈顶寄存器
            - task_struct 的 thread 结构体保留切换时需要修改的寄存器, 切换时将新进程 thread 写入 CPU tss 中
            - 具体各类指针保存位置和时刻
                - 用户栈, 切换进程内存空间时切换
                - 用户栈顶指针, 内核栈 pt_regs 中弹出
                - 用户指令指针, 从内核栈 pt_regs 中弹出
                - 内核栈, 由切换的 task_struct 中的 stack 指针指向
                - 内核栈顶指针, __switch_to_asm 函数切换(保存在 thread 中)
                - 内核指令指针寄存器: 进程调度最终都会调用 __schedule, 因此在让出(从当前进程)和取得(从其他进程) CPU 时, 该指针都指向同一个代码位置.
    2019-05-03
    29
  • 安排
    proc文件系统里面可以看运行时间和切换次数,还可以看自愿切换和非自愿切换次数。

    老师请教一个问题,A切到B, B切到C,C切到A,当最后切换回A的时候,A要知道自己是从C切换过来的,也就是last,这样做的目的是什么呢?A要对C做什么善后操作吗?

    作者回复: 是的 finish_task_switch完成清理工作

    2019-05-03
    9
  • 刘強
    看了三遍,因为有一些基础,大概明白了。我觉得有个地方很巧妙。当函数返回的时候,由于切换了上下文,包括栈指针,所以一个进程函数执行return返回到了另一个进程,也就是完成了进程的切换。由此也可以看出,cpu也是比较"笨的",它只提供了基本的机制,至于如何利用这种机制,玩出花样,那就是各个操作系统自由发挥了。

    作者回复: 是的,这一点比较绕

    2019-05-05
    7
  • coyang
    vmstat 、 pidstat 和 /proc/interrupts可以查看进程的上下文切换。
    2019-05-03
    3
  • 憨人
    进程切换需要搞明白:我从哪里来,我要到哪里去

    作者回复: 这句话赞

    2019-05-17
    1
  • 尚墨
    刘老师,每个用户的进程都会被分配一个内核栈吗?

    作者回复: 是的

    2019-05-08
    1
  • 一笔一画
    老师,我还是对三个参数不解,A->B->C,如果再来一个D怎么办?

    作者回复: 不影响,这里只站在a的角度看问题,从a到b,让后中间经历一万个进程,然后到c再到a,也是这个样子的

    2019-05-03
    1
  • neohope
    老师您好,对于进程切换这样理解对吗:

    假设单核环境下,进程切换顺序为:A->B->C->A

    //A->B时三个参数的值为prev=A,next=B
    //而且这句话是在A里面运行的
    switch_to(prev, next, prev);

    //第二句话保证执行顺序
    barrier();

    //第一句话与第三句话之间,CPU时间给到B、C进程,A进程等待
    //进程C调用了switch_to以后,prev=C
    //CPU时间给到A,A继续执行
    return finish_task_switch(prev);

    另外,其实是内核需要知道切换顺序,并非是用户态的进程需要知道切换顺序,我这样理解对吗?
    2019-12-09
  • 陈志恒
    摘抄:这一节我们讲主动调度的过程,也即一个运行中的进程主动调用 __schedule 让出 CPU。在 __schedule 里面会做两件事情,第一是选取下一个进程,第二是进行上下文切换。而上下文切换又分用户态进程空间的切换和内核态的切换。
    2019-11-25
  • 秋天
    表示 已经更不上思路了 代码也看不太懂 请刘总指点
    2019-10-30
  • JT
    个人对于最后一部分指令指针的理解:每个进程都会调用schedule函数进行调度,那么在调用schedule函数的时候,相应的调用过程都会被压入内核栈中,schedule切换进程仅仅只是切换内核栈,而当schedule返回的时候,就会返回到内核栈中压入的函数(此内核栈为切换后进程的内核栈,所以会返回到切换后进程调用schedule时候的函数位置)
    2019-09-29
  • 石将从
    指针指令这块很懵,先跳过吧
    2019-09-20
  • 天王
    调度中 1 进程调度 做着做着A项目,然后去做B项目去了,主要有2种方式,一种是A项目,sleep休息一会,或者在等待IO操作,让出CPU去做B项目,这种叫主动调度,另一种是另一种是A项目做的时间长,B项目也需要做一下,就把CPU让给A项目。2 主动调度 2.1 常见的2种场景,btrfs btree的文件系统 ,在文件写入的时候,写入需要一段时间,不需要CPU,还不如让给其他线程,另一种场景 从tap网络设备准备一个读取,当没有数据到来,需要等待,也可以把CPU让给其他进程。计算机主要处理计算,网络,存储3个方面,计算主要是CPU和内存,存储和网络,多是和外部设备的合作,在操作外部设备的时候,一般会让出CPU,选择调用schedule函数。3 schedule函数的调用过程 3.1 schedule有一个主函数,_schedule函数,函数定义如下:struct task_struct *prev,*next,long *switch_count,struct rq_flags rf;struct rq *rq;int cpu;cpu=smp_processor_id();rq=cpu_rq(cpu);prev=rq->curr;3.2 首先在当前CPU上取出任务队列rq,将prev只向当前的进程curr,因为一旦切换下来,就成前任了,3.3取出下一个任务 next=pick_next_task,大部分是普通进程,调用的是fair_schedule_class类,调用的是pick_next_task_fair,cfs_rq ,schedule_entity se,task_struct *p,int new_tasks,对于cfs调度类,取出相应的队列cfs_rq,取出当前正在运行的任务curr,如果进程处于运行状态,则调用update_curr更新vruntime,然后pick_next_entity取出红黑树最左边的节点,se=pick_next_entity(cfs_rq,curr);p=task_of(se);得到下一个调度实体对应的task_struct,如果发现和前任不一样,更新前任的vruntime,put_prev_entity放回红黑树,set_next_entity将设为当前任务,开始进行上下文切换,继任者进程开始正式运行。4 进程上下文切换 进程切换主要干2件事情,一是切换进程空间,二是切换寄存器和CPU上下文,4.1 context_switch函数,主要是内存空间的切换,switch_to 寄存器和栈的切换,他调用到了_switch_to_asm,栈的切换,切换的是栈顶指针。switch_to函数里面有一个Per CPU的结构体tss task state segment,这里面维护着所有的寄存器,还有一个tr 任务寄存器,指向某个进程的TSS,会给每个CPU都关联一个tss,有一个tr一直指向tss,在linux中参与进程切换的只有栈顶指针寄存器,有一个成员变量thread_struct thread,指向进程切换时需要切换的寄存器,进程切换,就是将tss的值更新到cpu里的TR指向的tss_struct。5指令指针的保存与恢复 ,5.1 从进程A切换到进程B,切换内存空间的时候,在内存空间里的用户栈已经切换,_switch_to里面将_curren_task指向当前的task_struct,里面的void * stack指针指向的就是内核栈,_switch_to_asm已经切换了栈顶指针,并且在_switch_to_加载到了tss里面,用户栈的栈顶指针,如果当前在内核栈,那他在内核栈顶部的pt_regs,当从内核返回用户态的时候,pt_regs里面所有的在用户态开始运行的上下文信息,就可以开始运行了。5.2 进程的调度都会调用_schedule函数,进程调度第一定律
    2019-09-04
  • 俩孩儿他爸
    "通过三个变量 switch_to(prev = A, next=B,last = C);A 进程就明白了,我当时被切换走的时候,是切换成 B,这次切换回来,是从 C 回来的";这段话中,进程A从进程C切换回来时,进程A内核栈中变量的定义:prev=last=C,netxt=B,由于当前就是在进程A的地址空间里,所以,可以进程A可以说,当年我被切换到进程B,现在,由进程C又切换回来了。



    作者回复: 是的

    2019-08-26
  • kdb_reboot
    看起来ps 里面的TIME就是进程的 cpu runtime吧; 查看上下文切换,可以用cat /proc/x/status

    作者回复: 赞

    2019-07-27
  • kdb_reboot
    补充一下,看了最后的那张图,感觉切换,就是切内核态的 stack/rsp/pc, 这样下一个任务就能找到在哪执行了,以及继续怎么执行, 而内核态共享一片内存空间,所以不需要mm_switch,切换完了,返回用户态,用户态的stack/rsp/pc都被切换了, 而用户态的内存空间需要单独切换
    老师,我理解的对吧?

    作者回复: 是的

    2019-07-27
  • kdb_reboot
    关于指令指针的讲解,厉害了...
    专栏有时候可以反者看, 先看最后总结,然后往上顺藤模块看你的分析
    同时在读的书:lkd/ulk, 推荐给大家

    作者回复: 先看总结也挺好的

    2019-07-27
  • garlic
    通过ps -o etime= -p "$$" 可以 查看,进程的运行时间, 通过/proc/{pid}/status 中的 voluntary_ctxt_switches: nonvoluntary_ctxt_switches: 可以看到主动调度和抢占调度的次数, 也可以单独安装sysstat 使用pidstat -w 查看相关进程的调度信息 https://garlicspace.com/2019/07/20/查看进程运行时间及上下文切换次数/

    作者回复: 赞

    2019-07-20
  • 栋能
    我有两个问题,希望老师解解惑:
    1.文中说进程切换会从红黑树中找到最左结点,如果不等于当前进程,则切换。但如果更新vruntime之后,树结构还是没变呢,即最左还是等于当前进程,那我当前进程切换不是又没效果了吗?(这点有疑问,是我理解vruntime是公平的,但主动调度一定存在某种情况,如vruntime变化不大,树结构不变的?)
    2.在指令指针的恢复与保存这部分内容中,你说A调用__schedule进行切换,在运行到finish_task_switch时进程已经是B了。你觉得这里没有问题,是因为A、B进程都是调用过__schedule方法进行进程切换,最后都执行finish_task_switch就是圆满了。可是进程切换除了主动调用之外,还有时间片用完,如B进程可能并没有调用过__schedule,那如果A进程切换,导致B进程执行finish_task_switch方法,那这是不是个问题呢?本来进程B并不需要执行它的。

    作者回复: 1.如果最左面的节点还是自己,并且自己还能运行,也即还是running状态,那就接着运行,说明时间片用完了,但是没有更加高级的需要运行,如果是等待IO,那就不是running状态,则不在树中。

    2.调度第一定律,B如果当年被调度走,哪怕是时间片到,也是调用了schedule的

    2019-07-16
    4
  • 雲至
    老师能具体编译一个讲下吗?

    作者回复: 编译内核,后面会有相应的章节

    2019-06-29
收起评论
26
返回
顶部