Linux系统:进程概念

进程理解

0. 计算机体系

0.1 冯诺依曼体系结构

一般主流的计算机,都遵循冯诺依曼体系结构。

计算机的信号分为两种数据信号和控制信号,我们这里只讨论数据信号。计算机硬件一般包括:

设备归类 具体 解释
输入单元 键盘、鼠标、显卡、网卡等 以内存为视角,输入信号的被称为输入设备
输出单元 显示器,打印机、显卡、网卡等 以内存为视角,输出信号的被称为输出设备
存储器 内存 存储器指的是内存,并不是磁盘等外部设备
运算器和控制器 中央处理器(CPU) 运算器执行数学运算和逻辑运算,控制器执行控制逻辑如执行和中断指令
  • 输入设备和输出设备并不是完全独立、非此即彼的,根据不同的情况可归为不同的类。输入输出设备被称为外设。
  • 内存内分为不同级别的存储单元,如下图所示:

  • 有了存储器,CPU 就不用直接和外设交互。而是将准备运算或已完成运算的数据放到内存中,CPU 和内存进行交互,提升了读写速度。
  • 内存是体系结构的核心。外设要输入或输出数据,只能写入或读取内存不可直接和 CPU 交互。同样 CPU 也只能对内存进行读写,不能直接访问外部设备。

0.2 操作系统的定义

系统启动之前,都存储在磁盘中。只有启动的操作系统才有意义。任何计算机系统都包含一个基本的程序集合,称为操作系统(Operator System)。

设计操作系统的目的:

  1. 对下:与硬件交互,管理所有的软硬件资源;
  2. 对上:为用户程序提供一个稳定、高效、安全的执行环境。

操作系统就是一款针对软硬件资源进行管理工作的软件。操作系统的核心思想就是管理,也就是对各种资源进行决策和执行。决策需要各种硬软件资源的数据信息,执行就需要下属的硬软件执行对应的指令。

计算机的管理

计算机的体系结构在宏观上指的是冯诺依曼体系结构,划分的更具体一些:

  • 最底层的就是一些硬件,如网卡、磁盘其他等,在其上就是该硬件对应的驱动程序。
  • 而操作系统对下通过调用驱动程序管理硬件资源 ,除此之外还有最基本的系统软件:进程管理、内存管理、文件系统、驱动管理。
  • 再往上就是承载在操作系统之上的各种软件和用户程序。

对于系统中繁多的硬软件资源,管理起来十分复杂。从操作系统的角度看,管理需要这样的工作:

  1. 聚合同一个个体的资源,定义一个个的结构体,用来描述个体对象。—— 先描述
  2. 关联同类的聚合数据,将同个结构体的对象,组织放在各种数据结构中。—— 再组织

管理的本质

先描述管理对象,再用特定的数据结构将管理对象组织起来。如此,对个体资源的管理工作,就变成了某个数据结构的增删查改。所有的资源都是以这样的管理方式管理起来的。

同样,进程管理也是先描述再组织,定义出一个描述进程的结构体,也就是进程控制块 PCB,再将该结构体对象组织起来。

0.3 系统调用和库函数

操作系统对下管理软硬件资源,对上提供良好的运行环境。对程序员而言,提供了各种系统调用的接口,以实现基本功能。

  • 系统调用 —— 从开发角度看,操作系统对外表现为一个整体,但是会提供一些接口,供上层开发使用。这部分由操作系统提供的接口,叫做系统调用。
  • 语言库函数 —— 系统调用使用较为复杂,功能较为基础,对用户的要求也比较高。所以语言创造者对部分系统调用进行一定程度的封装,集成成了库。有了库函数,就更利于上层开发者进行二次开发。

语言库函数在系统调用之上,是上下层的关系。当然,不是所有库函数都调用系统接口,一般只有和系统硬件交互的函数才会调用系统接口。

比如C语言库函数 printf,由C语言的开发人员封装了系统提供的硬件接口,才能实现将字符打印到显示器上。

1. 进程的概念

1.1 进程的定义

一般肤浅的来说,加载到内存中的程序就叫做进程。这个定义并没有体现出进程和程序的区别,具体的定义在了解进程之后便可以给出。

运行中的系统存在大量的进程,操作系统该如何管理这些进程呢?仍然是先描述再组织。

具体点就是,任何进程在形成之初,操作系统就会为其创建进程控制块 PCB。顾名思义,PCB 用于控制进程,其中存储着进程的所有属性。

