您当前的位置: 首页 > 

风间琉璃•

暂无认证

  • 2浏览

    0关注

    337博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

进程的定义与创建

风间琉璃• 发布时间:2021-09-01 21:06:05 ,浏览量:2

1.进程的定义

(1)程序 程序(program)是一个普通文件,是为了完成特定任务而准备好的指令序列与数据的集合,这些指令和数据以”可执行映像”的格式保存在磁盘中。

正如我们所写的一些代码,经过编译器编译后,就会生成对应的可执行文件,那么这个就是程序,或者称之为可执行程序

(2)进程 进程(process)则是程序执行的具体实例,比如一个可执行文件,在执行的时候,它就是一个进程,直到该程序执行完毕。

进程实体由数据段,程序段,进程控制块PCB(Process Control Block)组成

在这里插入图片描述 进程的特征 在这里插入图片描述

进程和程序的区别和联系

区别:
1.进程是动态的;程序是静态的
程序只是一系列指令序列与数据的集合,它本身没有任何运行的含义,它只是一个静
态的实体。
而进程则不同,它是程序在某个数据集上的执行过程,它是一个动态运行的实体,有
自己的生命周期,它因启动而产生,因调度而运行,因等待资源或事件而被处于等待																																																																																																																																																												
状态,因完成任务而被销毁。

2.进程有独立性,能并发执行; 程序不能并发执行

3.组成不同。进程包含PCB、程序段、数据段。程序包含数据和指令代码

4.程序是一个包含了所有指令和数据的静态实体。本身除占用磁盘的存储空间外,并
不占用系统如CPU、内存等运行资源。
进程由程序段、数据段和PCB构成,会占用系统如CPU、内存等运行资源

5.二者无一一对应关系
进程和程序并不是一一对应的,一个程序执行在不同的数据集上运行就会成为不同的
进程,可以用进程控制块来唯一地标识系统中的每个进程。而这一点正是程序无法做
到的,由于程序没有和数据产生直接的联系,既使是执行不同的数据的程序,他们的
指令的集合依然是一样的,所以无法唯一地标识出这些运行于不同数据集上的程序。
一般来说,一个进程肯定有一个与之对应的程序,而且有且只有一个。而一个程序有
可能没有与之对应的进程(因为这个程序没有被运行),也有可能有多个进程与之对
应(这个程序可能运行在多个不同的数据集上)。


联系:进程不能脱离具体程序而虚设, 程序规定了相应进程所要完成的动作
2.进程相关术语

在 Linux 中是通过检查表记录与进程相关的信息的,进程表就是一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的 PID、进程的状态、命令字符串和其他一些 ps 命令输出的各类信息。操作系统通过进程的 ID 对它们进行管理,这些 PID 是进程表的索引

查看进程: ps –aux 目前在系统上运行的所有进程 pstree 将进程以树状关系列出来

(1)进程 ID Linux 系统中的每个进程都都会被分配一个唯一的数字编号,我们称之为进程 ID(ProcessID,通常也被简称为 PID)。

进程 ID 是一个 16 位的正整数,默认取值范围是从 2 到 32768(PID 数字为 1 的值一般是为特殊进程 init 保留,即系统在运行时就存在的第一个进程, init 进程负责管理其他进程),由 Linux 在启动新进程的时候自动依次分配,当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的 PID

(2)父进程 ID 任何进程(除 init 进程)都是由另一个进程启动,该进程称为被启动进程的父进程,被启动的进程称为子进程, 父进程的进程号(PID)即为子进程的父进程号(PPID)。用户可以通过调用 getppid() 函数来获得当前进程的父进程号

进程启动时,启动进程是新进程的父进程,新进程是启动进程的子进程 每个进程都有一个父进程(除了系统中如“僵尸进程”这种特殊进程外) 可以把 Linux中的进程结构想象成一个树状结构,其中 init 进程就是树的“根”;或者可以把 init 进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由init 进程启动的,要么是由被 init 进程启动的其他进程启动的

