进程的概念

[toc]

进程的概念与创建

进程的概念

程序的概念

一般的讲,程序是一系列有序指令的集合,目的是告诉计算机如何完成某些指定的操作或者如何解决某个问题。

计算机早期程序采用机器语言设计,但是由于开发效率较低,所以后面设计出汇编语言,但是由于汇编语言的汇编指令较多,并且不同的公司及组织对研发平台设计出不同的汇编指令集,而这些汇编指令集并不兼容,所以为了进一步提高开发效率和降低开发难度,就设计出高级语言,比如面向过程编程的C语言,和面向对象编程的C++以及其他编程语言。

img

开发人员根据编程语言的语法和规则设计出程序,程序的格式和类型一般由使用的编程语言决定,比如采用C语言设计的程序就是以xxx.c作为后缀名,采用Python设计的程序就是以xxx.py作为后缀名。

程序只是开发人员为了实现某些需求设计出来的文本,是存储在计算机的磁盘中的,想要得到程序的运行结果,是需要使用编译器等工具对程序进行编译等一系列操作,目的是把程序转换为可执行文件,可执行文件内部其实就是对应平台可以识别的指令和数据的集合,

可执行文件得到运行就是计算机的中央处理器CPU对可执行文件内部的指令进行处理,对可执行文件内部的数据进行运算的过程。

img

大家想要了解程序中指令和数据具体的执行流程,需要熟悉计算机组成原理以及汇编语言,其中计算机组成部分一般遵循冯诺依曼结构,也就是由****控制器、运算器、存储器、输入设备、输出设备五个部分组成****。

img

程序的编译

一般在编写出程序之后,并不能直接运行,而是需要把程序通过编译器进行编译,生成可执行文件才能运行。而对于嵌入式学习,一般都是在Linux系统下使用GCC编译器对程序进行编译。

img

GCC编译器是Linux系统默认的C/C++编译器,大部分Linux发行版本中都是默认安装的。GCC编译器主要以Linux命令的形式在shell终端中使用,所以需要大家掌握关于GCC编译器的相关参数。

img

img

C语言程序编译过程:源程序 ---- 预处理 --- 编译 --- 汇编 --- 链接 --- 可执行文件

*预处理*

对源码进行简单的加工,GCC编译器会调用预处理器cpp对程序进行预处理,其实就是解释源程序中所有的预处理指令,如#include(文件包含)、#define(宏定义)、#if(条件编译)等以#号开头的预处理语句。

这些预处理指令将会在预处理阶段被解释掉,如会把被包含的文件拷贝进来,覆盖掉原来的#include语句,把所有的宏定义展开,所有的条件编译语句被执行,GCC还会把所有的注释删掉,添加必要的调试信息。

预处理指令: gcc -E xxx.c -o xxx.i 会生成预处理文件 xxx.i

*编译*

就是对经过预处理之后的.i文件进行进一步翻译,也就是对语法、词法的分析,最终生成对应硬件平台的汇编文件,具体生成什么平台的汇编文件取决于编译器,比如X86平台使用gcc编译器,而ARM平台使用交叉编译工具arm-linux-gcc。

编译指令:gcc -S xxx.i -o xxx.s 会生成对应汇编文件 xxx.s

*汇编*

GCC编译器会调用汇编器as将汇编文件翻译成可重定位文件,其实就是把.s文件的汇编代码翻译为相应的指令。

编译指令:gcc -c xxx.s -o xxx.o 会生成汇编后的文件 xxx.o

*链接*

经过汇编步骤后生成的.o文件其实是ELF格式的可重定位文件虽然已经生成了指令流,但是需要重定位函数地址等,所以需要链接系统提供的标准C库和其他的gcc基本库文件等,并且还要把其他的.o文件一起进行链接。-lc -lgcc 是默认的,可以省略

编译指令:gcc hello.o -o hello -lc -lgcc 会得到可执行文件 xxx // l是lib的缩写

程序的格式

一般对源文件进行编译和汇编之后,就会得到对应的目标文件xxx.o,目标文件也被称为可重定位文件,就是指还未完成链接的中间文件,用户可以把这些目标文件制作成在Linux系统下使用的静态库libxxx.a或者动态库libxxx.so。

其实目标文件和可执行文件的内容和结构非常相似,所以Linux系统下把目标文件xxx.o和可执行文件都称为ELF文件,ELF指的是Executable Linkable Format的缩写,中文翻译为可执行可链接格式。

注意:可以在linux系统中利用查看文件格式的指令: file 查阅目标文件和可执行文件的区别:

img

img

