本文选自 | 《操作系统基础:C 语言实现用户线程》
Chat 作者 | Allen()
整理 | linse
码农都懂的一句话:C 语言无所不能,什么都能搞。把 C 学好,上手其他语言也会事半功倍。因为热爱 C 语言,所以才有了以下这篇文——
一直以来,我们学习线程切换与调度,都是通过阅读操作系统书籍或 Linux 源码甚至反汇编 Window 内核代码。无论怎样,这些东西都很抽象,学习起来十分困难。
另外,随着现代化编程语言逐渐流行,C++20 的正式发布,都离不开用户态线程这些基础知识。再比如 Golang 的 Goroutine,腾讯公司的开源的 libco,百度的 BRPC 中的 bhtread,如果想深刻理解它们,都需要扎实的基本功。
本文会带你循序渐近的掌握以下知识点:
控制流切换原理
上下文切换
线程设计
调度函数的封装与代码模块化
线程的主动切换
时间片轮转调度
本文实验环境:
ubuntu 16.04 32 位操作系统(读者请务必提前安装好);
挑选一个你自己觉得好用的虚拟机软件,比如 VMWare;
请把你的虚拟机环境配置成单核 CPU。
学习时间:大于 5 小时
本场 Chat 作者——Allen()
百度高级研发工程师,CSDN 博客专家。目前从事智能交互式对话系统云端开发。负责过大流量、高并发等业务场景。
时间紧张,心急看全文的朋友来这里
扫码查看完整 Chat 原文
还能进入读者圈与作者深度交流
回归正文,我为什么要使用 32 位系统?因为方便初学者学习,也能更快速的掌握原理。
需要注意的是,本文的代码,并没有使用操作系统为我们提供的 pthread
系列函数,thread_create
和 thread_join
函数都是自己纯手工实现的。唯一使用操作系统的函数就是设置时钟,因此会有时钟信号产生,这一步是为了模拟时间片轮转算法而做的。
01
控制流,指的是一系列按顺序执行的指令。比如你编写的多线程程序,每个线程就可以看成是一个控制流,多个线程允许多个控制流一起执行。
在我们学习编程的时候,如果不借助操作系统提供的线程框架,几乎无法完成多控制流的运行的。
接下来剖析一下,我们的指令如何”莫名奇妙“的就切换到其它线程的。
不管你用的是什么语言编程,最后都要落实到 CPU 上,而 CPU 只认识它自己的语言,机器语言。机器语言可以抽象出对应 CPU 架构的汇编指令。
对于单核 CPU 来说,永远只有一条控制流,也就是只有一条指令序列。所以,宏观上模拟的多线程程序,本质上还只是单控制流,所谓的多线程,只不过是一种被制造出来的假像!
汇编指令在执行的时候,最重要地方在于它需要依赖 CPU 环境:
一套通用寄存器 (eax、edx、ecx、ebx、esp、ebp、esi、edi);
标志寄存器 eflags;
指令寄存器 eip (eip 用来保存下一条要被指令的地址)。
如果你不理解 CPU 寄存器是什么意思,把它想象成它是 CPU 中预先定义好的变量。
我们换个思路,不修改栈的内容,而是直接换一个栈,本质上也就是换 esp 的值,能不能达到相同的效果?
话说回来,这个真的是控制流“切换”吗,只是看起来像而已,本质上它只是个跳转。
切换出去,是完成线程调度,最关键的一点在于还得切换回来。
上下文切换不同于第 1 节所述的暴力切换,因为在那个实验里,我们永远无法重新返回到 main
函数中。如果你想从那个 fun
函数再跳回目的地,我们需要在切换控制流前保存当前寄存器环境,以及当前的栈顶位置。
有很多种手段保存寄存器环境,最简单的一种就是保存到定义好结构体去。
相信上下文切换的实验你已经写出来了,不过你肯定还很多不满意的地方:
线程的切换太暴力
代码看起来一箩筐没有组织
看起来根本没有线程的样子
在我们需要对程序进行改进时,主要有以下 2 点:
封装线程创建函数
完成一个简单的调度算法
在完整原文里,我带领大家完成了线程切换与线程调度的框架,在此基础上,还可以探索更多有关线程的东西,比如线程的同步与互斥,还有更多的线程状态。理解了本文之后,更深入的学习对你来说已经不是难事了!
最终的结果和下图的效果一样
▼
图1 用户态线程运行示例
理解“栈切换”的本质
掌握指令执行与栈的关系
掌握上下文是如何切换的
理解主动切换,知道主动切换可能会发生在哪些地方
理解什么是时间片
对于不主动让出 cpu 的线程,是如何切换到其它进程的
本文的代码托管在 Github,在文件夹 v5 里,有需要的朋友可以点击,订阅这场 Chat 获取代码!