(3)PCB PCB中记录了操作系统所需的,用于描述进程的当前情况以及控制进程运行的全部信息。 PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其他进程并发执行的进程。 OS是根据PCB来对并发执行的进程进行控制和管理的。 当OS要调度某进程执行时,要从该进程的PCB中查处其现行状态及优先级;在调度到某进程后,要根据其PCB中所保存的处理机状态信息,设置该进程恢复运行的现场,并根据其PCB中的程序和数据的内存始址,找到其程序和数据; 进程在执行过程中,当需要和与之合作的进程实现同步,通信或者访问文件时,也都需要访问PCB; 当进程由于某种原因而暂停执行时,又须将器断点的处理机环境保存在PCB中。 可见,在进程的整个生命期中,系统总是通过PCB对进程进行控制的,即系统是根据进程的PCB而不是任何别的什么而感知到该进程的存在的。 所以说,PCB是进程存在的唯一标志。

(4)ps命令 主要有两种: 1.ps aux 关注进程本身 2.ps axjf 关注进程间的关系

a   显示一个终端的所有进程
u  显示进程的归属用户及内存使用情况
x  显示没有关联控制终端的进程
j   显示进程归属的进程组id,会话id,父进程id
f   以ascII形式显示出进程的层次关系

ps aux 在这里插入图片描述 内容显示如上

user  			进程是那个用户产生的
pid    			进程的身份证号码
%cpu  			表示进程占用了cpu计算能力的百分比
%men  		表示进程占用了系统内存的百分比
vsz       		进程使用的虚拟内存大小
rss       		进程使用的物理内存大小
tty        		表示进程关联的终端
stat     			 表示进程当前的状态
time     		记录进程的启动时间
command    表示进程执行的具体程序

ps axjf 在这里插入图片描述

ppid       		表示进程的父进程
pid    			进程的身份证号码
pgid             进程所在进程组的id
sid   			进程所在会话的id
tty   				表示进程关联的终端
tpgid            值为-1,表示进程为守护进程
stat     			表示进程当前的状态
uid    启动进程的用户的id
time     		记录进程的启动时间
command    表示进程的层次关系

进程的状态 在这里插入图片描述 在内核中

进程状态:
  • TASK_RUNNING:就绪/运行状态
  • TASK_INTERRUPTIBLE:可中断睡眠状态
  • TASK_UNINTERRUPTIBLE:不可中断睡眠状态
  • TASK_TRACED:调试态
  • TASK_STOPPED:暂停状态
  • EXIT_ZOMBIE:僵死状态
  • EXIT_DEAD:死亡态

(5)进程组、会话、终端

进程组: 在shell里面直接执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程。进程组只有一个进程

如果进程调用了fork函数,那么父子进程同属一个进程组,父进程为首进程

在shell中通过管道执行连接起来的应用程序,两个程序同属一个进程组,第一个程序为进程组的首进程进程组id:pgid,由首进程pid决定

会话:

  • 调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话首进程
  • 用户在终端正确登录之后,启动shell时linux系统会创建一个新的会话,shell进程作为会话首进程会话id:会话首进程id,SID

作用:管理进程组 shell进程启动时,默认是前台进程组的首进程。 前台进程组的首进程会占用会话所关联的终端来运行,shell启动其他应用程序时,其他程序成为首进程

后台进程中的程序是不会占用终端 在shell进程里启动程序时,加上&符号可以指定程序运行在后台进程组里面

ctrl+z 前台切换后台 jobs:查看有哪些后台进程组 fg+job id可以把后台进程组切换为前台进程组

终端:

  • 物理终端

    • 串口终端
    • lcd终端
  • 伪终端

    • ssh远程连接产生的终端
    • 桌面系统启动的终端
  • 虚拟终端

    linux内核自带的,ctrl+alt+f0~f6可以打开7个虚拟终端

(6)僵尸进程和托孤进程

进程的正常退出步骤:

  • 子进程调用exit()函数退出
  • 父进程调用wait()函数为子进程处理其他事情

