多进程与操作系统基础
上一个内容我们讲了多进程图像,强调多进程图像是操作系统最核心的图像。我们还通过Windows任务管理器,实际观察了操作系统里的进程。
- 进程是操作系统的核心内容,管理好多个进程,就能管理好操作系统和CPU。
- 上节课提到操作系统支持多进程图像的四个方面:
- 组织方式:操作系统通过状态(如就绪态、阻塞态等)和队列(就绪态队列、阻塞队列等)来组织进程,涉及简单数据结构知识,此部分不做详细讲解。
- 进程切换:多进程的切换非常关键,是重点内容。
- 进程分离:在内存管理部分详细展开如何让进程之间地址空间不相互影响 。
- 进程合作:在进程同步章节详细讲解操作系统如何让多个进程合作。
线程概念的引出
本次课重点探讨操作系统如何实现多个进程的切换。然而,讲进程切换却提到线程,这是因为线程的切换是进程切换的重要组成部分。
- 多进程图像回顾:一个进程执行一系列指令,执行时可能因启动I/O(如磁盘操作)而执行不下去,需要切换。这里引发思考:能否只切换指令序列,而不切换资源(如映射表) ?
- 线程概念形成:实际上,将资源和指令执行分开,只切换指令序列是可行的。这样切换速度更快,因为只需切换程序计数器(PC)和一些寄存器,无需切换映射表等资源。这种在一个资源下启动多个轻巧的指令序列,且可来回切换的方式,就是线程。线程保留了并发优点,同时避免了进程切换的高代价,其实质是映射表不变而PC指针变化。
线程实用性的示例——网页浏览器
以网页浏览器为例说明线程的实用性。
- 浏览器显示过程:打开斯坦福网站时,网页数据包含文本、图片等。若所有操作按顺序执行,先下载文本,再显示文本,最后显示图片,会出现一段时间屏幕空白,用户体验差。
- 多线程实现方式:实际浏览器是通过多线程实现的:
- 一个线程从服务器接收数据。
- 一个线程显示文本。
- 一个线程处理图片(如解压缩)。
- 一个线程显示图片。
- 线程共享资源优势:这些线程共享资源,比如接收的数据放在缓冲区,显示文本和图片的线程都从该缓冲区读取数据。若采用进程方式,因进程地址空间分离,数据传递会很麻烦,所以采用线程方式更合适。
线程切换实现示例
以WebExplorer
应用程序为例,展示线程切换的实现:
- 程序基本结构:程序申请共享缓冲区,创建多个线程,每个线程执行一个函数。如
GetData
函数负责从网站下载数据(建立Socket连接,下载数据包并放入缓冲区),Show
函数从缓冲区取出内容显示到显示器上。 - 线程交替执行关键:核心是实现线程交替执行,这需要用到
Yield
函数。当GetData
函数下载一部分数据后,调用Yield
函数暂停当前线程执行,操作系统保存当前线程状态(如PC值、寄存器值等),然后从线程就绪队列中选取另一个线程(如Show
线程),恢复其状态并让其执行。Show
线程执行完相关操作后,也调用Yield
函数,将执行权交回操作系统,操作系统再选取其他就绪线程执行,如此循环实现线程交替执行。
线程切换原理深入分析
-
Create
与Yield
:Create
用于制造第一次切换时的状态,Yield
是线程切换的核心。
能切换就需清楚切换时的状态,比如
Yield
操作就是程序计数器(PC)从一个地址跳到另一个地址,如从100跳到300 。 -
栈的变化:开始是一个栈,存在问题。后来发展为两个栈(对应两个线程),每个线程有自己的栈和线程控制块(TCB)。
Yield
切换时要先切换栈,例如Yield
函数中先将当前栈指针(esp)保存到当前TCB中,然后将esp设置为下一个线程TCB中的esp值,实现栈的切换。
-
ThreadCreate
核心:ThreadCreate
函数的核心是创建两个TCB、两个栈,并将切换的PC值存于栈中。具体实现为申请TCB和栈的内存空间,将函数入口地址(如100)存入栈中,并将栈指针与TCB关联。
综合示例与用户级线程特点
-
综合示例:将所有相关函数组合在一起,
WebExplorer
函数(类似main
函数)中创建线程(调用ThreadCreate
),并通过while(1)Yield()
不断进行线程切换;GetData
函数在下载数据过程中调用Yield
实现线程切换;ThreadCreate
函数负责申请栈和TCB等操作;Yield
函数负责保存现场、切换栈等操作。还提到编译相关命令,如gcc -o explorer get.c yield.c...
或gcc get.c... -lthread
。
-
用户级线程特点:强调
Yield
是用户程序,说明这是用户级线程。用户级线程切换控制权在用户程序手中,在用户态执行。若进程中的某个线程进入内核并阻塞(如GetData
函数中连接URL发起请求后等待网卡I/O ,导致进程阻塞),其他线程仍可通过Yield
进行切换执行,实现并发效果。但用户级线程也有局限性,如线程长时间执行不主动调用Yield
,会导致其他线程无法执行,后续会讲解内核级线程来解决此类问题。