思考:还未完成链接的目标文件中存储了哪些内容,以及如何查看目标文件中的内容信息???

回答:目标文件中的内容至少有编译后的机器指令代码、数据。除了这些内容以外,目标文件中还包括了链接时所需要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储有时候也叫“段”(Segment),一般情况下,它们都表示一个一定长度的区域,基本上不加以区别

程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字有“.code”或“.text”。全局变量和局部静态变量数据经常放在数据段(Data Section),数据段一般名字都叫“.data”。

注意:linux系统的GCC编译套件有一款工具叫做objdump,可以查看目标文件内部的数据!

img

数据段中有一个叫做.bss段, bss以前其实是汇编伪指令,作用是为某些数据预留一块空间,bss其实是Block Started by Symbol,也就是用于存储未被初始化的全局变量和静态局部变量,但是在程序编译阶段是没有分配空间的,在程序运行时会得到空间。

img

*常用的段名* *说* *明*
.rodatal 这种段里存放的是只读数据,比如字符串常量、全局const变量。跟".rodata"一样
.comment 存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0”
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号与编译后指令的对应表
.note 额外的编译器信息。比如程序的公司名、发布版本号等
.strtab String Table.字符串表,用于存储ELF文件中用到的各种字符串
.symtab Symbol Table.符号表
.shstrtab Section String Table.段名表
.plt 动态链接的跳转表和全局入口表
.init 程序初始化与终结代码段。

另外,objdump工具也可以实现把可执行文件进行反汇编,可以把得到的反汇编代码重定向到某个文本中进行查看 “objdump -D demo > > xxx.txt”

进程的概念

当源文件编译通过并且完成链接动作之后,则得到一个可以执行的ELF文件,ELF文件中包含了程序的指令和数据以及相关函数接口的跳转地址等信息当用户运行程序时,操作系统就会把程序相关的指令和数据载入内存,并为程序分配对应的内存空间,然后通知CPU完成程序的相关动作。

用户编写的源文件是存储在外存(磁盘)中的,而使用编译器生成的ELF可执行文件也是存储在外存中的,这两者都是静态的,操作系统也并没有为两者分配系统资源。

img

当可执行文件得到运行,操作系统才会把可执行文件中的指令和数据从外存中读取出来并写入内存,并且会为程序分配系统资源(内存空间、CPU使用权等),所以程序从静态变为动态,这种正在进行中的程序,操作系统就把其称之为进程(process)。也可以理解为进程是程序在处理器上的一次执行过程

进程定义:进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行程序的实例。

*注意:进程是操作系统分配资源的基本单位!操作系统是以进程为单位来分配系统资源的,比如内存空间、CPU使用权等。 线程是操作系统调度资源的最小单位! 进程包含线程!*

进程的特征

进程具有四个基本特征,分别是动态性、并发性、独立性、异步性,具体的区别如下所示:

(1) 动态性:进程是程序在处理器上的一次执行过程,因而是动态的。比如进程会在程序运行时被创建出来,当无法获取足够的系统资源时进程会暂停,当得到资源后会继续执行。

(2) 并发性:多个进程同时存在于内存中,目的是与其他程序并发执行,提高资源的利用率。

(3) 独立性:进程是一个能独立运行的基本单位,也是系统进行资源分配和调度的独立单位。

(4) 异步性:指的是进程以各自独立的、不可预知的速度向前推进。一个进程的执行过程可能是无法被预测的,因为会受到各方面因素的影响,比如当系统资源不够时或者出现其他进程抢占资源时,都会影响进程的执行进度。

进程的组成

思考:操作系统中可能会有n个进程同时执行,请问操作系统如何掌握这些进程的执行情况?

回答:一般进程在创建之后,操作系统都会为进程分配一块内存来记录进程的各项参数信息,所以这块内存被称为进程控制块(Processing Control Block,缩写为PCB)。每个进程都有PCB,用于标识进程的存在以及记录进程的信息。

在Linux系统中进程控制块PCB是以一个名称叫做struct task_struct的结构体作为载体来存储信息的,该结构体被定义在一个名称叫做sched.h的头文件中

img

进程一般由三个部分组成,分别是进程控制块、代码段、数据段。代码段指的是进程中能被调度程序调度到CPU上执行的程序代码段数据段指的是进程对应的程序原始数据或者程序执行过程中产生的中间数据等。

不同操作系统对于一个进程的信息记录是不同的,也就是PCB中的内容会有一些区别,但是大同小异,基本都会包含以下内容:

进程标识符

每个进程都有一个有系统分配的唯一的PID进程标识符(process identifier),用于区分系统中的其他进程,这个PID也是Linux内核提供给用户访问进程的一个接口,用户可以通过PID控制进程