僵尸进程 子进程退出后,父进程没有调用wait()函数处理身后事,子进程变成僵尸进程

托孤进程 父进程比子进程先退出,子进程变为孤儿进程,Linux系统会把子进程托孤给1号进程(init进程)

程序变成进程

程序只是个静态的文件,而进程是一个动态的实体,程序到底是如何变成一个进程的呢

其实正如我们运行一个程序(可执行文件),通常在 Shell 中输入命令运行就可以了,在这运行的过程中包含了程序到进程转换的过程,整个转换过程主要包含以下 3 个步骤:

  1. 查找命令对应程序文件的位置。
  2. 使用 fork() 函数为启动一个新进程。
  3. 在新进程中调用 exec 族函数装载程序文件,并执行程序文件中的 main() 函数。
创建进程

(1)system() 函数 在这里插入图片描述

在这里插入图片描述 第 9行,就调用了这个 system() 函数,并且传入了一个命令“ls -l”这个命令与在 shell中运行的结果是一样的,调用 system() 函数的返回值就是被调用的 shell 命令的返回值。如果系统中 shell 自身无法运行, system() 函数返回 127;如果出现了其它错误, system() 函数将返回-1

局限:程序必须等待由 system() 函数启动的进程结束之后才能继续,因此我们不能立刻执行其他任务。 我们可以让"ls -l”命令在后台运行,只需在命令结束位置加上” &” 那么 system() 函数的调用将在 shell 命令结束后立刻返回。由于它是一个在后台运行程序的请求,所以 ls 程序一启动 shell 就返回了,

(2)fork() 函数 fork() 函数用于从一个已存在的进程中启动一个新进程,新进程称为子进程,而原进程称为父进程。使用 fork() 函数的本质是将父进程的内容复制一份,正如细胞分裂一样,得到的是几乎两个完全一样的细胞,因此这个启动的子进程基本上是父进程的一个复制品,但子进程与父进程有不一样的地方

子进程与父进程一致的内容: • 进程的地址空间。 • 进程上下文、代码段。 • 进程堆空间、栈空间,内存信息。 • 进程的环境变量。 • 标准 IO 的缓冲区。 • 打开的文件描述符。 • 信号响应函数。 • 当前工作路径。

子进程独有的内容: • 进程号 PID。 PID 是身份证号码,是进程的唯一标识符 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。 • 挂起的信号。这些信号是已经响应但尚未处理的信号,也就是”悬挂”的信号,子进程也 不会继承这些信号。

函数原型

pid_t fork(void);
返回值:
成功 :0或其他正整数
失败:-1

所在头文件

#include 
fork函数特性
  • 执行fork函数之后,fork函数会返回两次

  • 在子进程中返回时,返回值为0

  • 在旧进程返回时,返回值为进程的pid

叫做复制一个进程更加贴切

fork函数要点总结

在执行fork函数之前,操作系统只有一个进程,fork函数之前的代码只会被执行一次。

在执行fork函数之后,操作系统有两个几乎一样的进程,fork函数之后的代码会被执行两次。

在这里插入图片描述

在这里插入图片描述 首先调用了 fork 函数,调用 fork 函数后系统就会启动一个子进程,并且 子进程与父进程执行的内容是一样的(代码段),可以通过返回值 result 判断 fork() 函数的执行结果。 如果 result 的值为-1,那代表着 fork() 函数执行出错 如果返回的值为 0,则表示此时执行的代码是子进程,进程的 PID 通过 getpid() 函数获取得到。 如果返回的值大于 0,则表示此时执行的代码是父进程

注:父进程的返回值就是子进程的 PID,而子进程的返回值则是 0。而且子进程并不会再次执行 fork() 函数之前的内容,而 fork() 函数之后的内容在父进程和子进程都会执行一遍

(3)exec函数族 使用 fork() 函数启动一个子进程是并没有太大作用的,因为子进程跟父进程都是一样的,子进程能干的活父进程也一样能干