从语言层面看,进程信息都被放在一个叫做进程控制块的数据结构中,可以理解为数据结构的合集。在 Linux 系统中,PCB 指的是一个名为task_struct的结构体。

在 Linux 上写一个程序并使之跑起来,这时再查看进程就可以发现:

[yyx@VM-4-16-centos 4DefinitionOfProcess]$ ./myproc 
my proc
my proc
[yyx@VM-4-16-centos 4DefinitionOfProcess]# ps axj | head -1 && ps axj | grep 'myproc'
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25486 29733 29733 25486 pts/0    29733 S+    1003   0:00 ./myproc # 这个便是正在运行的proc进程
28110 30073 30072 28110 pts/1    30072 R+    1003   0:00 grep --color=auto myproc
[yyx@VM-4-16-centos 4DefinitionOfProcess]$ ^C
[yyx@VM-4-16-centos 4DefinitionOfProcess]# ps axj | head -1 && ps axj |grep 'myproc'
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
28110 30726 30725 28110 pts/1    30725 S+    1003   0:00 grep --color=auto myproc

启动程序的本质,就是在系统上创建进程。

进程和程序的区别

  • 程序生成的可执行文件就是程序,也就是后缀为.exe,.out的文件,可以说程序就是文件
  • 将程序文件加载到内存中,操作系统会自动为该进程创建一个进程控制块 PCB,以管理该进程。故进程是程序代码数据和与进程相关的数据结构的总和

这个与进程相关的数据结构可以理解为以 PCB 为节点的,组织 PCB 结构体的一种数据结构,可以是链表,树等等。

CPU 在处理进程时, 不是直接操作代码程序,而是与 PCB 进行交互,因为 PCB 中含有程序的所有属性。也就是说,所有进程管理任务与进程对应的程序毫无关系,只与系统创建的对应的 PCB 强相关

PCB 的内部构成

Linux中PCB task_struct结构

  • 进程编号 —— 每个进程都有编号或称标识符,也就是 PID,具有唯一性用来区别于其他进程。
NAME
       getpid, getppid - get process identification
SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>
       pid_t getpid(void);
       pid_t getppid(void);
DESCRIPTION
       getpid()  returns  the process ID of the calling process.  (This is often used by
       routines that generate unique temporary filenames.)
       getppid() returns the process ID of the parent of the calling process

一般在命令行运行的命令,其父进程都是-bash

  • 进程状态 —— 包括进程退出时的退出码、退出信号、任务状态等。

函数结束时的返回值就是返回给系统的退出码,最后被父进程获取。比如 main 函数结束时的return 0;。任务状态描述进程的运行状态,比如有等待状态,死亡状态,阻塞状态,挂起状态等。

$ echo $? # 获取最近一次执行命令的退出码

  • 优先级 —— 进程很多而 CPU 只有几个,不可能同时运行多个进程,优先级决定了进程的运行先后顺序。
  • 程序计数器 —— 或称 pc 指针,保存程序中即将被执行的指令的地址。
  • 内存指针 —— 指向程序代码和进程相关数据的指针,还有和其他进程共享内存块的指针。
  • I/O状态 —— 包含显式的IO请求,分配给进程的IO设备,被进程使用的文件列表。
  • 审计信息 —— CPU 中有调度模块,负责均衡地调度在运行的进程,使之能较为公平的获得CPU资源得以被执行。而调度模块需要的参考信息就是存在 PCB 中的审计信息。
上下文数据

如图所示,CPU需要在不同的进程之间来回调度,以保证每个进程相对公平地获取资源。

  • 保存上下文:进程之间的来回切换,必须确保在进程被切换后,其的临时数据被更新并存储在它对应的 PCB 中,以便之后再接着上次继续执行。
  • 恢复上下文:这些临时数据就叫做上下文数据,都被存储在寄存器中,当进程被执行时,其 PCB 中的上下文数据就交给了 CPU,以便 CPU 接着上次的地方继续执行。

1.2 进程的查看

进程信息可通过查看系统目录/proc的方式查看:

Linux 系统提供以查看文件的方式查看进程。每个进程创建之初都会在/proc目录下创建一个目录,并以进程编号命名。进程结束时自动消失。

该目录是虚拟目录并不是真实存储在磁盘上的。不仅如此,用户通过命令行查看到的所有文件都是被加载到内存中的,而不是在磁盘上的,

1.3 进程的创建

当我们在系统上运行程序时,也就是创建了一个进程。本次介绍在程序中创建进程的系统调用函数 fork

NAME
       fork - create a child process
SYNOPSIS
       #include <unistd.h>
       pid_t fork(void);