比如Windows系统可以通过任务管理器了解系统中正在执行的进程的数量以及进程的状态:

img

思考:windows系统下可以通过任务管理器查看进程的PID,那Linux系统如何查看进程PID?

回答:Linux系统中提供了关于获取进程状态的shell命令:ps ,该命令的使用方法详见man手册,一般使用 ps -ef 或者 ps -aux来查看Linux系统中所有用户相关的进程的所有信息。

img

img

进程当前状态

在进程的运行过程中由于系统中多个进程的并发运行和相互制约的结果,所以使进程的状态不断发生变化。操作系统以进程的状态来作为调度程序分配处理器的依据

通常一个运行中的进程至少可划分为5种基本状态:就绪态、运行态、阻塞态、创建态、结束态。当然进程状态的叫法是比较灵活的,大家不用死记硬背,重在理解相关概念。

  1. 创建态

指的是进程正在被创建,但是还没有准备好,也就是还没有达到就绪状态,此时系统申请进程PCB需要的内存空间,并向PCB中填写关于进程的管理信息,然后系统分配进程运行需要的资源,最后把进程转换为就绪状态。

  1. 就绪态

指的是进程已经获得了除了处理器之外的其他资源,相当于“万事俱备,只欠东风”,只要进程获得处理器资源,就可以立即执行。

  1. 执行态

执行态也被称为运行态,指的是进程已经获得了其他资源和处理器资源,并正在被CPU执行的状态。

  1. 阻塞态

阻塞态也被称为暂停态,指的是进程在执行过程中由于某个事件导致无法继续执行下去,此时进程处于暂时停止的状态。

  1. 结束态

指的是进程正在从系统中消失,原因可能是进程正常退出或者由于其他问题中断退出运行。有一种情况比较特殊,就是如果该进程退出之后资源还没有释放,但是此时进程已经无法被调度和运行,进程就会进入僵尸态,此时用户需要对这类进程进行处理,避免占用过多资源。

img

思考:Linux系统下运行了n个进程,每个进程的状态都不同,请问如何区分进程的状态???

回答:在Linux系统的终端中输入shell指令: ps -aux,就可以看到当前系统中运行的进程状态。

img

img

注意:进程不是一直处于某一个状态,进程的状态会受到外界因素和执行进度的影响而发生改变,当然,进程的状态在某一个时刻是唯一的,也就是在某一时刻进程必须且只能处于一种状态。

进程的控制

进程控制指的是对系统中的所有进程实施有效的管理,其功能一般包括进程的创建、进程的撤销、进程的阻塞与唤醒等。这些功能一般是由操作系统的内核来实现的。

进程的创建

Linux系统中的一个进程中可以创建若干个新进程,新创建的进程中又可以创建子进程,所以一般使用****进程前趋图来描述创建的进程之间的关系****。进程前趋图也被称为进程树,是为了描述进程家族关系的树。

img

比如在进程A中创建了一个新进程B,则进程B就是进程A的子进程,进程A就是进程B的父进程。

如果在进程A中创建了2个子进程B和C,进程B中创建了2个子进程D和E,进程C中创建了1个子进程F,则进程A就是该进程家族的祖先。

在Linux系统中运行的所有进程也可以构成一个进程树,并且该进程树也有一个祖先,用户可以通过Linux系统提供的shell命令:pstree 来打印进程关系。

img

可以发现,Linux系统中的所有进程都和一个进程有关系,那就是systemd进程,该进程是Linux系统运行的第一个进程,英文全称是system daemon,中文翻译为系统守护进程,系统中其他进程都是该进程的子进程。

img

守护进程也被翻译为精灵进程或者后台进程,是一种旨在运行于相对干净环境、不受终端影响的、常驻内存的进程,就像神话中的精灵拥有不死的特性长期稳定提供某种功能或服务

systemd其实是一个 Linux 系统基础组件的集合,它提供了一个系统和服务管理器,然后运行为 PID 1的进程,负责在系统启动或运行时激活系统资源,并且管理服务器进程和其它进程。

img

注意:Linux系统中只有进程才可以在系统运行,所以一个程序想要运行,就必须为该程序创建进程。linux内核提供了一个名字叫做fork()的系统调用接口,该接口可以在进程中创建一个子进程。

img

img

可以看到fork函数的返回值对于父进程和子进程而言是不同的,fork函数在父进程中返回的是创建成功的子进程的PID,fork函数在子进程中的返回值是0,当然,如果子进程失败则返回-1。