exec 系列函数,这个系列函数主要是用于替换进程的执行程序,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换。 exec 族实际包含有 6 个不同的 exec 函数,它们功能一样,主要是传参的形式不同都包含在头文件 <unistd.h>

int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
int execle(const char *path, const char *arg, ..., char *const envp[])
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])

l:代表以列表形式传参(list) v:代表以矢量数组形式传参(vector) p:代表使用环境变量Path来寻找指定执行文件 e:代表用户提供自定义的环境变量

要点总结
  • l后缀和v后缀必须两者选其一来使用

  • p后缀和e后缀是可选的,可用可不用

这些函数可以分为两大类, execl、 execlp 和 execle 传递给子程序的参数个数是可变的,如“ls -la”示例中,“-la”为子程序“ls”的参数。 execv、 execvp 和 execve 通过数组去装载子程序的参数,无论那种形式,参数都以一个空指针 NULL 结束,

后缀来区分他们的作用: • 名称包含 l 字母的函数(execl、 execlp 和 execle)接收参数列表“list”作为调用程序的参数。

• 名称包含 p 字母的函数(execvp 和 execlp)可接受一个程序名作为参数,它会在当前的执行路径和环境变量“PATH”中搜索并执行这个程序(即可使用相对路径);名字不包含 p 字母的函数在调用时必须指定程序的完整路径(即要求绝对路径)。

• 名称包含 v 字母的函数(execv、 execvp 和 execve)的子程序参数通过一个数组“vector”装载。

• 名称包含 e 字母的函数(execve 和 execle)比其它函数多接收一个指明环境变量列表的参数,并且可以通过参数 envp 传递字符串数组作为新程序的环境变量,这个 envp 参数的格式应为一个以 NULL 指针作为结束标记的字符串数组,每个字符串应该表示为“environment= virables”的形式

实列: 在这里插入图片描述 在这里插入图片描述 execl() 函数用于执行参数 path 字符串所代表的文件路径(必须指定路径),接下来是一系列可变参数,它们代表执行该文件时传递过去的 argv[0]、 argv[1]… argv[n] ,最后一个参数必须用空指针 NULL 作为结束的标志

execl() 函数的参数列表调用了 ls 命令程序,然后将第二个以后的参数当做该文件的argv[0]、 argv[1]… argv[n],最后一个参数必须用空指针 NULL 作为结束的标志。它其实就是与我们在终端上运行” ls -la”产生的结果是一样的

注:exec 系列函数是直接将当前进程给替换掉的,当调用 exec 系列函数后,当前进程将不会再继续执行,所以示例程序中的“Done! ”将不被输出,因为当前进程已经被替换了,一般情况下, exec 系列函数函数是不会返回的,除非发生了错误。出现错误时, exec 系列函数将返回-1,并且会设置错误变量 errno。

通过调用 fork() 复制启动一个子进程,并且在子进程中调用 exec 系列函数替换子进程,这样把 fork() 和 exec 系列函数结合在一起使用就是创建一个新进程所需要的一切

终止进程

终止进程方式有 5 种,可以分为正常终止与异常终止

正常终止 • 从 main 函数返回。 • 调用 exit() 函数终止。 • 调用 _exit() 函数终止。

异常终止 • 调用 abort() 函数异常终止。 • 由系统信号终止。

在 Linux 系统中, exit() 函数定义在 stdlib.h 中,而 _exit() 定义在 unistd.h 中, exit() 和 _exit() 函数都是用来终止进程的,当程序执行到 exit() 或 _exit() 函数时,进程会无条件地停止剩下的所有操作,清除包括 PCB 在内的各种数据结构,并终止当前进程的运行。 区别 在这里插入图片描述 _exit() 函数的作用最为简单:直接通过系统调用使进程终止运行,当然,在终止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构;而 exit() 函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序:比如 exit() 函数在调用 exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除 I/O 缓冲”