DESCRIPTION
	   fork()  creates  a  new process by duplicating the calling process.  The new process, referred to as the child, is an exact duplicate of the calling process, referred to as the parent, except for the following points:

fork 之后会出现两个执行流,一个是原有进程的,一个是 fork 创建出来的进程。所以之后打印语句两个进程都会执行一遍。通过打印进程的结果可以看出:

  • fork 创建的进程编号为9329,其父进程为9328,而原进程的编号为9328,其父进程为4871
  • 创建出来的进程的父进程就是原进程。而原进程的父进程就是命令行解释器 bash 。

fork 的本质

fork 的本质就是创建进程,系统中**多出一个进程,也就是多出了一份程序代码数据以及与进程相关的内核数据结构。**子进程和父进程的进程数据结构和程序代码数据有什么关系呢?

  • 子进程的内核数据结构task_struct也会以父进程的为模板,来初始化自己的内核数据结构;
  • 一般情况下,子进程会“继承”父进程的代码和数据,也就是执行和访问父进程之后的代码和数据

由于程序代码在运行时是不可被修改的,所以代码只有一份。fork 之后,子进程和父进程会共享代码,但子进程只会执行 fork 之后的代码。

默认情况下,数据同样也是共享的。但如果**父子进程任意一方修改了某个数据,此时该数据就会发生“写时拷贝”,**将该数据在内存中拷贝一份,原位置留给另外一方访问,修改方会访问并修改新位置的该变量。以保证数据的独立性。

通常创建子进程是用来完成其他任务,而不是将父进程的代码执行两边。此时使用 fork 的返回值进行分流。

fork 的返回值分两种情况:

  1. 进程创建失败时:给父子进程都返回一个负值;
  2. 进程创建成功时:给父进程返回子进程的pid,给子进程返回0

如何理解两个返回值?

fork函数已经返回,说明 fork 函数创建进程的核心任务已经完成,也就是此时子进程已被创建完成。

子进程就开始和父进程一样执行之后的代码,所以return返回语句父子进程执行流都会执行一遍,所以父子进程都会得到一个返回值。返回值是不同的,也发生了写时拷贝。

为什么要有两个返回值?

父进程只有一个,而子进程有多个。父进程只能通过返回值的形式获取子进程的 pid,而子进程可通过 getppid 函数可获得父进程的id。

pid_t id = fork();      
if (id == 0) {
    
     //child       
	//...
}    
else if (id > 0) {
    
     //parent     
	//...
}    
else {
    
     // fail    
	//... 
}                                    

fork 之后,父子运行的先后问题不受用户控制,和 CPU 环境有关。

2. 进程的状态

  • 当进程获得处理器,由就绪状态转为运行状态。
  • 当进程被剥夺处理器,系统分配的时间片结束,进程由运行状态转为就绪状态。
  • 当运行进程因某事件受阻,如资源被占用,IO传输未完成,进程由运行状态转为阻塞状态。
  • 当事件等待结束,如得到申请资源,I/O传输完成,进程由阻塞变为就绪状态。

以上都是笼统抽象的进程运行状态,不易理解用处不大,只是适用范围广。弄清楚系统的运行状态,必须深入一款操作系统。

2.1 查看进程状态

进程的状态信息都保存在进程进程控制块 task_struct 中。进程状态用于描述该进程当前所处的运行状态,意义是便于操作系统快速判断进程的状态,按状态分类进程,以更高效地管理进程。

LInux 中具体的状态分为如下几种:

进程状态 状态 解释
R running 运行状态,相当于就绪和运行态两种之和。进程可能正在被 CPU 调用,也可能在运行队列中等待时间片到来。
S sleeping 进程想完成某种任务,但任务条件不具备。如资源被占、IO进行中,此时进程被放入等待队列。
D disk sleeping S 是可中断睡眠,D是不可中断睡眠(深度睡眠)当进程使磁盘长时间读写,该进程长时间等待结果,为防止操作系统将其误杀,此时进程状态改为D状态,该状态仅作了解。
T stopped 暂停状态,S 是浅度睡眠,进程数据可被更新,进程可以被唤醒。但 T 状态是彻底的暂停状态,仅作了解。
t tracked 追踪暂停状态,进程正在被追踪故暂停,比如程序遇到断点而暂停。
X dead 进程终止,内核数据结构和代码数据都被释放回收完成。
Z zombie 进程僵尸状态,进程运行结束但操作系统正在或还未检测,其所占空间未被回收。一般进程结束时都要经历从 Z 到 X 状态。

