多进程图像

article/2025/10/1 17:26:05

多进程图像

  • 1 多进程设计
  • 2 一个大概的设计思路
  • 3 一个实际的进程切换案例
    • 3.1 进程的创建 - fork函数
    • 3.2 进程的切换 - schedule函数
    • 3.3 进程状态转换图
    • 3.4 如何执行我们自己的代码
  • 参考资料

所谓多进程图像就是:多道程序,交替执行。本章主要介绍操作系统为支持多进程图像做了哪些工作。

1 多进程设计

CPU 作为计算机最关键的设备,使用好 CPU 自然而然成为了操作系统的重中之重。CPU 是一个“取指执行”的设备(设置好 PC 指针的初值,然后每次执行一条指令CPU会让“PC加1”)。如果CPU只需要处理单个任务(单进程),那么一条一条的“取指执行”是毫无问题的,因为就算遇到了 IO 指令,那也没办法,只能等着,不能跳过。

那如果有多个任务需要处理呢?当一个进程需要等待资源(如等待磁盘数据),可不可以切换到其他进程呢?这样才能提高CPU的使用率。操作系统支持多进程图像的设计由此开始。

2 一个大概的设计思路

让 CPU 切换到另一个进程去执行,可以通过修改 PC 指针实现。可是怎么切回来呢?为了保证切回来时是接着上次的状态继续执行,因此切换前,应该先记录好切换前进程的“样子”(包括切换前各个寄存器的值,进程执行的状态等),然后再修改PC 指针。Linux0.11 设计了一个结构体:struct task_struct{...},用于记录进程的“样子”,每个进程都有一个该结构体的对象——PCB(进程控制块)。

如何选择下一个要运行的进程呢?一个简单又实用的办法就是利用队列,将所有进程的PCB指针存放在队列中,然后用先进先出的方式安排下一个要运行的进程。Linux0.11 中设计了一个这样的队列 task :

struct task_struct * task[NR_TASKS] = {&(init_task.task), };     //定义任务指针数组

Linux0.11中编写了 schedule 函数用来选择下一个要运行的进程,并且在schedule中调用了 switch_to 函数实现切换到下一个进程的功能。task 队列中有各种状态的进程的PCB,所以在switch_to 函数切换前可以先判断一下,下一个要切换到的进程是不是阻塞态的,如果是的话就先跳过。

谁来执行切换的工作呢?其实也很好猜,因为进程要不断且快速的切来切去,才能让用户感觉所有任务都不卡,因此用定时器中断来切换再合适不过,此外也可以在当进程阻塞的时候就直接切出去。在 Linux0.11 中定义了一个 do_time 函数,该函数在 timer_interrupt(系统时钟中断,每10ms发生一次时钟中断)中 被调用的。do_time 函数最后调用了 schedule函数。

前面提到了阻塞态。一个进程在其生命周期内,可以存在多个状态,当进程在内核执行时需要读磁盘,此时进程会要进入阻塞态,等待资源;当进程等到资源时,就可以进入就绪态了;如果之后进程抢到了CPU,那么进程又进入了运行态。因此可以设计一个进程状态图,用来描述进程的各种状态之间的转换关系。在struct task_struct中就有一个成员变量 state,用于记录进程当前的运行状态。

最后还有一个问题,并发时如何保证各个进程不相互干扰?比如说进程1执行了 mov [100], ax ,而内存地址[100]处恰好有进程2存放的重要数据,如果让进程1执行了该指令,那进程2的重要数据就被破坏了。一种办法是将各个进程的地址空间分离开来,比如进程3、进程4都调用了mov [200], ax,那就把进程3的[200]映射到物理内存的 7000H 处,而将进程4的[200]映射到物理内存的 8000H 处。利用映射表(实际上也就是MMU)将各个进程的地址空间分离。这部分属于操作系统内存管理的部分,之后再分析。

3 一个实际的进程切换案例