思考:由于子进程会拷贝父进程的代码段和数据段,所以父进程剩余的程序会执行两遍,请问如何区分到底是哪个进程在执行程序?是否父进程的程序会优先于子进程执行?

回答:通过fork函数的返回值来区分父进程和子进程,父进程和子进程的执行顺序是由操作系统的调度器决定的。也可以选择利用某些机制来调整执行顺序。

练习:编写程序,要求在程序中创建一个子进程,让父进程和子进程分别打印不同的内容。

img

img

练习:设计一个程序,要求在进程中创建一个子进程,并区分父进程和子进程,并在父进程中输出进程的PID,在子进程中输出子进程的PID和子进程的父进程的PID。提示:获取当前进程PID的函数接口getpid(),获取当前进程的父进程PID的函数接口是getppid()

img

思考:调用fork函数之后可以创建一个子进程,但是调用了一次fork函数为什么会有两个不同的返回值?

回答:就是调用fork的时候子进程已经拷贝了父进程的代码段、数据段、堆栈段,所以fork函数还没有执行完成,就会在两个进程空间继续执行,就会得到两个不同的返回值。

进程的撤销

一个进程在完成自身任务之后应该及时撤销,这样就可以及时的释放进程的资源,此时可以分为两种情况:一种是撤销指定进程,另一种是撤销指定进程以及该进程的所有子孙进程。不管是哪种情况,都应该及时回收进程占用的资源。

Linux系统中提供了一个名称叫做wait()的系统调用接口,该接口用于让父进程等待子进程的状态改变并获取已经改变状态的子进程的信息。

img

僵尸态指的是进程终止但是并未释放相关资源的一种状态,此时由系统内核对处于僵尸态的进程进行维护,处于僵尸态的进程会占用创建进程的资源和数量,如果僵尸态进程数量太多,则会导致系统无法创建新进程

所以父进程应该及时的对终止的子进程进行等待,这样就可以回收子进程占用的资源,当然,如果父进程终止,则处于僵尸态的子进程会由系统内核完成资源的回收

注意:如果当前进程没有子进程,则wait函数立即返回,如果当前进程有很多个子进程则wait函数会回收第一个变为僵尸态的子进程资源。

wait函数的参数是一个指针,用于记录子进程的退出状态,如果该参数为NULL,则表示当前进程放弃子进程的退出状态。对于该指针中记录的值,用户可以通过系统提供的宏定义来分析子进程的退出状态。

img

通过man手册可以发现Linux内核还提供了另一个叫做waitpid()的系统调用函数,该函数也可以等待子进程状态改变并回收子进程的系统资源,只不过该函数的针对性更强,可以指定回收某个子进程的系统资源。

img

img

练习:编程产生一个进程链,父进程派生一个子进程后,输出自己的PID,然后退出,该子进程继续派生子进程,然后打印PID,然后退出,以此类推,要求父进程比子进程先输出PID,另外进程链的数量由用户通过键盘输入。

进程的执行

思考:既然在一个进程中可以创建多个子进程,并且子进程和父进程的数据段和代码段是相同的,也就是一份程序会执行两次,一般情况下是没有意义和必要的,请问能否让子进程单独的加载新的程序的代码段和数据段,如果可以,应该如何实现?

回答:当然是可以的,Linux系统标准库中提供了一组函数接口来实现在进程中加载和执行指定的程序。

img

img

这组接口可以把exec作为前缀,然后以exec后面的字符进行分类,比如l指的是list的缩写,p指的是path的缩写,e指的是environment的缩写,v指的是vector的缩写。

(1) l : 以列表的方式来组织指定程序的参数

(2) v: 以数组的方式来组织指定程序的参数

(3) e: 执行指定程序前顺便设置环境变量

(4) p: 执行程序时可自动搜索环境变量PATH的路径

img

以 execl(const char *pathname, const char *arg, ...) 为例,参数pathname是需要加载的指定程序,而arg则是该程序运行时的命令行参数,命令行参数包括程序名本身,并且全部是字符串,其实和带参数的main函数通过命令行传参的流程类似,但是函数的参数列表必须以NULL结束,也就是函数的最后一个参数填NULL即可。

练习:编写一个程序,使之产生一个子进程,在子进程中执行系统命令ls -l去查看某个文件的信息,父进程判断子进程是否执行成功。要求父进程等待子进程结束之后再结束。提示:Linux系统中的shell命令属于可执行文件

其实**,linux系统还提供一个函数,名称叫做system(),这个函数也可以让新进程执行shell命令。**

img

posted @ 2024-06-02 22:09  晖_IL  阅读(4)  评论(0编辑  收藏  举报