用_exit() 函数直接将进程关闭,缓冲区中的数据就会丢失。若想保证数据的完整性,就一定要使用 exit() 函数

函数原型

#include   --->  void _exit(int status)
#include   --->  void exit(int status)
status:进程终止时的状态码
0 表示正常终止,其他非 0 值表示异常终止
等待进程

当一个进程调用了 exit() 之后,该进程并不会立刻完全消失,而是 变成了一个僵尸进程。僵尸进程是一种非常特殊的进程,它已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。那么无论如何,父进程都要 回收这个僵尸进程,因此调用 wait() 或者 waitpid() 函数其实就是将这些僵尸进程回收,释放僵尸进程占有的内存空间

wait() 函数原型 头文件:#include

pid_t wait(int *wstatus)

wait() 函数在被调用的时候,系统将暂停父进程的执行,直到有信号来到或子进程结束,如果在调用 wait() 函数时子进程已经结束,则会立即返回子进程结束状态值。

子进程的结束状态信息会由参数 wstatus 返回,与此同时该函数会返子进程的 PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,如果不在意子进程的结束状态信息,则参数wstatus 可以设成 NULL

注意 wait() 要与 fork() 配套出现,如果在使用 fork() 之前调用 wait(), wait() 的返回值则为-1,正常情况下 wait() 的返回值为子进程的 PID。

参数 wstatus 用来保存被收集进程退出时的一些状态,它是一个指向 int 类型的指针,但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样做),我们就可以设定这个参数为 NULL。

进程退出状态的一些宏定义

WIFEXITED(status) :如果子进程正常结束,返回一个非零值
WEXITSTATUS(status):如果 WIFEXITED 非零,返回子进程退出码
WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
WTERMSIG(status) :如果 WIFSIGNALED 非零,返回信号代码
WIFSTOPPED(status):如果子进程被暂停,返回一个非零值
WSTOPSIG(status):如果 WIFSTOPPED 非零,返回一个信号代码

实例 在这里插入图片描述

在这里插入图片描述 (1) :首先调用 fork() 函数启动一个子进程。

(2) :如果 fork() 函数返回的值 pid 为 0,则表示此时运行的是子进程,那么就让子进程输出一段信息,并且休眠 3 秒。

(3) :休眠结束后调用 exit() 函数退出,退出状态为 0,表示子进程正常退出。

(4) :如果 fork() 函数返回的值 pid 不为 0,则表示此时运行的是父进程,那么在父进程中调用 wait(&status) 函数等待子进程的退出,子进程的退出状态将保存在 status 变量中。

(5) :若发现子进程退出(通过 wait() 函数返回的子进程 pid 判断),则打印出相应信息,如子进程的 pid 与 status

waitpid()原型

pid_t waitpid(pid_t pid, int *wstatus, int options)

pid:参数 pid 为要等待的子进程 ID,其具体含义如下:
– pid  0:等待指定进程号为 pid 的子进程。

wstatus:与 wait() 函数一样  子进程的退出状态

options:参数 options 提供了一些另外的选项来控制 waitpid() 函数的行为。如
果不想使用这些选项,则可以把这个参数设为 0。

– WNOHANG:如果 pid 指定的子进程没有终止运行,则 waitpid() 函数立即返回 
0,而不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程号与状态信息。

– WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则
马上返回。

– WCONTINUED:如果子进程恢复通过 SIGCONT 信号运行,也会立即返回

waitpid() 函数的作用和 wait() 函数一样,但它并不一定要等待第一个终止的子进程,它还有其他选项,比如指定等待某个 pid 的子进程、提供一个非阻塞版本的 wait() 功能

实际上 wait() 函数只是 waitpid() 函数的一个特例,在 Linux 内部实现 wait 函数时直接调用的就是 waitpid 函数

当 waitpid() 函数的参数为 (子进程 pid, status,0) 时, waitpid() 函数就完全退化成了 wait()函数。

关注
打赏
1665385461
查看更多评论
立即登录/注册

微信扫码登录

0.0597s