本节主要分析Linux0.11中进程切换的过程。Linux中使用PCB来描述一个进程,实际上PCB就是一个结构体对象,下面列出了本节会用的的该结构体的几个重要字段:

struct task_struct {long state;   //进程当前运行状态,有TASK_RUNNING(就绪态)、TASK_INTERRUPTIBLE等几种取值long counter; //任务运行时间计数,即运行时间片。采用递减方式,counter越大表明任务已经运行的时间越短long priority;//运行优先数,用于给counter赋初值。一个进程刚被创建时counter = priority。
...
}

3.1 进程的创建 - fork函数

调用 fork() 时会创建一个子进程,因此分析进程切换应该从 fork() 开始。 fork() 的执行过程如下:

  1. fork()内执行int 0x80指令,进入内核
  2. 执行system_call:程序(汇编程序)
  3. 执行sys_fork:程序(汇编程序)
  4. 执行copy_process()函数(C程序)

copy_process()才是真正创建子进程的地方。"sys_fork:"程序调用"copy_process()"是汇编调用C函数的过程,copy_process()中的那一大堆形参都是通过在汇编程序中压栈传递的,可以看出在copy_process()前面的汇编程序将许多寄存器进行了压栈。copy_process()的工作内容如下(程序内容进行了裁剪):

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,long ebx,long ecx,long edx,long fs,long es,long ds,long eip,long cs,long eflags,long esp,long ss)
{struct task_struct *p;int i;struct file *f;
//1、为子进程的PCB分配空间,注意这里分配了一页内存(4KB),
//其实这里连同内核栈的空间一起分配了p = (struct task_struct *) get_free_page();if (!p)return -EAGAIN;
//2、将子进程加入到总调度队列中。nr指向了task[]中的一个空位置task[nr] = p;
//3、将父进程(current:当前进程)的PCB复制给子进程,然后修改子进程PCB的部分字段*p = *current;	/* NOTE! this doesn't copy the supervisor stack *///这样做不会复制堆栈部分,只复制结构体p->state = TASK_UNINTERRUPTIBLE;p->pid = last_pid;/* last_pid是最新进程号,也就是子进程的pid */p->father = current->pid;p->counter = p->priority;
...
//4、设置子进程内核栈的栈顶指针指向"p"这一页内存的最高处(地址最大处)p->tss.esp0 = PAGE_SIZE + (long) p;p->tss.ss0 = 0x10;  //0x10为内核数据段的选择符p->tss.eip = eip;/* 让子进程和父进程执行相同的程序。注意这里的eip是在执行 int 0x80 压入的eip,也就是说子进程在下次被调度执行的的时候(也就是第一次被调度的时候),是从 int 0x80 后面一句指令开始执行的,而不是从copy_process()开始执行*/p->tss.eflags = eflags;p->tss.eax = 0;/* 子进程fork()完后返回0的原因所在*/
...
//5、设置子进程的用户栈,让子进程与父进程共用一个用户栈p->tss.esp = esp;p->tss.ss = ss & 0xffff;
...
//6、将子进程设置为就绪态,然后父进程返回p->state = TASK_RUNNING;	/* do this last, just in case */return last_pid;/* return会让返回值(last_pid)保存在eax中。这里是父进程在fork()完后要返回的子进程的pid。那么子进程fork()完后要返回的0在哪里返回的呢?在 _syscall0(int,fork) 函数的那个return返回。*/
}

在执行完copy_process()后,子进程的内核栈就被创建成了如下模样:

图3.1 内核栈

3.2 进程的切换 - schedule函数

本节主要分析定时器中的进程切换,即do_time()中的进程切换。其实schedule()除了在do_time()中被调用外,在其他地方也有被调用。
Linux0.11中有一个定时器中断,每10ms进入一次,在这个中断中调用了do_time()。这个定时器中断主要做了如下工作:

timer_interrupt:
...
#1、让jiffies加1。jiffies为全局变量,表示从开机时到现在发生的时钟中断次数,这个数也被称为“滴答数”。incl jiffies
...
#2、调用do_timer(), 其中eax为do_timer()的传入参数,当前特权级。movl CS(%esp),%eaxandl $3,%eax		# %eax is CPL (0 or 3, 0=supervisor)pushl %eaxcall do_timer		# 'do_timer(long CPL)' does everything from
...

do_timer()主要工作如下:

void do_timer(long cpl)
{
...
#1、当前任务运行计数值减1,若计数值不为0(即时间片还未用完),则继续运行当前线程。if ((--current->counter)>0) return;current->counter=0;if (!cpl) return;#若是内核代码则不进行调度,因为内核代码不参与调度。
#2、若时间片用完了,则进行调度,切换到下一个任务schedule();
}

从do_timer()可以看出,Linux0.11中采用了时间片的机制来切换进程,即:给每个进程分配一个时间片,若时间片用完了则调用schedule()函数。schedule()会重新分配各个进程的时间片,并在 task 队列中找到下一个需要运行的进程,然后调用switch_to。switch_to将当前进程的寄存器状态保存起来(保存在当前进程的 tss 中),然后将下一个进程的tss中的内容扣在CPU的寄存器中(包括PC指针)从而实现了进程的切换。关于schedule()的详细注释可以参考实验4:基于内核栈切换的进程切换

3.3 进程状态转换图

本节主要介绍几个与状态切换相关的函数。进程状态图如下:

图3.2 进程状态转换图

顺便贴上一个进程状态表:

内核表示含义
TASK_RUNNING可运行(就绪态或运行态)
TASK_INTERRUPTIBLE可中断的等待状态,是阻塞态的一种
TASK_UNINTERRUPTIBLE不可中断的等待状态,是阻塞态的一种
TASK_ZOMBIE僵尸态(图中的终止态)
TASK_STOPPED暂停

下面列出几个有改变进程状态功能的函数,帮助理解进程状态转换图:

  1. do_exit():由sys_exit()函数调用。会将当前进程置为僵尸态,然后调用schedule()切换到下一个进程;
  2. sys_waitpid():回收子进程,若子进程还未变为僵尸态,则该函数会将当前进程变为阻塞态(TASK_INTERRUPTIBLE),然后调用schedule()切换到下一个进程;
  3. copy_process():创建子进程,创建前子进程为新建态,创建结束后会将子进程设置为就绪态;
  4. schedule():调度函数。首先进行信号处理,可能会将一些阻塞态的进程变为就绪态。然后找到下一个需要运行的进程,并执行它(此时该进程就变为运行态了);
  5. sys_pause():将当前进程变为阻塞态(TASK_INTERRUPTIBLE),然后调用schedule()切换到下一个进程;
  6. wake_up():将进程置为就绪态;

3.4 如何执行我们自己的代码

子进程在被创建之后默认执行的是父进程的代码,通常子进程可以调用execve()来加载执行自己的代码。execve()是一个系统调用,真正实现其功能的是do_execve()函数,这两个函数的原型如下:

int execve(const char * filename, char ** argv, char ** envp);
int do_execve(unsigned long * eip,long tmp,char * filename,char ** argv, char ** envp)

应用程序调用 execve() 函数,可以进入内核。当一个任务进入内核态运行时,就会使用其 TSS 段给出的特权级0的堆栈指针:tss.ss0、tss.esp0,即内核栈。 也就是说进程进入内核态后,硬件自动帮进程由用户栈切换到内核栈

和其他系统调用一样,execve()也是通过压栈的方式向内核传递参数。从调用execve(),到执行do_execve()前,内核栈的被压入了如下内容:

图3.3 执行exevc函数后的压栈情况