进程不仅可以等待 CPU 调度,也可以等待硬件资源读写操作。

  • 等待 CPU 调度的进程处于R状态,被放入运行队列中随时被调用。运行状态的进程被从运行队列放入等待队列,被称为挂起等待,从上层角度叫做阻塞。
  • 等待资源或读写的进程处于S/D状态,被放入等待队列中,以待条件具备并完成任务。进程被从等待队列放入运行队列,就叫做唤醒进程,等待成功了。

R S T 三种状态展示,+ 表示在前台运行。

2.2 两种特殊状态

命令行监控进程状态脚本:

while :; 
	do ps axj | head -1 && ps axj | grep myproc | grep -v grep; 
	sleep 1; 
	echo "/################################################################/"; 
done

僵尸进程

一般进程运行结束时需要回收资源,检测进程是否终止和回收资源的工作由该进程的父进程承担。如果进程没有被及时检测并回收,该进程结束就进入 Z 状态。

因此,在父进程休眠的过程中,及时将子进程杀死,防止父进程将其回收,就可以看到子进程的僵尸状态。

倘若此时父进程一直休眠,子进程一直不被回收,就造成了内存泄漏,所以要避免这种僵尸状态的发生。

如何避免的问题放到进程控制中在详谈。

孤儿进程

当子进程正在运行,但父进程提前退出时,其父进程就变为1号进程也就是操作系统。称为子进程被操作系统“领养”,该子进程就被叫做“孤儿进程”。

区别

  • 僵尸进程是父进程休眠中杀死子进程,子进程无法被收尸。
  • 孤儿进程是子进程运行中杀死父进程,子进程被系统领养。

3. 进程优先级

3.1 优先级的概念

一般计算机中 CPU 只有一两个而进程可能有小一千个。这种局面促使我们必须要提出解决方法。

CPU 资源太少而进程太多“僧多粥少”,所以进程必须要进行排队等待调度分配 CPU 资源。

故赋予进程“优先级”(priority)的属性,提高或降低进程原本的优先权,促使其优先或延迟被 CPU 处理,可以提升系统运行的效率。

进程的性质

  • 竞争性:系统进程数目众多,而 CPU 资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
  • 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
  • 并行:多个进程在多个 CPU 下分别同时进行运行,这称之为并行,真正意义上的多进程同时运行。
  • 并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间片之内,让多个进程都得以推进,称之为并发。

3.2 查看进程优先级

$ ps -l -a # 查看进程相关信息
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1003 24805 24154  0  80   0 -  1833 hrtime pts/1    00:00:00 myproc
0 R  1003 24970 24704  0  80   0 - 38595 -      pts/2    00:00:00 ps
标识符 解释 含义
UID UserID 创建进程的用户的编号。
PRI priority 表示进程的基础优先级,值越低优先级越高。
NI nice 优先级的修正值,取值范围 [ − 20 , 19 ] [-20,19] [20,19]共40个级别,调整 NI 值就是调整优先级。
80 + N I = P I R n e w 80+NI=PIR_{new} 80+NI=PIRnew,初始值加修正值才是进程的真正优先级。

一般进程的 PIR 都是80,PIR 不同说明进程大类不同。在此基础上的修正值用于动态调整,二者区分开,便于查看调整幅度。

3.3 调整进程优先级

进程优先级一般由操作系统自主管理,人为修改可能导致降低效率适得其反。

$ man sched_get_priority_max
$ nice
$ renice

上述是系统提供修改进程优先级的调用接口或指令。但不建议轻易修改。

更推荐使用简单的top指令,输入r,输入对应进程编号,以及 NI 值。

  • P I R PIR PIR 显示的都是调整之后的新优先级值。准确的来说,是 80 + N I 80+NI 80+NI,这个 N I NI NI是本次输入的修正值。

不管之前进程的优先级是多少,本次修改都是从80开始算起。修改之后的 P I R PIR PIR 显示的都是初始值 80 80 80 加上输入的修正值 N I NI NI

  • 不管输入的 N I NI NI 值是多少,修正的范围只有 [ − 20 , 19 ] [-20,19] [20,19]。换言之,输入的值超出这个范围无效,相当于输入区间的最值。

优先级不是绝对的、是一种相对的概念,这也是 N I NI NI的调整范围较小的原因。如果调整的跨度很大,很可能导致其他进程被搁置,产生“饥饿问题”。

调度器就是要让所有进程都较为均衡地享受到 CPU 资源。

猜你喜欢

转载自blog.csdn.net/yourfriendyo/article/details/124700053