可以看出此时的内核栈中有子进程的PC指针和用户栈指针。为了能让子进程能去执行新的程序,do_execve()会替换掉栈中的PC指针和用户栈指针,将原来的PC指针替换为新执行程序的运行地址。当中断返回,执行“iret”指令后,栈中的PC指针和用户栈指针就会被弹出,并赋值到对应的寄存器中,从而让子进程切换到新的程序去执行。

关于内核栈和用户栈之间切换的方式在《Linux内核完全剖析——基于0.12内核》的第5.8节:Linux系统中堆栈的使用方法,中有详细介绍。

参考资料

图3.2 进程状态图截取自哈工大操作系统课程的课件。

[1] 操作系统-哈尔滨工业大学-中国大学MOOC
[2] 哈工大操作系统实验手册
[3] Linux内核完全剖析——基于0.12内核


http://chatgpt.dhexx.cn/article/UvbkyEIJ.shtml

相关文章

操作系统之进程创建与进程状态

一、进程的创建 阻塞状态:正在运行的进程由于某些原因调用阻塞原语把自己阻塞(如果不把自己阻塞的话会一直占用处理机),等待相应的事件出现后才被唤醒,事件完成回到就绪状态。 通常这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞…

linux进程管理原理

Linux 是一种动态系统,能够适应不断变化的计算需求。 linux 计算需求的表现是以进程的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程…

进程系统调用——fork函数深入理解

转载 进程系统调用——fork函数深入理解 当我们在一个现代系统上运行一个程序的时候,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一程序。我们的程序好像是独占的使用处理器和存储器。处理器就是无间断的一条一条地执行我们程序中的指令。…

进程调度实验

一、实验目的 通过编写程序实现进程或作业先来先服务、高优先权、按时间片轮转调度算法,进一步掌握进程调度的概念和算法,加深对处理机分配的理解。了解进程(线程)的调度机制。学习使用进程(线程)调度算法…

基于C++实现的进程调度算法

资源下载地址:https://download.csdn.net/download/sheziqiong/85650672 一、问题描述与分析 1.1 设计构想 程序能够完成以下操作:选择调度算法;查看历史记录;创建进程;进程调度:进程创建完成后就选择进程调度算法,每次执行的结果都在屏幕上输出。 1…

短进程优先调度算法c语言spf,短进程优先的调度算法详解

短进程优先的调度算法详解 发布时间:2020-05-17 04:52:01 来源:51CTO 阅读:293 作者:张立达 一、SPF算法简介 SJF算法SJF(shortest job first)是以进程的运行时间长度作为优先级,进程运行时间越短,优先级越高。 SJF算法的缺点必须预知进程的运行时间。即使是程序员也很难…

进程调度算法(c语言)

对一个非抢占式多道批处理系统采用以下算法的任意两种,实现进程调度,并计算进程的开始执行时间,周转时间,带权周转时间,平均周转时间,平均带权周转时间 1.先来先服务算法 2.短进程优先算法 *3.高响应比优先算法 一、设计思想 每个进程有一个进程控制块(…

进程的创建——fork函数

1. 进程的信息 进程的结构 在Linux中,一切皆文件,进程也是保存在内存中的一个实例,下图描述了进程的结构: 堆栈:保存局部变量数据段:一般存放全局变量和静态变量代码段:存储进程的代码文件TSS状态段:进程做切换时,需要保存进程现场…

C语言结构体

本节主要讲解下结构体的一些易错点和重要内容 结构体变量定义 (使用typedef起别名) 一般的结构体定义:定义类型变量 struct student {long stuID;char stuName[10];char stuSex;char birthYear;int mathScore; }stu1;可以用typedef取别…

深入探索 Linux 进程信号的奥秘

Linux 进程信号 0 查看IPC(进程间通信)资源的指令1 学习进程信号的过程2 Linux 进程信号的基本概念2.1 对信号的基本认知 3 Linux 进程信号的产生方式4 Linux 进程信号的保存和处理5 Linux 进程信号递达6 volatile关键字 0 查看IPC(进程间通信)资源的指令 ipcs -m : 查看共享内…

Linux 进程信号深剖

目录 传统艺能😎概念🤔信号发送🤔信号记录🤔信号产生🤔常见信号处理方式🤔终端按键产生信号🤔核心转储😋如何调试🤔 系统函数发送信号🤔raise函数&#x1f91…

Linux进程信号

文章目录 一.信号入门二. 产生信号(1). 通过键盘按键产生信号(2). 硬件异常产生信号(3).通过系统函数发送信号(4). 由软件条件产生信号 三.阻塞信号(1). 阻塞/递达/未决概念 :(2). 信号在内核中的表示(3). sigset_t(4). 信号集操作函数(5). 处理信号 四. 可重入函数/不可重入函…

[培训-DSP快速入门-7]:C54x DSP开发环境与第一个汇编语言程序

作者主页(文火冰糖的硅基工坊):https://blog.csdn.net/HiWangWenBing 本文网址:https://blog.csdn.net/HiWangWenBing/article/details/119011489 目录 引言: 第1章 DSP汇编语言编程的流程概述 第2章 汇编语言程序建立过程 2.1 建立工程…

[培训-DSP快速入门-6]:C54x DSP开发中C语言库函数的使用

作者主页(文火冰糖的硅基工坊):https://blog.csdn.net/HiWangWenBing 本文网址:https://blog.csdn.net/HiWangWenBing/article/details/119010855 目录 第1章 DSP库函数概述 第2章 运行时支持库 2.1 如何加入运行时支持库 2.2 为什么需要运行时的库…

【DSP开发】帮您快速入门 TI 的 Codec Engine

德州仪器半导体技术(上海)有限公司 通用DSP 技术应用工程师 崔晶 德州仪器(TI)的第一颗达芬奇(DaVinci)芯片(处理器)DM6446已经问世快三年了。继DM644x之后,TI又陆续推出…

DSP开发,使用CCS软件建立工程以及烧录

DSP开发,使用CCS软件建立工程以及烧录 1 概述1.1 资源概述1.2 DSP介绍 2 工程建立步骤4 烧录到flash中4.1 通过增减文件实现4.2 增加预编译宏 5 独立下载工具5.1 Uniflash5.2 C2prog 6 程序源码6.1main()函数6.2 leds.c文件6.3 leds.h文件 1 概述 实验的代码已经上…

浅谈DSP开发创建第一个工程Hello World

浅谈DSP开发创建第一个工程Hello World 本教程以TI公司的TMS320F2812芯片为例进行演示开发环境搭建(CCS)CMD文件概述编写第一个工程Hello World概述 本教程以TI公司的TMS320F2812芯片为例进行演示 开发环境搭建(CCS) 首先开发环境问题:目前最新TI官方发布的开发环…

DSP(数字信号处理器)技术概要

数字信号处理器(digital signal processor,DSP)是一种用于数字信号处理的可编程微处理器,它的诞生与快速发展,使各种数字信号处理算送得以实时实现,为数字信号处理的研究和应用打开了新局面,提供了低成本的实际工作环境和应用平台…

DSP_1 环境搭建

1、打开ccs6.0,将DSP281x_common、DSP281x_headers两个库文件导入到根目录当中。 2、在project的Properties当中添加库文件路径,使编译器能够找到这些文件。 3、exclude那些重定义的文件,即可。 4、编译的过程分为compile与link&#xff0c…

从BES蓝牙耳机开发中谈DSP开发与嵌入式ARM的区别

对比下DSP开发与嵌入式ARM的区别,DSP开发,发开算法,注意链接文件的使用。 一先看BES的DSP开发 1 lds链接文件之代码段text 存放可执行代码和浮点数常量 2 data数据段 3 .bss段 存放没有初始化的全局变量和静态变量。 二 ARM开发 主要是配置…